@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,55 @@
1
+ export { assertExistsIn } from "./assert-exists-in";
2
+ export { flattenCompoundTypes, rehydrateCompoundTypes } from "./compound-types";
3
+ export type { DbConnection, DbConnectionOptions, DbRow, DbRunner, DbTx } from "./connection";
4
+ export { createDbConnection, dbConnectionOptionsFromEnv } from "./connection";
5
+ export type { CursorQueryOptions, CursorResult } from "./cursor";
6
+ export { applyCursorQuery, decodeCursor, encodeCursor } from "./cursor";
7
+ export type { SelectQuery, TableColumns } from "./dialect";
8
+ export {
9
+ boolean,
10
+ instant,
11
+ integer,
12
+ jsonb,
13
+ primaryKey,
14
+ serial,
15
+ table,
16
+ text,
17
+ timestamp,
18
+ uniqueIndex,
19
+ uuid,
20
+ } from "./dialect";
21
+ export type { EagerLoadEntityResolver, EagerloadedRow } from "./eagerload";
22
+ export {
23
+ collectReferenceFields,
24
+ enrichRowWithReferences,
25
+ enrichWithReferences,
26
+ } from "./eagerload";
27
+ export type { EncryptionProvider } from "./encryption";
28
+ export { createEncryptionProvider } from "./encryption";
29
+ export type {
30
+ EntityLifecycleVerb,
31
+ EventStoreExecutor,
32
+ EventStoreExecutorOptions,
33
+ } from "./event-store-executor";
34
+ export { createEventStoreExecutor, entityEventName } from "./event-store-executor";
35
+ export { flattenLocatedTimestamp, rehydrateLocatedTimestamp } from "./located-timestamp";
36
+ export { flattenMoney, rehydrateMoney } from "./money";
37
+ export {
38
+ constraintOf,
39
+ extractPgError,
40
+ isTableAlreadyExists,
41
+ isUniqueViolation,
42
+ type PgErrorInfo,
43
+ } from "./pg-error";
44
+ export { seedReferenceData } from "./reference-data";
45
+ export { fetchOne } from "./row-helpers";
46
+ export { tableExists } from "./schema-inspection";
47
+ export {
48
+ buildBaseColumns,
49
+ buildDrizzleTable,
50
+ type DrizzleTable,
51
+ toSnakeCase,
52
+ toTableName,
53
+ } from "./table-builder";
54
+ export type { TenantDb, TenantDbMode } from "./tenant-db";
55
+ export { castTenantRows, createTenantDb } from "./tenant-db";
@@ -0,0 +1,114 @@
1
+ // Auto-Convert für locatedTimestamp-Felder im DB-Layer.
2
+ //
3
+ // Vertrag (siehe auch db/money.ts — gleicher Compound-Type-Pattern):
4
+ // API-Form: { at, tz } | { utc, tz }
5
+ // DB-Form: <name>Utc TIMESTAMPTZ + <name>Tz TEXT
6
+ // Read-Form: { at, tz, utc }
7
+ //
8
+ // `at`-Default-Sicht beim Read: Pickup-Ort-lokal (utc projiziert in
9
+ // gespeicherter tz). Server kennt User-TZ nicht — User-spezifische
10
+ // Anzeige passiert client-seitig aus utc.
11
+
12
+ import type { EntityDefinition } from "../engine/types";
13
+
14
+ // Sprint F: <name>Utc-Spalte ist jetzt instant() (siehe dialect.ts) —
15
+ // Drizzle gibt direkt Temporal.Instant zurück. Vor Sprint F kam ein PG-
16
+ // Wire-Format-String "2026-04-15 09:00:00+00" rein der via String-Massage
17
+ // zu ISO-8601 gemacht werden musste. Heute übernimmt der customType die
18
+ // Konversion DB↔Instant — diese Funktion ist nur noch defensive Glue für
19
+ // Legacy-Code-Pfade die noch Strings durchreichen (z.B. raw SQL).
20
+ function toInstant(value: unknown): Temporal.Instant | undefined {
21
+ if (value instanceof Temporal.Instant) return value;
22
+ if (typeof value !== "string") return undefined;
23
+ const iso = value.includes("T") ? value : value.replace(" ", "T").replace(/\+00$/, "Z");
24
+ return Temporal.Instant.from(iso);
25
+ }
26
+
27
+ /**
28
+ * API → DB: locatedTimestamp-Felder zu zwei flachen Spalten flatten.
29
+ *
30
+ * - `{ at, tz }` (UI-Form) → `{ <name>Utc, <name>Tz }` (utc via Temporal berechnet)
31
+ * - `{ utc, tz }` (Server-Form) → utc gewinnt direkt
32
+ * - `{ at, tz, utc }` → utc gewinnt; at wird ignoriert (Konsistenz-Check ist
33
+ * Caller-Verantwortung)
34
+ *
35
+ * Pure — mutiert nicht.
36
+ */
37
+ export function flattenLocatedTimestamp(
38
+ payload: Record<string, unknown>,
39
+ entity: EntityDefinition,
40
+ ): Record<string, unknown> {
41
+ const T = Temporal;
42
+ const result: Record<string, unknown> = { ...payload };
43
+
44
+ for (const [name, field] of Object.entries(entity.fields)) {
45
+ if (field.type !== "locatedTimestamp") continue;
46
+
47
+ const raw = result[name];
48
+ if (raw === undefined || raw === null) continue;
49
+ if (typeof raw !== "object") {
50
+ throw new Error(
51
+ `flattenLocatedTimestamp: field "${name}" expects { at, tz } or { utc, tz } object, got ${typeof raw}`,
52
+ );
53
+ }
54
+ const pair = raw as { at?: string; tz?: string; utc?: string };
55
+
56
+ delete result[name];
57
+
58
+ if (pair.tz === undefined) continue;
59
+ const tz = pair.tz;
60
+ // Sprint F: <name>Utc-Spalte ist instant() — Drizzle erwartet
61
+ // Temporal.Instant. Konvertierung pair.utc-string → Instant geht via
62
+ // toInstant() (kennt String + Instant); pair.at + tz → Instant via
63
+ // Temporal-Math.
64
+ const instant: Temporal.Instant | undefined =
65
+ pair.utc !== undefined
66
+ ? toInstant(pair.utc)
67
+ : pair.at !== undefined
68
+ ? T.PlainDateTime.from(pair.at).toZonedDateTime(tz).toInstant()
69
+ : undefined;
70
+ if (instant === undefined) continue;
71
+
72
+ result[`${name}Utc`] = instant;
73
+ result[`${name}Tz`] = tz;
74
+ }
75
+
76
+ return result;
77
+ }
78
+
79
+ /**
80
+ * DB → API: zwei flache Spalten zu combined { at, tz, utc } rehydraten.
81
+ *
82
+ * `at` ist immer Wall-Clock in der gespeicherten `tz` (Pickup-Ort-lokal).
83
+ * Wer User-Sicht braucht, leitet aus `utc` selbst ab — der Server kennt
84
+ * keine User-TZ.
85
+ *
86
+ * Pure — mutiert nicht.
87
+ */
88
+ export function rehydrateLocatedTimestamp(
89
+ row: Record<string, unknown>,
90
+ entity: EntityDefinition,
91
+ ): Record<string, unknown> {
92
+ const result: Record<string, unknown> = { ...row };
93
+
94
+ for (const [name, field] of Object.entries(entity.fields)) {
95
+ if (field.type !== "locatedTimestamp") continue;
96
+
97
+ const utcRaw = result[`${name}Utc`];
98
+ const tzRaw = result[`${name}Tz`];
99
+
100
+ delete result[`${name}Utc`];
101
+ delete result[`${name}Tz`];
102
+
103
+ if (typeof tzRaw !== "string") continue;
104
+ const utcInstant = toInstant(utcRaw);
105
+ if (utcInstant === undefined) continue;
106
+
107
+ const localZdt = utcInstant.toZonedDateTimeISO(tzRaw);
108
+ const at = localZdt.toPlainDateTime().toString();
109
+
110
+ result[name] = { at, tz: tzRaw, utc: utcInstant.toString() };
111
+ }
112
+
113
+ return result;
114
+ }
@@ -0,0 +1,120 @@
1
+ // Auto-Convert für money-Felder im DB-Layer.
2
+ //
3
+ // Vertrag (siehe auch db/located-timestamp.ts — gleicher Compound-Type-Pattern):
4
+ // API-Form: { amount, currency } | number (permissiv für Legacy)
5
+ // DB-Form: <name> BIGINT + <name>Currency TEXT
6
+ // Read-Form: { amount, currency }
7
+ //
8
+ // Permissiv-Insert: primitive number wird als amount akzeptiert (Legacy aus
9
+ // pre-Stufe-3-Samples). Currency fällt dann auf entity.defaultCurrency
10
+ // zurück (oder DEFAULT_CURRENCIES[0] = "EUR" als Framework-Fallback).
11
+ //
12
+ // Anders als locatedTimestamp behalten wir den Field-Namen `<name>` als
13
+ // amount-Spalte (Legacy DB-Convention für Money — `SUM(buying_price)` bleibt
14
+ // idiomatisch). `<name>Currency` ist die zusätzliche Spalte.
15
+
16
+ import type { EntityDefinition } from "../engine/types";
17
+ import { DEFAULT_CURRENCIES } from "../engine/types";
18
+
19
+ const FRAMEWORK_DEFAULT_CURRENCY = DEFAULT_CURRENCIES[0]; // "EUR"
20
+
21
+ /**
22
+ * API → DB: money-Felder zu zwei flachen Spalten flatten.
23
+ *
24
+ * - `{ amount, currency }` → `{ <name>: amount, <name>Currency: currency }`
25
+ * - `number` (legacy) → `{ <name>: number, <name>Currency: defaultCurrency }`
26
+ *
27
+ * Pure — mutiert nicht.
28
+ */
29
+ export function flattenMoney(
30
+ payload: Record<string, unknown>,
31
+ entity: EntityDefinition,
32
+ ): Record<string, unknown> {
33
+ const result: Record<string, unknown> = { ...payload };
34
+ const fallbackCurrency = entity.defaultCurrency ?? FRAMEWORK_DEFAULT_CURRENCY;
35
+
36
+ for (const [name, field] of Object.entries(entity.fields)) {
37
+ if (field.type !== "money") continue;
38
+
39
+ const raw = result[name];
40
+ if (raw === undefined || raw === null) continue;
41
+
42
+ let amount: number;
43
+ let currency: string;
44
+
45
+ if (typeof raw === "object" && "amount" in raw) {
46
+ const pair = raw as { amount: number; currency?: string };
47
+ amount = pair.amount;
48
+ currency = pair.currency ?? fallbackCurrency;
49
+ } else if (typeof raw === "number") {
50
+ amount = raw;
51
+ // Expliziter currency-key im Payload überschreibt den Default-Fallback.
52
+ const explicitCurrency = result[`${name}Currency`];
53
+ currency = typeof explicitCurrency === "string" ? explicitCurrency : fallbackCurrency;
54
+ } else {
55
+ throw new Error(
56
+ `flattenMoney: field "${name}" expects { amount, currency } object or number, got ${typeof raw}`,
57
+ );
58
+ }
59
+
60
+ delete result[name];
61
+ result[name] = amount;
62
+ result[`${name}Currency`] = currency;
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ /**
69
+ * DB → API: zwei flache Spalten zu combined { amount, currency } rehydraten.
70
+ *
71
+ * Wirft loud bei korrupter DB-Form (string das nicht zur Zahl wird) — silent
72
+ * data-loss wäre Bug-Vektor. NULL/undefined amount → field aus Output entfernt.
73
+ *
74
+ * Pure — mutiert nicht.
75
+ */
76
+ export function rehydrateMoney(
77
+ row: Record<string, unknown>,
78
+ entity: EntityDefinition,
79
+ ): Record<string, unknown> {
80
+ const result: Record<string, unknown> = { ...row };
81
+ const fallbackCurrency = entity.defaultCurrency ?? FRAMEWORK_DEFAULT_CURRENCY;
82
+
83
+ for (const [name, field] of Object.entries(entity.fields)) {
84
+ if (field.type !== "money") continue;
85
+
86
+ const amountRaw = result[name];
87
+ const currencyRaw = result[`${name}Currency`];
88
+
89
+ delete result[`${name}Currency`];
90
+
91
+ if (amountRaw === null || amountRaw === undefined) {
92
+ delete result[name];
93
+ continue;
94
+ }
95
+
96
+ let amount: number;
97
+ if (typeof amountRaw === "number") {
98
+ amount = amountRaw;
99
+ } else if (typeof amountRaw === "string" && amountRaw !== "") {
100
+ // PG-driver liefert BIGINT manchmal als String (>2^53 sicher).
101
+ amount = Number(amountRaw);
102
+ if (Number.isNaN(amount)) {
103
+ throw new Error(
104
+ `rehydrateMoney: field "${name}" amount string "${amountRaw}" is not a number — DB corruption?`,
105
+ );
106
+ }
107
+ } else {
108
+ throw new Error(
109
+ `rehydrateMoney: field "${name}" amount has unexpected type ${typeof amountRaw}`,
110
+ );
111
+ }
112
+
113
+ const currency =
114
+ typeof currencyRaw === "string" && currencyRaw !== "" ? currencyRaw : fallbackCurrency;
115
+
116
+ result[name] = { amount, currency };
117
+ }
118
+
119
+ return result;
120
+ }
@@ -0,0 +1,46 @@
1
+ // Drizzle wraps postgres-js errors in `DrizzleQueryError`; the original PG
2
+ // error (with SQLSTATE `code` and `constraint_name`) lives in `.cause`. We
3
+ // unwrap both layers so callers don't have to know which layer produced the
4
+ // error. Used by the event-store to distinguish a unique-violation on the
5
+ // aggregate-version index (optimistic-concurrency conflict) from the one on
6
+ // the request-id idempotency index (replay signal).
7
+
8
+ export type PgErrorInfo = {
9
+ readonly code: string | undefined;
10
+ readonly constraint_name: string | undefined;
11
+ };
12
+
13
+ export function extractPgError(e: unknown): PgErrorInfo | null {
14
+ if (typeof e !== "object" || e === null) return null;
15
+ const layers: unknown[] = [e];
16
+ // @cast-boundary error-details — DrizzleQueryError wraps PG-error in .cause
17
+ const cause = (e as { cause?: unknown }).cause;
18
+ if (typeof cause === "object" && cause !== null) layers.push(cause);
19
+
20
+ for (const layer of layers) {
21
+ // @cast-boundary error-details — postgres-js error shape (code, constraint_name)
22
+ const code = (layer as { code?: string }).code;
23
+ const constraintName = (layer as { constraint_name?: string }).constraint_name;
24
+ if (code !== undefined || constraintName !== undefined) {
25
+ return { code, constraint_name: constraintName };
26
+ }
27
+ }
28
+ return null;
29
+ }
30
+
31
+ export function isUniqueViolation(e: unknown): boolean {
32
+ return extractPgError(e)?.code === "23505";
33
+ }
34
+
35
+ // PG SQLSTATE 42P07 — "relation already exists". Raised when CREATE
36
+ // TABLE (or drizzle-kit's generated equivalent) runs against a table
37
+ // that's already been created. Useful for idempotent boot-paths like
38
+ // the dev-server, where a persistent DB carries the table over from
39
+ // the previous restart.
40
+ export function isTableAlreadyExists(e: unknown): boolean {
41
+ return extractPgError(e)?.code === "42P07";
42
+ }
43
+
44
+ export function constraintOf(e: unknown): string | undefined {
45
+ return extractPgError(e)?.constraint_name;
46
+ }
@@ -0,0 +1,77 @@
1
+ import { eq } from "drizzle-orm";
2
+ import type { ReferenceDataDef } from "../engine/types";
3
+ import { SYSTEM_TENANT_ID } from "../engine/types";
4
+ import type { DbConnection, DbRow } from "./connection";
5
+ import type { TableColumns } from "./dialect";
6
+ import { toSnakeCase } from "./table-builder";
7
+
8
+ // biome-ignore lint/suspicious/noExplicitAny: Drizzle dynamic tables
9
+ type Table = TableColumns<any>;
10
+
11
+ /**
12
+ * Seed reference data at boot time.
13
+ * For each ReferenceDataDef: upsert rows (insert missing, update changed, never delete).
14
+ * Upsert key defaults to the first field in the data object.
15
+ */
16
+ export async function seedReferenceData(
17
+ defs: readonly ReferenceDataDef[],
18
+ tables: ReadonlyMap<string, Table>,
19
+ db: DbConnection,
20
+ ): Promise<{ inserted: number; updated: number }> {
21
+ let inserted = 0;
22
+ let updated = 0;
23
+
24
+ for (const def of defs) {
25
+ const table = tables.get(def.entityName);
26
+ if (!table) continue;
27
+ if (def.data.length === 0) continue;
28
+
29
+ const firstRow = def.data[0];
30
+ if (!firstRow) continue;
31
+ const firstKey = Object.keys(firstRow)[0];
32
+ if (!firstKey) continue;
33
+ const upsertKey = def.upsertKey ?? firstKey;
34
+ const snakeKey = toSnakeCase(upsertKey);
35
+
36
+ for (const row of def.data) {
37
+ const keyValue = row[upsertKey];
38
+ if (keyValue === undefined) continue;
39
+
40
+ // Check if row exists
41
+ const [existing] = await db
42
+ .select()
43
+ .from(table)
44
+ .where(eq(table[upsertKey] ?? table[snakeKey], keyValue))
45
+ .limit(1);
46
+
47
+ if (existing) {
48
+ // Update if any field changed
49
+ const existingData = existing as DbRow;
50
+ const changes: Record<string, unknown> = {};
51
+ for (const [field, value] of Object.entries(row)) {
52
+ if (field === upsertKey) continue;
53
+ if (existingData[field] !== value) {
54
+ changes[field] = value;
55
+ }
56
+ }
57
+ if (Object.keys(changes).length > 0) {
58
+ await db
59
+ .update(table)
60
+ .set(changes)
61
+ .where(eq(table[upsertKey] ?? table[snakeKey], keyValue));
62
+ updated++;
63
+ }
64
+ } else {
65
+ await db.insert(table).values({
66
+ ...row,
67
+ tenantId: SYSTEM_TENANT_ID,
68
+ version: 1,
69
+ insertedAt: Temporal.Now.instant(),
70
+ });
71
+ inserted++;
72
+ }
73
+ }
74
+ }
75
+
76
+ return { inserted, updated };
77
+ }
@@ -0,0 +1,53 @@
1
+ import { and, type SQL } from "drizzle-orm";
2
+ import type { DbRow } from "./connection";
3
+ import type { TableColumns } from "./dialect";
4
+
5
+ // biome-ignore lint/suspicious/noExplicitAny: Mirrors the erased ProjectionTable / event-store-executor pattern — the framework doesn't know user column shapes.
6
+ type AnyTable = TableColumns<any>;
7
+
8
+ // Minimal DB surface fetchOne uses — structurally satisfied by raw DbRunner
9
+ // (connection / tx) AND TenantDb (tenant-scoped wrapper). Both expose the
10
+ // same `select().from().where().limit()` chain with compatible rows, so the
11
+ // helper types against the shared shape instead of a union that TS can't
12
+ // narrow cleanly.
13
+ type SelectChainDb = {
14
+ select: () => {
15
+ from: (table: AnyTable) => {
16
+ where: (cond: SQL | undefined) => {
17
+ limit: (n: number) => PromiseLike<readonly Record<string, unknown>[]>;
18
+ };
19
+ };
20
+ };
21
+ };
22
+
23
+ // SELECT * FROM <table> WHERE <...conditions> LIMIT 1 → first row or undefined.
24
+ // Collapses the "const [row] = await db.select()...limit(1)" destructure
25
+ // that repeats in every detail-query-style handler and existence-check.
26
+ //
27
+ // Conditions are variadic and non-empty — the tuple `[SQL, ...SQL[]]` rejects
28
+ // `fetchOne(db, table)` (would silently pick any row) and `fetchOne(db, table,
29
+ // undefined)` (would do the same) at compile time. Multiple conditions are
30
+ // combined with AND.
31
+ //
32
+ // const existing = await fetchOne<{ id: number }>(db, userTable,
33
+ // eq(userTable.email, payload.email));
34
+ // if (existing) return writeFailure(new ConflictError({ ... }));
35
+ //
36
+ // const row = await fetchOne(db, membershipTable,
37
+ // eq(membershipTable.userId, userId),
38
+ // eq(membershipTable.tenantId, tenantId),
39
+ // );
40
+ //
41
+ // For dynamic condition arrays (length known only at runtime), spread
42
+ // explicitly: `fetchOne(db, table, first, ...rest)`. Raw `...arr` with
43
+ // `arr: SQL[]` won't type-check because TS can't prove the array is non-
44
+ // empty — a feature, not a bug.
45
+ export async function fetchOne<TRow = DbRow>(
46
+ db: SelectChainDb,
47
+ table: AnyTable,
48
+ ...conditions: readonly [SQL, ...SQL[]]
49
+ ): Promise<TRow | undefined> {
50
+ const where = conditions.length === 1 ? conditions[0] : and(...conditions);
51
+ const rows = await db.select().from(table).where(where).limit(1);
52
+ return rows[0] as TRow | undefined;
53
+ }
@@ -0,0 +1,25 @@
1
+ import { sql } from "drizzle-orm";
2
+ import type { DbConnection, DbTx } from "./connection";
3
+
4
+ // True when `<fullyQualifiedName>` refers to an existing relation in the
5
+ // current database. Thin wrapper over `to_regclass`, which returns NULL
6
+ // when the name doesn't resolve — the only postgres query that cheaply
7
+ // reports existence without raising an error on a missing relation.
8
+ //
9
+ // Used by framework-managed tables (events, archived_streams, snapshots,
10
+ // projections, event-consumers) whose createX() is called from multiple
11
+ // boot paths (setupTestStack, production boot, manual test setups). The
12
+ // guard keeps those calls idempotent without having to interpret the
13
+ // "already exists" error code.
14
+ //
15
+ // if (await tableExists(db, "public.events")) return;
16
+ // await pushTables(db, { events: eventsTable });
17
+ export async function tableExists(
18
+ db: DbConnection | DbTx,
19
+ fullyQualifiedName: string,
20
+ ): Promise<boolean> {
21
+ const rows = await db.execute<{ exists: boolean }>(
22
+ sql`SELECT to_regclass(${fullyQualifiedName}) IS NOT NULL AS exists`,
23
+ );
24
+ return rows[0]?.exists ?? false;
25
+ }