zinkee 0.1.32 → 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.32",
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
@@ -2461,10 +2461,25 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
2461
2461
  ]),
2462
2462
  "records delete": buildCommandExamples([
2463
2463
  {
2464
- description: "Delete a record.",
2465
- 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`,
2466
2466
  response: `{
2467
- "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
+ ],
2468
2483
  "deleted": true
2469
2484
  }`,
2470
2485
  },