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,6 +1,5 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, readdirSync } from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { parseConfigToml } from "../lib/config-toml.js";
|
|
4
3
|
import { ApiClient } from "../lib/api-client.js";
|
|
5
4
|
import { getCurrentAppId } from "../lib/config.js";
|
|
6
5
|
import { resolveCodegenSourceDir } from "../lib/codegen-shared/resolveCodegenSourceDir.js";
|
|
@@ -10,6 +9,9 @@ import { generateDbTypes, } from "../lib/db-codegen/dbGenerator.js";
|
|
|
10
9
|
import { generateDbSwiftTypes } from "../lib/swift-codegen/dbGenerator.js";
|
|
11
10
|
import { success, error, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, json, } from "../lib/output.js";
|
|
12
11
|
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
12
|
+
import { parseFilterOptions } from "../lib/record-filter.js";
|
|
13
|
+
import { parseDataOption } from "../lib/data-input.js";
|
|
14
|
+
import { ulid } from "ulid";
|
|
13
15
|
function resolveAppId(appId, options) {
|
|
14
16
|
const resolved = appId || options.app || getCurrentAppId();
|
|
15
17
|
if (!resolved) {
|
|
@@ -18,6 +20,43 @@ function resolveAppId(appId, options) {
|
|
|
18
20
|
}
|
|
19
21
|
return resolved;
|
|
20
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Render the flat `{ success, id, appliedFields }` response that
|
|
25
|
+
* `records/save` and `records/patch` return — these endpoints answer with a
|
|
26
|
+
* write receipt, not the stored record, so the human view shows the resolved id
|
|
27
|
+
* plus any server-applied fields (autoStamps / trigger-computed values).
|
|
28
|
+
*/
|
|
29
|
+
function renderRecordWriteResult(result, fallbackId, message) {
|
|
30
|
+
success(message);
|
|
31
|
+
keyValue("id", String(result?.id ?? fallbackId));
|
|
32
|
+
const applied = result?.appliedFields;
|
|
33
|
+
if (applied && typeof applied === "object" && Object.keys(applied).length > 0) {
|
|
34
|
+
for (const [key, value] of Object.entries(applied)) {
|
|
35
|
+
keyValue(`applied.${key}`, value === null || value === undefined ? "—" : String(value));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Refuse a write whose `--data` carries an `id` that is not the record being
|
|
41
|
+
* written. The server strips `id` from the record body on both the save and the
|
|
42
|
+
* patch path, so such an `id` would be dropped silently and the write would
|
|
43
|
+
* land on the id from the argument — somewhere the caller did not intend.
|
|
44
|
+
*
|
|
45
|
+
* `recordId` is the id the caller passed as an argument (absent on a `save`
|
|
46
|
+
* that mints one); `writtenId` is the id actually being written. Exits non-zero
|
|
47
|
+
* when they disagree with `--data` (#1965 review).
|
|
48
|
+
*/
|
|
49
|
+
function assertDataIdMatches(data, recordId, writtenId, commandPath) {
|
|
50
|
+
if (!data || typeof data !== "object")
|
|
51
|
+
return;
|
|
52
|
+
const dataId = data.id;
|
|
53
|
+
if (dataId === undefined || dataId === writtenId)
|
|
54
|
+
return;
|
|
55
|
+
error(recordId
|
|
56
|
+
? `The "id" in --data ("${dataId}") does not match the record id "${recordId}". Remove it, or pass the id you mean as the argument.`
|
|
57
|
+
: `Pass the record id as the argument, not inside --data: ${commandPath} ${dataId} --data ...`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
21
60
|
/**
|
|
22
61
|
* Validate that the named operation is an import-compatible, save-like
|
|
23
62
|
* mutation before importing (issue #145, Codex finding (a)).
|
|
@@ -68,11 +107,17 @@ export function registerDatabasesCommands(program) {
|
|
|
68
107
|
.addHelpText("after", `
|
|
69
108
|
Examples:
|
|
70
109
|
$ primitive databases list
|
|
110
|
+
$ primitive databases list --owner <user-id>
|
|
71
111
|
$ primitive databases create "My Database"
|
|
72
112
|
$ primitive databases get <database-id>
|
|
73
113
|
$ primitive databases records models <database-id>
|
|
74
114
|
$ primitive databases records describe <database-id> <model-name>
|
|
75
115
|
$ primitive databases records query <database-id> <model-name> --filter '{"name":"Alice"}'
|
|
116
|
+
$ primitive databases records get <database-id> <model-name> <record-id>
|
|
117
|
+
$ primitive databases records count <database-id> <model-name> --filter '{"status":"open"}'
|
|
118
|
+
$ primitive databases records aggregate <database-id> <model-name> --op avg --field price
|
|
119
|
+
$ primitive databases records save <database-id> <model-name> [record-id] --data '{"name":"Alice"}'
|
|
120
|
+
$ primitive databases records patch <database-id> <model-name> <record-id> --data '{"name":"Alice"}'
|
|
76
121
|
$ primitive databases operations list <database-id>
|
|
77
122
|
$ primitive databases operations execute <database-id> <operation-name> --params '{"key":"value"}'
|
|
78
123
|
$ primitive databases indexes list <database-id>
|
|
@@ -94,12 +139,15 @@ the 'database-types' group:
|
|
|
94
139
|
.description("List databases in an app")
|
|
95
140
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
96
141
|
.option("--app <app-id>", "App ID")
|
|
142
|
+
.option("--owner <user-id>", "Only list databases created by this user")
|
|
97
143
|
.option("--json", "Output as JSON")
|
|
98
144
|
.action(async (appId, options) => {
|
|
99
145
|
const resolvedAppId = resolveAppId(appId, options);
|
|
100
146
|
const client = new ApiClient();
|
|
101
147
|
try {
|
|
102
|
-
const result = await client.listDatabases(resolvedAppId
|
|
148
|
+
const result = await client.listDatabases(resolvedAppId, {
|
|
149
|
+
owner: options.owner,
|
|
150
|
+
});
|
|
103
151
|
const list = Array.isArray(result) ? result : result?.databases ?? [];
|
|
104
152
|
if (options.json) {
|
|
105
153
|
json(list);
|
|
@@ -114,6 +162,18 @@ the 'database-types' group:
|
|
|
114
162
|
{ header: "TITLE", key: "title" },
|
|
115
163
|
{ header: "TYPE", key: "databaseType", format: (v) => v || "—" },
|
|
116
164
|
{ header: "PERMISSION", key: "permission" },
|
|
165
|
+
// `permission` reads "owner" on every row for an app admin, so it
|
|
166
|
+
// cannot confirm --owner narrowed anything. Show who created each
|
|
167
|
+
// database when the filter is in play.
|
|
168
|
+
...(options.owner
|
|
169
|
+
? [
|
|
170
|
+
{
|
|
171
|
+
header: "CREATED BY",
|
|
172
|
+
key: "createdBy",
|
|
173
|
+
format: (v) => (v ? formatId(v) : "—"),
|
|
174
|
+
},
|
|
175
|
+
]
|
|
176
|
+
: []),
|
|
117
177
|
{ header: "CREATED", key: "createdAt", format: formatDate },
|
|
118
178
|
]));
|
|
119
179
|
}
|
|
@@ -465,7 +525,7 @@ the 'database-types' group:
|
|
|
465
525
|
process.exit(1);
|
|
466
526
|
}
|
|
467
527
|
});
|
|
468
|
-
// Query records (direct introspection via
|
|
528
|
+
// Query records (direct introspection via the records route)
|
|
469
529
|
records
|
|
470
530
|
.command("query")
|
|
471
531
|
.description("Query records in a database model (admin only)")
|
|
@@ -480,36 +540,7 @@ the 'database-types' group:
|
|
|
480
540
|
.action(async (databaseId, modelName, options) => {
|
|
481
541
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
482
542
|
const client = new ApiClient();
|
|
483
|
-
|
|
484
|
-
error("Cannot use both --filter and --filter-file.");
|
|
485
|
-
process.exit(1);
|
|
486
|
-
}
|
|
487
|
-
let filter;
|
|
488
|
-
if (options.filterFile) {
|
|
489
|
-
try {
|
|
490
|
-
const content = readFileSync(options.filterFile, "utf-8");
|
|
491
|
-
if (options.filterFile.endsWith(".toml")) {
|
|
492
|
-
const parsed = parseConfigToml(content);
|
|
493
|
-
filter = parsed.filter || parsed;
|
|
494
|
-
}
|
|
495
|
-
else {
|
|
496
|
-
filter = JSON.parse(content);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
catch (err) {
|
|
500
|
-
error(`Failed to read filter file: ${err.message}`);
|
|
501
|
-
process.exit(1);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
else if (options.filter) {
|
|
505
|
-
try {
|
|
506
|
-
filter = JSON.parse(options.filter);
|
|
507
|
-
}
|
|
508
|
-
catch {
|
|
509
|
-
error("Invalid --filter JSON.");
|
|
510
|
-
process.exit(1);
|
|
511
|
-
}
|
|
512
|
-
}
|
|
543
|
+
const filter = parseFilterOptions(options);
|
|
513
544
|
try {
|
|
514
545
|
const result = await client.queryDatabaseRecords(resolvedAppId, databaseId, modelName, { filter, limit: options.limit, cursor: options.cursor });
|
|
515
546
|
if (options.json) {
|
|
@@ -560,6 +591,218 @@ the 'database-types' group:
|
|
|
560
591
|
process.exit(1);
|
|
561
592
|
}
|
|
562
593
|
});
|
|
594
|
+
// Get a single record by id
|
|
595
|
+
records
|
|
596
|
+
.command("get")
|
|
597
|
+
.description("Get a single record by id (admin only)")
|
|
598
|
+
.argument("<database-id>", "Database ID")
|
|
599
|
+
.argument("<model-name>", "Model name")
|
|
600
|
+
.argument("<record-id>", "Record ID")
|
|
601
|
+
.option("--app <app-id>", "App ID")
|
|
602
|
+
.option("--json", "Output as JSON")
|
|
603
|
+
.action(async (databaseId, modelName, recordId, options) => {
|
|
604
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
605
|
+
const client = new ApiClient();
|
|
606
|
+
try {
|
|
607
|
+
// A single-record fetch is a query filtered by primary-key id (there is
|
|
608
|
+
// no dedicated get endpoint — reuse queryDatabaseRecords).
|
|
609
|
+
const result = await client.queryDatabaseRecords(resolvedAppId, databaseId, modelName, { filter: { id: recordId }, limit: 1 });
|
|
610
|
+
const record = (result?.data || [])[0];
|
|
611
|
+
if (options.json) {
|
|
612
|
+
json(record ?? null);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (!record) {
|
|
616
|
+
info(`No record found with id ${recordId} in ${modelName}.`);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
for (const [key, value] of Object.entries(record)) {
|
|
620
|
+
const rendered = value === null || value === undefined
|
|
621
|
+
? "—"
|
|
622
|
+
: typeof value === "object"
|
|
623
|
+
? JSON.stringify(value)
|
|
624
|
+
: String(value);
|
|
625
|
+
keyValue(key, rendered);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
error(err.message);
|
|
630
|
+
process.exit(1);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
// Count records
|
|
634
|
+
records
|
|
635
|
+
.command("count")
|
|
636
|
+
.description("Count records in a database model (admin only)")
|
|
637
|
+
.argument("<database-id>", "Database ID")
|
|
638
|
+
.argument("<model-name>", "Model name")
|
|
639
|
+
.option("--app <app-id>", "App ID")
|
|
640
|
+
.option("--filter <json>", "Filter as JSON (e.g. '{\"name\":\"Alice\"}')")
|
|
641
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
642
|
+
.option("--json", "Output as JSON")
|
|
643
|
+
.action(async (databaseId, modelName, options) => {
|
|
644
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
645
|
+
const filter = parseFilterOptions(options);
|
|
646
|
+
const client = new ApiClient();
|
|
647
|
+
try {
|
|
648
|
+
const result = await client.countDatabaseRecords(resolvedAppId, databaseId, modelName, filter);
|
|
649
|
+
if (options.json) {
|
|
650
|
+
json(result);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
keyValue("count", String(result?.count ?? 0));
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
error(err.message);
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
// Aggregate records
|
|
661
|
+
const AGGREGATE_OPS = ["count", "sum", "avg", "min", "max"];
|
|
662
|
+
records
|
|
663
|
+
.command("aggregate")
|
|
664
|
+
.description("Aggregate records in a database model (count/sum/avg/min/max, optional group-by; admin only)")
|
|
665
|
+
.argument("<database-id>", "Database ID")
|
|
666
|
+
.argument("<model-name>", "Model name")
|
|
667
|
+
.option("--app <app-id>", "App ID")
|
|
668
|
+
.requiredOption("--op <operation>", `Aggregate operation: ${AGGREGATE_OPS.join("|")}`)
|
|
669
|
+
.option("--field <field>", "Field to aggregate (required for sum/avg/min/max)")
|
|
670
|
+
.option("--group-by <field>", "Group results by this field (repeatable)", (value, previous = []) => previous.concat(value), [])
|
|
671
|
+
.option("--filter <json>", "Filter as JSON")
|
|
672
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
673
|
+
.option("--json", "Output as JSON")
|
|
674
|
+
.action(async (databaseId, modelName, options) => {
|
|
675
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
676
|
+
if (!AGGREGATE_OPS.includes(options.op)) {
|
|
677
|
+
error(`Invalid --op "${options.op}". Expected one of: ${AGGREGATE_OPS.join(", ")}.`);
|
|
678
|
+
process.exit(1);
|
|
679
|
+
}
|
|
680
|
+
const operation = { type: options.op };
|
|
681
|
+
if (options.op === "count") {
|
|
682
|
+
if (options.field) {
|
|
683
|
+
error("--field is not valid for --op count.");
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
if (!options.field) {
|
|
689
|
+
error(`--op ${options.op} requires --field.`);
|
|
690
|
+
process.exit(1);
|
|
691
|
+
}
|
|
692
|
+
operation.field = options.field;
|
|
693
|
+
}
|
|
694
|
+
const filter = parseFilterOptions(options);
|
|
695
|
+
const groupBy = options.groupBy || [];
|
|
696
|
+
const client = new ApiClient();
|
|
697
|
+
try {
|
|
698
|
+
const result = await client.aggregateDatabaseRecords(resolvedAppId, databaseId, modelName, { operations: [operation], groupBy, filter });
|
|
699
|
+
if (options.json) {
|
|
700
|
+
json(result);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const aggregated = result?.result;
|
|
704
|
+
// Label the value column after the operation, including the field it
|
|
705
|
+
// ran over: `SUM` alone does not say what was summed.
|
|
706
|
+
const valueLabel = options.op === "count"
|
|
707
|
+
? "COUNT"
|
|
708
|
+
: `${options.op.toUpperCase()}(${options.field})`;
|
|
709
|
+
if (groupBy.length === 0) {
|
|
710
|
+
// Ungrouped: one scalar per operation, keyed by the endpoint's
|
|
711
|
+
// operation name (`count`, `sum_<field>`, …). Render it as a
|
|
712
|
+
// key/value line — there is no group column to table.
|
|
713
|
+
const value = (aggregated ?? {})[options.op === "count" ? "count" : `${options.op}_${options.field}`];
|
|
714
|
+
keyValue(valueLabel.toLowerCase(), value === null || value === undefined ? "—" : String(value));
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (aggregated &&
|
|
718
|
+
typeof aggregated === "object" &&
|
|
719
|
+
!Array.isArray(aggregated) &&
|
|
720
|
+
Object.values(aggregated).every((v) => typeof v !== "object" || v === null)) {
|
|
721
|
+
// Flat { group -> value } shape (single operation, single group-by): render a table.
|
|
722
|
+
const rows = Object.entries(aggregated).map(([group, value]) => ({
|
|
723
|
+
group,
|
|
724
|
+
value: value === null || value === undefined ? "—" : String(value),
|
|
725
|
+
}));
|
|
726
|
+
console.log(formatTable(rows, [
|
|
727
|
+
{ header: "GROUP", key: "group" },
|
|
728
|
+
{ header: valueLabel, key: "value" },
|
|
729
|
+
]));
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
// Grouped/nested or multi-operation shape: print the structured result.
|
|
733
|
+
console.log(JSON.stringify(aggregated ?? {}, null, 2));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
catch (err) {
|
|
737
|
+
error(err.message);
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
// Upsert a record (create, or merge into an existing one)
|
|
742
|
+
records
|
|
743
|
+
.command("save")
|
|
744
|
+
.description("Create a record, or merge fields into an existing one (admin only)")
|
|
745
|
+
.argument("<database-id>", "Database ID")
|
|
746
|
+
.argument("<model-name>", "Model name")
|
|
747
|
+
.argument("[record-id]", "Record ID (generated when omitted)")
|
|
748
|
+
.option("--app <app-id>", "App ID")
|
|
749
|
+
.option("--data <json>", "Record fields as inline JSON")
|
|
750
|
+
.option("--data-file <path>", "Read the record fields from a JSON file")
|
|
751
|
+
.option("--json", "Output as JSON")
|
|
752
|
+
.action(async (databaseId, modelName, recordId, options) => {
|
|
753
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
754
|
+
const data = parseDataOption(options, "record");
|
|
755
|
+
const client = new ApiClient();
|
|
756
|
+
// The DatabaseDO requires an id on the plain save path (it only generates
|
|
757
|
+
// one for `upsertOn` writes). Mint the id client-side for a create so
|
|
758
|
+
// `save` works without an id, the same way CSV import does
|
|
759
|
+
// (`lib/csv.ts`).
|
|
760
|
+
const id = recordId || ulid();
|
|
761
|
+
assertDataIdMatches(data, recordId, id, "primitive databases records save <database-id> <model-name>");
|
|
762
|
+
try {
|
|
763
|
+
const result = await client.saveDatabaseRecord(resolvedAppId, databaseId, modelName, id, data);
|
|
764
|
+
if (options.json) {
|
|
765
|
+
json(result);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
renderRecordWriteResult(result, id, `Record saved to ${modelName}.`);
|
|
769
|
+
}
|
|
770
|
+
catch (err) {
|
|
771
|
+
error(err.message);
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
});
|
|
775
|
+
// Patch a record (partial merge)
|
|
776
|
+
records
|
|
777
|
+
.command("patch")
|
|
778
|
+
.description("Merge fields into an existing record (admin only)")
|
|
779
|
+
.argument("<database-id>", "Database ID")
|
|
780
|
+
.argument("<model-name>", "Model name")
|
|
781
|
+
.argument("<record-id>", "Record ID")
|
|
782
|
+
.option("--app <app-id>", "App ID")
|
|
783
|
+
.option("--data <json>", "Fields to merge as inline JSON")
|
|
784
|
+
.option("--data-file <path>", "Read the fields to merge from a JSON file")
|
|
785
|
+
.option("--json", "Output as JSON")
|
|
786
|
+
.action(async (databaseId, modelName, recordId, options) => {
|
|
787
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
788
|
+
const data = parseDataOption(options, "fields to merge");
|
|
789
|
+
const client = new ApiClient();
|
|
790
|
+
// Same guard as `save` — on this path `recordId` is required, so the
|
|
791
|
+
// "pass it as the argument" branch is unreachable here.
|
|
792
|
+
assertDataIdMatches(data, recordId, recordId, "primitive databases records patch <database-id> <model-name>");
|
|
793
|
+
try {
|
|
794
|
+
const result = await client.patchDatabaseRecord(resolvedAppId, databaseId, modelName, recordId, data);
|
|
795
|
+
if (options.json) {
|
|
796
|
+
json(result);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
renderRecordWriteResult(result, recordId, `Record ${recordId} patched in ${modelName}.`);
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
error(err.message);
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
});
|
|
563
806
|
// Delete a single record
|
|
564
807
|
records
|
|
565
808
|
.command("delete")
|
|
@@ -608,21 +851,13 @@ the 'database-types' group:
|
|
|
608
851
|
.argument("<model-name>", "Model name")
|
|
609
852
|
.option("--app <app-id>", "App ID")
|
|
610
853
|
.option("--filter <json>", "Filter as JSON to select which records to delete")
|
|
854
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
611
855
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
612
856
|
.option("--json", "Output as JSON")
|
|
613
857
|
.action(async (databaseId, modelName, options) => {
|
|
614
858
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
615
859
|
const client = new ApiClient();
|
|
616
|
-
|
|
617
|
-
if (options.filter) {
|
|
618
|
-
try {
|
|
619
|
-
filter = JSON.parse(options.filter);
|
|
620
|
-
}
|
|
621
|
-
catch {
|
|
622
|
-
error("Invalid --filter JSON.");
|
|
623
|
-
process.exit(1);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
860
|
+
const filter = parseFilterOptions(options);
|
|
626
861
|
try {
|
|
627
862
|
// First, query to find all matching records
|
|
628
863
|
let allRecords = [];
|
|
@@ -1094,7 +1329,7 @@ the 'database-types' group:
|
|
|
1094
1329
|
}
|
|
1095
1330
|
// Validate --batch-size up front so a bad value fails before any write.
|
|
1096
1331
|
// Unlike `import-csv` (registered-batch path, hard 100k server cap),
|
|
1097
|
-
// `
|
|
1332
|
+
// `records/batch` has no fixed server item cap — bound it with a
|
|
1098
1333
|
// conservative client-side ceiling to keep each request within
|
|
1099
1334
|
// Cloudflare body-size and Durable Object memory limits.
|
|
1100
1335
|
const parsedBatchSize = parseBatchSize(options.batchSize, {
|
|
@@ -1685,7 +1920,7 @@ async function exportSingleDatabase(client, appId, databaseId, outputDir, jsonOu
|
|
|
1685
1920
|
}
|
|
1686
1921
|
/**
|
|
1687
1922
|
* Conservative client-side ceiling for `databases import --batch-size`.
|
|
1688
|
-
* `
|
|
1923
|
+
* `records/batch` has no fixed server-side item cap (unlike the
|
|
1689
1924
|
* registered-batch path used by `import-csv`), so this bounds each request to
|
|
1690
1925
|
* keep it within Cloudflare body-size and Durable Object memory limits.
|
|
1691
1926
|
*/
|