primitive-admin 1.0.21 → 1.0.23

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.
@@ -0,0 +1,645 @@
1
+ import { ApiClient } from "../lib/api-client.js";
2
+ import { getCurrentAppId } from "../lib/config.js";
3
+ import { success, error, info, keyValue, formatTable, formatId, formatDate, json, } from "../lib/output.js";
4
+ function resolveAppId(appId, options) {
5
+ const resolved = appId || options.app || getCurrentAppId();
6
+ if (!resolved) {
7
+ error("No app specified. Use <app-id>, --app, or 'primitive use <app-id>' to set context.");
8
+ process.exit(1);
9
+ }
10
+ return resolved;
11
+ }
12
+ export function registerDatabasesCommands(program) {
13
+ const databases = program
14
+ .command("databases")
15
+ .description("Create, manage, and configure online databases")
16
+ .addHelpText("after", `
17
+ Examples:
18
+ $ primitive databases list
19
+ $ primitive databases create "My Database"
20
+ $ primitive databases get <database-id>
21
+ $ primitive databases records models <database-id>
22
+ $ primitive databases records describe <database-id> <model-name>
23
+ $ primitive databases operations list <database-id>
24
+ $ primitive databases operations execute <database-id> <operation-name> --params '{"key":"value"}'
25
+ $ primitive databases indexes list <database-id>
26
+ $ primitive databases indexes create <database-id> --model contacts --field email
27
+ $ primitive databases permissions list <database-id>
28
+ `);
29
+ // List databases
30
+ databases
31
+ .command("list")
32
+ .description("List databases in an app")
33
+ .argument("[app-id]", "App ID (uses current app if not specified)")
34
+ .option("--app <app-id>", "App ID")
35
+ .option("--json", "Output as JSON")
36
+ .action(async (appId, options) => {
37
+ const resolvedAppId = resolveAppId(appId, options);
38
+ const client = new ApiClient();
39
+ try {
40
+ const result = await client.listDatabases(resolvedAppId);
41
+ const list = Array.isArray(result) ? result : result?.databases ?? [];
42
+ if (options.json) {
43
+ json(list);
44
+ return;
45
+ }
46
+ if (list.length === 0) {
47
+ info("No databases found.");
48
+ return;
49
+ }
50
+ console.log(formatTable(list, [
51
+ { header: "ID", key: "databaseId", format: formatId },
52
+ { header: "TITLE", key: "title" },
53
+ { header: "TYPE", key: "databaseType", format: (v) => v || "—" },
54
+ { header: "PERMISSION", key: "permission" },
55
+ { header: "CREATED", key: "createdAt", format: formatDate },
56
+ ]));
57
+ }
58
+ catch (err) {
59
+ error(err.message);
60
+ process.exit(1);
61
+ }
62
+ });
63
+ // Create database
64
+ databases
65
+ .command("create")
66
+ .description("Create a new database")
67
+ .argument("<title>", "Database title")
68
+ .option("--app <app-id>", "App ID")
69
+ .requiredOption("--type <database-type>", "Database type (required)")
70
+ .option("--json", "Output as JSON")
71
+ .action(async (title, options) => {
72
+ const resolvedAppId = resolveAppId(undefined, options);
73
+ const client = new ApiClient();
74
+ try {
75
+ const params = { title, databaseType: options.type };
76
+ const result = await client.createDatabase(resolvedAppId, params);
77
+ if (options.json) {
78
+ json(result);
79
+ return;
80
+ }
81
+ success("Database created.");
82
+ keyValue("Database ID", result.databaseId);
83
+ keyValue("Title", result.title);
84
+ if (result.databaseType) {
85
+ keyValue("Database Type", result.databaseType);
86
+ }
87
+ }
88
+ catch (err) {
89
+ error(err.message);
90
+ process.exit(1);
91
+ }
92
+ });
93
+ // Get database
94
+ databases
95
+ .command("get")
96
+ .description("Get database details")
97
+ .argument("<database-id>", "Database ID")
98
+ .option("--app <app-id>", "App ID")
99
+ .option("--json", "Output as JSON")
100
+ .action(async (databaseId, options) => {
101
+ const resolvedAppId = resolveAppId(undefined, options);
102
+ const client = new ApiClient();
103
+ try {
104
+ const result = await client.getDatabase(resolvedAppId, databaseId);
105
+ if (options.json) {
106
+ json(result);
107
+ return;
108
+ }
109
+ keyValue("Database ID", result.databaseId);
110
+ keyValue("Title", result.title);
111
+ keyValue("Database Type", result.databaseType || "none");
112
+ keyValue("Permission", result.permission || "none");
113
+ keyValue("Created", formatDate(result.createdAt));
114
+ }
115
+ catch (err) {
116
+ error(err.message);
117
+ process.exit(1);
118
+ }
119
+ });
120
+ // Update database
121
+ databases
122
+ .command("update")
123
+ .description("Update a database (title, type)")
124
+ .argument("<database-id>", "Database ID")
125
+ .option("--app <app-id>", "App ID")
126
+ .option("--title <title>", "New title")
127
+ .option("--type <database-type>", "Database type (use 'none' to clear)")
128
+ .option("--json", "Output as JSON")
129
+ .action(async (databaseId, options) => {
130
+ const resolvedAppId = resolveAppId(undefined, options);
131
+ const client = new ApiClient();
132
+ const params = {};
133
+ if (options.title) {
134
+ params.title = options.title;
135
+ }
136
+ if (options.type !== undefined) {
137
+ params.databaseType =
138
+ options.type === "none"
139
+ ? null
140
+ : options.type;
141
+ }
142
+ if (Object.keys(params).length === 0) {
143
+ error("Provide at least one of --title or --type.");
144
+ process.exit(1);
145
+ }
146
+ try {
147
+ const result = await client.updateDatabase(resolvedAppId, databaseId, params);
148
+ if (options.json) {
149
+ json(result);
150
+ return;
151
+ }
152
+ success("Database updated.");
153
+ keyValue("Database ID", result.databaseId);
154
+ keyValue("Title", result.title);
155
+ keyValue("Database Type", result.databaseType || "none");
156
+ }
157
+ catch (err) {
158
+ error(err.message);
159
+ process.exit(1);
160
+ }
161
+ });
162
+ // Delete database
163
+ databases
164
+ .command("delete")
165
+ .description("Delete a database")
166
+ .argument("<database-id>", "Database ID")
167
+ .option("--app <app-id>", "App ID")
168
+ .option("-y, --yes", "Skip confirmation prompt")
169
+ .action(async (databaseId, options) => {
170
+ const resolvedAppId = resolveAppId(undefined, options);
171
+ if (!options.yes) {
172
+ const inquirer = await import("inquirer");
173
+ const { confirm } = await inquirer.default.prompt([
174
+ {
175
+ type: "confirm",
176
+ name: "confirm",
177
+ message: `Delete database ${databaseId}? This cannot be undone.`,
178
+ default: false,
179
+ },
180
+ ]);
181
+ if (!confirm) {
182
+ info("Cancelled.");
183
+ return;
184
+ }
185
+ }
186
+ const client = new ApiClient();
187
+ try {
188
+ await client.deleteDatabase(resolvedAppId, databaseId);
189
+ success(`Database ${databaseId} deleted.`);
190
+ }
191
+ catch (err) {
192
+ error(err.message);
193
+ process.exit(1);
194
+ }
195
+ });
196
+ // ---- Permissions subcommand group ----
197
+ const permissions = databases
198
+ .command("permissions")
199
+ .description("Manage user permissions on a database");
200
+ // List permissions
201
+ permissions
202
+ .command("list")
203
+ .description("List user permissions for a database")
204
+ .argument("<database-id>", "Database ID")
205
+ .option("--app <app-id>", "App ID")
206
+ .option("--json", "Output as JSON")
207
+ .action(async (databaseId, options) => {
208
+ const resolvedAppId = resolveAppId(undefined, options);
209
+ const client = new ApiClient();
210
+ try {
211
+ const result = await client.listDatabasePermissions(resolvedAppId, databaseId);
212
+ const list = Array.isArray(result) ? result : result?.permissions ?? [];
213
+ if (options.json) {
214
+ json(list);
215
+ return;
216
+ }
217
+ if (list.length === 0) {
218
+ info("No permissions found.");
219
+ return;
220
+ }
221
+ console.log(formatTable(list, [
222
+ { header: "USER_ID", key: "userId", format: formatId },
223
+ { header: "EMAIL", key: "userEmail" },
224
+ { header: "PERMISSION", key: "permission" },
225
+ { header: "GRANTED", key: "grantedAt", format: formatDate },
226
+ ]));
227
+ }
228
+ catch (err) {
229
+ error(err.message);
230
+ process.exit(1);
231
+ }
232
+ });
233
+ // Grant permission
234
+ permissions
235
+ .command("grant")
236
+ .description("Grant a user permission on a database")
237
+ .argument("<database-id>", "Database ID")
238
+ .requiredOption("--user-id <user-id>", "User ID to grant permission to")
239
+ .requiredOption("--permission <permission>", "Permission level: manager")
240
+ .option("--app <app-id>", "App ID")
241
+ .option("--json", "Output as JSON")
242
+ .action(async (databaseId, options) => {
243
+ const resolvedAppId = resolveAppId(undefined, options);
244
+ const client = new ApiClient();
245
+ try {
246
+ const result = await client.grantDatabasePermission(resolvedAppId, databaseId, {
247
+ userId: options.userId,
248
+ permission: options.permission,
249
+ });
250
+ if (options.json) {
251
+ json(result);
252
+ return;
253
+ }
254
+ success(`Permission '${options.permission}' granted to user ${options.userId}.`);
255
+ }
256
+ catch (err) {
257
+ error(err.message);
258
+ process.exit(1);
259
+ }
260
+ });
261
+ // Revoke permission
262
+ permissions
263
+ .command("revoke")
264
+ .description("Revoke a user's permission on a database")
265
+ .argument("<database-id>", "Database ID")
266
+ .argument("<user-id>", "User ID to revoke")
267
+ .option("--app <app-id>", "App ID")
268
+ .option("-y, --yes", "Skip confirmation prompt")
269
+ .action(async (databaseId, userId, options) => {
270
+ const resolvedAppId = resolveAppId(undefined, options);
271
+ if (!options.yes) {
272
+ const inquirer = await import("inquirer");
273
+ const { confirm } = await inquirer.default.prompt([
274
+ {
275
+ type: "confirm",
276
+ name: "confirm",
277
+ message: `Revoke permission for user ${userId} on database ${databaseId}?`,
278
+ default: false,
279
+ },
280
+ ]);
281
+ if (!confirm) {
282
+ info("Cancelled.");
283
+ return;
284
+ }
285
+ }
286
+ const client = new ApiClient();
287
+ try {
288
+ await client.revokeDatabasePermission(resolvedAppId, databaseId, userId);
289
+ success(`Permission revoked for user ${userId}.`);
290
+ }
291
+ catch (err) {
292
+ error(err.message);
293
+ process.exit(1);
294
+ }
295
+ });
296
+ // ---- Records subcommand group (models & describe only) ----
297
+ const records = databases
298
+ .command("records")
299
+ .description("Inspect models and schemas in a database");
300
+ // List models
301
+ records
302
+ .command("models")
303
+ .description("List model names in a database")
304
+ .argument("<database-id>", "Database ID")
305
+ .option("--app <app-id>", "App ID")
306
+ .option("--json", "Output as JSON")
307
+ .action(async (databaseId, options) => {
308
+ const resolvedAppId = resolveAppId(undefined, options);
309
+ const client = new ApiClient();
310
+ try {
311
+ const result = await client.listDatabaseModels(resolvedAppId, databaseId);
312
+ const models = result.models || [];
313
+ if (options.json) {
314
+ json(models);
315
+ return;
316
+ }
317
+ if (models.length === 0) {
318
+ info("No models found.");
319
+ return;
320
+ }
321
+ for (const m of models) {
322
+ console.log(` ${m}`);
323
+ }
324
+ }
325
+ catch (err) {
326
+ error(err.message);
327
+ process.exit(1);
328
+ }
329
+ });
330
+ // Describe model
331
+ records
332
+ .command("describe")
333
+ .description("Show inferred schema for a model")
334
+ .argument("<database-id>", "Database ID")
335
+ .argument("<model-name>", "Model name")
336
+ .option("--app <app-id>", "App ID")
337
+ .option("--json", "Output as JSON")
338
+ .action(async (databaseId, modelName, options) => {
339
+ const resolvedAppId = resolveAppId(undefined, options);
340
+ const client = new ApiClient();
341
+ try {
342
+ const result = await client.describeDatabaseModel(resolvedAppId, databaseId, modelName);
343
+ const fields = result.fields || [];
344
+ if (options.json) {
345
+ json(fields);
346
+ return;
347
+ }
348
+ if (fields.length === 0) {
349
+ info("No fields detected.");
350
+ return;
351
+ }
352
+ console.log(formatTable(fields, [
353
+ { header: "FIELD", key: "field_name" },
354
+ { header: "TYPE", key: "inferred_type" },
355
+ ]));
356
+ }
357
+ catch (err) {
358
+ error(err.message);
359
+ process.exit(1);
360
+ }
361
+ });
362
+ // ---- Metadata subcommand group ----
363
+ const metadata = databases
364
+ .command("metadata")
365
+ .description("Manage database metadata");
366
+ metadata
367
+ .command("update")
368
+ .description("Update database metadata (merge with existing)")
369
+ .argument("<database-id>", "Database ID")
370
+ .requiredOption("--data <json>", "Metadata fields as JSON (merged with existing)")
371
+ .option("--app <app-id>", "App ID")
372
+ .option("--json", "Output as JSON")
373
+ .action(async (databaseId, options) => {
374
+ const resolvedAppId = resolveAppId(undefined, options);
375
+ const client = new ApiClient();
376
+ let data;
377
+ try {
378
+ data = JSON.parse(options.data);
379
+ }
380
+ catch {
381
+ error("Invalid --data JSON.");
382
+ process.exit(1);
383
+ return;
384
+ }
385
+ try {
386
+ const result = await client.updateDatabaseMetadata(resolvedAppId, databaseId, data);
387
+ if (options.json) {
388
+ json(result);
389
+ return;
390
+ }
391
+ success("Metadata updated.");
392
+ if (result.metadata) {
393
+ keyValue("Metadata", JSON.stringify(result.metadata));
394
+ }
395
+ }
396
+ catch (err) {
397
+ error(err.message);
398
+ process.exit(1);
399
+ }
400
+ });
401
+ // ---- Operations subcommand group ----
402
+ const operations = databases
403
+ .command("operations")
404
+ .description("List and execute registered operations on a database");
405
+ // List operations
406
+ operations
407
+ .command("list")
408
+ .description("List registered operations available on a database")
409
+ .argument("<database-id>", "Database ID")
410
+ .option("--app <app-id>", "App ID")
411
+ .option("--json", "Output as JSON")
412
+ .action(async (databaseId, options) => {
413
+ const resolvedAppId = resolveAppId(undefined, options);
414
+ const client = new ApiClient();
415
+ try {
416
+ const result = await client.listDatabaseOperations(resolvedAppId, databaseId);
417
+ const list = Array.isArray(result) ? result : [];
418
+ if (options.json) {
419
+ json(list);
420
+ return;
421
+ }
422
+ if (list.length === 0) {
423
+ info("No operations found.");
424
+ return;
425
+ }
426
+ console.log(formatTable(list, [
427
+ { header: "NAME", key: "name" },
428
+ { header: "TYPE", key: "type" },
429
+ { header: "MODEL", key: "modelName" },
430
+ { header: "ACCESS", key: "access" },
431
+ ]));
432
+ }
433
+ catch (err) {
434
+ error(err.message);
435
+ process.exit(1);
436
+ }
437
+ });
438
+ // Execute operation
439
+ operations
440
+ .command("execute")
441
+ .description("Execute a registered operation on a database")
442
+ .argument("<database-id>", "Database ID")
443
+ .argument("<operation-name>", "Operation name")
444
+ .option("--params <json>", "Operation parameters as JSON")
445
+ .option("--limit <n>", "Max records to return (for queries)")
446
+ .option("--cursor <cursor>", "Pagination cursor")
447
+ .option("--token <jwt>", "Execute as a specific user (dev/test — JWT visible in process args)")
448
+ .option("--app <app-id>", "App ID")
449
+ .option("--json", "Output as JSON")
450
+ .option("--timing", "Show server-side timing breakdown")
451
+ .action(async (databaseId, operationName, options) => {
452
+ const resolvedAppId = resolveAppId(undefined, options);
453
+ const client = new ApiClient();
454
+ let params;
455
+ if (options.params) {
456
+ try {
457
+ params = JSON.parse(options.params);
458
+ }
459
+ catch {
460
+ error("Invalid --params JSON.");
461
+ process.exit(1);
462
+ return;
463
+ }
464
+ }
465
+ const body = {};
466
+ if (params)
467
+ body.params = params;
468
+ if (options.limit)
469
+ body.limit = parseInt(options.limit, 10);
470
+ if (options.cursor)
471
+ body.cursor = options.cursor;
472
+ try {
473
+ const result = await client.executeDatabaseOperation(resolvedAppId, databaseId, operationName, body, options.token, { timing: !!options.timing });
474
+ if (options.json) {
475
+ json(result);
476
+ return;
477
+ }
478
+ // Smart display based on result shape
479
+ if (result.data && Array.isArray(result.data)) {
480
+ if (result.data.length === 0) {
481
+ info("No records found.");
482
+ }
483
+ else {
484
+ const cols = Object.keys(result.data[0]).filter((k) => k !== "type");
485
+ console.log(formatTable(result.data, cols.map((c) => ({
486
+ header: c.toUpperCase(),
487
+ key: c,
488
+ format: (v) => {
489
+ if (v === null || v === undefined)
490
+ return "—";
491
+ if (typeof v === "object") {
492
+ const s = JSON.stringify(v);
493
+ return s.length > 50 ? s.slice(0, 47) + "..." : s;
494
+ }
495
+ const s = String(v);
496
+ return s.length > 50 ? s.slice(0, 47) + "..." : s;
497
+ },
498
+ }))));
499
+ }
500
+ if (result.hasMore) {
501
+ info(`More records available. Use --cursor ${result.cursor} to continue.`);
502
+ }
503
+ }
504
+ else if (result.count !== undefined) {
505
+ console.log(`Count: ${result.count}`);
506
+ }
507
+ else if (result.result !== undefined) {
508
+ // Aggregate result
509
+ json(result);
510
+ }
511
+ else if (result.results && Array.isArray(result.results)) {
512
+ // Batch/mutation results
513
+ success(`Operation executed (${result.results.length} result${result.results.length === 1 ? "" : "s"}).`);
514
+ }
515
+ else if (result.success !== undefined) {
516
+ success("Operation executed.");
517
+ }
518
+ else {
519
+ json(result);
520
+ }
521
+ // Display timing if present
522
+ if (result._timing) {
523
+ console.log("");
524
+ console.log("Timing:");
525
+ for (const [key, value] of Object.entries(result._timing)) {
526
+ console.log(` ${key}: ${value}ms`);
527
+ }
528
+ }
529
+ }
530
+ catch (err) {
531
+ error(err.message);
532
+ process.exit(1);
533
+ }
534
+ });
535
+ // ---- Indexes subcommand group ----
536
+ const indexes = databases
537
+ .command("indexes")
538
+ .description("Manage indexes on a database");
539
+ // List indexes
540
+ indexes
541
+ .command("list")
542
+ .description("List indexes for a database or model")
543
+ .argument("<database-id>", "Database ID")
544
+ .option("--model <model-name>", "Filter by model name")
545
+ .option("--app <app-id>", "App ID")
546
+ .option("--json", "Output as JSON")
547
+ .action(async (databaseId, options) => {
548
+ const resolvedAppId = resolveAppId(undefined, options);
549
+ const client = new ApiClient();
550
+ try {
551
+ const result = await client.listDatabaseIndexes(resolvedAppId, databaseId, options.model);
552
+ const list = result.indexes || [];
553
+ if (options.json) {
554
+ json(list);
555
+ return;
556
+ }
557
+ if (list.length === 0) {
558
+ info("No indexes found.");
559
+ return;
560
+ }
561
+ console.log(formatTable(list, [
562
+ { header: "MODEL", key: "model_name" },
563
+ { header: "FIELD", key: "field_name" },
564
+ { header: "TYPE", key: "field_type" },
565
+ { header: "UNIQUE", key: "is_unique", format: (v) => (v ? "Yes" : "No") },
566
+ ]));
567
+ }
568
+ catch (err) {
569
+ error(err.message);
570
+ process.exit(1);
571
+ }
572
+ });
573
+ // Register index
574
+ indexes
575
+ .command("create")
576
+ .description("Create an index on a model field")
577
+ .argument("<database-id>", "Database ID")
578
+ .requiredOption("--model <model-name>", "Model name")
579
+ .requiredOption("--field <field-name>", "Field name to index")
580
+ .option("--type <field-type>", "Field type: string, number, or boolean", "string")
581
+ .option("--unique", "Make this a unique index")
582
+ .option("--app <app-id>", "App ID")
583
+ .option("--json", "Output as JSON")
584
+ .action(async (databaseId, options) => {
585
+ const resolvedAppId = resolveAppId(undefined, options);
586
+ const client = new ApiClient();
587
+ try {
588
+ const result = await client.registerDatabaseIndex(resolvedAppId, databaseId, {
589
+ modelName: options.model,
590
+ fieldName: options.field,
591
+ fieldType: options.type,
592
+ unique: !!options.unique,
593
+ });
594
+ if (options.json) {
595
+ json(result);
596
+ return;
597
+ }
598
+ success(`Index created on ${options.model}.${options.field} (${options.type}${options.unique ? ", unique" : ""}).`);
599
+ }
600
+ catch (err) {
601
+ error(err.message);
602
+ process.exit(1);
603
+ }
604
+ });
605
+ // Drop index
606
+ indexes
607
+ .command("drop")
608
+ .description("Drop an index from a model field")
609
+ .argument("<database-id>", "Database ID")
610
+ .requiredOption("--model <model-name>", "Model name")
611
+ .requiredOption("--field <field-name>", "Field name")
612
+ .option("--app <app-id>", "App ID")
613
+ .option("-y, --yes", "Skip confirmation prompt")
614
+ .action(async (databaseId, options) => {
615
+ const resolvedAppId = resolveAppId(undefined, options);
616
+ if (!options.yes) {
617
+ const inquirer = await import("inquirer");
618
+ const { confirm } = await inquirer.default.prompt([
619
+ {
620
+ type: "confirm",
621
+ name: "confirm",
622
+ message: `Drop index on ${options.model}.${options.field}?`,
623
+ default: false,
624
+ },
625
+ ]);
626
+ if (!confirm) {
627
+ info("Cancelled.");
628
+ return;
629
+ }
630
+ }
631
+ const client = new ApiClient();
632
+ try {
633
+ await client.dropDatabaseIndex(resolvedAppId, databaseId, {
634
+ modelName: options.model,
635
+ fieldName: options.field,
636
+ });
637
+ success(`Index dropped on ${options.model}.${options.field}.`);
638
+ }
639
+ catch (err) {
640
+ error(err.message);
641
+ process.exit(1);
642
+ }
643
+ });
644
+ }
645
+ //# sourceMappingURL=databases.js.map