@cosmicdrift/kumiko-framework 0.14.0 → 0.16.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 (342) hide show
  1. package/package.json +6 -6
  2. package/src/__tests__/{anonymous-access.integration.ts → anonymous-access.integration.test.ts} +12 -9
  3. package/src/__tests__/{error-contract.integration.ts → error-contract.integration.test.ts} +5 -4
  4. package/src/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +3 -3
  5. package/src/__tests__/{full-stack.integration.ts → full-stack.integration.test.ts} +7 -16
  6. package/src/__tests__/{ownership.integration.ts → ownership.integration.test.ts} +3 -2
  7. package/src/__tests__/{raw-table.integration.ts → raw-table.integration.test.ts} +18 -30
  8. package/src/__tests__/{reference-data.integration.ts → reference-data.integration.test.ts} +24 -11
  9. package/src/__tests__/{transition-guard.integration.ts → transition-guard.integration.test.ts} +12 -10
  10. package/src/api/__tests__/api.test.ts +1 -1
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +1 -1
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +1 -1
  13. package/src/api/__tests__/{batch.integration.ts → batch.integration.test.ts} +30 -30
  14. package/src/api/__tests__/body-limit.test.ts +1 -1
  15. package/src/api/__tests__/csrf-middleware.test.ts +1 -1
  16. package/src/api/__tests__/{dispatcher-live.integration.ts → dispatcher-live.integration.test.ts} +10 -9
  17. package/src/api/__tests__/metrics-endpoint.test.ts +1 -1
  18. package/src/api/__tests__/{nested-write.integration.ts → nested-write.integration.test.ts} +13 -16
  19. package/src/api/__tests__/readiness.test.ts +1 -1
  20. package/src/api/__tests__/request-id-middleware.test.ts +1 -1
  21. package/src/api/__tests__/sse-broker.test.ts +12 -12
  22. package/src/api/__tests__/sse-route.test.ts +1 -1
  23. package/src/api/auth-routes.ts +2 -5
  24. package/src/api/readiness.ts +2 -2
  25. package/src/auth/__tests__/roles.test.ts +2 -2
  26. package/src/bun-db/__tests__/PATTERN.md +73 -0
  27. package/src/bun-db/__tests__/_helpers.ts +103 -0
  28. package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
  29. package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
  30. package/src/bun-db/__tests__/bun-test-db.ts +19 -0
  31. package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
  32. package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
  33. package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
  34. package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
  35. package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
  36. package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
  37. package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
  38. package/src/bun-db/connection.ts +84 -0
  39. package/src/bun-db/index.ts +31 -0
  40. package/src/bun-db/query.ts +842 -0
  41. package/src/compliance/__tests__/duration-spec.test.ts +1 -1
  42. package/src/compliance/__tests__/profiles.test.ts +1 -1
  43. package/src/compliance/__tests__/sub-processors.test.ts +1 -1
  44. package/src/compliance/profiles.ts +1 -4
  45. package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
  46. package/src/db/__tests__/big-int-field.test.ts +15 -14
  47. package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
  48. package/src/db/__tests__/compound-types.test.ts +1 -1
  49. package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
  50. package/src/db/__tests__/connection-options.test.ts +1 -1
  51. package/src/db/__tests__/cursor.test.ts +8 -32
  52. package/src/db/__tests__/dialect-instant.test.ts +1 -1
  53. package/src/db/__tests__/encryption.test.ts +1 -1
  54. package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
  55. package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
  56. package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
  57. package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
  58. package/src/db/__tests__/located-timestamp.test.ts +1 -1
  59. package/src/db/__tests__/migrate-generator.test.ts +71 -0
  60. package/src/db/__tests__/migrate-runner.test.ts +19 -0
  61. package/src/db/__tests__/money.test.ts +1 -1
  62. package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
  63. package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
  64. package/src/db/__tests__/pg-error.test.ts +43 -0
  65. package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
  66. package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
  67. package/src/db/__tests__/sql-inventory.test.ts +56 -0
  68. package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
  69. package/src/db/__tests__/table-builder-required.test.ts +20 -22
  70. package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
  71. package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
  72. package/src/db/api.ts +46 -0
  73. package/src/db/apply-entity-event.ts +45 -36
  74. package/src/db/assert-exists-in.ts +5 -16
  75. package/src/db/bun-provider.ts +37 -0
  76. package/src/db/config-seed.ts +4 -4
  77. package/src/db/connection.ts +14 -57
  78. package/src/db/cursor.ts +5 -56
  79. package/src/db/dialect.ts +472 -99
  80. package/src/db/eagerload.ts +5 -12
  81. package/src/db/entity-table-meta.ts +390 -0
  82. package/src/db/event-store-executor.ts +158 -100
  83. package/src/db/index.ts +33 -5
  84. package/src/db/migrate-generator.ts +350 -0
  85. package/src/db/migrate-runner.ts +206 -0
  86. package/src/db/postgres-provider.ts +25 -0
  87. package/src/db/queries/entity-read.ts +15 -0
  88. package/src/db/queries/es-ops.ts +17 -0
  89. package/src/db/queries/event-consumer.ts +170 -0
  90. package/src/db/queries/event-store-admin.ts +127 -0
  91. package/src/db/queries/event-store.ts +155 -0
  92. package/src/db/queries/projection-rebuild.ts +59 -0
  93. package/src/db/queries/raw-sql.ts +15 -0
  94. package/src/db/queries/schema-drift.ts +35 -0
  95. package/src/db/queries/seed-context.ts +58 -0
  96. package/src/db/queries/table-ops.ts +11 -0
  97. package/src/db/queries/test-stack.ts +56 -0
  98. package/src/db/query-api.ts +22 -0
  99. package/src/db/query.ts +30 -0
  100. package/src/db/reference-data.ts +19 -22
  101. package/src/db/render-ddl.ts +57 -0
  102. package/src/db/row-helpers.ts +3 -52
  103. package/src/db/schema-inspection.ts +17 -4
  104. package/src/db/sql-inventory.ts +208 -0
  105. package/src/db/table-builder.ts +54 -46
  106. package/src/db/tenant-db.ts +105 -326
  107. package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
  108. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
  109. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
  110. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
  111. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
  112. package/src/engine/__tests__/boot-validator.test.ts +4 -3
  113. package/src/engine/__tests__/build-app-schema.test.ts +1 -1
  114. package/src/engine/__tests__/build-target.test.ts +1 -1
  115. package/src/engine/__tests__/claim-keys.test.ts +1 -1
  116. package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
  117. package/src/engine/__tests__/config-helpers.test.ts +1 -1
  118. package/src/engine/__tests__/duration-utils.test.ts +16 -0
  119. package/src/engine/__tests__/effective-features.test.ts +1 -1
  120. package/src/engine/__tests__/engine.test.ts +1 -1
  121. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  122. package/src/engine/__tests__/event-helpers.test.ts +3 -3
  123. package/src/engine/__tests__/extends-registrar.test.ts +4 -4
  124. package/src/engine/__tests__/factories-long-text.test.ts +1 -1
  125. package/src/engine/__tests__/factories-time.test.ts +1 -1
  126. package/src/engine/__tests__/field-access.test.ts +38 -0
  127. package/src/engine/__tests__/field-predicates.test.ts +1 -1
  128. package/src/engine/__tests__/hook-phases.test.ts +1 -1
  129. package/src/engine/__tests__/identifiers.test.ts +1 -1
  130. package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
  131. package/src/engine/__tests__/nav.test.ts +1 -1
  132. package/src/engine/__tests__/no-return-guard.test.ts +17 -0
  133. package/src/engine/__tests__/ownership.test.ts +10 -11
  134. package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
  135. package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
  136. package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
  137. package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
  138. package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
  139. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
  140. package/src/engine/__tests__/post-query-hook.test.ts +1 -1
  141. package/src/engine/__tests__/projection-helpers.test.ts +25 -17
  142. package/src/engine/__tests__/projection.test.ts +4 -4
  143. package/src/engine/__tests__/qualified-name.test.ts +1 -1
  144. package/src/engine/__tests__/raw-table.test.ts +9 -8
  145. package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
  146. package/src/engine/__tests__/run-in.test.ts +1 -1
  147. package/src/engine/__tests__/schema-builder.test.ts +1 -1
  148. package/src/engine/__tests__/screen.test.ts +1 -1
  149. package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
  150. package/src/engine/__tests__/state-machine.test.ts +1 -1
  151. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
  152. package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
  153. package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
  154. package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
  155. package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
  156. package/src/engine/__tests__/steps-read.test.ts +34 -40
  157. package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
  158. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
  159. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
  160. package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
  161. package/src/engine/__tests__/steps-workflow.test.ts +7 -7
  162. package/src/engine/__tests__/system-user.test.ts +1 -1
  163. package/src/engine/__tests__/unmanaged-table.test.ts +98 -0
  164. package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
  165. package/src/engine/__tests__/validation-hooks.test.ts +1 -1
  166. package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
  167. package/src/engine/boot-validator/entity-handler.ts +3 -3
  168. package/src/engine/boot-validator/ownership.ts +1 -1
  169. package/src/engine/define-feature.ts +37 -2
  170. package/src/engine/entity-handlers.ts +5 -5
  171. package/src/engine/factories.ts +1 -1
  172. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
  173. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
  174. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
  175. package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
  176. package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
  177. package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
  178. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
  179. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
  180. package/src/engine/feature-ast/extractors/shared.ts +2 -3
  181. package/src/engine/ownership.ts +113 -41
  182. package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
  183. package/src/engine/projection-helpers.ts +2 -11
  184. package/src/engine/registry.ts +21 -2
  185. package/src/engine/steps/read-find-many.ts +13 -13
  186. package/src/engine/steps/read-find-one.ts +7 -9
  187. package/src/engine/steps/unsafe-projection-delete.ts +4 -5
  188. package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
  189. package/src/engine/types/feature.ts +47 -2
  190. package/src/engine/types/fields.ts +4 -5
  191. package/src/engine/types/index.ts +2 -0
  192. package/src/engine/types/step.ts +10 -10
  193. package/src/engine/validate-projection-allowlist.ts +23 -3
  194. package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
  195. package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
  196. package/src/env/__tests__/compose-env-schema.test.ts +1 -1
  197. package/src/env/__tests__/dry-run.test.ts +1 -1
  198. package/src/errors/__tests__/classes.test.ts +1 -1
  199. package/src/errors/__tests__/error-helpers.test.ts +44 -0
  200. package/src/errors/__tests__/field-issue-compat.test.ts +16 -0
  201. package/src/errors/__tests__/write-failures.test.ts +1 -1
  202. package/src/errors/classes.ts +5 -19
  203. package/src/errors/field-issue.ts +11 -0
  204. package/src/errors/index.ts +1 -0
  205. package/src/errors/zod-bridge.ts +3 -2
  206. package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
  207. package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
  208. package/src/es-ops/__tests__/runner.test.ts +29 -19
  209. package/src/es-ops/context.ts +11 -56
  210. package/src/es-ops/operations-schema.ts +2 -2
  211. package/src/es-ops/runner.ts +12 -26
  212. package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
  213. package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
  214. package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
  215. package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
  216. package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
  217. package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
  218. package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
  219. package/src/event-store/admin-api.ts +55 -83
  220. package/src/event-store/archive.ts +15 -39
  221. package/src/event-store/event-store.ts +92 -86
  222. package/src/event-store/events-schema.ts +2 -1
  223. package/src/event-store/index.ts +1 -0
  224. package/src/event-store/snapshot.ts +26 -24
  225. package/src/event-store/upcaster-dead-letter.ts +19 -18
  226. package/src/files/__tests__/content-disposition.test.ts +1 -1
  227. package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
  228. package/src/files/__tests__/file-handle.test.ts +1 -1
  229. package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
  230. package/src/files/__tests__/read-stream.test.ts +1 -1
  231. package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
  232. package/src/files/__tests__/write-stream.test.ts +1 -1
  233. package/src/files/__tests__/zip-stream.test.ts +1 -1
  234. package/src/files/file-ref-table.ts +2 -2
  235. package/src/files/file-routes.ts +7 -9
  236. package/src/files/storage-tracking.ts +9 -17
  237. package/src/i18n/__tests__/i18n.test.ts +1 -1
  238. package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
  239. package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
  240. package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
  241. package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
  242. package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
  243. package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
  244. package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
  245. package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
  246. package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
  247. package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
  248. package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
  249. package/src/migrations/projection-detection.ts +12 -1
  250. package/src/migrations/schema-drift.ts +7 -23
  251. package/src/observability/__tests__/console-provider.test.ts +1 -1
  252. package/src/observability/__tests__/metric-validator.test.ts +1 -1
  253. package/src/observability/__tests__/noop-provider.test.ts +1 -1
  254. package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
  255. package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
  256. package/src/observability/__tests__/recording-meter.test.ts +1 -1
  257. package/src/observability/__tests__/recording-tracer.test.ts +1 -1
  258. package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
  259. package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
  260. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
  261. package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
  262. package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
  263. package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
  264. package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
  265. package/src/pipeline/__tests__/dispatcher-utils.test.ts +107 -0
  266. package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
  267. package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
  268. package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
  269. package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
  270. package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
  271. package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
  272. package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
  273. package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
  274. package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
  275. package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
  276. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
  277. package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
  278. package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
  279. package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
  280. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
  281. package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
  282. package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
  283. package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
  284. package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
  285. package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
  286. package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
  287. package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
  288. package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
  289. package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
  290. package/src/pipeline/__tests__/redis-keys.test.ts +12 -0
  291. package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
  292. package/src/pipeline/cascade-handler.ts +13 -21
  293. package/src/pipeline/dispatcher-utils.ts +8 -7
  294. package/src/pipeline/dispatcher.ts +43 -48
  295. package/src/pipeline/event-consumer-state.ts +11 -2
  296. package/src/pipeline/event-dispatcher.ts +86 -146
  297. package/src/pipeline/event-retention.ts +14 -24
  298. package/src/pipeline/msp-rebuild.ts +54 -78
  299. package/src/pipeline/projection-rebuild.ts +65 -67
  300. package/src/pipeline/projection-state.ts +2 -2
  301. package/src/random/__tests__/generate.test.ts +13 -13
  302. package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
  303. package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
  304. package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
  305. package/src/redis/__tests__/redis-options.test.ts +1 -1
  306. package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
  307. package/src/search/__tests__/search-adapter.test.ts +1 -1
  308. package/src/secrets/__tests__/dek-cache.test.ts +1 -3
  309. package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
  310. package/src/secrets/__tests__/envelope.test.ts +1 -1
  311. package/src/secrets/__tests__/leak-guard.test.ts +1 -1
  312. package/src/secrets/__tests__/rotation.test.ts +1 -1
  313. package/src/stack/db.ts +25 -48
  314. package/src/stack/push-entity-projection-tables.ts +2 -4
  315. package/src/stack/table-helpers.ts +98 -61
  316. package/src/stack/test-stack.ts +10 -9
  317. package/src/testing/__tests__/db-cleanup.test.ts +40 -0
  318. package/src/testing/__tests__/e2e-generator.test.ts +1 -1
  319. package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
  320. package/src/testing/db-cleanup.ts +44 -0
  321. package/src/testing/expect-error.ts +1 -1
  322. package/src/testing/index.ts +2 -0
  323. package/src/testing/multipart-helper.ts +94 -0
  324. package/src/testing/shared-entities.ts +5 -5
  325. package/src/time/__tests__/polyfill.test.ts +1 -1
  326. package/src/time/__tests__/tz-context.test.ts +1 -1
  327. package/src/utils/__tests__/assert.test.ts +1 -1
  328. package/src/utils/__tests__/case.test.ts +16 -0
  329. package/src/utils/__tests__/env-parse.test.ts +1 -1
  330. package/src/utils/__tests__/is-plain-object.test.ts +16 -0
  331. package/src/utils/__tests__/parse-string-array-json.test.ts +16 -0
  332. package/src/utils/__tests__/safe-json.test.ts +22 -0
  333. package/src/utils/case.ts +6 -0
  334. package/src/utils/index.ts +3 -0
  335. package/src/utils/is-plain-object.ts +4 -0
  336. package/src/utils/parse-string-array-json.ts +14 -0
  337. package/CHANGELOG.md +0 -474
  338. package/src/db/__tests__/db-helpers.test.ts +0 -369
  339. package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
  340. package/src/db/__tests__/row-helpers.test.ts +0 -59
  341. package/src/engine/steps/_drizzle-boundary.ts +0 -19
  342. package/src/files/__tests__/file-field-column.integration.ts +0 -103
@@ -8,7 +8,7 @@
8
8
  // Unit-level because the check happens before any DB call — no pgClient,
9
9
  // no event-store schema, no setupTestStack needed.
10
10
 
11
- import { describe, expect, test } from "vitest";
11
+ import { describe, expect, test } from "bun:test";
12
12
  import type { AppContext, Registry } from "../../engine/types";
13
13
  import { createEventDispatcher, type EventConsumer } from "../event-dispatcher";
14
14
 
@@ -35,18 +35,20 @@ describe("event-dispatcher — strict runOnce precondition", () => {
35
35
  test("ensureRegistered() is a valid alternative to start() — runOnce no longer throws", async () => {
36
36
  const consumers: EventConsumer[] = [{ name: "noop", handler: async () => {} }];
37
37
  let insertCalls = 0;
38
+ // bun-db path: dispatcher uses db/queries/ for transition guard + transaction().
39
+ // INSERT/SELECT/UPDATE. Match by SQL substring so we count INSERTs and
40
+ // hand back empty rows for SELECT.
41
+ const unsafe = async (sqlText: string): Promise<unknown[]> => {
42
+ if (/INSERT INTO "kumiko_event_consumers"/.test(sqlText)) {
43
+ insertCalls += 1;
44
+ return [];
45
+ }
46
+ return [];
47
+ };
38
48
  const stubDb = {
39
- insert: () => ({
40
- values: () => ({ onConflictDoNothing: async () => ++insertCalls }),
41
- }),
42
- transaction: async (fn: (tx: unknown) => Promise<unknown>) =>
43
- fn({
44
- select: () => ({
45
- from: () => ({ where: () => ({ for: () => [] }) }),
46
- }),
47
- execute: async () => [],
48
- update: () => ({ set: () => ({ where: () => Promise.resolve() }) }),
49
- }),
49
+ unsafe,
50
+ begin: async (fn: (tx: unknown) => Promise<unknown>) => fn(stubDb),
51
+ transaction: async (fn: (tx: unknown) => Promise<unknown>) => fn(stubDb),
50
52
  };
51
53
  const dispatcher = createEventDispatcher({
52
54
  db: stubDb as never,
@@ -13,14 +13,10 @@
13
13
  // production would take once ops wires CreateApp. No createEventDispatcher
14
14
  // calls in the test — only the registry round-trip.
15
15
 
16
- import { sql } from "drizzle-orm";
17
- import { afterEach, beforeAll, describe, expect, test } from "vitest";
18
- import {
19
- integer as drizzleInteger,
20
- table as drizzlePgTable,
21
- uuid as drizzleUuid,
22
- } from "../../db/dialect";
16
+ import { afterEach, beforeAll, describe, expect, test } from "bun:test";
17
+ import { integer, table as pgTable, uuid } from "../../db/dialect";
23
18
  import { createEventStoreExecutor } from "../../db/event-store-executor";
19
+ import { selectMany } from "../../db/query";
24
20
  import { createTenantDb, type TenantDb } from "../../db/tenant-db";
25
21
  import { defineFeature, type FeatureDefinition } from "../../engine";
26
22
  import type { StoredEvent } from "../../event-store";
@@ -40,10 +36,10 @@ import { sharedWidgetEntity, sharedWidgetTable } from "../../testing";
40
36
  // A tiny state table a subscriber mutates so we can observe "the handler was
41
37
  // called with this event" without relying on in-memory arrays — the state row
42
38
  // survives even if the test framework resets process state.
43
- const subscriberLogTable = drizzlePgTable("read_dispatcher_subscriber_log", {
44
- id: drizzleUuid("id").primaryKey().defaultRandom(),
45
- eventId: drizzleInteger("event_id").notNull(),
46
- eventType: drizzleUuid("event_type"), // unused, kept to avoid another drizzle type import
39
+ const subscriberLogTable = pgTable("read_dispatcher_subscriber_log", {
40
+ id: uuid("id").primaryKey().defaultRandom(),
41
+ eventId: integer("event_id").notNull(),
42
+ eventType: uuid("event_type"), // unused, kept to avoid another drizzle type import
47
43
  });
48
44
 
49
45
  // Per-test capture. The subscriber handlers push here; beforeEach resets.
@@ -195,10 +191,7 @@ describe("event-dispatcher — isolation between consumers", () => {
195
191
  // the per-consumer transaction boundary holds.
196
192
  // Pre-registered state rows exist from boot (strict Sprint-E mode) — at
197
193
  // this point they're at cursor=0 / status=idle for both observers.
198
- const state = await stack.db
199
- .select()
200
- .from(eventConsumerStateTable)
201
- .where(sql`${eventConsumerStateTable.name} = ${qnA}`);
194
+ const state = await selectMany(stack.db, eventConsumerStateTable, { name: qnA });
202
195
  expect(state).toHaveLength(1);
203
196
  expect(state[0]?.lastProcessedEventId).toBe(0n);
204
197
 
@@ -10,9 +10,9 @@
10
10
  // 4. olderThanDays / olderThan convenience: both resolve to the same
11
11
  // cutoff semantics (createdAt < cutoff).
12
12
 
13
- import { eq, sql } from "drizzle-orm";
14
- import { afterEach, beforeAll, describe, expect, test } from "vitest";
13
+ import { afterEach, beforeAll, describe, expect, test } from "bun:test";
15
14
  import { createEventStoreExecutor } from "../../db/event-store-executor";
15
+ import { asRawClient, insertOne, selectMany, updateMany } from "../../db/query";
16
16
  import { createTenantDb, type TenantDb } from "../../db/tenant-db";
17
17
  import { defineFeature } from "../../engine";
18
18
  import { eventsTable } from "../../event-store";
@@ -77,20 +77,17 @@ async function seedOldAggregateEvent(
77
77
  type: string,
78
78
  aggregateType = "widget",
79
79
  ): Promise<bigint> {
80
- const [row] = await stack.db
81
- .insert(eventsTable)
82
- .values({
83
- aggregateId: generateId(),
84
- aggregateType,
85
- tenantId: admin.tenantId,
86
- version: 1,
87
- type,
88
- payload: {},
89
- metadata: { userId: admin.id },
90
- createdAt,
91
- createdBy: admin.id,
92
- })
93
- .returning({ id: eventsTable.id });
80
+ const row = await insertOne<{ id: bigint }>(stack.db, eventsTable, {
81
+ aggregateId: generateId(),
82
+ aggregateType,
83
+ tenantId: admin.tenantId,
84
+ version: 1,
85
+ type,
86
+ payload: {},
87
+ metadata: { userId: admin.id },
88
+ createdAt,
89
+ createdBy: admin.id,
90
+ });
94
91
  if (!row) throw new Error("seed failed");
95
92
  return row.id;
96
93
  }
@@ -121,7 +118,7 @@ describe("E.2 — explicit-aggregateTypes pruning", () => {
121
118
  expect(result.deletedCount).toBe(1);
122
119
  expect(result.aggregateTypes).toEqual(["obsolete"]);
123
120
 
124
- const remaining = await stack.db.select().from(eventsTable);
121
+ const remaining = await selectMany(stack.db, eventsTable);
125
122
  const ids = remaining.map((r) => r.id);
126
123
  expect(ids).toContain(widgetId);
127
124
  expect(ids).not.toContain(obsoleteId);
@@ -146,7 +143,7 @@ describe("E.2 — explicit-aggregateTypes pruning", () => {
146
143
  });
147
144
  expect(result.deletedCount).toBe(1);
148
145
 
149
- const remaining = await stack.db.select().from(eventsTable);
146
+ const remaining = await selectMany(stack.db, eventsTable);
150
147
  const ids = remaining.map((r) => r.id).sort();
151
148
  expect(ids).toEqual([freshId]);
152
149
  expect(ids.includes(staleId)).toBe(false);
@@ -165,7 +162,7 @@ describe("E.2 — explicit-aggregateTypes pruning", () => {
165
162
  expect(result.deletedCount).toBe(1);
166
163
  expect(result.dryRun).toBe(true);
167
164
 
168
- const remaining = await stack.db.select().from(eventsTable);
165
+ const remaining = await selectMany(stack.db, eventsTable);
169
166
  expect(remaining).toHaveLength(1);
170
167
  });
171
168
  });
@@ -182,13 +179,17 @@ describe("E.2 — consumer-lag guard", () => {
182
179
  // Only let the first one through.
183
180
  await stack.eventDispatcher?.runOnce();
184
181
  // Force cursor to 1 so the guard sees "consumer at 1, max candidate 3".
185
- await stack.db
186
- .update(eventConsumerStateTable)
187
- .set({ lastProcessedEventId: 1n, status: "idle" })
188
- .where(eq(eventConsumerStateTable.name, observerQn));
182
+ await updateMany(
183
+ stack.db,
184
+ eventConsumerStateTable,
185
+ { lastProcessedEventId: 1n, status: "idle" },
186
+ { name: observerQn },
187
+ );
189
188
 
190
189
  // Age all three events past the cutoff.
191
- await stack.db.execute(sql`UPDATE kumiko_events SET created_at = now() - interval '30 days'`);
190
+ await asRawClient(stack.db).unsafe(
191
+ `UPDATE kumiko_events SET created_at = now() - interval '30 days'`,
192
+ );
192
193
 
193
194
  await expect(
194
195
  pruneEvents(stack.db, {
@@ -204,7 +205,9 @@ describe("E.2 — consumer-lag guard", () => {
204
205
  await disableConsumer(stack.db, observerQn);
205
206
 
206
207
  // Cursor is at 1 but consumer is disabled — should be skipped.
207
- await stack.db.execute(sql`UPDATE kumiko_events SET created_at = now() - interval '30 days'`);
208
+ await asRawClient(stack.db).unsafe(
209
+ `UPDATE kumiko_events SET created_at = now() - interval '30 days'`,
210
+ );
208
211
 
209
212
  const result = await pruneEvents(stack.db, {
210
213
  olderThanDays: 7,
@@ -7,11 +7,11 @@
7
7
  // the stream version internally — a sequence of appendOne calls writes
8
8
  // consecutive versions without re-reading the DB.
9
9
 
10
- import { sql } from "drizzle-orm";
11
- import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
10
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
12
11
  import { z } from "zod";
13
12
  import { createEventStoreExecutor } from "../../db/event-store-executor";
14
- import { buildDrizzleTable } from "../../db/table-builder";
13
+ import { asRawClient } from "../../db/query-api";
14
+ import { buildEntityTable } from "../../db/table-builder";
15
15
  import { createEntity, createTextField, defineFeature } from "../../engine";
16
16
  import { UnprocessableError, writeFailure } from "../../errors";
17
17
  import { loadAggregate } from "../../event-store";
@@ -26,7 +26,7 @@ const cartEntity = createEntity({
26
26
  },
27
27
  });
28
28
 
29
- const cartTable = buildDrizzleTable("f4wCart", cartEntity);
29
+ const cartTable = buildEntityTable("f4wCart", cartEntity);
30
30
 
31
31
  const cartFeature = defineFeature("f4w", (r) => {
32
32
  r.entity("f4wCart", cartEntity);
@@ -144,8 +144,8 @@ afterAll(async () => {
144
144
  });
145
145
 
146
146
  afterEach(async () => {
147
- await stack.db.execute(
148
- sql`TRUNCATE kumiko_events, read_f4w_carts, kumiko_event_consumers RESTART IDENTITY CASCADE`,
147
+ await asRawClient(stack.db).unsafe(
148
+ `TRUNCATE kumiko_events, read_f4w_carts, kumiko_event_consumers RESTART IDENTITY CASCADE`,
149
149
  );
150
150
  await stack.eventDispatcher?.ensureRegistered();
151
151
  });
@@ -1,4 +1,4 @@
1
- import { describe, expect, test, vi } from "vitest";
1
+ import { describe, expect, spyOn, test } from "bun:test";
2
2
  import { z } from "zod";
3
3
  import {
4
4
  createEntity,
@@ -200,7 +200,7 @@ describe("runPostSave", () => {
200
200
 
201
201
  test("postSave errors don't throw — logged and continued", async () => {
202
202
  const calls: string[] = [];
203
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
203
+ const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
204
204
 
205
205
  const registry = makeRegistry({
206
206
  postSave: [
@@ -234,7 +234,7 @@ describe("runPostSave", () => {
234
234
 
235
235
  test("system hook failure doesn't block other system hooks", async () => {
236
236
  const calls: string[] = [];
237
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
237
+ const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
238
238
 
239
239
  const registry = makeRegistry();
240
240
 
@@ -334,7 +334,7 @@ describe("runPostSave phase routing", () => {
334
334
  });
335
335
 
336
336
  test("afterCommit phase: hook errors are logged, never thrown", async () => {
337
- const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
337
+ const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
338
338
  const afterRan: string[] = [];
339
339
  const feature = defineFeature("phases", (r) => {
340
340
  r.entity("user", createEntity({ table: "Users", fields: {} }));
@@ -5,11 +5,15 @@
5
5
  // returned events into whatever domain-state shape the feature wants.
6
6
  // Events are upcasted by the dispatcher, so the reducer sees the current
7
7
  // payload shape even for old v1 events.
8
+ //
9
+ // Bun.SQL-only setup. KEIN postgres-js, KEIN setupTestStack.
8
10
 
9
- import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
11
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
10
12
  import { z } from "zod";
13
+ import type { DbConnection, DbTx } from "../../db/connection";
11
14
  import { createEventStoreExecutor } from "../../db/event-store-executor";
12
- import { buildDrizzleTable } from "../../db/table-builder";
15
+ import { insertOne } from "../../db/query";
16
+ import { buildEntityTable } from "../../db/table-builder";
13
17
  import { createEntity, createTextField, defineFeature } from "../../engine";
14
18
  import { append, loadAggregate as loadAggregateRaw } from "../../event-store";
15
19
  import {
@@ -29,7 +33,7 @@ const invoiceEntity = createEntity({
29
33
  status: createTextField({ required: true }),
30
34
  },
31
35
  });
32
- const invoiceTable = buildDrizzleTable("asof-invoice", invoiceEntity);
36
+ const invoiceTable = buildEntityTable("asof-invoice", invoiceEntity);
33
37
 
34
38
  // --- Feature ---
35
39
 
@@ -214,8 +218,8 @@ describe("ctx.loadAggregate via queryHandler — Marten AggregateStreamAsync equ
214
218
  // against v2 (integer cents) — without upcasting it would blow up or
215
219
  // produce garbage.
216
220
  const invoiceId = "00000000-0000-4000-8000-000000000042";
217
- await stack.db.transaction(async (tx) => {
218
- await tx.insert(invoiceTable).values({
221
+ await (stack.db as DbConnection).begin(async (tx: DbTx) => {
222
+ await insertOne(tx, invoiceTable, {
219
223
  id: invoiceId,
220
224
  tenantId: admin.tenantId,
221
225
  customer: "LegacyCo",
@@ -9,7 +9,7 @@
9
9
  // error on the skip counter, advances the cursor, and keeps delivering. The
10
10
  // consumer stays "idle".
11
11
 
12
- import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
12
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
13
13
  import { createEventStoreExecutor } from "../../db/event-store-executor";
14
14
  import { createTenantDb, type TenantDb } from "../../db/tenant-db";
15
15
  import { defineFeature } from "../../engine";
@@ -10,10 +10,11 @@
10
10
  // a three-hop causal chain (order.placed → order.confirmed → order.shipped).
11
11
  // 5. ctx.loadAggregate reads the triggering stream's full history.
12
12
 
13
- import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
13
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
14
14
  import { z } from "zod";
15
15
  import { createEventStoreExecutor } from "../../db/event-store-executor";
16
- import { buildDrizzleTable } from "../../db/table-builder";
16
+ import { selectMany } from "../../db/query";
17
+ import { buildEntityTable } from "../../db/table-builder";
17
18
  import { createEntity, createTextField, defineFeature } from "../../engine";
18
19
  import { eventsTable } from "../../event-store";
19
20
  import {
@@ -33,7 +34,7 @@ const orderEntity = createEntity({
33
34
  },
34
35
  });
35
36
 
36
- const orderTable = buildDrizzleTable("mmh-order", orderEntity);
37
+ const orderTable = buildEntityTable("mmh-order", orderEntity);
37
38
 
38
39
  // Snapshot what each MSP-apply observed via ctx.loadAggregate.
39
40
  const confirmLoadCounts: number[] = [];
@@ -132,7 +133,7 @@ async function postWrite(correlationId: string, item: string) {
132
133
  async function drainUntilShipped(aggregateId: string, maxPasses = 10): Promise<void> {
133
134
  for (let i = 0; i < maxPasses; i++) {
134
135
  await stack.eventDispatcher?.runOnce();
135
- const rows = await stack.db.select().from(eventsTable);
136
+ const rows = await selectMany(stack.db, eventsTable);
136
137
  if (rows.some((r) => r.aggregateId === aggregateId && r.type === "mmh:event:shipped")) return;
137
138
  }
138
139
  throw new Error(`drainUntilShipped: never saw shipped event for ${aggregateId}`);
@@ -147,7 +148,7 @@ describe("Runde 3 / C.2b — MSP-apply ctx cascades", () => {
147
148
 
148
149
  await stack.eventDispatcher?.runOnce();
149
150
 
150
- const rows = await stack.db.select().from(eventsTable);
151
+ const rows = await selectMany(stack.db, eventsTable);
151
152
  const types = rows.map((r) => r.type).sort();
152
153
  // Order: CRUD create, placed, confirmed (hop 1 fired).
153
154
  expect(types).toContain("mmh-order.created");
@@ -160,7 +161,7 @@ describe("Runde 3 / C.2b — MSP-apply ctx cascades", () => {
160
161
 
161
162
  // Find the aggregateId from the first placed event.
162
163
  await stack.eventDispatcher?.runOnce();
163
- const placedRow = (await stack.db.select().from(eventsTable)).find(
164
+ const placedRow = (await selectMany(stack.db, eventsTable)).find(
164
165
  (r) => r.type === "mmh:event:placed",
165
166
  );
166
167
  expect(placedRow).toBeDefined();
@@ -168,7 +169,7 @@ describe("Runde 3 / C.2b — MSP-apply ctx cascades", () => {
168
169
 
169
170
  await drainUntilShipped(aggregateId);
170
171
 
171
- const rows = (await stack.db.select().from(eventsTable))
172
+ const rows = (await selectMany(stack.db, eventsTable))
172
173
  .filter((r) => r.aggregateId === aggregateId)
173
174
  .sort((a, b) => Number(a.id - b.id));
174
175
 
@@ -220,7 +221,7 @@ describe("Runde 3 / C.2b — MSP-apply ctx cascades", () => {
220
221
  await stack.eventDispatcher?.runOnce();
221
222
 
222
223
  // Every event written on this chain belongs to the admin's tenant.
223
- const rows = await stack.db.select().from(eventsTable);
224
+ const rows = await selectMany(stack.db, eventsTable);
224
225
  for (const row of rows) {
225
226
  expect(row.tenantId).toBe(admin.tenantId);
226
227
  }
@@ -15,12 +15,13 @@
15
15
  // carry a different state row (kumiko_event_consumers, not kumiko_projections)
16
16
  // and a different apply signature (3rd ctx arg).
17
17
 
18
- import { eq, sql } from "drizzle-orm";
19
- import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
18
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
19
+ import { sql } from "@cosmicdrift/kumiko-framework/db";
20
20
  import { z } from "zod";
21
21
  import { integer as pgInteger, table as pgTable, uuid as pgUuid } from "../../db/dialect";
22
22
  import { createEventStoreExecutor } from "../../db/event-store-executor";
23
- import { buildDrizzleTable } from "../../db/table-builder";
23
+ import { asRawClient, selectMany, updateMany } from "../../db/query";
24
+ import { buildEntityTable } from "../../db/table-builder";
24
25
  import { createEntity, createTextField, defineFeature } from "../../engine";
25
26
  import {
26
27
  eventConsumerStateTable,
@@ -41,13 +42,13 @@ const invoiceEntity = createEntity({
41
42
  table: "read_mspreb_invoices",
42
43
  fields: { customer: createTextField({ required: true }) },
43
44
  });
44
- const invoiceTable = buildDrizzleTable("msp-reb-invoice", invoiceEntity);
45
+ const invoiceTable = buildEntityTable("msp-reb-invoice", invoiceEntity);
45
46
 
46
47
  const paymentEntity = createEntity({
47
48
  table: "read_mspreb_payments",
48
49
  fields: { customer: createTextField({ required: true }) },
49
50
  });
50
- const paymentTable = buildDrizzleTable("msp-reb-payment", paymentEntity);
51
+ const paymentTable = buildEntityTable("msp-reb-payment", paymentEntity);
51
52
 
52
53
  // Main read-model: running balance per customer.
53
54
  const balanceTable = pgTable("read_mspreb_balance", {
@@ -88,33 +89,17 @@ const feature = defineFeature("mspreb", (r) => {
88
89
  apply: {
89
90
  [invoiceBilled.name]: async (event, tx) => {
90
91
  const p = event.payload as { customer: string; cents: number };
91
- await tx
92
- .insert(balanceTable)
93
- .values({
94
- customer: p.customer,
95
- tenantId: event.tenantId,
96
- invoicesCents: p.cents,
97
- paymentsCents: 0,
98
- })
99
- .onConflictDoUpdate({
100
- target: balanceTable.customer,
101
- set: { invoicesCents: sql`${balanceTable.invoicesCents} + ${p.cents}` },
102
- });
92
+ await asRawClient(tx).unsafe(
93
+ `INSERT INTO "read_mspreb_balance" (customer, tenant_id, invoices_cents, payments_cents) VALUES ($1::uuid, $2::uuid, $3, 0) ON CONFLICT (customer) DO UPDATE SET invoices_cents = read_mspreb_balance.invoices_cents + $3`,
94
+ [p.customer, event.tenantId, p.cents],
95
+ );
103
96
  },
104
97
  [paymentReceived.name]: async (event, tx) => {
105
98
  const p = event.payload as { customer: string; cents: number };
106
- await tx
107
- .insert(balanceTable)
108
- .values({
109
- customer: p.customer,
110
- tenantId: event.tenantId,
111
- invoicesCents: 0,
112
- paymentsCents: p.cents,
113
- })
114
- .onConflictDoUpdate({
115
- target: balanceTable.customer,
116
- set: { paymentsCents: sql`${balanceTable.paymentsCents} + ${p.cents}` },
117
- });
99
+ await asRawClient(tx).unsafe(
100
+ `INSERT INTO "read_mspreb_balance" (customer, tenant_id, invoices_cents, payments_cents) VALUES ($1::uuid, $2::uuid, 0, $3) ON CONFLICT (customer) DO UPDATE SET payments_cents = read_mspreb_balance.payments_cents + $3`,
101
+ [p.customer, event.tenantId, p.cents],
102
+ );
118
103
  },
119
104
  },
120
105
  });
@@ -248,10 +233,12 @@ describe("rebuildMultiStreamProjection — rebuildable read-model", () => {
248
233
  // Disable the saga MSP for this test — it runs on the same event types
249
234
  // and would trip its own ctx.appendEvent path during live delivery
250
235
  // (which is fine in production, but noise here).
251
- await stack.db
252
- .update(eventConsumerStateTable)
253
- .set({ status: "disabled", updatedAt: sql`now()` })
254
- .where(eq(eventConsumerStateTable.name, SAGA_MSP));
236
+ await updateMany(
237
+ stack.db,
238
+ eventConsumerStateTable,
239
+ { status: "disabled", updatedAt: sql`now()` },
240
+ { name: SAGA_MSP },
241
+ );
255
242
 
256
243
  await stack.http.writeOk("mspreb:write:invoice:bill", { customer: alice, cents: 10_00 }, admin);
257
244
  await stack.http.writeOk("mspreb:write:invoice:bill", { customer: alice, cents: 5_00 }, admin);
@@ -263,7 +250,7 @@ describe("rebuildMultiStreamProjection — rebuildable read-model", () => {
263
250
  await stack.http.writeOk("mspreb:write:invoice:bill", { customer: bob, cents: 7_50 }, admin);
264
251
  await runFullDispatcher();
265
252
 
266
- const liveRows = await stack.db.select().from(balanceTable).orderBy(balanceTable.customer);
253
+ const liveRows = await selectMany(stack.db, balanceTable);
267
254
  const aliceLive = liveRows.find((r) => r.customer === alice);
268
255
  const bobLive = liveRows.find((r) => r.customer === bob);
269
256
  expect(aliceLive).toMatchObject({ invoicesCents: 15_00, paymentsCents: 3_00 });
@@ -279,7 +266,7 @@ describe("rebuildMultiStreamProjection — rebuildable read-model", () => {
279
266
  expect(result.eventsProcessed).toBe(4); // 2 invoices + 1 payment + 1 invoice
280
267
  expect(result.lastProcessedEventId).toBeGreaterThan(0n);
281
268
 
282
- const rebuiltRows = await stack.db.select().from(balanceTable).orderBy(balanceTable.customer);
269
+ const rebuiltRows = await selectMany(stack.db, balanceTable);
283
270
  expect(rebuiltRows).toEqual(liveRows);
284
271
 
285
272
  // Consumer cursor is at head after rebuild — the live dispatcher should
@@ -291,28 +278,29 @@ describe("rebuildMultiStreamProjection — rebuildable read-model", () => {
291
278
 
292
279
  test("rebuild after table corruption restores the correct state", async () => {
293
280
  const carol = "00000000-0000-4000-8000-000000000c03";
294
- await stack.db
295
- .update(eventConsumerStateTable)
296
- .set({ status: "disabled", updatedAt: sql`now()` })
297
- .where(eq(eventConsumerStateTable.name, SAGA_MSP));
281
+ await updateMany(
282
+ stack.db,
283
+ eventConsumerStateTable,
284
+ { status: "disabled", updatedAt: sql`now()` },
285
+ { name: SAGA_MSP },
286
+ );
298
287
  await stack.http.writeOk("mspreb:write:invoice:bill", { customer: carol, cents: 42_00 }, admin);
299
288
  await runFullDispatcher();
300
289
 
301
290
  // Corrupt the read-model — simulate a buggy apply() landing bad numbers.
302
- await stack.db
303
- .update(balanceTable)
304
- .set({ invoicesCents: -999, paymentsCents: 999 })
305
- .where(eq(balanceTable.customer, carol));
291
+ await updateMany(
292
+ stack.db,
293
+ balanceTable,
294
+ { invoicesCents: -999, paymentsCents: 999 },
295
+ { customer: carol },
296
+ );
306
297
 
307
298
  await rebuildMultiStreamProjection(BALANCE_MSP, {
308
299
  db: stack.db,
309
300
  registry: stack.registry,
310
301
  });
311
302
 
312
- const [row] = await stack.db
313
- .select()
314
- .from(balanceTable)
315
- .where(eq(balanceTable.customer, carol));
303
+ const [row] = await selectMany(stack.db, balanceTable, { customer: carol });
316
304
  expect(row).toMatchObject({ invoicesCents: 42_00, paymentsCents: 0 });
317
305
  });
318
306
  });
@@ -339,18 +327,22 @@ describe("rebuildMultiStreamProjection — guard rails", () => {
339
327
  test("saga MSP using ctx.appendEvent fails rebuild at the first appendEvent call", async () => {
340
328
  const dave = "00000000-0000-4000-8000-000000000d04";
341
329
  // Disable the saga in live passes so we control when the apply runs.
342
- await stack.db
343
- .update(eventConsumerStateTable)
344
- .set({ status: "disabled", updatedAt: sql`now()` })
345
- .where(eq(eventConsumerStateTable.name, SAGA_MSP));
330
+ await updateMany(
331
+ stack.db,
332
+ eventConsumerStateTable,
333
+ { status: "disabled", updatedAt: sql`now()` },
334
+ { name: SAGA_MSP },
335
+ );
346
336
  await stack.http.writeOk("mspreb:write:invoice:bill", { customer: dave, cents: 1_00 }, admin);
347
337
  // Put the consumer back to idle so rebuild doesn't treat it as "just
348
338
  // disabled on purpose" — rebuild is opinionated about WHEN it refuses,
349
339
  // not about the consumer's live-status.
350
- await stack.db
351
- .update(eventConsumerStateTable)
352
- .set({ status: "idle", updatedAt: sql`now()` })
353
- .where(eq(eventConsumerStateTable.name, SAGA_MSP));
340
+ await updateMany(
341
+ stack.db,
342
+ eventConsumerStateTable,
343
+ { status: "idle", updatedAt: sql`now()` },
344
+ { name: SAGA_MSP },
345
+ );
354
346
 
355
347
  await expect(
356
348
  rebuildMultiStreamProjection(SAGA_MSP, {