zinkee 0.1.41 → 0.1.43

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.
@@ -0,0 +1,243 @@
1
+ import type { Writable } from "node:stream";
2
+
3
+ import { Command, InvalidArgumentError } from "commander";
4
+
5
+ import {
6
+ FormulasApi,
7
+ type FormulaRecomputeInput,
8
+ type FormulaRecomputeResponse,
9
+ } from "../api/formulas.js";
10
+ import { ApiClient } from "../client.js";
11
+ import { defaultConfigPath, readRuntimeConfig } from "../config.js";
12
+ import {
13
+ normalizeUuidOrSlug,
14
+ type NormalizedUuidOrSlug,
15
+ } from "../parsers/selectors.js";
16
+ import { resolveRuntimeContext } from "../runtime-context.js";
17
+ import type { RuntimeOverrides } from "../types.js";
18
+ import { CliError } from "../utils/errors.js";
19
+ import { buildJsonSuccess } from "../utils/output.js";
20
+
21
+ interface CommandIo {
22
+ write(chunk: string): unknown;
23
+ }
24
+
25
+ export interface RegisterFormulasCommandsOptions {
26
+ configPath?: string;
27
+ stdout?: CommandIo;
28
+ }
29
+
30
+ interface GlobalOptions extends RuntimeOverrides {}
31
+
32
+ interface ConfirmedCommandOptions {
33
+ yes?: boolean;
34
+ }
35
+
36
+ function getCommandIo(stdout: CommandIo | undefined): CommandIo {
37
+ return stdout ?? (process.stdout as Writable);
38
+ }
39
+
40
+ function writeLine(stdout: CommandIo, value: string): void {
41
+ stdout.write(`${value}\n`);
42
+ }
43
+
44
+ function getGlobalOptions(command: Command): GlobalOptions {
45
+ return command.optsWithGlobals<GlobalOptions>();
46
+ }
47
+
48
+ function toConfigurationError(filePath: string, error: unknown): never {
49
+ if (error instanceof CliError) {
50
+ throw error;
51
+ }
52
+
53
+ if (
54
+ typeof error === "object" &&
55
+ error !== null &&
56
+ "code" in error &&
57
+ error.code === "ENOENT"
58
+ ) {
59
+ throw new CliError("configuration_error", `Config file not found at ${filePath}.`);
60
+ }
61
+
62
+ if (error instanceof Error) {
63
+ throw new CliError("configuration_error", error.message);
64
+ }
65
+
66
+ throw error;
67
+ }
68
+
69
+ function createFormulasApi(
70
+ command: Command,
71
+ filePath: string,
72
+ ): { api: FormulasApi; json: boolean; readOnly: boolean } {
73
+ try {
74
+ const config = readRuntimeConfig(filePath);
75
+ const runtime = resolveRuntimeContext(config, getGlobalOptions(command));
76
+
77
+ if (!runtime.baseUrl) {
78
+ throw new CliError(
79
+ "configuration_error",
80
+ `Resolved profile "${runtime.profile}" is missing a base URL.`,
81
+ );
82
+ }
83
+
84
+ return {
85
+ api: new FormulasApi(
86
+ new ApiClient({
87
+ baseUrl: runtime.baseUrl,
88
+ apiKey: runtime.apiKey,
89
+ }),
90
+ ),
91
+ json: runtime.json,
92
+ readOnly: runtime.readOnly,
93
+ };
94
+ } catch (error) {
95
+ toConfigurationError(filePath, error);
96
+ }
97
+ }
98
+
99
+ function ensureWritable(commandName: string, readOnly: boolean): void {
100
+ if (readOnly) {
101
+ throw new CliError(
102
+ "command_not_allowed_in_read_only_mode",
103
+ `Command "${commandName}" is not allowed in read-only mode.`,
104
+ {
105
+ commandMode: "write",
106
+ executionMode: "read-only",
107
+ },
108
+ );
109
+ }
110
+ }
111
+
112
+ function ensureConfirmed(commandName: string, confirmed: boolean | undefined): void {
113
+ if (!confirmed) {
114
+ throw new CliError(
115
+ "invalid_cli_usage",
116
+ `Command "${commandName}" requires --yes because it may enqueue a large recalculation.`,
117
+ );
118
+ }
119
+ }
120
+
121
+ function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
122
+ try {
123
+ return normalizeUuidOrSlug(input);
124
+ } catch (error) {
125
+ const message = error instanceof Error ? error.message : "Invalid selector.";
126
+ throw new InvalidArgumentError(message);
127
+ }
128
+ }
129
+
130
+ function emitResult(
131
+ stdout: CommandIo,
132
+ commandName: string,
133
+ result: FormulaRecomputeResponse,
134
+ configPath: string,
135
+ json: boolean,
136
+ ): void {
137
+ if (json) {
138
+ writeLine(
139
+ stdout,
140
+ JSON.stringify(buildJsonSuccess(result, { command: commandName, configPath })),
141
+ );
142
+ return;
143
+ }
144
+
145
+ writeLine(
146
+ stdout,
147
+ `Queued ${result.eventsQueued} formula recalculation event(s) for ${result.scope.toLowerCase()} scope (request ${result.requestId}).`,
148
+ );
149
+ }
150
+
151
+ async function recompute(
152
+ command: Command,
153
+ commandName: string,
154
+ input: FormulaRecomputeInput,
155
+ options: RegisterFormulasCommandsOptions,
156
+ requireConfirmation: boolean,
157
+ confirmed?: boolean,
158
+ ): Promise<void> {
159
+ const configPath = options.configPath ?? defaultConfigPath;
160
+ const stdout = getCommandIo(options.stdout);
161
+ const { api, json, readOnly } = createFormulasApi(command, configPath);
162
+
163
+ ensureWritable(commandName, readOnly);
164
+ if (requireConfirmation) {
165
+ ensureConfirmed(commandName, confirmed);
166
+ }
167
+
168
+ const result = await api.recompute(input);
169
+ emitResult(stdout, commandName, result, configPath, json);
170
+ }
171
+
172
+ export function registerFormulasCommands(
173
+ program: Command,
174
+ options: RegisterFormulasCommandsOptions = {},
175
+ ): void {
176
+ const formulas = program
177
+ .command("formulas")
178
+ .description("Recalculate formula values");
179
+ const recomputeCommand = formulas
180
+ .command("recompute")
181
+ .description("Queue a full formula recalculation");
182
+
183
+ recomputeCommand
184
+ .command("field")
185
+ .description("Queue recalculation of one formula column")
186
+ .argument("<schema>", "schema UUID or slug", parseUuidOrSlugSelector)
187
+ .argument("<field>", "formula field UUID or slug", parseUuidOrSlugSelector)
188
+ .action(
189
+ async function action(
190
+ this: Command,
191
+ schema: NormalizedUuidOrSlug,
192
+ field: NormalizedUuidOrSlug,
193
+ ) {
194
+ await recompute(
195
+ this,
196
+ "formulas recompute field",
197
+ { scope: "FIELD", schema, field },
198
+ options,
199
+ false,
200
+ );
201
+ },
202
+ );
203
+
204
+ recomputeCommand
205
+ .command("schema")
206
+ .description("Queue recalculation of all active formulas in one schema")
207
+ .argument("<schema>", "schema UUID or slug", parseUuidOrSlugSelector)
208
+ .option("--yes", "confirm the potentially expensive recalculation")
209
+ .action(
210
+ async function action(
211
+ this: Command,
212
+ schema: NormalizedUuidOrSlug,
213
+ commandOptions: ConfirmedCommandOptions,
214
+ ) {
215
+ await recompute(
216
+ this,
217
+ "formulas recompute schema",
218
+ { scope: "SCHEMA", schema },
219
+ options,
220
+ true,
221
+ commandOptions.yes,
222
+ );
223
+ },
224
+ );
225
+
226
+ recomputeCommand
227
+ .command("workspace")
228
+ .description("Queue recalculation of all active formulas in the workspace")
229
+ .option("--yes", "confirm the potentially expensive recalculation")
230
+ .action(async function action(
231
+ this: Command,
232
+ commandOptions: ConfirmedCommandOptions,
233
+ ) {
234
+ await recompute(
235
+ this,
236
+ "formulas recompute workspace",
237
+ { scope: "WORKSPACE" },
238
+ options,
239
+ true,
240
+ commandOptions.yes,
241
+ );
242
+ });
243
+ }
@@ -381,6 +381,48 @@ describe("schemas commands", () => {
381
381
  });
382
382
  });
383
383
 
384
+ it("creates fields with a description from the dedicated flag", async () => {
385
+ const configPath = createApiConfig();
386
+ const output = createStdoutBuffer();
387
+ const fetchMock = createFetchMock();
388
+ mockSchemaLookup(fetchMock);
389
+ fetchMock.mockResolvedValueOnce(
390
+ jsonResponse({
391
+ id: "field-1",
392
+ type: "TextField",
393
+ slug: "email",
394
+ label: "Email",
395
+ description: "Primary contact email",
396
+ }),
397
+ );
398
+ const program = buildProgram({ configPath, stdout: output.stdout });
399
+
400
+ await program.parseAsync([
401
+ "node",
402
+ "zinkee",
403
+ "--json",
404
+ "schemas",
405
+ "fields",
406
+ "create",
407
+ "contacts",
408
+ "--type",
409
+ "text",
410
+ "--slug",
411
+ "email",
412
+ "--label",
413
+ "Email",
414
+ "--description",
415
+ "Primary contact email",
416
+ ]);
417
+
418
+ expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
419
+ type: "TextField",
420
+ slug: "email",
421
+ label: "Email",
422
+ description: "Primary contact email",
423
+ });
424
+ });
425
+
384
426
  it("creates fields with a default value flag", async () => {
385
427
  const configPath = createApiConfig();
386
428
  const output = createStdoutBuffer();
@@ -589,6 +631,53 @@ describe("schemas commands", () => {
589
631
  });
590
632
  });
591
633
 
634
+ it.each([
635
+ ["sets", "Shown in customer notifications"],
636
+ ["clears", ""],
637
+ ])("%s a field description without another metadata change", async (_action, description) => {
638
+ const configPath = createApiConfig();
639
+ const output = createStdoutBuffer();
640
+ const fetchMock = createFetchMock();
641
+ mockSchemaLookup(fetchMock);
642
+ fetchMock.mockResolvedValueOnce(
643
+ jsonResponse({
644
+ id: "550e8400-e29b-41d4-a716-446655440003",
645
+ type: "TextField",
646
+ slug: "notes",
647
+ label: "Notes",
648
+ required: false,
649
+ }),
650
+ );
651
+ fetchMock.mockResolvedValueOnce(
652
+ jsonResponse({
653
+ id: "550e8400-e29b-41d4-a716-446655440003",
654
+ type: "TextField",
655
+ slug: "notes",
656
+ label: "Notes",
657
+ description,
658
+ }),
659
+ );
660
+ const program = buildProgram({ configPath, stdout: output.stdout });
661
+
662
+ await program.parseAsync([
663
+ "node",
664
+ "zinkee",
665
+ "--json",
666
+ "schemas",
667
+ "fields",
668
+ "update",
669
+ "contacts",
670
+ "550E8400-E29B-41D4-A716-446655440003",
671
+ "--description",
672
+ description,
673
+ ]);
674
+
675
+ expect(JSON.parse(String(fetchMock.mock.calls[2]?.[1]?.body))).toEqual({
676
+ type: "TextField",
677
+ description,
678
+ });
679
+ });
680
+
592
681
  it("strips formula status from update payloads", async () => {
593
682
  const configPath = createApiConfig();
594
683
  const output = createStdoutBuffer();
@@ -78,6 +78,7 @@ interface FieldCreateCommandOptions {
78
78
  type?: string;
79
79
  slug?: string;
80
80
  label?: string;
81
+ description?: string;
81
82
  required?: boolean;
82
83
  default?: string;
83
84
  raw?: string;
@@ -86,6 +87,7 @@ interface FieldCreateCommandOptions {
86
87
 
87
88
  interface FieldUpdateCommandOptions {
88
89
  label?: string;
90
+ description?: string;
89
91
  required?: boolean;
90
92
  default?: string;
91
93
  raw?: string;
@@ -386,6 +388,10 @@ function buildFieldCreateInput(
386
388
  input.required = options.required;
387
389
  }
388
390
 
391
+ if (options.description !== undefined) {
392
+ input.description = options.description;
393
+ }
394
+
389
395
  applyFieldDefaultValue(input, options.default);
390
396
 
391
397
  if ("config" in input && isJsonObject(input.config) && isEmptyObject(input.config)) {
@@ -462,6 +468,10 @@ function buildFieldUpdateInput(
462
468
  input.required = options.required;
463
469
  }
464
470
 
471
+ if (options.description !== undefined) {
472
+ input.description = options.description;
473
+ }
474
+
465
475
  applyFieldDefaultValue(input, options.default);
466
476
 
467
477
  return input;
@@ -769,6 +779,7 @@ export function registerSchemasCommands(
769
779
  .option("--type <type>", `field type (${formatSchemaFieldTypes()})`)
770
780
  .option("--slug <slug>", "field slug")
771
781
  .option("--label <label>", "field label")
782
+ .option("--description <text>", "field description")
772
783
  .option("--required <bool>", "required flag", parseBooleanFlag)
773
784
  .option("--default <value>", "default field value")
774
785
  .option("--raw <json>", 'raw JSON object payload (see type-specific shapes in --help)')
@@ -803,6 +814,7 @@ export function registerSchemasCommands(
803
814
  .argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
804
815
  .argument("<field>", "field uuid or slug", parseUuidOrSlugSelector)
805
816
  .option("--label <label>", "field label")
817
+ .option("--description <text>", 'field description (use "" to clear)')
806
818
  .option("--required <bool>", "required flag", parseBooleanFlag)
807
819
  .option("--default <value>", "default field value")
808
820
  .option("--raw <json>", "raw JSON object payload")
@@ -832,7 +844,7 @@ export function registerSchemasCommands(
832
844
  if (isEmptyObject(input)) {
833
845
  throw new CliError(
834
846
  "invalid_cli_usage",
835
- 'Provide at least one field update via "--label", "--required", "--raw", or "--raw-file".',
847
+ 'Provide at least one field update via "--label", "--description", "--required", "--default", "--raw", or "--raw-file".',
836
848
  );
837
849
  }
838
850
 
package/src/program.ts CHANGED
@@ -8,6 +8,7 @@ import { registerCommentsCommands } from "./commands/comments.js";
8
8
  import { registerDisplaysCommands } from "./commands/displays.js";
9
9
  import { registerDocumentTemplatesCommands } from "./commands/document-templates.js";
10
10
  import { registerFilesCommands } from "./commands/files.js";
11
+ import { registerFormulasCommands } from "./commands/formulas.js";
11
12
  import { registerLogsCommands } from "./commands/logs.js";
12
13
  import { registerNavigationCommands } from "./commands/navigation.js";
13
14
  import { registerProfilesCommands } from "./commands/profiles.js";
@@ -51,6 +52,7 @@ export function buildProgram(options: BuildProgramOptions = {}): Command {
51
52
  registerDisplaysCommands(program, options);
52
53
  registerDocumentTemplatesCommands(program, options);
53
54
  registerFilesCommands(program, options);
55
+ registerFormulasCommands(program, options);
54
56
  registerNavigationCommands(program, options);
55
57
  registerSchemasCommands(program, options);
56
58
  registerRecordsCommands(program, options);
@@ -27,6 +27,60 @@ export function buildCommandExamples(examples: readonly CommandExample[]): reado
27
27
  const SAMPLE_UUID = "550e8400-e29b-41d4-a716-446655440001";
28
28
 
29
29
  const specificExamples: Record<string, readonly CommandExample[]> = {
30
+ "formulas recompute field": buildCommandExamples([
31
+ {
32
+ description: "Queue a full recalculation of one formula column.",
33
+ command: "zinkee --json formulas recompute field invoices total",
34
+ raw: `{
35
+ "scope": "FIELD",
36
+ "schema": "invoices",
37
+ "field": "total"
38
+ }`,
39
+ response: `{
40
+ "requestId": "02ab5d47-cb5f-4eb2-afb8-e9a3a9eb34d4",
41
+ "scope": "FIELD",
42
+ "formulaCount": 1,
43
+ "skippedFormulaCount": 0,
44
+ "eventsQueued": 1,
45
+ "status": "QUEUED"
46
+ }`,
47
+ },
48
+ ]),
49
+ "formulas recompute schema": buildCommandExamples([
50
+ {
51
+ description: "Queue recalculation of every active formula in one schema.",
52
+ command: "zinkee --json formulas recompute schema invoices --yes",
53
+ raw: `{
54
+ "scope": "SCHEMA",
55
+ "schema": "invoices"
56
+ }`,
57
+ response: `{
58
+ "requestId": "02ab5d47-cb5f-4eb2-afb8-e9a3a9eb34d4",
59
+ "scope": "SCHEMA",
60
+ "formulaCount": 8,
61
+ "skippedFormulaCount": 1,
62
+ "eventsQueued": 8,
63
+ "status": "QUEUED"
64
+ }`,
65
+ },
66
+ ]),
67
+ "formulas recompute workspace": buildCommandExamples([
68
+ {
69
+ description: "Queue recalculation of every active formula in the workspace.",
70
+ command: "zinkee --json formulas recompute workspace --yes",
71
+ raw: `{
72
+ "scope": "WORKSPACE"
73
+ }`,
74
+ response: `{
75
+ "requestId": "02ab5d47-cb5f-4eb2-afb8-e9a3a9eb34d4",
76
+ "scope": "WORKSPACE",
77
+ "formulaCount": 24,
78
+ "skippedFormulaCount": 2,
79
+ "eventsQueued": 24,
80
+ "status": "QUEUED"
81
+ }`,
82
+ },
83
+ ]),
30
84
  "profiles list": buildCommandExamples([
31
85
  {
32
86
  description:
@@ -350,6 +404,22 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
350
404
  },
351
405
  ]),
352
406
  "schemas fields update": buildCommandExamples([
407
+ {
408
+ description: "Set the field description without changing its label or configuration.",
409
+ command:
410
+ 'zinkee --json schemas fields update contacts contact-name --description "Primary name shown for the customer"',
411
+ raw: `{
412
+ "description": "Primary name shown for the customer"
413
+ }`,
414
+ },
415
+ {
416
+ description: "Clear the field description.",
417
+ command:
418
+ 'zinkee --json schemas fields update contacts contact-name --description ""',
419
+ raw: `{
420
+ "description": ""
421
+ }`,
422
+ },
353
423
  {
354
424
  description: "Rename slug and label.",
355
425
  command:
@@ -1175,11 +1245,12 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
1175
1245
  "displays create": buildCommandExamples([
1176
1246
  {
1177
1247
  description:
1178
- "Create a freeform display with an initial widget layout. Freeform layouts travel through --freeform-layouts.",
1248
+ "Create a described freeform display with an initial widget layout. Freeform layouts travel through --freeform-layouts.",
1179
1249
  command:
1180
- `zinkee --json displays create --name "Control Proyecto Freeform" --template freeform --freeform-layouts '{"lg":[{"i":"widget-1","x":0,"y":0,"w":6,"h":5}]}'`,
1250
+ `zinkee --json displays create --name "Control Proyecto Freeform" --description "Project delivery overview" --template freeform --freeform-layouts '{"lg":[{"i":"widget-1","x":0,"y":0,"w":6,"h":5}]}'`,
1181
1251
  raw: `{
1182
1252
  "name": "Control Proyecto Freeform",
1253
+ "description": "Project delivery overview",
1183
1254
  "layout": {
1184
1255
  "template": "freeform",
1185
1256
  "freeformLayouts": {
@@ -3081,6 +3152,22 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
3081
3152
  },
3082
3153
  ]),
3083
3154
  "displays update": buildCommandExamples([
3155
+ {
3156
+ description: "Update only the display description.",
3157
+ command:
3158
+ 'zinkee --json displays update 0f6f062f-1478-4df2-96a1-c495d037d260 --description "Sales pipeline overview"',
3159
+ raw: `{
3160
+ "description": "Sales pipeline overview"
3161
+ }`,
3162
+ },
3163
+ {
3164
+ description: "Clear the display description.",
3165
+ command:
3166
+ 'zinkee --json displays update 0f6f062f-1478-4df2-96a1-c495d037d260 --description ""',
3167
+ raw: `{
3168
+ "description": ""
3169
+ }`,
3170
+ },
3084
3171
  {
3085
3172
  description: "Rename a display (patch of top-level metadata only).",
3086
3173
  command:
@@ -4463,8 +4550,26 @@ function buildSchemaFieldCreateExamples(type?: string): readonly CommandExample[
4463
4550
  definition.examplePayloads.map((example) => ({ definition, example })),
4464
4551
  );
4465
4552
 
4466
- return buildCommandExamples(
4467
- flat.map(({ example }, index) => {
4553
+ const flagExamples: CommandExample[] =
4554
+ selectedDefinition === undefined || selectedDefinition.key === "text"
4555
+ ? [
4556
+ {
4557
+ description: "Text field with a description.",
4558
+ command:
4559
+ 'zinkee --json schemas fields create contacts --type text --slug customer-name --label "Customer Name" --description "Primary name shown for the customer"',
4560
+ raw: `{
4561
+ "type": "TextField",
4562
+ "slug": "customer-name",
4563
+ "label": "Customer Name",
4564
+ "description": "Primary name shown for the customer"
4565
+ }`,
4566
+ },
4567
+ ]
4568
+ : [];
4569
+
4570
+ return buildCommandExamples([
4571
+ ...flagExamples,
4572
+ ...flat.map(({ example }, index) => {
4468
4573
  const slug =
4469
4574
  typeof example.payload.slug === "string" ? example.payload.slug : "field";
4470
4575
  const command = `zinkee --json schemas fields create contacts --raw '${JSON.stringify(example.payload)}'`;
@@ -4486,7 +4591,7 @@ function buildSchemaFieldCreateExamples(type?: string): readonly CommandExample[
4486
4591
  command,
4487
4592
  };
4488
4593
  }),
4489
- );
4594
+ ]);
4490
4595
  }
4491
4596
 
4492
4597
  export function getCommandExamples(