zinkee 0.1.0
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 +46 -0
- package/README.md +395 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +7142 -0
- package/dist/index.js.map +1 -0
- package/package.json +32 -0
- package/src/api/automations.ts +403 -0
- package/src/api/comments.ts +51 -0
- package/src/api/displays.ts +276 -0
- package/src/api/files.ts +50 -0
- package/src/api/navigation.ts +71 -0
- package/src/api/records.ts +167 -0
- package/src/api/schemas.ts +130 -0
- package/src/api/teamspace.ts +110 -0
- package/src/cli-examples.ts +175 -0
- package/src/cli-runner.ts +73 -0
- package/src/client.test.ts +128 -0
- package/src/client.ts +243 -0
- package/src/command-registry.ts +749 -0
- package/src/commands/automations.test.ts +728 -0
- package/src/commands/automations.ts +2013 -0
- package/src/commands/comments.test.ts +214 -0
- package/src/commands/comments.ts +303 -0
- package/src/commands/config.test.ts +81 -0
- package/src/commands/config.ts +150 -0
- package/src/commands/displays.test.ts +266 -0
- package/src/commands/displays.ts +755 -0
- package/src/commands/files.test.ts +284 -0
- package/src/commands/files.ts +280 -0
- package/src/commands/navigation.test.ts +214 -0
- package/src/commands/navigation.ts +348 -0
- package/src/commands/profiles.test.ts +82 -0
- package/src/commands/profiles.ts +281 -0
- package/src/commands/records.test.ts +367 -0
- package/src/commands/records.ts +726 -0
- package/src/commands/schemas.test.ts +748 -0
- package/src/commands/schemas.ts +746 -0
- package/src/commands/teamspace.test.ts +229 -0
- package/src/commands/teamspace.ts +540 -0
- package/src/config.test.ts +165 -0
- package/src/config.ts +248 -0
- package/src/index.test.ts +140 -0
- package/src/index.ts +8 -0
- package/src/parsers/expressions.test.ts +50 -0
- package/src/parsers/expressions.ts +111 -0
- package/src/parsers/kv.test.ts +35 -0
- package/src/parsers/kv.ts +49 -0
- package/src/parsers/selectors.test.ts +23 -0
- package/src/parsers/selectors.ts +29 -0
- package/src/program.ts +56 -0
- package/src/runtime-context.ts +18 -0
- package/src/types.ts +71 -0
- package/src/utils/errors.test.ts +48 -0
- package/src/utils/errors.ts +183 -0
- package/src/utils/examples.test.ts +67 -0
- package/src/utils/examples.ts +855 -0
- package/src/utils/output.test.ts +39 -0
- package/src/utils/output.ts +124 -0
- package/src/utils/schema-fields.ts +529 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +13 -0
|
@@ -0,0 +1,748 @@
|
|
|
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("creates fields by merging raw JSON with explicit flags", async () => {
|
|
217
|
+
const configPath = createApiConfig();
|
|
218
|
+
const output = createStdoutBuffer();
|
|
219
|
+
const fetchMock = createFetchMock();
|
|
220
|
+
mockSchemaLookup(fetchMock);
|
|
221
|
+
fetchMock.mockResolvedValueOnce(
|
|
222
|
+
jsonResponse({
|
|
223
|
+
id: "field-1",
|
|
224
|
+
type: "text",
|
|
225
|
+
slug: "email",
|
|
226
|
+
label: "Email",
|
|
227
|
+
required: true,
|
|
228
|
+
settings: {
|
|
229
|
+
maxLength: 255,
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
234
|
+
|
|
235
|
+
await program.parseAsync([
|
|
236
|
+
"node",
|
|
237
|
+
"zinkee",
|
|
238
|
+
"--json",
|
|
239
|
+
"schemas",
|
|
240
|
+
"fields",
|
|
241
|
+
"create",
|
|
242
|
+
"contacts",
|
|
243
|
+
"--type",
|
|
244
|
+
"text",
|
|
245
|
+
"--slug",
|
|
246
|
+
"email",
|
|
247
|
+
"--label",
|
|
248
|
+
"Email",
|
|
249
|
+
"--required",
|
|
250
|
+
"true",
|
|
251
|
+
"--raw",
|
|
252
|
+
'{"settings":{"maxLength":255},"label":"Wrong","required":false}',
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
const init = fetchMock.mock.calls[1]?.[1];
|
|
256
|
+
|
|
257
|
+
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
258
|
+
"https://api.zinkee.test/api/v2/schemas/schema-1/fields",
|
|
259
|
+
);
|
|
260
|
+
expect(init?.method).toBe("POST");
|
|
261
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
262
|
+
settings: {
|
|
263
|
+
maxLength: 255,
|
|
264
|
+
},
|
|
265
|
+
type: "TextField",
|
|
266
|
+
slug: "email",
|
|
267
|
+
label: "Email",
|
|
268
|
+
required: true,
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("updates fields from a raw file and normalizes field UUID selectors", async () => {
|
|
273
|
+
const configPath = createApiConfig();
|
|
274
|
+
const output = createStdoutBuffer();
|
|
275
|
+
const fetchMock = createFetchMock();
|
|
276
|
+
const rawFile = join(
|
|
277
|
+
mkdtempSync(join(tmpdir(), "zinkee-field-update-")),
|
|
278
|
+
"field.json",
|
|
279
|
+
);
|
|
280
|
+
writeFileSync(
|
|
281
|
+
rawFile,
|
|
282
|
+
JSON.stringify({
|
|
283
|
+
settings: {
|
|
284
|
+
multiline: true,
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
287
|
+
"utf8",
|
|
288
|
+
);
|
|
289
|
+
mockSchemaLookup(fetchMock);
|
|
290
|
+
fetchMock.mockResolvedValueOnce(
|
|
291
|
+
jsonResponse({
|
|
292
|
+
id: "550e8400-e29b-41d4-a716-446655440001",
|
|
293
|
+
type: "TextField",
|
|
294
|
+
slug: "notes",
|
|
295
|
+
label: "Notes",
|
|
296
|
+
required: false,
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
fetchMock.mockResolvedValueOnce(
|
|
300
|
+
jsonResponse({
|
|
301
|
+
id: "550e8400-e29b-41d4-a716-446655440001",
|
|
302
|
+
slug: "notes",
|
|
303
|
+
label: "Notes",
|
|
304
|
+
required: false,
|
|
305
|
+
settings: {
|
|
306
|
+
multiline: true,
|
|
307
|
+
},
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
311
|
+
|
|
312
|
+
await program.parseAsync([
|
|
313
|
+
"node",
|
|
314
|
+
"zinkee",
|
|
315
|
+
"--json",
|
|
316
|
+
"schemas",
|
|
317
|
+
"fields",
|
|
318
|
+
"update",
|
|
319
|
+
"contacts",
|
|
320
|
+
"550E8400-E29B-41D4-A716-446655440001",
|
|
321
|
+
"--label",
|
|
322
|
+
"Notes",
|
|
323
|
+
"--required",
|
|
324
|
+
"false",
|
|
325
|
+
"--raw-file",
|
|
326
|
+
rawFile,
|
|
327
|
+
]);
|
|
328
|
+
|
|
329
|
+
const init = fetchMock.mock.calls[2]?.[1];
|
|
330
|
+
|
|
331
|
+
expect(fetchMock.mock.calls[2]?.[0]).toBe(
|
|
332
|
+
"https://api.zinkee.test/api/v2/schemas/schema-1/fields/550e8400-e29b-41d4-a716-446655440001",
|
|
333
|
+
);
|
|
334
|
+
expect(init?.method).toBe("PATCH");
|
|
335
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
336
|
+
settings: {
|
|
337
|
+
multiline: true,
|
|
338
|
+
},
|
|
339
|
+
type: "TextField",
|
|
340
|
+
label: "Notes",
|
|
341
|
+
required: false,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("strips formula status from update payloads", async () => {
|
|
346
|
+
const configPath = createApiConfig();
|
|
347
|
+
const output = createStdoutBuffer();
|
|
348
|
+
const fetchMock = createFetchMock();
|
|
349
|
+
mockSchemaLookup(fetchMock);
|
|
350
|
+
fetchMock.mockResolvedValueOnce(
|
|
351
|
+
jsonResponse({
|
|
352
|
+
id: "550e8400-e29b-41d4-a716-446655440002",
|
|
353
|
+
type: "FormulaField",
|
|
354
|
+
slug: "total",
|
|
355
|
+
label: "Total",
|
|
356
|
+
required: false,
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
fetchMock.mockResolvedValueOnce(
|
|
360
|
+
jsonResponse({
|
|
361
|
+
id: "550e8400-e29b-41d4-a716-446655440002",
|
|
362
|
+
type: "FormulaField",
|
|
363
|
+
slug: "total",
|
|
364
|
+
label: "Total",
|
|
365
|
+
required: false,
|
|
366
|
+
}),
|
|
367
|
+
);
|
|
368
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
369
|
+
|
|
370
|
+
await program.parseAsync([
|
|
371
|
+
"node",
|
|
372
|
+
"zinkee",
|
|
373
|
+
"--json",
|
|
374
|
+
"schemas",
|
|
375
|
+
"fields",
|
|
376
|
+
"update",
|
|
377
|
+
"contacts",
|
|
378
|
+
"550E8400-E29B-41D4-A716-446655440002",
|
|
379
|
+
"--raw",
|
|
380
|
+
'{"status":"draft","config":{"formulaText":"{price} * {quantity}","status":"active","uiFormatKind":"number"}}',
|
|
381
|
+
]);
|
|
382
|
+
|
|
383
|
+
expect(JSON.parse(String(fetchMock.mock.calls[2]?.[1]?.body))).toEqual({
|
|
384
|
+
type: "FormulaField",
|
|
385
|
+
config: {
|
|
386
|
+
formulaText: "{price} * {quantity}",
|
|
387
|
+
uiFormatKind: "number",
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it("creates fields from a complete raw payload without duplicating required flags", async () => {
|
|
393
|
+
const configPath = createApiConfig();
|
|
394
|
+
const output = createStdoutBuffer();
|
|
395
|
+
const fetchMock = createFetchMock();
|
|
396
|
+
mockSchemaLookup(fetchMock);
|
|
397
|
+
fetchMock.mockResolvedValueOnce(
|
|
398
|
+
jsonResponse({
|
|
399
|
+
id: "field-2",
|
|
400
|
+
type: "TextField",
|
|
401
|
+
slug: "phone",
|
|
402
|
+
label: "Phone",
|
|
403
|
+
required: true,
|
|
404
|
+
}),
|
|
405
|
+
);
|
|
406
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
407
|
+
|
|
408
|
+
await program.parseAsync([
|
|
409
|
+
"node",
|
|
410
|
+
"zinkee",
|
|
411
|
+
"--json",
|
|
412
|
+
"schemas",
|
|
413
|
+
"fields",
|
|
414
|
+
"create",
|
|
415
|
+
"contacts",
|
|
416
|
+
"--raw",
|
|
417
|
+
'{"type":"text","slug":"phone","label":"Phone","required":true}',
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
421
|
+
"https://api.zinkee.test/api/v2/schemas/schema-1/fields",
|
|
422
|
+
);
|
|
423
|
+
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
424
|
+
type: "TextField",
|
|
425
|
+
slug: "phone",
|
|
426
|
+
label: "Phone",
|
|
427
|
+
required: true,
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("strips formula status from create payloads", async () => {
|
|
432
|
+
const configPath = createApiConfig();
|
|
433
|
+
const output = createStdoutBuffer();
|
|
434
|
+
const fetchMock = createFetchMock();
|
|
435
|
+
mockSchemaLookup(fetchMock);
|
|
436
|
+
fetchMock.mockResolvedValueOnce(
|
|
437
|
+
jsonResponse({
|
|
438
|
+
id: "field-formula-1",
|
|
439
|
+
type: "FormulaField",
|
|
440
|
+
slug: "total",
|
|
441
|
+
label: "Total",
|
|
442
|
+
}),
|
|
443
|
+
);
|
|
444
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
445
|
+
|
|
446
|
+
await program.parseAsync([
|
|
447
|
+
"node",
|
|
448
|
+
"zinkee",
|
|
449
|
+
"--json",
|
|
450
|
+
"schemas",
|
|
451
|
+
"fields",
|
|
452
|
+
"create",
|
|
453
|
+
"contacts",
|
|
454
|
+
"--raw",
|
|
455
|
+
'{"type":"formula","slug":"total","label":"Total","status":"draft","config":{"formulaText":"{price} * {quantity}","status":"active","uiFormatKind":"number"}}',
|
|
456
|
+
]);
|
|
457
|
+
|
|
458
|
+
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
459
|
+
type: "FormulaField",
|
|
460
|
+
slug: "total",
|
|
461
|
+
label: "Total",
|
|
462
|
+
config: {
|
|
463
|
+
formulaText: "{price} * {quantity}",
|
|
464
|
+
uiFormatKind: "number",
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("normalizes autoincremental field aliases to AutoIncrementalField", async () => {
|
|
470
|
+
const configPath = createApiConfig();
|
|
471
|
+
const output = createStdoutBuffer();
|
|
472
|
+
const fetchMock = createFetchMock();
|
|
473
|
+
mockSchemaLookup(fetchMock);
|
|
474
|
+
fetchMock.mockResolvedValueOnce(
|
|
475
|
+
jsonResponse({
|
|
476
|
+
id: "field-3",
|
|
477
|
+
type: "AutoIncrementalField",
|
|
478
|
+
slug: "counter",
|
|
479
|
+
label: "Counter",
|
|
480
|
+
}),
|
|
481
|
+
);
|
|
482
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
483
|
+
|
|
484
|
+
await program.parseAsync([
|
|
485
|
+
"node",
|
|
486
|
+
"zinkee",
|
|
487
|
+
"--json",
|
|
488
|
+
"schemas",
|
|
489
|
+
"fields",
|
|
490
|
+
"create",
|
|
491
|
+
"contacts",
|
|
492
|
+
"--type",
|
|
493
|
+
"autoincremental",
|
|
494
|
+
"--slug",
|
|
495
|
+
"counter",
|
|
496
|
+
"--label",
|
|
497
|
+
"Counter",
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
501
|
+
type: "AutoIncrementalField",
|
|
502
|
+
slug: "counter",
|
|
503
|
+
label: "Counter",
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it("normalizes member field aliases to MemberField", async () => {
|
|
508
|
+
const configPath = createApiConfig();
|
|
509
|
+
const output = createStdoutBuffer();
|
|
510
|
+
const fetchMock = createFetchMock();
|
|
511
|
+
mockSchemaLookup(fetchMock);
|
|
512
|
+
fetchMock.mockResolvedValueOnce(
|
|
513
|
+
jsonResponse({
|
|
514
|
+
id: "field-member-1",
|
|
515
|
+
type: "MemberField",
|
|
516
|
+
slug: "owner",
|
|
517
|
+
label: "Owner",
|
|
518
|
+
}),
|
|
519
|
+
);
|
|
520
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
521
|
+
|
|
522
|
+
await program.parseAsync([
|
|
523
|
+
"node",
|
|
524
|
+
"zinkee",
|
|
525
|
+
"--json",
|
|
526
|
+
"schemas",
|
|
527
|
+
"fields",
|
|
528
|
+
"create",
|
|
529
|
+
"contacts",
|
|
530
|
+
"--type",
|
|
531
|
+
"member",
|
|
532
|
+
"--slug",
|
|
533
|
+
"owner",
|
|
534
|
+
"--label",
|
|
535
|
+
"Owner",
|
|
536
|
+
]);
|
|
537
|
+
|
|
538
|
+
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
539
|
+
type: "MemberField",
|
|
540
|
+
slug: "owner",
|
|
541
|
+
label: "Owner",
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it("normalizes option field shorthand payloads into the backend shape", async () => {
|
|
546
|
+
const configPath = createApiConfig();
|
|
547
|
+
const output = createStdoutBuffer();
|
|
548
|
+
const fetchMock = createFetchMock();
|
|
549
|
+
mockSchemaLookup(fetchMock);
|
|
550
|
+
fetchMock.mockResolvedValueOnce(
|
|
551
|
+
jsonResponse({
|
|
552
|
+
id: "field-4",
|
|
553
|
+
type: "OptionField",
|
|
554
|
+
slug: "status",
|
|
555
|
+
label: "Status",
|
|
556
|
+
}),
|
|
557
|
+
);
|
|
558
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
559
|
+
|
|
560
|
+
await program.parseAsync([
|
|
561
|
+
"node",
|
|
562
|
+
"zinkee",
|
|
563
|
+
"--json",
|
|
564
|
+
"schemas",
|
|
565
|
+
"fields",
|
|
566
|
+
"create",
|
|
567
|
+
"contacts",
|
|
568
|
+
"--raw",
|
|
569
|
+
'{"type":"option","slug":"status","label":"Status","options":[{"id":1,"label":"Open"},{"id":2,"label":"Closed","color":"success"}]}',
|
|
570
|
+
]);
|
|
571
|
+
|
|
572
|
+
expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body))).toEqual({
|
|
573
|
+
type: "OptionField",
|
|
574
|
+
slug: "status",
|
|
575
|
+
label: "Status",
|
|
576
|
+
config: {
|
|
577
|
+
options: [
|
|
578
|
+
{
|
|
579
|
+
id: 1,
|
|
580
|
+
name: "Open",
|
|
581
|
+
color: "default",
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
id: 2,
|
|
585
|
+
name: "Closed",
|
|
586
|
+
color: "success",
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it("rejects unsupported field types before making network requests", async () => {
|
|
594
|
+
const configPath = createApiConfig();
|
|
595
|
+
const output = createStdoutBuffer();
|
|
596
|
+
const fetchMock = createFetchMock();
|
|
597
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
598
|
+
|
|
599
|
+
await expect(
|
|
600
|
+
program.parseAsync([
|
|
601
|
+
"node",
|
|
602
|
+
"zinkee",
|
|
603
|
+
"schemas",
|
|
604
|
+
"fields",
|
|
605
|
+
"create",
|
|
606
|
+
"contacts",
|
|
607
|
+
"--type",
|
|
608
|
+
"invalid",
|
|
609
|
+
"--slug",
|
|
610
|
+
"bad",
|
|
611
|
+
"--label",
|
|
612
|
+
"Bad",
|
|
613
|
+
]),
|
|
614
|
+
).rejects.toThrowError(
|
|
615
|
+
'Unsupported field type "InvalidField". Valid types: text, number, date, option, file, reference, formula, member, autoincremental.',
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("shows valid types and raw shapes in schemas fields create help", () => {
|
|
622
|
+
const program = buildProgram();
|
|
623
|
+
const schemas = program.commands.find((command) => command.name() === "schemas");
|
|
624
|
+
const fields = schemas?.commands.find((command) => command.name() === "fields");
|
|
625
|
+
const create = fields?.commands.find((command) => command.name() === "create");
|
|
626
|
+
const help = create?.helpInformation() ?? "";
|
|
627
|
+
|
|
628
|
+
expect(help).toContain("--type <type>");
|
|
629
|
+
expect(help).toContain("member");
|
|
630
|
+
expect(help).toContain("autoincremental");
|
|
631
|
+
expect(help).toContain("Raw payload shapes by type:");
|
|
632
|
+
expect(help).toContain('"type":"number","slug":"price-eur"');
|
|
633
|
+
expect(help).toContain('"defaultValue":"0"');
|
|
634
|
+
expect(help).toContain('"symbolEnabled":true');
|
|
635
|
+
expect(help).toContain('- member: Workspace member field.');
|
|
636
|
+
expect(help).toContain('"type":"member","slug":"owner","label":"Owner"');
|
|
637
|
+
expect(help).toContain('- option: Dropdown/select field with options.');
|
|
638
|
+
expect(help).toContain('"type":"option","slug":"status","label":"Status"');
|
|
639
|
+
expect(help).toContain('Use "zinkee --example schemas fields create --type <type>"');
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("surfaces a descriptive option payload error before hitting the backend", async () => {
|
|
643
|
+
const configPath = createApiConfig();
|
|
644
|
+
const output = createStdoutBuffer();
|
|
645
|
+
const fetchMock = createFetchMock();
|
|
646
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
647
|
+
|
|
648
|
+
await expect(
|
|
649
|
+
program.parseAsync([
|
|
650
|
+
"node",
|
|
651
|
+
"zinkee",
|
|
652
|
+
"schemas",
|
|
653
|
+
"fields",
|
|
654
|
+
"create",
|
|
655
|
+
"contacts",
|
|
656
|
+
"--raw",
|
|
657
|
+
'{"type":"option","slug":"status","label":"Status","options":[1,2]}',
|
|
658
|
+
]),
|
|
659
|
+
).rejects.toThrowError(
|
|
660
|
+
'Option fields require an "options" array with at least one entry like {"id":1,"name":"Open","color":"default"}.',
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
it("enriches generic backend validation errors with the expected raw shape", async () => {
|
|
667
|
+
const configPath = createApiConfig();
|
|
668
|
+
const output = createStdoutBuffer();
|
|
669
|
+
const fetchMock = createFetchMock();
|
|
670
|
+
mockSchemaLookup(fetchMock);
|
|
671
|
+
fetchMock.mockResolvedValueOnce(
|
|
672
|
+
jsonResponse(
|
|
673
|
+
{
|
|
674
|
+
error: {
|
|
675
|
+
message: "Schema payload is invalid. One or more fields are invalid.",
|
|
676
|
+
},
|
|
677
|
+
},
|
|
678
|
+
400,
|
|
679
|
+
),
|
|
680
|
+
);
|
|
681
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
682
|
+
|
|
683
|
+
await expect(
|
|
684
|
+
program.parseAsync([
|
|
685
|
+
"node",
|
|
686
|
+
"zinkee",
|
|
687
|
+
"schemas",
|
|
688
|
+
"fields",
|
|
689
|
+
"create",
|
|
690
|
+
"contacts",
|
|
691
|
+
"--raw",
|
|
692
|
+
'{"type":"option","slug":"status","label":"Status","options":[{"id":1,"name":"Open","color":"default"}]}',
|
|
693
|
+
]),
|
|
694
|
+
).rejects.toThrowError(
|
|
695
|
+
'Invalid payload for field type "option". Expected raw shape: {"type":"option","slug":"status","label":"Status","config":{"options":[{"id":1,"name":"Open","color":"default"},{"id":2,"name":"Closed","color":"success"}]}}',
|
|
696
|
+
);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("deletes fields by slug after resolving the field id", async () => {
|
|
700
|
+
const configPath = createApiConfig();
|
|
701
|
+
const output = createStdoutBuffer();
|
|
702
|
+
const fetchMock = createFetchMock();
|
|
703
|
+
mockSchemaLookup(fetchMock);
|
|
704
|
+
fetchMock.mockResolvedValueOnce(
|
|
705
|
+
jsonResponse({
|
|
706
|
+
id: "schema-1",
|
|
707
|
+
slug: "contacts",
|
|
708
|
+
fields: [
|
|
709
|
+
{
|
|
710
|
+
id: "550e8400-e29b-41d4-a716-446655440001",
|
|
711
|
+
slug: "email",
|
|
712
|
+
type: "TextField",
|
|
713
|
+
label: "Email",
|
|
714
|
+
},
|
|
715
|
+
],
|
|
716
|
+
}),
|
|
717
|
+
);
|
|
718
|
+
fetchMock.mockResolvedValueOnce(jsonResponse({ deleted: true }));
|
|
719
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
720
|
+
|
|
721
|
+
await program.parseAsync([
|
|
722
|
+
"node",
|
|
723
|
+
"zinkee",
|
|
724
|
+
"--json",
|
|
725
|
+
"schemas",
|
|
726
|
+
"fields",
|
|
727
|
+
"delete",
|
|
728
|
+
"contacts",
|
|
729
|
+
"email",
|
|
730
|
+
]);
|
|
731
|
+
|
|
732
|
+
expect(fetchMock.mock.calls[2]?.[0]).toBe(
|
|
733
|
+
"https://api.zinkee.test/api/v2/schemas/schema-1/fields/550e8400-e29b-41d4-a716-446655440001",
|
|
734
|
+
);
|
|
735
|
+
expect(fetchMock.mock.calls[2]?.[1]?.method).toBe("DELETE");
|
|
736
|
+
expect(JSON.parse(output.read())).toEqual({
|
|
737
|
+
data: {
|
|
738
|
+
schema: "schema-1",
|
|
739
|
+
field: "550e8400-e29b-41d4-a716-446655440001",
|
|
740
|
+
deleted: true,
|
|
741
|
+
},
|
|
742
|
+
meta: {
|
|
743
|
+
command: "schemas fields delete",
|
|
744
|
+
configPath,
|
|
745
|
+
},
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
});
|