Skip to content

Commit 6211a46

Browse files
srousseyclaude
andcommitted
feat(cli-v2): implement filing query command
Add queryFilings() with support for CIK, form type, date range, and text search filters. Wire into the query filings subcommand. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent aa8c28e commit 6211a46

3 files changed

Lines changed: 259 additions & 2 deletions

File tree

src/cli/groups/query.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Command } from "commander";
22
import { queryEntities } from "../queries/EntityQuery";
3+
import { queryFilings } from "../queries/FilingQuery";
34
import { renderTable } from "../output/TableRenderer";
45

56
export function addQueryCommands(program: Command): void {
@@ -57,8 +58,35 @@ export function addQueryCommands(program: Command): void {
5758
.option("--limit <n>", "Limit results")
5859
.option("--offset <n>", "Offset results")
5960
.option("--format <format>", "Output format (table, json, csv)")
60-
.action(async () => {
61-
console.log("not yet implemented");
61+
.action(async (search: string | undefined, options: Record<string, string>) => {
62+
const limit = parseInt(options.limit ?? "25");
63+
const offset = parseInt(options.offset ?? "0");
64+
const result = await queryFilings({
65+
search,
66+
cik: options.cik ? parseInt(options.cik) : undefined,
67+
form: options.form,
68+
after: options.after,
69+
before: options.before,
70+
limit,
71+
offset,
72+
});
73+
74+
const columns = [
75+
{ key: "cik", header: "CIK", width: 10 },
76+
{ key: "accession_number", header: "Accession", width: 20 },
77+
{ key: "form", header: "Form", width: 8 },
78+
{ key: "filing_date", header: "Filed", width: 12 },
79+
{ key: "primary_doc", header: "Document", width: 25 },
80+
];
81+
82+
console.log(
83+
renderTable(result.rows as Record<string, unknown>[], columns, {
84+
format: options.format as "table" | "csv" | "json",
85+
total: result.total,
86+
offset,
87+
limit,
88+
})
89+
);
6290
});
6391

6492
query
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { beforeEach, describe, expect, it } from "bun:test";
2+
import { resetDependencyInjectionsForTesting } from "../../config/TestingDI";
3+
import { globalServiceRegistry } from "@workglow/util";
4+
import { FILING_REPOSITORY_TOKEN } from "../../storage/filing/FilingSchema";
5+
import { queryFilings } from "./FilingQuery";
6+
7+
function makeFiling(overrides: Partial<Parameters<typeof repo.put>[0]> = {}) {
8+
return {
9+
cik: 1318605,
10+
accession_number: "0001-26-001",
11+
filing_date: "2026-03-01",
12+
form: "10-K",
13+
primary_doc: "doc.htm",
14+
acceptance_date: "2026-03-01T00:00:00",
15+
report_date: null,
16+
file_number: null,
17+
film_number: null,
18+
primary_doc_description: null,
19+
size: null,
20+
is_xbrl: null,
21+
is_inline_xbrl: null,
22+
items: null,
23+
act: null,
24+
...overrides,
25+
};
26+
}
27+
28+
let repo: ReturnType<typeof globalServiceRegistry.get<typeof FILING_REPOSITORY_TOKEN>>;
29+
30+
describe("queryFilings", () => {
31+
beforeEach(() => {
32+
resetDependencyInjectionsForTesting();
33+
repo = globalServiceRegistry.get(FILING_REPOSITORY_TOKEN);
34+
});
35+
36+
it("returns empty results for empty DB", async () => {
37+
const result = await queryFilings({});
38+
expect(result.rows).toEqual([]);
39+
expect(result.total).toBe(0);
40+
});
41+
42+
it("filters by CIK", async () => {
43+
await repo.put(makeFiling({ cik: 1318605, accession_number: "0001-26-001" }));
44+
await repo.put(makeFiling({ cik: 320193, accession_number: "0001-26-002" }));
45+
46+
const result = await queryFilings({ cik: 1318605 });
47+
expect(result.rows.length).toBe(1);
48+
expect(result.total).toBe(1);
49+
expect(result.rows[0].cik).toBe(1318605);
50+
});
51+
52+
it("filters by form type", async () => {
53+
await repo.put(makeFiling({ accession_number: "0001-26-001", form: "10-K" }));
54+
await repo.put(makeFiling({ accession_number: "0001-26-002", form: "10-Q" }));
55+
56+
const result = await queryFilings({ form: "10-K" });
57+
expect(result.rows.length).toBe(1);
58+
expect(result.total).toBe(1);
59+
expect(result.rows[0].form).toBe("10-K");
60+
});
61+
62+
it("filters by date range (after)", async () => {
63+
await repo.put(
64+
makeFiling({ accession_number: "0001-26-001", filing_date: "2026-01-15" })
65+
);
66+
await repo.put(
67+
makeFiling({ accession_number: "0001-26-002", filing_date: "2026-03-15" })
68+
);
69+
70+
const result = await queryFilings({ after: "2026-02-01" });
71+
expect(result.rows.length).toBe(1);
72+
expect(result.total).toBe(1);
73+
expect(result.rows[0].filing_date).toBe("2026-03-15");
74+
});
75+
76+
it("filters by date range (before)", async () => {
77+
await repo.put(
78+
makeFiling({ accession_number: "0001-26-001", filing_date: "2026-01-15" })
79+
);
80+
await repo.put(
81+
makeFiling({ accession_number: "0001-26-002", filing_date: "2026-03-15" })
82+
);
83+
84+
const result = await queryFilings({ before: "2026-02-01" });
85+
expect(result.rows.length).toBe(1);
86+
expect(result.total).toBe(1);
87+
expect(result.rows[0].filing_date).toBe("2026-01-15");
88+
});
89+
90+
it("filters by search on primary_doc_description", async () => {
91+
await repo.put(
92+
makeFiling({
93+
accession_number: "0001-26-001",
94+
primary_doc_description: "Annual Report",
95+
})
96+
);
97+
await repo.put(
98+
makeFiling({
99+
accession_number: "0001-26-002",
100+
primary_doc_description: "Quarterly Report",
101+
})
102+
);
103+
104+
const result = await queryFilings({ search: "annual" });
105+
expect(result.rows.length).toBe(1);
106+
expect(result.total).toBe(1);
107+
expect(result.rows[0].primary_doc_description).toBe("Annual Report");
108+
});
109+
110+
it("combines filters", async () => {
111+
await repo.put(
112+
makeFiling({
113+
cik: 1318605,
114+
accession_number: "0001-26-001",
115+
form: "10-K",
116+
filing_date: "2026-03-01",
117+
})
118+
);
119+
await repo.put(
120+
makeFiling({
121+
cik: 1318605,
122+
accession_number: "0001-26-002",
123+
form: "10-Q",
124+
filing_date: "2026-03-15",
125+
})
126+
);
127+
await repo.put(
128+
makeFiling({
129+
cik: 320193,
130+
accession_number: "0001-26-003",
131+
form: "10-K",
132+
filing_date: "2026-01-10",
133+
})
134+
);
135+
136+
const result = await queryFilings({
137+
cik: 1318605,
138+
form: "10-K",
139+
after: "2026-02-01",
140+
});
141+
expect(result.rows.length).toBe(1);
142+
expect(result.total).toBe(1);
143+
expect(result.rows[0].accession_number).toBe("0001-26-001");
144+
});
145+
146+
it("respects limit and offset", async () => {
147+
for (let i = 1; i <= 5; i++) {
148+
await repo.put(
149+
makeFiling({
150+
cik: 1318605,
151+
accession_number: `0001-26-${String(i).padStart(3, "0")}`,
152+
filing_date: `2026-03-${String(i).padStart(2, "0")}`,
153+
})
154+
);
155+
}
156+
157+
const result = await queryFilings({ limit: 2, offset: 1 });
158+
expect(result.rows.length).toBe(2);
159+
expect(result.total).toBe(5);
160+
});
161+
});

src/cli/queries/FilingQuery.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import type { Filing } from "../../storage/filing/FilingSchema";
2+
import { FILING_REPOSITORY_TOKEN } from "../../storage/filing/FilingSchema";
3+
import { globalServiceRegistry } from "@workglow/util";
4+
import type { QueryResult } from "./EntityQuery";
5+
6+
export interface FilingQueryParams {
7+
readonly search?: string;
8+
readonly cik?: number;
9+
readonly form?: string;
10+
readonly after?: string;
11+
readonly before?: string;
12+
readonly limit?: number;
13+
readonly offset?: number;
14+
}
15+
16+
export async function queryFilings(params: FilingQueryParams): Promise<QueryResult<Filing>> {
17+
const repo = globalServiceRegistry.get(FILING_REPOSITORY_TOKEN);
18+
const limit = params.limit ?? 25;
19+
const offset = params.offset ?? 0;
20+
21+
const needsClientFilter =
22+
params.search !== undefined || params.after !== undefined || params.before !== undefined;
23+
24+
let filings: Filing[];
25+
26+
if (!needsClientFilter && (params.cik !== undefined || params.form !== undefined)) {
27+
const criteria: Partial<Filing> = {};
28+
if (params.cik !== undefined) criteria.cik = params.cik;
29+
if (params.form !== undefined) criteria.form = params.form;
30+
filings = (await repo.query(criteria)) ?? [];
31+
} else {
32+
filings = (await repo.getAll()) ?? [];
33+
}
34+
35+
// Apply exact filters when fetched via getAll
36+
if (needsClientFilter) {
37+
if (params.cik !== undefined) {
38+
filings = filings.filter((f) => f.cik === params.cik);
39+
}
40+
if (params.form !== undefined) {
41+
filings = filings.filter((f) => f.form === params.form);
42+
}
43+
}
44+
45+
// Apply text search on primary_doc_description
46+
if (params.search) {
47+
const searchLower = params.search.toLowerCase();
48+
filings = filings.filter(
49+
(f) =>
50+
f.primary_doc_description !== null &&
51+
f.primary_doc_description !== undefined &&
52+
f.primary_doc_description.toLowerCase().includes(searchLower)
53+
);
54+
}
55+
56+
// Apply date range filters
57+
if (params.after !== undefined) {
58+
filings = filings.filter((f) => f.filing_date >= params.after!);
59+
}
60+
if (params.before !== undefined) {
61+
filings = filings.filter((f) => f.filing_date <= params.before!);
62+
}
63+
64+
const total = filings.length;
65+
const rows = filings.slice(offset, offset + limit);
66+
67+
return { rows, total };
68+
}

0 commit comments

Comments
 (0)