Skip to content

Commit f7b030f

Browse files
srousseyclaude
andcommitted
feat(cli-v2): implement all query commands (offerings, crowdfunding, facts, persons)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6211a46 commit f7b030f

9 files changed

Lines changed: 675 additions & 8 deletions

src/cli/groups/query.ts

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import type { Command } from "commander";
22
import { queryEntities } from "../queries/EntityQuery";
33
import { queryFilings } from "../queries/FilingQuery";
4+
import { queryOfferings } from "../queries/OfferingQuery";
5+
import { queryCrowdfunding } from "../queries/CrowdfundingQuery";
6+
import { queryFacts } from "../queries/FactsQuery";
7+
import { queryPersons } from "../queries/PersonQuery";
48
import { renderTable } from "../output/TableRenderer";
59

610
export function addQueryCommands(program: Command): void {
@@ -100,8 +104,35 @@ export function addQueryCommands(program: Command): void {
100104
.option("--limit <n>", "Limit results")
101105
.option("--offset <n>", "Offset results")
102106
.option("--format <format>", "Output format (table, json, csv)")
103-
.action(async () => {
104-
console.log("not yet implemented");
107+
.action(async (search: string | undefined, options: Record<string, string>) => {
108+
const limit = parseInt(options.limit ?? "25");
109+
const offset = parseInt(options.offset ?? "0");
110+
const result = await queryOfferings({
111+
search,
112+
cik: options.cik ? parseInt(options.cik) : undefined,
113+
industry: options.industry,
114+
exemption: options.exemption,
115+
after: options.after,
116+
before: options.before,
117+
limit,
118+
offset,
119+
});
120+
121+
const columns = [
122+
{ key: "cik", header: "CIK", width: 10 },
123+
{ key: "file_number", header: "File #", width: 12 },
124+
{ key: "industry_group", header: "Industry", width: 20 },
125+
{ key: "date_of_first_sale", header: "First Sale", width: 12 },
126+
];
127+
128+
console.log(
129+
renderTable(result.rows as Record<string, unknown>[], columns, {
130+
format: options.format as "table" | "csv" | "json",
131+
total: result.total,
132+
offset,
133+
limit,
134+
})
135+
);
105136
});
106137

107138
query
@@ -114,8 +145,34 @@ export function addQueryCommands(program: Command): void {
114145
.option("--limit <n>", "Limit results")
115146
.option("--offset <n>", "Offset results")
116147
.option("--format <format>", "Output format (table, json, csv)")
117-
.action(async () => {
118-
console.log("not yet implemented");
148+
.action(async (search: string | undefined, options: Record<string, string>) => {
149+
const limit = parseInt(options.limit ?? "25");
150+
const offset = parseInt(options.offset ?? "0");
151+
const result = await queryCrowdfunding({
152+
search,
153+
cik: options.cik ? parseInt(options.cik) : undefined,
154+
portal: options.portal ? parseInt(options.portal) : undefined,
155+
after: options.after,
156+
before: options.before,
157+
limit,
158+
offset,
159+
});
160+
161+
const columns = [
162+
{ key: "cik", header: "CIK", width: 10 },
163+
{ key: "name", header: "Name", width: 25 },
164+
{ key: "filing_date", header: "Filed", width: 12 },
165+
{ key: "status", header: "Status", width: 10 },
166+
];
167+
168+
console.log(
169+
renderTable(result.rows as Record<string, unknown>[], columns, {
170+
format: options.format as "table" | "csv" | "json",
171+
total: result.total,
172+
offset,
173+
limit,
174+
})
175+
);
119176
});
120177

121178
query
@@ -127,8 +184,35 @@ export function addQueryCommands(program: Command): void {
127184
.option("--limit <n>", "Limit results")
128185
.option("--offset <n>", "Offset results")
129186
.option("--format <format>", "Output format (table, json, csv)")
130-
.action(async () => {
131-
console.log("not yet implemented");
187+
.action(async (cik: string, options: Record<string, string>) => {
188+
const limit = parseInt(options.limit ?? "25");
189+
const offset = parseInt(options.offset ?? "0");
190+
const result = await queryFacts({
191+
cik: parseInt(cik),
192+
name: options.name,
193+
taxonomy: options.taxonomy,
194+
year: options.year ? parseInt(options.year) : undefined,
195+
limit,
196+
offset,
197+
});
198+
199+
const columns = [
200+
{ key: "name", header: "Fact", width: 25 },
201+
{ key: "val", header: "Value", width: 15 },
202+
{ key: "val_unit", header: "Unit", width: 10 },
203+
{ key: "fy", header: "FY", width: 6 },
204+
{ key: "fp", header: "FP", width: 4 },
205+
{ key: "filed_date", header: "Filed", width: 12 },
206+
];
207+
208+
console.log(
209+
renderTable(result.rows as Record<string, unknown>[], columns, {
210+
format: options.format as "table" | "csv" | "json",
211+
total: result.total,
212+
offset,
213+
limit,
214+
})
215+
);
132216
});
133217

134218
query
@@ -139,7 +223,31 @@ export function addQueryCommands(program: Command): void {
139223
.option("--limit <n>", "Limit results")
140224
.option("--offset <n>", "Offset results")
141225
.option("--format <format>", "Output format (table, json, csv)")
142-
.action(async () => {
143-
console.log("not yet implemented");
226+
.action(async (search: string | undefined, options: Record<string, string>) => {
227+
const limit = parseInt(options.limit ?? "25");
228+
const offset = parseInt(options.offset ?? "0");
229+
const result = await queryPersons({
230+
search,
231+
cik: options.cik ? parseInt(options.cik) : undefined,
232+
role: options.role,
233+
limit,
234+
offset,
235+
});
236+
237+
const columns = [
238+
{ key: "first", header: "First", width: 15 },
239+
{ key: "last", header: "Last", width: 20 },
240+
{ key: "title", header: "Title", width: 20 },
241+
{ key: "cik", header: "CIK", width: 10 },
242+
];
243+
244+
console.log(
245+
renderTable(result.rows as Record<string, unknown>[], columns, {
246+
format: options.format as "table" | "csv" | "json",
247+
total: result.total,
248+
offset,
249+
limit,
250+
})
251+
);
144252
});
145253
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { beforeEach, describe, expect, it } from "bun:test";
2+
import { resetDependencyInjectionsForTesting } from "../../config/TestingDI";
3+
import { globalServiceRegistry } from "@workglow/util";
4+
import { CROWDFUNDING_REPOSITORY_TOKEN } from "../../storage/portal/CrowdfundingSchema";
5+
import { queryCrowdfunding } from "./CrowdfundingQuery";
6+
7+
function makeCrowdfunding(overrides: Partial<Parameters<typeof repo.put>[0]> = {}) {
8+
return {
9+
cik: 1318605,
10+
file_number: "020-12345",
11+
filing_date: "2026-03-01",
12+
name: "Acme Corp",
13+
legal_status: "Corporation",
14+
state_jurisdiction: "DE",
15+
date_incorporation: "2020-01-01",
16+
url: "https://acme.example.com",
17+
portal_cik: 9999999,
18+
status: "active",
19+
...overrides,
20+
};
21+
}
22+
23+
let repo: ReturnType<typeof globalServiceRegistry.get<typeof CROWDFUNDING_REPOSITORY_TOKEN>>;
24+
25+
describe("queryCrowdfunding", () => {
26+
beforeEach(() => {
27+
resetDependencyInjectionsForTesting();
28+
repo = globalServiceRegistry.get(CROWDFUNDING_REPOSITORY_TOKEN);
29+
});
30+
31+
it("returns empty results for empty DB", async () => {
32+
const result = await queryCrowdfunding({});
33+
expect(result.rows).toEqual([]);
34+
expect(result.total).toBe(0);
35+
});
36+
37+
it("filters by CIK", async () => {
38+
await repo.put(makeCrowdfunding({ cik: 1318605, file_number: "020-001" }));
39+
await repo.put(makeCrowdfunding({ cik: 320193, file_number: "020-002" }));
40+
41+
const result = await queryCrowdfunding({ cik: 1318605 });
42+
expect(result.rows.length).toBe(1);
43+
expect(result.total).toBe(1);
44+
expect(result.rows[0].cik).toBe(1318605);
45+
});
46+
47+
it("filters by search on name (partial, case-insensitive)", async () => {
48+
await repo.put(makeCrowdfunding({ file_number: "020-001", name: "Acme Corp" }));
49+
await repo.put(makeCrowdfunding({ file_number: "020-002", name: "Beta Inc" }));
50+
51+
const result = await queryCrowdfunding({ search: "acme" });
52+
expect(result.rows.length).toBe(1);
53+
expect(result.rows[0].name).toBe("Acme Corp");
54+
});
55+
56+
it("filters by portal CIK", async () => {
57+
await repo.put(makeCrowdfunding({ file_number: "020-001", portal_cik: 9999999 }));
58+
await repo.put(makeCrowdfunding({ file_number: "020-002", portal_cik: 8888888 }));
59+
60+
const result = await queryCrowdfunding({ portal: 9999999 });
61+
expect(result.rows.length).toBe(1);
62+
expect(result.rows[0].portal_cik).toBe(9999999);
63+
});
64+
65+
it("filters by date range", async () => {
66+
await repo.put(
67+
makeCrowdfunding({ file_number: "020-001", filing_date: "2026-01-15" })
68+
);
69+
await repo.put(
70+
makeCrowdfunding({ file_number: "020-002", filing_date: "2026-03-15" })
71+
);
72+
73+
const result = await queryCrowdfunding({ after: "2026-02-01" });
74+
expect(result.rows.length).toBe(1);
75+
expect(result.rows[0].filing_date).toBe("2026-03-15");
76+
});
77+
78+
it("respects limit and offset", async () => {
79+
for (let i = 1; i <= 5; i++) {
80+
await repo.put(
81+
makeCrowdfunding({
82+
file_number: `020-${String(i).padStart(3, "0")}`,
83+
})
84+
);
85+
}
86+
87+
const result = await queryCrowdfunding({ limit: 2, offset: 1 });
88+
expect(result.rows.length).toBe(2);
89+
expect(result.total).toBe(5);
90+
});
91+
});
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { Crowdfunding } from "../../storage/portal/CrowdfundingSchema";
2+
import { CROWDFUNDING_REPOSITORY_TOKEN } from "../../storage/portal/CrowdfundingSchema";
3+
import { globalServiceRegistry } from "@workglow/util";
4+
import type { QueryResult } from "./EntityQuery";
5+
6+
export interface CrowdfundingQueryParams {
7+
readonly search?: string;
8+
readonly cik?: number;
9+
readonly portal?: number;
10+
readonly after?: string;
11+
readonly before?: string;
12+
readonly limit?: number;
13+
readonly offset?: number;
14+
}
15+
16+
export async function queryCrowdfunding(
17+
params: CrowdfundingQueryParams
18+
): Promise<QueryResult<Crowdfunding>> {
19+
const repo = globalServiceRegistry.get(CROWDFUNDING_REPOSITORY_TOKEN);
20+
const limit = params.limit ?? 25;
21+
const offset = params.offset ?? 0;
22+
23+
let items: Crowdfunding[];
24+
25+
if (params.cik !== undefined) {
26+
items = (await repo.query({ cik: params.cik } as Partial<Crowdfunding>)) ?? [];
27+
} else {
28+
items = (await repo.getAll()) ?? [];
29+
}
30+
31+
if (params.search) {
32+
const searchLower = params.search.toLowerCase();
33+
items = items.filter((c) => c.name.toLowerCase().includes(searchLower));
34+
}
35+
36+
if (params.portal !== undefined) {
37+
items = items.filter((c) => c.portal_cik === params.portal);
38+
}
39+
40+
if (params.after !== undefined) {
41+
items = items.filter((c) => c.filing_date >= params.after!);
42+
}
43+
44+
if (params.before !== undefined) {
45+
items = items.filter((c) => c.filing_date <= params.before!);
46+
}
47+
48+
const total = items.length;
49+
const rows = items.slice(offset, offset + limit);
50+
51+
return { rows, total };
52+
}

src/cli/queries/FactsQuery.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { beforeEach, describe, expect, it } from "bun:test";
2+
import { resetDependencyInjectionsForTesting } from "../../config/TestingDI";
3+
import { globalServiceRegistry } from "@workglow/util";
4+
import { COMPANY_FACTS_REPOSITORY_TOKEN } from "../../storage/facts/CompanyFactsSchema";
5+
import { queryFacts } from "./FactsQuery";
6+
7+
function makeFact(overrides: Partial<Parameters<typeof repo.put>[0]> = {}) {
8+
return {
9+
cik: 1318605,
10+
grouping: "us-gaap",
11+
name: "Revenue",
12+
filed_date: "2026-03-01",
13+
form: "10-K",
14+
val_unit: "USD",
15+
frame: null,
16+
accession_number: "0001-26-001",
17+
start_date: null,
18+
end_date: null,
19+
val: 1000000,
20+
fy: 2025,
21+
fp: "FY",
22+
...overrides,
23+
};
24+
}
25+
26+
let repo: ReturnType<typeof globalServiceRegistry.get<typeof COMPANY_FACTS_REPOSITORY_TOKEN>>;
27+
28+
describe("queryFacts", () => {
29+
beforeEach(() => {
30+
resetDependencyInjectionsForTesting();
31+
repo = globalServiceRegistry.get(COMPANY_FACTS_REPOSITORY_TOKEN);
32+
});
33+
34+
it("returns empty results for a CIK with no facts", async () => {
35+
const result = await queryFacts({ cik: 1318605 });
36+
expect(result.rows).toEqual([]);
37+
expect(result.total).toBe(0);
38+
});
39+
40+
it("returns facts for a given CIK", async () => {
41+
await repo.put(makeFact({ cik: 1318605, accession_number: "0001-26-001" }));
42+
await repo.put(makeFact({ cik: 320193, accession_number: "0001-26-002" }));
43+
44+
const result = await queryFacts({ cik: 1318605 });
45+
expect(result.rows.length).toBe(1);
46+
expect(result.total).toBe(1);
47+
expect(result.rows[0].cik).toBe(1318605);
48+
});
49+
50+
it("filters by name (partial match)", async () => {
51+
await repo.put(makeFact({ name: "Revenue", accession_number: "0001-26-001" }));
52+
await repo.put(makeFact({ name: "NetIncome", accession_number: "0001-26-002" }));
53+
54+
const result = await queryFacts({ cik: 1318605, name: "revenue" });
55+
expect(result.rows.length).toBe(1);
56+
expect(result.rows[0].name).toBe("Revenue");
57+
});
58+
59+
it("filters by taxonomy (grouping)", async () => {
60+
await repo.put(makeFact({ grouping: "us-gaap", accession_number: "0001-26-001" }));
61+
await repo.put(makeFact({ grouping: "dei", accession_number: "0001-26-002" }));
62+
63+
const result = await queryFacts({ cik: 1318605, taxonomy: "gaap" });
64+
expect(result.rows.length).toBe(1);
65+
expect(result.rows[0].grouping).toBe("us-gaap");
66+
});
67+
68+
it("filters by fiscal year", async () => {
69+
await repo.put(makeFact({ fy: 2025, accession_number: "0001-26-001", val: 100 }));
70+
await repo.put(makeFact({ fy: 2024, accession_number: "0001-26-002", val: 200 }));
71+
72+
const result = await queryFacts({ cik: 1318605, year: 2025 });
73+
expect(result.rows.length).toBe(1);
74+
expect(result.rows[0].fy).toBe(2025);
75+
});
76+
77+
it("respects limit and offset", async () => {
78+
for (let i = 1; i <= 5; i++) {
79+
await repo.put(
80+
makeFact({
81+
accession_number: `0001-26-${String(i).padStart(3, "0")}`,
82+
val: i * 1000,
83+
})
84+
);
85+
}
86+
87+
const result = await queryFacts({ cik: 1318605, limit: 2, offset: 1 });
88+
expect(result.rows.length).toBe(2);
89+
expect(result.total).toBe(5);
90+
});
91+
});

0 commit comments

Comments
 (0)