zinkee 0.1.32 → 0.1.34
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 +9 -3
- package/dist/index.js +197 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/records.ts +21 -11
- package/src/commands/records.test.ts +236 -10
- package/src/commands/records.ts +169 -22
- package/src/utils/examples.ts +60 -25
package/package.json
CHANGED
package/src/api/records.ts
CHANGED
|
@@ -33,8 +33,18 @@ export interface RecordQueryInput {
|
|
|
33
33
|
offset?: number;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export
|
|
37
|
-
|
|
36
|
+
export type CreateRecordValues = Record<string, unknown>;
|
|
37
|
+
|
|
38
|
+
export interface CreateRecordError {
|
|
39
|
+
index: number;
|
|
40
|
+
reason: string;
|
|
41
|
+
message: string;
|
|
42
|
+
action: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface CreateRecordsResult {
|
|
46
|
+
created: string[];
|
|
47
|
+
errors: CreateRecordError[];
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
export interface UpdateRecordInput {
|
|
@@ -102,13 +112,13 @@ export class RecordsApi {
|
|
|
102
112
|
);
|
|
103
113
|
}
|
|
104
114
|
|
|
105
|
-
|
|
115
|
+
createRecords(
|
|
106
116
|
schema: NormalizedUuidOrSlug,
|
|
107
|
-
|
|
108
|
-
): Promise<
|
|
109
|
-
return this.client.requestJson<
|
|
117
|
+
records: CreateRecordValues[],
|
|
118
|
+
): Promise<CreateRecordsResult> {
|
|
119
|
+
return this.client.requestJson<CreateRecordsResult>(
|
|
110
120
|
`/api/v2/schemas/${selectorValue(schema)}/records`,
|
|
111
|
-
jsonOptions(
|
|
121
|
+
jsonOptions({ records }, "POST"),
|
|
112
122
|
);
|
|
113
123
|
}
|
|
114
124
|
|
|
@@ -123,13 +133,13 @@ export class RecordsApi {
|
|
|
123
133
|
);
|
|
124
134
|
}
|
|
125
135
|
|
|
126
|
-
|
|
136
|
+
deleteRecords(
|
|
127
137
|
schema: NormalizedUuidOrSlug,
|
|
128
|
-
|
|
138
|
+
recordIds: string[],
|
|
129
139
|
): Promise<Record<string, unknown>> {
|
|
130
140
|
return this.client.requestJson<Record<string, unknown>>(
|
|
131
|
-
`/api/v2/schemas/${selectorValue(schema)}/records
|
|
132
|
-
{
|
|
141
|
+
`/api/v2/schemas/${selectorValue(schema)}/records:delete`,
|
|
142
|
+
jsonOptions({ recordIds }, "POST"),
|
|
133
143
|
);
|
|
134
144
|
}
|
|
135
145
|
|
|
@@ -367,15 +367,15 @@ describe("records commands", () => {
|
|
|
367
367
|
);
|
|
368
368
|
});
|
|
369
369
|
|
|
370
|
-
it("creates
|
|
370
|
+
it("creates a single record by merging raw JSON with set and set-json assignments", async () => {
|
|
371
371
|
const configPath = createApiConfig();
|
|
372
372
|
const output = createStdoutBuffer();
|
|
373
373
|
const fetchMock = createFetchMock();
|
|
374
374
|
mockSchemaLookup(fetchMock);
|
|
375
375
|
fetchMock.mockResolvedValueOnce(
|
|
376
376
|
jsonResponse({
|
|
377
|
-
|
|
378
|
-
|
|
377
|
+
created: ["record-1"],
|
|
378
|
+
errors: [],
|
|
379
379
|
}),
|
|
380
380
|
);
|
|
381
381
|
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
@@ -402,16 +402,108 @@ describe("records commands", () => {
|
|
|
402
402
|
);
|
|
403
403
|
expect(init?.method).toBe("POST");
|
|
404
404
|
expect(JSON.parse(String(init?.body))).toEqual({
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
405
|
+
records: [
|
|
406
|
+
{
|
|
407
|
+
name: "Ana",
|
|
408
|
+
status: "active",
|
|
409
|
+
profile: {
|
|
410
|
+
tier: "gold",
|
|
411
|
+
},
|
|
410
412
|
},
|
|
411
|
-
|
|
413
|
+
],
|
|
412
414
|
});
|
|
413
415
|
});
|
|
414
416
|
|
|
417
|
+
it("creates multiple records from a --records-file", async () => {
|
|
418
|
+
const configPath = createApiConfig();
|
|
419
|
+
const recordsFile = createTempConfig(
|
|
420
|
+
JSON.stringify([
|
|
421
|
+
{ "contact-name": "Ada", status: 2 },
|
|
422
|
+
{ "contact-name": "Grace", tags: ["a", "b"] },
|
|
423
|
+
]),
|
|
424
|
+
);
|
|
425
|
+
const output = createStdoutBuffer();
|
|
426
|
+
const fetchMock = createFetchMock();
|
|
427
|
+
mockSchemaLookup(fetchMock);
|
|
428
|
+
fetchMock.mockResolvedValueOnce(
|
|
429
|
+
jsonResponse({
|
|
430
|
+
created: ["record-1", "record-2"],
|
|
431
|
+
errors: [],
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
435
|
+
|
|
436
|
+
await program.parseAsync([
|
|
437
|
+
"node",
|
|
438
|
+
"zinkee",
|
|
439
|
+
"--json",
|
|
440
|
+
"records",
|
|
441
|
+
"create",
|
|
442
|
+
"contacts",
|
|
443
|
+
"--records-file",
|
|
444
|
+
recordsFile,
|
|
445
|
+
]);
|
|
446
|
+
|
|
447
|
+
const init = fetchMock.mock.calls[1]?.[1];
|
|
448
|
+
|
|
449
|
+
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
450
|
+
"https://api.zinkee.test/api/v2/schemas/schema-1/records",
|
|
451
|
+
);
|
|
452
|
+
expect(init?.method).toBe("POST");
|
|
453
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
454
|
+
records: [
|
|
455
|
+
{ "contact-name": "Ada", status: 2 },
|
|
456
|
+
{ "contact-name": "Grace", tags: ["a", "b"] },
|
|
457
|
+
],
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("rejects combining --records-file with single-record flags", async () => {
|
|
462
|
+
const configPath = createApiConfig();
|
|
463
|
+
const recordsFile = createTempConfig(JSON.stringify([{ "contact-name": "Ada" }]));
|
|
464
|
+
const output = createStdoutBuffer();
|
|
465
|
+
const fetchMock = createFetchMock();
|
|
466
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
467
|
+
|
|
468
|
+
await expect(
|
|
469
|
+
program.parseAsync([
|
|
470
|
+
"node",
|
|
471
|
+
"zinkee",
|
|
472
|
+
"records",
|
|
473
|
+
"create",
|
|
474
|
+
"contacts",
|
|
475
|
+
"--records-file",
|
|
476
|
+
recordsFile,
|
|
477
|
+
"--set",
|
|
478
|
+
"contact-name=Grace",
|
|
479
|
+
]),
|
|
480
|
+
).rejects.toThrowError(/not both/);
|
|
481
|
+
|
|
482
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("rejects a --records-file that is not a JSON array", async () => {
|
|
486
|
+
const configPath = createApiConfig();
|
|
487
|
+
const recordsFile = createTempConfig(JSON.stringify({ "contact-name": "Ada" }));
|
|
488
|
+
const output = createStdoutBuffer();
|
|
489
|
+
const fetchMock = createFetchMock();
|
|
490
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
491
|
+
|
|
492
|
+
await expect(
|
|
493
|
+
program.parseAsync([
|
|
494
|
+
"node",
|
|
495
|
+
"zinkee",
|
|
496
|
+
"records",
|
|
497
|
+
"create",
|
|
498
|
+
"contacts",
|
|
499
|
+
"--records-file",
|
|
500
|
+
recordsFile,
|
|
501
|
+
]),
|
|
502
|
+
).rejects.toThrowError(/JSON array/);
|
|
503
|
+
|
|
504
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
505
|
+
});
|
|
506
|
+
|
|
415
507
|
it("wraps raw-file fields into the set payload", async () => {
|
|
416
508
|
const configPath = createApiConfig();
|
|
417
509
|
const output = createStdoutBuffer();
|
|
@@ -567,13 +659,147 @@ describe("records commands", () => {
|
|
|
567
659
|
"records",
|
|
568
660
|
"delete",
|
|
569
661
|
"contacts",
|
|
570
|
-
"record
|
|
662
|
+
"--record",
|
|
663
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
571
664
|
]),
|
|
572
665
|
).rejects.toThrowError('Command "records delete" is not allowed in read-only mode.');
|
|
573
666
|
|
|
574
667
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
575
668
|
});
|
|
576
669
|
|
|
670
|
+
it("deletes multiple records via repeatable --record flags and de-duplicates", async () => {
|
|
671
|
+
const configPath = createApiConfig();
|
|
672
|
+
const output = createStdoutBuffer();
|
|
673
|
+
const fetchMock = createFetchMock();
|
|
674
|
+
mockSchemaLookup(fetchMock);
|
|
675
|
+
fetchMock.mockResolvedValueOnce(
|
|
676
|
+
jsonResponse({
|
|
677
|
+
recordIds: [
|
|
678
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
679
|
+
"550e8400-e29b-41d4-a716-446655440001",
|
|
680
|
+
],
|
|
681
|
+
deleted: true,
|
|
682
|
+
}),
|
|
683
|
+
);
|
|
684
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
685
|
+
|
|
686
|
+
await program.parseAsync([
|
|
687
|
+
"node",
|
|
688
|
+
"zinkee",
|
|
689
|
+
"--json",
|
|
690
|
+
"records",
|
|
691
|
+
"delete",
|
|
692
|
+
"contacts",
|
|
693
|
+
"--record",
|
|
694
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
695
|
+
"--record",
|
|
696
|
+
"550e8400-e29b-41d4-a716-446655440001",
|
|
697
|
+
"--record",
|
|
698
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
699
|
+
]);
|
|
700
|
+
|
|
701
|
+
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
702
|
+
"https://api.zinkee.test/api/v2/schemas/schema-1/records:delete",
|
|
703
|
+
);
|
|
704
|
+
|
|
705
|
+
const init = fetchMock.mock.calls[1]?.[1];
|
|
706
|
+
|
|
707
|
+
expect(init?.method).toBe("POST");
|
|
708
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
709
|
+
recordIds: [
|
|
710
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
711
|
+
"550e8400-e29b-41d4-a716-446655440001",
|
|
712
|
+
],
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
const payload = JSON.parse(output.read());
|
|
716
|
+
|
|
717
|
+
expect(payload.data).toEqual({
|
|
718
|
+
recordIds: [
|
|
719
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
720
|
+
"550e8400-e29b-41d4-a716-446655440001",
|
|
721
|
+
],
|
|
722
|
+
deleted: true,
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("loads record ids from a file and merges them with --record", async () => {
|
|
727
|
+
const configPath = createApiConfig();
|
|
728
|
+
const idsFile = createTempConfig(
|
|
729
|
+
"550e8400-e29b-41d4-a716-446655440000\n 550e8400-e29b-41d4-a716-446655440002 \n\n",
|
|
730
|
+
);
|
|
731
|
+
const output = createStdoutBuffer();
|
|
732
|
+
const fetchMock = createFetchMock();
|
|
733
|
+
mockSchemaLookup(fetchMock);
|
|
734
|
+
fetchMock.mockResolvedValueOnce(
|
|
735
|
+
jsonResponse({
|
|
736
|
+
recordIds: [
|
|
737
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
738
|
+
"550e8400-e29b-41d4-a716-446655440002",
|
|
739
|
+
],
|
|
740
|
+
deleted: true,
|
|
741
|
+
}),
|
|
742
|
+
);
|
|
743
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
744
|
+
|
|
745
|
+
await program.parseAsync([
|
|
746
|
+
"node",
|
|
747
|
+
"zinkee",
|
|
748
|
+
"--json",
|
|
749
|
+
"records",
|
|
750
|
+
"delete",
|
|
751
|
+
"contacts",
|
|
752
|
+
"--record",
|
|
753
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
754
|
+
"--records-file",
|
|
755
|
+
idsFile,
|
|
756
|
+
]);
|
|
757
|
+
|
|
758
|
+
const init = fetchMock.mock.calls[1]?.[1];
|
|
759
|
+
|
|
760
|
+
expect(init?.method).toBe("POST");
|
|
761
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
762
|
+
recordIds: [
|
|
763
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
764
|
+
"550e8400-e29b-41d4-a716-446655440002",
|
|
765
|
+
],
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
it("requires at least one record id for delete", async () => {
|
|
770
|
+
const configPath = createApiConfig();
|
|
771
|
+
const output = createStdoutBuffer();
|
|
772
|
+
const fetchMock = createFetchMock();
|
|
773
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
774
|
+
|
|
775
|
+
await expect(
|
|
776
|
+
program.parseAsync(["node", "zinkee", "records", "delete", "contacts"]),
|
|
777
|
+
).rejects.toThrowError(/at least one record id/);
|
|
778
|
+
|
|
779
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it("rejects a non-uuid record id for delete", async () => {
|
|
783
|
+
const configPath = createApiConfig();
|
|
784
|
+
const output = createStdoutBuffer();
|
|
785
|
+
const fetchMock = createFetchMock();
|
|
786
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
787
|
+
|
|
788
|
+
await expect(
|
|
789
|
+
program.parseAsync([
|
|
790
|
+
"node",
|
|
791
|
+
"zinkee",
|
|
792
|
+
"records",
|
|
793
|
+
"delete",
|
|
794
|
+
"contacts",
|
|
795
|
+
"--record",
|
|
796
|
+
"not-a-uuid",
|
|
797
|
+
]),
|
|
798
|
+
).rejects.toThrow();
|
|
799
|
+
|
|
800
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
801
|
+
});
|
|
802
|
+
|
|
577
803
|
it("clears all records in a schema", async () => {
|
|
578
804
|
const configPath = createApiConfig();
|
|
579
805
|
const output = createStdoutBuffer();
|
package/src/commands/records.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Command, InvalidArgumentError } from "commander";
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
type ApiRecord,
|
|
8
|
-
type
|
|
8
|
+
type CreateRecordValues,
|
|
9
9
|
type RecordQueryExpression,
|
|
10
10
|
type RecordQueryInput,
|
|
11
11
|
type RecordQueryResult,
|
|
@@ -66,12 +66,18 @@ interface RecordsCreateCommandOptions {
|
|
|
66
66
|
setJson?: JsonKeyValueAssignment[];
|
|
67
67
|
raw?: string;
|
|
68
68
|
rawFile?: string;
|
|
69
|
+
recordsFile?: string;
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
interface RecordsUpdateCommandOptions extends RecordsCreateCommandOptions {
|
|
72
73
|
unset?: string[];
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
interface RecordsDeleteCommandOptions {
|
|
77
|
+
record?: string[];
|
|
78
|
+
recordsFile?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
75
81
|
function getCommandIo(stdout: CommandIo | undefined): CommandIo {
|
|
76
82
|
return stdout ?? (process.stdout as Writable);
|
|
77
83
|
}
|
|
@@ -237,6 +243,71 @@ function dedupeStrings(values: string[]): string[] {
|
|
|
237
243
|
return [...new Set(values)];
|
|
238
244
|
}
|
|
239
245
|
|
|
246
|
+
function toRecordUuid(value: string): string | null {
|
|
247
|
+
try {
|
|
248
|
+
const normalized = normalizeUuidOrSlug(value);
|
|
249
|
+
return normalized.kind === "uuid" ? normalized.value : null;
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function collectRecordIds(value: string, previous: string[] = []): string[] {
|
|
256
|
+
const uuid = toRecordUuid(value.trim());
|
|
257
|
+
if (!uuid) {
|
|
258
|
+
throw new InvalidArgumentError(`Expected a record UUID, received "${value}".`);
|
|
259
|
+
}
|
|
260
|
+
return [...previous, uuid];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function readRecordIdsFromFile(path: string): string[] {
|
|
264
|
+
let contents: string;
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
contents = readFileSync(path, "utf8");
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const message = error instanceof Error ? error.message : "Unable to read file.";
|
|
270
|
+
throw new CliError(
|
|
271
|
+
"invalid_cli_usage",
|
|
272
|
+
`Failed to read record ids from ${path}: ${message}`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const ids: string[] = [];
|
|
277
|
+
|
|
278
|
+
contents.split(/\r?\n/).forEach((line, index) => {
|
|
279
|
+
const value = line.trim();
|
|
280
|
+
if (!value) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const uuid = toRecordUuid(value);
|
|
284
|
+
if (!uuid) {
|
|
285
|
+
throw new CliError(
|
|
286
|
+
"invalid_cli_usage",
|
|
287
|
+
`Invalid record UUID "${value}" at ${path}:${index + 1}.`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
ids.push(uuid);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return ids;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function resolveDeleteRecordIds(options: RecordsDeleteCommandOptions): string[] {
|
|
297
|
+
const flagIds = options.record ?? [];
|
|
298
|
+
const fileIds = options.recordsFile ? readRecordIdsFromFile(options.recordsFile) : [];
|
|
299
|
+
const recordIds = dedupeStrings([...flagIds, ...fileIds]);
|
|
300
|
+
|
|
301
|
+
if (recordIds.length === 0) {
|
|
302
|
+
throw new CliError(
|
|
303
|
+
"invalid_cli_usage",
|
|
304
|
+
'Provide at least one record id via "--record <uuid>" (repeatable) or "--records-file <path>".',
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return recordIds;
|
|
309
|
+
}
|
|
310
|
+
|
|
240
311
|
function collectStringValues(value: string, previous: string[] = []): string[] {
|
|
241
312
|
return [...previous, value];
|
|
242
313
|
}
|
|
@@ -403,9 +474,72 @@ function buildRecordQueryInput(
|
|
|
403
474
|
return input;
|
|
404
475
|
}
|
|
405
476
|
|
|
406
|
-
function
|
|
407
|
-
|
|
408
|
-
|
|
477
|
+
function readRecordsFromFile(path: string): CreateRecordValues[] {
|
|
478
|
+
let contents: string;
|
|
479
|
+
|
|
480
|
+
try {
|
|
481
|
+
contents = readFileSync(path, "utf8");
|
|
482
|
+
} catch (error) {
|
|
483
|
+
const message = error instanceof Error ? error.message : "Unable to read file.";
|
|
484
|
+
throw new CliError(
|
|
485
|
+
"invalid_cli_usage",
|
|
486
|
+
`Failed to read records from ${path}: ${message}`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
let parsed: unknown;
|
|
491
|
+
try {
|
|
492
|
+
parsed = JSON.parse(contents);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
const message = error instanceof Error ? error.message : "Invalid JSON.";
|
|
495
|
+
throw new CliError(
|
|
496
|
+
"invalid_cli_usage",
|
|
497
|
+
`Failed to parse JSON from --records-file ${path}: ${message}`,
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (!Array.isArray(parsed)) {
|
|
502
|
+
throw new CliError(
|
|
503
|
+
"invalid_cli_usage",
|
|
504
|
+
`Expected --records-file ${path} to contain a JSON array of record objects.`,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (parsed.length === 0) {
|
|
509
|
+
throw new CliError(
|
|
510
|
+
"invalid_cli_usage",
|
|
511
|
+
`--records-file ${path} must contain at least one record.`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
return parsed.map((record, index) => {
|
|
516
|
+
if (!isJsonObject(record)) {
|
|
517
|
+
throw new CliError(
|
|
518
|
+
"invalid_cli_usage",
|
|
519
|
+
`Record at index ${index} in --records-file ${path} must be a JSON object.`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
return record;
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function buildCreateRecords(options: RecordsCreateCommandOptions): CreateRecordValues[] {
|
|
527
|
+
const hasSingleRecordFlags =
|
|
528
|
+
(options.set?.length ?? 0) > 0 ||
|
|
529
|
+
(options.setJson?.length ?? 0) > 0 ||
|
|
530
|
+
options.raw !== undefined ||
|
|
531
|
+
options.rawFile !== undefined;
|
|
532
|
+
|
|
533
|
+
if (options.recordsFile !== undefined) {
|
|
534
|
+
if (hasSingleRecordFlags) {
|
|
535
|
+
throw new CliError(
|
|
536
|
+
"invalid_cli_usage",
|
|
537
|
+
'Provide either "--records-file" or the single-record flags (--set/--set-json/--raw/--raw-file), not both.',
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
return readRecordsFromFile(options.recordsFile);
|
|
541
|
+
}
|
|
542
|
+
|
|
409
543
|
const values = {
|
|
410
544
|
...readRawObject(options.raw, options.rawFile),
|
|
411
545
|
...buildAssignmentObject(options.set, options.setJson),
|
|
@@ -414,11 +548,11 @@ function buildRecordCreateInput(
|
|
|
414
548
|
if (isEmptyObject(values)) {
|
|
415
549
|
throw new CliError(
|
|
416
550
|
"invalid_cli_usage",
|
|
417
|
-
'Provide record fields via "--set", "--set-json", "--raw", or "--
|
|
551
|
+
'Provide record fields via "--set", "--set-json", "--raw", "--raw-file", or load multiple records with "--records-file".',
|
|
418
552
|
);
|
|
419
553
|
}
|
|
420
554
|
|
|
421
|
-
return
|
|
555
|
+
return [values];
|
|
422
556
|
}
|
|
423
557
|
|
|
424
558
|
function buildRecordUpdateInput(
|
|
@@ -614,12 +748,16 @@ export function registerRecordsCommands(
|
|
|
614
748
|
|
|
615
749
|
records
|
|
616
750
|
.command("create")
|
|
617
|
-
.description("Create
|
|
751
|
+
.description("Create one or more records")
|
|
618
752
|
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
619
753
|
.option("--set <field=value>", "set a string field", collectKeyValueAssignments, [])
|
|
620
754
|
.option("--set-json <field=json>", "set a structured field", collectJsonAssignments, [])
|
|
621
755
|
.option("--raw <json>", "raw JSON object payload")
|
|
622
756
|
.option("--raw-file <path>", "path to a raw JSON object payload")
|
|
757
|
+
.option(
|
|
758
|
+
"--records-file <path>",
|
|
759
|
+
"path to a JSON array of record objects (mutually exclusive with --set/--set-json/--raw/--raw-file)",
|
|
760
|
+
)
|
|
623
761
|
.action(async function action(
|
|
624
762
|
this: Command,
|
|
625
763
|
schema: NormalizedUuidOrSlug,
|
|
@@ -627,19 +765,24 @@ export function registerRecordsCommands(
|
|
|
627
765
|
) {
|
|
628
766
|
const { api, schemasApi, json, readOnly } = createRecordsApi(this, filePath);
|
|
629
767
|
ensureWritable("records create", readOnly, "write");
|
|
768
|
+
|
|
769
|
+
const records = buildCreateRecords(commandOptions);
|
|
630
770
|
const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
|
|
631
771
|
|
|
632
|
-
const data = await api.
|
|
633
|
-
resolvedSchema,
|
|
634
|
-
buildRecordCreateInput(commandOptions),
|
|
635
|
-
);
|
|
772
|
+
const data = await api.createRecords(resolvedSchema, records);
|
|
636
773
|
|
|
637
774
|
if (json) {
|
|
638
775
|
emitJson(stdout, "records create", data, filePath);
|
|
639
776
|
return;
|
|
640
777
|
}
|
|
641
778
|
|
|
642
|
-
writeLine(stdout, `Created record "${
|
|
779
|
+
writeLine(stdout, `Created ${data.created.length} record(s) in schema "${schema.value}".`);
|
|
780
|
+
if (data.errors.length > 0) {
|
|
781
|
+
writeLine(stdout, `${data.errors.length} record(s) rejected:`);
|
|
782
|
+
for (const error of data.errors) {
|
|
783
|
+
writeLine(stdout, ` - index ${error.index}: ${error.message}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
643
786
|
});
|
|
644
787
|
|
|
645
788
|
records
|
|
@@ -678,31 +821,35 @@ export function registerRecordsCommands(
|
|
|
678
821
|
|
|
679
822
|
records
|
|
680
823
|
.command("delete")
|
|
681
|
-
.description("Delete
|
|
824
|
+
.description("Delete one or more records")
|
|
682
825
|
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
683
|
-
.
|
|
826
|
+
.option("--record <uuid>", "record uuid to delete (repeatable)", collectRecordIds, [])
|
|
827
|
+
.option(
|
|
828
|
+
"--records-file <path>",
|
|
829
|
+
"path to a file with one record uuid per line (merged with --record)",
|
|
830
|
+
)
|
|
684
831
|
.action(async function action(
|
|
685
832
|
this: Command,
|
|
686
833
|
schema: NormalizedUuidOrSlug,
|
|
687
|
-
|
|
834
|
+
commandOptions: RecordsDeleteCommandOptions,
|
|
688
835
|
) {
|
|
689
836
|
const { api, schemasApi, json, readOnly } = createRecordsApi(this, filePath);
|
|
690
837
|
ensureWritable("records delete", readOnly, "destructive");
|
|
838
|
+
|
|
839
|
+
const recordIds = resolveDeleteRecordIds(commandOptions);
|
|
691
840
|
const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
|
|
692
841
|
|
|
693
|
-
await api.
|
|
694
|
-
const data = {
|
|
695
|
-
schema: resolvedSchema.value,
|
|
696
|
-
record: record.value,
|
|
697
|
-
deleted: true,
|
|
698
|
-
};
|
|
842
|
+
const data = await api.deleteRecords(resolvedSchema, recordIds);
|
|
699
843
|
|
|
700
844
|
if (json) {
|
|
701
845
|
emitJson(stdout, "records delete", data, filePath);
|
|
702
846
|
return;
|
|
703
847
|
}
|
|
704
848
|
|
|
705
|
-
writeLine(
|
|
849
|
+
writeLine(
|
|
850
|
+
stdout,
|
|
851
|
+
`Deleted ${recordIds.length} record(s) from schema "${schema.value}".`,
|
|
852
|
+
);
|
|
706
853
|
});
|
|
707
854
|
|
|
708
855
|
records
|