@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,419 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
2
+ import type { TableColumns } from "../../db/dialect";
3
+ import { createEventStoreExecutor, type EventStoreExecutor } from "../../db/event-store-executor";
4
+ import { buildDrizzleTable } from "../../db/table-builder";
5
+ import { createTenantDb, type TenantDb } from "../../db/tenant-db";
6
+ import {
7
+ createEntity,
8
+ createRegistry,
9
+ createTextField,
10
+ defineFeature,
11
+ type Registry,
12
+ } from "../../engine";
13
+ import { createEventsTable } from "../../event-store";
14
+ import { createEntityTable, createTestDb, type TestDb, TestUsers } from "../../stack";
15
+ import { createCascadeDeleteHook } from "../cascade-handler";
16
+
17
+ // biome-ignore lint/suspicious/noExplicitAny: Drizzle dynamic tables
18
+ type Table = TableColumns<any>;
19
+
20
+ let testDb: TestDb;
21
+ let tdb: TenantDb;
22
+ let registry: Registry;
23
+ let departmentTable: Table;
24
+ let userTable: Table;
25
+ let sessionTable: Table;
26
+ let groupTable: Table;
27
+ let userGroupRestrictTable: Table;
28
+ let userGroupCascadeTable: Table;
29
+ let teamTable: Table;
30
+ let memberTable: Table;
31
+ let departmentExecutor: EventStoreExecutor;
32
+ let userExecutor: EventStoreExecutor;
33
+ let sessionExecutor: EventStoreExecutor;
34
+ let groupExecutor: EventStoreExecutor;
35
+ let userGroupRestrictExecutor: EventStoreExecutor;
36
+ let userGroupCascadeExecutor: EventStoreExecutor;
37
+ let teamExecutor: EventStoreExecutor;
38
+ let memberExecutor: EventStoreExecutor;
39
+
40
+ const admin = TestUsers.admin;
41
+
42
+ const departmentEntity = createEntity({
43
+ table: "cascade_departments",
44
+ fields: { name: createTextField() },
45
+ });
46
+ const userEntity = createEntity({
47
+ table: "cascade_users",
48
+ fields: { name: createTextField(), departmentId: createTextField() },
49
+ });
50
+ const sessionEntity = createEntity({
51
+ table: "cascade_sessions",
52
+ fields: { userId: createTextField(), token: createTextField() },
53
+ });
54
+ const groupEntity = createEntity({
55
+ table: "cascade_groups",
56
+ fields: { name: createTextField() },
57
+ });
58
+ // Two separate junction tables so one Registry can host both onDelete
59
+ // strategies (restrict + cascade) on the same user→group pair.
60
+ const userGroupRestrictEntity = createEntity({
61
+ table: "cascade_user_group_restrict",
62
+ fields: { userId: createTextField(), groupId: createTextField() },
63
+ });
64
+ const userGroupCascadeEntity = createEntity({
65
+ table: "cascade_user_group_cascade",
66
+ fields: { userId: createTextField(), groupId: createTextField() },
67
+ });
68
+ // setNull pair: team→member with onDelete "setNull" — members survive
69
+ // the team deletion with teamId nulled out.
70
+ const teamEntity = createEntity({
71
+ table: "cascade_teams",
72
+ fields: { name: createTextField() },
73
+ });
74
+ const memberEntity = createEntity({
75
+ table: "cascade_members",
76
+ fields: { name: createTextField(), teamId: createTextField() },
77
+ });
78
+
79
+ beforeAll(async () => {
80
+ testDb = await createTestDb();
81
+ await createEventsTable(testDb.db);
82
+ tdb = createTenantDb(testDb.db, admin.tenantId);
83
+
84
+ await createEntityTable(testDb.db, departmentEntity);
85
+ await createEntityTable(testDb.db, userEntity);
86
+ await createEntityTable(testDb.db, sessionEntity);
87
+ await createEntityTable(testDb.db, groupEntity);
88
+ await createEntityTable(testDb.db, userGroupRestrictEntity);
89
+ await createEntityTable(testDb.db, userGroupCascadeEntity);
90
+ await createEntityTable(testDb.db, teamEntity);
91
+ await createEntityTable(testDb.db, memberEntity);
92
+
93
+ departmentTable = buildDrizzleTable("department", departmentEntity);
94
+ userTable = buildDrizzleTable("user", userEntity);
95
+ sessionTable = buildDrizzleTable("session", sessionEntity);
96
+ groupTable = buildDrizzleTable("group", groupEntity);
97
+ userGroupRestrictTable = buildDrizzleTable("user-group-restrict", userGroupRestrictEntity);
98
+ userGroupCascadeTable = buildDrizzleTable("user-group-cascade", userGroupCascadeEntity);
99
+ teamTable = buildDrizzleTable("team", teamEntity);
100
+ memberTable = buildDrizzleTable("member", memberEntity);
101
+
102
+ const feature = defineFeature("cascade", (r) => {
103
+ r.entity("department", departmentEntity);
104
+ r.entity("user", userEntity);
105
+ r.entity("session", sessionEntity);
106
+ r.entity("group", groupEntity);
107
+ r.entity("user-group-restrict", userGroupRestrictEntity);
108
+ r.entity("user-group-cascade", userGroupCascadeEntity);
109
+ r.entity("team", teamEntity);
110
+ r.entity("member", memberEntity);
111
+
112
+ r.relation("department", "users", {
113
+ type: "hasMany",
114
+ target: "user",
115
+ foreignKey: "departmentId",
116
+ onDelete: "restrict",
117
+ });
118
+
119
+ r.relation("user", "sessions", {
120
+ type: "hasMany",
121
+ target: "session",
122
+ foreignKey: "userId",
123
+ onDelete: "cascade",
124
+ });
125
+
126
+ r.relation("user", "groupsRestrict", {
127
+ type: "manyToMany",
128
+ target: "group",
129
+ through: { table: "user-group-restrict", sourceKey: "userId", targetKey: "groupId" },
130
+ onDelete: "restrict",
131
+ });
132
+
133
+ r.relation("user", "groupsCascade", {
134
+ type: "manyToMany",
135
+ target: "group",
136
+ through: { table: "user-group-cascade", sourceKey: "userId", targetKey: "groupId" },
137
+ onDelete: "cascade",
138
+ });
139
+
140
+ r.relation("team", "members", {
141
+ type: "hasMany",
142
+ target: "member",
143
+ foreignKey: "teamId",
144
+ onDelete: "setNull",
145
+ });
146
+ });
147
+
148
+ registry = createRegistry([feature]);
149
+ departmentExecutor = createEventStoreExecutor(departmentTable, departmentEntity, {
150
+ entityName: "department",
151
+ });
152
+ userExecutor = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
153
+ sessionExecutor = createEventStoreExecutor(sessionTable, sessionEntity, {
154
+ entityName: "session",
155
+ });
156
+ groupExecutor = createEventStoreExecutor(groupTable, groupEntity, { entityName: "group" });
157
+ userGroupRestrictExecutor = createEventStoreExecutor(
158
+ userGroupRestrictTable,
159
+ userGroupRestrictEntity,
160
+ { entityName: "user-group-restrict" },
161
+ );
162
+ userGroupCascadeExecutor = createEventStoreExecutor(
163
+ userGroupCascadeTable,
164
+ userGroupCascadeEntity,
165
+ { entityName: "user-group-cascade" },
166
+ );
167
+ teamExecutor = createEventStoreExecutor(teamTable, teamEntity, { entityName: "team" });
168
+ memberExecutor = createEventStoreExecutor(memberTable, memberEntity, { entityName: "member" });
169
+ });
170
+
171
+ afterAll(async () => {
172
+ await testDb.cleanup();
173
+ });
174
+
175
+ describe("cascade delete: restrict", () => {
176
+ test("blocks delete when related records exist", async () => {
177
+ const dept = await departmentExecutor.create({ name: "Engineering" }, admin, tdb);
178
+ if (!dept.isSuccess) throw new Error("Setup failed");
179
+
180
+ await userExecutor.create({ name: "Marc", departmentId: dept.data.id }, admin, tdb);
181
+
182
+ const cascadeHook = createCascadeDeleteHook(registry, new Map([["user", userTable]]));
183
+
184
+ await expect(
185
+ cascadeHook.fn(
186
+ {
187
+ kind: "delete",
188
+ id: dept.data.id,
189
+ data: { tenantId: "00000000-0000-4000-8000-000000000001" },
190
+ entityName: "department",
191
+ },
192
+ { db: tdb },
193
+ ),
194
+ ).rejects.toMatchObject({ code: "conflict", details: { reason: "delete_restricted" } });
195
+ });
196
+
197
+ test("allows delete when no related records", async () => {
198
+ const dept = await departmentExecutor.create({ name: "Empty" }, admin, tdb);
199
+ if (!dept.isSuccess) throw new Error("Setup failed");
200
+
201
+ const cascadeHook = createCascadeDeleteHook(registry, new Map([["user", userTable]]));
202
+
203
+ await expect(
204
+ cascadeHook.fn(
205
+ {
206
+ kind: "delete",
207
+ id: dept.data.id,
208
+ data: { tenantId: "00000000-0000-4000-8000-000000000001" },
209
+ entityName: "department",
210
+ },
211
+ { db: tdb },
212
+ ),
213
+ ).resolves.toBeUndefined();
214
+ });
215
+ });
216
+
217
+ describe("cascade delete: cascade", () => {
218
+ test("deletes related records when parent is deleted", async () => {
219
+ const user = await userExecutor.create({ name: "Cascade User" }, admin, tdb);
220
+ if (!user.isSuccess) throw new Error("Setup failed");
221
+
222
+ await sessionExecutor.create({ userId: user.data.id, token: "abc" }, admin, tdb);
223
+ await sessionExecutor.create({ userId: user.data.id, token: "def" }, admin, tdb);
224
+
225
+ // Verify sessions exist
226
+ const before = await sessionExecutor.list({}, admin, tdb);
227
+ const sessionsBefore = before.rows.filter((r) => r["userId"] === user.data.id);
228
+ expect(sessionsBefore.length).toBe(2);
229
+
230
+ // Run cascade
231
+ const cascadeHook = createCascadeDeleteHook(registry, new Map([["session", sessionTable]]));
232
+
233
+ await cascadeHook.fn(
234
+ {
235
+ kind: "delete",
236
+ id: user.data.id,
237
+ data: { tenantId: "00000000-0000-4000-8000-000000000001" },
238
+ entityName: "user",
239
+ },
240
+ { db: tdb },
241
+ );
242
+
243
+ // Sessions should be gone
244
+ const after = await sessionExecutor.list({}, admin, tdb);
245
+ const sessionsAfter = after.rows.filter((r) => r["userId"] === user.data.id);
246
+ expect(sessionsAfter.length).toBe(0);
247
+ });
248
+ });
249
+
250
+ describe("cascade delete: manyToMany restrict", () => {
251
+ test("blocks delete when through-records exist", async () => {
252
+ const user = await userExecutor.create({ name: "M2M Restrict User" }, admin, tdb);
253
+ const group = await groupExecutor.create({ name: "Admins" }, admin, tdb);
254
+ if (!user.isSuccess || !group.isSuccess) throw new Error("Setup failed");
255
+
256
+ await userGroupRestrictExecutor.create(
257
+ { userId: user.data.id, groupId: group.data.id },
258
+ admin,
259
+ tdb,
260
+ );
261
+
262
+ const cascadeHook = createCascadeDeleteHook(
263
+ registry,
264
+ new Map([["user-group-restrict", userGroupRestrictTable]]),
265
+ );
266
+
267
+ await expect(
268
+ cascadeHook.fn(
269
+ {
270
+ kind: "delete",
271
+ id: user.data.id,
272
+ data: { tenantId: "00000000-0000-4000-8000-000000000001" },
273
+ entityName: "user",
274
+ },
275
+ { db: tdb },
276
+ ),
277
+ ).rejects.toMatchObject({
278
+ code: "conflict",
279
+ details: {
280
+ reason: "delete_restricted",
281
+ blockingEntity: "user-group-restrict",
282
+ },
283
+ });
284
+ });
285
+
286
+ test("allows delete when no through-records reference this entity", async () => {
287
+ const user = await userExecutor.create({ name: "M2M Free User" }, admin, tdb);
288
+ if (!user.isSuccess) throw new Error("Setup failed");
289
+
290
+ const cascadeHook = createCascadeDeleteHook(
291
+ registry,
292
+ new Map([["user-group-restrict", userGroupRestrictTable]]),
293
+ );
294
+
295
+ await expect(
296
+ cascadeHook.fn(
297
+ {
298
+ kind: "delete",
299
+ id: user.data.id,
300
+ data: { tenantId: "00000000-0000-4000-8000-000000000001" },
301
+ entityName: "user",
302
+ },
303
+ { db: tdb },
304
+ ),
305
+ ).resolves.toBeUndefined();
306
+ });
307
+ });
308
+
309
+ describe("cascade delete: manyToMany cascade", () => {
310
+ test("deletes through-records but keeps target entities", async () => {
311
+ const user = await userExecutor.create({ name: "M2M Cascade User" }, admin, tdb);
312
+ const groupA = await groupExecutor.create({ name: "Group A" }, admin, tdb);
313
+ const groupB = await groupExecutor.create({ name: "Group B" }, admin, tdb);
314
+ if (!user.isSuccess || !groupA.isSuccess || !groupB.isSuccess) throw new Error("Setup failed");
315
+
316
+ await userGroupCascadeExecutor.create(
317
+ { userId: user.data.id, groupId: groupA.data.id },
318
+ admin,
319
+ tdb,
320
+ );
321
+ await userGroupCascadeExecutor.create(
322
+ { userId: user.data.id, groupId: groupB.data.id },
323
+ admin,
324
+ tdb,
325
+ );
326
+
327
+ const before = await userGroupCascadeExecutor.list({}, admin, tdb);
328
+ expect(before.rows.filter((r) => r["userId"] === user.data.id).length).toBe(2);
329
+
330
+ const cascadeHook = createCascadeDeleteHook(
331
+ registry,
332
+ new Map([["user-group-cascade", userGroupCascadeTable]]),
333
+ );
334
+
335
+ await cascadeHook.fn(
336
+ {
337
+ kind: "delete",
338
+ id: user.data.id,
339
+ data: { tenantId: "00000000-0000-4000-8000-000000000001" },
340
+ entityName: "user",
341
+ },
342
+ { db: tdb },
343
+ );
344
+
345
+ // Through-records for this user must be gone
346
+ const after = await userGroupCascadeExecutor.list({}, admin, tdb);
347
+ expect(after.rows.filter((r) => r["userId"] === user.data.id).length).toBe(0);
348
+
349
+ // Target groups themselves must remain — cascade drops the M:N link,
350
+ // not the referenced entities.
351
+ const groups = await groupExecutor.list({}, admin, tdb);
352
+ const groupIds = groups.rows.map((r) => r["id"]);
353
+ expect(groupIds).toContain(groupA.data.id);
354
+ expect(groupIds).toContain(groupB.data.id);
355
+ });
356
+ });
357
+
358
+ describe("cascade delete: hasMany setNull", () => {
359
+ test("nulls out FK on related records when parent is deleted", async () => {
360
+ const team = await teamExecutor.create({ name: "SetNull Team" }, admin, tdb);
361
+ if (!team.isSuccess) throw new Error("Setup failed");
362
+
363
+ const m1 = await memberExecutor.create({ name: "Alice", teamId: team.data.id }, admin, tdb);
364
+ const m2 = await memberExecutor.create({ name: "Bob", teamId: team.data.id }, admin, tdb);
365
+ if (!m1.isSuccess || !m2.isSuccess) throw new Error("Setup failed");
366
+
367
+ // Verify FK is set before cascade
368
+ const before = await memberExecutor.list({}, admin, tdb);
369
+ const teamMembers = before.rows.filter((r) => r["id"] === m1.data.id || r["id"] === m2.data.id);
370
+ expect(teamMembers.every((r) => r["teamId"] === team.data.id)).toBe(true);
371
+
372
+ const cascadeHook = createCascadeDeleteHook(registry, new Map([["member", memberTable]]));
373
+
374
+ await cascadeHook.fn(
375
+ {
376
+ kind: "delete",
377
+ id: team.data.id,
378
+ data: { tenantId: "00000000-0000-4000-8000-000000000001" },
379
+ entityName: "team",
380
+ },
381
+ { db: tdb },
382
+ );
383
+
384
+ // Members still exist, but teamId is now null
385
+ const after = await memberExecutor.list({}, admin, tdb);
386
+ const afterMembers = after.rows.filter((r) => r["id"] === m1.data.id || r["id"] === m2.data.id);
387
+ expect(afterMembers.length).toBe(2);
388
+ expect(afterMembers.every((r) => r["teamId"] === null)).toBe(true);
389
+ });
390
+
391
+ test("leaves unrelated records untouched", async () => {
392
+ const teamA = await teamExecutor.create({ name: "Team A" }, admin, tdb);
393
+ const teamB = await teamExecutor.create({ name: "Team B" }, admin, tdb);
394
+ if (!teamA.isSuccess || !teamB.isSuccess) throw new Error("Setup failed");
395
+
396
+ const mA = await memberExecutor.create({ name: "A-member", teamId: teamA.data.id }, admin, tdb);
397
+ const mB = await memberExecutor.create({ name: "B-member", teamId: teamB.data.id }, admin, tdb);
398
+ if (!mA.isSuccess || !mB.isSuccess) throw new Error("Setup failed");
399
+
400
+ const cascadeHook = createCascadeDeleteHook(registry, new Map([["member", memberTable]]));
401
+
402
+ // Delete team A — only mA should lose its teamId, mB must stay intact
403
+ await cascadeHook.fn(
404
+ {
405
+ kind: "delete",
406
+ id: teamA.data.id,
407
+ data: { tenantId: "00000000-0000-4000-8000-000000000001" },
408
+ entityName: "team",
409
+ },
410
+ { db: tdb },
411
+ );
412
+
413
+ const after = await memberExecutor.list({}, admin, tdb);
414
+ const aAfter = after.rows.find((r) => r["id"] === mA.data.id);
415
+ const bAfter = after.rows.find((r) => r["id"] === mB.data.id);
416
+ expect(aAfter?.["teamId"]).toBeNull();
417
+ expect(bAfter?.["teamId"]).toBe(teamB.data.id);
418
+ });
419
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
3
+
4
+ describe("getIncomingRelations", () => {
5
+ const feature = defineFeature("core", (r) => {
6
+ r.entity(
7
+ "department",
8
+ createEntity({ table: "Departments", fields: { name: createTextField() } }),
9
+ );
10
+ r.entity("user", createEntity({ table: "Users", fields: { departmentId: createTextField() } }));
11
+ r.entity("session", createEntity({ table: "Sessions", fields: { userId: createTextField() } }));
12
+
13
+ r.relation("department", "users", {
14
+ type: "hasMany",
15
+ target: "user",
16
+ foreignKey: "departmentId",
17
+ onDelete: "restrict",
18
+ });
19
+ r.relation("user", "sessions", {
20
+ type: "hasMany",
21
+ target: "session",
22
+ foreignKey: "userId",
23
+ onDelete: "cascade",
24
+ });
25
+ });
26
+
27
+ const registry = createRegistry([feature]);
28
+
29
+ test("finds hasMany relation pointing to user from department", () => {
30
+ const incoming = registry.getIncomingRelations("user");
31
+ const match = incoming.find(
32
+ (r) => r.sourceEntity === "department" && r.relation.type === "hasMany",
33
+ );
34
+ expect(match?.relation.type === "hasMany" && match.relation.onDelete).toBe("restrict");
35
+ });
36
+
37
+ test("finds hasMany relation pointing to session from user", () => {
38
+ const incoming = registry.getIncomingRelations("session");
39
+ expect(incoming).toHaveLength(1);
40
+ const rel = incoming[0]?.relation;
41
+ expect(rel?.type === "hasMany" && rel.onDelete).toBe("cascade");
42
+ });
43
+
44
+ test("no incoming relations for department", () => {
45
+ expect(registry.getIncomingRelations("department")).toEqual([]);
46
+ });
47
+
48
+ test("onDelete strategy preserved", () => {
49
+ const rel = registry.getRelations("department")["users"];
50
+ expect(rel?.type === "hasMany" && rel.onDelete).toBe("restrict");
51
+ });
52
+ });
@@ -0,0 +1,206 @@
1
+ // Runde 2 — correlationId + causationId propagation.
2
+ //
3
+ // Claims pinned here:
4
+ // 1. Root HTTP request without x-correlation-id → correlationId == requestId,
5
+ // causationId absent.
6
+ // 2. Root with x-correlation-id header → correlationId == header value,
7
+ // stamped on every event the request writes (CRUD + ctx.appendEvent).
8
+ // 3. The event-dispatcher wraps MSP-apply in requestContext.run so downstream
9
+ // writes from the apply inherit correlationId and set causationId to the
10
+ // triggering event.id.
11
+ //
12
+ // Note on MSP → new events: this test predates Runde 3 / C.2b. Claim 3 is
13
+ // observable via `requestContext.get()` inside the apply — the wrap carries
14
+ // the right values even when the apply doesn't call ctx.appendEvent.
15
+ // The active propagation into cascaded writes is covered by
16
+ // msp-multi-hop.integration.ts.
17
+
18
+ import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
19
+ import { z } from "zod";
20
+ import { requestContext } from "../../api/request-context";
21
+ import { createEventStoreExecutor } from "../../db/event-store-executor";
22
+ import { buildDrizzleTable } from "../../db/table-builder";
23
+ import { createEntity, createTextField, defineFeature } from "../../engine";
24
+ import { eventsTable } from "../../event-store";
25
+ import {
26
+ createEntityTable,
27
+ resetEventStore,
28
+ setupTestStack,
29
+ type TestStack,
30
+ TestUsers,
31
+ } from "../../stack";
32
+
33
+ // --- Feature ---
34
+
35
+ const orderEntity = createEntity({
36
+ table: "read_causation_orders",
37
+ fields: {
38
+ item: createTextField({ required: true }),
39
+ },
40
+ });
41
+
42
+ const orderTable = buildDrizzleTable("causation-order", orderEntity);
43
+
44
+ // MSP-apply observation sink — every apply run pushes its reqCtx snapshot
45
+ // here so the tests can assert what the event-dispatcher wrapped it with.
46
+ type ReqCtxSnapshot = {
47
+ readonly forEventId: string;
48
+ readonly correlationId: string | undefined;
49
+ readonly causationId: string | undefined;
50
+ };
51
+ const applyObservations: ReqCtxSnapshot[] = [];
52
+
53
+ const causationFeature = defineFeature("causation", (r) => {
54
+ r.entity("causation-order", orderEntity);
55
+
56
+ const placed = r.defineEvent("placed", z.object({ orderId: z.uuid() }));
57
+
58
+ const orderExecutor = createEventStoreExecutor(orderTable, orderEntity, {
59
+ entityName: "causation-order",
60
+ });
61
+
62
+ r.writeHandler(
63
+ "order:place",
64
+ z.object({ item: z.string() }),
65
+ async (event, ctx) => {
66
+ const created = await orderExecutor.create({ item: event.payload.item }, event.user, ctx.db);
67
+ if (!created.isSuccess) return created;
68
+ await ctx.appendEventUnsafe({
69
+ aggregateId: String(created.data.id),
70
+ aggregateType: "causation-order",
71
+ type: placed.name,
72
+ payload: { orderId: String(created.data.id) },
73
+ });
74
+ return created;
75
+ },
76
+ { access: { roles: ["Admin"] } },
77
+ );
78
+
79
+ // Observation-only MSP — records what the dispatcher's requestContext.run
80
+ // wrap set for each event. Proves that the wrap is live and carries the
81
+ // triggering event's id as causationId.
82
+ r.multiStreamProjection({
83
+ name: "observer",
84
+ apply: {
85
+ [placed.name]: async (event) => {
86
+ const ctx = requestContext.get();
87
+ applyObservations.push({
88
+ forEventId: String(event.id),
89
+ correlationId: ctx?.correlationId,
90
+ causationId: ctx?.causationId,
91
+ });
92
+ },
93
+ },
94
+ });
95
+ });
96
+
97
+ // --- Stack ---
98
+
99
+ let stack: TestStack;
100
+ const admin = TestUsers.admin;
101
+
102
+ beforeAll(async () => {
103
+ stack = await setupTestStack({
104
+ features: [causationFeature],
105
+ systemHooks: [],
106
+ });
107
+ await createEntityTable(stack.db, orderEntity, "causation-order");
108
+ });
109
+
110
+ afterAll(async () => {
111
+ await stack.cleanup();
112
+ });
113
+
114
+ afterEach(async () => {
115
+ applyObservations.length = 0;
116
+ await resetEventStore(stack, ["read_causation_orders"]);
117
+ });
118
+
119
+ // --- Helpers ---
120
+
121
+ type EventRow = typeof eventsTable.$inferSelect;
122
+
123
+ async function eventsByType(type: string): Promise<EventRow[]> {
124
+ const rows = await stack.db.select().from(eventsTable);
125
+ return rows.filter((r) => r.type === type);
126
+ }
127
+
128
+ // --- Tests ---
129
+
130
+ describe("Runde 2 — correlationId on root HTTP request", () => {
131
+ test("no x-correlation-id: correlationId mirrors requestId, causationId absent", async () => {
132
+ await stack.http.writeOk("causation:write:order:place", { item: "widget" }, admin);
133
+
134
+ const [placedEvent] = await eventsByType("causation:event:placed");
135
+ expect(placedEvent).toBeDefined();
136
+ const meta = placedEvent?.metadata as {
137
+ requestId?: string;
138
+ correlationId?: string;
139
+ causationId?: string;
140
+ };
141
+ // Default: correlationId == requestId so single-call tracing works
142
+ // without any client co-operation.
143
+ expect(meta.correlationId).toBeDefined();
144
+ expect(meta.requestId).toBe(meta.correlationId);
145
+ expect(meta.causationId).toBeUndefined();
146
+ });
147
+
148
+ test("with x-correlation-id header: every event this request writes carries the header value", async () => {
149
+ const res = await stack.http.writeWithHeaders(
150
+ "causation:write:order:place",
151
+ { item: "sprocket" },
152
+ admin,
153
+ { "X-Correlation-ID": "test-chain-abc123" },
154
+ );
155
+ expect(res.status).toBe(200);
156
+
157
+ // The handler writes TWO events: one CRUD (causationOrder.created) and
158
+ // one domain (causation:event:placed). Both share the correlationId.
159
+ const crudEvent = (await eventsByType("causation-order.created"))[0];
160
+ const placedEvent = (await eventsByType("causation:event:placed"))[0];
161
+
162
+ expect((crudEvent?.metadata as { correlationId?: string })?.correlationId).toBe(
163
+ "test-chain-abc123",
164
+ );
165
+ expect((placedEvent?.metadata as { correlationId?: string })?.correlationId).toBe(
166
+ "test-chain-abc123",
167
+ );
168
+ });
169
+
170
+ test("response echoes x-correlation-id back in the same header", async () => {
171
+ const res = await stack.http.writeWithHeaders(
172
+ "causation:write:order:place",
173
+ { item: "rotor" },
174
+ admin,
175
+ { "X-Correlation-ID": "echo-me-xyz" },
176
+ );
177
+ expect(res.headers.get("x-correlation-id")).toBe("echo-me-xyz");
178
+ });
179
+ });
180
+
181
+ describe("Runde 2 — event-dispatcher propagates correlation + causation to MSP-apply", () => {
182
+ test("MSP-apply sees the triggering event.id as causationId and inherits correlationId", async () => {
183
+ // Root write with a known correlation token.
184
+ await stack.http.writeWithHeaders("causation:write:order:place", { item: "gasket" }, admin, {
185
+ "X-Correlation-ID": "msp-chain-token",
186
+ });
187
+
188
+ // Drain the dispatcher — MSP-apply fires, pushes its reqCtx snapshot.
189
+ await stack.eventDispatcher?.runOnce();
190
+
191
+ // Find the placed event by row id (BigInt). Its id is what the MSP
192
+ // should have seen as causationId.
193
+ const [placedEvent] = await eventsByType("causation:event:placed");
194
+ expect(placedEvent).toBeDefined();
195
+ const placedId = String(placedEvent?.id);
196
+
197
+ // Observation recorded inside the MSP apply.
198
+ expect(applyObservations).toHaveLength(1);
199
+ const obs = applyObservations[0];
200
+ expect(obs?.forEventId).toBe(placedId);
201
+ // Correlation inherited from the triggering event.
202
+ expect(obs?.correlationId).toBe("msp-chain-token");
203
+ // Causation = the id of the event that triggered this apply.
204
+ expect(obs?.causationId).toBe(placedId);
205
+ });
206
+ });