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.
- package/AGENTS.md +1 -0
- package/README.md +31 -2
- package/dist/index.js +506 -192
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/displays.ts +14 -2
- package/src/api/formulas.test.ts +65 -0
- package/src/api/formulas.ts +67 -0
- package/src/api/schemas.ts +3 -0
- package/src/command-registry.ts +21 -0
- package/src/commands/displays.test.ts +99 -0
- package/src/commands/displays.ts +19 -3
- package/src/commands/formulas.test.ts +194 -0
- package/src/commands/formulas.ts +243 -0
- package/src/commands/schemas.test.ts +89 -0
- package/src/commands/schemas.ts +13 -1
- package/src/program.ts +2 -0
- package/src/utils/examples.ts +110 -5
package/package.json
CHANGED
package/src/api/displays.ts
CHANGED
|
@@ -5,6 +5,7 @@ export interface ApiDisplay {
|
|
|
5
5
|
[key: string]: unknown;
|
|
6
6
|
id?: string;
|
|
7
7
|
name?: string;
|
|
8
|
+
description?: string;
|
|
8
9
|
layoutTemplate?: string;
|
|
9
10
|
layout?: {
|
|
10
11
|
template?: string;
|
|
@@ -62,6 +63,17 @@ export interface MoveDisplayInput {
|
|
|
62
63
|
export type DisplaySummary = ApiDisplay;
|
|
63
64
|
export type DisplayResource = ApiDisplay;
|
|
64
65
|
|
|
66
|
+
export interface CreateDisplayInput {
|
|
67
|
+
name: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
layout: Record<string, unknown>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface UpdateDisplayInput {
|
|
73
|
+
name?: string;
|
|
74
|
+
description?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
function jsonOptions(body: unknown, method: string): RequestInit {
|
|
66
78
|
return {
|
|
67
79
|
method,
|
|
@@ -91,13 +103,13 @@ export class DisplaysApi {
|
|
|
91
103
|
return this.client.getJson<ApiDisplay>(`/api/v2/displays/${selectorValue(display)}`);
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
createDisplay(body:
|
|
106
|
+
createDisplay(body: CreateDisplayInput): Promise<ApiDisplay> {
|
|
95
107
|
return this.client.requestJson<ApiDisplay>("/api/v2/displays", jsonOptions(body, "POST"));
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
updateDisplay(
|
|
99
111
|
display: NormalizedUuidOrSlug,
|
|
100
|
-
body:
|
|
112
|
+
body: UpdateDisplayInput,
|
|
101
113
|
): Promise<ApiDisplay> {
|
|
102
114
|
return this.client.requestJson<ApiDisplay>(
|
|
103
115
|
`/api/v2/displays/${selectorValue(display)}`,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { normalizeUuidOrSlug } from "../parsers/selectors.js";
|
|
4
|
+
import { ApiClient } from "../client.js";
|
|
5
|
+
import { FormulasApi } from "./formulas.js";
|
|
6
|
+
|
|
7
|
+
function jsonResponse(body: unknown): Response {
|
|
8
|
+
return new Response(JSON.stringify(body), {
|
|
9
|
+
status: 202,
|
|
10
|
+
headers: { "content-type": "application/json" },
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("FormulasApi", () => {
|
|
15
|
+
it.each([
|
|
16
|
+
{
|
|
17
|
+
input: {
|
|
18
|
+
scope: "FIELD" as const,
|
|
19
|
+
schema: normalizeUuidOrSlug("invoices"),
|
|
20
|
+
field: normalizeUuidOrSlug("total"),
|
|
21
|
+
},
|
|
22
|
+
body: { scope: "FIELD", schema: "invoices", field: "total" },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
input: {
|
|
26
|
+
scope: "SCHEMA" as const,
|
|
27
|
+
schema: normalizeUuidOrSlug("550e8400-e29b-41d4-a716-446655440001"),
|
|
28
|
+
},
|
|
29
|
+
body: {
|
|
30
|
+
scope: "SCHEMA",
|
|
31
|
+
schema: "550e8400-e29b-41d4-a716-446655440001",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
input: { scope: "WORKSPACE" as const },
|
|
36
|
+
body: { scope: "WORKSPACE" },
|
|
37
|
+
},
|
|
38
|
+
])("posts the $input.scope payload to the recompute endpoint", async ({ input, body }) => {
|
|
39
|
+
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(
|
|
40
|
+
jsonResponse({
|
|
41
|
+
requestId: "request-1",
|
|
42
|
+
scope: input.scope,
|
|
43
|
+
formulaCount: 1,
|
|
44
|
+
skippedFormulaCount: 0,
|
|
45
|
+
eventsQueued: 1,
|
|
46
|
+
status: "QUEUED",
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
const api = new FormulasApi(
|
|
50
|
+
new ApiClient({
|
|
51
|
+
baseUrl: "https://api.zinkee.test",
|
|
52
|
+
apiKey: "test-token",
|
|
53
|
+
fetch: fetchMock,
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
await api.recompute(input);
|
|
58
|
+
|
|
59
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
60
|
+
const [url, init] = fetchMock.mock.calls[0]!;
|
|
61
|
+
expect(url).toBe("https://api.zinkee.test/api/v2/formulas:recompute");
|
|
62
|
+
expect(init?.method).toBe("POST");
|
|
63
|
+
expect(JSON.parse(String(init?.body))).toEqual(body);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ApiClient } from "../client.js";
|
|
2
|
+
import type { NormalizedUuidOrSlug } from "../parsers/selectors.js";
|
|
3
|
+
|
|
4
|
+
export type FormulaRecomputeScope = "FIELD" | "SCHEMA" | "WORKSPACE";
|
|
5
|
+
|
|
6
|
+
export interface FormulaRecomputeResponse {
|
|
7
|
+
requestId: string;
|
|
8
|
+
scope: FormulaRecomputeScope;
|
|
9
|
+
formulaCount: number;
|
|
10
|
+
skippedFormulaCount: number;
|
|
11
|
+
eventsQueued: number;
|
|
12
|
+
status: "QUEUED";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type FormulaRecomputeInput =
|
|
16
|
+
| {
|
|
17
|
+
scope: "FIELD";
|
|
18
|
+
schema: NormalizedUuidOrSlug;
|
|
19
|
+
field: NormalizedUuidOrSlug;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
scope: "SCHEMA";
|
|
23
|
+
schema: NormalizedUuidOrSlug;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
scope: "WORKSPACE";
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function selectorValue(selector: NormalizedUuidOrSlug): string {
|
|
30
|
+
return selector.value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function toRequestBody(input: FormulaRecomputeInput): Record<string, string> {
|
|
34
|
+
if (input.scope === "FIELD") {
|
|
35
|
+
return {
|
|
36
|
+
scope: input.scope,
|
|
37
|
+
schema: selectorValue(input.schema),
|
|
38
|
+
field: selectorValue(input.field),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (input.scope === "SCHEMA") {
|
|
43
|
+
return {
|
|
44
|
+
scope: input.scope,
|
|
45
|
+
schema: selectorValue(input.schema),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { scope: input.scope };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class FormulasApi {
|
|
53
|
+
constructor(private readonly client: ApiClient) {}
|
|
54
|
+
|
|
55
|
+
recompute(input: FormulaRecomputeInput): Promise<FormulaRecomputeResponse> {
|
|
56
|
+
return this.client.requestJson<FormulaRecomputeResponse>(
|
|
57
|
+
"/api/v2/formulas:recompute",
|
|
58
|
+
{
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: {
|
|
61
|
+
"content-type": "application/json",
|
|
62
|
+
},
|
|
63
|
+
body: JSON.stringify(toRequestBody(input)),
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/api/schemas.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface ApiSchemaField {
|
|
|
7
7
|
type?: string;
|
|
8
8
|
slug?: string;
|
|
9
9
|
label?: string;
|
|
10
|
+
description?: string;
|
|
10
11
|
required?: boolean;
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -47,12 +48,14 @@ export interface CreateSchemaFieldInput {
|
|
|
47
48
|
type: string;
|
|
48
49
|
slug: string;
|
|
49
50
|
label: string;
|
|
51
|
+
description?: string;
|
|
50
52
|
required?: boolean;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
export interface UpdateSchemaFieldInput {
|
|
54
56
|
[key: string]: unknown;
|
|
55
57
|
label?: string;
|
|
58
|
+
description?: string;
|
|
56
59
|
required?: boolean;
|
|
57
60
|
}
|
|
58
61
|
|
package/src/command-registry.ts
CHANGED
|
@@ -14,6 +14,27 @@ const readOutput: CommandOutputCapability = "table-json";
|
|
|
14
14
|
const writeOutput: CommandOutputCapability = "json-only";
|
|
15
15
|
|
|
16
16
|
export const commandRegistry = {
|
|
17
|
+
"formulas recompute field": {
|
|
18
|
+
command: "formulas recompute field",
|
|
19
|
+
accessLevel: "write",
|
|
20
|
+
supportsExample: true,
|
|
21
|
+
supportsRaw: false,
|
|
22
|
+
output: writeOutput,
|
|
23
|
+
},
|
|
24
|
+
"formulas recompute schema": {
|
|
25
|
+
command: "formulas recompute schema",
|
|
26
|
+
accessLevel: "write",
|
|
27
|
+
supportsExample: true,
|
|
28
|
+
supportsRaw: false,
|
|
29
|
+
output: writeOutput,
|
|
30
|
+
},
|
|
31
|
+
"formulas recompute workspace": {
|
|
32
|
+
command: "formulas recompute workspace",
|
|
33
|
+
accessLevel: "write",
|
|
34
|
+
supportsExample: true,
|
|
35
|
+
supportsRaw: false,
|
|
36
|
+
output: writeOutput,
|
|
37
|
+
},
|
|
17
38
|
"config path": {
|
|
18
39
|
command: "config path",
|
|
19
40
|
accessLevel: "read",
|
|
@@ -94,6 +94,80 @@ describe("displays commands", () => {
|
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
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
|
+
|
|
97
171
|
it("creates freeform displays with nested freeform layouts", async () => {
|
|
98
172
|
const configPath = createApiConfig();
|
|
99
173
|
const output = createStdoutBuffer();
|
|
@@ -167,6 +241,31 @@ describe("displays commands", () => {
|
|
|
167
241
|
});
|
|
168
242
|
});
|
|
169
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
|
+
|
|
170
269
|
it("deep merges chart flags with chart data from --raw", async () => {
|
|
171
270
|
const configPath = createApiConfig();
|
|
172
271
|
const output = createStdoutBuffer();
|
package/src/commands/displays.ts
CHANGED
|
@@ -38,6 +38,7 @@ interface GlobalOptions extends RuntimeOverrides {}
|
|
|
38
38
|
|
|
39
39
|
interface DisplayCreateOptions {
|
|
40
40
|
name: string;
|
|
41
|
+
description?: string;
|
|
41
42
|
template: string;
|
|
42
43
|
freeformLayouts?: string;
|
|
43
44
|
freeformLayoutsFile?: string;
|
|
@@ -45,6 +46,7 @@ interface DisplayCreateOptions {
|
|
|
45
46
|
|
|
46
47
|
interface DisplayUpdateOptions {
|
|
47
48
|
name?: string;
|
|
49
|
+
description?: string;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
interface LayoutUpdateOptions {
|
|
@@ -979,6 +981,7 @@ export function registerDisplaysCommands(
|
|
|
979
981
|
.command("create")
|
|
980
982
|
.description("Create a display")
|
|
981
983
|
.requiredOption("--name <name>", "display name")
|
|
984
|
+
.option("--description <text>", "display description")
|
|
982
985
|
.requiredOption("--template <template>", "layout template")
|
|
983
986
|
.option("--freeform-layouts <json>", "freeform layouts JSON")
|
|
984
987
|
.option("--freeform-layouts-file <path>", "freeform layouts JSON file")
|
|
@@ -993,7 +996,11 @@ export function registerDisplaysCommands(
|
|
|
993
996
|
"--freeform-layouts-file",
|
|
994
997
|
);
|
|
995
998
|
if (freeformLayouts !== undefined) layout.freeformLayouts = freeformLayouts;
|
|
996
|
-
const data = await api.createDisplay({
|
|
999
|
+
const data = await api.createDisplay({
|
|
1000
|
+
name: commandOptions.name,
|
|
1001
|
+
description: commandOptions.description,
|
|
1002
|
+
layout,
|
|
1003
|
+
});
|
|
997
1004
|
if (json) {
|
|
998
1005
|
emitJson(stdout, "displays create", data);
|
|
999
1006
|
return;
|
|
@@ -1006,11 +1013,20 @@ export function registerDisplaysCommands(
|
|
|
1006
1013
|
.description("Update a display")
|
|
1007
1014
|
.argument("<display>", "display uuid or slug", parseUuidOrSlugSelector)
|
|
1008
1015
|
.option("--name <name>", "display name")
|
|
1016
|
+
.option("--description <text>", 'display description (use "" to clear)')
|
|
1009
1017
|
.action(async function action(this: Command, display: NormalizedUuidOrSlug, commandOptions: DisplayUpdateOptions) {
|
|
1010
1018
|
const { api, json, readOnly } = createDisplaysApi(this, filePath);
|
|
1011
1019
|
ensureWritable("displays update", readOnly, "write");
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1020
|
+
if (commandOptions.name === undefined && commandOptions.description === undefined) {
|
|
1021
|
+
throw new CliError("invalid_cli_usage", "Nothing to update.");
|
|
1022
|
+
}
|
|
1023
|
+
if (commandOptions.name !== undefined && commandOptions.name.trim().length === 0) {
|
|
1024
|
+
throw new CliError("invalid_cli_usage", "Display name cannot be empty.");
|
|
1025
|
+
}
|
|
1026
|
+
const data = await api.updateDisplay(display, {
|
|
1027
|
+
name: commandOptions.name,
|
|
1028
|
+
description: commandOptions.description,
|
|
1029
|
+
});
|
|
1014
1030
|
if (json) {
|
|
1015
1031
|
emitJson(stdout, "displays update", data);
|
|
1016
1032
|
return;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
import { registerFormulasCommands } from "./formulas.js";
|
|
9
|
+
|
|
10
|
+
function createConfig(): string {
|
|
11
|
+
const dir = mkdtempSync(join(tmpdir(), "zinkee-formulas-"));
|
|
12
|
+
const filePath = join(dir, "config.toml");
|
|
13
|
+
writeFileSync(
|
|
14
|
+
filePath,
|
|
15
|
+
`default_profile = "test"\n\n[profiles.test]\napi_key = "token"\nbase_url = "https://api.zinkee.test"\n`,
|
|
16
|
+
"utf8",
|
|
17
|
+
);
|
|
18
|
+
return filePath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createStdoutBuffer() {
|
|
22
|
+
let output = "";
|
|
23
|
+
return {
|
|
24
|
+
stdout: {
|
|
25
|
+
write(chunk: string) {
|
|
26
|
+
output += chunk;
|
|
27
|
+
return true;
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
read: () => output,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildProgram(options: { configPath: string; stdout: { write(chunk: string): unknown } }): Command {
|
|
35
|
+
const program = new Command();
|
|
36
|
+
program
|
|
37
|
+
.name("zinkee")
|
|
38
|
+
.enablePositionalOptions()
|
|
39
|
+
.option("--read-only")
|
|
40
|
+
.option("--json");
|
|
41
|
+
registerFormulasCommands(program, options);
|
|
42
|
+
return program;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function queuedResponse(scope: "FIELD" | "SCHEMA" | "WORKSPACE", eventsQueued = 1): Response {
|
|
46
|
+
return new Response(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
requestId: "request-1",
|
|
49
|
+
scope,
|
|
50
|
+
formulaCount: eventsQueued,
|
|
51
|
+
skippedFormulaCount: 0,
|
|
52
|
+
eventsQueued,
|
|
53
|
+
status: "QUEUED",
|
|
54
|
+
}),
|
|
55
|
+
{ status: 202, headers: { "content-type": "application/json" } },
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
vi.unstubAllGlobals();
|
|
61
|
+
vi.restoreAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("formulas commands", () => {
|
|
65
|
+
it("registers the formulas command group", () => {
|
|
66
|
+
const output = createStdoutBuffer();
|
|
67
|
+
const program = buildProgram({ configPath: createConfig(), stdout: output.stdout });
|
|
68
|
+
|
|
69
|
+
expect(program.commands.some((command) => command.name() === "formulas")).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("queues one field by slug and emits the JSON envelope", async () => {
|
|
73
|
+
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(queuedResponse("FIELD"));
|
|
74
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
75
|
+
const configPath = createConfig();
|
|
76
|
+
const output = createStdoutBuffer();
|
|
77
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
78
|
+
|
|
79
|
+
await program.parseAsync([
|
|
80
|
+
"node",
|
|
81
|
+
"zinkee",
|
|
82
|
+
"--json",
|
|
83
|
+
"formulas",
|
|
84
|
+
"recompute",
|
|
85
|
+
"field",
|
|
86
|
+
"invoices",
|
|
87
|
+
"total",
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
const [, init] = fetchMock.mock.calls[0]!;
|
|
91
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
92
|
+
scope: "FIELD",
|
|
93
|
+
schema: "invoices",
|
|
94
|
+
field: "total",
|
|
95
|
+
});
|
|
96
|
+
expect(JSON.parse(output.read())).toEqual({
|
|
97
|
+
data: {
|
|
98
|
+
requestId: "request-1",
|
|
99
|
+
scope: "FIELD",
|
|
100
|
+
formulaCount: 1,
|
|
101
|
+
skippedFormulaCount: 0,
|
|
102
|
+
eventsQueued: 1,
|
|
103
|
+
status: "QUEUED",
|
|
104
|
+
},
|
|
105
|
+
meta: { command: "formulas recompute field", configPath },
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it.each([
|
|
110
|
+
["schema", ["invoices"]],
|
|
111
|
+
["workspace", []],
|
|
112
|
+
])("requires --yes before the %s request", async (scope, args) => {
|
|
113
|
+
const fetchMock = vi.fn<typeof fetch>();
|
|
114
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
115
|
+
const output = createStdoutBuffer();
|
|
116
|
+
const program = buildProgram({ configPath: createConfig(), stdout: output.stdout });
|
|
117
|
+
|
|
118
|
+
await expect(
|
|
119
|
+
program.parseAsync([
|
|
120
|
+
"node",
|
|
121
|
+
"zinkee",
|
|
122
|
+
"formulas",
|
|
123
|
+
"recompute",
|
|
124
|
+
scope,
|
|
125
|
+
...args,
|
|
126
|
+
]),
|
|
127
|
+
).rejects.toThrow("requires --yes");
|
|
128
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("queues every schema formula after confirmation", async () => {
|
|
132
|
+
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(queuedResponse("SCHEMA", 3));
|
|
133
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
134
|
+
const output = createStdoutBuffer();
|
|
135
|
+
const program = buildProgram({ configPath: createConfig(), stdout: output.stdout });
|
|
136
|
+
|
|
137
|
+
await program.parseAsync([
|
|
138
|
+
"node",
|
|
139
|
+
"zinkee",
|
|
140
|
+
"formulas",
|
|
141
|
+
"recompute",
|
|
142
|
+
"schema",
|
|
143
|
+
"invoices",
|
|
144
|
+
"--yes",
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
expect(output.read()).toContain("Queued 3 formula recalculation event(s)");
|
|
148
|
+
expect(output.read()).not.toContain("completed");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("queues the workspace payload after confirmation", async () => {
|
|
152
|
+
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(queuedResponse("WORKSPACE", 5));
|
|
153
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
154
|
+
const output = createStdoutBuffer();
|
|
155
|
+
const program = buildProgram({ configPath: createConfig(), stdout: output.stdout });
|
|
156
|
+
|
|
157
|
+
await program.parseAsync([
|
|
158
|
+
"node",
|
|
159
|
+
"zinkee",
|
|
160
|
+
"--json",
|
|
161
|
+
"formulas",
|
|
162
|
+
"recompute",
|
|
163
|
+
"workspace",
|
|
164
|
+
"--yes",
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const [, init] = fetchMock.mock.calls[0]!;
|
|
168
|
+
expect(JSON.parse(String(init?.body))).toEqual({ scope: "WORKSPACE" });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it.each([
|
|
172
|
+
["field", ["invoices", "total"]],
|
|
173
|
+
["schema", ["invoices", "--yes"]],
|
|
174
|
+
["workspace", ["--yes"]],
|
|
175
|
+
])("blocks %s in read-only mode before any network request", async (scope, args) => {
|
|
176
|
+
const fetchMock = vi.fn<typeof fetch>();
|
|
177
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
178
|
+
const output = createStdoutBuffer();
|
|
179
|
+
const program = buildProgram({ configPath: createConfig(), stdout: output.stdout });
|
|
180
|
+
|
|
181
|
+
await expect(
|
|
182
|
+
program.parseAsync([
|
|
183
|
+
"node",
|
|
184
|
+
"zinkee",
|
|
185
|
+
"--read-only",
|
|
186
|
+
"formulas",
|
|
187
|
+
"recompute",
|
|
188
|
+
scope,
|
|
189
|
+
...args,
|
|
190
|
+
]),
|
|
191
|
+
).rejects.toThrow("not allowed in read-only mode");
|
|
192
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
});
|