@cosmicdrift/kumiko-framework 0.386.0 → 0.388.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 (468) hide show
  1. package/AGENTS.md +6 -1
  2. package/LICENSE +21 -57
  3. package/README.md +2 -2
  4. package/agents/examples/anonymous-access.md +1 -1
  5. package/agents/examples/apex-landing.md +204 -0
  6. package/agents/examples/api-worker-split.md +64 -0
  7. package/agents/examples/app-config.md +14 -0
  8. package/agents/examples/auth-mfa.md +115 -0
  9. package/agents/examples/env-composition.md +28 -1
  10. package/agents/examples/legal-pages.md +1 -1
  11. package/agents/examples/prod-boot-options.md +16 -0
  12. package/agents/examples/rate-limiting.md +48 -0
  13. package/agents/examples/record-detail-layout.md +692 -0
  14. package/agents/examples/seed-migration.md +212 -0
  15. package/agents/examples/wizard-form.md +941 -0
  16. package/agents/examples/writeform-section.md +548 -0
  17. package/agents/features.md +19 -2
  18. package/agents/widgets.md +40 -39
  19. package/dist/api/api-constants.js +2 -2
  20. package/dist/api/auth-middleware.js +7 -7
  21. package/dist/api/auth-rate-limiters.js +7 -3
  22. package/dist/api/auth-routes-mfa.js +1 -1
  23. package/dist/api/auth-routes-session.js +1 -1
  24. package/dist/api/auth-routes-shared.js +7 -3
  25. package/dist/api/auth-routes-signup.js +1 -1
  26. package/dist/api/client-error-route.js +8 -2
  27. package/dist/api/client-ip.js +3 -1
  28. package/dist/api/extra-route.js +1 -1
  29. package/dist/api/index.d.ts +3 -1
  30. package/dist/api/index.js +3 -1
  31. package/dist/api/pii-leak-guard.js +4 -2
  32. package/dist/api/readiness.js +5 -5
  33. package/dist/api/redis-sse-broker.js +8 -6
  34. package/dist/api/request-context.d.ts +1 -1
  35. package/dist/api/request-context.js +1 -1
  36. package/dist/api/routes.js +2 -2
  37. package/dist/api/server-boot-checks.d.ts +10 -0
  38. package/dist/api/server-boot-checks.js +157 -0
  39. package/dist/api/server-event-dispatcher.d.ts +26 -0
  40. package/dist/api/server-event-dispatcher.js +208 -0
  41. package/dist/api/server-extra-routes.d.ts +11 -0
  42. package/dist/api/server-extra-routes.js +203 -0
  43. package/dist/api/server-middleware.d.ts +20 -0
  44. package/dist/api/server-middleware.js +211 -0
  45. package/dist/api/server-routes.d.ts +19 -0
  46. package/dist/api/server-routes.js +293 -0
  47. package/dist/api/server-websocket-routes.d.ts +4 -0
  48. package/dist/api/server-websocket-routes.js +108 -0
  49. package/dist/api/server-wiring.d.ts +32 -0
  50. package/dist/api/server-wiring.js +236 -0
  51. package/dist/api/server.d.ts +10 -15
  52. package/dist/api/server.js +48 -1250
  53. package/dist/api/session-cookies.js +4 -4
  54. package/dist/api/sse-broker.js +1 -1
  55. package/dist/api/sse-route.d.ts +13 -13
  56. package/dist/api/sse-route.js +31 -27
  57. package/dist/auth/index.js +4 -4
  58. package/dist/auth/roles.d.ts +4 -4
  59. package/dist/auth/roles.js +24 -24
  60. package/dist/compliance/duration-spec.js +18 -20
  61. package/dist/compliance/index.js +2 -3
  62. package/dist/compliance/override-schema.d.ts +4 -4
  63. package/dist/compliance/override-schema.js +19 -22
  64. package/dist/compliance/profiles.d.ts +26 -29
  65. package/dist/compliance/profiles.js +62 -69
  66. package/dist/compliance/sub-processors.d.ts +28 -30
  67. package/dist/compliance/sub-processors.js +17 -21
  68. package/dist/consumer-cli.js +4 -4
  69. package/dist/crypto/blind-index.js +18 -17
  70. package/dist/crypto/ciphertext-pattern.js +1 -1
  71. package/dist/crypto/event-pii.js +3 -3
  72. package/dist/crypto/kek-source.d.ts +1 -1
  73. package/dist/crypto/kek-source.js +3 -2
  74. package/dist/crypto/kms-adapter.d.ts +1 -1
  75. package/dist/crypto/kms-adapter.js +4 -6
  76. package/dist/crypto/kms-wiring.d.ts +1 -2
  77. package/dist/crypto/kms-wiring.js +2 -2
  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.js +2 -6
  81. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  82. package/dist/crypto/pii-field-encryption.js +7 -5
  83. package/dist/crypto/subject-resolver.d.ts +1 -1
  84. package/dist/crypto/subject-resolver.js +2 -2
  85. package/dist/db/aggregate-sql.d.ts +21 -0
  86. package/dist/db/aggregate-sql.js +155 -0
  87. package/dist/db/apply-entity-event.d.ts +6 -6
  88. package/dist/db/apply-entity-event.js +55 -55
  89. package/dist/db/blind-index-cleanup.js +7 -7
  90. package/dist/db/closed-connection.d.ts +9 -0
  91. package/dist/db/closed-connection.js +95 -0
  92. package/dist/db/collect-table-metas.js +10 -11
  93. package/dist/db/compound-types.d.ts +1 -1
  94. package/dist/db/compound-types.js +12 -12
  95. package/dist/db/cursor.js +3 -4
  96. package/dist/db/dialect.d.ts +2 -2
  97. package/dist/db/dialect.js +3 -3
  98. package/dist/db/distinct-on.d.ts +1 -1
  99. package/dist/db/driver-row-coercion.d.ts +2 -0
  100. package/dist/db/driver-row-coercion.js +186 -0
  101. package/dist/db/driver-value-preparation.d.ts +16 -0
  102. package/dist/db/driver-value-preparation.js +99 -0
  103. package/dist/db/eagerload.d.ts +3 -3
  104. package/dist/db/eagerload.js +21 -12
  105. package/dist/db/entity-field-encryption.d.ts +1 -0
  106. package/dist/db/entity-field-encryption.js +1 -1
  107. package/dist/db/entity-table-meta.d.ts +2 -2
  108. package/dist/db/entity-table-meta.js +105 -112
  109. package/dist/db/event-store-executor-context.d.ts +3 -2
  110. package/dist/db/event-store-executor-context.js +13 -13
  111. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  112. package/dist/db/event-store-executor-read-aggregate.js +112 -0
  113. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  114. package/dist/db/event-store-executor-read-filter.js +203 -0
  115. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  116. package/dist/db/event-store-executor-read-paging.js +99 -0
  117. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  118. package/dist/db/event-store-executor-read-rows.js +32 -0
  119. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  120. package/dist/db/event-store-executor-read-search.js +221 -0
  121. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  122. package/dist/db/event-store-executor-read-sort.js +130 -0
  123. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  124. package/dist/db/event-store-executor-read-where.js +81 -0
  125. package/dist/db/event-store-executor-read.d.ts +2 -9
  126. package/dist/db/event-store-executor-read.js +50 -704
  127. package/dist/db/event-store-executor-write-steps.d.ts +20 -0
  128. package/dist/db/event-store-executor-write-steps.js +100 -0
  129. package/dist/db/event-store-executor-write.js +401 -510
  130. package/dist/db/index.d.ts +6 -2
  131. package/dist/db/index.js +5 -1
  132. package/dist/db/list-aggregate-labels.d.ts +14 -0
  133. package/dist/db/list-aggregate-labels.js +26 -0
  134. package/dist/db/located-timestamp.d.ts +11 -11
  135. package/dist/db/located-timestamp.js +29 -30
  136. package/dist/db/migrate-generator.d.ts +1 -1
  137. package/dist/db/migrate-generator.js +97 -105
  138. package/dist/db/migrate-runner.js +29 -26
  139. package/dist/db/money.d.ts +6 -6
  140. package/dist/db/money.js +29 -27
  141. package/dist/db/queries/backfill-pii.js +5 -5
  142. package/dist/db/queries/ddl.js +1 -1
  143. package/dist/db/queries/es-ops.d.ts +1 -1
  144. package/dist/db/queries/es-ops.js +1 -1
  145. package/dist/db/queries/event-consumer.js +3 -3
  146. package/dist/db/queries/event-store.js +4 -2
  147. package/dist/db/queries/projection-rebuild.js +3 -3
  148. package/dist/db/queries/raw-sql.d.ts +2 -2
  149. package/dist/db/queries/raw-sql.js +2 -2
  150. package/dist/db/queries/shadow-swap.js +19 -19
  151. package/dist/db/query.d.ts +3 -16
  152. package/dist/db/query.js +8 -761
  153. package/dist/db/rebuild-marker.js +20 -21
  154. package/dist/db/reference-data.js +6 -1
  155. package/dist/db/replay-migration-sql.js +2 -2
  156. package/dist/db/schema-inspection.js +4 -5
  157. package/dist/db/table-builder.d.ts +1 -2
  158. package/dist/db/table-builder.js +137 -190
  159. package/dist/db/table-info.d.ts +10 -0
  160. package/dist/db/table-info.js +45 -0
  161. package/dist/db/temporal-value-guards.d.ts +6 -0
  162. package/dist/db/temporal-value-guards.js +15 -0
  163. package/dist/db/tenant-db-runner.d.ts +1 -0
  164. package/dist/db/tenant-db-runner.js +3 -0
  165. package/dist/db/tenant-db.d.ts +2 -2
  166. package/dist/db/tenant-db.js +48 -11
  167. package/dist/db/where-clause.d.ts +6 -0
  168. package/dist/db/where-clause.js +236 -0
  169. package/dist/derivatives/delete-stored-file.js +6 -2
  170. package/dist/derivatives/derivatives-context.js +1 -1
  171. package/dist/engine/boot-validator/access-roles.js +4 -3
  172. package/dist/engine/boot-validator/api-ext.js +14 -14
  173. package/dist/engine/boot-validator/boot-check.js +1 -1
  174. package/dist/engine/boot-validator/config-deps.js +7 -6
  175. package/dist/engine/boot-validator/detail-screens.js +4 -4
  176. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  177. package/dist/engine/boot-validator/entity-handler.js +77 -61
  178. package/dist/engine/boot-validator/form-derivations.js +35 -1
  179. package/dist/engine/boot-validator/gdpr-storage.js +4 -4
  180. package/dist/engine/boot-validator/i18n-keys.js +6 -4
  181. package/dist/engine/boot-validator/index.d.ts +2 -2
  182. package/dist/engine/boot-validator/index.js +22 -20
  183. package/dist/engine/boot-validator/nav.js +5 -5
  184. package/dist/engine/boot-validator/ownership.js +12 -9
  185. package/dist/engine/boot-validator/parent-ref.js +1 -1
  186. package/dist/engine/boot-validator/pii-retention.js +258 -187
  187. package/dist/engine/boot-validator/projection-list-screens.js +7 -7
  188. package/dist/engine/boot-validator/query-refs.js +1 -1
  189. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  190. package/dist/engine/boot-validator/screens-actions.js +23 -17
  191. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  192. package/dist/engine/boot-validator/screens-forms.js +12 -17
  193. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  194. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  195. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  196. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  197. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  198. package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
  199. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  200. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  201. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  202. package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
  203. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  204. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  205. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  206. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  207. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  208. package/dist/engine/boot-validator/screens-lists.js +26 -24
  209. package/dist/engine/boot-validator/screens.d.ts +0 -2
  210. package/dist/engine/boot-validator/screens.js +102 -869
  211. package/dist/engine/boot-validator/security-baseline.js +3 -2
  212. package/dist/engine/boot-validator/transfer-graph.js +2 -2
  213. package/dist/engine/boot-validator/workspaces.js +3 -3
  214. package/dist/engine/bridge-stub.js +3 -4
  215. package/dist/engine/build-app-schema.js +130 -170
  216. package/dist/engine/build-config-feature-schema.js +50 -50
  217. package/dist/engine/build-target.js +20 -23
  218. package/dist/engine/codemod/pipeline-codemod.js +20 -13
  219. package/dist/engine/config-helpers.js +4 -4
  220. package/dist/engine/constants.js +2 -2
  221. package/dist/engine/create-app.js +2 -2
  222. package/dist/engine/define-feature.js +21 -13
  223. package/dist/engine/define-handler.js +13 -11
  224. package/dist/engine/define-step.js +1 -1
  225. package/dist/engine/define-workflow.d.ts +5 -5
  226. package/dist/engine/define-workflow.js +6 -6
  227. package/dist/engine/entity-handlers.d.ts +13 -0
  228. package/dist/engine/entity-handlers.js +22 -34
  229. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  230. package/dist/engine/entity-list-runtime-options.js +18 -0
  231. package/dist/engine/event-def.js +2 -2
  232. package/dist/engine/extension-names.d.ts +16 -17
  233. package/dist/engine/extension-names.js +37 -38
  234. package/dist/engine/extensions/escape-hatch-usage.js +1 -1
  235. package/dist/engine/extensions/tenant-data.js +2 -2
  236. package/dist/engine/extensions/user-data.d.ts +23 -24
  237. package/dist/engine/factories.d.ts +14 -13
  238. package/dist/engine/factories.js +38 -37
  239. package/dist/engine/feature-ast/entity-field-types.js +1 -1
  240. package/dist/engine/feature-ast/extractors/handlers.js +2 -1
  241. package/dist/engine/feature-ast/extractors/hooks.js +1 -1
  242. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  243. package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
  244. package/dist/engine/feature-ast/extractors/shared.js +2 -3
  245. package/dist/engine/feature-ast/parse.js +13 -13
  246. package/dist/engine/feature-ast/patch.js +4 -182
  247. package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
  248. package/dist/engine/feature-ast/pattern-change-schema.js +2 -1
  249. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  250. package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
  251. package/dist/engine/feature-ast/patterns.d.ts +11 -2
  252. package/dist/engine/feature-ast/patterns.js +43 -51
  253. package/dist/engine/feature-ast/render.js +50 -89
  254. package/dist/engine/feature-config-events-jobs.js +15 -12
  255. package/dist/engine/feature-entity-handlers.js +12 -5
  256. package/dist/engine/feature-manifest.d.ts +9 -9
  257. package/dist/engine/feature-manifest.js +6 -7
  258. package/dist/engine/feature-ui-extensions.d.ts +3 -3
  259. package/dist/engine/feature-ui-extensions.js +30 -22
  260. package/dist/engine/field-access.js +5 -6
  261. package/dist/engine/form-derivations.js +18 -7
  262. package/dist/engine/handler-field-references.js +1 -1
  263. package/dist/engine/index.d.ts +1 -0
  264. package/dist/engine/index.js +4 -4
  265. package/dist/engine/object-form.d.ts +1 -0
  266. package/dist/engine/object-form.js +14 -4
  267. package/dist/engine/ownership.js +3 -3
  268. package/dist/engine/parse-ref-target.js +5 -7
  269. package/dist/engine/pattern-library/index.js +2 -2
  270. package/dist/engine/pattern-library/library.js +2 -2
  271. package/dist/engine/pattern-library/types.js +3 -3
  272. package/dist/engine/pipeline.js +2 -2
  273. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  274. package/dist/engine/project-app-schema-for-roles.js +7 -5
  275. package/dist/engine/projection-helpers.js +9 -9
  276. package/dist/engine/qualified-name.js +2 -2
  277. package/dist/engine/registry-facade.js +1 -1
  278. package/dist/engine/registry-ingest.js +13 -13
  279. package/dist/engine/registry-state.js +17 -19
  280. package/dist/engine/registry-validate.js +12 -12
  281. package/dist/engine/run-in.js +2 -2
  282. package/dist/engine/run-pipeline.js +7 -0
  283. package/dist/engine/schema-builder.js +251 -233
  284. package/dist/engine/screen-filter-ops.js +17 -20
  285. package/dist/engine/screen-helpers.js +4 -3
  286. package/dist/engine/screen-kind.d.ts +7 -0
  287. package/dist/engine/screen-kind.js +1 -0
  288. package/dist/engine/state-machine.d.ts +12 -12
  289. package/dist/engine/state-machine.js +7 -7
  290. package/dist/engine/steps/_resolver-utils.js +4 -3
  291. package/dist/engine/steps/for-each.js +4 -4
  292. package/dist/engine/steps/unsafe-projection-delete.js +1 -1
  293. package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
  294. package/dist/engine/steps/wait-for-event.js +4 -4
  295. package/dist/engine/steps/workflow-system-events.js +3 -3
  296. package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
  297. package/dist/engine/tier-resolver-extension.d.ts +29 -27
  298. package/dist/engine/tier-resolver-extension.js +26 -26
  299. package/dist/engine/transfer-adjacency.js +2 -2
  300. package/dist/engine/validate-projection-allowlist.js +9 -28
  301. package/dist/engine/walk-step-instances.d.ts +2 -0
  302. package/dist/engine/walk-step-instances.js +38 -0
  303. package/dist/engine/where-rule-lint.js +1 -1
  304. package/dist/engine/write-handler-qn-extract.js +1 -1
  305. package/dist/entrypoint/index.d.ts +4 -1
  306. package/dist/entrypoint/index.js +10 -4
  307. package/dist/env/index.d.ts +7 -6
  308. package/dist/env/index.js +10 -9
  309. package/dist/errors/docs-url.d.ts +1 -1
  310. package/dist/errors/docs-url.js +22 -2
  311. package/dist/errors/transition-details.js +7 -7
  312. package/dist/errors/write-error-info.d.ts +6 -6
  313. package/dist/errors/write-error-info.js +6 -6
  314. package/dist/es-ops/context.d.ts +9 -9
  315. package/dist/es-ops/context.js +29 -29
  316. package/dist/es-ops/index.js +7 -9
  317. package/dist/es-ops/operations-schema.js +4 -10
  318. package/dist/es-ops/runner.d.ts +11 -11
  319. package/dist/es-ops/runner.js +49 -53
  320. package/dist/es-ops/types.d.ts +55 -55
  321. package/dist/es-ops/types.js +11 -11
  322. package/dist/event-store/admin-api.js +7 -8
  323. package/dist/event-store/errors.js +1 -1
  324. package/dist/event-store/event-store.js +12 -6
  325. package/dist/event-store/rebuild-dead-letter.js +1 -1
  326. package/dist/files/bind-pending-file-refs.js +3 -3
  327. package/dist/files/create-file-ref-from-bytes.js +4 -1
  328. package/dist/files/feature.js +9 -10
  329. package/dist/files/file-handle.js +7 -9
  330. package/dist/files/file-ref-entity.js +15 -15
  331. package/dist/files/file-ref-table.js +7 -8
  332. package/dist/files/file-routes.d.ts +7 -0
  333. package/dist/files/file-routes.js +22 -13
  334. package/dist/files/in-memory-provider.js +8 -9
  335. package/dist/files/local-provider.js +18 -25
  336. package/dist/files/provider-resolver.d.ts +7 -7
  337. package/dist/files/resolve-content-type.js +43 -0
  338. package/dist/files/storage-tracking.js +2 -2
  339. package/dist/files/upload-ledger.js +7 -0
  340. package/dist/files/zip-stream.d.ts +12 -19
  341. package/dist/files/zip-stream.js +44 -63
  342. package/dist/i18n/required-surface-keys.d.ts +1 -1
  343. package/dist/i18n/required-surface-keys.js +188 -212
  344. package/dist/internal/stack-support.d.ts +2 -1
  345. package/dist/internal/stack-support.js +1 -1
  346. package/dist/jobs/job-runner-context.d.ts +32 -0
  347. package/dist/jobs/job-runner-context.js +305 -0
  348. package/dist/jobs/job-runner-execution.d.ts +7 -0
  349. package/dist/jobs/job-runner-execution.js +160 -0
  350. package/dist/jobs/job-runner-gates.d.ts +19 -0
  351. package/dist/jobs/job-runner-gates.js +198 -0
  352. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  353. package/dist/jobs/job-runner-helpers.js +69 -4
  354. package/dist/jobs/job-runner-schedulers.d.ts +1 -1
  355. package/dist/jobs/job-runner-schedulers.js +3 -4
  356. package/dist/jobs/job-runner-scope.d.ts +44 -0
  357. package/dist/jobs/job-runner-scope.js +102 -0
  358. package/dist/jobs/job-runner-types.d.ts +12 -0
  359. package/dist/jobs/job-runner.d.ts +0 -1
  360. package/dist/jobs/job-runner.js +57 -669
  361. package/dist/lifecycle/lifecycle.js +3 -5
  362. package/dist/lifecycle/signal-handlers.js +5 -4
  363. package/dist/logging/utils.d.ts +2 -1
  364. package/dist/logging/utils.js +38 -10
  365. package/dist/migrations/index.js +1 -1
  366. package/dist/migrations/kumiko-drift.js +14 -13
  367. package/dist/migrations/pending-rebuilds.d.ts +21 -20
  368. package/dist/migrations/pending-rebuilds.js +27 -27
  369. package/dist/migrations/projection-table-index.d.ts +3 -3
  370. package/dist/migrations/projection-table-index.js +8 -8
  371. package/dist/observability/escape-hatch-report.d.ts +3 -0
  372. package/dist/observability/escape-hatch-report.js +12 -1
  373. package/dist/observability/metric-validator.js +1 -1
  374. package/dist/observability/metrics-handle.js +3 -3
  375. package/dist/pipeline/append-event-core.js +11 -2
  376. package/dist/pipeline/cascade-handler.d.ts +1 -1
  377. package/dist/pipeline/cascade-handler.js +165 -138
  378. package/dist/pipeline/dispatch-batch.js +2 -2
  379. package/dist/pipeline/dispatch-query.js +20 -13
  380. package/dist/pipeline/dispatch-shared.d.ts +14 -3
  381. package/dist/pipeline/dispatch-shared.js +67 -721
  382. package/dist/pipeline/dispatch-stream.js +1 -1
  383. package/dist/pipeline/dispatch-write.js +297 -190
  384. package/dist/pipeline/dispatcher-utils.d.ts +1 -1
  385. package/dist/pipeline/dispatcher-utils.js +1 -1
  386. package/dist/pipeline/event-consumer-state.js +2 -2
  387. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  388. package/dist/pipeline/event-dispatcher-delivery.js +4 -6
  389. package/dist/pipeline/event-dispatcher.js +17 -28
  390. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  391. package/dist/pipeline/handler-context-auth.js +54 -0
  392. package/dist/pipeline/handler-context-config.d.ts +14 -0
  393. package/dist/pipeline/handler-context-config.js +113 -0
  394. package/dist/pipeline/handler-context-db.d.ts +24 -0
  395. package/dist/pipeline/handler-context-db.js +149 -0
  396. package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
  397. package/dist/pipeline/handler-context-dispatch.js +89 -0
  398. package/dist/pipeline/handler-context-events.d.ts +43 -0
  399. package/dist/pipeline/handler-context-events.js +237 -0
  400. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  401. package/dist/pipeline/handler-context-notify.js +55 -0
  402. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  403. package/dist/pipeline/handler-context-observability.js +21 -0
  404. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  405. package/dist/pipeline/handler-context-scope.js +88 -0
  406. package/dist/pipeline/irreversible-operation-gate.js +1 -1
  407. package/dist/pipeline/lifecycle-pipeline.js +9 -9
  408. package/dist/pipeline/member-read-only-transaction.js +1 -3
  409. package/dist/pipeline/msp-rebuild.js +1 -1
  410. package/dist/pipeline/notify-scope.js +12 -1
  411. package/dist/pipeline/pre-handler-gates.js +20 -4
  412. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  413. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  414. package/dist/pipeline/projection-rebuild.js +15 -13
  415. package/dist/pipeline/system-hooks.js +39 -41
  416. package/dist/pipeline/system-identity-switch.js +1 -3
  417. package/dist/pipeline/tenant-timezone-cache.js +1 -1
  418. package/dist/random/generate.d.ts +29 -29
  419. package/dist/random/generate.js +30 -30
  420. package/dist/random/words.js +20 -20
  421. package/dist/rate-limit/middleware.js +9 -5
  422. package/dist/redis/client.js +2 -2
  423. package/dist/redis/pubsub-signal.js +14 -17
  424. package/dist/schema-cli/apply-command.d.ts +2 -0
  425. package/dist/schema-cli/apply-command.js +97 -0
  426. package/dist/schema-cli/baseline-command.d.ts +2 -0
  427. package/dist/schema-cli/baseline-command.js +33 -0
  428. package/dist/schema-cli/generate-command.d.ts +2 -0
  429. package/dist/schema-cli/generate-command.js +90 -0
  430. package/dist/schema-cli/shared.d.ts +39 -0
  431. package/dist/schema-cli/shared.js +95 -0
  432. package/dist/schema-cli/status-command.d.ts +2 -0
  433. package/dist/schema-cli/status-command.js +73 -0
  434. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  435. package/dist/schema-cli/validate-boot-input.js +130 -0
  436. package/dist/schema-cli/validate-command.d.ts +2 -0
  437. package/dist/schema-cli/validate-command.js +130 -0
  438. package/dist/schema-cli.d.ts +2 -18
  439. package/dist/schema-cli.js +47 -510
  440. package/dist/search/index.js +4 -4
  441. package/dist/search/meilisearch-adapter.js +2 -1
  442. package/dist/search/purge-subject.js +4 -5
  443. package/dist/search/reindex-entity.js +6 -9
  444. package/dist/time/boot-tz-warning.d.ts +2 -2
  445. package/dist/time/boot-tz-warning.js +8 -8
  446. package/dist/time/iana.js +10 -10
  447. package/dist/time/index.js +9 -9
  448. package/dist/time/legacy-date.d.ts +2 -2
  449. package/dist/time/legacy-date.js +6 -8
  450. package/dist/time/polyfill.js +1 -1
  451. package/dist/time/tz-context.d.ts +1 -1
  452. package/dist/time/tz-context.js +1 -1
  453. package/dist/ui-types/app-schema.js +7 -7
  454. package/dist/ui-types/list-row-meta.js +4 -4
  455. package/dist/upgrade-cli.js +159 -130
  456. package/dist/utils/compare.js +1 -1
  457. package/package.json +5 -5
  458. package/src/changes.json +358 -0
  459. package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
  460. package/src/scripts/codemod/feature-layout.ts +10 -2
  461. package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
  462. package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
  463. package/src/scripts/codemod/object-form.ts +41 -4
  464. package/src/scripts/codemod/package-merge.ts +58 -8
  465. package/src/scripts/codemod/pii-personal-migration.ts +192 -153
  466. package/src/scripts/codemod/testing-injection.ts +122 -42
  467. package/src/scripts/codemod/testing-move.ts +29 -7
  468. package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
@@ -0,0 +1,203 @@
1
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
2
+ import { computeBlindIndex, configuredBlindIndexKey } from "../crypto/index.js";
3
+ import { shiftParams } from "../engine/ownership.js";
4
+ import { ValidationError } from "../errors/index.js";
5
+ import { isListFilterAllowed } from "./entity-field-encryption.js";
6
+ import { buildFilterWhere } from "./event-store-executor-context.js";
7
+ // Rebases a clause's local $1..$n onto `params` and returns the shifted text.
8
+ export function bindShiftedClause(params, clause) {
9
+ const shifted = shiftParams({ sqlText: clause.sqlText, params: clause.params }, params.length);
10
+ for (const p of shifted.params)
11
+ params.push(p);
12
+ return shifted.sqlText;
13
+ }
14
+ export function appendShiftedClause(where, clause) {
15
+ where.whereSql.push(bindShiftedClause(where.params, clause));
16
+ }
17
+ export function bindPlaceholders(params, values) {
18
+ return values
19
+ .map((value) => {
20
+ params.push(value);
21
+ return `$${params.length}`;
22
+ })
23
+ .join(", ");
24
+ }
25
+ // multiSelect stores its options as a jsonb array — a filter value is one
26
+ // option, not the whole array, so eq/ne/in must check array containment
27
+ // (`@>`) instead of scalar `=`/`<>`/`IN` against the jsonb column.
28
+ // lt/gt/lte/gte have no containment analogue; the boot-validator already
29
+ // blocks them for screen-declared filters (screen-filter-ops.ts
30
+ // EQUALITY_ONLY), but a client-supplied facet filter reaches here
31
+ // unvalidated, so treat it as unsatisfiable rather than emitting SQL
32
+ // Postgres would reject.
33
+ function applyMultiSelectFilter(colSql, whereSql, params, f) {
34
+ if (f.op === "lt" || f.op === "gt" || f.op === "lte" || f.op === "gte") {
35
+ whereSql.push("FALSE");
36
+ // skip: lt/gt/lte/gte is unsatisfiable on a multiSelect column
37
+ return;
38
+ }
39
+ if (f.op === "in") {
40
+ if (!Array.isArray(f.value) || f.value.length === 0) {
41
+ whereSql.push("FALSE");
42
+ // skip: empty/non-array `in` is unsatisfiable, mirrors buildFilterWhere's "in" short-circuit
43
+ return;
44
+ }
45
+ const parts = f.value.map((v) => {
46
+ params.push(JSON.stringify([v]));
47
+ return `${colSql(f.field)} @> $${params.length}::text::jsonb`;
48
+ });
49
+ whereSql.push(`(${parts.join(" OR ")})`);
50
+ // skip: containment condition already pushed — don't fall through to the eq/ne branch below
51
+ return;
52
+ }
53
+ const candidates = Array.isArray(f.value) ? f.value : [f.value];
54
+ // `@> '[]'` is true for every non-null row, so an empty array would turn
55
+ // the filter into a silent match-all.
56
+ if (candidates.length === 0) {
57
+ whereSql.push("FALSE");
58
+ // skip: empty candidate list is unsatisfiable
59
+ return;
60
+ }
61
+ // Bound as text then cast: a JS array makes postgres.js infer boolean[]/int4[]
62
+ // for client-supplied non-string values, and `boolean[]::jsonb` is rejected
63
+ // (500 on client input). An array value means "contains all of these".
64
+ params.push(JSON.stringify(candidates));
65
+ const containment = `${colSql(f.field)} @> $${params.length}::text::jsonb`;
66
+ // A NULL column must count as "does not contain" — plain NOT(...) yields
67
+ // NULL and would drop those rows.
68
+ whereSql.push(f.op === "ne" ? `(${colSql(f.field)} IS NULL OR NOT (${containment}))` : containment);
69
+ }
70
+ // Falls through to the screen-filter WHERE builder shared with
71
+ // screen-declared filters — buildFilterWhere flattens the op into a
72
+ // WhereObject, which this then lowers to raw SQL fragments.
73
+ function applyScreenFilter(table, colSql, whereSql, params, f) {
74
+ const screen = buildFilterWhere(f.field, f.op, f.value);
75
+ if (screen === null) {
76
+ whereSql.push("FALSE");
77
+ // skip: filter is unsatisfiable → emit FALSE, no params to bind
78
+ return;
79
+ }
80
+ for (const [field, value] of Object.entries(screen)) {
81
+ // `x <> NULL` is never true — mirror buildWhereClause's IS [NOT] NULL handling in db/where-clause.ts.
82
+ if (value === null) {
83
+ whereSql.push(`${colSql(field)} IS NULL`);
84
+ }
85
+ else if (Array.isArray(value)) {
86
+ const placeholders = value.map((v) => {
87
+ params.push(v);
88
+ return `$${params.length}`;
89
+ });
90
+ whereSql.push(`${colSql(field)} IN (${placeholders.join(", ")})`);
91
+ }
92
+ else if (isPlainObject(value)) {
93
+ if (value["ne"] === null && Object.keys(value).length === 1) {
94
+ whereSql.push(`${colSql(field)} IS NOT NULL`);
95
+ continue;
96
+ }
97
+ const opMap = {
98
+ gt: ">",
99
+ gte: ">=",
100
+ lt: "<",
101
+ lte: "<=",
102
+ ne: "<>",
103
+ };
104
+ for (const [opKey, opSym] of Object.entries(opMap)) {
105
+ if (!(opKey in value))
106
+ continue;
107
+ params.push(value[opKey]);
108
+ whereSql.push(`${colSql(field)} ${opSym} $${params.length}`);
109
+ }
110
+ }
111
+ else {
112
+ // Blind-index OR rewrite, lock-step with buildWhereClause
113
+ // in db/where-clause.ts — equality on lookupable fields matches the
114
+ // plaintext arm OR the HMAC arm.
115
+ const bidxKey = configuredBlindIndexKey();
116
+ if (bidxKey !== undefined && typeof value === "string" && table[`${field}Bidx`]) {
117
+ params.push(value, computeBlindIndex(bidxKey, value));
118
+ whereSql.push(`(${colSql(field)} = $${params.length - 1} OR ${colSql(`${field}Bidx`)} = $${params.length})`);
119
+ }
120
+ else {
121
+ params.push(value);
122
+ whereSql.push(`${colSql(field)} = $${params.length}`);
123
+ }
124
+ }
125
+ }
126
+ }
127
+ // A `filter`/`filters` entry that already pins the parentRef's entityType
128
+ // field is a real subset of the parent-ref clause's own OR-branches — the
129
+ // client-supplied filter can't widen the result, only narrow which branches
130
+ // the SQL even needs to consider. Skipped for multiSelect fields, where `eq`
131
+ // means containment (see applyMultiSelectFilter) rather than equality.
132
+ export function deriveParentNarrowTypes(entity, payload) {
133
+ const parentRef = entity.parentRef;
134
+ if (parentRef === undefined)
135
+ return undefined;
136
+ if (entity.fields[parentRef.entityTypeField]?.type === "multiSelect")
137
+ return undefined;
138
+ const typeFilters = [
139
+ ...(payload.filter ? [payload.filter] : []),
140
+ ...(payload.filters ?? []),
141
+ ].filter((f) => f.field === parentRef.entityTypeField);
142
+ let narrowed;
143
+ for (const f of typeFilters) {
144
+ let values;
145
+ if (f.op === "eq" && typeof f.value === "string") {
146
+ values = [f.value];
147
+ }
148
+ else if (f.op === "in" &&
149
+ Array.isArray(f.value) &&
150
+ f.value.every((v) => typeof v === "string")) {
151
+ values = f.value;
152
+ }
153
+ if (values === undefined)
154
+ continue;
155
+ narrowed =
156
+ narrowed === undefined
157
+ ? new Set(values)
158
+ : new Set([...narrowed].filter((v) => values.includes(v)));
159
+ }
160
+ return narrowed;
161
+ }
162
+ export function collectListFilters(payload) {
163
+ const single = payload.filter !== undefined ? [payload.filter] : [];
164
+ return payload.filters !== undefined ? [...single, ...payload.filters] : single;
165
+ }
166
+ export function applyListFilter(where, f, table, entity, fieldReadClause) {
167
+ if (!Object.hasOwn(table, f.field)) {
168
+ // skip: unknown field — not a real column, drop the filter (injection guard)
169
+ return;
170
+ }
171
+ const readClause = fieldReadClause(f.field);
172
+ if (readClause.kind === "empty") {
173
+ where.whereSql.push("FALSE");
174
+ // skip: no role grants read on this field, so no row may be probed
175
+ // through it — an unsatisfiable filter beats an error, which would
176
+ // itself confirm the field exists (field-access.ts strips silently)
177
+ return;
178
+ }
179
+ if (!isListFilterAllowed(entity, f.field, f)) {
180
+ throw new ValidationError({
181
+ fields: [
182
+ {
183
+ path: `filter.${f.field}`,
184
+ code: "field_not_filterable",
185
+ i18nKey: "errors.validation.invalid_value",
186
+ params: {
187
+ field: f.field,
188
+ op: f.op,
189
+ hint: "Encrypted, personal and secret fields cannot be filtered; only equality on a field with find: exact or fuzzy.",
190
+ },
191
+ },
192
+ ],
193
+ });
194
+ }
195
+ if (readClause.kind === "sql")
196
+ appendShiftedClause(where, readClause);
197
+ if (entity.fields[f.field]?.type === "multiSelect") {
198
+ applyMultiSelectFilter(where.colSql, where.whereSql, where.params, f);
199
+ // skip: multiSelect already handled — don't fall through to applyScreenFilter
200
+ return;
201
+ }
202
+ applyScreenFilter(table, where.colSql, where.whereSql, where.params, f);
203
+ }
@@ -0,0 +1,14 @@
1
+ import type { DbRunner } from "./connection.ts";
2
+ import type { Table } from "./event-store-executor-context.ts";
3
+ import type { ListWhere } from "./event-store-executor-read-filter.ts";
4
+ import { type ListSort } from "./event-store-executor-read-sort.ts";
5
+ export declare function resolveListPagination(payload: {
6
+ readonly limit?: unknown;
7
+ readonly offset?: unknown;
8
+ }): {
9
+ readonly limit: number;
10
+ readonly offset: number;
11
+ };
12
+ export declare function appendCursorBoundary(where: ListWhere, encodedCursor: string, sort: ListSort): void;
13
+ export declare function encodeNextCursor(cursorId: string, sort: ListSort, lastRaw: Record<string, unknown>, lastSortLabel: unknown, table: Table): string;
14
+ export declare function countListRows(runner: DbRunner, tableName: string, whereClauseSqlText: string, whereParams: readonly unknown[]): Promise<number>;
@@ -0,0 +1,99 @@
1
+ import { UnprocessableError } from "../errors/index.js";
2
+ import { decodeKeysetCursor, encodeCursor, encodeKeysetCursor } from "./cursor.js";
3
+ import { renderSortSql } from "./event-store-executor-read-sort.js";
4
+ import { executeRawQueryRead } from "./queries/raw-sql.js";
5
+ import { physicalColumnName } from "./table-builder.js";
6
+ // Defense-in-depth pagination guard. The handler boundary (entityListSchema)
7
+ // already validates limit/offset, but the executor is public API for custom
8
+ // handlers that pass their payload straight through — a non-integer limit
9
+ // would otherwise be interpolated raw into `LIMIT ${limit}` SQL text.
10
+ const MAX_LIST_LIMIT = 200; // keep in sync with engine/entity-handlers.ts MAX_LIST_LIMIT
11
+ export function resolveListPagination(payload) {
12
+ const limit = payload.limit ?? 50;
13
+ const offset = payload.offset ?? 0;
14
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 0) {
15
+ throw new UnprocessableError("invalid_list_limit", {
16
+ details: { hint: "limit must be a non-negative integer" },
17
+ });
18
+ }
19
+ if (typeof offset !== "number" || !Number.isInteger(offset) || offset < 0) {
20
+ throw new UnprocessableError("invalid_list_offset", {
21
+ details: { hint: "offset must be a non-negative integer" },
22
+ });
23
+ }
24
+ return { limit: Math.min(limit, MAX_LIST_LIMIT), offset };
25
+ }
26
+ // Cursor sort values travel as text and bind as plain string params — Postgres
27
+ // infers the parameter type from the column they are compared against, the same
28
+ // way prepareValue binds a timestamptz as an ISO string. undefined means the
29
+ // driver value has no faithful text form, which downgrades the page to the
30
+ // legacy id-only boundary instead of emitting a wrong one.
31
+ function toCursorSortText(value) {
32
+ if (value === undefined)
33
+ return undefined;
34
+ if (value === null)
35
+ return null;
36
+ if (typeof value === "string")
37
+ return value;
38
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean") {
39
+ return String(value);
40
+ }
41
+ if (value instanceof Date)
42
+ return value.toISOString();
43
+ if (typeof value === "object") {
44
+ const tag = Reflect.get(value, Symbol.toStringTag);
45
+ if (typeof tag === "string" && tag.startsWith("Temporal."))
46
+ return String(value);
47
+ return JSON.stringify(value);
48
+ }
49
+ return undefined;
50
+ }
51
+ // Keyset boundary for `ORDER BY <sort> <dir>, id ASC` under Postgres' DEFAULT
52
+ // null ordering (ASC → NULLS LAST, DESC → NULLS FIRST). An explicit NULLS clause
53
+ // would change the visible order and cost the sort its index.
54
+ function keysetBoundarySql(sortCol, idCol, cursor, descending, params) {
55
+ params.push(cursor.id);
56
+ const afterId = `${idCol} > $${params.length}`;
57
+ const nullsComeFirst = descending;
58
+ if (cursor.sortValue === null) {
59
+ return nullsComeFirst
60
+ ? `(${sortCol} IS NOT NULL OR ${afterId})`
61
+ : `(${sortCol} IS NULL AND ${afterId})`;
62
+ }
63
+ params.push(cursor.sortValue);
64
+ const sortParam = `$${params.length}`;
65
+ const beyond = `${sortCol} ${descending ? "<" : ">"} ${sortParam}`;
66
+ const tie = `(${sortCol} = ${sortParam} AND ${afterId})`;
67
+ return nullsComeFirst
68
+ ? `(${sortCol} IS NOT NULL AND (${beyond} OR ${tie}))`
69
+ : `(${sortCol} IS NULL OR ${beyond} OR ${tie})`;
70
+ }
71
+ export function appendCursorBoundary(where, encodedCursor, sort) {
72
+ const cursor = decodeKeysetCursor(encodedCursor);
73
+ if (sort.kind === "unsorted" || cursor.sortValue === undefined) {
74
+ where.params.push(cursor.id);
75
+ where.whereSql.push(`${where.colSql("id")} > $${where.params.length}`);
76
+ }
77
+ else {
78
+ where.whereSql.push(keysetBoundarySql(renderSortSql(sort, where), where.colSql("id"), { id: cursor.id, sortValue: cursor.sortValue }, sort.descending, where.params));
79
+ }
80
+ }
81
+ export function encodeNextCursor(cursorId, sort, lastRaw, lastSortLabel, table) {
82
+ const sortText = sort.kind === "unsorted"
83
+ ? undefined
84
+ : toCursorSortText(sort.kind === "referenceLabel"
85
+ ? lastSortLabel
86
+ : lastRaw[physicalColumnName(table, sort.field)]);
87
+ return sortText === undefined ? encodeCursor(cursorId) : encodeKeysetCursor(sortText, cursorId);
88
+ }
89
+ // total: extra COUNT(*) — only when explicitly requested (pager UI).
90
+ // Postgres cost is O(table-scan) without a filter, with a filter as
91
+ // expensive as the corresponding WHERE — cheap enough on indexed columns.
92
+ // Always counted over the full WHERE: search ids, reference matches, tenant,
93
+ // soft-delete, cursor, ownership and filters all narrow the result, so the
94
+ // search hit count alone is never the total.
95
+ export async function countListRows(runner, tableName, whereClauseSqlText, whereParams) {
96
+ const countSql = `SELECT COUNT(*)::int AS count FROM "${tableName}"${whereClauseSqlText}`;
97
+ const countRows = await executeRawQueryRead(runner, countSql, whereParams);
98
+ return countRows[0]?.count ?? 0;
99
+ }
@@ -0,0 +1,6 @@
1
+ import type { EntityDefinition, TenantId } from "../engine/types/index.ts";
2
+ import type { TableInfo } from "./table-info.ts";
3
+ export declare function withStreamVersion(row: Record<string, unknown>, streamVersion: number): Record<string, unknown>;
4
+ export declare function coerceAndRehydrateRow(raw: Record<string, unknown>, tableInfo: TableInfo, entity: EntityDefinition): Record<string, unknown>;
5
+ export declare function appendStreamVersionProjection(params: unknown[], tableName: string, streamTenantId: TenantId): string;
6
+ export declare function takeStreamVersions(rawRows: readonly Record<string, unknown>[]): number[];
@@ -0,0 +1,32 @@
1
+ import { rehydrateCompoundTypes } from "./compound-types.js";
2
+ import { coerceRow } from "./query.js";
3
+ const STREAM_VERSION_ALIAS = "__streamVersion";
4
+ export function withStreamVersion(row, streamVersion) {
5
+ return streamVersion > 0 ? { ...row, version: streamVersion } : row;
6
+ }
7
+ // Coerce BEFORE rehydrate/decrypt: raw SQL rows carry snake_case column names,
8
+ // while compound-type lookups (rehydrateMoney et al.) and the encrypted/pii
9
+ // field lists are all camelCase — running either first on a still-snake_case
10
+ // row silently no-ops (money) or skips every multi-word field (ciphertext
11
+ // leaked to the caller).
12
+ export function coerceAndRehydrateRow(raw, tableInfo, entity) {
13
+ return rehydrateCompoundTypes(coerceRow(raw, tableInfo), entity);
14
+ }
15
+ // Stream version in the same statement as the rows: a separate read could pair a
16
+ // post-write version with a pre-write row, and update({version}) would then pass
17
+ // the optimistic lock against a state the client never saw (see loadDetailSnapshot).
18
+ export function appendStreamVersionProjection(params, tableName, streamTenantId) {
19
+ params.push(streamTenantId);
20
+ return (`, (SELECT MAX("version") FROM "kumiko_events" ` +
21
+ `WHERE "aggregate_id" = "${tableName}"."id" AND "tenant_id" = $${params.length}) ` +
22
+ `AS "${STREAM_VERSION_ALIAS}"`);
23
+ }
24
+ // The alias is a query artifact, not a column of this entity — stripped before
25
+ // coercion so it never reaches the caller or the entity cache.
26
+ export function takeStreamVersions(rawRows) {
27
+ return rawRows.map((r) => {
28
+ const v = r[STREAM_VERSION_ALIAS];
29
+ delete r[STREAM_VERSION_ALIAS];
30
+ return typeof v === "number" ? v : 0;
31
+ });
32
+ }
@@ -0,0 +1,28 @@
1
+ import { type OwnershipClause } from "../engine/ownership.ts";
2
+ import type { EntityDefinition, SessionUser, TenantId } from "../engine/types/index.ts";
3
+ import type { SearchAdapter } from "../search/types.ts";
4
+ import type { EventStoreExecutor } from "./event-store-executor.ts";
5
+ import type { Table } from "./event-store-executor-context.ts";
6
+ import { type ListWhere } from "./event-store-executor-read-filter.ts";
7
+ import type { TenantDb } from "./tenant-db.ts";
8
+ type ReferenceSearchOption = NonNullable<Parameters<EventStoreExecutor["list"]>[3]>["referenceSearch"];
9
+ export declare function isSoftDeletedTargetHidden(targetEntity: EntityDefinition, targetTable: Table): boolean;
10
+ export type ListSearchRequest = {
11
+ readonly searchTerm: string;
12
+ readonly searchAdapter: SearchAdapter | undefined;
13
+ readonly entityName: string;
14
+ readonly table: Table;
15
+ readonly referenceSearch: ReferenceSearchOption;
16
+ readonly fieldReadClause: (field: string) => OwnershipClause;
17
+ readonly streamTenantFor: (user: SessionUser) => TenantId;
18
+ readonly user: SessionUser;
19
+ readonly db: TenantDb;
20
+ };
21
+ export type ListSearchOutcome = {
22
+ readonly kind: "noMatches";
23
+ } | {
24
+ readonly kind: "applied";
25
+ readonly searchTruncated: boolean;
26
+ };
27
+ export declare function applyListSearch(request: ListSearchRequest, where: ListWhere): Promise<ListSearchOutcome>;
28
+ export {};
@@ -0,0 +1,221 @@
1
+ import { escapeLikePattern } from "../crypto/ciphertext-pattern.js";
2
+ import { buildOwnershipClause, normalizeAccessEntry, shiftParams, } from "../engine/ownership.js";
3
+ import { SYSTEM_TENANT_ID } from "../engine/types/identifiers.js";
4
+ import { UnprocessableError } from "../errors/index.js";
5
+ import { LIST_ROW_META_REFERENCES } from "../ui-types/list-row-meta.js";
6
+ import { hasSearchablePlaintext, isSensitiveLabelField } from "./entity-field-encryption.js";
7
+ import { bindPlaceholders } from "./event-store-executor-read-filter.js";
8
+ import { executeRawQueryRead } from "./queries/raw-sql.js";
9
+ import { buildEntityTable, physicalColumnName } from "./table-builder.js";
10
+ import { schemaTableName } from "./table-introspection.js";
11
+ import { tenantDbRunner } from "./tenant-db-runner.js";
12
+ // Above this many target-row matches, drop the reference clause
13
+ // entirely instead of truncating: an arbitrary 200-row slice of "which
14
+ // targets match" would silently hide rows a full-cardinality match would
15
+ // have found. Native text search on the entity's own fields still applies.
16
+ const MAX_REFERENCE_SEARCH_IDS = 200;
17
+ // The hit ids become an `id IN (...)` filter beneath the SQL cursor/filters/sort, so the
18
+ // adapter must return the whole candidate set, not a page; matches Meilisearch's default
19
+ // maxTotalHits, which caps a response at exactly this many hits however high a larger limit is
20
+ // asked. A response that reaches the cap is therefore flagged `searchTruncated` rather than
21
+ // treated as complete (a cap of N+1 could never be observed).
22
+ const MAX_SEARCH_CANDIDATE_IDS = 1000;
23
+ // Registry-declared `searchable` reference fields, plus the implicit
24
+ // tenantId→tenant.name row-meta reference — both resolve a text
25
+ // search term against a target row's label instead of the raw FK column.
26
+ function collectReferenceSearchDescriptors(table, referenceSearch, dbMode) {
27
+ if (!referenceSearch)
28
+ return [];
29
+ const descriptors = referenceSearch.fields.map((f) => ({
30
+ ownColumn: f.fieldName,
31
+ targetEntityName: f.targetEntityName,
32
+ labelField: f.labelField,
33
+ }));
34
+ for (const [ownColumn, ref] of Object.entries(LIST_ROW_META_REFERENCES)) {
35
+ if (table[ownColumn] === undefined)
36
+ continue;
37
+ // In "tenant" mode the outer tenant filter already pins tenantId to one
38
+ // of exactly two values, so a by-name lookup here can never narrow that further.
39
+ if (ownColumn === "tenantId" && dbMode === "tenant")
40
+ continue;
41
+ descriptors.push({ ownColumn, targetEntityName: ref.refEntity, labelField: ref.refLabelField });
42
+ }
43
+ return descriptors;
44
+ }
45
+ // Matches the main list's own default: soft-deleted rows are invisible, so a
46
+ // deleted target must neither match a search nor influence an ordering.
47
+ export function isSoftDeletedTargetHidden(targetEntity, targetTable) {
48
+ return targetEntity.softDelete === true && targetTable["isDeleted"] !== undefined;
49
+ }
50
+ // Tenant-scoped ILIKE lookup against one reference target — never raw/
51
+ // unscoped SQL, a match in a foreign tenant must never surface a row here.
52
+ // Mirrors the main table's own tenant filter (see the `table["tenantId"]`
53
+ // check further down in list()).
54
+ async function resolveReferenceMatchesViaIlike(descriptor, searchTerm, targetEntity, targetTable, targetTableName, user, db) {
55
+ // The label must not leak through match counts: a viewer who cannot read the
56
+ // target row or its label field would otherwise probe names by searching.
57
+ const labelAccess = buildOwnershipClause(user, normalizeAccessEntry(targetEntity.fields[descriptor.labelField]?.access?.read), targetTable);
58
+ if (labelAccess.kind !== "pass")
59
+ return undefined;
60
+ const ownership = buildOwnershipClause(user, targetEntity.access?.read, targetTable);
61
+ if (ownership.kind === "empty")
62
+ return undefined;
63
+ const labelCol = physicalColumnName(targetTable, descriptor.labelField);
64
+ const subParams = [`%${escapeLikePattern(searchTerm)}%`];
65
+ let tenantClause = "";
66
+ if (targetTable["tenantId"] !== undefined && db.mode === "tenant") {
67
+ subParams.push(db.tenantId, SYSTEM_TENANT_ID);
68
+ tenantClause = ` AND "${physicalColumnName(targetTable, "tenantId")}" IN ($2, $3)`;
69
+ }
70
+ let ownershipClause = "";
71
+ if (ownership.kind === "sql") {
72
+ const shifted = shiftParams({ sqlText: ownership.sqlText, params: ownership.params }, subParams.length);
73
+ ownershipClause = ` AND ${shifted.sqlText}`;
74
+ for (const p of shifted.params)
75
+ subParams.push(p);
76
+ }
77
+ const deletedClause = isSoftDeletedTargetHidden(targetEntity, targetTable)
78
+ ? ` AND "${physicalColumnName(targetTable, "isDeleted")}" = FALSE`
79
+ : "";
80
+ // ::text cast covers a non-text labelField (e.g. a number/select column
81
+ // used as label) — Postgres has no ILIKE for those types otherwise.
82
+ const sql = `SELECT "id" FROM "${targetTableName}" WHERE ("${labelCol}")::text ILIKE $1${tenantClause}${ownershipClause}${deletedClause} ` +
83
+ `LIMIT ${MAX_REFERENCE_SEARCH_IDS + 1}`;
84
+ const rows = await executeRawQueryRead(tenantDbRunner(db), sql, subParams);
85
+ if (rows.length === 0 || rows.length > MAX_REFERENCE_SEARCH_IDS) {
86
+ // skip: no match, or over the cap — drop the clause rather than
87
+ // truncate to an arbitrary slice of matching targets
88
+ return undefined;
89
+ }
90
+ return { ownColumn: descriptor.ownColumn, ids: rows.map((r) => r.id) };
91
+ }
92
+ // Same lookup, but the target's index is tenant-wide, not per-viewer, so
93
+ // candidate ids get re-checked against the target entity's own read access.
94
+ async function resolveReferenceMatchesViaSearchIndex(descriptor, searchTerm, targetEntity, targetTable, targetTableName, user, db, searchAdapter) {
95
+ const searchTenantId = targetEntity.systemStream ? SYSTEM_TENANT_ID : user.tenantId;
96
+ const results = await searchAdapter.search(searchTenantId, searchTerm, {
97
+ filterType: descriptor.targetEntityName,
98
+ limit: MAX_REFERENCE_SEARCH_IDS + 1,
99
+ });
100
+ if (results.length === 0 || results.length > MAX_REFERENCE_SEARCH_IDS) {
101
+ // skip: no match, or over the cap — same drop-the-clause policy as the ILIKE path
102
+ return undefined;
103
+ }
104
+ const ownership = buildOwnershipClause(user, targetEntity.access?.read, targetTable);
105
+ if (ownership.kind === "empty")
106
+ return undefined;
107
+ const candidateIds = results.map((r) => String(r.entityId));
108
+ // The index is only trusted to name candidates: a stale or foreign-tenant document must
109
+ // never become a filter value, so tenant-scoped targets are always re-read from the DB.
110
+ const tenantScoped = targetTable["tenantId"] !== undefined && db.mode === "tenant";
111
+ if (ownership.kind === "pass" && !tenantScoped) {
112
+ return { ownColumn: descriptor.ownColumn, ids: candidateIds };
113
+ }
114
+ const params = [...candidateIds];
115
+ const idPlaceholders = candidateIds.map((_, i) => `$${i + 1}`);
116
+ let tenantClause = "";
117
+ if (tenantScoped) {
118
+ params.push(db.tenantId, SYSTEM_TENANT_ID);
119
+ tenantClause = ` AND "${physicalColumnName(targetTable, "tenantId")}" IN ($${params.length - 1}, $${params.length})`;
120
+ }
121
+ let ownershipClause = "";
122
+ if (ownership.kind === "sql") {
123
+ const shifted = shiftParams({ sqlText: ownership.sqlText, params: ownership.params }, params.length);
124
+ for (const p of shifted.params)
125
+ params.push(p);
126
+ ownershipClause = ` AND ${shifted.sqlText}`;
127
+ }
128
+ const sql = `SELECT "id" FROM "${targetTableName}" WHERE "id" IN (${idPlaceholders.join(", ")})` +
129
+ `${tenantClause}${ownershipClause}`;
130
+ const rows = await executeRawQueryRead(tenantDbRunner(db), sql, params);
131
+ if (rows.length === 0)
132
+ return undefined;
133
+ return { ownColumn: descriptor.ownColumn, ids: rows.map((r) => r.id) };
134
+ }
135
+ async function resolveReferenceMatches(descriptor, searchTerm, resolveEntity, db, user, searchAdapter) {
136
+ const targetEntity = resolveEntity(descriptor.targetEntityName);
137
+ if (targetEntity === undefined) {
138
+ // skip: unknown target entity — registry inconsistency the boot-validator
139
+ // should have caught; fail open (no clause) rather than 500 the request
140
+ return undefined;
141
+ }
142
+ if (targetEntity.fields[descriptor.labelField] === undefined) {
143
+ // skip: labelField isn't a real (declared) column on the target entity —
144
+ // row-meta references (e.g. tenant.name) always resolve here since
145
+ // `name` is a declared field on the tenant entity, not an id/row-meta column
146
+ return undefined;
147
+ }
148
+ const targetTable = referenceTargetTable(descriptor.targetEntityName, targetEntity);
149
+ const targetTableName = String(schemaTableName(targetTable));
150
+ if (!isSensitiveLabelField(targetEntity, descriptor.labelField)) {
151
+ return resolveReferenceMatchesViaIlike(descriptor, searchTerm, targetEntity, targetTable, targetTableName, user, db);
152
+ }
153
+ if (!hasSearchablePlaintext(targetEntity, descriptor.labelField)) {
154
+ // skip: no plaintext anywhere to match — boot rejects this normally,
155
+ // this is defense-in-depth for a target entity resolved only at runtime.
156
+ return undefined;
157
+ }
158
+ return resolveReferenceMatchesViaSearchIndex(descriptor, searchTerm, targetEntity, targetTable, targetTableName, user, db, searchAdapter);
159
+ }
160
+ // Building the table is pure but not free, and runs on every search request;
161
+ // keyed by the definition object so a re-registered entity never sees a stale table.
162
+ const referenceTargetTables = new WeakMap();
163
+ function referenceTargetTable(name, entity) {
164
+ const cached = referenceTargetTables.get(entity);
165
+ if (cached?.name === name)
166
+ return cached.table;
167
+ const table = buildEntityTable(name, entity);
168
+ referenceTargetTables.set(entity, { name, table });
169
+ return table;
170
+ }
171
+ export async function applyListSearch(request, where) {
172
+ const { searchAdapter } = request;
173
+ // A search term with no adapter wired must fail loud, not
174
+ // silently return the unfiltered list dressed up as a search result.
175
+ if (!searchAdapter) {
176
+ throw new UnprocessableError("search_adapter_not_wired", {
177
+ details: {
178
+ entity: request.entityName,
179
+ hint: "Wire a SearchAdapter for this entity, or remove `searchable` from the field/screen.",
180
+ },
181
+ });
182
+ }
183
+ // Same choke-point as writes/index: systemStream docs live under
184
+ // SYSTEM_TENANT_ID; everything else under the session tenant — db.mode
185
+ // must not invent a second predicate.
186
+ const searchTenantId = request.streamTenantFor(request.user);
187
+ const results = await searchAdapter.search(searchTenantId, request.searchTerm, {
188
+ filterType: request.entityName,
189
+ limit: MAX_SEARCH_CANDIDATE_IDS,
190
+ });
191
+ const filterIds = results.map((r) => r.entityId);
192
+ const referenceMatches = await resolveSearchableReferenceMatches(request, searchAdapter);
193
+ if (filterIds.length === 0 && referenceMatches.length === 0)
194
+ return { kind: "noMatches" };
195
+ const orParts = [];
196
+ if (filterIds.length > 0) {
197
+ orParts.push(`${where.colSql("id")} IN (${bindPlaceholders(where.params, filterIds)})`);
198
+ }
199
+ for (const match of referenceMatches) {
200
+ orParts.push(`${where.colSql(match.ownColumn)} IN (${bindPlaceholders(where.params, match.ids)})`);
201
+ }
202
+ // Parenthesized as one whereSql entry — whereSql is joined with
203
+ // " AND " below, an unparenthesized OR here would leak past the
204
+ // tenant filter that follows.
205
+ where.whereSql.push(`(${orParts.join(" OR ")})`);
206
+ return { kind: "applied", searchTruncated: results.length >= MAX_SEARCH_CANDIDATE_IDS };
207
+ }
208
+ // A searchable reference field (or the implicit tenantId→tenant.name row-meta
209
+ // reference) whose target row's labelField matches the search term also counts
210
+ // as a hit on this entity, not just a raw text-field hit.
211
+ async function resolveSearchableReferenceMatches(request, searchAdapter) {
212
+ const { referenceSearch } = request;
213
+ if (!referenceSearch)
214
+ return [];
215
+ // A caller who cannot read the reference column itself must not be able
216
+ // to infer its value by matching the target's label — same probe
217
+ // surface as filtering/sorting on it directly.
218
+ const descriptors = collectReferenceSearchDescriptors(request.table, referenceSearch, request.db.mode).filter((d) => request.fieldReadClause(d.ownColumn).kind === "pass");
219
+ const matches = await Promise.all(descriptors.map((d) => resolveReferenceMatches(d, request.searchTerm, referenceSearch.resolveEntity, request.db, request.user, searchAdapter)));
220
+ return matches.filter((m) => m !== undefined);
221
+ }
@@ -0,0 +1,41 @@
1
+ import { type OwnershipClause } from "../engine/ownership.ts";
2
+ import type { EntityDefinition, SessionUser } from "../engine/types/index.ts";
3
+ import type { EventStoreExecutor } from "./event-store-executor.ts";
4
+ import type { Table } 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
+ type ListPayload = Parameters<EventStoreExecutor["list"]>[0];
8
+ type ReferenceSortOption = NonNullable<Parameters<EventStoreExecutor["list"]>[3]>["referenceSort"];
9
+ export type ListSort = {
10
+ readonly kind: "unsorted";
11
+ } | {
12
+ readonly kind: "column";
13
+ readonly field: string;
14
+ readonly descending: boolean;
15
+ } | {
16
+ readonly kind: "referenceLabel";
17
+ readonly field: string;
18
+ readonly descending: boolean;
19
+ readonly labelSql: (params: unknown[]) => string;
20
+ };
21
+ export type SortedListSort = Exclude<ListSort, {
22
+ readonly kind: "unsorted";
23
+ }>;
24
+ export declare function resolveListSort(request: {
25
+ readonly payload: Pick<ListPayload, "sort" | "sortDirection">;
26
+ readonly table: Table;
27
+ readonly entity: EntityDefinition;
28
+ readonly tableName: string;
29
+ readonly colSql: (field: string) => string;
30
+ readonly fieldReadClause: (field: string) => OwnershipClause;
31
+ readonly referenceSort: ReferenceSortOption;
32
+ readonly db: TenantDb;
33
+ readonly user: SessionUser;
34
+ }): ListSort;
35
+ export declare function renderSortSql(sort: SortedListSort, where: ListWhere): string;
36
+ export declare function renderListOrderBy(sort: ListSort, where: ListWhere): {
37
+ readonly orderByClause: string;
38
+ readonly sortLabelProjection: string;
39
+ };
40
+ export declare function takeLastSortLabel(rawRows: readonly Record<string, unknown>[], sort: ListSort): unknown;
41
+ export {};