primitive-admin 1.0.61 → 1.0.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -69
- package/assets/skill/skills/primitive-platform/SKILL.md +336 -37
- package/dist/bin/primitive.js +25 -14
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/analytics.js +36 -16
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +52 -188
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +13 -153
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +14 -100
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/collections.js +47 -8
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/config.d.ts +46 -0
- package/dist/src/commands/config.js +479 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/connections.js +12 -7
- package/dist/src/commands/connections.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +84 -176
- package/dist/src/commands/cron-triggers.js.map +1 -1
- package/dist/src/commands/database-type-configs.d.ts +2 -0
- package/dist/src/commands/database-type-configs.js +171 -0
- package/dist/src/commands/database-type-configs.js.map +1 -0
- package/dist/src/commands/database-types.js +4 -4
- package/dist/src/commands/database-types.js.map +1 -1
- package/dist/src/commands/databases.d.ts +63 -0
- package/dist/src/commands/databases.js +649 -87
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +869 -12
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +21 -128
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/env.d.ts +12 -1
- package/dist/src/commands/env.js +115 -26
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/feature-flags.d.ts +14 -0
- package/dist/src/commands/feature-flags.js +116 -0
- package/dist/src/commands/feature-flags.js.map +1 -0
- package/dist/src/commands/group-type-configs.js +14 -109
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/init.d.ts +24 -1
- package/dist/src/commands/init.js +1194 -279
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +166 -435
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/metadata-category-configs.d.ts +12 -0
- package/dist/src/commands/metadata-category-configs.js +112 -0
- package/dist/src/commands/metadata-category-configs.js.map +1 -0
- package/dist/src/commands/metadata.js +33 -141
- package/dist/src/commands/metadata.js.map +1 -1
- package/dist/src/commands/prompts.js +206 -563
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +1 -1
- package/dist/src/commands/rule-sets.js +16 -132
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.d.ts +10 -0
- package/dist/src/commands/scripts.js +203 -241
- package/dist/src/commands/scripts.js.map +1 -1
- package/dist/src/commands/sessions.js +17 -5
- package/dist/src/commands/sessions.js.map +1 -1
- package/dist/src/commands/sync-app-settings.d.ts +113 -70
- package/dist/src/commands/sync-app-settings.js +193 -242
- package/dist/src/commands/sync-app-settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +1904 -51
- package/dist/src/commands/sync.js +9468 -1790
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +8 -2
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +92 -1
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.js +44 -58
- package/dist/src/commands/vars.js.map +1 -1
- package/dist/src/commands/webhooks.js +318 -156
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.d.ts +49 -12
- package/dist/src/commands/workflows.js +652 -1086
- 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 +541 -61
- package/dist/src/lib/api-client.js +559 -102
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +180 -25
- package/dist/src/lib/app-settings-descriptor.js +403 -82
- package/dist/src/lib/app-settings-descriptor.js.map +1 -1
- package/dist/src/lib/canonical-json.d.ts +12 -0
- package/dist/src/lib/canonical-json.js +35 -0
- package/dist/src/lib/canonical-json.js.map +1 -0
- package/dist/src/lib/cli-manifest.d.ts +8 -0
- package/dist/src/lib/cli-manifest.js +1 -0
- package/dist/src/lib/cli-manifest.js.map +1 -1
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js +9 -2
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +6 -6
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +8 -8
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
- package/dist/src/lib/config-object-descriptor.d.ts +127 -0
- package/dist/src/lib/config-object-descriptor.js +658 -0
- package/dist/src/lib/config-object-descriptor.js.map +1 -0
- package/dist/src/lib/config-payload.d.ts +85 -0
- package/dist/src/lib/config-payload.js +116 -0
- package/dist/src/lib/config-payload.js.map +1 -0
- package/dist/src/lib/config-surface.d.ts +130 -0
- package/dist/src/lib/config-surface.js +300 -0
- package/dist/src/lib/config-surface.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +10 -0
- package/dist/src/lib/credentials-store.js +23 -0
- package/dist/src/lib/credentials-store.js.map +1 -1
- package/dist/src/lib/data-input.d.ts +23 -0
- package/dist/src/lib/data-input.js +50 -0
- package/dist/src/lib/data-input.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTemplates.js +1 -1
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +1 -1
- package/dist/src/lib/env-resolver-core.d.ts +187 -0
- package/dist/src/lib/env-resolver-core.js +325 -0
- package/dist/src/lib/env-resolver-core.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +28 -6
- package/dist/src/lib/env-resolver.js +44 -32
- package/dist/src/lib/env-resolver.js.map +1 -1
- package/dist/src/lib/generated-allowlist.js +12 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
- package/dist/src/lib/generated-config-surfaces.js +4058 -0
- package/dist/src/lib/generated-config-surfaces.js.map +1 -0
- package/dist/src/lib/generated-template-lint.d.ts +212 -0
- package/dist/src/lib/generated-template-lint.js +624 -0
- package/dist/src/lib/generated-template-lint.js.map +1 -0
- package/dist/src/lib/init-adopt.d.ts +16 -0
- package/dist/src/lib/init-adopt.js +34 -0
- package/dist/src/lib/init-adopt.js.map +1 -0
- package/dist/src/lib/init-assets.d.ts +39 -0
- package/dist/src/lib/init-assets.js +97 -0
- package/dist/src/lib/init-assets.js.map +1 -0
- package/dist/src/lib/init-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 +44 -5
- package/dist/src/lib/init-config.js +80 -7
- package/dist/src/lib/init-config.js.map +1 -1
- package/dist/src/lib/init-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 +90 -0
- package/dist/src/lib/init-ios-links.js +218 -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/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/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 +135 -0
- package/dist/src/lib/local-test-cases.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +568 -0
- package/dist/src/lib/log-inspection.js +639 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +2 -2
- package/dist/src/lib/migration-nag.js +3 -3
- package/dist/src/lib/object-status-filter.d.ts +22 -0
- package/dist/src/lib/object-status-filter.js +45 -0
- package/dist/src/lib/object-status-filter.js.map +1 -0
- package/dist/src/lib/output.d.ts +24 -1
- package/dist/src/lib/output.js +42 -1
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/package-manager.d.ts +140 -0
- package/dist/src/lib/package-manager.js +305 -0
- package/dist/src/lib/package-manager.js.map +1 -0
- package/dist/src/lib/paginate.d.ts +50 -0
- package/dist/src/lib/paginate.js +53 -0
- package/dist/src/lib/paginate.js.map +1 -1
- package/dist/src/lib/platform-owned.d.ts +29 -3
- package/dist/src/lib/platform-owned.js +42 -3
- package/dist/src/lib/platform-owned.js.map +1 -1
- package/dist/src/lib/project-config.d.ts +8 -0
- package/dist/src/lib/project-config.js +30 -5
- package/dist/src/lib/project-config.js.map +1 -1
- package/dist/src/lib/query-operators.d.ts +1 -1
- package/dist/src/lib/query-operators.js +1 -1
- package/dist/src/lib/record-filter.d.ts +18 -0
- package/dist/src/lib/record-filter.js +55 -0
- package/dist/src/lib/record-filter.js.map +1 -0
- package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
- package/dist/src/lib/resolve-init-dev-port.js +55 -0
- package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
- package/dist/src/lib/resolve-init-server.d.ts +64 -0
- package/dist/src/lib/resolve-init-server.js +77 -0
- package/dist/src/lib/resolve-init-server.js.map +1 -0
- package/dist/src/lib/resolve-platform.d.ts +43 -14
- package/dist/src/lib/resolve-platform.js +74 -12
- package/dist/src/lib/resolve-platform.js.map +1 -1
- package/dist/src/lib/run-status.d.ts +19 -0
- package/dist/src/lib/run-status.generated.d.ts +39 -0
- package/dist/src/lib/run-status.generated.js +66 -0
- package/dist/src/lib/run-status.generated.js.map +1 -0
- package/dist/src/lib/run-status.js +19 -0
- package/dist/src/lib/run-status.js.map +1 -0
- package/dist/src/lib/server-text-normalization.d.ts +51 -0
- package/dist/src/lib/server-text-normalization.js +90 -0
- package/dist/src/lib/server-text-normalization.js.map +1 -0
- package/dist/src/lib/server-url.d.ts +22 -0
- package/dist/src/lib/server-url.js +33 -0
- package/dist/src/lib/server-url.js.map +1 -0
- package/dist/src/lib/signing-secret-status.d.ts +81 -0
- package/dist/src/lib/signing-secret-status.js +116 -0
- package/dist/src/lib/signing-secret-status.js.map +1 -0
- package/dist/src/lib/skill-installer.js +6 -3
- package/dist/src/lib/skill-installer.js.map +1 -1
- package/dist/src/lib/snapshots.d.ts +2 -2
- package/dist/src/lib/snapshots.js +2 -2
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +15 -2
- package/dist/src/lib/swift-codegen/dbGenerator.js +250 -18
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/generator.d.ts +11 -2
- package/dist/src/lib/swift-codegen/generator.js +181 -7
- package/dist/src/lib/swift-codegen/generator.js.map +1 -1
- package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +7 -3
- package/dist/src/lib/swift-codegen/siblingSymbols.js +4 -1
- package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -1
- package/dist/src/lib/sync-dir-selector.d.ts +21 -0
- package/dist/src/lib/sync-dir-selector.js +30 -0
- package/dist/src/lib/sync-dir-selector.js.map +1 -0
- package/dist/src/lib/sync-paths.d.ts +39 -0
- package/dist/src/lib/sync-paths.js +70 -2
- package/dist/src/lib/sync-paths.js.map +1 -1
- package/dist/src/lib/sync-resource-types.d.ts +138 -31
- package/dist/src/lib/sync-resource-types.js +241 -47
- package/dist/src/lib/sync-resource-types.js.map +1 -1
- package/dist/src/lib/sync-selectors.d.ts +95 -0
- package/dist/src/lib/sync-selectors.js +228 -0
- package/dist/src/lib/sync-selectors.js.map +1 -0
- package/dist/src/lib/template.d.ts +62 -4
- package/dist/src/lib/template.js +147 -39
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/test-case-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 +15 -0
- package/dist/src/lib/test-case-variables.js +29 -0
- package/dist/src/lib/test-case-variables.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +4 -4
- package/dist/src/lib/toml-database-config.js +27 -34
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/toml-metadata-config.d.ts +9 -5
- package/dist/src/lib/toml-metadata-config.js +40 -45
- package/dist/src/lib/toml-metadata-config.js.map +1 -1
- package/dist/src/lib/toml-native-form.d.ts +1 -1
- package/dist/src/lib/toml-native-form.js +1 -1
- package/dist/src/lib/toml-params-validator.d.ts +1 -1
- package/dist/src/lib/toml-params-validator.js +1 -1
- package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
- package/dist/src/lib/toml-scalar-edit.js +283 -0
- package/dist/src/lib/toml-scalar-edit.js.map +1 -0
- package/dist/src/lib/user-selector.d.ts +24 -0
- package/dist/src/lib/user-selector.js +33 -0
- package/dist/src/lib/user-selector.js.map +1 -0
- package/dist/src/lib/version-check.d.ts +25 -0
- package/dist/src/lib/version-check.js +77 -8
- package/dist/src/lib/version-check.js.map +1 -1
- package/dist/src/lib/watch.d.ts +121 -0
- package/dist/src/lib/watch.js +169 -0
- package/dist/src/lib/watch.js.map +1 -0
- package/dist/src/lib/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/workflow-apply.d.ts +69 -45
- package/dist/src/lib/workflow-apply.js +99 -72
- package/dist/src/lib/workflow-apply.js.map +1 -1
- package/dist/src/lib/workflow-codegen/generator.js +4 -4
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +11 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +5 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
- package/dist/src/lib/workflow-config-apply.d.ts +70 -0
- package/dist/src/lib/workflow-config-apply.js +137 -0
- package/dist/src/lib/workflow-config-apply.js.map +1 -0
- package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
- package/dist/src/lib/workflow-config-sidecar.js +96 -0
- package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
- package/dist/src/lib/workflow-defaults.d.ts +29 -0
- package/dist/src/lib/workflow-defaults.js +41 -0
- package/dist/src/lib/workflow-defaults.js.map +1 -0
- package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
- package/dist/src/lib/workflow-include-preserve.js +286 -0
- package/dist/src/lib/workflow-include-preserve.js.map +1 -0
- package/dist/src/lib/workflow-payload.d.ts +31 -20
- package/dist/src/lib/workflow-payload.js +100 -110
- package/dist/src/lib/workflow-payload.js.map +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +37 -16
- package/dist/src/lib/workflow-toml-validator.js +141 -16
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/dist/src/types/index.d.ts +78 -22
- package/dist/src/validators.d.ts +7 -7
- package/dist/src/validators.js +6 -6
- package/package.json +11 -6
- package/dist/src/commands/settings.d.ts +0 -15
- package/dist/src/commands/settings.js +0 -102
- package/dist/src/commands/settings.js.map +0 -1
|
@@ -1,18 +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, keyValue, warn, } from "../lib/output.js";
|
|
3
|
+
import { success, error, info, formatTable, formatId, formatDate, json, keyValue, result as printResult, warn, } from "../lib/output.js";
|
|
4
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";
|
|
5
8
|
import * as fs from "fs";
|
|
6
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
|
+
}
|
|
7
28
|
export function registerDocumentsCommands(program) {
|
|
8
29
|
const documents = program
|
|
9
30
|
.command("documents")
|
|
10
31
|
.description("Manage documents within an app")
|
|
11
32
|
.addHelpText("after", `
|
|
12
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>
|
|
13
53
|
$ primitive documents transfer-owner <document-id> <new-owner-id>
|
|
14
54
|
$ primitive documents group-permissions list <document-id>
|
|
15
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
|
+
});
|
|
16
878
|
// Transfer document ownership
|
|
17
879
|
documents
|
|
18
880
|
.command("transfer-owner")
|
|
@@ -337,20 +1199,15 @@ Examples:
|
|
|
337
1199
|
process.exit(1);
|
|
338
1200
|
}
|
|
339
1201
|
const client = new ApiClient();
|
|
340
|
-
// Resolve --owner to a userId
|
|
1202
|
+
// Resolve --owner to a userId through the one shared lookup (#2763).
|
|
341
1203
|
let ownerUserId;
|
|
342
1204
|
if (options.owner) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (!user) {
|
|
346
|
-
error(`No user found with email "${options.owner}" in app ${resolvedAppId}.`);
|
|
347
|
-
process.exit(1);
|
|
348
|
-
}
|
|
349
|
-
ownerUserId = user.userId || user.id;
|
|
350
|
-
info(`Resolved owner email to userId: ${ownerUserId}`);
|
|
1205
|
+
try {
|
|
1206
|
+
ownerUserId = await resolveOwnerUserId(client, resolvedAppId, options.owner);
|
|
351
1207
|
}
|
|
352
|
-
|
|
353
|
-
|
|
1208
|
+
catch (err) {
|
|
1209
|
+
error(err.message);
|
|
1210
|
+
process.exit(1);
|
|
354
1211
|
}
|
|
355
1212
|
}
|
|
356
1213
|
try {
|