zinkee 0.1.44 → 0.1.46
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-67U5IRBS.js} +866 -1141
- package/dist/examples-67U5IRBS.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,860 +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-records-"));
|
|
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 = "records-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("records commands", () => {
|
|
75
|
-
it("registers the records command group", () => {
|
|
76
|
-
const program = buildProgram();
|
|
77
|
-
|
|
78
|
-
expect(program.commands.some((command) => command.name() === "records")).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it("lists records with simple pagination and emits page metadata", async () => {
|
|
82
|
-
const configPath = createApiConfig();
|
|
83
|
-
const output = createStdoutBuffer();
|
|
84
|
-
const fetchMock = createFetchMock();
|
|
85
|
-
mockSchemaLookup(fetchMock);
|
|
86
|
-
fetchMock.mockResolvedValueOnce(
|
|
87
|
-
jsonResponse({
|
|
88
|
-
records: [
|
|
89
|
-
{
|
|
90
|
-
recordId: "record-1",
|
|
91
|
-
values: { name: "Ana" },
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
page: { offset: 20, limit: 10, total: 1, count: 1, hasMore: false },
|
|
95
|
-
}),
|
|
96
|
-
);
|
|
97
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
98
|
-
|
|
99
|
-
await program.parseAsync([
|
|
100
|
-
"node",
|
|
101
|
-
"zinkee",
|
|
102
|
-
"--json",
|
|
103
|
-
"records",
|
|
104
|
-
"list",
|
|
105
|
-
"contacts",
|
|
106
|
-
"--limit",
|
|
107
|
-
"10",
|
|
108
|
-
"--offset",
|
|
109
|
-
"20",
|
|
110
|
-
]);
|
|
111
|
-
|
|
112
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe("https://api.zinkee.test/api/v2/schemas");
|
|
113
|
-
|
|
114
|
-
const url = new URL(String(fetchMock.mock.calls[1]?.[0]));
|
|
115
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
116
|
-
|
|
117
|
-
expect(url.origin + url.pathname).toBe("https://api.zinkee.test/api/v2/schemas/schema-1/records");
|
|
118
|
-
expect(init?.method).toBe("GET");
|
|
119
|
-
expect(url.searchParams.get("limit")).toBe("10");
|
|
120
|
-
expect(url.searchParams.get("offset")).toBe("20");
|
|
121
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
122
|
-
data: [{ recordId: "record-1", values: { name: "Ana" } }],
|
|
123
|
-
meta: {
|
|
124
|
-
command: "records list",
|
|
125
|
-
configPath,
|
|
126
|
-
page: { offset: 20, limit: 10, total: 1, count: 1, hasMore: false },
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("queries records with the same normalized query model used by list", async () => {
|
|
132
|
-
const configPath = createApiConfig();
|
|
133
|
-
const output = createStdoutBuffer();
|
|
134
|
-
const fetchMock = createFetchMock();
|
|
135
|
-
mockSchemaLookup(fetchMock);
|
|
136
|
-
fetchMock.mockResolvedValueOnce(
|
|
137
|
-
jsonResponse({
|
|
138
|
-
records: [{ recordId: "record-1", values: { name: "Ana" } }],
|
|
139
|
-
page: { offset: 10, limit: 5, total: 1, count: 1, hasMore: false },
|
|
140
|
-
}),
|
|
141
|
-
);
|
|
142
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
143
|
-
|
|
144
|
-
await program.parseAsync([
|
|
145
|
-
"node",
|
|
146
|
-
"zinkee",
|
|
147
|
-
"--json",
|
|
148
|
-
"records",
|
|
149
|
-
"query",
|
|
150
|
-
"contacts",
|
|
151
|
-
"--select",
|
|
152
|
-
"id,name",
|
|
153
|
-
"--where",
|
|
154
|
-
"status eq active",
|
|
155
|
-
"--sort",
|
|
156
|
-
"name:asc,createdAt:desc",
|
|
157
|
-
"--limit",
|
|
158
|
-
"5",
|
|
159
|
-
"--offset",
|
|
160
|
-
"10",
|
|
161
|
-
"--raw",
|
|
162
|
-
'{"select":["wrong"],"extra":"keep"}',
|
|
163
|
-
]);
|
|
164
|
-
|
|
165
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
166
|
-
const headers = new Headers(init?.headers);
|
|
167
|
-
|
|
168
|
-
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
169
|
-
"https://api.zinkee.test/api/v2/schemas/schema-1/records:query",
|
|
170
|
-
);
|
|
171
|
-
expect(init?.method).toBe("POST");
|
|
172
|
-
expect(headers.get("content-type")).toBe("application/json");
|
|
173
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
174
|
-
extra: "keep",
|
|
175
|
-
select: ["id", "name"],
|
|
176
|
-
where: [
|
|
177
|
-
{
|
|
178
|
-
field: "status",
|
|
179
|
-
op: "eq",
|
|
180
|
-
value: "active",
|
|
181
|
-
},
|
|
182
|
-
],
|
|
183
|
-
sort: [
|
|
184
|
-
{ field: "name", direction: "ASC" },
|
|
185
|
-
{ field: "createdAt", direction: "DESC" },
|
|
186
|
-
],
|
|
187
|
-
page: { limit: 5, offset: 10 },
|
|
188
|
-
});
|
|
189
|
-
expect(JSON.parse(output.read())).toEqual({
|
|
190
|
-
data: [{ recordId: "record-1", values: { name: "Ana" } }],
|
|
191
|
-
meta: {
|
|
192
|
-
command: "records query",
|
|
193
|
-
configPath,
|
|
194
|
-
page: { offset: 10, limit: 5, total: 1, count: 1, hasMore: false },
|
|
195
|
-
},
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("maps --search to a search.term object in the query body", async () => {
|
|
200
|
-
const configPath = createApiConfig();
|
|
201
|
-
const output = createStdoutBuffer();
|
|
202
|
-
const fetchMock = createFetchMock();
|
|
203
|
-
mockSchemaLookup(fetchMock);
|
|
204
|
-
fetchMock.mockResolvedValueOnce(
|
|
205
|
-
jsonResponse({
|
|
206
|
-
records: [{ recordId: "record-1", values: { name: "Ana Garcia" } }],
|
|
207
|
-
page: { offset: 0, limit: 25, total: 1, count: 1, hasMore: false },
|
|
208
|
-
}),
|
|
209
|
-
);
|
|
210
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
211
|
-
|
|
212
|
-
await program.parseAsync([
|
|
213
|
-
"node",
|
|
214
|
-
"zinkee",
|
|
215
|
-
"--json",
|
|
216
|
-
"records",
|
|
217
|
-
"query",
|
|
218
|
-
"contacts",
|
|
219
|
-
"--select",
|
|
220
|
-
"id,name,status",
|
|
221
|
-
"--where",
|
|
222
|
-
"status in OPEN",
|
|
223
|
-
"--search",
|
|
224
|
-
"garcia",
|
|
225
|
-
"--limit",
|
|
226
|
-
"25",
|
|
227
|
-
]);
|
|
228
|
-
|
|
229
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
230
|
-
|
|
231
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
232
|
-
select: ["id", "name", "status"],
|
|
233
|
-
where: [{ field: "status", op: "in", value: ["OPEN"] }],
|
|
234
|
-
search: { term: "garcia" },
|
|
235
|
-
page: { limit: 25 },
|
|
236
|
-
});
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("accumulates repeated --select flags into a single field selection", async () => {
|
|
240
|
-
const configPath = createApiConfig();
|
|
241
|
-
const output = createStdoutBuffer();
|
|
242
|
-
const fetchMock = createFetchMock();
|
|
243
|
-
mockSchemaLookup(fetchMock);
|
|
244
|
-
fetchMock.mockResolvedValueOnce(
|
|
245
|
-
jsonResponse({
|
|
246
|
-
records: [{ recordId: "record-1", values: { name: "Ana" } }],
|
|
247
|
-
page: { offset: 0, limit: 25, total: 1, count: 1, hasMore: false },
|
|
248
|
-
}),
|
|
249
|
-
);
|
|
250
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
251
|
-
|
|
252
|
-
await program.parseAsync([
|
|
253
|
-
"node",
|
|
254
|
-
"zinkee",
|
|
255
|
-
"--json",
|
|
256
|
-
"records",
|
|
257
|
-
"query",
|
|
258
|
-
"contacts",
|
|
259
|
-
"--select",
|
|
260
|
-
"campo-1",
|
|
261
|
-
"--select",
|
|
262
|
-
"campo-2",
|
|
263
|
-
]);
|
|
264
|
-
|
|
265
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
266
|
-
|
|
267
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
268
|
-
select: ["campo-1", "campo-2"],
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("mixes comma-separated and repeated --select flags preserving order", async () => {
|
|
273
|
-
const configPath = createApiConfig();
|
|
274
|
-
const output = createStdoutBuffer();
|
|
275
|
-
const fetchMock = createFetchMock();
|
|
276
|
-
mockSchemaLookup(fetchMock);
|
|
277
|
-
fetchMock.mockResolvedValueOnce(
|
|
278
|
-
jsonResponse({
|
|
279
|
-
records: [{ recordId: "record-1", values: { name: "Ana" } }],
|
|
280
|
-
page: { offset: 0, limit: 25, total: 1, count: 1, hasMore: false },
|
|
281
|
-
}),
|
|
282
|
-
);
|
|
283
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
284
|
-
|
|
285
|
-
await program.parseAsync([
|
|
286
|
-
"node",
|
|
287
|
-
"zinkee",
|
|
288
|
-
"--json",
|
|
289
|
-
"records",
|
|
290
|
-
"query",
|
|
291
|
-
"contacts",
|
|
292
|
-
"--select",
|
|
293
|
-
"a,b",
|
|
294
|
-
"--select",
|
|
295
|
-
"c",
|
|
296
|
-
]);
|
|
297
|
-
|
|
298
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
299
|
-
|
|
300
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
301
|
-
select: ["a", "b", "c"],
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("deduplicates repeated fields across --select and --sort flags", async () => {
|
|
306
|
-
const configPath = createApiConfig();
|
|
307
|
-
const output = createStdoutBuffer();
|
|
308
|
-
const fetchMock = createFetchMock();
|
|
309
|
-
mockSchemaLookup(fetchMock);
|
|
310
|
-
fetchMock.mockResolvedValueOnce(
|
|
311
|
-
jsonResponse({
|
|
312
|
-
records: [{ recordId: "record-1", values: { name: "Ana" } }],
|
|
313
|
-
page: { offset: 0, limit: 25, total: 1, count: 1, hasMore: false },
|
|
314
|
-
}),
|
|
315
|
-
);
|
|
316
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
317
|
-
|
|
318
|
-
await program.parseAsync([
|
|
319
|
-
"node",
|
|
320
|
-
"zinkee",
|
|
321
|
-
"--json",
|
|
322
|
-
"records",
|
|
323
|
-
"query",
|
|
324
|
-
"contacts",
|
|
325
|
-
"--select",
|
|
326
|
-
"id",
|
|
327
|
-
"--select",
|
|
328
|
-
"id",
|
|
329
|
-
"--sort",
|
|
330
|
-
"name:asc",
|
|
331
|
-
"--sort",
|
|
332
|
-
"name:asc",
|
|
333
|
-
]);
|
|
334
|
-
|
|
335
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
336
|
-
|
|
337
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
338
|
-
select: ["id"],
|
|
339
|
-
sort: [{ field: "name", direction: "ASC" }],
|
|
340
|
-
});
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it("normalizes UUID selectors for records get", async () => {
|
|
344
|
-
const configPath = createApiConfig();
|
|
345
|
-
const output = createStdoutBuffer();
|
|
346
|
-
const fetchMock = createFetchMock();
|
|
347
|
-
fetchMock.mockResolvedValueOnce(
|
|
348
|
-
jsonResponse({
|
|
349
|
-
recordId: "550e8400-e29b-41d4-a716-446655440001",
|
|
350
|
-
values: { name: "Ana" },
|
|
351
|
-
}),
|
|
352
|
-
);
|
|
353
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
354
|
-
|
|
355
|
-
await program.parseAsync([
|
|
356
|
-
"node",
|
|
357
|
-
"zinkee",
|
|
358
|
-
"--json",
|
|
359
|
-
"records",
|
|
360
|
-
"get",
|
|
361
|
-
"550E8400-E29B-41D4-A716-446655440000",
|
|
362
|
-
"550E8400-E29B-41D4-A716-446655440001",
|
|
363
|
-
]);
|
|
364
|
-
|
|
365
|
-
expect(fetchMock.mock.calls[0]?.[0]).toBe(
|
|
366
|
-
"https://api.zinkee.test/api/v2/schemas/550e8400-e29b-41d4-a716-446655440000/records/550e8400-e29b-41d4-a716-446655440001",
|
|
367
|
-
);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it("creates a single record by merging raw JSON with set and set-json assignments", async () => {
|
|
371
|
-
const configPath = createApiConfig();
|
|
372
|
-
const output = createStdoutBuffer();
|
|
373
|
-
const fetchMock = createFetchMock();
|
|
374
|
-
mockSchemaLookup(fetchMock);
|
|
375
|
-
fetchMock.mockResolvedValueOnce(
|
|
376
|
-
jsonResponse({
|
|
377
|
-
created: ["record-1"],
|
|
378
|
-
errors: [],
|
|
379
|
-
}),
|
|
380
|
-
);
|
|
381
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
382
|
-
|
|
383
|
-
await program.parseAsync([
|
|
384
|
-
"node",
|
|
385
|
-
"zinkee",
|
|
386
|
-
"--json",
|
|
387
|
-
"records",
|
|
388
|
-
"create",
|
|
389
|
-
"contacts",
|
|
390
|
-
"--set",
|
|
391
|
-
"name=Ana",
|
|
392
|
-
"--set-json",
|
|
393
|
-
'profile={"tier":"gold"}',
|
|
394
|
-
"--raw",
|
|
395
|
-
'{"name":"Wrong","status":"active"}',
|
|
396
|
-
]);
|
|
397
|
-
|
|
398
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
399
|
-
|
|
400
|
-
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
401
|
-
"https://api.zinkee.test/api/v2/schemas/schema-1/records",
|
|
402
|
-
);
|
|
403
|
-
expect(init?.method).toBe("POST");
|
|
404
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
405
|
-
records: [
|
|
406
|
-
{
|
|
407
|
-
name: "Ana",
|
|
408
|
-
status: "active",
|
|
409
|
-
profile: {
|
|
410
|
-
tier: "gold",
|
|
411
|
-
},
|
|
412
|
-
},
|
|
413
|
-
],
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it("creates multiple records from a --records-file", async () => {
|
|
418
|
-
const configPath = createApiConfig();
|
|
419
|
-
const recordsFile = createTempConfig(
|
|
420
|
-
JSON.stringify([
|
|
421
|
-
{ "contact-name": "Ada", status: 2 },
|
|
422
|
-
{ "contact-name": "Grace", tags: ["a", "b"] },
|
|
423
|
-
]),
|
|
424
|
-
);
|
|
425
|
-
const output = createStdoutBuffer();
|
|
426
|
-
const fetchMock = createFetchMock();
|
|
427
|
-
mockSchemaLookup(fetchMock);
|
|
428
|
-
fetchMock.mockResolvedValueOnce(
|
|
429
|
-
jsonResponse({
|
|
430
|
-
created: ["record-1", "record-2"],
|
|
431
|
-
errors: [],
|
|
432
|
-
}),
|
|
433
|
-
);
|
|
434
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
435
|
-
|
|
436
|
-
await program.parseAsync([
|
|
437
|
-
"node",
|
|
438
|
-
"zinkee",
|
|
439
|
-
"--json",
|
|
440
|
-
"records",
|
|
441
|
-
"create",
|
|
442
|
-
"contacts",
|
|
443
|
-
"--records-file",
|
|
444
|
-
recordsFile,
|
|
445
|
-
]);
|
|
446
|
-
|
|
447
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
448
|
-
|
|
449
|
-
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
450
|
-
"https://api.zinkee.test/api/v2/schemas/schema-1/records",
|
|
451
|
-
);
|
|
452
|
-
expect(init?.method).toBe("POST");
|
|
453
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
454
|
-
records: [
|
|
455
|
-
{ "contact-name": "Ada", status: 2 },
|
|
456
|
-
{ "contact-name": "Grace", tags: ["a", "b"] },
|
|
457
|
-
],
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
it("rejects combining --records-file with single-record flags", async () => {
|
|
462
|
-
const configPath = createApiConfig();
|
|
463
|
-
const recordsFile = createTempConfig(JSON.stringify([{ "contact-name": "Ada" }]));
|
|
464
|
-
const output = createStdoutBuffer();
|
|
465
|
-
const fetchMock = createFetchMock();
|
|
466
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
467
|
-
|
|
468
|
-
await expect(
|
|
469
|
-
program.parseAsync([
|
|
470
|
-
"node",
|
|
471
|
-
"zinkee",
|
|
472
|
-
"records",
|
|
473
|
-
"create",
|
|
474
|
-
"contacts",
|
|
475
|
-
"--records-file",
|
|
476
|
-
recordsFile,
|
|
477
|
-
"--set",
|
|
478
|
-
"contact-name=Grace",
|
|
479
|
-
]),
|
|
480
|
-
).rejects.toThrowError(/not both/);
|
|
481
|
-
|
|
482
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it("rejects a --records-file that is not a JSON array", async () => {
|
|
486
|
-
const configPath = createApiConfig();
|
|
487
|
-
const recordsFile = createTempConfig(JSON.stringify({ "contact-name": "Ada" }));
|
|
488
|
-
const output = createStdoutBuffer();
|
|
489
|
-
const fetchMock = createFetchMock();
|
|
490
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
491
|
-
|
|
492
|
-
await expect(
|
|
493
|
-
program.parseAsync([
|
|
494
|
-
"node",
|
|
495
|
-
"zinkee",
|
|
496
|
-
"records",
|
|
497
|
-
"create",
|
|
498
|
-
"contacts",
|
|
499
|
-
"--records-file",
|
|
500
|
-
recordsFile,
|
|
501
|
-
]),
|
|
502
|
-
).rejects.toThrowError(/JSON array/);
|
|
503
|
-
|
|
504
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
it("wraps raw-file fields into the set payload", async () => {
|
|
508
|
-
const configPath = createApiConfig();
|
|
509
|
-
const output = createStdoutBuffer();
|
|
510
|
-
const fetchMock = createFetchMock();
|
|
511
|
-
const rawFile = join(
|
|
512
|
-
mkdtempSync(join(tmpdir(), "zinkee-record-update-")),
|
|
513
|
-
"record.json",
|
|
514
|
-
);
|
|
515
|
-
writeFileSync(
|
|
516
|
-
rawFile,
|
|
517
|
-
JSON.stringify({
|
|
518
|
-
status: "inactive",
|
|
519
|
-
metadata: {
|
|
520
|
-
imported: true,
|
|
521
|
-
},
|
|
522
|
-
}),
|
|
523
|
-
"utf8",
|
|
524
|
-
);
|
|
525
|
-
mockSchemaLookup(fetchMock);
|
|
526
|
-
fetchMock.mockResolvedValueOnce(
|
|
527
|
-
jsonResponse({
|
|
528
|
-
id: "550e8400-e29b-41d4-a716-446655440001",
|
|
529
|
-
status: "active",
|
|
530
|
-
}),
|
|
531
|
-
);
|
|
532
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
533
|
-
|
|
534
|
-
await program.parseAsync([
|
|
535
|
-
"node",
|
|
536
|
-
"zinkee",
|
|
537
|
-
"--json",
|
|
538
|
-
"records",
|
|
539
|
-
"update",
|
|
540
|
-
"contacts",
|
|
541
|
-
"550E8400-E29B-41D4-A716-446655440001",
|
|
542
|
-
"--set",
|
|
543
|
-
"status=active",
|
|
544
|
-
"--set-json",
|
|
545
|
-
'profile={"tier":"gold"}',
|
|
546
|
-
"--unset",
|
|
547
|
-
"tempField",
|
|
548
|
-
"--unset",
|
|
549
|
-
"status",
|
|
550
|
-
"--raw-file",
|
|
551
|
-
rawFile,
|
|
552
|
-
]);
|
|
553
|
-
|
|
554
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
555
|
-
|
|
556
|
-
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
557
|
-
"https://api.zinkee.test/api/v2/schemas/schema-1/records/550e8400-e29b-41d4-a716-446655440001",
|
|
558
|
-
);
|
|
559
|
-
expect(init?.method).toBe("PATCH");
|
|
560
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
561
|
-
set: {
|
|
562
|
-
status: "active",
|
|
563
|
-
metadata: {
|
|
564
|
-
imported: true,
|
|
565
|
-
},
|
|
566
|
-
profile: {
|
|
567
|
-
tier: "gold",
|
|
568
|
-
},
|
|
569
|
-
},
|
|
570
|
-
unset: ["tempField"],
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it("wraps bare raw fields into the set payload", async () => {
|
|
575
|
-
const configPath = createApiConfig();
|
|
576
|
-
const output = createStdoutBuffer();
|
|
577
|
-
const fetchMock = createFetchMock();
|
|
578
|
-
mockSchemaLookup(fetchMock);
|
|
579
|
-
fetchMock.mockResolvedValueOnce(
|
|
580
|
-
jsonResponse({
|
|
581
|
-
id: "550e8400-e29b-41d4-a716-446655440001",
|
|
582
|
-
uno: "prueba mod",
|
|
583
|
-
}),
|
|
584
|
-
);
|
|
585
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
586
|
-
|
|
587
|
-
await program.parseAsync([
|
|
588
|
-
"node",
|
|
589
|
-
"zinkee",
|
|
590
|
-
"--json",
|
|
591
|
-
"records",
|
|
592
|
-
"update",
|
|
593
|
-
"contacts",
|
|
594
|
-
"550E8400-E29B-41D4-A716-446655440001",
|
|
595
|
-
"--raw",
|
|
596
|
-
'{"uno":"prueba mod"}',
|
|
597
|
-
]);
|
|
598
|
-
|
|
599
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
600
|
-
|
|
601
|
-
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
602
|
-
"https://api.zinkee.test/api/v2/schemas/schema-1/records/550e8400-e29b-41d4-a716-446655440001",
|
|
603
|
-
);
|
|
604
|
-
expect(init?.method).toBe("PATCH");
|
|
605
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
606
|
-
set: {
|
|
607
|
-
uno: "prueba mod",
|
|
608
|
-
},
|
|
609
|
-
});
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
it("lets --set override raw fields with the same key", async () => {
|
|
613
|
-
const configPath = createApiConfig();
|
|
614
|
-
const output = createStdoutBuffer();
|
|
615
|
-
const fetchMock = createFetchMock();
|
|
616
|
-
mockSchemaLookup(fetchMock);
|
|
617
|
-
fetchMock.mockResolvedValueOnce(
|
|
618
|
-
jsonResponse({
|
|
619
|
-
id: "550e8400-e29b-41d4-a716-446655440001",
|
|
620
|
-
}),
|
|
621
|
-
);
|
|
622
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
623
|
-
|
|
624
|
-
await program.parseAsync([
|
|
625
|
-
"node",
|
|
626
|
-
"zinkee",
|
|
627
|
-
"--json",
|
|
628
|
-
"records",
|
|
629
|
-
"update",
|
|
630
|
-
"contacts",
|
|
631
|
-
"550E8400-E29B-41D4-A716-446655440001",
|
|
632
|
-
"--raw",
|
|
633
|
-
'{"uno":"raw value"}',
|
|
634
|
-
"--set",
|
|
635
|
-
"uno=set value",
|
|
636
|
-
]);
|
|
637
|
-
|
|
638
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
639
|
-
|
|
640
|
-
expect(init?.method).toBe("PATCH");
|
|
641
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
642
|
-
set: {
|
|
643
|
-
uno: "set value",
|
|
644
|
-
},
|
|
645
|
-
});
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
it("blocks destructive record commands in read-only mode", async () => {
|
|
649
|
-
const configPath = createApiConfig();
|
|
650
|
-
const output = createStdoutBuffer();
|
|
651
|
-
const fetchMock = createFetchMock();
|
|
652
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
653
|
-
|
|
654
|
-
await expect(
|
|
655
|
-
program.parseAsync([
|
|
656
|
-
"node",
|
|
657
|
-
"zinkee",
|
|
658
|
-
"--read-only",
|
|
659
|
-
"records",
|
|
660
|
-
"delete",
|
|
661
|
-
"contacts",
|
|
662
|
-
"--record",
|
|
663
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
664
|
-
]),
|
|
665
|
-
).rejects.toThrowError('Command "records delete" is not allowed in read-only mode.');
|
|
666
|
-
|
|
667
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
it("deletes multiple records via repeatable --record flags and de-duplicates", async () => {
|
|
671
|
-
const configPath = createApiConfig();
|
|
672
|
-
const output = createStdoutBuffer();
|
|
673
|
-
const fetchMock = createFetchMock();
|
|
674
|
-
mockSchemaLookup(fetchMock);
|
|
675
|
-
fetchMock.mockResolvedValueOnce(
|
|
676
|
-
jsonResponse({
|
|
677
|
-
recordIds: [
|
|
678
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
679
|
-
"550e8400-e29b-41d4-a716-446655440001",
|
|
680
|
-
],
|
|
681
|
-
deleted: true,
|
|
682
|
-
}),
|
|
683
|
-
);
|
|
684
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
685
|
-
|
|
686
|
-
await program.parseAsync([
|
|
687
|
-
"node",
|
|
688
|
-
"zinkee",
|
|
689
|
-
"--json",
|
|
690
|
-
"records",
|
|
691
|
-
"delete",
|
|
692
|
-
"contacts",
|
|
693
|
-
"--record",
|
|
694
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
695
|
-
"--record",
|
|
696
|
-
"550e8400-e29b-41d4-a716-446655440001",
|
|
697
|
-
"--record",
|
|
698
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
699
|
-
]);
|
|
700
|
-
|
|
701
|
-
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
702
|
-
"https://api.zinkee.test/api/v2/schemas/schema-1/records:delete",
|
|
703
|
-
);
|
|
704
|
-
|
|
705
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
706
|
-
|
|
707
|
-
expect(init?.method).toBe("POST");
|
|
708
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
709
|
-
recordIds: [
|
|
710
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
711
|
-
"550e8400-e29b-41d4-a716-446655440001",
|
|
712
|
-
],
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
const payload = JSON.parse(output.read());
|
|
716
|
-
|
|
717
|
-
expect(payload.data).toEqual({
|
|
718
|
-
recordIds: [
|
|
719
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
720
|
-
"550e8400-e29b-41d4-a716-446655440001",
|
|
721
|
-
],
|
|
722
|
-
deleted: true,
|
|
723
|
-
});
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
it("loads record ids from a file and merges them with --record", async () => {
|
|
727
|
-
const configPath = createApiConfig();
|
|
728
|
-
const idsFile = createTempConfig(
|
|
729
|
-
"550e8400-e29b-41d4-a716-446655440000\n 550e8400-e29b-41d4-a716-446655440002 \n\n",
|
|
730
|
-
);
|
|
731
|
-
const output = createStdoutBuffer();
|
|
732
|
-
const fetchMock = createFetchMock();
|
|
733
|
-
mockSchemaLookup(fetchMock);
|
|
734
|
-
fetchMock.mockResolvedValueOnce(
|
|
735
|
-
jsonResponse({
|
|
736
|
-
recordIds: [
|
|
737
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
738
|
-
"550e8400-e29b-41d4-a716-446655440002",
|
|
739
|
-
],
|
|
740
|
-
deleted: true,
|
|
741
|
-
}),
|
|
742
|
-
);
|
|
743
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
744
|
-
|
|
745
|
-
await program.parseAsync([
|
|
746
|
-
"node",
|
|
747
|
-
"zinkee",
|
|
748
|
-
"--json",
|
|
749
|
-
"records",
|
|
750
|
-
"delete",
|
|
751
|
-
"contacts",
|
|
752
|
-
"--record",
|
|
753
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
754
|
-
"--records-file",
|
|
755
|
-
idsFile,
|
|
756
|
-
]);
|
|
757
|
-
|
|
758
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
759
|
-
|
|
760
|
-
expect(init?.method).toBe("POST");
|
|
761
|
-
expect(JSON.parse(String(init?.body))).toEqual({
|
|
762
|
-
recordIds: [
|
|
763
|
-
"550e8400-e29b-41d4-a716-446655440000",
|
|
764
|
-
"550e8400-e29b-41d4-a716-446655440002",
|
|
765
|
-
],
|
|
766
|
-
});
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
it("requires at least one record id for delete", async () => {
|
|
770
|
-
const configPath = createApiConfig();
|
|
771
|
-
const output = createStdoutBuffer();
|
|
772
|
-
const fetchMock = createFetchMock();
|
|
773
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
774
|
-
|
|
775
|
-
await expect(
|
|
776
|
-
program.parseAsync(["node", "zinkee", "records", "delete", "contacts"]),
|
|
777
|
-
).rejects.toThrowError(/at least one record id/);
|
|
778
|
-
|
|
779
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
it("rejects a non-uuid record id for delete", async () => {
|
|
783
|
-
const configPath = createApiConfig();
|
|
784
|
-
const output = createStdoutBuffer();
|
|
785
|
-
const fetchMock = createFetchMock();
|
|
786
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
787
|
-
|
|
788
|
-
await expect(
|
|
789
|
-
program.parseAsync([
|
|
790
|
-
"node",
|
|
791
|
-
"zinkee",
|
|
792
|
-
"records",
|
|
793
|
-
"delete",
|
|
794
|
-
"contacts",
|
|
795
|
-
"--record",
|
|
796
|
-
"not-a-uuid",
|
|
797
|
-
]),
|
|
798
|
-
).rejects.toThrow();
|
|
799
|
-
|
|
800
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
it("clears all records in a schema", async () => {
|
|
804
|
-
const configPath = createApiConfig();
|
|
805
|
-
const output = createStdoutBuffer();
|
|
806
|
-
const fetchMock = createFetchMock();
|
|
807
|
-
mockSchemaLookup(fetchMock);
|
|
808
|
-
fetchMock.mockResolvedValueOnce(
|
|
809
|
-
jsonResponse({
|
|
810
|
-
schemaId: "schema-1",
|
|
811
|
-
cleared: true,
|
|
812
|
-
}),
|
|
813
|
-
);
|
|
814
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
815
|
-
|
|
816
|
-
await program.parseAsync([
|
|
817
|
-
"node",
|
|
818
|
-
"zinkee",
|
|
819
|
-
"--json",
|
|
820
|
-
"records",
|
|
821
|
-
"clear",
|
|
822
|
-
"contacts",
|
|
823
|
-
]);
|
|
824
|
-
|
|
825
|
-
const init = fetchMock.mock.calls[1]?.[1];
|
|
826
|
-
|
|
827
|
-
expect(fetchMock.mock.calls[1]?.[0]).toBe(
|
|
828
|
-
"https://api.zinkee.test/api/v2/schemas/schema-1/records:clear",
|
|
829
|
-
);
|
|
830
|
-
expect(init?.method).toBe("DELETE");
|
|
831
|
-
expect(init?.body).toBeUndefined();
|
|
832
|
-
|
|
833
|
-
const payload = JSON.parse(output.read());
|
|
834
|
-
|
|
835
|
-
expect(payload.data).toEqual({
|
|
836
|
-
schemaId: "schema-1",
|
|
837
|
-
cleared: true,
|
|
838
|
-
});
|
|
839
|
-
});
|
|
840
|
-
|
|
841
|
-
it("blocks records clear in read-only mode", async () => {
|
|
842
|
-
const configPath = createApiConfig();
|
|
843
|
-
const output = createStdoutBuffer();
|
|
844
|
-
const fetchMock = createFetchMock();
|
|
845
|
-
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
846
|
-
|
|
847
|
-
await expect(
|
|
848
|
-
program.parseAsync([
|
|
849
|
-
"node",
|
|
850
|
-
"zinkee",
|
|
851
|
-
"--read-only",
|
|
852
|
-
"records",
|
|
853
|
-
"clear",
|
|
854
|
-
"contacts",
|
|
855
|
-
]),
|
|
856
|
-
).rejects.toThrowError('Command "records clear" is not allowed in read-only mode.');
|
|
857
|
-
|
|
858
|
-
expect(fetchMock).not.toHaveBeenCalled();
|
|
859
|
-
});
|
|
860
|
-
});
|