@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,361 @@
1
+ // Admin-API integration tests — appendRaw + appendRawBatch.
2
+ //
3
+ // Contract (Prod-Readiness Welle 3, Step 3.1):
4
+ // - Pipeline-Bypass: no pg_notify, no projection, no SSE/Search/Audit.
5
+ // - Historical timestamps preserved: createdAt + createdBy flow through
6
+ // from caller parameter to DB row unchanged.
7
+ // - Version-check kept: UNIQUE (tenant_id, aggregate_id, version) catches
8
+ // duplicates; predecessor-EXISTS for expectedVersion > 0 catches gaps.
9
+ // - Batch: single INSERT with multi-VALUES; atomic rollback on any
10
+ // failure; predecessor pre-flight per aggregate in the batch.
11
+
12
+ import { sql } from "drizzle-orm";
13
+ import { drizzle } from "drizzle-orm/postgres-js";
14
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
15
+ import { createTestDb, type TestDb } from "../../stack";
16
+ import { generateId as uuid } from "../../utils";
17
+ import { appendRaw, appendRawBatch, type RawEventToAppend } from "../admin-api";
18
+ import { VersionConflictError } from "../errors";
19
+ import { append, loadAggregate } from "../event-store";
20
+ import { createEventsTable } from "../events-schema";
21
+
22
+ let testDb: TestDb;
23
+
24
+ const tenantA = uuid();
25
+ const userMigration = "migration-importer";
26
+ const legacyUser = "legacy-user-42";
27
+
28
+ beforeAll(async () => {
29
+ testDb = await createTestDb();
30
+ await createEventsTable(testDb.db);
31
+ });
32
+
33
+ afterAll(async () => {
34
+ await testDb.cleanup();
35
+ });
36
+
37
+ beforeEach(async () => {
38
+ await testDb.db.execute(sql`TRUNCATE kumiko_events RESTART IDENTITY`);
39
+ });
40
+
41
+ function makeEvent(partial: Partial<RawEventToAppend> = {}): RawEventToAppend {
42
+ return {
43
+ aggregateId: partial.aggregateId ?? uuid(),
44
+ aggregateType: partial.aggregateType ?? "legacy-order",
45
+ tenantId: partial.tenantId ?? tenantA,
46
+ expectedVersion: partial.expectedVersion ?? 0,
47
+ type: partial.type ?? "legacy.order.created",
48
+ payload: partial.payload ?? { legacyId: 100 },
49
+ metadata: partial.metadata ?? { userId: userMigration, requestId: "import-batch-1" },
50
+ createdAt: partial.createdAt ?? Temporal.Instant.from("2023-01-15T10:00:00Z"),
51
+ createdBy: partial.createdBy ?? legacyUser,
52
+ eventVersion: partial.eventVersion,
53
+ };
54
+ }
55
+
56
+ describe("appendRaw — single event", () => {
57
+ test("preserves historical createdAt (NOT now())", async () => {
58
+ // Sub-second precision matters for migration: Legacy events with
59
+ // distinct millisecond timestamps must keep their exact ordering.
60
+ // Comparing via Temporal.Instant (not Date → isoString) preserves
61
+ // the full precision the `instant()` column round-trips.
62
+ const historicTs = Temporal.Instant.from("2021-06-03T14:22:10.123Z");
63
+ const aggregateId = uuid();
64
+
65
+ await appendRaw(testDb.db, makeEvent({ aggregateId, createdAt: historicTs }));
66
+
67
+ // loadAggregate uses the typed-builder path → createdAt already comes
68
+ // back as Temporal.Instant. Compare via epochMilliseconds for an exact
69
+ // moment-level match (no Date-roundtrip precision loss).
70
+ const [stored] = await loadAggregate(testDb.db, aggregateId, tenantA);
71
+ expect(stored).toBeDefined();
72
+ expect(stored!.createdAt.epochMilliseconds).toBe(historicTs.epochMilliseconds);
73
+ });
74
+
75
+ test("preserves historical createdBy (NOT metadata.userId)", async () => {
76
+ const aggregateId = uuid();
77
+ // Migration-runner's own id lives in metadata.userId; the legacy actor
78
+ // lives in createdBy. These are DIFFERENT for an import — the raw-path
79
+ // must NOT conflate them.
80
+ await appendRaw(
81
+ testDb.db,
82
+ makeEvent({
83
+ aggregateId,
84
+ createdBy: legacyUser,
85
+ metadata: { userId: userMigration, requestId: "import-batch-1" },
86
+ }),
87
+ );
88
+
89
+ const rows = await testDb.db.execute<{ created_by: string }>(sql`
90
+ SELECT created_by FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid
91
+ `);
92
+ expect(rows[0]?.created_by).toBe(legacyUser);
93
+ expect(rows[0]?.created_by).not.toBe(userMigration);
94
+ });
95
+
96
+ test("events written via appendRaw are structurally identical to append — full payload/metadata round-trip", async () => {
97
+ const aggregateId = uuid();
98
+ const payload = { legacyId: 7, state: "Accepted", nested: { amount: "12.50" } };
99
+ const metadata = {
100
+ userId: userMigration,
101
+ requestId: "import-batch-2",
102
+ correlationId: "legacy-order-7",
103
+ headers: { source: "beammycar-prod-dump" },
104
+ };
105
+
106
+ await appendRaw(
107
+ testDb.db,
108
+ makeEvent({
109
+ aggregateId,
110
+ type: "legacy.order.accepted",
111
+ payload,
112
+ metadata,
113
+ }),
114
+ );
115
+
116
+ const rows = await testDb.db.execute<{
117
+ payload: Record<string, unknown>;
118
+ metadata: Record<string, unknown>;
119
+ }>(sql`
120
+ SELECT payload, metadata FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid
121
+ `);
122
+ expect(rows[0]?.payload).toEqual(payload);
123
+ expect(rows[0]?.metadata).toEqual(metadata);
124
+ });
125
+
126
+ test("does NOT fire pg_notify on EVENTS_PUBSUB_CHANNEL — contrast with append", async () => {
127
+ // Open a dedicated LISTEN connection. postgres-js exposes listen() on the
128
+ // client; the callback is invoked per NOTIFY payload. The resolved value
129
+ // is a meta-object with an `unlisten` method, not a plain function.
130
+ const notifications: string[] = [];
131
+ const subscription = await testDb.client.listen("kumiko_events_new", (payload) => {
132
+ notifications.push(payload);
133
+ });
134
+
135
+ try {
136
+ // appendRaw path — MUST NOT fire.
137
+ await appendRaw(testDb.db, makeEvent());
138
+ // Give the event-loop a moment; LISTEN delivery is async but within
139
+ // the same tick usually.
140
+ await new Promise((r) => setTimeout(r, 50));
141
+ expect(notifications).toHaveLength(0);
142
+
143
+ // Control: regular append() DOES fire. Same test-DB, same LISTEN.
144
+ await append(testDb.db, {
145
+ aggregateId: uuid(),
146
+ aggregateType: "control",
147
+ tenantId: tenantA,
148
+ expectedVersion: 0,
149
+ type: "control.created",
150
+ payload: {},
151
+ metadata: { userId: userMigration },
152
+ });
153
+ await new Promise((r) => setTimeout(r, 50));
154
+ expect(notifications.length).toBeGreaterThan(0);
155
+ } finally {
156
+ await subscription.unlisten();
157
+ }
158
+ });
159
+
160
+ test("version_conflict on duplicate (aggregateId, expectedVersion)", async () => {
161
+ const aggregateId = uuid();
162
+ await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 0 }));
163
+
164
+ await expect(
165
+ appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 0 })),
166
+ ).rejects.toBeInstanceOf(VersionConflictError);
167
+ });
168
+
169
+ test("version_conflict on missing predecessor (appendRaw v=5 without v=1..4)", async () => {
170
+ const aggregateId = uuid();
171
+ // Try to write version=5 (expectedVersion=4) against an empty stream.
172
+ // Predecessor check must catch this — otherwise orphaned events would
173
+ // land in the DB.
174
+ await expect(
175
+ appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 4 })),
176
+ ).rejects.toBeInstanceOf(VersionConflictError);
177
+
178
+ // Sanity: no row landed.
179
+ const rows = await testDb.db.execute<{ c: number }>(sql`
180
+ SELECT count(*)::int as c FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid
181
+ `);
182
+ expect(rows[0]?.c).toBe(0);
183
+ });
184
+
185
+ test("appendRaw writes version = expectedVersion + 1 — matches append semantics", async () => {
186
+ const aggregateId = uuid();
187
+ await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 0 }));
188
+ await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 1 }));
189
+ await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 2 }));
190
+
191
+ const rows = await testDb.db.execute<{ version: number }>(sql`
192
+ SELECT version FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid ORDER BY version
193
+ `);
194
+ expect(rows.map((r) => r.version)).toEqual([1, 2, 3]);
195
+ });
196
+ });
197
+
198
+ describe("appendRawBatch — multi-event", () => {
199
+ test("writes all events in a single INSERT statement (query-log spy)", async () => {
200
+ const queries: string[] = [];
201
+ const loggedDb = drizzle(testDb.client, {
202
+ logger: {
203
+ logQuery: (q) => queries.push(q),
204
+ },
205
+ });
206
+
207
+ const aggregateId = uuid();
208
+ const events: readonly RawEventToAppend[] = [
209
+ makeEvent({ aggregateId, expectedVersion: 0, type: "legacy.order.created" }),
210
+ makeEvent({ aggregateId, expectedVersion: 1, type: "legacy.order.accepted" }),
211
+ makeEvent({ aggregateId, expectedVersion: 2, type: "legacy.order.canceled" }),
212
+ ];
213
+
214
+ await appendRawBatch(loggedDb, events);
215
+
216
+ const inserts = queries.filter((q) => /insert\s+into\s+"?kumiko_events"?/i.test(q));
217
+ expect(inserts).toHaveLength(1);
218
+
219
+ // All three events persisted with ascending versions.
220
+ const rows = await testDb.db.execute<{ version: number; type: string }>(sql`
221
+ SELECT version, type FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid ORDER BY version
222
+ `);
223
+ expect(rows.map((r) => ({ v: r.version, t: r.type }))).toEqual([
224
+ { v: 1, t: "legacy.order.created" },
225
+ { v: 2, t: "legacy.order.accepted" },
226
+ { v: 3, t: "legacy.order.canceled" },
227
+ ]);
228
+ });
229
+
230
+ test("preserves per-event historical createdAt across the batch", async () => {
231
+ // Three distinct sub-second timestamps: verifies the batch INSERT path
232
+ // doesn't collapse them to now() or to a single batch-timestamp.
233
+ const aggregateId = uuid();
234
+ const t1 = Temporal.Instant.from("2020-03-01T08:00:00.111Z");
235
+ const t2 = Temporal.Instant.from("2020-03-02T09:30:00.222Z");
236
+ const t3 = Temporal.Instant.from("2020-03-05T12:15:45.333Z");
237
+
238
+ await appendRawBatch(testDb.db, [
239
+ makeEvent({ aggregateId, expectedVersion: 0, createdAt: t1 }),
240
+ makeEvent({ aggregateId, expectedVersion: 1, createdAt: t2 }),
241
+ makeEvent({ aggregateId, expectedVersion: 2, createdAt: t3 }),
242
+ ]);
243
+
244
+ const stored = await loadAggregate(testDb.db, aggregateId, tenantA);
245
+ expect(stored.map((s) => s.createdAt.epochMilliseconds)).toEqual([
246
+ t1.epochMilliseconds,
247
+ t2.epochMilliseconds,
248
+ t3.epochMilliseconds,
249
+ ]);
250
+ });
251
+
252
+ test("atomic rollback: if ANY event in the batch conflicts, NO events land in the DB", async () => {
253
+ const aggregateId = uuid();
254
+ // Seed version 1 so the batch's first event collides.
255
+ await appendRaw(testDb.db, makeEvent({ aggregateId, expectedVersion: 0 }));
256
+
257
+ const batch: readonly RawEventToAppend[] = [
258
+ makeEvent({ aggregateId, expectedVersion: 0 }), // DUPLICATE — will fail
259
+ makeEvent({ aggregateId: uuid(), expectedVersion: 0 }), // valid
260
+ makeEvent({ aggregateId: uuid(), expectedVersion: 0 }), // valid
261
+ ];
262
+
263
+ await expect(appendRawBatch(testDb.db, batch)).rejects.toBeInstanceOf(VersionConflictError);
264
+
265
+ // Only the seed event survived — multi-VALUES INSERT is atomic.
266
+ const rows = await testDb.db.execute<{ c: number }>(sql`
267
+ SELECT count(*)::int as c FROM kumiko_events
268
+ `);
269
+ expect(rows[0]?.c).toBe(1);
270
+ });
271
+
272
+ test("version_conflict when first-in-aggregate event has missing predecessor", async () => {
273
+ // Batch tries to write v=5..6 for an empty stream. Pre-flight predecessor
274
+ // check per aggregate-group catches the gap before the INSERT runs.
275
+ const aggregateId = uuid();
276
+ await expect(
277
+ appendRawBatch(testDb.db, [
278
+ makeEvent({ aggregateId, expectedVersion: 4 }),
279
+ makeEvent({ aggregateId, expectedVersion: 5 }),
280
+ ]),
281
+ ).rejects.toBeInstanceOf(VersionConflictError);
282
+
283
+ const rows = await testDb.db.execute<{ c: number }>(sql`
284
+ SELECT count(*)::int as c FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid
285
+ `);
286
+ expect(rows[0]?.c).toBe(0);
287
+ });
288
+
289
+ test("version_conflict on gap within a single-aggregate batch (defense-in-depth against buggy mapper)", async () => {
290
+ // Mapper bug scenario: produces events [v=1, v=3] for the same aggregate
291
+ // (expectedVersions [0, 2] — v=2 missing). Without the contiguity check,
292
+ // UNIQUE wouldn't catch the gap (no collision), predecessor-EXISTS
293
+ // wouldn't catch it (min expectedVersion is 0, check skipped), and v=2
294
+ // would silently be orphaned. Must fail loud at batch-entry.
295
+ const aggregateId = uuid();
296
+ await expect(
297
+ appendRawBatch(testDb.db, [
298
+ makeEvent({ aggregateId, expectedVersion: 0 }),
299
+ makeEvent({ aggregateId, expectedVersion: 2 }),
300
+ ]),
301
+ ).rejects.toBeInstanceOf(VersionConflictError);
302
+
303
+ // Zero events persisted — the whole batch is rejected before the INSERT.
304
+ const rows = await testDb.db.execute<{ c: number }>(sql`
305
+ SELECT count(*)::int as c FROM kumiko_events WHERE aggregate_id = ${aggregateId}::uuid
306
+ `);
307
+ expect(rows[0]?.c).toBe(0);
308
+ });
309
+
310
+ test("contiguity check is per-aggregate — independent aggregates with non-overlapping versions pass", async () => {
311
+ // Two different aggregates. agg1 at [v=1,2], agg2 at [v=1]. The contiguity
312
+ // check groups by aggregate_id, so agg1's [0→1, 1→2] and agg2's [0→1] are
313
+ // checked independently — no spurious cross-aggregate gap false-positive.
314
+ const agg1 = uuid();
315
+ const agg2 = uuid();
316
+ await appendRawBatch(testDb.db, [
317
+ makeEvent({ aggregateId: agg1, expectedVersion: 0 }),
318
+ makeEvent({ aggregateId: agg2, expectedVersion: 0 }),
319
+ makeEvent({ aggregateId: agg1, expectedVersion: 1 }),
320
+ ]);
321
+
322
+ const s1 = await loadAggregate(testDb.db, agg1, tenantA);
323
+ const s2 = await loadAggregate(testDb.db, agg2, tenantA);
324
+ expect(s1.map((e) => e.version)).toEqual([1, 2]);
325
+ expect(s2.map((e) => e.version)).toEqual([1]);
326
+ });
327
+
328
+ test("empty array is a no-op — no query, no throw", async () => {
329
+ const queries: string[] = [];
330
+ const loggedDb = drizzle(testDb.client, {
331
+ logger: {
332
+ logQuery: (q) => queries.push(q),
333
+ },
334
+ });
335
+
336
+ await appendRawBatch(loggedDb, []);
337
+ expect(queries).toHaveLength(0);
338
+ });
339
+
340
+ test("multi-aggregate batch: each aggregate lands on its own stream with the right type", async () => {
341
+ // Mixed batch: two DIFFERENT aggregates, v=0 each. Single INSERT, both
342
+ // land, each on its own stream with the event type that was paired with
343
+ // it at call-time (no cross-talk between rows in the multi-VALUES list).
344
+ const agg1 = uuid();
345
+ const agg2 = uuid();
346
+ await appendRawBatch(testDb.db, [
347
+ makeEvent({ aggregateId: agg1, expectedVersion: 0, type: "legacy.order.created" }),
348
+ makeEvent({ aggregateId: agg2, expectedVersion: 0, type: "legacy.driver.created" }),
349
+ ]);
350
+
351
+ const stream1 = await loadAggregate(testDb.db, agg1, tenantA);
352
+ const stream2 = await loadAggregate(testDb.db, agg2, tenantA);
353
+
354
+ expect(stream1.map((s) => ({ v: s.version, t: s.type }))).toEqual([
355
+ { v: 1, t: "legacy.order.created" },
356
+ ]);
357
+ expect(stream2.map((s) => ({ v: s.version, t: s.type }))).toEqual([
358
+ { v: 1, t: "legacy.driver.created" },
359
+ ]);
360
+ });
361
+ });