zinkee 0.1.19 → 0.1.21

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.19",
3
+ "version": "0.1.21",
4
4
  "description": "CLI for Zinkee API v2",
5
5
  "type": "module",
6
6
  "bin": {
@@ -252,6 +252,229 @@ describe("displays commands", () => {
252
252
  });
253
253
  });
254
254
 
255
+ it("translates --filter into the structured display filter contract", async () => {
256
+ const configPath = createApiConfig();
257
+ const output = createStdoutBuffer();
258
+ const fetchMock = createFetchMock();
259
+ fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-f1" }));
260
+ const program = buildProgram({ configPath, stdout: output.stdout });
261
+
262
+ await program.parseAsync([
263
+ "node",
264
+ "zinkee",
265
+ "--json",
266
+ "displays",
267
+ "widgets",
268
+ "create",
269
+ "display-1",
270
+ "--type",
271
+ "table",
272
+ "--origin-resource",
273
+ "schema-1",
274
+ "--filter",
275
+ "edad:gt:30",
276
+ ]);
277
+
278
+ const init = fetchMock.mock.calls[0]?.[1];
279
+ expect(JSON.parse(String(init?.body))).toMatchObject({
280
+ type: "TABLE_OR_SUBTABLE",
281
+ query: {
282
+ filters: [
283
+ {
284
+ origin: { fieldId: "edad" },
285
+ comparisonOperator: "GREATER",
286
+ values: ["30"],
287
+ },
288
+ ],
289
+ },
290
+ });
291
+ });
292
+
293
+ it("maps the `in` comparator to ANY_OF with a list of values", async () => {
294
+ const configPath = createApiConfig();
295
+ const fetchMock = createFetchMock();
296
+ fetchMock.mockResolvedValueOnce(jsonResponse({ widgetId: "widget-f2" }));
297
+ const program = buildProgram({ configPath });
298
+
299
+ await program.parseAsync([
300
+ "node",
301
+ "zinkee",
302
+ "--json",
303
+ "displays",
304
+ "widgets",
305
+ "create",
306
+ "display-1",
307
+ "--type",
308
+ "table",
309
+ "--filter",
310
+ "status:in:1,2,3",
311
+ ]);
312
+
313
+ const init = fetchMock.mock.calls[0]?.[1];
314
+ expect(JSON.parse(String(init?.body)).query.filters).toEqual([
315
+ {
316
+ origin: { fieldId: "status" },
317
+ comparisonOperator: "ANY_OF",
318
+ values: ["1", "2", "3"],
319
+ },
320
+ ]);
321
+ });
322
+
323
+ it("maps the `is_not_empty` comparator with empty values array", async () => {
324
+ const configPath = createApiConfig();
325
+ const fetchMock = createFetchMock();
326
+ fetchMock.mockResolvedValueOnce(jsonResponse({ widgetId: "widget-f3" }));
327
+ const program = buildProgram({ configPath });
328
+
329
+ await program.parseAsync([
330
+ "node",
331
+ "zinkee",
332
+ "--json",
333
+ "displays",
334
+ "widgets",
335
+ "create",
336
+ "display-1",
337
+ "--type",
338
+ "table",
339
+ "--filter",
340
+ "email:is_not_empty",
341
+ ]);
342
+
343
+ const init = fetchMock.mock.calls[0]?.[1];
344
+ expect(JSON.parse(String(init?.body)).query.filters).toEqual([
345
+ {
346
+ origin: { fieldId: "email" },
347
+ comparisonOperator: "IS_NOT_EMPTY",
348
+ values: [],
349
+ },
350
+ ]);
351
+ });
352
+
353
+ it("accumulates multiple --filter flags in order", async () => {
354
+ const configPath = createApiConfig();
355
+ const fetchMock = createFetchMock();
356
+ fetchMock.mockResolvedValueOnce(jsonResponse({ widgetId: "widget-f4" }));
357
+ const program = buildProgram({ configPath });
358
+
359
+ await program.parseAsync([
360
+ "node",
361
+ "zinkee",
362
+ "--json",
363
+ "displays",
364
+ "widgets",
365
+ "create",
366
+ "display-1",
367
+ "--type",
368
+ "table",
369
+ "--filter",
370
+ "status:eq:2",
371
+ "--filter",
372
+ "amount:gte:500",
373
+ ]);
374
+
375
+ const init = fetchMock.mock.calls[0]?.[1];
376
+ expect(JSON.parse(String(init?.body)).query.filters).toEqual([
377
+ { origin: { fieldId: "status" }, comparisonOperator: "EQUAL", values: ["2"] },
378
+ { origin: { fieldId: "amount" }, comparisonOperator: "GREATER_OR_EQUAL", values: ["500"] },
379
+ ]);
380
+ });
381
+
382
+ it("rejects --filter with an unsupported comparator before issuing a request", async () => {
383
+ const configPath = createApiConfig();
384
+ const fetchMock = createFetchMock();
385
+ const program = buildProgram({ configPath });
386
+ const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
387
+
388
+ await expect(
389
+ program.parseAsync([
390
+ "node",
391
+ "zinkee",
392
+ "--json",
393
+ "displays",
394
+ "widgets",
395
+ "create",
396
+ "display-1",
397
+ "--type",
398
+ "table",
399
+ "--filter",
400
+ "edad:invalido:30",
401
+ ]),
402
+ ).rejects.toThrow();
403
+
404
+ expect(fetchMock).not.toHaveBeenCalled();
405
+ expect(stderrSpy.mock.calls.map(([chunk]) => String(chunk)).join("")).toMatch(/Unsupported comparator/);
406
+ stderrSpy.mockRestore();
407
+ });
408
+
409
+ it("preserves --raw query.filters when only --field is passed (no --filter)", async () => {
410
+ const configPath = createApiConfig();
411
+ const fetchMock = createFetchMock();
412
+ fetchMock.mockResolvedValueOnce(jsonResponse({ widgetId: "widget-f6" }));
413
+ const program = buildProgram({ configPath });
414
+
415
+ await program.parseAsync([
416
+ "node",
417
+ "zinkee",
418
+ "--json",
419
+ "displays",
420
+ "widgets",
421
+ "create",
422
+ "display-1",
423
+ "--type",
424
+ "table",
425
+ "--field",
426
+ "foo",
427
+ "--raw",
428
+ '{"query":{"filters":[{"origin":{"fieldId":"status"},"comparisonOperator":"EQUAL","values":["2"]}]}}',
429
+ ]);
430
+
431
+ const init = fetchMock.mock.calls[0]?.[1];
432
+ expect(JSON.parse(String(init?.body))).toMatchObject({
433
+ view: { fields: ["foo"] },
434
+ query: {
435
+ filters: [
436
+ { origin: { fieldId: "status" }, comparisonOperator: "EQUAL", values: ["2"] },
437
+ ],
438
+ },
439
+ });
440
+ });
441
+
442
+ it("translates --filter for variable mutations into the structured filter contract", async () => {
443
+ const configPath = createApiConfig();
444
+ const fetchMock = createFetchMock();
445
+ fetchMock.mockResolvedValueOnce(jsonResponse({ variableId: "var-1" }));
446
+ const program = buildProgram({ configPath });
447
+
448
+ await program.parseAsync([
449
+ "node",
450
+ "zinkee",
451
+ "--json",
452
+ "displays",
453
+ "variables",
454
+ "create",
455
+ "display-1",
456
+ "--type",
457
+ "reference",
458
+ "--name",
459
+ "Activos",
460
+ "--filter",
461
+ "estado:eq:activo",
462
+ ]);
463
+
464
+ const init = fetchMock.mock.calls[0]?.[1];
465
+ expect(JSON.parse(String(init?.body))).toMatchObject({
466
+ type: "reference",
467
+ name: "Activos",
468
+ filters: [
469
+ {
470
+ origin: { fieldId: "estado" },
471
+ comparisonOperator: "EQUAL",
472
+ values: ["activo"],
473
+ },
474
+ ],
475
+ });
476
+ });
477
+
255
478
  it("blocks destructive display commands in read-only mode", async () => {
256
479
  const configPath = createApiConfig();
257
480
  const fetchMock = createFetchMock();
@@ -6,6 +6,11 @@ import { Command, InvalidArgumentError } from "commander";
6
6
  import { DisplaysApi, type DisplayResource, type DisplaySummary } from "../api/displays.js";
7
7
  import { ApiClient } from "../client.js";
8
8
  import { defaultConfigPath, readRuntimeConfig } from "../config.js";
9
+ import {
10
+ parseExpression,
11
+ type ExpressionComparator,
12
+ type ParsedExpression,
13
+ } from "../parsers/expressions.js";
9
14
  import { normalizeUuidOrSlug, type NormalizedUuidOrSlug } from "../parsers/selectors.js";
10
15
  import { resolveRuntimeContext } from "../runtime-context.js";
11
16
  import type { RuntimeOverrides } from "../types.js";
@@ -54,7 +59,7 @@ interface VariableMutationOptions {
54
59
  defaultValue?: string[];
55
60
  sourceSchema?: string;
56
61
  sourceField?: string;
57
- filter?: string[];
62
+ filter?: ParsedExpression[];
58
63
  dateMode?: string;
59
64
  dateDynamicOption?: string;
60
65
  dateSpecific?: string;
@@ -78,7 +83,7 @@ interface WidgetMutationOptions {
78
83
  layoutFile?: string;
79
84
  field?: string[];
80
85
  sort?: string[];
81
- filter?: string[];
86
+ filter?: ParsedExpression[];
82
87
  widgetReadOnly?: boolean;
83
88
  hideAddButton?: boolean;
84
89
  hideDeleteButton?: boolean;
@@ -189,6 +194,46 @@ function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
189
194
  }
190
195
  }
191
196
 
197
+ const COMPARATOR_TO_DISPLAY_OPERATOR: Record<ExpressionComparator, string> = {
198
+ eq: "EQUAL",
199
+ neq: "NOT_EQUAL",
200
+ gt: "GREATER",
201
+ gte: "GREATER_OR_EQUAL",
202
+ lt: "LESS",
203
+ lte: "LESS_OR_EQUAL",
204
+ contains: "CONTAINS",
205
+ in: "ANY_OF",
206
+ is_empty: "IS_EMPTY",
207
+ is_not_empty: "IS_NOT_EMPTY",
208
+ };
209
+
210
+ function toDisplayFilter(parsed: ParsedExpression): Record<string, unknown> {
211
+ const values =
212
+ parsed.value === null
213
+ ? []
214
+ : Array.isArray(parsed.value)
215
+ ? [...parsed.value]
216
+ : [parsed.value];
217
+ return {
218
+ origin: { fieldId: parsed.field },
219
+ comparisonOperator: COMPARATOR_TO_DISPLAY_OPERATOR[parsed.comparator],
220
+ values,
221
+ };
222
+ }
223
+
224
+ function parseFilterExpression(input: string): ParsedExpression {
225
+ try {
226
+ return parseExpression(input);
227
+ } catch (error) {
228
+ const message = error instanceof Error ? error.message : "Invalid filter expression.";
229
+ throw new InvalidArgumentError(message);
230
+ }
231
+ }
232
+
233
+ function collectFilters(value: string, previous: ParsedExpression[] = []): ParsedExpression[] {
234
+ return [...previous, parseFilterExpression(value)];
235
+ }
236
+
192
237
  function parseBooleanFlag(input: string): boolean {
193
238
  const normalized = input.trim().toLowerCase();
194
239
  if (["true", "1", "yes", "y", "on"].includes(normalized)) return true;
@@ -278,7 +323,7 @@ function buildVariableBody(options: VariableMutationOptions): Record<string, unk
278
323
  };
279
324
  }
280
325
  if (options.filter && options.filter.length > 0) {
281
- body.filters = options.filter.map((entry) => ({ raw: entry }));
326
+ body.filters = options.filter.map(toDisplayFilter);
282
327
  }
283
328
  if (
284
329
  options.dateMode ||
@@ -348,15 +393,19 @@ function buildWidgetBody(options: WidgetMutationOptions): Record<string, unknown
348
393
  };
349
394
  }
350
395
 
351
- if (options.field?.length || options.sort?.length || options.filter?.length) {
396
+ if (options.field && options.field.length > 0) {
352
397
  body.view = {
353
398
  ...(typeof body.view === "object" && body.view ? body.view as Record<string, unknown> : {}),
354
399
  fields: options.field,
355
400
  };
401
+ }
402
+ const queryMerge: Record<string, unknown> = {};
403
+ if (options.sort && options.sort.length > 0) queryMerge.sorts = options.sort;
404
+ if (options.filter && options.filter.length > 0) queryMerge.filters = options.filter.map(toDisplayFilter);
405
+ if (Object.keys(queryMerge).length > 0) {
356
406
  body.query = {
357
407
  ...(typeof body.query === "object" && body.query ? body.query as Record<string, unknown> : {}),
358
- sorts: options.sort,
359
- filters: options.filter?.map((entry) => ({ raw: entry })),
408
+ ...queryMerge,
360
409
  };
361
410
  }
362
411
 
@@ -624,7 +673,7 @@ export function registerDisplaysCommands(
624
673
  variables.command("create").description("Create variable").argument("<display>", "display uuid or slug", parseUuidOrSlugSelector)
625
674
  .option("--type <type>").option("--name <name>").option("--allow-multiple").option("--static")
626
675
  .option("--default <value>").option("--default-value <value>", "repeatable default value", (v, p: string[] = []) => [...p, v], [])
627
- .option("--source-schema <id>").option("--source-field <id>").option("--filter <expr>", "repeatable filter", (v, p: string[] = []) => [...p, v], [])
676
+ .option("--source-schema <id>").option("--source-field <id>").option("--filter <expr>", "repeatable filter", collectFilters, [])
628
677
  .option("--date-mode <mode>").option("--date-dynamic-option <option>").option("--date-specific <iso>").option("--date-range-start <iso>").option("--date-range-end <iso>")
629
678
  .option("--raw <json>").option("--raw-file <path>")
630
679
  .action(async function action(this: Command, display: NormalizedUuidOrSlug, commandOptions: VariableMutationOptions) {
@@ -636,7 +685,7 @@ export function registerDisplaysCommands(
636
685
  variables.command("update").description("Update variable").argument("<display>", "display uuid or slug", parseUuidOrSlugSelector).argument("<variable>", "variable id")
637
686
  .option("--type <type>").option("--name <name>").option("--allow-multiple").option("--static")
638
687
  .option("--default <value>").option("--default-value <value>", "repeatable default value", (v, p: string[] = []) => [...p, v], [])
639
- .option("--source-schema <id>").option("--source-field <id>").option("--filter <expr>", "repeatable filter", (v, p: string[] = []) => [...p, v], [])
688
+ .option("--source-schema <id>").option("--source-field <id>").option("--filter <expr>", "repeatable filter", collectFilters, [])
640
689
  .option("--date-mode <mode>").option("--date-dynamic-option <option>").option("--date-specific <iso>").option("--date-range-start <iso>").option("--date-range-end <iso>")
641
690
  .option("--raw <json>").option("--raw-file <path>")
642
691
  .action(async function action(this: Command, display: NormalizedUuidOrSlug, variableId: string, commandOptions: VariableMutationOptions) {
@@ -668,10 +717,10 @@ export function registerDisplaysCommands(
668
717
  .option("--tab <tabId>").option("--slot <slotId>").option("--layout <json>").option("--layout-file <path>")
669
718
  .option("--field <field>", "repeatable field", (v, p: string[] = []) => [...p, v], [])
670
719
  .option("--sort <sort>", "repeatable sort", (v, p: string[] = []) => [...p, v], [])
671
- .option("--filter <expr>", "repeatable filter", (v, p: string[] = []) => [...p, v], [])
720
+ .option("--filter <expr>", "repeatable filter", collectFilters, [])
672
721
  .option("--widget-read-only").option("--hide-add-button").option("--hide-delete-button").option("--hide-sort-button").option("--hide-filter-button").option("--hide-see-button")
673
722
  .option("--calc <type>").option("--calc-field <uuid>").option("--kpi-config <json>")
674
- .option("--chart-type <type>").option("--x-axis-field <uuid>").option("--x-axis-date-format <granularity>").option("--x-axis-time-series")
723
+ .option("--chart-type <type>", "chart type (lines, bars, stackedBars, combined)").option("--x-axis-field <uuid>").option("--x-axis-date-format <granularity>", "time granularity (DATE, MONTH_YEAR, QUARTER_YEAR, YEAR)").option("--x-axis-time-series")
675
724
  .option("--y-axis-serie <json>", "repeatable y axis serie", (v, p: string[] = []) => [...p, v], [])
676
725
  .option("--legend <json>").option("--raw <json>").option("--raw-file <path>")
677
726
  .action(async function action(this: Command, display: NormalizedUuidOrSlug, commandOptions: WidgetMutationOptions) {
@@ -686,10 +735,10 @@ export function registerDisplaysCommands(
686
735
  .option("--tab <tabId>").option("--slot <slotId>").option("--layout <json>").option("--layout-file <path>")
687
736
  .option("--field <field>", "repeatable field", (v, p: string[] = []) => [...p, v], [])
688
737
  .option("--sort <sort>", "repeatable sort", (v, p: string[] = []) => [...p, v], [])
689
- .option("--filter <expr>", "repeatable filter", (v, p: string[] = []) => [...p, v], [])
738
+ .option("--filter <expr>", "repeatable filter", collectFilters, [])
690
739
  .option("--widget-read-only").option("--hide-add-button").option("--hide-delete-button").option("--hide-sort-button").option("--hide-filter-button").option("--hide-see-button")
691
740
  .option("--calc <type>").option("--calc-field <uuid>").option("--kpi-config <json>")
692
- .option("--chart-type <type>").option("--x-axis-field <uuid>").option("--x-axis-date-format <granularity>").option("--x-axis-time-series")
741
+ .option("--chart-type <type>", "chart type (lines, bars, stackedBars, combined)").option("--x-axis-field <uuid>").option("--x-axis-date-format <granularity>", "time granularity (DATE, MONTH_YEAR, QUARTER_YEAR, YEAR)").option("--x-axis-time-series")
693
742
  .option("--y-axis-serie <json>", "repeatable y axis serie", (v, p: string[] = []) => [...p, v], [])
694
743
  .option("--legend <json>").option("--raw <json>").option("--raw-file <path>")
695
744
  .action(async function action(this: Command, display: NormalizedUuidOrSlug, widgetId: string, commandOptions: WidgetMutationOptions) {
@@ -1385,9 +1385,40 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
1385
1385
  }`,
1386
1386
  },
1387
1387
  {
1388
- description: "Create a bars CHART widget with a time-serie x-axis and two y-axis series.",
1388
+ description: "Create a CHART widget with a daily time-serie x-axis.",
1389
1389
  command:
1390
- `zinkee --json displays widgets create 0f6f062f-1478-4df2-96a1-c495d037d260 --type CHART --name "Revenue by month" --origin-resource 2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e --layout '{"x":0,"y":0,"w":8,"h":6}' --chart-type bars --x-axis-field due-date --x-axis-date-format MONTH --x-axis-time-series --y-axis-serie '{"typeCalc":"SUM","fieldCalc":"amount","color":"#4B9CD3","axisType":"PRIMARY"}' --y-axis-serie '{"typeCalc":"COUNT","fieldCalc":"id","color":"#E57373","axisType":"SECONDARY"}' --legend '{"position":"bottom"}'`,
1390
+ `zinkee --json displays widgets create 0f6f062f-1478-4df2-96a1-c495d037d260 --type CHART --name "Revenue by day" --origin-resource 2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e --layout '{"x":0,"y":0,"w":8,"h":6}' --chart-type lines --x-axis-field due-date --x-axis-date-format DATE --x-axis-time-series --y-axis-serie '{"typeCalc":"SUM","fieldCalc":"amount","color":"#4B9CD3","axisType":"PRIMARY"}' --legend '{"position":"bottom"}'`,
1391
+ raw: `{
1392
+ "type": "CHART",
1393
+ "source": {
1394
+ "originResourceId": "2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e",
1395
+ "isOriginSubSchema": false
1396
+ },
1397
+ "placement": {
1398
+ "layout": { "x": 0, "y": 0, "w": 8, "h": 6 }
1399
+ },
1400
+ "presentation": { "name": "Revenue by day" },
1401
+ "chart": {
1402
+ "chartType": "lines",
1403
+ "xAxisFieldId": "due-date",
1404
+ "xAxisDateFormat": "DATE",
1405
+ "xAxisAsTimeSerie": true,
1406
+ "yAxisSeries": [
1407
+ {
1408
+ "typeCalc": "SUM",
1409
+ "fieldCalc": "amount",
1410
+ "color": "#4B9CD3",
1411
+ "axisType": "PRIMARY"
1412
+ }
1413
+ ],
1414
+ "legend": { "position": "bottom" }
1415
+ }
1416
+ }`,
1417
+ },
1418
+ {
1419
+ description: "Create a bars CHART widget with a monthly time-serie x-axis and two y-axis series.",
1420
+ command:
1421
+ `zinkee --json displays widgets create 0f6f062f-1478-4df2-96a1-c495d037d260 --type CHART --name "Revenue by month" --origin-resource 2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e --layout '{"x":0,"y":0,"w":8,"h":6}' --chart-type bars --x-axis-field due-date --x-axis-date-format MONTH_YEAR --x-axis-time-series --y-axis-serie '{"typeCalc":"SUM","fieldCalc":"amount","color":"#4B9CD3","axisType":"PRIMARY"}' --y-axis-serie '{"typeCalc":"COUNT","fieldCalc":"id","color":"#E57373","axisType":"SECONDARY"}' --legend '{"position":"bottom"}'`,
1391
1422
  raw: `{
1392
1423
  "type": "CHART",
1393
1424
  "source": {
@@ -1401,7 +1432,7 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
1401
1432
  "chart": {
1402
1433
  "chartType": "bars",
1403
1434
  "xAxisFieldId": "due-date",
1404
- "xAxisDateFormat": "MONTH",
1435
+ "xAxisDateFormat": "MONTH_YEAR",
1405
1436
  "xAxisAsTimeSerie": true,
1406
1437
  "yAxisSeries": [
1407
1438
  {
@@ -1419,6 +1450,68 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
1419
1450
  ],
1420
1451
  "legend": { "position": "bottom" }
1421
1452
  }
1453
+ }`,
1454
+ },
1455
+ {
1456
+ description: "Create a bars CHART widget with a quarterly time-serie x-axis.",
1457
+ command:
1458
+ `zinkee --json displays widgets create 0f6f062f-1478-4df2-96a1-c495d037d260 --type CHART --name "Revenue by quarter" --origin-resource 2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e --layout '{"x":0,"y":0,"w":8,"h":6}' --chart-type bars --x-axis-field due-date --x-axis-date-format QUARTER_YEAR --x-axis-time-series --y-axis-serie '{"typeCalc":"SUM","fieldCalc":"amount","color":"#4B9CD3","axisType":"PRIMARY"}' --legend '{"position":"bottom"}'`,
1459
+ raw: `{
1460
+ "type": "CHART",
1461
+ "source": {
1462
+ "originResourceId": "2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e",
1463
+ "isOriginSubSchema": false
1464
+ },
1465
+ "placement": {
1466
+ "layout": { "x": 0, "y": 0, "w": 8, "h": 6 }
1467
+ },
1468
+ "presentation": { "name": "Revenue by quarter" },
1469
+ "chart": {
1470
+ "chartType": "bars",
1471
+ "xAxisFieldId": "due-date",
1472
+ "xAxisDateFormat": "QUARTER_YEAR",
1473
+ "xAxisAsTimeSerie": true,
1474
+ "yAxisSeries": [
1475
+ {
1476
+ "typeCalc": "SUM",
1477
+ "fieldCalc": "amount",
1478
+ "color": "#4B9CD3",
1479
+ "axisType": "PRIMARY"
1480
+ }
1481
+ ],
1482
+ "legend": { "position": "bottom" }
1483
+ }
1484
+ }`,
1485
+ },
1486
+ {
1487
+ description: "Create a CHART widget with a yearly time-serie x-axis.",
1488
+ command:
1489
+ `zinkee --json displays widgets create 0f6f062f-1478-4df2-96a1-c495d037d260 --type CHART --name "Revenue by year" --origin-resource 2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e --layout '{"x":0,"y":0,"w":8,"h":6}' --chart-type lines --x-axis-field due-date --x-axis-date-format YEAR --x-axis-time-series --y-axis-serie '{"typeCalc":"SUM","fieldCalc":"amount","color":"#4B9CD3","axisType":"PRIMARY"}' --legend '{"position":"bottom"}'`,
1490
+ raw: `{
1491
+ "type": "CHART",
1492
+ "source": {
1493
+ "originResourceId": "2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e",
1494
+ "isOriginSubSchema": false
1495
+ },
1496
+ "placement": {
1497
+ "layout": { "x": 0, "y": 0, "w": 8, "h": 6 }
1498
+ },
1499
+ "presentation": { "name": "Revenue by year" },
1500
+ "chart": {
1501
+ "chartType": "lines",
1502
+ "xAxisFieldId": "due-date",
1503
+ "xAxisDateFormat": "YEAR",
1504
+ "xAxisAsTimeSerie": true,
1505
+ "yAxisSeries": [
1506
+ {
1507
+ "typeCalc": "SUM",
1508
+ "fieldCalc": "amount",
1509
+ "color": "#4B9CD3",
1510
+ "axisType": "PRIMARY"
1511
+ }
1512
+ ],
1513
+ "legend": { "position": "bottom" }
1514
+ }
1422
1515
  }`,
1423
1516
  },
1424
1517
  {
@@ -1518,30 +1611,41 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
1518
1611
  },
1519
1612
  {
1520
1613
  description:
1521
- "Add query filters that match the full comparisonOperator contract (requires --raw because --filter only accepts free-form expressions).",
1614
+ "Add query filters with --filter. The CLI parses the expression and emits the structured comparisonOperator payload the API expects.",
1522
1615
  command:
1523
- `zinkee --json displays widgets update 0f6f062f-1478-4df2-96a1-c495d037d260 11111111-1111-4111-a111-111111111111 --raw '{"query":{"filters":[{"origin":{"schemaId":"contacts","fieldId":"status"},"comparisonOperator":"EQUAL","values":["2"]},{"origin":{"schemaId":"contacts","fieldId":"amount"},"comparisonOperator":"GREATER_OR_EQUAL","values":["500"]}]}}'`,
1616
+ `zinkee --json displays widgets update 0f6f062f-1478-4df2-96a1-c495d037d260 11111111-1111-4111-a111-111111111111 --filter "status:eq:2" --filter "amount:gte:500"`,
1524
1617
  raw: `{
1525
1618
  "query": {
1526
1619
  "filters": [
1527
1620
  {
1528
- "origin": {
1529
- "schemaId": "contacts",
1530
- "fieldId": "status"
1531
- },
1621
+ "origin": { "fieldId": "status" },
1532
1622
  "comparisonOperator": "EQUAL",
1533
1623
  "values": ["2"]
1534
1624
  },
1535
1625
  {
1536
- "origin": {
1537
- "schemaId": "contacts",
1538
- "fieldId": "amount"
1539
- },
1626
+ "origin": { "fieldId": "amount" },
1540
1627
  "comparisonOperator": "GREATER_OR_EQUAL",
1541
1628
  "values": ["500"]
1542
1629
  }
1543
1630
  ]
1544
1631
  }
1632
+ }`,
1633
+ },
1634
+ {
1635
+ description:
1636
+ "Fall back to --raw for comparators the parser does not cover yet (e.g. NOT_CONTAINS, RANGE).",
1637
+ command:
1638
+ `zinkee --json displays widgets update 0f6f062f-1478-4df2-96a1-c495d037d260 11111111-1111-4111-a111-111111111111 --raw '{"query":{"filters":[{"origin":{"fieldId":"due-date"},"comparisonOperator":"RANGE","values":["2025-01-01","2025-12-31"]}]}}'`,
1639
+ raw: `{
1640
+ "query": {
1641
+ "filters": [
1642
+ {
1643
+ "origin": { "fieldId": "due-date" },
1644
+ "comparisonOperator": "RANGE",
1645
+ "values": ["2025-01-01", "2025-12-31"]
1646
+ }
1647
+ ]
1648
+ }
1545
1649
  }`,
1546
1650
  },
1547
1651
  {
@@ -1569,14 +1673,14 @@ const specificExamples: Record<string, readonly CommandExample[]> = {
1569
1673
  }`,
1570
1674
  },
1571
1675
  {
1572
- description: "Replace the chart series with weekly lines.",
1676
+ description: "Replace the chart series with quarterly lines.",
1573
1677
  command:
1574
- `zinkee --json displays widgets update 0f6f062f-1478-4df2-96a1-c495d037d260 22222222-2222-4222-a222-222222222222 --chart-type lines --x-axis-field due-date --x-axis-date-format WEEK --x-axis-time-series --y-axis-serie '{"typeCalc":"SUM","fieldCalc":"amount","color":"#2E7D32","axisType":"PRIMARY"}'`,
1678
+ `zinkee --json displays widgets update 0f6f062f-1478-4df2-96a1-c495d037d260 22222222-2222-4222-a222-222222222222 --chart-type lines --x-axis-field due-date --x-axis-date-format QUARTER_YEAR --x-axis-time-series --y-axis-serie '{"typeCalc":"SUM","fieldCalc":"amount","color":"#2E7D32","axisType":"PRIMARY"}'`,
1575
1679
  raw: `{
1576
1680
  "chart": {
1577
1681
  "chartType": "lines",
1578
1682
  "xAxisFieldId": "due-date",
1579
- "xAxisDateFormat": "WEEK",
1683
+ "xAxisDateFormat": "QUARTER_YEAR",
1580
1684
  "xAxisAsTimeSerie": true,
1581
1685
  "yAxisSeries": [
1582
1686
  {