primitive-admin 1.0.60 → 1.0.62
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 +125 -69
- package/assets/skill/skills/primitive-platform/SKILL.md +336 -37
- package/dist/bin/primitive.js +29 -14
- 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 +96 -16
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +49 -188
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +50 -155
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +14 -100
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/collections.js +47 -8
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/config.d.ts +46 -0
- package/dist/src/commands/config.js +479 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/connections.d.ts +2 -0
- package/dist/src/commands/connections.js +100 -0
- package/dist/src/commands/connections.js.map +1 -0
- package/dist/src/commands/cron-triggers.js +84 -176
- package/dist/src/commands/cron-triggers.js.map +1 -1
- package/dist/src/commands/database-type-configs.d.ts +2 -0
- package/dist/src/commands/database-type-configs.js +171 -0
- package/dist/src/commands/database-type-configs.js.map +1 -0
- package/dist/src/commands/database-types.js +4 -4
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.d.ts +63 -0
- package/dist/src/commands/databases.js +649 -87
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +869 -12
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +18 -125
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/env.d.ts +12 -1
- package/dist/src/commands/env.js +98 -25
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/feature-flags.d.ts +14 -0
- package/dist/src/commands/feature-flags.js +116 -0
- package/dist/src/commands/feature-flags.js.map +1 -0
- package/dist/src/commands/group-type-configs.js +14 -109
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/init.d.ts +9 -1
- package/dist/src/commands/init.js +1058 -279
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +136 -421
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/metadata-category-configs.d.ts +12 -0
- package/dist/src/commands/metadata-category-configs.js +112 -0
- package/dist/src/commands/metadata-category-configs.js.map +1 -0
- package/dist/src/commands/metadata.js +33 -141
- package/dist/src/commands/metadata.js.map +1 -1
- package/dist/src/commands/prompts.js +206 -563
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +1 -1
- package/dist/src/commands/rule-sets.js +16 -132
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.js +67 -234
- package/dist/src/commands/scripts.js.map +1 -1
- package/dist/src/commands/sessions.d.ts +2 -0
- package/dist/src/commands/sessions.js +75 -0
- package/dist/src/commands/sessions.js.map +1 -0
- 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 +113 -70
- package/dist/src/commands/sync-app-settings.js +193 -242
- package/dist/src/commands/sync-app-settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +1932 -49
- package/dist/src/commands/sync.js +9584 -1725
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +8 -2
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +92 -1
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.js +44 -58
- package/dist/src/commands/vars.js.map +1 -1
- package/dist/src/commands/webhooks.js +318 -156
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.d.ts +49 -12
- package/dist/src/commands/workflows.js +652 -1043
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/access-rule-display.d.ts +21 -0
- package/dist/src/lib/access-rule-display.js +34 -0
- package/dist/src/lib/access-rule-display.js.map +1 -0
- package/dist/src/lib/api-client.d.ts +630 -61
- package/dist/src/lib/api-client.js +676 -116
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +180 -25
- package/dist/src/lib/app-settings-descriptor.js +403 -82
- package/dist/src/lib/app-settings-descriptor.js.map +1 -1
- package/dist/src/lib/canonical-json.d.ts +12 -0
- package/dist/src/lib/canonical-json.js +35 -0
- package/dist/src/lib/canonical-json.js.map +1 -0
- package/dist/src/lib/cli-manifest.d.ts +8 -0
- package/dist/src/lib/cli-manifest.js +1 -0
- package/dist/src/lib/cli-manifest.js.map +1 -1
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js +9 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +6 -6
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +8 -8
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.d.ts +127 -0
- package/dist/src/lib/config-object-descriptor.js +658 -0
- package/dist/src/lib/config-object-descriptor.js.map +1 -0
- package/dist/src/lib/config-payload.d.ts +85 -0
- package/dist/src/lib/config-payload.js +116 -0
- package/dist/src/lib/config-payload.js.map +1 -0
- package/dist/src/lib/config-surface.d.ts +130 -0
- package/dist/src/lib/config-surface.js +300 -0
- package/dist/src/lib/config-surface.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +10 -0
- package/dist/src/lib/credentials-store.js +23 -0
- package/dist/src/lib/credentials-store.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/db-codegen/dbTemplates.js +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +1 -1
- package/dist/src/lib/env-resolver-core.d.ts +147 -0
- package/dist/src/lib/env-resolver-core.js +265 -0
- package/dist/src/lib/env-resolver-core.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +28 -6
- package/dist/src/lib/env-resolver.js +44 -32
- package/dist/src/lib/env-resolver.js.map +1 -1
- package/dist/src/lib/generated-allowlist.js +13 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
- package/dist/src/lib/generated-config-surfaces.js +4058 -0
- package/dist/src/lib/generated-config-surfaces.js.map +1 -0
- package/dist/src/lib/generated-template-lint.d.ts +212 -0
- package/dist/src/lib/generated-template-lint.js +624 -0
- package/dist/src/lib/generated-template-lint.js.map +1 -0
- package/dist/src/lib/init-adopt.d.ts +16 -0
- package/dist/src/lib/init-adopt.js +34 -0
- package/dist/src/lib/init-adopt.js.map +1 -0
- package/dist/src/lib/init-assets.d.ts +39 -0
- package/dist/src/lib/init-assets.js +97 -0
- package/dist/src/lib/init-assets.js.map +1 -0
- package/dist/src/lib/init-config.d.ts +44 -5
- package/dist/src/lib/init-config.js +80 -7
- package/dist/src/lib/init-config.js.map +1 -1
- package/dist/src/lib/init-ios-links.d.ts +50 -0
- package/dist/src/lib/init-ios-links.js +153 -0
- package/dist/src/lib/init-ios-links.js.map +1 -0
- package/dist/src/lib/init-plan.d.ts +80 -0
- package/dist/src/lib/init-plan.js +95 -0
- package/dist/src/lib/init-plan.js.map +1 -0
- package/dist/src/lib/init-production-env.d.ts +48 -0
- package/dist/src/lib/init-production-env.js +59 -0
- package/dist/src/lib/init-production-env.js.map +1 -0
- package/dist/src/lib/init-schema.d.ts +74 -0
- package/dist/src/lib/init-schema.js +358 -0
- package/dist/src/lib/init-schema.js.map +1 -0
- package/dist/src/lib/init-xcode.d.ts +33 -0
- package/dist/src/lib/init-xcode.js +114 -0
- package/dist/src/lib/init-xcode.js.map +1 -0
- package/dist/src/lib/integration-request-config.d.ts +30 -0
- package/dist/src/lib/integration-request-config.js +145 -0
- package/dist/src/lib/integration-request-config.js.map +1 -0
- package/dist/src/lib/local-state.d.ts +55 -0
- package/dist/src/lib/local-state.js +167 -0
- package/dist/src/lib/local-state.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +568 -0
- package/dist/src/lib/log-inspection.js +639 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +2 -2
- package/dist/src/lib/migration-nag.js +3 -3
- package/dist/src/lib/object-status-filter.d.ts +22 -0
- package/dist/src/lib/object-status-filter.js +45 -0
- package/dist/src/lib/object-status-filter.js.map +1 -0
- package/dist/src/lib/output.d.ts +24 -1
- package/dist/src/lib/output.js +42 -1
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/package-manager.d.ts +140 -0
- package/dist/src/lib/package-manager.js +305 -0
- package/dist/src/lib/package-manager.js.map +1 -0
- package/dist/src/lib/paginate.d.ts +50 -0
- package/dist/src/lib/paginate.js +53 -0
- package/dist/src/lib/paginate.js.map +1 -1
- package/dist/src/lib/platform-owned.d.ts +29 -3
- package/dist/src/lib/platform-owned.js +42 -3
- package/dist/src/lib/platform-owned.js.map +1 -1
- package/dist/src/lib/project-config.js +13 -5
- package/dist/src/lib/project-config.js.map +1 -1
- package/dist/src/lib/query-operators.d.ts +1 -1
- package/dist/src/lib/query-operators.js +1 -1
- package/dist/src/lib/record-filter.d.ts +18 -0
- package/dist/src/lib/record-filter.js +55 -0
- package/dist/src/lib/record-filter.js.map +1 -0
- package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
- package/dist/src/lib/resolve-init-dev-port.js +55 -0
- package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
- package/dist/src/lib/resolve-init-server.d.ts +64 -0
- package/dist/src/lib/resolve-init-server.js +77 -0
- package/dist/src/lib/resolve-init-server.js.map +1 -0
- package/dist/src/lib/resolve-platform.d.ts +43 -14
- package/dist/src/lib/resolve-platform.js +74 -12
- package/dist/src/lib/resolve-platform.js.map +1 -1
- package/dist/src/lib/run-status.d.ts +19 -0
- package/dist/src/lib/run-status.generated.d.ts +39 -0
- package/dist/src/lib/run-status.generated.js +66 -0
- package/dist/src/lib/run-status.generated.js.map +1 -0
- package/dist/src/lib/run-status.js +19 -0
- package/dist/src/lib/run-status.js.map +1 -0
- package/dist/src/lib/server-text-normalization.d.ts +51 -0
- package/dist/src/lib/server-text-normalization.js +90 -0
- package/dist/src/lib/server-text-normalization.js.map +1 -0
- package/dist/src/lib/server-url.d.ts +22 -0
- package/dist/src/lib/server-url.js +33 -0
- package/dist/src/lib/server-url.js.map +1 -0
- package/dist/src/lib/signing-secret-status.d.ts +81 -0
- package/dist/src/lib/signing-secret-status.js +116 -0
- package/dist/src/lib/signing-secret-status.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +4 -2
- package/dist/src/lib/skill-installer.js +143 -12
- package/dist/src/lib/skill-installer.js.map +1 -1
- package/dist/src/lib/snapshots.d.ts +2 -2
- package/dist/src/lib/snapshots.js +2 -2
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +15 -2
- package/dist/src/lib/swift-codegen/dbGenerator.js +250 -18
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/generator.d.ts +11 -2
- package/dist/src/lib/swift-codegen/generator.js +181 -7
- package/dist/src/lib/swift-codegen/generator.js.map +1 -1
- package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +7 -3
- package/dist/src/lib/swift-codegen/siblingSymbols.js +4 -1
- package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -1
- package/dist/src/lib/sync-dir-selector.d.ts +21 -0
- package/dist/src/lib/sync-dir-selector.js +30 -0
- package/dist/src/lib/sync-dir-selector.js.map +1 -0
- package/dist/src/lib/sync-paths.d.ts +39 -0
- package/dist/src/lib/sync-paths.js +70 -2
- package/dist/src/lib/sync-paths.js.map +1 -1
- package/dist/src/lib/sync-resource-types.d.ts +138 -31
- package/dist/src/lib/sync-resource-types.js +241 -47
- package/dist/src/lib/sync-resource-types.js.map +1 -1
- package/dist/src/lib/sync-selectors.d.ts +95 -0
- package/dist/src/lib/sync-selectors.js +228 -0
- package/dist/src/lib/sync-selectors.js.map +1 -0
- package/dist/src/lib/template.d.ts +62 -4
- package/dist/src/lib/template.js +147 -39
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/test-case-keys.d.ts +29 -0
- package/dist/src/lib/test-case-keys.js +55 -0
- package/dist/src/lib/test-case-keys.js.map +1 -0
- package/dist/src/lib/test-case-variables.d.ts +15 -0
- package/dist/src/lib/test-case-variables.js +29 -0
- package/dist/src/lib/test-case-variables.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +4 -4
- package/dist/src/lib/toml-database-config.js +27 -34
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/toml-metadata-config.d.ts +9 -5
- package/dist/src/lib/toml-metadata-config.js +40 -45
- package/dist/src/lib/toml-metadata-config.js.map +1 -1
- package/dist/src/lib/toml-native-form.d.ts +1 -1
- package/dist/src/lib/toml-native-form.js +1 -1
- package/dist/src/lib/toml-params-validator.d.ts +1 -1
- package/dist/src/lib/toml-params-validator.js +1 -1
- package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
- package/dist/src/lib/toml-scalar-edit.js +283 -0
- package/dist/src/lib/toml-scalar-edit.js.map +1 -0
- package/dist/src/lib/user-selector.d.ts +24 -0
- package/dist/src/lib/user-selector.js +33 -0
- package/dist/src/lib/user-selector.js.map +1 -0
- package/dist/src/lib/version-check.d.ts +25 -0
- package/dist/src/lib/version-check.js +77 -8
- package/dist/src/lib/version-check.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-apply.d.ts +69 -45
- package/dist/src/lib/workflow-apply.js +99 -72
- package/dist/src/lib/workflow-apply.js.map +1 -1
- package/dist/src/lib/workflow-codegen/generator.js +4 -4
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +11 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +5 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
- package/dist/src/lib/workflow-config-apply.d.ts +70 -0
- package/dist/src/lib/workflow-config-apply.js +137 -0
- package/dist/src/lib/workflow-config-apply.js.map +1 -0
- package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
- package/dist/src/lib/workflow-config-sidecar.js +96 -0
- package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
- package/dist/src/lib/workflow-defaults.d.ts +29 -0
- package/dist/src/lib/workflow-defaults.js +41 -0
- package/dist/src/lib/workflow-defaults.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-include-preserve.d.ts +76 -0
- package/dist/src/lib/workflow-include-preserve.js +286 -0
- package/dist/src/lib/workflow-include-preserve.js.map +1 -0
- package/dist/src/lib/workflow-payload.d.ts +31 -20
- package/dist/src/lib/workflow-payload.js +100 -93
- package/dist/src/lib/workflow-payload.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +80 -3
- package/dist/src/lib/workflow-toml-validator.js +231 -3
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/dist/src/types/index.d.ts +78 -22
- package/dist/src/validators.d.ts +7 -7
- package/dist/src/validators.js +6 -6
- package/package.json +11 -6
- package/dist/src/commands/settings.d.ts +0 -15
- package/dist/src/commands/settings.js +0 -102
- package/dist/src/commands/settings.js.map +0 -1
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
import { Option } from "commander";
|
|
1
2
|
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, readdirSync } from "fs";
|
|
2
3
|
import * as path from "path";
|
|
3
|
-
import { parseConfigToml } from "../lib/config-toml.js";
|
|
4
4
|
import { ApiClient } from "../lib/api-client.js";
|
|
5
5
|
import { getCurrentAppId } from "../lib/config.js";
|
|
6
6
|
import { resolveCodegenSourceDir } from "../lib/codegen-shared/resolveCodegenSourceDir.js";
|
|
7
|
+
import { resolveSyncDirOverride } from "../lib/sync-dir-selector.js";
|
|
7
8
|
import { parseCsv, applyColumnMap, buildRows } from "../lib/csv.js";
|
|
8
9
|
import { chunk, parseBatchSize } from "../lib/batch.js";
|
|
9
10
|
import { generateDbTypes, } from "../lib/db-codegen/dbGenerator.js";
|
|
10
11
|
import { generateDbSwiftTypes } from "../lib/swift-codegen/dbGenerator.js";
|
|
11
12
|
import { success, error, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, json, } from "../lib/output.js";
|
|
12
13
|
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
14
|
+
import { parseFilterOptions } from "../lib/record-filter.js";
|
|
15
|
+
import { parseDataOption } from "../lib/data-input.js";
|
|
16
|
+
import { ulid } from "ulid";
|
|
13
17
|
function resolveAppId(appId, options) {
|
|
14
18
|
const resolved = appId || options.app || getCurrentAppId();
|
|
15
19
|
if (!resolved) {
|
|
@@ -18,6 +22,43 @@ function resolveAppId(appId, options) {
|
|
|
18
22
|
}
|
|
19
23
|
return resolved;
|
|
20
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Render the flat `{ success, id, appliedFields }` response that
|
|
27
|
+
* `records/save` and `records/patch` return — these endpoints answer with a
|
|
28
|
+
* write receipt, not the stored record, so the human view shows the resolved id
|
|
29
|
+
* plus any server-applied fields (autoStamps / trigger-computed values).
|
|
30
|
+
*/
|
|
31
|
+
function renderRecordWriteResult(result, fallbackId, message) {
|
|
32
|
+
success(message);
|
|
33
|
+
keyValue("id", String(result?.id ?? fallbackId));
|
|
34
|
+
const applied = result?.appliedFields;
|
|
35
|
+
if (applied && typeof applied === "object" && Object.keys(applied).length > 0) {
|
|
36
|
+
for (const [key, value] of Object.entries(applied)) {
|
|
37
|
+
keyValue(`applied.${key}`, value === null || value === undefined ? "—" : String(value));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Refuse a write whose `--data` carries an `id` that is not the record being
|
|
43
|
+
* written. The server strips `id` from the record body on both the save and the
|
|
44
|
+
* patch path, so such an `id` would be dropped silently and the write would
|
|
45
|
+
* land on the id from the argument — somewhere the caller did not intend.
|
|
46
|
+
*
|
|
47
|
+
* `recordId` is the id the caller passed as an argument (absent on a `save`
|
|
48
|
+
* that mints one); `writtenId` is the id actually being written. Exits non-zero
|
|
49
|
+
* when they disagree with `--data` (#1965 review).
|
|
50
|
+
*/
|
|
51
|
+
function assertDataIdMatches(data, recordId, writtenId, commandPath) {
|
|
52
|
+
if (!data || typeof data !== "object")
|
|
53
|
+
return;
|
|
54
|
+
const dataId = data.id;
|
|
55
|
+
if (dataId === undefined || dataId === writtenId)
|
|
56
|
+
return;
|
|
57
|
+
error(recordId
|
|
58
|
+
? `The "id" in --data ("${dataId}") does not match the record id "${recordId}". Remove it, or pass the id you mean as the argument.`
|
|
59
|
+
: `Pass the record id as the argument, not inside --data: ${commandPath} ${dataId} --data ...`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
21
62
|
/**
|
|
22
63
|
* Validate that the named operation is an import-compatible, save-like
|
|
23
64
|
* mutation before importing (issue #145, Codex finding (a)).
|
|
@@ -61,6 +102,95 @@ async function validateImportOperation(client, appId, databaseId, operationName)
|
|
|
61
102
|
return;
|
|
62
103
|
}
|
|
63
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* The full `databases records bulk` precondition value space, audited in one
|
|
107
|
+
* pass (issue #2437, Codex cycle 3). A precondition is field-equality only, so
|
|
108
|
+
* a value is one of: `null`, a boolean, a number, or a string. For each, what
|
|
109
|
+
* the DatabaseDO `condition` grammar can express, compared against the
|
|
110
|
+
* documents twin's strict JS equality (`evaluatePrecondition`):
|
|
111
|
+
*
|
|
112
|
+
* | Value | Disposition | Why |
|
|
113
|
+
* |-------------------|-------------|----------------------------------------------------------------------|
|
|
114
|
+
* | string | round-trips | bound as TEXT; SQLite never equates TEXT with a number or a JSON bool |
|
|
115
|
+
* | number ≠ 0 and ≠1 | round-trips | bound as-is; no other JSON type extracts to that value |
|
|
116
|
+
* | `null` | rewritten | paired with `$exists: true` so an absent key fails (cycle-2 fix) |
|
|
117
|
+
* | `true` / `false` | REJECTED | bound as 1/0, and `json_extract` yields 1/0 for JSON booleans too |
|
|
118
|
+
* | number 0 or 1 | REJECTED | same collision seen from the other side: matches a stored `false`/`true` |
|
|
119
|
+
* | `{}` (no fields) | see #2511 | sent as an empty `condition`, which the DO reads as "record must exist" |
|
|
120
|
+
* | object / array | rejected | not field-equality; rejected by the bulk validator before reaching here |
|
|
121
|
+
*
|
|
122
|
+
* The two REJECTED rows are the boolean/number collision: `JsonQueryTranslator`
|
|
123
|
+
* binds `true`/`false` as `1`/`0`, and SQLite's `json_extract` also returns a
|
|
124
|
+
* stored JSON boolean as `1`/`0`, so `{ enabled: true }` matches a record
|
|
125
|
+
* holding the number `1` and `{ retries: 1 }` matches a record holding `true`.
|
|
126
|
+
* The documents twin compares with `!==` and fails both. The `condition`
|
|
127
|
+
* grammar has no type predicate (`$exists` only distinguishes present from
|
|
128
|
+
* absent), so this cannot be translated exactly — the CLI rejects those values
|
|
129
|
+
* at the boundary instead, so the databases twin fails loudly rather than
|
|
130
|
+
* running with a weaker guard than the caller wrote. Issue #2507 tracks giving
|
|
131
|
+
* the grammar a type-aware equality, which would let these values be accepted.
|
|
132
|
+
*
|
|
133
|
+
* Returns an explanation when the value cannot be translated exactly, or
|
|
134
|
+
* `null` when it round-trips or is rewritten.
|
|
135
|
+
*/
|
|
136
|
+
export function untranslatablePreconditionValue(value) {
|
|
137
|
+
if (typeof value === "boolean") {
|
|
138
|
+
return (`a boolean precondition cannot be checked exactly on a database — the ` +
|
|
139
|
+
`record store compares JSON booleans as 1/0, so \`${value}\` would also ` +
|
|
140
|
+
`match a record holding the number ${value ? 1 : 0}`);
|
|
141
|
+
}
|
|
142
|
+
if (value === 0 || value === 1) {
|
|
143
|
+
return (`the number ${value} cannot be checked exactly on a database — the ` +
|
|
144
|
+
`record store compares JSON booleans as 1/0, so ${value} would also ` +
|
|
145
|
+
`match a record holding \`${value === 1}\``);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Translate a `databases records bulk` op's documents-surface `precondition`
|
|
151
|
+
* into the DatabaseDO's per-op `condition` (issue #2437). See
|
|
152
|
+
* {@link untranslatablePreconditionValue} for the audit of the whole value
|
|
153
|
+
* space; this function handles the one value that is REWRITTEN rather than
|
|
154
|
+
* passed through or rejected.
|
|
155
|
+
*
|
|
156
|
+
* The two are NOT the same grammar, and a straight hand-off silently weakens
|
|
157
|
+
* the guard for one value: `null`. A `condition` is a query filter, so
|
|
158
|
+
* `{ approvedAt: null }` translates to
|
|
159
|
+
* `json_extract(_data, '$.approvedAt') IS NULL` — which is also true when the
|
|
160
|
+
* key is ABSENT. A guarded patch/delete would then execute against a record
|
|
161
|
+
* that never had the field. The documents surface evaluates the same
|
|
162
|
+
* precondition in JS (`evaluatePrecondition` in `src/document-models.ts`): an
|
|
163
|
+
* absent field reads `undefined`, and `undefined !== null`, so the guard fails
|
|
164
|
+
* closed there.
|
|
165
|
+
*
|
|
166
|
+
* Pin the database side to the documents meaning — "the key is present AND
|
|
167
|
+
* holds null" — by pairing each null check with an existence constraint.
|
|
168
|
+
* `$exists: true` translates to `json_type(_data, '$.<field>') IS NOT NULL`,
|
|
169
|
+
* true for a key holding JSON null and false for an absent key, so the pair
|
|
170
|
+
* emits `json_extract(...) IS NULL AND json_type(...) IS NOT NULL`.
|
|
171
|
+
*
|
|
172
|
+
* Non-null scalars need no rewrite: `= ?` against `json_extract` already fails
|
|
173
|
+
* for an absent field, because NULL never equals a bound value.
|
|
174
|
+
*
|
|
175
|
+
* Assumes the caller has already rejected `$`-prefixed keys, non-scalar values,
|
|
176
|
+
* and the values {@link untranslatablePreconditionValue} names — which is what
|
|
177
|
+
* leaves `$and` free to use here and what makes a pass-through exact.
|
|
178
|
+
*/
|
|
179
|
+
export function preconditionToCondition(precondition) {
|
|
180
|
+
const nullFields = Object.keys(precondition).filter((f) => precondition[f] === null);
|
|
181
|
+
if (nullFields.length === 0)
|
|
182
|
+
return { ...precondition };
|
|
183
|
+
const condition = {};
|
|
184
|
+
for (const [field, expected] of Object.entries(precondition)) {
|
|
185
|
+
if (expected !== null)
|
|
186
|
+
condition[field] = expected;
|
|
187
|
+
}
|
|
188
|
+
condition.$and = nullFields.flatMap((field) => [
|
|
189
|
+
{ [field]: null },
|
|
190
|
+
{ [field]: { $exists: true } },
|
|
191
|
+
]);
|
|
192
|
+
return condition;
|
|
193
|
+
}
|
|
64
194
|
export function registerDatabasesCommands(program) {
|
|
65
195
|
const databases = program
|
|
66
196
|
.command("databases")
|
|
@@ -68,11 +198,17 @@ export function registerDatabasesCommands(program) {
|
|
|
68
198
|
.addHelpText("after", `
|
|
69
199
|
Examples:
|
|
70
200
|
$ primitive databases list
|
|
201
|
+
$ primitive databases list --owner <user-id>
|
|
71
202
|
$ primitive databases create "My Database"
|
|
72
203
|
$ primitive databases get <database-id>
|
|
73
204
|
$ primitive databases records models <database-id>
|
|
74
205
|
$ primitive databases records describe <database-id> <model-name>
|
|
75
206
|
$ primitive databases records query <database-id> <model-name> --filter '{"name":"Alice"}'
|
|
207
|
+
$ primitive databases records get <database-id> <model-name> <record-id>
|
|
208
|
+
$ primitive databases records count <database-id> <model-name> --filter '{"status":"open"}'
|
|
209
|
+
$ primitive databases records aggregate <database-id> <model-name> --op avg --field price
|
|
210
|
+
$ primitive databases records save <database-id> <model-name> [record-id] --data '{"name":"Alice"}'
|
|
211
|
+
$ primitive databases records patch <database-id> <model-name> <record-id> --data '{"name":"Alice"}'
|
|
76
212
|
$ primitive databases operations list <database-id>
|
|
77
213
|
$ primitive databases operations execute <database-id> <operation-name> --params '{"key":"value"}'
|
|
78
214
|
$ primitive databases indexes list <database-id>
|
|
@@ -80,13 +216,14 @@ Examples:
|
|
|
80
216
|
$ primitive databases records delete <database-id> <model-name> <record-id> --yes
|
|
81
217
|
$ primitive databases records delete-all <database-id> <model-name> --yes
|
|
82
218
|
$ primitive databases records delete-all <database-id> <model-name> --filter '{"status":"draft"}' --yes
|
|
219
|
+
$ primitive databases records bulk <database-id> --data-file ops.json --yes
|
|
83
220
|
$ primitive databases permissions list <database-id>
|
|
84
221
|
|
|
85
|
-
Note: this group manages database *instances*.
|
|
86
|
-
|
|
87
|
-
the 'database-
|
|
88
|
-
$ primitive database-
|
|
89
|
-
$
|
|
222
|
+
Note: this group manages database *instances*. Database *type* configurations
|
|
223
|
+
(schemas, rules, operations) are authored in TOML — see 'primitive config fields
|
|
224
|
+
database-type-config' and the 'database-type-configs' group for inspection:
|
|
225
|
+
$ primitive database-type-configs list
|
|
226
|
+
$ rm database-type-configs/<database-type>.toml && primitive config push --prune
|
|
90
227
|
`);
|
|
91
228
|
// List databases
|
|
92
229
|
databases
|
|
@@ -94,12 +231,15 @@ the 'database-types' group:
|
|
|
94
231
|
.description("List databases in an app")
|
|
95
232
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
96
233
|
.option("--app <app-id>", "App ID")
|
|
234
|
+
.option("--owner <user-id>", "Only list databases created by this user")
|
|
97
235
|
.option("--json", "Output as JSON")
|
|
98
236
|
.action(async (appId, options) => {
|
|
99
237
|
const resolvedAppId = resolveAppId(appId, options);
|
|
100
238
|
const client = new ApiClient();
|
|
101
239
|
try {
|
|
102
|
-
const result = await client.listDatabases(resolvedAppId
|
|
240
|
+
const result = await client.listDatabases(resolvedAppId, {
|
|
241
|
+
owner: options.owner,
|
|
242
|
+
});
|
|
103
243
|
const list = Array.isArray(result) ? result : result?.databases ?? [];
|
|
104
244
|
if (options.json) {
|
|
105
245
|
json(list);
|
|
@@ -114,6 +254,18 @@ the 'database-types' group:
|
|
|
114
254
|
{ header: "TITLE", key: "title" },
|
|
115
255
|
{ header: "TYPE", key: "databaseType", format: (v) => v || "—" },
|
|
116
256
|
{ header: "PERMISSION", key: "permission" },
|
|
257
|
+
// `permission` reads "owner" on every row for an app admin, so it
|
|
258
|
+
// cannot confirm --owner narrowed anything. Show who created each
|
|
259
|
+
// database when the filter is in play.
|
|
260
|
+
...(options.owner
|
|
261
|
+
? [
|
|
262
|
+
{
|
|
263
|
+
header: "CREATED BY",
|
|
264
|
+
key: "createdBy",
|
|
265
|
+
format: (v) => (v ? formatId(v) : "—"),
|
|
266
|
+
},
|
|
267
|
+
]
|
|
268
|
+
: []),
|
|
117
269
|
{ header: "CREATED", key: "createdAt", format: formatDate },
|
|
118
270
|
]));
|
|
119
271
|
}
|
|
@@ -465,7 +617,7 @@ the 'database-types' group:
|
|
|
465
617
|
process.exit(1);
|
|
466
618
|
}
|
|
467
619
|
});
|
|
468
|
-
// Query records (direct introspection via
|
|
620
|
+
// Query records (direct introspection via the records route)
|
|
469
621
|
records
|
|
470
622
|
.command("query")
|
|
471
623
|
.description("Query records in a database model (admin only)")
|
|
@@ -480,43 +632,14 @@ the 'database-types' group:
|
|
|
480
632
|
.action(async (databaseId, modelName, options) => {
|
|
481
633
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
482
634
|
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
|
-
}
|
|
635
|
+
const filter = parseFilterOptions(options);
|
|
513
636
|
try {
|
|
514
637
|
const result = await client.queryDatabaseRecords(resolvedAppId, databaseId, modelName, { filter, limit: options.limit, cursor: options.cursor });
|
|
515
638
|
if (options.json) {
|
|
516
639
|
json(result);
|
|
517
640
|
return;
|
|
518
641
|
}
|
|
519
|
-
const records = result.
|
|
642
|
+
const records = result.items;
|
|
520
643
|
if (records.length === 0) {
|
|
521
644
|
info("No records found.");
|
|
522
645
|
return;
|
|
@@ -560,6 +683,228 @@ the 'database-types' group:
|
|
|
560
683
|
process.exit(1);
|
|
561
684
|
}
|
|
562
685
|
});
|
|
686
|
+
// Get a single record by id
|
|
687
|
+
records
|
|
688
|
+
.command("get")
|
|
689
|
+
.description("Get a single record by id (admin only)")
|
|
690
|
+
.argument("<database-id>", "Database ID")
|
|
691
|
+
.argument("<model-name>", "Model name")
|
|
692
|
+
.argument("<record-id>", "Record ID")
|
|
693
|
+
.option("--app <app-id>", "App ID")
|
|
694
|
+
.option("--json", "Output as JSON")
|
|
695
|
+
.action(async (databaseId, modelName, recordId, options) => {
|
|
696
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
697
|
+
const client = new ApiClient();
|
|
698
|
+
try {
|
|
699
|
+
// A single-record fetch is a query filtered by primary-key id (there is
|
|
700
|
+
// no dedicated get endpoint — reuse queryDatabaseRecords).
|
|
701
|
+
const result = await client.queryDatabaseRecords(resolvedAppId, databaseId, modelName, { filter: { id: recordId }, limit: 1 });
|
|
702
|
+
const record = result.items[0];
|
|
703
|
+
if (options.json) {
|
|
704
|
+
json(record ?? null);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (!record) {
|
|
708
|
+
info(`No record found with id ${recordId} in ${modelName}.`);
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
// `get` is a data command, so its fields go to stdout via `result()`
|
|
712
|
+
// — the #711 split (`result()` = the data the caller asked for,
|
|
713
|
+
// `keyValue()` = a post-action diagnostic summary). Issue #2437; the
|
|
714
|
+
// documents twin renders identically.
|
|
715
|
+
for (const [key, value] of Object.entries(record)) {
|
|
716
|
+
const rendered = value === null || value === undefined
|
|
717
|
+
? "—"
|
|
718
|
+
: typeof value === "object"
|
|
719
|
+
? JSON.stringify(value)
|
|
720
|
+
: String(value);
|
|
721
|
+
printResult(key, rendered);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
error(err.message);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
// Count records
|
|
730
|
+
records
|
|
731
|
+
.command("count")
|
|
732
|
+
.description("Count records in a database model (admin only)")
|
|
733
|
+
.argument("<database-id>", "Database ID")
|
|
734
|
+
.argument("<model-name>", "Model name")
|
|
735
|
+
.option("--app <app-id>", "App ID")
|
|
736
|
+
.option("--filter <json>", "Filter as JSON (e.g. '{\"name\":\"Alice\"}')")
|
|
737
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
738
|
+
.option("--json", "Output as JSON")
|
|
739
|
+
.action(async (databaseId, modelName, options) => {
|
|
740
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
741
|
+
const filter = parseFilterOptions(options);
|
|
742
|
+
const client = new ApiClient();
|
|
743
|
+
try {
|
|
744
|
+
const result = await client.countDatabaseRecords(resolvedAppId, databaseId, modelName, filter);
|
|
745
|
+
if (options.json) {
|
|
746
|
+
json(result);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
keyValue("count", String(result?.count ?? 0));
|
|
750
|
+
}
|
|
751
|
+
catch (err) {
|
|
752
|
+
error(err.message);
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
});
|
|
756
|
+
// Aggregate records
|
|
757
|
+
const AGGREGATE_OPS = ["count", "sum", "avg", "min", "max"];
|
|
758
|
+
records
|
|
759
|
+
.command("aggregate")
|
|
760
|
+
.description("Aggregate records in a database model (count/sum/avg/min/max, optional group-by; admin only)")
|
|
761
|
+
.argument("<database-id>", "Database ID")
|
|
762
|
+
.argument("<model-name>", "Model name")
|
|
763
|
+
.option("--app <app-id>", "App ID")
|
|
764
|
+
.requiredOption("--op <operation>", `Aggregate operation: ${AGGREGATE_OPS.join("|")}`)
|
|
765
|
+
.option("--field <field>", "Field to aggregate (required for sum/avg/min/max)")
|
|
766
|
+
.option("--group-by <field>", "Group results by this field (repeatable)", (value, previous = []) => previous.concat(value), [])
|
|
767
|
+
.option("--filter <json>", "Filter as JSON")
|
|
768
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
769
|
+
.option("--json", "Output as JSON")
|
|
770
|
+
.action(async (databaseId, modelName, options) => {
|
|
771
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
772
|
+
if (!AGGREGATE_OPS.includes(options.op)) {
|
|
773
|
+
error(`Invalid --op "${options.op}". Expected one of: ${AGGREGATE_OPS.join(", ")}.`);
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
const operation = { type: options.op };
|
|
777
|
+
if (options.op === "count") {
|
|
778
|
+
if (options.field) {
|
|
779
|
+
error("--field is not valid for --op count.");
|
|
780
|
+
process.exit(1);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
if (!options.field) {
|
|
785
|
+
error(`--op ${options.op} requires --field.`);
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
operation.field = options.field;
|
|
789
|
+
}
|
|
790
|
+
const filter = parseFilterOptions(options);
|
|
791
|
+
const groupBy = options.groupBy || [];
|
|
792
|
+
const client = new ApiClient();
|
|
793
|
+
try {
|
|
794
|
+
const result = await client.aggregateDatabaseRecords(resolvedAppId, databaseId, modelName, { operations: [operation], groupBy, filter });
|
|
795
|
+
if (options.json) {
|
|
796
|
+
json(result);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const aggregated = result?.result;
|
|
800
|
+
// Label the value column after the operation, including the field it
|
|
801
|
+
// ran over: `SUM` alone does not say what was summed. The field name is
|
|
802
|
+
// reproduced verbatim in both labels — it is a user-supplied
|
|
803
|
+
// identifier, and case-folding it produced `avg(lastvalue)` for
|
|
804
|
+
// `--field lastValue` (issue #2357).
|
|
805
|
+
const valueLabel = options.op === "count"
|
|
806
|
+
? "COUNT"
|
|
807
|
+
: `${options.op.toUpperCase()}(${options.field})`;
|
|
808
|
+
const scalarLabel = options.op === "count"
|
|
809
|
+
? "count"
|
|
810
|
+
: `${options.op.toLowerCase()}(${options.field})`;
|
|
811
|
+
if (groupBy.length === 0) {
|
|
812
|
+
// Ungrouped: one scalar per operation, keyed by the endpoint's
|
|
813
|
+
// operation name (`count`, `sum_<field>`, …). Render it as a
|
|
814
|
+
// key/value line — there is no group column to table.
|
|
815
|
+
const value = (aggregated ?? {})[options.op === "count" ? "count" : `${options.op}_${options.field}`];
|
|
816
|
+
keyValue(scalarLabel, value === null || value === undefined ? "—" : String(value));
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (aggregated &&
|
|
820
|
+
typeof aggregated === "object" &&
|
|
821
|
+
!Array.isArray(aggregated) &&
|
|
822
|
+
Object.values(aggregated).every((v) => typeof v !== "object" || v === null)) {
|
|
823
|
+
// Flat { group -> value } shape (single operation, single group-by): render a table.
|
|
824
|
+
const rows = Object.entries(aggregated).map(([group, value]) => ({
|
|
825
|
+
group,
|
|
826
|
+
value: value === null || value === undefined ? "—" : String(value),
|
|
827
|
+
}));
|
|
828
|
+
console.log(formatTable(rows, [
|
|
829
|
+
{ header: "GROUP", key: "group" },
|
|
830
|
+
{ header: valueLabel, key: "value" },
|
|
831
|
+
]));
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
// Grouped/nested or multi-operation shape: print the structured result.
|
|
835
|
+
console.log(JSON.stringify(aggregated ?? {}, null, 2));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
catch (err) {
|
|
839
|
+
error(err.message);
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
// Upsert a record (create, or merge into an existing one)
|
|
844
|
+
records
|
|
845
|
+
.command("save")
|
|
846
|
+
.description("Create a record, or merge fields into an existing one (admin only)")
|
|
847
|
+
.argument("<database-id>", "Database ID")
|
|
848
|
+
.argument("<model-name>", "Model name")
|
|
849
|
+
.argument("[record-id]", "Record ID (generated when omitted)")
|
|
850
|
+
.option("--app <app-id>", "App ID")
|
|
851
|
+
.option("--data <json>", "Record fields as inline JSON")
|
|
852
|
+
.option("--data-file <path>", "Read the record fields from a JSON file")
|
|
853
|
+
.option("--json", "Output as JSON")
|
|
854
|
+
.action(async (databaseId, modelName, recordId, options) => {
|
|
855
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
856
|
+
const data = parseDataOption(options, "record");
|
|
857
|
+
const client = new ApiClient();
|
|
858
|
+
// The DatabaseDO requires an id on the plain save path (it only generates
|
|
859
|
+
// one for `upsertOn` writes). Mint the id client-side for a create so
|
|
860
|
+
// `save` works without an id, the same way CSV import does
|
|
861
|
+
// (`lib/csv.ts`).
|
|
862
|
+
const id = recordId || ulid();
|
|
863
|
+
assertDataIdMatches(data, recordId, id, "primitive databases records save <database-id> <model-name>");
|
|
864
|
+
try {
|
|
865
|
+
const result = await client.saveDatabaseRecord(resolvedAppId, databaseId, modelName, id, data);
|
|
866
|
+
if (options.json) {
|
|
867
|
+
json(result);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
renderRecordWriteResult(result, id, `Record saved to ${modelName}.`);
|
|
871
|
+
}
|
|
872
|
+
catch (err) {
|
|
873
|
+
error(err.message);
|
|
874
|
+
process.exit(1);
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
// Patch a record (partial merge)
|
|
878
|
+
records
|
|
879
|
+
.command("patch")
|
|
880
|
+
.description("Merge fields into an existing record (admin only)")
|
|
881
|
+
.argument("<database-id>", "Database ID")
|
|
882
|
+
.argument("<model-name>", "Model name")
|
|
883
|
+
.argument("<record-id>", "Record ID")
|
|
884
|
+
.option("--app <app-id>", "App ID")
|
|
885
|
+
.option("--data <json>", "Fields to merge as inline JSON")
|
|
886
|
+
.option("--data-file <path>", "Read the fields to merge from a JSON file")
|
|
887
|
+
.option("--json", "Output as JSON")
|
|
888
|
+
.action(async (databaseId, modelName, recordId, options) => {
|
|
889
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
890
|
+
const data = parseDataOption(options, "fields to merge");
|
|
891
|
+
const client = new ApiClient();
|
|
892
|
+
// Same guard as `save` — on this path `recordId` is required, so the
|
|
893
|
+
// "pass it as the argument" branch is unreachable here.
|
|
894
|
+
assertDataIdMatches(data, recordId, recordId, "primitive databases records patch <database-id> <model-name>");
|
|
895
|
+
try {
|
|
896
|
+
const result = await client.patchDatabaseRecord(resolvedAppId, databaseId, modelName, recordId, data);
|
|
897
|
+
if (options.json) {
|
|
898
|
+
json(result);
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
renderRecordWriteResult(result, recordId, `Record ${recordId} patched in ${modelName}.`);
|
|
902
|
+
}
|
|
903
|
+
catch (err) {
|
|
904
|
+
error(err.message);
|
|
905
|
+
process.exit(1);
|
|
906
|
+
}
|
|
907
|
+
});
|
|
563
908
|
// Delete a single record
|
|
564
909
|
records
|
|
565
910
|
.command("delete")
|
|
@@ -608,21 +953,13 @@ the 'database-types' group:
|
|
|
608
953
|
.argument("<model-name>", "Model name")
|
|
609
954
|
.option("--app <app-id>", "App ID")
|
|
610
955
|
.option("--filter <json>", "Filter as JSON to select which records to delete")
|
|
956
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
611
957
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
612
958
|
.option("--json", "Output as JSON")
|
|
613
959
|
.action(async (databaseId, modelName, options) => {
|
|
614
960
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
615
961
|
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
|
-
}
|
|
962
|
+
const filter = parseFilterOptions(options);
|
|
626
963
|
try {
|
|
627
964
|
// First, query to find all matching records
|
|
628
965
|
let allRecords = [];
|
|
@@ -630,9 +967,9 @@ the 'database-types' group:
|
|
|
630
967
|
let hasMore = true;
|
|
631
968
|
while (hasMore) {
|
|
632
969
|
const result = await client.queryDatabaseRecords(resolvedAppId, databaseId, modelName, { filter, limit: 100, cursor });
|
|
633
|
-
const records = result
|
|
970
|
+
const records = result.items;
|
|
634
971
|
allRecords = allRecords.concat(records);
|
|
635
|
-
cursor = result
|
|
972
|
+
cursor = result.nextCursor;
|
|
636
973
|
hasMore = !!cursor && records.length > 0;
|
|
637
974
|
}
|
|
638
975
|
if (allRecords.length === 0) {
|
|
@@ -681,6 +1018,206 @@ the 'database-types' group:
|
|
|
681
1018
|
process.exit(1);
|
|
682
1019
|
}
|
|
683
1020
|
});
|
|
1021
|
+
// Apply an atomic multi-model operations blob (issue #2437).
|
|
1022
|
+
//
|
|
1023
|
+
// Takes the SAME `--data-file` blob as `documents records bulk` — one file
|
|
1024
|
+
// works against either surface — and translates it to the DatabaseDO's
|
|
1025
|
+
// `records/batch` wire shape (`{ op, modelName, id, data }`) with
|
|
1026
|
+
// `atomic: true`, so the whole blob is all-or-nothing like the documents
|
|
1027
|
+
// twin. Both guards the documents blob can carry survive the translation:
|
|
1028
|
+
// `action: create` maps to `save` with `ifNotExists: true` (the DatabaseDO's
|
|
1029
|
+
// strict-create flag, a 409 `ALREADY_EXISTS` inside the transaction), and an
|
|
1030
|
+
// op's `precondition` maps to the DO's per-op `condition` (a 409
|
|
1031
|
+
// `CONDITION_NOT_MET`). Both abort the whole batch, matching the documents
|
|
1032
|
+
// twin.
|
|
1033
|
+
const BULK_ACTIONS = ["create", "patch", "delete"];
|
|
1034
|
+
const BULK_ACTION_TO_OP = {
|
|
1035
|
+
create: "save",
|
|
1036
|
+
patch: "patch",
|
|
1037
|
+
delete: "delete",
|
|
1038
|
+
};
|
|
1039
|
+
// Mirrors the documents surface's `BULK_WRITE_OP_KEYS` / `BULK_DELETE_OP_KEYS`
|
|
1040
|
+
// (`src/document-models.ts`). Unknown keys are an error rather than ignored:
|
|
1041
|
+
// a guard the author asked for must never be dropped silently.
|
|
1042
|
+
const BULK_OP_KEYS = ["model", "action", "id", "data", "precondition"];
|
|
1043
|
+
const BULK_DELETE_OP_KEYS = ["model", "action", "id", "precondition"];
|
|
1044
|
+
records
|
|
1045
|
+
.command("bulk")
|
|
1046
|
+
.description("Apply an atomic create/patch/delete operations blob to a database (admin only)")
|
|
1047
|
+
.argument("<database-id>", "Database ID")
|
|
1048
|
+
.requiredOption("--data-file <file>", "JSON file with { operations: [...] } or a bare operations array. " +
|
|
1049
|
+
"Each op is { model, action: create|patch|delete, id, data, precondition? } " +
|
|
1050
|
+
"— the same shape `documents records bulk` takes. `data` holds the " +
|
|
1051
|
+
"record fields and is required on create/patch, not allowed on delete. " +
|
|
1052
|
+
"A `create` against an id that already exists fails the whole batch, " +
|
|
1053
|
+
"and a `precondition` that does not hold rolls the whole batch back")
|
|
1054
|
+
.option("--app <app-id>", "App ID")
|
|
1055
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
1056
|
+
.option("--json", "Output as JSON")
|
|
1057
|
+
.action(async (databaseId, options) => {
|
|
1058
|
+
const parsed = parseDataOption(options, "operations blob", {
|
|
1059
|
+
allowArray: true,
|
|
1060
|
+
});
|
|
1061
|
+
const rawOperations = Array.isArray(parsed) ? parsed : parsed?.operations;
|
|
1062
|
+
if (!Array.isArray(rawOperations) || rawOperations.length === 0) {
|
|
1063
|
+
error("--data-file must contain { operations: [...] } or a non-empty operations array.");
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
// Translate to the batch wire shape, rejecting a malformed op before any
|
|
1067
|
+
// request goes out so the caller sees which entry is wrong.
|
|
1068
|
+
const operations = [];
|
|
1069
|
+
for (let i = 0; i < rawOperations.length; i++) {
|
|
1070
|
+
const op = rawOperations[i];
|
|
1071
|
+
if (!op || typeof op !== "object" || Array.isArray(op)) {
|
|
1072
|
+
error(`Operation ${i}: each operation must be a JSON object.`);
|
|
1073
|
+
process.exit(1);
|
|
1074
|
+
}
|
|
1075
|
+
const action = op.action;
|
|
1076
|
+
if (!BULK_ACTIONS.includes(action)) {
|
|
1077
|
+
error(`Operation ${i}: invalid action "${action}". Expected one of: ${BULK_ACTIONS.join(", ")}.`);
|
|
1078
|
+
process.exit(1);
|
|
1079
|
+
}
|
|
1080
|
+
if (!op.model || typeof op.model !== "string") {
|
|
1081
|
+
error(`Operation ${i}: "model" is required and must be a model name.`);
|
|
1082
|
+
process.exit(1);
|
|
1083
|
+
}
|
|
1084
|
+
if (!op.id || typeof op.id !== "string") {
|
|
1085
|
+
error(`Operation ${i}: "id" is required.`);
|
|
1086
|
+
process.exit(1);
|
|
1087
|
+
}
|
|
1088
|
+
// `data` on a delete gets its own message before the allowlist runs, so
|
|
1089
|
+
// the caller is not told a first-class key is "unknown".
|
|
1090
|
+
if (action === "delete" && op.data !== undefined) {
|
|
1091
|
+
error(`Operation ${i}: "data" is not allowed on a delete.`);
|
|
1092
|
+
process.exit(1);
|
|
1093
|
+
}
|
|
1094
|
+
const allowedKeys = action === "delete" ? BULK_DELETE_OP_KEYS : BULK_OP_KEYS;
|
|
1095
|
+
const unknownKey = Object.keys(op).find((k) => !allowedKeys.includes(k));
|
|
1096
|
+
if (unknownKey) {
|
|
1097
|
+
error(`Operation ${i}: unknown key "${unknownKey}". Expected one of: ${allowedKeys.join(", ")}.`);
|
|
1098
|
+
process.exit(1);
|
|
1099
|
+
}
|
|
1100
|
+
// A `precondition` is field-equality only, the same grammar the
|
|
1101
|
+
// documents surface enforces (`validatePrecondition` in
|
|
1102
|
+
// `src/document-models.ts`), so one blob means the same thing on both.
|
|
1103
|
+
// `preconditionToCondition` translates it to the DO's per-op
|
|
1104
|
+
// `condition`, checked inside the transaction: a mismatch is a 409 that
|
|
1105
|
+
// rolls the whole batch back.
|
|
1106
|
+
let condition;
|
|
1107
|
+
if (op.precondition !== undefined && op.precondition !== null) {
|
|
1108
|
+
if (typeof op.precondition !== "object" || Array.isArray(op.precondition)) {
|
|
1109
|
+
error(`Operation ${i}: "precondition" must be a field-equality object ({ field: scalar, … }).`);
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
for (const [field, expected] of Object.entries(op.precondition)) {
|
|
1113
|
+
if (field.startsWith("$")) {
|
|
1114
|
+
error(`Operation ${i}: precondition operator "${field}" is not supported — preconditions are field-equality only ({ field: scalar, … }).`);
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
if (expected !== null && typeof expected === "object") {
|
|
1118
|
+
error(`Operation ${i}: precondition on field "${field}" must be a scalar equality value — operators and nested objects/arrays are not supported.`);
|
|
1119
|
+
process.exit(1);
|
|
1120
|
+
}
|
|
1121
|
+
// Fail loudly on the values the `condition` grammar cannot check
|
|
1122
|
+
// exactly, rather than running with a weaker guard than the caller
|
|
1123
|
+
// wrote (issue #2437; the grammar fix is tracked in #2507).
|
|
1124
|
+
const untranslatable = untranslatablePreconditionValue(expected);
|
|
1125
|
+
if (untranslatable) {
|
|
1126
|
+
error(`Operation ${i}: precondition on field "${field}" is not supported — ${untranslatable}. ` +
|
|
1127
|
+
`Guard on a field whose value is a string or a number other than 0/1, or read the record ` +
|
|
1128
|
+
`first and gate the write on that.`);
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
condition = preconditionToCondition(op.precondition);
|
|
1133
|
+
}
|
|
1134
|
+
if (action === "delete") {
|
|
1135
|
+
if (op.data !== undefined) {
|
|
1136
|
+
error(`Operation ${i}: "data" is not allowed on a delete.`);
|
|
1137
|
+
process.exit(1);
|
|
1138
|
+
}
|
|
1139
|
+
operations.push({
|
|
1140
|
+
op: "delete",
|
|
1141
|
+
modelName: op.model,
|
|
1142
|
+
id: op.id,
|
|
1143
|
+
...(condition ? { condition } : {}),
|
|
1144
|
+
});
|
|
1145
|
+
continue;
|
|
1146
|
+
}
|
|
1147
|
+
if (!op.data ||
|
|
1148
|
+
typeof op.data !== "object" ||
|
|
1149
|
+
Array.isArray(op.data) ||
|
|
1150
|
+
Object.keys(op.data).length === 0) {
|
|
1151
|
+
error(`Operation ${i}: "data" is required and must be a non-empty object on a ${action}.`);
|
|
1152
|
+
process.exit(1);
|
|
1153
|
+
}
|
|
1154
|
+
operations.push({
|
|
1155
|
+
op: BULK_ACTION_TO_OP[action],
|
|
1156
|
+
modelName: op.model,
|
|
1157
|
+
id: op.id,
|
|
1158
|
+
data: op.data,
|
|
1159
|
+
// `create` is a STRICT create on both twins: `ifNotExists` is the
|
|
1160
|
+
// DatabaseDO batch's strict-create flag (409 `ALREADY_EXISTS`), so a
|
|
1161
|
+
// create against a live id fails the batch instead of merging.
|
|
1162
|
+
...(action === "create" ? { ifNotExists: true } : {}),
|
|
1163
|
+
...(condition ? { condition } : {}),
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1167
|
+
if (!options.yes) {
|
|
1168
|
+
let confirm;
|
|
1169
|
+
try {
|
|
1170
|
+
confirm = await confirmPrompt(`Apply ${operations.length} operation(s) atomically to database ${databaseId}?`);
|
|
1171
|
+
}
|
|
1172
|
+
catch (err) {
|
|
1173
|
+
error(err.message);
|
|
1174
|
+
process.exit(1);
|
|
1175
|
+
}
|
|
1176
|
+
if (!confirm) {
|
|
1177
|
+
info("Cancelled.");
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const client = new ApiClient();
|
|
1182
|
+
try {
|
|
1183
|
+
await client.batchDatabaseRecords(resolvedAppId, databaseId, operations, {
|
|
1184
|
+
atomic: true,
|
|
1185
|
+
});
|
|
1186
|
+
// The batch endpoint answers with a per-operation `results` array; the
|
|
1187
|
+
// documents twin answers with a rolled-up summary. Report the twin's
|
|
1188
|
+
// summary so one blob produces one contract on both surfaces — the
|
|
1189
|
+
// batch is atomic, so a 2xx means every op in the input applied.
|
|
1190
|
+
//
|
|
1191
|
+
// One reporting difference, documented alongside the verb: the
|
|
1192
|
+
// documents twin's `deleted` counts records actually removed, because
|
|
1193
|
+
// its DO reports that. The database batch issues a plain DELETE and
|
|
1194
|
+
// reports no rows-affected, so `deleted` here counts delete operations
|
|
1195
|
+
// applied — deleting an id that was not there still counts 1.
|
|
1196
|
+
const added = rawOperations
|
|
1197
|
+
.filter((op) => op.action === "create")
|
|
1198
|
+
.map((op) => ({ model: op.model, id: op.id }));
|
|
1199
|
+
const updated = rawOperations
|
|
1200
|
+
.filter((op) => op.action === "patch")
|
|
1201
|
+
.map((op) => ({ model: op.model, id: op.id }));
|
|
1202
|
+
const deleted = rawOperations.filter((op) => op.action === "delete").length;
|
|
1203
|
+
const summary = {
|
|
1204
|
+
applied: added.length + updated.length + deleted,
|
|
1205
|
+
added,
|
|
1206
|
+
updated,
|
|
1207
|
+
deleted,
|
|
1208
|
+
};
|
|
1209
|
+
if (options.json) {
|
|
1210
|
+
json(summary);
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
success(`Applied ${summary.applied} operation(s): ` +
|
|
1214
|
+
`${added.length} added, ${updated.length} updated, ${deleted} deleted.`);
|
|
1215
|
+
}
|
|
1216
|
+
catch (err) {
|
|
1217
|
+
error(err.message);
|
|
1218
|
+
process.exit(1);
|
|
1219
|
+
}
|
|
1220
|
+
});
|
|
684
1221
|
// ---- CEL context subcommand group ----
|
|
685
1222
|
// `cel-context` is the user-facing name for the dict that feeds CEL
|
|
686
1223
|
// access rules and filter placeholders. `metadata` is kept as a hidden
|
|
@@ -837,14 +1374,17 @@ the 'database-types' group:
|
|
|
837
1374
|
json(result);
|
|
838
1375
|
return;
|
|
839
1376
|
}
|
|
840
|
-
// Smart display based on result shape
|
|
841
|
-
|
|
842
|
-
|
|
1377
|
+
// Smart display based on result shape. `executeDatabaseOperation()`
|
|
1378
|
+
// has already normalized a query page to `{ items, hasMore,
|
|
1379
|
+
// nextCursor? }` (#2440), so `items` is exactly "this result was a
|
|
1380
|
+
// list" — the two branches cannot disagree about it.
|
|
1381
|
+
if (Array.isArray(result.items)) {
|
|
1382
|
+
if (result.items.length === 0) {
|
|
843
1383
|
info("No records found.");
|
|
844
1384
|
}
|
|
845
1385
|
else {
|
|
846
|
-
const cols = Object.keys(result.
|
|
847
|
-
console.log(formatTable(result.
|
|
1386
|
+
const cols = Object.keys(result.items[0]).filter((k) => k !== "type");
|
|
1387
|
+
console.log(formatTable(result.items, cols.map((c) => ({
|
|
848
1388
|
header: c.toUpperCase(),
|
|
849
1389
|
key: c,
|
|
850
1390
|
format: (v) => {
|
|
@@ -859,8 +1399,10 @@ the 'database-types' group:
|
|
|
859
1399
|
},
|
|
860
1400
|
}))));
|
|
861
1401
|
}
|
|
862
|
-
|
|
863
|
-
|
|
1402
|
+
// Gate on the cursor itself, the way `records query` does: `hasMore`
|
|
1403
|
+
// without a cursor used to print the literal `undefined` (#2440).
|
|
1404
|
+
if (result.nextCursor) {
|
|
1405
|
+
info(`More records available. Use --cursor ${result.nextCursor} to continue.`);
|
|
864
1406
|
}
|
|
865
1407
|
}
|
|
866
1408
|
else if (result.count !== undefined) {
|
|
@@ -1094,7 +1636,7 @@ the 'database-types' group:
|
|
|
1094
1636
|
}
|
|
1095
1637
|
// Validate --batch-size up front so a bad value fails before any write.
|
|
1096
1638
|
// Unlike `import-csv` (registered-batch path, hard 100k server cap),
|
|
1097
|
-
// `
|
|
1639
|
+
// `records/batch` has no fixed server item cap — bound it with a
|
|
1098
1640
|
// conservative client-side ceiling to keep each request within
|
|
1099
1641
|
// Cloudflare body-size and Durable Object memory limits.
|
|
1100
1642
|
const parsedBatchSize = parseBatchSize(options.batchSize, {
|
|
@@ -1368,13 +1910,17 @@ the 'database-types' group:
|
|
|
1368
1910
|
.description("Manage the TOML schema attached to a database type");
|
|
1369
1911
|
schema
|
|
1370
1912
|
.command("generate")
|
|
1371
|
-
.description("Scaffold a starting TOML schema for <database-type> from existing operations + DO introspection, and insert it into the local database-
|
|
1913
|
+
.description("Scaffold a starting TOML schema for <database-type> from existing operations + DO introspection, and insert it into the local database-type-configs/<type>.toml file.")
|
|
1372
1914
|
.argument("<database-type>", "Database type name")
|
|
1373
1915
|
.option("--app <app-id>", "App ID")
|
|
1374
|
-
.option("--
|
|
1916
|
+
.option("--dir <path>", "Config directory (overrides the auto-resolved per-env path)")
|
|
1917
|
+
.addOption(new Option("--sync-dir <path>", "Deprecated alias of --dir").hideHelp())
|
|
1375
1918
|
.option("-f, --force", "Overwrite an existing [models.*] block in the local file")
|
|
1376
1919
|
.option("--json", "Output the generated TOML as JSON")
|
|
1377
1920
|
.action(async (databaseType, options) => {
|
|
1921
|
+
// Resolve the sync-dir override before the API call, so a caller who
|
|
1922
|
+
// passed both spellings hears about it without a network round trip.
|
|
1923
|
+
const configDir = resolveSyncDirOverride(options, "databases schema generate");
|
|
1378
1924
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
1379
1925
|
const client = new ApiClient();
|
|
1380
1926
|
try {
|
|
@@ -1385,14 +1931,14 @@ the 'database-types' group:
|
|
|
1385
1931
|
json({ schema: scaffoldedToml });
|
|
1386
1932
|
return;
|
|
1387
1933
|
}
|
|
1388
|
-
// 2. Locate the local `database-
|
|
1934
|
+
// 2. Locate the local `database-type-configs/<type>.toml` file. Per the
|
|
1389
1935
|
// plan-of-record CLI sync layout, the file lives at
|
|
1390
|
-
// `<
|
|
1391
|
-
// exact
|
|
1936
|
+
// `<config-dir>/database-type-configs/<type>.toml`. We don't need to know
|
|
1937
|
+
// the exact config dir for the happy path — just look for an existing
|
|
1392
1938
|
// file relative to the CWD's `.primitive/sync/` directory tree.
|
|
1393
1939
|
const candidateFiles = [];
|
|
1394
|
-
if (
|
|
1395
|
-
candidateFiles.push(path.join(
|
|
1940
|
+
if (configDir) {
|
|
1941
|
+
candidateFiles.push(path.join(configDir, "database-type-configs", `${databaseType}.toml`));
|
|
1396
1942
|
}
|
|
1397
1943
|
else {
|
|
1398
1944
|
// Search any env/appId subtree under .primitive/sync that matches.
|
|
@@ -1402,7 +1948,7 @@ the 'database-types' group:
|
|
|
1402
1948
|
const envDir = path.join(root, env);
|
|
1403
1949
|
try {
|
|
1404
1950
|
for (const app of readdirSync(envDir)) {
|
|
1405
|
-
const candidate = path.join(envDir, app, "database-
|
|
1951
|
+
const candidate = path.join(envDir, app, "database-type-configs", `${databaseType}.toml`);
|
|
1406
1952
|
if (existsSync(candidate)) {
|
|
1407
1953
|
candidateFiles.push(candidate);
|
|
1408
1954
|
}
|
|
@@ -1417,7 +1963,7 @@ the 'database-types' group:
|
|
|
1417
1963
|
if (candidateFiles.length === 0) {
|
|
1418
1964
|
// No local file found — print the scaffold to stdout and tell
|
|
1419
1965
|
// the user where to put it.
|
|
1420
|
-
info(`No local database-
|
|
1966
|
+
info(`No local database-type-configs/${databaseType}.toml found under .primitive/sync/. Generated TOML:`);
|
|
1421
1967
|
console.log("");
|
|
1422
1968
|
console.log(scaffoldedToml);
|
|
1423
1969
|
return;
|
|
@@ -1457,7 +2003,7 @@ the 'database-types' group:
|
|
|
1457
2003
|
writeFileSync(targetPath, merged);
|
|
1458
2004
|
success(`Inserted schema into ${targetPath}`);
|
|
1459
2005
|
}
|
|
1460
|
-
info("Review the generated schema, fix types the generator guessed wrong, then run `primitive
|
|
2006
|
+
info("Review the generated schema, fix types the generator guessed wrong, then run `primitive config push` (or `primitive config push --dry-run`).");
|
|
1461
2007
|
}
|
|
1462
2008
|
catch (err) {
|
|
1463
2009
|
error(err.message);
|
|
@@ -1469,11 +2015,12 @@ the 'database-types' group:
|
|
|
1469
2015
|
// ============================================
|
|
1470
2016
|
databases
|
|
1471
2017
|
.command("codegen")
|
|
1472
|
-
.description("Generate record + operation types from the local database-
|
|
1473
|
-
.argument("[database-type]", "Generate for a single database type (defaults to every database-
|
|
2018
|
+
.description("Generate record + operation types from the local database-type-configs/*.toml schema. TypeScript (default): a <type>.generated.ts per database type (record interfaces, per-op input-params interfaces, per-op result aliases, and a typed <type>Ops factory). Swift (--lang swift): a <type>.generated.swift per database type (record structs, per-op params structs, per-op result typealiases, and a typed <Type>Ops factory).\n\nFormatting: the emitted TypeScript is already formatted the way Prettier's DEFAULT config formats it, so a `codegen` -> `prettier --write` -> `codegen --check` pipeline stays green. If your project uses a NON-default Prettier config (a different printWidth, trailingComma, or quote style), Prettier will reformat the generated files and `--check` will then fail: add the generated directory to `.prettierignore` and commit the raw emission instead.")
|
|
2019
|
+
.argument("[database-type]", "Generate for a single database type (defaults to every database-type-configs/*.toml found)")
|
|
1474
2020
|
.option("--app <app-id>", "App ID")
|
|
1475
|
-
.option("--
|
|
1476
|
-
.
|
|
2021
|
+
.option("--dir <path>", "Config directory (overrides the auto-resolved per-env path)")
|
|
2022
|
+
.addOption(new Option("--sync-dir <path>", "Deprecated alias of --dir").hideHelp())
|
|
2023
|
+
.option("-o, --output <dir>", "Output directory for the generated files (defaults to <config-dir>/database-type-configs/generated/)")
|
|
1477
2024
|
.option("--lang <lang>", "Target language for the generated types: 'ts' (default) or 'swift'.", "ts")
|
|
1478
2025
|
.option("--check", "Exit non-zero if generated output is out of date (CI guard); does not write. Compares byte-for-byte, so the file on disk must match the emission exactly.")
|
|
1479
2026
|
.option("--json", "Output the result summary as JSON")
|
|
@@ -1486,14 +2033,17 @@ the 'database-types' group:
|
|
|
1486
2033
|
error(`Unknown --lang "${options.lang}". Use "ts" or "swift".`);
|
|
1487
2034
|
process.exit(1);
|
|
1488
2035
|
}
|
|
1489
|
-
// 1. Resolve the single source database-
|
|
2036
|
+
// 1. Resolve the single source database-type-configs/ directory via the
|
|
1490
2037
|
// shared active-environment resolver (issue #1510). Honors
|
|
1491
|
-
// --
|
|
2038
|
+
// --dir / --app overrides and resolves the active env
|
|
1492
2039
|
// (--env → PRIMITIVE_ENV → defaultEnvironment → single-env) instead
|
|
1493
2040
|
// of silently merging types across every synced env.
|
|
1494
2041
|
const dbTypesSourceDir = resolveCodegenSourceDir({
|
|
1495
|
-
subdir: "database-
|
|
1496
|
-
options: {
|
|
2042
|
+
subdir: "database-type-configs",
|
|
2043
|
+
options: {
|
|
2044
|
+
app: options.app,
|
|
2045
|
+
syncDir: resolveSyncDirOverride(options, "databases codegen"),
|
|
2046
|
+
},
|
|
1497
2047
|
});
|
|
1498
2048
|
// 2. Collect the source .toml files (one per database type), filtered
|
|
1499
2049
|
// to a single type when an argument is given.
|
|
@@ -1513,8 +2063,8 @@ the 'database-types' group:
|
|
|
1513
2063
|
}
|
|
1514
2064
|
if (inputs.length === 0) {
|
|
1515
2065
|
error(databaseType
|
|
1516
|
-
? `No database-
|
|
1517
|
-
: "No database-
|
|
2066
|
+
? `No database-type-configs/${databaseType}.toml found under .primitive/sync/.`
|
|
2067
|
+
: "No database-type-configs/*.toml files found to generate from.");
|
|
1518
2068
|
process.exit(1);
|
|
1519
2069
|
}
|
|
1520
2070
|
// 3. Resolve the output directory. Default: a `generated/` subdir of
|
|
@@ -1552,9 +2102,21 @@ the 'database-types' group:
|
|
|
1552
2102
|
for (const m of codegenResult.mismatches) {
|
|
1553
2103
|
error(` ${m.reason}: ${path.relative(process.cwd(), m.filePath)}`);
|
|
1554
2104
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
2105
|
+
// Echo back the options that decide WHERE and WHAT was
|
|
2106
|
+
// checked, the way `workflows codegen` does (#2895). Suggesting
|
|
2107
|
+
// a bare `databases codegen` after a `--check -o <dir>` sent
|
|
2108
|
+
// people to regenerate into the default
|
|
2109
|
+
// <config-dir>/database-type-configs/generated/ instead of the
|
|
2110
|
+
// directory that is actually out of date, so the files stayed
|
|
2111
|
+
// stale and the next check failed the same way (#2911).
|
|
2112
|
+
const regenerate = ["primitive databases codegen"];
|
|
2113
|
+
if (databaseType)
|
|
2114
|
+
regenerate.push(databaseType);
|
|
2115
|
+
if (lang === "swift")
|
|
2116
|
+
regenerate.push("--lang swift");
|
|
2117
|
+
if (options.output)
|
|
2118
|
+
regenerate.push(`-o ${options.output}`);
|
|
2119
|
+
info(`Run \`${regenerate.join(" ")}\` to regenerate.`);
|
|
1558
2120
|
}
|
|
1559
2121
|
process.exit(1);
|
|
1560
2122
|
}
|
|
@@ -1620,7 +2182,7 @@ async function exportSingleDatabase(client, appId, databaseId, outputDir, jsonOu
|
|
|
1620
2182
|
const dbMeta = await client.getDatabase(appId, databaseId);
|
|
1621
2183
|
// Warn about databaseType sync
|
|
1622
2184
|
if (dbMeta.databaseType) {
|
|
1623
|
-
warn(`This database uses type "${dbMeta.databaseType}". Make sure to \`primitive
|
|
2185
|
+
warn(`This database uses type "${dbMeta.databaseType}". Make sure to \`primitive config push\` the type config before importing into a new app.`);
|
|
1624
2186
|
}
|
|
1625
2187
|
// 2. Write metadata
|
|
1626
2188
|
const metadata = {
|
|
@@ -1648,13 +2210,13 @@ async function exportSingleDatabase(client, appId, databaseId, outputDir, jsonOu
|
|
|
1648
2210
|
limit: 100,
|
|
1649
2211
|
cursor,
|
|
1650
2212
|
});
|
|
1651
|
-
const records = result
|
|
2213
|
+
const records = result.items;
|
|
1652
2214
|
for (const record of records) {
|
|
1653
2215
|
const line = JSON.stringify({ _type: modelName, _id: record.id, _data: record });
|
|
1654
2216
|
appendFileSync(recordsFile, line + "\n");
|
|
1655
2217
|
totalRecords++;
|
|
1656
2218
|
}
|
|
1657
|
-
cursor = result
|
|
2219
|
+
cursor = result.nextCursor;
|
|
1658
2220
|
hasMore = !!cursor && records.length > 0;
|
|
1659
2221
|
}
|
|
1660
2222
|
}
|
|
@@ -1685,7 +2247,7 @@ async function exportSingleDatabase(client, appId, databaseId, outputDir, jsonOu
|
|
|
1685
2247
|
}
|
|
1686
2248
|
/**
|
|
1687
2249
|
* Conservative client-side ceiling for `databases import --batch-size`.
|
|
1688
|
-
* `
|
|
2250
|
+
* `records/batch` has no fixed server-side item cap (unlike the
|
|
1689
2251
|
* registered-batch path used by `import-csv`), so this bounds each request to
|
|
1690
2252
|
* keep it within Cloudflare body-size and Durable Object memory limits.
|
|
1691
2253
|
*/
|
|
@@ -1713,7 +2275,7 @@ async function importSingleDatabase(client, appId, dbDir, options) {
|
|
|
1713
2275
|
}
|
|
1714
2276
|
// Check if database type exists — warn if not
|
|
1715
2277
|
if (metadata.databaseType) {
|
|
1716
|
-
warn(`Database uses type "${metadata.databaseType}". Ensure the type config exists in the target app (use \`primitive
|
|
2278
|
+
warn(`Database uses type "${metadata.databaseType}". Ensure the type config exists in the target app (use \`primitive config push\`).`);
|
|
1717
2279
|
}
|
|
1718
2280
|
// Check if database exists — use the original ID for overwrite scenarios
|
|
1719
2281
|
let targetDatabaseId = databaseId;
|