primitive-admin 1.0.62 → 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.
Files changed (344) hide show
  1. package/README.md +132 -42
  2. package/assets/skill/skills/primitive-platform/SKILL.md +142 -661
  3. package/dist/bin/primitive.js +121 -41
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/admins.js +37 -50
  6. package/dist/src/commands/admins.js.map +1 -1
  7. package/dist/src/commands/analytics.js +178 -9
  8. package/dist/src/commands/analytics.js.map +1 -1
  9. package/dist/src/commands/apps.js +17 -7
  10. package/dist/src/commands/apps.js.map +1 -1
  11. package/dist/src/commands/auth-sessions.d.ts +7 -0
  12. package/dist/src/commands/auth-sessions.js +144 -0
  13. package/dist/src/commands/auth-sessions.js.map +1 -0
  14. package/dist/src/commands/auth.js +76 -116
  15. package/dist/src/commands/auth.js.map +1 -1
  16. package/dist/src/commands/blob-buckets.js +2 -1
  17. package/dist/src/commands/blob-buckets.js.map +1 -1
  18. package/dist/src/commands/catalog.js +26 -10
  19. package/dist/src/commands/catalog.js.map +1 -1
  20. package/dist/src/commands/collection-type-configs.js +3 -10
  21. package/dist/src/commands/collection-type-configs.js.map +1 -1
  22. package/dist/src/commands/collections.js +736 -19
  23. package/dist/src/commands/collections.js.map +1 -1
  24. package/dist/src/commands/config.js +13 -27
  25. package/dist/src/commands/config.js.map +1 -1
  26. package/dist/src/commands/connections.js +8 -9
  27. package/dist/src/commands/connections.js.map +1 -1
  28. package/dist/src/commands/cron-triggers.js +2 -1
  29. package/dist/src/commands/cron-triggers.js.map +1 -1
  30. package/dist/src/commands/database-type-configs.js +5 -12
  31. package/dist/src/commands/database-type-configs.js.map +1 -1
  32. package/dist/src/commands/databases.js +59 -72
  33. package/dist/src/commands/databases.js.map +1 -1
  34. package/dist/src/commands/documents.d.ts +58 -0
  35. package/dist/src/commands/documents.js +1623 -95
  36. package/dist/src/commands/documents.js.map +1 -1
  37. package/dist/src/commands/email-templates.js +5 -4
  38. package/dist/src/commands/email-templates.js.map +1 -1
  39. package/dist/src/commands/env.d.ts +1 -1
  40. package/dist/src/commands/env.js +106 -44
  41. package/dist/src/commands/env.js.map +1 -1
  42. package/dist/src/commands/feature-flags.js +2 -1
  43. package/dist/src/commands/feature-flags.js.map +1 -1
  44. package/dist/src/commands/functions.d.ts +20 -0
  45. package/dist/src/commands/functions.js +1630 -0
  46. package/dist/src/commands/functions.js.map +1 -0
  47. package/dist/src/commands/group-type-configs.js +2 -1
  48. package/dist/src/commands/group-type-configs.js.map +1 -1
  49. package/dist/src/commands/groups.js +30 -17
  50. package/dist/src/commands/groups.js.map +1 -1
  51. package/dist/src/commands/guides.js +17 -11
  52. package/dist/src/commands/guides.js.map +1 -1
  53. package/dist/src/commands/init.d.ts +24 -12
  54. package/dist/src/commands/init.js +198 -110
  55. package/dist/src/commands/init.js.map +1 -1
  56. package/dist/src/commands/integrations.js +65 -26
  57. package/dist/src/commands/integrations.js.map +1 -1
  58. package/dist/src/commands/locks.js +17 -2
  59. package/dist/src/commands/locks.js.map +1 -1
  60. package/dist/src/commands/metadata-category-configs.js +2 -1
  61. package/dist/src/commands/metadata-category-configs.js.map +1 -1
  62. package/dist/src/commands/metadata.js +8 -1
  63. package/dist/src/commands/metadata.js.map +1 -1
  64. package/dist/src/commands/prompts.js +106 -59
  65. package/dist/src/commands/prompts.js.map +1 -1
  66. package/dist/src/commands/rule-sets.js +3 -10
  67. package/dist/src/commands/rule-sets.js.map +1 -1
  68. package/dist/src/commands/scripts.d.ts +10 -0
  69. package/dist/src/commands/scripts.js +159 -25
  70. package/dist/src/commands/scripts.js.map +1 -1
  71. package/dist/src/commands/secrets.js +2 -1
  72. package/dist/src/commands/secrets.js.map +1 -1
  73. package/dist/src/commands/sessions.js +10 -9
  74. package/dist/src/commands/sessions.js.map +1 -1
  75. package/dist/src/commands/sync-app-settings.js +3 -5
  76. package/dist/src/commands/sync-app-settings.js.map +1 -1
  77. package/dist/src/commands/sync.d.ts +391 -44
  78. package/dist/src/commands/sync.js +2622 -534
  79. package/dist/src/commands/sync.js.map +1 -1
  80. package/dist/src/commands/tokens.js +2 -1
  81. package/dist/src/commands/tokens.js.map +1 -1
  82. package/dist/src/commands/users.js +19 -10
  83. package/dist/src/commands/users.js.map +1 -1
  84. package/dist/src/commands/vars.js +2 -1
  85. package/dist/src/commands/vars.js.map +1 -1
  86. package/dist/src/commands/waitlist.js +2 -1
  87. package/dist/src/commands/waitlist.js.map +1 -1
  88. package/dist/src/commands/webhooks.d.ts +29 -0
  89. package/dist/src/commands/webhooks.js +135 -64
  90. package/dist/src/commands/webhooks.js.map +1 -1
  91. package/dist/src/commands/workflows.d.ts +1 -29
  92. package/dist/src/commands/workflows.js +113 -179
  93. package/dist/src/commands/workflows.js.map +1 -1
  94. package/dist/src/lib/api-client.d.ts +628 -14
  95. package/dist/src/lib/api-client.js +776 -70
  96. package/dist/src/lib/api-client.js.map +1 -1
  97. package/dist/src/lib/app-settings-descriptor.d.ts +1 -1
  98. package/dist/src/lib/app-settings-descriptor.js +9 -1
  99. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  100. package/dist/src/lib/block-selector.d.ts +58 -0
  101. package/dist/src/lib/block-selector.js +92 -0
  102. package/dist/src/lib/block-selector.js.map +1 -0
  103. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +24 -54
  104. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +31 -153
  105. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
  106. package/dist/src/lib/collection-export.d.ts +184 -0
  107. package/dist/src/lib/collection-export.js +252 -0
  108. package/dist/src/lib/collection-export.js.map +1 -0
  109. package/dist/src/lib/config-json-field.d.ts +28 -0
  110. package/dist/src/lib/config-json-field.js +56 -0
  111. package/dist/src/lib/config-json-field.js.map +1 -0
  112. package/dist/src/lib/config-object-descriptor.js +88 -6
  113. package/dist/src/lib/config-object-descriptor.js.map +1 -1
  114. package/dist/src/lib/config-payload.d.ts +8 -1
  115. package/dist/src/lib/config-payload.js +49 -4
  116. package/dist/src/lib/config-payload.js.map +1 -1
  117. package/dist/src/lib/config-surface.d.ts +12 -1
  118. package/dist/src/lib/config-surface.js +71 -3
  119. package/dist/src/lib/config-surface.js.map +1 -1
  120. package/dist/src/lib/config.d.ts +31 -13
  121. package/dist/src/lib/config.js +34 -70
  122. package/dist/src/lib/config.js.map +1 -1
  123. package/dist/src/lib/credentials-store.d.ts +37 -22
  124. package/dist/src/lib/credentials-store.js +103 -97
  125. package/dist/src/lib/credentials-store.js.map +1 -1
  126. package/dist/src/lib/db-codegen/dbTypeIR.js +8 -0
  127. package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -1
  128. package/dist/src/lib/deprecation.d.ts +22 -0
  129. package/dist/src/lib/deprecation.js +43 -0
  130. package/dist/src/lib/deprecation.js.map +1 -0
  131. package/dist/src/lib/document-export-permissions.d.ts +30 -0
  132. package/dist/src/lib/document-export-permissions.js +54 -0
  133. package/dist/src/lib/document-export-permissions.js.map +1 -0
  134. package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
  135. package/dist/src/lib/document-ingest-artifact.js +505 -0
  136. package/dist/src/lib/document-ingest-artifact.js.map +1 -0
  137. package/dist/src/lib/document-ingest-input.d.ts +51 -0
  138. package/dist/src/lib/document-ingest-input.js +132 -0
  139. package/dist/src/lib/document-ingest-input.js.map +1 -0
  140. package/dist/src/lib/document-ingest-rows.d.ts +137 -0
  141. package/dist/src/lib/document-ingest-rows.js +181 -0
  142. package/dist/src/lib/document-ingest-rows.js.map +1 -0
  143. package/dist/src/lib/document-ingest.d.ts +101 -0
  144. package/dist/src/lib/document-ingest.js +384 -0
  145. package/dist/src/lib/document-ingest.js.map +1 -0
  146. package/dist/src/lib/env-resolver-core.d.ts +128 -17
  147. package/dist/src/lib/env-resolver-core.js +204 -22
  148. package/dist/src/lib/env-resolver-core.js.map +1 -1
  149. package/dist/src/lib/env-resolver.d.ts +18 -3
  150. package/dist/src/lib/env-resolver.js +23 -3
  151. package/dist/src/lib/env-resolver.js.map +1 -1
  152. package/dist/src/lib/function-bundle.d.ts +147 -0
  153. package/dist/src/lib/function-bundle.js +341 -0
  154. package/dist/src/lib/function-bundle.js.map +1 -0
  155. package/dist/src/lib/function-collect.d.ts +123 -0
  156. package/dist/src/lib/function-collect.js +610 -0
  157. package/dist/src/lib/function-collect.js.map +1 -0
  158. package/dist/src/lib/function-db-types.d.ts +202 -0
  159. package/dist/src/lib/function-db-types.js +870 -0
  160. package/dist/src/lib/function-db-types.js.map +1 -0
  161. package/dist/src/lib/function-document-types.d.ts +144 -0
  162. package/dist/src/lib/function-document-types.js +370 -0
  163. package/dist/src/lib/function-document-types.js.map +1 -0
  164. package/dist/src/lib/function-grants-preflight.d.ts +64 -0
  165. package/dist/src/lib/function-grants-preflight.js +105 -0
  166. package/dist/src/lib/function-grants-preflight.js.map +1 -0
  167. package/dist/src/lib/function-log-lines.d.ts +76 -0
  168. package/dist/src/lib/function-log-lines.js +160 -0
  169. package/dist/src/lib/function-log-lines.js.map +1 -0
  170. package/dist/src/lib/function-log-row.d.ts +29 -0
  171. package/dist/src/lib/function-log-row.js +73 -0
  172. package/dist/src/lib/function-log-row.js.map +1 -0
  173. package/dist/src/lib/function-log-tail.d.ts +132 -0
  174. package/dist/src/lib/function-log-tail.js +262 -0
  175. package/dist/src/lib/function-log-tail.js.map +1 -0
  176. package/dist/src/lib/function-run.d.ts +289 -0
  177. package/dist/src/lib/function-run.js +389 -0
  178. package/dist/src/lib/function-run.js.map +1 -0
  179. package/dist/src/lib/function-schema-codegen.d.ts +143 -0
  180. package/dist/src/lib/function-schema-codegen.js +420 -0
  181. package/dist/src/lib/function-schema-codegen.js.map +1 -0
  182. package/dist/src/lib/function-sync.d.ts +459 -0
  183. package/dist/src/lib/function-sync.js +1258 -0
  184. package/dist/src/lib/function-sync.js.map +1 -0
  185. package/dist/src/lib/function-trigger-listing.d.ts +27 -0
  186. package/dist/src/lib/function-trigger-listing.js +70 -0
  187. package/dist/src/lib/function-trigger-listing.js.map +1 -0
  188. package/dist/src/lib/function-typecheck.d.ts +86 -0
  189. package/dist/src/lib/function-typecheck.js +370 -0
  190. package/dist/src/lib/function-typecheck.js.map +1 -0
  191. package/dist/src/lib/function-versions.d.ts +122 -0
  192. package/dist/src/lib/function-versions.js +182 -0
  193. package/dist/src/lib/function-versions.js.map +1 -0
  194. package/dist/src/lib/generated-allowlist.js +4 -0
  195. package/dist/src/lib/generated-allowlist.js.map +1 -1
  196. package/dist/src/lib/generated-config-surfaces.d.ts +2332 -78
  197. package/dist/src/lib/generated-config-surfaces.js +6979 -468
  198. package/dist/src/lib/generated-config-surfaces.js.map +1 -1
  199. package/dist/src/lib/generated-sdk-types.d.ts +12 -0
  200. package/dist/src/lib/generated-sdk-types.js +13 -0
  201. package/dist/src/lib/generated-sdk-types.js.map +1 -0
  202. package/dist/src/lib/init-adopt.js +2 -2
  203. package/dist/src/lib/init-adopt.js.map +1 -1
  204. package/dist/src/lib/init-assets.js +3 -3
  205. package/dist/src/lib/init-client-platforms.d.ts +14 -0
  206. package/dist/src/lib/init-client-platforms.js +71 -0
  207. package/dist/src/lib/init-client-platforms.js.map +1 -0
  208. package/dist/src/lib/init-config.d.ts +1 -1
  209. package/dist/src/lib/init-email-redirect-uris.d.ts +37 -0
  210. package/dist/src/lib/init-email-redirect-uris.js +46 -0
  211. package/dist/src/lib/init-email-redirect-uris.js.map +1 -0
  212. package/dist/src/lib/init-ios-links.d.ts +41 -0
  213. package/dist/src/lib/init-ios-links.js +67 -1
  214. package/dist/src/lib/init-ios-links.js.map +1 -1
  215. package/dist/src/lib/init-plan.d.ts +1 -1
  216. package/dist/src/lib/init-plan.js +1 -1
  217. package/dist/src/lib/init-xcode.d.ts +6 -5
  218. package/dist/src/lib/init-xcode.js +34 -10
  219. package/dist/src/lib/init-xcode.js.map +1 -1
  220. package/dist/src/lib/integration-selector.d.ts +42 -0
  221. package/dist/src/lib/integration-selector.js +46 -0
  222. package/dist/src/lib/integration-selector.js.map +1 -0
  223. package/dist/src/lib/ios-app-id.d.ts +34 -0
  224. package/dist/src/lib/ios-app-id.js +69 -0
  225. package/dist/src/lib/ios-app-id.js.map +1 -0
  226. package/dist/src/lib/list-options.d.ts +68 -0
  227. package/dist/src/lib/list-options.js +89 -0
  228. package/dist/src/lib/list-options.js.map +1 -0
  229. package/dist/src/lib/local-state.d.ts +1 -1
  230. package/dist/src/lib/local-state.js +1 -1
  231. package/dist/src/lib/local-test-cases.d.ts +63 -0
  232. package/dist/src/lib/local-test-cases.js +136 -0
  233. package/dist/src/lib/local-test-cases.js.map +1 -0
  234. package/dist/src/lib/log-inspection.d.ts +152 -5
  235. package/dist/src/lib/log-inspection.js +179 -2
  236. package/dist/src/lib/log-inspection.js.map +1 -1
  237. package/dist/src/lib/logout-admin-session.d.ts +33 -0
  238. package/dist/src/lib/logout-admin-session.js +70 -0
  239. package/dist/src/lib/logout-admin-session.js.map +1 -0
  240. package/dist/src/lib/output.d.ts +15 -0
  241. package/dist/src/lib/output.js +28 -0
  242. package/dist/src/lib/output.js.map +1 -1
  243. package/dist/src/lib/paginate.d.ts +15 -0
  244. package/dist/src/lib/paginate.js +17 -0
  245. package/dist/src/lib/paginate.js.map +1 -1
  246. package/dist/src/lib/project-config.d.ts +46 -21
  247. package/dist/src/lib/project-config.js +74 -47
  248. package/dist/src/lib/project-config.js.map +1 -1
  249. package/dist/src/lib/prompt-cost-format.d.ts +11 -0
  250. package/dist/src/lib/prompt-cost-format.js +41 -0
  251. package/dist/src/lib/prompt-cost-format.js.map +1 -0
  252. package/dist/src/lib/prompt-schema-codegen.d.ts +147 -0
  253. package/dist/src/lib/prompt-schema-codegen.js +462 -0
  254. package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
  255. package/dist/src/lib/refresh-admin-credentials.d.ts +9 -1
  256. package/dist/src/lib/refresh-admin-credentials.js +22 -2
  257. package/dist/src/lib/refresh-admin-credentials.js.map +1 -1
  258. package/dist/src/lib/resolve-owner.d.ts +19 -0
  259. package/dist/src/lib/resolve-owner.js +20 -0
  260. package/dist/src/lib/resolve-owner.js.map +1 -0
  261. package/dist/src/lib/skill-installer.d.ts +49 -3
  262. package/dist/src/lib/skill-installer.js +275 -100
  263. package/dist/src/lib/skill-installer.js.map +1 -1
  264. package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
  265. package/dist/src/lib/snapshot-audit-source.js +58 -0
  266. package/dist/src/lib/snapshot-audit-source.js.map +1 -0
  267. package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
  268. package/dist/src/lib/snapshot-audit-store.js +196 -0
  269. package/dist/src/lib/snapshot-audit-store.js.map +1 -0
  270. package/dist/src/lib/snapshot-audit.d.ts +207 -0
  271. package/dist/src/lib/snapshot-audit.js +431 -0
  272. package/dist/src/lib/snapshot-audit.js.map +1 -0
  273. package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
  274. package/dist/src/lib/snapshot-build-rows.js +87 -0
  275. package/dist/src/lib/snapshot-build-rows.js.map +1 -0
  276. package/dist/src/lib/snapshot-build.d.ts +50 -0
  277. package/dist/src/lib/snapshot-build.js +111 -0
  278. package/dist/src/lib/snapshot-build.js.map +1 -0
  279. package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
  280. package/dist/src/lib/snapshot-manifest-layout.js +70 -0
  281. package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
  282. package/dist/src/lib/snapshots.d.ts +8 -9
  283. package/dist/src/lib/snapshots.js +20 -83
  284. package/dist/src/lib/snapshots.js.map +1 -1
  285. package/dist/src/lib/step-run-table.d.ts +43 -0
  286. package/dist/src/lib/step-run-table.js +129 -0
  287. package/dist/src/lib/step-run-table.js.map +1 -0
  288. package/dist/src/lib/storage-pending-retry.d.ts +26 -0
  289. package/dist/src/lib/storage-pending-retry.js +42 -0
  290. package/dist/src/lib/storage-pending-retry.js.map +1 -0
  291. package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
  292. package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
  293. package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
  294. package/dist/src/lib/swift-codegen/banners.d.ts +24 -0
  295. package/dist/src/lib/swift-codegen/banners.js +25 -0
  296. package/dist/src/lib/swift-codegen/banners.js.map +1 -0
  297. package/dist/src/lib/swift-codegen/dbGenerator.js +19 -7
  298. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  299. package/dist/src/lib/swift-codegen/functionGenerator.d.ts +139 -0
  300. package/dist/src/lib/swift-codegen/functionGenerator.js +462 -0
  301. package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -0
  302. package/dist/src/lib/swift-codegen/generator.d.ts +7 -1
  303. package/dist/src/lib/swift-codegen/generator.js +28 -11
  304. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  305. package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +15 -0
  306. package/dist/src/lib/swift-codegen/schemaToSwift.js +21 -4
  307. package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -1
  308. package/dist/src/lib/sync-paths.d.ts +72 -55
  309. package/dist/src/lib/sync-paths.js +97 -100
  310. package/dist/src/lib/sync-paths.js.map +1 -1
  311. package/dist/src/lib/sync-resource-types.d.ts +23 -4
  312. package/dist/src/lib/sync-resource-types.js +106 -8
  313. package/dist/src/lib/sync-resource-types.js.map +1 -1
  314. package/dist/src/lib/sync-selectors.d.ts +45 -2
  315. package/dist/src/lib/sync-selectors.js +66 -5
  316. package/dist/src/lib/sync-selectors.js.map +1 -1
  317. package/dist/src/lib/template.d.ts +2 -2
  318. package/dist/src/lib/template.js +2 -2
  319. package/dist/src/lib/test-case-file-names.d.ts +40 -0
  320. package/dist/src/lib/test-case-file-names.js +91 -0
  321. package/dist/src/lib/test-case-file-names.js.map +1 -0
  322. package/dist/src/lib/test-case-keys.d.ts +1 -1
  323. package/dist/src/lib/test-case-keys.js +1 -1
  324. package/dist/src/lib/test-case-variables.d.ts +14 -0
  325. package/dist/src/lib/test-case-variables.js +42 -0
  326. package/dist/src/lib/test-case-variables.js.map +1 -1
  327. package/dist/src/lib/toml-database-config.js +19 -2
  328. package/dist/src/lib/toml-database-config.js.map +1 -1
  329. package/dist/src/lib/web-url.d.ts +40 -0
  330. package/dist/src/lib/web-url.js +76 -0
  331. package/dist/src/lib/web-url.js.map +1 -0
  332. package/dist/src/lib/webhook-deliver.d.ts +209 -0
  333. package/dist/src/lib/webhook-deliver.js +519 -0
  334. package/dist/src/lib/webhook-deliver.js.map +1 -0
  335. package/dist/src/lib/workflow-codegen/generator.d.ts +1 -1
  336. package/dist/src/lib/workflow-codegen/generator.js +1 -1
  337. package/dist/src/lib/workflow-toml-validator.d.ts +17 -8
  338. package/dist/src/lib/workflow-toml-validator.js +25 -12
  339. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  340. package/dist/src/lib/workflow-usage.d.ts +196 -0
  341. package/dist/src/lib/workflow-usage.js +310 -0
  342. package/dist/src/lib/workflow-usage.js.map +1 -0
  343. package/dist/src/types/index.d.ts +12 -2
  344. 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 result = options.all
36
- ? await client.listAllCollections(resolvedAppId)
37
- : await client.listCollections(resolvedAppId);
38
- const list = Array.isArray(result) ? result : result?.items ?? [];
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(result);
51
+ json(page);
41
52
  return;
42
53
  }
43
54
  if (list.length === 0) {
44
- info("No collections found.");
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
- if (result?.nextCursor) {
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 result = await client.listCollectionDocuments(resolvedAppId, collectionId);
344
- const list = Array.isArray(result) ? result : result?.items ?? [];
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(result);
410
+ json(page);
347
411
  return;
348
412
  }
349
413
  if (list.length === 0) {
350
- info("No documents in this collection.");
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
- if (result?.nextCursor) {
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 array, matching `groups members list` (#2764).
485
- json(list);
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