zinkee 0.1.33 → 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 +4 -1
- package/dist/index.js +112 -32
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/records.ts +17 -7
- package/src/commands/records.test.ts +101 -9
- package/src/commands/records.ts +85 -12
- package/src/utils/examples.ts +42 -22
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
|
|
|
@@ -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
|
+
],
|
|
414
|
+
});
|
|
415
|
+
});
|
|
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
|
+
],
|
|
412
458
|
});
|
|
413
459
|
});
|
|
414
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();
|
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,6 +66,7 @@ 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 {
|
|
@@ -473,9 +474,72 @@ function buildRecordQueryInput(
|
|
|
473
474
|
return input;
|
|
474
475
|
}
|
|
475
476
|
|
|
476
|
-
function
|
|
477
|
-
|
|
478
|
-
|
|
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
|
+
|
|
479
543
|
const values = {
|
|
480
544
|
...readRawObject(options.raw, options.rawFile),
|
|
481
545
|
...buildAssignmentObject(options.set, options.setJson),
|
|
@@ -484,11 +548,11 @@ function buildRecordCreateInput(
|
|
|
484
548
|
if (isEmptyObject(values)) {
|
|
485
549
|
throw new CliError(
|
|
486
550
|
"invalid_cli_usage",
|
|
487
|
-
'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".',
|
|
488
552
|
);
|
|
489
553
|
}
|
|
490
554
|
|
|
491
|
-
return
|
|
555
|
+
return [values];
|
|
492
556
|
}
|
|
493
557
|
|
|
494
558
|
function buildRecordUpdateInput(
|
|
@@ -684,12 +748,16 @@ export function registerRecordsCommands(
|
|
|
684
748
|
|
|
685
749
|
records
|
|
686
750
|
.command("create")
|
|
687
|
-
.description("Create
|
|
751
|
+
.description("Create one or more records")
|
|
688
752
|
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
689
753
|
.option("--set <field=value>", "set a string field", collectKeyValueAssignments, [])
|
|
690
754
|
.option("--set-json <field=json>", "set a structured field", collectJsonAssignments, [])
|
|
691
755
|
.option("--raw <json>", "raw JSON object payload")
|
|
692
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
|
+
)
|
|
693
761
|
.action(async function action(
|
|
694
762
|
this: Command,
|
|
695
763
|
schema: NormalizedUuidOrSlug,
|
|
@@ -697,19 +765,24 @@ export function registerRecordsCommands(
|
|
|
697
765
|
) {
|
|
698
766
|
const { api, schemasApi, json, readOnly } = createRecordsApi(this, filePath);
|
|
699
767
|
ensureWritable("records create", readOnly, "write");
|
|
768
|
+
|
|
769
|
+
const records = buildCreateRecords(commandOptions);
|
|
700
770
|
const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
|
|
701
771
|
|
|
702
|
-
const data = await api.
|
|
703
|
-
resolvedSchema,
|
|
704
|
-
buildRecordCreateInput(commandOptions),
|
|
705
|
-
);
|
|
772
|
+
const data = await api.createRecords(resolvedSchema, records);
|
|
706
773
|
|
|
707
774
|
if (json) {
|
|
708
775
|
emitJson(stdout, "records create", data, filePath);
|
|
709
776
|
return;
|
|
710
777
|
}
|
|
711
778
|
|
|
712
|
-
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
|
+
}
|
|
713
786
|
});
|
|
714
787
|
|
|
715
788
|
records
|
package/src/utils/examples.ts
CHANGED
|
@@ -677,38 +677,58 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
|
|
|
677
677
|
]),
|
|
678
678
|
"records create": buildCommandExamples([
|
|
679
679
|
{
|
|
680
|
-
description:
|
|
680
|
+
description:
|
|
681
|
+
"Create a single record with --set/--set-json (the single-record flags build one record, sent as a one-element 'records' array).",
|
|
681
682
|
command:
|
|
682
683
|
`zinkee --json records create contacts --set contact-name="Ada Lovelace" --set contact-email=ada@example.com`,
|
|
683
684
|
raw: `{
|
|
684
|
-
"
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
685
|
+
"records": [
|
|
686
|
+
{
|
|
687
|
+
"contact-name": "Ada Lovelace",
|
|
688
|
+
"contact-email": "ada@example.com"
|
|
689
|
+
}
|
|
690
|
+
]
|
|
688
691
|
}`,
|
|
689
692
|
response: `{
|
|
690
|
-
"
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
}
|
|
693
|
+
"created": [
|
|
694
|
+
"550e8400-e29b-41d4-a716-446655440000"
|
|
695
|
+
],
|
|
696
|
+
"errors": []
|
|
695
697
|
}`,
|
|
696
698
|
},
|
|
697
699
|
{
|
|
698
700
|
description:
|
|
699
|
-
"Create a
|
|
700
|
-
command:
|
|
701
|
-
`zinkee --json records create contacts --set contact-name="Grace Hopper" --set contact-email=grace@example.com --set status=2 --set amount=1999.99 --set due-date=2026-06-01T09:00:00Z --set customer-ref="CUST-12" --set owner=6c8a3e0d-4e5f-4a1b-9c2d-3e4f5a6b7c8d`,
|
|
701
|
+
"Create many records from a JSON array file (one object per record, slug -> value). Best-effort: valid records are created and invalid ones are reported per index in 'errors'. ReferenceField values must match the target's current targetDisplayField.",
|
|
702
|
+
command: `zinkee --json records create contacts --records-file ./records.json`,
|
|
702
703
|
raw: `{
|
|
703
|
-
"
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
704
|
+
"records": [
|
|
705
|
+
{
|
|
706
|
+
"contact-name": "Ada Lovelace",
|
|
707
|
+
"contact-email": "ada@example.com"
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
"contact-name": "Grace Hopper",
|
|
711
|
+
"contact-email": "grace@example.com",
|
|
712
|
+
"status": 2,
|
|
713
|
+
"amount": 1999.99,
|
|
714
|
+
"due-date": "2026-06-01T09:00:00Z",
|
|
715
|
+
"customer-ref": "8ad602a1-f6e3-43f6-9ab2-f1c5afc99621",
|
|
716
|
+
"owner": "6c8a3e0d-4e5f-4a1b-9c2d-3e4f5a6b7c8d"
|
|
717
|
+
}
|
|
718
|
+
]
|
|
719
|
+
}`,
|
|
720
|
+
response: `{
|
|
721
|
+
"created": [
|
|
722
|
+
"550e8400-e29b-41d4-a716-446655440000"
|
|
723
|
+
],
|
|
724
|
+
"errors": [
|
|
725
|
+
{
|
|
726
|
+
"index": 1,
|
|
727
|
+
"reason": "record_field_not_found",
|
|
728
|
+
"message": "Field 'contact-emial' was not found in schema '2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e'.",
|
|
729
|
+
"action": "Use a valid field slug from the schema and retry."
|
|
730
|
+
}
|
|
731
|
+
]
|
|
712
732
|
}`,
|
|
713
733
|
},
|
|
714
734
|
]),
|