primitive-admin 1.1.0-alpha.7 → 1.1.0-alpha.71
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 +404 -80
- package/assets/skill/skills/primitive-platform/SKILL.md +808 -0
- package/dist/bin/primitive.d.ts +2 -0
- package/dist/bin/primitive.js +294 -21
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.d.ts +2 -0
- package/dist/src/commands/admins.js +138 -19
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.d.ts +2 -0
- package/dist/src/commands/analytics.js +544 -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 +51 -96
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth.d.ts +2 -0
- package/dist/src/commands/auth.js +177 -7
- 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 +330 -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 +37 -38
- 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 +92 -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 +565 -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 +479 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/connections.d.ts +2 -0
- package/dist/src/commands/connections.js +100 -0
- package/dist/src/commands/connections.js.map +1 -0
- package/dist/src/commands/cron-triggers.d.ts +2 -0
- package/dist/src/commands/cron-triggers.js +265 -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 +171 -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 +2140 -112
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +2 -0
- package/dist/src/commands/documents.js +1357 -19
- 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 +174 -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 +333 -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 +116 -0
- package/dist/src/commands/feature-flags.js.map +1 -0
- package/dist/src/commands/group-type-configs.d.ts +2 -0
- package/dist/src/commands/group-type-configs.js +86 -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 +38 -99
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.d.ts +223 -0
- package/dist/src/commands/guides.js +617 -65
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/init.d.ts +25 -0
- package/dist/src/commands/init.js +1605 -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 +380 -178
- 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 +160 -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 +112 -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 +281 -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 +225 -584
- 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 +272 -0
- package/dist/src/commands/rule-sets.js.map +1 -0
- package/dist/src/commands/scripts.d.ts +20 -0
- package/dist/src/commands/scripts.js +554 -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 +108 -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 +75 -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 +330 -0
- package/dist/src/commands/sync-app-settings.js.map +1 -0
- package/dist/src/commands/sync.d.ts +2323 -0
- package/dist/src/commands/sync.js +15065 -843
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.d.ts +2 -0
- package/dist/src/commands/tokens.js +130 -21
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.d.ts +2 -0
- package/dist/src/commands/users.js +532 -23
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.d.ts +8 -0
- package/dist/src/commands/vars.js +96 -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 +10 -10
- package/dist/src/commands/waitlist.js.map +1 -1
- package/dist/src/commands/webhooks.d.ts +2 -0
- package/dist/src/commands/webhooks.js +562 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/workflows.d.ts +116 -0
- package/dist/src/commands/workflows.js +1583 -681
- 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 +1936 -0
- package/dist/src/lib/api-client.js +1826 -138
- 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 +575 -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/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 +68 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +168 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
- package/dist/src/lib/config-object-descriptor.d.ts +127 -0
- package/dist/src/lib/config-object-descriptor.js +658 -0
- package/dist/src/lib/config-object-descriptor.js.map +1 -0
- package/dist/src/lib/config-payload.d.ts +85 -0
- package/dist/src/lib/config-payload.js +116 -0
- package/dist/src/lib/config-payload.js.map +1 -0
- package/dist/src/lib/config-surface.d.ts +130 -0
- package/dist/src/lib/config-surface.js +300 -0
- package/dist/src/lib/config-surface.js.map +1 -0
- package/dist/src/lib/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 +53 -0
- package/dist/src/lib/config.js +92 -53
- 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 +89 -0
- package/dist/src/lib/credentials-store.js +330 -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 +517 -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/env-resolver-core.d.ts +147 -0
- package/dist/src/lib/env-resolver-core.js +265 -0
- package/dist/src/lib/env-resolver-core.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +84 -0
- package/dist/src/lib/env-resolver.js +133 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/fetch.d.ts +5 -0
- package/dist/src/lib/generated-allowlist.d.ts +28 -0
- package/dist/src/lib/generated-allowlist.js +277 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -0
- package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
- package/dist/src/lib/generated-config-surfaces.js +4058 -0
- package/dist/src/lib/generated-config-surfaces.js.map +1 -0
- package/dist/src/lib/generated-template-lint.d.ts +212 -0
- package/dist/src/lib/generated-template-lint.js +624 -0
- package/dist/src/lib/generated-template-lint.js.map +1 -0
- package/dist/src/lib/init-adopt.d.ts +16 -0
- package/dist/src/lib/init-adopt.js +34 -0
- package/dist/src/lib/init-adopt.js.map +1 -0
- package/dist/src/lib/init-assets.d.ts +39 -0
- package/dist/src/lib/init-assets.js +97 -0
- package/dist/src/lib/init-assets.js.map +1 -0
- package/dist/src/lib/init-config.d.ts +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-ios-links.d.ts +50 -0
- package/dist/src/lib/init-ios-links.js +153 -0
- package/dist/src/lib/init-ios-links.js.map +1 -0
- package/dist/src/lib/init-plan.d.ts +80 -0
- package/dist/src/lib/init-plan.js +95 -0
- package/dist/src/lib/init-plan.js.map +1 -0
- package/dist/src/lib/init-production-env.d.ts +48 -0
- package/dist/src/lib/init-production-env.js +59 -0
- package/dist/src/lib/init-production-env.js.map +1 -0
- package/dist/src/lib/init-schema.d.ts +74 -0
- package/dist/src/lib/init-schema.js +358 -0
- package/dist/src/lib/init-schema.js.map +1 -0
- package/dist/src/lib/init-xcode.d.ts +33 -0
- package/dist/src/lib/init-xcode.js +114 -0
- package/dist/src/lib/init-xcode.js.map +1 -0
- package/dist/src/lib/integration-request-config.d.ts +30 -0
- package/dist/src/lib/integration-request-config.js +145 -0
- package/dist/src/lib/integration-request-config.js.map +1 -0
- package/dist/src/lib/local-state.d.ts +55 -0
- package/dist/src/lib/local-state.js +167 -0
- package/dist/src/lib/local-state.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +568 -0
- package/dist/src/lib/log-inspection.js +639 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +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 +109 -0
- package/dist/src/lib/output.js +191 -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 +83 -0
- package/dist/src/lib/paginate.js +95 -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 +97 -0
- package/dist/src/lib/project-config.js +217 -0
- package/dist/src/lib/project-config.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 +65 -0
- package/dist/src/lib/refresh-admin-credentials.js +103 -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-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 +25 -0
- package/dist/src/lib/skill-installer.js +266 -0
- package/dist/src/lib/skill-installer.js.map +1 -0
- package/dist/src/lib/snapshots.d.ts +99 -0
- package/dist/src/lib/snapshots.js +357 -0
- package/dist/src/lib/snapshots.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +113 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js +914 -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/generator.d.ts +94 -0
- package/dist/src/lib/swift-codegen/generator.js +440 -0
- package/dist/src/lib/swift-codegen/generator.js.map +1 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +72 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js +644 -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 +111 -0
- package/dist/src/lib/sync-paths.js +198 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/sync-resource-types.d.ts +544 -0
- package/dist/src/lib/sync-resource-types.js +975 -0
- package/dist/src/lib/sync-resource-types.js.map +1 -0
- package/dist/src/lib/sync-selectors.d.ts +95 -0
- package/dist/src/lib/sync-selectors.js +228 -0
- package/dist/src/lib/sync-selectors.js.map +1 -0
- package/dist/src/lib/template.d.ts +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-keys.d.ts +29 -0
- package/dist/src/lib/test-case-keys.js +55 -0
- package/dist/src/lib/test-case-keys.js.map +1 -0
- package/dist/src/lib/test-case-variables.d.ts +15 -0
- package/dist/src/lib/test-case-variables.js +29 -0
- package/dist/src/lib/test-case-variables.js.map +1 -0
- package/dist/src/lib/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 +527 -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/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 +202 -0
- package/dist/src/lib/workflow-toml-validator.js +757 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- package/dist/src/types/index.d.ts +581 -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 +32 -8
|
@@ -1,15 +1,880 @@
|
|
|
1
1
|
import { ApiClient } from "../lib/api-client.js";
|
|
2
2
|
import { resolveAppId } from "../lib/config.js";
|
|
3
|
-
import { success, error, info, formatTable, formatDate, json, } from "../lib/output.js";
|
|
3
|
+
import { success, error, info, formatTable, formatId, formatDate, json, keyValue, result as printResult, warn, } from "../lib/output.js";
|
|
4
|
+
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
5
|
+
import { parseDataOption } from "../lib/data-input.js";
|
|
6
|
+
import { parseFilterOptions } from "../lib/record-filter.js";
|
|
7
|
+
import { ulid } from "ulid";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as path from "path";
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a `--owner` value to a userId. A value containing "@" is looked up
|
|
12
|
+
* by exact email through the member-accessible `users/lookup` endpoint
|
|
13
|
+
* (issue #2763); anything else is already a user id and is passed through.
|
|
14
|
+
* Failures throw with the server's own message — a permission problem must
|
|
15
|
+
* not read as "no such user", which is how the old admin-only lookup
|
|
16
|
+
* reported it.
|
|
17
|
+
*/
|
|
18
|
+
async function resolveOwnerUserId(client, appId, owner) {
|
|
19
|
+
if (!owner.includes("@"))
|
|
20
|
+
return owner;
|
|
21
|
+
const lookup = await client.lookupUserByEmail(appId, owner);
|
|
22
|
+
if (!lookup?.exists || !lookup.user) {
|
|
23
|
+
throw new Error(`No app user found with email "${owner}" in app ${appId}.`);
|
|
24
|
+
}
|
|
25
|
+
info(`Resolved owner email to userId: ${lookup.user.userId}`);
|
|
26
|
+
return lookup.user.userId;
|
|
27
|
+
}
|
|
4
28
|
export function registerDocumentsCommands(program) {
|
|
5
29
|
const documents = program
|
|
6
30
|
.command("documents")
|
|
7
31
|
.description("Manage documents within an app")
|
|
8
32
|
.addHelpText("after", `
|
|
9
33
|
Examples:
|
|
34
|
+
$ primitive documents list --user-id <user-id>
|
|
35
|
+
$ primitive documents get <document-id>
|
|
36
|
+
$ primitive documents create "My Doc"
|
|
37
|
+
$ primitive documents create "My Doc" --owner user@example.com
|
|
38
|
+
$ primitive documents delete <document-id> -y
|
|
39
|
+
$ primitive documents permissions list <document-id>
|
|
40
|
+
$ primitive documents permissions grant <document-id> --user-id <user-id> --permission reader
|
|
41
|
+
$ primitive documents permissions revoke <document-id> <user-id>
|
|
42
|
+
$ primitive documents records models <document-id>
|
|
43
|
+
$ primitive documents records describe <document-id> <model-name>
|
|
44
|
+
$ primitive documents records query <document-id> <model-name> --filter '{"status":"open"}'
|
|
45
|
+
$ primitive documents records count <document-id> <model-name>
|
|
46
|
+
$ primitive documents records save <document-id> <model-name> --data '{"qty":10}'
|
|
47
|
+
$ primitive documents records patch <document-id> <model-name> <record-id> --data '{"qty":11}'
|
|
48
|
+
$ primitive documents records delete <document-id> <model-name> <record-id> -y
|
|
49
|
+
$ primitive documents records aggregate <document-id> <model-name> --op avg --field price
|
|
50
|
+
$ primitive documents records bulk <document-id> --data-file ops.json -y
|
|
51
|
+
$ primitive documents dump <document-id>
|
|
52
|
+
$ primitive documents stats <document-id>
|
|
10
53
|
$ primitive documents transfer-owner <document-id> <new-owner-id>
|
|
11
54
|
$ primitive documents group-permissions list <document-id>
|
|
12
55
|
`);
|
|
56
|
+
// ---- List a user's documents ----
|
|
57
|
+
// No app-wide enumeration: `--user-id` is required (the admin token has no
|
|
58
|
+
// "self"). Reads the per-user permission partition via the admin endpoint.
|
|
59
|
+
documents
|
|
60
|
+
.command("list")
|
|
61
|
+
.description("List a user's documents")
|
|
62
|
+
.requiredOption("--user-id <id>", "User ID whose documents to list")
|
|
63
|
+
.option("--app <app-id>", "App ID")
|
|
64
|
+
.option("--json", "Output as JSON")
|
|
65
|
+
.action(async (options) => {
|
|
66
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
67
|
+
const client = new ApiClient();
|
|
68
|
+
try {
|
|
69
|
+
const list = await client.listAdminDocuments(resolvedAppId, options.userId);
|
|
70
|
+
if (options.json) {
|
|
71
|
+
json(list);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (list.length === 0) {
|
|
75
|
+
info("No documents found.");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
console.log(formatTable(list, [
|
|
79
|
+
{ header: "DOCUMENT_ID", key: "documentId", format: formatId },
|
|
80
|
+
{ header: "TITLE", key: "title", flex: true },
|
|
81
|
+
{ header: "PERMISSION", key: "permission" },
|
|
82
|
+
{ header: "GRANTED", key: "grantedAt", format: formatDate },
|
|
83
|
+
]));
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
error(err.message);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
// ---- Get a single document's metadata ----
|
|
91
|
+
documents
|
|
92
|
+
.command("get")
|
|
93
|
+
.description("Show a document's metadata and the caller's access to it")
|
|
94
|
+
.argument("<document-id>", "Document ID")
|
|
95
|
+
.option("--app <app-id>", "App ID")
|
|
96
|
+
.option("--json", "Output as JSON")
|
|
97
|
+
.action(async (documentId, options) => {
|
|
98
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
99
|
+
const client = new ApiClient();
|
|
100
|
+
try {
|
|
101
|
+
const doc = await client.getDocument(resolvedAppId, documentId);
|
|
102
|
+
if (options.json) {
|
|
103
|
+
json(doc);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
keyValue("Document ID", doc.documentId);
|
|
107
|
+
keyValue("Title", doc.title);
|
|
108
|
+
keyValue("Created By", doc.createdBy);
|
|
109
|
+
keyValue("Created", formatDate(doc.createdAt));
|
|
110
|
+
keyValue("Modified", formatDate(doc.modifiedAt ?? doc.lastModified));
|
|
111
|
+
// `GET documents/:id` reports the caller's own access, not the
|
|
112
|
+
// document's grant/alias/collection inventory — printing counts for
|
|
113
|
+
// fields the endpoint never returns read as a definitive "0" for
|
|
114
|
+
// documents that demonstrably have them. Use
|
|
115
|
+
// `documents permissions list` for the grant list.
|
|
116
|
+
if (doc.permission)
|
|
117
|
+
keyValue("Your Permission", doc.permission);
|
|
118
|
+
if (doc.accessSource)
|
|
119
|
+
keyValue("Access Source", doc.accessSource);
|
|
120
|
+
if (doc.linkAccess)
|
|
121
|
+
keyValue("Link Access", doc.linkAccess);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
error(err.message);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
// ---- Create a document ----
|
|
129
|
+
// The same `POST /documents` every client uses (#2763). Who ends up owning
|
|
130
|
+
// the document is the server's call, decided from the token's identity —
|
|
131
|
+
// the help below states that matrix rather than the CLI guessing at a role.
|
|
132
|
+
documents
|
|
133
|
+
.command("create")
|
|
134
|
+
.description("Create a document")
|
|
135
|
+
.argument("<title>", "Document title")
|
|
136
|
+
.option("--app <app-id>", "App ID")
|
|
137
|
+
.option("--owner <userId-or-email>", "User ID or email to own the document (super-admin or assigned-console-admin tokens only)")
|
|
138
|
+
.option("--json", "Output as JSON")
|
|
139
|
+
.addHelpText("after", `
|
|
140
|
+
Ownership:
|
|
141
|
+
An app-user token — member, admin, or owner — always creates the document
|
|
142
|
+
owned by the caller: the server ignores --owner for those tokens.
|
|
143
|
+
A super-admin token, or a console-admin token assigned to this app, acts
|
|
144
|
+
through an admin shadow app user — without --owner that shadow user owns the
|
|
145
|
+
document, with --owner the named user does.
|
|
146
|
+
A console admin not assigned to this app has no access to the app at all.
|
|
147
|
+
|
|
148
|
+
An email given to --owner is resolved to a user id before anything is
|
|
149
|
+
created; a user who is not in the app fails the command without creating a
|
|
150
|
+
document.
|
|
151
|
+
`)
|
|
152
|
+
.action(async (title, options) => {
|
|
153
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
154
|
+
const client = new ApiClient();
|
|
155
|
+
let ownerUserId;
|
|
156
|
+
if (options.owner) {
|
|
157
|
+
try {
|
|
158
|
+
ownerUserId = await resolveOwnerUserId(client, resolvedAppId, options.owner);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
error(err.message);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const result = await client.createDocument(resolvedAppId, {
|
|
167
|
+
title,
|
|
168
|
+
...(ownerUserId ? { createdBy: ownerUserId } : {}),
|
|
169
|
+
});
|
|
170
|
+
if (options.json) {
|
|
171
|
+
json(result);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
success("Document created.");
|
|
175
|
+
keyValue("Document ID", result.documentId);
|
|
176
|
+
keyValue("Title", result.title);
|
|
177
|
+
keyValue("Owner", result.createdBy);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
error(err.message);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
// ---- Delete a document ----
|
|
185
|
+
// The runtime-resource delete verb (#2756): the server endpoint cascades
|
|
186
|
+
// through records, Yjs history, blobs, aliases and permissions, so an
|
|
187
|
+
// operator resetting an app's data no longer needs a one-off SDK script.
|
|
188
|
+
// Who may run it is the server's decision, so the help below states that
|
|
189
|
+
// model (#2763) rather than leaving it to be discovered from a 403.
|
|
190
|
+
documents
|
|
191
|
+
.command("delete")
|
|
192
|
+
.description("Delete a document and all its data (records, history, blobs, aliases, permissions)")
|
|
193
|
+
.argument("<document-id>", "Document ID")
|
|
194
|
+
.option("--app <app-id>", "App ID")
|
|
195
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
196
|
+
.option("--json", "Output as JSON")
|
|
197
|
+
.addHelpText("after", `
|
|
198
|
+
Permissions (enforced by the server, not the CLI):
|
|
199
|
+
The document's owner, the app owner, and super-admin or assigned-console-admin
|
|
200
|
+
tokens — which act with app-owner authority — delete directly.
|
|
201
|
+
Everyone else, including app-role admins, can delete only when a containing
|
|
202
|
+
collection's document.delete rule allows it: an app admin cannot delete a
|
|
203
|
+
standalone document (one in no collection) they do not own.
|
|
204
|
+
The app's root document cannot be deleted.
|
|
205
|
+
`)
|
|
206
|
+
.action(async (documentId, options) => {
|
|
207
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
208
|
+
if (!options.yes) {
|
|
209
|
+
let confirm;
|
|
210
|
+
try {
|
|
211
|
+
confirm = await confirmPrompt(`Delete document ${documentId}? This permanently deletes its records, ` +
|
|
212
|
+
`update history, blobs, aliases, and permissions. This cannot be undone.`);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
error(err.message);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
if (!confirm) {
|
|
219
|
+
info("Cancelled.");
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const client = new ApiClient();
|
|
224
|
+
try {
|
|
225
|
+
const result = await client.deleteDocument(resolvedAppId, documentId);
|
|
226
|
+
if (options.json) {
|
|
227
|
+
json(result);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
success(`Document ${documentId} deleted.`);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
// Server refusals (404 not found, 403 root document / owner-only, the
|
|
234
|
+
// non-owner fail-closed 503) are printed verbatim, never swallowed.
|
|
235
|
+
error(err.message);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
// ---- User-level permissions subcommand group ----
|
|
240
|
+
const permissions = documents
|
|
241
|
+
.command("permissions")
|
|
242
|
+
.description("Manage user-level permissions on a document");
|
|
243
|
+
// List user-level permissions
|
|
244
|
+
permissions
|
|
245
|
+
.command("list")
|
|
246
|
+
.description("List user-level permissions for a document")
|
|
247
|
+
.argument("<document-id>", "Document ID")
|
|
248
|
+
.option("--app <app-id>", "App ID")
|
|
249
|
+
.option("--json", "Output as JSON")
|
|
250
|
+
.action(async (documentId, options) => {
|
|
251
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
252
|
+
const client = new ApiClient();
|
|
253
|
+
try {
|
|
254
|
+
const result = await client.listDocumentPermissions(resolvedAppId, documentId);
|
|
255
|
+
const list = Array.isArray(result) ? result : result?.permissions ?? [];
|
|
256
|
+
if (options.json) {
|
|
257
|
+
json(list);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (list.length === 0) {
|
|
261
|
+
info("No permissions found.");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
console.log(formatTable(list, [
|
|
265
|
+
{ header: "USER_ID", key: "userId", format: formatId },
|
|
266
|
+
{ header: "EMAIL", key: "email", flex: true },
|
|
267
|
+
{ header: "PERMISSION", key: "permission" },
|
|
268
|
+
{ header: "GRANTED", key: "grantedAt", format: formatDate },
|
|
269
|
+
]));
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
error(err.message);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
// Grant (or update) a user-level permission
|
|
277
|
+
permissions
|
|
278
|
+
.command("grant")
|
|
279
|
+
.description("Grant or update a user's permission on a document")
|
|
280
|
+
.argument("<document-id>", "Document ID")
|
|
281
|
+
.option("--user-id <id>", "User ID to grant the permission to")
|
|
282
|
+
.option("--email <email>", "Email of the user to grant the permission to")
|
|
283
|
+
.requiredOption("--permission <permission>", "Permission level: reader or read-write")
|
|
284
|
+
.option("--app <app-id>", "App ID")
|
|
285
|
+
.option("--json", "Output as JSON")
|
|
286
|
+
.action(async (documentId, options) => {
|
|
287
|
+
if (!options.userId && !options.email) {
|
|
288
|
+
error("Either --user-id or --email is required.");
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
if (options.userId && options.email) {
|
|
292
|
+
error("Provide only one of --user-id or --email, not both.");
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
if (!["reader", "read-write"].includes(options.permission)) {
|
|
296
|
+
error("Invalid --permission. Must be 'reader' or 'read-write'.");
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
300
|
+
const client = new ApiClient();
|
|
301
|
+
try {
|
|
302
|
+
const entry = options.userId
|
|
303
|
+
? { userId: options.userId, permission: options.permission }
|
|
304
|
+
: { email: options.email, permission: options.permission };
|
|
305
|
+
const result = await client.grantDocumentPermission(resolvedAppId, documentId, [entry]);
|
|
306
|
+
if (options.json) {
|
|
307
|
+
json(result);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const target = options.userId ? options.userId : options.email;
|
|
311
|
+
success(`Permission '${options.permission}' granted to ${target}.`);
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
error(err.message);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
// Revoke a user-level permission
|
|
319
|
+
permissions
|
|
320
|
+
.command("revoke")
|
|
321
|
+
.description("Revoke a user's permission on a document")
|
|
322
|
+
.argument("<document-id>", "Document ID")
|
|
323
|
+
.argument("[user-id]", "User ID to revoke (or use --email)")
|
|
324
|
+
.option("--email <email>", "Email of the user to revoke (alternative to <user-id>)")
|
|
325
|
+
.option("--app <app-id>", "App ID")
|
|
326
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
327
|
+
.action(async (documentId, userId, options) => {
|
|
328
|
+
if (!userId && !options.email) {
|
|
329
|
+
error("Either a <user-id> argument or --email is required.");
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
if (userId && options.email) {
|
|
333
|
+
error("Provide only one of <user-id> or --email, not both.");
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
const target = userId ?? options.email;
|
|
337
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
338
|
+
if (!options.yes) {
|
|
339
|
+
let confirm;
|
|
340
|
+
try {
|
|
341
|
+
confirm = await confirmPrompt(`Revoke permission for ${target} on document ${documentId}?`);
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
error(err.message);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
if (!confirm) {
|
|
348
|
+
info("Cancelled.");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
const client = new ApiClient();
|
|
353
|
+
try {
|
|
354
|
+
await client.revokeDocumentPermission(resolvedAppId, documentId, {
|
|
355
|
+
userId,
|
|
356
|
+
email: options.email,
|
|
357
|
+
});
|
|
358
|
+
success(`Permission revoked for ${target}.`);
|
|
359
|
+
}
|
|
360
|
+
catch (err) {
|
|
361
|
+
error(err.message);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
// ---- Records introspection subcommand group ----
|
|
366
|
+
const records = documents
|
|
367
|
+
.command("records")
|
|
368
|
+
.description("Inspect a document's models and records");
|
|
369
|
+
// List model names
|
|
370
|
+
records
|
|
371
|
+
.command("models")
|
|
372
|
+
.description("List model names in a document")
|
|
373
|
+
.argument("<document-id>", "Document ID")
|
|
374
|
+
.option("--app <app-id>", "App ID")
|
|
375
|
+
.option("--json", "Output as JSON")
|
|
376
|
+
.action(async (documentId, options) => {
|
|
377
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
378
|
+
const client = new ApiClient();
|
|
379
|
+
try {
|
|
380
|
+
const result = await client.getDocumentSchema(resolvedAppId, documentId);
|
|
381
|
+
const models = result?.modelNames ?? Object.keys(result?.schema?.models ?? {});
|
|
382
|
+
if (options.json) {
|
|
383
|
+
json(models);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (models.length === 0) {
|
|
387
|
+
info("No models found.");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
for (const m of models) {
|
|
391
|
+
console.log(` ${m}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
error(err.message);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
// Describe a model's fields and indexes
|
|
400
|
+
records
|
|
401
|
+
.command("describe")
|
|
402
|
+
.description("Show a model's fields and indexes")
|
|
403
|
+
.argument("<document-id>", "Document ID")
|
|
404
|
+
.argument("<model-name>", "Model name")
|
|
405
|
+
.option("--app <app-id>", "App ID")
|
|
406
|
+
.option("--json", "Output as JSON")
|
|
407
|
+
.action(async (documentId, modelName, options) => {
|
|
408
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
409
|
+
const client = new ApiClient();
|
|
410
|
+
try {
|
|
411
|
+
const result = await client.getDocumentSchema(resolvedAppId, documentId);
|
|
412
|
+
const model = result?.schema?.models?.[modelName];
|
|
413
|
+
if (!model) {
|
|
414
|
+
if (options.json) {
|
|
415
|
+
json(null);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
info(`Model '${modelName}' not found in this document.`);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const fieldsObj = model.fields ?? {};
|
|
422
|
+
const rows = Object.entries(fieldsObj).map(([name, meta]) => ({
|
|
423
|
+
field: name,
|
|
424
|
+
type: meta?.type ?? "unknown",
|
|
425
|
+
indexed: meta?.indexed ? "yes" : "",
|
|
426
|
+
unique: meta?.unique ? "yes" : "",
|
|
427
|
+
required: meta?.required ? "yes" : "",
|
|
428
|
+
}));
|
|
429
|
+
if (options.json) {
|
|
430
|
+
json({ fields: fieldsObj, constraints: model.constraints ?? {} });
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (rows.length === 0) {
|
|
434
|
+
info("No fields detected for this model.");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
console.log(formatTable(rows, [
|
|
438
|
+
{ header: "FIELD", key: "field", flex: true },
|
|
439
|
+
{ header: "TYPE", key: "type" },
|
|
440
|
+
{ header: "INDEXED", key: "indexed" },
|
|
441
|
+
{ header: "UNIQUE", key: "unique" },
|
|
442
|
+
{ header: "REQUIRED", key: "required" },
|
|
443
|
+
]));
|
|
444
|
+
}
|
|
445
|
+
catch (err) {
|
|
446
|
+
error(err.message);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
// Query records in a document model
|
|
451
|
+
records
|
|
452
|
+
.command("query")
|
|
453
|
+
.description("Query records in a document model")
|
|
454
|
+
.argument("<document-id>", "Document ID")
|
|
455
|
+
.argument("<model-name>", "Model name to query")
|
|
456
|
+
.option("--app <app-id>", "App ID")
|
|
457
|
+
.option("--filter <json>", "Filter as JSON (e.g. '{\"status\":\"open\"}')")
|
|
458
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
459
|
+
.option("--limit <n>", "Maximum number of records to return (max 100)", parseInt)
|
|
460
|
+
.option("--cursor <cursor>", "Pagination cursor from a previous query")
|
|
461
|
+
.option("--json", "Output as JSON")
|
|
462
|
+
.action(async (documentId, modelName, options) => {
|
|
463
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
464
|
+
const filter = parseFilterOptions(options);
|
|
465
|
+
const client = new ApiClient();
|
|
466
|
+
try {
|
|
467
|
+
const result = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { filter, limit: options.limit, cursor: options.cursor });
|
|
468
|
+
if (options.json) {
|
|
469
|
+
json(result);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const items = result.items;
|
|
473
|
+
if (items.length === 0) {
|
|
474
|
+
info("No records found.");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const allKeys = new Set();
|
|
478
|
+
for (const rec of items) {
|
|
479
|
+
for (const key of Object.keys(rec))
|
|
480
|
+
allKeys.add(key);
|
|
481
|
+
}
|
|
482
|
+
const orderedKeys = [];
|
|
483
|
+
if (allKeys.has("id")) {
|
|
484
|
+
orderedKeys.push("id");
|
|
485
|
+
allKeys.delete("id");
|
|
486
|
+
}
|
|
487
|
+
orderedKeys.push(...[...allKeys].sort());
|
|
488
|
+
const columns = orderedKeys.map((key) => ({
|
|
489
|
+
header: key.toUpperCase(),
|
|
490
|
+
key,
|
|
491
|
+
format: (v) => {
|
|
492
|
+
if (v === null || v === undefined)
|
|
493
|
+
return "—";
|
|
494
|
+
if (typeof v === "object")
|
|
495
|
+
return JSON.stringify(v);
|
|
496
|
+
return String(v);
|
|
497
|
+
},
|
|
498
|
+
}));
|
|
499
|
+
console.log(formatTable(items, columns));
|
|
500
|
+
if (result.nextCursor) {
|
|
501
|
+
info(`More results available. Use --cursor ${result.nextCursor}`);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
error(err.message);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
// Get a single record by id (mirrors `databases records get`, issue #2357).
|
|
510
|
+
records
|
|
511
|
+
.command("get")
|
|
512
|
+
.description("Get a single record by id from a document model")
|
|
513
|
+
.argument("<document-id>", "Document ID")
|
|
514
|
+
.argument("<model-name>", "Model name")
|
|
515
|
+
.argument("<record-id>", "Record ID")
|
|
516
|
+
.option("--app <app-id>", "App ID")
|
|
517
|
+
.option("--json", "Output as JSON")
|
|
518
|
+
.action(async (documentId, modelName, recordId, options) => {
|
|
519
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
520
|
+
const client = new ApiClient();
|
|
521
|
+
try {
|
|
522
|
+
// A single-record fetch is a query filtered by primary-key id (there is
|
|
523
|
+
// no dedicated get endpoint — reuse queryDocumentRecords, exactly as
|
|
524
|
+
// the `databases records get` twin reuses queryDatabaseRecords).
|
|
525
|
+
const result = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { filter: { id: recordId }, limit: 1 });
|
|
526
|
+
const record = result.items[0];
|
|
527
|
+
if (options.json) {
|
|
528
|
+
json(record ?? null);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
if (!record) {
|
|
532
|
+
info(`No record found with id ${recordId} in ${modelName}.`);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
// `get` is a data command, so its fields go to stdout via `result()`
|
|
536
|
+
// — the #711 split (`result()` = the data the caller asked for,
|
|
537
|
+
// `keyValue()` = a post-action diagnostic summary). Issue #2437; the
|
|
538
|
+
// databases twin renders identically.
|
|
539
|
+
for (const [key, value] of Object.entries(record)) {
|
|
540
|
+
const rendered = value === null || value === undefined
|
|
541
|
+
? "—"
|
|
542
|
+
: typeof value === "object"
|
|
543
|
+
? JSON.stringify(value)
|
|
544
|
+
: String(value);
|
|
545
|
+
printResult(key, rendered);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
error(err.message);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
// Count records in a document model
|
|
554
|
+
records
|
|
555
|
+
.command("count")
|
|
556
|
+
.description("Count records in a document model")
|
|
557
|
+
.argument("<document-id>", "Document ID")
|
|
558
|
+
.argument("<model-name>", "Model name to count")
|
|
559
|
+
.option("--app <app-id>", "App ID")
|
|
560
|
+
.option("--filter <json>", "Filter as JSON (e.g. '{\"status\":\"open\"}')")
|
|
561
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
562
|
+
.option("--json", "Output as JSON")
|
|
563
|
+
.action(async (documentId, modelName, options) => {
|
|
564
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
565
|
+
const filter = parseFilterOptions(options);
|
|
566
|
+
const client = new ApiClient();
|
|
567
|
+
try {
|
|
568
|
+
const result = await client.countDocumentRecords(resolvedAppId, documentId, modelName, { filter });
|
|
569
|
+
if (options.json) {
|
|
570
|
+
json(result);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
console.log(` ${result.count}`);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
error(err.message);
|
|
577
|
+
process.exit(1);
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
// Aggregate records in a document model (issue #2437). Mirrors
|
|
581
|
+
// `databases records aggregate` flag for flag and rendering for rendering —
|
|
582
|
+
// including the deliberate `keyValue()` for the ungrouped scalar, so the two
|
|
583
|
+
// twins keep printing the same thing.
|
|
584
|
+
const AGGREGATE_OPS = ["count", "sum", "avg", "min", "max"];
|
|
585
|
+
records
|
|
586
|
+
.command("aggregate")
|
|
587
|
+
.description("Aggregate records in a document model (count/sum/avg/min/max, optional group-by)")
|
|
588
|
+
.argument("<document-id>", "Document ID")
|
|
589
|
+
.argument("<model-name>", "Model name")
|
|
590
|
+
.option("--app <app-id>", "App ID")
|
|
591
|
+
.requiredOption("--op <operation>", `Aggregate operation: ${AGGREGATE_OPS.join("|")}`)
|
|
592
|
+
.option("--field <field>", "Field to aggregate (required for sum/avg/min/max)")
|
|
593
|
+
.option("--group-by <field>", "Group results by this plain field (repeatable; StringSet fields are not supported)", (value, previous = []) => previous.concat(value), [])
|
|
594
|
+
.option("--filter <json>", "Filter as JSON")
|
|
595
|
+
.option("--filter-file <path>", "Read filter from a JSON or TOML file")
|
|
596
|
+
.option("--json", "Output as JSON")
|
|
597
|
+
.action(async (documentId, modelName, options) => {
|
|
598
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
599
|
+
if (!AGGREGATE_OPS.includes(options.op)) {
|
|
600
|
+
error(`Invalid --op "${options.op}". Expected one of: ${AGGREGATE_OPS.join(", ")}.`);
|
|
601
|
+
process.exit(1);
|
|
602
|
+
}
|
|
603
|
+
const operation = { type: options.op };
|
|
604
|
+
if (options.op === "count") {
|
|
605
|
+
if (options.field) {
|
|
606
|
+
error("--field is not valid for --op count.");
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
if (!options.field) {
|
|
612
|
+
error(`--op ${options.op} requires --field.`);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
operation.field = options.field;
|
|
616
|
+
}
|
|
617
|
+
const filter = parseFilterOptions(options);
|
|
618
|
+
const groupBy = options.groupBy || [];
|
|
619
|
+
const client = new ApiClient();
|
|
620
|
+
try {
|
|
621
|
+
const response = await client.aggregateDocumentRecords(resolvedAppId, documentId, modelName, { operations: [operation], groupBy, filter });
|
|
622
|
+
if (options.json) {
|
|
623
|
+
json(response);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const aggregated = response?.result;
|
|
627
|
+
// Same labelling rule as the databases twin: the field name is
|
|
628
|
+
// reproduced verbatim, never case-folded (issue #2357).
|
|
629
|
+
const valueLabel = options.op === "count"
|
|
630
|
+
? "COUNT"
|
|
631
|
+
: `${options.op.toUpperCase()}(${options.field})`;
|
|
632
|
+
const scalarLabel = options.op === "count"
|
|
633
|
+
? "count"
|
|
634
|
+
: `${options.op.toLowerCase()}(${options.field})`;
|
|
635
|
+
if (groupBy.length === 0) {
|
|
636
|
+
const value = (aggregated ?? {})[options.op === "count" ? "count" : `${options.op}_${options.field}`];
|
|
637
|
+
keyValue(scalarLabel, value === null || value === undefined ? "—" : String(value));
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
if (aggregated &&
|
|
641
|
+
typeof aggregated === "object" &&
|
|
642
|
+
!Array.isArray(aggregated) &&
|
|
643
|
+
Object.values(aggregated).every((v) => typeof v !== "object" || v === null)) {
|
|
644
|
+
const rows = Object.entries(aggregated).map(([group, value]) => ({
|
|
645
|
+
group,
|
|
646
|
+
value: value === null || value === undefined ? "—" : String(value),
|
|
647
|
+
}));
|
|
648
|
+
console.log(formatTable(rows, [
|
|
649
|
+
{ header: "GROUP", key: "group" },
|
|
650
|
+
{ header: valueLabel, key: "value" },
|
|
651
|
+
]));
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
console.log(JSON.stringify(aggregated ?? {}, null, 2));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
error(err.message);
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
// ---- Record writes (#1964 Phase 4) ----
|
|
663
|
+
// Server-side writes through the document facade: Yjs-first, CRDT-safe,
|
|
664
|
+
// broadcast to connected clients. Requires read-write or higher on the
|
|
665
|
+
// document (or an admin token). `--data` / `--data-file` are validated
|
|
666
|
+
// CLI-side before any request is sent.
|
|
667
|
+
// `--data`/`--data-file` parsing (including the object-shape check and the
|
|
668
|
+
// both-flags rule) is the shared `parseDataOption` helper, so these verbs
|
|
669
|
+
// behave and diagnose identically to `metadata` and `databases records`.
|
|
670
|
+
// Create or replace a record
|
|
671
|
+
records
|
|
672
|
+
.command("save")
|
|
673
|
+
.description("Create or replace a record in a document model")
|
|
674
|
+
.argument("<document-id>", "Document ID")
|
|
675
|
+
.argument("<model-name>", "Model name to write to")
|
|
676
|
+
.option("--app <app-id>", "App ID")
|
|
677
|
+
.option("--id <record-id>", "Record ID (a ULID is generated when omitted)")
|
|
678
|
+
.option("--data <json>", "Record fields as JSON (e.g. '{\"qty\":10}')")
|
|
679
|
+
.option("--data-file <file>", "Read record fields from a JSON file")
|
|
680
|
+
.option("--upsert-on <field>", "Update the record whose <field> matches instead of creating")
|
|
681
|
+
.option("--json", "Output as JSON")
|
|
682
|
+
.action(async (documentId, modelName, options) => {
|
|
683
|
+
const data = parseDataOption(options, "record fields");
|
|
684
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
685
|
+
const recordId = options.id || ulid();
|
|
686
|
+
const writeOptions = options.upsertOn ? { upsertOn: options.upsertOn } : undefined;
|
|
687
|
+
const client = new ApiClient();
|
|
688
|
+
try {
|
|
689
|
+
const result = await client.saveDocumentRecord(resolvedAppId, documentId, modelName, { id: recordId, data, ...(writeOptions ? { options: writeOptions } : {}) });
|
|
690
|
+
if (options.json) {
|
|
691
|
+
json(result);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
success(`Record saved: ${result.record?.id ?? recordId}`);
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
error(err.message);
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
// Merge fields into an existing record
|
|
702
|
+
records
|
|
703
|
+
.command("patch")
|
|
704
|
+
.description("Merge fields into an existing record in a document model")
|
|
705
|
+
.argument("<document-id>", "Document ID")
|
|
706
|
+
.argument("<model-name>", "Model name")
|
|
707
|
+
.argument("<record-id>", "Record ID to patch")
|
|
708
|
+
.option("--app <app-id>", "App ID")
|
|
709
|
+
.option("--data <json>", "Fields to merge as JSON (e.g. '{\"status\":\"closed\"}')")
|
|
710
|
+
.option("--data-file <file>", "Read fields from a JSON file")
|
|
711
|
+
.option("--json", "Output as JSON")
|
|
712
|
+
.action(async (documentId, modelName, recordId, options) => {
|
|
713
|
+
const data = parseDataOption(options, "fields to merge");
|
|
714
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
715
|
+
const client = new ApiClient();
|
|
716
|
+
try {
|
|
717
|
+
const result = await client.patchDocumentRecord(resolvedAppId, documentId, modelName, recordId, { data });
|
|
718
|
+
if (options.json) {
|
|
719
|
+
json(result);
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
success(`Record patched: ${recordId}`);
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
error(err.message);
|
|
726
|
+
process.exit(1);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
// Delete a record
|
|
730
|
+
records
|
|
731
|
+
.command("delete")
|
|
732
|
+
.description("Delete a record from a document model")
|
|
733
|
+
.argument("<document-id>", "Document ID")
|
|
734
|
+
.argument("<model-name>", "Model name")
|
|
735
|
+
.argument("<record-id>", "Record ID to delete")
|
|
736
|
+
.option("--app <app-id>", "App ID")
|
|
737
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
738
|
+
.option("--json", "Output as JSON")
|
|
739
|
+
.action(async (documentId, modelName, recordId, options) => {
|
|
740
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
741
|
+
if (!options.yes) {
|
|
742
|
+
const confirmed = await confirmPrompt(`Delete record ${recordId} from ${modelName} in document ${documentId}?`);
|
|
743
|
+
if (!confirmed) {
|
|
744
|
+
info("Cancelled.");
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const client = new ApiClient();
|
|
749
|
+
try {
|
|
750
|
+
const result = await client.deleteDocumentRecord(resolvedAppId, documentId, modelName, recordId);
|
|
751
|
+
if (options.json) {
|
|
752
|
+
json(result);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
// A delete of a missing record is a silent no-op server-side, so
|
|
756
|
+
// this reports the request outcome, not record existence.
|
|
757
|
+
success(`Record deleted: ${recordId}`);
|
|
758
|
+
}
|
|
759
|
+
catch (err) {
|
|
760
|
+
error(err.message);
|
|
761
|
+
process.exit(1);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
// Apply an atomic multi-model operations blob
|
|
765
|
+
records
|
|
766
|
+
.command("bulk")
|
|
767
|
+
.description("Apply an atomic create/patch/delete operations blob to a document")
|
|
768
|
+
.argument("<document-id>", "Document ID")
|
|
769
|
+
.requiredOption("--data-file <file>", "JSON file with { operations: [...] } or a bare operations array. " +
|
|
770
|
+
"Each op is { model, action: create|patch|delete, id, data, precondition? } " +
|
|
771
|
+
"— `data` holds the record fields and is required (non-empty) on " +
|
|
772
|
+
"create/patch, not allowed on delete; a create `id` must be a 26-char " +
|
|
773
|
+
"uppercase Crockford ULID")
|
|
774
|
+
.option("--app <app-id>", "App ID")
|
|
775
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
776
|
+
.option("--json", "Output as JSON")
|
|
777
|
+
.action(async (documentId, options) => {
|
|
778
|
+
const parsed = parseDataOption(options, "operations blob", {
|
|
779
|
+
allowArray: true,
|
|
780
|
+
});
|
|
781
|
+
const operations = Array.isArray(parsed) ? parsed : parsed?.operations;
|
|
782
|
+
if (!Array.isArray(operations) || operations.length === 0) {
|
|
783
|
+
error("--data-file must contain { operations: [...] } or a non-empty operations array.");
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
787
|
+
if (!options.yes) {
|
|
788
|
+
const confirmed = await confirmPrompt(`Apply ${operations.length} operation(s) atomically to document ${documentId}?`);
|
|
789
|
+
if (!confirmed) {
|
|
790
|
+
info("Cancelled.");
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
const client = new ApiClient();
|
|
795
|
+
try {
|
|
796
|
+
const result = await client.bulkDocumentRecords(resolvedAppId, documentId, operations);
|
|
797
|
+
if (options.json) {
|
|
798
|
+
json(result);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
success(`Applied ${result.applied} operation(s): ` +
|
|
802
|
+
`${result.added.length} added, ${result.updated.length} updated, ${result.deleted} deleted.`);
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
error(err.message);
|
|
806
|
+
process.exit(1);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
// Dump a document's records grouped by model (CLI-side composition)
|
|
810
|
+
documents
|
|
811
|
+
.command("dump")
|
|
812
|
+
.description("Dump a document's records grouped by model as JSON")
|
|
813
|
+
.argument("<document-id>", "Document ID")
|
|
814
|
+
.option("--app <app-id>", "App ID")
|
|
815
|
+
.option("--output <file>", "Write the JSON dump to a file instead of stdout")
|
|
816
|
+
.option("--json", "Output as JSON (default)")
|
|
817
|
+
.action(async (documentId, options) => {
|
|
818
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
819
|
+
const client = new ApiClient();
|
|
820
|
+
try {
|
|
821
|
+
// Model names come from the schema; records are assembled from paged
|
|
822
|
+
// reads per model (not a single atomic snapshot — the paged path is
|
|
823
|
+
// the contract, same pattern as `databases export`).
|
|
824
|
+
const schema = await client.getDocumentSchema(resolvedAppId, documentId);
|
|
825
|
+
const modelNames = schema?.modelNames ?? Object.keys(schema?.schema?.models ?? {});
|
|
826
|
+
const grouped = {};
|
|
827
|
+
for (const modelName of modelNames) {
|
|
828
|
+
const records = [];
|
|
829
|
+
let cursor;
|
|
830
|
+
do {
|
|
831
|
+
const page = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { limit: 100, cursor });
|
|
832
|
+
records.push(...page.items);
|
|
833
|
+
cursor = page.nextCursor;
|
|
834
|
+
} while (cursor);
|
|
835
|
+
grouped[modelName] = records;
|
|
836
|
+
}
|
|
837
|
+
const output = JSON.stringify(grouped, null, 2);
|
|
838
|
+
if (options.output) {
|
|
839
|
+
fs.writeFileSync(options.output, output);
|
|
840
|
+
success(`Document dump written to ${options.output}`);
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
console.log(output);
|
|
844
|
+
}
|
|
845
|
+
catch (err) {
|
|
846
|
+
error(err.message);
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
// Document statistics (platform-vocabulary projection)
|
|
851
|
+
documents
|
|
852
|
+
.command("stats")
|
|
853
|
+
.description("Show a document's record/model/blob counts and approximate size")
|
|
854
|
+
.argument("<document-id>", "Document ID")
|
|
855
|
+
.option("--app <app-id>", "App ID")
|
|
856
|
+
.option("--json", "Output as JSON")
|
|
857
|
+
.action(async (documentId, options) => {
|
|
858
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
859
|
+
const client = new ApiClient();
|
|
860
|
+
try {
|
|
861
|
+
const result = await client.getDocumentStats(resolvedAppId, documentId);
|
|
862
|
+
if (options.json) {
|
|
863
|
+
json(result);
|
|
864
|
+
return;
|
|
865
|
+
}
|
|
866
|
+
keyValue("Document ID", result.documentId);
|
|
867
|
+
keyValue("Records", result.recordCount);
|
|
868
|
+
keyValue("Models", result.modelCount);
|
|
869
|
+
keyValue("Blobs", result.blobCount);
|
|
870
|
+
keyValue("Size (bytes, approx)", result.sizeBytes);
|
|
871
|
+
keyValue("Last modified", result.lastModifiedAt ? formatDate(result.lastModifiedAt) : "—");
|
|
872
|
+
}
|
|
873
|
+
catch (err) {
|
|
874
|
+
error(err.message);
|
|
875
|
+
process.exit(1);
|
|
876
|
+
}
|
|
877
|
+
});
|
|
13
878
|
// Transfer document ownership
|
|
14
879
|
documents
|
|
15
880
|
.command("transfer-owner")
|
|
@@ -43,15 +908,14 @@ Examples:
|
|
|
43
908
|
process.exit(1);
|
|
44
909
|
}
|
|
45
910
|
if (!options.yes) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
]);
|
|
911
|
+
let confirm;
|
|
912
|
+
try {
|
|
913
|
+
confirm = await confirmPrompt(`Transfer ownership of document ${documentId} to user ${newOwnerId}? This cannot be undone.`);
|
|
914
|
+
}
|
|
915
|
+
catch (err) {
|
|
916
|
+
error(err.message);
|
|
917
|
+
process.exit(1);
|
|
918
|
+
}
|
|
55
919
|
if (!confirm) {
|
|
56
920
|
info("Cancelled.");
|
|
57
921
|
return;
|
|
@@ -138,6 +1002,282 @@ Examples:
|
|
|
138
1002
|
process.exit(1);
|
|
139
1003
|
}
|
|
140
1004
|
});
|
|
1005
|
+
// ---- Link access ("anyone with the link") ----
|
|
1006
|
+
const linkAccess = documents
|
|
1007
|
+
.command("link-access")
|
|
1008
|
+
.description('Manage "anyone with the link" access on a document');
|
|
1009
|
+
linkAccess
|
|
1010
|
+
.command("get")
|
|
1011
|
+
.description("Show the current link-access level for a document")
|
|
1012
|
+
.argument("<document-id>", "Document ID")
|
|
1013
|
+
.option("--app <app-id>", "App ID")
|
|
1014
|
+
.option("--json", "Output as JSON")
|
|
1015
|
+
.action(async (documentId, options) => {
|
|
1016
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1017
|
+
const client = new ApiClient();
|
|
1018
|
+
try {
|
|
1019
|
+
const state = await client.getDocumentLinkAccess(resolvedAppId, documentId);
|
|
1020
|
+
const level = state?.linkAccess ?? null;
|
|
1021
|
+
if (options.json) {
|
|
1022
|
+
json({ documentId, linkAccess: level });
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (!level) {
|
|
1026
|
+
info("Link access is off for this document.");
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
keyValue("Document", documentId);
|
|
1030
|
+
keyValue("Link access", level);
|
|
1031
|
+
}
|
|
1032
|
+
catch (err) {
|
|
1033
|
+
error(err.message);
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
});
|
|
1037
|
+
linkAccess
|
|
1038
|
+
.command("set")
|
|
1039
|
+
.description("Turn on link access at a given level")
|
|
1040
|
+
.argument("<document-id>", "Document ID")
|
|
1041
|
+
.requiredOption("--level <level>", "Link access level: reader or read-write")
|
|
1042
|
+
.option("--app <app-id>", "App ID")
|
|
1043
|
+
.option("--json", "Output as JSON")
|
|
1044
|
+
.action(async (documentId, options) => {
|
|
1045
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1046
|
+
const client = new ApiClient();
|
|
1047
|
+
try {
|
|
1048
|
+
const result = await client.setDocumentLinkAccess(resolvedAppId, documentId, options.level);
|
|
1049
|
+
if (options.json) {
|
|
1050
|
+
json(result);
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
success(`Link access set to '${options.level}' for document ${documentId}.`);
|
|
1054
|
+
}
|
|
1055
|
+
catch (err) {
|
|
1056
|
+
error(err.message);
|
|
1057
|
+
process.exit(1);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
linkAccess
|
|
1061
|
+
.command("clear")
|
|
1062
|
+
.description("Turn off link access")
|
|
1063
|
+
.argument("<document-id>", "Document ID")
|
|
1064
|
+
.option("--app <app-id>", "App ID")
|
|
1065
|
+
.option("--json", "Output as JSON")
|
|
1066
|
+
.action(async (documentId, options) => {
|
|
1067
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
1068
|
+
const client = new ApiClient();
|
|
1069
|
+
try {
|
|
1070
|
+
const result = await client.clearDocumentLinkAccess(resolvedAppId, documentId);
|
|
1071
|
+
if (options.json) {
|
|
1072
|
+
json(result);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
success(`Link access turned off for document ${documentId}.`);
|
|
1076
|
+
}
|
|
1077
|
+
catch (err) {
|
|
1078
|
+
error(err.message);
|
|
1079
|
+
process.exit(1);
|
|
1080
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
// ---- Export / Import commands ----
|
|
1083
|
+
documents
|
|
1084
|
+
.command("export")
|
|
1085
|
+
.description("Export a document (Yjs state, blobs, permissions, aliases)")
|
|
1086
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
1087
|
+
.argument("<document-id>", "Document ID to export")
|
|
1088
|
+
.option("--app <app-id>", "App ID")
|
|
1089
|
+
.option("--output <dir>", "Output directory", "./primitive-export")
|
|
1090
|
+
.option("--no-blobs", "Skip blob data")
|
|
1091
|
+
.option("--json", "Output as JSON")
|
|
1092
|
+
.action(async (first, second, options) => {
|
|
1093
|
+
let resolvedAppId;
|
|
1094
|
+
let documentId;
|
|
1095
|
+
if (second && !second.startsWith("-")) {
|
|
1096
|
+
resolvedAppId = resolveAppId(first, options);
|
|
1097
|
+
documentId = second;
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
resolvedAppId = resolveAppId(undefined, options);
|
|
1101
|
+
documentId = first;
|
|
1102
|
+
}
|
|
1103
|
+
if (!documentId) {
|
|
1104
|
+
error("Document ID is required.");
|
|
1105
|
+
process.exit(1);
|
|
1106
|
+
}
|
|
1107
|
+
const client = new ApiClient();
|
|
1108
|
+
try {
|
|
1109
|
+
await exportSingleDocument(client, resolvedAppId, documentId, options.output, options.blobs !== false ? true : false, options.json);
|
|
1110
|
+
}
|
|
1111
|
+
catch (err) {
|
|
1112
|
+
error(err.message);
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
documents
|
|
1117
|
+
.command("export-all")
|
|
1118
|
+
.description("Export all documents for a user")
|
|
1119
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
1120
|
+
.requiredOption("--user-id <id>", "User ID to export documents for")
|
|
1121
|
+
.option("--app <app-id>", "App ID")
|
|
1122
|
+
.option("--owned-only", "Only export documents owned by the user")
|
|
1123
|
+
.option("--output <dir>", "Output directory", "./primitive-export")
|
|
1124
|
+
.option("--no-blobs", "Skip blob data")
|
|
1125
|
+
.option("--json", "Output as JSON")
|
|
1126
|
+
.action(async (appIdArg, options) => {
|
|
1127
|
+
const resolvedAppId = resolveAppId(appIdArg, options);
|
|
1128
|
+
const client = new ApiClient();
|
|
1129
|
+
const includeBlobs = options.blobs !== false;
|
|
1130
|
+
try {
|
|
1131
|
+
const documents = await client.listAdminDocuments(resolvedAppId, options.userId);
|
|
1132
|
+
let docList = Array.isArray(documents) ? documents : [];
|
|
1133
|
+
if (options.ownedOnly) {
|
|
1134
|
+
docList = docList.filter((d) => d.permission === "owner");
|
|
1135
|
+
}
|
|
1136
|
+
if (docList.length === 0) {
|
|
1137
|
+
info("No documents found.");
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
info(`Exporting ${docList.length} document(s)...`);
|
|
1141
|
+
const exportedIds = [];
|
|
1142
|
+
for (const doc of docList) {
|
|
1143
|
+
const docId = doc.documentId || doc.id;
|
|
1144
|
+
try {
|
|
1145
|
+
await exportSingleDocument(client, resolvedAppId, docId, options.output, includeBlobs, false);
|
|
1146
|
+
exportedIds.push(docId);
|
|
1147
|
+
}
|
|
1148
|
+
catch (err) {
|
|
1149
|
+
warn(`Failed to export ${docId}: ${err.message}`);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
// Write manifest
|
|
1153
|
+
const manifestPath = path.join(options.output, "manifest.json");
|
|
1154
|
+
const manifest = {
|
|
1155
|
+
version: 1,
|
|
1156
|
+
exportedAt: new Date().toISOString(),
|
|
1157
|
+
sourceAppId: resolvedAppId,
|
|
1158
|
+
documentCount: exportedIds.length,
|
|
1159
|
+
documents: exportedIds,
|
|
1160
|
+
};
|
|
1161
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
1162
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
1163
|
+
if (options.json) {
|
|
1164
|
+
json(manifest);
|
|
1165
|
+
}
|
|
1166
|
+
else {
|
|
1167
|
+
success(`Exported ${exportedIds.length} document(s) to ${options.output}`);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
catch (err) {
|
|
1171
|
+
error(err.message);
|
|
1172
|
+
process.exit(1);
|
|
1173
|
+
}
|
|
1174
|
+
});
|
|
1175
|
+
documents
|
|
1176
|
+
.command("import")
|
|
1177
|
+
.description("Import documents from an export directory")
|
|
1178
|
+
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
1179
|
+
.argument("<path>", "Path to export directory or single document directory")
|
|
1180
|
+
.option("--app <app-id>", "App ID")
|
|
1181
|
+
.option("--aliases <mode>", "Alias conflict handling: overwrite or skip", "skip")
|
|
1182
|
+
.option("--overwrite", "Replace existing documents if IDs collide")
|
|
1183
|
+
.option("--dry-run", "Show what would be imported without making changes")
|
|
1184
|
+
.option("--owner <userId-or-email>", "User ID or email to set as document owner (admin only)")
|
|
1185
|
+
.option("--json", "Output as JSON")
|
|
1186
|
+
.action(async (first, second, options) => {
|
|
1187
|
+
let resolvedAppId;
|
|
1188
|
+
let importPath;
|
|
1189
|
+
if (second && !second.startsWith("-")) {
|
|
1190
|
+
resolvedAppId = resolveAppId(first, options);
|
|
1191
|
+
importPath = second;
|
|
1192
|
+
}
|
|
1193
|
+
else {
|
|
1194
|
+
resolvedAppId = resolveAppId(undefined, options);
|
|
1195
|
+
importPath = first;
|
|
1196
|
+
}
|
|
1197
|
+
if (!importPath) {
|
|
1198
|
+
error("Import path is required.");
|
|
1199
|
+
process.exit(1);
|
|
1200
|
+
}
|
|
1201
|
+
const client = new ApiClient();
|
|
1202
|
+
// Resolve --owner to a userId through the one shared lookup (#2763).
|
|
1203
|
+
let ownerUserId;
|
|
1204
|
+
if (options.owner) {
|
|
1205
|
+
try {
|
|
1206
|
+
ownerUserId = await resolveOwnerUserId(client, resolvedAppId, options.owner);
|
|
1207
|
+
}
|
|
1208
|
+
catch (err) {
|
|
1209
|
+
error(err.message);
|
|
1210
|
+
process.exit(1);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
try {
|
|
1214
|
+
// Determine if this is a manifest-based export or a single document
|
|
1215
|
+
const manifestPath = path.join(importPath, "manifest.json");
|
|
1216
|
+
let docDirs = [];
|
|
1217
|
+
if (fs.existsSync(manifestPath)) {
|
|
1218
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
|
|
1219
|
+
for (const docId of manifest.documents) {
|
|
1220
|
+
const docDir = path.join(importPath, "documents", docId);
|
|
1221
|
+
if (fs.existsSync(docDir)) {
|
|
1222
|
+
docDirs.push(docDir);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
else if (fs.existsSync(path.join(importPath, "metadata.json"))) {
|
|
1227
|
+
// Single document directory
|
|
1228
|
+
docDirs = [importPath];
|
|
1229
|
+
}
|
|
1230
|
+
else if (fs.existsSync(path.join(importPath, "documents"))) {
|
|
1231
|
+
// Export directory without manifest — discover documents
|
|
1232
|
+
const entries = fs.readdirSync(path.join(importPath, "documents"), { withFileTypes: true });
|
|
1233
|
+
for (const entry of entries) {
|
|
1234
|
+
if (entry.isDirectory()) {
|
|
1235
|
+
docDirs.push(path.join(importPath, "documents", entry.name));
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
error("No valid export data found at the specified path.");
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
if (docDirs.length === 0) {
|
|
1244
|
+
info("No documents to import.");
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const summary = {
|
|
1248
|
+
created: 0,
|
|
1249
|
+
updated: 0,
|
|
1250
|
+
skipped: 0,
|
|
1251
|
+
blobsUploaded: 0,
|
|
1252
|
+
aliasesSet: 0,
|
|
1253
|
+
aliasesSkipped: 0,
|
|
1254
|
+
};
|
|
1255
|
+
for (const docDir of docDirs) {
|
|
1256
|
+
await importSingleDocument(client, resolvedAppId, docDir, options.overwrite || false, options.aliases || "skip", options.dryRun || false, summary, ownerUserId);
|
|
1257
|
+
}
|
|
1258
|
+
if (options.json) {
|
|
1259
|
+
json(summary);
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
success(`Import complete:`);
|
|
1263
|
+
keyValue(" Documents created", String(summary.created));
|
|
1264
|
+
if (summary.updated > 0)
|
|
1265
|
+
keyValue(" Documents updated", String(summary.updated));
|
|
1266
|
+
if (summary.skipped > 0)
|
|
1267
|
+
keyValue(" Documents skipped", String(summary.skipped));
|
|
1268
|
+
if (summary.blobsUploaded > 0)
|
|
1269
|
+
keyValue(" Blobs uploaded", String(summary.blobsUploaded));
|
|
1270
|
+
if (summary.aliasesSet > 0)
|
|
1271
|
+
keyValue(" Aliases set", String(summary.aliasesSet));
|
|
1272
|
+
if (summary.aliasesSkipped > 0)
|
|
1273
|
+
keyValue(" Aliases skipped", String(summary.aliasesSkipped));
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
catch (err) {
|
|
1277
|
+
error(err.message);
|
|
1278
|
+
process.exit(1);
|
|
1279
|
+
}
|
|
1280
|
+
});
|
|
141
1281
|
// Revoke group permission
|
|
142
1282
|
groupPermissions
|
|
143
1283
|
.command("revoke")
|
|
@@ -150,15 +1290,14 @@ Examples:
|
|
|
150
1290
|
.action(async (documentId, groupType, groupId, options) => {
|
|
151
1291
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
152
1292
|
if (!options.yes) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
{
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
]);
|
|
1293
|
+
let confirm;
|
|
1294
|
+
try {
|
|
1295
|
+
confirm = await confirmPrompt(`Revoke permission for group ${groupType}/${groupId} on document ${documentId}?`);
|
|
1296
|
+
}
|
|
1297
|
+
catch (err) {
|
|
1298
|
+
error(err.message);
|
|
1299
|
+
process.exit(1);
|
|
1300
|
+
}
|
|
162
1301
|
if (!confirm) {
|
|
163
1302
|
info("Cancelled.");
|
|
164
1303
|
return;
|
|
@@ -175,4 +1314,203 @@ Examples:
|
|
|
175
1314
|
}
|
|
176
1315
|
});
|
|
177
1316
|
}
|
|
1317
|
+
// ============================================
|
|
1318
|
+
// Helper functions for export/import
|
|
1319
|
+
// ============================================
|
|
1320
|
+
async function exportSingleDocument(client, appId, documentId, outputDir, includeBlobs, jsonOutput) {
|
|
1321
|
+
const docDir = path.join(outputDir, "documents", documentId);
|
|
1322
|
+
fs.mkdirSync(docDir, { recursive: true });
|
|
1323
|
+
// 1. Fetch document metadata
|
|
1324
|
+
const docMeta = await client.getDocument(appId, documentId);
|
|
1325
|
+
// 2. Export Yjs state
|
|
1326
|
+
const stateResult = await client.exportDocumentState(appId, documentId);
|
|
1327
|
+
fs.writeFileSync(path.join(docDir, "document.yjs"), Buffer.from(stateResult.state, "base64"));
|
|
1328
|
+
// 3. Fetch permissions
|
|
1329
|
+
let permissions = [];
|
|
1330
|
+
try {
|
|
1331
|
+
const permResult = await client.listDocumentPermissions(appId, documentId);
|
|
1332
|
+
permissions = Array.isArray(permResult) ? permResult : permResult?.permissions || [];
|
|
1333
|
+
}
|
|
1334
|
+
catch {
|
|
1335
|
+
// Permissions endpoint might not return data for all docs
|
|
1336
|
+
}
|
|
1337
|
+
// 4. Fetch invitations
|
|
1338
|
+
let invitations = [];
|
|
1339
|
+
try {
|
|
1340
|
+
const invResult = await client.listDocumentInvitations(appId, documentId);
|
|
1341
|
+
invitations = Array.isArray(invResult) ? invResult : invResult?.invitations || [];
|
|
1342
|
+
}
|
|
1343
|
+
catch {
|
|
1344
|
+
// May not have invitations
|
|
1345
|
+
}
|
|
1346
|
+
// Merge permissions and invitations into a single list with email identifiers
|
|
1347
|
+
const permissionsExport = [
|
|
1348
|
+
...permissions.map((p) => ({
|
|
1349
|
+
email: p.email,
|
|
1350
|
+
permission: p.permission,
|
|
1351
|
+
grantedAt: p.grantedAt,
|
|
1352
|
+
})),
|
|
1353
|
+
...invitations
|
|
1354
|
+
.filter((i) => !i.accepted && new Date(i.expiresAt) > new Date())
|
|
1355
|
+
.map((i) => ({
|
|
1356
|
+
email: i.email,
|
|
1357
|
+
permission: i.permission,
|
|
1358
|
+
source: "invitation",
|
|
1359
|
+
status: "pending",
|
|
1360
|
+
})),
|
|
1361
|
+
];
|
|
1362
|
+
fs.writeFileSync(path.join(docDir, "permissions.json"), JSON.stringify(permissionsExport, null, 2));
|
|
1363
|
+
// 5. Fetch aliases (user-scoped only)
|
|
1364
|
+
let aliases = [];
|
|
1365
|
+
try {
|
|
1366
|
+
const aliasResult = await client.listDocumentAliases(appId, documentId);
|
|
1367
|
+
aliases = (Array.isArray(aliasResult) ? aliasResult : aliasResult?.aliases || [])
|
|
1368
|
+
.filter((a) => (a.scope || a.aliasScope) === "user");
|
|
1369
|
+
}
|
|
1370
|
+
catch {
|
|
1371
|
+
// May not have aliases
|
|
1372
|
+
}
|
|
1373
|
+
// 6. Write metadata
|
|
1374
|
+
const metadata = {
|
|
1375
|
+
documentId: docMeta.documentId || documentId,
|
|
1376
|
+
title: docMeta.title,
|
|
1377
|
+
tags: docMeta.tags || [],
|
|
1378
|
+
createdAt: docMeta.createdAt,
|
|
1379
|
+
createdBy: docMeta.createdByEmail || docMeta.createdBy,
|
|
1380
|
+
aliases: aliases.map((a) => ({
|
|
1381
|
+
aliasScope: a.scope || a.aliasScope,
|
|
1382
|
+
aliasKey: a.aliasKey,
|
|
1383
|
+
})),
|
|
1384
|
+
};
|
|
1385
|
+
fs.writeFileSync(path.join(docDir, "metadata.json"), JSON.stringify(metadata, null, 2));
|
|
1386
|
+
// 7. Blobs
|
|
1387
|
+
if (includeBlobs) {
|
|
1388
|
+
let blobs = [];
|
|
1389
|
+
try {
|
|
1390
|
+
const blobResult = await client.listDocumentBlobs(appId, documentId);
|
|
1391
|
+
blobs = Array.isArray(blobResult) ? blobResult : blobResult?.items || blobResult?.blobs || [];
|
|
1392
|
+
}
|
|
1393
|
+
catch {
|
|
1394
|
+
// May not have blobs
|
|
1395
|
+
}
|
|
1396
|
+
if (blobs.length > 0) {
|
|
1397
|
+
const blobsDir = path.join(docDir, "blobs");
|
|
1398
|
+
fs.mkdirSync(blobsDir, { recursive: true });
|
|
1399
|
+
const blobIndex = blobs.map((b) => ({
|
|
1400
|
+
blobId: b.blobId,
|
|
1401
|
+
filename: b.filename,
|
|
1402
|
+
contentType: b.contentType,
|
|
1403
|
+
sha256: b.sha256,
|
|
1404
|
+
numBytes: b.numBytes,
|
|
1405
|
+
}));
|
|
1406
|
+
fs.writeFileSync(path.join(blobsDir, "index.json"), JSON.stringify(blobIndex, null, 2));
|
|
1407
|
+
// Download blobs (sequentially to avoid overwhelming the server)
|
|
1408
|
+
for (const blob of blobs) {
|
|
1409
|
+
const data = await client.downloadBlob(appId, documentId, blob.blobId);
|
|
1410
|
+
fs.writeFileSync(path.join(blobsDir, `${blob.blobId}.bin`), data);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (jsonOutput) {
|
|
1415
|
+
json({
|
|
1416
|
+
documentId,
|
|
1417
|
+
title: metadata.title,
|
|
1418
|
+
stateBytes: stateResult.byteLength,
|
|
1419
|
+
permissions: permissionsExport.length,
|
|
1420
|
+
aliases: aliases.length,
|
|
1421
|
+
exportPath: docDir,
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
success(`Exported document ${documentId} (${metadata.title || "untitled"}) to ${docDir}`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function importSingleDocument(client, appId, docDir, overwrite, aliasMode, dryRun, summary, ownerUserId) {
|
|
1429
|
+
const metadataPath = path.join(docDir, "metadata.json");
|
|
1430
|
+
if (!fs.existsSync(metadataPath)) {
|
|
1431
|
+
warn(`Skipping ${docDir}: no metadata.json found`);
|
|
1432
|
+
summary.skipped++;
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
|
|
1436
|
+
const documentId = metadata.documentId;
|
|
1437
|
+
if (dryRun) {
|
|
1438
|
+
info(`[dry-run] Would import document ${documentId} (${metadata.title || "untitled"})`);
|
|
1439
|
+
summary.created++;
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
// Check if document exists
|
|
1443
|
+
let exists = false;
|
|
1444
|
+
try {
|
|
1445
|
+
await client.getDocument(appId, documentId);
|
|
1446
|
+
exists = true;
|
|
1447
|
+
}
|
|
1448
|
+
catch {
|
|
1449
|
+
exists = false;
|
|
1450
|
+
}
|
|
1451
|
+
if (exists && !overwrite) {
|
|
1452
|
+
warn(`Skipping ${documentId}: already exists (use --overwrite to replace)`);
|
|
1453
|
+
summary.skipped++;
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
// Create document if it doesn't exist
|
|
1457
|
+
if (!exists) {
|
|
1458
|
+
await client.createDocument(appId, {
|
|
1459
|
+
title: metadata.title || "Imported Document",
|
|
1460
|
+
documentId,
|
|
1461
|
+
tags: metadata.tags,
|
|
1462
|
+
...(ownerUserId ? { createdBy: ownerUserId } : {}),
|
|
1463
|
+
});
|
|
1464
|
+
summary.created++;
|
|
1465
|
+
}
|
|
1466
|
+
else {
|
|
1467
|
+
summary.updated++;
|
|
1468
|
+
}
|
|
1469
|
+
// Import Yjs state
|
|
1470
|
+
const yjsPath = path.join(docDir, "document.yjs");
|
|
1471
|
+
if (fs.existsSync(yjsPath)) {
|
|
1472
|
+
const stateBuffer = fs.readFileSync(yjsPath);
|
|
1473
|
+
const stateBase64 = stateBuffer.toString("base64");
|
|
1474
|
+
await client.importDocumentState(appId, documentId, stateBase64);
|
|
1475
|
+
}
|
|
1476
|
+
// Upload blobs
|
|
1477
|
+
const blobIndexPath = path.join(docDir, "blobs", "index.json");
|
|
1478
|
+
if (fs.existsSync(blobIndexPath)) {
|
|
1479
|
+
const blobIndex = JSON.parse(fs.readFileSync(blobIndexPath, "utf-8"));
|
|
1480
|
+
for (const blob of blobIndex) {
|
|
1481
|
+
const blobPath = path.join(docDir, "blobs", `${blob.blobId}.bin`);
|
|
1482
|
+
if (fs.existsSync(blobPath)) {
|
|
1483
|
+
const data = fs.readFileSync(blobPath);
|
|
1484
|
+
await client.uploadBlob(appId, documentId, blob.blobId, data, {
|
|
1485
|
+
filename: blob.filename,
|
|
1486
|
+
contentType: blob.contentType,
|
|
1487
|
+
sha256: blob.sha256,
|
|
1488
|
+
});
|
|
1489
|
+
summary.blobsUploaded++;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
// Permissions are exported for reference but not restored during import.
|
|
1494
|
+
// The importing admin is the new owner; sharing is managed in the target app.
|
|
1495
|
+
// Restore aliases
|
|
1496
|
+
if (metadata.aliases && Array.isArray(metadata.aliases)) {
|
|
1497
|
+
for (const alias of metadata.aliases) {
|
|
1498
|
+
if (alias.aliasScope !== "user")
|
|
1499
|
+
continue;
|
|
1500
|
+
if (!ownerUserId) {
|
|
1501
|
+
summary.aliasesSkipped++;
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
try {
|
|
1505
|
+
await client.setDocumentAlias(appId, alias.aliasScope, alias.aliasKey, documentId, ownerUserId, aliasMode === "skip" // mustNotExist: true for skip mode, false for overwrite
|
|
1506
|
+
);
|
|
1507
|
+
summary.aliasesSet++;
|
|
1508
|
+
}
|
|
1509
|
+
catch {
|
|
1510
|
+
summary.aliasesSkipped++;
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
info(`Imported document ${documentId} (${metadata.title || "untitled"})`);
|
|
1515
|
+
}
|
|
178
1516
|
//# sourceMappingURL=documents.js.map
|