primitive-admin 1.1.0-alpha.53 → 1.1.0-alpha.55
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 +24 -10
- package/assets/skill/skills/primitive-platform/SKILL.md +134 -0
- package/dist/bin/primitive.js +4 -0
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.js +22 -8
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.js +71 -1
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +16 -14
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +37 -2
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/connections.d.ts +2 -0
- package/dist/src/commands/connections.js +95 -0
- package/dist/src/commands/connections.js.map +1 -0
- package/dist/src/commands/databases.js +280 -45
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +615 -1
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/init.js +2 -2
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +11 -88
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/metadata.js +47 -30
- package/dist/src/commands/metadata.js.map +1 -1
- package/dist/src/commands/prompts.js +14 -5
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/sessions.d.ts +2 -0
- package/dist/src/commands/sessions.js +63 -0
- package/dist/src/commands/sessions.js.map +1 -0
- package/dist/src/commands/settings.d.ts +1 -2
- package/dist/src/commands/settings.js +8 -6
- package/dist/src/commands/settings.js.map +1 -1
- package/dist/src/commands/skill.js +2 -2
- package/dist/src/commands/skill.js.map +1 -1
- package/dist/src/commands/sync-app-settings.d.ts +26 -37
- package/dist/src/commands/sync-app-settings.js +43 -56
- package/dist/src/commands/sync-app-settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +4 -3
- package/dist/src/commands/sync.js +245 -30
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/webhooks.js +71 -23
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.js +364 -55
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +294 -13
- package/dist/src/lib/api-client.js +345 -42
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +15 -13
- package/dist/src/lib/app-settings-descriptor.js +25 -23
- package/dist/src/lib/app-settings-descriptor.js.map +1 -1
- package/dist/src/lib/data-input.d.ts +23 -0
- package/dist/src/lib/data-input.js +50 -0
- package/dist/src/lib/data-input.js.map +1 -0
- package/dist/src/lib/generated-allowlist.js +1 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-workflow-model-fields.d.ts +20 -0
- package/dist/src/lib/generated-workflow-model-fields.js +54 -0
- package/dist/src/lib/generated-workflow-model-fields.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +550 -0
- package/dist/src/lib/log-inspection.js +609 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/output.d.ts +10 -0
- package/dist/src/lib/output.js +12 -0
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/record-filter.d.ts +16 -0
- package/dist/src/lib/record-filter.js +53 -0
- package/dist/src/lib/record-filter.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +4 -2
- package/dist/src/lib/skill-installer.js +139 -11
- package/dist/src/lib/skill-installer.js.map +1 -1
- package/dist/src/lib/watch.d.ts +121 -0
- package/dist/src/lib/watch.js +169 -0
- package/dist/src/lib/watch.js.map +1 -0
- package/dist/src/lib/workflow-field-descriptor.d.ts +92 -0
- package/dist/src/lib/workflow-field-descriptor.js +163 -0
- package/dist/src/lib/workflow-field-descriptor.js.map +1 -0
- package/dist/src/lib/workflow-fragments.d.ts +23 -0
- package/dist/src/lib/workflow-fragments.js +229 -8
- package/dist/src/lib/workflow-fragments.js.map +1 -1
- package/dist/src/lib/workflow-payload.d.ts +1 -1
- package/dist/src/lib/workflow-payload.js +17 -0
- package/dist/src/lib/workflow-payload.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +57 -1
- package/dist/src/lib/workflow-toml-validator.js +104 -1
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/dist/src/types/index.d.ts +21 -16
- package/package.json +7 -4
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { ApiClient } from "../lib/api-client.js";
|
|
2
2
|
import { resolveAppId } from "../lib/config.js";
|
|
3
|
-
import { success, error, info, formatTable, formatDate, json, keyValue, warn, } from "../lib/output.js";
|
|
3
|
+
import { success, error, info, formatTable, formatId, formatDate, json, keyValue, warn, } from "../lib/output.js";
|
|
4
4
|
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
5
|
+
import { parseDataOption } from "../lib/data-input.js";
|
|
6
|
+
import { ulid } from "ulid";
|
|
5
7
|
import * as fs from "fs";
|
|
6
8
|
import * as path from "path";
|
|
7
9
|
export function registerDocumentsCommands(program) {
|
|
@@ -10,9 +12,621 @@ export function registerDocumentsCommands(program) {
|
|
|
10
12
|
.description("Manage documents within an app")
|
|
11
13
|
.addHelpText("after", `
|
|
12
14
|
Examples:
|
|
15
|
+
$ primitive documents list --user-id <user-id>
|
|
16
|
+
$ primitive documents get <document-id>
|
|
17
|
+
$ primitive documents permissions list <document-id>
|
|
18
|
+
$ primitive documents permissions grant <document-id> --user-id <user-id> --permission reader
|
|
19
|
+
$ primitive documents permissions revoke <document-id> <user-id>
|
|
20
|
+
$ primitive documents records models <document-id>
|
|
21
|
+
$ primitive documents records describe <document-id> <model-name>
|
|
22
|
+
$ primitive documents records query <document-id> <model-name> --filter '{"status":"open"}'
|
|
23
|
+
$ primitive documents records count <document-id> <model-name>
|
|
24
|
+
$ primitive documents records save <document-id> <model-name> --data '{"qty":10}'
|
|
25
|
+
$ primitive documents records patch <document-id> <model-name> <record-id> --data '{"qty":11}'
|
|
26
|
+
$ primitive documents records delete <document-id> <model-name> <record-id> -y
|
|
27
|
+
$ primitive documents records bulk <document-id> --data-file ops.json -y
|
|
28
|
+
$ primitive documents dump <document-id>
|
|
29
|
+
$ primitive documents stats <document-id>
|
|
13
30
|
$ primitive documents transfer-owner <document-id> <new-owner-id>
|
|
14
31
|
$ primitive documents group-permissions list <document-id>
|
|
15
32
|
`);
|
|
33
|
+
// ---- List a user's documents ----
|
|
34
|
+
// No app-wide enumeration: `--user-id` is required (the admin token has no
|
|
35
|
+
// "self"). Reads the per-user permission partition via the admin endpoint.
|
|
36
|
+
documents
|
|
37
|
+
.command("list")
|
|
38
|
+
.description("List a user's documents")
|
|
39
|
+
.requiredOption("--user-id <id>", "User ID whose documents to list")
|
|
40
|
+
.option("--app <app-id>", "App ID")
|
|
41
|
+
.option("--json", "Output as JSON")
|
|
42
|
+
.action(async (options) => {
|
|
43
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
44
|
+
const client = new ApiClient();
|
|
45
|
+
try {
|
|
46
|
+
const list = await client.listAdminDocuments(resolvedAppId, options.userId);
|
|
47
|
+
if (options.json) {
|
|
48
|
+
json(list);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (list.length === 0) {
|
|
52
|
+
info("No documents found.");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
console.log(formatTable(list, [
|
|
56
|
+
{ header: "DOCUMENT_ID", key: "documentId", format: formatId },
|
|
57
|
+
{ header: "TITLE", key: "title", flex: true },
|
|
58
|
+
{ header: "PERMISSION", key: "permission" },
|
|
59
|
+
{ header: "GRANTED", key: "grantedAt", format: formatDate },
|
|
60
|
+
]));
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
error(err.message);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// ---- Get a single document's metadata ----
|
|
68
|
+
documents
|
|
69
|
+
.command("get")
|
|
70
|
+
.description("Show a document's metadata and the caller's access to it")
|
|
71
|
+
.argument("<document-id>", "Document ID")
|
|
72
|
+
.option("--app <app-id>", "App ID")
|
|
73
|
+
.option("--json", "Output as JSON")
|
|
74
|
+
.action(async (documentId, options) => {
|
|
75
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
76
|
+
const client = new ApiClient();
|
|
77
|
+
try {
|
|
78
|
+
const doc = await client.getDocument(resolvedAppId, documentId);
|
|
79
|
+
if (options.json) {
|
|
80
|
+
json(doc);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
keyValue("Document ID", doc.documentId);
|
|
84
|
+
keyValue("Title", doc.title);
|
|
85
|
+
keyValue("Created By", doc.createdBy);
|
|
86
|
+
keyValue("Created", formatDate(doc.createdAt));
|
|
87
|
+
keyValue("Modified", formatDate(doc.modifiedAt ?? doc.lastModified));
|
|
88
|
+
// `GET documents/:id` reports the caller's own access, not the
|
|
89
|
+
// document's grant/alias/collection inventory — printing counts for
|
|
90
|
+
// fields the endpoint never returns read as a definitive "0" for
|
|
91
|
+
// documents that demonstrably have them. Use
|
|
92
|
+
// `documents permissions list` for the grant list.
|
|
93
|
+
if (doc.permission)
|
|
94
|
+
keyValue("Your Permission", doc.permission);
|
|
95
|
+
if (doc.accessSource)
|
|
96
|
+
keyValue("Access Source", doc.accessSource);
|
|
97
|
+
if (doc.linkAccess)
|
|
98
|
+
keyValue("Link Access", doc.linkAccess);
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
error(err.message);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
// ---- User-level permissions subcommand group ----
|
|
106
|
+
const permissions = documents
|
|
107
|
+
.command("permissions")
|
|
108
|
+
.description("Manage user-level permissions on a document");
|
|
109
|
+
// List user-level permissions
|
|
110
|
+
permissions
|
|
111
|
+
.command("list")
|
|
112
|
+
.description("List user-level permissions for a document")
|
|
113
|
+
.argument("<document-id>", "Document ID")
|
|
114
|
+
.option("--app <app-id>", "App ID")
|
|
115
|
+
.option("--json", "Output as JSON")
|
|
116
|
+
.action(async (documentId, options) => {
|
|
117
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
118
|
+
const client = new ApiClient();
|
|
119
|
+
try {
|
|
120
|
+
const result = await client.listDocumentPermissions(resolvedAppId, documentId);
|
|
121
|
+
const list = Array.isArray(result) ? result : result?.permissions ?? [];
|
|
122
|
+
if (options.json) {
|
|
123
|
+
json(list);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (list.length === 0) {
|
|
127
|
+
info("No permissions found.");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
console.log(formatTable(list, [
|
|
131
|
+
{ header: "USER_ID", key: "userId", format: formatId },
|
|
132
|
+
{ header: "EMAIL", key: "email", flex: true },
|
|
133
|
+
{ header: "PERMISSION", key: "permission" },
|
|
134
|
+
{ header: "GRANTED", key: "grantedAt", format: formatDate },
|
|
135
|
+
]));
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
error(err.message);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// Grant (or update) a user-level permission
|
|
143
|
+
permissions
|
|
144
|
+
.command("grant")
|
|
145
|
+
.description("Grant or update a user's permission on a document")
|
|
146
|
+
.argument("<document-id>", "Document ID")
|
|
147
|
+
.option("--user-id <id>", "User ID to grant the permission to")
|
|
148
|
+
.option("--email <email>", "Email of the user to grant the permission to")
|
|
149
|
+
.requiredOption("--permission <permission>", "Permission level: reader or read-write")
|
|
150
|
+
.option("--app <app-id>", "App ID")
|
|
151
|
+
.option("--json", "Output as JSON")
|
|
152
|
+
.action(async (documentId, options) => {
|
|
153
|
+
if (!options.userId && !options.email) {
|
|
154
|
+
error("Either --user-id or --email is required.");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
if (options.userId && options.email) {
|
|
158
|
+
error("Provide only one of --user-id or --email, not both.");
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
if (!["reader", "read-write"].includes(options.permission)) {
|
|
162
|
+
error("Invalid --permission. Must be 'reader' or 'read-write'.");
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
166
|
+
const client = new ApiClient();
|
|
167
|
+
try {
|
|
168
|
+
const entry = options.userId
|
|
169
|
+
? { userId: options.userId, permission: options.permission }
|
|
170
|
+
: { email: options.email, permission: options.permission };
|
|
171
|
+
const result = await client.grantDocumentPermission(resolvedAppId, documentId, [entry]);
|
|
172
|
+
if (options.json) {
|
|
173
|
+
json(result);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const target = options.userId ? options.userId : options.email;
|
|
177
|
+
success(`Permission '${options.permission}' granted to ${target}.`);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
error(err.message);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// Revoke a user-level permission
|
|
185
|
+
permissions
|
|
186
|
+
.command("revoke")
|
|
187
|
+
.description("Revoke a user's permission on a document")
|
|
188
|
+
.argument("<document-id>", "Document ID")
|
|
189
|
+
.argument("[user-id]", "User ID to revoke (or use --email)")
|
|
190
|
+
.option("--email <email>", "Email of the user to revoke (alternative to <user-id>)")
|
|
191
|
+
.option("--app <app-id>", "App ID")
|
|
192
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
193
|
+
.action(async (documentId, userId, options) => {
|
|
194
|
+
if (!userId && !options.email) {
|
|
195
|
+
error("Either a <user-id> argument or --email is required.");
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
if (userId && options.email) {
|
|
199
|
+
error("Provide only one of <user-id> or --email, not both.");
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const target = userId ?? options.email;
|
|
203
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
204
|
+
if (!options.yes) {
|
|
205
|
+
let confirm;
|
|
206
|
+
try {
|
|
207
|
+
confirm = await confirmPrompt(`Revoke permission for ${target} on document ${documentId}?`);
|
|
208
|
+
}
|
|
209
|
+
catch (err) {
|
|
210
|
+
error(err.message);
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
if (!confirm) {
|
|
214
|
+
info("Cancelled.");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const client = new ApiClient();
|
|
219
|
+
try {
|
|
220
|
+
await client.revokeDocumentPermission(resolvedAppId, documentId, {
|
|
221
|
+
userId,
|
|
222
|
+
email: options.email,
|
|
223
|
+
});
|
|
224
|
+
success(`Permission revoked for ${target}.`);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
error(err.message);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
// ---- Records introspection subcommand group ----
|
|
232
|
+
const records = documents
|
|
233
|
+
.command("records")
|
|
234
|
+
.description("Inspect a document's models and records");
|
|
235
|
+
// List model names
|
|
236
|
+
records
|
|
237
|
+
.command("models")
|
|
238
|
+
.description("List model names in a document")
|
|
239
|
+
.argument("<document-id>", "Document ID")
|
|
240
|
+
.option("--app <app-id>", "App ID")
|
|
241
|
+
.option("--json", "Output as JSON")
|
|
242
|
+
.action(async (documentId, options) => {
|
|
243
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
244
|
+
const client = new ApiClient();
|
|
245
|
+
try {
|
|
246
|
+
const result = await client.getDocumentSchema(resolvedAppId, documentId);
|
|
247
|
+
const models = result?.modelNames ?? Object.keys(result?.schema?.models ?? {});
|
|
248
|
+
if (options.json) {
|
|
249
|
+
json(models);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (models.length === 0) {
|
|
253
|
+
info("No models found.");
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
for (const m of models) {
|
|
257
|
+
console.log(` ${m}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
error(err.message);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
// Describe a model's fields and indexes
|
|
266
|
+
records
|
|
267
|
+
.command("describe")
|
|
268
|
+
.description("Show a model's fields and indexes")
|
|
269
|
+
.argument("<document-id>", "Document ID")
|
|
270
|
+
.argument("<model-name>", "Model name")
|
|
271
|
+
.option("--app <app-id>", "App ID")
|
|
272
|
+
.option("--json", "Output as JSON")
|
|
273
|
+
.action(async (documentId, modelName, options) => {
|
|
274
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
275
|
+
const client = new ApiClient();
|
|
276
|
+
try {
|
|
277
|
+
const result = await client.getDocumentSchema(resolvedAppId, documentId);
|
|
278
|
+
const model = result?.schema?.models?.[modelName];
|
|
279
|
+
if (!model) {
|
|
280
|
+
if (options.json) {
|
|
281
|
+
json(null);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
info(`Model '${modelName}' not found in this document.`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const fieldsObj = model.fields ?? {};
|
|
288
|
+
const rows = Object.entries(fieldsObj).map(([name, meta]) => ({
|
|
289
|
+
field: name,
|
|
290
|
+
type: meta?.type ?? "unknown",
|
|
291
|
+
indexed: meta?.indexed ? "yes" : "",
|
|
292
|
+
unique: meta?.unique ? "yes" : "",
|
|
293
|
+
required: meta?.required ? "yes" : "",
|
|
294
|
+
}));
|
|
295
|
+
if (options.json) {
|
|
296
|
+
json({ fields: fieldsObj, constraints: model.constraints ?? {} });
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (rows.length === 0) {
|
|
300
|
+
info("No fields detected for this model.");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
console.log(formatTable(rows, [
|
|
304
|
+
{ header: "FIELD", key: "field", flex: true },
|
|
305
|
+
{ header: "TYPE", key: "type" },
|
|
306
|
+
{ header: "INDEXED", key: "indexed" },
|
|
307
|
+
{ header: "UNIQUE", key: "unique" },
|
|
308
|
+
{ header: "REQUIRED", key: "required" },
|
|
309
|
+
]));
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
error(err.message);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
// Query records in a document model
|
|
317
|
+
records
|
|
318
|
+
.command("query")
|
|
319
|
+
.description("Query records in a document model")
|
|
320
|
+
.argument("<document-id>", "Document ID")
|
|
321
|
+
.argument("<model-name>", "Model name to query")
|
|
322
|
+
.option("--app <app-id>", "App ID")
|
|
323
|
+
.option("--filter <json>", "Filter as JSON (e.g. '{\"status\":\"open\"}')")
|
|
324
|
+
.option("--limit <n>", "Maximum number of records to return (max 100)", parseInt)
|
|
325
|
+
.option("--cursor <cursor>", "Pagination cursor from a previous query")
|
|
326
|
+
.option("--json", "Output as JSON")
|
|
327
|
+
.action(async (documentId, modelName, options) => {
|
|
328
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
329
|
+
let filter;
|
|
330
|
+
if (options.filter) {
|
|
331
|
+
try {
|
|
332
|
+
filter = JSON.parse(options.filter);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
error("Invalid --filter JSON.");
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const client = new ApiClient();
|
|
340
|
+
try {
|
|
341
|
+
const result = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { filter, limit: options.limit, cursor: options.cursor });
|
|
342
|
+
if (options.json) {
|
|
343
|
+
json(result);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const items = result.items || [];
|
|
347
|
+
if (items.length === 0) {
|
|
348
|
+
info("No records found.");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const allKeys = new Set();
|
|
352
|
+
for (const rec of items) {
|
|
353
|
+
for (const key of Object.keys(rec))
|
|
354
|
+
allKeys.add(key);
|
|
355
|
+
}
|
|
356
|
+
const orderedKeys = [];
|
|
357
|
+
if (allKeys.has("id")) {
|
|
358
|
+
orderedKeys.push("id");
|
|
359
|
+
allKeys.delete("id");
|
|
360
|
+
}
|
|
361
|
+
orderedKeys.push(...[...allKeys].sort());
|
|
362
|
+
const columns = orderedKeys.map((key) => ({
|
|
363
|
+
header: key.toUpperCase(),
|
|
364
|
+
key,
|
|
365
|
+
format: (v) => {
|
|
366
|
+
if (v === null || v === undefined)
|
|
367
|
+
return "—";
|
|
368
|
+
if (typeof v === "object")
|
|
369
|
+
return JSON.stringify(v);
|
|
370
|
+
return String(v);
|
|
371
|
+
},
|
|
372
|
+
}));
|
|
373
|
+
console.log(formatTable(items, columns));
|
|
374
|
+
if (result.nextCursor) {
|
|
375
|
+
info(`More results available. Use --cursor ${result.nextCursor}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
error(err.message);
|
|
380
|
+
process.exit(1);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
// Count records in a document model
|
|
384
|
+
records
|
|
385
|
+
.command("count")
|
|
386
|
+
.description("Count records in a document model")
|
|
387
|
+
.argument("<document-id>", "Document ID")
|
|
388
|
+
.argument("<model-name>", "Model name to count")
|
|
389
|
+
.option("--app <app-id>", "App ID")
|
|
390
|
+
.option("--filter <json>", "Filter as JSON (e.g. '{\"status\":\"open\"}')")
|
|
391
|
+
.option("--json", "Output as JSON")
|
|
392
|
+
.action(async (documentId, modelName, options) => {
|
|
393
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
394
|
+
let filter;
|
|
395
|
+
if (options.filter) {
|
|
396
|
+
try {
|
|
397
|
+
filter = JSON.parse(options.filter);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
error("Invalid --filter JSON.");
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const client = new ApiClient();
|
|
405
|
+
try {
|
|
406
|
+
const result = await client.countDocumentRecords(resolvedAppId, documentId, modelName, { filter });
|
|
407
|
+
if (options.json) {
|
|
408
|
+
json(result);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
console.log(` ${result.count}`);
|
|
412
|
+
}
|
|
413
|
+
catch (err) {
|
|
414
|
+
error(err.message);
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
// ---- Record writes (#1964 Phase 4) ----
|
|
419
|
+
// Server-side writes through the document facade: Yjs-first, CRDT-safe,
|
|
420
|
+
// broadcast to connected clients. Requires read-write or higher on the
|
|
421
|
+
// document (or an admin token). `--data` / `--data-file` are validated
|
|
422
|
+
// CLI-side before any request is sent.
|
|
423
|
+
// `--data`/`--data-file` parsing (including the object-shape check and the
|
|
424
|
+
// both-flags rule) is the shared `parseDataOption` helper, so these verbs
|
|
425
|
+
// behave and diagnose identically to `metadata` and `databases records`.
|
|
426
|
+
// Create or replace a record
|
|
427
|
+
records
|
|
428
|
+
.command("save")
|
|
429
|
+
.description("Create or replace a record in a document model")
|
|
430
|
+
.argument("<document-id>", "Document ID")
|
|
431
|
+
.argument("<model-name>", "Model name to write to")
|
|
432
|
+
.option("--app <app-id>", "App ID")
|
|
433
|
+
.option("--id <record-id>", "Record ID (a ULID is generated when omitted)")
|
|
434
|
+
.option("--data <json>", "Record fields as JSON (e.g. '{\"qty\":10}')")
|
|
435
|
+
.option("--data-file <file>", "Read record fields from a JSON file")
|
|
436
|
+
.option("--upsert-on <field>", "Update the record whose <field> matches instead of creating")
|
|
437
|
+
.option("--json", "Output as JSON")
|
|
438
|
+
.action(async (documentId, modelName, options) => {
|
|
439
|
+
const data = parseDataOption(options, "record fields");
|
|
440
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
441
|
+
const recordId = options.id || ulid();
|
|
442
|
+
const writeOptions = options.upsertOn ? { upsertOn: options.upsertOn } : undefined;
|
|
443
|
+
const client = new ApiClient();
|
|
444
|
+
try {
|
|
445
|
+
const result = await client.saveDocumentRecord(resolvedAppId, documentId, modelName, { id: recordId, data, ...(writeOptions ? { options: writeOptions } : {}) });
|
|
446
|
+
if (options.json) {
|
|
447
|
+
json(result);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
success(`Record saved: ${result.record?.id ?? recordId}`);
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
error(err.message);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
// Merge fields into an existing record
|
|
458
|
+
records
|
|
459
|
+
.command("patch")
|
|
460
|
+
.description("Merge fields into an existing record in a document model")
|
|
461
|
+
.argument("<document-id>", "Document ID")
|
|
462
|
+
.argument("<model-name>", "Model name")
|
|
463
|
+
.argument("<record-id>", "Record ID to patch")
|
|
464
|
+
.option("--app <app-id>", "App ID")
|
|
465
|
+
.option("--data <json>", "Fields to merge as JSON (e.g. '{\"status\":\"closed\"}')")
|
|
466
|
+
.option("--data-file <file>", "Read fields from a JSON file")
|
|
467
|
+
.option("--json", "Output as JSON")
|
|
468
|
+
.action(async (documentId, modelName, recordId, options) => {
|
|
469
|
+
const data = parseDataOption(options, "fields to merge");
|
|
470
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
471
|
+
const client = new ApiClient();
|
|
472
|
+
try {
|
|
473
|
+
const result = await client.patchDocumentRecord(resolvedAppId, documentId, modelName, recordId, { data });
|
|
474
|
+
if (options.json) {
|
|
475
|
+
json(result);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
success(`Record patched: ${recordId}`);
|
|
479
|
+
}
|
|
480
|
+
catch (err) {
|
|
481
|
+
error(err.message);
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
// Delete a record
|
|
486
|
+
records
|
|
487
|
+
.command("delete")
|
|
488
|
+
.description("Delete a record from a document model")
|
|
489
|
+
.argument("<document-id>", "Document ID")
|
|
490
|
+
.argument("<model-name>", "Model name")
|
|
491
|
+
.argument("<record-id>", "Record ID to delete")
|
|
492
|
+
.option("--app <app-id>", "App ID")
|
|
493
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
494
|
+
.option("--json", "Output as JSON")
|
|
495
|
+
.action(async (documentId, modelName, recordId, options) => {
|
|
496
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
497
|
+
if (!options.yes) {
|
|
498
|
+
const confirmed = await confirmPrompt(`Delete record ${recordId} from ${modelName} in document ${documentId}?`);
|
|
499
|
+
if (!confirmed) {
|
|
500
|
+
info("Cancelled.");
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const client = new ApiClient();
|
|
505
|
+
try {
|
|
506
|
+
const result = await client.deleteDocumentRecord(resolvedAppId, documentId, modelName, recordId);
|
|
507
|
+
if (options.json) {
|
|
508
|
+
json(result);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
// A delete of a missing record is a silent no-op server-side, so
|
|
512
|
+
// this reports the request outcome, not record existence.
|
|
513
|
+
success(`Record deleted: ${recordId}`);
|
|
514
|
+
}
|
|
515
|
+
catch (err) {
|
|
516
|
+
error(err.message);
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
// Apply an atomic multi-model operations blob
|
|
521
|
+
records
|
|
522
|
+
.command("bulk")
|
|
523
|
+
.description("Apply an atomic create/patch/delete operations blob to a document")
|
|
524
|
+
.argument("<document-id>", "Document ID")
|
|
525
|
+
.requiredOption("--data-file <file>", "JSON file with { operations: [...] } or a bare operations array")
|
|
526
|
+
.option("--app <app-id>", "App ID")
|
|
527
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
528
|
+
.option("--json", "Output as JSON")
|
|
529
|
+
.action(async (documentId, options) => {
|
|
530
|
+
const parsed = parseDataOption(options, "operations blob", {
|
|
531
|
+
allowArray: true,
|
|
532
|
+
});
|
|
533
|
+
const operations = Array.isArray(parsed) ? parsed : parsed?.operations;
|
|
534
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
535
|
+
error("--data-file must contain { operations: [...] } or a non-empty operations array.");
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
539
|
+
if (!options.yes) {
|
|
540
|
+
const confirmed = await confirmPrompt(`Apply ${operations.length} operation(s) atomically to document ${documentId}?`);
|
|
541
|
+
if (!confirmed) {
|
|
542
|
+
info("Cancelled.");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const client = new ApiClient();
|
|
547
|
+
try {
|
|
548
|
+
const result = await client.bulkDocumentRecords(resolvedAppId, documentId, operations);
|
|
549
|
+
if (options.json) {
|
|
550
|
+
json(result);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
success(`Applied ${result.applied} operation(s): ` +
|
|
554
|
+
`${result.added.length} added, ${result.updated.length} updated, ${result.deleted} deleted.`);
|
|
555
|
+
}
|
|
556
|
+
catch (err) {
|
|
557
|
+
error(err.message);
|
|
558
|
+
process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
// Dump a document's records grouped by model (CLI-side composition)
|
|
562
|
+
documents
|
|
563
|
+
.command("dump")
|
|
564
|
+
.description("Dump a document's records grouped by model as JSON")
|
|
565
|
+
.argument("<document-id>", "Document ID")
|
|
566
|
+
.option("--app <app-id>", "App ID")
|
|
567
|
+
.option("--output <file>", "Write the JSON dump to a file instead of stdout")
|
|
568
|
+
.option("--json", "Output as JSON (default)")
|
|
569
|
+
.action(async (documentId, options) => {
|
|
570
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
571
|
+
const client = new ApiClient();
|
|
572
|
+
try {
|
|
573
|
+
// Model names come from the schema; records are assembled from paged
|
|
574
|
+
// reads per model (not a single atomic snapshot — the paged path is
|
|
575
|
+
// the contract, same pattern as `databases export`).
|
|
576
|
+
const schema = await client.getDocumentSchema(resolvedAppId, documentId);
|
|
577
|
+
const modelNames = schema?.modelNames ?? Object.keys(schema?.schema?.models ?? {});
|
|
578
|
+
const grouped = {};
|
|
579
|
+
for (const modelName of modelNames) {
|
|
580
|
+
const records = [];
|
|
581
|
+
let cursor;
|
|
582
|
+
do {
|
|
583
|
+
const page = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { limit: 100, cursor });
|
|
584
|
+
records.push(...(page.items || []));
|
|
585
|
+
cursor = page.nextCursor;
|
|
586
|
+
} while (cursor);
|
|
587
|
+
grouped[modelName] = records;
|
|
588
|
+
}
|
|
589
|
+
const output = JSON.stringify(grouped, null, 2);
|
|
590
|
+
if (options.output) {
|
|
591
|
+
fs.writeFileSync(options.output, output);
|
|
592
|
+
success(`Document dump written to ${options.output}`);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
console.log(output);
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
error(err.message);
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
// Document statistics (platform-vocabulary projection)
|
|
603
|
+
documents
|
|
604
|
+
.command("stats")
|
|
605
|
+
.description("Show a document's record/model/blob counts and approximate size")
|
|
606
|
+
.argument("<document-id>", "Document ID")
|
|
607
|
+
.option("--app <app-id>", "App ID")
|
|
608
|
+
.option("--json", "Output as JSON")
|
|
609
|
+
.action(async (documentId, options) => {
|
|
610
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
611
|
+
const client = new ApiClient();
|
|
612
|
+
try {
|
|
613
|
+
const result = await client.getDocumentStats(resolvedAppId, documentId);
|
|
614
|
+
if (options.json) {
|
|
615
|
+
json(result);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
keyValue("Document ID", result.documentId);
|
|
619
|
+
keyValue("Records", result.recordCount);
|
|
620
|
+
keyValue("Models", result.modelCount);
|
|
621
|
+
keyValue("Blobs", result.blobCount);
|
|
622
|
+
keyValue("Size (bytes, approx)", result.sizeBytes);
|
|
623
|
+
keyValue("Last modified", result.lastModifiedAt ? formatDate(result.lastModifiedAt) : "—");
|
|
624
|
+
}
|
|
625
|
+
catch (err) {
|
|
626
|
+
error(err.message);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
}
|
|
629
|
+
});
|
|
16
630
|
// Transfer document ownership
|
|
17
631
|
documents
|
|
18
632
|
.command("transfer-owner")
|