@cosmicdrift/kumiko-framework 0.1.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 (388) hide show
  1. package/README.md +159 -0
  2. package/package.json +91 -0
  3. package/src/__tests__/anonymous-access.integration.ts +325 -0
  4. package/src/__tests__/error-contract.integration.ts +435 -0
  5. package/src/__tests__/field-access.integration.ts +269 -0
  6. package/src/__tests__/full-stack.integration.ts +914 -0
  7. package/src/__tests__/ownership.integration.ts +449 -0
  8. package/src/__tests__/reference-data.integration.ts +198 -0
  9. package/src/__tests__/transition-guard.integration.ts +340 -0
  10. package/src/api/__tests__/api.test.ts +337 -0
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
  13. package/src/api/__tests__/batch.integration.ts +404 -0
  14. package/src/api/__tests__/body-limit.test.ts +88 -0
  15. package/src/api/__tests__/csrf-middleware.test.ts +97 -0
  16. package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
  17. package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
  18. package/src/api/__tests__/nested-write.integration.ts +213 -0
  19. package/src/api/__tests__/readiness.test.ts +76 -0
  20. package/src/api/__tests__/request-id-middleware.test.ts +72 -0
  21. package/src/api/__tests__/sse-broker.test.ts +58 -0
  22. package/src/api/__tests__/sse-route.test.ts +112 -0
  23. package/src/api/anonymous-cookie.ts +60 -0
  24. package/src/api/api-constants.ts +64 -0
  25. package/src/api/auth-middleware.ts +418 -0
  26. package/src/api/auth-routes.ts +982 -0
  27. package/src/api/csrf-middleware.ts +77 -0
  28. package/src/api/index.ts +31 -0
  29. package/src/api/jwt.ts +66 -0
  30. package/src/api/observability-middleware.ts +89 -0
  31. package/src/api/readiness.ts +132 -0
  32. package/src/api/request-context.ts +49 -0
  33. package/src/api/request-id-middleware.ts +50 -0
  34. package/src/api/route-registrars.ts +195 -0
  35. package/src/api/routes.ts +135 -0
  36. package/src/api/server.ts +640 -0
  37. package/src/api/sse-broker.ts +71 -0
  38. package/src/api/sse-route.ts +62 -0
  39. package/src/api/tokens.ts +16 -0
  40. package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
  41. package/src/db/__tests__/compound-types.test.ts +114 -0
  42. package/src/db/__tests__/connection-options.test.ts +68 -0
  43. package/src/db/__tests__/cursor.test.ts +41 -0
  44. package/src/db/__tests__/db-helpers.test.ts +369 -0
  45. package/src/db/__tests__/dialect-instant.test.ts +50 -0
  46. package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
  47. package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
  48. package/src/db/__tests__/encryption.test.ts +39 -0
  49. package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
  50. package/src/db/__tests__/event-store-executor.integration.ts +235 -0
  51. package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
  52. package/src/db/__tests__/located-timestamp.test.ts +184 -0
  53. package/src/db/__tests__/money.test.ts +199 -0
  54. package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
  55. package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
  56. package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
  57. package/src/db/__tests__/row-helpers.test.ts +59 -0
  58. package/src/db/__tests__/schema-migration.integration.ts +273 -0
  59. package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
  60. package/src/db/__tests__/table-builder-required.test.ts +216 -0
  61. package/src/db/__tests__/tenant-db.integration.ts +606 -0
  62. package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
  63. package/src/db/apply-entity-event.ts +188 -0
  64. package/src/db/assert-exists-in.ts +59 -0
  65. package/src/db/compound-types.ts +47 -0
  66. package/src/db/connection.ts +104 -0
  67. package/src/db/cursor.ts +83 -0
  68. package/src/db/dialect.ts +109 -0
  69. package/src/db/eagerload.ts +174 -0
  70. package/src/db/encryption.ts +39 -0
  71. package/src/db/event-store-executor.ts +906 -0
  72. package/src/db/index.ts +55 -0
  73. package/src/db/located-timestamp.ts +114 -0
  74. package/src/db/money.ts +120 -0
  75. package/src/db/pg-error.ts +46 -0
  76. package/src/db/reference-data.ts +77 -0
  77. package/src/db/row-helpers.ts +53 -0
  78. package/src/db/schema-inspection.ts +25 -0
  79. package/src/db/table-builder.ts +475 -0
  80. package/src/db/tenant-db.ts +434 -0
  81. package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
  82. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
  83. package/src/engine/__tests__/boot-validator.test.ts +1865 -0
  84. package/src/engine/__tests__/build-app-schema.test.ts +154 -0
  85. package/src/engine/__tests__/claim-keys.test.ts +274 -0
  86. package/src/engine/__tests__/config-helpers.test.ts +236 -0
  87. package/src/engine/__tests__/effective-features.test.ts +86 -0
  88. package/src/engine/__tests__/engine.test.ts +1461 -0
  89. package/src/engine/__tests__/entity-handlers.test.ts +274 -0
  90. package/src/engine/__tests__/event-helpers.test.ts +68 -0
  91. package/src/engine/__tests__/extends-registrar.test.ts +159 -0
  92. package/src/engine/__tests__/factories-long-text.test.ts +84 -0
  93. package/src/engine/__tests__/factories-time.test.ts +158 -0
  94. package/src/engine/__tests__/field-predicates.test.ts +48 -0
  95. package/src/engine/__tests__/hook-phases.test.ts +132 -0
  96. package/src/engine/__tests__/identifiers.test.ts +35 -0
  97. package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
  98. package/src/engine/__tests__/nav.test.ts +267 -0
  99. package/src/engine/__tests__/ownership.test.ts +421 -0
  100. package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
  101. package/src/engine/__tests__/projection-helpers.test.ts +62 -0
  102. package/src/engine/__tests__/projection.test.ts +191 -0
  103. package/src/engine/__tests__/qualified-name.test.ts +264 -0
  104. package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
  105. package/src/engine/__tests__/run-in.test.ts +38 -0
  106. package/src/engine/__tests__/schema-builder.test.ts +380 -0
  107. package/src/engine/__tests__/screen.test.ts +408 -0
  108. package/src/engine/__tests__/state-machine.test.ts +148 -0
  109. package/src/engine/__tests__/system-user.test.ts +57 -0
  110. package/src/engine/__tests__/validation-hooks.test.ts +71 -0
  111. package/src/engine/access.ts +23 -0
  112. package/src/engine/boot-validator.ts +1528 -0
  113. package/src/engine/build-app-schema.ts +125 -0
  114. package/src/engine/config-helpers.ts +115 -0
  115. package/src/engine/constants.ts +85 -0
  116. package/src/engine/create-app.ts +98 -0
  117. package/src/engine/define-feature.ts +702 -0
  118. package/src/engine/define-handler.ts +78 -0
  119. package/src/engine/define-roles.ts +19 -0
  120. package/src/engine/effective-features.ts +87 -0
  121. package/src/engine/entity-handlers.ts +364 -0
  122. package/src/engine/event-helpers.ts +73 -0
  123. package/src/engine/factories.ts +328 -0
  124. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
  125. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
  126. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
  127. package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
  128. package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
  129. package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
  130. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
  131. package/src/engine/feature-ast/extractors.ts +2562 -0
  132. package/src/engine/feature-ast/index.ts +105 -0
  133. package/src/engine/feature-ast/parse.ts +369 -0
  134. package/src/engine/feature-ast/patch.ts +525 -0
  135. package/src/engine/feature-ast/patcher.ts +518 -0
  136. package/src/engine/feature-ast/patterns.ts +434 -0
  137. package/src/engine/feature-ast/render.ts +602 -0
  138. package/src/engine/feature-ast/source-location.ts +45 -0
  139. package/src/engine/field-access.ts +120 -0
  140. package/src/engine/index.ts +254 -0
  141. package/src/engine/ownership.ts +337 -0
  142. package/src/engine/parse-ref-target.ts +22 -0
  143. package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
  144. package/src/engine/pattern-library/index.ts +24 -0
  145. package/src/engine/pattern-library/library.ts +1117 -0
  146. package/src/engine/pattern-library/types.ts +255 -0
  147. package/src/engine/projection-helpers.ts +85 -0
  148. package/src/engine/qualified-name.ts +122 -0
  149. package/src/engine/read-claim.ts +31 -0
  150. package/src/engine/registry.ts +1325 -0
  151. package/src/engine/resolve-config-or-param.ts +153 -0
  152. package/src/engine/run-in.ts +29 -0
  153. package/src/engine/schema-builder.ts +175 -0
  154. package/src/engine/screen-filter-ops.ts +51 -0
  155. package/src/engine/state-machine.ts +70 -0
  156. package/src/engine/system-user.ts +32 -0
  157. package/src/engine/types/config.ts +306 -0
  158. package/src/engine/types/event-type-map.ts +37 -0
  159. package/src/engine/types/feature.ts +574 -0
  160. package/src/engine/types/fields.ts +422 -0
  161. package/src/engine/types/handlers.ts +742 -0
  162. package/src/engine/types/hooks.ts +142 -0
  163. package/src/engine/types/http-route.ts +54 -0
  164. package/src/engine/types/identifiers.ts +47 -0
  165. package/src/engine/types/index.ts +208 -0
  166. package/src/engine/types/nav.ts +46 -0
  167. package/src/engine/types/projection.ts +132 -0
  168. package/src/engine/types/relations.ts +51 -0
  169. package/src/engine/types/screen.ts +452 -0
  170. package/src/engine/types/workspace.ts +42 -0
  171. package/src/engine/validation.ts +33 -0
  172. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
  173. package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
  174. package/src/entrypoint/index.ts +442 -0
  175. package/src/errors/__tests__/classes.test.ts +371 -0
  176. package/src/errors/__tests__/write-failures.test.ts +109 -0
  177. package/src/errors/classes.ts +249 -0
  178. package/src/errors/i18n/de.yaml +83 -0
  179. package/src/errors/i18n/en.yaml +80 -0
  180. package/src/errors/index.ts +41 -0
  181. package/src/errors/kumiko-error.ts +67 -0
  182. package/src/errors/reasons.ts +36 -0
  183. package/src/errors/serialize.ts +136 -0
  184. package/src/errors/transition-details.ts +30 -0
  185. package/src/errors/write-error-info.ts +123 -0
  186. package/src/errors/zod-bridge.ts +49 -0
  187. package/src/event-store/__tests__/admin-api.integration.ts +361 -0
  188. package/src/event-store/__tests__/event-store.integration.ts +584 -0
  189. package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
  190. package/src/event-store/__tests__/perf.integration.ts +255 -0
  191. package/src/event-store/__tests__/snapshot.integration.ts +267 -0
  192. package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
  193. package/src/event-store/__tests__/upcaster.integration.ts +460 -0
  194. package/src/event-store/admin-api.ts +257 -0
  195. package/src/event-store/archive.ts +106 -0
  196. package/src/event-store/errors.ts +35 -0
  197. package/src/event-store/event-store.ts +405 -0
  198. package/src/event-store/events-schema.ts +90 -0
  199. package/src/event-store/index.ts +50 -0
  200. package/src/event-store/snapshot.ts +210 -0
  201. package/src/event-store/upcaster-dead-letter.ts +119 -0
  202. package/src/event-store/upcaster.ts +147 -0
  203. package/src/files/__tests__/content-disposition.test.ts +123 -0
  204. package/src/files/__tests__/file-field-column.integration.ts +103 -0
  205. package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
  206. package/src/files/__tests__/file-handle.test.ts +122 -0
  207. package/src/files/__tests__/files.integration.ts +830 -0
  208. package/src/files/__tests__/storage-tracking.integration.ts +153 -0
  209. package/src/files/content-disposition.ts +55 -0
  210. package/src/files/file-handle.ts +63 -0
  211. package/src/files/file-ref-table.ts +22 -0
  212. package/src/files/file-routes.ts +353 -0
  213. package/src/files/in-memory-provider.ts +62 -0
  214. package/src/files/index.ts +29 -0
  215. package/src/files/local-provider.ts +35 -0
  216. package/src/files/storage-tracking.ts +60 -0
  217. package/src/files/types.ts +118 -0
  218. package/src/i18n/__tests__/i18n.test.ts +72 -0
  219. package/src/i18n/index.ts +29 -0
  220. package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
  221. package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
  222. package/src/jobs/__tests__/jobs.integration.ts +566 -0
  223. package/src/jobs/index.ts +2 -0
  224. package/src/jobs/job-runner.ts +574 -0
  225. package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
  226. package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
  227. package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
  228. package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
  229. package/src/lifecycle/index.ts +13 -0
  230. package/src/lifecycle/lifecycle.ts +160 -0
  231. package/src/lifecycle/signal-handlers.ts +62 -0
  232. package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
  233. package/src/logging/index.ts +3 -0
  234. package/src/logging/pino-logger.ts +64 -0
  235. package/src/logging/types.ts +7 -0
  236. package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
  237. package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
  238. package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
  239. package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
  240. package/src/migrations/index.ts +28 -0
  241. package/src/migrations/projection-detection.ts +149 -0
  242. package/src/migrations/rebuild-marker.ts +64 -0
  243. package/src/migrations/schema-drift.ts +395 -0
  244. package/src/observability/__tests__/console-provider.test.ts +67 -0
  245. package/src/observability/__tests__/metric-validator.test.ts +87 -0
  246. package/src/observability/__tests__/noop-provider.test.ts +82 -0
  247. package/src/observability/__tests__/observability.integration.ts +559 -0
  248. package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
  249. package/src/observability/__tests__/recording-meter.test.ts +101 -0
  250. package/src/observability/__tests__/recording-tracer.test.ts +110 -0
  251. package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
  252. package/src/observability/console-provider.ts +130 -0
  253. package/src/observability/context.ts +26 -0
  254. package/src/observability/fallback.ts +34 -0
  255. package/src/observability/ids.ts +25 -0
  256. package/src/observability/index.ts +79 -0
  257. package/src/observability/metric-validator.ts +86 -0
  258. package/src/observability/metrics-handle.ts +56 -0
  259. package/src/observability/noop-provider.ts +146 -0
  260. package/src/observability/prometheus-meter.ts +284 -0
  261. package/src/observability/recording-meter.ts +156 -0
  262. package/src/observability/recording-tracer.ts +198 -0
  263. package/src/observability/redis-wrapper.ts +132 -0
  264. package/src/observability/sensitive-filter.ts +108 -0
  265. package/src/observability/standard-metrics.ts +213 -0
  266. package/src/observability/types/index.ts +29 -0
  267. package/src/observability/types/metric.ts +56 -0
  268. package/src/observability/types/provider.ts +32 -0
  269. package/src/observability/types/span.ts +64 -0
  270. package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
  271. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
  272. package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
  273. package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
  274. package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
  275. package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
  276. package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
  277. package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
  278. package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
  279. package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
  280. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
  281. package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
  282. package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
  283. package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
  284. package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
  285. package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
  286. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
  287. package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
  288. package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
  289. package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
  290. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
  291. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
  292. package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
  293. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
  294. package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
  295. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
  296. package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
  297. package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
  298. package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
  299. package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
  300. package/src/pipeline/append-event-core.ts +117 -0
  301. package/src/pipeline/auth-claims-resolver.ts +103 -0
  302. package/src/pipeline/cascade-handler.ts +113 -0
  303. package/src/pipeline/dispatcher.ts +1585 -0
  304. package/src/pipeline/distributed-lock.ts +37 -0
  305. package/src/pipeline/entity-cache.ts +113 -0
  306. package/src/pipeline/event-consumer-state.ts +108 -0
  307. package/src/pipeline/event-dedup.ts +23 -0
  308. package/src/pipeline/event-dispatcher.ts +1016 -0
  309. package/src/pipeline/event-retention.ts +154 -0
  310. package/src/pipeline/idempotency.ts +76 -0
  311. package/src/pipeline/index.ts +66 -0
  312. package/src/pipeline/lifecycle-pipeline.ts +409 -0
  313. package/src/pipeline/msp-rebuild.ts +242 -0
  314. package/src/pipeline/multi-stream-apply-context.ts +115 -0
  315. package/src/pipeline/projection-rebuild.ts +334 -0
  316. package/src/pipeline/projection-state.ts +72 -0
  317. package/src/pipeline/projections-runner.ts +56 -0
  318. package/src/pipeline/redis-keys.ts +11 -0
  319. package/src/pipeline/system-hooks.ts +190 -0
  320. package/src/random/__tests__/generate.test.ts +149 -0
  321. package/src/random/generate.ts +141 -0
  322. package/src/random/index.ts +8 -0
  323. package/src/random/words.ts +392 -0
  324. package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
  325. package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
  326. package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
  327. package/src/rate-limit/bucket.ts +36 -0
  328. package/src/rate-limit/index.ts +14 -0
  329. package/src/rate-limit/middleware.ts +152 -0
  330. package/src/rate-limit/resolver.ts +267 -0
  331. package/src/redis/__tests__/redis-options.test.ts +54 -0
  332. package/src/redis/index.ts +74 -0
  333. package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
  334. package/src/search/__tests__/search-adapter.test.ts +256 -0
  335. package/src/search/in-memory-adapter.ts +123 -0
  336. package/src/search/index.ts +12 -0
  337. package/src/search/meilisearch-adapter.ts +106 -0
  338. package/src/search/types.ts +39 -0
  339. package/src/secrets/__tests__/dek-cache.test.ts +213 -0
  340. package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
  341. package/src/secrets/__tests__/envelope.test.ts +74 -0
  342. package/src/secrets/__tests__/leak-guard.test.ts +92 -0
  343. package/src/secrets/__tests__/rotation.test.ts +149 -0
  344. package/src/secrets/dek-cache.ts +116 -0
  345. package/src/secrets/env-master-key-provider.ts +162 -0
  346. package/src/secrets/envelope.ts +55 -0
  347. package/src/secrets/index.ts +19 -0
  348. package/src/secrets/leak-guard.ts +87 -0
  349. package/src/secrets/rotation.ts +34 -0
  350. package/src/secrets/types.ts +107 -0
  351. package/src/stack/db.ts +104 -0
  352. package/src/stack/event-collector.ts +23 -0
  353. package/src/stack/index.ts +32 -0
  354. package/src/stack/redis.ts +44 -0
  355. package/src/stack/request-helper.ts +168 -0
  356. package/src/stack/table-helpers.ts +104 -0
  357. package/src/stack/test-stack.ts +357 -0
  358. package/src/stack/test-users.ts +37 -0
  359. package/src/testing/__tests__/e2e-generator.test.ts +230 -0
  360. package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
  361. package/src/testing/access-assertions.ts +15 -0
  362. package/src/testing/assertions.ts +35 -0
  363. package/src/testing/e2e-generator.ts +465 -0
  364. package/src/testing/expect-error.ts +25 -0
  365. package/src/testing/handler-context.ts +125 -0
  366. package/src/testing/http-cookies.ts +52 -0
  367. package/src/testing/index.ts +41 -0
  368. package/src/testing/late-bound.ts +39 -0
  369. package/src/testing/mutable-master-key-provider.ts +31 -0
  370. package/src/testing/observability-recorder.ts +54 -0
  371. package/src/testing/shared-entities.ts +49 -0
  372. package/src/testing/utils.ts +1 -0
  373. package/src/testing/wait-for.ts +31 -0
  374. package/src/time/__tests__/polyfill.test.ts +73 -0
  375. package/src/time/__tests__/tz-context.test.ts +121 -0
  376. package/src/time/index.ts +21 -0
  377. package/src/time/polyfill.ts +70 -0
  378. package/src/time/tz-context.ts +107 -0
  379. package/src/ui-types/app-schema.ts +57 -0
  380. package/src/ui-types/index.ts +65 -0
  381. package/src/utils/__tests__/assert.test.ts +17 -0
  382. package/src/utils/__tests__/env-parse.test.ts +54 -0
  383. package/src/utils/assert.ts +18 -0
  384. package/src/utils/env-parse.ts +16 -0
  385. package/src/utils/ids.ts +16 -0
  386. package/src/utils/index.ts +5 -0
  387. package/src/utils/safe-json.ts +30 -0
  388. package/src/utils/serialization.ts +7 -0
@@ -0,0 +1,34 @@
1
+ // KEK rotation: unwrap the DEK with the OLD kekVersion, re-wrap with the
2
+ // CURRENT one. The ciphertext never changes — this is why envelope
3
+ // encryption makes rotation affordable on large tables.
4
+
5
+ import type { Envelope, MasterKeyProvider } from "./types";
6
+
7
+ // Re-wrap the DEK of a single envelope so it references the current KEK
8
+ // version. No-op when the envelope is already current (caller-side this
9
+ // means "filter WHERE kekVersion != currentVersion" before calling).
10
+ export async function rewrapDek(
11
+ envelope: Envelope,
12
+ provider: MasterKeyProvider,
13
+ ): Promise<Envelope> {
14
+ if (envelope.kekVersion === provider.currentVersion()) {
15
+ return envelope;
16
+ }
17
+ // Unwrap with the old KEK. Provider must still know the old version
18
+ // (keyring contains both until ops retires the old KEK).
19
+ const dek = await provider.unwrapDek(envelope.encryptedDek, envelope.kekVersion);
20
+ try {
21
+ const { encryptedDek, kekVersion } = await provider.wrapDek(dek);
22
+ return {
23
+ ciphertext: envelope.ciphertext,
24
+ iv: envelope.iv,
25
+ authTag: envelope.authTag,
26
+ encryptedDek,
27
+ kekVersion,
28
+ };
29
+ } finally {
30
+ // Zero the unwrapped DEK — it was held in plaintext only long enough
31
+ // to wrap it again.
32
+ dek.fill(0);
33
+ }
34
+ }
@@ -0,0 +1,107 @@
1
+ // Envelope Encryption types. Separating DEK (per-value) from KEK (central)
2
+ // is what makes key rotation cheap: on rotation we only re-wrap the small
3
+ // encryptedDek, never touch the ciphertext.
4
+
5
+ import type { TenantId } from "../engine";
6
+
7
+ // Plaintext-secret wrapper (branded). Carries the actual string internally
8
+ // but the nominal typing stops it from landing in an HTTP response by
9
+ // accident — a response-serializer guard + the reveal() cost make the leak
10
+ // intentional. Framework code that sees `Secret<string>` knows the caller
11
+ // has already gone through the audited ctx.secrets.get path.
12
+ //
13
+ // The brand is a real (non-registered) Symbol so it exists at runtime for
14
+ // isSecret() without clashing with user-land symbols of the same name.
15
+ const SecretBrand: unique symbol = Symbol("kumiko.secret");
16
+
17
+ export type Secret<T = string> = {
18
+ readonly [SecretBrand]: true;
19
+ readonly reveal: () => T;
20
+ };
21
+
22
+ // Implementation helper — bundled-features uses this to wrap a plaintext after
23
+ // decryption. Kept in the framework so both sides share one canonical brand.
24
+ export function createSecret<T>(value: T): Secret<T> {
25
+ return {
26
+ [SecretBrand]: true as const,
27
+ reveal: () => value,
28
+ };
29
+ }
30
+
31
+ // True for any object carrying the Secret brand. Used by the response guard
32
+ // to reject leaks before serialization.
33
+ export function isSecret(v: unknown): v is Secret<unknown> {
34
+ return typeof v === "object" && v !== null && SecretBrand in v;
35
+ }
36
+
37
+ // Per-read audit context. Populated by requireSecretsContext() wrapper so
38
+ // handlers don't need to pass userId/handlerName manually on every call.
39
+ // Undefined for framework-internal reads (rotation job, tests) — the audit
40
+ // table stays a "who touched this credential" log, not a crash-report sink.
41
+ export type SecretAuditContext = {
42
+ readonly userId: string;
43
+ readonly handlerName: string;
44
+ };
45
+
46
+ // Feature code can pass either the raw qualified-name string or a typed
47
+ // handle returned by r.secret. The handle form is safer — renaming the
48
+ // r.secret call updates all references through the import graph.
49
+ export type SecretKeyRef = string | { readonly name: string };
50
+
51
+ // The ctx.secrets contract. Concrete implementation lives in bundled-features
52
+ // (createSecretsContext) where the DB and MasterKeyProvider are known. This
53
+ // lean interface is what the framework's HandlerContext carries so engine
54
+ // code can talk about it without pulling in bundled-features.
55
+ export interface SecretsContext {
56
+ get(
57
+ tenantId: TenantId,
58
+ key: SecretKeyRef,
59
+ auditCtx?: SecretAuditContext,
60
+ ): Promise<Secret<string> | undefined>;
61
+ set(
62
+ tenantId: TenantId,
63
+ key: SecretKeyRef,
64
+ value: string,
65
+ opts?: { redact?: (plaintext: string) => string; hint?: string; updatedBy?: string },
66
+ ): Promise<void>;
67
+ delete(tenantId: TenantId, key: SecretKeyRef, opts?: { deletedBy?: string }): Promise<boolean>;
68
+ }
69
+
70
+ export type Envelope = {
71
+ // AES-256-GCM ciphertext of the plaintext, keyed with a DEK.
72
+ readonly ciphertext: Buffer;
73
+ // GCM nonce (12 bytes). Generated fresh per encryption.
74
+ readonly iv: Buffer;
75
+ // GCM auth tag (16 bytes). Guarantees the ciphertext wasn't tampered.
76
+ readonly authTag: Buffer;
77
+ // DEK wrapped with the current KEK. Decryption needs provider.unwrapDek
78
+ // with the kekVersion to recover the DEK.
79
+ readonly encryptedDek: Buffer;
80
+ // Which KEK version was used to wrap the DEK. On rotation, rows with old
81
+ // versions still decrypt — the provider keeps a keyring of historical KEKs.
82
+ readonly kekVersion: number;
83
+ };
84
+
85
+ // The contract a KEK backend must fulfil. The framework sees only this
86
+ // interface; concrete implementations live in separate packages
87
+ // (@cosmicdrift/kumiko-secrets-vault, @cosmicdrift/kumiko-secrets-aws-kms, ...). The default is
88
+ // EnvMasterKeyProvider which reads keys from environment variables.
89
+ export interface MasterKeyProvider {
90
+ // Wrap a fresh DEK with the current KEK. Returns the wrapped bytes + the
91
+ // KEK version used — the version ends up in the Envelope so decryption
92
+ // later knows which KEK to ask for.
93
+ wrapDek(dek: Buffer): Promise<{ encryptedDek: Buffer; kekVersion: number }>;
94
+
95
+ // Unwrap a previously-wrapped DEK. During rotation the provider must
96
+ // accept older kekVersion values (2-version window minimum), otherwise
97
+ // old rows become unreadable.
98
+ unwrapDek(encryptedDek: Buffer, kekVersion: number): Promise<Buffer>;
99
+
100
+ // Which KEK version new wraps use. Rotation flips this to a new value
101
+ // and older-version reads continue to work until rows are re-wrapped.
102
+ currentVersion(): number;
103
+
104
+ // Health check: can the provider talk to its backend? Used by
105
+ // /health/ready. Cheap probe, no KEK material read.
106
+ isAvailable(): Promise<boolean>;
107
+ }
@@ -0,0 +1,104 @@
1
+ import { drizzle } from "drizzle-orm/postgres-js";
2
+ import postgres from "postgres";
3
+ import { generateId } from "../utils";
4
+
5
+ function requireEnv(name: string): string {
6
+ const value = process.env[name];
7
+ if (!value) {
8
+ throw new Error(
9
+ `Missing required env var: ${name}. Copy .env.example to .env and fill in values.`,
10
+ );
11
+ }
12
+ return value;
13
+ }
14
+
15
+ export type TestDb = {
16
+ db: ReturnType<typeof drizzle>;
17
+ client: ReturnType<typeof postgres>;
18
+ dbName: string;
19
+ cleanup: () => Promise<void>;
20
+ };
21
+
22
+ export type CreateTestDbOptions = {
23
+ /** Override TEST_DATABASE_URL. Rare — mostly for tests that want a
24
+ * non-default Postgres (e.g. a read-replica probe). */
25
+ readonly baseUrl?: string;
26
+ /** Use a specific DB name instead of the default
27
+ * `kumiko_test_<8chars>`. Combined with `persistent: true`, lets a
28
+ * dev server keep state across restarts. Must be a legal Postgres
29
+ * identifier — the caller is responsible for matching the usual
30
+ * [a-z_0-9]+ shape. */
31
+ readonly dbName?: string;
32
+ /** When true, cleanup() is a no-op and the DB survives. Also
33
+ * changes CREATE DATABASE to IF-NOT-EXISTS semantics so restarts
34
+ * reuse the same storage. Default false (test contract: fresh DB
35
+ * per call, dropped on cleanup). */
36
+ readonly persistent?: boolean;
37
+ };
38
+
39
+ /**
40
+ * Accepts a baseUrl string (legacy shorthand used by most tests) OR an
41
+ * options object. The string form is kept because thousands of tests
42
+ * call `createTestDb()` with no args; only dev-server and niche tests
43
+ * need the options form.
44
+ */
45
+ export async function createTestDb(arg?: string | CreateTestDbOptions): Promise<TestDb> {
46
+ const opts: CreateTestDbOptions = typeof arg === "string" ? { baseUrl: arg } : (arg ?? {});
47
+ const url = opts.baseUrl ?? requireEnv("TEST_DATABASE_URL");
48
+ // slice(-8) — the last 8 hex chars of a UUIDv7 are pure random (the
49
+ // front 48 bits are a timestamp, which would collide across workers
50
+ // that start within the same millisecond).
51
+ const dbName = opts.dbName ?? `kumiko_test_${generateId().slice(-8)}`;
52
+ const adminUrl = url.replace(/\/[^/]+$/, "/postgres");
53
+
54
+ const adminClient = postgres(adminUrl);
55
+ try {
56
+ if (opts.persistent) {
57
+ // Postgres has no CREATE DATABASE IF NOT EXISTS; emulate with a
58
+ // catalog probe so restarts are idempotent.
59
+ const existing = await adminClient<{ exists: boolean }[]>`
60
+ SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = ${dbName}) AS exists
61
+ `;
62
+ if (!existing[0]?.exists) {
63
+ await adminClient.unsafe(`CREATE DATABASE "${dbName}"`);
64
+ }
65
+ } else {
66
+ await adminClient.unsafe(`CREATE DATABASE "${dbName}"`);
67
+ }
68
+ } finally {
69
+ await adminClient.end();
70
+ }
71
+
72
+ const testUrl = url.replace(/\/[^/]+$/, `/${dbName}`);
73
+ const client = postgres(testUrl);
74
+ const db = drizzle(client);
75
+
76
+ // Every ES-entity writes events; auto-create the events table so tests that
77
+ // go straight to createTestDb (not setupTestStack) also work out of the box.
78
+ // In persistent mode this is idempotent: createEventsTable emits IF NOT
79
+ // EXISTS so a second boot is a no-op.
80
+ const { createEventsTable } = await import("../event-store");
81
+ await createEventsTable(db);
82
+
83
+ return {
84
+ db,
85
+ client,
86
+ dbName,
87
+ cleanup: async () => {
88
+ await client.end();
89
+ // Persistent mode: dev-server owns the DB lifecycle — don't drop
90
+ // on process exit. `yarn kumiko clean-test-dbs` is the escape
91
+ // hatch when you really want to start over.
92
+ if (!opts.persistent) {
93
+ const admin = postgres(adminUrl);
94
+ try {
95
+ await admin.unsafe(`DROP DATABASE IF EXISTS "${dbName}"`);
96
+ } finally {
97
+ await admin.end();
98
+ }
99
+ }
100
+ },
101
+ };
102
+ }
103
+
104
+ export { requireEnv };
@@ -0,0 +1,23 @@
1
+ import type { SseEvent } from "../api/sse-broker";
2
+ import type { SaveContext } from "../engine/types";
3
+
4
+ export type EventCollector = {
5
+ readonly sse: SseEvent[];
6
+ readonly postSave: SaveContext[];
7
+ /** Clears all collected events — call in beforeEach for per-test isolation */
8
+ reset(): void;
9
+ };
10
+
11
+ export function createEventCollector(): EventCollector {
12
+ const sse: SseEvent[] = [];
13
+ const postSave: SaveContext[] = [];
14
+
15
+ return {
16
+ sse,
17
+ postSave,
18
+ reset() {
19
+ sse.length = 0;
20
+ postSave.length = 0;
21
+ },
22
+ };
23
+ }
@@ -0,0 +1,32 @@
1
+ // Runtime-safe Stack-Builder. Was hier liegt, wird vom dev-server zum Hochfahren
2
+ // einer kompletten Kumiko-Instanz genutzt — DB, Redis, Hono-App, Dispatcher,
3
+ // SSE-Broker. Die Files heißen historisch `test*` (createTestDb,
4
+ // setupTestStack, TestUsers, …), bedienen aber heute Dev- UND Test-Code: das
5
+ // ist genau derselbe Hochfahr-Pfad, nur einmal mit ephemeral-DB (test) und
6
+ // einmal mit persistent-DB (dev).
7
+ //
8
+ // Wichtig: dieses Modul darf KEINE vitest-Imports enthalten und keine
9
+ // Vitest-only Helper transitiv ziehen — sonst crasht jedes Tooling, das den
10
+ // dev-server unter Node lädt (drizzle-kit, build-scripts).
11
+
12
+ export {
13
+ type CreateTestDbOptions,
14
+ createTestDb,
15
+ type TestDb,
16
+ } from "./db";
17
+ export { createEventCollector, type EventCollector } from "./event-collector";
18
+ export { createTestRedis, type TestRedis } from "./redis";
19
+ export { createRequestHelper, type RequestHelper } from "./request-helper";
20
+ export {
21
+ createEntityTable,
22
+ ensureEntityTable,
23
+ pushTables,
24
+ resetEventStore,
25
+ } from "./table-helpers";
26
+ export { setupTestStack, type TestStack, type TestStackOptions } from "./test-stack";
27
+ export {
28
+ createTestUser,
29
+ TestUsers,
30
+ testTenantId,
31
+ testUserId,
32
+ } from "./test-users";
@@ -0,0 +1,44 @@
1
+ import { generateId } from "../utils";
2
+ import { requireEnv } from "./db";
3
+
4
+ export type TestRedis = {
5
+ redis: import("ioredis").default;
6
+ /** Delete every key this test created (prefix-scoped). Replaces the old
7
+ * `redis.flushdb()` — that wiped other parallel tests' BullMQ state. */
8
+ flushNamespace: () => Promise<void>;
9
+ cleanup: () => Promise<void>;
10
+ };
11
+
12
+ export async function createTestRedis(): Promise<TestRedis> {
13
+ const Redis = (await import("ioredis")).default;
14
+ const redisUrl = requireEnv("REDIS_URL");
15
+ // Every test gets a per-file key prefix on a shared DB (no DB-pool-of-15
16
+ // round-robin). Collisions at birthday-paradox rates are gone — the
17
+ // prefix space is unbounded. See Track B.3 in docs/plans/tests-refactor.
18
+ const prefix = `kt:${generateId().slice(-8)}:`;
19
+ const redis = new Redis(redisUrl, { keyPrefix: prefix });
20
+
21
+ async function flushNamespace(): Promise<void> {
22
+ // Open a prefix-less client for the scan — ioredis' keyPrefix is applied
23
+ // per-command but SCAN's returned keys are full names, so managing the
24
+ // del set with the prefix already on the connection is error-prone.
25
+ const raw = new Redis(redisUrl);
26
+ try {
27
+ const stream = raw.scanStream({ match: `${prefix}*`, count: 500 });
28
+ const keys: string[] = [];
29
+ for await (const batch of stream) keys.push(...batch);
30
+ if (keys.length > 0) await raw.del(...keys);
31
+ } finally {
32
+ raw.disconnect();
33
+ }
34
+ }
35
+
36
+ return {
37
+ redis,
38
+ flushNamespace,
39
+ cleanup: async () => {
40
+ await flushNamespace();
41
+ redis.disconnect();
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,168 @@
1
+ import type { Hono } from "hono";
2
+ import type { JwtHelper } from "../api/jwt";
3
+ import type { SessionUser } from "../engine/types";
4
+
5
+ export type BatchCommand = { type: string; payload: unknown };
6
+
7
+ export type RequestHelper = {
8
+ write: (
9
+ type: string,
10
+ payload: unknown,
11
+ user: SessionUser,
12
+ requestId?: string,
13
+ ) => Promise<Response>;
14
+ query: (type: string, payload: unknown, user: SessionUser) => Promise<Response>;
15
+ command: (type: string, payload: unknown, user: SessionUser) => Promise<Response>;
16
+ batch: (
17
+ commands: readonly BatchCommand[],
18
+ user: SessionUser,
19
+ requestId?: string,
20
+ ) => Promise<Response>;
21
+ raw: (
22
+ method: string,
23
+ path: string,
24
+ body?: unknown,
25
+ headers?: Record<string, string>,
26
+ ) => Promise<Response>;
27
+
28
+ /** write + json + assert isSuccess — returns data directly */
29
+ writeOk: <T = Record<string, unknown>>(
30
+ type: string,
31
+ payload: unknown,
32
+ user: SessionUser,
33
+ requestId?: string,
34
+ ) => Promise<T>;
35
+ /** write + json + assert isSuccess === false — returns the structured
36
+ * WriteErrorInfo with `httpStatus` filled in from the HTTP response. */
37
+ writeErr: (
38
+ type: string,
39
+ payload: unknown,
40
+ user: SessionUser,
41
+ ) => Promise<import("../errors").WriteErrorInfo>;
42
+ /** query + json — returns data directly */
43
+ queryOk: <T = unknown>(type: string, payload: unknown, user: SessionUser) => Promise<T>;
44
+
45
+ /** write + additional HTTP headers (e.g. X-Correlation-ID). Returns the
46
+ * raw Response so callers can assert on status + headers + body as needed. */
47
+ writeWithHeaders: (
48
+ type: string,
49
+ payload: unknown,
50
+ user: SessionUser,
51
+ extraHeaders: Record<string, string>,
52
+ ) => Promise<Response>;
53
+ };
54
+
55
+ export function createRequestHelper(app: Hono, jwt: JwtHelper): RequestHelper {
56
+ async function authHeader(user: SessionUser): Promise<Record<string, string>> {
57
+ const token = await jwt.sign(user);
58
+ return { Authorization: `Bearer ${token}` };
59
+ }
60
+
61
+ async function req(
62
+ method: string,
63
+ path: string,
64
+ body?: unknown,
65
+ headers?: Record<string, string>,
66
+ ): Promise<Response> {
67
+ const init: RequestInit = {
68
+ method,
69
+ headers: { "Content-Type": "application/json", ...headers },
70
+ };
71
+ if (body) init.body = JSON.stringify(body);
72
+ return app.request(path, init);
73
+ }
74
+
75
+ async function writeRaw(
76
+ type: string,
77
+ payload: unknown,
78
+ user: SessionUser,
79
+ requestId?: string,
80
+ ): Promise<Response> {
81
+ const headers = await authHeader(user);
82
+ return req("POST", "/api/write", { type, payload, requestId }, headers);
83
+ }
84
+
85
+ async function queryRaw(type: string, payload: unknown, user: SessionUser): Promise<Response> {
86
+ const headers = await authHeader(user);
87
+ return req("POST", "/api/query", { type, payload }, headers);
88
+ }
89
+
90
+ return {
91
+ write: writeRaw,
92
+ query: queryRaw,
93
+
94
+ async command(type, payload, user) {
95
+ const headers = await authHeader(user);
96
+ return req("POST", "/api/command", { type, payload }, headers);
97
+ },
98
+
99
+ async batch(commands, user, requestId) {
100
+ const headers = await authHeader(user);
101
+ return req("POST", "/api/batch", { commands, requestId }, headers);
102
+ },
103
+
104
+ raw: req,
105
+
106
+ async writeOk<T = Record<string, unknown>>(
107
+ type: string,
108
+ payload: unknown,
109
+ user: SessionUser,
110
+ requestId?: string,
111
+ ): Promise<T> {
112
+ const res = await writeRaw(type, payload, user, requestId);
113
+ // wire-body shape direkt nach JSON.parse — Caller-Code prüft danach
114
+ // selber ob isSuccess/error/data tatsächlich da sind.
115
+ const body = (await res.json()) as {
116
+ isSuccess?: boolean;
117
+ data?: unknown;
118
+ error?: { code?: string } | string;
119
+ };
120
+ // Success path still has { isSuccess: true, data }. Error responses now
121
+ // follow the error-contract shape { error: { code, i18nKey, ... } } with
122
+ // a 4xx/5xx status — no isSuccess flag. Detect either.
123
+ if (body.isSuccess !== true) {
124
+ const code =
125
+ (typeof body.error === "object" ? body.error?.code : undefined) ??
126
+ (typeof body.error === "string" ? body.error : "unknown");
127
+ throw new Error(`Expected write "${type}" to succeed but got error: ${code}`);
128
+ }
129
+ return body.data as T;
130
+ },
131
+
132
+ async writeErr(
133
+ type: string,
134
+ payload: unknown,
135
+ user: SessionUser,
136
+ ): Promise<import("../errors").WriteErrorInfo> {
137
+ const res = await writeRaw(type, payload, user);
138
+ const body = (await res.json()) as {
139
+ isSuccess?: boolean;
140
+ error?: Omit<import("../errors").WriteErrorInfo, "httpStatus">;
141
+ };
142
+ if (body.isSuccess === true) {
143
+ throw new Error(`Expected write "${type}" to fail but it succeeded`);
144
+ }
145
+ const wire = body.error;
146
+ if (!wire || typeof wire !== "object" || typeof wire.code !== "string") {
147
+ throw new Error(
148
+ `Expected error response for "${type}" but got unexpected shape: ${JSON.stringify(body)}`,
149
+ );
150
+ }
151
+ // The wire body doesn't carry httpStatus (it would be redundant with
152
+ // the HTTP response status). Fill it in from res.status so callers can
153
+ // assert against either code OR status without a second request round.
154
+ return { ...wire, httpStatus: res.status };
155
+ },
156
+
157
+ async queryOk<T = unknown>(type: string, payload: unknown, user: SessionUser): Promise<T> {
158
+ const res = await queryRaw(type, payload, user);
159
+ const body = (await res.json()) as { data: unknown };
160
+ return body.data as T;
161
+ },
162
+
163
+ async writeWithHeaders(type, payload, user, extraHeaders) {
164
+ const authHeaders = await authHeader(user);
165
+ return req("POST", "/api/write", { type, payload }, { ...authHeaders, ...extraHeaders });
166
+ },
167
+ };
168
+ }
@@ -0,0 +1,104 @@
1
+ import { generateDrizzleJson, generateMigration } from "drizzle-kit/api";
2
+ import { getTableName, sql } from "drizzle-orm";
3
+ import type { PgTable } from "drizzle-orm/pg-core";
4
+ import type { drizzle } from "drizzle-orm/postgres-js";
5
+ import { tableExists } from "../db/schema-inspection";
6
+ import { buildDrizzleTable, toTableName } from "../db/table-builder";
7
+ import type { TestStack } from "./test-stack";
8
+
9
+ /**
10
+ * Syncs a Drizzle table to the database via drizzle-kit migration.
11
+ * No manual SQL — Drizzle generates CREATE/ALTER TABLE statements.
12
+ * Strict: raises a postgres `relation already exists` (42P07) error if
13
+ * the table is already there. Use `ensureEntityTable` for idempotent
14
+ * boot paths.
15
+ */
16
+ export async function createEntityTable(
17
+ db: ReturnType<typeof drizzle>,
18
+ entity: import("../engine/types").EntityDefinition,
19
+ entityName?: string,
20
+ ): Promise<void> {
21
+ const table = buildDrizzleTable(entityName ?? "entity", entity);
22
+ await pushTables(db, { [entityName ?? "entity"]: table });
23
+ }
24
+
25
+ /**
26
+ * Idempotent variant of `createEntityTable`: checks whether the entity's
27
+ * table already exists and skips creation if so. Schema-drift is *not*
28
+ * detected — if the table is there but has the wrong columns, that's
29
+ * the caller's problem (the dev-server contract is "drop the DB by
30
+ * hand when you change the schema"). Tests should use
31
+ * `createEntityTable` instead, since they rely on fresh DBs.
32
+ */
33
+ export async function ensureEntityTable(
34
+ db: ReturnType<typeof drizzle>,
35
+ entity: import("../engine/types").EntityDefinition,
36
+ entityName?: string,
37
+ ): Promise<boolean> {
38
+ const resolvedName = entity.table ?? toTableName(entityName ?? "entity");
39
+ if (await tableExists(db, `public.${resolvedName}`)) return false;
40
+ await createEntityTable(db, entity, entityName);
41
+ return true;
42
+ }
43
+
44
+ /**
45
+ * Pushes Drizzle table definitions to the database.
46
+ * Uses drizzle-kit's generateDrizzleJson + generateMigration to produce SQL,
47
+ * then executes it. Same SQL that `drizzle-kit push` would generate.
48
+ *
49
+ * @param prevTables - Previous table definitions (for ALTER TABLE scenarios).
50
+ * If omitted, assumes empty DB (CREATE TABLE).
51
+ */
52
+ export async function pushTables(
53
+ db: ReturnType<typeof drizzle>,
54
+ tables: Record<string, unknown>,
55
+ prevTables?: Record<string, unknown>,
56
+ ): Promise<void> {
57
+ const prevJson = generateDrizzleJson(prevTables ?? {});
58
+ const targetJson = generateDrizzleJson(tables);
59
+ const statements = await generateMigration(prevJson, targetJson);
60
+ for (const stmt of statements) {
61
+ await db.execute(sql.raw(stmt));
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Wipes event store + framework-state + the given feature read-models in
67
+ * one TRUNCATE, then re-registers the event-consumer state rows. Used in
68
+ * test beforeEach-hooks to return the stack to a clean slate without
69
+ * rebuilding it.
70
+ *
71
+ * Fixed list of framework tables (kumiko_events, kumiko_event_consumers,
72
+ * kumiko_archived_streams, kumiko_snapshots, kumiko_projections) is always
73
+ * included — any event-sourced test setup needs those cleared. The
74
+ * `extraTables` arg covers the feature's own read-model tables that would
75
+ * otherwise accumulate rows across tests.
76
+ *
77
+ * Accepts either a Drizzle PgTable (for locally-defined tables: getTableName
78
+ * extracts the SQL name) or a plain string (for SQL names whose Drizzle
79
+ * reference lives in another module and importing it for the TRUNCATE
80
+ * alone would be overkill). Both round-trip to the same TRUNCATE list.
81
+ *
82
+ * Pre-existing code duplicates this block 30+ times, each with its own
83
+ * list of extras. The helper collapses that to a one-liner per test and
84
+ * lets a future change to the framework-table set (e.g. adding a new
85
+ * consumer-state table) ripple through without touching every suite.
86
+ */
87
+ export async function resetEventStore(
88
+ stack: TestStack,
89
+ extraTables: readonly (PgTable | string)[] = [],
90
+ ): Promise<void> {
91
+ const frameworkTables = [
92
+ "kumiko_events",
93
+ "kumiko_event_consumers",
94
+ "kumiko_archived_streams",
95
+ "kumiko_snapshots",
96
+ "kumiko_projections",
97
+ ];
98
+ const extraNames = extraTables.map((t) => (typeof t === "string" ? t : getTableName(t)));
99
+ const allTables = [...frameworkTables, ...extraNames];
100
+ await stack.db.execute(sql.raw(`TRUNCATE ${allTables.join(", ")} RESTART IDENTITY CASCADE`));
101
+ if (stack.eventDispatcher) {
102
+ await stack.eventDispatcher.ensureRegistered();
103
+ }
104
+ }