primitive-admin 1.0.61 → 1.0.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (320) hide show
  1. package/README.md +125 -69
  2. package/assets/skill/skills/primitive-platform/SKILL.md +336 -37
  3. package/dist/bin/primitive.js +25 -14
  4. package/dist/bin/primitive.js.map +1 -1
  5. package/dist/src/commands/analytics.js +36 -16
  6. package/dist/src/commands/analytics.js.map +1 -1
  7. package/dist/src/commands/apps.js +52 -188
  8. package/dist/src/commands/apps.js.map +1 -1
  9. package/dist/src/commands/blob-buckets.js +13 -153
  10. package/dist/src/commands/blob-buckets.js.map +1 -1
  11. package/dist/src/commands/collection-type-configs.js +14 -100
  12. package/dist/src/commands/collection-type-configs.js.map +1 -1
  13. package/dist/src/commands/collections.js +47 -8
  14. package/dist/src/commands/collections.js.map +1 -1
  15. package/dist/src/commands/config.d.ts +46 -0
  16. package/dist/src/commands/config.js +479 -0
  17. package/dist/src/commands/config.js.map +1 -0
  18. package/dist/src/commands/connections.js +12 -7
  19. package/dist/src/commands/connections.js.map +1 -1
  20. package/dist/src/commands/cron-triggers.js +84 -176
  21. package/dist/src/commands/cron-triggers.js.map +1 -1
  22. package/dist/src/commands/database-type-configs.d.ts +2 -0
  23. package/dist/src/commands/database-type-configs.js +171 -0
  24. package/dist/src/commands/database-type-configs.js.map +1 -0
  25. package/dist/src/commands/database-types.js +4 -4
  26. package/dist/src/commands/database-types.js.map +1 -1
  27. package/dist/src/commands/databases.d.ts +63 -0
  28. package/dist/src/commands/databases.js +649 -87
  29. package/dist/src/commands/databases.js.map +1 -1
  30. package/dist/src/commands/documents.js +869 -12
  31. package/dist/src/commands/documents.js.map +1 -1
  32. package/dist/src/commands/email-templates.js +21 -128
  33. package/dist/src/commands/email-templates.js.map +1 -1
  34. package/dist/src/commands/env.d.ts +12 -1
  35. package/dist/src/commands/env.js +115 -26
  36. package/dist/src/commands/env.js.map +1 -1
  37. package/dist/src/commands/feature-flags.d.ts +14 -0
  38. package/dist/src/commands/feature-flags.js +116 -0
  39. package/dist/src/commands/feature-flags.js.map +1 -0
  40. package/dist/src/commands/group-type-configs.js +14 -109
  41. package/dist/src/commands/group-type-configs.js.map +1 -1
  42. package/dist/src/commands/init.d.ts +24 -1
  43. package/dist/src/commands/init.js +1194 -279
  44. package/dist/src/commands/init.js.map +1 -1
  45. package/dist/src/commands/integrations.js +166 -435
  46. package/dist/src/commands/integrations.js.map +1 -1
  47. package/dist/src/commands/metadata-category-configs.d.ts +12 -0
  48. package/dist/src/commands/metadata-category-configs.js +112 -0
  49. package/dist/src/commands/metadata-category-configs.js.map +1 -0
  50. package/dist/src/commands/metadata.js +33 -141
  51. package/dist/src/commands/metadata.js.map +1 -1
  52. package/dist/src/commands/prompts.js +206 -563
  53. package/dist/src/commands/prompts.js.map +1 -1
  54. package/dist/src/commands/rule-sets.d.ts +1 -1
  55. package/dist/src/commands/rule-sets.js +16 -132
  56. package/dist/src/commands/rule-sets.js.map +1 -1
  57. package/dist/src/commands/scripts.d.ts +10 -0
  58. package/dist/src/commands/scripts.js +203 -241
  59. package/dist/src/commands/scripts.js.map +1 -1
  60. package/dist/src/commands/sessions.js +17 -5
  61. package/dist/src/commands/sessions.js.map +1 -1
  62. package/dist/src/commands/sync-app-settings.d.ts +113 -70
  63. package/dist/src/commands/sync-app-settings.js +193 -242
  64. package/dist/src/commands/sync-app-settings.js.map +1 -1
  65. package/dist/src/commands/sync.d.ts +1904 -51
  66. package/dist/src/commands/sync.js +9468 -1790
  67. package/dist/src/commands/sync.js.map +1 -1
  68. package/dist/src/commands/tokens.js +8 -2
  69. package/dist/src/commands/tokens.js.map +1 -1
  70. package/dist/src/commands/users.js +92 -1
  71. package/dist/src/commands/users.js.map +1 -1
  72. package/dist/src/commands/vars.js +44 -58
  73. package/dist/src/commands/vars.js.map +1 -1
  74. package/dist/src/commands/webhooks.js +318 -156
  75. package/dist/src/commands/webhooks.js.map +1 -1
  76. package/dist/src/commands/workflows.d.ts +49 -12
  77. package/dist/src/commands/workflows.js +652 -1086
  78. package/dist/src/commands/workflows.js.map +1 -1
  79. package/dist/src/lib/access-rule-display.d.ts +21 -0
  80. package/dist/src/lib/access-rule-display.js +34 -0
  81. package/dist/src/lib/access-rule-display.js.map +1 -0
  82. package/dist/src/lib/api-client.d.ts +541 -61
  83. package/dist/src/lib/api-client.js +559 -102
  84. package/dist/src/lib/api-client.js.map +1 -1
  85. package/dist/src/lib/app-settings-descriptor.d.ts +180 -25
  86. package/dist/src/lib/app-settings-descriptor.js +403 -82
  87. package/dist/src/lib/app-settings-descriptor.js.map +1 -1
  88. package/dist/src/lib/canonical-json.d.ts +12 -0
  89. package/dist/src/lib/canonical-json.js +35 -0
  90. package/dist/src/lib/canonical-json.js.map +1 -0
  91. package/dist/src/lib/cli-manifest.d.ts +8 -0
  92. package/dist/src/lib/cli-manifest.js +1 -0
  93. package/dist/src/lib/cli-manifest.js.map +1 -1
  94. package/dist/src/lib/codegen-shared/generatedFiles.d.ts +6 -2
  95. package/dist/src/lib/codegen-shared/generatedFiles.js +9 -2
  96. package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -1
  97. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +6 -6
  98. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +8 -8
  99. package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -1
  100. package/dist/src/lib/config-object-descriptor.d.ts +127 -0
  101. package/dist/src/lib/config-object-descriptor.js +658 -0
  102. package/dist/src/lib/config-object-descriptor.js.map +1 -0
  103. package/dist/src/lib/config-payload.d.ts +85 -0
  104. package/dist/src/lib/config-payload.js +116 -0
  105. package/dist/src/lib/config-payload.js.map +1 -0
  106. package/dist/src/lib/config-surface.d.ts +130 -0
  107. package/dist/src/lib/config-surface.js +300 -0
  108. package/dist/src/lib/config-surface.js.map +1 -0
  109. package/dist/src/lib/credentials-store.d.ts +10 -0
  110. package/dist/src/lib/credentials-store.js +23 -0
  111. package/dist/src/lib/credentials-store.js.map +1 -1
  112. package/dist/src/lib/data-input.d.ts +23 -0
  113. package/dist/src/lib/data-input.js +50 -0
  114. package/dist/src/lib/data-input.js.map +1 -0
  115. package/dist/src/lib/db-codegen/dbTemplates.js +1 -1
  116. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -1
  117. package/dist/src/lib/db-codegen/dbTypeIR.d.ts +1 -1
  118. package/dist/src/lib/env-resolver-core.d.ts +187 -0
  119. package/dist/src/lib/env-resolver-core.js +325 -0
  120. package/dist/src/lib/env-resolver-core.js.map +1 -0
  121. package/dist/src/lib/env-resolver.d.ts +28 -6
  122. package/dist/src/lib/env-resolver.js +44 -32
  123. package/dist/src/lib/env-resolver.js.map +1 -1
  124. package/dist/src/lib/generated-allowlist.js +12 -0
  125. package/dist/src/lib/generated-allowlist.js.map +1 -1
  126. package/dist/src/lib/generated-config-surfaces.d.ts +682 -0
  127. package/dist/src/lib/generated-config-surfaces.js +4058 -0
  128. package/dist/src/lib/generated-config-surfaces.js.map +1 -0
  129. package/dist/src/lib/generated-template-lint.d.ts +212 -0
  130. package/dist/src/lib/generated-template-lint.js +624 -0
  131. package/dist/src/lib/generated-template-lint.js.map +1 -0
  132. package/dist/src/lib/init-adopt.d.ts +16 -0
  133. package/dist/src/lib/init-adopt.js +34 -0
  134. package/dist/src/lib/init-adopt.js.map +1 -0
  135. package/dist/src/lib/init-assets.d.ts +39 -0
  136. package/dist/src/lib/init-assets.js +97 -0
  137. package/dist/src/lib/init-assets.js.map +1 -0
  138. package/dist/src/lib/init-client-platforms.d.ts +14 -0
  139. package/dist/src/lib/init-client-platforms.js +71 -0
  140. package/dist/src/lib/init-client-platforms.js.map +1 -0
  141. package/dist/src/lib/init-config.d.ts +44 -5
  142. package/dist/src/lib/init-config.js +80 -7
  143. package/dist/src/lib/init-config.js.map +1 -1
  144. package/dist/src/lib/init-email-redirect-uris.d.ts +37 -0
  145. package/dist/src/lib/init-email-redirect-uris.js +46 -0
  146. package/dist/src/lib/init-email-redirect-uris.js.map +1 -0
  147. package/dist/src/lib/init-ios-links.d.ts +90 -0
  148. package/dist/src/lib/init-ios-links.js +218 -0
  149. package/dist/src/lib/init-ios-links.js.map +1 -0
  150. package/dist/src/lib/init-plan.d.ts +80 -0
  151. package/dist/src/lib/init-plan.js +95 -0
  152. package/dist/src/lib/init-plan.js.map +1 -0
  153. package/dist/src/lib/init-production-env.d.ts +48 -0
  154. package/dist/src/lib/init-production-env.js +59 -0
  155. package/dist/src/lib/init-production-env.js.map +1 -0
  156. package/dist/src/lib/init-schema.d.ts +74 -0
  157. package/dist/src/lib/init-schema.js +358 -0
  158. package/dist/src/lib/init-schema.js.map +1 -0
  159. package/dist/src/lib/init-xcode.d.ts +33 -0
  160. package/dist/src/lib/init-xcode.js +114 -0
  161. package/dist/src/lib/init-xcode.js.map +1 -0
  162. package/dist/src/lib/integration-request-config.d.ts +30 -0
  163. package/dist/src/lib/integration-request-config.js +145 -0
  164. package/dist/src/lib/integration-request-config.js.map +1 -0
  165. package/dist/src/lib/integration-selector.d.ts +42 -0
  166. package/dist/src/lib/integration-selector.js +46 -0
  167. package/dist/src/lib/integration-selector.js.map +1 -0
  168. package/dist/src/lib/local-state.d.ts +55 -0
  169. package/dist/src/lib/local-state.js +167 -0
  170. package/dist/src/lib/local-state.js.map +1 -0
  171. package/dist/src/lib/local-test-cases.d.ts +63 -0
  172. package/dist/src/lib/local-test-cases.js +135 -0
  173. package/dist/src/lib/local-test-cases.js.map +1 -0
  174. package/dist/src/lib/log-inspection.d.ts +568 -0
  175. package/dist/src/lib/log-inspection.js +639 -0
  176. package/dist/src/lib/log-inspection.js.map +1 -0
  177. package/dist/src/lib/migration-nag.d.ts +2 -2
  178. package/dist/src/lib/migration-nag.js +3 -3
  179. package/dist/src/lib/object-status-filter.d.ts +22 -0
  180. package/dist/src/lib/object-status-filter.js +45 -0
  181. package/dist/src/lib/object-status-filter.js.map +1 -0
  182. package/dist/src/lib/output.d.ts +24 -1
  183. package/dist/src/lib/output.js +42 -1
  184. package/dist/src/lib/output.js.map +1 -1
  185. package/dist/src/lib/package-manager.d.ts +140 -0
  186. package/dist/src/lib/package-manager.js +305 -0
  187. package/dist/src/lib/package-manager.js.map +1 -0
  188. package/dist/src/lib/paginate.d.ts +50 -0
  189. package/dist/src/lib/paginate.js +53 -0
  190. package/dist/src/lib/paginate.js.map +1 -1
  191. package/dist/src/lib/platform-owned.d.ts +29 -3
  192. package/dist/src/lib/platform-owned.js +42 -3
  193. package/dist/src/lib/platform-owned.js.map +1 -1
  194. package/dist/src/lib/project-config.d.ts +8 -0
  195. package/dist/src/lib/project-config.js +30 -5
  196. package/dist/src/lib/project-config.js.map +1 -1
  197. package/dist/src/lib/query-operators.d.ts +1 -1
  198. package/dist/src/lib/query-operators.js +1 -1
  199. package/dist/src/lib/record-filter.d.ts +18 -0
  200. package/dist/src/lib/record-filter.js +55 -0
  201. package/dist/src/lib/record-filter.js.map +1 -0
  202. package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
  203. package/dist/src/lib/resolve-init-dev-port.js +55 -0
  204. package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
  205. package/dist/src/lib/resolve-init-server.d.ts +64 -0
  206. package/dist/src/lib/resolve-init-server.js +77 -0
  207. package/dist/src/lib/resolve-init-server.js.map +1 -0
  208. package/dist/src/lib/resolve-platform.d.ts +43 -14
  209. package/dist/src/lib/resolve-platform.js +74 -12
  210. package/dist/src/lib/resolve-platform.js.map +1 -1
  211. package/dist/src/lib/run-status.d.ts +19 -0
  212. package/dist/src/lib/run-status.generated.d.ts +39 -0
  213. package/dist/src/lib/run-status.generated.js +66 -0
  214. package/dist/src/lib/run-status.generated.js.map +1 -0
  215. package/dist/src/lib/run-status.js +19 -0
  216. package/dist/src/lib/run-status.js.map +1 -0
  217. package/dist/src/lib/server-text-normalization.d.ts +51 -0
  218. package/dist/src/lib/server-text-normalization.js +90 -0
  219. package/dist/src/lib/server-text-normalization.js.map +1 -0
  220. package/dist/src/lib/server-url.d.ts +22 -0
  221. package/dist/src/lib/server-url.js +33 -0
  222. package/dist/src/lib/server-url.js.map +1 -0
  223. package/dist/src/lib/signing-secret-status.d.ts +81 -0
  224. package/dist/src/lib/signing-secret-status.js +116 -0
  225. package/dist/src/lib/signing-secret-status.js.map +1 -0
  226. package/dist/src/lib/skill-installer.js +6 -3
  227. package/dist/src/lib/skill-installer.js.map +1 -1
  228. package/dist/src/lib/snapshots.d.ts +2 -2
  229. package/dist/src/lib/snapshots.js +2 -2
  230. package/dist/src/lib/swift-codegen/dbGenerator.d.ts +15 -2
  231. package/dist/src/lib/swift-codegen/dbGenerator.js +250 -18
  232. package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -1
  233. package/dist/src/lib/swift-codegen/generator.d.ts +11 -2
  234. package/dist/src/lib/swift-codegen/generator.js +181 -7
  235. package/dist/src/lib/swift-codegen/generator.js.map +1 -1
  236. package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +7 -3
  237. package/dist/src/lib/swift-codegen/siblingSymbols.js +4 -1
  238. package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -1
  239. package/dist/src/lib/sync-dir-selector.d.ts +21 -0
  240. package/dist/src/lib/sync-dir-selector.js +30 -0
  241. package/dist/src/lib/sync-dir-selector.js.map +1 -0
  242. package/dist/src/lib/sync-paths.d.ts +39 -0
  243. package/dist/src/lib/sync-paths.js +70 -2
  244. package/dist/src/lib/sync-paths.js.map +1 -1
  245. package/dist/src/lib/sync-resource-types.d.ts +138 -31
  246. package/dist/src/lib/sync-resource-types.js +241 -47
  247. package/dist/src/lib/sync-resource-types.js.map +1 -1
  248. package/dist/src/lib/sync-selectors.d.ts +95 -0
  249. package/dist/src/lib/sync-selectors.js +228 -0
  250. package/dist/src/lib/sync-selectors.js.map +1 -0
  251. package/dist/src/lib/template.d.ts +62 -4
  252. package/dist/src/lib/template.js +147 -39
  253. package/dist/src/lib/template.js.map +1 -1
  254. package/dist/src/lib/test-case-file-names.d.ts +40 -0
  255. package/dist/src/lib/test-case-file-names.js +91 -0
  256. package/dist/src/lib/test-case-file-names.js.map +1 -0
  257. package/dist/src/lib/test-case-keys.d.ts +29 -0
  258. package/dist/src/lib/test-case-keys.js +55 -0
  259. package/dist/src/lib/test-case-keys.js.map +1 -0
  260. package/dist/src/lib/test-case-variables.d.ts +15 -0
  261. package/dist/src/lib/test-case-variables.js +29 -0
  262. package/dist/src/lib/test-case-variables.js.map +1 -0
  263. package/dist/src/lib/toml-database-config.d.ts +4 -4
  264. package/dist/src/lib/toml-database-config.js +27 -34
  265. package/dist/src/lib/toml-database-config.js.map +1 -1
  266. package/dist/src/lib/toml-metadata-config.d.ts +9 -5
  267. package/dist/src/lib/toml-metadata-config.js +40 -45
  268. package/dist/src/lib/toml-metadata-config.js.map +1 -1
  269. package/dist/src/lib/toml-native-form.d.ts +1 -1
  270. package/dist/src/lib/toml-native-form.js +1 -1
  271. package/dist/src/lib/toml-params-validator.d.ts +1 -1
  272. package/dist/src/lib/toml-params-validator.js +1 -1
  273. package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
  274. package/dist/src/lib/toml-scalar-edit.js +283 -0
  275. package/dist/src/lib/toml-scalar-edit.js.map +1 -0
  276. package/dist/src/lib/user-selector.d.ts +24 -0
  277. package/dist/src/lib/user-selector.js +33 -0
  278. package/dist/src/lib/user-selector.js.map +1 -0
  279. package/dist/src/lib/version-check.d.ts +25 -0
  280. package/dist/src/lib/version-check.js +77 -8
  281. package/dist/src/lib/version-check.js.map +1 -1
  282. package/dist/src/lib/watch.d.ts +121 -0
  283. package/dist/src/lib/watch.js +169 -0
  284. package/dist/src/lib/watch.js.map +1 -0
  285. package/dist/src/lib/web-url.d.ts +40 -0
  286. package/dist/src/lib/web-url.js +76 -0
  287. package/dist/src/lib/web-url.js.map +1 -0
  288. package/dist/src/lib/workflow-apply.d.ts +69 -45
  289. package/dist/src/lib/workflow-apply.js +99 -72
  290. package/dist/src/lib/workflow-apply.js.map +1 -1
  291. package/dist/src/lib/workflow-codegen/generator.js +4 -4
  292. package/dist/src/lib/workflow-codegen/generator.js.map +1 -1
  293. package/dist/src/lib/workflow-codegen/invokerIR.d.ts +11 -0
  294. package/dist/src/lib/workflow-codegen/invokerIR.js +5 -0
  295. package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -1
  296. package/dist/src/lib/workflow-config-apply.d.ts +70 -0
  297. package/dist/src/lib/workflow-config-apply.js +137 -0
  298. package/dist/src/lib/workflow-config-apply.js.map +1 -0
  299. package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
  300. package/dist/src/lib/workflow-config-sidecar.js +96 -0
  301. package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
  302. package/dist/src/lib/workflow-defaults.d.ts +29 -0
  303. package/dist/src/lib/workflow-defaults.js +41 -0
  304. package/dist/src/lib/workflow-defaults.js.map +1 -0
  305. package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
  306. package/dist/src/lib/workflow-include-preserve.js +286 -0
  307. package/dist/src/lib/workflow-include-preserve.js.map +1 -0
  308. package/dist/src/lib/workflow-payload.d.ts +31 -20
  309. package/dist/src/lib/workflow-payload.js +100 -110
  310. package/dist/src/lib/workflow-payload.js.map +1 -1
  311. package/dist/src/lib/workflow-toml-validator.d.ts +37 -16
  312. package/dist/src/lib/workflow-toml-validator.js +141 -16
  313. package/dist/src/lib/workflow-toml-validator.js.map +1 -1
  314. package/dist/src/types/index.d.ts +78 -22
  315. package/dist/src/validators.d.ts +7 -7
  316. package/dist/src/validators.js +6 -6
  317. package/package.json +11 -6
  318. package/dist/src/commands/settings.d.ts +0 -15
  319. package/dist/src/commands/settings.js +0 -102
  320. package/dist/src/commands/settings.js.map +0 -1
@@ -1,18 +1,880 @@
1
1
  import { ApiClient } from "../lib/api-client.js";
2
2
  import { resolveAppId } from "../lib/config.js";
3
- import { success, error, info, formatTable, formatDate, json, keyValue, warn, } from "../lib/output.js";
3
+ import { success, error, info, formatTable, formatId, formatDate, json, keyValue, result as printResult, warn, } from "../lib/output.js";
4
4
  import { confirmPrompt } from "../lib/confirm-prompt.js";
5
+ import { parseDataOption } from "../lib/data-input.js";
6
+ import { parseFilterOptions } from "../lib/record-filter.js";
7
+ import { ulid } from "ulid";
5
8
  import * as fs from "fs";
6
9
  import * as path from "path";
10
+ /**
11
+ * Resolve a `--owner` value to a userId. A value containing "@" is looked up
12
+ * by exact email through the member-accessible `users/lookup` endpoint
13
+ * (issue #2763); anything else is already a user id and is passed through.
14
+ * Failures throw with the server's own message — a permission problem must
15
+ * not read as "no such user", which is how the old admin-only lookup
16
+ * reported it.
17
+ */
18
+ async function resolveOwnerUserId(client, appId, owner) {
19
+ if (!owner.includes("@"))
20
+ return owner;
21
+ const lookup = await client.lookupUserByEmail(appId, owner);
22
+ if (!lookup?.exists || !lookup.user) {
23
+ throw new Error(`No app user found with email "${owner}" in app ${appId}.`);
24
+ }
25
+ info(`Resolved owner email to userId: ${lookup.user.userId}`);
26
+ return lookup.user.userId;
27
+ }
7
28
  export function registerDocumentsCommands(program) {
8
29
  const documents = program
9
30
  .command("documents")
10
31
  .description("Manage documents within an app")
11
32
  .addHelpText("after", `
12
33
  Examples:
34
+ $ primitive documents list --user-id <user-id>
35
+ $ primitive documents get <document-id>
36
+ $ primitive documents create "My Doc"
37
+ $ primitive documents create "My Doc" --owner user@example.com
38
+ $ primitive documents delete <document-id> -y
39
+ $ primitive documents permissions list <document-id>
40
+ $ primitive documents permissions grant <document-id> --user-id <user-id> --permission reader
41
+ $ primitive documents permissions revoke <document-id> <user-id>
42
+ $ primitive documents records models <document-id>
43
+ $ primitive documents records describe <document-id> <model-name>
44
+ $ primitive documents records query <document-id> <model-name> --filter '{"status":"open"}'
45
+ $ primitive documents records count <document-id> <model-name>
46
+ $ primitive documents records save <document-id> <model-name> --data '{"qty":10}'
47
+ $ primitive documents records patch <document-id> <model-name> <record-id> --data '{"qty":11}'
48
+ $ primitive documents records delete <document-id> <model-name> <record-id> -y
49
+ $ primitive documents records aggregate <document-id> <model-name> --op avg --field price
50
+ $ primitive documents records bulk <document-id> --data-file ops.json -y
51
+ $ primitive documents dump <document-id>
52
+ $ primitive documents stats <document-id>
13
53
  $ primitive documents transfer-owner <document-id> <new-owner-id>
14
54
  $ primitive documents group-permissions list <document-id>
15
55
  `);
56
+ // ---- List a user's documents ----
57
+ // No app-wide enumeration: `--user-id` is required (the admin token has no
58
+ // "self"). Reads the per-user permission partition via the admin endpoint.
59
+ documents
60
+ .command("list")
61
+ .description("List a user's documents")
62
+ .requiredOption("--user-id <id>", "User ID whose documents to list")
63
+ .option("--app <app-id>", "App ID")
64
+ .option("--json", "Output as JSON")
65
+ .action(async (options) => {
66
+ const resolvedAppId = resolveAppId(undefined, options);
67
+ const client = new ApiClient();
68
+ try {
69
+ const list = await client.listAdminDocuments(resolvedAppId, options.userId);
70
+ if (options.json) {
71
+ json(list);
72
+ return;
73
+ }
74
+ if (list.length === 0) {
75
+ info("No documents found.");
76
+ return;
77
+ }
78
+ console.log(formatTable(list, [
79
+ { header: "DOCUMENT_ID", key: "documentId", format: formatId },
80
+ { header: "TITLE", key: "title", flex: true },
81
+ { header: "PERMISSION", key: "permission" },
82
+ { header: "GRANTED", key: "grantedAt", format: formatDate },
83
+ ]));
84
+ }
85
+ catch (err) {
86
+ error(err.message);
87
+ process.exit(1);
88
+ }
89
+ });
90
+ // ---- Get a single document's metadata ----
91
+ documents
92
+ .command("get")
93
+ .description("Show a document's metadata and the caller's access to it")
94
+ .argument("<document-id>", "Document ID")
95
+ .option("--app <app-id>", "App ID")
96
+ .option("--json", "Output as JSON")
97
+ .action(async (documentId, options) => {
98
+ const resolvedAppId = resolveAppId(undefined, options);
99
+ const client = new ApiClient();
100
+ try {
101
+ const doc = await client.getDocument(resolvedAppId, documentId);
102
+ if (options.json) {
103
+ json(doc);
104
+ return;
105
+ }
106
+ keyValue("Document ID", doc.documentId);
107
+ keyValue("Title", doc.title);
108
+ keyValue("Created By", doc.createdBy);
109
+ keyValue("Created", formatDate(doc.createdAt));
110
+ keyValue("Modified", formatDate(doc.modifiedAt ?? doc.lastModified));
111
+ // `GET documents/:id` reports the caller's own access, not the
112
+ // document's grant/alias/collection inventory — printing counts for
113
+ // fields the endpoint never returns read as a definitive "0" for
114
+ // documents that demonstrably have them. Use
115
+ // `documents permissions list` for the grant list.
116
+ if (doc.permission)
117
+ keyValue("Your Permission", doc.permission);
118
+ if (doc.accessSource)
119
+ keyValue("Access Source", doc.accessSource);
120
+ if (doc.linkAccess)
121
+ keyValue("Link Access", doc.linkAccess);
122
+ }
123
+ catch (err) {
124
+ error(err.message);
125
+ process.exit(1);
126
+ }
127
+ });
128
+ // ---- Create a document ----
129
+ // The same `POST /documents` every client uses (#2763). Who ends up owning
130
+ // the document is the server's call, decided from the token's identity —
131
+ // the help below states that matrix rather than the CLI guessing at a role.
132
+ documents
133
+ .command("create")
134
+ .description("Create a document")
135
+ .argument("<title>", "Document title")
136
+ .option("--app <app-id>", "App ID")
137
+ .option("--owner <userId-or-email>", "User ID or email to own the document (super-admin or assigned-console-admin tokens only)")
138
+ .option("--json", "Output as JSON")
139
+ .addHelpText("after", `
140
+ Ownership:
141
+ An app-user token — member, admin, or owner — always creates the document
142
+ owned by the caller: the server ignores --owner for those tokens.
143
+ A super-admin token, or a console-admin token assigned to this app, acts
144
+ through an admin shadow app user — without --owner that shadow user owns the
145
+ document, with --owner the named user does.
146
+ A console admin not assigned to this app has no access to the app at all.
147
+
148
+ An email given to --owner is resolved to a user id before anything is
149
+ created; a user who is not in the app fails the command without creating a
150
+ document.
151
+ `)
152
+ .action(async (title, options) => {
153
+ const resolvedAppId = resolveAppId(undefined, options);
154
+ const client = new ApiClient();
155
+ let ownerUserId;
156
+ if (options.owner) {
157
+ try {
158
+ ownerUserId = await resolveOwnerUserId(client, resolvedAppId, options.owner);
159
+ }
160
+ catch (err) {
161
+ error(err.message);
162
+ process.exit(1);
163
+ }
164
+ }
165
+ try {
166
+ const result = await client.createDocument(resolvedAppId, {
167
+ title,
168
+ ...(ownerUserId ? { createdBy: ownerUserId } : {}),
169
+ });
170
+ if (options.json) {
171
+ json(result);
172
+ return;
173
+ }
174
+ success("Document created.");
175
+ keyValue("Document ID", result.documentId);
176
+ keyValue("Title", result.title);
177
+ keyValue("Owner", result.createdBy);
178
+ }
179
+ catch (err) {
180
+ error(err.message);
181
+ process.exit(1);
182
+ }
183
+ });
184
+ // ---- Delete a document ----
185
+ // The runtime-resource delete verb (#2756): the server endpoint cascades
186
+ // through records, Yjs history, blobs, aliases and permissions, so an
187
+ // operator resetting an app's data no longer needs a one-off SDK script.
188
+ // Who may run it is the server's decision, so the help below states that
189
+ // model (#2763) rather than leaving it to be discovered from a 403.
190
+ documents
191
+ .command("delete")
192
+ .description("Delete a document and all its data (records, history, blobs, aliases, permissions)")
193
+ .argument("<document-id>", "Document ID")
194
+ .option("--app <app-id>", "App ID")
195
+ .option("-y, --yes", "Skip confirmation prompt")
196
+ .option("--json", "Output as JSON")
197
+ .addHelpText("after", `
198
+ Permissions (enforced by the server, not the CLI):
199
+ The document's owner, the app owner, and super-admin or assigned-console-admin
200
+ tokens — which act with app-owner authority — delete directly.
201
+ Everyone else, including app-role admins, can delete only when a containing
202
+ collection's document.delete rule allows it: an app admin cannot delete a
203
+ standalone document (one in no collection) they do not own.
204
+ The app's root document cannot be deleted.
205
+ `)
206
+ .action(async (documentId, options) => {
207
+ const resolvedAppId = resolveAppId(undefined, options);
208
+ if (!options.yes) {
209
+ let confirm;
210
+ try {
211
+ confirm = await confirmPrompt(`Delete document ${documentId}? This permanently deletes its records, ` +
212
+ `update history, blobs, aliases, and permissions. This cannot be undone.`);
213
+ }
214
+ catch (err) {
215
+ error(err.message);
216
+ process.exit(1);
217
+ }
218
+ if (!confirm) {
219
+ info("Cancelled.");
220
+ return;
221
+ }
222
+ }
223
+ const client = new ApiClient();
224
+ try {
225
+ const result = await client.deleteDocument(resolvedAppId, documentId);
226
+ if (options.json) {
227
+ json(result);
228
+ return;
229
+ }
230
+ success(`Document ${documentId} deleted.`);
231
+ }
232
+ catch (err) {
233
+ // Server refusals (404 not found, 403 root document / owner-only, the
234
+ // non-owner fail-closed 503) are printed verbatim, never swallowed.
235
+ error(err.message);
236
+ process.exit(1);
237
+ }
238
+ });
239
+ // ---- User-level permissions subcommand group ----
240
+ const permissions = documents
241
+ .command("permissions")
242
+ .description("Manage user-level permissions on a document");
243
+ // List user-level permissions
244
+ permissions
245
+ .command("list")
246
+ .description("List user-level permissions for a document")
247
+ .argument("<document-id>", "Document ID")
248
+ .option("--app <app-id>", "App ID")
249
+ .option("--json", "Output as JSON")
250
+ .action(async (documentId, options) => {
251
+ const resolvedAppId = resolveAppId(undefined, options);
252
+ const client = new ApiClient();
253
+ try {
254
+ const result = await client.listDocumentPermissions(resolvedAppId, documentId);
255
+ const list = Array.isArray(result) ? result : result?.permissions ?? [];
256
+ if (options.json) {
257
+ json(list);
258
+ return;
259
+ }
260
+ if (list.length === 0) {
261
+ info("No permissions found.");
262
+ return;
263
+ }
264
+ console.log(formatTable(list, [
265
+ { header: "USER_ID", key: "userId", format: formatId },
266
+ { header: "EMAIL", key: "email", flex: true },
267
+ { header: "PERMISSION", key: "permission" },
268
+ { header: "GRANTED", key: "grantedAt", format: formatDate },
269
+ ]));
270
+ }
271
+ catch (err) {
272
+ error(err.message);
273
+ process.exit(1);
274
+ }
275
+ });
276
+ // Grant (or update) a user-level permission
277
+ permissions
278
+ .command("grant")
279
+ .description("Grant or update a user's permission on a document")
280
+ .argument("<document-id>", "Document ID")
281
+ .option("--user-id <id>", "User ID to grant the permission to")
282
+ .option("--email <email>", "Email of the user to grant the permission to")
283
+ .requiredOption("--permission <permission>", "Permission level: reader or read-write")
284
+ .option("--app <app-id>", "App ID")
285
+ .option("--json", "Output as JSON")
286
+ .action(async (documentId, options) => {
287
+ if (!options.userId && !options.email) {
288
+ error("Either --user-id or --email is required.");
289
+ process.exit(1);
290
+ }
291
+ if (options.userId && options.email) {
292
+ error("Provide only one of --user-id or --email, not both.");
293
+ process.exit(1);
294
+ }
295
+ if (!["reader", "read-write"].includes(options.permission)) {
296
+ error("Invalid --permission. Must be 'reader' or 'read-write'.");
297
+ process.exit(1);
298
+ }
299
+ const resolvedAppId = resolveAppId(undefined, options);
300
+ const client = new ApiClient();
301
+ try {
302
+ const entry = options.userId
303
+ ? { userId: options.userId, permission: options.permission }
304
+ : { email: options.email, permission: options.permission };
305
+ const result = await client.grantDocumentPermission(resolvedAppId, documentId, [entry]);
306
+ if (options.json) {
307
+ json(result);
308
+ return;
309
+ }
310
+ const target = options.userId ? options.userId : options.email;
311
+ success(`Permission '${options.permission}' granted to ${target}.`);
312
+ }
313
+ catch (err) {
314
+ error(err.message);
315
+ process.exit(1);
316
+ }
317
+ });
318
+ // Revoke a user-level permission
319
+ permissions
320
+ .command("revoke")
321
+ .description("Revoke a user's permission on a document")
322
+ .argument("<document-id>", "Document ID")
323
+ .argument("[user-id]", "User ID to revoke (or use --email)")
324
+ .option("--email <email>", "Email of the user to revoke (alternative to <user-id>)")
325
+ .option("--app <app-id>", "App ID")
326
+ .option("-y, --yes", "Skip confirmation prompt")
327
+ .action(async (documentId, userId, options) => {
328
+ if (!userId && !options.email) {
329
+ error("Either a <user-id> argument or --email is required.");
330
+ process.exit(1);
331
+ }
332
+ if (userId && options.email) {
333
+ error("Provide only one of <user-id> or --email, not both.");
334
+ process.exit(1);
335
+ }
336
+ const target = userId ?? options.email;
337
+ const resolvedAppId = resolveAppId(undefined, options);
338
+ if (!options.yes) {
339
+ let confirm;
340
+ try {
341
+ confirm = await confirmPrompt(`Revoke permission for ${target} on document ${documentId}?`);
342
+ }
343
+ catch (err) {
344
+ error(err.message);
345
+ process.exit(1);
346
+ }
347
+ if (!confirm) {
348
+ info("Cancelled.");
349
+ return;
350
+ }
351
+ }
352
+ const client = new ApiClient();
353
+ try {
354
+ await client.revokeDocumentPermission(resolvedAppId, documentId, {
355
+ userId,
356
+ email: options.email,
357
+ });
358
+ success(`Permission revoked for ${target}.`);
359
+ }
360
+ catch (err) {
361
+ error(err.message);
362
+ process.exit(1);
363
+ }
364
+ });
365
+ // ---- Records introspection subcommand group ----
366
+ const records = documents
367
+ .command("records")
368
+ .description("Inspect a document's models and records");
369
+ // List model names
370
+ records
371
+ .command("models")
372
+ .description("List model names in a document")
373
+ .argument("<document-id>", "Document ID")
374
+ .option("--app <app-id>", "App ID")
375
+ .option("--json", "Output as JSON")
376
+ .action(async (documentId, options) => {
377
+ const resolvedAppId = resolveAppId(undefined, options);
378
+ const client = new ApiClient();
379
+ try {
380
+ const result = await client.getDocumentSchema(resolvedAppId, documentId);
381
+ const models = result?.modelNames ?? Object.keys(result?.schema?.models ?? {});
382
+ if (options.json) {
383
+ json(models);
384
+ return;
385
+ }
386
+ if (models.length === 0) {
387
+ info("No models found.");
388
+ return;
389
+ }
390
+ for (const m of models) {
391
+ console.log(` ${m}`);
392
+ }
393
+ }
394
+ catch (err) {
395
+ error(err.message);
396
+ process.exit(1);
397
+ }
398
+ });
399
+ // Describe a model's fields and indexes
400
+ records
401
+ .command("describe")
402
+ .description("Show a model's fields and indexes")
403
+ .argument("<document-id>", "Document ID")
404
+ .argument("<model-name>", "Model name")
405
+ .option("--app <app-id>", "App ID")
406
+ .option("--json", "Output as JSON")
407
+ .action(async (documentId, modelName, options) => {
408
+ const resolvedAppId = resolveAppId(undefined, options);
409
+ const client = new ApiClient();
410
+ try {
411
+ const result = await client.getDocumentSchema(resolvedAppId, documentId);
412
+ const model = result?.schema?.models?.[modelName];
413
+ if (!model) {
414
+ if (options.json) {
415
+ json(null);
416
+ return;
417
+ }
418
+ info(`Model '${modelName}' not found in this document.`);
419
+ return;
420
+ }
421
+ const fieldsObj = model.fields ?? {};
422
+ const rows = Object.entries(fieldsObj).map(([name, meta]) => ({
423
+ field: name,
424
+ type: meta?.type ?? "unknown",
425
+ indexed: meta?.indexed ? "yes" : "",
426
+ unique: meta?.unique ? "yes" : "",
427
+ required: meta?.required ? "yes" : "",
428
+ }));
429
+ if (options.json) {
430
+ json({ fields: fieldsObj, constraints: model.constraints ?? {} });
431
+ return;
432
+ }
433
+ if (rows.length === 0) {
434
+ info("No fields detected for this model.");
435
+ return;
436
+ }
437
+ console.log(formatTable(rows, [
438
+ { header: "FIELD", key: "field", flex: true },
439
+ { header: "TYPE", key: "type" },
440
+ { header: "INDEXED", key: "indexed" },
441
+ { header: "UNIQUE", key: "unique" },
442
+ { header: "REQUIRED", key: "required" },
443
+ ]));
444
+ }
445
+ catch (err) {
446
+ error(err.message);
447
+ process.exit(1);
448
+ }
449
+ });
450
+ // Query records in a document model
451
+ records
452
+ .command("query")
453
+ .description("Query records in a document model")
454
+ .argument("<document-id>", "Document ID")
455
+ .argument("<model-name>", "Model name to query")
456
+ .option("--app <app-id>", "App ID")
457
+ .option("--filter <json>", "Filter as JSON (e.g. '{\"status\":\"open\"}')")
458
+ .option("--filter-file <path>", "Read filter from a JSON or TOML file")
459
+ .option("--limit <n>", "Maximum number of records to return (max 100)", parseInt)
460
+ .option("--cursor <cursor>", "Pagination cursor from a previous query")
461
+ .option("--json", "Output as JSON")
462
+ .action(async (documentId, modelName, options) => {
463
+ const resolvedAppId = resolveAppId(undefined, options);
464
+ const filter = parseFilterOptions(options);
465
+ const client = new ApiClient();
466
+ try {
467
+ const result = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { filter, limit: options.limit, cursor: options.cursor });
468
+ if (options.json) {
469
+ json(result);
470
+ return;
471
+ }
472
+ const items = result.items;
473
+ if (items.length === 0) {
474
+ info("No records found.");
475
+ return;
476
+ }
477
+ const allKeys = new Set();
478
+ for (const rec of items) {
479
+ for (const key of Object.keys(rec))
480
+ allKeys.add(key);
481
+ }
482
+ const orderedKeys = [];
483
+ if (allKeys.has("id")) {
484
+ orderedKeys.push("id");
485
+ allKeys.delete("id");
486
+ }
487
+ orderedKeys.push(...[...allKeys].sort());
488
+ const columns = orderedKeys.map((key) => ({
489
+ header: key.toUpperCase(),
490
+ key,
491
+ format: (v) => {
492
+ if (v === null || v === undefined)
493
+ return "—";
494
+ if (typeof v === "object")
495
+ return JSON.stringify(v);
496
+ return String(v);
497
+ },
498
+ }));
499
+ console.log(formatTable(items, columns));
500
+ if (result.nextCursor) {
501
+ info(`More results available. Use --cursor ${result.nextCursor}`);
502
+ }
503
+ }
504
+ catch (err) {
505
+ error(err.message);
506
+ process.exit(1);
507
+ }
508
+ });
509
+ // Get a single record by id (mirrors `databases records get`, issue #2357).
510
+ records
511
+ .command("get")
512
+ .description("Get a single record by id from a document model")
513
+ .argument("<document-id>", "Document ID")
514
+ .argument("<model-name>", "Model name")
515
+ .argument("<record-id>", "Record ID")
516
+ .option("--app <app-id>", "App ID")
517
+ .option("--json", "Output as JSON")
518
+ .action(async (documentId, modelName, recordId, options) => {
519
+ const resolvedAppId = resolveAppId(undefined, options);
520
+ const client = new ApiClient();
521
+ try {
522
+ // A single-record fetch is a query filtered by primary-key id (there is
523
+ // no dedicated get endpoint — reuse queryDocumentRecords, exactly as
524
+ // the `databases records get` twin reuses queryDatabaseRecords).
525
+ const result = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { filter: { id: recordId }, limit: 1 });
526
+ const record = result.items[0];
527
+ if (options.json) {
528
+ json(record ?? null);
529
+ return;
530
+ }
531
+ if (!record) {
532
+ info(`No record found with id ${recordId} in ${modelName}.`);
533
+ return;
534
+ }
535
+ // `get` is a data command, so its fields go to stdout via `result()`
536
+ // — the #711 split (`result()` = the data the caller asked for,
537
+ // `keyValue()` = a post-action diagnostic summary). Issue #2437; the
538
+ // databases twin renders identically.
539
+ for (const [key, value] of Object.entries(record)) {
540
+ const rendered = value === null || value === undefined
541
+ ? "—"
542
+ : typeof value === "object"
543
+ ? JSON.stringify(value)
544
+ : String(value);
545
+ printResult(key, rendered);
546
+ }
547
+ }
548
+ catch (err) {
549
+ error(err.message);
550
+ process.exit(1);
551
+ }
552
+ });
553
+ // Count records in a document model
554
+ records
555
+ .command("count")
556
+ .description("Count records in a document model")
557
+ .argument("<document-id>", "Document ID")
558
+ .argument("<model-name>", "Model name to count")
559
+ .option("--app <app-id>", "App ID")
560
+ .option("--filter <json>", "Filter as JSON (e.g. '{\"status\":\"open\"}')")
561
+ .option("--filter-file <path>", "Read filter from a JSON or TOML file")
562
+ .option("--json", "Output as JSON")
563
+ .action(async (documentId, modelName, options) => {
564
+ const resolvedAppId = resolveAppId(undefined, options);
565
+ const filter = parseFilterOptions(options);
566
+ const client = new ApiClient();
567
+ try {
568
+ const result = await client.countDocumentRecords(resolvedAppId, documentId, modelName, { filter });
569
+ if (options.json) {
570
+ json(result);
571
+ return;
572
+ }
573
+ console.log(` ${result.count}`);
574
+ }
575
+ catch (err) {
576
+ error(err.message);
577
+ process.exit(1);
578
+ }
579
+ });
580
+ // Aggregate records in a document model (issue #2437). Mirrors
581
+ // `databases records aggregate` flag for flag and rendering for rendering —
582
+ // including the deliberate `keyValue()` for the ungrouped scalar, so the two
583
+ // twins keep printing the same thing.
584
+ const AGGREGATE_OPS = ["count", "sum", "avg", "min", "max"];
585
+ records
586
+ .command("aggregate")
587
+ .description("Aggregate records in a document model (count/sum/avg/min/max, optional group-by)")
588
+ .argument("<document-id>", "Document ID")
589
+ .argument("<model-name>", "Model name")
590
+ .option("--app <app-id>", "App ID")
591
+ .requiredOption("--op <operation>", `Aggregate operation: ${AGGREGATE_OPS.join("|")}`)
592
+ .option("--field <field>", "Field to aggregate (required for sum/avg/min/max)")
593
+ .option("--group-by <field>", "Group results by this plain field (repeatable; StringSet fields are not supported)", (value, previous = []) => previous.concat(value), [])
594
+ .option("--filter <json>", "Filter as JSON")
595
+ .option("--filter-file <path>", "Read filter from a JSON or TOML file")
596
+ .option("--json", "Output as JSON")
597
+ .action(async (documentId, modelName, options) => {
598
+ const resolvedAppId = resolveAppId(undefined, options);
599
+ if (!AGGREGATE_OPS.includes(options.op)) {
600
+ error(`Invalid --op "${options.op}". Expected one of: ${AGGREGATE_OPS.join(", ")}.`);
601
+ process.exit(1);
602
+ }
603
+ const operation = { type: options.op };
604
+ if (options.op === "count") {
605
+ if (options.field) {
606
+ error("--field is not valid for --op count.");
607
+ process.exit(1);
608
+ }
609
+ }
610
+ else {
611
+ if (!options.field) {
612
+ error(`--op ${options.op} requires --field.`);
613
+ process.exit(1);
614
+ }
615
+ operation.field = options.field;
616
+ }
617
+ const filter = parseFilterOptions(options);
618
+ const groupBy = options.groupBy || [];
619
+ const client = new ApiClient();
620
+ try {
621
+ const response = await client.aggregateDocumentRecords(resolvedAppId, documentId, modelName, { operations: [operation], groupBy, filter });
622
+ if (options.json) {
623
+ json(response);
624
+ return;
625
+ }
626
+ const aggregated = response?.result;
627
+ // Same labelling rule as the databases twin: the field name is
628
+ // reproduced verbatim, never case-folded (issue #2357).
629
+ const valueLabel = options.op === "count"
630
+ ? "COUNT"
631
+ : `${options.op.toUpperCase()}(${options.field})`;
632
+ const scalarLabel = options.op === "count"
633
+ ? "count"
634
+ : `${options.op.toLowerCase()}(${options.field})`;
635
+ if (groupBy.length === 0) {
636
+ const value = (aggregated ?? {})[options.op === "count" ? "count" : `${options.op}_${options.field}`];
637
+ keyValue(scalarLabel, value === null || value === undefined ? "—" : String(value));
638
+ return;
639
+ }
640
+ if (aggregated &&
641
+ typeof aggregated === "object" &&
642
+ !Array.isArray(aggregated) &&
643
+ Object.values(aggregated).every((v) => typeof v !== "object" || v === null)) {
644
+ const rows = Object.entries(aggregated).map(([group, value]) => ({
645
+ group,
646
+ value: value === null || value === undefined ? "—" : String(value),
647
+ }));
648
+ console.log(formatTable(rows, [
649
+ { header: "GROUP", key: "group" },
650
+ { header: valueLabel, key: "value" },
651
+ ]));
652
+ }
653
+ else {
654
+ console.log(JSON.stringify(aggregated ?? {}, null, 2));
655
+ }
656
+ }
657
+ catch (err) {
658
+ error(err.message);
659
+ process.exit(1);
660
+ }
661
+ });
662
+ // ---- Record writes (#1964 Phase 4) ----
663
+ // Server-side writes through the document facade: Yjs-first, CRDT-safe,
664
+ // broadcast to connected clients. Requires read-write or higher on the
665
+ // document (or an admin token). `--data` / `--data-file` are validated
666
+ // CLI-side before any request is sent.
667
+ // `--data`/`--data-file` parsing (including the object-shape check and the
668
+ // both-flags rule) is the shared `parseDataOption` helper, so these verbs
669
+ // behave and diagnose identically to `metadata` and `databases records`.
670
+ // Create or replace a record
671
+ records
672
+ .command("save")
673
+ .description("Create or replace a record in a document model")
674
+ .argument("<document-id>", "Document ID")
675
+ .argument("<model-name>", "Model name to write to")
676
+ .option("--app <app-id>", "App ID")
677
+ .option("--id <record-id>", "Record ID (a ULID is generated when omitted)")
678
+ .option("--data <json>", "Record fields as JSON (e.g. '{\"qty\":10}')")
679
+ .option("--data-file <file>", "Read record fields from a JSON file")
680
+ .option("--upsert-on <field>", "Update the record whose <field> matches instead of creating")
681
+ .option("--json", "Output as JSON")
682
+ .action(async (documentId, modelName, options) => {
683
+ const data = parseDataOption(options, "record fields");
684
+ const resolvedAppId = resolveAppId(undefined, options);
685
+ const recordId = options.id || ulid();
686
+ const writeOptions = options.upsertOn ? { upsertOn: options.upsertOn } : undefined;
687
+ const client = new ApiClient();
688
+ try {
689
+ const result = await client.saveDocumentRecord(resolvedAppId, documentId, modelName, { id: recordId, data, ...(writeOptions ? { options: writeOptions } : {}) });
690
+ if (options.json) {
691
+ json(result);
692
+ return;
693
+ }
694
+ success(`Record saved: ${result.record?.id ?? recordId}`);
695
+ }
696
+ catch (err) {
697
+ error(err.message);
698
+ process.exit(1);
699
+ }
700
+ });
701
+ // Merge fields into an existing record
702
+ records
703
+ .command("patch")
704
+ .description("Merge fields into an existing record in a document model")
705
+ .argument("<document-id>", "Document ID")
706
+ .argument("<model-name>", "Model name")
707
+ .argument("<record-id>", "Record ID to patch")
708
+ .option("--app <app-id>", "App ID")
709
+ .option("--data <json>", "Fields to merge as JSON (e.g. '{\"status\":\"closed\"}')")
710
+ .option("--data-file <file>", "Read fields from a JSON file")
711
+ .option("--json", "Output as JSON")
712
+ .action(async (documentId, modelName, recordId, options) => {
713
+ const data = parseDataOption(options, "fields to merge");
714
+ const resolvedAppId = resolveAppId(undefined, options);
715
+ const client = new ApiClient();
716
+ try {
717
+ const result = await client.patchDocumentRecord(resolvedAppId, documentId, modelName, recordId, { data });
718
+ if (options.json) {
719
+ json(result);
720
+ return;
721
+ }
722
+ success(`Record patched: ${recordId}`);
723
+ }
724
+ catch (err) {
725
+ error(err.message);
726
+ process.exit(1);
727
+ }
728
+ });
729
+ // Delete a record
730
+ records
731
+ .command("delete")
732
+ .description("Delete a record from a document model")
733
+ .argument("<document-id>", "Document ID")
734
+ .argument("<model-name>", "Model name")
735
+ .argument("<record-id>", "Record ID to delete")
736
+ .option("--app <app-id>", "App ID")
737
+ .option("-y, --yes", "Skip confirmation prompt")
738
+ .option("--json", "Output as JSON")
739
+ .action(async (documentId, modelName, recordId, options) => {
740
+ const resolvedAppId = resolveAppId(undefined, options);
741
+ if (!options.yes) {
742
+ const confirmed = await confirmPrompt(`Delete record ${recordId} from ${modelName} in document ${documentId}?`);
743
+ if (!confirmed) {
744
+ info("Cancelled.");
745
+ return;
746
+ }
747
+ }
748
+ const client = new ApiClient();
749
+ try {
750
+ const result = await client.deleteDocumentRecord(resolvedAppId, documentId, modelName, recordId);
751
+ if (options.json) {
752
+ json(result);
753
+ return;
754
+ }
755
+ // A delete of a missing record is a silent no-op server-side, so
756
+ // this reports the request outcome, not record existence.
757
+ success(`Record deleted: ${recordId}`);
758
+ }
759
+ catch (err) {
760
+ error(err.message);
761
+ process.exit(1);
762
+ }
763
+ });
764
+ // Apply an atomic multi-model operations blob
765
+ records
766
+ .command("bulk")
767
+ .description("Apply an atomic create/patch/delete operations blob to a document")
768
+ .argument("<document-id>", "Document ID")
769
+ .requiredOption("--data-file <file>", "JSON file with { operations: [...] } or a bare operations array. " +
770
+ "Each op is { model, action: create|patch|delete, id, data, precondition? } " +
771
+ "— `data` holds the record fields and is required (non-empty) on " +
772
+ "create/patch, not allowed on delete; a create `id` must be a 26-char " +
773
+ "uppercase Crockford ULID")
774
+ .option("--app <app-id>", "App ID")
775
+ .option("-y, --yes", "Skip confirmation prompt")
776
+ .option("--json", "Output as JSON")
777
+ .action(async (documentId, options) => {
778
+ const parsed = parseDataOption(options, "operations blob", {
779
+ allowArray: true,
780
+ });
781
+ const operations = Array.isArray(parsed) ? parsed : parsed?.operations;
782
+ if (!Array.isArray(operations) || operations.length === 0) {
783
+ error("--data-file must contain { operations: [...] } or a non-empty operations array.");
784
+ process.exit(1);
785
+ }
786
+ const resolvedAppId = resolveAppId(undefined, options);
787
+ if (!options.yes) {
788
+ const confirmed = await confirmPrompt(`Apply ${operations.length} operation(s) atomically to document ${documentId}?`);
789
+ if (!confirmed) {
790
+ info("Cancelled.");
791
+ return;
792
+ }
793
+ }
794
+ const client = new ApiClient();
795
+ try {
796
+ const result = await client.bulkDocumentRecords(resolvedAppId, documentId, operations);
797
+ if (options.json) {
798
+ json(result);
799
+ return;
800
+ }
801
+ success(`Applied ${result.applied} operation(s): ` +
802
+ `${result.added.length} added, ${result.updated.length} updated, ${result.deleted} deleted.`);
803
+ }
804
+ catch (err) {
805
+ error(err.message);
806
+ process.exit(1);
807
+ }
808
+ });
809
+ // Dump a document's records grouped by model (CLI-side composition)
810
+ documents
811
+ .command("dump")
812
+ .description("Dump a document's records grouped by model as JSON")
813
+ .argument("<document-id>", "Document ID")
814
+ .option("--app <app-id>", "App ID")
815
+ .option("--output <file>", "Write the JSON dump to a file instead of stdout")
816
+ .option("--json", "Output as JSON (default)")
817
+ .action(async (documentId, options) => {
818
+ const resolvedAppId = resolveAppId(undefined, options);
819
+ const client = new ApiClient();
820
+ try {
821
+ // Model names come from the schema; records are assembled from paged
822
+ // reads per model (not a single atomic snapshot — the paged path is
823
+ // the contract, same pattern as `databases export`).
824
+ const schema = await client.getDocumentSchema(resolvedAppId, documentId);
825
+ const modelNames = schema?.modelNames ?? Object.keys(schema?.schema?.models ?? {});
826
+ const grouped = {};
827
+ for (const modelName of modelNames) {
828
+ const records = [];
829
+ let cursor;
830
+ do {
831
+ const page = await client.queryDocumentRecords(resolvedAppId, documentId, modelName, { limit: 100, cursor });
832
+ records.push(...page.items);
833
+ cursor = page.nextCursor;
834
+ } while (cursor);
835
+ grouped[modelName] = records;
836
+ }
837
+ const output = JSON.stringify(grouped, null, 2);
838
+ if (options.output) {
839
+ fs.writeFileSync(options.output, output);
840
+ success(`Document dump written to ${options.output}`);
841
+ return;
842
+ }
843
+ console.log(output);
844
+ }
845
+ catch (err) {
846
+ error(err.message);
847
+ process.exit(1);
848
+ }
849
+ });
850
+ // Document statistics (platform-vocabulary projection)
851
+ documents
852
+ .command("stats")
853
+ .description("Show a document's record/model/blob counts and approximate size")
854
+ .argument("<document-id>", "Document ID")
855
+ .option("--app <app-id>", "App ID")
856
+ .option("--json", "Output as JSON")
857
+ .action(async (documentId, options) => {
858
+ const resolvedAppId = resolveAppId(undefined, options);
859
+ const client = new ApiClient();
860
+ try {
861
+ const result = await client.getDocumentStats(resolvedAppId, documentId);
862
+ if (options.json) {
863
+ json(result);
864
+ return;
865
+ }
866
+ keyValue("Document ID", result.documentId);
867
+ keyValue("Records", result.recordCount);
868
+ keyValue("Models", result.modelCount);
869
+ keyValue("Blobs", result.blobCount);
870
+ keyValue("Size (bytes, approx)", result.sizeBytes);
871
+ keyValue("Last modified", result.lastModifiedAt ? formatDate(result.lastModifiedAt) : "—");
872
+ }
873
+ catch (err) {
874
+ error(err.message);
875
+ process.exit(1);
876
+ }
877
+ });
16
878
  // Transfer document ownership
17
879
  documents
18
880
  .command("transfer-owner")
@@ -337,20 +1199,15 @@ Examples:
337
1199
  process.exit(1);
338
1200
  }
339
1201
  const client = new ApiClient();
340
- // Resolve --owner to a userId
1202
+ // Resolve --owner to a userId through the one shared lookup (#2763).
341
1203
  let ownerUserId;
342
1204
  if (options.owner) {
343
- if (options.owner.includes("@")) {
344
- const user = await client.findUserByEmail(resolvedAppId, options.owner);
345
- if (!user) {
346
- error(`No user found with email "${options.owner}" in app ${resolvedAppId}.`);
347
- process.exit(1);
348
- }
349
- ownerUserId = user.userId || user.id;
350
- info(`Resolved owner email to userId: ${ownerUserId}`);
1205
+ try {
1206
+ ownerUserId = await resolveOwnerUserId(client, resolvedAppId, options.owner);
351
1207
  }
352
- else {
353
- ownerUserId = options.owner;
1208
+ catch (err) {
1209
+ error(err.message);
1210
+ process.exit(1);
354
1211
  }
355
1212
  }
356
1213
  try {