primitive-admin 1.0.21 → 1.0.22
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/dist/src/commands/databases.js +768 -0
- package/dist/src/commands/databases.js.map +1 -0
- package/dist/src/commands/groups.js +432 -0
- package/dist/src/commands/groups.js.map +1 -0
- package/dist/src/commands/rule-sets.js +274 -0
- package/dist/src/commands/rule-sets.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,768 @@
|
|
|
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 query <database-id> <model-name>
|
|
23
|
+
$ primitive databases records save <database-id> <model-name> --data '{"name":"Alice"}'
|
|
24
|
+
$ primitive databases indexes list <database-id>
|
|
25
|
+
$ primitive databases indexes create <database-id> --model contacts --field email
|
|
26
|
+
$ primitive databases permissions list <database-id>
|
|
27
|
+
$ primitive databases group-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: "DEFAULT PERM", key: "defaultPermission", 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
|
+
.option("--default-permission <level>", "Default permission for all app users: reader or read-write")
|
|
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 };
|
|
76
|
+
if (options.defaultPermission) {
|
|
77
|
+
params.defaultPermission = options.defaultPermission;
|
|
78
|
+
}
|
|
79
|
+
const result = await client.createDatabase(resolvedAppId, params);
|
|
80
|
+
if (options.json) {
|
|
81
|
+
json(result);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
success("Database created.");
|
|
85
|
+
keyValue("Database ID", result.databaseId);
|
|
86
|
+
keyValue("Title", result.title);
|
|
87
|
+
if (result.defaultPermission) {
|
|
88
|
+
keyValue("Default Permission", result.defaultPermission);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
error(err.message);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// Get database
|
|
97
|
+
databases
|
|
98
|
+
.command("get")
|
|
99
|
+
.description("Get database details")
|
|
100
|
+
.argument("<database-id>", "Database ID")
|
|
101
|
+
.option("--app <app-id>", "App ID")
|
|
102
|
+
.option("--json", "Output as JSON")
|
|
103
|
+
.action(async (databaseId, options) => {
|
|
104
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
105
|
+
const client = new ApiClient();
|
|
106
|
+
try {
|
|
107
|
+
const result = await client.getDatabase(resolvedAppId, databaseId);
|
|
108
|
+
if (options.json) {
|
|
109
|
+
json(result);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
keyValue("Database ID", result.databaseId);
|
|
113
|
+
keyValue("Title", result.title);
|
|
114
|
+
keyValue("Default Permission", result.defaultPermission || "none");
|
|
115
|
+
keyValue("Rule Set", result.ruleSetId || "none");
|
|
116
|
+
keyValue("Permission", result.permission);
|
|
117
|
+
keyValue("Created", formatDate(result.createdAt));
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
error(err.message);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
// Update database
|
|
125
|
+
databases
|
|
126
|
+
.command("update")
|
|
127
|
+
.description("Update a database (title, default permission, rule set)")
|
|
128
|
+
.argument("<database-id>", "Database ID")
|
|
129
|
+
.option("--app <app-id>", "App ID")
|
|
130
|
+
.option("--title <title>", "New title")
|
|
131
|
+
.option("--default-permission <level>", "Default permission for all app users: reader, read-write, or none")
|
|
132
|
+
.option("--rule-set-id <id>", "Attach a rule set (use 'none' to detach)")
|
|
133
|
+
.option("--json", "Output as JSON")
|
|
134
|
+
.action(async (databaseId, options) => {
|
|
135
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
136
|
+
const client = new ApiClient();
|
|
137
|
+
const params = {};
|
|
138
|
+
if (options.title) {
|
|
139
|
+
params.title = options.title;
|
|
140
|
+
}
|
|
141
|
+
if (options.defaultPermission !== undefined) {
|
|
142
|
+
params.defaultPermission =
|
|
143
|
+
options.defaultPermission === "none"
|
|
144
|
+
? null
|
|
145
|
+
: options.defaultPermission;
|
|
146
|
+
}
|
|
147
|
+
if (options.ruleSetId !== undefined) {
|
|
148
|
+
params.ruleSetId =
|
|
149
|
+
options.ruleSetId === "none"
|
|
150
|
+
? null
|
|
151
|
+
: options.ruleSetId;
|
|
152
|
+
}
|
|
153
|
+
if (Object.keys(params).length === 0) {
|
|
154
|
+
error("Provide at least one of --title, --default-permission, or --rule-set-id.");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const result = await client.updateDatabase(resolvedAppId, databaseId, params);
|
|
159
|
+
if (options.json) {
|
|
160
|
+
json(result);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
success("Database updated.");
|
|
164
|
+
keyValue("Database ID", result.databaseId);
|
|
165
|
+
keyValue("Title", result.title);
|
|
166
|
+
keyValue("Default Permission", result.defaultPermission || "none");
|
|
167
|
+
keyValue("Rule Set", result.ruleSetId || "none");
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
error(err.message);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// Delete database
|
|
175
|
+
databases
|
|
176
|
+
.command("delete")
|
|
177
|
+
.description("Delete a database")
|
|
178
|
+
.argument("<database-id>", "Database ID")
|
|
179
|
+
.option("--app <app-id>", "App ID")
|
|
180
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
181
|
+
.action(async (databaseId, options) => {
|
|
182
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
183
|
+
if (!options.yes) {
|
|
184
|
+
const inquirer = await import("inquirer");
|
|
185
|
+
const { confirm } = await inquirer.default.prompt([
|
|
186
|
+
{
|
|
187
|
+
type: "confirm",
|
|
188
|
+
name: "confirm",
|
|
189
|
+
message: `Delete database ${databaseId}? This cannot be undone.`,
|
|
190
|
+
default: false,
|
|
191
|
+
},
|
|
192
|
+
]);
|
|
193
|
+
if (!confirm) {
|
|
194
|
+
info("Cancelled.");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const client = new ApiClient();
|
|
199
|
+
try {
|
|
200
|
+
await client.deleteDatabase(resolvedAppId, databaseId);
|
|
201
|
+
success(`Database ${databaseId} deleted.`);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
error(err.message);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
// ---- Permissions subcommand group ----
|
|
209
|
+
const permissions = databases
|
|
210
|
+
.command("permissions")
|
|
211
|
+
.description("Manage user permissions on a database");
|
|
212
|
+
// List permissions
|
|
213
|
+
permissions
|
|
214
|
+
.command("list")
|
|
215
|
+
.description("List user permissions for a database")
|
|
216
|
+
.argument("<database-id>", "Database ID")
|
|
217
|
+
.option("--app <app-id>", "App ID")
|
|
218
|
+
.option("--json", "Output as JSON")
|
|
219
|
+
.action(async (databaseId, options) => {
|
|
220
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
221
|
+
const client = new ApiClient();
|
|
222
|
+
try {
|
|
223
|
+
const result = await client.listDatabasePermissions(resolvedAppId, databaseId);
|
|
224
|
+
const list = Array.isArray(result) ? result : result?.permissions ?? [];
|
|
225
|
+
if (options.json) {
|
|
226
|
+
json(list);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (list.length === 0) {
|
|
230
|
+
info("No permissions found.");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
console.log(formatTable(list, [
|
|
234
|
+
{ header: "USER_ID", key: "userId", format: formatId },
|
|
235
|
+
{ header: "EMAIL", key: "userEmail" },
|
|
236
|
+
{ header: "PERMISSION", key: "permission" },
|
|
237
|
+
{ header: "GRANTED", key: "grantedAt", format: formatDate },
|
|
238
|
+
]));
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
error(err.message);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
// Grant permission
|
|
246
|
+
permissions
|
|
247
|
+
.command("grant")
|
|
248
|
+
.description("Grant a user permission on a database")
|
|
249
|
+
.argument("<database-id>", "Database ID")
|
|
250
|
+
.requiredOption("--user-id <user-id>", "User ID to grant permission to")
|
|
251
|
+
.requiredOption("--permission <permission>", "Permission level: owner, read-write, or reader")
|
|
252
|
+
.option("--app <app-id>", "App ID")
|
|
253
|
+
.option("--json", "Output as JSON")
|
|
254
|
+
.action(async (databaseId, options) => {
|
|
255
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
256
|
+
const client = new ApiClient();
|
|
257
|
+
try {
|
|
258
|
+
const result = await client.grantDatabasePermission(resolvedAppId, databaseId, {
|
|
259
|
+
userId: options.userId,
|
|
260
|
+
permission: options.permission,
|
|
261
|
+
});
|
|
262
|
+
if (options.json) {
|
|
263
|
+
json(result);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
success(`Permission '${options.permission}' granted to user ${options.userId}.`);
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
error(err.message);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
// Revoke permission
|
|
274
|
+
permissions
|
|
275
|
+
.command("revoke")
|
|
276
|
+
.description("Revoke a user's permission on a database")
|
|
277
|
+
.argument("<database-id>", "Database ID")
|
|
278
|
+
.argument("<user-id>", "User ID to revoke")
|
|
279
|
+
.option("--app <app-id>", "App ID")
|
|
280
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
281
|
+
.action(async (databaseId, userId, options) => {
|
|
282
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
283
|
+
if (!options.yes) {
|
|
284
|
+
const inquirer = await import("inquirer");
|
|
285
|
+
const { confirm } = await inquirer.default.prompt([
|
|
286
|
+
{
|
|
287
|
+
type: "confirm",
|
|
288
|
+
name: "confirm",
|
|
289
|
+
message: `Revoke permission for user ${userId} on database ${databaseId}?`,
|
|
290
|
+
default: false,
|
|
291
|
+
},
|
|
292
|
+
]);
|
|
293
|
+
if (!confirm) {
|
|
294
|
+
info("Cancelled.");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const client = new ApiClient();
|
|
299
|
+
try {
|
|
300
|
+
await client.revokeDatabasePermission(resolvedAppId, databaseId, userId);
|
|
301
|
+
success(`Permission revoked for user ${userId}.`);
|
|
302
|
+
}
|
|
303
|
+
catch (err) {
|
|
304
|
+
error(err.message);
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
// ---- Records subcommand group ----
|
|
309
|
+
const records = databases
|
|
310
|
+
.command("records")
|
|
311
|
+
.description("Query, create, and manage records in a database");
|
|
312
|
+
// List models
|
|
313
|
+
records
|
|
314
|
+
.command("models")
|
|
315
|
+
.description("List model names in a database")
|
|
316
|
+
.argument("<database-id>", "Database ID")
|
|
317
|
+
.option("--app <app-id>", "App ID")
|
|
318
|
+
.option("--json", "Output as JSON")
|
|
319
|
+
.action(async (databaseId, options) => {
|
|
320
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
321
|
+
const client = new ApiClient();
|
|
322
|
+
try {
|
|
323
|
+
const result = await client.listDatabaseModels(resolvedAppId, databaseId);
|
|
324
|
+
const models = result.models || [];
|
|
325
|
+
if (options.json) {
|
|
326
|
+
json(models);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (models.length === 0) {
|
|
330
|
+
info("No models found.");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
for (const m of models) {
|
|
334
|
+
console.log(` ${m}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
error(err.message);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
// Describe model
|
|
343
|
+
records
|
|
344
|
+
.command("describe")
|
|
345
|
+
.description("Show inferred schema for a model")
|
|
346
|
+
.argument("<database-id>", "Database ID")
|
|
347
|
+
.argument("<model-name>", "Model name")
|
|
348
|
+
.option("--app <app-id>", "App ID")
|
|
349
|
+
.option("--json", "Output as JSON")
|
|
350
|
+
.action(async (databaseId, modelName, options) => {
|
|
351
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
352
|
+
const client = new ApiClient();
|
|
353
|
+
try {
|
|
354
|
+
const result = await client.describeDatabaseModel(resolvedAppId, databaseId, modelName);
|
|
355
|
+
const fields = result.fields || [];
|
|
356
|
+
if (options.json) {
|
|
357
|
+
json(fields);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (fields.length === 0) {
|
|
361
|
+
info("No fields detected.");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
console.log(formatTable(fields, [
|
|
365
|
+
{ header: "FIELD", key: "field_name" },
|
|
366
|
+
{ header: "TYPE", key: "inferred_type" },
|
|
367
|
+
]));
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
error(err.message);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
// Query records
|
|
375
|
+
records
|
|
376
|
+
.command("query")
|
|
377
|
+
.description("Query records in a database")
|
|
378
|
+
.argument("<database-id>", "Database ID")
|
|
379
|
+
.argument("<model-name>", "Model name")
|
|
380
|
+
.option("--filter <json>", "Filter as JSON", "{}")
|
|
381
|
+
.option("--limit <n>", "Max records to return", "25")
|
|
382
|
+
.option("--cursor <cursor>", "Pagination cursor")
|
|
383
|
+
.option("--app <app-id>", "App ID")
|
|
384
|
+
.option("--json", "Output as JSON")
|
|
385
|
+
.action(async (databaseId, modelName, options) => {
|
|
386
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
387
|
+
const client = new ApiClient();
|
|
388
|
+
let filter = {};
|
|
389
|
+
try {
|
|
390
|
+
filter = JSON.parse(options.filter);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
error("Invalid --filter JSON.");
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const body = {
|
|
398
|
+
modelName,
|
|
399
|
+
filter,
|
|
400
|
+
options: { limit: parseInt(options.limit, 10) },
|
|
401
|
+
};
|
|
402
|
+
if (options.cursor)
|
|
403
|
+
body.options.cursor = options.cursor;
|
|
404
|
+
const result = await client.queryDatabaseRecords(resolvedAppId, databaseId, body);
|
|
405
|
+
const data = result.data || [];
|
|
406
|
+
if (options.json) {
|
|
407
|
+
json(result);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (data.length === 0) {
|
|
411
|
+
info("No records found.");
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
// Derive columns from first record
|
|
415
|
+
const cols = Object.keys(data[0]).filter((k) => k !== "type");
|
|
416
|
+
console.log(formatTable(data, cols.map((c) => ({
|
|
417
|
+
header: c.toUpperCase(),
|
|
418
|
+
key: c,
|
|
419
|
+
format: (v) => {
|
|
420
|
+
if (v === null || v === undefined)
|
|
421
|
+
return "—";
|
|
422
|
+
if (typeof v === "object") {
|
|
423
|
+
const s = JSON.stringify(v);
|
|
424
|
+
return s.length > 50 ? s.slice(0, 47) + "..." : s;
|
|
425
|
+
}
|
|
426
|
+
const s = String(v);
|
|
427
|
+
return s.length > 50 ? s.slice(0, 47) + "..." : s;
|
|
428
|
+
},
|
|
429
|
+
}))));
|
|
430
|
+
if (result.hasMore) {
|
|
431
|
+
info(`More records available. Use --cursor ${result.cursor} to continue.`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
error(err.message);
|
|
436
|
+
process.exit(1);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
// Count records
|
|
440
|
+
records
|
|
441
|
+
.command("count")
|
|
442
|
+
.description("Count records of a model")
|
|
443
|
+
.argument("<database-id>", "Database ID")
|
|
444
|
+
.argument("<model-name>", "Model name")
|
|
445
|
+
.option("--filter <json>", "Filter as JSON", "{}")
|
|
446
|
+
.option("--app <app-id>", "App ID")
|
|
447
|
+
.option("--json", "Output as JSON")
|
|
448
|
+
.action(async (databaseId, modelName, options) => {
|
|
449
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
450
|
+
const client = new ApiClient();
|
|
451
|
+
let filter = {};
|
|
452
|
+
try {
|
|
453
|
+
filter = JSON.parse(options.filter);
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
error("Invalid --filter JSON.");
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
const result = await client.countDatabaseRecords(resolvedAppId, databaseId, {
|
|
461
|
+
modelName,
|
|
462
|
+
filter,
|
|
463
|
+
});
|
|
464
|
+
if (options.json) {
|
|
465
|
+
json(result);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
console.log(`Count: ${result.count}`);
|
|
469
|
+
}
|
|
470
|
+
catch (err) {
|
|
471
|
+
error(err.message);
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
// Save record
|
|
476
|
+
records
|
|
477
|
+
.command("save")
|
|
478
|
+
.description("Create or update a record")
|
|
479
|
+
.argument("<database-id>", "Database ID")
|
|
480
|
+
.argument("<model-name>", "Model name")
|
|
481
|
+
.option("--id <id>", "Record ID (auto-generated if omitted)")
|
|
482
|
+
.requiredOption("--data <json>", "Record data as JSON")
|
|
483
|
+
.option("--app <app-id>", "App ID")
|
|
484
|
+
.option("--json", "Output as JSON")
|
|
485
|
+
.action(async (databaseId, modelName, options) => {
|
|
486
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
487
|
+
const client = new ApiClient();
|
|
488
|
+
let data;
|
|
489
|
+
try {
|
|
490
|
+
data = JSON.parse(options.data);
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
error("Invalid --data JSON.");
|
|
494
|
+
process.exit(1);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const id = options.id || crypto.randomUUID().replace(/-/g, "").slice(0, 20);
|
|
498
|
+
try {
|
|
499
|
+
const result = await client.saveDatabaseRecord(resolvedAppId, databaseId, {
|
|
500
|
+
modelName,
|
|
501
|
+
id,
|
|
502
|
+
data,
|
|
503
|
+
});
|
|
504
|
+
if (options.json) {
|
|
505
|
+
json(result);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
success(`Record saved.`);
|
|
509
|
+
keyValue("ID", result.id || id);
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
error(err.message);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
// Delete record
|
|
517
|
+
records
|
|
518
|
+
.command("delete")
|
|
519
|
+
.description("Delete a record")
|
|
520
|
+
.argument("<database-id>", "Database ID")
|
|
521
|
+
.argument("<model-name>", "Model name")
|
|
522
|
+
.argument("<record-id>", "Record ID")
|
|
523
|
+
.option("--app <app-id>", "App ID")
|
|
524
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
525
|
+
.action(async (databaseId, modelName, recordId, options) => {
|
|
526
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
527
|
+
if (!options.yes) {
|
|
528
|
+
const inquirer = await import("inquirer");
|
|
529
|
+
const { confirm } = await inquirer.default.prompt([
|
|
530
|
+
{
|
|
531
|
+
type: "confirm",
|
|
532
|
+
name: "confirm",
|
|
533
|
+
message: `Delete record ${recordId} from ${modelName}?`,
|
|
534
|
+
default: false,
|
|
535
|
+
},
|
|
536
|
+
]);
|
|
537
|
+
if (!confirm) {
|
|
538
|
+
info("Cancelled.");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const client = new ApiClient();
|
|
543
|
+
try {
|
|
544
|
+
await client.deleteDatabaseRecord(resolvedAppId, databaseId, {
|
|
545
|
+
modelName,
|
|
546
|
+
id: recordId,
|
|
547
|
+
});
|
|
548
|
+
success(`Record ${recordId} deleted.`);
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
error(err.message);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
// ---- Indexes subcommand group ----
|
|
556
|
+
const indexes = databases
|
|
557
|
+
.command("indexes")
|
|
558
|
+
.description("Manage indexes on a database");
|
|
559
|
+
// List indexes
|
|
560
|
+
indexes
|
|
561
|
+
.command("list")
|
|
562
|
+
.description("List indexes for a database or model")
|
|
563
|
+
.argument("<database-id>", "Database ID")
|
|
564
|
+
.option("--model <model-name>", "Filter by model name")
|
|
565
|
+
.option("--app <app-id>", "App ID")
|
|
566
|
+
.option("--json", "Output as JSON")
|
|
567
|
+
.action(async (databaseId, options) => {
|
|
568
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
569
|
+
const client = new ApiClient();
|
|
570
|
+
try {
|
|
571
|
+
const result = await client.listDatabaseIndexes(resolvedAppId, databaseId, options.model);
|
|
572
|
+
const list = result.indexes || [];
|
|
573
|
+
if (options.json) {
|
|
574
|
+
json(list);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (list.length === 0) {
|
|
578
|
+
info("No indexes found.");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
console.log(formatTable(list, [
|
|
582
|
+
{ header: "MODEL", key: "model_name" },
|
|
583
|
+
{ header: "FIELD", key: "field_name" },
|
|
584
|
+
{ header: "TYPE", key: "field_type" },
|
|
585
|
+
{ header: "UNIQUE", key: "is_unique", format: (v) => (v ? "Yes" : "No") },
|
|
586
|
+
]));
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
error(err.message);
|
|
590
|
+
process.exit(1);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
// Register index
|
|
594
|
+
indexes
|
|
595
|
+
.command("create")
|
|
596
|
+
.description("Create an index on a model field")
|
|
597
|
+
.argument("<database-id>", "Database ID")
|
|
598
|
+
.requiredOption("--model <model-name>", "Model name")
|
|
599
|
+
.requiredOption("--field <field-name>", "Field name to index")
|
|
600
|
+
.option("--type <field-type>", "Field type: string, number, or boolean", "string")
|
|
601
|
+
.option("--unique", "Make this a unique index")
|
|
602
|
+
.option("--app <app-id>", "App ID")
|
|
603
|
+
.option("--json", "Output as JSON")
|
|
604
|
+
.action(async (databaseId, options) => {
|
|
605
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
606
|
+
const client = new ApiClient();
|
|
607
|
+
try {
|
|
608
|
+
const result = await client.registerDatabaseIndex(resolvedAppId, databaseId, {
|
|
609
|
+
modelName: options.model,
|
|
610
|
+
fieldName: options.field,
|
|
611
|
+
fieldType: options.type,
|
|
612
|
+
unique: !!options.unique,
|
|
613
|
+
});
|
|
614
|
+
if (options.json) {
|
|
615
|
+
json(result);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
success(`Index created on ${options.model}.${options.field} (${options.type}${options.unique ? ", unique" : ""}).`);
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
error(err.message);
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
// Drop index
|
|
626
|
+
indexes
|
|
627
|
+
.command("drop")
|
|
628
|
+
.description("Drop an index from a model field")
|
|
629
|
+
.argument("<database-id>", "Database ID")
|
|
630
|
+
.requiredOption("--model <model-name>", "Model name")
|
|
631
|
+
.requiredOption("--field <field-name>", "Field name")
|
|
632
|
+
.option("--app <app-id>", "App ID")
|
|
633
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
634
|
+
.action(async (databaseId, options) => {
|
|
635
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
636
|
+
if (!options.yes) {
|
|
637
|
+
const inquirer = await import("inquirer");
|
|
638
|
+
const { confirm } = await inquirer.default.prompt([
|
|
639
|
+
{
|
|
640
|
+
type: "confirm",
|
|
641
|
+
name: "confirm",
|
|
642
|
+
message: `Drop index on ${options.model}.${options.field}?`,
|
|
643
|
+
default: false,
|
|
644
|
+
},
|
|
645
|
+
]);
|
|
646
|
+
if (!confirm) {
|
|
647
|
+
info("Cancelled.");
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const client = new ApiClient();
|
|
652
|
+
try {
|
|
653
|
+
await client.dropDatabaseIndex(resolvedAppId, databaseId, {
|
|
654
|
+
modelName: options.model,
|
|
655
|
+
fieldName: options.field,
|
|
656
|
+
});
|
|
657
|
+
success(`Index dropped on ${options.model}.${options.field}.`);
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
error(err.message);
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
// ---- Group Permissions subcommand group ----
|
|
665
|
+
const groupPermissions = databases
|
|
666
|
+
.command("group-permissions")
|
|
667
|
+
.description("Manage group permissions on a database");
|
|
668
|
+
// List group permissions
|
|
669
|
+
groupPermissions
|
|
670
|
+
.command("list")
|
|
671
|
+
.description("List group permissions for a database")
|
|
672
|
+
.argument("<database-id>", "Database ID")
|
|
673
|
+
.option("--app <app-id>", "App ID")
|
|
674
|
+
.option("--json", "Output as JSON")
|
|
675
|
+
.action(async (databaseId, options) => {
|
|
676
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
677
|
+
const client = new ApiClient();
|
|
678
|
+
try {
|
|
679
|
+
const result = await client.listDatabaseGroupPermissions(resolvedAppId, databaseId);
|
|
680
|
+
const list = Array.isArray(result) ? result : result?.permissions ?? [];
|
|
681
|
+
if (options.json) {
|
|
682
|
+
json(list);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (list.length === 0) {
|
|
686
|
+
info("No group permissions found.");
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
console.log(formatTable(list, [
|
|
690
|
+
{ header: "GROUP_TYPE", key: "groupType" },
|
|
691
|
+
{ header: "GROUP_ID", key: "groupId" },
|
|
692
|
+
{ header: "PERMISSION", key: "permission" },
|
|
693
|
+
{ header: "GRANTED", key: "grantedAt", format: formatDate },
|
|
694
|
+
]));
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
error(err.message);
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
// Grant group permission
|
|
702
|
+
groupPermissions
|
|
703
|
+
.command("grant")
|
|
704
|
+
.description("Grant a group permission on a database")
|
|
705
|
+
.argument("<database-id>", "Database ID")
|
|
706
|
+
.requiredOption("--group-type <type>", "Group type")
|
|
707
|
+
.requiredOption("--group-id <id>", "Group ID")
|
|
708
|
+
.requiredOption("--permission <permission>", "Permission level: read-write or reader")
|
|
709
|
+
.option("--app <app-id>", "App ID")
|
|
710
|
+
.option("--json", "Output as JSON")
|
|
711
|
+
.action(async (databaseId, options) => {
|
|
712
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
713
|
+
const client = new ApiClient();
|
|
714
|
+
try {
|
|
715
|
+
const result = await client.grantDatabaseGroupPermission(resolvedAppId, databaseId, {
|
|
716
|
+
groupType: options.groupType,
|
|
717
|
+
groupId: options.groupId,
|
|
718
|
+
permission: options.permission,
|
|
719
|
+
});
|
|
720
|
+
if (options.json) {
|
|
721
|
+
json(result);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
success(`Permission '${options.permission}' granted to group ${options.groupType}/${options.groupId}.`);
|
|
725
|
+
}
|
|
726
|
+
catch (err) {
|
|
727
|
+
error(err.message);
|
|
728
|
+
process.exit(1);
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
// Revoke group permission
|
|
732
|
+
groupPermissions
|
|
733
|
+
.command("revoke")
|
|
734
|
+
.description("Revoke a group's permission on a database")
|
|
735
|
+
.argument("<database-id>", "Database ID")
|
|
736
|
+
.argument("<group-type>", "Group type")
|
|
737
|
+
.argument("<group-id>", "Group ID")
|
|
738
|
+
.option("--app <app-id>", "App ID")
|
|
739
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
740
|
+
.action(async (databaseId, groupType, groupId, options) => {
|
|
741
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
742
|
+
if (!options.yes) {
|
|
743
|
+
const inquirer = await import("inquirer");
|
|
744
|
+
const { confirm } = await inquirer.default.prompt([
|
|
745
|
+
{
|
|
746
|
+
type: "confirm",
|
|
747
|
+
name: "confirm",
|
|
748
|
+
message: `Revoke permission for group ${groupType}/${groupId} on database ${databaseId}?`,
|
|
749
|
+
default: false,
|
|
750
|
+
},
|
|
751
|
+
]);
|
|
752
|
+
if (!confirm) {
|
|
753
|
+
info("Cancelled.");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
const client = new ApiClient();
|
|
758
|
+
try {
|
|
759
|
+
await client.revokeDatabaseGroupPermission(resolvedAppId, databaseId, groupType, groupId);
|
|
760
|
+
success(`Permission revoked for group ${groupType}/${groupId}.`);
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
error(err.message);
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
//# sourceMappingURL=databases.js.map
|