@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,15 @@
1
+ import type { AccessRule } from "../engine/types";
2
+
3
+ // Test-only helper: extracts the role list from a role-based AccessRule,
4
+ // narrowing the union safely. Throws when the rule is openToAll or missing —
5
+ // the tests that call this always expect roles, and a clear error beats a
6
+ // cryptic undefined assertion downstream.
7
+ export function rolesOf(access: AccessRule | undefined): readonly string[] {
8
+ if (!access) {
9
+ throw new Error("expected role-based access rule, got undefined");
10
+ }
11
+ if (!("roles" in access)) {
12
+ throw new Error("expected role-based access rule, got openToAll");
13
+ }
14
+ return access.roles;
15
+ }
@@ -0,0 +1,35 @@
1
+ import type { WriteResult } from "../engine/types";
2
+ import type { WriteErrorInfo } from "../errors";
3
+
4
+ export function expectSuccess<T>(
5
+ result: WriteResult<T>,
6
+ ): asserts result is { isSuccess: true; data: T } {
7
+ if (!result.isSuccess) {
8
+ throw new Error(
9
+ `Expected success but got error: ${result.error.code} (${result.error.message})`,
10
+ );
11
+ }
12
+ }
13
+
14
+ // `matcher` checks either the error code (e.g. "not_found") or looks for a
15
+ // substring in the message. Both are useful: code matches are stable across
16
+ // i18n changes, substrings catch feature-specific detail text.
17
+ export function expectError(
18
+ result: WriteResult,
19
+ matcher?: string,
20
+ ): asserts result is { isSuccess: false; error: WriteErrorInfo } {
21
+ if (result.isSuccess) {
22
+ throw new Error("Expected error but got success");
23
+ }
24
+ if (matcher === undefined) {
25
+ // skip: caller only asked for the type narrowing; nothing else to check.
26
+ return;
27
+ }
28
+ const err = result.error;
29
+ const hit = err.code === matcher || err.message.includes(matcher);
30
+ if (!hit) {
31
+ throw new Error(
32
+ `Expected error code or message to match "${matcher}" but got code="${err.code}", message="${err.message}"`,
33
+ );
34
+ }
35
+ }
@@ -0,0 +1,465 @@
1
+ // @runtime tooling
2
+ //
3
+ // E2E-Generator — leitet strukturierte TestSpecs aus der Registry ab.
4
+ //
5
+ // Grundidee: jede r.screen(...) ist eine testbare Oberfläche. Der Generator
6
+ // iteriert getAllScreens() und baut pro Screen einen Satz TestSpecs (vier
7
+ // Kinds, einer pro "was muss mindestens funktionieren"). Die Specs sind
8
+ // JSON-serialisierbar — Consumer (Sample/Package-e2e) schreiben sie via
9
+ // bun-Script als JSON auf Platte, und ein Playwright-globalSetup triggert
10
+ // das vor jedem Run. Der eigentliche Playwright-Runner liest die JSON und
11
+ // iteriert mit kind-spezifischen Handlern gegen den echten Renderer.
12
+ //
13
+ // Vorteil des 2-Prozess-Modells: die framework-runtime (Registry, drizzle,
14
+ // ioredis-types etc.) lädt NIE im Playwright-Worker. Würde sie das, kollidierte
15
+ // sie mit Playwrights Object.prototype-Symbolen ($$jest-matchers-object) und
16
+ // der Worker würde beim spec-import crashen.
17
+ //
18
+ // Zwei Stufen, jede einzeln testbar:
19
+ // 1. generateE2ESpec(registry) — Registry → TestSpec[]
20
+ // 2. generateZodFixture(schema) — ZodType → plausibler Value
21
+ //
22
+ // Stufe 2 ist intentionally minimal (Strategy a aus dem Plan): string/number/
23
+ // boolean/date/enum/uuid + ZodOptional/ZodDefault-Unwrap. Alles andere wirft
24
+ // "not supported yet" — wir füllen nach, wenn ein echter Caller es braucht.
25
+ // Fake-komplexe-Werte würden nur False-Green-Tests produzieren.
26
+
27
+ import type { z } from "zod";
28
+ import {
29
+ type EntityDefinition,
30
+ type EntityEditScreenDefinition,
31
+ type EntityListScreenDefinition,
32
+ type FieldDefinition,
33
+ normalizeEditField,
34
+ normalizeListColumn,
35
+ parseQn,
36
+ qn,
37
+ type Registry,
38
+ toKebab,
39
+ } from "../engine";
40
+
41
+ // --- Spec-Shape ---
42
+
43
+ // Jeder TestSpec-Variant enthält alles was der Renderer zum Templaten braucht
44
+ // — keine Registry-Referenz, keine Lazy-Lookups. So sind Specs JSON-fähig
45
+ // (debug-dump, Snapshot-Vergleich) und der Renderer bleibt rein Template.
46
+ export type E2ETestSpec =
47
+ | {
48
+ readonly kind: "list-renders";
49
+ readonly screenQn: string;
50
+ readonly title: string;
51
+ readonly urlPath: string;
52
+ }
53
+ | {
54
+ readonly kind: "list-has-fixture-row";
55
+ readonly screenQn: string;
56
+ readonly title: string;
57
+ readonly urlPath: string;
58
+ readonly writeHandlerQn: string;
59
+ readonly fixture: Readonly<Record<string, unknown>>;
60
+ readonly identifyingValue: string;
61
+ }
62
+ | {
63
+ readonly kind: "edit-validates-required";
64
+ readonly screenQn: string;
65
+ readonly title: string;
66
+ readonly urlPath: string;
67
+ readonly requiredFields: readonly string[];
68
+ }
69
+ | {
70
+ readonly kind: "edit-save-persists";
71
+ readonly screenQn: string;
72
+ readonly title: string;
73
+ readonly urlPath: string;
74
+ readonly listUrlPath?: string;
75
+ readonly fills: readonly EditFillOp[];
76
+ readonly identifyingValue: string;
77
+ readonly identifyingField: string;
78
+ };
79
+
80
+ // Strukturierte Anweisung für den edit-save-persists-Renderer. Der Fixture
81
+ // allein sagt nicht, ob ein String in ein Text-Feld (`.fill`) oder in ein
82
+ // Dropdown (`.selectOption`) gehört — der Generator entscheidet anhand der
83
+ // FieldDefinition, der Renderer templated nur.
84
+ export type EditFillOp =
85
+ | { readonly kind: "fill"; readonly field: string; readonly value: string }
86
+ | { readonly kind: "check"; readonly field: string; readonly value: boolean }
87
+ | { readonly kind: "select"; readonly field: string; readonly value: string };
88
+
89
+ export type E2EGeneratorOptions = {
90
+ // Tenant-Slug/UUID als Template-Placeholder. Default "{tenant}" — der
91
+ // Test-Runner ersetzt zur Laufzeit. Wer die URL fix tenant-scoped will,
92
+ // setzt hier den Slug.
93
+ readonly tenantPlaceholder?: string;
94
+ };
95
+
96
+ // --- Stufe 1: Spec-Ableitung aus der Registry ---
97
+
98
+ export function generateE2ESpec(
99
+ registry: Registry,
100
+ options: E2EGeneratorOptions = {},
101
+ ): readonly E2ETestSpec[] {
102
+ const tenant = options.tenantPlaceholder ?? "{tenant}";
103
+ const specs: E2ETestSpec[] = [];
104
+
105
+ for (const [screenQn, screen] of registry.getAllScreens()) {
106
+ if (screen.type === "custom") continue; // keine generische Annahme möglich
107
+ // actionForm: kein generischer E2E-Spec — Author-defined Handler
108
+ // braucht Author-defined Test-Daten, die der Generator nicht kennt.
109
+ if (screen.type === "actionForm") continue;
110
+ // configEdit: dito — die Werte werden über config:write:set
111
+ // pro Field geschrieben, ohne CRUD-Zustand zu generieren wäre der
112
+ // Spec wertlos. Branding/SMTP/etc. sind Author-spezifisch.
113
+ if (screen.type === "configEdit") continue;
114
+ const { scope: feature, name: short } = parseQn(screenQn);
115
+ const urlPath = `/t/${tenant}/${feature}/${short}`;
116
+ const title = `${feature}/${short}`;
117
+
118
+ if (screen.type === "entityList") {
119
+ specs.push(...buildListSpecs(screen, screenQn, title, urlPath, registry));
120
+ } else {
121
+ specs.push(...buildEditSpecs(screen, screenQn, title, urlPath, registry, tenant));
122
+ }
123
+ }
124
+
125
+ return specs;
126
+ }
127
+
128
+ function buildListSpecs(
129
+ screen: EntityListScreenDefinition,
130
+ screenQn: string,
131
+ title: string,
132
+ urlPath: string,
133
+ registry: Registry,
134
+ ): E2ETestSpec[] {
135
+ const out: E2ETestSpec[] = [{ kind: "list-renders", screenQn, title, urlPath }];
136
+
137
+ const entity = registry.getEntity(screen.entity);
138
+ if (!entity) return out;
139
+
140
+ const writeHandlerQn = findCreateHandlerQn(registry, screen.entity);
141
+ if (!writeHandlerQn) return out;
142
+
143
+ const fixture = buildEntityFixture(entity);
144
+ const identifyingValue = pickIdentifyingValue(fixture, screen, entity);
145
+ if (identifyingValue === undefined) return out;
146
+
147
+ out.push({
148
+ kind: "list-has-fixture-row",
149
+ screenQn,
150
+ title,
151
+ urlPath,
152
+ writeHandlerQn,
153
+ fixture,
154
+ identifyingValue,
155
+ });
156
+
157
+ return out;
158
+ }
159
+
160
+ function buildEditSpecs(
161
+ screen: EntityEditScreenDefinition,
162
+ screenQn: string,
163
+ title: string,
164
+ urlPath: string,
165
+ registry: Registry,
166
+ tenant: string,
167
+ ): E2ETestSpec[] {
168
+ const out: E2ETestSpec[] = [];
169
+ const entity = registry.getEntity(screen.entity);
170
+ if (!entity) return out;
171
+
172
+ const requiredFields = collectRequiredEditFields(screen, entity);
173
+ if (requiredFields.length > 0) {
174
+ out.push({ kind: "edit-validates-required", screenQn, title, urlPath, requiredFields });
175
+ }
176
+
177
+ const fixture = buildEntityFixture(entity);
178
+ const listScreen = findListScreenForEntity(registry, screen.entity);
179
+ const listUrlPath = listScreen ? buildScreenUrlPath(listScreen.qn, tenant) : undefined;
180
+
181
+ const identifying = pickIdentifyingForEdit(fixture, screen, entity, listScreen?.def);
182
+ if (identifying) {
183
+ const fills = buildEditFillOps(screen, entity, fixture);
184
+ out.push({
185
+ kind: "edit-save-persists",
186
+ screenQn,
187
+ title,
188
+ urlPath,
189
+ listUrlPath,
190
+ fills,
191
+ identifyingValue: identifying.value,
192
+ identifyingField: identifying.field,
193
+ });
194
+ }
195
+
196
+ return out;
197
+ }
198
+
199
+ function buildScreenUrlPath(qn: string, tenant: string): string {
200
+ const { scope, name } = parseQn(qn);
201
+ return `/t/${tenant}/${scope}/${name}`;
202
+ }
203
+
204
+ function findCreateHandlerQn(registry: Registry, entityName: string): string | undefined {
205
+ // feature.writeHandlers ist mit Short-Names gekeyt ("task:create"), der
206
+ // Registry-Lookup braucht die qualified Form ("tasks:write:task:create").
207
+ // Wir qualifizieren via qn() + toKebab wie die Registry selbst — das ist
208
+ // der Vertrag aus qualified-name.ts.
209
+ for (const feature of registry.features.values()) {
210
+ for (const shortName of Object.keys(feature.writeHandlers ?? {})) {
211
+ if (!shortName.endsWith(":create")) continue;
212
+ const qualified = qn(toKebab(feature.name), "write", toKebab(shortName));
213
+ if (registry.getHandlerEntity(qualified) === entityName) return qualified;
214
+ }
215
+ }
216
+ return undefined;
217
+ }
218
+
219
+ function findListScreenForEntity(
220
+ registry: Registry,
221
+ entityName: string,
222
+ ): { qn: string; def: EntityListScreenDefinition } | undefined {
223
+ for (const [qn, screen] of registry.getAllScreens()) {
224
+ if (screen.type === "entityList" && screen.entity === entityName) {
225
+ return { qn, def: screen };
226
+ }
227
+ }
228
+ return undefined;
229
+ }
230
+
231
+ function collectRequiredEditFields(
232
+ screen: EntityEditScreenDefinition,
233
+ entity: EntityDefinition,
234
+ ): string[] {
235
+ const out: string[] = [];
236
+ for (const section of screen.layout.sections) {
237
+ for (const rawField of section.fields) {
238
+ const { field } = normalizeEditField(rawField);
239
+ const def = entity.fields[field];
240
+ if (def && "required" in def && def.required === true) {
241
+ out.push(field);
242
+ }
243
+ }
244
+ }
245
+ return out;
246
+ }
247
+
248
+ function pickIdentifyingValue(
249
+ fixture: Readonly<Record<string, unknown>>,
250
+ screen: EntityListScreenDefinition,
251
+ entity: EntityDefinition,
252
+ ): string | undefined {
253
+ // Erste Text-Spalte mit Fixture-Wert — das ist die visuell zuverlässigste
254
+ // Identifikation in einer generischen Tabelle.
255
+ for (const raw of screen.columns) {
256
+ const { field } = normalizeListColumn(raw);
257
+ if (entity.fields[field]?.type !== "text") continue;
258
+ const v = fixture[field];
259
+ if (typeof v === "string" && v.length > 0) return v;
260
+ }
261
+ return undefined;
262
+ }
263
+
264
+ function buildEditFillOps(
265
+ screen: EntityEditScreenDefinition,
266
+ entity: EntityDefinition,
267
+ fixture: Readonly<Record<string, unknown>>,
268
+ ): EditFillOp[] {
269
+ // Laufe die Layout-Reihenfolge — das Resultat spiegelt, was ein User
270
+ // tatsächlich ausfüllt. Per Field-Typ entscheiden wir die Interaktions-
271
+ // form; Felder ohne Fixture-Wert (file/image/…) werden übersprungen.
272
+ const ops: EditFillOp[] = [];
273
+ for (const section of screen.layout.sections) {
274
+ for (const raw of section.fields) {
275
+ const { field } = normalizeEditField(raw);
276
+ const def = entity.fields[field];
277
+ if (!def) continue;
278
+ const v = fixture[field];
279
+ if (v === undefined) continue;
280
+
281
+ switch (def.type) {
282
+ case "boolean":
283
+ if (typeof v === "boolean") ops.push({ kind: "check", field, value: v });
284
+ break;
285
+ case "select":
286
+ if (typeof v === "string") ops.push({ kind: "select", field, value: v });
287
+ break;
288
+ case "text":
289
+ case "longText":
290
+ case "number":
291
+ case "date":
292
+ case "timestamp":
293
+ case "tz":
294
+ ops.push({ kind: "fill", field, value: String(v) });
295
+ break;
296
+ // embedded/money/locatedTimestamp/file/image/files/images: keine
297
+ // generische Interaktion — der Test-Autor liefert später einen
298
+ // Hand-Override oder überspringt das Feld.
299
+ default:
300
+ break;
301
+ }
302
+ }
303
+ }
304
+ return ops;
305
+ }
306
+
307
+ function pickIdentifyingForEdit(
308
+ fixture: Readonly<Record<string, unknown>>,
309
+ editScreen: EntityEditScreenDefinition,
310
+ entity: EntityDefinition,
311
+ listScreen: EntityListScreenDefinition | undefined,
312
+ ): { field: string; value: string } | undefined {
313
+ // Bevorzugt ein Feld das im List-Screen als Column auftaucht — sonst ist
314
+ // die "taucht in Liste auf"-Assertion nicht durchführbar.
315
+ const columns = listScreen
316
+ ? new Set(listScreen.columns.map((c) => normalizeListColumn(c).field))
317
+ : undefined;
318
+
319
+ for (const section of editScreen.layout.sections) {
320
+ for (const rawField of section.fields) {
321
+ const { field } = normalizeEditField(rawField);
322
+ if (entity.fields[field]?.type !== "text") continue;
323
+ if (columns && !columns.has(field)) continue;
324
+ const v = fixture[field];
325
+ if (typeof v === "string" && v.length > 0) return { field, value: v };
326
+ }
327
+ }
328
+ return undefined;
329
+ }
330
+
331
+ // --- Stufe 2: Zod-Fixture (Strategy a) ---
332
+
333
+ // Zod 4 stabilisiert die Introspection auf `._def.type` (lowercase Discriminator)
334
+ // + `._def.format` für String-Formate (email/url/uuid/datetime) + `._def.entries`
335
+ // für Enums + `._def.innerType` für optional/default/nullable. Wir greifen
336
+ // gezielt auf diese Felder zu — genau die Form die unser buildInsertSchema
337
+ // emittiert. Andere Typen werfen "not supported yet", bis ein Sample den Fall
338
+ // konkret braucht.
339
+
340
+ type ZodInternals = {
341
+ readonly type?: string;
342
+ readonly format?: string;
343
+ readonly innerType?: z.ZodTypeAny;
344
+ readonly entries?: Record<string, string>;
345
+ };
346
+
347
+ function readZodInternals(schema: z.ZodTypeAny): ZodInternals | undefined {
348
+ return (schema as unknown as { _def?: ZodInternals })._def;
349
+ }
350
+
351
+ export function generateZodFixture(schema: z.ZodTypeAny): unknown {
352
+ const def = readZodInternals(schema);
353
+ const typeName = def?.type;
354
+
355
+ switch (typeName) {
356
+ case "optional":
357
+ case "nullable":
358
+ case "default": {
359
+ if (!def?.innerType) throw new Error(`zod ${typeName} without innerType`);
360
+ return generateZodFixture(def.innerType);
361
+ }
362
+ case "string":
363
+ return fixtureString(def?.format);
364
+ case "number":
365
+ return 1;
366
+ case "boolean":
367
+ return true;
368
+ case "enum": {
369
+ const first = def?.entries ? Object.values(def.entries)[0] : undefined;
370
+ return first ?? "";
371
+ }
372
+ case "date":
373
+ return new Date("2026-01-01T00:00:00Z");
374
+ default:
375
+ throw new Error(`generateZodFixture: not supported yet: ${typeName ?? "<unknown>"}`);
376
+ }
377
+ }
378
+
379
+ function fixtureString(format: string | undefined): string {
380
+ if (format === "email") return "e2e@example.com";
381
+ if (format === "url") return "https://example.com";
382
+ if (format === "uuid" || format === "guid") return "00000000-0000-4000-8000-000000000000";
383
+ if (format === "datetime" || format === "date") return "2026-01-01T00:00:00Z";
384
+ return "e2e-fixture";
385
+ }
386
+
387
+ // --- Fixture aus FieldDefinition (für Stufe 1 statt Zod-Schema) ---
388
+ //
389
+ // Bewusstes Duplicate zu generateZodFixture. Die beiden haben unterschiedliche
390
+ // Aufgaben:
391
+ // generateZodFixture — public, generisch, weiß nur Zod-Primitives
392
+ // buildEntityFixture — intern, Kumiko-Domain, weiß über file-skip,
393
+ // embedded-Shape, money/tz-Objekte und nutzt den
394
+ // Feldnamen für lesbare Prefixes ("e2e-email@...")
395
+ //
396
+ // Ein Zusammenziehen über buildInsertSchema + generateZodFixture würde entweder
397
+ // den Feldnamen-Hint verlieren (alle email-Fixtures bekämen denselben Wert —
398
+ // Unique-Constraints wären inkonsistent) oder einen hint-Parameter in die
399
+ // public API von generateZodFixture drücken, den externe Caller nie brauchen.
400
+ function buildEntityFixture(entity: EntityDefinition): Record<string, unknown> {
401
+ const out: Record<string, unknown> = {};
402
+ for (const [name, field] of Object.entries(entity.fields)) {
403
+ const value = fieldToFixture(name, field);
404
+ if (value !== undefined) out[name] = value;
405
+ }
406
+ return out;
407
+ }
408
+
409
+ function fieldToFixture(name: string, field: FieldDefinition): unknown {
410
+ switch (field.type) {
411
+ case "text": {
412
+ if (field.format === "email") return `e2e-${name}@example.com`;
413
+ if (field.format === "url") return "https://example.com";
414
+ return `e2e ${name}`;
415
+ }
416
+ case "longText":
417
+ // longText hat keine format-Optionen — generic placeholder reicht.
418
+ return `e2e ${name} (long-form content)`;
419
+ case "boolean":
420
+ return true;
421
+ case "select":
422
+ return field.options[0] ?? "";
423
+ case "multiSelect": {
424
+ const first = field.options[0];
425
+ return first ? [first] : [];
426
+ }
427
+ case "number":
428
+ return 1;
429
+ case "money":
430
+ return { amount: 1, currency: "EUR" };
431
+ case "date":
432
+ return "2026-01-01";
433
+ case "timestamp":
434
+ return field.locatedBy !== undefined ? "2026-01-01T00:00:00" : "2026-01-01T00:00:00Z";
435
+ case "tz":
436
+ return "Europe/Berlin";
437
+ case "locatedTimestamp":
438
+ return { at: "2026-01-01T00:00:00", tz: "Europe/Berlin" };
439
+ case "embedded": {
440
+ const sub: Record<string, unknown> = {};
441
+ for (const [subName, subDef] of Object.entries(field.schema)) {
442
+ sub[subName] =
443
+ subDef.type === "text"
444
+ ? `e2e ${subName}`
445
+ : subDef.type === "number"
446
+ ? 1
447
+ : subDef.type === "boolean"
448
+ ? true
449
+ : "2026-01-01";
450
+ }
451
+ return sub;
452
+ }
453
+ case "file":
454
+ case "image":
455
+ case "files":
456
+ case "images":
457
+ // File-Upload-Fixture ist noch nicht generisch: Playwright müsste
458
+ // Dateiinhalte mitliefern, und die Write-API verlangt bereits ge-
459
+ // uploadete fileRef-UUIDs. Lässt sich nachziehen, sobald Sample die
460
+ // Pfade klärt (M4).
461
+ return undefined;
462
+ default:
463
+ return undefined;
464
+ }
465
+ }
@@ -0,0 +1,25 @@
1
+ import { expect } from "vitest";
2
+ import type { WriteErrorInfo } from "../errors";
3
+
4
+ // Vitest's toContain doesn't operate on plain objects, so after the move from
5
+ // string errors to typed WriteErrorInfo the legacy `expect(error).toContain(x)`
6
+ // assertions break. This helper concatenates code, message, and serialized
7
+ // details so existing substring checks against short reason strings keep
8
+ // working without each call site needing to know the new shape.
9
+ //
10
+ // Accepts `string | null` too, so call sites that still get raw strings
11
+ // (e.g. helpers that haven't moved to typed errors yet) keep working.
12
+ export function expectErrorIncludes(
13
+ err: WriteErrorInfo | string | null | undefined,
14
+ substring: string,
15
+ ): void {
16
+ let haystack: string;
17
+ if (err === null || err === undefined) {
18
+ haystack = String(err);
19
+ } else if (typeof err === "string") {
20
+ haystack = err;
21
+ } else {
22
+ haystack = `${err.code} ${err.message} ${JSON.stringify(err.details ?? {})}`;
23
+ }
24
+ expect(haystack).toContain(substring);
25
+ }
@@ -0,0 +1,125 @@
1
+ // @runtime runtime
2
+ //
3
+ // bridgeStub liefert eine HandlerContext-Shape mit throw-on-use Bridge-Methods
4
+ // (ctx.query/write/loadAggregate/...). Wird von Test-Code UND Production-
5
+ // Services genutzt (delivery-service nutzt es um cross-feature notify-Calls
6
+ // ohne echten Dispatcher zu fahren). Daher runtime-Klassifizierung trotz
7
+ // Wohnsitz unter `testing/` — keine vitest-Imports, keine Test-Side-Effects.
8
+ import type {
9
+ AppendEventArgs,
10
+ FetchForWritingArgs,
11
+ HandlerContext,
12
+ SessionUser,
13
+ WriteResult,
14
+ } from "../engine/types";
15
+ import { createNoopMetricsHandle, getFallbackTracer } from "../observability";
16
+ import { createTzContext } from "../time";
17
+
18
+ // Test/service helper: cross-feature bridge methods that throw on use.
19
+ //
20
+ // Production code always receives a full HandlerContext from the Dispatcher's
21
+ // buildHandlerContext (with real query/write closures). Some internal services
22
+ // and tests construct a mini-context manually (typically just `{ db, registry }`)
23
+ // to invoke a single handler. Those call sites don't use ctx.query/write —
24
+ // the stubs make the TypeScript shape match while still failing loudly if
25
+ // anything downstream accidentally reaches for them.
26
+ //
27
+ // Use: `{ db, registry, ...bridgeStub() }`
28
+
29
+ const notAvailable = (what: string) => async (): Promise<never> => {
30
+ throw new Error(
31
+ `ctx.${what} not available in this context — use the dispatcher, not a stubbed handler context`,
32
+ );
33
+ };
34
+
35
+ // Noop observability — hand back the shared fallback tracer so ctx.tracer has
36
+ // a valid Tracer shape. No allocations per call.
37
+ const noopTracer = getFallbackTracer();
38
+
39
+ export function bridgeStub(opts?: {
40
+ readonly user?: SessionUser;
41
+ }): Pick<
42
+ HandlerContext,
43
+ | "query"
44
+ | "queryAs"
45
+ | "write"
46
+ | "writeAs"
47
+ | "appendEvent"
48
+ | "appendEventUnsafe"
49
+ | "fetchForWriting"
50
+ | "loadAggregate"
51
+ | "archiveStream"
52
+ | "restoreStream"
53
+ | "isStreamArchived"
54
+ | "snapshotAggregate"
55
+ | "loadAggregateWithSnapshot"
56
+ | "queryProjection"
57
+ | "resolveAuthClaims"
58
+ | "hasFeature"
59
+ | "metrics"
60
+ | "tracer"
61
+ | "tz"
62
+ | "user"
63
+ > {
64
+ // ctx.user ist Convenience-Alias zu event.user (siehe HandlerContext-
65
+ // Doku). Caller-Code erwartet das Feld; bridgeStub liefert es als
66
+ // Stub mit den Anonymous-Default-Werten wenn kein User explizit
67
+ // übergeben wird. Test-Code mit Identity-Bezug übergibt seinen
68
+ // SessionUser hier und bekommt ihn am ctx zurück.
69
+ const stubUser: SessionUser = opts?.user ?? {
70
+ id: "00000000-0000-0000-0000-000000000000",
71
+ tenantId: "00000000-0000-0000-0000-000000000000" as SessionUser["tenantId"],
72
+ roles: ["all"],
73
+ };
74
+ return {
75
+ user: stubUser,
76
+ query: notAvailable("query") as HandlerContext["query"],
77
+ queryAs: notAvailable("queryAs") as unknown as (
78
+ user: SessionUser,
79
+ qn: string,
80
+ payload: unknown,
81
+ ) => Promise<unknown>,
82
+ write: notAvailable("write") as unknown as (
83
+ qn: string,
84
+ payload: unknown,
85
+ ) => Promise<WriteResult>,
86
+ writeAs: notAvailable("writeAs") as unknown as (
87
+ user: SessionUser,
88
+ qn: string,
89
+ payload: unknown,
90
+ ) => Promise<WriteResult>,
91
+ appendEvent: notAvailable("appendEvent") as unknown as (args: AppendEventArgs) => Promise<void>,
92
+ appendEventUnsafe: notAvailable("appendEventUnsafe") as unknown as (
93
+ args: AppendEventArgs,
94
+ ) => Promise<void>,
95
+ fetchForWriting: notAvailable("fetchForWriting") as unknown as (
96
+ args: FetchForWritingArgs,
97
+ ) => ReturnType<HandlerContext["fetchForWriting"]>,
98
+ loadAggregate: notAvailable("loadAggregate") as unknown as HandlerContext["loadAggregate"],
99
+ archiveStream: notAvailable("archiveStream") as unknown as HandlerContext["archiveStream"],
100
+ restoreStream: notAvailable("restoreStream") as unknown as HandlerContext["restoreStream"],
101
+ isStreamArchived: notAvailable(
102
+ "isStreamArchived",
103
+ ) as unknown as HandlerContext["isStreamArchived"],
104
+ snapshotAggregate: notAvailable(
105
+ "snapshotAggregate",
106
+ ) as unknown as HandlerContext["snapshotAggregate"],
107
+ loadAggregateWithSnapshot: notAvailable(
108
+ "loadAggregateWithSnapshot",
109
+ ) as unknown as HandlerContext["loadAggregateWithSnapshot"],
110
+ queryProjection: notAvailable(
111
+ "queryProjection",
112
+ ) as unknown as HandlerContext["queryProjection"],
113
+ resolveAuthClaims: notAvailable(
114
+ "resolveAuthClaims",
115
+ ) as unknown as HandlerContext["resolveAuthClaims"],
116
+ // Stub defaults to always-enabled — matches the dispatcher's behaviour
117
+ // when no effectiveFeatures resolver is wired (tests without toggles).
118
+ hasFeature: () => true,
119
+ metrics: createNoopMetricsHandle(),
120
+ tracer: noopTracer,
121
+ // Echter TzContext, kein notAvailable — Test-Code nutzt ctx.tz häufig
122
+ // ohne dass es ein "Bridge"-Konzept ist. Default UTC.
123
+ tz: createTzContext(),
124
+ };
125
+ }