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,1252 +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-schemas-"));
|
|
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 = "schema-token-1234"
|
|
22
|
-
base_url = "https://api.zinkee.test"
|
|
23
|
-
`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function createStdoutBuffer() {
|
|
27
|
-
let output = "";
|
|
28
|
-
|
|
29
|
-
return {
|
|
30
|
-
stdout: {
|
|
31
|
-
write(chunk: string) {
|
|
32
|
-
output += chunk;
|
|
33
|
-
return true;
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
read() {
|
|
37
|
-
return output;
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function jsonResponse(body: unknown, status: number = 200): Response {
|
|
43
|
-
return new Response(JSON.stringify(body), {
|
|
44
|
-
status,
|
|
45
|
-
headers: {
|
|
46
|
-
"content-type": "application/json",
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function createFetchMock() {
|
|
52
|
-
const fetchMock = vi.fn<typeof fetch>();
|
|
53
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
54
|
-
return fetchMock;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function mockSchemaLookup(fetchMock: ReturnType<typeof createFetchMock>, id: string = "schema-1") {
|
|
58
|
-
fetchMock.mockResolvedValueOnce(
|
|
59
|
-
jsonResponse([
|
|
60
|
-
{
|
|
61
|
-
id,
|
|
62
|
-
slug: "contacts",
|
|
63
|
-
name: "Contacts",
|
|
64
|
-
},
|
|
65
|
-
]),
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
afterEach(() => {
|
|
70
|
-
vi.unstubAllGlobals();
|
|
71
|
-
vi.restoreAllMocks();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe("schemas commands", () => {
|
|
75
|
-
it("registers the schemas command group", () => {
|
|
76
|
-
const program = buildProgram();
|
|
77
|
-
|
|
78
|
-
expect(program.commands.some((command) => command.name() === "schemas")).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("lists schemas through /api/v2/schemas and emits a JSON envelope", async () => {
|
|
82
|
-
const configPath = createApiConfig();
|
|
83
|
-
const output = createStdoutBuffer();
|
|
84
|
-
const fetchMock = createFetchMock();
|
|
85
|
-
fetchMock.mockResolvedValueOnce(
|
|
86
|
-
jsonResponse([
|
|
87
|
-
{
|
|
88
|
-
id: "schema-1",
|
|
89
|
-
name: "Contacts",
|
|
90
|
-
slug: "contacts",
|
|
91
|
-
auditable: true,
|
|
92
|
-
commentable: false,
|
|
93
|
-
},
|
|
94
|
-
]),
|
|
95
|
-
);
|
|
96
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
97
|
-
|
|
98
|
-
await program.parseAsync(["node", "zinkee", "--json", "schemas", "list"]);
|
|
99
|
-
|
|
100
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
101
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/schemas");
|
|
102
|
-
|
|
103
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
104
|
-
const headers = new Headers(init?.headers);
|
|
105
|
-
|
|
106
|
-
expect(init?.method).toBe("GET");
|
|
107
|
-
expect(headers.get("accept")).toBe("application/json");
|
|
108
|
-
expect(headers.get("authorization")).toBe("Bearer schema-token-1234");
|
|
109
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
110
|
-
data: [
|
|
111
|
-
{
|
|
112
|
-
id: "schema-1",
|
|
113
|
-
name: "Contacts",
|
|
114
|
-
slug: "contacts",
|
|
115
|
-
auditable: true,
|
|
116
|
-
commentable: false,
|
|
117
|
-
},
|
|
118
|
-
],
|
|
119
|
-
meta: {
|
|
120
|
-
command: "schemas list",
|
|
121
|
-
configPath,
|
|
122
|
-
},
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("normalizes UUID selectors for schemas get", async () => {
|
|
127
|
-
const configPath = createApiConfig();
|
|
128
|
-
const output = createStdoutBuffer();
|
|
129
|
-
const fetchMock = createFetchMock();
|
|
130
|
-
fetchMock.mockResolvedValueOnce(
|
|
131
|
-
jsonResponse({
|
|
132
|
-
id: "550e8400-e29b-41d4-a716-446655440000",
|
|
133
|
-
name: "Contacts",
|
|
134
|
-
slug: "contacts",
|
|
135
|
-
}),
|
|
136
|
-
);
|
|
137
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
138
|
-
|
|
139
|
-
await program.parseAsync([
|
|
140
|
-
"node",
|
|
141
|
-
"zinkee",
|
|
142
|
-
"--json",
|
|
143
|
-
"schemas",
|
|
144
|
-
"get",
|
|
145
|
-
"550E8400-E29B-41D4-A716-446655440000",
|
|
146
|
-
]);
|
|
147
|
-
|
|
148
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
149
|
-
"https://api.zinkee.test/api/v2/schemas/550e8400-e29b-41d4-a716-446655440000",
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("creates schemas with the expected request body", async () => {
|
|
154
|
-
const configPath = createApiConfig();
|
|
155
|
-
const output = createStdoutBuffer();
|
|
156
|
-
const fetchMock = createFetchMock();
|
|
157
|
-
fetchMock.mockResolvedValueOnce(
|
|
158
|
-
jsonResponse({
|
|
159
|
-
id: "schema-1",
|
|
160
|
-
name: "Contacts",
|
|
161
|
-
slug: "contacts",
|
|
162
|
-
auditable: true,
|
|
163
|
-
commentable: true,
|
|
164
|
-
}),
|
|
165
|
-
);
|
|
166
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
167
|
-
|
|
168
|
-
await program.parseAsync([
|
|
169
|
-
"node",
|
|
170
|
-
"zinkee",
|
|
171
|
-
"--json",
|
|
172
|
-
"schemas",
|
|
173
|
-
"create",
|
|
174
|
-
"--name",
|
|
175
|
-
"Contacts",
|
|
176
|
-
"--slug",
|
|
177
|
-
"contacts",
|
|
178
|
-
"--auditable",
|
|
179
|
-
"--commentable",
|
|
180
|
-
]);
|
|
181
|
-
|
|
182
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
183
|
-
const headers = new Headers(init?.headers);
|
|
184
|
-
|
|
185
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/schemas");
|
|
186
|
-
expect(init?.method).toBe("POST");
|
|
187
|
-
expect(headers.get("content-type")).toBe("application/json");
|
|
188
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
189
|
-
name: "Contacts",
|
|
190
|
-
slug: "contacts",
|
|
191
|
-
auditable: true,
|
|
192
|
-
commentable: true,
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("blocks destructive schema commands in read-only mode", async () => {
|
|
197
|
-
const configPath = createApiConfig();
|
|
198
|
-
const output = createStdoutBuffer();
|
|
199
|
-
const fetchMock = createFetchMock();
|
|
200
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
201
|
-
|
|
202
|
-
await expect(
|
|
203
|
-
program.parseAsync([
|
|
204
|
-
"node",
|
|
205
|
-
"zinkee",
|
|
206
|
-
"--read-only",
|
|
207
|
-
"schemas",
|
|
208
|
-
"delete",
|
|
209
|
-
"contacts",
|
|
210
|
-
]),
|
|
211
|
-
).rejects.toThrowError('Command "schemas delete" is not allowed in read-only mode.');
|
|
212
|
-
|
|
213
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it("sets unique fields through PUT /unique-fields with the expected body", async () => {
|
|
217
|
-
const configPath = createApiConfig();
|
|
218
|
-
const output = createStdoutBuffer();
|
|
219
|
-
const fetchMock = createFetchMock();
|
|
220
|
-
fetchMock.mockResolvedValueOnce(
|
|
221
|
-
jsonResponse({
|
|
222
|
-
id: "2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e",
|
|
223
|
-
uniqueFields: [
|
|
224
|
-
"c60b8526-5835-4c9f-aecd-9a16c7704a34",
|
|
225
|
-
"1d2e3f4a-5b6c-7d8e-9f01-23456789abcd",
|
|
226
|
-
],
|
|
227
|
-
}),
|
|
228
|
-
);
|
|
229
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
230
|
-
|
|
231
|
-
await program.parseAsync([
|
|
232
|
-
"node",
|
|
233
|
-
"zinkee",
|
|
234
|
-
"--json",
|
|
235
|
-
"schemas",
|
|
236
|
-
"set-unique-fields",
|
|
237
|
-
"2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e",
|
|
238
|
-
"--field",
|
|
239
|
-
"c60b8526-5835-4c9f-aecd-9a16c7704a34",
|
|
240
|
-
"--field",
|
|
241
|
-
"1d2e3f4a-5b6c-7d8e-9f01-23456789abcd",
|
|
242
|
-
]);
|
|
243
|
-
|
|
244
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
245
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
246
|
-
"https://api.zinkee.test/api/v2/schemas/2eb8bdcf-6d9c-4fbb-9018-7fa8180f4c5e/unique-fields",
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
250
|
-
expect(init?.method).toBe("PUT");
|
|
251
|
-
expect(new Headers(init?.headers).get("content-type")).toBe("application/json");
|
|
252
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
253
|
-
fieldIds: [
|
|
254
|
-
"c60b8526-5835-4c9f-aecd-9a16c7704a34",
|
|
255
|
-
"1d2e3f4a-5b6c-7d8e-9f01-23456789abcd",
|
|
256
|
-
],
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it("clears the unique constraint with --clear", async () => {
|
|
261
|
-
const configPath = createApiConfig();
|
|
262
|
-
const output = createStdoutBuffer();
|
|
263
|
-
const fetchMock = createFetchMock();
|
|
264
|
-
fetchMock.mockResolvedValueOnce(
|
|
265
|
-
jsonResponse({ id: "550e8400-e29b-41d4-a716-446655440000", uniqueFields: [] }),
|
|
266
|
-
);
|
|
267
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
268
|
-
|
|
269
|
-
await program.parseAsync([
|
|
270
|
-
"node",
|
|
271
|
-
"zinkee",
|
|
272
|
-
"--json",
|
|
273
|
-
"schemas",
|
|
274
|
-
"set-unique-fields",
|
|
275
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
276
|
-
"--clear",
|
|
277
|
-
]);
|
|
278
|
-
|
|
279
|
-
const init = fetchMock.mock.calls[0]?.[1];
|
|
280
|
-
expect(init?.method).toBe("PUT");
|
|
281
|
-
expect(JSON.parse(String(init?.body))).toEqual({ fieldIds: [] });
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it("rejects set-unique-fields without fields or --clear", async () => {
|
|
285
|
-
const configPath = createApiConfig();
|
|
286
|
-
const output = createStdoutBuffer();
|
|
287
|
-
const fetchMock = createFetchMock();
|
|
288
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
289
|
-
|
|
290
|
-
await expect(
|
|
291
|
-
program.parseAsync([
|
|
292
|
-
"node",
|
|
293
|
-
"zinkee",
|
|
294
|
-
"schemas",
|
|
295
|
-
"set-unique-fields",
|
|
296
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
297
|
-
]),
|
|
298
|
-
).rejects.toThrowError(
|
|
299
|
-
'Provide at least one field via "--field", or pass "--clear" to remove the unique constraint.',
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("blocks set-unique-fields in read-only mode", async () => {
|
|
306
|
-
const configPath = createApiConfig();
|
|
307
|
-
const output = createStdoutBuffer();
|
|
308
|
-
const fetchMock = createFetchMock();
|
|
309
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
310
|
-
|
|
311
|
-
await expect(
|
|
312
|
-
program.parseAsync([
|
|
313
|
-
"node",
|
|
314
|
-
"zinkee",
|
|
315
|
-
"--read-only",
|
|
316
|
-
"schemas",
|
|
317
|
-
"set-unique-fields",
|
|
318
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
319
|
-
"--clear",
|
|
320
|
-
]),
|
|
321
|
-
).rejects.toThrowError(
|
|
322
|
-
'Command "schemas set-unique-fields" is not allowed in read-only mode.',
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it("creates fields by merging raw JSON with explicit flags", async () => {
|
|
329
|
-
const configPath = createApiConfig();
|
|
330
|
-
const output = createStdoutBuffer();
|
|
331
|
-
const fetchMock = createFetchMock();
|
|
332
|
-
mockSchemaLookup(fetchMock);
|
|
333
|
-
fetchMock.mockResolvedValueOnce(
|
|
334
|
-
jsonResponse({
|
|
335
|
-
id: "field-1",
|
|
336
|
-
type: "text",
|
|
337
|
-
slug: "email",
|
|
338
|
-
label: "Email",
|
|
339
|
-
required: true,
|
|
340
|
-
settings: {
|
|
341
|
-
maxLength: 255,
|
|
342
|
-
},
|
|
343
|
-
}),
|
|
344
|
-
);
|
|
345
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
346
|
-
|
|
347
|
-
await program.parseAsync([
|
|
348
|
-
"node",
|
|
349
|
-
"zinkee",
|
|
350
|
-
"--json",
|
|
351
|
-
"schemas",
|
|
352
|
-
"fields",
|
|
353
|
-
"create",
|
|
354
|
-
"contacts",
|
|
355
|
-
"--type",
|
|
356
|
-
"text",
|
|
357
|
-
"--slug",
|
|
358
|
-
"email",
|
|
359
|
-
"--label",
|
|
360
|
-
"Email",
|
|
361
|
-
"--required",
|
|
362
|
-
"true",
|
|
363
|
-
"--raw",
|
|
364
|
-
'{"settings":{"maxLength":255},"label":"Wrong","required":false}',
|
|
365
|
-
]);
|
|
366
|
-
|
|
367
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
368
|
-
|
|
369
|
-
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
370
|
-
"https://api.zinkee.test/api/v2/schemas/schema-1/fields",
|
|
371
|
-
);
|
|
372
|
-
expect(init?.method).toBe("POST");
|
|
373
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
374
|
-
settings: {
|
|
375
|
-
maxLength: 255,
|
|
376
|
-
},
|
|
377
|
-
type: "TextField",
|
|
378
|
-
slug: "email",
|
|
379
|
-
label: "Email",
|
|
380
|
-
required: true,
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it("creates fields with a description from the dedicated flag", async () => {
|
|
385
|
-
const configPath = createApiConfig();
|
|
386
|
-
const output = createStdoutBuffer();
|
|
387
|
-
const fetchMock = createFetchMock();
|
|
388
|
-
mockSchemaLookup(fetchMock);
|
|
389
|
-
fetchMock.mockResolvedValueOnce(
|
|
390
|
-
jsonResponse({
|
|
391
|
-
id: "field-1",
|
|
392
|
-
type: "TextField",
|
|
393
|
-
slug: "email",
|
|
394
|
-
label: "Email",
|
|
395
|
-
description: "Primary contact email",
|
|
396
|
-
}),
|
|
397
|
-
);
|
|
398
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
399
|
-
|
|
400
|
-
await program.parseAsync([
|
|
401
|
-
"node",
|
|
402
|
-
"zinkee",
|
|
403
|
-
"--json",
|
|
404
|
-
"schemas",
|
|
405
|
-
"fields",
|
|
406
|
-
"create",
|
|
407
|
-
"contacts",
|
|
408
|
-
"--type",
|
|
409
|
-
"text",
|
|
410
|
-
"--slug",
|
|
411
|
-
"email",
|
|
412
|
-
"--label",
|
|
413
|
-
"Email",
|
|
414
|
-
"--description",
|
|
415
|
-
"Primary contact email",
|
|
416
|
-
]);
|
|
417
|
-
|
|
418
|
-
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
419
|
-
type: "TextField",
|
|
420
|
-
slug: "email",
|
|
421
|
-
label: "Email",
|
|
422
|
-
description: "Primary contact email",
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
it("creates fields with a default value flag", async () => {
|
|
427
|
-
const configPath = createApiConfig();
|
|
428
|
-
const output = createStdoutBuffer();
|
|
429
|
-
const fetchMock = createFetchMock();
|
|
430
|
-
mockSchemaLookup(fetchMock);
|
|
431
|
-
fetchMock.mockResolvedValueOnce(
|
|
432
|
-
jsonResponse({
|
|
433
|
-
id: "field-budget",
|
|
434
|
-
type: "NumberField",
|
|
435
|
-
slug: "budget",
|
|
436
|
-
label: "Budget",
|
|
437
|
-
required: true,
|
|
438
|
-
}),
|
|
439
|
-
);
|
|
440
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
441
|
-
|
|
442
|
-
await program.parseAsync([
|
|
443
|
-
"node",
|
|
444
|
-
"zinkee",
|
|
445
|
-
"--json",
|
|
446
|
-
"schemas",
|
|
447
|
-
"fields",
|
|
448
|
-
"create",
|
|
449
|
-
"contacts",
|
|
450
|
-
"--type",
|
|
451
|
-
"number",
|
|
452
|
-
"--slug",
|
|
453
|
-
"budget",
|
|
454
|
-
"--label",
|
|
455
|
-
"Budget",
|
|
456
|
-
"--required",
|
|
457
|
-
"true",
|
|
458
|
-
"--default",
|
|
459
|
-
"0",
|
|
460
|
-
]);
|
|
461
|
-
|
|
462
|
-
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
463
|
-
type: "NumberField",
|
|
464
|
-
slug: "budget",
|
|
465
|
-
label: "Budget",
|
|
466
|
-
required: true,
|
|
467
|
-
config: {
|
|
468
|
-
defaultValue: "0",
|
|
469
|
-
},
|
|
470
|
-
});
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
it("lets the default value flag override raw config defaults", async () => {
|
|
474
|
-
const configPath = createApiConfig();
|
|
475
|
-
const output = createStdoutBuffer();
|
|
476
|
-
const fetchMock = createFetchMock();
|
|
477
|
-
mockSchemaLookup(fetchMock);
|
|
478
|
-
fetchMock.mockResolvedValueOnce(
|
|
479
|
-
jsonResponse({
|
|
480
|
-
id: "field-budget",
|
|
481
|
-
type: "NumberField",
|
|
482
|
-
slug: "budget",
|
|
483
|
-
label: "Budget",
|
|
484
|
-
}),
|
|
485
|
-
);
|
|
486
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
487
|
-
|
|
488
|
-
await program.parseAsync([
|
|
489
|
-
"node",
|
|
490
|
-
"zinkee",
|
|
491
|
-
"--json",
|
|
492
|
-
"schemas",
|
|
493
|
-
"fields",
|
|
494
|
-
"create",
|
|
495
|
-
"contacts",
|
|
496
|
-
"--raw",
|
|
497
|
-
'{"type":"number","slug":"budget","label":"Budget","config":{"defaultValue":"10","formatOptions":{"format":"number"}}}',
|
|
498
|
-
"--default",
|
|
499
|
-
"0",
|
|
500
|
-
]);
|
|
501
|
-
|
|
502
|
-
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
503
|
-
type: "NumberField",
|
|
504
|
-
slug: "budget",
|
|
505
|
-
label: "Budget",
|
|
506
|
-
config: {
|
|
507
|
-
defaultValue: "0",
|
|
508
|
-
formatOptions: {
|
|
509
|
-
format: "number",
|
|
510
|
-
},
|
|
511
|
-
},
|
|
512
|
-
});
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
it("updates fields from a raw file and normalizes field UUID selectors", async () => {
|
|
516
|
-
const configPath = createApiConfig();
|
|
517
|
-
const output = createStdoutBuffer();
|
|
518
|
-
const fetchMock = createFetchMock();
|
|
519
|
-
const rawFile = join(
|
|
520
|
-
mkdtempSync(join(tmpdir(), "zinkee-field-update-")),
|
|
521
|
-
"field.json",
|
|
522
|
-
);
|
|
523
|
-
writeFileSync(
|
|
524
|
-
rawFile,
|
|
525
|
-
JSON.stringify({
|
|
526
|
-
settings: {
|
|
527
|
-
multiline: true,
|
|
528
|
-
},
|
|
529
|
-
}),
|
|
530
|
-
"utf8",
|
|
531
|
-
);
|
|
532
|
-
mockSchemaLookup(fetchMock);
|
|
533
|
-
fetchMock.mockResolvedValueOnce(
|
|
534
|
-
jsonResponse({
|
|
535
|
-
id: "550e8400-e29b-41d4-a716-446655440001",
|
|
536
|
-
type: "TextField",
|
|
537
|
-
slug: "notes",
|
|
538
|
-
label: "Notes",
|
|
539
|
-
required: false,
|
|
540
|
-
}),
|
|
541
|
-
);
|
|
542
|
-
fetchMock.mockResolvedValueOnce(
|
|
543
|
-
jsonResponse({
|
|
544
|
-
id: "550e8400-e29b-41d4-a716-446655440001",
|
|
545
|
-
slug: "notes",
|
|
546
|
-
label: "Notes",
|
|
547
|
-
required: false,
|
|
548
|
-
settings: {
|
|
549
|
-
multiline: true,
|
|
550
|
-
},
|
|
551
|
-
}),
|
|
552
|
-
);
|
|
553
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
554
|
-
|
|
555
|
-
await program.parseAsync([
|
|
556
|
-
"node",
|
|
557
|
-
"zinkee",
|
|
558
|
-
"--json",
|
|
559
|
-
"schemas",
|
|
560
|
-
"fields",
|
|
561
|
-
"update",
|
|
562
|
-
"contacts",
|
|
563
|
-
"550E8400-E29B-41D4-A716-446655440001",
|
|
564
|
-
"--label",
|
|
565
|
-
"Notes",
|
|
566
|
-
"--required",
|
|
567
|
-
"false",
|
|
568
|
-
"--raw-file",
|
|
569
|
-
rawFile,
|
|
570
|
-
]);
|
|
571
|
-
|
|
572
|
-
const init = fetchMock.mock.calls[2]?.[1];
|
|
573
|
-
|
|
574
|
-
expect(fetchMock.mock.calls[2]?.[0]).toBe(
|
|
575
|
-
"https://api.zinkee.test/api/v2/schemas/schema-1/fields/550e8400-e29b-41d4-a716-446655440001",
|
|
576
|
-
);
|
|
577
|
-
expect(init?.method).toBe("PATCH");
|
|
578
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
579
|
-
settings: {
|
|
580
|
-
multiline: true,
|
|
581
|
-
},
|
|
582
|
-
type: "TextField",
|
|
583
|
-
label: "Notes",
|
|
584
|
-
required: false,
|
|
585
|
-
});
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
it("updates fields with a default value flag", async () => {
|
|
589
|
-
const configPath = createApiConfig();
|
|
590
|
-
const output = createStdoutBuffer();
|
|
591
|
-
const fetchMock = createFetchMock();
|
|
592
|
-
mockSchemaLookup(fetchMock);
|
|
593
|
-
fetchMock.mockResolvedValueOnce(
|
|
594
|
-
jsonResponse({
|
|
595
|
-
id: "field-budget",
|
|
596
|
-
type: "NumberField",
|
|
597
|
-
slug: "budget",
|
|
598
|
-
label: "Budget",
|
|
599
|
-
required: false,
|
|
600
|
-
}),
|
|
601
|
-
);
|
|
602
|
-
fetchMock.mockResolvedValueOnce(
|
|
603
|
-
jsonResponse({
|
|
604
|
-
id: "field-budget",
|
|
605
|
-
type: "NumberField",
|
|
606
|
-
slug: "budget",
|
|
607
|
-
label: "Budget",
|
|
608
|
-
required: false,
|
|
609
|
-
}),
|
|
610
|
-
);
|
|
611
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
612
|
-
|
|
613
|
-
await program.parseAsync([
|
|
614
|
-
"node",
|
|
615
|
-
"zinkee",
|
|
616
|
-
"--json",
|
|
617
|
-
"schemas",
|
|
618
|
-
"fields",
|
|
619
|
-
"update",
|
|
620
|
-
"contacts",
|
|
621
|
-
"550E8400-E29B-41D4-A716-446655440002",
|
|
622
|
-
"--default",
|
|
623
|
-
"0",
|
|
624
|
-
]);
|
|
625
|
-
|
|
626
|
-
expect(JSON.parse(String(fetchMock.mock.calls[2]?.[1]?.body))).toEqual({
|
|
627
|
-
type: "NumberField",
|
|
628
|
-
config: {
|
|
629
|
-
defaultValue: "0",
|
|
630
|
-
},
|
|
631
|
-
});
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
it.each([
|
|
635
|
-
["sets", "Shown in customer notifications"],
|
|
636
|
-
["clears", ""],
|
|
637
|
-
])("%s a field description without another metadata change", async (_action, description) => {
|
|
638
|
-
const configPath = createApiConfig();
|
|
639
|
-
const output = createStdoutBuffer();
|
|
640
|
-
const fetchMock = createFetchMock();
|
|
641
|
-
mockSchemaLookup(fetchMock);
|
|
642
|
-
fetchMock.mockResolvedValueOnce(
|
|
643
|
-
jsonResponse({
|
|
644
|
-
id: "550e8400-e29b-41d4-a716-446655440003",
|
|
645
|
-
type: "TextField",
|
|
646
|
-
slug: "notes",
|
|
647
|
-
label: "Notes",
|
|
648
|
-
required: false,
|
|
649
|
-
}),
|
|
650
|
-
);
|
|
651
|
-
fetchMock.mockResolvedValueOnce(
|
|
652
|
-
jsonResponse({
|
|
653
|
-
id: "550e8400-e29b-41d4-a716-446655440003",
|
|
654
|
-
type: "TextField",
|
|
655
|
-
slug: "notes",
|
|
656
|
-
label: "Notes",
|
|
657
|
-
description,
|
|
658
|
-
}),
|
|
659
|
-
);
|
|
660
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
661
|
-
|
|
662
|
-
await program.parseAsync([
|
|
663
|
-
"node",
|
|
664
|
-
"zinkee",
|
|
665
|
-
"--json",
|
|
666
|
-
"schemas",
|
|
667
|
-
"fields",
|
|
668
|
-
"update",
|
|
669
|
-
"contacts",
|
|
670
|
-
"550E8400-E29B-41D4-A716-446655440003",
|
|
671
|
-
"--description",
|
|
672
|
-
description,
|
|
673
|
-
]);
|
|
674
|
-
|
|
675
|
-
expect(JSON.parse(String(fetchMock.mock.calls[2]?.[1]?.body))).toEqual({
|
|
676
|
-
type: "TextField",
|
|
677
|
-
description,
|
|
678
|
-
});
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
it("strips formula status from update payloads", async () => {
|
|
682
|
-
const configPath = createApiConfig();
|
|
683
|
-
const output = createStdoutBuffer();
|
|
684
|
-
const fetchMock = createFetchMock();
|
|
685
|
-
mockSchemaLookup(fetchMock);
|
|
686
|
-
fetchMock.mockResolvedValueOnce(
|
|
687
|
-
jsonResponse({
|
|
688
|
-
id: "550e8400-e29b-41d4-a716-446655440002",
|
|
689
|
-
type: "FormulaField",
|
|
690
|
-
slug: "total",
|
|
691
|
-
label: "Total",
|
|
692
|
-
required: false,
|
|
693
|
-
}),
|
|
694
|
-
);
|
|
695
|
-
fetchMock.mockResolvedValueOnce(
|
|
696
|
-
jsonResponse({
|
|
697
|
-
id: "550e8400-e29b-41d4-a716-446655440002",
|
|
698
|
-
type: "FormulaField",
|
|
699
|
-
slug: "total",
|
|
700
|
-
label: "Total",
|
|
701
|
-
required: false,
|
|
702
|
-
}),
|
|
703
|
-
);
|
|
704
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
705
|
-
|
|
706
|
-
await program.parseAsync([
|
|
707
|
-
"node",
|
|
708
|
-
"zinkee",
|
|
709
|
-
"--json",
|
|
710
|
-
"schemas",
|
|
711
|
-
"fields",
|
|
712
|
-
"update",
|
|
713
|
-
"contacts",
|
|
714
|
-
"550E8400-E29B-41D4-A716-446655440002",
|
|
715
|
-
"--raw",
|
|
716
|
-
'{"status":"draft","config":{"formulaText":"{price} * {quantity}","status":"active","uiFormatKind":"number"}}',
|
|
717
|
-
]);
|
|
718
|
-
|
|
719
|
-
expect(JSON.parse(String(fetchMock.mock.calls[2]?.[1]?.body))).toEqual({
|
|
720
|
-
type: "FormulaField",
|
|
721
|
-
config: {
|
|
722
|
-
formulaText: "{price} * {quantity}",
|
|
723
|
-
uiFormatKind: "number",
|
|
724
|
-
},
|
|
725
|
-
});
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
it("creates fields from a complete raw payload without duplicating required flags", async () => {
|
|
729
|
-
const configPath = createApiConfig();
|
|
730
|
-
const output = createStdoutBuffer();
|
|
731
|
-
const fetchMock = createFetchMock();
|
|
732
|
-
mockSchemaLookup(fetchMock);
|
|
733
|
-
fetchMock.mockResolvedValueOnce(
|
|
734
|
-
jsonResponse({
|
|
735
|
-
id: "field-2",
|
|
736
|
-
type: "TextField",
|
|
737
|
-
slug: "phone",
|
|
738
|
-
label: "Phone",
|
|
739
|
-
required: true,
|
|
740
|
-
}),
|
|
741
|
-
);
|
|
742
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
743
|
-
|
|
744
|
-
await program.parseAsync([
|
|
745
|
-
"node",
|
|
746
|
-
"zinkee",
|
|
747
|
-
"--json",
|
|
748
|
-
"schemas",
|
|
749
|
-
"fields",
|
|
750
|
-
"create",
|
|
751
|
-
"contacts",
|
|
752
|
-
"--raw",
|
|
753
|
-
'{"type":"text","slug":"phone","label":"Phone","required":true}',
|
|
754
|
-
]);
|
|
755
|
-
|
|
756
|
-
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
757
|
-
"https://api.zinkee.test/api/v2/schemas/schema-1/fields",
|
|
758
|
-
);
|
|
759
|
-
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
760
|
-
type: "TextField",
|
|
761
|
-
slug: "phone",
|
|
762
|
-
label: "Phone",
|
|
763
|
-
required: true,
|
|
764
|
-
});
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
it("strips formula status from create payloads", async () => {
|
|
768
|
-
const configPath = createApiConfig();
|
|
769
|
-
const output = createStdoutBuffer();
|
|
770
|
-
const fetchMock = createFetchMock();
|
|
771
|
-
mockSchemaLookup(fetchMock);
|
|
772
|
-
fetchMock.mockResolvedValueOnce(
|
|
773
|
-
jsonResponse({
|
|
774
|
-
id: "field-formula-1",
|
|
775
|
-
type: "FormulaField",
|
|
776
|
-
slug: "total",
|
|
777
|
-
label: "Total",
|
|
778
|
-
}),
|
|
779
|
-
);
|
|
780
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
781
|
-
|
|
782
|
-
await program.parseAsync([
|
|
783
|
-
"node",
|
|
784
|
-
"zinkee",
|
|
785
|
-
"--json",
|
|
786
|
-
"schemas",
|
|
787
|
-
"fields",
|
|
788
|
-
"create",
|
|
789
|
-
"contacts",
|
|
790
|
-
"--raw",
|
|
791
|
-
'{"type":"formula","slug":"total","label":"Total","status":"draft","config":{"formulaText":"{price} * {quantity}","status":"active","uiFormatKind":"number"}}',
|
|
792
|
-
]);
|
|
793
|
-
|
|
794
|
-
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
795
|
-
type: "FormulaField",
|
|
796
|
-
slug: "total",
|
|
797
|
-
label: "Total",
|
|
798
|
-
config: {
|
|
799
|
-
formulaText: "{price} * {quantity}",
|
|
800
|
-
uiFormatKind: "number",
|
|
801
|
-
},
|
|
802
|
-
});
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
it("normalizes autoincremental field aliases to AutoIncrementalField", async () => {
|
|
806
|
-
const configPath = createApiConfig();
|
|
807
|
-
const output = createStdoutBuffer();
|
|
808
|
-
const fetchMock = createFetchMock();
|
|
809
|
-
mockSchemaLookup(fetchMock);
|
|
810
|
-
fetchMock.mockResolvedValueOnce(
|
|
811
|
-
jsonResponse({
|
|
812
|
-
id: "field-3",
|
|
813
|
-
type: "AutoIncrementalField",
|
|
814
|
-
slug: "counter",
|
|
815
|
-
label: "Counter",
|
|
816
|
-
}),
|
|
817
|
-
);
|
|
818
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
819
|
-
|
|
820
|
-
await program.parseAsync([
|
|
821
|
-
"node",
|
|
822
|
-
"zinkee",
|
|
823
|
-
"--json",
|
|
824
|
-
"schemas",
|
|
825
|
-
"fields",
|
|
826
|
-
"create",
|
|
827
|
-
"contacts",
|
|
828
|
-
"--type",
|
|
829
|
-
"autoincremental",
|
|
830
|
-
"--slug",
|
|
831
|
-
"counter",
|
|
832
|
-
"--label",
|
|
833
|
-
"Counter",
|
|
834
|
-
]);
|
|
835
|
-
|
|
836
|
-
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
837
|
-
type: "AutoIncrementalField",
|
|
838
|
-
slug: "counter",
|
|
839
|
-
label: "Counter",
|
|
840
|
-
});
|
|
841
|
-
});
|
|
842
|
-
|
|
843
|
-
it("normalizes member field aliases to MemberField", async () => {
|
|
844
|
-
const configPath = createApiConfig();
|
|
845
|
-
const output = createStdoutBuffer();
|
|
846
|
-
const fetchMock = createFetchMock();
|
|
847
|
-
mockSchemaLookup(fetchMock);
|
|
848
|
-
fetchMock.mockResolvedValueOnce(
|
|
849
|
-
jsonResponse({
|
|
850
|
-
id: "field-member-1",
|
|
851
|
-
type: "MemberField",
|
|
852
|
-
slug: "owner",
|
|
853
|
-
label: "Owner",
|
|
854
|
-
}),
|
|
855
|
-
);
|
|
856
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
857
|
-
|
|
858
|
-
await program.parseAsync([
|
|
859
|
-
"node",
|
|
860
|
-
"zinkee",
|
|
861
|
-
"--json",
|
|
862
|
-
"schemas",
|
|
863
|
-
"fields",
|
|
864
|
-
"create",
|
|
865
|
-
"contacts",
|
|
866
|
-
"--type",
|
|
867
|
-
"member",
|
|
868
|
-
"--slug",
|
|
869
|
-
"owner",
|
|
870
|
-
"--label",
|
|
871
|
-
"Owner",
|
|
872
|
-
]);
|
|
873
|
-
|
|
874
|
-
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
875
|
-
type: "MemberField",
|
|
876
|
-
slug: "owner",
|
|
877
|
-
label: "Owner",
|
|
878
|
-
});
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
it("normalizes option field shorthand payloads into the backend shape", async () => {
|
|
882
|
-
const configPath = createApiConfig();
|
|
883
|
-
const output = createStdoutBuffer();
|
|
884
|
-
const fetchMock = createFetchMock();
|
|
885
|
-
mockSchemaLookup(fetchMock);
|
|
886
|
-
fetchMock.mockResolvedValueOnce(
|
|
887
|
-
jsonResponse({
|
|
888
|
-
id: "field-4",
|
|
889
|
-
type: "OptionField",
|
|
890
|
-
slug: "status",
|
|
891
|
-
label: "Status",
|
|
892
|
-
}),
|
|
893
|
-
);
|
|
894
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
895
|
-
|
|
896
|
-
await program.parseAsync([
|
|
897
|
-
"node",
|
|
898
|
-
"zinkee",
|
|
899
|
-
"--json",
|
|
900
|
-
"schemas",
|
|
901
|
-
"fields",
|
|
902
|
-
"create",
|
|
903
|
-
"contacts",
|
|
904
|
-
"--raw",
|
|
905
|
-
'{"type":"option","slug":"status","label":"Status","options":[{"id":1,"label":"Open"},{"id":2,"label":"Closed","color":"success"}]}',
|
|
906
|
-
]);
|
|
907
|
-
|
|
908
|
-
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
909
|
-
type: "OptionField",
|
|
910
|
-
slug: "status",
|
|
911
|
-
label: "Status",
|
|
912
|
-
config: {
|
|
913
|
-
options: [
|
|
914
|
-
{
|
|
915
|
-
id: 1,
|
|
916
|
-
name: "Open",
|
|
917
|
-
color: "default",
|
|
918
|
-
},
|
|
919
|
-
{
|
|
920
|
-
id: 2,
|
|
921
|
-
name: "Closed",
|
|
922
|
-
color: "success",
|
|
923
|
-
},
|
|
924
|
-
],
|
|
925
|
-
},
|
|
926
|
-
});
|
|
927
|
-
});
|
|
928
|
-
|
|
929
|
-
it("normalizes schedule field shorthand payloads into the backend shape", async () => {
|
|
930
|
-
const configPath = createApiConfig();
|
|
931
|
-
const output = createStdoutBuffer();
|
|
932
|
-
const fetchMock = createFetchMock();
|
|
933
|
-
mockSchemaLookup(fetchMock);
|
|
934
|
-
fetchMock.mockResolvedValueOnce(
|
|
935
|
-
jsonResponse({
|
|
936
|
-
id: "field-5",
|
|
937
|
-
type: "ScheduleField",
|
|
938
|
-
slug: "monthly-recurrence",
|
|
939
|
-
label: "Monthly Recurrence",
|
|
940
|
-
}),
|
|
941
|
-
);
|
|
942
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
943
|
-
|
|
944
|
-
await program.parseAsync([
|
|
945
|
-
"node",
|
|
946
|
-
"zinkee",
|
|
947
|
-
"--json",
|
|
948
|
-
"schemas",
|
|
949
|
-
"fields",
|
|
950
|
-
"create",
|
|
951
|
-
"contacts",
|
|
952
|
-
"--raw",
|
|
953
|
-
'{"type":"schedule","slug":"monthly-recurrence","label":"Monthly Recurrence","referenceField":"c52efe3f-2825-4c60-a7e3-34dd4c1ce043","beginningPeriodField":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90","endingPeriodField":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90","frequency":"monthly"}',
|
|
954
|
-
]);
|
|
955
|
-
|
|
956
|
-
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
957
|
-
type: "ScheduleField",
|
|
958
|
-
slug: "monthly-recurrence",
|
|
959
|
-
label: "Monthly Recurrence",
|
|
960
|
-
config: {
|
|
961
|
-
referenceField: "c52efe3f-2825-4c60-a7e3-34dd4c1ce043",
|
|
962
|
-
beginningPeriodField: "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90",
|
|
963
|
-
endingPeriodField: "d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90",
|
|
964
|
-
frequency: "monthly",
|
|
965
|
-
},
|
|
966
|
-
});
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
it("rejects schedule fields with invalid frequency before hitting the backend", async () => {
|
|
970
|
-
const configPath = createApiConfig();
|
|
971
|
-
const output = createStdoutBuffer();
|
|
972
|
-
const fetchMock = createFetchMock();
|
|
973
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
974
|
-
|
|
975
|
-
await expect(
|
|
976
|
-
program.parseAsync([
|
|
977
|
-
"node",
|
|
978
|
-
"zinkee",
|
|
979
|
-
"schemas",
|
|
980
|
-
"fields",
|
|
981
|
-
"create",
|
|
982
|
-
"contacts",
|
|
983
|
-
"--raw",
|
|
984
|
-
'{"type":"ScheduleField","slug":"weekly","label":"Weekly","config":{"referenceField":"c52efe3f-2825-4c60-a7e3-34dd4c1ce043","beginningPeriodField":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90","endingPeriodField":"d4e5f6a7-b8c9-0d1e-2f3a-4b5c6d7e8f90","frequency":"weekly"}}',
|
|
985
|
-
]),
|
|
986
|
-
).rejects.toThrowError(
|
|
987
|
-
'Schedule "frequency" must be "monthly" or "daily" (received "weekly").',
|
|
988
|
-
);
|
|
989
|
-
|
|
990
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
991
|
-
});
|
|
992
|
-
|
|
993
|
-
it("rejects schedule fields missing required config fields", async () => {
|
|
994
|
-
const configPath = createApiConfig();
|
|
995
|
-
const output = createStdoutBuffer();
|
|
996
|
-
const fetchMock = createFetchMock();
|
|
997
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
998
|
-
|
|
999
|
-
await expect(
|
|
1000
|
-
program.parseAsync([
|
|
1001
|
-
"node",
|
|
1002
|
-
"zinkee",
|
|
1003
|
-
"schemas",
|
|
1004
|
-
"fields",
|
|
1005
|
-
"create",
|
|
1006
|
-
"contacts",
|
|
1007
|
-
"--raw",
|
|
1008
|
-
'{"type":"ScheduleField","slug":"recurrence","label":"Recurrence","config":{"frequency":"daily"}}',
|
|
1009
|
-
]),
|
|
1010
|
-
).rejects.toThrowError(
|
|
1011
|
-
/Schedule fields require "referenceField", "beginningPeriodField", "endingPeriodField" and "frequency"/,
|
|
1012
|
-
);
|
|
1013
|
-
|
|
1014
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
1015
|
-
});
|
|
1016
|
-
|
|
1017
|
-
it("rejects unsupported field types before making network requests", async () => {
|
|
1018
|
-
const configPath = createApiConfig();
|
|
1019
|
-
const output = createStdoutBuffer();
|
|
1020
|
-
const fetchMock = createFetchMock();
|
|
1021
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
1022
|
-
|
|
1023
|
-
await expect(
|
|
1024
|
-
program.parseAsync([
|
|
1025
|
-
"node",
|
|
1026
|
-
"zinkee",
|
|
1027
|
-
"schemas",
|
|
1028
|
-
"fields",
|
|
1029
|
-
"create",
|
|
1030
|
-
"contacts",
|
|
1031
|
-
"--type",
|
|
1032
|
-
"invalid",
|
|
1033
|
-
"--slug",
|
|
1034
|
-
"bad",
|
|
1035
|
-
"--label",
|
|
1036
|
-
"Bad",
|
|
1037
|
-
]),
|
|
1038
|
-
).rejects.toThrowError(
|
|
1039
|
-
'Unsupported field type "InvalidField". Valid types: text, number, date, option, file, reference, formula, member, autoincremental, schedule.',
|
|
1040
|
-
);
|
|
1041
|
-
|
|
1042
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
1043
|
-
});
|
|
1044
|
-
|
|
1045
|
-
it("shows valid types and raw shapes in schemas fields create help", () => {
|
|
1046
|
-
const program = buildProgram();
|
|
1047
|
-
const schemas = program.commands.find((command) => command.name() === "schemas");
|
|
1048
|
-
const fields = schemas?.commands.find((command) => command.name() === "fields");
|
|
1049
|
-
const create = fields?.commands.find((command) => command.name() === "create");
|
|
1050
|
-
const help = create?.helpInformation() ?? "";
|
|
1051
|
-
|
|
1052
|
-
expect(help).toContain("--type <type>");
|
|
1053
|
-
expect(help).toContain("--default <value>");
|
|
1054
|
-
expect(help).toContain("member");
|
|
1055
|
-
expect(help).toContain("autoincremental");
|
|
1056
|
-
expect(help).toContain("Raw payload shapes by type:");
|
|
1057
|
-
expect(help).toContain('"type":"NumberField","slug":"quantity"');
|
|
1058
|
-
expect(help).toContain('"defaultValue":"0"');
|
|
1059
|
-
expect(help).toContain('- member: Workspace member field.');
|
|
1060
|
-
expect(help).toContain('"type":"MemberField","slug":"owner","label":"Owner"');
|
|
1061
|
-
expect(help).toContain('- option: Dropdown/select field with options.');
|
|
1062
|
-
expect(help).toContain('"type":"OptionField","slug":"status","label":"Status"');
|
|
1063
|
-
expect(help).toContain('Use "zinkee --example schemas fields create --type <type>"');
|
|
1064
|
-
});
|
|
1065
|
-
|
|
1066
|
-
it("surfaces a descriptive option payload error before hitting the backend", async () => {
|
|
1067
|
-
const configPath = createApiConfig();
|
|
1068
|
-
const output = createStdoutBuffer();
|
|
1069
|
-
const fetchMock = createFetchMock();
|
|
1070
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
1071
|
-
|
|
1072
|
-
await expect(
|
|
1073
|
-
program.parseAsync([
|
|
1074
|
-
"node",
|
|
1075
|
-
"zinkee",
|
|
1076
|
-
"schemas",
|
|
1077
|
-
"fields",
|
|
1078
|
-
"create",
|
|
1079
|
-
"contacts",
|
|
1080
|
-
"--raw",
|
|
1081
|
-
'{"type":"option","slug":"status","label":"Status","options":[1,2]}',
|
|
1082
|
-
]),
|
|
1083
|
-
).rejects.toThrowError(
|
|
1084
|
-
'Option fields require an "options" array with at least one entry like {"id":1,"name":"Open","color":"default"}.',
|
|
1085
|
-
);
|
|
1086
|
-
|
|
1087
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
1088
|
-
});
|
|
1089
|
-
|
|
1090
|
-
it("enriches generic backend validation errors with the expected raw shape", async () => {
|
|
1091
|
-
const configPath = createApiConfig();
|
|
1092
|
-
const output = createStdoutBuffer();
|
|
1093
|
-
const fetchMock = createFetchMock();
|
|
1094
|
-
mockSchemaLookup(fetchMock);
|
|
1095
|
-
fetchMock.mockResolvedValueOnce(
|
|
1096
|
-
jsonResponse(
|
|
1097
|
-
{
|
|
1098
|
-
error: {
|
|
1099
|
-
message: "Schema payload is invalid. One or more fields are invalid.",
|
|
1100
|
-
},
|
|
1101
|
-
},
|
|
1102
|
-
400,
|
|
1103
|
-
),
|
|
1104
|
-
);
|
|
1105
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
1106
|
-
|
|
1107
|
-
await expect(
|
|
1108
|
-
program.parseAsync([
|
|
1109
|
-
"node",
|
|
1110
|
-
"zinkee",
|
|
1111
|
-
"schemas",
|
|
1112
|
-
"fields",
|
|
1113
|
-
"create",
|
|
1114
|
-
"contacts",
|
|
1115
|
-
"--raw",
|
|
1116
|
-
'{"type":"option","slug":"status","label":"Status","options":[{"id":1,"name":"Open","color":"default"}]}',
|
|
1117
|
-
]),
|
|
1118
|
-
).rejects.toThrowError(
|
|
1119
|
-
'Invalid payload for field type "option". Expected raw shape: {"type":"OptionField","slug":"status","label":"Status","config":{"options":[{"id":1,"name":"Open","color":"default"},{"id":2,"name":"Closed","color":"success"}]}}',
|
|
1120
|
-
);
|
|
1121
|
-
});
|
|
1122
|
-
|
|
1123
|
-
it("deletes fields by slug after resolving the field id", async () => {
|
|
1124
|
-
const configPath = createApiConfig();
|
|
1125
|
-
const output = createStdoutBuffer();
|
|
1126
|
-
const fetchMock = createFetchMock();
|
|
1127
|
-
mockSchemaLookup(fetchMock);
|
|
1128
|
-
fetchMock.mockResolvedValueOnce(
|
|
1129
|
-
jsonResponse({
|
|
1130
|
-
id: "schema-1",
|
|
1131
|
-
slug: "contacts",
|
|
1132
|
-
fields: [
|
|
1133
|
-
{
|
|
1134
|
-
id: "550e8400-e29b-41d4-a716-446655440001",
|
|
1135
|
-
slug: "email",
|
|
1136
|
-
type: "TextField",
|
|
1137
|
-
label: "Email",
|
|
1138
|
-
},
|
|
1139
|
-
],
|
|
1140
|
-
}),
|
|
1141
|
-
);
|
|
1142
|
-
fetchMock.mockResolvedValueOnce(jsonResponse({ deleted: true }));
|
|
1143
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
1144
|
-
|
|
1145
|
-
await program.parseAsync([
|
|
1146
|
-
"node",
|
|
1147
|
-
"zinkee",
|
|
1148
|
-
"--json",
|
|
1149
|
-
"schemas",
|
|
1150
|
-
"fields",
|
|
1151
|
-
"delete",
|
|
1152
|
-
"contacts",
|
|
1153
|
-
"email",
|
|
1154
|
-
]);
|
|
1155
|
-
|
|
1156
|
-
expect(fetchMock.mock.calls[2]?.[0]).toBe(
|
|
1157
|
-
"https://api.zinkee.test/api/v2/schemas/schema-1/fields/550e8400-e29b-41d4-a716-446655440001",
|
|
1158
|
-
);
|
|
1159
|
-
expect(fetchMock.mock.calls[2]?.[1]?.method).toBe("DELETE");
|
|
1160
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
1161
|
-
data: {
|
|
1162
|
-
schema: "schema-1",
|
|
1163
|
-
field: "550e8400-e29b-41d4-a716-446655440001",
|
|
1164
|
-
deleted: true,
|
|
1165
|
-
},
|
|
1166
|
-
meta: {
|
|
1167
|
-
command: "schemas fields delete",
|
|
1168
|
-
configPath,
|
|
1169
|
-
},
|
|
1170
|
-
});
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
it("updates a schema highlighted field by slug after resolving the field id", async () => {
|
|
1174
|
-
const configPath = createApiConfig();
|
|
1175
|
-
const output = createStdoutBuffer();
|
|
1176
|
-
const fetchMock = createFetchMock();
|
|
1177
|
-
mockSchemaLookup(fetchMock);
|
|
1178
|
-
fetchMock.mockResolvedValueOnce(
|
|
1179
|
-
jsonResponse({
|
|
1180
|
-
id: "schema-1",
|
|
1181
|
-
slug: "contacts",
|
|
1182
|
-
fields: [
|
|
1183
|
-
{
|
|
1184
|
-
id: "550e8400-e29b-41d4-a716-446655440001",
|
|
1185
|
-
slug: "email",
|
|
1186
|
-
type: "TextField",
|
|
1187
|
-
label: "Email",
|
|
1188
|
-
},
|
|
1189
|
-
],
|
|
1190
|
-
}),
|
|
1191
|
-
);
|
|
1192
|
-
fetchMock.mockResolvedValueOnce(
|
|
1193
|
-
jsonResponse({
|
|
1194
|
-
id: "schema-1",
|
|
1195
|
-
slug: "contacts",
|
|
1196
|
-
highlightedField: "550e8400-e29b-41d4-a716-446655440001",
|
|
1197
|
-
}),
|
|
1198
|
-
);
|
|
1199
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
1200
|
-
|
|
1201
|
-
await program.parseAsync([
|
|
1202
|
-
"node",
|
|
1203
|
-
"zinkee",
|
|
1204
|
-
"--json",
|
|
1205
|
-
"schemas",
|
|
1206
|
-
"update",
|
|
1207
|
-
"contacts",
|
|
1208
|
-
"--highlighted-field",
|
|
1209
|
-
"email",
|
|
1210
|
-
]);
|
|
1211
|
-
|
|
1212
|
-
expect(fetchMock.mock.calls[2]?.[0]).toBe("https://api.zinkee.test/api/v2/schemas/schema-1");
|
|
1213
|
-
expect(fetchMock.mock.calls[2]?.[1]?.method).toBe("PATCH");
|
|
1214
|
-
expect(JSON.parse(String(fetchMock.mock.calls[2]?.[1]?.body))).toEqual({
|
|
1215
|
-
highlightedField: "550e8400-e29b-41d4-a716-446655440001",
|
|
1216
|
-
});
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
it("updates a schema highlighted field by uuid without extra lookups", async () => {
|
|
1220
|
-
const configPath = createApiConfig();
|
|
1221
|
-
const output = createStdoutBuffer();
|
|
1222
|
-
const fetchMock = createFetchMock();
|
|
1223
|
-
fetchMock.mockResolvedValueOnce(
|
|
1224
|
-
jsonResponse({
|
|
1225
|
-
id: "550e8400-e29b-41d4-a716-446655440000",
|
|
1226
|
-
slug: "contacts",
|
|
1227
|
-
highlightedField: "550e8400-e29b-41d4-a716-446655440001",
|
|
1228
|
-
}),
|
|
1229
|
-
);
|
|
1230
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
1231
|
-
|
|
1232
|
-
await program.parseAsync([
|
|
1233
|
-
"node",
|
|
1234
|
-
"zinkee",
|
|
1235
|
-
"--json",
|
|
1236
|
-
"schemas",
|
|
1237
|
-
"update",
|
|
1238
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
1239
|
-
"--highlighted-field",
|
|
1240
|
-
"550e8400-e29b-41d4-a716-446655440001",
|
|
1241
|
-
]);
|
|
1242
|
-
|
|
1243
|
-
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
1244
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
1245
|
-
"https://api.zinkee.test/api/v2/schemas/550e8400-e29b-41d4-a716-446655440000",
|
|
1246
|
-
);
|
|
1247
|
-
expect(fetchMock.mock.calls[0]?.[1]?.method).toBe("PATCH");
|
|
1248
|
-
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toEqual({
|
|
1249
|
-
highlightedField: "550e8400-e29b-41d4-a716-446655440001",
|
|
1250
|
-
});
|
|
1251
|
-
});
|
|
1252
|
-
});
|