zinkee 0.1.7 → 0.1.9

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.7",
3
+ "version": "0.1.9",
4
4
  "description": "CLI for Zinkee API v2",
5
5
  "type": "module",
6
6
  "bin": {
@@ -269,6 +269,95 @@ describe("schemas commands", () => {
269
269
  });
270
270
  });
271
271
 
272
+ it("creates fields with a default value flag", async () => {
273
+ const configPath = createApiConfig();
274
+ const output = createStdoutBuffer();
275
+ const fetchMock = createFetchMock();
276
+ mockSchemaLookup(fetchMock);
277
+ fetchMock.mockResolvedValueOnce(
278
+ jsonResponse({
279
+ id: "field-budget",
280
+ type: "NumberField",
281
+ slug: "budget",
282
+ label: "Budget",
283
+ required: true,
284
+ }),
285
+ );
286
+ const program = buildProgram({ configPath, stdout: output.stdout });
287
+
288
+ await program.parseAsync([
289
+ "node",
290
+ "zinkee",
291
+ "--json",
292
+ "schemas",
293
+ "fields",
294
+ "create",
295
+ "contacts",
296
+ "--type",
297
+ "number",
298
+ "--slug",
299
+ "budget",
300
+ "--label",
301
+ "Budget",
302
+ "--required",
303
+ "true",
304
+ "--default",
305
+ "0",
306
+ ]);
307
+
308
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
309
+ type: "NumberField",
310
+ slug: "budget",
311
+ label: "Budget",
312
+ required: true,
313
+ config: {
314
+ defaultValue: "0",
315
+ },
316
+ });
317
+ });
318
+
319
+ it("lets the default value flag override raw config defaults", async () => {
320
+ const configPath = createApiConfig();
321
+ const output = createStdoutBuffer();
322
+ const fetchMock = createFetchMock();
323
+ mockSchemaLookup(fetchMock);
324
+ fetchMock.mockResolvedValueOnce(
325
+ jsonResponse({
326
+ id: "field-budget",
327
+ type: "NumberField",
328
+ slug: "budget",
329
+ label: "Budget",
330
+ }),
331
+ );
332
+ const program = buildProgram({ configPath, stdout: output.stdout });
333
+
334
+ await program.parseAsync([
335
+ "node",
336
+ "zinkee",
337
+ "--json",
338
+ "schemas",
339
+ "fields",
340
+ "create",
341
+ "contacts",
342
+ "--raw",
343
+ '{"type":"number","slug":"budget","label":"Budget","config":{"defaultValue":"10","formatOptions":{"format":"number"}}}',
344
+ "--default",
345
+ "0",
346
+ ]);
347
+
348
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
349
+ type: "NumberField",
350
+ slug: "budget",
351
+ label: "Budget",
352
+ config: {
353
+ defaultValue: "0",
354
+ formatOptions: {
355
+ format: "number",
356
+ },
357
+ },
358
+ });
359
+ });
360
+
272
361
  it("updates fields from a raw file and normalizes field UUID selectors", async () => {
273
362
  const configPath = createApiConfig();
274
363
  const output = createStdoutBuffer();
@@ -342,6 +431,52 @@ describe("schemas commands", () => {
342
431
  });
343
432
  });
344
433
 
434
+ it("updates fields with a default value flag", async () => {
435
+ const configPath = createApiConfig();
436
+ const output = createStdoutBuffer();
437
+ const fetchMock = createFetchMock();
438
+ mockSchemaLookup(fetchMock);
439
+ fetchMock.mockResolvedValueOnce(
440
+ jsonResponse({
441
+ id: "field-budget",
442
+ type: "NumberField",
443
+ slug: "budget",
444
+ label: "Budget",
445
+ required: false,
446
+ }),
447
+ );
448
+ fetchMock.mockResolvedValueOnce(
449
+ jsonResponse({
450
+ id: "field-budget",
451
+ type: "NumberField",
452
+ slug: "budget",
453
+ label: "Budget",
454
+ required: false,
455
+ }),
456
+ );
457
+ const program = buildProgram({ configPath, stdout: output.stdout });
458
+
459
+ await program.parseAsync([
460
+ "node",
461
+ "zinkee",
462
+ "--json",
463
+ "schemas",
464
+ "fields",
465
+ "update",
466
+ "contacts",
467
+ "550E8400-E29B-41D4-A716-446655440002",
468
+ "--default",
469
+ "0",
470
+ ]);
471
+
472
+ expect(JSON.parse(String(fetchMock.mock.calls[2]?.[1]?.body))).toEqual({
473
+ type: "NumberField",
474
+ config: {
475
+ defaultValue: "0",
476
+ },
477
+ });
478
+ });
479
+
345
480
  it("strips formula status from update payloads", async () => {
346
481
  const configPath = createApiConfig();
347
482
  const output = createStdoutBuffer();
@@ -590,6 +725,94 @@ describe("schemas commands", () => {
590
725
  });
591
726
  });
592
727
 
728
+ it("normalizes schedule field shorthand payloads into the backend shape", async () => {
729
+ const configPath = createApiConfig();
730
+ const output = createStdoutBuffer();
731
+ const fetchMock = createFetchMock();
732
+ mockSchemaLookup(fetchMock);
733
+ fetchMock.mockResolvedValueOnce(
734
+ jsonResponse({
735
+ id: "field-5",
736
+ type: "ScheduleField",
737
+ slug: "monthly-recurrence",
738
+ label: "Monthly Recurrence",
739
+ }),
740
+ );
741
+ const program = buildProgram({ configPath, stdout: output.stdout });
742
+
743
+ await program.parseAsync([
744
+ "node",
745
+ "zinkee",
746
+ "--json",
747
+ "schemas",
748
+ "fields",
749
+ "create",
750
+ "contacts",
751
+ "--raw",
752
+ '{"type":"schedule","slug":"monthly-recurrence","label":"Monthly Recurrence","referenceField":"c52efe3f-2825-4c60-a7e3-34dd4c1ce043","beginningPeriodField":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90","endingPeriodField":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90","frequency":"monthly"}',
753
+ ]);
754
+
755
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
756
+ type: "ScheduleField",
757
+ slug: "monthly-recurrence",
758
+ label: "Monthly Recurrence",
759
+ config: {
760
+ referenceField: "c52efe3f-2825-4c60-a7e3-34dd4c1ce043",
761
+ beginningPeriodField: "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90",
762
+ endingPeriodField: "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90",
763
+ frequency: "monthly",
764
+ },
765
+ });
766
+ });
767
+
768
+ it("rejects schedule fields with invalid frequency before hitting the backend", async () => {
769
+ const configPath = createApiConfig();
770
+ const output = createStdoutBuffer();
771
+ const fetchMock = createFetchMock();
772
+ const program = buildProgram({ configPath, stdout: output.stdout });
773
+
774
+ await expect(
775
+ program.parseAsync([
776
+ "node",
777
+ "zinkee",
778
+ "schemas",
779
+ "fields",
780
+ "create",
781
+ "contacts",
782
+ "--raw",
783
+ '{"type":"ScheduleField","slug":"weekly","label":"Weekly","config":{"referenceField":"c52efe3f-2825-4c60-a7e3-34dd4c1ce043","beginningPeriodField":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90","endingPeriodField":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90","frequency":"weekly"}}',
784
+ ]),
785
+ ).rejects.toThrowError(
786
+ 'Schedule "frequency" must be "monthly" or "daily" (received "weekly").',
787
+ );
788
+
789
+ expect(fetchMock).not.toHaveBeenCalled();
790
+ });
791
+
792
+ it("rejects schedule fields missing required config fields", async () => {
793
+ const configPath = createApiConfig();
794
+ const output = createStdoutBuffer();
795
+ const fetchMock = createFetchMock();
796
+ const program = buildProgram({ configPath, stdout: output.stdout });
797
+
798
+ await expect(
799
+ program.parseAsync([
800
+ "node",
801
+ "zinkee",
802
+ "schemas",
803
+ "fields",
804
+ "create",
805
+ "contacts",
806
+ "--raw",
807
+ '{"type":"ScheduleField","slug":"recurrence","label":"Recurrence","config":{"frequency":"daily"}}',
808
+ ]),
809
+ ).rejects.toThrowError(
810
+ /Schedule fields require "referenceField", "beginningPeriodField", "endingPeriodField" and "frequency"/,
811
+ );
812
+
813
+ expect(fetchMock).not.toHaveBeenCalled();
814
+ });
815
+
593
816
  it("rejects unsupported field types before making network requests", async () => {
594
817
  const configPath = createApiConfig();
595
818
  const output = createStdoutBuffer();
@@ -612,7 +835,7 @@ describe("schemas commands", () => {
612
835
  "Bad",
613
836
  ]),
614
837
  ).rejects.toThrowError(
615
- 'Unsupported field type "InvalidField". Valid types: text, number, date, option, file, reference, formula, member, autoincremental.',
838
+ 'Unsupported field type "InvalidField". Valid types: text, number, date, option, file, reference, formula, member, autoincremental, schedule.',
616
839
  );
617
840
 
618
841
  expect(fetchMock).not.toHaveBeenCalled();
@@ -626,16 +849,16 @@ describe("schemas commands", () => {
626
849
  const help = create?.helpInformation() ?? "";
627
850
 
628
851
  expect(help).toContain("--type <type>");
852
+ expect(help).toContain("--default <value>");
629
853
  expect(help).toContain("member");
630
854
  expect(help).toContain("autoincremental");
631
855
  expect(help).toContain("Raw payload shapes by type:");
632
- expect(help).toContain('"type":"number","slug":"price-eur"');
856
+ expect(help).toContain('"type":"NumberField","slug":"quantity"');
633
857
  expect(help).toContain('"defaultValue":"0"');
634
- expect(help).toContain('"symbolEnabled":true');
635
858
  expect(help).toContain('- member: Workspace member field.');
636
- expect(help).toContain('"type":"member","slug":"owner","label":"Owner"');
859
+ expect(help).toContain('"type":"MemberField","slug":"owner","label":"Owner"');
637
860
  expect(help).toContain('- option: Dropdown/select field with options.');
638
- expect(help).toContain('"type":"option","slug":"status","label":"Status"');
861
+ expect(help).toContain('"type":"OptionField","slug":"status","label":"Status"');
639
862
  expect(help).toContain('Use "zinkee --example schemas fields create --type <type>"');
640
863
  });
641
864
 
@@ -692,7 +915,7 @@ describe("schemas commands", () => {
692
915
  '{"type":"option","slug":"status","label":"Status","options":[{"id":1,"name":"Open","color":"default"}]}',
693
916
  ]),
694
917
  ).rejects.toThrowError(
695
- 'Invalid payload for field type "option". Expected raw shape: {"type":"option","slug":"status","label":"Status","config":{"options":[{"id":1,"name":"Open","color":"default"},{"id":2,"name":"Closed","color":"success"}]}}',
918
+ 'Invalid payload for field type "option". Expected raw shape: {"type":"OptionField","slug":"status","label":"Status","config":{"options":[{"id":1,"name":"Open","color":"default"},{"id":2,"name":"Closed","color":"success"}]}}',
696
919
  );
697
920
  });
698
921
 
@@ -63,6 +63,7 @@ interface FieldCreateCommandOptions {
63
63
  slug?: string;
64
64
  label?: string;
65
65
  required?: boolean;
66
+ default?: string;
66
67
  raw?: string;
67
68
  rawFile?: string;
68
69
  }
@@ -70,6 +71,7 @@ interface FieldCreateCommandOptions {
70
71
  interface FieldUpdateCommandOptions {
71
72
  label?: string;
72
73
  required?: boolean;
74
+ default?: string;
73
75
  raw?: string;
74
76
  rawFile?: string;
75
77
  }
@@ -301,6 +303,29 @@ function buildSchemaUpdateInput(options: SchemaUpdateCommandOptions): UpdateSche
301
303
  return input;
302
304
  }
303
305
 
306
+ function applyFieldDefaultValue(
307
+ input: CreateSchemaFieldInput | UpdateSchemaFieldInput,
308
+ defaultValue: string | undefined,
309
+ ): void {
310
+ if (defaultValue === undefined) {
311
+ return;
312
+ }
313
+
314
+ const config = input.config;
315
+
316
+ if (config !== undefined && !isJsonObject(config)) {
317
+ throw new CliError(
318
+ "invalid_cli_usage",
319
+ 'Expected field "config" to be a JSON object when using "--default".',
320
+ );
321
+ }
322
+
323
+ input.config = {
324
+ ...(config ?? {}),
325
+ defaultValue,
326
+ };
327
+ }
328
+
304
329
  function buildFieldCreateInput(
305
330
  options: FieldCreateCommandOptions,
306
331
  ): CreateSchemaFieldInput {
@@ -337,6 +362,8 @@ function buildFieldCreateInput(
337
362
  input.required = options.required;
338
363
  }
339
364
 
365
+ applyFieldDefaultValue(input, options.default);
366
+
340
367
  if ("config" in input && isJsonObject(input.config) && isEmptyObject(input.config)) {
341
368
  delete input.config;
342
369
  }
@@ -411,6 +438,8 @@ function buildFieldUpdateInput(
411
438
  input.required = options.required;
412
439
  }
413
440
 
441
+ applyFieldDefaultValue(input, options.default);
442
+
414
443
  return input;
415
444
  }
416
445
 
@@ -640,6 +669,7 @@ export function registerSchemasCommands(
640
669
  .option("--slug <slug>", "field slug")
641
670
  .option("--label <label>", "field label")
642
671
  .option("--required <bool>", "required flag", parseBooleanFlag)
672
+ .option("--default <value>", "default field value")
643
673
  .option("--raw <json>", 'raw JSON object payload (see type-specific shapes in --help)')
644
674
  .option("--raw-file <path>", "path to a raw JSON object payload")
645
675
  .action(async function action(
@@ -673,6 +703,7 @@ export function registerSchemasCommands(
673
703
  .argument("<field>", "field uuid or slug", parseUuidOrSlugSelector)
674
704
  .option("--label <label>", "field label")
675
705
  .option("--required <bool>", "required flag", parseBooleanFlag)
706
+ .option("--default <value>", "default field value")
676
707
  .option("--raw <json>", "raw JSON object payload")
677
708
  .option("--raw-file <path>", "path to a raw JSON object payload")
678
709
  .action(async function action(
package/src/index.test.ts CHANGED
@@ -85,12 +85,12 @@ describe("project bootstrap", () => {
85
85
  );
86
86
 
87
87
  expect(output).toContain('zinkee --json schemas fields create contacts --raw');
88
- expect(output).toContain('"type":"option"');
89
- expect(output).not.toContain('"type":"text"');
90
- expect(output).toContain("Create a option field");
88
+ expect(output).toContain('"type":"OptionField"');
89
+ expect(output).not.toContain('"type":"TextField"');
90
+ expect(output).toContain("Option field (select dropdown)");
91
91
  });
92
92
 
93
- it("shows both number creation examples, including Moneda formatting", async () => {
93
+ it("shows every backend number variant, including currency formatting", async () => {
94
94
  let output = "";
95
95
 
96
96
  await showExamples(
@@ -105,9 +105,15 @@ describe("project bootstrap", () => {
105
105
  { type: "number" },
106
106
  );
107
107
 
108
- expect(output).toContain('"slug":"amount"');
108
+ expect(output).toContain('"slug":"quantity"');
109
+ expect(output).toContain('"slug":"amount-eur"');
110
+ expect(output).toContain('"slug":"amount-usd"');
111
+ expect(output).toContain('"defaultValue":"0"');
109
112
  expect(output).toContain('"symbolEnabled":true');
113
+ expect(output).toContain('"symbolPosition":"before"');
110
114
  expect(output).toContain('"symbolPosition":"right"');
115
+ expect(output).toContain('"format":"percentage"');
116
+ expect(output).toContain('"durationFormat":"HH:MM"');
111
117
  });
112
118
 
113
119
  it("shows automation examples with descriptions and equivalent payloads", async () => {
@@ -21,18 +21,54 @@ describe("getCommandExamples", () => {
21
21
  const examples = getCommandExamples("schemas fields create", { type: "member" });
22
22
 
23
23
  expect(examples).toHaveLength(1);
24
- expect(examples[0]?.command).toContain('"type":"member"');
25
- expect(examples[0]?.description).toContain("member field");
24
+ expect(examples[0]?.command).toContain('"type":"MemberField"');
25
+ expect(examples[0]?.description).toBe("Member field");
26
26
  });
27
27
 
28
- it("returns both plain and currency-style examples for number fields", () => {
29
- const examples = getCommandExamples("schemas fields create", { type: "number" });
28
+ it("returns monthly and daily variants for schedule fields", () => {
29
+ const examples = getCommandExamples("schemas fields create", { type: "schedule" });
30
30
 
31
31
  expect(examples).toHaveLength(2);
32
- expect(examples[0]?.command).toContain('"slug":"amount"');
33
- expect(examples[1]?.command).toContain('"symbolEnabled":true');
34
- expect(examples[1]?.command).toContain('"symbolValue":" €"');
35
- expect(examples[1]?.description).toContain("Moneda");
32
+ expect(examples.map((example) => example.description)).toEqual([
33
+ "Schedule field (monthly)",
34
+ "Schedule field (daily)",
35
+ ]);
36
+ expect(examples[0]?.command).toContain('"type":"ScheduleField"');
37
+ expect(examples[0]?.command).toContain('"frequency":"monthly"');
38
+ expect(examples[1]?.command).toContain('"frequency":"daily"');
39
+ });
40
+
41
+ it("covers every backend number field variant", () => {
42
+ const examples = getCommandExamples("schemas fields create", { type: "number" });
43
+
44
+ expect(examples).toHaveLength(6);
45
+
46
+ const descriptions = examples.map((example) => example.description);
47
+ expect(descriptions).toEqual([
48
+ "Number field (plain integer)",
49
+ "Number field (decimal with thousands)",
50
+ "Number field (currency € prefix)",
51
+ "Number field (currency $ suffix)",
52
+ "Number field (percentage)",
53
+ "Number field (duration HH:MM)",
54
+ ]);
55
+
56
+ expect(examples[0]?.command).toContain('"slug":"quantity"');
57
+ expect(examples[0]?.command).toContain('"format":"number"');
58
+
59
+ const eurCurrency = examples[2]?.command ?? "";
60
+ expect(eurCurrency).toContain('"format":"currency"');
61
+ expect(eurCurrency).toContain('"symbolEnabled":true');
62
+ expect(eurCurrency).toContain('"symbolPosition":"before"');
63
+ expect(eurCurrency).toContain('"symbolValue":"€"');
64
+
65
+ const usdCurrency = examples[3]?.command ?? "";
66
+ expect(usdCurrency).toContain('"symbolPosition":"right"');
67
+ expect(usdCurrency).toContain('"symbolValue":" $"');
68
+
69
+ expect(examples[4]?.command).toContain('"format":"percentage"');
70
+ expect(examples[5]?.command).toContain('"format":"duration"');
71
+ expect(examples[5]?.command).toContain('"durationFormat":"HH:MM"');
36
72
  });
37
73
 
38
74
  it("returns canonical automation creation examples", () => {