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,726 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import type { Writable } from "node:stream";
|
|
3
|
+
|
|
4
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
type ApiRecord,
|
|
8
|
+
type CreateRecordInput,
|
|
9
|
+
type RecordQueryExpression,
|
|
10
|
+
type RecordQueryInput,
|
|
11
|
+
type RecordQueryResult,
|
|
12
|
+
RecordsApi,
|
|
13
|
+
type UpdateRecordInput,
|
|
14
|
+
} from "../api/records.js";
|
|
15
|
+
import { SchemasApi } from "../api/schemas.js";
|
|
16
|
+
import { ApiClient } from "../client.js";
|
|
17
|
+
import { defaultConfigPath, readConfig } from "../config.js";
|
|
18
|
+
import {
|
|
19
|
+
type ParsedExpression,
|
|
20
|
+
parseExpression,
|
|
21
|
+
} from "../parsers/expressions.js";
|
|
22
|
+
import {
|
|
23
|
+
parseJsonAssignment,
|
|
24
|
+
parseKeyValue,
|
|
25
|
+
type JsonKeyValueAssignment,
|
|
26
|
+
type KeyValueAssignment,
|
|
27
|
+
} from "../parsers/kv.js";
|
|
28
|
+
import {
|
|
29
|
+
normalizeUuidOrSlug,
|
|
30
|
+
type NormalizedUuidOrSlug,
|
|
31
|
+
} from "../parsers/selectors.js";
|
|
32
|
+
import { resolveRuntimeContext } from "../runtime-context.js";
|
|
33
|
+
import type { JsonObject, RuntimeOverrides } from "../types.js";
|
|
34
|
+
import { CliError } from "../utils/errors.js";
|
|
35
|
+
import { buildJsonSuccess, printTable } from "../utils/output.js";
|
|
36
|
+
|
|
37
|
+
interface CommandIo {
|
|
38
|
+
write(chunk: string): unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RegisterRecordsCommandsOptions {
|
|
42
|
+
configPath?: string;
|
|
43
|
+
stdout?: CommandIo;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface GlobalOptions extends RuntimeOverrides {}
|
|
47
|
+
|
|
48
|
+
interface RecordsQueryCommandOptions {
|
|
49
|
+
select?: string[];
|
|
50
|
+
where?: ParsedExpression[];
|
|
51
|
+
sort?: string[];
|
|
52
|
+
limit?: number;
|
|
53
|
+
offset?: number;
|
|
54
|
+
raw?: string;
|
|
55
|
+
rawFile?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface RecordsListCommandOptions {
|
|
59
|
+
limit?: number;
|
|
60
|
+
offset?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface RecordsCreateCommandOptions {
|
|
64
|
+
set?: KeyValueAssignment[];
|
|
65
|
+
setJson?: JsonKeyValueAssignment[];
|
|
66
|
+
raw?: string;
|
|
67
|
+
rawFile?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface RecordsUpdateCommandOptions extends RecordsCreateCommandOptions {
|
|
71
|
+
unset?: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getCommandIo(stdout: CommandIo | undefined): CommandIo {
|
|
75
|
+
return stdout ?? (process.stdout as Writable);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function writeLine(stdout: CommandIo, value: string): void {
|
|
79
|
+
stdout.write(`${value}\n`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getGlobalOptions(command: Command): GlobalOptions {
|
|
83
|
+
return command.optsWithGlobals<GlobalOptions>();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function emitJson(
|
|
87
|
+
stdout: CommandIo,
|
|
88
|
+
commandName: string,
|
|
89
|
+
data: unknown,
|
|
90
|
+
configPath: string,
|
|
91
|
+
meta: Record<string, unknown> = {},
|
|
92
|
+
): void {
|
|
93
|
+
writeLine(
|
|
94
|
+
stdout,
|
|
95
|
+
JSON.stringify(buildJsonSuccess(data, { command: commandName, configPath, ...meta })),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toConfigurationError(filePath: string, error: unknown): never {
|
|
100
|
+
if (error instanceof CliError) {
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
typeof error === "object" &&
|
|
106
|
+
error !== null &&
|
|
107
|
+
"code" in error &&
|
|
108
|
+
error.code === "ENOENT"
|
|
109
|
+
) {
|
|
110
|
+
throw new CliError("configuration_error", `Config file not found at ${filePath}.`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (error instanceof Error) {
|
|
114
|
+
throw new CliError("configuration_error", error.message);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createRecordsApi(
|
|
121
|
+
command: Command,
|
|
122
|
+
filePath: string,
|
|
123
|
+
): {
|
|
124
|
+
api: RecordsApi;
|
|
125
|
+
schemasApi: SchemasApi;
|
|
126
|
+
json: boolean;
|
|
127
|
+
profile: string;
|
|
128
|
+
readOnly: boolean;
|
|
129
|
+
} {
|
|
130
|
+
try {
|
|
131
|
+
const config = readConfig(filePath);
|
|
132
|
+
const runtime = resolveRuntimeContext(config, getGlobalOptions(command));
|
|
133
|
+
|
|
134
|
+
if (!runtime.baseUrl) {
|
|
135
|
+
throw new CliError(
|
|
136
|
+
"configuration_error",
|
|
137
|
+
`Resolved profile "${runtime.profile}" is missing a base URL.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const client = new ApiClient({
|
|
142
|
+
baseUrl: runtime.baseUrl,
|
|
143
|
+
apiKey: runtime.apiKey,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
api: new RecordsApi(client),
|
|
148
|
+
schemasApi: new SchemasApi(client),
|
|
149
|
+
json: runtime.json,
|
|
150
|
+
profile: runtime.profile,
|
|
151
|
+
readOnly: runtime.readOnly,
|
|
152
|
+
};
|
|
153
|
+
} catch (error) {
|
|
154
|
+
toConfigurationError(filePath, error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ensureWritable(
|
|
159
|
+
commandName: string,
|
|
160
|
+
readOnly: boolean,
|
|
161
|
+
commandMode: "write" | "destructive",
|
|
162
|
+
): void {
|
|
163
|
+
if (readOnly) {
|
|
164
|
+
throw new CliError(
|
|
165
|
+
"command_not_allowed_in_read_only_mode",
|
|
166
|
+
`Command "${commandName}" is not allowed in read-only mode.`,
|
|
167
|
+
{
|
|
168
|
+
commandMode,
|
|
169
|
+
executionMode: "read-only",
|
|
170
|
+
},
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseUuidOrSlugSelector(input: string): NormalizedUuidOrSlug {
|
|
176
|
+
try {
|
|
177
|
+
return normalizeUuidOrSlug(input);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
const message = error instanceof Error ? error.message : "Invalid selector.";
|
|
180
|
+
throw new InvalidArgumentError(message);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseNonNegativeInteger(input: string): number {
|
|
185
|
+
if (!/^\d+$/.test(input.trim())) {
|
|
186
|
+
throw new InvalidArgumentError(`Expected a non-negative integer, received "${input}".`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Number(input);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function parseCommaSeparatedValues(
|
|
193
|
+
input: string,
|
|
194
|
+
label: string,
|
|
195
|
+
): string[] {
|
|
196
|
+
const values = input
|
|
197
|
+
.split(",")
|
|
198
|
+
.map((value) => value.trim())
|
|
199
|
+
.filter(Boolean);
|
|
200
|
+
|
|
201
|
+
if (values.length === 0) {
|
|
202
|
+
throw new InvalidArgumentError(`Expected ${label} to contain at least one value.`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return values;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseExpressionValue(input: string): ParsedExpression {
|
|
209
|
+
try {
|
|
210
|
+
return parseExpression(input);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
const message = error instanceof Error ? error.message : "Invalid expression.";
|
|
213
|
+
throw new InvalidArgumentError(message);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseKeyValueAssignment(input: string): KeyValueAssignment {
|
|
218
|
+
try {
|
|
219
|
+
return parseKeyValue(input);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const message = error instanceof Error ? error.message : "Invalid assignment.";
|
|
222
|
+
throw new InvalidArgumentError(message);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseJsonKeyValueAssignment(input: string): JsonKeyValueAssignment {
|
|
227
|
+
try {
|
|
228
|
+
return parseJsonAssignment(input);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
const message = error instanceof Error ? error.message : "Invalid JSON assignment.";
|
|
231
|
+
throw new InvalidArgumentError(message);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function collectStringValues(value: string, previous: string[] = []): string[] {
|
|
236
|
+
return [...previous, value];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function collectSortValues(value: string, previous: string[] = []): string[] {
|
|
240
|
+
return [...previous, ...parseCommaSeparatedValues(value, "--sort")];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function collectWhereExpressions(
|
|
244
|
+
value: string,
|
|
245
|
+
previous: ParsedExpression[] = [],
|
|
246
|
+
): ParsedExpression[] {
|
|
247
|
+
return [...previous, parseExpressionValue(value)];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function collectKeyValueAssignments(
|
|
251
|
+
value: string,
|
|
252
|
+
previous: KeyValueAssignment[] = [],
|
|
253
|
+
): KeyValueAssignment[] {
|
|
254
|
+
return [...previous, parseKeyValueAssignment(value)];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function collectJsonAssignments(
|
|
258
|
+
value: string,
|
|
259
|
+
previous: JsonKeyValueAssignment[] = [],
|
|
260
|
+
): JsonKeyValueAssignment[] {
|
|
261
|
+
return [...previous, parseJsonKeyValueAssignment(value)];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isJsonObject(value: unknown): value is JsonObject {
|
|
265
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function parseRawJsonObject(input: string, sourceLabel: string): JsonObject {
|
|
269
|
+
let parsed: unknown;
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
parsed = JSON.parse(input);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const message = error instanceof Error ? error.message : "Invalid JSON.";
|
|
275
|
+
throw new CliError(
|
|
276
|
+
"invalid_cli_usage",
|
|
277
|
+
`Failed to parse JSON from ${sourceLabel}: ${message}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!isJsonObject(parsed)) {
|
|
282
|
+
throw new CliError(
|
|
283
|
+
"invalid_cli_usage",
|
|
284
|
+
`Expected ${sourceLabel} to contain a JSON object.`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return parsed;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function readRawObject(
|
|
292
|
+
raw: string | undefined,
|
|
293
|
+
rawFile: string | undefined,
|
|
294
|
+
): JsonObject {
|
|
295
|
+
if (raw && rawFile) {
|
|
296
|
+
throw new CliError(
|
|
297
|
+
"invalid_cli_usage",
|
|
298
|
+
'Provide either "--raw" or "--raw-file", not both.',
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (raw) {
|
|
303
|
+
return parseRawJsonObject(raw, "--raw");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (rawFile) {
|
|
307
|
+
let contents: string;
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
contents = readFileSync(rawFile, "utf8");
|
|
311
|
+
} catch (error) {
|
|
312
|
+
const message = error instanceof Error ? error.message : "Unable to read file.";
|
|
313
|
+
throw new CliError(
|
|
314
|
+
"invalid_cli_usage",
|
|
315
|
+
`Failed to read raw payload from ${rawFile}: ${message}`,
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return parseRawJsonObject(contents, `--raw-file ${rawFile}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isEmptyObject(value: object): boolean {
|
|
326
|
+
return Object.keys(value).length === 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function buildAssignmentObject(
|
|
330
|
+
assignments: readonly KeyValueAssignment[] = [],
|
|
331
|
+
jsonAssignments: readonly JsonKeyValueAssignment[] = [],
|
|
332
|
+
): Record<string, unknown> {
|
|
333
|
+
const output: Record<string, unknown> = {};
|
|
334
|
+
|
|
335
|
+
for (const assignment of assignments) {
|
|
336
|
+
output[assignment.key] = assignment.value;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (const assignment of jsonAssignments) {
|
|
340
|
+
output[assignment.key] = assignment.value;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return output;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function toQueryExpressions(
|
|
347
|
+
expressions: readonly ParsedExpression[] | undefined,
|
|
348
|
+
): RecordQueryExpression[] | undefined {
|
|
349
|
+
if (!expressions || expressions.length === 0) {
|
|
350
|
+
return undefined;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return expressions.map((expression) => ({
|
|
354
|
+
field: expression.field,
|
|
355
|
+
comparator: expression.comparator,
|
|
356
|
+
value: expression.value,
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function buildRecordQueryInput(
|
|
361
|
+
options: RecordsQueryCommandOptions,
|
|
362
|
+
allowRaw: boolean,
|
|
363
|
+
): RecordQueryInput {
|
|
364
|
+
const input: RecordQueryInput = allowRaw
|
|
365
|
+
? readRawObject(options.raw, options.rawFile)
|
|
366
|
+
: {};
|
|
367
|
+
|
|
368
|
+
if (options.select && options.select.length > 0) {
|
|
369
|
+
input.select = [...options.select];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const where = toQueryExpressions(options.where);
|
|
373
|
+
|
|
374
|
+
if (where && where.length > 0) {
|
|
375
|
+
input.where = where;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (options.sort && options.sort.length > 0) {
|
|
379
|
+
input.sort = [...options.sort];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (options.limit !== undefined) {
|
|
383
|
+
input.limit = options.limit;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (options.offset !== undefined) {
|
|
387
|
+
input.offset = options.offset;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return input;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function buildRecordCreateInput(
|
|
394
|
+
options: RecordsCreateCommandOptions,
|
|
395
|
+
): CreateRecordInput {
|
|
396
|
+
const values = {
|
|
397
|
+
...readRawObject(options.raw, options.rawFile),
|
|
398
|
+
...buildAssignmentObject(options.set, options.setJson),
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
if (isEmptyObject(values)) {
|
|
402
|
+
throw new CliError(
|
|
403
|
+
"invalid_cli_usage",
|
|
404
|
+
'Provide record fields via "--set", "--set-json", "--raw", or "--raw-file".',
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return { values };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function readRawSetPayload(raw: JsonObject): Record<string, unknown> {
|
|
412
|
+
const setValue = raw.set;
|
|
413
|
+
|
|
414
|
+
if (setValue === undefined) {
|
|
415
|
+
return {};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!isJsonObject(setValue)) {
|
|
419
|
+
throw new CliError(
|
|
420
|
+
"invalid_cli_usage",
|
|
421
|
+
'Expected raw update payload field "set" to be a JSON object.',
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return { ...setValue };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function readRawUnsetPayload(raw: JsonObject): string[] {
|
|
429
|
+
const unsetValue = raw.unset;
|
|
430
|
+
|
|
431
|
+
if (unsetValue === undefined) {
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!Array.isArray(unsetValue) || !unsetValue.every((value) => typeof value === "string")) {
|
|
436
|
+
throw new CliError(
|
|
437
|
+
"invalid_cli_usage",
|
|
438
|
+
'Expected raw update payload field "unset" to be an array of strings.',
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return [...unsetValue];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function buildRecordUpdateInput(
|
|
446
|
+
options: RecordsUpdateCommandOptions,
|
|
447
|
+
): UpdateRecordInput {
|
|
448
|
+
const raw = readRawObject(options.raw, options.rawFile);
|
|
449
|
+
const input: UpdateRecordInput = { ...raw };
|
|
450
|
+
const setPayload = {
|
|
451
|
+
...readRawSetPayload(raw),
|
|
452
|
+
...buildAssignmentObject(options.set, options.setJson),
|
|
453
|
+
};
|
|
454
|
+
const unsetPayload = [...readRawUnsetPayload(raw), ...(options.unset ?? [])];
|
|
455
|
+
|
|
456
|
+
for (const key of Object.keys(setPayload)) {
|
|
457
|
+
const unsetIndex = unsetPayload.indexOf(key);
|
|
458
|
+
|
|
459
|
+
if (unsetIndex !== -1) {
|
|
460
|
+
unsetPayload.splice(unsetIndex, 1);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (Object.keys(setPayload).length > 0) {
|
|
465
|
+
input.set = setPayload;
|
|
466
|
+
} else {
|
|
467
|
+
delete input.set;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (unsetPayload.length > 0) {
|
|
471
|
+
input.unset = [...new Set(unsetPayload)];
|
|
472
|
+
} else {
|
|
473
|
+
delete input.unset;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (isEmptyObject(input)) {
|
|
477
|
+
throw new CliError(
|
|
478
|
+
"invalid_cli_usage",
|
|
479
|
+
'Provide record updates via "--set", "--set-json", "--unset", "--raw", or "--raw-file".',
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return input;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function toTableRows(data: unknown): Record<string, unknown>[] {
|
|
487
|
+
if (Array.isArray(data)) {
|
|
488
|
+
return data.map((entry) => (isJsonObject(entry) ? entry : { value: entry }));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (isJsonObject(data)) {
|
|
492
|
+
return [data];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return [{ value: data }];
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function emitTable(stdout: CommandIo, data: unknown): void {
|
|
499
|
+
printTable(toTableRows(data), undefined, stdout);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function getQueryRecords(result: RecordQueryResult): ApiRecord[] {
|
|
503
|
+
return Array.isArray(result.records) ? result.records : [];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function getRecordIdentifier(record: ApiRecord): string {
|
|
507
|
+
return typeof record.recordId === "string" ? record.recordId : "unknown";
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function resolveSchemaSelector(
|
|
511
|
+
api: SchemasApi,
|
|
512
|
+
selector: NormalizedUuidOrSlug,
|
|
513
|
+
): Promise<NormalizedUuidOrSlug> {
|
|
514
|
+
if (selector.kind === "uuid") {
|
|
515
|
+
return selector;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const schemas = await api.listSchemas();
|
|
519
|
+
const match = schemas.find((entry) => entry.slug === selector.value);
|
|
520
|
+
|
|
521
|
+
if (!match || typeof match.id !== "string") {
|
|
522
|
+
throw new CliError("invalid_cli_usage", `Schema "${selector.value}" not found.`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
kind: "uuid",
|
|
527
|
+
value: match.id,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export function registerRecordsCommands(
|
|
532
|
+
program: Command,
|
|
533
|
+
options: RegisterRecordsCommandsOptions = {},
|
|
534
|
+
): void {
|
|
535
|
+
const filePath = options.configPath ?? defaultConfigPath;
|
|
536
|
+
const stdout = getCommandIo(options.stdout);
|
|
537
|
+
const records = program.command("records").description("Manage schema records");
|
|
538
|
+
|
|
539
|
+
records
|
|
540
|
+
.command("list")
|
|
541
|
+
.description("List records for a schema with simple pagination")
|
|
542
|
+
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
543
|
+
.option("--limit <number>", "limit results", parseNonNegativeInteger)
|
|
544
|
+
.option("--offset <number>", "offset results", parseNonNegativeInteger)
|
|
545
|
+
.action(async function action(
|
|
546
|
+
this: Command,
|
|
547
|
+
schema: NormalizedUuidOrSlug,
|
|
548
|
+
commandOptions: RecordsListCommandOptions,
|
|
549
|
+
) {
|
|
550
|
+
const { api, schemasApi, json } = createRecordsApi(this, filePath);
|
|
551
|
+
const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
|
|
552
|
+
const data = await api.listRecords(
|
|
553
|
+
resolvedSchema,
|
|
554
|
+
buildRecordQueryInput(commandOptions, false),
|
|
555
|
+
);
|
|
556
|
+
const recordsData = getQueryRecords(data);
|
|
557
|
+
|
|
558
|
+
if (json) {
|
|
559
|
+
emitJson(
|
|
560
|
+
stdout,
|
|
561
|
+
"records list",
|
|
562
|
+
recordsData,
|
|
563
|
+
filePath,
|
|
564
|
+
data.page ? { page: data.page } : {},
|
|
565
|
+
);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
emitTable(stdout, recordsData);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
records
|
|
573
|
+
.command("query")
|
|
574
|
+
.description("Query records for a schema")
|
|
575
|
+
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
576
|
+
.option("--select <fields>", "comma-separated field selection", (value) => (
|
|
577
|
+
parseCommaSeparatedValues(value, "--select")
|
|
578
|
+
))
|
|
579
|
+
.option("--where <expression>", "where expression", collectWhereExpressions, [])
|
|
580
|
+
.option("--sort <field:direction>", "sort field(s)", collectSortValues, [])
|
|
581
|
+
.option("--limit <number>", "limit results", parseNonNegativeInteger)
|
|
582
|
+
.option("--offset <number>", "offset results", parseNonNegativeInteger)
|
|
583
|
+
.option("--raw <json>", "raw JSON object payload")
|
|
584
|
+
.option("--raw-file <path>", "path to a raw JSON object payload")
|
|
585
|
+
.action(async function action(
|
|
586
|
+
this: Command,
|
|
587
|
+
schema: NormalizedUuidOrSlug,
|
|
588
|
+
commandOptions: RecordsQueryCommandOptions,
|
|
589
|
+
) {
|
|
590
|
+
const { api, schemasApi, json } = createRecordsApi(this, filePath);
|
|
591
|
+
const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
|
|
592
|
+
const data = await api.queryRecords(
|
|
593
|
+
resolvedSchema,
|
|
594
|
+
buildRecordQueryInput(commandOptions, true),
|
|
595
|
+
);
|
|
596
|
+
const recordsData = getQueryRecords(data);
|
|
597
|
+
|
|
598
|
+
if (json) {
|
|
599
|
+
emitJson(
|
|
600
|
+
stdout,
|
|
601
|
+
"records query",
|
|
602
|
+
recordsData,
|
|
603
|
+
filePath,
|
|
604
|
+
data.page ? { page: data.page } : {},
|
|
605
|
+
);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
emitTable(stdout, recordsData);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
records
|
|
613
|
+
.command("get")
|
|
614
|
+
.description("Get a record by UUID or slug")
|
|
615
|
+
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
616
|
+
.argument("<record>", "record uuid or slug", parseUuidOrSlugSelector)
|
|
617
|
+
.action(async function action(
|
|
618
|
+
this: Command,
|
|
619
|
+
schema: NormalizedUuidOrSlug,
|
|
620
|
+
record: NormalizedUuidOrSlug,
|
|
621
|
+
) {
|
|
622
|
+
const { api, schemasApi, json } = createRecordsApi(this, filePath);
|
|
623
|
+
const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
|
|
624
|
+
const data = await api.getRecord(resolvedSchema, record);
|
|
625
|
+
|
|
626
|
+
if (json) {
|
|
627
|
+
emitJson(stdout, "records get", data, filePath);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
emitTable(stdout, data);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
records
|
|
635
|
+
.command("create")
|
|
636
|
+
.description("Create a record")
|
|
637
|
+
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
638
|
+
.option("--set <field=value>", "set a string field", collectKeyValueAssignments, [])
|
|
639
|
+
.option("--set-json <field=json>", "set a structured field", collectJsonAssignments, [])
|
|
640
|
+
.option("--raw <json>", "raw JSON object payload")
|
|
641
|
+
.option("--raw-file <path>", "path to a raw JSON object payload")
|
|
642
|
+
.action(async function action(
|
|
643
|
+
this: Command,
|
|
644
|
+
schema: NormalizedUuidOrSlug,
|
|
645
|
+
commandOptions: RecordsCreateCommandOptions,
|
|
646
|
+
) {
|
|
647
|
+
const { api, schemasApi, json, readOnly } = createRecordsApi(this, filePath);
|
|
648
|
+
ensureWritable("records create", readOnly, "write");
|
|
649
|
+
const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
|
|
650
|
+
|
|
651
|
+
const data = await api.createRecord(
|
|
652
|
+
resolvedSchema,
|
|
653
|
+
buildRecordCreateInput(commandOptions),
|
|
654
|
+
);
|
|
655
|
+
|
|
656
|
+
if (json) {
|
|
657
|
+
emitJson(stdout, "records create", data, filePath);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
writeLine(stdout, `Created record "${getRecordIdentifier(data)}".`);
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
records
|
|
665
|
+
.command("update")
|
|
666
|
+
.description("Update a record")
|
|
667
|
+
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
668
|
+
.argument("<record>", "record uuid or slug", parseUuidOrSlugSelector)
|
|
669
|
+
.option("--set <field=value>", "set a string field", collectKeyValueAssignments, [])
|
|
670
|
+
.option("--set-json <field=json>", "set a structured field", collectJsonAssignments, [])
|
|
671
|
+
.option("--unset <field>", "unset a field", collectStringValues, [])
|
|
672
|
+
.option("--raw <json>", "raw JSON object payload")
|
|
673
|
+
.option("--raw-file <path>", "path to a raw JSON object payload")
|
|
674
|
+
.action(async function action(
|
|
675
|
+
this: Command,
|
|
676
|
+
schema: NormalizedUuidOrSlug,
|
|
677
|
+
record: NormalizedUuidOrSlug,
|
|
678
|
+
commandOptions: RecordsUpdateCommandOptions,
|
|
679
|
+
) {
|
|
680
|
+
const { api, schemasApi, json, readOnly } = createRecordsApi(this, filePath);
|
|
681
|
+
ensureWritable("records update", readOnly, "write");
|
|
682
|
+
const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
|
|
683
|
+
|
|
684
|
+
const data = await api.updateRecord(
|
|
685
|
+
resolvedSchema,
|
|
686
|
+
record,
|
|
687
|
+
buildRecordUpdateInput(commandOptions),
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
if (json) {
|
|
691
|
+
emitJson(stdout, "records update", data, filePath);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
writeLine(stdout, `Updated record "${getRecordIdentifier(data)}".`);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
records
|
|
699
|
+
.command("delete")
|
|
700
|
+
.description("Delete a record")
|
|
701
|
+
.argument("<schema>", "schema uuid or slug", parseUuidOrSlugSelector)
|
|
702
|
+
.argument("<record>", "record uuid or slug", parseUuidOrSlugSelector)
|
|
703
|
+
.action(async function action(
|
|
704
|
+
this: Command,
|
|
705
|
+
schema: NormalizedUuidOrSlug,
|
|
706
|
+
record: NormalizedUuidOrSlug,
|
|
707
|
+
) {
|
|
708
|
+
const { api, schemasApi, json, readOnly } = createRecordsApi(this, filePath);
|
|
709
|
+
ensureWritable("records delete", readOnly, "destructive");
|
|
710
|
+
const resolvedSchema = await resolveSchemaSelector(schemasApi, schema);
|
|
711
|
+
|
|
712
|
+
await api.deleteRecord(resolvedSchema, record);
|
|
713
|
+
const data = {
|
|
714
|
+
schema: resolvedSchema.value,
|
|
715
|
+
record: record.value,
|
|
716
|
+
deleted: true,
|
|
717
|
+
};
|
|
718
|
+
|
|
719
|
+
if (json) {
|
|
720
|
+
emitJson(stdout, "records delete", data, filePath);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
writeLine(stdout, `Deleted record "${record.value}" from schema "${schema.value}".`);
|
|
725
|
+
});
|
|
726
|
+
}
|