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/README.md +5 -2
- package/dist/index.js +97 -17
- 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/commands/schemas.ts +13 -2
- package/src/utils/examples.ts +20 -4
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/commands/schemas.ts
CHANGED
|
@@ -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
|
-
"
|
|
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")
|
package/src/utils/examples.ts
CHANGED
|
@@ -264,7 +264,8 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
|
|
|
264
264
|
}`,
|
|
265
265
|
},
|
|
266
266
|
{
|
|
267
|
-
description:
|
|
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
|
|
2464
|
-
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`,
|
|
2465
2466
|
response: `{
|
|
2466
|
-
"
|
|
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
|
},
|