primitive-admin 1.1.0-alpha.9 → 1.1.0
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 +523 -109
- package/assets/skill/skills/primitive-platform/SKILL.md +289 -0
- package/dist/bin/primitive.d.ts +2 -0
- package/dist/bin/primitive.js +372 -21
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.d.ts +2 -0
- package/dist/src/commands/admins.js +136 -30
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.d.ts +2 -0
- package/dist/src/commands/analytics.js +713 -55
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.d.ts +2 -0
- package/dist/src/commands/apps.js +65 -100
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth-sessions.d.ts +7 -0
- package/dist/src/commands/auth-sessions.js +144 -0
- package/dist/src/commands/auth-sessions.js.map +1 -0
- package/dist/src/commands/auth.d.ts +2 -0
- package/dist/src/commands/auth.js +238 -108
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.d.ts +2 -0
- package/dist/src/commands/blob-buckets.js +331 -0
- package/dist/src/commands/blob-buckets.js.map +1 -0
- package/dist/src/commands/catalog.d.ts +2 -0
- package/dist/src/commands/catalog.js +63 -48
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.d.ts +2 -0
- package/dist/src/commands/collection-type-configs.js +85 -0
- package/dist/src/commands/collection-type-configs.js.map +1 -0
- package/dist/src/commands/collections.d.ts +2 -0
- package/dist/src/commands/collections.js +1282 -0
- package/dist/src/commands/collections.js.map +1 -0
- package/dist/src/commands/comparisons.d.ts +2 -0
- package/dist/src/commands/comparisons.js +6 -6
- package/dist/src/commands/comparisons.js.map +1 -1
- package/dist/src/commands/config.d.ts +46 -0
- package/dist/src/commands/config.js +465 -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 +99 -0
- package/dist/src/commands/connections.js.map +1 -0
- package/dist/src/commands/cron-triggers.d.ts +2 -0
- package/dist/src/commands/cron-triggers.js +266 -0
- package/dist/src/commands/cron-triggers.js.map +1 -0
- package/dist/src/commands/database-type-configs.d.ts +2 -0
- package/dist/src/commands/database-type-configs.js +164 -0
- package/dist/src/commands/database-type-configs.js.map +1 -0
- package/dist/src/commands/database-types.d.ts +2 -0
- package/dist/src/commands/database-types.js +471 -0
- package/dist/src/commands/database-types.js.map +1 -0
- package/dist/src/commands/databases.d.ts +65 -0
- package/dist/src/commands/databases.js +1887 -243
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +60 -0
- package/dist/src/commands/documents.js +2888 -22
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.d.ts +2 -0
- package/dist/src/commands/email-templates.js +175 -0
- package/dist/src/commands/email-templates.js.map +1 -0
- package/dist/src/commands/env.d.ts +23 -0
- package/dist/src/commands/env.js +395 -0
- package/dist/src/commands/env.js.map +1 -0
- package/dist/src/commands/feature-flags.d.ts +14 -0
- package/dist/src/commands/feature-flags.js +117 -0
- package/dist/src/commands/feature-flags.js.map +1 -0
- package/dist/src/commands/functions.d.ts +20 -0
- package/dist/src/commands/functions.js +1630 -0
- package/dist/src/commands/functions.js.map +1 -0
- package/dist/src/commands/group-type-configs.d.ts +2 -0
- package/dist/src/commands/group-type-configs.js +87 -0
- package/dist/src/commands/group-type-configs.js.map +1 -0
- package/dist/src/commands/groups.d.ts +2 -0
- package/dist/src/commands/groups.js +65 -113
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.d.ts +223 -0
- package/dist/src/commands/guides.js +627 -69
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/init.d.ts +37 -0
- package/dist/src/commands/init.js +1693 -208
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.d.ts +2 -0
- package/dist/src/commands/integrations.js +428 -187
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/llm.d.ts +2 -0
- package/dist/src/commands/llm.js +4 -2
- package/dist/src/commands/llm.js.map +1 -1
- package/dist/src/commands/locks.d.ts +8 -0
- package/dist/src/commands/locks.js +175 -0
- package/dist/src/commands/locks.js.map +1 -0
- package/dist/src/commands/metadata-category-configs.d.ts +12 -0
- package/dist/src/commands/metadata-category-configs.js +113 -0
- package/dist/src/commands/metadata-category-configs.js.map +1 -0
- package/dist/src/commands/metadata.d.ts +2 -0
- package/dist/src/commands/metadata.js +288 -0
- package/dist/src/commands/metadata.js.map +1 -0
- package/dist/src/commands/prompts.d.ts +2 -0
- package/dist/src/commands/prompts.js +322 -634
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +3 -0
- package/dist/src/commands/rule-sets.js +139 -148
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.d.ts +30 -0
- package/dist/src/commands/scripts.js +688 -0
- package/dist/src/commands/scripts.js.map +1 -0
- package/dist/src/commands/secrets.d.ts +2 -0
- package/dist/src/commands/secrets.js +109 -0
- package/dist/src/commands/secrets.js.map +1 -0
- package/dist/src/commands/sessions.d.ts +2 -0
- package/dist/src/commands/sessions.js +76 -0
- package/dist/src/commands/sessions.js.map +1 -0
- package/dist/src/commands/skill.d.ts +2 -0
- package/dist/src/commands/skill.js +29 -0
- package/dist/src/commands/skill.js.map +1 -0
- package/dist/src/commands/sync-app-settings.d.ts +158 -0
- package/dist/src/commands/sync-app-settings.js +328 -0
- package/dist/src/commands/sync-app-settings.js.map +1 -0
- package/dist/src/commands/sync.d.ts +2670 -0
- package/dist/src/commands/sync.js +17144 -834
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.d.ts +2 -0
- package/dist/src/commands/tokens.js +132 -22
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.d.ts +2 -0
- package/dist/src/commands/users.js +542 -24
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.d.ts +8 -0
- package/dist/src/commands/vars.js +97 -0
- package/dist/src/commands/vars.js.map +1 -0
- package/dist/src/commands/waitlist.d.ts +2 -0
- package/dist/src/commands/waitlist.js +12 -11
- package/dist/src/commands/waitlist.js.map +1 -1
- package/dist/src/commands/webhooks.d.ts +31 -0
- package/dist/src/commands/webhooks.js +633 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/workflows.d.ts +88 -0
- package/dist/src/commands/workflows.js +1568 -742
- 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 +2550 -0
- package/dist/src/lib/api-client.js +2503 -160
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +263 -0
- package/dist/src/lib/app-settings-descriptor.js +583 -0
- package/dist/src/lib/app-settings-descriptor.js.map +1 -0
- package/dist/src/lib/auth-flow.d.ts +8 -0
- package/dist/src/lib/batch.d.ts +26 -0
- package/dist/src/lib/batch.js +32 -0
- package/dist/src/lib/batch.js.map +1 -0
- package/dist/src/lib/block-layout.d.ts +160 -0
- package/dist/src/lib/block-layout.js +451 -0
- package/dist/src/lib/block-layout.js.map +1 -0
- package/dist/src/lib/block-selector.d.ts +58 -0
- package/dist/src/lib/block-selector.js +92 -0
- package/dist/src/lib/block-selector.js.map +1 -0
- 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/channel.d.ts +30 -0
- package/dist/src/lib/channel.js +68 -0
- package/dist/src/lib/channel.js.map +1 -0
- package/dist/src/lib/cli-manifest.d.ts +68 -0
- package/dist/src/lib/cli-manifest.js +71 -0
- package/dist/src/lib/cli-manifest.js.map +1 -0
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +101 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js +191 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -0
- package/dist/src/lib/codegen-shared/prettierStable.d.ts +262 -0
- package/dist/src/lib/codegen-shared/prettierStable.js +610 -0
- package/dist/src/lib/codegen-shared/prettierStable.js.map +1 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +38 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +46 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
- package/dist/src/lib/collection-export.d.ts +184 -0
- package/dist/src/lib/collection-export.js +252 -0
- package/dist/src/lib/collection-export.js.map +1 -0
- package/dist/src/lib/config-json-field.d.ts +28 -0
- package/dist/src/lib/config-json-field.js +56 -0
- package/dist/src/lib/config-json-field.js.map +1 -0
- package/dist/src/lib/config-object-descriptor.d.ts +127 -0
- package/dist/src/lib/config-object-descriptor.js +740 -0
- package/dist/src/lib/config-object-descriptor.js.map +1 -0
- package/dist/src/lib/config-payload.d.ts +92 -0
- package/dist/src/lib/config-payload.js +161 -0
- package/dist/src/lib/config-payload.js.map +1 -0
- package/dist/src/lib/config-surface.d.ts +141 -0
- package/dist/src/lib/config-surface.js +368 -0
- package/dist/src/lib/config-surface.js.map +1 -0
- package/dist/src/lib/config-toml.d.ts +10 -0
- package/dist/src/lib/config-toml.js +42 -0
- package/dist/src/lib/config-toml.js.map +1 -0
- package/dist/src/lib/config.d.ts +71 -0
- package/dist/src/lib/config.js +71 -68
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/confirm-prompt.d.ts +83 -0
- package/dist/src/lib/confirm-prompt.js +110 -0
- package/dist/src/lib/confirm-prompt.js.map +1 -0
- package/dist/src/lib/constants.d.ts +11 -0
- package/dist/src/lib/constants.js +12 -0
- package/dist/src/lib/constants.js.map +1 -0
- package/dist/src/lib/crash-handlers.d.ts +20 -0
- package/dist/src/lib/crash-handlers.js +49 -0
- package/dist/src/lib/crash-handlers.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +104 -0
- package/dist/src/lib/credentials-store.js +336 -0
- package/dist/src/lib/credentials-store.js.map +1 -0
- package/dist/src/lib/csv.d.ts +47 -0
- package/dist/src/lib/csv.js +172 -0
- package/dist/src/lib/csv.js.map +1 -0
- 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/dbFingerprint.d.ts +10 -0
- package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
- package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
- package/dist/src/lib/db-codegen/dbGenerator.d.ts +67 -0
- package/dist/src/lib/db-codegen/dbGenerator.js +170 -0
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/db-codegen/dbNaming.d.ts +87 -0
- package/dist/src/lib/db-codegen/dbNaming.js +180 -0
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +272 -0
- package/dist/src/lib/db-codegen/dbTemplates.js +480 -0
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTsTypes.d.ts +73 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js +139 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +146 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js +525 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.d.ts +112 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js +211 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js.map +1 -0
- package/dist/src/lib/deprecation.d.ts +22 -0
- package/dist/src/lib/deprecation.js +43 -0
- package/dist/src/lib/deprecation.js.map +1 -0
- package/dist/src/lib/document-export-permissions.d.ts +30 -0
- package/dist/src/lib/document-export-permissions.js +54 -0
- package/dist/src/lib/document-export-permissions.js.map +1 -0
- package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
- package/dist/src/lib/document-ingest-artifact.js +505 -0
- package/dist/src/lib/document-ingest-artifact.js.map +1 -0
- package/dist/src/lib/document-ingest-input.d.ts +51 -0
- package/dist/src/lib/document-ingest-input.js +132 -0
- package/dist/src/lib/document-ingest-input.js.map +1 -0
- package/dist/src/lib/document-ingest-rows.d.ts +137 -0
- package/dist/src/lib/document-ingest-rows.js +181 -0
- package/dist/src/lib/document-ingest-rows.js.map +1 -0
- package/dist/src/lib/document-ingest.d.ts +101 -0
- package/dist/src/lib/document-ingest.js +384 -0
- package/dist/src/lib/document-ingest.js.map +1 -0
- package/dist/src/lib/env-resolver-core.d.ts +258 -0
- package/dist/src/lib/env-resolver-core.js +447 -0
- package/dist/src/lib/env-resolver-core.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +99 -0
- package/dist/src/lib/env-resolver.js +153 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/fetch.d.ts +5 -0
- package/dist/src/lib/function-bundle.d.ts +147 -0
- package/dist/src/lib/function-bundle.js +341 -0
- package/dist/src/lib/function-bundle.js.map +1 -0
- package/dist/src/lib/function-collect.d.ts +123 -0
- package/dist/src/lib/function-collect.js +610 -0
- package/dist/src/lib/function-collect.js.map +1 -0
- package/dist/src/lib/function-db-types.d.ts +202 -0
- package/dist/src/lib/function-db-types.js +870 -0
- package/dist/src/lib/function-db-types.js.map +1 -0
- package/dist/src/lib/function-document-types.d.ts +144 -0
- package/dist/src/lib/function-document-types.js +370 -0
- package/dist/src/lib/function-document-types.js.map +1 -0
- package/dist/src/lib/function-grants-preflight.d.ts +64 -0
- package/dist/src/lib/function-grants-preflight.js +105 -0
- package/dist/src/lib/function-grants-preflight.js.map +1 -0
- package/dist/src/lib/function-log-lines.d.ts +76 -0
- package/dist/src/lib/function-log-lines.js +160 -0
- package/dist/src/lib/function-log-lines.js.map +1 -0
- package/dist/src/lib/function-log-row.d.ts +29 -0
- package/dist/src/lib/function-log-row.js +73 -0
- package/dist/src/lib/function-log-row.js.map +1 -0
- package/dist/src/lib/function-log-tail.d.ts +132 -0
- package/dist/src/lib/function-log-tail.js +262 -0
- package/dist/src/lib/function-log-tail.js.map +1 -0
- package/dist/src/lib/function-run.d.ts +289 -0
- package/dist/src/lib/function-run.js +389 -0
- package/dist/src/lib/function-run.js.map +1 -0
- package/dist/src/lib/function-schema-codegen.d.ts +143 -0
- package/dist/src/lib/function-schema-codegen.js +420 -0
- package/dist/src/lib/function-schema-codegen.js.map +1 -0
- package/dist/src/lib/function-sync.d.ts +459 -0
- package/dist/src/lib/function-sync.js +1258 -0
- package/dist/src/lib/function-sync.js.map +1 -0
- package/dist/src/lib/function-trigger-listing.d.ts +27 -0
- package/dist/src/lib/function-trigger-listing.js +70 -0
- package/dist/src/lib/function-trigger-listing.js.map +1 -0
- package/dist/src/lib/function-typecheck.d.ts +86 -0
- package/dist/src/lib/function-typecheck.js +370 -0
- package/dist/src/lib/function-typecheck.js.map +1 -0
- package/dist/src/lib/function-versions.d.ts +122 -0
- package/dist/src/lib/function-versions.js +182 -0
- package/dist/src/lib/function-versions.js.map +1 -0
- package/dist/src/lib/generated-allowlist.d.ts +28 -0
- package/dist/src/lib/generated-allowlist.js +281 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -0
- package/dist/src/lib/generated-config-surfaces.d.ts +2936 -0
- package/dist/src/lib/generated-config-surfaces.js +10569 -0
- package/dist/src/lib/generated-config-surfaces.js.map +1 -0
- package/dist/src/lib/generated-sdk-types.d.ts +12 -0
- package/dist/src/lib/generated-sdk-types.js +13 -0
- package/dist/src/lib/generated-sdk-types.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-client-platforms.d.ts +14 -0
- package/dist/src/lib/init-client-platforms.js +71 -0
- package/dist/src/lib/init-client-platforms.js.map +1 -0
- package/dist/src/lib/init-config.d.ts +98 -0
- package/dist/src/lib/init-config.js +186 -0
- package/dist/src/lib/init-config.js.map +1 -0
- package/dist/src/lib/init-email-redirect-uris.d.ts +37 -0
- package/dist/src/lib/init-email-redirect-uris.js +46 -0
- package/dist/src/lib/init-email-redirect-uris.js.map +1 -0
- package/dist/src/lib/init-ios-links.d.ts +91 -0
- package/dist/src/lib/init-ios-links.js +219 -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 +34 -0
- package/dist/src/lib/init-xcode.js +138 -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/integration-selector.d.ts +42 -0
- package/dist/src/lib/integration-selector.js +46 -0
- package/dist/src/lib/integration-selector.js.map +1 -0
- package/dist/src/lib/ios-app-id.d.ts +34 -0
- package/dist/src/lib/ios-app-id.js +69 -0
- package/dist/src/lib/ios-app-id.js.map +1 -0
- package/dist/src/lib/list-options.d.ts +68 -0
- package/dist/src/lib/list-options.js +89 -0
- package/dist/src/lib/list-options.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/local-test-cases.d.ts +63 -0
- package/dist/src/lib/local-test-cases.js +136 -0
- package/dist/src/lib/local-test-cases.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +715 -0
- package/dist/src/lib/log-inspection.js +816 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/logout-admin-session.d.ts +33 -0
- package/dist/src/lib/logout-admin-session.js +70 -0
- package/dist/src/lib/logout-admin-session.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +49 -0
- package/dist/src/lib/migration-nag.js +163 -0
- package/dist/src/lib/migration-nag.js.map +1 -0
- 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 +124 -0
- package/dist/src/lib/output.js +219 -8
- 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 +98 -0
- package/dist/src/lib/paginate.js +112 -0
- package/dist/src/lib/paginate.js.map +1 -0
- package/dist/src/lib/platform-owned.d.ts +63 -0
- package/dist/src/lib/platform-owned.js +85 -0
- package/dist/src/lib/platform-owned.js.map +1 -0
- package/dist/src/lib/project-config.d.ts +122 -0
- package/dist/src/lib/project-config.js +244 -0
- package/dist/src/lib/project-config.js.map +1 -0
- package/dist/src/lib/prompt-cost-format.d.ts +11 -0
- package/dist/src/lib/prompt-cost-format.js +41 -0
- package/dist/src/lib/prompt-cost-format.js.map +1 -0
- package/dist/src/lib/prompt-schema-codegen.d.ts +147 -0
- package/dist/src/lib/prompt-schema-codegen.js +462 -0
- package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
- package/dist/src/lib/query-operators.d.ts +43 -0
- package/dist/src/lib/query-operators.js +80 -0
- package/dist/src/lib/query-operators.js.map +1 -0
- 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/refresh-admin-credentials.d.ts +73 -0
- package/dist/src/lib/refresh-admin-credentials.js +123 -0
- package/dist/src/lib/refresh-admin-credentials.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-owner.d.ts +19 -0
- package/dist/src/lib/resolve-owner.js +20 -0
- package/dist/src/lib/resolve-owner.js.map +1 -0
- package/dist/src/lib/resolve-platform.d.ts +74 -0
- package/dist/src/lib/resolve-platform.js +105 -0
- package/dist/src/lib/resolve-platform.js.map +1 -0
- 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 +71 -0
- package/dist/src/lib/skill-installer.js +441 -0
- package/dist/src/lib/skill-installer.js.map +1 -0
- package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
- package/dist/src/lib/snapshot-audit-source.js +58 -0
- package/dist/src/lib/snapshot-audit-source.js.map +1 -0
- package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
- package/dist/src/lib/snapshot-audit-store.js +196 -0
- package/dist/src/lib/snapshot-audit-store.js.map +1 -0
- package/dist/src/lib/snapshot-audit.d.ts +207 -0
- package/dist/src/lib/snapshot-audit.js +431 -0
- package/dist/src/lib/snapshot-audit.js.map +1 -0
- package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
- package/dist/src/lib/snapshot-build-rows.js +87 -0
- package/dist/src/lib/snapshot-build-rows.js.map +1 -0
- package/dist/src/lib/snapshot-build.d.ts +50 -0
- package/dist/src/lib/snapshot-build.js +111 -0
- package/dist/src/lib/snapshot-build.js.map +1 -0
- package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
- package/dist/src/lib/snapshot-manifest-layout.js +70 -0
- package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
- package/dist/src/lib/snapshots.d.ts +98 -0
- package/dist/src/lib/snapshots.js +294 -0
- package/dist/src/lib/snapshots.js.map +1 -0
- package/dist/src/lib/step-run-table.d.ts +43 -0
- package/dist/src/lib/step-run-table.js +129 -0
- package/dist/src/lib/step-run-table.js.map +1 -0
- package/dist/src/lib/storage-pending-retry.d.ts +26 -0
- package/dist/src/lib/storage-pending-retry.js +42 -0
- package/dist/src/lib/storage-pending-retry.js.map +1 -0
- package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/banners.d.ts +24 -0
- package/dist/src/lib/swift-codegen/banners.js +25 -0
- package/dist/src/lib/swift-codegen/banners.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +113 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js +926 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.d.ts +42 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js +100 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js.map +1 -0
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +139 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js +462 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/generator.d.ts +100 -0
- package/dist/src/lib/swift-codegen/generator.js +457 -0
- package/dist/src/lib/swift-codegen/generator.js.map +1 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +87 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js +661 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +94 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.js +155 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -0
- package/dist/src/lib/swift-codegen/swiftNaming.d.ts +85 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js +198 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js.map +1 -0
- 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 +128 -0
- package/dist/src/lib/sync-paths.js +195 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/sync-resource-types.d.ts +563 -0
- package/dist/src/lib/sync-resource-types.js +1073 -0
- package/dist/src/lib/sync-resource-types.js.map +1 -0
- package/dist/src/lib/sync-selectors.d.ts +138 -0
- package/dist/src/lib/sync-selectors.js +289 -0
- package/dist/src/lib/sync-selectors.js.map +1 -0
- package/dist/src/lib/template.d.ts +170 -0
- package/dist/src/lib/template.js +484 -68
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/test-case-file-names.d.ts +40 -0
- package/dist/src/lib/test-case-file-names.js +91 -0
- package/dist/src/lib/test-case-file-names.js.map +1 -0
- 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 +29 -0
- package/dist/src/lib/test-case-variables.js +71 -0
- package/dist/src/lib/test-case-variables.js.map +1 -0
- package/dist/src/lib/token-inject.d.ts +56 -0
- package/dist/src/lib/token-inject.js +204 -0
- package/dist/src/lib/token-inject.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +123 -0
- package/dist/src/lib/toml-database-config.js +544 -0
- package/dist/src/lib/toml-database-config.js.map +1 -0
- package/dist/src/lib/toml-metadata-config.d.ts +151 -0
- package/dist/src/lib/toml-metadata-config.js +476 -0
- package/dist/src/lib/toml-metadata-config.js.map +1 -0
- package/dist/src/lib/toml-native-form.d.ts +46 -0
- package/dist/src/lib/toml-native-form.js +78 -0
- package/dist/src/lib/toml-native-form.js.map +1 -0
- package/dist/src/lib/toml-params-validator.d.ts +129 -0
- package/dist/src/lib/toml-params-validator.js +298 -0
- package/dist/src/lib/toml-params-validator.js.map +1 -0
- 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 +35 -0
- package/dist/src/lib/version-check.js +241 -0
- package/dist/src/lib/version-check.js.map +1 -0
- 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/web-url.d.ts +40 -0
- package/dist/src/lib/web-url.js +76 -0
- package/dist/src/lib/web-url.js.map +1 -0
- package/dist/src/lib/webhook-deliver.d.ts +209 -0
- package/dist/src/lib/webhook-deliver.js +519 -0
- package/dist/src/lib/webhook-deliver.js.map +1 -0
- package/dist/src/lib/workflow-apply.d.ts +110 -0
- package/dist/src/lib/workflow-apply.js +164 -0
- package/dist/src/lib/workflow-apply.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +129 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +269 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generator.d.ts +96 -0
- package/dist/src/lib/workflow-codegen/generator.js +361 -0
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -0
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +94 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +76 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -0
- package/dist/src/lib/workflow-codegen/naming.d.ts +33 -0
- package/dist/src/lib/workflow-codegen/naming.js +81 -0
- package/dist/src/lib/workflow-codegen/naming.js.map +1 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +80 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js +303 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -0
- 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 +64 -0
- package/dist/src/lib/workflow-fragments.js +342 -0
- package/dist/src/lib/workflow-fragments.js.map +1 -0
- 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 +98 -0
- package/dist/src/lib/workflow-payload.js +178 -0
- package/dist/src/lib/workflow-payload.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.d.ts +211 -0
- package/dist/src/lib/workflow-toml-validator.js +770 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- package/dist/src/lib/workflow-usage.d.ts +196 -0
- package/dist/src/lib/workflow-usage.js +310 -0
- package/dist/src/lib/workflow-usage.js.map +1 -0
- package/dist/src/types/index.d.ts +591 -0
- package/dist/src/validators.d.ts +65 -0
- package/dist/src/validators.js +64 -0
- package/dist/src/validators.js.map +1 -0
- package/package.json +34 -9
|
@@ -1,13 +1,191 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, appendFileSync, mkdirSync, existsSync, readdirSync } from "fs";
|
|
2
|
+
import * as path from "path";
|
|
1
3
|
import { ApiClient } from "../lib/api-client.js";
|
|
2
|
-
import { getCurrentAppId } from "../lib/config.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { getCurrentAppId, resolveAppId } from "../lib/config.js";
|
|
5
|
+
import { resolveCodegenSourceDir } from "../lib/codegen-shared/resolveCodegenSourceDir.js";
|
|
6
|
+
import { resolveSyncDir } from "../lib/sync-paths.js";
|
|
7
|
+
import { parseCsv, applyColumnMap, buildRows } from "../lib/csv.js";
|
|
8
|
+
import { chunk, parseBatchSize } from "../lib/batch.js";
|
|
9
|
+
import { generateDbTypes, } from "../lib/db-codegen/dbGenerator.js";
|
|
10
|
+
import { generateDbSwiftTypes } from "../lib/swift-codegen/dbGenerator.js";
|
|
11
|
+
import { success, error, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, json, } from "../lib/output.js";
|
|
12
|
+
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
13
|
+
import { deprecateCommandGroup, warnDeprecated, } from "../lib/deprecation.js";
|
|
14
|
+
/** Successor for the deprecated database CEL context commands and flags. */
|
|
15
|
+
const CEL_CONTEXT_SUCCESSOR = "resource metadata categories (`primitive metadata get|set database <database-id> <category>`; define categories with `primitive config push`)";
|
|
16
|
+
import { parseFilterOptions } from "../lib/record-filter.js";
|
|
17
|
+
import { parseDataOption } from "../lib/data-input.js";
|
|
18
|
+
import { ulid } from "ulid";
|
|
19
|
+
import { normalizeCliListEnvelope, wholeListEnvelope, } from "../lib/paginate.js";
|
|
20
|
+
import { pageCursorOption, pageLimitOption, parsePageLimit, printEmptyPage, printPageHint, } from "../lib/list-options.js";
|
|
21
|
+
/**
|
|
22
|
+
* Render the flat `{ success, id, appliedFields }` response that
|
|
23
|
+
* `records/save` and `records/patch` return — these endpoints answer with a
|
|
24
|
+
* write receipt, not the stored record, so the human view shows the resolved id
|
|
25
|
+
* plus any server-applied fields (autoStamps / trigger-computed values).
|
|
26
|
+
*/
|
|
27
|
+
function renderRecordWriteResult(result, fallbackId, message) {
|
|
28
|
+
success(message);
|
|
29
|
+
keyValue("id", String(result?.id ?? fallbackId));
|
|
30
|
+
const applied = result?.appliedFields;
|
|
31
|
+
if (applied && typeof applied === "object" && Object.keys(applied).length > 0) {
|
|
32
|
+
for (const [key, value] of Object.entries(applied)) {
|
|
33
|
+
keyValue(`applied.${key}`, value === null || value === undefined ? "—" : String(value));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Refuse a write whose `--data` carries an `id` that is not the record being
|
|
39
|
+
* written. The server strips `id` from the record body on both the save and the
|
|
40
|
+
* patch path, so such an `id` would be dropped silently and the write would
|
|
41
|
+
* land on the id from the argument — somewhere the caller did not intend.
|
|
42
|
+
*
|
|
43
|
+
* `recordId` is the id the caller passed as an argument (absent on a `save`
|
|
44
|
+
* that mints one); `writtenId` is the id actually being written. Exits non-zero
|
|
45
|
+
* when they disagree with `--data` (#1965 review).
|
|
46
|
+
*/
|
|
47
|
+
function assertDataIdMatches(data, recordId, writtenId, commandPath) {
|
|
48
|
+
if (!data || typeof data !== "object")
|
|
49
|
+
return;
|
|
50
|
+
const dataId = data.id;
|
|
51
|
+
if (dataId === undefined || dataId === writtenId)
|
|
52
|
+
return;
|
|
53
|
+
error(recordId
|
|
54
|
+
? `The "id" in --data ("${dataId}") does not match the record id "${recordId}". Remove it, or pass the id you mean as the argument.`
|
|
55
|
+
: `Pass the record id as the argument, not inside --data: ${commandPath} ${dataId} --data ...`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Validate that the named operation is an import-compatible, save-like
|
|
60
|
+
* mutation before importing (issue #145, Codex finding (a)).
|
|
61
|
+
*
|
|
62
|
+
* The importer always builds `params: { modelName, id, data }`, and the batch
|
|
63
|
+
* endpoint rejects undeclared params per item. So we require the op to be a
|
|
64
|
+
* `mutation` whose declared `params` cover `modelName`, `id`, and `data`. This
|
|
65
|
+
* surfaces a clear up-front error instead of letting every row fail
|
|
66
|
+
* server-side. Exits non-zero on any mismatch.
|
|
67
|
+
*/
|
|
68
|
+
async function validateImportOperation(client, appId, databaseId, operationName) {
|
|
69
|
+
let ops;
|
|
70
|
+
try {
|
|
71
|
+
ops = await client.listDatabaseOperations(appId, databaseId);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
error(`Could not list operations for this database: ${err?.message || err}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const op = (ops || []).find((o) => o?.name === operationName);
|
|
79
|
+
if (!op) {
|
|
80
|
+
error(`Operation "${operationName}" not found — create a mutation op (e.g. ` +
|
|
81
|
+
`\`seed_save\`) on this database type first.`);
|
|
8
82
|
process.exit(1);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (op.type !== "mutation") {
|
|
86
|
+
error(`Operation "${operationName}" is a "${op.type}" op, not an import-compatible ` +
|
|
87
|
+
`mutation. CSV import needs a save-like mutation taking {modelName, id, data}.`);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const params = op.params || {};
|
|
92
|
+
const missing = ["modelName", "id", "data"].filter((p) => !(p in params));
|
|
93
|
+
if (missing.length > 0) {
|
|
94
|
+
error(`Operation "${operationName}" is not import-compatible: its params are missing ` +
|
|
95
|
+
`${missing.join(", ")}. CSV import builds {modelName, id, data} per item, so the ` +
|
|
96
|
+
`op must declare those params (like \`seed_save\`).`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* The full `databases records bulk` precondition value space, audited in one
|
|
103
|
+
* pass (issue #2437, Codex cycle 3). A precondition is field-equality only, so
|
|
104
|
+
* a value is one of: `null`, a boolean, a number, or a string. For each, what
|
|
105
|
+
* the DatabaseDO `condition` grammar can express, compared against the
|
|
106
|
+
* documents twin's strict JS equality (`evaluatePrecondition`):
|
|
107
|
+
*
|
|
108
|
+
* | Value | Disposition | Why |
|
|
109
|
+
* |-------------------|-------------|----------------------------------------------------------------------|
|
|
110
|
+
* | string | round-trips | bound as TEXT; SQLite never equates TEXT with a number or a JSON bool |
|
|
111
|
+
* | number ≠ 0 and ≠1 | round-trips | bound as-is; no other JSON type extracts to that value |
|
|
112
|
+
* | `null` | rewritten | paired with `$exists: true` so an absent key fails (cycle-2 fix) |
|
|
113
|
+
* | `true` / `false` | REJECTED | bound as 1/0, and `json_extract` yields 1/0 for JSON booleans too |
|
|
114
|
+
* | number 0 or 1 | REJECTED | same collision seen from the other side: matches a stored `false`/`true` |
|
|
115
|
+
* | `{}` (no fields) | see #2511 | sent as an empty `condition`, which the DO reads as "record must exist" |
|
|
116
|
+
* | object / array | rejected | not field-equality; rejected by the bulk validator before reaching here |
|
|
117
|
+
*
|
|
118
|
+
* The two REJECTED rows are the boolean/number collision: `JsonQueryTranslator`
|
|
119
|
+
* binds `true`/`false` as `1`/`0`, and SQLite's `json_extract` also returns a
|
|
120
|
+
* stored JSON boolean as `1`/`0`, so `{ enabled: true }` matches a record
|
|
121
|
+
* holding the number `1` and `{ retries: 1 }` matches a record holding `true`.
|
|
122
|
+
* The documents twin compares with `!==` and fails both. The `condition`
|
|
123
|
+
* grammar has no type predicate (`$exists` only distinguishes present from
|
|
124
|
+
* absent), so this cannot be translated exactly — the CLI rejects those values
|
|
125
|
+
* at the boundary instead, so the databases twin fails loudly rather than
|
|
126
|
+
* running with a weaker guard than the caller wrote. Issue #2507 tracks giving
|
|
127
|
+
* the grammar a type-aware equality, which would let these values be accepted.
|
|
128
|
+
*
|
|
129
|
+
* Returns an explanation when the value cannot be translated exactly, or
|
|
130
|
+
* `null` when it round-trips or is rewritten.
|
|
131
|
+
*/
|
|
132
|
+
export function untranslatablePreconditionValue(value) {
|
|
133
|
+
if (typeof value === "boolean") {
|
|
134
|
+
return (`a boolean precondition cannot be checked exactly on a database — the ` +
|
|
135
|
+
`record store compares JSON booleans as 1/0, so \`${value}\` would also ` +
|
|
136
|
+
`match a record holding the number ${value ? 1 : 0}`);
|
|
137
|
+
}
|
|
138
|
+
if (value === 0 || value === 1) {
|
|
139
|
+
return (`the number ${value} cannot be checked exactly on a database — the ` +
|
|
140
|
+
`record store compares JSON booleans as 1/0, so ${value} would also ` +
|
|
141
|
+
`match a record holding \`${value === 1}\``);
|
|
9
142
|
}
|
|
10
|
-
return
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Translate a `databases records bulk` op's documents-surface `precondition`
|
|
147
|
+
* into the DatabaseDO's per-op `condition` (issue #2437). See
|
|
148
|
+
* {@link untranslatablePreconditionValue} for the audit of the whole value
|
|
149
|
+
* space; this function handles the one value that is REWRITTEN rather than
|
|
150
|
+
* passed through or rejected.
|
|
151
|
+
*
|
|
152
|
+
* The two are NOT the same grammar, and a straight hand-off silently weakens
|
|
153
|
+
* the guard for one value: `null`. A `condition` is a query filter, so
|
|
154
|
+
* `{ approvedAt: null }` translates to
|
|
155
|
+
* `json_extract(_data, '$.approvedAt') IS NULL` — which is also true when the
|
|
156
|
+
* key is ABSENT. A guarded patch/delete would then execute against a record
|
|
157
|
+
* that never had the field. The documents surface evaluates the same
|
|
158
|
+
* precondition in JS (`evaluatePrecondition` in `src/document-models.ts`): an
|
|
159
|
+
* absent field reads `undefined`, and `undefined !== null`, so the guard fails
|
|
160
|
+
* closed there.
|
|
161
|
+
*
|
|
162
|
+
* Pin the database side to the documents meaning — "the key is present AND
|
|
163
|
+
* holds null" — by pairing each null check with an existence constraint.
|
|
164
|
+
* `$exists: true` translates to `json_type(_data, '$.<field>') IS NOT NULL`,
|
|
165
|
+
* true for a key holding JSON null and false for an absent key, so the pair
|
|
166
|
+
* emits `json_extract(...) IS NULL AND json_type(...) IS NOT NULL`.
|
|
167
|
+
*
|
|
168
|
+
* Non-null scalars need no rewrite: `= ?` against `json_extract` already fails
|
|
169
|
+
* for an absent field, because NULL never equals a bound value.
|
|
170
|
+
*
|
|
171
|
+
* Assumes the caller has already rejected `$`-prefixed keys, non-scalar values,
|
|
172
|
+
* and the values {@link untranslatablePreconditionValue} names — which is what
|
|
173
|
+
* leaves `$and` free to use here and what makes a pass-through exact.
|
|
174
|
+
*/
|
|
175
|
+
export function preconditionToCondition(precondition) {
|
|
176
|
+
const nullFields = Object.keys(precondition).filter((f) => precondition[f] === null);
|
|
177
|
+
if (nullFields.length === 0)
|
|
178
|
+
return { ...precondition };
|
|
179
|
+
const condition = {};
|
|
180
|
+
for (const [field, expected] of Object.entries(precondition)) {
|
|
181
|
+
if (expected !== null)
|
|
182
|
+
condition[field] = expected;
|
|
183
|
+
}
|
|
184
|
+
condition.$and = nullFields.flatMap((field) => [
|
|
185
|
+
{ [field]: null },
|
|
186
|
+
{ [field]: { $exists: true } },
|
|
187
|
+
]);
|
|
188
|
+
return condition;
|
|
11
189
|
}
|
|
12
190
|
export function registerDatabasesCommands(program) {
|
|
13
191
|
const databases = program
|
|
@@ -16,15 +194,32 @@ export function registerDatabasesCommands(program) {
|
|
|
16
194
|
.addHelpText("after", `
|
|
17
195
|
Examples:
|
|
18
196
|
$ primitive databases list
|
|
197
|
+
$ primitive databases list --owner <user-id>
|
|
19
198
|
$ primitive databases create "My Database"
|
|
20
199
|
$ primitive databases get <database-id>
|
|
21
200
|
$ primitive databases records models <database-id>
|
|
22
|
-
$ primitive databases records
|
|
23
|
-
$ primitive databases records
|
|
201
|
+
$ primitive databases records describe <database-id> <model-name>
|
|
202
|
+
$ primitive databases records query <database-id> <model-name> --filter '{"name":"Alice"}'
|
|
203
|
+
$ primitive databases records get <database-id> <model-name> <record-id>
|
|
204
|
+
$ primitive databases records count <database-id> <model-name> --filter '{"status":"open"}'
|
|
205
|
+
$ primitive databases records aggregate <database-id> <model-name> --op avg --field price
|
|
206
|
+
$ primitive databases records save <database-id> <model-name> [record-id] --data '{"name":"Alice"}'
|
|
207
|
+
$ primitive databases records patch <database-id> <model-name> <record-id> --data '{"name":"Alice"}'
|
|
208
|
+
$ primitive databases operations list <database-id>
|
|
209
|
+
$ primitive databases operations execute <database-id> <operation-name> --params '{"key":"value"}'
|
|
24
210
|
$ primitive databases indexes list <database-id>
|
|
25
211
|
$ primitive databases indexes create <database-id> --model contacts --field email
|
|
212
|
+
$ primitive databases records delete <database-id> <model-name> <record-id> --yes
|
|
213
|
+
$ primitive databases records delete-all <database-id> <model-name> --yes
|
|
214
|
+
$ primitive databases records delete-all <database-id> <model-name> --filter '{"status":"draft"}' --yes
|
|
215
|
+
$ primitive databases records bulk <database-id> --data-file ops.json --yes
|
|
26
216
|
$ primitive databases permissions list <database-id>
|
|
27
|
-
|
|
217
|
+
|
|
218
|
+
Note: this group manages database *instances*. Database *type* configurations
|
|
219
|
+
(schemas, rules, operations) are authored in TOML — see 'primitive config fields
|
|
220
|
+
database-type-config' and the 'database-type-configs' group for inspection:
|
|
221
|
+
$ primitive database-type-configs list
|
|
222
|
+
$ rm database-type-configs/<database-type>.toml && primitive config push --prune
|
|
28
223
|
`);
|
|
29
224
|
// List databases
|
|
30
225
|
databases
|
|
@@ -32,28 +227,49 @@ Examples:
|
|
|
32
227
|
.description("List databases in an app")
|
|
33
228
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
34
229
|
.option("--app <app-id>", "App ID")
|
|
230
|
+
.option("--owner <user-id>", "Only list databases created by this user")
|
|
231
|
+
.addOption(pageLimitOption())
|
|
232
|
+
.addOption(pageCursorOption())
|
|
35
233
|
.option("--json", "Output as JSON")
|
|
36
234
|
.action(async (appId, options) => {
|
|
235
|
+
const limit = parsePageLimit(options.limit);
|
|
37
236
|
const resolvedAppId = resolveAppId(appId, options);
|
|
38
237
|
const client = new ApiClient();
|
|
39
238
|
try {
|
|
40
|
-
const
|
|
41
|
-
|
|
239
|
+
const page = normalizeCliListEnvelope(await client.listDatabases(resolvedAppId, {
|
|
240
|
+
owner: options.owner,
|
|
241
|
+
limit,
|
|
242
|
+
cursor: options.cursor,
|
|
243
|
+
}));
|
|
244
|
+
const list = page.items;
|
|
42
245
|
if (options.json) {
|
|
43
|
-
json(
|
|
246
|
+
json(page);
|
|
44
247
|
return;
|
|
45
248
|
}
|
|
46
249
|
if (list.length === 0) {
|
|
47
|
-
|
|
250
|
+
printEmptyPage(page, "databases");
|
|
48
251
|
return;
|
|
49
252
|
}
|
|
50
253
|
console.log(formatTable(list, [
|
|
51
254
|
{ header: "ID", key: "databaseId", format: formatId },
|
|
52
255
|
{ header: "TITLE", key: "title" },
|
|
53
|
-
{ header: "
|
|
256
|
+
{ header: "TYPE", key: "databaseType", format: (v) => v || "—" },
|
|
54
257
|
{ header: "PERMISSION", key: "permission" },
|
|
258
|
+
// `permission` reads "owner" on every row for an app admin, so it
|
|
259
|
+
// cannot confirm --owner narrowed anything. Show who created each
|
|
260
|
+
// database when the filter is in play.
|
|
261
|
+
...(options.owner
|
|
262
|
+
? [
|
|
263
|
+
{
|
|
264
|
+
header: "CREATED BY",
|
|
265
|
+
key: "createdBy",
|
|
266
|
+
format: (v) => (v ? formatId(v) : "—"),
|
|
267
|
+
},
|
|
268
|
+
]
|
|
269
|
+
: []),
|
|
55
270
|
{ header: "CREATED", key: "createdAt", format: formatDate },
|
|
56
271
|
]));
|
|
272
|
+
printPageHint(page);
|
|
57
273
|
}
|
|
58
274
|
catch (err) {
|
|
59
275
|
error(err.message);
|
|
@@ -66,15 +282,42 @@ Examples:
|
|
|
66
282
|
.description("Create a new database")
|
|
67
283
|
.argument("<title>", "Database title")
|
|
68
284
|
.option("--app <app-id>", "App ID")
|
|
69
|
-
.
|
|
285
|
+
.requiredOption("--type <database-type>", "Database type (required)")
|
|
286
|
+
.option("--cel-context <json>", "[deprecated] Initial CEL context as JSON (read from rules as database.celContext.*). Prefer --initial-metadata (resource metadata categories, separate readRule/writeRule).")
|
|
287
|
+
.option("--metadata <json>", "[deprecated] Initial CEL context as JSON (legacy alias for --cel-context); prefer --initial-metadata")
|
|
288
|
+
.option("--initial-metadata <json>", "Create-time resource metadata as JSON, keyed by category name (issue #1420). The recommended replacement for --cel-context: read from rules as md.self.<category>.<key>.")
|
|
70
289
|
.option("--json", "Output as JSON")
|
|
71
290
|
.action(async (title, options) => {
|
|
72
291
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
73
292
|
const client = new ApiClient();
|
|
74
293
|
try {
|
|
75
|
-
const params = { title };
|
|
76
|
-
|
|
77
|
-
|
|
294
|
+
const params = { title, databaseType: options.type };
|
|
295
|
+
// `--cel-context` is the new user-facing name; `--metadata` remains
|
|
296
|
+
// supported for backwards compatibility. When both are provided,
|
|
297
|
+
// --cel-context wins.
|
|
298
|
+
if (options.celContext !== undefined || options.metadata !== undefined) {
|
|
299
|
+
warnDeprecated(`primitive databases create ${options.celContext !== undefined ? "--cel-context" : "--metadata"}`, "--initial-metadata (resource metadata categories)", 1815);
|
|
300
|
+
}
|
|
301
|
+
const contextSource = options.celContext ?? options.metadata;
|
|
302
|
+
if (contextSource) {
|
|
303
|
+
try {
|
|
304
|
+
params.metadata = JSON.parse(contextSource);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
error(`Invalid ${options.celContext ? "--cel-context" : "--metadata"} JSON.`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// `--initial-metadata` is a separate concept: create-time per-category
|
|
312
|
+
// ResourceMetadata rows (issue #1420), not the database CEL context.
|
|
313
|
+
if (options.initialMetadata) {
|
|
314
|
+
try {
|
|
315
|
+
params.initialMetadata = JSON.parse(options.initialMetadata);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
error("Invalid --initial-metadata JSON.");
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
78
321
|
}
|
|
79
322
|
const result = await client.createDatabase(resolvedAppId, params);
|
|
80
323
|
if (options.json) {
|
|
@@ -84,8 +327,8 @@ Examples:
|
|
|
84
327
|
success("Database created.");
|
|
85
328
|
keyValue("Database ID", result.databaseId);
|
|
86
329
|
keyValue("Title", result.title);
|
|
87
|
-
if (result.
|
|
88
|
-
keyValue("
|
|
330
|
+
if (result.databaseType) {
|
|
331
|
+
keyValue("Database Type", result.databaseType);
|
|
89
332
|
}
|
|
90
333
|
}
|
|
91
334
|
catch (err) {
|
|
@@ -109,12 +352,11 @@ Examples:
|
|
|
109
352
|
json(result);
|
|
110
353
|
return;
|
|
111
354
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
keyValue("Created", formatDate(result.createdAt));
|
|
355
|
+
printResult("Database ID", result.databaseId);
|
|
356
|
+
printResult("Title", result.title);
|
|
357
|
+
printResult("Database Type", result.databaseType || "none");
|
|
358
|
+
printResult("Permission", result.permission || "none");
|
|
359
|
+
printResult("Created", formatDate(result.createdAt));
|
|
118
360
|
}
|
|
119
361
|
catch (err) {
|
|
120
362
|
error(err.message);
|
|
@@ -124,12 +366,11 @@ Examples:
|
|
|
124
366
|
// Update database
|
|
125
367
|
databases
|
|
126
368
|
.command("update")
|
|
127
|
-
.description("Update a database (title,
|
|
369
|
+
.description("Update a database (title, type)")
|
|
128
370
|
.argument("<database-id>", "Database ID")
|
|
129
371
|
.option("--app <app-id>", "App ID")
|
|
130
372
|
.option("--title <title>", "New title")
|
|
131
|
-
.option("--
|
|
132
|
-
.option("--rule-set-id <id>", "Attach a rule set (use 'none' to detach)")
|
|
373
|
+
.option("--type <database-type>", "Database type (use 'none' to clear)")
|
|
133
374
|
.option("--json", "Output as JSON")
|
|
134
375
|
.action(async (databaseId, options) => {
|
|
135
376
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
@@ -138,20 +379,14 @@ Examples:
|
|
|
138
379
|
if (options.title) {
|
|
139
380
|
params.title = options.title;
|
|
140
381
|
}
|
|
141
|
-
if (options.
|
|
142
|
-
params.
|
|
143
|
-
options.
|
|
144
|
-
? null
|
|
145
|
-
: options.defaultPermission;
|
|
146
|
-
}
|
|
147
|
-
if (options.ruleSetId !== undefined) {
|
|
148
|
-
params.ruleSetId =
|
|
149
|
-
options.ruleSetId === "none"
|
|
382
|
+
if (options.type !== undefined) {
|
|
383
|
+
params.databaseType =
|
|
384
|
+
options.type === "none"
|
|
150
385
|
? null
|
|
151
|
-
: options.
|
|
386
|
+
: options.type;
|
|
152
387
|
}
|
|
153
388
|
if (Object.keys(params).length === 0) {
|
|
154
|
-
error("Provide at least one of --title
|
|
389
|
+
error("Provide at least one of --title or --type.");
|
|
155
390
|
process.exit(1);
|
|
156
391
|
}
|
|
157
392
|
try {
|
|
@@ -163,8 +398,7 @@ Examples:
|
|
|
163
398
|
success("Database updated.");
|
|
164
399
|
keyValue("Database ID", result.databaseId);
|
|
165
400
|
keyValue("Title", result.title);
|
|
166
|
-
keyValue("
|
|
167
|
-
keyValue("Rule Set", result.ruleSetId || "none");
|
|
401
|
+
keyValue("Database Type", result.databaseType || "none");
|
|
168
402
|
}
|
|
169
403
|
catch (err) {
|
|
170
404
|
error(err.message);
|
|
@@ -181,15 +415,14 @@ Examples:
|
|
|
181
415
|
.action(async (databaseId, options) => {
|
|
182
416
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
183
417
|
if (!options.yes) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
]);
|
|
418
|
+
let confirm;
|
|
419
|
+
try {
|
|
420
|
+
confirm = await confirmPrompt(`Delete database ${databaseId}? This cannot be undone.`);
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
error(err.message);
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
193
426
|
if (!confirm) {
|
|
194
427
|
info("Cancelled.");
|
|
195
428
|
return;
|
|
@@ -223,7 +456,7 @@ Examples:
|
|
|
223
456
|
const result = await client.listDatabasePermissions(resolvedAppId, databaseId);
|
|
224
457
|
const list = Array.isArray(result) ? result : result?.permissions ?? [];
|
|
225
458
|
if (options.json) {
|
|
226
|
-
json(list);
|
|
459
|
+
json(wholeListEnvelope(list));
|
|
227
460
|
return;
|
|
228
461
|
}
|
|
229
462
|
if (list.length === 0) {
|
|
@@ -242,54 +475,35 @@ Examples:
|
|
|
242
475
|
process.exit(1);
|
|
243
476
|
}
|
|
244
477
|
});
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
.command("grant")
|
|
248
|
-
.description("Grant a user permission on a database")
|
|
249
|
-
.argument("<database-id>", "Database ID")
|
|
250
|
-
.requiredOption("--user-id <user-id>", "User ID to grant permission to")
|
|
251
|
-
.requiredOption("--permission <permission>", "Permission level: owner, read-write, or reader")
|
|
252
|
-
.option("--app <app-id>", "App ID")
|
|
253
|
-
.option("--json", "Output as JSON")
|
|
254
|
-
.action(async (databaseId, options) => {
|
|
478
|
+
// Add manager action handler
|
|
479
|
+
const addManagerAction = async (databaseId, options) => {
|
|
255
480
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
256
481
|
const client = new ApiClient();
|
|
257
482
|
try {
|
|
258
|
-
const result = await client.
|
|
259
|
-
userId: options.userId,
|
|
260
|
-
permission: options.permission,
|
|
261
|
-
});
|
|
483
|
+
const result = await client.addDatabaseManager(resolvedAppId, databaseId, options.userId);
|
|
262
484
|
if (options.json) {
|
|
263
485
|
json(result);
|
|
264
486
|
return;
|
|
265
487
|
}
|
|
266
|
-
success(`
|
|
488
|
+
success(`Manager added: user ${options.userId}.`);
|
|
267
489
|
}
|
|
268
490
|
catch (err) {
|
|
269
491
|
error(err.message);
|
|
270
492
|
process.exit(1);
|
|
271
493
|
}
|
|
272
|
-
}
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
.command("revoke")
|
|
276
|
-
.description("Revoke a user's permission on a database")
|
|
277
|
-
.argument("<database-id>", "Database ID")
|
|
278
|
-
.argument("<user-id>", "User ID to revoke")
|
|
279
|
-
.option("--app <app-id>", "App ID")
|
|
280
|
-
.option("-y, --yes", "Skip confirmation prompt")
|
|
281
|
-
.action(async (databaseId, userId, options) => {
|
|
494
|
+
};
|
|
495
|
+
// Remove manager action handler
|
|
496
|
+
const removeManagerAction = async (databaseId, userId, options) => {
|
|
282
497
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
283
498
|
if (!options.yes) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
{
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
]);
|
|
499
|
+
let confirm;
|
|
500
|
+
try {
|
|
501
|
+
confirm = await confirmPrompt(`Remove manager ${userId} from database ${databaseId}?`);
|
|
502
|
+
}
|
|
503
|
+
catch (err) {
|
|
504
|
+
error(err.message);
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
293
507
|
if (!confirm) {
|
|
294
508
|
info("Cancelled.");
|
|
295
509
|
return;
|
|
@@ -297,18 +511,55 @@ Examples:
|
|
|
297
511
|
}
|
|
298
512
|
const client = new ApiClient();
|
|
299
513
|
try {
|
|
300
|
-
await client.
|
|
301
|
-
success(`
|
|
514
|
+
await client.removeDatabaseManager(resolvedAppId, databaseId, userId);
|
|
515
|
+
success(`Manager removed: user ${userId}.`);
|
|
302
516
|
}
|
|
303
517
|
catch (err) {
|
|
304
518
|
error(err.message);
|
|
305
519
|
process.exit(1);
|
|
306
520
|
}
|
|
307
|
-
}
|
|
521
|
+
};
|
|
522
|
+
// Add manager (primary command)
|
|
523
|
+
permissions
|
|
524
|
+
.command("add-manager")
|
|
525
|
+
.description("Add a user as a manager of a database")
|
|
526
|
+
.argument("<database-id>", "Database ID")
|
|
527
|
+
.requiredOption("--user-id <user-id>", "User ID to add as manager")
|
|
528
|
+
.option("--app <app-id>", "App ID")
|
|
529
|
+
.option("--json", "Output as JSON")
|
|
530
|
+
.action(addManagerAction);
|
|
531
|
+
// Remove manager (primary command)
|
|
532
|
+
permissions
|
|
533
|
+
.command("remove-manager")
|
|
534
|
+
.description("Remove a manager from a database")
|
|
535
|
+
.argument("<database-id>", "Database ID")
|
|
536
|
+
.argument("<user-id>", "User ID to remove")
|
|
537
|
+
.option("--app <app-id>", "App ID")
|
|
538
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
539
|
+
.action(removeManagerAction);
|
|
540
|
+
// Grant permission (deprecated alias for add-manager)
|
|
541
|
+
permissions
|
|
542
|
+
.command("grant")
|
|
543
|
+
.description("(deprecated: use add-manager) Grant a user permission on a database")
|
|
544
|
+
.argument("<database-id>", "Database ID")
|
|
545
|
+
.requiredOption("--user-id <user-id>", "User ID to grant permission to")
|
|
546
|
+
.option("--permission <permission>", "Permission level (ignored, always 'manager')")
|
|
547
|
+
.option("--app <app-id>", "App ID")
|
|
548
|
+
.option("--json", "Output as JSON")
|
|
549
|
+
.action(addManagerAction);
|
|
550
|
+
// Revoke permission (deprecated alias for remove-manager)
|
|
551
|
+
permissions
|
|
552
|
+
.command("revoke")
|
|
553
|
+
.description("(deprecated: use remove-manager) Revoke a user's permission on a database")
|
|
554
|
+
.argument("<database-id>", "Database ID")
|
|
555
|
+
.argument("<user-id>", "User ID to revoke")
|
|
556
|
+
.option("--app <app-id>", "App ID")
|
|
557
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
558
|
+
.action(removeManagerAction);
|
|
308
559
|
// ---- Records subcommand group ----
|
|
309
560
|
const records = databases
|
|
310
561
|
.command("records")
|
|
311
|
-
.description("
|
|
562
|
+
.description("Inspect and query records in a database");
|
|
312
563
|
// List models
|
|
313
564
|
records
|
|
314
565
|
.command("models")
|
|
@@ -371,64 +622,108 @@ Examples:
|
|
|
371
622
|
process.exit(1);
|
|
372
623
|
}
|
|
373
624
|
});
|
|
374
|
-
// Query records
|
|
625
|
+
// Query records (direct introspection via the records route)
|
|
375
626
|
records
|
|
376
627
|
.command("query")
|
|
377
|
-
.description("Query records in a database")
|
|
628
|
+
.description("Query records in a database model (admin only)")
|
|
378
629
|
.argument("<database-id>", "Database ID")
|
|
379
|
-
.argument("<model-name>", "Model name")
|
|
380
|
-
.option("--filter <json>", "Filter as JSON", "{}")
|
|
381
|
-
.option("--limit <n>", "Max records to return", "25")
|
|
382
|
-
.option("--cursor <cursor>", "Pagination cursor")
|
|
630
|
+
.argument("<model-name>", "Model name to query")
|
|
383
631
|
.option("--app <app-id>", "App ID")
|
|
632
|
+
.option("--filter <json>", "Filter as JSON (e.g. '{\"name\":\"Alice\"}')")
|
|
633
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
634
|
+
.option("--limit <n>", "Maximum number of records to return", parseInt)
|
|
635
|
+
.option("--cursor <cursor>", "Pagination cursor from previous query")
|
|
384
636
|
.option("--json", "Output as JSON")
|
|
385
637
|
.action(async (databaseId, modelName, options) => {
|
|
386
638
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
387
639
|
const client = new ApiClient();
|
|
388
|
-
|
|
389
|
-
try {
|
|
390
|
-
filter = JSON.parse(options.filter);
|
|
391
|
-
}
|
|
392
|
-
catch {
|
|
393
|
-
error("Invalid --filter JSON.");
|
|
394
|
-
process.exit(1);
|
|
395
|
-
}
|
|
640
|
+
const filter = parseFilterOptions(options);
|
|
396
641
|
try {
|
|
397
|
-
const
|
|
398
|
-
modelName,
|
|
399
|
-
filter,
|
|
400
|
-
options: { limit: parseInt(options.limit, 10) },
|
|
401
|
-
};
|
|
402
|
-
if (options.cursor)
|
|
403
|
-
body.options.cursor = options.cursor;
|
|
404
|
-
const result = await client.queryDatabaseRecords(resolvedAppId, databaseId, body);
|
|
405
|
-
const data = result.data || [];
|
|
642
|
+
const result = await client.queryDatabaseRecords(resolvedAppId, databaseId, modelName, { filter, limit: options.limit, cursor: options.cursor });
|
|
406
643
|
if (options.json) {
|
|
407
644
|
json(result);
|
|
408
645
|
return;
|
|
409
646
|
}
|
|
410
|
-
|
|
647
|
+
const records = result.items;
|
|
648
|
+
if (records.length === 0) {
|
|
411
649
|
info("No records found.");
|
|
412
650
|
return;
|
|
413
651
|
}
|
|
414
|
-
//
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
652
|
+
// Auto-detect columns from record keys
|
|
653
|
+
const allKeys = new Set();
|
|
654
|
+
for (const rec of records) {
|
|
655
|
+
for (const key of Object.keys(rec)) {
|
|
656
|
+
allKeys.add(key);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
// Put id first, then type, then the rest sorted
|
|
660
|
+
const orderedKeys = [];
|
|
661
|
+
if (allKeys.has("id")) {
|
|
662
|
+
orderedKeys.push("id");
|
|
663
|
+
allKeys.delete("id");
|
|
664
|
+
}
|
|
665
|
+
if (allKeys.has("type")) {
|
|
666
|
+
orderedKeys.push("type");
|
|
667
|
+
allKeys.delete("type");
|
|
668
|
+
}
|
|
669
|
+
orderedKeys.push(...[...allKeys].sort());
|
|
670
|
+
const columns = orderedKeys.map((key) => ({
|
|
671
|
+
header: key.toUpperCase(),
|
|
672
|
+
key,
|
|
419
673
|
format: (v) => {
|
|
420
674
|
if (v === null || v === undefined)
|
|
421
675
|
return "—";
|
|
422
|
-
if (typeof v === "object")
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
}
|
|
426
|
-
const s = String(v);
|
|
427
|
-
return s.length > 50 ? s.slice(0, 47) + "..." : s;
|
|
676
|
+
if (typeof v === "object")
|
|
677
|
+
return JSON.stringify(v);
|
|
678
|
+
return String(v);
|
|
428
679
|
},
|
|
429
|
-
}))
|
|
430
|
-
|
|
431
|
-
|
|
680
|
+
}));
|
|
681
|
+
console.log(formatTable(records, columns));
|
|
682
|
+
if (result.nextCursor) {
|
|
683
|
+
info(`More results available. Use --cursor ${result.nextCursor}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
catch (err) {
|
|
687
|
+
error(err.message);
|
|
688
|
+
process.exit(1);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
// Get a single record by id
|
|
692
|
+
records
|
|
693
|
+
.command("get")
|
|
694
|
+
.description("Get a single record by id (admin only)")
|
|
695
|
+
.argument("<database-id>", "Database ID")
|
|
696
|
+
.argument("<model-name>", "Model name")
|
|
697
|
+
.argument("<record-id>", "Record ID")
|
|
698
|
+
.option("--app <app-id>", "App ID")
|
|
699
|
+
.option("--json", "Output as JSON")
|
|
700
|
+
.action(async (databaseId, modelName, recordId, options) => {
|
|
701
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
702
|
+
const client = new ApiClient();
|
|
703
|
+
try {
|
|
704
|
+
// A single-record fetch is a query filtered by primary-key id (there is
|
|
705
|
+
// no dedicated get endpoint — reuse queryDatabaseRecords).
|
|
706
|
+
const result = await client.queryDatabaseRecords(resolvedAppId, databaseId, modelName, { filter: { id: recordId }, limit: 1 });
|
|
707
|
+
const record = result.items[0];
|
|
708
|
+
if (options.json) {
|
|
709
|
+
json(record ?? null);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (!record) {
|
|
713
|
+
info(`No record found with id ${recordId} in ${modelName}.`);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
// `get` is a data command, so its fields go to stdout via `result()`
|
|
717
|
+
// — the #711 split (`result()` = the data the caller asked for,
|
|
718
|
+
// `keyValue()` = a post-action diagnostic summary). Issue #2437; the
|
|
719
|
+
// documents twin renders identically.
|
|
720
|
+
for (const [key, value] of Object.entries(record)) {
|
|
721
|
+
const rendered = value === null || value === undefined
|
|
722
|
+
? "—"
|
|
723
|
+
: typeof value === "object"
|
|
724
|
+
? JSON.stringify(value)
|
|
725
|
+
: String(value);
|
|
726
|
+
printResult(key, rendered);
|
|
432
727
|
}
|
|
433
728
|
}
|
|
434
729
|
catch (err) {
|
|
@@ -439,101 +734,203 @@ Examples:
|
|
|
439
734
|
// Count records
|
|
440
735
|
records
|
|
441
736
|
.command("count")
|
|
442
|
-
.description("Count records
|
|
737
|
+
.description("Count records in a database model (admin only)")
|
|
443
738
|
.argument("<database-id>", "Database ID")
|
|
444
739
|
.argument("<model-name>", "Model name")
|
|
445
|
-
.option("--filter <json>", "Filter as JSON", "{}")
|
|
446
740
|
.option("--app <app-id>", "App ID")
|
|
741
|
+
.option("--filter <json>", "Filter as JSON (e.g. '{\"name\":\"Alice\"}')")
|
|
742
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
447
743
|
.option("--json", "Output as JSON")
|
|
448
744
|
.action(async (databaseId, modelName, options) => {
|
|
449
745
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
746
|
+
const filter = parseFilterOptions(options);
|
|
450
747
|
const client = new ApiClient();
|
|
451
|
-
let filter = {};
|
|
452
748
|
try {
|
|
453
|
-
|
|
749
|
+
const result = await client.countDatabaseRecords(resolvedAppId, databaseId, modelName, filter);
|
|
750
|
+
if (options.json) {
|
|
751
|
+
json(result);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
keyValue("count", String(result?.count ?? 0));
|
|
454
755
|
}
|
|
455
|
-
catch {
|
|
456
|
-
error(
|
|
756
|
+
catch (err) {
|
|
757
|
+
error(err.message);
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
// Aggregate records
|
|
762
|
+
const AGGREGATE_OPS = ["count", "sum", "avg", "min", "max"];
|
|
763
|
+
records
|
|
764
|
+
.command("aggregate")
|
|
765
|
+
.description("Aggregate records in a database model (count/sum/avg/min/max, optional group-by; admin only)")
|
|
766
|
+
.argument("<database-id>", "Database ID")
|
|
767
|
+
.argument("<model-name>", "Model name")
|
|
768
|
+
.option("--app <app-id>", "App ID")
|
|
769
|
+
.requiredOption("--op <operation>", `Aggregate operation: ${AGGREGATE_OPS.join("|")}`)
|
|
770
|
+
.option("--field <field>", "Field to aggregate (required for sum/avg/min/max)")
|
|
771
|
+
.option("--group-by <field>", "Group results by this field (repeatable)", (value, previous = []) => previous.concat(value), [])
|
|
772
|
+
.option("--filter <json>", "Filter as JSON")
|
|
773
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
774
|
+
.option("--json", "Output as JSON")
|
|
775
|
+
.action(async (databaseId, modelName, options) => {
|
|
776
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
777
|
+
if (!AGGREGATE_OPS.includes(options.op)) {
|
|
778
|
+
error(`Invalid --op "${options.op}". Expected one of: ${AGGREGATE_OPS.join(", ")}.`);
|
|
457
779
|
process.exit(1);
|
|
458
780
|
}
|
|
781
|
+
const operation = { type: options.op };
|
|
782
|
+
if (options.op === "count") {
|
|
783
|
+
if (options.field) {
|
|
784
|
+
error("--field is not valid for --op count.");
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
if (!options.field) {
|
|
790
|
+
error(`--op ${options.op} requires --field.`);
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
operation.field = options.field;
|
|
794
|
+
}
|
|
795
|
+
const filter = parseFilterOptions(options);
|
|
796
|
+
const groupBy = options.groupBy || [];
|
|
797
|
+
const client = new ApiClient();
|
|
459
798
|
try {
|
|
460
|
-
const result = await client.
|
|
461
|
-
modelName,
|
|
462
|
-
filter,
|
|
463
|
-
});
|
|
799
|
+
const result = await client.aggregateDatabaseRecords(resolvedAppId, databaseId, modelName, { operations: [operation], groupBy, filter });
|
|
464
800
|
if (options.json) {
|
|
465
801
|
json(result);
|
|
466
802
|
return;
|
|
467
803
|
}
|
|
468
|
-
|
|
804
|
+
const aggregated = result?.result;
|
|
805
|
+
// Label the value column after the operation, including the field it
|
|
806
|
+
// ran over: `SUM` alone does not say what was summed. The field name is
|
|
807
|
+
// reproduced verbatim in both labels — it is a user-supplied
|
|
808
|
+
// identifier, and case-folding it produced `avg(lastvalue)` for
|
|
809
|
+
// `--field lastValue` (issue #2357).
|
|
810
|
+
const valueLabel = options.op === "count"
|
|
811
|
+
? "COUNT"
|
|
812
|
+
: `${options.op.toUpperCase()}(${options.field})`;
|
|
813
|
+
const scalarLabel = options.op === "count"
|
|
814
|
+
? "count"
|
|
815
|
+
: `${options.op.toLowerCase()}(${options.field})`;
|
|
816
|
+
if (groupBy.length === 0) {
|
|
817
|
+
// Ungrouped: one scalar per operation, keyed by the endpoint's
|
|
818
|
+
// operation name (`count`, `sum_<field>`, …). Render it as a
|
|
819
|
+
// key/value line — there is no group column to table.
|
|
820
|
+
const value = (aggregated ?? {})[options.op === "count" ? "count" : `${options.op}_${options.field}`];
|
|
821
|
+
keyValue(scalarLabel, value === null || value === undefined ? "—" : String(value));
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (aggregated &&
|
|
825
|
+
typeof aggregated === "object" &&
|
|
826
|
+
!Array.isArray(aggregated) &&
|
|
827
|
+
Object.values(aggregated).every((v) => typeof v !== "object" || v === null)) {
|
|
828
|
+
// Flat { group -> value } shape (single operation, single group-by): render a table.
|
|
829
|
+
const rows = Object.entries(aggregated).map(([group, value]) => ({
|
|
830
|
+
group,
|
|
831
|
+
value: value === null || value === undefined ? "—" : String(value),
|
|
832
|
+
}));
|
|
833
|
+
console.log(formatTable(rows, [
|
|
834
|
+
{ header: "GROUP", key: "group" },
|
|
835
|
+
{ header: valueLabel, key: "value" },
|
|
836
|
+
]));
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
// Grouped/nested or multi-operation shape: print the structured result.
|
|
840
|
+
console.log(JSON.stringify(aggregated ?? {}, null, 2));
|
|
841
|
+
}
|
|
469
842
|
}
|
|
470
843
|
catch (err) {
|
|
471
844
|
error(err.message);
|
|
472
845
|
process.exit(1);
|
|
473
846
|
}
|
|
474
847
|
});
|
|
475
|
-
//
|
|
848
|
+
// Upsert a record (create, or merge into an existing one)
|
|
476
849
|
records
|
|
477
850
|
.command("save")
|
|
478
|
-
.description("Create or
|
|
851
|
+
.description("Create a record, or merge fields into an existing one (admin only)")
|
|
479
852
|
.argument("<database-id>", "Database ID")
|
|
480
853
|
.argument("<model-name>", "Model name")
|
|
481
|
-
.
|
|
482
|
-
.requiredOption("--data <json>", "Record data as JSON")
|
|
854
|
+
.argument("[record-id]", "Record ID (generated when omitted)")
|
|
483
855
|
.option("--app <app-id>", "App ID")
|
|
856
|
+
.option("--data <json>", "Record fields as inline JSON")
|
|
857
|
+
.option("--data-file <path>", "Read the record fields from a JSON file")
|
|
484
858
|
.option("--json", "Output as JSON")
|
|
485
|
-
.action(async (databaseId, modelName, options) => {
|
|
859
|
+
.action(async (databaseId, modelName, recordId, options) => {
|
|
486
860
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
861
|
+
const data = parseDataOption(options, "record");
|
|
487
862
|
const client = new ApiClient();
|
|
488
|
-
|
|
863
|
+
// The DatabaseDO requires an id on the plain save path (it only generates
|
|
864
|
+
// one for `upsertOn` writes). Mint the id client-side for a create so
|
|
865
|
+
// `save` works without an id, the same way CSV import does
|
|
866
|
+
// (`lib/csv.ts`).
|
|
867
|
+
const id = recordId || ulid();
|
|
868
|
+
assertDataIdMatches(data, recordId, id, "primitive databases records save <database-id> <model-name>");
|
|
489
869
|
try {
|
|
490
|
-
|
|
870
|
+
const result = await client.saveDatabaseRecord(resolvedAppId, databaseId, modelName, id, data);
|
|
871
|
+
if (options.json) {
|
|
872
|
+
json(result);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
renderRecordWriteResult(result, id, `Record saved to ${modelName}.`);
|
|
491
876
|
}
|
|
492
|
-
catch {
|
|
493
|
-
error(
|
|
877
|
+
catch (err) {
|
|
878
|
+
error(err.message);
|
|
494
879
|
process.exit(1);
|
|
495
|
-
return;
|
|
496
880
|
}
|
|
497
|
-
|
|
881
|
+
});
|
|
882
|
+
// Patch a record (partial merge)
|
|
883
|
+
records
|
|
884
|
+
.command("patch")
|
|
885
|
+
.description("Merge fields into an existing record (admin only)")
|
|
886
|
+
.argument("<database-id>", "Database ID")
|
|
887
|
+
.argument("<model-name>", "Model name")
|
|
888
|
+
.argument("<record-id>", "Record ID")
|
|
889
|
+
.option("--app <app-id>", "App ID")
|
|
890
|
+
.option("--data <json>", "Fields to merge as inline JSON")
|
|
891
|
+
.option("--data-file <path>", "Read the fields to merge from a JSON file")
|
|
892
|
+
.option("--json", "Output as JSON")
|
|
893
|
+
.action(async (databaseId, modelName, recordId, options) => {
|
|
894
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
895
|
+
const data = parseDataOption(options, "fields to merge");
|
|
896
|
+
const client = new ApiClient();
|
|
897
|
+
// Same guard as `save` — on this path `recordId` is required, so the
|
|
898
|
+
// "pass it as the argument" branch is unreachable here.
|
|
899
|
+
assertDataIdMatches(data, recordId, recordId, "primitive databases records patch <database-id> <model-name>");
|
|
498
900
|
try {
|
|
499
|
-
const result = await client.
|
|
500
|
-
modelName,
|
|
501
|
-
id,
|
|
502
|
-
data,
|
|
503
|
-
});
|
|
901
|
+
const result = await client.patchDatabaseRecord(resolvedAppId, databaseId, modelName, recordId, data);
|
|
504
902
|
if (options.json) {
|
|
505
903
|
json(result);
|
|
506
904
|
return;
|
|
507
905
|
}
|
|
508
|
-
|
|
509
|
-
keyValue("ID", result.id || id);
|
|
906
|
+
renderRecordWriteResult(result, recordId, `Record ${recordId} patched in ${modelName}.`);
|
|
510
907
|
}
|
|
511
908
|
catch (err) {
|
|
512
909
|
error(err.message);
|
|
513
910
|
process.exit(1);
|
|
514
911
|
}
|
|
515
912
|
});
|
|
516
|
-
// Delete record
|
|
913
|
+
// Delete a single record
|
|
517
914
|
records
|
|
518
915
|
.command("delete")
|
|
519
|
-
.description("Delete a record")
|
|
916
|
+
.description("Delete a single record from a database model (admin only)")
|
|
520
917
|
.argument("<database-id>", "Database ID")
|
|
521
918
|
.argument("<model-name>", "Model name")
|
|
522
|
-
.argument("<record-id>", "Record ID")
|
|
919
|
+
.argument("<record-id>", "Record ID to delete")
|
|
523
920
|
.option("--app <app-id>", "App ID")
|
|
524
921
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
922
|
+
.option("--json", "Output as JSON")
|
|
525
923
|
.action(async (databaseId, modelName, recordId, options) => {
|
|
526
924
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
527
925
|
if (!options.yes) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
{
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
]);
|
|
926
|
+
let confirm;
|
|
927
|
+
try {
|
|
928
|
+
confirm = await confirmPrompt(`Delete record ${recordId} from ${modelName}? This cannot be undone.`);
|
|
929
|
+
}
|
|
930
|
+
catch (err) {
|
|
931
|
+
error(err.message);
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
537
934
|
if (!confirm) {
|
|
538
935
|
info("Cancelled.");
|
|
539
936
|
return;
|
|
@@ -541,11 +938,506 @@ Examples:
|
|
|
541
938
|
}
|
|
542
939
|
const client = new ApiClient();
|
|
543
940
|
try {
|
|
544
|
-
await client.deleteDatabaseRecord(resolvedAppId, databaseId,
|
|
545
|
-
|
|
546
|
-
|
|
941
|
+
const result = await client.deleteDatabaseRecord(resolvedAppId, databaseId, modelName, recordId);
|
|
942
|
+
if (options.json) {
|
|
943
|
+
json(result);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
success(`Record ${recordId} deleted from ${modelName}.`);
|
|
947
|
+
}
|
|
948
|
+
catch (err) {
|
|
949
|
+
error(err.message);
|
|
950
|
+
process.exit(1);
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
// Delete all records for a model
|
|
954
|
+
records
|
|
955
|
+
.command("delete-all")
|
|
956
|
+
.description("Delete all records for a model (queries then batch deletes, admin only)")
|
|
957
|
+
.argument("<database-id>", "Database ID")
|
|
958
|
+
.argument("<model-name>", "Model name")
|
|
959
|
+
.option("--app <app-id>", "App ID")
|
|
960
|
+
.option("--filter <json>", "Filter as JSON to select which records to delete")
|
|
961
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
962
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
963
|
+
.option("--json", "Output as JSON")
|
|
964
|
+
.action(async (databaseId, modelName, options) => {
|
|
965
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
966
|
+
const client = new ApiClient();
|
|
967
|
+
const filter = parseFilterOptions(options);
|
|
968
|
+
try {
|
|
969
|
+
// First, query to find all matching records
|
|
970
|
+
let allRecords = [];
|
|
971
|
+
let cursor;
|
|
972
|
+
let hasMore = true;
|
|
973
|
+
while (hasMore) {
|
|
974
|
+
const result = await client.queryDatabaseRecords(resolvedAppId, databaseId, modelName, { filter, limit: 100, cursor });
|
|
975
|
+
const records = result.items;
|
|
976
|
+
allRecords = allRecords.concat(records);
|
|
977
|
+
cursor = result.nextCursor;
|
|
978
|
+
hasMore = !!cursor && records.length > 0;
|
|
979
|
+
}
|
|
980
|
+
if (allRecords.length === 0) {
|
|
981
|
+
if (options.json) {
|
|
982
|
+
json({ deleted: 0, modelName });
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
info("No matching records found.");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
if (!options.yes) {
|
|
989
|
+
let confirm;
|
|
990
|
+
try {
|
|
991
|
+
confirm = await confirmPrompt(`Delete ${allRecords.length} record(s) from ${modelName}? This cannot be undone.`);
|
|
992
|
+
}
|
|
993
|
+
catch (err) {
|
|
994
|
+
error(err.message);
|
|
995
|
+
process.exit(1);
|
|
996
|
+
}
|
|
997
|
+
if (!confirm) {
|
|
998
|
+
info("Cancelled.");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
// Batch delete in chunks of 50
|
|
1003
|
+
const BATCH_SIZE = 50;
|
|
1004
|
+
let deleted = 0;
|
|
1005
|
+
for (let i = 0; i < allRecords.length; i += BATCH_SIZE) {
|
|
1006
|
+
const chunk = allRecords.slice(i, i + BATCH_SIZE);
|
|
1007
|
+
const operations = chunk.map((r) => ({
|
|
1008
|
+
op: "delete",
|
|
1009
|
+
modelName,
|
|
1010
|
+
id: r.id,
|
|
1011
|
+
}));
|
|
1012
|
+
await client.batchDeleteDatabaseRecords(resolvedAppId, databaseId, operations);
|
|
1013
|
+
deleted += chunk.length;
|
|
1014
|
+
}
|
|
1015
|
+
if (options.json) {
|
|
1016
|
+
json({ deleted, modelName });
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
success(`Deleted ${deleted} record(s) from ${modelName}.`);
|
|
1020
|
+
}
|
|
1021
|
+
catch (err) {
|
|
1022
|
+
error(err.message);
|
|
1023
|
+
process.exit(1);
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
// Apply an atomic multi-model operations blob (issue #2437).
|
|
1027
|
+
//
|
|
1028
|
+
// Takes the SAME `--data-file` blob as `documents records bulk` — one file
|
|
1029
|
+
// works against either surface — and translates it to the DatabaseDO's
|
|
1030
|
+
// `records/batch` wire shape (`{ op, modelName, id, data }`) with
|
|
1031
|
+
// `atomic: true`, so the whole blob is all-or-nothing like the documents
|
|
1032
|
+
// twin. Both guards the documents blob can carry survive the translation:
|
|
1033
|
+
// `action: create` maps to `save` with `ifNotExists: true` (the DatabaseDO's
|
|
1034
|
+
// strict-create flag, a 409 `ALREADY_EXISTS` inside the transaction), and an
|
|
1035
|
+
// op's `precondition` maps to the DO's per-op `condition` (a 409
|
|
1036
|
+
// `CONDITION_NOT_MET`). Both abort the whole batch, matching the documents
|
|
1037
|
+
// twin.
|
|
1038
|
+
const BULK_ACTIONS = ["create", "patch", "delete"];
|
|
1039
|
+
const BULK_ACTION_TO_OP = {
|
|
1040
|
+
create: "save",
|
|
1041
|
+
patch: "patch",
|
|
1042
|
+
delete: "delete",
|
|
1043
|
+
};
|
|
1044
|
+
// Mirrors the documents surface's `BULK_WRITE_OP_KEYS` / `BULK_DELETE_OP_KEYS`
|
|
1045
|
+
// (`src/document-models.ts`). Unknown keys are an error rather than ignored:
|
|
1046
|
+
// a guard the author asked for must never be dropped silently.
|
|
1047
|
+
const BULK_OP_KEYS = ["model", "action", "id", "data", "precondition"];
|
|
1048
|
+
const BULK_DELETE_OP_KEYS = ["model", "action", "id", "precondition"];
|
|
1049
|
+
records
|
|
1050
|
+
.command("bulk")
|
|
1051
|
+
.description("Apply an atomic create/patch/delete operations blob to a database (admin only)")
|
|
1052
|
+
.argument("<database-id>", "Database ID")
|
|
1053
|
+
.requiredOption("--data-file <file>", "JSON file with { operations: [...] } or a bare operations array. " +
|
|
1054
|
+
"Each op is { model, action: create|patch|delete, id, data, precondition? } " +
|
|
1055
|
+
"— the same shape `documents records bulk` takes. `data` holds the " +
|
|
1056
|
+
"record fields and is required on create/patch, not allowed on delete. " +
|
|
1057
|
+
"A `create` against an id that already exists fails the whole batch, " +
|
|
1058
|
+
"and a `precondition` that does not hold rolls the whole batch back")
|
|
1059
|
+
.option("--app <app-id>", "App ID")
|
|
1060
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
1061
|
+
.option("--json", "Output as JSON")
|
|
1062
|
+
.action(async (databaseId, options) => {
|
|
1063
|
+
const parsed = parseDataOption(options, "operations blob", {
|
|
1064
|
+
allowArray: true,
|
|
1065
|
+
});
|
|
1066
|
+
const rawOperations = Array.isArray(parsed) ? parsed : parsed?.operations;
|
|
1067
|
+
if (!Array.isArray(rawOperations) || rawOperations.length === 0) {
|
|
1068
|
+
error("--data-file must contain { operations: [...] } or a non-empty operations array.");
|
|
1069
|
+
process.exit(1);
|
|
1070
|
+
}
|
|
1071
|
+
// Translate to the batch wire shape, rejecting a malformed op before any
|
|
1072
|
+
// request goes out so the caller sees which entry is wrong.
|
|
1073
|
+
const operations = [];
|
|
1074
|
+
for (let i = 0; i < rawOperations.length; i++) {
|
|
1075
|
+
const op = rawOperations[i];
|
|
1076
|
+
if (!op || typeof op !== "object" || Array.isArray(op)) {
|
|
1077
|
+
error(`Operation ${i}: each operation must be a JSON object.`);
|
|
1078
|
+
process.exit(1);
|
|
1079
|
+
}
|
|
1080
|
+
const action = op.action;
|
|
1081
|
+
if (!BULK_ACTIONS.includes(action)) {
|
|
1082
|
+
error(`Operation ${i}: invalid action "${action}". Expected one of: ${BULK_ACTIONS.join(", ")}.`);
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
}
|
|
1085
|
+
if (!op.model || typeof op.model !== "string") {
|
|
1086
|
+
error(`Operation ${i}: "model" is required and must be a model name.`);
|
|
1087
|
+
process.exit(1);
|
|
1088
|
+
}
|
|
1089
|
+
if (!op.id || typeof op.id !== "string") {
|
|
1090
|
+
error(`Operation ${i}: "id" is required.`);
|
|
1091
|
+
process.exit(1);
|
|
1092
|
+
}
|
|
1093
|
+
// `data` on a delete gets its own message before the allowlist runs, so
|
|
1094
|
+
// the caller is not told a first-class key is "unknown".
|
|
1095
|
+
if (action === "delete" && op.data !== undefined) {
|
|
1096
|
+
error(`Operation ${i}: "data" is not allowed on a delete.`);
|
|
1097
|
+
process.exit(1);
|
|
1098
|
+
}
|
|
1099
|
+
const allowedKeys = action === "delete" ? BULK_DELETE_OP_KEYS : BULK_OP_KEYS;
|
|
1100
|
+
const unknownKey = Object.keys(op).find((k) => !allowedKeys.includes(k));
|
|
1101
|
+
if (unknownKey) {
|
|
1102
|
+
error(`Operation ${i}: unknown key "${unknownKey}". Expected one of: ${allowedKeys.join(", ")}.`);
|
|
1103
|
+
process.exit(1);
|
|
1104
|
+
}
|
|
1105
|
+
// A `precondition` is field-equality only, the same grammar the
|
|
1106
|
+
// documents surface enforces (`validatePrecondition` in
|
|
1107
|
+
// `src/document-models.ts`), so one blob means the same thing on both.
|
|
1108
|
+
// `preconditionToCondition` translates it to the DO's per-op
|
|
1109
|
+
// `condition`, checked inside the transaction: a mismatch is a 409 that
|
|
1110
|
+
// rolls the whole batch back.
|
|
1111
|
+
let condition;
|
|
1112
|
+
if (op.precondition !== undefined && op.precondition !== null) {
|
|
1113
|
+
if (typeof op.precondition !== "object" || Array.isArray(op.precondition)) {
|
|
1114
|
+
error(`Operation ${i}: "precondition" must be a field-equality object ({ field: scalar, … }).`);
|
|
1115
|
+
process.exit(1);
|
|
1116
|
+
}
|
|
1117
|
+
for (const [field, expected] of Object.entries(op.precondition)) {
|
|
1118
|
+
if (field.startsWith("$")) {
|
|
1119
|
+
error(`Operation ${i}: precondition operator "${field}" is not supported — preconditions are field-equality only ({ field: scalar, … }).`);
|
|
1120
|
+
process.exit(1);
|
|
1121
|
+
}
|
|
1122
|
+
if (expected !== null && typeof expected === "object") {
|
|
1123
|
+
error(`Operation ${i}: precondition on field "${field}" must be a scalar equality value — operators and nested objects/arrays are not supported.`);
|
|
1124
|
+
process.exit(1);
|
|
1125
|
+
}
|
|
1126
|
+
// Fail loudly on the values the `condition` grammar cannot check
|
|
1127
|
+
// exactly, rather than running with a weaker guard than the caller
|
|
1128
|
+
// wrote (issue #2437; the grammar fix is tracked in #2507).
|
|
1129
|
+
const untranslatable = untranslatablePreconditionValue(expected);
|
|
1130
|
+
if (untranslatable) {
|
|
1131
|
+
error(`Operation ${i}: precondition on field "${field}" is not supported — ${untranslatable}. ` +
|
|
1132
|
+
`Guard on a field whose value is a string or a number other than 0/1, or read the record ` +
|
|
1133
|
+
`first and gate the write on that.`);
|
|
1134
|
+
process.exit(1);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
condition = preconditionToCondition(op.precondition);
|
|
1138
|
+
}
|
|
1139
|
+
if (action === "delete") {
|
|
1140
|
+
if (op.data !== undefined) {
|
|
1141
|
+
error(`Operation ${i}: "data" is not allowed on a delete.`);
|
|
1142
|
+
process.exit(1);
|
|
1143
|
+
}
|
|
1144
|
+
operations.push({
|
|
1145
|
+
op: "delete",
|
|
1146
|
+
modelName: op.model,
|
|
1147
|
+
id: op.id,
|
|
1148
|
+
...(condition ? { condition } : {}),
|
|
1149
|
+
});
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
if (!op.data ||
|
|
1153
|
+
typeof op.data !== "object" ||
|
|
1154
|
+
Array.isArray(op.data) ||
|
|
1155
|
+
Object.keys(op.data).length === 0) {
|
|
1156
|
+
error(`Operation ${i}: "data" is required and must be a non-empty object on a ${action}.`);
|
|
1157
|
+
process.exit(1);
|
|
1158
|
+
}
|
|
1159
|
+
operations.push({
|
|
1160
|
+
op: BULK_ACTION_TO_OP[action],
|
|
1161
|
+
modelName: op.model,
|
|
1162
|
+
id: op.id,
|
|
1163
|
+
data: op.data,
|
|
1164
|
+
// `create` is a STRICT create on both twins: `ifNotExists` is the
|
|
1165
|
+
// DatabaseDO batch's strict-create flag (409 `ALREADY_EXISTS`), so a
|
|
1166
|
+
// create against a live id fails the batch instead of merging.
|
|
1167
|
+
...(action === "create" ? { ifNotExists: true } : {}),
|
|
1168
|
+
...(condition ? { condition } : {}),
|
|
547
1169
|
});
|
|
548
|
-
|
|
1170
|
+
}
|
|
1171
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1172
|
+
if (!options.yes) {
|
|
1173
|
+
let confirm;
|
|
1174
|
+
try {
|
|
1175
|
+
confirm = await confirmPrompt(`Apply ${operations.length} operation(s) atomically to database ${databaseId}?`);
|
|
1176
|
+
}
|
|
1177
|
+
catch (err) {
|
|
1178
|
+
error(err.message);
|
|
1179
|
+
process.exit(1);
|
|
1180
|
+
}
|
|
1181
|
+
if (!confirm) {
|
|
1182
|
+
info("Cancelled.");
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
const client = new ApiClient();
|
|
1187
|
+
try {
|
|
1188
|
+
await client.batchDatabaseRecords(resolvedAppId, databaseId, operations, {
|
|
1189
|
+
atomic: true,
|
|
1190
|
+
});
|
|
1191
|
+
// The batch endpoint answers with a per-operation `results` array; the
|
|
1192
|
+
// documents twin answers with a rolled-up summary. Report the twin's
|
|
1193
|
+
// summary so one blob produces one contract on both surfaces — the
|
|
1194
|
+
// batch is atomic, so a 2xx means every op in the input applied.
|
|
1195
|
+
//
|
|
1196
|
+
// One reporting difference, documented alongside the verb: the
|
|
1197
|
+
// documents twin's `deleted` counts records actually removed, because
|
|
1198
|
+
// its DO reports that. The database batch issues a plain DELETE and
|
|
1199
|
+
// reports no rows-affected, so `deleted` here counts delete operations
|
|
1200
|
+
// applied — deleting an id that was not there still counts 1.
|
|
1201
|
+
const added = rawOperations
|
|
1202
|
+
.filter((op) => op.action === "create")
|
|
1203
|
+
.map((op) => ({ model: op.model, id: op.id }));
|
|
1204
|
+
const updated = rawOperations
|
|
1205
|
+
.filter((op) => op.action === "patch")
|
|
1206
|
+
.map((op) => ({ model: op.model, id: op.id }));
|
|
1207
|
+
const deleted = rawOperations.filter((op) => op.action === "delete").length;
|
|
1208
|
+
const summary = {
|
|
1209
|
+
applied: added.length + updated.length + deleted,
|
|
1210
|
+
added,
|
|
1211
|
+
updated,
|
|
1212
|
+
deleted,
|
|
1213
|
+
};
|
|
1214
|
+
if (options.json) {
|
|
1215
|
+
json(summary);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
success(`Applied ${summary.applied} operation(s): ` +
|
|
1219
|
+
`${added.length} added, ${updated.length} updated, ${deleted} deleted.`);
|
|
1220
|
+
}
|
|
1221
|
+
catch (err) {
|
|
1222
|
+
error(err.message);
|
|
1223
|
+
process.exit(1);
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
// ---- CEL context subcommand group ----
|
|
1227
|
+
// `cel-context` is the user-facing name for the dict that feeds CEL
|
|
1228
|
+
// access rules and filter placeholders. `metadata` is kept as a hidden
|
|
1229
|
+
// alias for backwards compatibility with existing scripts.
|
|
1230
|
+
const registerCelContextGroup = (name, hidden) => {
|
|
1231
|
+
const group = databases
|
|
1232
|
+
.command(name, { hidden })
|
|
1233
|
+
.description(hidden
|
|
1234
|
+
? "(deprecated: use resource metadata categories, `primitive metadata`) Manage database CEL context (alias of 'cel-context')"
|
|
1235
|
+
: "(deprecated: use resource metadata categories, `primitive metadata`) Manage database CEL context (the dict referenced by access rules and filters)");
|
|
1236
|
+
// CEL context is deprecated in favour of resource metadata categories
|
|
1237
|
+
// (#1420, #1815), which carry separate read and write rules.
|
|
1238
|
+
deprecateCommandGroup(group, CEL_CONTEXT_SUCCESSOR, { issue: 1815 });
|
|
1239
|
+
group
|
|
1240
|
+
.command("get")
|
|
1241
|
+
.description("Read database CEL context")
|
|
1242
|
+
.argument("<database-id>", "Database ID")
|
|
1243
|
+
.option("--app <app-id>", "App ID")
|
|
1244
|
+
.option("--json", "Output as JSON")
|
|
1245
|
+
.action(async (databaseId, options) => {
|
|
1246
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1247
|
+
const client = new ApiClient();
|
|
1248
|
+
try {
|
|
1249
|
+
const result = await client.getDatabaseCelContext(resolvedAppId, databaseId);
|
|
1250
|
+
if (options.json) {
|
|
1251
|
+
json(result);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
printResult("Database ID", result.databaseId);
|
|
1255
|
+
const ctx = result.celContext ?? result.metadata;
|
|
1256
|
+
if (ctx && Object.keys(ctx).length > 0) {
|
|
1257
|
+
printResult("CEL Context", JSON.stringify(ctx, null, 2));
|
|
1258
|
+
}
|
|
1259
|
+
else {
|
|
1260
|
+
info("No CEL context set.");
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
catch (err) {
|
|
1264
|
+
error(err.message);
|
|
1265
|
+
process.exit(1);
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
group
|
|
1269
|
+
.command("update")
|
|
1270
|
+
.description("Update database CEL context (merge with existing)")
|
|
1271
|
+
.argument("<database-id>", "Database ID")
|
|
1272
|
+
.requiredOption("--data <json>", "CEL context fields as JSON (merged with existing)")
|
|
1273
|
+
.option("--app <app-id>", "App ID")
|
|
1274
|
+
.option("--json", "Output as JSON")
|
|
1275
|
+
.action(async (databaseId, options) => {
|
|
1276
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1277
|
+
const client = new ApiClient();
|
|
1278
|
+
let data;
|
|
1279
|
+
try {
|
|
1280
|
+
data = JSON.parse(options.data);
|
|
1281
|
+
}
|
|
1282
|
+
catch {
|
|
1283
|
+
error("Invalid --data JSON.");
|
|
1284
|
+
process.exit(1);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
try {
|
|
1288
|
+
const result = await client.updateDatabaseCelContext(resolvedAppId, databaseId, data);
|
|
1289
|
+
if (options.json) {
|
|
1290
|
+
json(result);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
success("CEL context updated.");
|
|
1294
|
+
const ctx = result.celContext ?? result.metadata;
|
|
1295
|
+
if (ctx) {
|
|
1296
|
+
keyValue("CEL Context", JSON.stringify(ctx));
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
catch (err) {
|
|
1300
|
+
error(err.message);
|
|
1301
|
+
process.exit(1);
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
};
|
|
1305
|
+
registerCelContextGroup("cel-context", false);
|
|
1306
|
+
// Hidden legacy alias — `primitive databases metadata get/update` still works.
|
|
1307
|
+
registerCelContextGroup("metadata", true);
|
|
1308
|
+
// ---- Operations subcommand group ----
|
|
1309
|
+
const operations = databases
|
|
1310
|
+
.command("operations")
|
|
1311
|
+
.description("List and execute registered operations on a database");
|
|
1312
|
+
// List operations
|
|
1313
|
+
operations
|
|
1314
|
+
.command("list")
|
|
1315
|
+
.description("List registered operations available on a database")
|
|
1316
|
+
.argument("<database-id>", "Database ID")
|
|
1317
|
+
.option("--app <app-id>", "App ID")
|
|
1318
|
+
.option("--json", "Output as JSON")
|
|
1319
|
+
.action(async (databaseId, options) => {
|
|
1320
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1321
|
+
const client = new ApiClient();
|
|
1322
|
+
try {
|
|
1323
|
+
const result = await client.listDatabaseOperations(resolvedAppId, databaseId);
|
|
1324
|
+
const list = Array.isArray(result) ? result : [];
|
|
1325
|
+
if (options.json) {
|
|
1326
|
+
json(wholeListEnvelope(list));
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (list.length === 0) {
|
|
1330
|
+
info("No operations found.");
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
console.log(formatTable(list, [
|
|
1334
|
+
{ header: "NAME", key: "name" },
|
|
1335
|
+
{ header: "TYPE", key: "type" },
|
|
1336
|
+
{ header: "MODEL", key: "modelName" },
|
|
1337
|
+
{ header: "ACCESS", key: "access" },
|
|
1338
|
+
]));
|
|
1339
|
+
}
|
|
1340
|
+
catch (err) {
|
|
1341
|
+
error(err.message);
|
|
1342
|
+
process.exit(1);
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
// Execute operation
|
|
1346
|
+
operations
|
|
1347
|
+
.command("execute")
|
|
1348
|
+
.description("Execute a registered operation on a database")
|
|
1349
|
+
.argument("<database-id>", "Database ID")
|
|
1350
|
+
.argument("<operation-name>", "Operation name")
|
|
1351
|
+
.option("--params <json>", "Operation parameters as JSON")
|
|
1352
|
+
.option("--limit <n>", "Max records to return (for queries)")
|
|
1353
|
+
.option("--cursor <cursor>", "Pagination cursor")
|
|
1354
|
+
.option("--token <jwt>", "Execute as a specific user (dev/test — JWT visible in process args)")
|
|
1355
|
+
.option("--app <app-id>", "App ID")
|
|
1356
|
+
.option("--json", "Output as JSON")
|
|
1357
|
+
.option("--timing", "Show server-side timing breakdown")
|
|
1358
|
+
.action(async (databaseId, operationName, options) => {
|
|
1359
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1360
|
+
const client = new ApiClient();
|
|
1361
|
+
let params;
|
|
1362
|
+
if (options.params) {
|
|
1363
|
+
try {
|
|
1364
|
+
params = JSON.parse(options.params);
|
|
1365
|
+
}
|
|
1366
|
+
catch {
|
|
1367
|
+
error("Invalid --params JSON.");
|
|
1368
|
+
process.exit(1);
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
const body = {};
|
|
1373
|
+
if (params)
|
|
1374
|
+
body.params = params;
|
|
1375
|
+
if (options.limit)
|
|
1376
|
+
body.limit = parseInt(options.limit, 10);
|
|
1377
|
+
if (options.cursor)
|
|
1378
|
+
body.cursor = options.cursor;
|
|
1379
|
+
try {
|
|
1380
|
+
const result = await client.executeDatabaseOperation(resolvedAppId, databaseId, operationName, body, options.token, { timing: !!options.timing });
|
|
1381
|
+
if (options.json) {
|
|
1382
|
+
json(result);
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
// Smart display based on result shape. `executeDatabaseOperation()`
|
|
1386
|
+
// has already normalized a query page to `{ items, hasMore,
|
|
1387
|
+
// nextCursor? }` (#2440), so `items` is exactly "this result was a
|
|
1388
|
+
// list" — the two branches cannot disagree about it.
|
|
1389
|
+
if (Array.isArray(result.items)) {
|
|
1390
|
+
if (result.items.length === 0) {
|
|
1391
|
+
info("No records found.");
|
|
1392
|
+
}
|
|
1393
|
+
else {
|
|
1394
|
+
const cols = Object.keys(result.items[0]).filter((k) => k !== "type");
|
|
1395
|
+
console.log(formatTable(result.items, cols.map((c) => ({
|
|
1396
|
+
header: c.toUpperCase(),
|
|
1397
|
+
key: c,
|
|
1398
|
+
format: (v) => {
|
|
1399
|
+
if (v === null || v === undefined)
|
|
1400
|
+
return "—";
|
|
1401
|
+
if (typeof v === "object") {
|
|
1402
|
+
const s = JSON.stringify(v);
|
|
1403
|
+
return s.length > 50 ? s.slice(0, 47) + "..." : s;
|
|
1404
|
+
}
|
|
1405
|
+
const s = String(v);
|
|
1406
|
+
return s.length > 50 ? s.slice(0, 47) + "..." : s;
|
|
1407
|
+
},
|
|
1408
|
+
}))));
|
|
1409
|
+
}
|
|
1410
|
+
// Gate on the cursor itself, the way `records query` does: `hasMore`
|
|
1411
|
+
// without a cursor used to print the literal `undefined` (#2440).
|
|
1412
|
+
if (result.nextCursor) {
|
|
1413
|
+
info(`More records available. Use --cursor ${result.nextCursor} to continue.`);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
else if (result.count !== undefined) {
|
|
1417
|
+
console.log(`Count: ${result.count}`);
|
|
1418
|
+
}
|
|
1419
|
+
else if (result.result !== undefined) {
|
|
1420
|
+
// Aggregate result
|
|
1421
|
+
json(result);
|
|
1422
|
+
}
|
|
1423
|
+
else if (result.results && Array.isArray(result.results)) {
|
|
1424
|
+
// Batch/mutation results
|
|
1425
|
+
success(`Operation executed (${result.results.length} result${result.results.length === 1 ? "" : "s"}).`);
|
|
1426
|
+
}
|
|
1427
|
+
else if (result.success !== undefined) {
|
|
1428
|
+
success("Operation executed.");
|
|
1429
|
+
}
|
|
1430
|
+
else {
|
|
1431
|
+
json(result);
|
|
1432
|
+
}
|
|
1433
|
+
// Display timing if present
|
|
1434
|
+
if (result._timing) {
|
|
1435
|
+
console.log("");
|
|
1436
|
+
console.log("Timing:");
|
|
1437
|
+
for (const [key, value] of Object.entries(result._timing)) {
|
|
1438
|
+
console.log(` ${key}: ${value}ms`);
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
549
1441
|
}
|
|
550
1442
|
catch (err) {
|
|
551
1443
|
error(err.message);
|
|
@@ -571,7 +1463,7 @@ Examples:
|
|
|
571
1463
|
const result = await client.listDatabaseIndexes(resolvedAppId, databaseId, options.model);
|
|
572
1464
|
const list = result.indexes || [];
|
|
573
1465
|
if (options.json) {
|
|
574
|
-
json(list);
|
|
1466
|
+
json(wholeListEnvelope(list));
|
|
575
1467
|
return;
|
|
576
1468
|
}
|
|
577
1469
|
if (list.length === 0) {
|
|
@@ -634,15 +1526,14 @@ Examples:
|
|
|
634
1526
|
.action(async (databaseId, options) => {
|
|
635
1527
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
636
1528
|
if (!options.yes) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
{
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
]);
|
|
1529
|
+
let confirm;
|
|
1530
|
+
try {
|
|
1531
|
+
confirm = await confirmPrompt(`Drop index on ${options.model}.${options.field}?`);
|
|
1532
|
+
}
|
|
1533
|
+
catch (err) {
|
|
1534
|
+
error(err.message);
|
|
1535
|
+
process.exit(1);
|
|
1536
|
+
}
|
|
646
1537
|
if (!confirm) {
|
|
647
1538
|
info("Cancelled.");
|
|
648
1539
|
return;
|
|
@@ -661,108 +1552,861 @@ Examples:
|
|
|
661
1552
|
process.exit(1);
|
|
662
1553
|
}
|
|
663
1554
|
});
|
|
664
|
-
//
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
.
|
|
671
|
-
.description("List group permissions for a database")
|
|
1555
|
+
// Reindex an existing instance from its type schema (issue #974). The
|
|
1556
|
+
// on-demand remedy that brings a pre-existing database into compliance after
|
|
1557
|
+
// a schema declares a field `unique = true` — registers any declared-but-
|
|
1558
|
+
// missing unique index so `upsertOn` ops resolve. Idempotent.
|
|
1559
|
+
databases
|
|
1560
|
+
.command("reindex")
|
|
1561
|
+
.description("Reprovision a database's unique indexes from its type schema")
|
|
672
1562
|
.argument("<database-id>", "Database ID")
|
|
1563
|
+
.requiredOption("--from-schema", "Reprovision unique indexes declared in the type schema")
|
|
673
1564
|
.option("--app <app-id>", "App ID")
|
|
674
1565
|
.option("--json", "Output as JSON")
|
|
675
1566
|
.action(async (databaseId, options) => {
|
|
676
1567
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
677
1568
|
const client = new ApiClient();
|
|
678
1569
|
try {
|
|
679
|
-
const
|
|
680
|
-
const list = Array.isArray(result) ? result : result?.permissions ?? [];
|
|
1570
|
+
const res = await client.reindexDatabaseFromSchema(resolvedAppId, databaseId);
|
|
681
1571
|
if (options.json) {
|
|
682
|
-
json(
|
|
1572
|
+
json(res);
|
|
683
1573
|
return;
|
|
684
1574
|
}
|
|
685
|
-
if (
|
|
686
|
-
info(
|
|
1575
|
+
if (res.skipped) {
|
|
1576
|
+
info(`Nothing to reindex (${res.reason}).`);
|
|
687
1577
|
return;
|
|
688
1578
|
}
|
|
689
|
-
|
|
690
|
-
{ header: "GROUP_TYPE", key: "groupType" },
|
|
691
|
-
{ header: "GROUP_ID", key: "groupId" },
|
|
692
|
-
{ header: "PERMISSION", key: "permission" },
|
|
693
|
-
{ header: "GRANTED", key: "grantedAt", format: formatDate },
|
|
694
|
-
]));
|
|
1579
|
+
success(`Reindexed from schema: ${res.registered} unique index${res.registered === 1 ? "" : "es"} provisioned.`);
|
|
695
1580
|
}
|
|
696
1581
|
catch (err) {
|
|
697
1582
|
error(err.message);
|
|
698
1583
|
process.exit(1);
|
|
699
1584
|
}
|
|
700
1585
|
});
|
|
701
|
-
//
|
|
702
|
-
|
|
703
|
-
.command("
|
|
704
|
-
.description("
|
|
705
|
-
|
|
706
|
-
.
|
|
707
|
-
|
|
708
|
-
|
|
1586
|
+
// ---- Export / Import commands ----
|
|
1587
|
+
databases
|
|
1588
|
+
.command("export")
|
|
1589
|
+
.description("Export a database (records, indexes, constraints)")
|
|
1590
|
+
// Both positionals are declared optional and the action decides which is
|
|
1591
|
+
// which (#3645). A required positional AFTER an optional one makes the
|
|
1592
|
+
// optional one mandatory: commander binds the first value it sees to
|
|
1593
|
+
// `[app-id]` and then reports the required argument missing, so
|
|
1594
|
+
// `databases export <database-id>` never reached the action. The database
|
|
1595
|
+
// id is still required — enforced below, where the shift is known.
|
|
1596
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
1597
|
+
.argument("[database-id]", "Database ID to export (required)")
|
|
709
1598
|
.option("--app <app-id>", "App ID")
|
|
1599
|
+
.option("--output <dir>", "Output directory", "./primitive-export")
|
|
710
1600
|
.option("--json", "Output as JSON")
|
|
711
|
-
.action(async (
|
|
712
|
-
|
|
1601
|
+
.action(async (first, second, options) => {
|
|
1602
|
+
let resolvedAppId;
|
|
1603
|
+
let databaseId;
|
|
1604
|
+
if (second && !second.startsWith("-")) {
|
|
1605
|
+
resolvedAppId = resolveAppId(first, options);
|
|
1606
|
+
databaseId = second;
|
|
1607
|
+
}
|
|
1608
|
+
else {
|
|
1609
|
+
resolvedAppId = resolveAppId(undefined, options);
|
|
1610
|
+
databaseId = first;
|
|
1611
|
+
}
|
|
1612
|
+
if (!databaseId) {
|
|
1613
|
+
error("Database ID is required.");
|
|
1614
|
+
process.exit(1);
|
|
1615
|
+
}
|
|
713
1616
|
const client = new ApiClient();
|
|
714
1617
|
try {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1618
|
+
await exportSingleDatabase(client, resolvedAppId, databaseId, options.output, options.json);
|
|
1619
|
+
}
|
|
1620
|
+
catch (err) {
|
|
1621
|
+
error(err.message);
|
|
1622
|
+
process.exit(1);
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
databases
|
|
1626
|
+
.command("import")
|
|
1627
|
+
.description("Import a database from an export directory")
|
|
1628
|
+
// Optional in commander, required in the action (#3645) — see `export`
|
|
1629
|
+
// above for why a `<...>` here would make `[app-id]` mandatory.
|
|
1630
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
1631
|
+
.argument("[path]", "Path to database export directory (required)")
|
|
1632
|
+
.option("--app <app-id>", "App ID")
|
|
1633
|
+
.option("--overwrite", "Replace existing database if ID collides")
|
|
1634
|
+
.option("--dry-run", "Show what would be imported without making changes")
|
|
1635
|
+
.option("--batch-size <n>", "Records per batch write request (default: 5000)", "5000")
|
|
1636
|
+
.option("--stop-on-error", "Abort the whole run on the first failing chunk (default: continue and report)")
|
|
1637
|
+
.option("--json", "Output as JSON")
|
|
1638
|
+
.action(async (first, second, options) => {
|
|
1639
|
+
let resolvedAppId;
|
|
1640
|
+
let importPath;
|
|
1641
|
+
if (second && !second.startsWith("-")) {
|
|
1642
|
+
resolvedAppId = resolveAppId(first, options);
|
|
1643
|
+
importPath = second;
|
|
1644
|
+
}
|
|
1645
|
+
else {
|
|
1646
|
+
resolvedAppId = resolveAppId(undefined, options);
|
|
1647
|
+
importPath = first;
|
|
1648
|
+
}
|
|
1649
|
+
if (!importPath) {
|
|
1650
|
+
error("Import path is required.");
|
|
1651
|
+
process.exit(1);
|
|
1652
|
+
}
|
|
1653
|
+
// Validate --batch-size up front so a bad value fails before any write.
|
|
1654
|
+
// Unlike `import-csv` (registered-batch path, hard 100k server cap),
|
|
1655
|
+
// `records/batch` has no fixed server item cap — bound it with a
|
|
1656
|
+
// conservative client-side ceiling to keep each request within
|
|
1657
|
+
// Cloudflare body-size and Durable Object memory limits.
|
|
1658
|
+
const parsedBatchSize = parseBatchSize(options.batchSize, {
|
|
1659
|
+
max: IMPORT_BATCH_SIZE_CEILING,
|
|
1660
|
+
overMaxMessage: `--batch-size cannot exceed ${IMPORT_BATCH_SIZE_CEILING} (a conservative client-side limit for batch write requests).`,
|
|
1661
|
+
});
|
|
1662
|
+
if ("error" in parsedBatchSize) {
|
|
1663
|
+
error(parsedBatchSize.error);
|
|
1664
|
+
process.exit(1);
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
const batchSize = parsedBatchSize.value;
|
|
1668
|
+
const client = new ApiClient();
|
|
1669
|
+
try {
|
|
1670
|
+
// Determine if this is a manifest-based export or a single database
|
|
1671
|
+
let dbDirs = [];
|
|
1672
|
+
const manifestPath = path.join(importPath, "manifest.json");
|
|
1673
|
+
if (existsSync(path.join(importPath, "metadata.json"))) {
|
|
1674
|
+
// Single database directory
|
|
1675
|
+
dbDirs = [importPath];
|
|
1676
|
+
}
|
|
1677
|
+
else if (existsSync(manifestPath)) {
|
|
1678
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
1679
|
+
for (const dbId of manifest.databases || []) {
|
|
1680
|
+
const dbDir = path.join(importPath, "databases", dbId);
|
|
1681
|
+
if (existsSync(dbDir)) {
|
|
1682
|
+
dbDirs.push(dbDir);
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
else if (existsSync(path.join(importPath, "databases"))) {
|
|
1687
|
+
const entries = readdirSync(path.join(importPath, "databases"), { withFileTypes: true });
|
|
1688
|
+
for (const entry of entries) {
|
|
1689
|
+
if (entry.isDirectory()) {
|
|
1690
|
+
dbDirs.push(path.join(importPath, "databases", entry.name));
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
else {
|
|
1695
|
+
error("No valid database export data found at the specified path.");
|
|
1696
|
+
process.exit(1);
|
|
1697
|
+
}
|
|
1698
|
+
if (dbDirs.length === 0) {
|
|
1699
|
+
info("No databases to import.");
|
|
722
1700
|
return;
|
|
723
1701
|
}
|
|
724
|
-
|
|
1702
|
+
const summary = {
|
|
1703
|
+
created: 0,
|
|
1704
|
+
updated: 0,
|
|
1705
|
+
skipped: 0,
|
|
1706
|
+
recordsImported: 0,
|
|
1707
|
+
recordsFailed: 0,
|
|
1708
|
+
batches: 0,
|
|
1709
|
+
aborted: false,
|
|
1710
|
+
indexesRegistered: 0,
|
|
1711
|
+
batchErrors: [],
|
|
1712
|
+
};
|
|
1713
|
+
for (const dbDir of dbDirs) {
|
|
1714
|
+
await importSingleDatabase(client, resolvedAppId, dbDir, {
|
|
1715
|
+
overwrite: options.overwrite || false,
|
|
1716
|
+
dryRun: options.dryRun || false,
|
|
1717
|
+
batchSize,
|
|
1718
|
+
stopOnError: options.stopOnError || false,
|
|
1719
|
+
json: options.json || false,
|
|
1720
|
+
summary,
|
|
1721
|
+
});
|
|
1722
|
+
// Fork 3: --stop-on-error stops the whole run across a multi-DB dir.
|
|
1723
|
+
if (summary.aborted)
|
|
1724
|
+
break;
|
|
1725
|
+
}
|
|
1726
|
+
if (options.json) {
|
|
1727
|
+
json({ ...summary, dryRun: options.dryRun || false });
|
|
1728
|
+
}
|
|
1729
|
+
else {
|
|
1730
|
+
success("Database import complete:");
|
|
1731
|
+
keyValue(" Databases created", String(summary.created));
|
|
1732
|
+
if (summary.updated > 0)
|
|
1733
|
+
keyValue(" Databases updated", String(summary.updated));
|
|
1734
|
+
if (summary.skipped > 0)
|
|
1735
|
+
keyValue(" Databases skipped", String(summary.skipped));
|
|
1736
|
+
keyValue(" Records imported", String(summary.recordsImported));
|
|
1737
|
+
if (summary.recordsFailed > 0)
|
|
1738
|
+
keyValue(" Records failed", String(summary.recordsFailed));
|
|
1739
|
+
keyValue(" Batches", String(summary.batches));
|
|
1740
|
+
if (summary.indexesRegistered > 0)
|
|
1741
|
+
keyValue(" Indexes registered", String(summary.indexesRegistered));
|
|
1742
|
+
if (summary.aborted)
|
|
1743
|
+
warn("Aborted after a failing chunk (--stop-on-error).");
|
|
1744
|
+
if (summary.batchErrors.length > 0) {
|
|
1745
|
+
error(`${summary.batchErrors.length} chunk(s) errored:`);
|
|
1746
|
+
for (const e of summary.batchErrors) {
|
|
1747
|
+
error(` ${e.database} batch ${e.batch + 1}: ${e.error}`);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
// Non-zero exit if any chunk errored (default continue-and-report or
|
|
1752
|
+
// --stop-on-error both surface failures this way).
|
|
1753
|
+
if (summary.batchErrors.length > 0) {
|
|
1754
|
+
process.exit(1);
|
|
1755
|
+
}
|
|
725
1756
|
}
|
|
726
1757
|
catch (err) {
|
|
727
1758
|
error(err.message);
|
|
728
1759
|
process.exit(1);
|
|
729
1760
|
}
|
|
730
1761
|
});
|
|
731
|
-
//
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1762
|
+
// ============================================
|
|
1763
|
+
// CSV import (issue #145, Phase 1)
|
|
1764
|
+
// ============================================
|
|
1765
|
+
databases
|
|
1766
|
+
.command("import-csv")
|
|
1767
|
+
.description("Import rows from a CSV file into a database via a registered batch (bulk) save operation")
|
|
735
1768
|
.argument("<database-id>", "Database ID")
|
|
736
|
-
.argument("<
|
|
737
|
-
.
|
|
1769
|
+
.argument("<file>", "Path to the CSV file")
|
|
1770
|
+
.requiredOption("--model <name>", "Model name passed in each item's params")
|
|
1771
|
+
.option("--operation <op>", "Registered mutation operation to use (must be a {modelName,id,data} save-like op)", "seed_save")
|
|
1772
|
+
.option("--column-map <json>", 'Map CSV headers to field names, e.g. \'{"CSV Header":"fieldName"}\'')
|
|
1773
|
+
.option("--types <json>", 'Coerce fields by type, e.g. \'{"price":"number","active":"boolean"}\'')
|
|
1774
|
+
.option("--id-column <col>", "Use this CSV column for record ids (else a ULID is generated)")
|
|
1775
|
+
.option("--batch-size <n>", "Records per batch request (default: 5000)", "5000")
|
|
1776
|
+
.option("--delimiter <char>", "CSV field delimiter (default: ',')", ",")
|
|
738
1777
|
.option("--app <app-id>", "App ID")
|
|
739
|
-
.option("-
|
|
740
|
-
.
|
|
1778
|
+
.option("--dry-run", "Parse + report row/batch counts without writing")
|
|
1779
|
+
.option("--stop-on-error", "Abort the whole import on the first chunk error (default: best-effort continue)")
|
|
1780
|
+
.option("--json", "Output a machine-readable result")
|
|
1781
|
+
.action(async (databaseId, file, options) => {
|
|
741
1782
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1783
|
+
const client = new ApiClient();
|
|
1784
|
+
// Parse JSON flags up front for clear errors.
|
|
1785
|
+
let columnMap;
|
|
1786
|
+
if (options.columnMap) {
|
|
1787
|
+
try {
|
|
1788
|
+
columnMap = JSON.parse(options.columnMap);
|
|
1789
|
+
}
|
|
1790
|
+
catch {
|
|
1791
|
+
error("Invalid --column-map JSON.");
|
|
1792
|
+
process.exit(1);
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
let types;
|
|
1797
|
+
if (options.types) {
|
|
1798
|
+
try {
|
|
1799
|
+
types = JSON.parse(options.types);
|
|
1800
|
+
}
|
|
1801
|
+
catch {
|
|
1802
|
+
error("Invalid --types JSON.");
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
// The registered-batch (`executeBatch`) path rejects requests over
|
|
1808
|
+
// 100000 items server-side; keep chunks under it.
|
|
1809
|
+
const parsedBatchSize = parseBatchSize(options.batchSize, {
|
|
1810
|
+
max: 100000,
|
|
1811
|
+
overMaxMessage: "--batch-size cannot exceed 100000 (the server's per-request cap).",
|
|
1812
|
+
});
|
|
1813
|
+
if ("error" in parsedBatchSize) {
|
|
1814
|
+
error(parsedBatchSize.error);
|
|
1815
|
+
process.exit(1);
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
const batchSize = parsedBatchSize.value;
|
|
1819
|
+
if (!existsSync(file)) {
|
|
1820
|
+
error(`CSV file not found: ${file}`);
|
|
1821
|
+
process.exit(1);
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
try {
|
|
1825
|
+
// 1. Read + parse the CSV.
|
|
1826
|
+
const raw = readFileSync(file, "utf-8");
|
|
1827
|
+
let rows = parseCsv(raw, options.delimiter);
|
|
1828
|
+
// 2. Apply column map.
|
|
1829
|
+
rows = applyColumnMap(rows, columnMap);
|
|
1830
|
+
// 3. Coerce types, assign ids, shape into batch items.
|
|
1831
|
+
const { batch, rowCount } = buildRows(rows, {
|
|
1832
|
+
modelName: options.model,
|
|
1833
|
+
types,
|
|
1834
|
+
idColumn: options.idColumn,
|
|
1835
|
+
});
|
|
1836
|
+
const chunks = chunk(batch, batchSize);
|
|
1837
|
+
// 4. Validate the chosen op is an import-compatible save-like mutation
|
|
1838
|
+
// BEFORE writing (op-not-found / incompatible → clear up-front error
|
|
1839
|
+
// rather than per-row server-side failure). Skip the round-trip only
|
|
1840
|
+
// when there is genuinely nothing to import in a dry run.
|
|
1841
|
+
if (!options.dryRun || rowCount > 0) {
|
|
1842
|
+
await validateImportOperation(client, resolvedAppId, databaseId, options.operation);
|
|
1843
|
+
}
|
|
1844
|
+
// 5. Empty / headers-only CSV: no request, report 0/0.
|
|
1845
|
+
if (rowCount === 0) {
|
|
1846
|
+
if (options.json) {
|
|
1847
|
+
json({ imported: 0, failed: 0, rows: 0, batches: 0, errors: [], dryRun: !!options.dryRun });
|
|
1848
|
+
}
|
|
1849
|
+
else {
|
|
1850
|
+
info("No data rows found in CSV — nothing to import.");
|
|
1851
|
+
success("Imported: 0, Failed: 0");
|
|
1852
|
+
}
|
|
754
1853
|
return;
|
|
755
1854
|
}
|
|
1855
|
+
// 6. Dry run: report counts, no writes.
|
|
1856
|
+
if (options.dryRun) {
|
|
1857
|
+
if (options.json) {
|
|
1858
|
+
json({ dryRun: true, rows: rowCount, batches: chunks.length, batchSize });
|
|
1859
|
+
}
|
|
1860
|
+
else {
|
|
1861
|
+
info(`Dry run — no records written.`);
|
|
1862
|
+
keyValue(" Rows", String(rowCount));
|
|
1863
|
+
keyValue(" Batches", String(chunks.length));
|
|
1864
|
+
keyValue(" Batch size", String(batchSize));
|
|
1865
|
+
}
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
// 7. Import chunk by chunk; accumulate imported/failed + per-chunk errors.
|
|
1869
|
+
let imported = 0;
|
|
1870
|
+
let failed = 0;
|
|
1871
|
+
const errors = [];
|
|
1872
|
+
let aborted = false;
|
|
1873
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1874
|
+
const piece = chunks[i];
|
|
1875
|
+
try {
|
|
1876
|
+
const res = await client.executeBatch(resolvedAppId, databaseId, options.operation, piece);
|
|
1877
|
+
imported += res.imported ?? 0;
|
|
1878
|
+
failed += res.failed ?? 0;
|
|
1879
|
+
if (!options.json) {
|
|
1880
|
+
info(`Batch ${i + 1}/${chunks.length}: imported ${res.imported ?? 0}, failed ${res.failed ?? 0}`);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
catch (err) {
|
|
1884
|
+
const msg = err?.message || String(err);
|
|
1885
|
+
errors.push({ batch: i, error: msg });
|
|
1886
|
+
failed += piece.length;
|
|
1887
|
+
if (!options.json) {
|
|
1888
|
+
warn(`Batch ${i + 1}/${chunks.length} failed: ${msg}`);
|
|
1889
|
+
}
|
|
1890
|
+
if (options.stopOnError) {
|
|
1891
|
+
aborted = true;
|
|
1892
|
+
break;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
if (options.json) {
|
|
1897
|
+
json({ imported, failed, rows: rowCount, batches: chunks.length, errors, aborted });
|
|
1898
|
+
}
|
|
1899
|
+
else {
|
|
1900
|
+
if (aborted) {
|
|
1901
|
+
warn(`Aborted after a chunk error (--stop-on-error).`);
|
|
1902
|
+
}
|
|
1903
|
+
success(`Imported: ${imported}, Failed: ${failed}`);
|
|
1904
|
+
if (errors.length > 0) {
|
|
1905
|
+
error(`${errors.length} batch(es) errored:`);
|
|
1906
|
+
for (const e of errors) {
|
|
1907
|
+
error(` Batch ${e.batch + 1}: ${e.error}`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
// Non-zero exit if any chunk errored.
|
|
1912
|
+
if (errors.length > 0) {
|
|
1913
|
+
process.exit(1);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
catch (err) {
|
|
1917
|
+
error(err.message);
|
|
1918
|
+
process.exit(1);
|
|
756
1919
|
}
|
|
1920
|
+
});
|
|
1921
|
+
// ============================================
|
|
1922
|
+
// Schema commands (issue #666)
|
|
1923
|
+
// ============================================
|
|
1924
|
+
const schema = databases
|
|
1925
|
+
.command("schema")
|
|
1926
|
+
.description("Manage the TOML schema attached to a database type");
|
|
1927
|
+
schema
|
|
1928
|
+
.command("generate")
|
|
1929
|
+
.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.")
|
|
1930
|
+
.argument("<database-type>", "Database type name")
|
|
1931
|
+
.option("-f, --force", "Overwrite an existing [models.*] block in the local file")
|
|
1932
|
+
.option("--json", "Output the generated TOML as JSON")
|
|
1933
|
+
.action(async (databaseType, options) => {
|
|
1934
|
+
// Resolved before the API call, so a project the CLI cannot resolve is
|
|
1935
|
+
// heard about without a round trip.
|
|
1936
|
+
const targetDir = resolveSyncDir();
|
|
1937
|
+
const resolvedAppId = getCurrentAppId();
|
|
757
1938
|
const client = new ApiClient();
|
|
758
1939
|
try {
|
|
759
|
-
|
|
760
|
-
|
|
1940
|
+
// 1. Call the server's :scaffold endpoint.
|
|
1941
|
+
const result = await client.scaffoldDatabaseTypeSchema(resolvedAppId, databaseType);
|
|
1942
|
+
const scaffoldedToml = result?.schema ?? "";
|
|
1943
|
+
if (options.json) {
|
|
1944
|
+
json({ schema: scaffoldedToml });
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
// 2. Locate the local `database-type-configs/<type>.toml` file, at
|
|
1948
|
+
// `<config-dir>/database-type-configs/<type>.toml`.
|
|
1949
|
+
//
|
|
1950
|
+
// ONE file, for ONE environment (#3153, F4). This used to scan every
|
|
1951
|
+
// `primitive/<env>/` slot under the cwd and edit every
|
|
1952
|
+
// match, which spliced a scaffold generated against the selected
|
|
1953
|
+
// environment's server into other environments' committed files. The
|
|
1954
|
+
// directory now comes from the same resolver every other tree command
|
|
1955
|
+
// uses, so the file it edits is the selected environment's.
|
|
1956
|
+
const candidateFiles = [];
|
|
1957
|
+
const candidate = path.join(targetDir, "database-type-configs", `${databaseType}.toml`);
|
|
1958
|
+
if (existsSync(candidate))
|
|
1959
|
+
candidateFiles.push(candidate);
|
|
1960
|
+
if (candidateFiles.length === 0) {
|
|
1961
|
+
// No local file found — print the scaffold to stdout and tell
|
|
1962
|
+
// the user where to put it.
|
|
1963
|
+
info(`No local database-type-configs/${databaseType}.toml found at ${candidate}. Generated TOML:`);
|
|
1964
|
+
console.log("");
|
|
1965
|
+
console.log(scaffoldedToml);
|
|
1966
|
+
return;
|
|
1967
|
+
}
|
|
1968
|
+
// 3. For each candidate, splice the scaffolded TOML at a stable
|
|
1969
|
+
// anchor (before the first `[[operations]]` block, or at EOF). Per
|
|
1970
|
+
// the plan-of-record, textual insertion is deliberate — smol-toml
|
|
1971
|
+
// parse/stringify drops comments, so we splice as a raw string.
|
|
1972
|
+
for (const targetPath of candidateFiles) {
|
|
1973
|
+
const original = readFileSync(targetPath, "utf-8");
|
|
1974
|
+
// Detect existing [models.*] block. If present, refuse unless --force.
|
|
1975
|
+
const hasModelsBlock = /\n\[models\./.test("\n" + original);
|
|
1976
|
+
if (hasModelsBlock && !options.force) {
|
|
1977
|
+
error(`${targetPath} already has [models.*] blocks. Re-run with --force to overwrite.`);
|
|
1978
|
+
process.exit(1);
|
|
1979
|
+
}
|
|
1980
|
+
// Strip existing [models.*] blocks if --force.
|
|
1981
|
+
let cleaned = original;
|
|
1982
|
+
if (hasModelsBlock && options.force) {
|
|
1983
|
+
cleaned = stripModelsBlocks(original);
|
|
1984
|
+
}
|
|
1985
|
+
// Splice: insert before the first [[operations]] block, otherwise
|
|
1986
|
+
// at the end of file.
|
|
1987
|
+
let merged;
|
|
1988
|
+
const opsAnchor = cleaned.indexOf("\n[[operations]]");
|
|
1989
|
+
if (opsAnchor >= 0) {
|
|
1990
|
+
merged =
|
|
1991
|
+
cleaned.slice(0, opsAnchor + 1) +
|
|
1992
|
+
scaffoldedToml +
|
|
1993
|
+
"\n" +
|
|
1994
|
+
cleaned.slice(opsAnchor + 1);
|
|
1995
|
+
}
|
|
1996
|
+
else {
|
|
1997
|
+
const sep = cleaned.endsWith("\n") ? "" : "\n";
|
|
1998
|
+
merged = cleaned + sep + scaffoldedToml + "\n";
|
|
1999
|
+
}
|
|
2000
|
+
writeFileSync(targetPath, merged);
|
|
2001
|
+
success(`Inserted schema into ${targetPath}`);
|
|
2002
|
+
}
|
|
2003
|
+
info("Review the generated schema, fix types the generator guessed wrong, then run `primitive config push` (or `primitive config push --dry-run`).");
|
|
761
2004
|
}
|
|
762
2005
|
catch (err) {
|
|
763
2006
|
error(err.message);
|
|
764
2007
|
process.exit(1);
|
|
765
2008
|
}
|
|
766
2009
|
});
|
|
2010
|
+
// ============================================
|
|
2011
|
+
// Codegen command (issue #814)
|
|
2012
|
+
// ============================================
|
|
2013
|
+
databases
|
|
2014
|
+
.command("codegen")
|
|
2015
|
+
.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.")
|
|
2016
|
+
.argument("[database-type]", "Generate for a single database type (defaults to every database-type-configs/*.toml found)")
|
|
2017
|
+
.option("-o, --output <dir>", "Output directory for the generated files (defaults to <config-dir>/database-type-configs/generated/)")
|
|
2018
|
+
.option("--lang <lang>", "Target language for the generated types: 'ts' (default) or 'swift'.", "ts")
|
|
2019
|
+
.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.")
|
|
2020
|
+
.option("--json", "Output the result summary as JSON")
|
|
2021
|
+
.action(async (databaseType, options) => {
|
|
2022
|
+
try {
|
|
2023
|
+
// 0. Validate --lang up front so an unknown value fails clearly rather
|
|
2024
|
+
// than silently defaulting to the TS emitter.
|
|
2025
|
+
const lang = String(options.lang ?? "ts").toLowerCase();
|
|
2026
|
+
if (lang !== "ts" && lang !== "swift") {
|
|
2027
|
+
error(`Unknown --lang "${options.lang}". Use "ts" or "swift".`);
|
|
2028
|
+
process.exit(1);
|
|
2029
|
+
}
|
|
2030
|
+
// 1. The single source database-type-configs/ directory: the
|
|
2031
|
+
// environment's, and only ever the environment's (#1510, #3154).
|
|
2032
|
+
const dbTypesSourceDir = resolveCodegenSourceDir({
|
|
2033
|
+
subdir: "database-type-configs",
|
|
2034
|
+
});
|
|
2035
|
+
// 2. Collect the source .toml files (one per database type), filtered
|
|
2036
|
+
// to a single type when an argument is given.
|
|
2037
|
+
const inputs = [];
|
|
2038
|
+
for (const fileName of readdirSync(dbTypesSourceDir)) {
|
|
2039
|
+
if (!fileName.endsWith(".toml"))
|
|
2040
|
+
continue;
|
|
2041
|
+
const typeName = fileName.slice(0, -".toml".length);
|
|
2042
|
+
if (databaseType && typeName !== databaseType)
|
|
2043
|
+
continue;
|
|
2044
|
+
const tomlPath = path.join(dbTypesSourceDir, fileName);
|
|
2045
|
+
inputs.push({
|
|
2046
|
+
databaseType: typeName,
|
|
2047
|
+
tomlPath,
|
|
2048
|
+
tomlContent: readFileSync(tomlPath, "utf-8"),
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
if (inputs.length === 0) {
|
|
2052
|
+
error(databaseType
|
|
2053
|
+
? `No database-type-configs/${databaseType}.toml found under primitive/<env>/.`
|
|
2054
|
+
: "No database-type-configs/*.toml files found to generate from.");
|
|
2055
|
+
process.exit(1);
|
|
2056
|
+
}
|
|
2057
|
+
// 3. Resolve the output directory. Default: a `generated/` subdir of
|
|
2058
|
+
// the first source dir (keeps generated output beside the schema).
|
|
2059
|
+
const outputDir = options.output
|
|
2060
|
+
? path.resolve(options.output)
|
|
2061
|
+
: path.join(path.dirname(inputs[0].tomlPath), "generated");
|
|
2062
|
+
// 4. Run codegen (or --check) with the selected emitter. Both the TS
|
|
2063
|
+
// and Swift generators return the same WriteOrCheckResult shape, so
|
|
2064
|
+
// the reporting below is emitter-agnostic; only the emitter (and the
|
|
2065
|
+
// regenerate hint) differs. When a single database-type filter
|
|
2066
|
+
// argument is given, the input set is partial — flag it so stale
|
|
2067
|
+
// cleanup is scoped to that type and sibling types' generated files
|
|
2068
|
+
// are left intact.
|
|
2069
|
+
const codegenResult = lang === "swift"
|
|
2070
|
+
? await generateDbSwiftTypes({
|
|
2071
|
+
inputs,
|
|
2072
|
+
outputDir,
|
|
2073
|
+
check: !!options.check,
|
|
2074
|
+
singleType: !!databaseType,
|
|
2075
|
+
})
|
|
2076
|
+
: await generateDbTypes({
|
|
2077
|
+
inputs,
|
|
2078
|
+
outputDir,
|
|
2079
|
+
check: !!options.check,
|
|
2080
|
+
singleType: !!databaseType,
|
|
2081
|
+
});
|
|
2082
|
+
if (options.check) {
|
|
2083
|
+
if (codegenResult.mismatches.length > 0) {
|
|
2084
|
+
if (options.json) {
|
|
2085
|
+
json({ ok: false, mismatches: codegenResult.mismatches });
|
|
2086
|
+
}
|
|
2087
|
+
else {
|
|
2088
|
+
error(`Check failed: ${codegenResult.mismatches.length} file(s) out of date.`);
|
|
2089
|
+
for (const m of codegenResult.mismatches) {
|
|
2090
|
+
error(` ${m.reason}: ${path.relative(process.cwd(), m.filePath)}`);
|
|
2091
|
+
}
|
|
2092
|
+
// Echo back the options that decide WHERE and WHAT was
|
|
2093
|
+
// checked, the way `workflows codegen` does (#2895). Suggesting
|
|
2094
|
+
// a bare `databases codegen` after a `--check -o <dir>` sent
|
|
2095
|
+
// people to regenerate into the default
|
|
2096
|
+
// <config-dir>/database-type-configs/generated/ instead of the
|
|
2097
|
+
// directory that is actually out of date, so the files stayed
|
|
2098
|
+
// stale and the next check failed the same way (#2911).
|
|
2099
|
+
const regenerate = ["primitive databases codegen"];
|
|
2100
|
+
if (databaseType)
|
|
2101
|
+
regenerate.push(databaseType);
|
|
2102
|
+
if (lang === "swift")
|
|
2103
|
+
regenerate.push("--lang swift");
|
|
2104
|
+
if (options.output)
|
|
2105
|
+
regenerate.push(`-o ${options.output}`);
|
|
2106
|
+
info(`Run \`${regenerate.join(" ")}\` to regenerate.`);
|
|
2107
|
+
}
|
|
2108
|
+
process.exit(1);
|
|
2109
|
+
}
|
|
2110
|
+
if (options.json) {
|
|
2111
|
+
json({ ok: true, checked: codegenResult.writtenFiles.length });
|
|
2112
|
+
}
|
|
2113
|
+
else {
|
|
2114
|
+
success(`Check passed: ${codegenResult.writtenFiles.length} file(s) up to date.`);
|
|
2115
|
+
}
|
|
2116
|
+
return;
|
|
2117
|
+
}
|
|
2118
|
+
if (options.json) {
|
|
2119
|
+
json({
|
|
2120
|
+
written: codegenResult.writtenFiles,
|
|
2121
|
+
deleted: codegenResult.deletedFiles,
|
|
2122
|
+
});
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
success(`Generated ${codegenResult.writtenFiles.length} file(s)` +
|
|
2126
|
+
(codegenResult.deletedFiles.length > 0
|
|
2127
|
+
? `, deleted ${codegenResult.deletedFiles.length} stale file(s).`
|
|
2128
|
+
: "."));
|
|
2129
|
+
for (const f of codegenResult.writtenFiles) {
|
|
2130
|
+
keyValue(" wrote", path.relative(process.cwd(), f));
|
|
2131
|
+
}
|
|
2132
|
+
for (const f of codegenResult.deletedFiles) {
|
|
2133
|
+
keyValue(" deleted", path.relative(process.cwd(), f));
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
catch (err) {
|
|
2137
|
+
error(err.message);
|
|
2138
|
+
process.exit(1);
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
/** Remove every existing `[models.*]` block from a TOML file body. */
|
|
2143
|
+
function stripModelsBlocks(text) {
|
|
2144
|
+
const lines = text.split("\n");
|
|
2145
|
+
const result = [];
|
|
2146
|
+
let skipping = false;
|
|
2147
|
+
for (const line of lines) {
|
|
2148
|
+
const isModelsHeader = /^\s*\[models\b/.test(line);
|
|
2149
|
+
const isOtherHeader = /^\s*\[(?!models\b)/.test(line);
|
|
2150
|
+
if (isModelsHeader) {
|
|
2151
|
+
skipping = true;
|
|
2152
|
+
continue;
|
|
2153
|
+
}
|
|
2154
|
+
if (skipping && isOtherHeader) {
|
|
2155
|
+
skipping = false;
|
|
2156
|
+
}
|
|
2157
|
+
if (!skipping)
|
|
2158
|
+
result.push(line);
|
|
2159
|
+
}
|
|
2160
|
+
return result.join("\n");
|
|
2161
|
+
}
|
|
2162
|
+
// ============================================
|
|
2163
|
+
// Database export/import helpers
|
|
2164
|
+
// ============================================
|
|
2165
|
+
async function exportSingleDatabase(client, appId, databaseId, outputDir, jsonOutput) {
|
|
2166
|
+
const dbDir = path.join(outputDir, "databases", databaseId);
|
|
2167
|
+
mkdirSync(dbDir, { recursive: true });
|
|
2168
|
+
// 1. Fetch database metadata
|
|
2169
|
+
const dbMeta = await client.getDatabase(appId, databaseId);
|
|
2170
|
+
// Warn about databaseType sync
|
|
2171
|
+
if (dbMeta.databaseType) {
|
|
2172
|
+
warn(`This database uses type "${dbMeta.databaseType}". Make sure to \`primitive config push\` the type config before importing into a new app.`);
|
|
2173
|
+
}
|
|
2174
|
+
// 2. Write metadata
|
|
2175
|
+
const metadata = {
|
|
2176
|
+
databaseId: dbMeta.databaseId || databaseId,
|
|
2177
|
+
title: dbMeta.title,
|
|
2178
|
+
databaseType: dbMeta.databaseType || null,
|
|
2179
|
+
metadata: dbMeta.metadata || null,
|
|
2180
|
+
};
|
|
2181
|
+
writeFileSync(path.join(dbDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
2182
|
+
// 3. Export records: list models, then query each.
|
|
2183
|
+
// listDatabaseModels() resolves to `{ models: string[] }`, not a bare array,
|
|
2184
|
+
// so unwrap it before iterating (mirrors the `databases records models`
|
|
2185
|
+
// consumer above). Without this, `for (const modelName of models)` throws
|
|
2186
|
+
// "models is not iterable" and records.jsonl is left empty (#1396).
|
|
2187
|
+
const models = (await client.listDatabaseModels(appId, databaseId)).models || [];
|
|
2188
|
+
let totalRecords = 0;
|
|
2189
|
+
const recordsFile = path.join(dbDir, "records.jsonl");
|
|
2190
|
+
// Clear file
|
|
2191
|
+
writeFileSync(recordsFile, "");
|
|
2192
|
+
for (const modelName of models) {
|
|
2193
|
+
let cursor;
|
|
2194
|
+
let hasMore = true;
|
|
2195
|
+
while (hasMore) {
|
|
2196
|
+
const result = await client.queryDatabaseRecords(appId, databaseId, modelName, {
|
|
2197
|
+
limit: 100,
|
|
2198
|
+
cursor,
|
|
2199
|
+
});
|
|
2200
|
+
const records = result.items;
|
|
2201
|
+
for (const record of records) {
|
|
2202
|
+
const line = JSON.stringify({ _type: modelName, _id: record.id, _data: record });
|
|
2203
|
+
appendFileSync(recordsFile, line + "\n");
|
|
2204
|
+
totalRecords++;
|
|
2205
|
+
}
|
|
2206
|
+
cursor = result.nextCursor;
|
|
2207
|
+
hasMore = !!cursor && records.length > 0;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
// 4. Export indexes
|
|
2211
|
+
const indexes = await client.listDatabaseIndexes(appId, databaseId);
|
|
2212
|
+
writeFileSync(path.join(dbDir, "indexes.json"), JSON.stringify(indexes, null, 2));
|
|
2213
|
+
// 5. Export unique constraints
|
|
2214
|
+
const constraints = await client.listDatabaseUniqueConstraints(appId, databaseId);
|
|
2215
|
+
writeFileSync(path.join(dbDir, "constraints.json"), JSON.stringify(constraints, null, 2));
|
|
2216
|
+
if (jsonOutput) {
|
|
2217
|
+
json({
|
|
2218
|
+
databaseId,
|
|
2219
|
+
title: metadata.title,
|
|
2220
|
+
databaseType: metadata.databaseType,
|
|
2221
|
+
models: models.length,
|
|
2222
|
+
records: totalRecords,
|
|
2223
|
+
indexes: indexes.length,
|
|
2224
|
+
constraints: constraints.length,
|
|
2225
|
+
exportPath: dbDir,
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
else {
|
|
2229
|
+
success(`Exported database ${databaseId} (${metadata.title || "untitled"}) to ${dbDir}`);
|
|
2230
|
+
keyValue(" Models", String(models.length));
|
|
2231
|
+
keyValue(" Records", String(totalRecords));
|
|
2232
|
+
keyValue(" Indexes", String(indexes.length));
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
/**
|
|
2236
|
+
* Conservative client-side ceiling for `databases import --batch-size`.
|
|
2237
|
+
* `records/batch` has no fixed server-side item cap (unlike the
|
|
2238
|
+
* registered-batch path used by `import-csv`), so this bounds each request to
|
|
2239
|
+
* keep it within Cloudflare body-size and Durable Object memory limits.
|
|
2240
|
+
*/
|
|
2241
|
+
const IMPORT_BATCH_SIZE_CEILING = 25000;
|
|
2242
|
+
async function importSingleDatabase(client, appId, dbDir, options) {
|
|
2243
|
+
const { overwrite, dryRun, batchSize, stopOnError, json: jsonOutput, summary } = options;
|
|
2244
|
+
const metadataPath = path.join(dbDir, "metadata.json");
|
|
2245
|
+
if (!existsSync(metadataPath)) {
|
|
2246
|
+
warn(`Skipping ${dbDir}: no metadata.json found`);
|
|
2247
|
+
summary.skipped++;
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
const metadata = JSON.parse(readFileSync(metadataPath, "utf-8"));
|
|
2251
|
+
const databaseId = metadata.databaseId;
|
|
2252
|
+
if (dryRun) {
|
|
2253
|
+
// Report record/batch counts without writing anything (no DB creation,
|
|
2254
|
+
// no index registration, no batch write).
|
|
2255
|
+
const recordCount = countRecordLines(dbDir);
|
|
2256
|
+
const batchCount = chunk(new Array(recordCount), batchSize).length;
|
|
2257
|
+
summary.created++;
|
|
2258
|
+
summary.recordsImported += recordCount;
|
|
2259
|
+
summary.batches += batchCount;
|
|
2260
|
+
info(`[dry-run] Would import database ${databaseId} (${metadata.title || "untitled"}) — ${recordCount} record(s), ${batchCount} batch(es)`);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
// Check if database type exists — warn if not
|
|
2264
|
+
if (metadata.databaseType) {
|
|
2265
|
+
warn(`Database uses type "${metadata.databaseType}". Ensure the type config exists in the target app (use \`primitive config push\`).`);
|
|
2266
|
+
}
|
|
2267
|
+
// Check if database exists — use the original ID for overwrite scenarios
|
|
2268
|
+
let targetDatabaseId = databaseId;
|
|
2269
|
+
let exists = false;
|
|
2270
|
+
try {
|
|
2271
|
+
await client.getDatabase(appId, databaseId);
|
|
2272
|
+
exists = true;
|
|
2273
|
+
}
|
|
2274
|
+
catch {
|
|
2275
|
+
exists = false;
|
|
2276
|
+
}
|
|
2277
|
+
if (exists && !overwrite) {
|
|
2278
|
+
warn(`Skipping ${databaseId}: already exists (use --overwrite to replace)`);
|
|
2279
|
+
summary.skipped++;
|
|
2280
|
+
return;
|
|
2281
|
+
}
|
|
2282
|
+
// Create database if it doesn't exist
|
|
2283
|
+
// Note: server auto-generates the ID, so we capture it for subsequent operations
|
|
2284
|
+
if (!exists) {
|
|
2285
|
+
const createData = {
|
|
2286
|
+
title: metadata.title || "Imported Database",
|
|
2287
|
+
};
|
|
2288
|
+
if (metadata.databaseType) {
|
|
2289
|
+
createData.databaseType = metadata.databaseType;
|
|
2290
|
+
}
|
|
2291
|
+
if (metadata.metadata) {
|
|
2292
|
+
createData.metadata = metadata.metadata;
|
|
2293
|
+
}
|
|
2294
|
+
const created = await client.createDatabase(appId, createData);
|
|
2295
|
+
targetDatabaseId = created.databaseId || created.id || databaseId;
|
|
2296
|
+
summary.created++;
|
|
2297
|
+
}
|
|
2298
|
+
else {
|
|
2299
|
+
summary.updated++;
|
|
2300
|
+
}
|
|
2301
|
+
// Register indexes
|
|
2302
|
+
const indexesPath = path.join(dbDir, "indexes.json");
|
|
2303
|
+
if (existsSync(indexesPath)) {
|
|
2304
|
+
const indexes = JSON.parse(readFileSync(indexesPath, "utf-8"));
|
|
2305
|
+
for (const idx of indexes) {
|
|
2306
|
+
try {
|
|
2307
|
+
await client.registerDatabaseIndex(appId, targetDatabaseId, {
|
|
2308
|
+
modelName: idx.modelName || idx.model_name,
|
|
2309
|
+
fieldName: idx.fieldName || idx.field_name,
|
|
2310
|
+
fieldType: idx.fieldType || idx.field_type || "string",
|
|
2311
|
+
unique: idx.isUnique || idx.is_unique || false,
|
|
2312
|
+
});
|
|
2313
|
+
summary.indexesRegistered++;
|
|
2314
|
+
}
|
|
2315
|
+
catch {
|
|
2316
|
+
// Index may already exist
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
// Register unique constraints
|
|
2321
|
+
const constraintsPath = path.join(dbDir, "constraints.json");
|
|
2322
|
+
if (existsSync(constraintsPath)) {
|
|
2323
|
+
const constraints = JSON.parse(readFileSync(constraintsPath, "utf-8"));
|
|
2324
|
+
for (const c of constraints) {
|
|
2325
|
+
try {
|
|
2326
|
+
await client.registerDatabaseUniqueConstraint(appId, targetDatabaseId, {
|
|
2327
|
+
modelName: c.modelName || c.model_name,
|
|
2328
|
+
constraintName: c.constraintName || c.constraint_name,
|
|
2329
|
+
fields: c.fields ? (typeof c.fields === "string" ? JSON.parse(c.fields) : c.fields) : [],
|
|
2330
|
+
});
|
|
2331
|
+
}
|
|
2332
|
+
catch {
|
|
2333
|
+
// Constraint may already exist
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
// Import records via chunked batch writes (was one save per record).
|
|
2338
|
+
const recordsPath = path.join(dbDir, "records.jsonl");
|
|
2339
|
+
if (existsSync(recordsPath)) {
|
|
2340
|
+
const lines = readFileSync(recordsPath, "utf-8").split("\n").filter(Boolean);
|
|
2341
|
+
// Map each records.jsonl line to a batch save op. Preserve the explicit
|
|
2342
|
+
// `_type`/`_id`; strip the redundant id/type keys out of `_data` (the
|
|
2343
|
+
// per-record loop did the same) so they don't duplicate into stored data.
|
|
2344
|
+
const ops = lines.map((line) => {
|
|
2345
|
+
const record = JSON.parse(line);
|
|
2346
|
+
const { _type, _id, _data } = record;
|
|
2347
|
+
const { id: _ignoredId, type: _ignoredType, ...data } = _data;
|
|
2348
|
+
return { op: "save", modelName: _type, id: _id, data };
|
|
2349
|
+
});
|
|
2350
|
+
const chunks = chunk(ops, batchSize);
|
|
2351
|
+
summary.batches += chunks.length;
|
|
2352
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2353
|
+
const piece = chunks[i];
|
|
2354
|
+
let chunkFailed = false;
|
|
2355
|
+
try {
|
|
2356
|
+
const res = await client.batchDatabaseRecords(appId, targetDatabaseId, piece);
|
|
2357
|
+
const results = res?.results ?? [];
|
|
2358
|
+
let imported = 0;
|
|
2359
|
+
let failed = 0;
|
|
2360
|
+
const firstErrors = [];
|
|
2361
|
+
for (let j = 0; j < piece.length; j++) {
|
|
2362
|
+
const r = results[j];
|
|
2363
|
+
if (r && r.success) {
|
|
2364
|
+
imported++;
|
|
2365
|
+
}
|
|
2366
|
+
else {
|
|
2367
|
+
failed++;
|
|
2368
|
+
if (r?.error && firstErrors.length < 3)
|
|
2369
|
+
firstErrors.push(r.error);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
summary.recordsImported += imported;
|
|
2373
|
+
summary.recordsFailed += failed;
|
|
2374
|
+
if (failed > 0) {
|
|
2375
|
+
chunkFailed = true;
|
|
2376
|
+
summary.batchErrors.push({
|
|
2377
|
+
database: databaseId,
|
|
2378
|
+
batch: i,
|
|
2379
|
+
error: `${failed} record(s) failed${firstErrors.length ? `: ${firstErrors.join("; ")}` : ""}`,
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
if (!jsonOutput) {
|
|
2383
|
+
info(` ${databaseId} batch ${i + 1}/${chunks.length}: imported ${imported}, failed ${failed}`);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
catch (err) {
|
|
2387
|
+
// Chunk-level failure (e.g. a 400 for a malformed op): the whole chunk
|
|
2388
|
+
// is rejected, so every item in it counts as failed.
|
|
2389
|
+
const msg = err?.message || String(err);
|
|
2390
|
+
chunkFailed = true;
|
|
2391
|
+
summary.recordsFailed += piece.length;
|
|
2392
|
+
summary.batchErrors.push({ database: databaseId, batch: i, error: msg });
|
|
2393
|
+
if (!jsonOutput) {
|
|
2394
|
+
warn(` ${databaseId} batch ${i + 1}/${chunks.length} failed: ${msg}`);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
if (chunkFailed && stopOnError) {
|
|
2398
|
+
summary.aborted = true;
|
|
2399
|
+
return;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
info(`Imported database ${databaseId} (${metadata.title || "untitled"})`);
|
|
2404
|
+
}
|
|
2405
|
+
/** Count non-empty lines in a database export's records.jsonl (0 if absent). */
|
|
2406
|
+
function countRecordLines(dbDir) {
|
|
2407
|
+
const recordsPath = path.join(dbDir, "records.jsonl");
|
|
2408
|
+
if (!existsSync(recordsPath))
|
|
2409
|
+
return 0;
|
|
2410
|
+
return readFileSync(recordsPath, "utf-8").split("\n").filter(Boolean).length;
|
|
767
2411
|
}
|
|
768
2412
|
//# sourceMappingURL=databases.js.map
|