@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,78 @@
1
+ import type { ZodType, z } from "zod";
2
+ import type {
3
+ AccessRule,
4
+ HandlerContext,
5
+ KumikoEventTypeMap,
6
+ QueryEvent,
7
+ RateLimitOption,
8
+ WriteEvent,
9
+ WriteResult,
10
+ } from "./types";
11
+
12
+ // --- Write Handler Definition ---
13
+ //
14
+ // TMap propagates the strict event-type-map through the handler's
15
+ // HandlerContext. CRITICAL: TMap is declared as a generic parameter on the
16
+ // FUNCTION (defineWriteHandler), not just on the type. Generic-functions
17
+ // substitute TMap at the USE-site (the caller's compile context, where
18
+ // the augmentation is visible); generic-type-aliases substitute at the
19
+ // definition-site (framework's compile, where the augmentation isn't
20
+ // visible) and collapse `keyof TMap` to `never`. See the spike-findings
21
+ // memory for the empirical proof.
22
+
23
+ export type WriteHandlerDefinition<
24
+ TName extends string = string,
25
+ TSchema extends ZodType = ZodType,
26
+ TData = unknown,
27
+ TMap extends object = KumikoEventTypeMap,
28
+ > = {
29
+ readonly name: TName;
30
+ readonly schema: TSchema;
31
+ readonly access?: AccessRule;
32
+ readonly skipTransitionGuard?: boolean;
33
+ readonly rateLimit?: RateLimitOption;
34
+ readonly handler: (
35
+ event: WriteEvent<z.infer<TSchema>>,
36
+ context: HandlerContext<TMap>,
37
+ ) => Promise<WriteResult<TData>>;
38
+ };
39
+
40
+ export function defineWriteHandler<
41
+ const TName extends string,
42
+ TSchema extends ZodType,
43
+ TData = unknown,
44
+ TMap extends object = KumikoEventTypeMap,
45
+ >(
46
+ def: WriteHandlerDefinition<TName, TSchema, TData, TMap>,
47
+ ): WriteHandlerDefinition<TName, TSchema, TData, TMap> {
48
+ return def;
49
+ }
50
+
51
+ // --- Query Handler Definition ---
52
+
53
+ export type QueryHandlerDefinition<
54
+ TName extends string = string,
55
+ TSchema extends ZodType = ZodType,
56
+ TResult = unknown,
57
+ TMap extends object = KumikoEventTypeMap,
58
+ > = {
59
+ readonly name: TName;
60
+ readonly schema: TSchema;
61
+ readonly access?: AccessRule;
62
+ readonly rateLimit?: RateLimitOption;
63
+ readonly handler: (
64
+ query: QueryEvent<z.infer<TSchema>>,
65
+ context: HandlerContext<TMap>,
66
+ ) => Promise<TResult>;
67
+ };
68
+
69
+ export function defineQueryHandler<
70
+ const TName extends string,
71
+ TSchema extends ZodType,
72
+ TResult = unknown,
73
+ TMap extends object = KumikoEventTypeMap,
74
+ >(
75
+ def: QueryHandlerDefinition<TName, TSchema, TResult, TMap>,
76
+ ): QueryHandlerDefinition<TName, TSchema, TResult, TMap> {
77
+ return def;
78
+ }
@@ -0,0 +1,19 @@
1
+ // Identity function for type-safe role definitions.
2
+ // App defines roles once, all features reference them via the typed object.
3
+ //
4
+ // Usage:
5
+ // const roles = defineRoles(["Admin", "SystemAdmin", "Driver"] as const);
6
+ // roles.Admin // "Admin" — autocomplete + type-checked
7
+ // roles.Admni // TS error
8
+
9
+ type RoleMap<T extends readonly string[]> = {
10
+ readonly [K in T[number]]: K;
11
+ };
12
+
13
+ export function defineRoles<const T extends readonly string[]>(roles: T): RoleMap<T> {
14
+ const map = {} as Record<string, string>;
15
+ for (const role of roles) {
16
+ map[role] = role;
17
+ }
18
+ return map as RoleMap<T>;
19
+ }
@@ -0,0 +1,87 @@
1
+ import type { Registry } from "./types";
2
+
3
+ // Callback that returns the current global-toggle override for a feature.
4
+ // `true` = explicit global row says enabled.
5
+ // `false` = explicit global row says disabled.
6
+ // `undefined` = no row — fall back to the feature's declared toggleableDefault.
7
+ //
8
+ // The feature-toggles bundled feature provides this reader; tests can inject
9
+ // a map-backed stub. The reader is called once per feature during compute;
10
+ // callers that fetch toggles from a DB should batch-load upfront and expose
11
+ // a Map lookup to keep compute() allocation-light.
12
+ export type ToggleReader = (featureName: string) => boolean | undefined;
13
+
14
+ // Compute the set of effectively-enabled features for the current call.
15
+ //
16
+ // Rules (AND-combined, any false wins):
17
+ // 1. Always-on: feature without r.toggleable() → enabled, ignores overrides.
18
+ // 2. Toggleable: enabled = (globalOverride ?? toggleableDefault).
19
+ // 3. Cascade: a feature is only effectively enabled if ALL its r.requires()
20
+ // targets are effectively enabled. Applied transitively.
21
+ //
22
+ // Cascade semantics note: a NON-toggleable feature A that requires a
23
+ // toggleable feature B becomes effectively disabled when B is off. This is
24
+ // intentional — running A's handlers/hooks without its declared dependency
25
+ // would be a worse failure mode than gating A. Ops documentation must call
26
+ // this out so disabling "leaf" features doesn't surprise anyone.
27
+ //
28
+ // The result is a plain Set for O(1) `has(name)` checks in the dispatcher
29
+ // gate, hook filter, and MSP runner. Cycle-safety is delegated to the
30
+ // registry's existing boot-validation (cycles are rejected there).
31
+ export function computeEffectiveFeatures(
32
+ registry: Registry,
33
+ readToggle: ToggleReader,
34
+ ): ReadonlySet<string> {
35
+ // Raw enablement, before cascade.
36
+ const raw = new Map<string, boolean>();
37
+ for (const feature of registry.features.values()) {
38
+ if (feature.toggleableDefault === undefined) {
39
+ raw.set(feature.name, true);
40
+ continue;
41
+ }
42
+ const override = readToggle(feature.name);
43
+ raw.set(feature.name, override ?? feature.toggleableDefault);
44
+ }
45
+
46
+ // Transitive cascade via DFS with memoization. Cycles are already rejected
47
+ // at boot, so no cycle-breaking is needed here.
48
+ const effective = new Map<string, boolean>();
49
+
50
+ function resolve(name: string): boolean {
51
+ const cached = effective.get(name);
52
+ if (cached !== undefined) return cached;
53
+
54
+ const rawEnabled = raw.get(name) ?? true;
55
+ if (!rawEnabled) {
56
+ effective.set(name, false);
57
+ return false;
58
+ }
59
+
60
+ const feature = registry.getFeature(name);
61
+ // Feature referenced by requires() but not loaded — registry boot should
62
+ // have caught this, but be defensive: treat missing deps as disabled
63
+ // (surfaces the same behaviour as "dep is off", not a silent pass).
64
+ if (!feature) {
65
+ effective.set(name, false);
66
+ return false;
67
+ }
68
+
69
+ for (const dep of feature.requires) {
70
+ if (!resolve(dep)) {
71
+ effective.set(name, false);
72
+ return false;
73
+ }
74
+ }
75
+
76
+ effective.set(name, true);
77
+ return true;
78
+ }
79
+
80
+ for (const feature of registry.features.values()) resolve(feature.name);
81
+
82
+ const result = new Set<string>();
83
+ for (const [name, enabled] of effective) {
84
+ if (enabled) result.add(name);
85
+ }
86
+ return result;
87
+ }
@@ -0,0 +1,364 @@
1
+ import { type ZodType, z } from "zod";
2
+ import type { DbRow } from "../db/connection";
3
+ import type { TableColumns } from "../db/dialect";
4
+ import {
5
+ collectReferenceFields,
6
+ enrichRowWithReferences,
7
+ enrichWithReferences,
8
+ } from "../db/eagerload";
9
+ import { createEventStoreExecutor, type EventStoreExecutor } from "../db/event-store-executor";
10
+ import { buildDrizzleTable } from "../db/table-builder";
11
+ import { assertUnreachable } from "../utils";
12
+ import { buildInsertSchema, buildUpdateSchema } from "./schema-builder";
13
+ import type { AccessRule, EntityDefinition, QueryHandlerDef, WriteHandlerDef } from "./types";
14
+
15
+ // Convention-based handler factories for event-sourced aggregates.
16
+ //
17
+ // You register one handler per call (no auto-generation of a whole CRUD set),
18
+ // but the Schema and the executor body are inferred from the entity + verb.
19
+ // Pick the verbs you need — leave out the ones you don't.
20
+ //
21
+ // Two API shapes — pick one per project, don't mix:
22
+ //
23
+ // PREFERRED — one function per verb, type-safe, no magic strings:
24
+ // r.writeHandler(defineEntityCreateHandler("note", noteEntity, { access }))
25
+ // r.writeHandler(defineEntityUpdateHandler("note", noteEntity, { access }))
26
+ // r.writeHandler(defineEntityDeleteHandler("note", noteEntity, { access }))
27
+ // r.queryHandler(defineEntityListHandler("note", noteEntity, { access }))
28
+ // r.queryHandler(defineEntityDetailHandler("note", noteEntity, { access }))
29
+ //
30
+ // LEGACY — single function with verb in the name-string. Kept for
31
+ // backwards-compat; existing apps work as before. New code should use
32
+ // the verb-specific factories above:
33
+ // r.writeHandler(defineEntityCreateHandler("note", noteEntity, { access }))
34
+ // r.queryHandler(defineEntityDetailHandler("note", noteEntity, { access }))
35
+ //
36
+ // For custom logic (default values, business rules, side effects, custom
37
+ // executors with ctx.searchAdapter, ...) write the handler explicitly with
38
+ // r.writeHandler / r.queryHandler — these helpers cover the standard path only.
39
+ //
40
+ // Note on the `as` casts in the handler bodies: WriteHandlerDef.handler's
41
+ // payload type is `unknown` because the dispatcher hands the parsed payload
42
+ // through a runtime-only boundary. Each verb knows its post-parse shape (the
43
+ // schema we just built two lines up enforces it), so the casts are a
44
+ // localised re-declaration of that shape rather than a narrowing escape.
45
+
46
+ const WRITE_VERBS = ["create", "update", "delete", "restore"] as const;
47
+ const QUERY_VERBS = ["list", "detail"] as const;
48
+
49
+ type UpdatePayload = { id: string; version: number; changes: Record<string, unknown> };
50
+ type IdPayload = { id: string };
51
+ type ListPayload = {
52
+ cursor?: string;
53
+ limit?: number;
54
+ search?: string;
55
+ sort?: string;
56
+ sortDirection?: "asc" | "desc";
57
+ // Page-based Pagination: offset 0-basiert, mit limit zusammen statt
58
+ // cursor genutzt. Cursor ist für infinite-scroll / live-tailing
59
+ // präziser; offset ist für klassische Pager (← 1 2 ... N →) wo der
60
+ // User direkt zu "page 7" springen will. Nur EINE Variante pro
61
+ // Request — wenn beide gesetzt sind, gewinnt cursor (DB-stabil).
62
+ offset?: number;
63
+ // Wenn true, liefert der executor zusätzlich eine `total`-Zahl im
64
+ // Response. Extra-Roundtrip auf der DB (COUNT(*)), nur dann sinnvoll
65
+ // wenn der Pager "Page X of Y" rendern muss. Infinite-Scroll oder
66
+ // unbedingte Lists lassen das weg um die COUNT-Kosten zu sparen.
67
+ totalCount?: boolean;
68
+ // Screen-Level Filter (Tier 2.7c) — Author-deklarierter, server-side
69
+ // applizierter WHERE-Clause. Drei Buckets der selben Entity ohne
70
+ // Custom-Pages: jedes Screen hat sein eigenes filter, alle nutzen
71
+ // den gleichen Query-Handler.
72
+ filter?: {
73
+ readonly field: string;
74
+ readonly op: "eq" | "ne" | "lt" | "gt" | "in";
75
+ readonly value: unknown;
76
+ };
77
+ };
78
+
79
+ const idSchema = z.object({ id: z.uuid() });
80
+ const listSchema = z.object({
81
+ cursor: z.string().optional(),
82
+ limit: z.number().optional(),
83
+ search: z.string().optional(),
84
+ sort: z.string().optional(),
85
+ sortDirection: z.enum(["asc", "desc"]).optional(),
86
+ offset: z.number().int().nonnegative().optional(),
87
+ totalCount: z.boolean().optional(),
88
+ filter: z
89
+ .object({
90
+ field: z.string(),
91
+ op: z.enum(["eq", "ne", "lt", "gt", "in"]),
92
+ // Value ist `unknown` zur Compile-Zeit; Server-Side prüft beim
93
+ // Build der WHERE-Clause ob der Type zum Field passt. z.unknown()
94
+ // lässt alles durch; Type-Check kommt im executor.list.
95
+ value: z.unknown(),
96
+ })
97
+ .optional(),
98
+ });
99
+
100
+ function parseHandlerName<TVerb extends string>(
101
+ name: string,
102
+ validVerbs: readonly TVerb[],
103
+ ): { entityName: string; verb: TVerb } {
104
+ const colonIdx = name.indexOf(":");
105
+ if (colonIdx < 0) {
106
+ throw new Error(
107
+ `Handler name "${name}" must use the "<entity>:<verb>" pattern (e.g. "note:create").`,
108
+ );
109
+ }
110
+ const entityName = name.slice(0, colonIdx);
111
+ const verbCandidate = name.slice(colonIdx + 1);
112
+ if (!entityName) {
113
+ throw new Error(`Handler name "${name}" is missing the entity part before the colon.`);
114
+ }
115
+ // @cast-boundary engine-bridge — verbCandidate validated against validVerbs union
116
+ if (!(validVerbs as readonly string[]).includes(verbCandidate)) {
117
+ throw new Error(
118
+ `Unknown verb "${verbCandidate}" in handler name "${name}". Standard verbs: ${validVerbs.join("/")}. For custom verbs use the explicit r.writeHandler / r.queryHandler form.`,
119
+ );
120
+ }
121
+ return { entityName, verb: verbCandidate as TVerb }; // @cast-boundary engine-bridge
122
+ }
123
+
124
+ export function defineEntityWriteHandler(
125
+ name: string,
126
+ entity: EntityDefinition,
127
+ options?: { access?: AccessRule },
128
+ ): WriteHandlerDef {
129
+ const { entityName, verb } = parseHandlerName(name, WRITE_VERBS);
130
+ if (verb === "restore" && !entity.softDelete) {
131
+ throw new Error(
132
+ `"${name}": restore is only valid for entities declared with softDelete: true.`,
133
+ );
134
+ }
135
+
136
+ const table = buildDrizzleTable(entityName, entity);
137
+ const executor = createEventStoreExecutor(table, entity, { entityName });
138
+
139
+ let schema: ZodType;
140
+ let handler: WriteHandlerDef["handler"];
141
+
142
+ switch (verb) {
143
+ case "create":
144
+ schema = buildInsertSchema(entity);
145
+ handler = async (event, ctx) => executor.create(event.payload as DbRow, event.user, ctx.db);
146
+ break;
147
+ case "update":
148
+ schema = z.object({
149
+ id: z.uuid(),
150
+ version: z.number(),
151
+ changes: buildUpdateSchema(entity),
152
+ });
153
+ handler = async (event, ctx) =>
154
+ executor.update(event.payload as UpdatePayload, event.user, ctx.db);
155
+ break;
156
+ case "delete":
157
+ schema = idSchema;
158
+ handler = async (event, ctx) =>
159
+ executor.delete(event.payload as IdPayload, event.user, ctx.db);
160
+ break;
161
+ case "restore":
162
+ schema = idSchema;
163
+ handler = async (event, ctx) =>
164
+ executor.restore(event.payload as IdPayload, event.user, ctx.db);
165
+ break;
166
+ default:
167
+ assertUnreachable(verb, "write verb");
168
+ }
169
+
170
+ return {
171
+ name,
172
+ schema,
173
+ handler,
174
+ ...(options?.access && { access: options.access }),
175
+ };
176
+ }
177
+
178
+ export function defineEntityQueryHandler(
179
+ name: string,
180
+ entity: EntityDefinition,
181
+ options?: { access?: AccessRule },
182
+ ): QueryHandlerDef {
183
+ const { entityName, verb } = parseHandlerName(name, QUERY_VERBS);
184
+
185
+ const table = buildDrizzleTable(entityName, entity);
186
+ const executor = createEventStoreExecutor(table, entity, { entityName });
187
+
188
+ let schema: ZodType;
189
+ let handler: QueryHandlerDef["handler"];
190
+
191
+ // Tier 2.7e Server-Eagerload: wenn die entity reference-Felder hat,
192
+ // resolved der handler nach dem Haupt-Query die UUIDs gegen die
193
+ // referenced entities. Das `_refs`-Property landet auf jeder Row;
194
+ // Renderer-Side useReferenceLookup bleibt als Fallback bestehen
195
+ // (für Apps die manuell Custom-Handler schreiben ohne diesen
196
+ // Wrapper zu nutzen).
197
+ const hasRefFields = collectReferenceFields(entity).length > 0;
198
+
199
+ switch (verb) {
200
+ case "list":
201
+ schema = listSchema;
202
+ handler = async (query, ctx) => {
203
+ // Tier 2.7e Audit-Fix: SearchAdapter aus ctx durchreichen,
204
+ // damit payload.search zur Laufzeit gegen Meilisearch/InMem
205
+ // läuft (Remote-Combobox-Search). Der executor wird beim
206
+ // Definition-Time gebaut, kennt den Adapter also nicht —
207
+ // Runtime-Override holt das.
208
+ const result = await executor.list(query.payload as ListPayload, query.user, ctx.db, {
209
+ ...(ctx.searchAdapter !== undefined && { searchAdapter: ctx.searchAdapter }),
210
+ });
211
+ if (!hasRefFields) return result;
212
+ const enrichedRows = await enrichWithReferences(
213
+ result.rows,
214
+ entity,
215
+ (name) => ctx.registry.getEntity(name),
216
+ ctx.db,
217
+ );
218
+ return { ...result, rows: enrichedRows };
219
+ };
220
+ break;
221
+ case "detail":
222
+ schema = idSchema;
223
+ handler = async (query, ctx) => {
224
+ const row = await executor.detail(query.payload as IdPayload, query.user, ctx.db);
225
+ if (row === null || !hasRefFields) return row;
226
+ return enrichRowWithReferences(row, entity, (name) => ctx.registry.getEntity(name), ctx.db);
227
+ };
228
+ break;
229
+ default:
230
+ assertUnreachable(verb, "query verb");
231
+ }
232
+
233
+ return {
234
+ name,
235
+ schema,
236
+ handler,
237
+ ...(options?.access && { access: options.access }),
238
+ };
239
+ }
240
+
241
+ // ─── Verb-specific factories (preferred) ──────────────────────────────
242
+ //
243
+ // One function per verb — verb is the function name, no magic-string
244
+ // parsing. Each delegates to the legacy `defineEntityWriteHandler` /
245
+ // `defineEntityQueryHandler` with a fixed verb-suffix; the schema +
246
+ // handler-body logic is unchanged. Migration from the legacy API is a
247
+ // 1:1 rename — same arguments minus the verb-prefix in the name-string.
248
+ //
249
+ // Why prefer these over the legacy form:
250
+ // - Verb is checked at compile-time (no runtime "Unknown verb" throw)
251
+ // - IDE auto-completes the four/two verbs after typing `defineEntity`
252
+ // - Function name is self-documenting; no comment needed to explain
253
+ // - Entity-name appears once (used to be doubled: once in the string,
254
+ // once as the entity-arg)
255
+ //
256
+ // Restore-specific note: defineEntityRestoreHandler still validates at
257
+ // runtime that entity.softDelete === true. A compile-time-only check
258
+ // would need a Branded-EntityDefinition with `softDelete: true` literal
259
+ // — feasible but not yet wired; the runtime guard catches misuse.
260
+
261
+ type EntityHandlerOptions = { readonly access?: AccessRule };
262
+
263
+ export function defineEntityCreateHandler(
264
+ entityName: string,
265
+ entity: EntityDefinition,
266
+ options?: EntityHandlerOptions,
267
+ ): WriteHandlerDef {
268
+ return defineEntityWriteHandler(`${entityName}:create`, entity, options);
269
+ }
270
+
271
+ export function defineEntityUpdateHandler(
272
+ entityName: string,
273
+ entity: EntityDefinition,
274
+ options?: EntityHandlerOptions,
275
+ ): WriteHandlerDef {
276
+ return defineEntityWriteHandler(`${entityName}:update`, entity, options);
277
+ }
278
+
279
+ export function defineEntityDeleteHandler(
280
+ entityName: string,
281
+ entity: EntityDefinition,
282
+ options?: EntityHandlerOptions,
283
+ ): WriteHandlerDef {
284
+ return defineEntityWriteHandler(`${entityName}:delete`, entity, options);
285
+ }
286
+
287
+ export function defineEntityRestoreHandler(
288
+ entityName: string,
289
+ entity: EntityDefinition,
290
+ options?: EntityHandlerOptions,
291
+ ): WriteHandlerDef {
292
+ return defineEntityWriteHandler(`${entityName}:restore`, entity, options);
293
+ }
294
+
295
+ export function defineEntityListHandler(
296
+ entityName: string,
297
+ entity: EntityDefinition,
298
+ options?: EntityHandlerOptions,
299
+ ): QueryHandlerDef {
300
+ return defineEntityQueryHandler(`${entityName}:list`, entity, options);
301
+ }
302
+
303
+ export function defineEntityDetailHandler(
304
+ entityName: string,
305
+ entity: EntityDefinition,
306
+ options?: EntityHandlerOptions,
307
+ ): QueryHandlerDef {
308
+ return defineEntityQueryHandler(`${entityName}:detail`, entity, options);
309
+ }
310
+
311
+ // biome-ignore lint/suspicious/noExplicitAny: Drizzle dynamic table — erased on purpose, same as db/event-store-executor.ts does.
312
+ type AnyTable = TableColumns<any>;
313
+
314
+ // Bundle the two calls every custom write-handler opens with: build the
315
+ // Drizzle table from the entity, then wire an event-store executor onto it.
316
+ // The pair is identical in every sample that hand-writes handlers, so the
317
+ // helper collapses 3-4 lines + the { entityName } bookkeeping into one.
318
+ //
319
+ // const { table, executor } = createEntityExecutor("counter", counterEntity);
320
+ //
321
+ // Keep using the explicit buildDrizzleTable / createEventStoreExecutor duo
322
+ // when you need search-adapter / entity-cache options on the executor — this
323
+ // helper covers the zero-config case.
324
+ export function createEntityExecutor(
325
+ entityName: string,
326
+ entity: EntityDefinition,
327
+ ): { readonly table: AnyTable; readonly executor: EventStoreExecutor } {
328
+ const table = buildDrizzleTable(entityName, entity);
329
+ const executor = createEventStoreExecutor(table, entity, { entityName });
330
+ return { table, executor };
331
+ }
332
+
333
+ // Wrap a projection read into a zero-argument query handler. Use when the
334
+ // read is "give me all rows from projection X, tenant-scoped" — the common
335
+ // shape for list-views backed by an MSP/projection table.
336
+ //
337
+ // r.queryHandler(
338
+ // defineProjectionQueryHandler("revenue:list", "showcase:projection:customer-revenue", {
339
+ // access: { openToAll: true },
340
+ // }),
341
+ // );
342
+ //
343
+ // For anything more involved (filters, joins, custom shaping), write the
344
+ // query-handler explicitly with ctx.queryProjection or a raw select.
345
+ export function defineProjectionQueryHandler(
346
+ name: string,
347
+ projectionQualifiedName: string,
348
+ options?: { access?: AccessRule; allTenants?: boolean },
349
+ ): QueryHandlerDef {
350
+ return {
351
+ name,
352
+ schema: z.object({}),
353
+ // Returns the raw row array — matches ctx.queryProjection's shape so the
354
+ // helper is a drop-in for the inline `async (_q, ctx) => ctx.queryProjection(...)`
355
+ // handler. Wrap the result in the caller's handler when you need
356
+ // pagination envelopes or added metadata.
357
+ handler: async (_query, ctx) =>
358
+ ctx.queryProjection(
359
+ projectionQualifiedName,
360
+ options?.allTenants ? { allTenants: true } : undefined,
361
+ ),
362
+ ...(options?.access && { access: options.access }),
363
+ };
364
+ }
@@ -0,0 +1,73 @@
1
+ import type { AppendEventArgs, EventDef, HandlerContext } from "./types/handlers";
2
+
3
+ // The ctx-surface emitEvent needs. Accepting the narrow shape lets tests or
4
+ // MultiStreamApplyContext-style callers pass their own appendEvent without
5
+ // a full HandlerContext. Real handlers just pass `ctx`.
6
+ //
7
+ // Uses appendEventUnsafe internally because EventDef's TPayload comes from
8
+ // a runtime-defined zod-schema — emitEvent does the type-check itself via
9
+ // the EventDef generic, so it doesn't need the strict KumikoEventTypeMap
10
+ // path. The strict appendEvent is for direct in-handler callsites.
11
+ export type EmitCtx = Pick<HandlerContext, "appendEventUnsafe">;
12
+
13
+ // Typed wrapper around ctx.appendEvent. Two wins over the raw call:
14
+ //
15
+ // 1. The payload is checked against the EventDef's inferred TPayload,
16
+ // so a Zod-schema mismatch becomes a compile error at the emit site
17
+ // rather than a runtime reject from the event-store append.
18
+ // 2. The event name is carried by the def — no hand-typed
19
+ // "<feature>:event:<short>" string, no typos.
20
+ //
21
+ // await emitEvent(ctx, orderPlaced, {
22
+ // aggregateId: String(result.data.id),
23
+ // aggregateType: "pubsub-order",
24
+ // payload: { id, customer, product },
25
+ // });
26
+ //
27
+ // aggregateType stays explicit on purpose — the EventDef doesn't know which
28
+ // aggregate owns an event (cross-feature reuse is legal). Use the raw
29
+ // ctx.appendEvent when the event name is computed at runtime.
30
+ export async function emitEvent<TPayload>(
31
+ ctx: EmitCtx,
32
+ eventDef: EventDef<TPayload>,
33
+ args: {
34
+ readonly aggregateId: string;
35
+ readonly aggregateType: string;
36
+ readonly payload: TPayload;
37
+ },
38
+ ): Promise<void> {
39
+ const appendArgs: AppendEventArgs = {
40
+ aggregateId: args.aggregateId,
41
+ aggregateType: args.aggregateType,
42
+ type: eventDef.name,
43
+ payload: args.payload,
44
+ };
45
+ await ctx.appendEventUnsafe(appendArgs);
46
+ }
47
+
48
+ // Read-side counterpart: narrow a StoredEvent's `payload` (declared as
49
+ // `Record<string, unknown>` because a stream carries many event types) to
50
+ // the EventDef's inferred TPayload. Replaces the scattered
51
+ // `event.payload as { ... }` casts inside projection-apply handlers and
52
+ // reducers — the cast is named, the shape comes from a single source
53
+ // (the defineEvent() schema), and a mismatched event-type throws loudly.
54
+ //
55
+ // const p = typedPayload(event, approved);
56
+ // // p is typed as { amountCents: number; approvedBy: string }
57
+ //
58
+ // The runtime type-check guards against projection-apply maps that hand-
59
+ // build their key → handler mapping and accidentally route events to the
60
+ // wrong typedPayload call. No Zod-parse — validation happened at
61
+ // appendEvent time; this is a read-path helper.
62
+ export function typedPayload<TPayload>(
63
+ event: { readonly type: string; readonly payload: Record<string, unknown> },
64
+ eventDef: EventDef<TPayload>,
65
+ ): TPayload {
66
+ if (event.type !== eventDef.name) {
67
+ throw new Error(
68
+ `[Kumiko] typedPayload: event type "${event.type}" does not match EventDef "${eventDef.name}". ` +
69
+ `Check the projection-apply / reducer mapping — the event was routed to the wrong handler.`,
70
+ );
71
+ }
72
+ return event.payload as TPayload;
73
+ }