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/README.md +5 -2
- package/dist/index.js +85 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/records.ts +4 -4
- package/src/commands/records.test.ts +135 -1
- package/src/commands/records.ts +84 -10
- package/src/utils/examples.ts +18 -3
package/package.json
CHANGED
package/src/api/records.ts
CHANGED
|
@@ -123,13 +123,13 @@ export class RecordsApi {
|
|
|
123
123
|
);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
deleteRecords(
|
|
127
127
|
schema: NormalizedUuidOrSlug,
|
|
128
|
-
|
|
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
|
|
132
|
-
{
|
|
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
|
|
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();
|
package/src/commands/records.ts
CHANGED
|
@@ -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
|
|
751
|
+
.description("Delete one or more records")
|
|
682
752
|
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
683
|
-
.
|
|
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
|
-
|
|
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.
|
|
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(
|
|
776
|
+
writeLine(
|
|
777
|
+
stdout,
|
|
778
|
+
`Deleted ${recordIds.length} record(s) from schema "${schema.value}".`,
|
|
779
|
+
);
|
|
706
780
|
});
|
|
707
781
|
|
|
708
782
|
records
|
package/src/utils/examples.ts
CHANGED
|
@@ -2461,10 +2461,25 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
|
|
|
2461
2461
|
]),
|
|
2462
2462
|
"records delete": buildCommandExamples([
|
|
2463
2463
|
{
|
|
2464
|
-
description: "Delete
|
|
2465
|
-
command: `zinkee --json records delete contacts
|
|
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
|
-
"
|
|
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
|
},
|