zinkee 0.1.30 → 0.1.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zinkee",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "CLI for Zinkee API v2",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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)}`,
@@ -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();
@@ -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,11 @@ interface SchemaUpdateCommandOptions {
59
59
  highlightedField?: NormalizedUuidOrSlug;
60
60
  }
61
61
 
62
+ interface SetUniqueFieldsCommandOptions {
63
+ field?: NormalizedUuidOrSlug[];
64
+ clear?: boolean;
65
+ }
66
+
62
67
  interface FieldCreateCommandOptions {
63
68
  type?: string;
64
69
  slug?: string;
@@ -179,6 +184,13 @@ function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
179
184
  }
180
185
  }
181
186
 
187
+ function collectFieldSelectors(
188
+ value: string,
189
+ previous: NormalizedUuidOrSlug[] = [],
190
+ ): NormalizedUuidOrSlug[] {
191
+ return [...previous, parseUuidOrSlugSelector(value)];
192
+ }
193
+
182
194
  function parseBooleanFlag(input: string): boolean {
183
195
  const normalized = input.trim().toLowerCase();
184
196
 
@@ -611,6 +623,68 @@ export function registerSchemasCommands(
611
623
  writeLine(stdout, `Updated schema "${data.slug ?? schema.value}".`);
612
624
  });
613
625
 
626
+ schemas
627
+ .command("set-unique-fields")
628
+ .description("Set or clear the composite unique-field constraint on a schema")
629
+ .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
630
+ .option(
631
+ "--field <field>",
632
+ "field uuid or slug to include in the unique constraint (repeatable)",
633
+ collectFieldSelectors,
634
+ [],
635
+ )
636
+ .option("--clear", "clear the unique constraint (sends an empty field list)")
637
+ .action(async function action(
638
+ this: Command,
639
+ schema: NormalizedUuidOrSlug,
640
+ commandOptions: SetUniqueFieldsCommandOptions,
641
+ ) {
642
+ const { api, json, readOnly } = createSchemasApi(this, filePath);
643
+ ensureWritable("schemas set-unique-fields", readOnly, "write");
644
+
645
+ const selectors = commandOptions.field ?? [];
646
+
647
+ if (commandOptions.clear && selectors.length > 0) {
648
+ throw new CliError(
649
+ "invalid_cli_usage",
650
+ 'Use either "--field" or "--clear", not both.',
651
+ );
652
+ }
653
+
654
+ if (!commandOptions.clear && selectors.length === 0) {
655
+ throw new CliError(
656
+ "invalid_cli_usage",
657
+ 'Provide at least one field via "--field", or pass "--clear" to remove the unique constraint.',
658
+ );
659
+ }
660
+
661
+ const resolvedSchema = await resolveSchemaSelector(api, schema);
662
+ const resolvedFields = await Promise.all(
663
+ selectors.map((selector) => resolveFieldSelector(api, resolvedSchema, selector)),
664
+ );
665
+ const input: SetUniqueFieldsInput = {
666
+ fieldIds: resolvedFields.map((field) => field.value),
667
+ };
668
+
669
+ const data = await api.setUniqueFields(resolvedSchema, input);
670
+
671
+ if (json) {
672
+ emitJson(stdout, "schemas set-unique-fields", data, filePath);
673
+ return;
674
+ }
675
+
676
+ if (input.fieldIds.length === 0) {
677
+ writeLine(stdout, `Cleared unique fields on schema "${schema.value}".`);
678
+ return;
679
+ }
680
+
681
+ const count = input.fieldIds.length;
682
+ writeLine(
683
+ stdout,
684
+ `Set unique fields on schema "${schema.value}" (${count} field${count === 1 ? "" : "s"}).`,
685
+ );
686
+ });
687
+
614
688
  schemas
615
689
  .command("delete")
616
690
  .description("Delete a schema")
@@ -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
  {
@@ -268,6 +269,81 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
268
269
  'zinkee --json schemas update contacts --highlighted-field contact-name',
269
270
  raw: `{
270
271
  "highlightedField": "c60b8526-5835-4c9f-aecd-9a16c7704a34"
272
+ }`,
273
+ },
274
+ ]),
275
+ "schemas set-unique-fields": buildCommandExamples([
276
+ {
277
+ description:
278
+ "Set a composite unique constraint: the listed fields must be unique together across records.",
279
+ command:
280
+ "zinkee --json schemas set-unique-fields contacts --field contact-name --field notes",
281
+ raw: `{
282
+ "fieldIds": [
283
+ "c60b8526-5835-4c9f-aecd-9a16c7704a34",
284
+ "1d2e3f4a-5b6c-7d8e-9f01-23456789abcd"
285
+ ]
286
+ }`,
287
+ response: `{
288
+ "id": "2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e",
289
+ "name": "Contacts",
290
+ "slug": "contacts",
291
+ "folder": {
292
+ "id": "7f4e7c25-d8b2-4c1a-9f3e-6a7b5c8d9e1f",
293
+ "name": "Customers",
294
+ "isRoot": false
295
+ },
296
+ "auditable": true,
297
+ "commentable": true,
298
+ "highlightedField": "c60b8526-5835-4c9f-aecd-9a16c7704a34",
299
+ "fields": [
300
+ {
301
+ "id": "c60b8526-5835-4c9f-aecd-9a16c7704a34",
302
+ "type": "TextField",
303
+ "slug": "contact-name",
304
+ "label": "Customer Name",
305
+ "required": true,
306
+ "system": false,
307
+ "config": {
308
+ "textType": "NORMAL",
309
+ "defaultValue": "Unknown",
310
+ "conditionalFormatting": []
311
+ }
312
+ },
313
+ {
314
+ "id": "1d2e3f4a-5b6c-7d8e-9f01-23456789abcd",
315
+ "type": "TextField",
316
+ "slug": "notes",
317
+ "label": "Notes",
318
+ "required": false,
319
+ "system": false,
320
+ "config": {
321
+ "textType": "LONG",
322
+ "defaultValue": null,
323
+ "conditionalFormatting": []
324
+ }
325
+ }
326
+ ],
327
+ "uniqueFields": [
328
+ "c60b8526-5835-4c9f-aecd-9a16c7704a34",
329
+ "1d2e3f4a-5b6c-7d8e-9f01-23456789abcd"
330
+ ]
331
+ }`,
332
+ },
333
+ {
334
+ description: "Set a single-field unique constraint.",
335
+ command: "zinkee --json schemas set-unique-fields contacts --field contact-name",
336
+ raw: `{
337
+ "fieldIds": [
338
+ "c60b8526-5835-4c9f-aecd-9a16c7704a34"
339
+ ]
340
+ }`,
341
+ },
342
+ {
343
+ description: "Clear the unique constraint (no field must be unique anymore).",
344
+ command: "zinkee --json schemas set-unique-fields contacts --clear",
345
+ raw: `{
346
+ "fieldIds": []
271
347
  }`,
272
348
  },
273
349
  ]),
@@ -2222,6 +2298,10 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
2222
2298
  "conditionalFormatting": []
2223
2299
  }
2224
2300
  }
2301
+ ],
2302
+ "uniqueFields": [
2303
+ "c60b8526-5835-4c9f-aecd-9a16c7704a34",
2304
+ "1d2e3f4a-5b6c-7d8e-9f01-23456789abcd"
2225
2305
  ]
2226
2306
  }`,
2227
2307
  },