zinkee 0.1.26 → 0.1.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +3 -1
- package/README.md +3 -0
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/records.ts +1 -0
- package/src/commands/records.test.ts +144 -0
- package/src/commands/records.ts +19 -4
- package/src/utils/examples.test.ts +6 -0
- package/src/utils/examples.ts +1 -1
package/package.json
CHANGED
package/src/api/records.ts
CHANGED
|
@@ -196,6 +196,150 @@ describe("records commands", () => {
|
|
|
196
196
|
});
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
+
it("maps --search to a search.term object in the query body", async () => {
|
|
200
|
+
const configPath = createApiConfig();
|
|
201
|
+
const output = createStdoutBuffer();
|
|
202
|
+
const fetchMock = createFetchMock();
|
|
203
|
+
mockSchemaLookup(fetchMock);
|
|
204
|
+
fetchMock.mockResolvedValueOnce(
|
|
205
|
+
jsonResponse({
|
|
206
|
+
records: [{ recordId: "record-1", values: { name: "Ana Garcia" } }],
|
|
207
|
+
page: { offset: 0, limit: 25, total: 1, count: 1, hasMore: false },
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
211
|
+
|
|
212
|
+
await program.parseAsync([
|
|
213
|
+
"node",
|
|
214
|
+
"zinkee",
|
|
215
|
+
"--json",
|
|
216
|
+
"records",
|
|
217
|
+
"query",
|
|
218
|
+
"contacts",
|
|
219
|
+
"--select",
|
|
220
|
+
"id,name,status",
|
|
221
|
+
"--where",
|
|
222
|
+
"status in OPEN",
|
|
223
|
+
"--search",
|
|
224
|
+
"garcia",
|
|
225
|
+
"--limit",
|
|
226
|
+
"25",
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
const init = fetchMock.mock.calls[1]?.[1];
|
|
230
|
+
|
|
231
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
232
|
+
select: ["id", "name", "status"],
|
|
233
|
+
where: [{ field: "status", op: "in", value: ["OPEN"] }],
|
|
234
|
+
search: { term: "garcia" },
|
|
235
|
+
page: { limit: 25 },
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("accumulates repeated --select flags into a single field selection", async () => {
|
|
240
|
+
const configPath = createApiConfig();
|
|
241
|
+
const output = createStdoutBuffer();
|
|
242
|
+
const fetchMock = createFetchMock();
|
|
243
|
+
mockSchemaLookup(fetchMock);
|
|
244
|
+
fetchMock.mockResolvedValueOnce(
|
|
245
|
+
jsonResponse({
|
|
246
|
+
records: [{ recordId: "record-1", values: { name: "Ana" } }],
|
|
247
|
+
page: { offset: 0, limit: 25, total: 1, count: 1, hasMore: false },
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
251
|
+
|
|
252
|
+
await program.parseAsync([
|
|
253
|
+
"node",
|
|
254
|
+
"zinkee",
|
|
255
|
+
"--json",
|
|
256
|
+
"records",
|
|
257
|
+
"query",
|
|
258
|
+
"contacts",
|
|
259
|
+
"--select",
|
|
260
|
+
"campo-1",
|
|
261
|
+
"--select",
|
|
262
|
+
"campo-2",
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
const init = fetchMock.mock.calls[1]?.[1];
|
|
266
|
+
|
|
267
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
268
|
+
select: ["campo-1", "campo-2"],
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("mixes comma-separated and repeated --select flags preserving order", async () => {
|
|
273
|
+
const configPath = createApiConfig();
|
|
274
|
+
const output = createStdoutBuffer();
|
|
275
|
+
const fetchMock = createFetchMock();
|
|
276
|
+
mockSchemaLookup(fetchMock);
|
|
277
|
+
fetchMock.mockResolvedValueOnce(
|
|
278
|
+
jsonResponse({
|
|
279
|
+
records: [{ recordId: "record-1", values: { name: "Ana" } }],
|
|
280
|
+
page: { offset: 0, limit: 25, total: 1, count: 1, hasMore: false },
|
|
281
|
+
}),
|
|
282
|
+
);
|
|
283
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
284
|
+
|
|
285
|
+
await program.parseAsync([
|
|
286
|
+
"node",
|
|
287
|
+
"zinkee",
|
|
288
|
+
"--json",
|
|
289
|
+
"records",
|
|
290
|
+
"query",
|
|
291
|
+
"contacts",
|
|
292
|
+
"--select",
|
|
293
|
+
"a,b",
|
|
294
|
+
"--select",
|
|
295
|
+
"c",
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
const init = fetchMock.mock.calls[1]?.[1];
|
|
299
|
+
|
|
300
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
301
|
+
select: ["a", "b", "c"],
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("deduplicates repeated fields across --select and --sort flags", async () => {
|
|
306
|
+
const configPath = createApiConfig();
|
|
307
|
+
const output = createStdoutBuffer();
|
|
308
|
+
const fetchMock = createFetchMock();
|
|
309
|
+
mockSchemaLookup(fetchMock);
|
|
310
|
+
fetchMock.mockResolvedValueOnce(
|
|
311
|
+
jsonResponse({
|
|
312
|
+
records: [{ recordId: "record-1", values: { name: "Ana" } }],
|
|
313
|
+
page: { offset: 0, limit: 25, total: 1, count: 1, hasMore: false },
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
317
|
+
|
|
318
|
+
await program.parseAsync([
|
|
319
|
+
"node",
|
|
320
|
+
"zinkee",
|
|
321
|
+
"--json",
|
|
322
|
+
"records",
|
|
323
|
+
"query",
|
|
324
|
+
"contacts",
|
|
325
|
+
"--select",
|
|
326
|
+
"id",
|
|
327
|
+
"--select",
|
|
328
|
+
"id",
|
|
329
|
+
"--sort",
|
|
330
|
+
"name:asc",
|
|
331
|
+
"--sort",
|
|
332
|
+
"name:asc",
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
const init = fetchMock.mock.calls[1]?.[1];
|
|
336
|
+
|
|
337
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
338
|
+
select: ["id"],
|
|
339
|
+
sort: [{ field: "name", direction: "ASC" }],
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
199
343
|
it("normalizes UUID selectors for records get", async () => {
|
|
200
344
|
const configPath = createApiConfig();
|
|
201
345
|
const output = createStdoutBuffer();
|
package/src/commands/records.ts
CHANGED
|
@@ -48,6 +48,7 @@ interface GlobalOptions extends RuntimeOverrides {}
|
|
|
48
48
|
interface RecordsQueryCommandOptions {
|
|
49
49
|
select?: string[];
|
|
50
50
|
where?: ParsedExpression[];
|
|
51
|
+
search?: string;
|
|
51
52
|
sort?: string[];
|
|
52
53
|
limit?: number;
|
|
53
54
|
offset?: number;
|
|
@@ -232,12 +233,20 @@ function parseJsonKeyValueAssignment(input: string): JsonKeyValueAssignment {
|
|
|
232
233
|
}
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
function dedupeStrings(values: string[]): string[] {
|
|
237
|
+
return [...new Set(values)];
|
|
238
|
+
}
|
|
239
|
+
|
|
235
240
|
function collectStringValues(value: string, previous: string[] = []): string[] {
|
|
236
241
|
return [...previous, value];
|
|
237
242
|
}
|
|
238
243
|
|
|
244
|
+
function collectSelectValues(value: string, previous: string[] = []): string[] {
|
|
245
|
+
return dedupeStrings([...previous, ...parseCommaSeparatedValues(value, "--select")]);
|
|
246
|
+
}
|
|
247
|
+
|
|
239
248
|
function collectSortValues(value: string, previous: string[] = []): string[] {
|
|
240
|
-
return [...previous, ...parseCommaSeparatedValues(value, "--sort")];
|
|
249
|
+
return dedupeStrings([...previous, ...parseCommaSeparatedValues(value, "--sort")]);
|
|
241
250
|
}
|
|
242
251
|
|
|
243
252
|
function collectWhereExpressions(
|
|
@@ -375,6 +384,10 @@ function buildRecordQueryInput(
|
|
|
375
384
|
input.where = where;
|
|
376
385
|
}
|
|
377
386
|
|
|
387
|
+
if (options.search !== undefined) {
|
|
388
|
+
input.search = { term: options.search };
|
|
389
|
+
}
|
|
390
|
+
|
|
378
391
|
if (options.sort && options.sort.length > 0) {
|
|
379
392
|
input.sort = [...options.sort];
|
|
380
393
|
}
|
|
@@ -573,10 +586,12 @@ export function registerRecordsCommands(
|
|
|
573
586
|
.command("query")
|
|
574
587
|
.description("Query records for a schema")
|
|
575
588
|
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
576
|
-
.option("--select <fields>", "comma-separated
|
|
577
|
-
parseCommaSeparatedValues(value, "--select")
|
|
578
|
-
))
|
|
589
|
+
.option("--select <fields>", "field selection (comma-separated, repeatable)", collectSelectValues, [])
|
|
579
590
|
.option("--where <expression>", "where expression", collectWhereExpressions, [])
|
|
591
|
+
.option(
|
|
592
|
+
"--search <term>",
|
|
593
|
+
"free-text search term (matched across the schema's searchable fields)",
|
|
594
|
+
)
|
|
580
595
|
.option("--sort <field:direction>", "sort field(s)", collectSortValues, [])
|
|
581
596
|
.option("--limit <number>", "limit results", parseNonNegativeInteger)
|
|
582
597
|
.option("--offset <number>", "offset results", parseNonNegativeInteger)
|
|
@@ -9,6 +9,12 @@ describe("getCommandExamples", () => {
|
|
|
9
9
|
|
|
10
10
|
expect(examples.length).toBeGreaterThan(0);
|
|
11
11
|
expect(examples[0]?.command).toContain("records query");
|
|
12
|
+
|
|
13
|
+
const searchExample = examples.find((example) =>
|
|
14
|
+
example.command.includes("--search"),
|
|
15
|
+
);
|
|
16
|
+
expect(searchExample?.command).toContain("--search garcia");
|
|
17
|
+
expect(searchExample?.raw).toContain('"search": { "term": "garcia" }');
|
|
12
18
|
});
|
|
13
19
|
|
|
14
20
|
it("returns fallback examples for simple commands", () => {
|
package/src/utils/examples.ts
CHANGED
|
@@ -502,7 +502,7 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
|
|
|
502
502
|
{
|
|
503
503
|
description: "Filter by status with a free-text search term.",
|
|
504
504
|
command:
|
|
505
|
-
`zinkee --json records query contacts --select id,contact-name,status --where "status in OPEN" --
|
|
505
|
+
`zinkee --json records query contacts --select id,contact-name,status --where "status in OPEN" --search garcia --limit 25`,
|
|
506
506
|
raw: `{
|
|
507
507
|
"select": ["id", "contact-name", "status"],
|
|
508
508
|
"where": [
|