zinkee 0.1.44 → 0.1.45
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 +4 -0
- package/dist/chunk-MHEVRMDX.js +1230 -0
- package/dist/chunk-MHEVRMDX.js.map +1 -0
- package/{src/utils/examples.ts → dist/examples-E6FFEVBE.js} +856 -1141
- package/dist/examples-E6FFEVBE.js.map +1 -0
- package/dist/index.js +1145 -8456
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
- package/.github/workflows/npm-publish.yml +0 -77
- package/.github/workflows/pr-checks.yml +0 -36
- package/AGENTS.md +0 -110
- package/docs/cli-contract.md +0 -41
- package/docs/npm-release.md +0 -64
- package/docs/superpowers/plans/2026-03-24-zinkee-cli-implementation.md +0 -837
- package/docs/superpowers/plans/2026-03-25-cli-backend-error-contract.md +0 -503
- package/docs/superpowers/plans/2026-03-26-display-freeform-create.md +0 -389
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-backend-blockers.md +0 -172
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-design.md +0 -1576
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-checklist.md +0 -215
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-design.md +0 -492
- package/docs/superpowers/specs/2026-03-24-zinkee-cli-e2e-status.md +0 -307
- package/docs/superpowers/specs/2026-07-30-cli-pr-checks-design.md +0 -37
- package/src/api/automations.ts +0 -404
- package/src/api/comments.ts +0 -51
- package/src/api/displays.ts +0 -337
- package/src/api/document-templates.ts +0 -110
- package/src/api/files.ts +0 -50
- package/src/api/formulas.test.ts +0 -65
- package/src/api/formulas.ts +0 -67
- package/src/api/logs.ts +0 -34
- package/src/api/navigation.ts +0 -70
- package/src/api/records.ts +0 -184
- package/src/api/schemas.ts +0 -148
- package/src/api/teamspace.ts +0 -110
- package/src/cli-examples.ts +0 -130
- package/src/cli-runner.ts +0 -95
- package/src/client.test.ts +0 -189
- package/src/client.ts +0 -269
- package/src/command-registry.ts +0 -882
- package/src/commands/automations.test.ts +0 -1030
- package/src/commands/automations.ts +0 -2102
- package/src/commands/comments.test.ts +0 -214
- package/src/commands/comments.ts +0 -303
- package/src/commands/config.test.ts +0 -81
- package/src/commands/config.ts +0 -150
- package/src/commands/displays.test.ts +0 -1105
- package/src/commands/displays.ts +0 -1442
- package/src/commands/document-templates.test.ts +0 -569
- package/src/commands/document-templates.ts +0 -563
- package/src/commands/files.test.ts +0 -284
- package/src/commands/files.ts +0 -280
- package/src/commands/formulas.test.ts +0 -194
- package/src/commands/formulas.ts +0 -243
- package/src/commands/logs.test.ts +0 -123
- package/src/commands/logs.ts +0 -159
- package/src/commands/navigation.test.ts +0 -211
- package/src/commands/navigation.ts +0 -348
- package/src/commands/profiles.test.ts +0 -191
- package/src/commands/profiles.ts +0 -303
- package/src/commands/records.test.ts +0 -860
- package/src/commands/records.ts +0 -883
- package/src/commands/schemas.test.ts +0 -1252
- package/src/commands/schemas.ts +0 -890
- package/src/commands/teamspace.test.ts +0 -229
- package/src/commands/teamspace.ts +0 -546
- package/src/completion/engine.test.ts +0 -138
- package/src/completion/engine.ts +0 -168
- package/src/completion/install.test.ts +0 -179
- package/src/completion/install.ts +0 -260
- package/src/completion/runtime.ts +0 -150
- package/src/completion/scripts.ts +0 -91
- package/src/config.test.ts +0 -362
- package/src/config.ts +0 -294
- package/src/index.test.ts +0 -217
- package/src/index.ts +0 -8
- package/src/parsers/expressions.test.ts +0 -95
- package/src/parsers/expressions.ts +0 -128
- package/src/parsers/kv.test.ts +0 -35
- package/src/parsers/kv.ts +0 -49
- package/src/parsers/selectors.test.ts +0 -23
- package/src/parsers/selectors.ts +0 -29
- package/src/program.ts +0 -64
- package/src/runtime-context.ts +0 -103
- package/src/types.ts +0 -76
- package/src/utils/argv-rewrite.test.ts +0 -149
- package/src/utils/argv-rewrite.ts +0 -269
- package/src/utils/errors.test.ts +0 -85
- package/src/utils/errors.ts +0 -191
- package/src/utils/examples.test.ts +0 -374
- package/src/utils/output.test.ts +0 -65
- package/src/utils/output.ts +0 -154
- package/src/utils/schema-fields.ts +0 -1016
- package/tsconfig.json +0 -20
- package/tsup.config.ts +0 -13
|
@@ -1,1105 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
|
|
7
|
-
import { buildProgram } from "../index.js";
|
|
8
|
-
|
|
9
|
-
function createTempConfig(contents: string): string {
|
|
10
|
-
const dir = mkdtempSync(join(tmpdir(), "zinkee-displays-"));
|
|
11
|
-
const filePath = join(dir, "config.toml");
|
|
12
|
-
writeFileSync(filePath, contents, "utf8");
|
|
13
|
-
return filePath;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function createApiConfig(): string {
|
|
17
|
-
return createTempConfig(`
|
|
18
|
-
default_profile = "prod"
|
|
19
|
-
|
|
20
|
-
[profiles.prod]
|
|
21
|
-
api_key = "displays-token-1234"
|
|
22
|
-
base_url = "https://api.zinkee.test"
|
|
23
|
-
`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function createStdoutBuffer() {
|
|
27
|
-
let output = "";
|
|
28
|
-
return {
|
|
29
|
-
stdout: {
|
|
30
|
-
write(chunk: string) {
|
|
31
|
-
output += chunk;
|
|
32
|
-
return true;
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
read() {
|
|
36
|
-
return output;
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function jsonResponse(body: unknown, status: number = 200): Response {
|
|
42
|
-
return new Response(JSON.stringify(body), {
|
|
43
|
-
status,
|
|
44
|
-
headers: { "content-type": "application/json" },
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function createFetchMock() {
|
|
49
|
-
const fetchMock = vi.fn<typeof fetch>();
|
|
50
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
51
|
-
return fetchMock;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
afterEach(() => {
|
|
55
|
-
vi.unstubAllGlobals();
|
|
56
|
-
vi.restoreAllMocks();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe("displays commands", () => {
|
|
60
|
-
it("registers the displays command group", () => {
|
|
61
|
-
const program = buildProgram();
|
|
62
|
-
expect(program.commands.some((command) => command.name() === "displays")).toBe(true);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("creates displays with the expected payload", async () => {
|
|
66
|
-
const configPath = createApiConfig();
|
|
67
|
-
const output = createStdoutBuffer();
|
|
68
|
-
const fetchMock = createFetchMock();
|
|
69
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1" }));
|
|
70
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
71
|
-
|
|
72
|
-
await program.parseAsync([
|
|
73
|
-
"node",
|
|
74
|
-
"zinkee",
|
|
75
|
-
"--json",
|
|
76
|
-
"displays",
|
|
77
|
-
"create",
|
|
78
|
-
"--name",
|
|
79
|
-
"Pipeline",
|
|
80
|
-
"--template",
|
|
81
|
-
"widget_row_tabs",
|
|
82
|
-
]);
|
|
83
|
-
|
|
84
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
85
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/displays");
|
|
86
|
-
expect(init?.method).toBe("POST");
|
|
87
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
88
|
-
name: "Pipeline",
|
|
89
|
-
layout: { template: "widget_row_tabs" },
|
|
90
|
-
});
|
|
91
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
92
|
-
data: { displayId: "display-1" },
|
|
93
|
-
meta: { command: "displays create" },
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("creates displays with a description", async () => {
|
|
98
|
-
const configPath = createApiConfig();
|
|
99
|
-
const output = createStdoutBuffer();
|
|
100
|
-
const fetchMock = createFetchMock();
|
|
101
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1" }));
|
|
102
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
103
|
-
|
|
104
|
-
await program.parseAsync([
|
|
105
|
-
"node",
|
|
106
|
-
"zinkee",
|
|
107
|
-
"--json",
|
|
108
|
-
"displays",
|
|
109
|
-
"create",
|
|
110
|
-
"--name",
|
|
111
|
-
"Pipeline",
|
|
112
|
-
"--description",
|
|
113
|
-
"Sales pipeline overview",
|
|
114
|
-
"--template",
|
|
115
|
-
"widget_row_tabs",
|
|
116
|
-
]);
|
|
117
|
-
|
|
118
|
-
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
|
|
119
|
-
name: "Pipeline",
|
|
120
|
-
description: "Sales pipeline overview",
|
|
121
|
-
layout: { template: "widget_row_tabs" },
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it.each([
|
|
126
|
-
["sets", "Sales pipeline overview"],
|
|
127
|
-
["clears", ""],
|
|
128
|
-
])("%s a display description without renaming it", async (_action, description) => {
|
|
129
|
-
const configPath = createApiConfig();
|
|
130
|
-
const output = createStdoutBuffer();
|
|
131
|
-
const fetchMock = createFetchMock();
|
|
132
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1" }));
|
|
133
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
134
|
-
|
|
135
|
-
await program.parseAsync([
|
|
136
|
-
"node",
|
|
137
|
-
"zinkee",
|
|
138
|
-
"--json",
|
|
139
|
-
"displays",
|
|
140
|
-
"update",
|
|
141
|
-
"display-1",
|
|
142
|
-
"--description",
|
|
143
|
-
description,
|
|
144
|
-
]);
|
|
145
|
-
|
|
146
|
-
expect(fetchMock.mock.calls[0]?.[1]?.method).toBe("PATCH");
|
|
147
|
-
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({ description });
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it.each(["", " "])("rejects an empty display name", async (name) => {
|
|
151
|
-
const configPath = createApiConfig();
|
|
152
|
-
const output = createStdoutBuffer();
|
|
153
|
-
const fetchMock = createFetchMock();
|
|
154
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
155
|
-
|
|
156
|
-
await expect(
|
|
157
|
-
program.parseAsync([
|
|
158
|
-
"node",
|
|
159
|
-
"zinkee",
|
|
160
|
-
"--json",
|
|
161
|
-
"displays",
|
|
162
|
-
"update",
|
|
163
|
-
"display-1",
|
|
164
|
-
"--name",
|
|
165
|
-
name,
|
|
166
|
-
]),
|
|
167
|
-
).rejects.toThrow("Display name cannot be empty.");
|
|
168
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it("creates freeform displays with nested freeform layouts", async () => {
|
|
172
|
-
const configPath = createApiConfig();
|
|
173
|
-
const output = createStdoutBuffer();
|
|
174
|
-
const fetchMock = createFetchMock();
|
|
175
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-2" }));
|
|
176
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
177
|
-
|
|
178
|
-
await program.parseAsync([
|
|
179
|
-
"node",
|
|
180
|
-
"zinkee",
|
|
181
|
-
"--json",
|
|
182
|
-
"displays",
|
|
183
|
-
"create",
|
|
184
|
-
"--name",
|
|
185
|
-
"Control Proyecto Freeform",
|
|
186
|
-
"--template",
|
|
187
|
-
"freeform",
|
|
188
|
-
"--freeform-layouts",
|
|
189
|
-
'{"lg":[{"i":"widget-1","x":0,"y":0,"w":6,"h":5}]}',
|
|
190
|
-
]);
|
|
191
|
-
|
|
192
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
193
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
194
|
-
name: "Control Proyecto Freeform",
|
|
195
|
-
layout: {
|
|
196
|
-
template: "freeform",
|
|
197
|
-
freeformLayouts: {
|
|
198
|
-
lg: [{ i: "widget-1", x: 0, y: 0, w: 6, h: 5 }],
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
203
|
-
data: { displayId: "display-2" },
|
|
204
|
-
meta: { command: "displays create" },
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it("creates widgets through the generic --type path", async () => {
|
|
209
|
-
const configPath = createApiConfig();
|
|
210
|
-
const output = createStdoutBuffer();
|
|
211
|
-
const fetchMock = createFetchMock();
|
|
212
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-1" }));
|
|
213
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
214
|
-
|
|
215
|
-
await program.parseAsync([
|
|
216
|
-
"node",
|
|
217
|
-
"zinkee",
|
|
218
|
-
"--json",
|
|
219
|
-
"displays",
|
|
220
|
-
"widgets",
|
|
221
|
-
"create",
|
|
222
|
-
"display-1",
|
|
223
|
-
"--type",
|
|
224
|
-
"chart",
|
|
225
|
-
"--name",
|
|
226
|
-
"Revenue",
|
|
227
|
-
"--chart-type",
|
|
228
|
-
"bars",
|
|
229
|
-
]);
|
|
230
|
-
|
|
231
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
232
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/displays/display-1/widgets");
|
|
233
|
-
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
234
|
-
type: "CHART",
|
|
235
|
-
presentation: { name: "Revenue" },
|
|
236
|
-
chart: { chartType: "bars" },
|
|
237
|
-
});
|
|
238
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
239
|
-
data: { displayId: "display-1", widgetId: "widget-1" },
|
|
240
|
-
meta: { command: "displays widgets create" },
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("keeps the widget description flag in presentation and sends an empty clear value", async () => {
|
|
245
|
-
const configPath = createApiConfig();
|
|
246
|
-
const output = createStdoutBuffer();
|
|
247
|
-
const fetchMock = createFetchMock();
|
|
248
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-1" }));
|
|
249
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
250
|
-
|
|
251
|
-
await program.parseAsync([
|
|
252
|
-
"node",
|
|
253
|
-
"zinkee",
|
|
254
|
-
"--json",
|
|
255
|
-
"displays",
|
|
256
|
-
"widgets",
|
|
257
|
-
"update",
|
|
258
|
-
"display-1",
|
|
259
|
-
"widget-1",
|
|
260
|
-
"--description",
|
|
261
|
-
"",
|
|
262
|
-
]);
|
|
263
|
-
|
|
264
|
-
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
|
|
265
|
-
presentation: { description: "" },
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("deep merges chart flags with chart data from --raw", async () => {
|
|
270
|
-
const configPath = createApiConfig();
|
|
271
|
-
const output = createStdoutBuffer();
|
|
272
|
-
const fetchMock = createFetchMock();
|
|
273
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-2" }));
|
|
274
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
275
|
-
|
|
276
|
-
await program.parseAsync([
|
|
277
|
-
"node",
|
|
278
|
-
"zinkee",
|
|
279
|
-
"--json",
|
|
280
|
-
"displays",
|
|
281
|
-
"widgets",
|
|
282
|
-
"create",
|
|
283
|
-
"display-1",
|
|
284
|
-
"--type",
|
|
285
|
-
"chart",
|
|
286
|
-
"--name",
|
|
287
|
-
"Horas",
|
|
288
|
-
"--origin-resource",
|
|
289
|
-
"schema-1",
|
|
290
|
-
"--chart-type",
|
|
291
|
-
"bars",
|
|
292
|
-
"--x-axis-field",
|
|
293
|
-
"rh-empleado",
|
|
294
|
-
"--raw",
|
|
295
|
-
'{"chart":{"yAxisSeries":[{"typeCalc":"SUM","fieldCalc":"rh-horas","axisType":"PRIMARY"}],"legend":{"position":"bottom"}}}',
|
|
296
|
-
]);
|
|
297
|
-
|
|
298
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
299
|
-
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
300
|
-
type: "CHART",
|
|
301
|
-
presentation: { name: "Horas" },
|
|
302
|
-
source: { originResourceId: "schema-1" },
|
|
303
|
-
chart: {
|
|
304
|
-
chartType: "bars",
|
|
305
|
-
xAxisFieldId: "rh-empleado",
|
|
306
|
-
yAxisSeries: [{ typeCalc: "SUM", fieldCalc: "rh-horas", axisType: "PRIMARY" }],
|
|
307
|
-
legend: { position: "bottom" },
|
|
308
|
-
},
|
|
309
|
-
});
|
|
310
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
311
|
-
data: { displayId: "display-1", widgetId: "widget-2" },
|
|
312
|
-
meta: { command: "displays widgets create" },
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it("preserves raw chart properties when --y-axis-serie is used", async () => {
|
|
317
|
-
const configPath = createApiConfig();
|
|
318
|
-
const output = createStdoutBuffer();
|
|
319
|
-
const fetchMock = createFetchMock();
|
|
320
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-3" }));
|
|
321
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
322
|
-
|
|
323
|
-
await program.parseAsync([
|
|
324
|
-
"node",
|
|
325
|
-
"zinkee",
|
|
326
|
-
"--json",
|
|
327
|
-
"displays",
|
|
328
|
-
"widgets",
|
|
329
|
-
"create",
|
|
330
|
-
"display-1",
|
|
331
|
-
"--type",
|
|
332
|
-
"chart",
|
|
333
|
-
"--raw",
|
|
334
|
-
'{"chart":{"chartType":"bars","xAxisFieldId":"rh-empleado"}}',
|
|
335
|
-
"--y-axis-serie",
|
|
336
|
-
'{"typeCalc":"SUM","fieldCalc":"rh-horas","axisType":"PRIMARY"}',
|
|
337
|
-
]);
|
|
338
|
-
|
|
339
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
340
|
-
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
341
|
-
type: "CHART",
|
|
342
|
-
chart: {
|
|
343
|
-
chartType: "bars",
|
|
344
|
-
xAxisFieldId: "rh-empleado",
|
|
345
|
-
yAxisSeries: [{ typeCalc: "SUM", fieldCalc: "rh-horas", axisType: "PRIMARY" }],
|
|
346
|
-
},
|
|
347
|
-
});
|
|
348
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
349
|
-
data: { displayId: "display-1", widgetId: "widget-3" },
|
|
350
|
-
meta: { command: "displays widgets create" },
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it("builds KANBAN viewState fieldMapping, cardConfig and aggregation from flags", async () => {
|
|
355
|
-
const configPath = createApiConfig();
|
|
356
|
-
const output = createStdoutBuffer();
|
|
357
|
-
const fetchMock = createFetchMock();
|
|
358
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-k1" }));
|
|
359
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
360
|
-
|
|
361
|
-
await program.parseAsync([
|
|
362
|
-
"node",
|
|
363
|
-
"zinkee",
|
|
364
|
-
"--json",
|
|
365
|
-
"displays",
|
|
366
|
-
"widgets",
|
|
367
|
-
"create",
|
|
368
|
-
"display-1",
|
|
369
|
-
"--type",
|
|
370
|
-
"kanban",
|
|
371
|
-
"--field",
|
|
372
|
-
"name-uuid",
|
|
373
|
-
"--column-field",
|
|
374
|
-
"status-uuid",
|
|
375
|
-
"--swimlane-field",
|
|
376
|
-
"owner-uuid",
|
|
377
|
-
"--card-title-field",
|
|
378
|
-
"name-uuid",
|
|
379
|
-
"--card-category3-field",
|
|
380
|
-
"amount-uuid",
|
|
381
|
-
"--aggregation-method",
|
|
382
|
-
"sum",
|
|
383
|
-
"--aggregation-field",
|
|
384
|
-
"amount-uuid",
|
|
385
|
-
"--view-state",
|
|
386
|
-
'{"showNoValueColumn":true,"hideEmptyColumns":false}',
|
|
387
|
-
]);
|
|
388
|
-
|
|
389
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
390
|
-
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
391
|
-
type: "KANBAN",
|
|
392
|
-
view: {
|
|
393
|
-
fields: ["name-uuid"],
|
|
394
|
-
viewAdditionalProperties: {
|
|
395
|
-
viewState: {
|
|
396
|
-
showNoValueColumn: true,
|
|
397
|
-
hideEmptyColumns: false,
|
|
398
|
-
fieldMapping: {
|
|
399
|
-
columnField: "status-uuid",
|
|
400
|
-
swimlaneField: "owner-uuid",
|
|
401
|
-
cardConfig: { titleField: "name-uuid", category3Field: "amount-uuid" },
|
|
402
|
-
aggregationConfig: { method: "sum", fieldId: "amount-uuid" },
|
|
403
|
-
},
|
|
404
|
-
},
|
|
405
|
-
},
|
|
406
|
-
},
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
it("builds TIMELINE viewState fieldMapping with category4 from flags", async () => {
|
|
411
|
-
const configPath = createApiConfig();
|
|
412
|
-
const output = createStdoutBuffer();
|
|
413
|
-
const fetchMock = createFetchMock();
|
|
414
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-t1" }));
|
|
415
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
416
|
-
|
|
417
|
-
await program.parseAsync([
|
|
418
|
-
"node",
|
|
419
|
-
"zinkee",
|
|
420
|
-
"--json",
|
|
421
|
-
"displays",
|
|
422
|
-
"widgets",
|
|
423
|
-
"create",
|
|
424
|
-
"display-1",
|
|
425
|
-
"--type",
|
|
426
|
-
"timeline",
|
|
427
|
-
"--start-date-field",
|
|
428
|
-
"start-uuid",
|
|
429
|
-
"--end-date-field",
|
|
430
|
-
"end-uuid",
|
|
431
|
-
"--label-field",
|
|
432
|
-
"name-uuid",
|
|
433
|
-
"--card-category4-field",
|
|
434
|
-
"amount-uuid",
|
|
435
|
-
"--view-state",
|
|
436
|
-
'{"granularity":"month","sidebarWidth":300}',
|
|
437
|
-
]);
|
|
438
|
-
|
|
439
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
440
|
-
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
441
|
-
type: "TIMELINE",
|
|
442
|
-
view: {
|
|
443
|
-
viewAdditionalProperties: {
|
|
444
|
-
viewState: {
|
|
445
|
-
granularity: "month",
|
|
446
|
-
sidebarWidth: 300,
|
|
447
|
-
fieldMapping: {
|
|
448
|
-
startDateField: "start-uuid",
|
|
449
|
-
endDateField: "end-uuid",
|
|
450
|
-
labelField: "name-uuid",
|
|
451
|
-
cardConfig: { category4Field: "amount-uuid" },
|
|
452
|
-
},
|
|
453
|
-
},
|
|
454
|
-
},
|
|
455
|
-
},
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
|
|
459
|
-
it("builds HIERARCHY hierarchyConfig from columns, root and child flags", async () => {
|
|
460
|
-
const configPath = createApiConfig();
|
|
461
|
-
const output = createStdoutBuffer();
|
|
462
|
-
const fetchMock = createFetchMock();
|
|
463
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-h1" }));
|
|
464
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
465
|
-
|
|
466
|
-
await program.parseAsync([
|
|
467
|
-
"node",
|
|
468
|
-
"zinkee",
|
|
469
|
-
"--json",
|
|
470
|
-
"displays",
|
|
471
|
-
"widgets",
|
|
472
|
-
"create",
|
|
473
|
-
"display-1",
|
|
474
|
-
"--type",
|
|
475
|
-
"hierarchy",
|
|
476
|
-
"--hierarchy-label",
|
|
477
|
-
"Name",
|
|
478
|
-
"--hierarchy-info",
|
|
479
|
-
"col-info-status:Status",
|
|
480
|
-
"--hierarchy-number",
|
|
481
|
-
"col-num-amount:Total amount:sum",
|
|
482
|
-
"--hierarchy-root-name",
|
|
483
|
-
"Customers",
|
|
484
|
-
"--hierarchy-root-label-field",
|
|
485
|
-
"name-uuid",
|
|
486
|
-
"--hierarchy-root-info",
|
|
487
|
-
"col-info-status:status-uuid",
|
|
488
|
-
"--hierarchy-root-number",
|
|
489
|
-
"col-num-amount:amount-uuid",
|
|
490
|
-
"--hierarchy-child",
|
|
491
|
-
'{"name":"Projects","schemaId":"projects-uuid","childFieldId":"child-uuid","labelFieldId":"project-name-uuid","infoMappings":[],"numberMappings":[{"columnId":"col-num-amount","fieldId":"project-amount-uuid"}]}',
|
|
492
|
-
]);
|
|
493
|
-
|
|
494
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
495
|
-
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
496
|
-
type: "HIERARCHY",
|
|
497
|
-
view: {
|
|
498
|
-
hierarchyConfig: {
|
|
499
|
-
columns: {
|
|
500
|
-
label: "Name",
|
|
501
|
-
infos: [{ id: "col-info-status", name: "Status" }],
|
|
502
|
-
numbers: [{ id: "col-num-amount", name: "Total amount", aggregation: "SUM" }],
|
|
503
|
-
},
|
|
504
|
-
rootLevel: {
|
|
505
|
-
name: "Customers",
|
|
506
|
-
labelFieldId: "name-uuid",
|
|
507
|
-
infoMappings: [{ columnId: "col-info-status", fieldId: "status-uuid" }],
|
|
508
|
-
numberMappings: [{ columnId: "col-num-amount", fieldId: "amount-uuid" }],
|
|
509
|
-
},
|
|
510
|
-
childLevels: [
|
|
511
|
-
{
|
|
512
|
-
name: "Projects",
|
|
513
|
-
schemaId: "projects-uuid",
|
|
514
|
-
childFieldId: "child-uuid",
|
|
515
|
-
labelFieldId: "project-name-uuid",
|
|
516
|
-
infoMappings: [],
|
|
517
|
-
numberMappings: [{ columnId: "col-num-amount", fieldId: "project-amount-uuid" }],
|
|
518
|
-
},
|
|
519
|
-
],
|
|
520
|
-
},
|
|
521
|
-
},
|
|
522
|
-
});
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
it("deep merges KANBAN flags over view state coming from --raw", async () => {
|
|
526
|
-
const configPath = createApiConfig();
|
|
527
|
-
const output = createStdoutBuffer();
|
|
528
|
-
const fetchMock = createFetchMock();
|
|
529
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-k2" }));
|
|
530
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
531
|
-
|
|
532
|
-
await program.parseAsync([
|
|
533
|
-
"node",
|
|
534
|
-
"zinkee",
|
|
535
|
-
"--json",
|
|
536
|
-
"displays",
|
|
537
|
-
"widgets",
|
|
538
|
-
"create",
|
|
539
|
-
"display-1",
|
|
540
|
-
"--type",
|
|
541
|
-
"kanban",
|
|
542
|
-
"--raw",
|
|
543
|
-
'{"view":{"viewAdditionalProperties":{"viewState":{"columnOrder":["a","b"],"fieldMapping":{"columnField":"old-uuid","cardConfig":{"titleField":"name-uuid"}}}}}}',
|
|
544
|
-
"--column-field",
|
|
545
|
-
"status-uuid",
|
|
546
|
-
"--card-category1-field",
|
|
547
|
-
"status-uuid",
|
|
548
|
-
]);
|
|
549
|
-
|
|
550
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
551
|
-
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
552
|
-
type: "KANBAN",
|
|
553
|
-
view: {
|
|
554
|
-
viewAdditionalProperties: {
|
|
555
|
-
viewState: {
|
|
556
|
-
columnOrder: ["a", "b"],
|
|
557
|
-
fieldMapping: {
|
|
558
|
-
columnField: "status-uuid",
|
|
559
|
-
cardConfig: { titleField: "name-uuid", category1Field: "status-uuid" },
|
|
560
|
-
},
|
|
561
|
-
},
|
|
562
|
-
},
|
|
563
|
-
},
|
|
564
|
-
});
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
it("rejects --hierarchy-number with an invalid aggregation", async () => {
|
|
568
|
-
const configPath = createApiConfig();
|
|
569
|
-
const output = createStdoutBuffer();
|
|
570
|
-
const fetchMock = createFetchMock();
|
|
571
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
572
|
-
|
|
573
|
-
await expect(
|
|
574
|
-
program.parseAsync([
|
|
575
|
-
"node",
|
|
576
|
-
"zinkee",
|
|
577
|
-
"--json",
|
|
578
|
-
"displays",
|
|
579
|
-
"widgets",
|
|
580
|
-
"create",
|
|
581
|
-
"display-1",
|
|
582
|
-
"--type",
|
|
583
|
-
"hierarchy",
|
|
584
|
-
"--hierarchy-number",
|
|
585
|
-
"col-num-amount:Total:BOGUS",
|
|
586
|
-
]),
|
|
587
|
-
).rejects.toThrow(/aggregation/i);
|
|
588
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
589
|
-
});
|
|
590
|
-
|
|
591
|
-
it("translates --lock-* flags into behavior.lockedFields", async () => {
|
|
592
|
-
const configPath = createApiConfig();
|
|
593
|
-
const output = createStdoutBuffer();
|
|
594
|
-
const fetchMock = createFetchMock();
|
|
595
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-l1" }));
|
|
596
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
597
|
-
|
|
598
|
-
await program.parseAsync([
|
|
599
|
-
"node",
|
|
600
|
-
"zinkee",
|
|
601
|
-
"--json",
|
|
602
|
-
"displays",
|
|
603
|
-
"widgets",
|
|
604
|
-
"create",
|
|
605
|
-
"display-1",
|
|
606
|
-
"--type",
|
|
607
|
-
"table",
|
|
608
|
-
"--origin-resource",
|
|
609
|
-
"schema-1",
|
|
610
|
-
"--lock-field",
|
|
611
|
-
"contact-name",
|
|
612
|
-
"--lock-condition",
|
|
613
|
-
"status:invoice-number:is_empty",
|
|
614
|
-
"--lock-condition",
|
|
615
|
-
"status:amount:gte:500",
|
|
616
|
-
"--lock-message-before",
|
|
617
|
-
"status:Status locks until the invoice number is set.",
|
|
618
|
-
"--lock-message-after",
|
|
619
|
-
"status:Status is locked because the invoice number is missing.",
|
|
620
|
-
]);
|
|
621
|
-
|
|
622
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
623
|
-
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
624
|
-
type: "TABLE_OR_SUBTABLE",
|
|
625
|
-
behavior: {
|
|
626
|
-
lockedFields: [
|
|
627
|
-
{
|
|
628
|
-
field: { fieldId: "contact-name" },
|
|
629
|
-
conditions: [],
|
|
630
|
-
},
|
|
631
|
-
{
|
|
632
|
-
field: { fieldId: "status" },
|
|
633
|
-
conditions: [
|
|
634
|
-
{
|
|
635
|
-
origin: { fieldId: "invoice-number" },
|
|
636
|
-
comparisonOperator: "IS_EMPTY",
|
|
637
|
-
values: [],
|
|
638
|
-
},
|
|
639
|
-
{
|
|
640
|
-
origin: { fieldId: "amount" },
|
|
641
|
-
comparisonOperator: "GREATER_OR_EQUAL",
|
|
642
|
-
values: ["500"],
|
|
643
|
-
},
|
|
644
|
-
],
|
|
645
|
-
messageBeforeLock: "Status locks until the invoice number is set.",
|
|
646
|
-
messageAfterLock: "Status is locked because the invoice number is missing.",
|
|
647
|
-
},
|
|
648
|
-
],
|
|
649
|
-
},
|
|
650
|
-
});
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
it("clears locked fields with --clear-locked-fields", async () => {
|
|
654
|
-
const configPath = createApiConfig();
|
|
655
|
-
const output = createStdoutBuffer();
|
|
656
|
-
const fetchMock = createFetchMock();
|
|
657
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-l2" }));
|
|
658
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
659
|
-
|
|
660
|
-
await program.parseAsync([
|
|
661
|
-
"node",
|
|
662
|
-
"zinkee",
|
|
663
|
-
"--json",
|
|
664
|
-
"displays",
|
|
665
|
-
"widgets",
|
|
666
|
-
"update",
|
|
667
|
-
"display-1",
|
|
668
|
-
"widget-l2",
|
|
669
|
-
"--clear-locked-fields",
|
|
670
|
-
]);
|
|
671
|
-
|
|
672
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
673
|
-
expect(init?.method).toBe("PATCH");
|
|
674
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
675
|
-
behavior: { lockedFields: [] },
|
|
676
|
-
});
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
it("rejects --clear-locked-fields combined with --lock-* flags", async () => {
|
|
680
|
-
const configPath = createApiConfig();
|
|
681
|
-
const output = createStdoutBuffer();
|
|
682
|
-
const fetchMock = createFetchMock();
|
|
683
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
684
|
-
|
|
685
|
-
await expect(
|
|
686
|
-
program.parseAsync([
|
|
687
|
-
"node",
|
|
688
|
-
"zinkee",
|
|
689
|
-
"--json",
|
|
690
|
-
"displays",
|
|
691
|
-
"widgets",
|
|
692
|
-
"update",
|
|
693
|
-
"display-1",
|
|
694
|
-
"widget-l3",
|
|
695
|
-
"--clear-locked-fields",
|
|
696
|
-
"--lock-field",
|
|
697
|
-
"status",
|
|
698
|
-
]),
|
|
699
|
-
).rejects.toThrow(/not both/i);
|
|
700
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
it("rejects a --lock-condition without an expression", async () => {
|
|
704
|
-
const configPath = createApiConfig();
|
|
705
|
-
const output = createStdoutBuffer();
|
|
706
|
-
const fetchMock = createFetchMock();
|
|
707
|
-
const errorSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
708
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
709
|
-
|
|
710
|
-
// Option arg parser errors are handled by commander, which prints the
|
|
711
|
-
// InvalidArgumentError message to stderr and exits with code 1 (same
|
|
712
|
-
// behavior as an invalid --filter expression).
|
|
713
|
-
await expect(
|
|
714
|
-
program.parseAsync([
|
|
715
|
-
"node",
|
|
716
|
-
"zinkee",
|
|
717
|
-
"--json",
|
|
718
|
-
"displays",
|
|
719
|
-
"widgets",
|
|
720
|
-
"create",
|
|
721
|
-
"display-1",
|
|
722
|
-
"--type",
|
|
723
|
-
"table",
|
|
724
|
-
"--lock-condition",
|
|
725
|
-
"status",
|
|
726
|
-
]),
|
|
727
|
-
).rejects.toThrow(/process.exit/);
|
|
728
|
-
expect(errorSpy.mock.calls.flat().join("\n")).toMatch(/lockedField/i);
|
|
729
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
730
|
-
});
|
|
731
|
-
|
|
732
|
-
it("translates --filter into the structured display filter contract", async () => {
|
|
733
|
-
const configPath = createApiConfig();
|
|
734
|
-
const output = createStdoutBuffer();
|
|
735
|
-
const fetchMock = createFetchMock();
|
|
736
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1", widgetId: "widget-f1" }));
|
|
737
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
738
|
-
|
|
739
|
-
await program.parseAsync([
|
|
740
|
-
"node",
|
|
741
|
-
"zinkee",
|
|
742
|
-
"--json",
|
|
743
|
-
"displays",
|
|
744
|
-
"widgets",
|
|
745
|
-
"create",
|
|
746
|
-
"display-1",
|
|
747
|
-
"--type",
|
|
748
|
-
"table",
|
|
749
|
-
"--origin-resource",
|
|
750
|
-
"schema-1",
|
|
751
|
-
"--filter",
|
|
752
|
-
"edad:gt:30",
|
|
753
|
-
]);
|
|
754
|
-
|
|
755
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
756
|
-
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
757
|
-
type: "TABLE_OR_SUBTABLE",
|
|
758
|
-
query: {
|
|
759
|
-
filters: [
|
|
760
|
-
{
|
|
761
|
-
origin: { fieldId: "edad" },
|
|
762
|
-
comparisonOperator: "GREATER",
|
|
763
|
-
values: ["30"],
|
|
764
|
-
},
|
|
765
|
-
],
|
|
766
|
-
},
|
|
767
|
-
});
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
it("maps the `in` comparator to ANY_OF with a list of values", async () => {
|
|
771
|
-
const configPath = createApiConfig();
|
|
772
|
-
const fetchMock = createFetchMock();
|
|
773
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ widgetId: "widget-f2" }));
|
|
774
|
-
const program = buildProgram({ configPath });
|
|
775
|
-
|
|
776
|
-
await program.parseAsync([
|
|
777
|
-
"node",
|
|
778
|
-
"zinkee",
|
|
779
|
-
"--json",
|
|
780
|
-
"displays",
|
|
781
|
-
"widgets",
|
|
782
|
-
"create",
|
|
783
|
-
"display-1",
|
|
784
|
-
"--type",
|
|
785
|
-
"table",
|
|
786
|
-
"--filter",
|
|
787
|
-
"status:in:1,2,3",
|
|
788
|
-
]);
|
|
789
|
-
|
|
790
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
791
|
-
expect(JSON.parse(String(init?.body)).query.filters).toEqual([
|
|
792
|
-
{
|
|
793
|
-
origin: { fieldId: "status" },
|
|
794
|
-
comparisonOperator: "ANY_OF",
|
|
795
|
-
values: ["1", "2", "3"],
|
|
796
|
-
},
|
|
797
|
-
]);
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
it("maps the `is_not_empty` comparator with empty values array", async () => {
|
|
801
|
-
const configPath = createApiConfig();
|
|
802
|
-
const fetchMock = createFetchMock();
|
|
803
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ widgetId: "widget-f3" }));
|
|
804
|
-
const program = buildProgram({ configPath });
|
|
805
|
-
|
|
806
|
-
await program.parseAsync([
|
|
807
|
-
"node",
|
|
808
|
-
"zinkee",
|
|
809
|
-
"--json",
|
|
810
|
-
"displays",
|
|
811
|
-
"widgets",
|
|
812
|
-
"create",
|
|
813
|
-
"display-1",
|
|
814
|
-
"--type",
|
|
815
|
-
"table",
|
|
816
|
-
"--filter",
|
|
817
|
-
"email:is_not_empty",
|
|
818
|
-
]);
|
|
819
|
-
|
|
820
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
821
|
-
expect(JSON.parse(String(init?.body)).query.filters).toEqual([
|
|
822
|
-
{
|
|
823
|
-
origin: { fieldId: "email" },
|
|
824
|
-
comparisonOperator: "IS_NOT_EMPTY",
|
|
825
|
-
values: [],
|
|
826
|
-
},
|
|
827
|
-
]);
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
it("accumulates multiple --filter flags in order", async () => {
|
|
831
|
-
const configPath = createApiConfig();
|
|
832
|
-
const fetchMock = createFetchMock();
|
|
833
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ widgetId: "widget-f4" }));
|
|
834
|
-
const program = buildProgram({ configPath });
|
|
835
|
-
|
|
836
|
-
await program.parseAsync([
|
|
837
|
-
"node",
|
|
838
|
-
"zinkee",
|
|
839
|
-
"--json",
|
|
840
|
-
"displays",
|
|
841
|
-
"widgets",
|
|
842
|
-
"create",
|
|
843
|
-
"display-1",
|
|
844
|
-
"--type",
|
|
845
|
-
"table",
|
|
846
|
-
"--filter",
|
|
847
|
-
"status:eq:2",
|
|
848
|
-
"--filter",
|
|
849
|
-
"amount:gte:500",
|
|
850
|
-
]);
|
|
851
|
-
|
|
852
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
853
|
-
expect(JSON.parse(String(init?.body)).query.filters).toEqual([
|
|
854
|
-
{ origin: { fieldId: "status" }, comparisonOperator: "EQUAL", values: ["2"] },
|
|
855
|
-
{ origin: { fieldId: "amount" }, comparisonOperator: "GREATER_OR_EQUAL", values: ["500"] },
|
|
856
|
-
]);
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
it("rejects --filter with an unsupported comparator before issuing a request", async () => {
|
|
860
|
-
const configPath = createApiConfig();
|
|
861
|
-
const fetchMock = createFetchMock();
|
|
862
|
-
const program = buildProgram({ configPath });
|
|
863
|
-
const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
864
|
-
|
|
865
|
-
await expect(
|
|
866
|
-
program.parseAsync([
|
|
867
|
-
"node",
|
|
868
|
-
"zinkee",
|
|
869
|
-
"--json",
|
|
870
|
-
"displays",
|
|
871
|
-
"widgets",
|
|
872
|
-
"create",
|
|
873
|
-
"display-1",
|
|
874
|
-
"--type",
|
|
875
|
-
"table",
|
|
876
|
-
"--filter",
|
|
877
|
-
"edad:invalido:30",
|
|
878
|
-
]),
|
|
879
|
-
).rejects.toThrow();
|
|
880
|
-
|
|
881
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
882
|
-
expect(stderrSpy.mock.calls.map(([chunk]) => String(chunk)).join("")).toMatch(/Unsupported comparator/);
|
|
883
|
-
stderrSpy.mockRestore();
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
it("preserves --raw query.filters when only --field is passed (no --filter)", async () => {
|
|
887
|
-
const configPath = createApiConfig();
|
|
888
|
-
const fetchMock = createFetchMock();
|
|
889
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ widgetId: "widget-f6" }));
|
|
890
|
-
const program = buildProgram({ configPath });
|
|
891
|
-
|
|
892
|
-
await program.parseAsync([
|
|
893
|
-
"node",
|
|
894
|
-
"zinkee",
|
|
895
|
-
"--json",
|
|
896
|
-
"displays",
|
|
897
|
-
"widgets",
|
|
898
|
-
"create",
|
|
899
|
-
"display-1",
|
|
900
|
-
"--type",
|
|
901
|
-
"table",
|
|
902
|
-
"--field",
|
|
903
|
-
"foo",
|
|
904
|
-
"--raw",
|
|
905
|
-
'{"query":{"filters":[{"origin":{"fieldId":"status"},"comparisonOperator":"EQUAL","values":["2"]}]}}',
|
|
906
|
-
]);
|
|
907
|
-
|
|
908
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
909
|
-
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
910
|
-
view: { fields: ["foo"] },
|
|
911
|
-
query: {
|
|
912
|
-
filters: [
|
|
913
|
-
{ origin: { fieldId: "status" }, comparisonOperator: "EQUAL", values: ["2"] },
|
|
914
|
-
],
|
|
915
|
-
},
|
|
916
|
-
});
|
|
917
|
-
});
|
|
918
|
-
|
|
919
|
-
it("translates --filter for variable mutations into the structured filter contract", async () => {
|
|
920
|
-
const configPath = createApiConfig();
|
|
921
|
-
const fetchMock = createFetchMock();
|
|
922
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ variableId: "var-1" }));
|
|
923
|
-
const program = buildProgram({ configPath });
|
|
924
|
-
|
|
925
|
-
await program.parseAsync([
|
|
926
|
-
"node",
|
|
927
|
-
"zinkee",
|
|
928
|
-
"--json",
|
|
929
|
-
"displays",
|
|
930
|
-
"variables",
|
|
931
|
-
"create",
|
|
932
|
-
"display-1",
|
|
933
|
-
"--type",
|
|
934
|
-
"reference",
|
|
935
|
-
"--name",
|
|
936
|
-
"Activos",
|
|
937
|
-
"--filter",
|
|
938
|
-
"estado:eq:activo",
|
|
939
|
-
]);
|
|
940
|
-
|
|
941
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
942
|
-
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
943
|
-
type: "reference",
|
|
944
|
-
name: "Activos",
|
|
945
|
-
filters: [
|
|
946
|
-
{
|
|
947
|
-
origin: { fieldId: "estado" },
|
|
948
|
-
comparisonOperator: "EQUAL",
|
|
949
|
-
values: ["activo"],
|
|
950
|
-
},
|
|
951
|
-
],
|
|
952
|
-
});
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
it("blocks destructive display commands in read-only mode", async () => {
|
|
956
|
-
const configPath = createApiConfig();
|
|
957
|
-
const fetchMock = createFetchMock();
|
|
958
|
-
const program = buildProgram({ configPath });
|
|
959
|
-
|
|
960
|
-
await expect(
|
|
961
|
-
program.parseAsync(["node", "zinkee", "--read-only", "displays", "delete", "display-1"]),
|
|
962
|
-
).rejects.toThrowError('Command "displays delete" is not allowed in read-only mode.');
|
|
963
|
-
|
|
964
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
965
|
-
});
|
|
966
|
-
|
|
967
|
-
it("creates a display folder with just a name", async () => {
|
|
968
|
-
const configPath = createApiConfig();
|
|
969
|
-
const output = createStdoutBuffer();
|
|
970
|
-
const fetchMock = createFetchMock();
|
|
971
|
-
fetchMock.mockResolvedValueOnce(
|
|
972
|
-
jsonResponse({ id: "folder-1", name: "Executive Dashboards", isRoot: false, displayCount: 0 }),
|
|
973
|
-
);
|
|
974
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
975
|
-
|
|
976
|
-
await program.parseAsync([
|
|
977
|
-
"node",
|
|
978
|
-
"zinkee",
|
|
979
|
-
"--json",
|
|
980
|
-
"displays",
|
|
981
|
-
"folders",
|
|
982
|
-
"create",
|
|
983
|
-
"--name",
|
|
984
|
-
"Executive Dashboards",
|
|
985
|
-
]);
|
|
986
|
-
|
|
987
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/display-folders");
|
|
988
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
989
|
-
expect(init?.method).toBe("POST");
|
|
990
|
-
expect(JSON.parse(String(init?.body))).toEqual({ name: "Executive Dashboards" });
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
it("moves a display into a folder resolved by name", async () => {
|
|
994
|
-
const configPath = createApiConfig();
|
|
995
|
-
const output = createStdoutBuffer();
|
|
996
|
-
const fetchMock = createFetchMock();
|
|
997
|
-
fetchMock.mockResolvedValueOnce(
|
|
998
|
-
jsonResponse([{ id: "folder-1", name: "Executive Dashboards", isRoot: false, displayCount: 2 }]),
|
|
999
|
-
);
|
|
1000
|
-
fetchMock.mockResolvedValueOnce(
|
|
1001
|
-
jsonResponse({ id: "folder-1", name: "Executive Dashboards", isRoot: false }),
|
|
1002
|
-
);
|
|
1003
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
1004
|
-
|
|
1005
|
-
await program.parseAsync([
|
|
1006
|
-
"node",
|
|
1007
|
-
"zinkee",
|
|
1008
|
-
"--json",
|
|
1009
|
-
"displays",
|
|
1010
|
-
"folders",
|
|
1011
|
-
"move",
|
|
1012
|
-
"0f6f062f-1478-4df2-96a1-c495d037d260",
|
|
1013
|
-
"--to",
|
|
1014
|
-
"Executive Dashboards",
|
|
1015
|
-
]);
|
|
1016
|
-
|
|
1017
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/display-folders");
|
|
1018
|
-
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
1019
|
-
"https://api.zinkee.test/api/v2/display-folders/displays:move",
|
|
1020
|
-
);
|
|
1021
|
-
const moveInit = fetchMock.mock.calls[1]?.[1];
|
|
1022
|
-
expect(moveInit?.method).toBe("POST");
|
|
1023
|
-
expect(JSON.parse(String(moveInit?.body))).toEqual({
|
|
1024
|
-
displayId: "0f6f062f-1478-4df2-96a1-c495d037d260",
|
|
1025
|
-
targetFolderId: "folder-1",
|
|
1026
|
-
});
|
|
1027
|
-
});
|
|
1028
|
-
|
|
1029
|
-
it("blocks destructive display folder commands in read-only mode", async () => {
|
|
1030
|
-
const configPath = createApiConfig();
|
|
1031
|
-
const fetchMock = createFetchMock();
|
|
1032
|
-
const program = buildProgram({ configPath });
|
|
1033
|
-
|
|
1034
|
-
await expect(
|
|
1035
|
-
program.parseAsync(["node", "zinkee", "--read-only", "displays", "folders", "delete", "folder-1"]),
|
|
1036
|
-
).rejects.toThrowError('Command "displays folders delete" is not allowed in read-only mode.');
|
|
1037
|
-
|
|
1038
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
1039
|
-
});
|
|
1040
|
-
|
|
1041
|
-
it("wraps a flag-built cross widget filter configuration in an array", async () => {
|
|
1042
|
-
const configPath = createApiConfig();
|
|
1043
|
-
const output = createStdoutBuffer();
|
|
1044
|
-
const fetchMock = createFetchMock();
|
|
1045
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1" }));
|
|
1046
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
1047
|
-
|
|
1048
|
-
await program.parseAsync([
|
|
1049
|
-
"node",
|
|
1050
|
-
"zinkee",
|
|
1051
|
-
"--json",
|
|
1052
|
-
"displays",
|
|
1053
|
-
"cross-widget-filter",
|
|
1054
|
-
"set",
|
|
1055
|
-
"display-1",
|
|
1056
|
-
"--source-widget",
|
|
1057
|
-
"w-source",
|
|
1058
|
-
"--source-field",
|
|
1059
|
-
"f-source",
|
|
1060
|
-
"--target",
|
|
1061
|
-
"w-target:f-target",
|
|
1062
|
-
]);
|
|
1063
|
-
|
|
1064
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
1065
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
1066
|
-
"https://api.zinkee.test/api/v2/displays/display-1/cross-widget-filter",
|
|
1067
|
-
);
|
|
1068
|
-
expect(init?.method).toBe("PUT");
|
|
1069
|
-
expect(JSON.parse(String(init?.body))).toEqual([
|
|
1070
|
-
{
|
|
1071
|
-
source: { widgetId: "w-source", fieldId: "f-source" },
|
|
1072
|
-
targets: [{ widgetId: "w-target", fieldId: "f-target" }],
|
|
1073
|
-
},
|
|
1074
|
-
]);
|
|
1075
|
-
});
|
|
1076
|
-
|
|
1077
|
-
it("sends the raw array of cross widget filter configurations verbatim", async () => {
|
|
1078
|
-
const configPath = createApiConfig();
|
|
1079
|
-
const output = createStdoutBuffer();
|
|
1080
|
-
const fetchMock = createFetchMock();
|
|
1081
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ displayId: "display-1" }));
|
|
1082
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
1083
|
-
|
|
1084
|
-
const rawArray = [
|
|
1085
|
-
{ source: { widgetId: "w1", fieldId: "f1" }, targets: [{ widgetId: "t1", fieldId: "f1" }] },
|
|
1086
|
-
{ source: { widgetId: "w2", fieldId: "f2" }, targets: [{ widgetId: "t2", fieldId: "f2" }] },
|
|
1087
|
-
];
|
|
1088
|
-
|
|
1089
|
-
await program.parseAsync([
|
|
1090
|
-
"node",
|
|
1091
|
-
"zinkee",
|
|
1092
|
-
"--json",
|
|
1093
|
-
"displays",
|
|
1094
|
-
"cross-widget-filter",
|
|
1095
|
-
"set",
|
|
1096
|
-
"display-1",
|
|
1097
|
-
"--raw",
|
|
1098
|
-
JSON.stringify(rawArray),
|
|
1099
|
-
]);
|
|
1100
|
-
|
|
1101
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
1102
|
-
expect(init?.method).toBe("PUT");
|
|
1103
|
-
expect(JSON.parse(String(init?.body))).toEqual(rawArray);
|
|
1104
|
-
});
|
|
1105
|
-
});
|