@cosmicdrift/kumiko-framework 0.387.0 → 0.389.0

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 (375) hide show
  1. package/AGENTS.md +9 -1
  2. package/agents/examples/anonymous-access.md +1 -1
  3. package/agents/examples/apex-landing.md +204 -0
  4. package/agents/examples/api-worker-split.md +64 -0
  5. package/agents/examples/app-config.md +14 -0
  6. package/agents/examples/auth-mfa.md +115 -0
  7. package/agents/examples/env-composition.md +28 -1
  8. package/agents/examples/golden-path.md +13 -1
  9. package/agents/examples/legal-pages.md +1 -1
  10. package/agents/examples/lifecycle-hooks.md +55 -2
  11. package/agents/examples/list-table-options.md +466 -0
  12. package/agents/examples/pdf-audit-export.md +4 -2
  13. package/agents/examples/prod-boot-options.md +16 -0
  14. package/agents/examples/rate-limiting.md +48 -0
  15. package/agents/examples/record-detail-layout.md +692 -0
  16. package/agents/examples/seed-migration.md +212 -0
  17. package/agents/examples/state-machine.md +3 -2
  18. package/agents/examples/tenant-isolation.md +73 -1
  19. package/agents/examples/wizard-form.md +941 -0
  20. package/agents/examples/writeform-section.md +548 -0
  21. package/agents/examples.md +1 -0
  22. package/agents/features.md +19 -2
  23. package/agents/widgets.md +2 -0
  24. package/dist/api/api-constants.d.ts +1 -0
  25. package/dist/api/api-constants.js +2 -0
  26. package/dist/api/auth-routes-shared.d.ts +2 -0
  27. package/dist/api/auth-routes-shared.js +5 -0
  28. package/dist/api/client-error-route.js +10 -5
  29. package/dist/api/entity-read-access.d.ts +2 -1
  30. package/dist/api/entity-read-access.js +14 -0
  31. package/dist/api/export/export-cells-reference.d.ts +9 -0
  32. package/dist/api/export/export-cells-reference.js +21 -0
  33. package/dist/api/export/export-cells.d.ts +11 -0
  34. package/dist/api/export/export-cells.js +136 -0
  35. package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
  36. package/dist/api/export/export-ciphertext-guard.js +27 -0
  37. package/dist/api/export/export-columns.d.ts +29 -0
  38. package/dist/api/export/export-columns.js +85 -0
  39. package/dist/api/export/export-errors.d.ts +4 -0
  40. package/dist/api/export/export-errors.js +14 -0
  41. package/dist/api/export/export-route.d.ts +4 -0
  42. package/dist/api/export/export-route.js +74 -0
  43. package/dist/api/export/export-rows.d.ts +13 -0
  44. package/dist/api/export/export-rows.js +62 -0
  45. package/dist/api/export/export-screen.d.ts +12 -0
  46. package/dist/api/export/export-screen.js +47 -0
  47. package/dist/api/export/export-xlsx-writer.d.ts +4 -0
  48. package/dist/api/export/export-xlsx-writer.js +21 -0
  49. package/dist/api/extra-route.d.ts +4 -0
  50. package/dist/api/index.d.ts +3 -1
  51. package/dist/api/index.js +3 -1
  52. package/dist/api/pii-leak-guard.d.ts +3 -0
  53. package/dist/api/pii-leak-guard.js +3 -2
  54. package/dist/api/request-context.d.ts +1 -1
  55. package/dist/api/request-context.js +1 -1
  56. package/dist/api/routes.d.ts +16 -0
  57. package/dist/api/routes.js +2 -2
  58. package/dist/api/server-boot-checks.d.ts +10 -0
  59. package/dist/api/server-boot-checks.js +157 -0
  60. package/dist/api/server-event-dispatcher.d.ts +26 -0
  61. package/dist/api/server-event-dispatcher.js +208 -0
  62. package/dist/api/server-extra-routes.d.ts +11 -0
  63. package/dist/api/server-extra-routes.js +206 -0
  64. package/dist/api/server-middleware.d.ts +20 -0
  65. package/dist/api/server-middleware.js +211 -0
  66. package/dist/api/server-routes.d.ts +19 -0
  67. package/dist/api/server-routes.js +299 -0
  68. package/dist/api/server-websocket-routes.d.ts +4 -0
  69. package/dist/api/server-websocket-routes.js +108 -0
  70. package/dist/api/server-wiring.d.ts +32 -0
  71. package/dist/api/server-wiring.js +238 -0
  72. package/dist/api/server.d.ts +10 -15
  73. package/dist/api/server.js +48 -1251
  74. package/dist/api/session-cookies.js +4 -4
  75. package/dist/api/sse-route.d.ts +2 -1
  76. package/dist/api/sse-route.js +27 -13
  77. package/dist/config/index.d.ts +3 -1
  78. package/dist/crypto/owner-transition-rekey.d.ts +5 -1
  79. package/dist/crypto/owner-transition-rekey.js +1 -1
  80. package/dist/crypto/pg-kms-adapter.d.ts +1 -0
  81. package/dist/crypto/pg-kms-adapter.js +12 -18
  82. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  83. package/dist/crypto/pii-field-encryption.js +3 -1
  84. package/dist/db/aggregate-sql.d.ts +23 -0
  85. package/dist/db/aggregate-sql.js +165 -0
  86. package/dist/db/closed-connection.d.ts +9 -0
  87. package/dist/db/closed-connection.js +88 -0
  88. package/dist/db/connection.d.ts +4 -5
  89. package/dist/db/connection.js +29 -20
  90. package/dist/db/dialect.js +1 -1
  91. package/dist/db/distinct-on.d.ts +1 -1
  92. package/dist/db/driver-row-coercion.d.ts +2 -0
  93. package/dist/db/driver-row-coercion.js +186 -0
  94. package/dist/db/driver-value-preparation.d.ts +16 -0
  95. package/dist/db/driver-value-preparation.js +101 -0
  96. package/dist/db/entity-field-encryption.d.ts +1 -0
  97. package/dist/db/entity-field-encryption.js +1 -1
  98. package/dist/db/entity-table-meta.js +103 -110
  99. package/dist/db/event-store-executor-context.d.ts +2 -1
  100. package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
  101. package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
  102. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  103. package/dist/db/event-store-executor-read-aggregate.js +143 -0
  104. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  105. package/dist/db/event-store-executor-read-filter.js +206 -0
  106. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  107. package/dist/db/event-store-executor-read-paging.js +99 -0
  108. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  109. package/dist/db/event-store-executor-read-rows.js +32 -0
  110. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  111. package/dist/db/event-store-executor-read-search.js +221 -0
  112. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  113. package/dist/db/event-store-executor-read-sort.js +130 -0
  114. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  115. package/dist/db/event-store-executor-read-where.js +81 -0
  116. package/dist/db/event-store-executor-read.d.ts +2 -9
  117. package/dist/db/event-store-executor-read.js +48 -701
  118. package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
  119. package/dist/db/event-store-executor-write-atomic.js +41 -0
  120. package/dist/db/event-store-executor-write-steps.d.ts +17 -0
  121. package/dist/db/event-store-executor-write-steps.js +95 -0
  122. package/dist/db/event-store-executor-write.js +405 -509
  123. package/dist/db/event-store-executor.js +2 -1
  124. package/dist/db/executor-managed-tables.js +1 -1
  125. package/dist/db/index.d.ts +7 -3
  126. package/dist/db/index.js +6 -2
  127. package/dist/db/list-aggregate-labels.d.ts +14 -0
  128. package/dist/db/list-aggregate-labels.js +26 -0
  129. package/dist/db/migrate-generator.js +71 -86
  130. package/dist/db/migrate-runner.js +7 -4
  131. package/dist/db/pg-error.js +26 -21
  132. package/dist/db/queries/backfill-pii.js +3 -6
  133. package/dist/db/queries/database-now.js +2 -1
  134. package/dist/db/queries/shadow-swap.js +2 -3
  135. package/dist/db/query.d.ts +16 -15
  136. package/dist/db/query.js +76 -804
  137. package/dist/db/reference-data.js +19 -2
  138. package/dist/db/statement-timeout.js +2 -1
  139. package/dist/db/table-builder.js +113 -166
  140. package/dist/db/table-info.d.ts +10 -0
  141. package/dist/db/table-info.js +45 -0
  142. package/dist/db/temporal-value-guards.d.ts +6 -0
  143. package/dist/db/temporal-value-guards.js +15 -0
  144. package/dist/db/tenant-db-runner.d.ts +1 -0
  145. package/dist/db/tenant-db-runner.js +3 -0
  146. package/dist/db/tenant-db-upsert.d.ts +5 -0
  147. package/dist/db/tenant-db-upsert.js +91 -0
  148. package/dist/db/tenant-db.js +50 -10
  149. package/dist/db/where-clause.d.ts +6 -0
  150. package/dist/db/where-clause.js +236 -0
  151. package/dist/derivatives/delete-stored-file.js +2 -1
  152. package/dist/engine/boot-validator/boot-check.js +1 -1
  153. package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
  154. package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
  155. package/dist/engine/boot-validator/detail-screens.js +2 -2
  156. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  157. package/dist/engine/boot-validator/entity-handler.js +17 -0
  158. package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
  159. package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
  160. package/dist/engine/boot-validator/form-derivations.js +35 -1
  161. package/dist/engine/boot-validator/i18n-keys.js +5 -3
  162. package/dist/engine/boot-validator/index.js +2 -0
  163. package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
  164. package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
  165. package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
  166. package/dist/engine/boot-validator/list-column-layout.js +41 -0
  167. package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
  168. package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
  169. package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
  170. package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
  171. package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
  172. package/dist/engine/boot-validator/list-table-export-options.js +27 -0
  173. package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
  174. package/dist/engine/boot-validator/list-table-options.js +77 -0
  175. package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
  176. package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
  177. package/dist/engine/boot-validator/pii-retention.js +233 -155
  178. package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
  179. package/dist/engine/boot-validator/projection-list-screens.js +3 -13
  180. package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
  181. package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
  182. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  183. package/dist/engine/boot-validator/screens-actions.js +8 -1
  184. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  185. package/dist/engine/boot-validator/screens-forms.js +2 -6
  186. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  187. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  188. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  189. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  190. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  191. package/dist/engine/boot-validator/screens-kind-entity-list.js +235 -0
  192. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  193. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  194. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  195. package/dist/engine/boot-validator/screens-kind-projection-list.js +56 -0
  196. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  197. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  198. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  199. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  200. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  201. package/dist/engine/boot-validator/screens-lists.js +24 -11
  202. package/dist/engine/boot-validator/screens.d.ts +0 -2
  203. package/dist/engine/boot-validator/screens.js +99 -860
  204. package/dist/engine/build-app-schema.d.ts +2 -1
  205. package/dist/engine/build-app-schema.js +65 -97
  206. package/dist/engine/build-config-feature-schema.js +6 -1
  207. package/dist/engine/define-feature.js +13 -2
  208. package/dist/engine/define-handler.js +1 -0
  209. package/dist/engine/entity-handlers.d.ts +13 -0
  210. package/dist/engine/entity-handlers.js +10 -21
  211. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  212. package/dist/engine/entity-list-runtime-options.js +18 -0
  213. package/dist/engine/feature-ast/extractors/handlers.js +1 -0
  214. package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
  215. package/dist/engine/feature-ast/extractors/index.js +1 -1
  216. package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
  217. package/dist/engine/feature-ast/extractors/round1.js +6 -0
  218. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  219. package/dist/engine/feature-ast/index.d.ts +1 -1
  220. package/dist/engine/feature-ast/parse.js +3 -1
  221. package/dist/engine/feature-ast/patch.d.ts +2 -0
  222. package/dist/engine/feature-ast/patch.js +5 -182
  223. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
  224. package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
  225. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  226. package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
  227. package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
  228. package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
  229. package/dist/engine/feature-ast/patterns.d.ts +14 -1
  230. package/dist/engine/feature-ast/patterns.js +44 -51
  231. package/dist/engine/feature-ast/render.js +52 -87
  232. package/dist/engine/feature-builder-state.d.ts +1 -0
  233. package/dist/engine/feature-builder-state.js +1 -0
  234. package/dist/engine/feature-entity-handlers.js +3 -0
  235. package/dist/engine/feature-manifest.d.ts +1 -0
  236. package/dist/engine/feature-manifest.js +1 -0
  237. package/dist/engine/field-access.d.ts +1 -0
  238. package/dist/engine/field-access.js +12 -0
  239. package/dist/engine/form-derivations.js +18 -7
  240. package/dist/engine/handler-field-references.js +1 -1
  241. package/dist/engine/index.d.ts +2 -2
  242. package/dist/engine/index.js +1 -1
  243. package/dist/engine/pattern-library/library.js +2 -1
  244. package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
  245. package/dist/engine/pattern-library/static-schemas.js +9 -0
  246. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  247. package/dist/engine/project-app-schema-for-roles.js +6 -4
  248. package/dist/engine/registry-ingest.js +1 -1
  249. package/dist/engine/schema-builder.js +228 -212
  250. package/dist/engine/screen-filter-ops.js +2 -0
  251. package/dist/engine/screen-helpers.js +1 -1
  252. package/dist/engine/screen-kind.d.ts +7 -0
  253. package/dist/engine/screen-kind.js +1 -0
  254. package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
  255. package/dist/engine/types/feature.d.ts +1 -0
  256. package/dist/engine/types/index.d.ts +4 -3
  257. package/dist/engine/types/screen.d.ts +1 -1
  258. package/dist/engine/types/screen.js +1 -1
  259. package/dist/engine/validate-projection-allowlist.js +2 -1
  260. package/dist/entrypoint/index.d.ts +6 -1
  261. package/dist/entrypoint/index.js +10 -1
  262. package/dist/errors/docs-url.d.ts +1 -1
  263. package/dist/errors/docs-url.js +22 -2
  264. package/dist/es-ops/context.js +1 -1
  265. package/dist/event-store/admin-api.js +1 -1
  266. package/dist/event-store/event-store.d.ts +1 -0
  267. package/dist/event-store/event-store.js +17 -8
  268. package/dist/event-store/provenance-append.js +1 -1
  269. package/dist/files/bind-pending-file-refs.js +3 -3
  270. package/dist/files/create-file-ref-from-bytes.js +7 -1
  271. package/dist/files/create-file-ref.d.ts +3 -0
  272. package/dist/files/create-file-ref.js +11 -2
  273. package/dist/files/file-handle.js +2 -3
  274. package/dist/files/file-routes.d.ts +2 -0
  275. package/dist/files/file-routes.js +40 -10
  276. package/dist/files/provider-resolver.d.ts +1 -0
  277. package/dist/files/provider-resolver.js +1 -1
  278. package/dist/files/record-write-check.d.ts +6 -0
  279. package/dist/files/record-write-check.js +13 -0
  280. package/dist/files/resolve-content-type.js +43 -0
  281. package/dist/files/upload-ledger.d.ts +2 -2
  282. package/dist/files/upload-ledger.js +11 -4
  283. package/dist/i18n/required-surface-keys.js +189 -208
  284. package/dist/internal/stack-support.d.ts +2 -1
  285. package/dist/internal/stack-support.js +1 -1
  286. package/dist/internal/testing.js +1 -1
  287. package/dist/jobs/job-runner-context.d.ts +32 -0
  288. package/dist/jobs/job-runner-context.js +305 -0
  289. package/dist/jobs/job-runner-execution.d.ts +7 -0
  290. package/dist/jobs/job-runner-execution.js +160 -0
  291. package/dist/jobs/job-runner-gates.d.ts +19 -0
  292. package/dist/jobs/job-runner-gates.js +198 -0
  293. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  294. package/dist/jobs/job-runner-helpers.js +61 -2
  295. package/dist/jobs/job-runner-scope.d.ts +44 -0
  296. package/dist/jobs/job-runner-scope.js +102 -0
  297. package/dist/jobs/job-runner-types.d.ts +12 -0
  298. package/dist/jobs/job-runner.d.ts +0 -1
  299. package/dist/jobs/job-runner.js +50 -657
  300. package/dist/observability/escape-hatch-report.d.ts +3 -0
  301. package/dist/observability/escape-hatch-report.js +21 -3
  302. package/dist/pipeline/after-rollback-sink.d.ts +6 -0
  303. package/dist/pipeline/after-rollback-sink.js +32 -0
  304. package/dist/pipeline/cascade-handler.js +165 -138
  305. package/dist/pipeline/dispatch-batch.js +8 -1
  306. package/dist/pipeline/dispatch-query.js +10 -3
  307. package/dist/pipeline/dispatch-shared.d.ts +13 -3
  308. package/dist/pipeline/dispatch-shared.js +62 -710
  309. package/dist/pipeline/dispatch-write.js +296 -189
  310. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  311. package/dist/pipeline/event-dispatcher-delivery.js +6 -6
  312. package/dist/pipeline/event-dispatcher.d.ts +1 -0
  313. package/dist/pipeline/event-dispatcher.js +6 -9
  314. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  315. package/dist/pipeline/handler-context-auth.js +54 -0
  316. package/dist/pipeline/handler-context-config.d.ts +14 -0
  317. package/dist/pipeline/handler-context-config.js +113 -0
  318. package/dist/pipeline/handler-context-db.d.ts +24 -0
  319. package/dist/pipeline/handler-context-db.js +149 -0
  320. package/dist/pipeline/handler-context-dispatch.d.ts +23 -0
  321. package/dist/pipeline/handler-context-dispatch.js +101 -0
  322. package/dist/pipeline/handler-context-events.d.ts +41 -0
  323. package/dist/pipeline/handler-context-events.js +239 -0
  324. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  325. package/dist/pipeline/handler-context-notify.js +55 -0
  326. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  327. package/dist/pipeline/handler-context-observability.js +21 -0
  328. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  329. package/dist/pipeline/handler-context-scope.js +88 -0
  330. package/dist/pipeline/notify-scope.js +12 -1
  331. package/dist/pipeline/pre-handler-gates.js +20 -4
  332. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  333. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  334. package/dist/pipeline/projection-rebuild.js +2 -1
  335. package/dist/pipeline/system-hooks.d.ts +3 -1
  336. package/dist/pipeline/system-hooks.js +55 -6
  337. package/dist/pipeline/write-result-masking.js +13 -10
  338. package/dist/schema-cli/apply-command.d.ts +2 -0
  339. package/dist/schema-cli/apply-command.js +97 -0
  340. package/dist/schema-cli/baseline-command.d.ts +2 -0
  341. package/dist/schema-cli/baseline-command.js +33 -0
  342. package/dist/schema-cli/generate-command.d.ts +2 -0
  343. package/dist/schema-cli/generate-command.js +90 -0
  344. package/dist/schema-cli/shared.d.ts +39 -0
  345. package/dist/schema-cli/shared.js +95 -0
  346. package/dist/schema-cli/status-command.d.ts +2 -0
  347. package/dist/schema-cli/status-command.js +73 -0
  348. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  349. package/dist/schema-cli/validate-boot-input.js +130 -0
  350. package/dist/schema-cli/validate-command.d.ts +2 -0
  351. package/dist/schema-cli/validate-command.js +130 -0
  352. package/dist/schema-cli.d.ts +2 -18
  353. package/dist/schema-cli.js +47 -604
  354. package/dist/search/meilisearch-adapter.js +2 -1
  355. package/dist/search/reindex-entity.js +15 -5
  356. package/dist/ui-types/index.d.ts +2 -2
  357. package/dist/ui-types/index.js +1 -1
  358. package/dist/upgrade-changelog.d.ts +2 -0
  359. package/dist/upgrade-changelog.js +14 -0
  360. package/dist/upgrade-cli.d.ts +3 -12
  361. package/dist/upgrade-cli.js +171 -197
  362. package/dist/upgrade-marker-parse.d.ts +32 -0
  363. package/dist/upgrade-marker-parse.js +73 -0
  364. package/package.json +9 -6
  365. package/src/changes.json +555 -0
  366. package/src/scripts/codemod/codemod-support.ts +1 -1
  367. package/src/scripts/codemod/db-factory.ts +1 -1
  368. package/src/scripts/codemod/feature-layout.ts +10 -2
  369. package/src/scripts/codemod/object-form.ts +41 -4
  370. package/src/scripts/codemod/package-merge.ts +58 -8
  371. package/src/scripts/codemod/pii-personal-migration.ts +175 -145
  372. package/src/scripts/codemod/testing-injection.ts +122 -42
  373. package/src/scripts/codemod/testing-move.ts +29 -7
  374. package/dist/engine/provider-plugin-context.d.ts +0 -3
  375. package/dist/engine/provider-plugin-context.js +0 -28
@@ -210,30 +210,48 @@ export function resolveTableName(entityName, entity, featureName) {
210
210
  export function deriveEntityTableMeta(entityName, entity, options) {
211
211
  const tableName = resolveTableName(entityName, entity, options?.featureName);
212
212
  const source = options?.source ?? "managed";
213
- if (source === "unmanaged") {
214
- assertUnmanagedTableName(tableName, "deriveEntityTableMeta");
215
- }
216
213
  const idType = entity.idType ?? "uuid";
217
- if (source === "unmanaged" && idType === "serial") {
218
- // Unmanaged direct-write stores bypass validateRecordOwnedSubjects (it only walks
219
- // feature.entities), yet encryptForDirectWrite still keys recordOwned and
220
- // whenAbsent: "self" fields as `record:<entity>:<id>`; forgetSubject requires a UUID,
221
- // so the value could never be shredded.
222
- const recordOwnedField = Object.entries(entity.fields).find(([, field]) => isRecordKeyedField(field));
223
- if (recordOwnedField) {
224
- throw new Error(`deriveEntityTableMeta("${entityName}", { source: "unmanaged" }): field "${recordOwnedField[0]}" is recordOwned (or whenAbsent: "self") but the entity declares idType: "serial" — the record subject key is "record:${entityName}:<id>" and forgetSubject validates that id as a UUID, so this field could never be shredded. Use idType: "uuid", or give the field a different personal stance.`);
225
- }
214
+ if (source === "unmanaged")
215
+ assertUnmanagedRules(entityName, entity, tableName, idType);
216
+ const layout = collectEntityColumns(entity, idType);
217
+ const indexes = [
218
+ { name: `${tableName}_tenant_id_idx`, columns: ["tenant_id"] },
219
+ ...foreignKeyIndexes(tableName, entity, options?.relations, layout.fieldNameToSnake),
220
+ ...blindIndexIndexes(tableName, layout.bidxSnakeByFieldSnake),
221
+ ...declaredIndexes(tableName, entity, layout),
222
+ ];
223
+ const piiSubjectFields = collectPiiEncryptedFields(entity);
224
+ const meta = {
225
+ tableName,
226
+ columns: layout.columns,
227
+ indexes,
228
+ source,
229
+ ...(piiSubjectFields.length > 0 && { piiSubjectFields }),
230
+ ...(entity.tenancy === "global" && { tenancy: "global" }),
231
+ };
232
+ return source === "unmanaged" ? brandWritable(meta) : meta;
233
+ }
234
+ function assertUnmanagedRules(entityName, entity, tableName, idType) {
235
+ assertUnmanagedTableName(tableName, "deriveEntityTableMeta");
236
+ if (idType === "serial")
237
+ assertRecordSubjectsShreddable(entityName, entity);
238
+ }
239
+ // Unmanaged direct-write stores bypass validateRecordOwnedSubjects (it only walks
240
+ // feature.entities), yet encryptForDirectWrite still keys recordOwned and
241
+ // whenAbsent: "self" fields as `record:<entity>:<id>`; forgetSubject requires a UUID,
242
+ // so the value could never be shredded.
243
+ function assertRecordSubjectsShreddable(entityName, entity) {
244
+ const recordOwnedField = Object.entries(entity.fields).find(([, field]) => isRecordKeyedField(field));
245
+ if (recordOwnedField) {
246
+ throw new Error(`deriveEntityTableMeta("${entityName}", { source: "unmanaged" }): field "${recordOwnedField[0]}" is recordOwned (or whenAbsent: "self") but the entity declares idType: "serial" — the record subject key is "record:${entityName}:<id>" and forgetSubject validates that id as a UUID, so this field could never be shredded. Use idType: "uuid", or give the field a different personal stance.`);
226
247
  }
227
- // Base columns first, then user-fields. User-fields with the same
228
- // pg-name as a base-column OVERRIDE the base-column (last-wins, same
229
- // behavior as drizzle's `{ ...base, ...fields }` spread in the table
230
- // builder). Use case: user-session has `tenantId` as a field to enforce
231
- // access control, fileRef has `insertedAt` as a field for sortable/
232
- // filterable marking. The DB column stays the same, only the application
233
- // metadata on the field side changes.
234
- const baseCols = fullBaseColumns(idType, entity.softDelete === true);
248
+ }
249
+ // A field column with a base column's pg name overrides it in place (last wins,
250
+ // like `{ ...base, ...fields }`): user-session declares `tenantId` for access
251
+ // control, fileRef `insertedAt` for sort/filter flags; the DB column is unchanged.
252
+ function collectEntityColumns(entity, idType) {
235
253
  const colByName = new Map();
236
- for (const c of baseCols)
254
+ for (const c of fullBaseColumns(idType, entity.softDelete === true))
237
255
  colByName.set(c.name, c);
238
256
  const fieldNameToSnake = new Map();
239
257
  const bidxSnakeByFieldSnake = new Map();
@@ -242,8 +260,7 @@ export function deriveEntityTableMeta(entityName, entity, options) {
242
260
  for (const c of fieldCols)
243
261
  colByName.set(c.name, c);
244
262
  // Multi-column fields map to their primary column when its name IS the
245
- // field's snake (text+bidx, money+currency) — matches the toSnakeCase
246
- // fallback below, so explicit indexes keep resolving.
263
+ // field's snake (text+bidx, money+currency), so explicit indexes keep resolving.
247
264
  const primary = fieldCols[0];
248
265
  if (primary && primary.name === toSnakeCase(name))
249
266
  fieldNameToSnake.set(name, primary.name);
@@ -251,101 +268,77 @@ export function deriveEntityTableMeta(entityName, entity, options) {
251
268
  if (primary && bidxCol)
252
269
  bidxSnakeByFieldSnake.set(primary.name, bidxCol.name);
253
270
  }
254
- // Preserve base-col order, then any new user-col-names in fields-order.
255
- const columns = [];
256
- const seen = new Set();
257
- for (const c of baseCols) {
258
- const final = colByName.get(c.name);
259
- if (final && !seen.has(final.name)) {
260
- columns.push(final);
261
- seen.add(final.name);
262
- }
263
- }
264
- for (const c of colByName.values()) {
265
- if (!seen.has(c.name)) {
266
- columns.push(c);
267
- seen.add(c.name);
268
- }
269
- }
270
- const indexes = [{ name: `${tableName}_tenant_id_idx`, columns: ["tenant_id"] }];
271
- // FK-Indexes: file/image-Felder + belongsTo-Relations
271
+ // Map insertion order keeps base columns first (overrides stay in place), then new field columns.
272
+ return { columns: [...colByName.values()], fieldNameToSnake, bidxSnakeByFieldSnake };
273
+ }
274
+ function foreignKeyIndexes(tableName, entity, relations, fieldNameToSnake) {
272
275
  const fkSnakeNames = new Set();
273
276
  for (const [name, field] of Object.entries(entity.fields)) {
274
277
  if (field.type === "file" || field.type === "image")
275
278
  fkSnakeNames.add(toSnakeCase(name));
276
279
  }
277
- if (options?.relations) {
278
- for (const rel of Object.values(options.relations)) {
279
- if (rel.type === "belongsTo") {
280
- const snake = fieldNameToSnake.get(rel.foreignKey) ?? toSnakeCase(rel.foreignKey);
281
- fkSnakeNames.add(snake);
282
- }
283
- }
284
- }
285
- for (const snake of fkSnakeNames) {
286
- indexes.push({ name: `${tableName}_${snake}_idx`, columns: [snake] });
287
- }
288
- // lookupable fields: index on the bidx column — the query compiler's
289
- // OR rewrite hits it on every equality lookup.
290
- for (const bidxSnake of bidxSnakeByFieldSnake.values()) {
291
- indexes.push({ name: `${tableName}_${bidxSnake}_idx`, columns: [bidxSnake] });
292
- }
293
- // Explicitly declared indexes (EntityIndexDef). `def.where` is a
294
- // SqlExpression (`sql\`…\`` from @cosmicdrift/kumiko-framework/db) —
295
- // renderable via `.text`. Unknown where-shapes stay needsManualWhere.
296
- for (const def of entity.indexes ?? []) {
297
- const cols = def.columns.map((fieldName) => fieldNameToSnake.get(fieldName) ?? toSnakeCase(fieldName));
298
- const suffix = def.unique === true ? "unique" : "idx";
299
- const indexName = def.name ?? `${tableName}_${cols.join("_")}_${suffix}`;
300
- const explicitWhereSql = sqlExpressionText(def.where);
301
- // Soft-deleted rows keep their value, so a full unique index would
302
- // permanently block reuse; kept in lock-step with table-builder.ts.
303
- const whereSql = explicitWhereSql ??
304
- (def.where === undefined && def.unique === true && entity.softDelete === true
305
- ? SOFT_DELETE_LIVE_ROW_PREDICATE
306
- : undefined);
307
- indexes.push({
308
- name: indexName,
309
- columns: cols,
310
- ...(def.unique === true && { unique: true }),
311
- ...(whereSql !== undefined && { whereSql }),
312
- ...(def.where !== undefined && explicitWhereSql === undefined && { needsManualWhere: true }),
313
- });
314
- // Unique index over lookupable columns: partial bidx counterpart, so
315
- // uniqueness also holds for encrypted rows. The original stays
316
- // for plaintext legacy rows; partial (IS NOT NULL) because bidx is
317
- // NULL for erased/key-less rows.
318
- if (def.unique === true && def.where === undefined) {
319
- const bidxCols = cols.map((c) => bidxSnakeByFieldSnake.get(c) ?? c);
320
- if (bidxCols.some((c, i) => c !== cols[i])) {
321
- const notNullParts = bidxCols
322
- .filter((c, i) => c !== cols[i])
323
- .map((c) => `"${c}" IS NOT NULL`);
324
- // Soft-deleted rows keep their bidx hash — without this, a restored
325
- // row can't reuse a value a soft-deleted sibling still holds
326
- // Kept in lock-step with table-builder.ts.
327
- const whereParts = entity.softDelete === true
328
- ? [...notNullParts, SOFT_DELETE_LIVE_ROW_PREDICATE]
329
- : notNullParts;
330
- indexes.push({
331
- name: `${indexName}_bidx`,
332
- columns: bidxCols,
333
- unique: true,
334
- whereSql: whereParts.join(" AND "),
335
- });
336
- }
280
+ for (const rel of Object.values(relations ?? {})) {
281
+ if (rel.type === "belongsTo") {
282
+ fkSnakeNames.add(fieldNameToSnake.get(rel.foreignKey) ?? toSnakeCase(rel.foreignKey));
337
283
  }
338
284
  }
339
- const piiSubjectFields = collectPiiEncryptedFields(entity);
340
- const meta = {
341
- tableName,
342
- columns,
343
- indexes,
344
- source,
345
- ...(piiSubjectFields.length > 0 && { piiSubjectFields }),
346
- ...(entity.tenancy === "global" && { tenancy: "global" }),
285
+ return [...fkSnakeNames].map((snake) => ({
286
+ name: `${tableName}_${snake}_idx`,
287
+ columns: [snake],
288
+ }));
289
+ }
290
+ // The query compiler's OR rewrite hits the bidx column on every equality lookup.
291
+ function blindIndexIndexes(tableName, bidxSnakeByFieldSnake) {
292
+ return [...bidxSnakeByFieldSnake.values()].map((bidxSnake) => ({
293
+ name: `${tableName}_${bidxSnake}_idx`,
294
+ columns: [bidxSnake],
295
+ }));
296
+ }
297
+ function declaredIndexes(tableName, entity, layout) {
298
+ return (entity.indexes ?? []).flatMap((def) => declaredIndexWithBidxCounterpart(tableName, entity, def, layout));
299
+ }
300
+ // `def.where` is a SqlExpression renderable via `.text`; unknown where-shapes
301
+ // stay needsManualWhere.
302
+ function declaredIndexWithBidxCounterpart(tableName, entity, def, layout) {
303
+ const cols = def.columns.map((fieldName) => layout.fieldNameToSnake.get(fieldName) ?? toSnakeCase(fieldName));
304
+ const suffix = def.unique === true ? "unique" : "idx";
305
+ const indexName = def.name ?? `${tableName}_${cols.join("_")}_${suffix}`;
306
+ const explicitWhereSql = sqlExpressionText(def.where);
307
+ // Soft-deleted rows keep their value, so a full unique index would
308
+ // permanently block reuse; kept in lock-step with table-builder.ts.
309
+ const whereSql = explicitWhereSql ??
310
+ (def.where === undefined && def.unique === true && entity.softDelete === true
311
+ ? SOFT_DELETE_LIVE_ROW_PREDICATE
312
+ : undefined);
313
+ const declared = {
314
+ name: indexName,
315
+ columns: cols,
316
+ ...(def.unique === true && { unique: true }),
317
+ ...(whereSql !== undefined && { whereSql }),
318
+ ...(def.where !== undefined && explicitWhereSql === undefined && { needsManualWhere: true }),
319
+ };
320
+ if (def.unique !== true || def.where !== undefined)
321
+ return [declared];
322
+ const counterpart = bidxUniqueCounterpart(indexName, cols, entity.softDelete === true, layout.bidxSnakeByFieldSnake);
323
+ return counterpart ? [declared, counterpart] : [declared];
324
+ }
325
+ // Unique index over lookupable columns gets a partial bidx counterpart so
326
+ // uniqueness also holds for encrypted rows; IS NOT NULL because bidx is NULL
327
+ // for erased/key-less rows.
328
+ function bidxUniqueCounterpart(indexName, cols, softDelete, bidxSnakeByFieldSnake) {
329
+ const bidxCols = cols.map((c) => bidxSnakeByFieldSnake.get(c) ?? c);
330
+ const notNullParts = bidxCols.filter((c, i) => c !== cols[i]).map((c) => `"${c}" IS NOT NULL`);
331
+ if (notNullParts.length === 0)
332
+ return undefined;
333
+ // Soft-deleted rows keep their bidx hash, so without the live-row predicate a
334
+ // restored row could not reuse a value a soft-deleted sibling still holds.
335
+ const whereParts = softDelete ? [...notNullParts, SOFT_DELETE_LIVE_ROW_PREDICATE] : notNullParts;
336
+ return {
337
+ name: `${indexName}_bidx`,
338
+ columns: bidxCols,
339
+ unique: true,
340
+ whereSql: whereParts.join(" AND "),
347
341
  };
348
- return source === "unmanaged" ? brandWritable(meta) : meta;
349
342
  }
350
343
  function sqlExpressionText(where) {
351
344
  return isSqlExpression(where) ? where.text : undefined;
@@ -1,3 +1,4 @@
1
+ import type { ErasedSubjectHandling } from "../crypto/pii-field-encryption.ts";
1
2
  import type { WhereObject } from "../db/query.ts";
2
3
  import type { EntityDefinition, EntityId, SessionUser, TenantId } from "../engine/types/index.ts";
3
4
  import { type WriteFailure } from "../errors/index.ts";
@@ -62,7 +63,7 @@ export type ExecutorContext = {
62
63
  subjectSource?: Record<string, unknown>;
63
64
  keyTenantId?: TenantId | undefined;
64
65
  ownerFallback?: Record<string, unknown>;
65
- onErasedSubject?: "sentinel";
66
+ onErasedSubject?: ErasedSubjectHandling;
66
67
  }) => Promise<Record<string, unknown>>;
67
68
  readonly decryptForRead: (row: Record<string, unknown>, opts?: DecryptForReadOptions) => Promise<Record<string, unknown>>;
68
69
  readonly ownerTransitionRekeys: (previous: Record<string, unknown>, mergedNew: Record<string, unknown>, submittedChanges: Record<string, unknown>) => Record<string, unknown>;
@@ -0,0 +1,17 @@
1
+ import type { FieldDefinition } from "@cosmicdrift/kumiko-types/fields";
2
+ import type { ListAggregateCurrencyAmount, ListAggregateMeasureKind } from "@cosmicdrift/kumiko-types/list-aggregate-types";
3
+ import type { OwnershipClause } from "../engine/ownership.ts";
4
+ import type { ExecutorContext } from "./event-store-executor-context.ts";
5
+ import { type ListWhere } from "./event-store-executor-read-filter.ts";
6
+ import type { TenantDb } from "./tenant-db.ts";
7
+ export type CurrencyMeasureCandidate = {
8
+ readonly kind: ListAggregateMeasureKind;
9
+ readonly field: string;
10
+ readonly def: FieldDefinition;
11
+ readonly clause: OwnershipClause;
12
+ };
13
+ export type MeasureCurrencies = ReadonlyMap<string, readonly string[]>;
14
+ export declare function currencyColumnOf(field: string): string;
15
+ export declare function loadMeasureCurrencies(ctx: ExecutorContext, measures: readonly CurrencyMeasureCandidate[], where: ListWhere, db: TenantDb): Promise<MeasureCurrencies>;
16
+ export declare function currencyMeasureAlias(measureIndex: number, currencyIndex: number): string;
17
+ export declare function decodeCurrencyAmounts(row: Readonly<Record<string, unknown>>, measureIndex: number, currencies: readonly string[]): readonly ListAggregateCurrencyAmount[];
@@ -0,0 +1,40 @@
1
+ import { isPerCurrencyMeasure } from "./aggregate-sql.js";
2
+ import { bindShiftedClause } from "./event-store-executor-read-filter.js";
3
+ import { executeRawQueryRead } from "./queries/raw-sql.js";
4
+ import { schemaTableName } from "./table-introspection.js";
5
+ import { tenantDbRunner } from "./tenant-db-runner.js";
6
+ // Above the ~180 ISO 4217 codes, so the cap only bounds malformed data.
7
+ const MAX_AGGREGATE_CURRENCIES = 200;
8
+ export function currencyColumnOf(field) {
9
+ return `${field}Currency`;
10
+ }
11
+ // Runs before the measure query mutates `where`, on a copy of its params, and
12
+ // under the money field's own read clause: an unreadable amount must not reveal
13
+ // which currencies exist.
14
+ export async function loadMeasureCurrencies(ctx, measures, where, db) {
15
+ const byField = new Map();
16
+ const tableName = String(schemaTableName(ctx.table));
17
+ for (const m of measures) {
18
+ if (!isPerCurrencyMeasure(m.kind, m.def.type) || byField.has(m.field))
19
+ continue;
20
+ if (!Object.hasOwn(ctx.table, currencyColumnOf(m.field)))
21
+ continue;
22
+ const params = [...where.params];
23
+ const conditions = [...where.whereSql, `${where.colSql(m.field)} IS NOT NULL`];
24
+ if (m.clause.kind === "sql")
25
+ conditions.push(`(${bindShiftedClause(params, m.clause)})`);
26
+ const currencySql = where.colSql(currencyColumnOf(m.field));
27
+ const rows = await executeRawQueryRead(tenantDbRunner(db), `SELECT DISTINCT ${currencySql} AS cur FROM "${tableName}" WHERE ${conditions.join(" AND ")} AND ${currencySql} IS NOT NULL ORDER BY 1 LIMIT ${MAX_AGGREGATE_CURRENCIES}`, params);
28
+ byField.set(m.field, rows.map((row) => String(row.cur)));
29
+ }
30
+ return byField;
31
+ }
32
+ export function currencyMeasureAlias(measureIndex, currencyIndex) {
33
+ return `m${measureIndex}_${currencyIndex}`;
34
+ }
35
+ export function decodeCurrencyAmounts(row, measureIndex, currencies) {
36
+ return currencies.flatMap((currency, j) => {
37
+ const raw = row[currencyMeasureAlias(measureIndex, j)];
38
+ return raw === null || raw === undefined ? [] : [{ currency, value: Number(raw) }];
39
+ });
40
+ }
@@ -0,0 +1,8 @@
1
+ import type { ListAggregatePayload, ListAggregateResult } from "@cosmicdrift/kumiko-types/list-aggregate-types";
2
+ import type { SessionUser } from "../engine/types/index.ts";
3
+ import type { ExecutorContext } from "./event-store-executor-context.ts";
4
+ import { type ListRuntimeOptions, type ListWherePayload } from "./event-store-executor-read-where.ts";
5
+ import type { TenantDb } from "./tenant-db.ts";
6
+ export declare function aggregateList(ctx: ExecutorContext, payload: ListWherePayload & {
7
+ readonly aggregate: ListAggregatePayload;
8
+ }, user: SessionUser, db: TenantDb, runtimeOptions: ListRuntimeOptions | undefined): Promise<ListAggregateResult>;
@@ -0,0 +1,143 @@
1
+ import { ValidationError } from "../errors/index.js";
2
+ import { aggregateKeyOf, decodeListAggregateValue, emptyListAggregateValue, groupedKeyOrderLimitSql, isListAggregateGroupable, isListAggregateMeasureSupported, isPerCurrencyMeasure, listAggregateKeySql, listAggregateMeasureSql, listCurrencyMeasureSql, } from "./aggregate-sql.js";
3
+ import { holdsCiphertextOrSecret } from "./entity-field-encryption.js";
4
+ import { currencyColumnOf, currencyMeasureAlias, decodeCurrencyAmounts, loadMeasureCurrencies, } from "./event-store-executor-read-aggregate-currencies.js";
5
+ import { appendShiftedClause, bindShiftedClause, } from "./event-store-executor-read-filter.js";
6
+ import { buildListWhere, createFieldReadClause, } from "./event-store-executor-read-where.js";
7
+ import { executeRawQueryRead } from "./queries/raw-sql.js";
8
+ import { schemaTableName } from "./table-introspection.js";
9
+ import { tenantDbRunner } from "./tenant-db-runner.js";
10
+ // One group more than the cap is fetched to tell "exactly 200" from "more".
11
+ const MAX_AGGREGATE_GROUPS = 200;
12
+ function rejectAggregate(path, field, hint) {
13
+ throw new ValidationError({
14
+ fields: [
15
+ {
16
+ path,
17
+ code: "field_not_aggregatable",
18
+ i18nKey: "errors.validation.invalid_value",
19
+ params: { field, hint },
20
+ },
21
+ ],
22
+ });
23
+ }
24
+ // Unknown and unreadable fields share one silent outcome so the response cannot
25
+ // confirm that a field exists; only then are the sensitive-field and type gates applied.
26
+ function gateField(ctx, fieldReadClause, field, path) {
27
+ const def = ctx.entity.fields[field];
28
+ if (def === undefined ||
29
+ !Object.hasOwn(ctx.entity.fields, field) ||
30
+ !Object.hasOwn(ctx.table, field))
31
+ return { kind: "silent" };
32
+ const clause = fieldReadClause(field);
33
+ if (clause.kind === "empty")
34
+ return { kind: "silent" };
35
+ if (holdsCiphertextOrSecret(ctx.entity, field)) {
36
+ rejectAggregate(path, field, "Encrypted, personal and secret fields cannot be aggregated.");
37
+ }
38
+ return { kind: "ok", def, clause };
39
+ }
40
+ function planAggregate(ctx, fieldReadClause, aggregate) {
41
+ const groupBy = aggregate.groupBy === undefined
42
+ ? undefined
43
+ : {
44
+ field: aggregate.groupBy,
45
+ gate: gateField(ctx, fieldReadClause, aggregate.groupBy, "aggregate.groupBy"),
46
+ };
47
+ if (groupBy?.gate.kind === "ok") {
48
+ const { def } = groupBy.gate;
49
+ const multiple = def.type === "reference" && def.multiple === true;
50
+ if (!isListAggregateGroupable(def.type, multiple)) {
51
+ rejectAggregate("aggregate.groupBy", groupBy.field, "This field type cannot be grouped by.");
52
+ }
53
+ }
54
+ const measures = aggregate.measures.map((m, i) => {
55
+ const path = `aggregate.measures.${i}`;
56
+ const gate = gateField(ctx, fieldReadClause, m.field, path);
57
+ if (gate.kind === "ok" && !isListAggregateMeasureSupported(m.kind, gate.def.type)) {
58
+ rejectAggregate(path, m.field, `"${m.kind}" is not supported on a ${gate.def.type} field.`);
59
+ }
60
+ return { kind: m.kind, field: m.field, gate };
61
+ });
62
+ return { groupBy, measures };
63
+ }
64
+ function emptyMeasureValues(plan) {
65
+ return plan.measures.map((m) => {
66
+ if (m.gate.kind === "silent")
67
+ return null;
68
+ return isPerCurrencyMeasure(m.kind, m.gate.def.type) ? [] : emptyListAggregateValue(m.kind);
69
+ });
70
+ }
71
+ function currencyCandidates(plan) {
72
+ return plan.measures.flatMap((m) => m.gate.kind === "ok"
73
+ ? [{ kind: m.kind, field: m.field, def: m.gate.def, clause: m.gate.clause }]
74
+ : []);
75
+ }
76
+ function currencyMeasureSelects(measureIndex, kind, field, readFilterSql, currencies, where) {
77
+ return currencies.map((currency, j) => {
78
+ where.params.push(currency);
79
+ const currencyMatch = `${where.colSql(currencyColumnOf(field))} = $${where.params.length}`;
80
+ const filterSql = readFilterSql === undefined ? currencyMatch : `${currencyMatch} AND (${readFilterSql})`;
81
+ return `${listCurrencyMeasureSql(kind, where.colSql(field), filterSql)} AS ${currencyMeasureAlias(measureIndex, j)}`;
82
+ });
83
+ }
84
+ function renderSelectList(plan, where, currencies) {
85
+ const parts = [];
86
+ if (plan.groupBy?.gate.kind === "ok") {
87
+ const { def, clause } = plan.groupBy.gate;
88
+ parts.push(listAggregateKeySql(def.type, where.colSql(plan.groupBy.field), "k0"));
89
+ // WHERE, not CASE: rows the viewer may not read must not collapse into the real NULL group.
90
+ if (clause.kind === "sql")
91
+ appendShiftedClause(where, clause);
92
+ }
93
+ parts.push("COUNT(*)::float8 AS c");
94
+ for (const [i, m] of plan.measures.entries()) {
95
+ if (m.gate.kind === "silent")
96
+ continue; // skip: an unreadable measure stays out of the SELECT and decodes to null
97
+ const filterSql = m.gate.clause.kind === "sql" ? bindShiftedClause(where.params, m.gate.clause) : undefined;
98
+ if (isPerCurrencyMeasure(m.kind, m.gate.def.type)) {
99
+ const fieldCurrencies = currencies.get(m.field) ?? [];
100
+ parts.push(...currencyMeasureSelects(i, m.kind, m.field, filterSql, fieldCurrencies, where));
101
+ continue;
102
+ }
103
+ parts.push(`${listAggregateMeasureSql(m.kind, m.gate.def.type, where.colSql(m.field), filterSql)} AS m${i}`);
104
+ }
105
+ return parts.join(", ");
106
+ }
107
+ export async function aggregateList(ctx, payload, user, db, runtimeOptions) {
108
+ const plan = planAggregate(ctx, createFieldReadClause(ctx, user), payload.aggregate);
109
+ const grouped = plan.groupBy !== undefined;
110
+ if (plan.groupBy?.gate.kind === "silent")
111
+ return { groups: [] };
112
+ const built = await buildListWhere(ctx, payload, user, db, runtimeOptions);
113
+ if (built.kind === "empty") {
114
+ return {
115
+ groups: grouped ? [] : [{ key: null, count: 0, measures: emptyMeasureValues(plan) }],
116
+ };
117
+ }
118
+ const { where, searchTruncated } = built;
119
+ const currencies = await loadMeasureCurrencies(ctx, currencyCandidates(plan), where, db);
120
+ const selectList = renderSelectList(plan, where, currencies);
121
+ const whereText = where.whereSql.length > 0 ? ` WHERE ${where.whereSql.join(" AND ")}` : "";
122
+ const groupText = grouped ? groupedKeyOrderLimitSql(MAX_AGGREGATE_GROUPS + 1) : "";
123
+ const tableName = String(schemaTableName(ctx.table));
124
+ const rows = await executeRawQueryRead(tenantDbRunner(db), `SELECT ${selectList} FROM "${tableName}"${whereText}${groupText}`, where.params);
125
+ const truncated = rows.length > MAX_AGGREGATE_GROUPS;
126
+ const groups = rows.slice(0, MAX_AGGREGATE_GROUPS).map((row) => ({
127
+ key: grouped ? aggregateKeyOf(row["k0"]) : null,
128
+ count: Number(row["c"]),
129
+ measures: plan.measures.map((m, i) => {
130
+ if (m.gate.kind === "silent")
131
+ return null;
132
+ if (isPerCurrencyMeasure(m.kind, m.gate.def.type)) {
133
+ return decodeCurrencyAmounts(row, i, currencies.get(m.field) ?? []);
134
+ }
135
+ return decodeListAggregateValue(row[`m${i}`]);
136
+ }),
137
+ }));
138
+ return {
139
+ groups,
140
+ ...(truncated && { truncated: true }),
141
+ ...(searchTruncated && { searchTruncated: true }),
142
+ };
143
+ }
@@ -0,0 +1,31 @@
1
+ import { type OwnershipClause } from "../engine/ownership.ts";
2
+ import type { EntityDefinition } from "../engine/types/index.ts";
3
+ import { type Table } from "./event-store-executor-context.ts";
4
+ export type ListWhere = {
5
+ readonly whereSql: string[];
6
+ readonly params: unknown[];
7
+ readonly colSql: (field: string) => string;
8
+ };
9
+ export declare function bindShiftedClause(params: unknown[], clause: {
10
+ readonly sqlText: string;
11
+ readonly params: readonly unknown[];
12
+ }): string;
13
+ export declare function appendShiftedClause(where: ListWhere, clause: {
14
+ readonly sqlText: string;
15
+ readonly params: readonly unknown[];
16
+ }): void;
17
+ export declare function bindPlaceholders(params: unknown[], values: readonly unknown[]): string;
18
+ export type ListFilter = {
19
+ readonly field: string;
20
+ readonly op: "eq" | "ne" | "lt" | "gt" | "lte" | "gte" | "in";
21
+ readonly value: unknown;
22
+ };
23
+ export declare function deriveParentNarrowTypes(entity: EntityDefinition, payload: {
24
+ readonly filter?: ListFilter;
25
+ readonly filters?: readonly ListFilter[];
26
+ }): ReadonlySet<string> | undefined;
27
+ export declare function collectListFilters(payload: {
28
+ readonly filter?: ListFilter | undefined;
29
+ readonly filters?: readonly ListFilter[] | undefined;
30
+ }): readonly ListFilter[];
31
+ export declare function applyListFilter(where: ListWhere, f: ListFilter, table: Table, entity: EntityDefinition, fieldReadClause: (field: string) => OwnershipClause): void;