zinkee 0.1.30 → 0.1.32
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 +24 -0
- package/dist/index.js +149 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/schemas.ts +12 -0
- package/src/command-registry.ts +7 -0
- package/src/commands/schemas.test.ts +112 -0
- package/src/commands/schemas.ts +88 -3
- package/src/utils/examples.ts +83 -2
package/package.json
CHANGED
package/src/api/schemas.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface ApiSchema {
|
|
|
19
19
|
commentable?: boolean;
|
|
20
20
|
highlightedField?: string;
|
|
21
21
|
fields?: ApiSchemaField[];
|
|
22
|
+
uniqueFields?: string[];
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface CreateSchemaInput {
|
|
@@ -36,6 +37,10 @@ export interface UpdateSchemaInput {
|
|
|
36
37
|
highlightedField?: string;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export interface SetUniqueFieldsInput {
|
|
41
|
+
fieldIds: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
39
44
|
export interface CreateSchemaFieldInput {
|
|
40
45
|
[key: string]: unknown;
|
|
41
46
|
type: string;
|
|
@@ -86,6 +91,13 @@ export class SchemasApi {
|
|
|
86
91
|
);
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
setUniqueFields(schema: NormalizedUuidOrSlug, input: SetUniqueFieldsInput): Promise<ApiSchema> {
|
|
95
|
+
return this.client.requestJson<ApiSchema>(
|
|
96
|
+
`/api/v2/schemas/${selectorValue(schema)}/unique-fields`,
|
|
97
|
+
jsonOptions(input, "PUT"),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
deleteSchema(schema: NormalizedUuidOrSlug): Promise<Record<string, unknown>> {
|
|
90
102
|
return this.client.requestJson<Record<string, unknown>>(
|
|
91
103
|
`/api/v2/schemas/${selectorValue(schema)}`,
|
package/src/command-registry.ts
CHANGED
|
@@ -672,6 +672,13 @@ export const commandRegistry = {
|
|
|
672
672
|
supportsRaw: false,
|
|
673
673
|
output: writeOutput,
|
|
674
674
|
},
|
|
675
|
+
"schemas set-unique-fields": {
|
|
676
|
+
command: "schemas set-unique-fields",
|
|
677
|
+
accessLevel: "write",
|
|
678
|
+
supportsExample: true,
|
|
679
|
+
supportsRaw: false,
|
|
680
|
+
output: writeOutput,
|
|
681
|
+
},
|
|
675
682
|
"schemas delete": {
|
|
676
683
|
command: "schemas delete",
|
|
677
684
|
accessLevel: "destructive",
|
|
@@ -213,6 +213,118 @@ describe("schemas commands", () => {
|
|
|
213
213
|
expect(fetchMock).not.toHaveBeenCalled();
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
+
it("sets unique fields through PUT /unique-fields with the expected body", async () => {
|
|
217
|
+
const configPath = createApiConfig();
|
|
218
|
+
const output = createStdoutBuffer();
|
|
219
|
+
const fetchMock = createFetchMock();
|
|
220
|
+
fetchMock.mockResolvedValueOnce(
|
|
221
|
+
jsonResponse({
|
|
222
|
+
id: "2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e",
|
|
223
|
+
uniqueFields: [
|
|
224
|
+
"c60b8526-5835-4c9f-aecd-9a16c7704a34",
|
|
225
|
+
"1d2e3f4a-5b6c-7d8e-9f01-23456789abcd",
|
|
226
|
+
],
|
|
227
|
+
}),
|
|
228
|
+
);
|
|
229
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
230
|
+
|
|
231
|
+
await program.parseAsync([
|
|
232
|
+
"node",
|
|
233
|
+
"zinkee",
|
|
234
|
+
"--json",
|
|
235
|
+
"schemas",
|
|
236
|
+
"set-unique-fields",
|
|
237
|
+
"2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e",
|
|
238
|
+
"--field",
|
|
239
|
+
"c60b8526-5835-4c9f-aecd-9a16c7704a34",
|
|
240
|
+
"--field",
|
|
241
|
+
"1d2e3f4a-5b6c-7d8e-9f01-23456789abcd",
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
245
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
246
|
+
"https://api.zinkee.test/api/v2/schemas/2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e/unique-fields",
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
250
|
+
expect(init?.method).toBe("PUT");
|
|
251
|
+
expect(new Headers(init?.headers).get("content-type")).toBe("application/json");
|
|
252
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
253
|
+
fieldIds: [
|
|
254
|
+
"c60b8526-5835-4c9f-aecd-9a16c7704a34",
|
|
255
|
+
"1d2e3f4a-5b6c-7d8e-9f01-23456789abcd",
|
|
256
|
+
],
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("clears the unique constraint with --clear", async () => {
|
|
261
|
+
const configPath = createApiConfig();
|
|
262
|
+
const output = createStdoutBuffer();
|
|
263
|
+
const fetchMock = createFetchMock();
|
|
264
|
+
fetchMock.mockResolvedValueOnce(
|
|
265
|
+
jsonResponse({ id: "550e8400-e29b-41d4-a716-446655440000", uniqueFields: [] }),
|
|
266
|
+
);
|
|
267
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
268
|
+
|
|
269
|
+
await program.parseAsync([
|
|
270
|
+
"node",
|
|
271
|
+
"zinkee",
|
|
272
|
+
"--json",
|
|
273
|
+
"schemas",
|
|
274
|
+
"set-unique-fields",
|
|
275
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
276
|
+
"--clear",
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
const init = fetchMock.mock.calls[0]?.[1];
|
|
280
|
+
expect(init?.method).toBe("PUT");
|
|
281
|
+
expect(JSON.parse(String(init?.body))).toEqual({ fieldIds: [] });
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("rejects set-unique-fields without fields or --clear", async () => {
|
|
285
|
+
const configPath = createApiConfig();
|
|
286
|
+
const output = createStdoutBuffer();
|
|
287
|
+
const fetchMock = createFetchMock();
|
|
288
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
289
|
+
|
|
290
|
+
await expect(
|
|
291
|
+
program.parseAsync([
|
|
292
|
+
"node",
|
|
293
|
+
"zinkee",
|
|
294
|
+
"schemas",
|
|
295
|
+
"set-unique-fields",
|
|
296
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
297
|
+
]),
|
|
298
|
+
).rejects.toThrowError(
|
|
299
|
+
'Provide at least one field via "--field", or pass "--clear" to remove the unique constraint.',
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("blocks set-unique-fields in read-only mode", async () => {
|
|
306
|
+
const configPath = createApiConfig();
|
|
307
|
+
const output = createStdoutBuffer();
|
|
308
|
+
const fetchMock = createFetchMock();
|
|
309
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
310
|
+
|
|
311
|
+
await expect(
|
|
312
|
+
program.parseAsync([
|
|
313
|
+
"node",
|
|
314
|
+
"zinkee",
|
|
315
|
+
"--read-only",
|
|
316
|
+
"schemas",
|
|
317
|
+
"set-unique-fields",
|
|
318
|
+
"550e8400-e29b-41d4-a716-446655440000",
|
|
319
|
+
"--clear",
|
|
320
|
+
]),
|
|
321
|
+
).rejects.toThrowError(
|
|
322
|
+
'Command "schemas set-unique-fields" is not allowed in read-only mode.',
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
326
|
+
});
|
|
327
|
+
|
|
216
328
|
it("creates fields by merging raw JSON with explicit flags", async () => {
|
|
217
329
|
const configPath = createApiConfig();
|
|
218
330
|
const output = createStdoutBuffer();
|
package/src/commands/schemas.ts
CHANGED
|
@@ -9,11 +9,11 @@ import {
|
|
|
9
9
|
type ApiSchemaField,
|
|
10
10
|
type CreateSchemaFieldInput,
|
|
11
11
|
type CreateSchemaInput,
|
|
12
|
+
type SetUniqueFieldsInput,
|
|
12
13
|
type UpdateSchemaFieldInput,
|
|
13
14
|
type UpdateSchemaInput,
|
|
14
15
|
} from "../api/schemas.js";
|
|
15
|
-
import { ApiClientError } from "../client.js";
|
|
16
|
-
import { ApiClient } from "../client.js";
|
|
16
|
+
import { ApiClientError, ApiClient } from "../client.js";
|
|
17
17
|
import { defaultConfigPath, readRuntimeConfig } from "../config.js";
|
|
18
18
|
import {
|
|
19
19
|
normalizeUuidOrSlug,
|
|
@@ -59,6 +59,21 @@ interface SchemaUpdateCommandOptions {
|
|
|
59
59
|
highlightedField?: NormalizedUuidOrSlug;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
interface SetUniqueFieldsCommandOptions {
|
|
63
|
+
field?: NormalizedUuidOrSlug[];
|
|
64
|
+
clear?: boolean;
|
|
65
|
+
}
|
|
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
|
+
|
|
62
77
|
interface FieldCreateCommandOptions {
|
|
63
78
|
type?: string;
|
|
64
79
|
slug?: string;
|
|
@@ -179,6 +194,13 @@ function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
|
|
|
179
194
|
}
|
|
180
195
|
}
|
|
181
196
|
|
|
197
|
+
function collectFieldSelectors(
|
|
198
|
+
value: string,
|
|
199
|
+
previous: NormalizedUuidOrSlug[] = [],
|
|
200
|
+
): NormalizedUuidOrSlug[] {
|
|
201
|
+
return [...previous, parseUuidOrSlugSelector(value)];
|
|
202
|
+
}
|
|
203
|
+
|
|
182
204
|
function parseBooleanFlag(input: string): boolean {
|
|
183
205
|
const normalized = input.trim().toLowerCase();
|
|
184
206
|
|
|
@@ -571,7 +593,7 @@ export function registerSchemasCommands(
|
|
|
571
593
|
.option("--commentable <bool>", "commentable flag", parseBooleanFlag)
|
|
572
594
|
.option(
|
|
573
595
|
"--highlighted-field <field>",
|
|
574
|
-
"
|
|
596
|
+
"its value is used as each record's display name in the UI (field uuid or slug)",
|
|
575
597
|
parseUuidOrSlugSelector,
|
|
576
598
|
)
|
|
577
599
|
.action(async function action(
|
|
@@ -609,6 +631,69 @@ export function registerSchemasCommands(
|
|
|
609
631
|
}
|
|
610
632
|
|
|
611
633
|
writeLine(stdout, `Updated schema "${data.slug ?? schema.value}".`);
|
|
634
|
+
})
|
|
635
|
+
.addHelpText("after", HIGHLIGHTED_FIELD_HELP);
|
|
636
|
+
|
|
637
|
+
schemas
|
|
638
|
+
.command("set-unique-fields")
|
|
639
|
+
.description("Set or clear the composite unique-field constraint on a schema")
|
|
640
|
+
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
641
|
+
.option(
|
|
642
|
+
"--field <field>",
|
|
643
|
+
"field uuid or slug to include in the unique constraint (repeatable)",
|
|
644
|
+
collectFieldSelectors,
|
|
645
|
+
[],
|
|
646
|
+
)
|
|
647
|
+
.option("--clear", "clear the unique constraint (sends an empty field list)")
|
|
648
|
+
.action(async function action(
|
|
649
|
+
this: Command,
|
|
650
|
+
schema: NormalizedUuidOrSlug,
|
|
651
|
+
commandOptions: SetUniqueFieldsCommandOptions,
|
|
652
|
+
) {
|
|
653
|
+
const { api, json, readOnly } = createSchemasApi(this, filePath);
|
|
654
|
+
ensureWritable("schemas set-unique-fields", readOnly, "write");
|
|
655
|
+
|
|
656
|
+
const selectors = commandOptions.field ?? [];
|
|
657
|
+
|
|
658
|
+
if (commandOptions.clear && selectors.length > 0) {
|
|
659
|
+
throw new CliError(
|
|
660
|
+
"invalid_cli_usage",
|
|
661
|
+
'Use either "--field" or "--clear", not both.',
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (!commandOptions.clear && selectors.length === 0) {
|
|
666
|
+
throw new CliError(
|
|
667
|
+
"invalid_cli_usage",
|
|
668
|
+
'Provide at least one field via "--field", or pass "--clear" to remove the unique constraint.',
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const resolvedSchema = await resolveSchemaSelector(api, schema);
|
|
673
|
+
const resolvedFields = await Promise.all(
|
|
674
|
+
selectors.map((selector) => resolveFieldSelector(api, resolvedSchema, selector)),
|
|
675
|
+
);
|
|
676
|
+
const input: SetUniqueFieldsInput = {
|
|
677
|
+
fieldIds: resolvedFields.map((field) => field.value),
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const data = await api.setUniqueFields(resolvedSchema, input);
|
|
681
|
+
|
|
682
|
+
if (json) {
|
|
683
|
+
emitJson(stdout, "schemas set-unique-fields", data, filePath);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (input.fieldIds.length === 0) {
|
|
688
|
+
writeLine(stdout, `Cleared unique fields on schema "${schema.value}".`);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const count = input.fieldIds.length;
|
|
693
|
+
writeLine(
|
|
694
|
+
stdout,
|
|
695
|
+
`Set unique fields on schema "${schema.value}" (${count} field${count === 1 ? "" : "s"}).`,
|
|
696
|
+
);
|
|
612
697
|
});
|
|
613
698
|
|
|
614
699
|
schemas
|
package/src/utils/examples.ts
CHANGED
|
@@ -243,7 +243,8 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
|
|
|
243
243
|
"conditionalFormatting": []
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
|
-
]
|
|
246
|
+
],
|
|
247
|
+
"uniqueFields": []
|
|
247
248
|
}`,
|
|
248
249
|
},
|
|
249
250
|
{
|
|
@@ -263,11 +264,87 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
|
|
|
263
264
|
}`,
|
|
264
265
|
},
|
|
265
266
|
{
|
|
266
|
-
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).",
|
|
267
269
|
command:
|
|
268
270
|
'zinkee --json schemas update contacts --highlighted-field contact-name',
|
|
269
271
|
raw: `{
|
|
270
272
|
"highlightedField": "c60b8526-5835-4c9f-aecd-9a16c7704a34"
|
|
273
|
+
}`,
|
|
274
|
+
},
|
|
275
|
+
]),
|
|
276
|
+
"schemas set-unique-fields": buildCommandExamples([
|
|
277
|
+
{
|
|
278
|
+
description:
|
|
279
|
+
"Set a composite unique constraint: the listed fields must be unique together across records.",
|
|
280
|
+
command:
|
|
281
|
+
"zinkee --json schemas set-unique-fields contacts --field contact-name --field notes",
|
|
282
|
+
raw: `{
|
|
283
|
+
"fieldIds": [
|
|
284
|
+
"c60b8526-5835-4c9f-aecd-9a16c7704a34",
|
|
285
|
+
"1d2e3f4a-5b6c-7d8e-9f01-23456789abcd"
|
|
286
|
+
]
|
|
287
|
+
}`,
|
|
288
|
+
response: `{
|
|
289
|
+
"id": "2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e",
|
|
290
|
+
"name": "Contacts",
|
|
291
|
+
"slug": "contacts",
|
|
292
|
+
"folder": {
|
|
293
|
+
"id": "7f4e7c25-d8b2-4c1a-9f3e-6a7b5c8d9e1f",
|
|
294
|
+
"name": "Customers",
|
|
295
|
+
"isRoot": false
|
|
296
|
+
},
|
|
297
|
+
"auditable": true,
|
|
298
|
+
"commentable": true,
|
|
299
|
+
"highlightedField": "c60b8526-5835-4c9f-aecd-9a16c7704a34",
|
|
300
|
+
"fields": [
|
|
301
|
+
{
|
|
302
|
+
"id": "c60b8526-5835-4c9f-aecd-9a16c7704a34",
|
|
303
|
+
"type": "TextField",
|
|
304
|
+
"slug": "contact-name",
|
|
305
|
+
"label": "Customer Name",
|
|
306
|
+
"required": true,
|
|
307
|
+
"system": false,
|
|
308
|
+
"config": {
|
|
309
|
+
"textType": "NORMAL",
|
|
310
|
+
"defaultValue": "Unknown",
|
|
311
|
+
"conditionalFormatting": []
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"id": "1d2e3f4a-5b6c-7d8e-9f01-23456789abcd",
|
|
316
|
+
"type": "TextField",
|
|
317
|
+
"slug": "notes",
|
|
318
|
+
"label": "Notes",
|
|
319
|
+
"required": false,
|
|
320
|
+
"system": false,
|
|
321
|
+
"config": {
|
|
322
|
+
"textType": "LONG",
|
|
323
|
+
"defaultValue": null,
|
|
324
|
+
"conditionalFormatting": []
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
],
|
|
328
|
+
"uniqueFields": [
|
|
329
|
+
"c60b8526-5835-4c9f-aecd-9a16c7704a34",
|
|
330
|
+
"1d2e3f4a-5b6c-7d8e-9f01-23456789abcd"
|
|
331
|
+
]
|
|
332
|
+
}`,
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
description: "Set a single-field unique constraint.",
|
|
336
|
+
command: "zinkee --json schemas set-unique-fields contacts --field contact-name",
|
|
337
|
+
raw: `{
|
|
338
|
+
"fieldIds": [
|
|
339
|
+
"c60b8526-5835-4c9f-aecd-9a16c7704a34"
|
|
340
|
+
]
|
|
341
|
+
}`,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
description: "Clear the unique constraint (no field must be unique anymore).",
|
|
345
|
+
command: "zinkee --json schemas set-unique-fields contacts --clear",
|
|
346
|
+
raw: `{
|
|
347
|
+
"fieldIds": []
|
|
271
348
|
}`,
|
|
272
349
|
},
|
|
273
350
|
]),
|
|
@@ -2222,6 +2299,10 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
|
|
|
2222
2299
|
"conditionalFormatting": []
|
|
2223
2300
|
}
|
|
2224
2301
|
}
|
|
2302
|
+
],
|
|
2303
|
+
"uniqueFields": [
|
|
2304
|
+
"c60b8526-5835-4c9f-aecd-9a16c7704a34",
|
|
2305
|
+
"1d2e3f4a-5b6c-7d8e-9f01-23456789abcd"
|
|
2225
2306
|
]
|
|
2226
2307
|
}`,
|
|
2227
2308
|
},
|