@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,184 @@
1
+ // Pure Unit-Tests für die flatten/rehydrate Helpers — Auto-Convert für
2
+ // locatedTimestamp-Felder. Keine DB, kein Stack, nur Daten-Transform.
3
+
4
+ import { describe, expect, test } from "vitest";
5
+ import { createEntity, createLocatedTimestampField, createTextField } from "../../engine";
6
+ import type { EntityDefinition } from "../../engine/types";
7
+ import { flattenLocatedTimestamp, rehydrateLocatedTimestamp } from "../located-timestamp";
8
+
9
+ const orderEntity: EntityDefinition = createEntity({
10
+ fields: {
11
+ clientName: createTextField(),
12
+ pickup: createLocatedTimestampField(),
13
+ delivery: createLocatedTimestampField(),
14
+ },
15
+ });
16
+
17
+ // Sprint F: flattenLocatedTimestamp returnt jetzt Temporal.Instant für die
18
+ // <name>Utc-Spalte (statt ISO-String). Tests vergleichen via .toString()
19
+ // damit der Wert deterministisch lesbar bleibt — der canonical ISO-Output
20
+ // von Temporal.Instant ist stabil.
21
+ const utcStr = (v: unknown): string => (v as Temporal.Instant).toString();
22
+
23
+ describe("flattenLocatedTimestamp — Insert/Update Convert", () => {
24
+ test("{ at, tz } → { <name>Utc, <name>Tz } (utc berechnet via Temporal)", () => {
25
+ const flat = flattenLocatedTimestamp(
26
+ { pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon" } },
27
+ orderEntity,
28
+ );
29
+ // Lisbon WEST im April = UTC+1, also 10:00 Lisbon = 09:00 UTC.
30
+ expect(utcStr(flat["pickupUtc"])).toBe("2026-04-15T09:00:00Z");
31
+ expect(flat["pickupTz"]).toBe("Europe/Lisbon");
32
+ });
33
+
34
+ test("{ utc, tz } → wird direkt gespeichert (utc gewinnt)", () => {
35
+ const flat = flattenLocatedTimestamp(
36
+ { pickup: { utc: "2026-04-15T09:00:00Z", tz: "Europe/Lisbon" } },
37
+ orderEntity,
38
+ );
39
+ expect(utcStr(flat["pickupUtc"])).toBe("2026-04-15T09:00:00Z");
40
+ expect(flat["pickupTz"]).toBe("Europe/Lisbon");
41
+ });
42
+
43
+ test("{ at, tz, utc } — utc gewinnt deterministisch", () => {
44
+ const flat = flattenLocatedTimestamp(
45
+ {
46
+ pickup: {
47
+ at: "2026-04-15T10:00:00",
48
+ tz: "Europe/Lisbon",
49
+ utc: "2026-04-15T07:00:00Z", // bewusst inkonsistent
50
+ },
51
+ },
52
+ orderEntity,
53
+ );
54
+ // utc gewinnt — wir speichern was der Caller explizit angegeben hat.
55
+ expect(utcStr(flat["pickupUtc"])).toBe("2026-04-15T07:00:00Z");
56
+ });
57
+
58
+ test("Mehrere locatedTimestamp-Felder am gleichen Object", () => {
59
+ const flat = flattenLocatedTimestamp(
60
+ {
61
+ pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon" },
62
+ delivery: { at: "2026-04-16T18:00:00", tz: "Asia/Tokyo" },
63
+ },
64
+ orderEntity,
65
+ );
66
+ expect(utcStr(flat["pickupUtc"])).toBe("2026-04-15T09:00:00Z");
67
+ expect(flat["pickupTz"]).toBe("Europe/Lisbon");
68
+ // 18:00 Tokyo = 09:00 UTC
69
+ expect(utcStr(flat["deliveryUtc"])).toBe("2026-04-16T09:00:00Z");
70
+ expect(flat["deliveryTz"]).toBe("Asia/Tokyo");
71
+ });
72
+
73
+ test("andere Felder bleiben unverändert (clientName)", () => {
74
+ const flat = flattenLocatedTimestamp(
75
+ {
76
+ clientName: "Acme",
77
+ pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Berlin" },
78
+ },
79
+ orderEntity,
80
+ );
81
+ expect(flat["clientName"]).toBe("Acme");
82
+ });
83
+
84
+ test("fehlende Felder werden ignoriert (kein crash)", () => {
85
+ const flat = flattenLocatedTimestamp({ clientName: "X" }, orderEntity);
86
+ expect(flat).toEqual({ clientName: "X" });
87
+ });
88
+
89
+ test("DST-Übergang Berlin Spring-Forward 2026-03-29 — Konvertierung korrekt", () => {
90
+ // 04:30 Berlin am 29.03.2026 (nach DST-Sprung) = 02:30 UTC (CEST UTC+2)
91
+ const flat = flattenLocatedTimestamp(
92
+ { pickup: { at: "2026-03-29T04:30:00", tz: "Europe/Berlin" } },
93
+ orderEntity,
94
+ );
95
+ expect(utcStr(flat["pickupUtc"])).toBe("2026-03-29T02:30:00Z");
96
+ });
97
+
98
+ test("ist pure — input wird nicht mutiert", () => {
99
+ const input = { pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon" } };
100
+ const before = JSON.stringify(input);
101
+ flattenLocatedTimestamp(input, orderEntity);
102
+ expect(JSON.stringify(input)).toBe(before);
103
+ });
104
+ });
105
+
106
+ describe("rehydrateLocatedTimestamp — Read Convert", () => {
107
+ test("{ <name>Utc, <name>Tz } DB-Form → { at, tz, utc } API-Form (Pickup-Ort-lokal)", () => {
108
+ const out = rehydrateLocatedTimestamp(
109
+ { pickupUtc: "2026-04-15T09:00:00Z", pickupTz: "Europe/Lisbon" },
110
+ orderEntity,
111
+ );
112
+ // 09:00 UTC in Lissabon (WEST UTC+1 im April) = 10:00 lokal.
113
+ expect(out).toEqual({
114
+ pickup: {
115
+ at: "2026-04-15T10:00:00",
116
+ tz: "Europe/Lisbon",
117
+ utc: "2026-04-15T09:00:00Z",
118
+ },
119
+ });
120
+ });
121
+
122
+ test("PG-Wire-Format mit Space wird normalisiert (PG-mode:'string')", () => {
123
+ // Drizzle's mode:"string" liefert TIMESTAMPTZ als "2026-04-15 09:00:00+00".
124
+ const out = rehydrateLocatedTimestamp(
125
+ { pickupUtc: "2026-04-15 09:00:00+00", pickupTz: "Europe/Lisbon" },
126
+ orderEntity,
127
+ );
128
+ expect((out["pickup"] as { utc: string }).utc).toBe("2026-04-15T09:00:00Z");
129
+ expect((out["pickup"] as { at: string }).at).toBe("2026-04-15T10:00:00");
130
+ });
131
+
132
+ test("Round-Trip: flatten dann rehydrate ergibt original-equivalent", () => {
133
+ const original = { pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon" } };
134
+ const flat = flattenLocatedTimestamp(original, orderEntity);
135
+ const rehydrated = rehydrateLocatedTimestamp(flat, orderEntity);
136
+
137
+ const pickup = rehydrated["pickup"] as { at: string; tz: string; utc: string };
138
+ expect(pickup.at).toBe("2026-04-15T10:00:00");
139
+ expect(pickup.tz).toBe("Europe/Lisbon");
140
+ // utc wird IM Read zusätzlich befüllt — original hatte ihn nicht
141
+ expect(pickup.utc).toBe("2026-04-15T09:00:00Z");
142
+ });
143
+
144
+ test("Mehrere Felder: Pickup Lissabon + Delivery Tokyo gleichzeitig", () => {
145
+ const out = rehydrateLocatedTimestamp(
146
+ {
147
+ pickupUtc: "2026-04-15T09:00:00Z",
148
+ pickupTz: "Europe/Lisbon",
149
+ deliveryUtc: "2026-04-16T09:00:00Z",
150
+ deliveryTz: "Asia/Tokyo",
151
+ },
152
+ orderEntity,
153
+ );
154
+ expect((out["pickup"] as { tz: string }).tz).toBe("Europe/Lisbon");
155
+ expect((out["delivery"] as { tz: string }).tz).toBe("Asia/Tokyo");
156
+ // Tokyo: 09:00 UTC = 18:00 lokal (JST UTC+9)
157
+ expect((out["delivery"] as { at: string }).at).toBe("2026-04-16T18:00:00");
158
+ });
159
+
160
+ test("Tag-Wechsel: 23:30 Berlin → 21:30 UTC; Read in Tokyo gibt anderen Tag", () => {
161
+ // Beweis dass der Default `at` = Pickup-Ort-lokal IST. Hier speichern
162
+ // wir einen UTC-Instant mit Lissabon als gespeicherte tz, und prüfen
163
+ // dass der Read den Lissabon-Wall-Clock-Tag liefert (nicht z.B. Tokyo).
164
+ const out = rehydrateLocatedTimestamp(
165
+ { pickupUtc: "2026-04-15T22:30:00Z", pickupTz: "Europe/Lisbon" },
166
+ orderEntity,
167
+ );
168
+ // In Lissabon (WEST UTC+1) ist 22:30 UTC am 15.04 = 23:30 am 15.04 lokal
169
+ expect((out["pickup"] as { at: string }).at).toBe("2026-04-15T23:30:00");
170
+
171
+ // Wenn jemand User-Sicht "in Tokyo" wollte: muss er aus utc selbst ableiten.
172
+ // Dieser Test demonstriert nur die Server-Default-Sicht (Pickup-Ort-lokal).
173
+ });
174
+
175
+ test("fehlende Felder werden übersprungen (kein crash)", () => {
176
+ const out = rehydrateLocatedTimestamp({ clientName: "X" }, orderEntity);
177
+ expect(out).toEqual({ clientName: "X" });
178
+ });
179
+
180
+ test("partial: nur Tz ohne Utc → Pair wird nicht erzeugt (Daten korrupt, kein silent fix)", () => {
181
+ const out = rehydrateLocatedTimestamp({ pickupTz: "Europe/Lisbon" }, orderEntity);
182
+ expect(out["pickup"]).toBeUndefined();
183
+ });
184
+ });
@@ -0,0 +1,199 @@
1
+ // Pure Unit-Tests für money flatten/rehydrate Helpers.
2
+
3
+ import { describe, expect, test } from "vitest";
4
+ import { createEntity, createMoneyField, createTextField } from "../../engine";
5
+ import type { EntityDefinition } from "../../engine/types";
6
+ import { flattenMoney, rehydrateMoney } from "../money";
7
+
8
+ const orderEntity: EntityDefinition = createEntity({
9
+ defaultCurrency: "EUR",
10
+ fields: {
11
+ label: createTextField(),
12
+ buyingPrice: createMoneyField(),
13
+ sellingPrice: createMoneyField(),
14
+ },
15
+ });
16
+
17
+ const usdEntity: EntityDefinition = createEntity({
18
+ defaultCurrency: "USD",
19
+ fields: {
20
+ fee: createMoneyField(),
21
+ },
22
+ });
23
+
24
+ describe("flattenMoney — Insert/Update Convert", () => {
25
+ test("{ amount, currency } → { <name>: amount, <name>Currency: currency }", () => {
26
+ const flat = flattenMoney({ buyingPrice: { amount: 45000, currency: "EUR" } }, orderEntity);
27
+ expect(flat).toEqual({ buyingPrice: 45000, buyingPriceCurrency: "EUR" });
28
+ });
29
+
30
+ test("primitive number (legacy) wird akzeptiert + entity.defaultCurrency angehängt", () => {
31
+ const flat = flattenMoney({ buyingPrice: 45000 }, orderEntity);
32
+ expect(flat).toEqual({ buyingPrice: 45000, buyingPriceCurrency: "EUR" });
33
+ });
34
+
35
+ test("primitive number nutzt USD wenn entity.defaultCurrency = USD", () => {
36
+ const flat = flattenMoney({ fee: 199 }, usdEntity);
37
+ expect(flat).toEqual({ fee: 199, feeCurrency: "USD" });
38
+ });
39
+
40
+ test("expliziter <name>Currency im Payload überschreibt nicht", () => {
41
+ const flat = flattenMoney({ buyingPrice: 45000, buyingPriceCurrency: "USD" }, orderEntity);
42
+ // Wenn bereits gesetzt, nicht überschreiben — caller-explicit gewinnt
43
+ expect(flat["buyingPriceCurrency"]).toBe("USD");
44
+ });
45
+
46
+ test("mehrere money-Felder am gleichen Object", () => {
47
+ const flat = flattenMoney(
48
+ {
49
+ buyingPrice: { amount: 45000, currency: "EUR" },
50
+ sellingPrice: { amount: 60000, currency: "USD" },
51
+ },
52
+ orderEntity,
53
+ );
54
+ expect(flat).toEqual({
55
+ buyingPrice: 45000,
56
+ buyingPriceCurrency: "EUR",
57
+ sellingPrice: 60000,
58
+ sellingPriceCurrency: "USD",
59
+ });
60
+ });
61
+
62
+ test("andere Felder bleiben unverändert", () => {
63
+ const flat = flattenMoney(
64
+ { label: "Premium", buyingPrice: { amount: 100, currency: "EUR" } },
65
+ orderEntity,
66
+ );
67
+ expect(flat["label"]).toBe("Premium");
68
+ });
69
+
70
+ test("null/undefined money-Field wird ignoriert (kein crash)", () => {
71
+ const flat = flattenMoney({ buyingPrice: undefined, sellingPrice: null }, orderEntity);
72
+ expect(flat["buyingPrice"]).toBeUndefined();
73
+ expect(flat["sellingPrice"]).toBeNull();
74
+ });
75
+
76
+ test("Framework-Default 'EUR' wenn entity.defaultCurrency nicht gesetzt", () => {
77
+ const noCurrencyEntity: EntityDefinition = createEntity({
78
+ fields: { fee: createMoneyField() },
79
+ });
80
+ const flat = flattenMoney({ fee: 50 }, noCurrencyEntity);
81
+ expect(flat["feeCurrency"]).toBe("EUR");
82
+ });
83
+
84
+ test("ist pure — input wird nicht mutiert", () => {
85
+ const input = { buyingPrice: { amount: 45000, currency: "EUR" } };
86
+ const before = JSON.stringify(input);
87
+ flattenMoney(input, orderEntity);
88
+ expect(JSON.stringify(input)).toBe(before);
89
+ });
90
+ });
91
+
92
+ describe("rehydrateMoney — Read Convert", () => {
93
+ test("{ <name>: number, <name>Currency: string } → { <name>: { amount, currency } }", () => {
94
+ const out = rehydrateMoney({ buyingPrice: 45000, buyingPriceCurrency: "EUR" }, orderEntity);
95
+ expect(out).toEqual({ buyingPrice: { amount: 45000, currency: "EUR" } });
96
+ });
97
+
98
+ test("PG-BIGINT als String wird zu number gecastet", () => {
99
+ // Postgres-driver liefert BIGINT manchmal als String (>2^53 sicher).
100
+ const out = rehydrateMoney({ buyingPrice: "45000", buyingPriceCurrency: "EUR" }, orderEntity);
101
+ expect(out["buyingPrice"]).toEqual({ amount: 45000, currency: "EUR" });
102
+ });
103
+
104
+ test("fehlende Currency-Spalte fällt auf entity.defaultCurrency", () => {
105
+ const out = rehydrateMoney({ buyingPrice: 45000 }, orderEntity);
106
+ expect(out["buyingPrice"]).toEqual({ amount: 45000, currency: "EUR" });
107
+ });
108
+
109
+ test("null/undefined amount → Field wird aus Output entfernt", () => {
110
+ const out = rehydrateMoney({ buyingPrice: null, buyingPriceCurrency: "EUR" }, orderEntity);
111
+ expect(out["buyingPrice"]).toBeUndefined();
112
+ });
113
+
114
+ test("Mehrere money-Felder am gleichen Object", () => {
115
+ const out = rehydrateMoney(
116
+ {
117
+ buyingPrice: 45000,
118
+ buyingPriceCurrency: "EUR",
119
+ sellingPrice: 60000,
120
+ sellingPriceCurrency: "USD",
121
+ },
122
+ orderEntity,
123
+ );
124
+ expect(out).toEqual({
125
+ buyingPrice: { amount: 45000, currency: "EUR" },
126
+ sellingPrice: { amount: 60000, currency: "USD" },
127
+ });
128
+ });
129
+
130
+ test("Round-Trip: flatten dann rehydrate ergibt dasselbe", () => {
131
+ const original = {
132
+ buyingPrice: { amount: 45000, currency: "EUR" },
133
+ sellingPrice: { amount: 60000, currency: "USD" },
134
+ };
135
+ const flat = flattenMoney(original, orderEntity);
136
+ const rehydrated = rehydrateMoney(flat, orderEntity);
137
+ expect(rehydrated).toEqual(original);
138
+ });
139
+
140
+ test("Round-Trip primitive-Insert: flatten(45000) → rehydrate → { amount:45000, currency:EUR }", () => {
141
+ const flat = flattenMoney({ buyingPrice: 45000 }, orderEntity);
142
+ const out = rehydrateMoney(flat, orderEntity);
143
+ expect(out["buyingPrice"]).toEqual({ amount: 45000, currency: "EUR" });
144
+ });
145
+
146
+ test("ist pure — input wird nicht mutiert", () => {
147
+ const input = { buyingPrice: 45000, buyingPriceCurrency: "EUR" };
148
+ const before = JSON.stringify(input);
149
+ rehydrateMoney(input, orderEntity);
150
+ expect(JSON.stringify(input)).toBe(before);
151
+ });
152
+
153
+ test("korrupte string-amount (kein number) → loud throw, kein silent drop", () => {
154
+ expect(() =>
155
+ rehydrateMoney({ buyingPrice: "not-a-number", buyingPriceCurrency: "EUR" }, orderEntity),
156
+ ).toThrow(/not a number — DB corruption/);
157
+ });
158
+
159
+ test("unerwarteter amount-Typ (boolean) → loud throw", () => {
160
+ expect(() =>
161
+ rehydrateMoney({ buyingPrice: true, buyingPriceCurrency: "EUR" }, orderEntity),
162
+ ).toThrow(/unexpected type/);
163
+ });
164
+ });
165
+
166
+ describe("Round-Trip im Update-Pfad (Helper-Verkettung wie im Executor)", () => {
167
+ test("Update-Changes-Payload mit money geht durch flatten + zurück durch rehydrate", () => {
168
+ // Simuliert was der Executor macht: changes → flatten → DB → rehydrate
169
+ const changes = { buyingPrice: { amount: 99_000, currency: "USD" } };
170
+ const flat = flattenMoney(changes, orderEntity);
171
+ expect(flat).toEqual({ buyingPrice: 99_000, buyingPriceCurrency: "USD" });
172
+
173
+ // DB liefert dieselben Spalten zurück
174
+ const out = rehydrateMoney(flat, orderEntity);
175
+ expect(out).toEqual({ buyingPrice: { amount: 99_000, currency: "USD" } });
176
+ });
177
+
178
+ test("List-Pfad: mehrere Rows hintereinander rehydraten", () => {
179
+ const dbRows = [
180
+ { buyingPrice: 100, buyingPriceCurrency: "EUR" },
181
+ { buyingPrice: 200, buyingPriceCurrency: "USD" },
182
+ { buyingPrice: 300, buyingPriceCurrency: "GBP" },
183
+ ];
184
+ const apiRows = dbRows.map((r) => rehydrateMoney(r, orderEntity));
185
+ expect(apiRows).toEqual([
186
+ { buyingPrice: { amount: 100, currency: "EUR" } },
187
+ { buyingPrice: { amount: 200, currency: "USD" } },
188
+ { buyingPrice: { amount: 300, currency: "GBP" } },
189
+ ]);
190
+ });
191
+ });
192
+
193
+ describe("flattenMoney — Strict-Mode Throw", () => {
194
+ test("string als Wert (statt number/object) → loud throw", () => {
195
+ expect(() => flattenMoney({ buyingPrice: "100" }, orderEntity)).toThrow(
196
+ /expects \{ amount, currency \} object or number/,
197
+ );
198
+ });
199
+ });
@@ -0,0 +1,76 @@
1
+ // Sanity test for the multi-row INSERT id-default behaviour.
2
+ //
3
+ // While building the publicstatus showcase, a multi-row INSERT into a
4
+ // join-table appeared to drop one row silently. The fix in the showcase
5
+ // was a per-row INSERT. This test pins down the actual behaviour at the
6
+ // framework layer so we know whether buildBaseColumns / Drizzle / PG
7
+ // have a real footgun, or whether the showcase bug had a different root
8
+ // cause.
9
+
10
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
11
+ import { buildDrizzleTable } from "../../db/table-builder";
12
+ import { createEntity, createTextField } from "../../engine";
13
+ import { createEntityTable, setupTestStack, type TestStack } from "../../stack";
14
+
15
+ const linkEntity = createEntity({
16
+ table: "mri_links",
17
+ fields: {
18
+ leftId: createTextField({ required: true }),
19
+ rightId: createTextField({ required: true }),
20
+ },
21
+ });
22
+ const linkTable = buildDrizzleTable("link", linkEntity);
23
+
24
+ let stack: TestStack;
25
+
26
+ beforeAll(async () => {
27
+ stack = await setupTestStack({ features: [] });
28
+ await createEntityTable(stack.db, linkEntity);
29
+ });
30
+
31
+ afterAll(async () => stack?.cleanup());
32
+
33
+ describe("instant() customType is forgiving with ISO strings", () => {
34
+ // While building publicstatus we hit a footgun: Zod insert-schemas
35
+ // validate timestamp fields as z.iso.datetime() (string), but the
36
+ // instant() customType used to require Temporal.Instant on toDriver
37
+ // and crashed obscurely otherwise. Coercion at the customType boundary
38
+ // makes ISO-strings work without ceremony.
39
+
40
+ const tsEntity = createEntity({
41
+ table: "mri_ts",
42
+ fields: { name: createTextField({ required: true }) },
43
+ });
44
+ const tsTable = buildDrizzleTable("ts-row", tsEntity);
45
+
46
+ test("INSERT accepts an ISO string for an instant column (forgiving path)", async () => {
47
+ await createEntityTable(stack.db, tsEntity, "ts-row");
48
+ // insertedAt is base-column, type instant. Pass an ISO string —
49
+ // coercion in toDriver handles it. Without the fix, Drizzle-driver
50
+ // would call .toString() on a string and produce a malformed driver
51
+ // value that PG rejects.
52
+ const isoString = "2026-01-15T12:00:00Z";
53
+ await stack.db.insert(tsTable).values({
54
+ name: "x",
55
+ tenantId: "00000000-0000-4000-8000-000000000001",
56
+ insertedAt: isoString as unknown as Temporal.Instant,
57
+ });
58
+ const rows = await stack.db.select().from(tsTable);
59
+ expect(rows).toHaveLength(1);
60
+ expect(rows[0]?.["insertedAt"]).toBeInstanceOf(Temporal.Instant);
61
+ });
62
+ });
63
+
64
+ describe("multi-row INSERT", () => {
65
+ test("two rows with no id supplied → both rows persist (PG gen_random_uuid per row)", async () => {
66
+ await stack.db.insert(linkTable).values([
67
+ { leftId: "L1", rightId: "R1", tenantId: "00000000-0000-4000-8000-000000000001" },
68
+ { leftId: "L2", rightId: "R2", tenantId: "00000000-0000-4000-8000-000000000001" },
69
+ ]);
70
+ const rows = await stack.db.select().from(linkTable);
71
+ expect(rows).toHaveLength(2);
72
+ // Each row got its own id from the PG default.
73
+ const ids = new Set(rows.map((r) => r["id"] as string));
74
+ expect(ids.size).toBe(2);
75
+ });
76
+ });
@@ -0,0 +1,70 @@
1
+ // Unit-Tests für parseAutoVerb — die Mapping-Logik die entscheidet
2
+ // ob ein Event ein Auto-Verb auf seinem Aggregate ist (created/
3
+ // updated/deleted/restored) oder ein Domain-Event. Production-Behavior:
4
+ // die ImplicitProjection registriert Apply-Handler nur für die 4 Auto-
5
+ // Verben; Domain-Events laufen durch explicit r.projection oder MSP.
6
+ //
7
+ // Wenn parseAutoVerb für ein Domain-Event versehentlich einen Verb
8
+ // returnt, würde die ImplicitProjection den falschen Handler firen.
9
+
10
+ import { describe, expect, test } from "vitest";
11
+ import type { StoredEvent } from "../../event-store";
12
+ import { parseAutoVerb } from "../apply-entity-event";
13
+
14
+ function event(overrides: Partial<StoredEvent>): StoredEvent {
15
+ return {
16
+ id: "evt-1",
17
+ aggregateId: "agg-1",
18
+ aggregateType: "user",
19
+ tenantId: "tenant-1" as never,
20
+ version: 1,
21
+ type: "user.created",
22
+ eventVersion: 1,
23
+ payload: {},
24
+ metadata: { userId: "u-1" },
25
+ createdAt: { toString: () => "2026-04-27T00:00:00Z" } as never,
26
+ createdBy: "u-1",
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ describe("parseAutoVerb", () => {
32
+ test.each([
33
+ ["user.created", "created"],
34
+ ["user.updated", "updated"],
35
+ ["user.deleted", "deleted"],
36
+ ["user.restored", "restored"],
37
+ ] as const)("'%s' → '%s'", (type, verb) => {
38
+ expect(parseAutoVerb(event({ type }))).toBe(verb);
39
+ });
40
+
41
+ test("domain-event auf demselben aggregate → null", () => {
42
+ expect(parseAutoVerb(event({ type: "user.password-changed" }))).toBeNull();
43
+ });
44
+
45
+ test("auto-verb-Suffix auf falschem aggregate → null", () => {
46
+ // type "tenant.created" auf einem user-Aggregate ist defensive ein
47
+ // Domain-Event aus Sicht der user-Implicit-Projection — nicht ihr
48
+ // eigener Auto-Verb.
49
+ expect(parseAutoVerb(event({ aggregateType: "user", type: "tenant.created" }))).toBeNull();
50
+ });
51
+
52
+ test("kebab-case Aggregate (incident-update.created)", () => {
53
+ expect(
54
+ parseAutoVerb(event({ aggregateType: "incident-update", type: "incident-update.created" })),
55
+ ).toBe("created");
56
+ });
57
+
58
+ test("custom verb → null", () => {
59
+ expect(parseAutoVerb(event({ type: "user.imported" }))).toBeNull();
60
+ });
61
+
62
+ test("type ohne dot-separator → null", () => {
63
+ expect(parseAutoVerb(event({ type: "userCreated" }))).toBeNull();
64
+ });
65
+
66
+ test("type mit verschachteltem prefix → null", () => {
67
+ // "user.profile.updated" hat zwei dots — kein clean Auto-Verb
68
+ expect(parseAutoVerb(event({ type: "user.profile.updated" }))).toBeNull();
69
+ });
70
+ });
@@ -0,0 +1,105 @@
1
+ // Migration-Safety-Test: `ALTER TABLE … SET NOT NULL` schlägt fehl wenn
2
+ // die Spalte vor der Migration NULL-Werte enthält.
3
+ //
4
+ // Hintergrund: Der A-Fix (required → notNull in fieldToColumns) erzeugt
5
+ // für bestehende Apps eine drift-fix-Migration mit `SET NOT NULL` auf
6
+ // jedem required-Feld. Das ist sicher gegen FRISCHE DBs (keine Daten
7
+ // drin), aber gefährlich gegen Prod-DBs in denen historisch NULL-Werte
8
+ // reingerutscht sein könnten — die Migration kracht beim apply mit:
9
+ //
10
+ // ERROR: column "<name>" of relation "<table>" contains null values
11
+ // STATE: 23502 (not_null_violation)
12
+ //
13
+ // Dieser Test simuliert genau das. Schreibt eine Tabelle mit nullable
14
+ // `key`, fügt eine Zeile mit NULL ein, versucht dann SET NOT NULL —
15
+ // erwartet den Postgres-Error. Operations-Hinweis: vor dem deploy einer
16
+ // solchen Migration eine Sanity-Query auf NULL-Counts in den betroffenen
17
+ // Spalten laufen, oder DB drop'pen wenn der State Demo-State ist.
18
+
19
+ import { sql } from "drizzle-orm";
20
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
21
+ import { createTestDb, type TestDb } from "../../stack";
22
+
23
+ let testDb: TestDb;
24
+
25
+ beforeAll(async () => {
26
+ testDb = await createTestDb();
27
+ });
28
+
29
+ afterAll(async () => {
30
+ await testDb.cleanup();
31
+ });
32
+
33
+ beforeEach(async () => {
34
+ await testDb.db.execute(sql`DROP TABLE IF EXISTS migration_safety_test`);
35
+ });
36
+
37
+ describe("ALTER TABLE SET NOT NULL — Daten-Sicherheits-Verhalten", () => {
38
+ test("SET NOT NULL kracht wenn die Spalte NULL-Zeilen enthält", async () => {
39
+ await testDb.db.execute(sql`
40
+ CREATE TABLE migration_safety_test (
41
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
42
+ key text
43
+ )
44
+ `);
45
+ // NULL-Zeile einschleusen — simuliert prod state vor dem Drift-Fix.
46
+ await testDb.db.execute(sql`INSERT INTO migration_safety_test (key) VALUES (NULL)`);
47
+
48
+ let caught: unknown;
49
+ try {
50
+ await testDb.db.execute(sql`ALTER TABLE migration_safety_test ALTER COLUMN key SET NOT NULL`);
51
+ } catch (err) {
52
+ caught = err;
53
+ }
54
+ expect(caught).toBeDefined();
55
+ // Drizzle wrapped den PG-Error in einer DrizzleQueryError. Der echte
56
+ // not_null_violation steckt in `.cause` als postgres-js Error mit
57
+ // `.code === "23502"` und einem deutschsprachigen oder englischen
58
+ // `.message`. Wir prüfen pragmatisch beide Pfade.
59
+ const cause = (caught as { cause?: unknown }).cause;
60
+ const causeCode = (cause as { code?: string } | undefined)?.code;
61
+ expect(causeCode).toBe("23502");
62
+ });
63
+
64
+ test("SET NOT NULL läuft sauber durch wenn alle Zeilen Werte haben", async () => {
65
+ await testDb.db.execute(sql`
66
+ CREATE TABLE migration_safety_test (
67
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
68
+ key text
69
+ )
70
+ `);
71
+ await testDb.db.execute(sql`INSERT INTO migration_safety_test (key) VALUES ('foo')`);
72
+ await testDb.db.execute(sql`INSERT INTO migration_safety_test (key) VALUES ('bar')`);
73
+
74
+ // Sollte ohne Throw durchlaufen.
75
+ await testDb.db.execute(sql`ALTER TABLE migration_safety_test ALTER COLUMN key SET NOT NULL`);
76
+
77
+ // Verifizieren: zukünftige NULL-Inserts werden jetzt blockiert.
78
+ let caught: unknown;
79
+ try {
80
+ await testDb.db.execute(sql`INSERT INTO migration_safety_test (key) VALUES (NULL)`);
81
+ } catch (err) {
82
+ caught = err;
83
+ }
84
+ expect(caught).toBeDefined();
85
+ });
86
+
87
+ test("SET NOT NULL auf leerer Tabelle ist trivial sicher", async () => {
88
+ // Frisch erstellt, keine Zeilen — der Fall in dem `migrate apply` nach
89
+ // einem DB-drop läuft. Dieser Pfad muss IMMER grün sein, sonst wäre
90
+ // jeder Greenfield-Deploy kaputt.
91
+ await testDb.db.execute(sql`
92
+ CREATE TABLE migration_safety_test (
93
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
94
+ key text
95
+ )
96
+ `);
97
+ await testDb.db.execute(sql`ALTER TABLE migration_safety_test ALTER COLUMN key SET NOT NULL`);
98
+
99
+ // Beweis: information_schema zeigt die Spalte jetzt als NOT NULL.
100
+ const rows = await testDb.db.execute<{ is_nullable: string }>(
101
+ sql`SELECT is_nullable FROM information_schema.columns WHERE table_name = 'migration_safety_test' AND column_name = 'key'`,
102
+ );
103
+ expect(rows[0]?.is_nullable).toBe("NO");
104
+ });
105
+ });