@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,340 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
2
+ import { z } from "zod";
3
+ import { createEventStoreExecutor } from "../db/event-store-executor";
4
+ import { buildDrizzleTable } from "../db/table-builder";
5
+ import {
6
+ createBooleanField,
7
+ createEntity,
8
+ createSelectField,
9
+ createTextField,
10
+ defineFeature,
11
+ } from "../engine";
12
+ import { createEntityTable, setupTestStack, type TestStack, TestUsers } from "../stack";
13
+ import { expectErrorIncludes } from "../testing";
14
+
15
+ // Two entities, both with a field named `status`, but different transitions.
16
+ // Before the fix, the dispatcher cached the transition map by `fieldName`
17
+ // alone — so whichever entity ran through the pipeline first would poison
18
+ // the cache, and the other entity would be validated against the wrong map.
19
+
20
+ const invoiceEntity = createEntity({
21
+ table: "tg_invoices",
22
+ fields: {
23
+ title: createTextField({ required: true }),
24
+ status: createSelectField({ options: ["draft", "sent", "paid"] as const, default: "draft" }),
25
+ },
26
+ transitions: {
27
+ status: {
28
+ draft: ["sent"],
29
+ sent: ["paid"],
30
+ paid: [],
31
+ },
32
+ },
33
+ });
34
+
35
+ const orderEntity = createEntity({
36
+ table: "tg_orders",
37
+ fields: {
38
+ title: createTextField({ required: true }),
39
+ status: createSelectField({
40
+ options: ["open", "shipped", "delivered"] as const,
41
+ default: "open",
42
+ }),
43
+ },
44
+ transitions: {
45
+ status: {
46
+ open: ["shipped"],
47
+ shipped: ["delivered"],
48
+ delivered: [],
49
+ },
50
+ },
51
+ });
52
+
53
+ // A soft-deletable entity to verify the auto-guard skips isDeleted rows.
54
+ const ticketEntity = createEntity({
55
+ table: "tg_tickets",
56
+ fields: {
57
+ title: createTextField({ required: true }),
58
+ status: createSelectField({ options: ["open", "closed"] as const, default: "open" }),
59
+ isDeleted: createBooleanField({ default: false }),
60
+ },
61
+ softDelete: true,
62
+ transitions: {
63
+ status: {
64
+ open: ["closed"],
65
+ closed: [],
66
+ },
67
+ },
68
+ });
69
+
70
+ const invoiceTable = buildDrizzleTable("invoice", invoiceEntity);
71
+ const orderTable = buildDrizzleTable("order", orderEntity);
72
+ const ticketTable = buildDrizzleTable("ticket", ticketEntity);
73
+
74
+ const feature = defineFeature("txguard", (r) => {
75
+ r.entity("invoice", invoiceEntity);
76
+ r.entity("order", orderEntity);
77
+ r.entity("ticket", ticketEntity);
78
+
79
+ r.writeHandler(
80
+ "invoice:create",
81
+ z.object({ title: z.string(), status: z.string().optional() }),
82
+ async (event, ctx) =>
83
+ createEventStoreExecutor(invoiceTable, invoiceEntity, { entityName: "invoice" }).create(
84
+ event.payload,
85
+ event.user,
86
+ ctx.db,
87
+ ),
88
+ { access: { openToAll: true } },
89
+ );
90
+
91
+ r.writeHandler(
92
+ "invoice:update",
93
+ z.object({
94
+ id: z.uuid(),
95
+ version: z.number().optional(),
96
+ changes: z.record(z.string(), z.unknown()),
97
+ }),
98
+ async (event, ctx) =>
99
+ createEventStoreExecutor(invoiceTable, invoiceEntity, { entityName: "invoice" }).update(
100
+ event.payload,
101
+ event.user,
102
+ ctx.db,
103
+ ),
104
+ { access: { openToAll: true } },
105
+ );
106
+
107
+ r.writeHandler(
108
+ "order:create",
109
+ z.object({ title: z.string(), status: z.string().optional() }),
110
+ async (event, ctx) =>
111
+ createEventStoreExecutor(orderTable, orderEntity, { entityName: "order" }).create(
112
+ event.payload,
113
+ event.user,
114
+ ctx.db,
115
+ ),
116
+ { access: { openToAll: true } },
117
+ );
118
+
119
+ r.writeHandler(
120
+ "order:update",
121
+ z.object({
122
+ id: z.uuid(),
123
+ version: z.number().optional(),
124
+ changes: z.record(z.string(), z.unknown()),
125
+ }),
126
+ async (event, ctx) =>
127
+ createEventStoreExecutor(orderTable, orderEntity, { entityName: "order" }).update(
128
+ event.payload,
129
+ event.user,
130
+ ctx.db,
131
+ ),
132
+ { access: { openToAll: true } },
133
+ );
134
+
135
+ r.writeHandler(
136
+ "ticket:create",
137
+ z.object({ title: z.string() }),
138
+ async (event, ctx) =>
139
+ createEventStoreExecutor(ticketTable, ticketEntity, { entityName: "ticket" }).create(
140
+ event.payload,
141
+ event.user,
142
+ ctx.db,
143
+ ),
144
+ { access: { openToAll: true } },
145
+ );
146
+
147
+ r.writeHandler(
148
+ "ticket:delete",
149
+ z.object({ id: z.uuid() }),
150
+ async (event, ctx) =>
151
+ createEventStoreExecutor(ticketTable, ticketEntity, { entityName: "ticket" }).delete(
152
+ event.payload,
153
+ event.user,
154
+ ctx.db,
155
+ ),
156
+ { access: { openToAll: true } },
157
+ );
158
+
159
+ r.writeHandler(
160
+ "ticket:update",
161
+ z.object({
162
+ id: z.uuid(),
163
+ version: z.number().optional(),
164
+ changes: z.record(z.string(), z.unknown()),
165
+ }),
166
+ async (event, ctx) =>
167
+ createEventStoreExecutor(ticketTable, ticketEntity, { entityName: "ticket" }).update(
168
+ event.payload,
169
+ event.user,
170
+ ctx.db,
171
+ ),
172
+ { access: { openToAll: true } },
173
+ );
174
+ });
175
+
176
+ let stack: TestStack;
177
+ const admin = TestUsers.admin;
178
+
179
+ beforeAll(async () => {
180
+ stack = await setupTestStack({ features: [feature] });
181
+ await createEntityTable(stack.db, invoiceEntity);
182
+ await createEntityTable(stack.db, orderEntity);
183
+ await createEntityTable(stack.db, ticketEntity);
184
+ });
185
+
186
+ afterAll(async () => {
187
+ await stack.cleanup();
188
+ });
189
+
190
+ describe("auto transition guard: per-entity transition map (cache key includes entity)", () => {
191
+ test("entity A's transitions don't leak to entity B when both have `status`", async () => {
192
+ // Create both rows in their default states (draft / open)
193
+ const invoice = await stack.http.writeOk<{ id: number }>(
194
+ "txguard:write:invoice:create",
195
+ { title: "Inv-1" },
196
+ admin,
197
+ );
198
+ const order = await stack.http.writeOk<{ id: number }>(
199
+ "txguard:write:order:create",
200
+ { title: "Ord-1" },
201
+ admin,
202
+ );
203
+
204
+ // Invoice: draft → sent is ALLOWED by invoice transitions.
205
+ // If the cache collided with order's map (open→shipped), the dispatcher
206
+ // would reject "sent" as not a valid target from any known state.
207
+ const invoiceResult = await stack.http.writeOk<{ data: { status: string } }>(
208
+ "txguard:write:invoice:update",
209
+ { id: invoice["id"], changes: { status: "sent" }, version: 1 },
210
+ admin,
211
+ );
212
+ expect(invoiceResult.data.status).toBe("sent");
213
+
214
+ // Order: open → shipped is ALLOWED by order transitions.
215
+ // If the cache now holds invoice's map, this would be rejected.
216
+ const orderResult = await stack.http.writeOk<{ data: { status: string } }>(
217
+ "txguard:write:order:update",
218
+ { id: order["id"], changes: { status: "shipped" }, version: 1 },
219
+ admin,
220
+ );
221
+ expect(orderResult.data.status).toBe("shipped");
222
+ });
223
+
224
+ test("invalid transition on entity A still rejects (guard actually fires)", async () => {
225
+ const invoice = await stack.http.writeOk<{ id: number }>(
226
+ "txguard:write:invoice:create",
227
+ { title: "Inv-2" },
228
+ admin,
229
+ );
230
+
231
+ // draft → paid is NOT allowed (only draft → sent, sent → paid)
232
+ const err = await stack.http.writeErr(
233
+ "txguard:write:invoice:update",
234
+ { id: invoice["id"], changes: { status: "paid" }, version: 1 },
235
+ admin,
236
+ );
237
+ expectErrorIncludes(err, "Invalid transition");
238
+ expectErrorIncludes(err, "draft");
239
+ expectErrorIncludes(err, "paid");
240
+ });
241
+
242
+ test("invalid transition uses entity B's own map, not a leaked one", async () => {
243
+ const order = await stack.http.writeOk<{ id: number }>(
244
+ "txguard:write:order:create",
245
+ { title: "Ord-2" },
246
+ admin,
247
+ );
248
+
249
+ // open → delivered is NOT allowed (only open → shipped, shipped → delivered)
250
+ const err = await stack.http.writeErr(
251
+ "txguard:write:order:update",
252
+ { id: order["id"], changes: { status: "delivered" }, version: 1 },
253
+ admin,
254
+ );
255
+ expectErrorIncludes(err, "Invalid transition");
256
+ expectErrorIncludes(err, "open");
257
+ expectErrorIncludes(err, "delivered");
258
+ });
259
+
260
+ test("soft-deleted rows bypass the guard (no state-machine enforcement on zombies)", async () => {
261
+ const ticket = await stack.http.writeOk<{ id: string }>(
262
+ "txguard:write:ticket:create",
263
+ { title: "T-1" },
264
+ admin,
265
+ );
266
+
267
+ // Raw-DB-mark-deleted — we need a soft-deleted row whose status is a
268
+ // terminal state. If the guard fired, any status write would throw
269
+ // "Invalid transition: closed → <x>". We want it silently skipped.
270
+ const { eq } = await import("drizzle-orm");
271
+ await stack.db
272
+ .update(ticketTable)
273
+ .set({ status: "closed", isDeleted: true })
274
+ .where(eq(ticketTable["id"], ticket["id"]));
275
+
276
+ // Attempting to move a deleted ticket to "open" would normally violate
277
+ // "closed → []" (no allowed targets). With the softDelete skip, the
278
+ // guard steps aside and the request only fails because CrudExecutor
279
+ // filters deleted rows from updates — giving a `not_found`, not a
280
+ // transition error. That distinction proves the guard skipped.
281
+ const err = await stack.http.writeErr(
282
+ "txguard:write:ticket:update",
283
+ { id: ticket["id"], changes: { status: "open" }, version: 1 },
284
+ admin,
285
+ );
286
+ // Guard was skipped → we don't see "Invalid transition", we see a different
287
+ // failure (soft-deleted row is filtered out of the lookup, so "not_found").
288
+ expect(JSON.stringify(err)).not.toContain("Invalid transition");
289
+ expectErrorIncludes(err, "not_found");
290
+ });
291
+
292
+ test("concurrent writes on the same row serialize via SELECT FOR UPDATE", async () => {
293
+ // Two parallel requests both trying to move the SAME invoice from draft.
294
+ // One legal: draft → sent. One deliberately stale (would also be legal
295
+ // draft → sent, but since we hold the row lock, the second caller sees
296
+ // the updated state `sent` when its turn comes and must fail — either
297
+ // via the transition guard (sent → sent is not defined) or version
298
+ // conflict. Without the FOR UPDATE both could snapshot `draft` under
299
+ // READ COMMITTED and race past the guard; the second UPDATE would then
300
+ // fail only via the optimistic lock, losing the specific error signal.
301
+ const invoice = await stack.http.writeOk<{ id: number }>(
302
+ "txguard:write:invoice:create",
303
+ { title: "Concurrent-1" },
304
+ admin,
305
+ );
306
+
307
+ const [res1, res2] = await Promise.all([
308
+ stack.http.write(
309
+ "txguard:write:invoice:update",
310
+ { id: invoice["id"], changes: { status: "sent" }, version: 1 },
311
+ admin,
312
+ ),
313
+ stack.http.write(
314
+ "txguard:write:invoice:update",
315
+ { id: invoice["id"], changes: { status: "sent" }, version: 1 },
316
+ admin,
317
+ ),
318
+ ]);
319
+
320
+ const body1 = (await res1.json()) as { isSuccess: boolean; error?: unknown };
321
+ const body2 = (await res2.json()) as { isSuccess: boolean; error?: unknown };
322
+
323
+ // Exactly one of the two must win. If both succeeded, the row lock
324
+ // didn't serialize — both wrote draft→sent using a stale snapshot.
325
+ const successes = [body1, body2].filter((b) => b.isSuccess);
326
+ const failures = [body1, body2].filter((b) => !b.isSuccess);
327
+ expect(successes).toHaveLength(1);
328
+ expect(failures).toHaveLength(1);
329
+
330
+ // The loser saw state `sent` on its (now serialized) guard read and
331
+ // rejected the transition — NOT a version_conflict, which would be the
332
+ // optimistic-lock fallback if the guard had race-passed.
333
+ // The loser's UnprocessableError carries reason "invalid_transition" plus
334
+ // the from/to states for debugging.
335
+ const loser = failures[0]?.error as { code: string; details: { reason: string; from: string } };
336
+ expect(loser.code).toBe("unprocessable");
337
+ expect(loser.details.reason).toBe("invalid_transition");
338
+ expect(loser.details.from).toBe("sent");
339
+ });
340
+ });
@@ -0,0 +1,337 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { z } from "zod";
3
+ import {
4
+ createEntity,
5
+ createRegistry,
6
+ createTextField,
7
+ defineFeature,
8
+ type TenantId,
9
+ } from "../../engine";
10
+ import { createTestUser, TestUsers } from "../../stack";
11
+ import { buildServer } from "../server";
12
+
13
+ const JWT_SECRET = "test-secret-at-least-32-chars-long!!";
14
+
15
+ const testFeature = defineFeature("test", (r) => {
16
+ r.entity("item", createEntity({ table: "Items", fields: { name: createTextField() } }));
17
+
18
+ r.writeHandler(
19
+ "item:create",
20
+ z.object({ name: z.string().min(1) }),
21
+ async (event) => ({ isSuccess: true, data: { name: event.payload.name } }),
22
+ { access: { roles: ["Admin"] } },
23
+ );
24
+
25
+ r.queryHandler(
26
+ "item:list",
27
+ z.object({ search: z.string().optional() }),
28
+ async () => [{ id: 1, name: "Test" }],
29
+ { access: { openToAll: true } },
30
+ );
31
+ });
32
+
33
+ const registry = createRegistry([testFeature]);
34
+ const { app, jwt } = buildServer({ registry, context: {}, jwtSecret: JWT_SECRET });
35
+
36
+ const adminUser = TestUsers.admin;
37
+ const guestUser = createTestUser({ id: 2, roles: ["Guest"] });
38
+
39
+ async function authHeader(user: {
40
+ id: string;
41
+ tenantId: TenantId;
42
+ roles: readonly string[];
43
+ }): Promise<Record<string, string>> {
44
+ const token = await jwt.sign(user);
45
+ return { Authorization: `Bearer ${token}` };
46
+ }
47
+
48
+ function req(method: string, path: string, body?: unknown, headers?: Record<string, string>) {
49
+ const init: RequestInit = {
50
+ method,
51
+ headers: { "Content-Type": "application/json", ...headers },
52
+ };
53
+ if (body) init.body = JSON.stringify(body);
54
+ return app.request(path, init);
55
+ }
56
+
57
+ // --- Health ---
58
+
59
+ describe("health", () => {
60
+ test("GET /health returns ok", async () => {
61
+ const res = await req("GET", "/health");
62
+ expect(res.status).toBe(200);
63
+ expect(await res.json()).toEqual({ status: "ok" });
64
+ });
65
+ });
66
+
67
+ // --- Auth ---
68
+
69
+ describe("auth middleware", () => {
70
+ test("rejects request without token", async () => {
71
+ const res = await req("POST", "/api/write", {
72
+ type: "test:write:item:create",
73
+ payload: { name: "x" },
74
+ });
75
+ expect(res.status).toBe(401);
76
+ });
77
+
78
+ test("rejects invalid token", async () => {
79
+ const res = await req(
80
+ "POST",
81
+ "/api/write",
82
+ { type: "test:write:item:create", payload: { name: "x" } },
83
+ {
84
+ Authorization: "Bearer invalid.token.here",
85
+ },
86
+ );
87
+ expect(res.status).toBe(401);
88
+ });
89
+
90
+ test("accepts valid token", async () => {
91
+ const headers = await authHeader(adminUser);
92
+ const res = await req(
93
+ "POST",
94
+ "/api/write",
95
+ { type: "test:write:item:create", payload: { name: "Test" } },
96
+ headers,
97
+ );
98
+ expect(res.status).toBe(200);
99
+ });
100
+ });
101
+
102
+ // --- Write ---
103
+
104
+ describe("POST /api/write", () => {
105
+ test("dispatches write and returns result", async () => {
106
+ const headers = await authHeader(adminUser);
107
+ const res = await req(
108
+ "POST",
109
+ "/api/write",
110
+ { type: "test:write:item:create", payload: { name: "Hello" } },
111
+ headers,
112
+ );
113
+
114
+ expect(res.status).toBe(200);
115
+ const body = await res.json();
116
+ expect(body.isSuccess).toBe(true);
117
+ expect(body.data.name).toBe("Hello");
118
+ });
119
+
120
+ test("returns 400 for validation error", async () => {
121
+ const headers = await authHeader(adminUser);
122
+ const res = await req(
123
+ "POST",
124
+ "/api/write",
125
+ { type: "test:write:item:create", payload: { name: "" } },
126
+ headers,
127
+ );
128
+
129
+ expect(res.status).toBe(400);
130
+ const body = await res.json();
131
+ expect(body.error).toMatchObject({ code: "validation_error", i18nKey: expect.any(String) });
132
+ });
133
+
134
+ test("returns 403 for access denied", async () => {
135
+ const headers = await authHeader(guestUser);
136
+ const res = await req(
137
+ "POST",
138
+ "/api/write",
139
+ { type: "test:write:item:create", payload: { name: "Test" } },
140
+ headers,
141
+ );
142
+
143
+ expect(res.status).toBe(403);
144
+ const body = await res.json();
145
+ expect(body.error).toMatchObject({ code: "access_denied" });
146
+ });
147
+ });
148
+
149
+ // --- Query ---
150
+
151
+ describe("POST /api/query", () => {
152
+ test("dispatches query and returns data", async () => {
153
+ const headers = await authHeader(adminUser);
154
+ const res = await req(
155
+ "POST",
156
+ "/api/query",
157
+ { type: "test:query:item:list", payload: {} },
158
+ headers,
159
+ );
160
+
161
+ expect(res.status).toBe(200);
162
+ const body = await res.json();
163
+ expect(body.data).toEqual([{ id: 1, name: "Test" }]);
164
+ });
165
+
166
+ test("returns 404 for unknown query", async () => {
167
+ const headers = await authHeader(adminUser);
168
+ const res = await req("POST", "/api/query", { type: "nope", payload: {} }, headers);
169
+ expect(res.status).toBe(404);
170
+ });
171
+ });
172
+
173
+ // --- Command ---
174
+
175
+ describe("POST /api/command", () => {
176
+ test("dispatches command and returns 202", async () => {
177
+ const headers = await authHeader(adminUser);
178
+ const res = await req(
179
+ "POST",
180
+ "/api/command",
181
+ { type: "test:write:item:create", payload: { name: "Fire" } },
182
+ headers,
183
+ );
184
+
185
+ expect(res.status).toBe(202);
186
+ const body = await res.json();
187
+ expect(body.ok).toBe(true);
188
+ });
189
+
190
+ test("returns 403 for access denied", async () => {
191
+ const headers = await authHeader(guestUser);
192
+ const res = await req(
193
+ "POST",
194
+ "/api/command",
195
+ { type: "test:write:item:create", payload: { name: "x" } },
196
+ headers,
197
+ );
198
+ expect(res.status).toBe(403);
199
+ });
200
+ });
201
+
202
+ // --- SSE ---
203
+
204
+ describe("GET /api/sse", () => {
205
+ test("rejects without auth", async () => {
206
+ const res = await app.request("/api/sse");
207
+ expect(res.status).toBe(401);
208
+ });
209
+
210
+ test("returns event stream with auth", async () => {
211
+ const headers = await authHeader(adminUser);
212
+ const res = await app.request("/api/sse", { headers });
213
+ expect(res.status).toBe(200);
214
+ expect(res.headers.get("content-type")).toContain("text/event-stream");
215
+ });
216
+ });
217
+
218
+ // --- r.httpRoute (feature-deklarierte HTTP-Routes außerhalb /api/) ---
219
+
220
+ describe("feature-declared HTTP routes (r.httpRoute)", () => {
221
+ // Eigenes buildServer-Setup mit einem Feature das eine Route deklariert.
222
+ // Pinst die Verdrahtung end-to-end: r.httpRoute → registry → buildServer
223
+ // → Hono-app.{get,post}(path) → Response. deps.app erlaubt internal-call
224
+ // an /api/* (gleicher Auth-Pfad wie ein echter HTTP-Call).
225
+ const routeFeature = defineFeature("routes", (r) => {
226
+ r.entity("item", createEntity({ table: "Items", fields: { name: createTextField() } }));
227
+ r.queryHandler("item:list", z.object({}), async () => [{ id: 7 }], {
228
+ access: { openToAll: true },
229
+ });
230
+ r.httpRoute({
231
+ method: "GET",
232
+ path: "/version",
233
+ anonymous: true,
234
+ handler: (c) => c.json({ version: "1.2.3" }),
235
+ });
236
+ r.httpRoute({
237
+ method: "GET",
238
+ path: "/probe-deps",
239
+ anonymous: true,
240
+ handler: (c, deps) => {
241
+ // Beweist dass deps.app die Hono-App-Instanz ist — Handler kann
242
+ // sie für internal app.fetch(...)-Calls nutzen (typischer
243
+ // Use-Case: feed.xml ruft /api/query intern auf).
244
+ return c.json({
245
+ hasApp: typeof deps.app === "object" && typeof deps.app.fetch === "function",
246
+ });
247
+ },
248
+ });
249
+ });
250
+ const routeRegistry = createRegistry([routeFeature]);
251
+ const { app: routeApp } = buildServer({
252
+ registry: routeRegistry,
253
+ context: {},
254
+ jwtSecret: JWT_SECRET,
255
+ });
256
+
257
+ test("GET /version returnt deklarierten JSON-Response", async () => {
258
+ const res = await routeApp.request("/version");
259
+ expect(res.status).toBe(200);
260
+ expect(await res.json()).toEqual({ version: "1.2.3" });
261
+ });
262
+
263
+ test("Handler bekommt deps.app — Hono-Instance für internal-fetch", async () => {
264
+ const res = await routeApp.request("/probe-deps");
265
+ expect(res.status).toBe(200);
266
+ const body = (await res.json()) as { hasApp: boolean };
267
+ expect(body.hasApp).toBe(true);
268
+ });
269
+
270
+ test("Handler kann via deps.app intern /api/query aufrufen (anonymous + defaultTenantId)", async () => {
271
+ // Realistischer Use-Case (publicstatus feed.xml): die r.httpRoute
272
+ // baut eine View aus internen /api/query-Daten. Anonymous-Access mit
273
+ // defaultTenantId macht den inner-Call ohne Bearer-Token möglich;
274
+ // pinst dass deps.app.fetch identisch zu einem echten HTTP-Call läuft.
275
+ const inner = defineFeature("inner", (r) => {
276
+ r.entity("item", createEntity({ table: "Items", fields: { name: createTextField() } }));
277
+ // Bewusst "anonymous" — openToAll schließt anonymous-User explizit
278
+ // aus (siehe access.ts), damit das Aktivieren von anonymousAccess
279
+ // nicht versehentlich jeden openToAll-Handler public macht.
280
+ r.queryHandler("item:list", z.object({}), async () => [{ id: 42, name: "hello" }], {
281
+ access: { roles: ["anonymous"] },
282
+ });
283
+ r.httpRoute({
284
+ method: "GET",
285
+ path: "/feed",
286
+ anonymous: true,
287
+ handler: async (c, deps) => {
288
+ const queryRes = await deps.app.fetch(
289
+ new Request(`${new URL(c.req.url).origin}/api/query`, {
290
+ method: "POST",
291
+ headers: { "Content-Type": "application/json" },
292
+ body: JSON.stringify({ type: "inner:query:item:list", payload: {} }),
293
+ }),
294
+ );
295
+ const body = (await queryRes.json()) as { data?: unknown };
296
+ return c.json({ status: queryRes.status, items: body.data });
297
+ },
298
+ });
299
+ });
300
+ const innerRegistry = createRegistry([inner]);
301
+ const { app: innerApp } = buildServer({
302
+ registry: innerRegistry,
303
+ context: {},
304
+ jwtSecret: JWT_SECRET,
305
+ anonymousAccess: {
306
+ defaultTenantId: "00000000-0000-4000-8000-000000000000" as TenantId,
307
+ },
308
+ });
309
+
310
+ const res = await innerApp.request("/feed");
311
+ expect(res.status).toBe(200);
312
+ const body = (await res.json()) as { status: number; items: unknown };
313
+ expect(body.status).toBe(200);
314
+ expect(body.items).toEqual([{ id: 42, name: "hello" }]);
315
+ });
316
+
317
+ test("Boot-Validator: Route auf /api/* ist verboten", () => {
318
+ expect(() =>
319
+ defineFeature("bad", (r) => {
320
+ r.httpRoute({
321
+ method: "GET",
322
+ path: "/api/forbidden",
323
+ handler: (c) => c.text("nope"),
324
+ });
325
+ }),
326
+ ).toThrow(/\/api\/\* namespace.*reserved/);
327
+ });
328
+
329
+ test("Boot-Validator: doppelte method+path-Combo wird abgelehnt", () => {
330
+ expect(() =>
331
+ defineFeature("dup", (r) => {
332
+ r.httpRoute({ method: "GET", path: "/x", handler: (c) => c.text("a") });
333
+ r.httpRoute({ method: "GET", path: "/x", handler: (c) => c.text("b") });
334
+ }),
335
+ ).toThrow(/already registered/);
336
+ });
337
+ });