primitive-admin 1.0.63 → 1.0.64
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 +132 -42
- package/assets/skill/skills/primitive-platform/SKILL.md +142 -661
- package/dist/bin/primitive.js +121 -41
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.js +37 -50
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.js +178 -9
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +12 -5
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth-sessions.d.ts +7 -0
- package/dist/src/commands/auth-sessions.js +144 -0
- package/dist/src/commands/auth-sessions.js.map +1 -0
- package/dist/src/commands/auth.js +76 -116
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +2 -1
- package/dist/src/commands/blob-buckets.js.map +1 -1
- package/dist/src/commands/catalog.js +26 -10
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +3 -10
- package/dist/src/commands/collection-type-configs.js.map +1 -1
- package/dist/src/commands/collections.js +736 -19
- package/dist/src/commands/collections.js.map +1 -1
- package/dist/src/commands/config.js +13 -27
- package/dist/src/commands/config.js.map +1 -1
- package/dist/src/commands/connections.js +8 -9
- package/dist/src/commands/connections.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +2 -1
- package/dist/src/commands/cron-triggers.js.map +1 -1
- package/dist/src/commands/database-type-configs.js +5 -12
- package/dist/src/commands/database-type-configs.js.map +1 -1
- package/dist/src/commands/databases.js +59 -72
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +58 -0
- package/dist/src/commands/documents.js +1623 -95
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +2 -1
- package/dist/src/commands/email-templates.js.map +1 -1
- package/dist/src/commands/env.d.ts +1 -1
- package/dist/src/commands/env.js +89 -43
- package/dist/src/commands/env.js.map +1 -1
- package/dist/src/commands/feature-flags.js +2 -1
- package/dist/src/commands/feature-flags.js.map +1 -1
- package/dist/src/commands/functions.d.ts +20 -0
- package/dist/src/commands/functions.js +1630 -0
- package/dist/src/commands/functions.js.map +1 -0
- package/dist/src/commands/group-type-configs.js +2 -1
- package/dist/src/commands/group-type-configs.js.map +1 -1
- package/dist/src/commands/groups.js +30 -17
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.js +17 -11
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/init.d.ts +12 -15
- package/dist/src/commands/init.js +44 -92
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +35 -12
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/locks.js +17 -2
- package/dist/src/commands/locks.js.map +1 -1
- package/dist/src/commands/metadata-category-configs.js +2 -1
- package/dist/src/commands/metadata-category-configs.js.map +1 -1
- package/dist/src/commands/metadata.js +8 -1
- package/dist/src/commands/metadata.js.map +1 -1
- package/dist/src/commands/prompts.js +106 -59
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.js +3 -10
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.js +33 -28
- package/dist/src/commands/scripts.js.map +1 -1
- package/dist/src/commands/secrets.js +2 -1
- package/dist/src/commands/secrets.js.map +1 -1
- package/dist/src/commands/sessions.js +10 -9
- package/dist/src/commands/sessions.js.map +1 -1
- package/dist/src/commands/sync-app-settings.js +3 -5
- package/dist/src/commands/sync-app-settings.js.map +1 -1
- package/dist/src/commands/sync.d.ts +390 -13
- package/dist/src/commands/sync.js +2613 -451
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +2 -1
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +19 -10
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.js +2 -1
- package/dist/src/commands/vars.js.map +1 -1
- package/dist/src/commands/waitlist.js +2 -1
- package/dist/src/commands/waitlist.js.map +1 -1
- package/dist/src/commands/webhooks.d.ts +29 -0
- package/dist/src/commands/webhooks.js +135 -64
- package/dist/src/commands/webhooks.js.map +1 -1
- package/dist/src/commands/workflows.d.ts +1 -29
- package/dist/src/commands/workflows.js +113 -179
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.d.ts +628 -14
- package/dist/src/lib/api-client.js +776 -70
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +1 -1
- package/dist/src/lib/app-settings-descriptor.js +9 -1
- package/dist/src/lib/app-settings-descriptor.js.map +1 -1
- package/dist/src/lib/block-selector.d.ts +58 -0
- package/dist/src/lib/block-selector.js +92 -0
- package/dist/src/lib/block-selector.js.map +1 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +24 -54
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +31 -153
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
- package/dist/src/lib/collection-export.d.ts +184 -0
- package/dist/src/lib/collection-export.js +252 -0
- package/dist/src/lib/collection-export.js.map +1 -0
- package/dist/src/lib/config-json-field.d.ts +28 -0
- package/dist/src/lib/config-json-field.js +56 -0
- package/dist/src/lib/config-json-field.js.map +1 -0
- package/dist/src/lib/config-object-descriptor.js +88 -6
- package/dist/src/lib/config-object-descriptor.js.map +1 -1
- package/dist/src/lib/config-payload.d.ts +8 -1
- package/dist/src/lib/config-payload.js +49 -4
- package/dist/src/lib/config-payload.js.map +1 -1
- package/dist/src/lib/config-surface.d.ts +12 -1
- package/dist/src/lib/config-surface.js +71 -3
- package/dist/src/lib/config-surface.js.map +1 -1
- package/dist/src/lib/config.d.ts +31 -13
- package/dist/src/lib/config.js +34 -70
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/credentials-store.d.ts +37 -22
- package/dist/src/lib/credentials-store.js +103 -97
- package/dist/src/lib/credentials-store.js.map +1 -1
- package/dist/src/lib/db-codegen/dbTypeIR.js +8 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -1
- package/dist/src/lib/deprecation.d.ts +22 -0
- package/dist/src/lib/deprecation.js +43 -0
- package/dist/src/lib/deprecation.js.map +1 -0
- package/dist/src/lib/document-export-permissions.d.ts +30 -0
- package/dist/src/lib/document-export-permissions.js +54 -0
- package/dist/src/lib/document-export-permissions.js.map +1 -0
- package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
- package/dist/src/lib/document-ingest-artifact.js +505 -0
- package/dist/src/lib/document-ingest-artifact.js.map +1 -0
- package/dist/src/lib/document-ingest-input.d.ts +51 -0
- package/dist/src/lib/document-ingest-input.js +132 -0
- package/dist/src/lib/document-ingest-input.js.map +1 -0
- package/dist/src/lib/document-ingest-rows.d.ts +137 -0
- package/dist/src/lib/document-ingest-rows.js +181 -0
- package/dist/src/lib/document-ingest-rows.js.map +1 -0
- package/dist/src/lib/document-ingest.d.ts +101 -0
- package/dist/src/lib/document-ingest.js +384 -0
- package/dist/src/lib/document-ingest.js.map +1 -0
- package/dist/src/lib/env-resolver-core.d.ts +89 -18
- package/dist/src/lib/env-resolver-core.js +145 -23
- package/dist/src/lib/env-resolver-core.js.map +1 -1
- package/dist/src/lib/env-resolver.d.ts +18 -3
- package/dist/src/lib/env-resolver.js +23 -3
- package/dist/src/lib/env-resolver.js.map +1 -1
- package/dist/src/lib/function-bundle.d.ts +147 -0
- package/dist/src/lib/function-bundle.js +341 -0
- package/dist/src/lib/function-bundle.js.map +1 -0
- package/dist/src/lib/function-collect.d.ts +123 -0
- package/dist/src/lib/function-collect.js +610 -0
- package/dist/src/lib/function-collect.js.map +1 -0
- package/dist/src/lib/function-db-types.d.ts +202 -0
- package/dist/src/lib/function-db-types.js +870 -0
- package/dist/src/lib/function-db-types.js.map +1 -0
- package/dist/src/lib/function-document-types.d.ts +144 -0
- package/dist/src/lib/function-document-types.js +370 -0
- package/dist/src/lib/function-document-types.js.map +1 -0
- package/dist/src/lib/function-grants-preflight.d.ts +64 -0
- package/dist/src/lib/function-grants-preflight.js +105 -0
- package/dist/src/lib/function-grants-preflight.js.map +1 -0
- package/dist/src/lib/function-log-lines.d.ts +76 -0
- package/dist/src/lib/function-log-lines.js +160 -0
- package/dist/src/lib/function-log-lines.js.map +1 -0
- package/dist/src/lib/function-log-row.d.ts +29 -0
- package/dist/src/lib/function-log-row.js +73 -0
- package/dist/src/lib/function-log-row.js.map +1 -0
- package/dist/src/lib/function-log-tail.d.ts +132 -0
- package/dist/src/lib/function-log-tail.js +262 -0
- package/dist/src/lib/function-log-tail.js.map +1 -0
- package/dist/src/lib/function-run.d.ts +289 -0
- package/dist/src/lib/function-run.js +389 -0
- package/dist/src/lib/function-run.js.map +1 -0
- package/dist/src/lib/function-schema-codegen.d.ts +143 -0
- package/dist/src/lib/function-schema-codegen.js +420 -0
- package/dist/src/lib/function-schema-codegen.js.map +1 -0
- package/dist/src/lib/function-sync.d.ts +459 -0
- package/dist/src/lib/function-sync.js +1258 -0
- package/dist/src/lib/function-sync.js.map +1 -0
- package/dist/src/lib/function-trigger-listing.d.ts +27 -0
- package/dist/src/lib/function-trigger-listing.js +70 -0
- package/dist/src/lib/function-trigger-listing.js.map +1 -0
- package/dist/src/lib/function-typecheck.d.ts +86 -0
- package/dist/src/lib/function-typecheck.js +370 -0
- package/dist/src/lib/function-typecheck.js.map +1 -0
- package/dist/src/lib/function-versions.d.ts +122 -0
- package/dist/src/lib/function-versions.js +182 -0
- package/dist/src/lib/function-versions.js.map +1 -0
- package/dist/src/lib/generated-allowlist.js +4 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -1
- package/dist/src/lib/generated-config-surfaces.d.ts +2332 -78
- package/dist/src/lib/generated-config-surfaces.js +6979 -468
- package/dist/src/lib/generated-config-surfaces.js.map +1 -1
- package/dist/src/lib/generated-sdk-types.d.ts +12 -0
- package/dist/src/lib/generated-sdk-types.js +13 -0
- package/dist/src/lib/generated-sdk-types.js.map +1 -0
- package/dist/src/lib/init-adopt.js +2 -2
- package/dist/src/lib/init-adopt.js.map +1 -1
- package/dist/src/lib/init-assets.js +3 -3
- package/dist/src/lib/init-config.d.ts +1 -1
- package/dist/src/lib/init-ios-links.d.ts +6 -5
- package/dist/src/lib/init-ios-links.js +9 -8
- package/dist/src/lib/init-ios-links.js.map +1 -1
- package/dist/src/lib/init-plan.d.ts +1 -1
- package/dist/src/lib/init-plan.js +1 -1
- package/dist/src/lib/init-xcode.d.ts +6 -5
- package/dist/src/lib/init-xcode.js +34 -10
- package/dist/src/lib/init-xcode.js.map +1 -1
- package/dist/src/lib/ios-app-id.d.ts +34 -0
- package/dist/src/lib/ios-app-id.js +69 -0
- package/dist/src/lib/ios-app-id.js.map +1 -0
- package/dist/src/lib/list-options.d.ts +68 -0
- package/dist/src/lib/list-options.js +89 -0
- package/dist/src/lib/list-options.js.map +1 -0
- package/dist/src/lib/local-state.d.ts +1 -1
- package/dist/src/lib/local-state.js +1 -1
- package/dist/src/lib/local-test-cases.d.ts +1 -1
- package/dist/src/lib/local-test-cases.js +2 -1
- package/dist/src/lib/local-test-cases.js.map +1 -1
- package/dist/src/lib/log-inspection.d.ts +152 -5
- package/dist/src/lib/log-inspection.js +179 -2
- package/dist/src/lib/log-inspection.js.map +1 -1
- package/dist/src/lib/logout-admin-session.d.ts +33 -0
- package/dist/src/lib/logout-admin-session.js +70 -0
- package/dist/src/lib/logout-admin-session.js.map +1 -0
- package/dist/src/lib/output.d.ts +15 -0
- package/dist/src/lib/output.js +28 -0
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/paginate.d.ts +15 -0
- package/dist/src/lib/paginate.js +17 -0
- package/dist/src/lib/paginate.js.map +1 -1
- package/dist/src/lib/project-config.d.ts +38 -21
- package/dist/src/lib/project-config.js +57 -47
- package/dist/src/lib/project-config.js.map +1 -1
- package/dist/src/lib/prompt-cost-format.d.ts +11 -0
- package/dist/src/lib/prompt-cost-format.js +41 -0
- package/dist/src/lib/prompt-cost-format.js.map +1 -0
- package/dist/src/lib/prompt-schema-codegen.d.ts +147 -0
- package/dist/src/lib/prompt-schema-codegen.js +462 -0
- package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
- package/dist/src/lib/refresh-admin-credentials.d.ts +9 -1
- package/dist/src/lib/refresh-admin-credentials.js +22 -2
- package/dist/src/lib/refresh-admin-credentials.js.map +1 -1
- package/dist/src/lib/resolve-owner.d.ts +19 -0
- package/dist/src/lib/resolve-owner.js +20 -0
- package/dist/src/lib/resolve-owner.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +49 -3
- package/dist/src/lib/skill-installer.js +275 -100
- package/dist/src/lib/skill-installer.js.map +1 -1
- package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
- package/dist/src/lib/snapshot-audit-source.js +58 -0
- package/dist/src/lib/snapshot-audit-source.js.map +1 -0
- package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
- package/dist/src/lib/snapshot-audit-store.js +196 -0
- package/dist/src/lib/snapshot-audit-store.js.map +1 -0
- package/dist/src/lib/snapshot-audit.d.ts +207 -0
- package/dist/src/lib/snapshot-audit.js +431 -0
- package/dist/src/lib/snapshot-audit.js.map +1 -0
- package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
- package/dist/src/lib/snapshot-build-rows.js +87 -0
- package/dist/src/lib/snapshot-build-rows.js.map +1 -0
- package/dist/src/lib/snapshot-build.d.ts +50 -0
- package/dist/src/lib/snapshot-build.js +111 -0
- package/dist/src/lib/snapshot-build.js.map +1 -0
- package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
- package/dist/src/lib/snapshot-manifest-layout.js +70 -0
- package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
- package/dist/src/lib/snapshots.d.ts +8 -9
- package/dist/src/lib/snapshots.js +20 -83
- package/dist/src/lib/snapshots.js.map +1 -1
- package/dist/src/lib/step-run-table.d.ts +43 -0
- package/dist/src/lib/step-run-table.js +129 -0
- package/dist/src/lib/step-run-table.js.map +1 -0
- package/dist/src/lib/storage-pending-retry.d.ts +26 -0
- package/dist/src/lib/storage-pending-retry.js +42 -0
- package/dist/src/lib/storage-pending-retry.js.map +1 -0
- package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/banners.d.ts +24 -0
- package/dist/src/lib/swift-codegen/banners.js +25 -0
- package/dist/src/lib/swift-codegen/banners.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js +19 -7
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +139 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js +462 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/generator.d.ts +7 -1
- package/dist/src/lib/swift-codegen/generator.js +28 -11
- package/dist/src/lib/swift-codegen/generator.js.map +1 -1
- package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +15 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js +21 -4
- package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -1
- package/dist/src/lib/sync-paths.d.ts +72 -55
- package/dist/src/lib/sync-paths.js +97 -100
- package/dist/src/lib/sync-paths.js.map +1 -1
- package/dist/src/lib/sync-resource-types.d.ts +23 -4
- package/dist/src/lib/sync-resource-types.js +106 -8
- package/dist/src/lib/sync-resource-types.js.map +1 -1
- package/dist/src/lib/sync-selectors.d.ts +45 -2
- package/dist/src/lib/sync-selectors.js +66 -5
- package/dist/src/lib/sync-selectors.js.map +1 -1
- package/dist/src/lib/template.d.ts +2 -2
- package/dist/src/lib/template.js +2 -2
- package/dist/src/lib/test-case-keys.d.ts +1 -1
- package/dist/src/lib/test-case-keys.js +1 -1
- package/dist/src/lib/test-case-variables.d.ts +14 -0
- package/dist/src/lib/test-case-variables.js +42 -0
- package/dist/src/lib/test-case-variables.js.map +1 -1
- package/dist/src/lib/toml-database-config.js +19 -2
- package/dist/src/lib/toml-database-config.js.map +1 -1
- package/dist/src/lib/webhook-deliver.d.ts +209 -0
- package/dist/src/lib/webhook-deliver.js +519 -0
- package/dist/src/lib/webhook-deliver.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generator.d.ts +1 -1
- package/dist/src/lib/workflow-codegen/generator.js +1 -1
- package/dist/src/lib/workflow-toml-validator.d.ts +17 -8
- package/dist/src/lib/workflow-toml-validator.js +25 -12
- package/dist/src/lib/workflow-toml-validator.js.map +1 -1
- package/dist/src/lib/workflow-usage.d.ts +196 -0
- package/dist/src/lib/workflow-usage.js +310 -0
- package/dist/src/lib/workflow-usage.js.map +1 -0
- package/dist/src/types/index.d.ts +12 -2
- package/package.json +5 -4
|
@@ -1,7 +1,13 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { ApiClient } from "../lib/api-client.js";
|
|
2
4
|
import { resolveAppId } from "../lib/config.js";
|
|
3
|
-
import { success, error, info, keyValue, result as printResult, formatTable, formatId, formatDate, json, } from "../lib/output.js";
|
|
5
|
+
import { success, error, info, warn, keyValue, result as printResult, formatTable, formatId, formatDate, json, flushOutput, } from "../lib/output.js";
|
|
4
6
|
import { confirmPrompt } from "../lib/confirm-prompt.js";
|
|
7
|
+
import { pageCursorOption, pageLimitOption, parsePageLimit, printEmptyPage, printPageHint, } from "../lib/list-options.js";
|
|
8
|
+
import { normalizeCliListEnvelope, wholeListEnvelope, } from "../lib/paginate.js";
|
|
9
|
+
import { resolveOwnerUserId } from "../lib/resolve-owner.js";
|
|
10
|
+
import { COLLECTION_EXPORT_FILENAME, COLLECTION_EXPORT_VERSION, duplicateNameSources, emptyImportSummary, grantDisposition, grantUpdateLine, importExitCode, planCollection, readCollectionExportFile, serverReason, } from "../lib/collection-export.js";
|
|
5
11
|
export function registerCollectionsCommands(program) {
|
|
6
12
|
const collections = program
|
|
7
13
|
.command("collections")
|
|
@@ -10,6 +16,7 @@ export function registerCollectionsCommands(program) {
|
|
|
10
16
|
Examples:
|
|
11
17
|
$ primitive collections list
|
|
12
18
|
$ primitive collections create "Sprint 12"
|
|
19
|
+
$ primitive collections create "Sprint 12" --owner user@example.com
|
|
13
20
|
$ primitive collections get <collection-id>
|
|
14
21
|
$ primitive collections documents list <collection-id>
|
|
15
22
|
$ primitive collections documents add <collection-id> <document-id>
|
|
@@ -27,21 +34,25 @@ Examples:
|
|
|
27
34
|
.argument("[app-id]", "App ID (uses current app if not specified)")
|
|
28
35
|
.option("--app <app-id>", "App ID")
|
|
29
36
|
.option("--all", "List every collection in the app (admin-only). Default is collections you're a direct member of.")
|
|
37
|
+
.addOption(pageLimitOption())
|
|
38
|
+
.addOption(pageCursorOption())
|
|
30
39
|
.option("--json", "Output as JSON")
|
|
31
40
|
.action(async (appId, options) => {
|
|
41
|
+
const limit = parsePageLimit(options.limit);
|
|
32
42
|
const resolvedAppId = resolveAppId(appId, options);
|
|
33
43
|
const client = new ApiClient();
|
|
34
44
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
const params = { limit, cursor: options.cursor };
|
|
46
|
+
const page = normalizeCliListEnvelope(options.all
|
|
47
|
+
? await client.listAllCollections(resolvedAppId, params)
|
|
48
|
+
: await client.listCollections(resolvedAppId, params));
|
|
49
|
+
const list = page.items;
|
|
39
50
|
if (options.json) {
|
|
40
|
-
json(
|
|
51
|
+
json(page);
|
|
41
52
|
return;
|
|
42
53
|
}
|
|
43
54
|
if (list.length === 0) {
|
|
44
|
-
|
|
55
|
+
printEmptyPage(page, "collections");
|
|
45
56
|
return;
|
|
46
57
|
}
|
|
47
58
|
const columns = [
|
|
@@ -56,9 +67,7 @@ Examples:
|
|
|
56
67
|
columns.splice(4, 0, { header: "PERMISSION", key: "permission", format: (v) => v || "—" });
|
|
57
68
|
}
|
|
58
69
|
console.log(formatTable(list, columns));
|
|
59
|
-
|
|
60
|
-
info(`More results available. Use --json to get the cursor for pagination.`);
|
|
61
|
-
}
|
|
70
|
+
printPageHint(page);
|
|
62
71
|
}
|
|
63
72
|
catch (err) {
|
|
64
73
|
// Friendly message when --all is used by a non-admin
|
|
@@ -78,12 +87,45 @@ Examples:
|
|
|
78
87
|
.option("--description <description>", "Collection description")
|
|
79
88
|
.option("--initial-metadata <json>", "Create-time resource metadata as JSON, keyed by category name (issue #1420)")
|
|
80
89
|
.option("--app <app-id>", "App ID")
|
|
90
|
+
.option("--owner <userId-or-email>", "User ID or email to own the collection (super-admin or assigned-console-admin tokens only)")
|
|
81
91
|
.option("--json", "Output as JSON")
|
|
92
|
+
.addHelpText("after", `
|
|
93
|
+
Ownership:
|
|
94
|
+
An app-user token — member, admin, or owner — always creates the collection
|
|
95
|
+
owned by the caller: the server ignores --owner for those tokens.
|
|
96
|
+
A super-admin token, or a console-admin token assigned to this app, acts
|
|
97
|
+
through an admin shadow app user — without --owner that shadow user owns the
|
|
98
|
+
collection, with --owner the named user does.
|
|
99
|
+
A console admin not assigned to this app has no access to the app at all.
|
|
100
|
+
|
|
101
|
+
The owner is the collection's creator, so under the default rules they — and
|
|
102
|
+
no other non-admin — can edit it, delete it, and manage its documents and
|
|
103
|
+
members.
|
|
104
|
+
|
|
105
|
+
An email given to --owner is resolved to a user id before anything is
|
|
106
|
+
created; a user who is not in the app fails the command without creating a
|
|
107
|
+
collection.
|
|
108
|
+
`)
|
|
82
109
|
.action(async (name, options) => {
|
|
83
110
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
84
111
|
const client = new ApiClient();
|
|
112
|
+
// Resolve --owner to a userId through the one shared lookup (#2763),
|
|
113
|
+
// before the create: an email nobody answers to must fail with nothing
|
|
114
|
+
// created, not leave a collection owned by the admin.
|
|
115
|
+
let ownerUserId;
|
|
116
|
+
if (options.owner) {
|
|
117
|
+
try {
|
|
118
|
+
ownerUserId = await resolveOwnerUserId(client, resolvedAppId, options.owner);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
error(err.message);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
85
125
|
try {
|
|
86
126
|
const data = { name };
|
|
127
|
+
if (ownerUserId)
|
|
128
|
+
data.createdBy = ownerUserId;
|
|
87
129
|
if (options.description)
|
|
88
130
|
data.description = options.description;
|
|
89
131
|
// Create-time per-category ResourceMetadata rows (issue #1420).
|
|
@@ -104,6 +146,9 @@ Examples:
|
|
|
104
146
|
success("Collection created.");
|
|
105
147
|
keyValue("Collection ID", result.collectionId);
|
|
106
148
|
keyValue("Name", result.name);
|
|
149
|
+
// Read back from the RESPONSE, not from --owner: whom the server
|
|
150
|
+
// credited is the answer, and for a non-admin token it is the caller.
|
|
151
|
+
keyValue("Owner", result.createdBy);
|
|
107
152
|
if (result.description)
|
|
108
153
|
keyValue("Description", result.description);
|
|
109
154
|
}
|
|
@@ -180,6 +225,7 @@ Examples:
|
|
|
180
225
|
.argument("<collection-id>", "Collection ID")
|
|
181
226
|
.option("--app <app-id>", "App ID")
|
|
182
227
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
228
|
+
.option("--json", "Output as JSON")
|
|
183
229
|
.action(async (collectionId, options) => {
|
|
184
230
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
185
231
|
if (!options.yes) {
|
|
@@ -199,6 +245,13 @@ Examples:
|
|
|
199
245
|
const client = new ApiClient();
|
|
200
246
|
try {
|
|
201
247
|
await client.deleteCollection(resolvedAppId, collectionId);
|
|
248
|
+
// What a script gets to confirm the removal (#3647). The server's body
|
|
249
|
+
// here is a bare `{ success: true }` that names nothing, so the ids are
|
|
250
|
+
// named by the CLI, as `documents transfer-owner` does.
|
|
251
|
+
if (options.json) {
|
|
252
|
+
json({ success: true, collectionId });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
202
255
|
success(`Collection ${collectionId} deleted.`);
|
|
203
256
|
}
|
|
204
257
|
catch (err) {
|
|
@@ -293,6 +346,7 @@ Examples:
|
|
|
293
346
|
.requiredOption("--group <group>", "Group in format type/id (e.g. team/engineering)")
|
|
294
347
|
.option("--app <app-id>", "App ID")
|
|
295
348
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
349
|
+
.option("--json", "Output as JSON")
|
|
296
350
|
.action(async (collectionId, options) => {
|
|
297
351
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
298
352
|
const parts = options.group.split("/");
|
|
@@ -318,6 +372,10 @@ Examples:
|
|
|
318
372
|
const client = new ApiClient();
|
|
319
373
|
try {
|
|
320
374
|
await client.revokeCollectionGroupPermission(resolvedAppId, collectionId, groupType, groupId);
|
|
375
|
+
if (options.json) {
|
|
376
|
+
json({ success: true, collectionId, groupType, groupId });
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
321
379
|
success(`Revoked ${groupType}/${groupId} access from collection.`);
|
|
322
380
|
}
|
|
323
381
|
catch (err) {
|
|
@@ -335,28 +393,32 @@ Examples:
|
|
|
335
393
|
.description("List documents in a collection")
|
|
336
394
|
.argument("<collection-id>", "Collection ID")
|
|
337
395
|
.option("--app <app-id>", "App ID")
|
|
396
|
+
.addOption(pageLimitOption())
|
|
397
|
+
.addOption(pageCursorOption())
|
|
338
398
|
.option("--json", "Output as JSON")
|
|
339
399
|
.action(async (collectionId, options) => {
|
|
400
|
+
const limit = parsePageLimit(options.limit);
|
|
340
401
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
341
402
|
const client = new ApiClient();
|
|
342
403
|
try {
|
|
343
|
-
const
|
|
344
|
-
|
|
404
|
+
const page = normalizeCliListEnvelope(await client.listCollectionDocuments(resolvedAppId, collectionId, {
|
|
405
|
+
limit,
|
|
406
|
+
cursor: options.cursor,
|
|
407
|
+
}));
|
|
408
|
+
const list = page.items;
|
|
345
409
|
if (options.json) {
|
|
346
|
-
json(
|
|
410
|
+
json(page);
|
|
347
411
|
return;
|
|
348
412
|
}
|
|
349
413
|
if (list.length === 0) {
|
|
350
|
-
|
|
414
|
+
printEmptyPage(page, "documents", "No documents in this collection.");
|
|
351
415
|
return;
|
|
352
416
|
}
|
|
353
417
|
console.log(formatTable(list, [
|
|
354
418
|
{ header: "DOCUMENT_ID", key: "documentId", format: formatId },
|
|
355
419
|
{ header: "ADDED", key: "addedAt", format: formatDate },
|
|
356
420
|
]));
|
|
357
|
-
|
|
358
|
-
info(`More results available. Use --json to get the cursor for pagination.`);
|
|
359
|
-
}
|
|
421
|
+
printPageHint(page);
|
|
360
422
|
}
|
|
361
423
|
catch (err) {
|
|
362
424
|
error(err.message);
|
|
@@ -397,6 +459,7 @@ Examples:
|
|
|
397
459
|
.argument("<document-id>", "Document ID")
|
|
398
460
|
.option("--app <app-id>", "App ID")
|
|
399
461
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
462
|
+
.option("--json", "Output as JSON")
|
|
400
463
|
.action(async (collectionId, documentId, options) => {
|
|
401
464
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
402
465
|
if (!options.yes) {
|
|
@@ -416,6 +479,10 @@ Examples:
|
|
|
416
479
|
const client = new ApiClient();
|
|
417
480
|
try {
|
|
418
481
|
await client.removeCollectionDocument(resolvedAppId, collectionId, documentId);
|
|
482
|
+
if (options.json) {
|
|
483
|
+
json({ success: true, collectionId, documentId });
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
419
486
|
success(`Document ${documentId} removed from collection.`);
|
|
420
487
|
}
|
|
421
488
|
catch (err) {
|
|
@@ -481,8 +548,9 @@ Examples:
|
|
|
481
548
|
const result = await client.getCollectionAccess(resolvedAppId, collectionId);
|
|
482
549
|
const list = result.members ?? [];
|
|
483
550
|
if (options.json) {
|
|
484
|
-
// The members
|
|
485
|
-
|
|
551
|
+
// The members half of the access payload, in the one list envelope
|
|
552
|
+
// (#3646) — the same shape `groups members list` prints (#2764).
|
|
553
|
+
json(wholeListEnvelope(list));
|
|
486
554
|
return;
|
|
487
555
|
}
|
|
488
556
|
if (list.length === 0) {
|
|
@@ -535,6 +603,7 @@ Examples:
|
|
|
535
603
|
.argument("<user-id>", "User ID")
|
|
536
604
|
.option("--app <app-id>", "App ID")
|
|
537
605
|
.option("-y, --yes", "Skip confirmation prompt")
|
|
606
|
+
.option("--json", "Output as JSON")
|
|
538
607
|
.action(async (collectionId, userId, options) => {
|
|
539
608
|
const resolvedAppId = resolveAppId(undefined, options);
|
|
540
609
|
if (!options.yes) {
|
|
@@ -554,6 +623,10 @@ Examples:
|
|
|
554
623
|
const client = new ApiClient();
|
|
555
624
|
try {
|
|
556
625
|
await client.removeCollectionMember(resolvedAppId, collectionId, userId);
|
|
626
|
+
if (options.json) {
|
|
627
|
+
json({ success: true, collectionId, userId });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
557
630
|
success(`User ${userId} removed from collection.`);
|
|
558
631
|
}
|
|
559
632
|
catch (err) {
|
|
@@ -561,5 +634,649 @@ Examples:
|
|
|
561
634
|
process.exit(1);
|
|
562
635
|
}
|
|
563
636
|
});
|
|
637
|
+
registerCollectionsExport(collections);
|
|
638
|
+
registerCollectionsImport(collections);
|
|
639
|
+
}
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
// Export (#3643)
|
|
642
|
+
// ---------------------------------------------------------------------------
|
|
643
|
+
/**
|
|
644
|
+
* `primitive collections export` — the collections half of a migration.
|
|
645
|
+
*
|
|
646
|
+
* `documents export-all` writes `manifest.json` and a directory per document;
|
|
647
|
+
* this writes `collections.json` beside them, so one directory carries the
|
|
648
|
+
* whole app. Owner and member identities travel as EMAILS wherever the server
|
|
649
|
+
* can resolve one: users are app-scoped (`models.yaml`), so the same person is
|
|
650
|
+
* a different userId in every app and a recorded source userId means nothing
|
|
651
|
+
* in the target.
|
|
652
|
+
*/
|
|
653
|
+
function registerCollectionsExport(collections) {
|
|
654
|
+
collections
|
|
655
|
+
.command("export")
|
|
656
|
+
.description("Export every collection in an app (name, owner, members, documents, group grants)")
|
|
657
|
+
.option("--app <app-id>", "App ID")
|
|
658
|
+
.option("--output <dir>", "Output directory", "./primitive-export")
|
|
659
|
+
.option("--json", "Output as JSON")
|
|
660
|
+
.addHelpText("after", `
|
|
661
|
+
Admin only: the export reads the app-wide collection listing.
|
|
662
|
+
|
|
663
|
+
It writes <output>/collections.json, beside the manifest.json and documents/
|
|
664
|
+
that 'documents export-all' writes, so one directory is one app's migration.
|
|
665
|
+
Import it with 'primitive collections import' AFTER 'documents import', which
|
|
666
|
+
preserves document ids.
|
|
667
|
+
|
|
668
|
+
Owner and member identities are recorded as emails wherever the server can
|
|
669
|
+
resolve one — users are app-scoped, so a source app's userId means nothing in
|
|
670
|
+
the target. A userId with no resolvable email is recorded as-is and will only
|
|
671
|
+
import back into the same app.
|
|
672
|
+
|
|
673
|
+
Not exported: collection ids (the server mints new ones), collection resource
|
|
674
|
+
metadata, the groups themselves, pending member invitations.
|
|
675
|
+
`)
|
|
676
|
+
.action(async (options) => {
|
|
677
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
678
|
+
const client = new ApiClient();
|
|
679
|
+
try {
|
|
680
|
+
await exportCollections(client, resolvedAppId, options);
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
error(err.message);
|
|
684
|
+
await flushOutput();
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
/** Follow every page of the admin collection listing. */
|
|
690
|
+
async function readAllCollections(client, appId) {
|
|
691
|
+
const rows = [];
|
|
692
|
+
let cursor;
|
|
693
|
+
do {
|
|
694
|
+
const page = await client.listAllCollections(appId, {
|
|
695
|
+
limit: 200,
|
|
696
|
+
...(cursor ? { cursor } : {}),
|
|
697
|
+
});
|
|
698
|
+
rows.push(...(page.items ?? []));
|
|
699
|
+
cursor = page.nextCursor ?? undefined;
|
|
700
|
+
} while (cursor);
|
|
701
|
+
return rows;
|
|
702
|
+
}
|
|
703
|
+
/** Follow every page of one collection's document list. */
|
|
704
|
+
async function readAllCollectionDocuments(client, appId, collectionId) {
|
|
705
|
+
const rows = [];
|
|
706
|
+
let cursor;
|
|
707
|
+
do {
|
|
708
|
+
const page = await client.listCollectionDocuments(appId, collectionId, {
|
|
709
|
+
limit: 200,
|
|
710
|
+
...(cursor ? { cursor } : {}),
|
|
711
|
+
});
|
|
712
|
+
for (const item of page.items ?? []) {
|
|
713
|
+
rows.push({
|
|
714
|
+
documentId: item.documentId,
|
|
715
|
+
addedAt: item.addedAt ?? null,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
cursor = page.nextCursor ?? undefined;
|
|
719
|
+
} while (cursor);
|
|
720
|
+
return rows;
|
|
721
|
+
}
|
|
722
|
+
async function exportCollections(client, appId, options) {
|
|
723
|
+
const rows = await readAllCollections(client, appId);
|
|
724
|
+
// One lookup per DISTINCT userId across the whole run, cached: a busy app's
|
|
725
|
+
// owner appears on every collection it owns and again in each one's members.
|
|
726
|
+
const emailCache = new Map();
|
|
727
|
+
const emailFor = async (userId) => {
|
|
728
|
+
if (emailCache.has(userId))
|
|
729
|
+
return emailCache.get(userId);
|
|
730
|
+
let email;
|
|
731
|
+
try {
|
|
732
|
+
const found = await client.listUsers(appId, { userId, limit: 1 });
|
|
733
|
+
email = (found.items ?? [])[0]?.email || undefined;
|
|
734
|
+
}
|
|
735
|
+
catch {
|
|
736
|
+
// An unresolvable email is not a reason to lose the collection: the
|
|
737
|
+
// userId is still recorded, and a same-app import can still use it.
|
|
738
|
+
email = undefined;
|
|
739
|
+
}
|
|
740
|
+
emailCache.set(userId, email);
|
|
741
|
+
return email;
|
|
742
|
+
};
|
|
743
|
+
const identity = async (userId) => {
|
|
744
|
+
const email = await emailFor(userId);
|
|
745
|
+
return email ? { userId, email } : { userId };
|
|
746
|
+
};
|
|
747
|
+
const exported = [];
|
|
748
|
+
let skipped = 0;
|
|
749
|
+
for (const row of rows) {
|
|
750
|
+
const collectionId = row.collectionId;
|
|
751
|
+
try {
|
|
752
|
+
const documents = await readAllCollectionDocuments(client, appId, collectionId);
|
|
753
|
+
const access = await client.getCollectionAccess(appId, collectionId);
|
|
754
|
+
const members = [];
|
|
755
|
+
for (const m of access?.members ?? []) {
|
|
756
|
+
members.push({
|
|
757
|
+
...(await identity(m.userId)),
|
|
758
|
+
permission: m.permission,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
const groups = (access?.groups ?? [])
|
|
762
|
+
// The server already filters system groups out of this response; a
|
|
763
|
+
// `_`-prefixed type could never be re-granted anyway (it is reserved).
|
|
764
|
+
.filter((g) => !String(g.groupType).startsWith("_"))
|
|
765
|
+
.map((g) => ({
|
|
766
|
+
groupType: g.groupType,
|
|
767
|
+
groupId: g.groupId,
|
|
768
|
+
permission: g.permission,
|
|
769
|
+
}));
|
|
770
|
+
exported.push({
|
|
771
|
+
collectionId,
|
|
772
|
+
name: row.name,
|
|
773
|
+
description: row.description ?? null,
|
|
774
|
+
collectionType: row.collectionType ?? null,
|
|
775
|
+
contextId: row.contextId ?? null,
|
|
776
|
+
createdAt: row.createdAt ?? null,
|
|
777
|
+
owner: row.createdBy ? await identity(row.createdBy) : null,
|
|
778
|
+
documents,
|
|
779
|
+
members,
|
|
780
|
+
groups,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
catch (err) {
|
|
784
|
+
skipped += 1;
|
|
785
|
+
warn(`Skipping collection "${row.name}" (${collectionId}): ${err.message}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const filePath = path.join(options.output, COLLECTION_EXPORT_FILENAME);
|
|
789
|
+
// Each run REPLACES the file. A collection the previous one listed and this
|
|
790
|
+
// one does not becomes unreachable through the file an import reads, so say
|
|
791
|
+
// so — the same warning `documents export-all` gives for its manifest.
|
|
792
|
+
if (fs.existsSync(filePath)) {
|
|
793
|
+
try {
|
|
794
|
+
const previous = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
795
|
+
const kept = new Set(exported.map((c) => c.collectionId));
|
|
796
|
+
const dropped = (Array.isArray(previous?.collections) ? previous.collections : []).filter((c) => !kept.has(c?.collectionId));
|
|
797
|
+
if (dropped.length > 0) {
|
|
798
|
+
warn(`The ${COLLECTION_EXPORT_FILENAME} already in ${options.output} lists ` +
|
|
799
|
+
`${dropped.length} collection(s) this export does not: ` +
|
|
800
|
+
`${dropped.map((c) => c?.name ?? c?.collectionId).join(", ")}. ` +
|
|
801
|
+
`Replacing it leaves them unreachable through the file an import reads.`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
// An unreadable previous file is not a reason to refuse the export.
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
const file = {
|
|
809
|
+
version: COLLECTION_EXPORT_VERSION,
|
|
810
|
+
exportedAt: new Date().toISOString(),
|
|
811
|
+
sourceAppId: appId,
|
|
812
|
+
collectionCount: exported.length,
|
|
813
|
+
collections: exported,
|
|
814
|
+
};
|
|
815
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
816
|
+
fs.writeFileSync(filePath, JSON.stringify(file, null, 2));
|
|
817
|
+
if (options.json) {
|
|
818
|
+
// Counts and the path, not the bodies: the file is where those live.
|
|
819
|
+
json({
|
|
820
|
+
version: file.version,
|
|
821
|
+
exportedAt: file.exportedAt,
|
|
822
|
+
sourceAppId: file.sourceAppId,
|
|
823
|
+
collectionCount: file.collectionCount,
|
|
824
|
+
skipped,
|
|
825
|
+
path: filePath,
|
|
826
|
+
});
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
success(`Exported ${exported.length} collection(s) to ${filePath}`);
|
|
830
|
+
}
|
|
831
|
+
// ---------------------------------------------------------------------------
|
|
832
|
+
// Import (#3643)
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
/**
|
|
835
|
+
* `primitive collections import` — the other half of the migration.
|
|
836
|
+
*
|
|
837
|
+
* Two phases, in this order and never interleaved:
|
|
838
|
+
*
|
|
839
|
+
* 1. RESOLVE, reads only. Every owner, member, document and group the file
|
|
840
|
+
* names is looked up in the target app and the plan is decided from the
|
|
841
|
+
* answers, before anything at all is written. Only a DEFINITIVE absence
|
|
842
|
+
* (a 404, or `exists: false` from the email lookup) reads as "not here";
|
|
843
|
+
* any other failure fails the run with the collection and the item named,
|
|
844
|
+
* exactly as `documents import` decides its root-document plans (#3096).
|
|
845
|
+
* 2. APPLY, in the order create → group grants → members → documents. Grants
|
|
846
|
+
* go first ON PURPOSE: `addDocument` materializes every existing grant
|
|
847
|
+
* onto a newly added document, so a fresh collection's access never
|
|
848
|
+
* depends on the grant's own fan-out over the document list (F4).
|
|
849
|
+
*/
|
|
850
|
+
function registerCollectionsImport(collections) {
|
|
851
|
+
collections
|
|
852
|
+
.command("import")
|
|
853
|
+
.description("Recreate collections from a collections.json export (owner, members, documents, group grants)")
|
|
854
|
+
.argument("<path>", "Export directory holding collections.json, or the file itself")
|
|
855
|
+
.option("--app <app-id>", "App ID")
|
|
856
|
+
.option("--owner <userId-or-email>", "Own every imported collection with this user, instead of the owner each one records")
|
|
857
|
+
.option("--overwrite", "Merge into a collection that already carries the same name")
|
|
858
|
+
.option("--dry-run", "Show the plan and the problems without writing anything")
|
|
859
|
+
.option("--json", "Output as JSON")
|
|
860
|
+
.addHelpText("after", `
|
|
861
|
+
For admin tokens. Assigning an owner is admin-only, so under an app-user token
|
|
862
|
+
the server creates every collection owned by the caller and each one whose
|
|
863
|
+
recorded owner is somebody else is reported as an 'owner' problem.
|
|
864
|
+
|
|
865
|
+
Run 'primitive documents import' FIRST: it preserves document ids, which is
|
|
866
|
+
what the collections file refers to.
|
|
867
|
+
|
|
868
|
+
Owners and members are resolved by EMAIL — users are app-scoped, so a source
|
|
869
|
+
app's userId means nothing here. A recorded userId is used only when the file
|
|
870
|
+
came from this same app.
|
|
871
|
+
|
|
872
|
+
Matching is by name. Without --overwrite a name already in the target is
|
|
873
|
+
skipped; with it, the collection is merged — but only when its owner, type and
|
|
874
|
+
context id match, since none of those can be changed after creation.
|
|
875
|
+
|
|
876
|
+
A group grant the target already holds at another level is UPDATED to the level
|
|
877
|
+
the file records, and what the group can reach follows the change. On a
|
|
878
|
+
collection holding more than 200 documents the server refuses a collection-wide
|
|
879
|
+
group change whole (COLLECTION_FANOUT_LIMIT): that is reported as a 'group'
|
|
880
|
+
problem, and the rest of the collection is still merged.
|
|
881
|
+
|
|
882
|
+
Exit code 1 when any per-item problem was reported. A skip is not a problem.
|
|
883
|
+
`)
|
|
884
|
+
.action(async (importPath, options) => {
|
|
885
|
+
const resolvedAppId = resolveAppId(undefined, options);
|
|
886
|
+
const client = new ApiClient();
|
|
887
|
+
let code = 0;
|
|
888
|
+
try {
|
|
889
|
+
code = await importCollections(client, resolvedAppId, importPath, options);
|
|
890
|
+
}
|
|
891
|
+
catch (err) {
|
|
892
|
+
error(err.message);
|
|
893
|
+
code = 1;
|
|
894
|
+
}
|
|
895
|
+
await flushOutput();
|
|
896
|
+
if (code !== 0)
|
|
897
|
+
process.exit(code);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
/** A definitive "not here", as opposed to a read that simply failed. */
|
|
901
|
+
function isNotFound(err) {
|
|
902
|
+
return err?.statusCode === 404;
|
|
903
|
+
}
|
|
904
|
+
async function importCollections(client, appId, importPath, options) {
|
|
905
|
+
const file = readCollectionExportFile(importPath);
|
|
906
|
+
// A source userId is only meaningful when the file came from THIS app —
|
|
907
|
+
// users are tenant-scoped, so the same person has a different userId in
|
|
908
|
+
// every app (`models.yaml`).
|
|
909
|
+
const sameApp = file.sourceAppId === appId;
|
|
910
|
+
/** userId ← recorded userId: the admin read, cached, one per distinct id. */
|
|
911
|
+
const byUserId = new Map();
|
|
912
|
+
const resolveRecordedUserId = async (userId) => {
|
|
913
|
+
if (byUserId.has(userId))
|
|
914
|
+
return byUserId.get(userId);
|
|
915
|
+
const found = await client.listUsers(appId, { userId, limit: 1 });
|
|
916
|
+
const hit = (found.items ?? [])[0];
|
|
917
|
+
// A row with no role is a user who is no longer in this app.
|
|
918
|
+
const resolved = hit && hit.role ? hit.userId : null;
|
|
919
|
+
byUserId.set(userId, resolved);
|
|
920
|
+
return resolved;
|
|
921
|
+
};
|
|
922
|
+
// `--owner` is resolved ONCE, before a single collection of the file is
|
|
923
|
+
// looked at: a name nobody answers to must fail the run with nothing written.
|
|
924
|
+
let explicitOwnerUserId;
|
|
925
|
+
if (options.owner) {
|
|
926
|
+
explicitOwnerUserId = await resolveOwnerUserId(client, appId, options.owner);
|
|
927
|
+
if (!options.owner.includes("@")) {
|
|
928
|
+
// `resolveOwnerUserId` hands a value with no "@" straight back — it is a
|
|
929
|
+
// userId as far as the flag is concerned, but nothing has yet checked
|
|
930
|
+
// that this app has such a member. Left unchecked, an id nobody answers
|
|
931
|
+
// to planned a create for every collection, `--dry-run` reported that
|
|
932
|
+
// plan as fine, and only the real run found out, one failed create at a
|
|
933
|
+
// time. The flag fails the run here instead, the way an unknown email
|
|
934
|
+
// already does.
|
|
935
|
+
let found;
|
|
936
|
+
try {
|
|
937
|
+
found = await resolveRecordedUserId(explicitOwnerUserId);
|
|
938
|
+
}
|
|
939
|
+
catch (err) {
|
|
940
|
+
throw new Error(`Checking --owner ${explicitOwnerUserId} against app ${appId} ` +
|
|
941
|
+
`failed: ${err?.message ?? err}. Nothing has been written.`);
|
|
942
|
+
}
|
|
943
|
+
if (!found) {
|
|
944
|
+
throw new Error(`No app user found with id "${explicitOwnerUserId}" in app ${appId}.`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
const problems = [];
|
|
949
|
+
const summary = emptyImportSummary();
|
|
950
|
+
// ── Resolve phase ──────────────────────────────────────────────────────
|
|
951
|
+
const targetByName = new Map();
|
|
952
|
+
for (const row of await readAllCollections(client, appId)) {
|
|
953
|
+
if (!targetByName.has(row.name))
|
|
954
|
+
targetByName.set(row.name, row);
|
|
955
|
+
}
|
|
956
|
+
/** userId ← email, cached: one lookup per distinct email per run. */
|
|
957
|
+
const byEmail = new Map();
|
|
958
|
+
const resolveEmail = async (email) => {
|
|
959
|
+
if (byEmail.has(email))
|
|
960
|
+
return byEmail.get(email);
|
|
961
|
+
const lookup = await client.lookupUserByEmail(appId, email);
|
|
962
|
+
const userId = lookup?.exists && lookup.user ? lookup.user.userId : null;
|
|
963
|
+
byEmail.set(email, userId);
|
|
964
|
+
return userId;
|
|
965
|
+
};
|
|
966
|
+
/**
|
|
967
|
+
* One identity from the file to a userId in the target app.
|
|
968
|
+
*
|
|
969
|
+
* `{ userId: null, reason }` is a definitive absence to report; anything the
|
|
970
|
+
* reads could not settle has already thrown by the time this returns.
|
|
971
|
+
*/
|
|
972
|
+
const resolveIdentity = async (identity) => {
|
|
973
|
+
if (!identity) {
|
|
974
|
+
return { userId: null, reason: "the export records no identity for it", id: "(none)" };
|
|
975
|
+
}
|
|
976
|
+
const id = identity.email || identity.userId || "(none)";
|
|
977
|
+
if (identity.email) {
|
|
978
|
+
const userId = await resolveEmail(identity.email);
|
|
979
|
+
return userId
|
|
980
|
+
? { userId, id }
|
|
981
|
+
: {
|
|
982
|
+
userId: null,
|
|
983
|
+
id,
|
|
984
|
+
reason: `no user with email "${identity.email}" is a member of ${appId}`,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
if (identity.userId && sameApp) {
|
|
988
|
+
const userId = await resolveRecordedUserId(identity.userId);
|
|
989
|
+
return userId
|
|
990
|
+
? { userId, id }
|
|
991
|
+
: {
|
|
992
|
+
userId: null,
|
|
993
|
+
id,
|
|
994
|
+
reason: `no user with id ${identity.userId} is a member of ${appId}`,
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
return {
|
|
998
|
+
userId: null,
|
|
999
|
+
id,
|
|
1000
|
+
reason: `it records only a userId from app ${file.sourceAppId || "(unknown)"}, ` +
|
|
1001
|
+
`and users are scoped to their app — the same person has a different ` +
|
|
1002
|
+
`userId here, so only an email can identify them across apps`,
|
|
1003
|
+
};
|
|
1004
|
+
};
|
|
1005
|
+
const duplicates = duplicateNameSources(file.collections);
|
|
1006
|
+
const planned = [];
|
|
1007
|
+
for (const source of file.collections) {
|
|
1008
|
+
const note = (kind, id, reason) => {
|
|
1009
|
+
problems.push({
|
|
1010
|
+
sourceCollectionId: source.collectionId,
|
|
1011
|
+
name: source.name,
|
|
1012
|
+
kind,
|
|
1013
|
+
id,
|
|
1014
|
+
reason,
|
|
1015
|
+
});
|
|
1016
|
+
warn(`${source.name}: ${kind} ${id} — ${reason}`);
|
|
1017
|
+
};
|
|
1018
|
+
/** Any read that is not a definitive absence stops the whole run. */
|
|
1019
|
+
const fatal = (item, err) => {
|
|
1020
|
+
throw new Error(`Reading ${item} for collection "${source.name}" ` +
|
|
1021
|
+
`(${source.collectionId}) failed: ${err?.message ?? err}. ` +
|
|
1022
|
+
`Nothing has been written.`);
|
|
1023
|
+
};
|
|
1024
|
+
let ownerUserId = explicitOwnerUserId ?? null;
|
|
1025
|
+
let ownerReason;
|
|
1026
|
+
if (!ownerUserId) {
|
|
1027
|
+
try {
|
|
1028
|
+
const resolved = await resolveIdentity(source.owner);
|
|
1029
|
+
ownerUserId = resolved.userId;
|
|
1030
|
+
ownerReason = resolved.reason;
|
|
1031
|
+
}
|
|
1032
|
+
catch (err) {
|
|
1033
|
+
fatal(`owner ${source.owner?.email ?? source.owner?.userId ?? ""}`, err);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
const existingRow = targetByName.get(source.name);
|
|
1037
|
+
const row = planCollection(source, {
|
|
1038
|
+
ownerUserId,
|
|
1039
|
+
ownerReason,
|
|
1040
|
+
existing: existingRow
|
|
1041
|
+
? {
|
|
1042
|
+
collectionId: existingRow.collectionId,
|
|
1043
|
+
createdBy: existingRow.createdBy,
|
|
1044
|
+
collectionType: existingRow.collectionType ?? null,
|
|
1045
|
+
contextId: existingRow.contextId ?? null,
|
|
1046
|
+
}
|
|
1047
|
+
: null,
|
|
1048
|
+
duplicateOfSourceId: duplicates.get(source.collectionId) ?? null,
|
|
1049
|
+
}, { overwrite: Boolean(options.overwrite) });
|
|
1050
|
+
if (row.problem) {
|
|
1051
|
+
note(row.problem.kind, row.problem.id, row.problem.reason);
|
|
1052
|
+
}
|
|
1053
|
+
if (row.action === "refused" || row.action === "skip") {
|
|
1054
|
+
planned.push({ row, source, grants: [], members: [], documents: [] });
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
// What the target already grants, so a merge can compare levels.
|
|
1058
|
+
let targetGrants = [];
|
|
1059
|
+
if (row.action === "merge" && row.targetCollectionId) {
|
|
1060
|
+
try {
|
|
1061
|
+
const access = await client.getCollectionAccess(appId, row.targetCollectionId);
|
|
1062
|
+
targetGrants = (access?.groups ?? []).map((g) => ({
|
|
1063
|
+
groupType: g.groupType,
|
|
1064
|
+
groupId: g.groupId,
|
|
1065
|
+
permission: g.permission,
|
|
1066
|
+
}));
|
|
1067
|
+
}
|
|
1068
|
+
catch (err) {
|
|
1069
|
+
fatal(`the access of ${row.targetCollectionId}`, err);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const grants = [];
|
|
1073
|
+
for (const grant of source.groups ?? []) {
|
|
1074
|
+
const label = `${grant.groupType}/${grant.groupId}`;
|
|
1075
|
+
// The server reserves the `_` prefix for its own system groups; an
|
|
1076
|
+
// export never writes one, and re-granting one is a 400.
|
|
1077
|
+
if (String(grant.groupType).startsWith("_")) {
|
|
1078
|
+
warn(`${source.name}: skipping the reserved group type ${grant.groupType} ` +
|
|
1079
|
+
`(${label}); the server manages those itself.`);
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
try {
|
|
1083
|
+
await client.getGroup(appId, grant.groupType, grant.groupId);
|
|
1084
|
+
}
|
|
1085
|
+
catch (err) {
|
|
1086
|
+
if (isNotFound(err)) {
|
|
1087
|
+
note("group", label, `no group ${label} exists in ${appId}`);
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
fatal(`group ${label}`, err);
|
|
1091
|
+
}
|
|
1092
|
+
const disposition = grantDisposition(grant, targetGrants);
|
|
1093
|
+
const existing = targetGrants.find((g) => g.groupType === grant.groupType && g.groupId === grant.groupId);
|
|
1094
|
+
grants.push({
|
|
1095
|
+
grant,
|
|
1096
|
+
disposition,
|
|
1097
|
+
...(existing ? { existingPermission: existing.permission } : {}),
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
const members = [];
|
|
1101
|
+
for (const member of source.members ?? []) {
|
|
1102
|
+
let resolved;
|
|
1103
|
+
try {
|
|
1104
|
+
resolved = await resolveIdentity(member);
|
|
1105
|
+
}
|
|
1106
|
+
catch (err) {
|
|
1107
|
+
fatal(`member ${member.email ?? member.userId}`, err);
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
if (!resolved.userId) {
|
|
1111
|
+
note("member", resolved.id, resolved.reason);
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
members.push({ userId: resolved.userId, permission: member.permission });
|
|
1115
|
+
}
|
|
1116
|
+
const documents = [];
|
|
1117
|
+
for (const document of source.documents ?? []) {
|
|
1118
|
+
try {
|
|
1119
|
+
await client.getDocument(appId, document.documentId);
|
|
1120
|
+
}
|
|
1121
|
+
catch (err) {
|
|
1122
|
+
if (isNotFound(err)) {
|
|
1123
|
+
note("document", document.documentId, `no document ${document.documentId} exists in ${appId}; ` +
|
|
1124
|
+
`run 'primitive documents import' first — it preserves ids`);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
fatal(`document ${document.documentId}`, err);
|
|
1128
|
+
}
|
|
1129
|
+
documents.push(document.documentId);
|
|
1130
|
+
}
|
|
1131
|
+
planned.push({ row, source, grants, members, documents });
|
|
1132
|
+
}
|
|
1133
|
+
// ── Apply phase ────────────────────────────────────────────────────────
|
|
1134
|
+
for (const entry of planned) {
|
|
1135
|
+
const { row, source } = entry;
|
|
1136
|
+
const note = (kind, id, reason) => {
|
|
1137
|
+
problems.push({
|
|
1138
|
+
sourceCollectionId: source.collectionId,
|
|
1139
|
+
name: source.name,
|
|
1140
|
+
kind,
|
|
1141
|
+
id,
|
|
1142
|
+
reason,
|
|
1143
|
+
});
|
|
1144
|
+
warn(`${source.name}: ${kind} ${id} — ${reason}`);
|
|
1145
|
+
};
|
|
1146
|
+
if (row.action === "refused") {
|
|
1147
|
+
summary.refused += 1;
|
|
1148
|
+
info(`Refusing ${source.name}: ${row.reason}`);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
if (row.action === "skip") {
|
|
1152
|
+
summary.skipped += 1;
|
|
1153
|
+
info(`Skipping ${source.name}: ${row.reason}`);
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if (options.dryRun) {
|
|
1157
|
+
// The plan, and nothing else: not one write is issued.
|
|
1158
|
+
if (row.action === "create") {
|
|
1159
|
+
summary.created += 1;
|
|
1160
|
+
info(`[dry-run] Would create ${source.name}`);
|
|
1161
|
+
}
|
|
1162
|
+
else {
|
|
1163
|
+
summary.merged += 1;
|
|
1164
|
+
info(`[dry-run] Would merge ${source.name} into ${row.targetCollectionId}`);
|
|
1165
|
+
}
|
|
1166
|
+
for (const { grant, disposition, existingPermission } of entry.grants) {
|
|
1167
|
+
if (disposition === "already-applied")
|
|
1168
|
+
continue;
|
|
1169
|
+
const label = `${grant.groupType}/${grant.groupId}`;
|
|
1170
|
+
info(disposition === "update"
|
|
1171
|
+
? `[dry-run] Would update ${grantUpdateLine(label, existingPermission, grant.permission)}`
|
|
1172
|
+
: `[dry-run] Would grant ${label} ${grant.permission}`);
|
|
1173
|
+
summary.groupsGranted += 1;
|
|
1174
|
+
}
|
|
1175
|
+
summary.membersAdded += entry.members.length;
|
|
1176
|
+
summary.documentsAdded += entry.documents.length;
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
let targetCollectionId = row.targetCollectionId;
|
|
1180
|
+
if (row.action === "create") {
|
|
1181
|
+
try {
|
|
1182
|
+
const created = await client.createCollection(appId, {
|
|
1183
|
+
name: source.name,
|
|
1184
|
+
...(source.description ? { description: source.description } : {}),
|
|
1185
|
+
...(source.collectionType ? { collectionType: source.collectionType } : {}),
|
|
1186
|
+
...(source.contextId ? { contextId: source.contextId } : {}),
|
|
1187
|
+
createdBy: row.ownerUserId,
|
|
1188
|
+
});
|
|
1189
|
+
targetCollectionId = created.collectionId;
|
|
1190
|
+
row.targetCollectionId = targetCollectionId;
|
|
1191
|
+
summary.created += 1;
|
|
1192
|
+
info(`Created ${source.name} as ${targetCollectionId}`);
|
|
1193
|
+
// Whom the server CREDITED is the answer. A server predating #3641
|
|
1194
|
+
// ignores the field, and so does any server for a non-admin token.
|
|
1195
|
+
if (created.createdBy && created.createdBy !== row.ownerUserId) {
|
|
1196
|
+
note("owner", row.ownerUserId, `the server did not assign the owner (it credited ` +
|
|
1197
|
+
`${created.createdBy}); the token is not an admin one, or the ` +
|
|
1198
|
+
`server predates #3641`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
catch (err) {
|
|
1202
|
+
note("collection", source.collectionId, serverReason(err));
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
else {
|
|
1207
|
+
summary.merged += 1;
|
|
1208
|
+
info(`Merged ${source.name} into ${targetCollectionId}`);
|
|
1209
|
+
}
|
|
1210
|
+
for (const { grant, disposition, existingPermission } of entry.grants) {
|
|
1211
|
+
if (disposition === "already-applied")
|
|
1212
|
+
continue;
|
|
1213
|
+
const label = `${grant.groupType}/${grant.groupId}`;
|
|
1214
|
+
try {
|
|
1215
|
+
await client.grantCollectionGroupPermission(appId, targetCollectionId, {
|
|
1216
|
+
groupType: grant.groupType,
|
|
1217
|
+
groupId: grant.groupId,
|
|
1218
|
+
permission: grant.permission,
|
|
1219
|
+
});
|
|
1220
|
+
summary.groupsGranted += 1;
|
|
1221
|
+
// A level change is worth saying out loud: it is the one grant write
|
|
1222
|
+
// that can take access away from people who already had it.
|
|
1223
|
+
if (disposition === "update") {
|
|
1224
|
+
info(`Updated ${grantUpdateLine(label, existingPermission, grant.permission)}`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
catch (err) {
|
|
1228
|
+
note("group", label, serverReason(err));
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
for (const member of entry.members) {
|
|
1232
|
+
try {
|
|
1233
|
+
const result = await client.addCollectionMember(appId, targetCollectionId, {
|
|
1234
|
+
userId: member.userId,
|
|
1235
|
+
permission: member.permission,
|
|
1236
|
+
});
|
|
1237
|
+
// `already_member` is the server saying it was already so — including
|
|
1238
|
+
// the owner's own `_col-writer` enrolment, which every export records.
|
|
1239
|
+
if (result?.status !== "already_member")
|
|
1240
|
+
summary.membersAdded += 1;
|
|
1241
|
+
}
|
|
1242
|
+
catch (err) {
|
|
1243
|
+
note("member", member.userId, serverReason(err));
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
for (const documentId of entry.documents) {
|
|
1247
|
+
try {
|
|
1248
|
+
await client.addCollectionDocument(appId, targetCollectionId, { documentId });
|
|
1249
|
+
summary.documentsAdded += 1;
|
|
1250
|
+
}
|
|
1251
|
+
catch (err) {
|
|
1252
|
+
// 409 is "already in this collection": applied, not a problem.
|
|
1253
|
+
if (err?.statusCode === 409)
|
|
1254
|
+
continue;
|
|
1255
|
+
note("document", documentId, serverReason(err));
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
if (options.json) {
|
|
1260
|
+
json({
|
|
1261
|
+
...summary,
|
|
1262
|
+
collections: planned.map(({ row }) => ({
|
|
1263
|
+
sourceCollectionId: row.sourceCollectionId,
|
|
1264
|
+
name: row.name,
|
|
1265
|
+
action: row.action,
|
|
1266
|
+
...(row.targetCollectionId
|
|
1267
|
+
? { targetCollectionId: row.targetCollectionId }
|
|
1268
|
+
: {}),
|
|
1269
|
+
...(row.reason ? { reason: row.reason } : {}),
|
|
1270
|
+
})),
|
|
1271
|
+
problems,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
success(`${summary.created} created, ${summary.merged} merged, ` +
|
|
1276
|
+
`${summary.skipped} skipped, ${summary.refused} refused; ` +
|
|
1277
|
+
`${summary.groupsGranted} group grant(s), ${summary.membersAdded} member(s), ` +
|
|
1278
|
+
`${summary.documentsAdded} document(s) added; ${problems.length} problem(s).`);
|
|
1279
|
+
}
|
|
1280
|
+
return importExitCode(problems);
|
|
564
1281
|
}
|
|
565
1282
|
//# sourceMappingURL=collections.js.map
|