@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
@@ -1,7 +1,8 @@
1
- import { type AnyColumn, eq } from "drizzle-orm";
2
1
  import { requestContext } from "../api/request-context";
3
2
  import type { DbConnection, DbRow, DbTx } from "../db/connection";
4
- import { buildDrizzleTable } from "../db/table-builder";
3
+ import { selectRowForUpdateById } from "../db/queries/entity-read";
4
+ import { selectMany, transaction } from "../db/query";
5
+ import { buildEntityTable, toSnakeCase } from "../db/table-builder";
5
6
  import { createTenantDb } from "../db/tenant-db";
6
7
  import { hasAccess } from "../engine/access";
7
8
  import { checkWriteFieldRoles, filterReadFields } from "../engine/field-access";
@@ -173,15 +174,22 @@ export function createDispatcher(
173
174
  ): Dispatcher {
174
175
  const { idempotency, lifecycle, jobRunner, effectiveFeatures } = options;
175
176
 
177
+ // Narrowing-helper: AppContext.db ist DbConnection|TenantDb|undefined. Die
178
+ // dispatch-Pfade brauchen DbConnection (oder DbTx aus Caller-Scope) für
179
+ // appendEvent/projection-writes; TenantDb-Branch wird hier ausgeschlossen.
180
+ function resolveDbSource(tx: DbTx | undefined): DbConnection | DbTx | undefined {
181
+ return tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
182
+ }
183
+
176
184
  // Pre-build tables and transition maps for auto-guard (avoid per-request allocation)
177
- const tableCache = new Map<string, ReturnType<typeof buildDrizzleTable>>();
185
+ const tableCache = new Map<string, ReturnType<typeof buildEntityTable>>();
178
186
  const transitionCache = new Map<string, ReturnType<typeof defineTransitions>>();
179
187
 
180
- function getTable(entityName: string): ReturnType<typeof buildDrizzleTable> | undefined {
188
+ function getTable(entityName: string): ReturnType<typeof buildEntityTable> | undefined {
181
189
  if (tableCache.has(entityName)) return tableCache.get(entityName);
182
190
  const entity = registry.getEntity(entityName);
183
191
  if (!entity) return undefined;
184
- const table = buildDrizzleTable(entityName, entity, {
192
+ const table = buildEntityTable(entityName, entity, {
185
193
  relations: registry.getRelations(entityName),
186
194
  });
187
195
  tableCache.set(entityName, table);
@@ -215,8 +223,7 @@ export function createDispatcher(
215
223
  tx: DbTx | undefined,
216
224
  callerFeature: string | undefined,
217
225
  ): Promise<void> {
218
- const dbSource: DbConnection | DbTx | undefined =
219
- tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
226
+ const dbSource = resolveDbSource(tx);
220
227
  if (!dbSource) {
221
228
  throw new InternalError({
222
229
  message: `ctx.appendEvent("${args.type}") requires a database connection — none is configured.`,
@@ -245,8 +252,7 @@ export function createDispatcher(
245
252
  // The outer dispatcher receives a DbConnection from the server/stack;
246
253
  // AppContext's `db` union also allows TenantDb (for downstream hook calls),
247
254
  // but at this point we're the root of the pipeline — cast is safe.
248
- const dbSource: DbConnection | DbTx | undefined =
249
- tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
255
+ const dbSource = resolveDbSource(tx);
250
256
  const reqCtx = requestContext.get();
251
257
  const db = dbSource
252
258
  ? createTenantDb(
@@ -313,8 +319,7 @@ export function createDispatcher(
313
319
  await appendDomainEvent(args, user, tx, registry.getHandlerFeature(type));
314
320
  },
315
321
  fetchForWriting: async (args: FetchForWritingArgs): Promise<AggregateStreamHandle> => {
316
- const dbSource: DbConnection | DbTx | undefined =
317
- tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
322
+ const dbSource = resolveDbSource(tx);
318
323
  if (!dbSource) {
319
324
  throw new InternalError({
320
325
  message: `ctx.fetchForWriting("${args.aggregateId}") requires a database connection — none is configured.`,
@@ -377,8 +382,7 @@ export function createDispatcher(
377
382
  aggregateId: string,
378
383
  loadOptions?: { readonly asOf?: Temporal.Instant },
379
384
  ): Promise<readonly StoredEvent[]> => {
380
- const dbSource: DbConnection | DbTx | undefined =
381
- tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
385
+ const dbSource = resolveDbSource(tx);
382
386
  if (!dbSource) {
383
387
  throw new InternalError({
384
388
  message: `ctx.loadAggregate("${aggregateId}") requires a database connection — none is configured.`,
@@ -396,8 +400,7 @@ export function createDispatcher(
396
400
  aggregateId: string,
397
401
  archiveArgs: { readonly aggregateType: string; readonly reason?: string },
398
402
  ): Promise<void> => {
399
- const dbSource: DbConnection | DbTx | undefined =
400
- tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
403
+ const dbSource = resolveDbSource(tx);
401
404
  if (!dbSource) {
402
405
  throw new InternalError({
403
406
  message: `ctx.archiveStream("${aggregateId}") requires a database connection — none is configured.`,
@@ -412,8 +415,7 @@ export function createDispatcher(
412
415
  });
413
416
  },
414
417
  restoreStream: async (aggregateId: string): Promise<void> => {
415
- const dbSource: DbConnection | DbTx | undefined =
416
- tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
418
+ const dbSource = resolveDbSource(tx);
417
419
  if (!dbSource) {
418
420
  throw new InternalError({
419
421
  message: `ctx.restoreStream("${aggregateId}") requires a database connection — none is configured.`,
@@ -422,8 +424,7 @@ export function createDispatcher(
422
424
  await restoreStreamHelper(dbSource, user.tenantId, aggregateId);
423
425
  },
424
426
  isStreamArchived: async (aggregateId: string): Promise<boolean> => {
425
- const dbSource: DbConnection | DbTx | undefined =
426
- tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
427
+ const dbSource = resolveDbSource(tx);
427
428
  if (!dbSource) {
428
429
  throw new InternalError({
429
430
  message: `ctx.isStreamArchived("${aggregateId}") requires a database connection — none is configured.`,
@@ -437,8 +438,7 @@ export function createDispatcher(
437
438
  readonly version: number;
438
439
  readonly state: Record<string, unknown>;
439
440
  }): Promise<void> => {
440
- const dbSource: DbConnection | DbTx | undefined =
441
- tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
441
+ const dbSource = resolveDbSource(tx);
442
442
  if (!dbSource) {
443
443
  throw new InternalError({
444
444
  message: `ctx.snapshotAggregate("${snapshotArgs.aggregateId}") requires a database connection — none is configured.`,
@@ -457,8 +457,7 @@ export function createDispatcher(
457
457
  reducer: SnapshotReducer<TState>,
458
458
  initial: TState,
459
459
  ): Promise<LoadAggregateWithSnapshotResult<TState>> => {
460
- const dbSource: DbConnection | DbTx | undefined =
461
- tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
460
+ const dbSource = resolveDbSource(tx);
462
461
  if (!dbSource) {
463
462
  throw new InternalError({
464
463
  message: `ctx.loadAggregateWithSnapshot("${aggregateId}") requires a database connection — none is configured.`,
@@ -502,8 +501,7 @@ export function createDispatcher(
502
501
  `table-less MSP (side-effect-only). Known queryable projections: ${all.join(", ") || "(none)"}`,
503
502
  });
504
503
  }
505
- const dbSource: DbConnection | DbTx | undefined =
506
- tx ?? (context.db as DbConnection | undefined); // @cast-boundary db-operator
504
+ const dbSource = resolveDbSource(tx);
507
505
  if (!dbSource) {
508
506
  throw new InternalError({
509
507
  message: `ctx.queryProjection("${qualifiedName}") requires a database connection — none is configured.`,
@@ -513,17 +511,10 @@ export function createDispatcher(
513
511
  // filter keeps cross-tenant leaks out unless the handler explicitly
514
512
  // opts in. Works with any drizzle-table whose tenant column is named
515
513
  // tenantId on the JS side.
516
- // @cast-boundary dynamic-key drizzle's PgTable columns are schema-dependent
517
- const tenantCol = (projTable as Record<string, AnyColumn | undefined>)["tenantId"]; // @cast-boundary dynamic-key
518
- let rows: readonly Record<string, unknown>[];
519
- if (tenantCol && !queryOptions?.unsafeAllTenants) {
520
- rows = (await dbSource
521
- .select()
522
- .from(projTable)
523
- .where(eq(tenantCol, user.tenantId))) as readonly Record<string, unknown>[]; // @cast-boundary db-row
524
- } else {
525
- rows = (await dbSource.select().from(projTable)) as readonly Record<string, unknown>[]; // @cast-boundary db-row
526
- }
514
+ const tenantCol = (projTable as Record<string, unknown>)["tenantId"];
515
+ const where =
516
+ tenantCol && !queryOptions?.unsafeAllTenants ? { tenantId: user.tenantId } : undefined;
517
+ const rows = await selectMany<Record<string, unknown>>(dbSource, projTable, where);
527
518
  return rows as readonly T[]; // @cast-boundary engine-payload
528
519
  },
529
520
  // Thin pass-through: one resolve impl lives on the dispatcher, the
@@ -1123,9 +1114,12 @@ export function createDispatcher(
1123
1114
  // can false-pass; optimistic locking would catch it later, but with
1124
1115
  // a less specific error. Falls back to a plain SELECT if no tx is
1125
1116
  // active (tests without a DB connection).
1126
- const selectQuery = handlerContext.db.select().from(table);
1127
- const filtered = selectQuery.where(eq(table["id"], id));
1128
- const rows = tx ? await filtered.for("update") : await filtered;
1117
+ const tableName = String(
1118
+ (table as { [key: symbol]: unknown })[Symbol.for("kumiko:schema:Name")],
1119
+ );
1120
+ const rows = tx
1121
+ ? await selectRowForUpdateById(handlerContext.db, tableName, id)
1122
+ : await selectMany(handlerContext.db, table, { id });
1129
1123
  const row = rows[0];
1130
1124
 
1131
1125
  if (!row) continue;
@@ -1133,10 +1127,13 @@ export function createDispatcher(
1133
1127
  // at all; a handler that wants to move a deleted row should use
1134
1128
  // unsafeSkipTransitionGuard or restore first.
1135
1129
  const rowAsRow = row as DbRow; // @cast-boundary engine-payload
1136
- if (entity.softDelete && rowAsRow["isDeleted"] === true) {
1130
+ const isDeleted = rowAsRow["isDeleted"] ?? rowAsRow["is_deleted"];
1131
+ if (entity.softDelete && isDeleted === true) {
1137
1132
  continue;
1138
1133
  }
1139
- const currentValue = (row as DbRow)[fieldName] as string; // @cast-boundary engine-bridge
1134
+ const currentValue =
1135
+ ((row as DbRow)[fieldName] as string | undefined) ??
1136
+ ((row as DbRow)[toSnakeCase(fieldName)] as string); // @cast-boundary engine-bridge
1140
1137
  guardTransition(
1141
1138
  getTransitions({ entityName, fieldName, map: transitionMap }),
1142
1139
  currentValue,
@@ -1278,7 +1275,9 @@ export function createDispatcher(
1278
1275
  }
1279
1276
  };
1280
1277
 
1281
- const db = context.db as DbConnection | undefined; // @cast-boundary db-operator
1278
+ // batch() opens its own outer transaction needs the top-level
1279
+ // connection's `.begin()` (TransactionSql exposes only `.savepoint()`).
1280
+ const db = resolveDbSource(undefined) as DbConnection | undefined;
1282
1281
  if (!db) {
1283
1282
  // Without a DB connection there is no transaction to open. Fall back to
1284
1283
  // sequential execution — useful for unit tests that don't touch the DB.
@@ -1306,7 +1305,7 @@ export function createDispatcher(
1306
1305
  }
1307
1306
 
1308
1307
  try {
1309
- await db.transaction(async (tx) => {
1308
+ await transaction(db, async (tx) => {
1310
1309
  for (let i = 0; i < commands.length; i++) {
1311
1310
  const cmd = commands[i];
1312
1311
  if (!cmd) continue;
@@ -1326,10 +1325,6 @@ export function createDispatcher(
1326
1325
  results,
1327
1326
  });
1328
1327
  }
1329
- // Unexpected throw — typically a DB driver error from commit/rollback.
1330
- // executeWrite already traps handler + lifecycle throws into WriteResult,
1331
- // so anything reaching here is infrastructure-level. Wrap as InternalError
1332
- // so the contract ("non-Kumiko → InternalError") holds uniformly.
1333
1328
  return finalize({
1334
1329
  isSuccess: false,
1335
1330
  error: toWriteErrorInfo(wrapToKumiko(e)),
@@ -1368,7 +1363,7 @@ export function createDispatcher(
1368
1363
  // scoped as "tenant" and no tx is threaded through. Hooks that need
1369
1364
  // cross-tenant lookups opt in explicitly via queryAs(systemUser, ...).
1370
1365
  function buildAuthClaimsContext(user: SessionUser): AuthClaimsContext {
1371
- const dbSource: DbConnection | undefined = context.db as DbConnection | undefined; // @cast-boundary db-operator
1366
+ const dbSource = resolveDbSource(undefined);
1372
1367
  if (!dbSource) {
1373
1368
  throw new InternalError({
1374
1369
  message:
@@ -1,6 +1,15 @@
1
- import { sql } from "drizzle-orm";
1
+ // sql now comes from native dialect
2
2
  import type { DbConnection } from "../db/connection";
3
- import { bigint, index, instant, integer, table as pgTable, primaryKey, text } from "../db/dialect";
3
+ import {
4
+ bigint,
5
+ index,
6
+ instant,
7
+ integer,
8
+ table as pgTable,
9
+ primaryKey,
10
+ sql,
11
+ text,
12
+ } from "../db/dialect";
4
13
  import { tableExists } from "../db/schema-inspection";
5
14
  import { unsafePushTables } from "../stack";
6
15
 
@@ -1,6 +1,15 @@
1
- import { and, asc, eq, gt, sql } from "drizzle-orm";
2
1
  import { requestContext } from "../api/request-context";
3
2
  import type { DbConnection, DbTx, PgClient } from "../db/connection";
3
+ import {
4
+ advanceConsumerPastEventReturning,
5
+ insertConsumerIfAbsent,
6
+ markConsumerProcessing,
7
+ selectConsumerForUpdateSkipLocked,
8
+ updateConsumerDeliveryOutcome,
9
+ updateConsumerStatusReturning,
10
+ } from "../db/queries/event-consumer";
11
+ import { selectEventsHeadId, selectNextEventIdAfter } from "../db/queries/event-store";
12
+ import { coerceRow, extractTableInfo, selectMany } from "../db/query";
4
13
  import type { AppContext } from "../engine/types";
5
14
  import { SYSTEM_TENANT_ID } from "../engine/types/identifiers";
6
15
  import {
@@ -154,7 +163,30 @@ const DEFAULT_MAX_ATTEMPTS = 10;
154
163
  // dispatcher's main pass logic stays under ~50 LOC. Every helper takes an
155
164
  // explicit `tx` — none of them use the outer dispatcher's closure state.
156
165
 
157
- type ConsumerStateRow = typeof eventConsumerStateTable.$inferSelect;
166
+ type ConsumerStateRowShape = {
167
+ readonly name: string;
168
+ readonly instanceId: string;
169
+ readonly lastProcessedEventId: bigint;
170
+ readonly status: string;
171
+ readonly attempts: number;
172
+ readonly lastError: string | null;
173
+ readonly updatedAt: Temporal.Instant;
174
+ };
175
+ type ConsumerStateRow = ConsumerStateRowShape;
176
+
177
+ type StoredEventRow = {
178
+ readonly id: bigint;
179
+ readonly aggregateId: string;
180
+ readonly aggregateType: string;
181
+ readonly tenantId: string;
182
+ readonly version: number;
183
+ readonly type: string;
184
+ readonly eventVersion: number;
185
+ readonly payload: Record<string, unknown>;
186
+ readonly metadata: import("../event-store/event-store").EventMetadata;
187
+ readonly createdAt: Temporal.Instant;
188
+ readonly createdBy: string;
189
+ };
158
190
 
159
191
  type AcquireOutcome =
160
192
  | { readonly state: ConsumerStateRow; readonly skip: null }
@@ -180,16 +212,13 @@ async function acquireConsumerState(
180
212
  name: string,
181
213
  instanceId: string,
182
214
  ): Promise<AcquireOutcome> {
183
- const [state] = (await tx
184
- .select()
185
- .from(eventConsumerStateTable)
186
- .where(
187
- and(
188
- eq(eventConsumerStateTable.name, name),
189
- eq(eventConsumerStateTable.instanceId, instanceId),
190
- ),
191
- )
192
- .for("update", { skipLocked: true })) as [ConsumerStateRow | undefined]; // @cast-boundary db-row
215
+ const rawState = await selectConsumerForUpdateSkipLocked(tx, name, instanceId);
216
+
217
+ if (!rawState) {
218
+ return { state: null, skip: "not_registered" };
219
+ }
220
+
221
+ const state = coerceRow(rawState, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow;
193
222
 
194
223
  if (!state) {
195
224
  // Either the row never existed (no pre-reg, no ensureRegistered) or
@@ -218,12 +247,7 @@ async function preRegisterConsumers(
218
247
  ): Promise<void> {
219
248
  for (const consumer of consumers) {
220
249
  const instanceId = consumerInstanceId(consumer, dispatcherInstanceId);
221
- await db
222
- .insert(eventConsumerStateTable)
223
- .values({ name: consumer.name, instanceId, status: "idle" })
224
- .onConflictDoNothing({
225
- target: [eventConsumerStateTable.name, eventConsumerStateTable.instanceId],
226
- });
250
+ await insertConsumerIfAbsent(db, consumer.name, instanceId);
227
251
  }
228
252
  }
229
253
 
@@ -249,28 +273,20 @@ function consumerInstanceId(
249
273
  // lock already guarantees single-writer semantics; this is purely
250
274
  // informational (and resets on commit to idle/dead via persistConsumerOutcome).
251
275
  async function markProcessing(tx: DbTx, name: string, instanceId: string): Promise<void> {
252
- await tx
253
- .update(eventConsumerStateTable)
254
- .set({ status: "processing", updatedAt: sql`now()` })
255
- .where(
256
- and(
257
- eq(eventConsumerStateTable.name, name),
258
- eq(eventConsumerStateTable.instanceId, instanceId),
259
- ),
260
- );
276
+ await markConsumerProcessing(tx, name, instanceId);
261
277
  }
262
278
 
263
279
  async function fetchPendingEvents(
264
280
  tx: DbTx,
265
281
  cursor: bigint,
266
282
  batchSize: number,
267
- ): Promise<ReadonlyArray<typeof eventsTable.$inferSelect>> {
268
- return (await tx
269
- .select()
270
- .from(eventsTable)
271
- .where(gt(eventsTable.id, cursor))
272
- .orderBy(asc(eventsTable.id))
273
- .limit(batchSize)) as ReadonlyArray<typeof eventsTable.$inferSelect>; // @cast-boundary db-row
283
+ ): Promise<ReadonlyArray<StoredEventRow>> {
284
+ return (await selectMany(
285
+ tx,
286
+ eventsTable,
287
+ { id: { gt: cursor } },
288
+ { orderBy: { col: "id", direction: "asc" }, limit: batchSize },
289
+ )) as ReadonlyArray<StoredEventRow>; // @cast-boundary db-row
274
290
  }
275
291
 
276
292
  type DeliveryOutcome = {
@@ -282,7 +298,7 @@ type DeliveryOutcome = {
282
298
  readonly failed: number;
283
299
  };
284
300
 
285
- function rowToStoredEvent(row: typeof eventsTable.$inferSelect): StoredEvent {
301
+ function rowToStoredEvent(row: StoredEventRow): StoredEvent {
286
302
  return {
287
303
  id: String(row.id),
288
304
  aggregateId: row.aggregateId,
@@ -305,7 +321,7 @@ function rowToStoredEvent(row: typeof eventsTable.$inferSelect): StoredEvent {
305
321
  // restartConsumer / skipPoisonEvent).
306
322
  async function deliverEvents(
307
323
  consumer: EventConsumer,
308
- events: ReadonlyArray<typeof eventsTable.$inferSelect>,
324
+ events: ReadonlyArray<StoredEventRow>,
309
325
  context: AppContext,
310
326
  maxAttempts: number,
311
327
  state: ConsumerStateRow,
@@ -376,21 +392,7 @@ async function persistConsumerOutcome(
376
392
  instanceId: string,
377
393
  outcome: DeliveryOutcome,
378
394
  ): Promise<void> {
379
- await tx
380
- .update(eventConsumerStateTable)
381
- .set({
382
- lastProcessedEventId: outcome.cursor,
383
- attempts: outcome.attempts,
384
- status: outcome.deadLettered ? "dead" : "idle",
385
- lastError: outcome.lastError,
386
- updatedAt: sql`now()`,
387
- })
388
- .where(
389
- and(
390
- eq(eventConsumerStateTable.name, name),
391
- eq(eventConsumerStateTable.instanceId, instanceId),
392
- ),
393
- );
395
+ await updateConsumerDeliveryOutcome(tx, name, instanceId, outcome);
394
396
  }
395
397
 
396
398
  // Emit the lag gauge inside the consumer pass's tx so ops sees a snapshot
@@ -403,13 +405,7 @@ async function emitLagFromTx(
403
405
  cursor: bigint,
404
406
  meter: Meter,
405
407
  ): Promise<void> {
406
- const result = await tx.execute(
407
- sql`SELECT COALESCE(MAX(id), 0)::bigint AS head FROM kumiko_events`,
408
- );
409
- // @cast-boundary db-row — raw drizzle.execute() COALESCE-aggregate row
410
- const rows = Array.isArray(result) ? (result as Array<{ head?: bigint | string | null }>) : []; // @cast-boundary db-row
411
- const raw = rows[0]?.head;
412
- const head = typeof raw === "bigint" ? raw : BigInt(raw ?? 0);
408
+ const head = await selectEventsHeadId(tx);
413
409
  const lag = head > cursor ? Number(head - cursor) : 0;
414
410
  emitEventConsumerLag(meter, { consumer: consumerName, instanceId }, lag);
415
411
  }
@@ -535,7 +531,7 @@ export function createEventDispatcher(options: EventDispatcherOptions): EventDis
535
531
  });
536
532
 
537
533
  try {
538
- await db.transaction(async (tx) => {
534
+ await db.begin(async (tx: DbTx) => {
539
535
  const acquired = await acquireConsumerState(tx, consumer.name, instanceId);
540
536
  // skip: another instance holds the lock, or the consumer is
541
537
  // disabled/dead. Nothing to deliver this pass.
@@ -701,9 +697,7 @@ export function createEventDispatcher(options: EventDispatcherOptions): EventDis
701
697
  // lastError=null, status="idle". For events that will
702
698
  // never succeed (broken payload, removed feature code).
703
699
 
704
- function normalizeConsumerState(
705
- row: typeof eventConsumerStateTable.$inferSelect,
706
- ): ConsumerRecoveryState {
700
+ function normalizeConsumerState(row: ConsumerStateRowShape): ConsumerRecoveryState {
707
701
  return {
708
702
  name: row.name,
709
703
  instanceId: row.instanceId,
@@ -735,16 +729,11 @@ async function requireConsumerRow(
735
729
  db: DbConnection,
736
730
  name: string,
737
731
  instanceId: string,
738
- ): Promise<typeof eventConsumerStateTable.$inferSelect> {
739
- const [row] = await db
740
- .select()
741
- .from(eventConsumerStateTable)
742
- .where(
743
- and(
744
- eq(eventConsumerStateTable.name, name),
745
- eq(eventConsumerStateTable.instanceId, instanceId),
746
- ),
747
- );
732
+ ): Promise<ConsumerStateRowShape> {
733
+ const [row] = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable, {
734
+ name,
735
+ instanceId,
736
+ });
748
737
  if (!row) {
749
738
  throw new Error(
750
739
  `Consumer "${name}" (instance_id="${instanceId}") has no state row — it hasn't run yet, the name is misspelled, or the instance is misspelled. ` +
@@ -765,16 +754,9 @@ export async function restartConsumer(
765
754
  `Consumer "${name}" (instance_id="${instanceId}") is not dead (status="${before.status}"). Restart only applies to dead consumers; use "enable" for a disabled one.`,
766
755
  );
767
756
  }
768
- const [updated] = await db
769
- .update(eventConsumerStateTable)
770
- .set({ status: "idle", attempts: 0, lastError: null, updatedAt: sql`now()` })
771
- .where(
772
- and(
773
- eq(eventConsumerStateTable.name, name),
774
- eq(eventConsumerStateTable.instanceId, instanceId),
775
- ),
776
- )
777
- .returning();
757
+ const raw = await updateConsumerStatusReturning(db, name, instanceId, "idle");
758
+ const updated =
759
+ raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
778
760
  if (!updated) {
779
761
  throw new Error(
780
762
  `Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
@@ -789,16 +771,9 @@ export async function disableConsumer(
789
771
  instanceId: string = SHARED_INSTANCE_SENTINEL,
790
772
  ): Promise<ConsumerRecoveryState> {
791
773
  await requireConsumerRow(db, name, instanceId);
792
- const [updated] = await db
793
- .update(eventConsumerStateTable)
794
- .set({ status: "disabled", updatedAt: sql`now()` })
795
- .where(
796
- and(
797
- eq(eventConsumerStateTable.name, name),
798
- eq(eventConsumerStateTable.instanceId, instanceId),
799
- ),
800
- )
801
- .returning();
774
+ const raw = await updateConsumerStatusReturning(db, name, instanceId, "disabled");
775
+ const updated =
776
+ raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
802
777
  if (!updated) {
803
778
  throw new Error(
804
779
  `Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
@@ -818,16 +793,9 @@ export async function enableConsumer(
818
793
  `Consumer "${name}" (instance_id="${instanceId}") is not disabled (status="${before.status}"). Enable only flips disabled → idle; use "restart" for a dead consumer.`,
819
794
  );
820
795
  }
821
- const [updated] = await db
822
- .update(eventConsumerStateTable)
823
- .set({ status: "idle", attempts: 0, lastError: null, updatedAt: sql`now()` })
824
- .where(
825
- and(
826
- eq(eventConsumerStateTable.name, name),
827
- eq(eventConsumerStateTable.instanceId, instanceId),
828
- ),
829
- )
830
- .returning();
796
+ const raw = await updateConsumerStatusReturning(db, name, instanceId, "idle");
797
+ const updated =
798
+ raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
831
799
  if (!updated) {
832
800
  throw new Error(
833
801
  `Consumer "${name}" (instance_id="${instanceId}") vanished between read and write — retry.`,
@@ -846,48 +814,25 @@ export async function skipPoisonEvent(
846
814
  instanceId: string = SHARED_INSTANCE_SENTINEL,
847
815
  ): Promise<ConsumerRecoveryState & { readonly skippedEventId: bigint | null }> {
848
816
  const before = await requireConsumerRow(db, name, instanceId);
849
- return db.transaction(async (tx) => {
850
- const [poison] = (await tx
851
- .select({ id: eventsTable.id })
852
- .from(eventsTable)
853
- .where(gt(eventsTable.id, before.lastProcessedEventId))
854
- .orderBy(asc(eventsTable.id))
855
- .limit(1)) as ReadonlyArray<{ id: bigint }>; // @cast-boundary db-row
856
- if (!poison) {
857
- const [unchanged] = await tx
858
- .select()
859
- .from(eventConsumerStateTable)
860
- .where(
861
- and(
862
- eq(eventConsumerStateTable.name, name),
863
- eq(eventConsumerStateTable.instanceId, instanceId),
864
- ),
865
- );
817
+ return db.begin(async (tx: DbTx) => {
818
+ const poisonId = await selectNextEventIdAfter(tx, before.lastProcessedEventId);
819
+ if (poisonId === null) {
820
+ const [unchanged] = await selectMany<ConsumerStateRow>(tx, eventConsumerStateTable, {
821
+ name,
822
+ instanceId,
823
+ });
866
824
  if (!unchanged)
867
825
  throw new Error(`Consumer "${name}" (instance_id="${instanceId}") vanished — retry.`);
868
826
  return { ...normalizeConsumerState(unchanged), skippedEventId: null };
869
827
  }
870
- const [updated] = await tx
871
- .update(eventConsumerStateTable)
872
- .set({
873
- lastProcessedEventId: poison.id,
874
- status: "idle",
875
- attempts: 0,
876
- lastError: null,
877
- updatedAt: sql`now()`,
878
- })
879
- .where(
880
- and(
881
- eq(eventConsumerStateTable.name, name),
882
- eq(eventConsumerStateTable.instanceId, instanceId),
883
- ),
884
- )
885
- .returning();
828
+ const raw = await advanceConsumerPastEventReturning(tx, name, instanceId, poisonId);
829
+ const updated =
830
+ raw && (coerceRow(raw, extractTableInfo(eventConsumerStateTable)) as ConsumerStateRow);
886
831
  if (!updated)
887
832
  throw new Error(
888
833
  `Consumer "${name}" (instance_id="${instanceId}") vanished mid-skip — retry.`,
889
834
  );
890
- return { ...normalizeConsumerState(updated), skippedEventId: poison.id };
835
+ return { ...normalizeConsumerState(updated), skippedEventId: poisonId };
891
836
  });
892
837
  }
893
838
 
@@ -905,15 +850,10 @@ export async function getConsumerState(
905
850
  readonly lastError: string | null;
906
851
  readonly updatedAt: Temporal.Instant;
907
852
  } | null> {
908
- const [row] = await db
909
- .select()
910
- .from(eventConsumerStateTable)
911
- .where(
912
- and(
913
- eq(eventConsumerStateTable.name, name),
914
- eq(eventConsumerStateTable.instanceId, instanceId),
915
- ),
916
- );
853
+ const [row] = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable, {
854
+ name,
855
+ instanceId,
856
+ });
917
857
  if (!row) return null;
918
858
  return {
919
859
  name: row.name,
@@ -947,7 +887,7 @@ export async function listConsumersWithState(
947
887
  readonly lastError: string | null;
948
888
  }>
949
889
  > {
950
- const stateRows = await db.select().from(eventConsumerStateTable);
890
+ const stateRows = await selectMany<ConsumerStateRow>(db, eventConsumerStateTable);
951
891
  const registered = new Set(registeredNames);
952
892
 
953
893
  // Materialize one output row per (name, instance_id). Registered names