zinkee 0.1.31 → 0.1.33

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zinkee",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "description": "CLI for Zinkee API v2",
5
5
  "type": "module",
6
6
  "bin": {
@@ -123,13 +123,13 @@ export class RecordsApi {
123
123
  );
124
124
  }
125
125
 
126
- deleteRecord(
126
+ deleteRecords(
127
127
  schema: NormalizedUuidOrSlug,
128
- record: NormalizedUuidOrSlug,
128
+ recordIds: string[],
129
129
  ): Promise<Record<string, unknown>> {
130
130
  return this.client.requestJson<Record<string, unknown>>(
131
- `/api/v2/schemas/${selectorValue(schema)}/records/${selectorValue(record)}`,
132
- { method: "DELETE" },
131
+ `/api/v2/schemas/${selectorValue(schema)}/records:delete`,
132
+ jsonOptions({ recordIds }, "POST"),
133
133
  );
134
134
  }
135
135
 
@@ -567,13 +567,147 @@ describe("records commands", () => {
567
567
  "records",
568
568
  "delete",
569
569
  "contacts",
570
- "record-1",
570
+ "--record",
571
+ "550e8400-e29b-41d4-a716-446655440000",
571
572
  ]),
572
573
  ).rejects.toThrowError('Command "records delete" is not allowed in read-only mode.');
573
574
 
574
575
  expect(fetchMock).not.toHaveBeenCalled();
575
576
  });
576
577
 
578
+ it("deletes multiple records via repeatable --record flags and de-duplicates", async () => {
579
+ const configPath = createApiConfig();
580
+ const output = createStdoutBuffer();
581
+ const fetchMock = createFetchMock();
582
+ mockSchemaLookup(fetchMock);
583
+ fetchMock.mockResolvedValueOnce(
584
+ jsonResponse({
585
+ recordIds: [
586
+ "550e8400-e29b-41d4-a716-446655440000",
587
+ "550e8400-e29b-41d4-a716-446655440001",
588
+ ],
589
+ deleted: true,
590
+ }),
591
+ );
592
+ const program = buildProgram({ configPath, stdout: output.stdout });
593
+
594
+ await program.parseAsync([
595
+ "node",
596
+ "zinkee",
597
+ "--json",
598
+ "records",
599
+ "delete",
600
+ "contacts",
601
+ "--record",
602
+ "550e8400-e29b-41d4-a716-446655440000",
603
+ "--record",
604
+ "550e8400-e29b-41d4-a716-446655440001",
605
+ "--record",
606
+ "550e8400-e29b-41d4-a716-446655440000",
607
+ ]);
608
+
609
+ expect(fetchMock.mock.calls[1]?.[0]).toBe(
610
+ "https://api.zinkee.test/api/v2/schemas/schema-1/records:delete",
611
+ );
612
+
613
+ const init = fetchMock.mock.calls[1]?.[1];
614
+
615
+ expect(init?.method).toBe("POST");
616
+ expect(JSON.parse(String(init?.body))).toEqual({
617
+ recordIds: [
618
+ "550e8400-e29b-41d4-a716-446655440000",
619
+ "550e8400-e29b-41d4-a716-446655440001",
620
+ ],
621
+ });
622
+
623
+ const payload = JSON.parse(output.read());
624
+
625
+ expect(payload.data).toEqual({
626
+ recordIds: [
627
+ "550e8400-e29b-41d4-a716-446655440000",
628
+ "550e8400-e29b-41d4-a716-446655440001",
629
+ ],
630
+ deleted: true,
631
+ });
632
+ });
633
+
634
+ it("loads record ids from a file and merges them with --record", async () => {
635
+ const configPath = createApiConfig();
636
+ const idsFile = createTempConfig(
637
+ "550e8400-e29b-41d4-a716-446655440000\n 550e8400-e29b-41d4-a716-446655440002 \n\n",
638
+ );
639
+ const output = createStdoutBuffer();
640
+ const fetchMock = createFetchMock();
641
+ mockSchemaLookup(fetchMock);
642
+ fetchMock.mockResolvedValueOnce(
643
+ jsonResponse({
644
+ recordIds: [
645
+ "550e8400-e29b-41d4-a716-446655440000",
646
+ "550e8400-e29b-41d4-a716-446655440002",
647
+ ],
648
+ deleted: true,
649
+ }),
650
+ );
651
+ const program = buildProgram({ configPath, stdout: output.stdout });
652
+
653
+ await program.parseAsync([
654
+ "node",
655
+ "zinkee",
656
+ "--json",
657
+ "records",
658
+ "delete",
659
+ "contacts",
660
+ "--record",
661
+ "550e8400-e29b-41d4-a716-446655440000",
662
+ "--records-file",
663
+ idsFile,
664
+ ]);
665
+
666
+ const init = fetchMock.mock.calls[1]?.[1];
667
+
668
+ expect(init?.method).toBe("POST");
669
+ expect(JSON.parse(String(init?.body))).toEqual({
670
+ recordIds: [
671
+ "550e8400-e29b-41d4-a716-446655440000",
672
+ "550e8400-e29b-41d4-a716-446655440002",
673
+ ],
674
+ });
675
+ });
676
+
677
+ it("requires at least one record id for delete", async () => {
678
+ const configPath = createApiConfig();
679
+ const output = createStdoutBuffer();
680
+ const fetchMock = createFetchMock();
681
+ const program = buildProgram({ configPath, stdout: output.stdout });
682
+
683
+ await expect(
684
+ program.parseAsync(["node", "zinkee", "records", "delete", "contacts"]),
685
+ ).rejects.toThrowError(/at least one record id/);
686
+
687
+ expect(fetchMock).not.toHaveBeenCalled();
688
+ });
689
+
690
+ it("rejects a non-uuid record id for delete", async () => {
691
+ const configPath = createApiConfig();
692
+ const output = createStdoutBuffer();
693
+ const fetchMock = createFetchMock();
694
+ const program = buildProgram({ configPath, stdout: output.stdout });
695
+
696
+ await expect(
697
+ program.parseAsync([
698
+ "node",
699
+ "zinkee",
700
+ "records",
701
+ "delete",
702
+ "contacts",
703
+ "--record",
704
+ "not-a-uuid",
705
+ ]),
706
+ ).rejects.toThrow();
707
+
708
+ expect(fetchMock).not.toHaveBeenCalled();
709
+ });
710
+
577
711
  it("clears all records in a schema", async () => {
578
712
  const configPath = createApiConfig();
579
713
  const output = createStdoutBuffer();
@@ -72,6 +72,11 @@ interface RecordsUpdateCommandOptions extends RecordsCreateCommandOptions {
72
72
  unset?: string[];
73
73
  }
74
74
 
75
+ interface RecordsDeleteCommandOptions {
76
+ record?: string[];
77
+ recordsFile?: string;
78
+ }
79
+
75
80
  function getCommandIo(stdout: CommandIo | undefined): CommandIo {
76
81
  return stdout ?? (process.stdout as Writable);
77
82
  }
@@ -237,6 +242,71 @@ function dedupeStrings(values: string[]): string[] {
237
242
  return [...new Set(values)];
238
243
  }
239
244
 
245
+ function toRecordUuid(value: string): string | null {
246
+ try {
247
+ const normalized = normalizeUuidOrSlug(value);
248
+ return normalized.kind === "uuid" ? normalized.value : null;
249
+ } catch {
250
+ return null;
251
+ }
252
+ }
253
+
254
+ function collectRecordIds(value: string, previous: string[] = []): string[] {
255
+ const uuid = toRecordUuid(value.trim());
256
+ if (!uuid) {
257
+ throw new InvalidArgumentError(`Expected a record UUID, received "${value}".`);
258
+ }
259
+ return [...previous, uuid];
260
+ }
261
+
262
+ function readRecordIdsFromFile(path: string): string[] {
263
+ let contents: string;
264
+
265
+ try {
266
+ contents = readFileSync(path, "utf8");
267
+ } catch (error) {
268
+ const message = error instanceof Error ? error.message : "Unable to read file.";
269
+ throw new CliError(
270
+ "invalid_cli_usage",
271
+ `Failed to read record ids from ${path}: ${message}`,
272
+ );
273
+ }
274
+
275
+ const ids: string[] = [];
276
+
277
+ contents.split(/\r?\n/).forEach((line, index) => {
278
+ const value = line.trim();
279
+ if (!value) {
280
+ return;
281
+ }
282
+ const uuid = toRecordUuid(value);
283
+ if (!uuid) {
284
+ throw new CliError(
285
+ "invalid_cli_usage",
286
+ `Invalid record UUID "${value}" at ${path}:${index + 1}.`,
287
+ );
288
+ }
289
+ ids.push(uuid);
290
+ });
291
+
292
+ return ids;
293
+ }
294
+
295
+ function resolveDeleteRecordIds(options: RecordsDeleteCommandOptions): string[] {
296
+ const flagIds = options.record ?? [];
297
+ const fileIds = options.recordsFile ? readRecordIdsFromFile(options.recordsFile) : [];
298
+ const recordIds = dedupeStrings([...flagIds, ...fileIds]);
299
+
300
+ if (recordIds.length === 0) {
301
+ throw new CliError(
302
+ "invalid_cli_usage",
303
+ 'Provide at least one record id via "--record <uuid>" (repeatable) or "--records-file <path>".',
304
+ );
305
+ }
306
+
307
+ return recordIds;
308
+ }
309
+
240
310
  function collectStringValues(value: string, previous: string[] = []): string[] {
241
311
  return [...previous, value];
242
312
  }
@@ -678,31 +748,35 @@ export function registerRecordsCommands(
678
748
 
679
749
  records
680
750
  .command("delete")
681
- .description("Delete a record")
751
+ .description("Delete one or more records")
682
752
  .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
683
- .argument("<record>", "record uuid or slug", parseUuidOrSlugSelector)
753
+ .option("--record <uuid>", "record uuid to delete (repeatable)", collectRecordIds, [])
754
+ .option(
755
+ "--records-file <path>",
756
+ "path to a file with one record uuid per line (merged with --record)",
757
+ )
684
758
  .action(async function action(
685
759
  this: Command,
686
760
  schema: NormalizedUuidOrSlug,
687
- record: NormalizedUuidOrSlug,
761
+ commandOptions: RecordsDeleteCommandOptions,
688
762
  ) {
689
763
  const { api, schemasApi, json, readOnly } = createRecordsApi(this, filePath);
690
764
  ensureWritable("records delete", readOnly, "destructive");
765
+
766
+ const recordIds = resolveDeleteRecordIds(commandOptions);
691
767
  const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
692
768
 
693
- await api.deleteRecord(resolvedSchema, record);
694
- const data = {
695
- schema: resolvedSchema.value,
696
- record: record.value,
697
- deleted: true,
698
- };
769
+ const data = await api.deleteRecords(resolvedSchema, recordIds);
699
770
 
700
771
  if (json) {
701
772
  emitJson(stdout, "records delete", data, filePath);
702
773
  return;
703
774
  }
704
775
 
705
- writeLine(stdout, `Deleted record "${record.value}" from schema "${schema.value}".`);
776
+ writeLine(
777
+ stdout,
778
+ `Deleted ${recordIds.length} record(s) from schema "${schema.value}".`,
779
+ );
706
780
  });
707
781
 
708
782
  records
@@ -64,6 +64,16 @@ interface SetUniqueFieldsCommandOptions {
64
64
  clear?: boolean;
65
65
  }
66
66
 
67
+ const HIGHLIGHTED_FIELD_HELP = `
68
+ Highlighted field:
69
+ The schema's "main" field. The frontend uses its value as each record's
70
+ display name, in two places:
71
+ - Activity panel: the record's title in the activity side panel
72
+ (the panel with the record's logs and comments).
73
+ - Detail view: the record's name in the left-hand record list you
74
+ browse to open each record's form.
75
+ `;
76
+
67
77
  interface FieldCreateCommandOptions {
68
78
  type?: string;
69
79
  slug?: string;
@@ -583,7 +593,7 @@ export function registerSchemasCommands(
583
593
  .option("--commentable <bool>", "commentable flag", parseBooleanFlag)
584
594
  .option(
585
595
  "--highlighted-field <field>",
586
- "highlighted (main) field uuid or slug",
596
+ "its value is used as each record's display name in the UI (field uuid or slug)",
587
597
  parseUuidOrSlugSelector,
588
598
  )
589
599
  .action(async function action(
@@ -621,7 +631,8 @@ export function registerSchemasCommands(
621
631
  }
622
632
 
623
633
  writeLine(stdout, `Updated schema "${data.slug ?? schema.value}".`);
624
- });
634
+ })
635
+ .addHelpText("after", HIGHLIGHTED_FIELD_HELP);
625
636
 
626
637
  schemas
627
638
  .command("set-unique-fields")
@@ -264,7 +264,8 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
264
264
  }`,
265
265
  },
266
266
  {
267
- description: "Set the highlighted (main) field of a schema.",
267
+ description:
268
+ "Set the schema's highlighted (main) field — the field whose value names each record in the UI (activity-panel title and detail-view record list).",
268
269
  command:
269
270
  'zinkee --json schemas update contacts --highlighted-field contact-name',
270
271
  raw: `{
@@ -2460,10 +2461,25 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
2460
2461
  ]),
2461
2462
  "records delete": buildCommandExamples([
2462
2463
  {
2463
- description: "Delete a record.",
2464
- command: `zinkee --json records delete contacts ${SAMPLE_UUID}`,
2464
+ description: "Delete one or more records by id (--record is repeatable).",
2465
+ command: `zinkee --json records delete contacts --record 550e8400-e29b-41d4-a716-446655440000 --record 6c8a3e0d-4e5f-4a1b-9c2d-3e4f5a6b7c8d`,
2465
2466
  response: `{
2466
- "recordId": "550e8400-e29b-41d4-a716-446655440000",
2467
+ "recordIds": [
2468
+ "550e8400-e29b-41d4-a716-446655440000",
2469
+ "6c8a3e0d-4e5f-4a1b-9c2d-3e4f5a6b7c8d"
2470
+ ],
2471
+ "deleted": true
2472
+ }`,
2473
+ },
2474
+ {
2475
+ description:
2476
+ "Delete many records listing one uuid per line in a file (merged with any --record flags and de-duplicated).",
2477
+ command: `zinkee --json records delete contacts --records-file ./record-ids.txt`,
2478
+ response: `{
2479
+ "recordIds": [
2480
+ "550e8400-e29b-41d4-a716-446655440000",
2481
+ "6c8a3e0d-4e5f-4a1b-9c2d-3e4f5a6b7c8d"
2482
+ ],
2467
2483
  "deleted": true
2468
2484
  }`,
2469
2485
  },