@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,434 @@
1
+ import { SYSTEM_TENANT_ID, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
2
+ import { and, type Column, eq, getTableName, or, type SQL } from "drizzle-orm";
3
+ import { emitDbQuery, type Meter, registerStandardMetrics, type Tracer } from "../observability";
4
+ import type { DbRunner } from "./connection";
5
+ import type { TableColumns } from "./dialect";
6
+
7
+ // biome-ignore lint/suspicious/noExplicitAny: Drizzle dynamic tables
8
+ type Table = TableColumns<any>;
9
+
10
+ // biome-ignore lint/suspicious/noExplicitAny: Drizzle column selection
11
+ type ColumnSelection = Record<string, any>;
12
+
13
+ /**
14
+ * TenantDb scope modes:
15
+ *
16
+ * - "tenant" (default): SELECT/UPDATE/DELETE filtered by tenantId + reference data (tenantId=0).
17
+ * INSERT forces tenantId — handler cannot override.
18
+ *
19
+ * - "system" (r.systemScope()): No tenant filter on reads/updates/deletes.
20
+ * INSERT uses tenantId as default but handler can override (e.g. write a
21
+ * cross-tenant row to a shared sentinel like SYSTEM_TENANT_ID).
22
+ *
23
+ * Tables without a tenantId column are always unfiltered regardless of mode.
24
+ */
25
+ export type TenantDbMode = "tenant" | "system";
26
+
27
+ export type TenantDb = {
28
+ readonly tenantId: TenantId;
29
+ readonly mode: TenantDbMode;
30
+ /**
31
+ * Underlying DbRunner. Framework-internal use (event-store, migrations) —
32
+ * bypasses tenant-filter. Feature code should stick to the typed wrappers
33
+ * above so the automatic scoping stays intact.
34
+ */
35
+ readonly raw: DbRunner;
36
+ select(): TenantSelect;
37
+ select(columns: ColumnSelection): TenantSelect;
38
+ insert(table: Table): TenantInsert;
39
+ update(table: Table): TenantUpdate;
40
+ delete(table: Table): TenantDelete;
41
+ };
42
+
43
+ type TenantSelect = {
44
+ from(table: Table): TenantSelectQuery;
45
+ };
46
+
47
+ type WhereCondition = SQL | undefined;
48
+
49
+ type RowLockStrength = "update" | "no key update" | "share" | "key share";
50
+
51
+ type TenantSelectQuery = PromiseLike<Record<string, unknown>[]> & {
52
+ where(condition: WhereCondition): TenantSelectQuery;
53
+ limit(n: number): TenantSelectQuery;
54
+ offset(n: number): TenantSelectQuery;
55
+ orderBy(...columns: (SQL | Column)[]): TenantSelectQuery;
56
+ /** Row-level locking (FOR UPDATE / FOR SHARE). Must be called inside a tx. */
57
+ for(strength: RowLockStrength): TenantSelectQuery;
58
+ };
59
+
60
+ type TenantInsert = {
61
+ values(data: Record<string, unknown>): TenantInsertValues;
62
+ };
63
+
64
+ type ConflictTarget = Column | readonly Column[];
65
+ type ConflictUpdate = {
66
+ target: ConflictTarget;
67
+ set: Record<string, unknown>;
68
+ };
69
+
70
+ type TenantInsertValues = PromiseLike<void> & {
71
+ returning(): PromiseLike<Record<string, unknown>[]>;
72
+ onConflictDoUpdate(spec: ConflictUpdate): PromiseLike<void>;
73
+ onConflictDoNothing(spec?: { target: ConflictTarget }): PromiseLike<void>;
74
+ };
75
+
76
+ type TenantUpdate = {
77
+ set(data: Record<string, unknown>): TenantUpdateSet;
78
+ };
79
+
80
+ type TenantUpdateSet = PromiseLike<void> & {
81
+ where(condition: WhereCondition): TenantUpdateWhere;
82
+ returning(): PromiseLike<Record<string, unknown>[]>;
83
+ };
84
+
85
+ type TenantUpdateWhere = PromiseLike<void> & {
86
+ returning(): PromiseLike<Record<string, unknown>[]>;
87
+ };
88
+
89
+ type TenantDelete = {
90
+ where(condition: WhereCondition): PromiseLike<void>;
91
+ };
92
+
93
+ /**
94
+ * Cast helper for the `Record<string, unknown>[]` rows that
95
+ * `TenantDb.select()` returns.
96
+ *
97
+ * Usage:
98
+ * const rows = castTenantRows<MyRow>(
99
+ * await ctx.db.select({...}).from(myTable),
100
+ * );
101
+ *
102
+ * Why this exists: drizzle's `.select({col1: t.col1, ...})` natively
103
+ * returns `Array<{col1: T1, ...}>`, but our TenantDb wrapper erases
104
+ * that shape to `Record<string, unknown>[]` so it can centralize tenant-
105
+ * scoping. Until the wrapper preserves the typed-row shape (see memory:
106
+ * project_tenant_db_typed_rows), call sites need to assert the column
107
+ * shape they just selected. This helper:
108
+ * - centralises the cast (single grep target for the future refactor)
109
+ * - tags it with `@cast-boundary tenant-db-row` for the as-cast audit
110
+ * - documents the trade-off once instead of N times
111
+ *
112
+ * Removal plan: when TenantSelectQuery becomes generic over the
113
+ * column-shape, every `castTenantRows<T>(...)` call is just `await ...`
114
+ * and this helper goes away.
115
+ */
116
+ // @cast-boundary tenant-db-row
117
+ export function castTenantRows<T>(rows: readonly Record<string, unknown>[]): readonly T[] {
118
+ return rows as unknown as readonly T[];
119
+ }
120
+
121
+ export function createTenantDb(
122
+ db: DbRunner,
123
+ tenantId: TenantId,
124
+ mode: TenantDbMode = "tenant",
125
+ tracer?: Tracer,
126
+ meter?: Meter,
127
+ // Pre-flight cancellation: when set, every query check
128
+ // `signal.throwIfAborted()` BEFORE issuing the SQL. The currently
129
+ // running query is not actively cancelled (postgres-js connection
130
+ // cancel is a separate, riskier feature). This still saves the bulk
131
+ // of the wasted work in handlers that fire many sequential queries
132
+ // — once the client disconnects, the next query throws and the rest
133
+ // of the chain falls away.
134
+ signal?: AbortSignal,
135
+ ): TenantDb {
136
+ // If a meter was passed, make sure standard metrics are registered on it
137
+ // before we try to emit. Idempotent — buildServer typically registers them
138
+ // up front; this guards against test call-sites that wire up a TenantDb
139
+ // directly with a fresh meter.
140
+ if (meter) registerStandardMetrics(meter);
141
+
142
+ function hasTenantColumn(table: Table): boolean {
143
+ return table["tenantId"] !== undefined;
144
+ }
145
+
146
+ // Drizzle's terminal builders (insert, update().where, delete().where) are
147
+ // thenable — `.then` is there so `await` works — but the declared return
148
+ // types don't include PromiseLike. Cast via this helper so the double-
149
+ // cast is named and lives in exactly one place per scope.
150
+ function asDrizzleThenable<T>(builder: unknown): PromiseLike<T> {
151
+ return builder as PromiseLike<T>;
152
+ }
153
+
154
+ // Wrap a DB query promise in a `db.query` span + emit the DB duration
155
+ // histogram. Row count is recorded when the result is an array (SELECTs
156
+ // + *.returning()). Metric is emitted both on success and on throw so
157
+ // slow failing queries show up too.
158
+ function withDbSpan<T>(
159
+ operation: "select" | "insert" | "update" | "delete",
160
+ table: Table,
161
+ exec: () => PromiseLike<T>,
162
+ ): PromiseLike<T> {
163
+ // Pre-flight cancellation. Sits above the early-return so the check
164
+ // fires regardless of observability config — cancellation is a
165
+ // correctness feature, not an observability one.
166
+ signal?.throwIfAborted();
167
+ if (!tracer && !meter) return exec();
168
+ const tableName = getTableName(table);
169
+ const start = performance.now();
170
+ const emitMetric = () => {
171
+ if (meter) {
172
+ emitDbQuery(meter, { operation, table: tableName }, (performance.now() - start) / 1000);
173
+ }
174
+ };
175
+
176
+ if (!tracer) {
177
+ // Tracer absent but meter present: just time + emit, no span.
178
+ return (async () => {
179
+ try {
180
+ return await exec();
181
+ } finally {
182
+ emitMetric();
183
+ }
184
+ })();
185
+ }
186
+
187
+ return tracer.withSpan(
188
+ "db.query",
189
+ {
190
+ kind: "client",
191
+ attributes: {
192
+ "db.system": "postgresql",
193
+ "db.operation": operation,
194
+ "db.table": tableName,
195
+ },
196
+ },
197
+ async (span) => {
198
+ try {
199
+ const result = await exec();
200
+ if (Array.isArray(result)) {
201
+ span.setAttribute("db.row_count", result.length);
202
+ }
203
+ return result;
204
+ } finally {
205
+ emitMetric();
206
+ }
207
+ },
208
+ );
209
+ }
210
+
211
+ // --- Read filter (SELECT WHERE clause) ---
212
+ //
213
+ // Reads in tenant mode see their own rows AND global reference data (rows
214
+ // with tenantId = SYSTEM_TENANT_ID). Writes explicitly do NOT — see writeFilter.
215
+
216
+ function readFilter(table: Table, ...extra: SQL[]): SQL | undefined {
217
+ if (!hasTenantColumn(table)) {
218
+ return extra.length > 0 ? and(...extra) : undefined;
219
+ }
220
+
221
+ if (mode === "system") {
222
+ // System mode: no tenant restriction, only pass through extra conditions
223
+ return extra.length > 0 ? and(...extra) : undefined;
224
+ }
225
+
226
+ // Tenant mode: own data + reference data (zero-UUID tenantId for global rows).
227
+ // Drizzle's `or()` is typed `SQL | undefined` (variadic-empty case); both
228
+ // `eq()` args always produce SQL, so the cast documents that assumption.
229
+ const ownOrGlobal = or(
230
+ eq(table["tenantId"], tenantId),
231
+ eq(table["tenantId"], SYSTEM_TENANT_ID),
232
+ ) as SQL;
233
+ return extra.length > 0 ? and(ownOrGlobal, ...extra) : ownOrGlobal;
234
+ }
235
+
236
+ // --- Write filter (UPDATE/DELETE WHERE clause) ---
237
+ //
238
+ // Writes in tenant mode must NEVER match reference rows — otherwise a tenant
239
+ // could mutate global data by coincidence of id/condition. Only system-scope
240
+ // (r.systemScope()) may modify reference data.
241
+
242
+ function writeFilter(table: Table, ...extra: SQL[]): SQL | undefined {
243
+ if (!hasTenantColumn(table)) {
244
+ return extra.length > 0 ? and(...extra) : undefined;
245
+ }
246
+
247
+ if (mode === "system") {
248
+ return extra.length > 0 ? and(...extra) : undefined;
249
+ }
250
+
251
+ const ownOnly = eq(table["tenantId"], tenantId);
252
+ return extra.length > 0 ? and(ownOnly, ...extra) : ownOnly;
253
+ }
254
+
255
+ // --- Write values (INSERT tenantId handling) ---
256
+
257
+ function insertValues(table: Table, data: Record<string, unknown>): Record<string, unknown> {
258
+ if (!hasTenantColumn(table)) return data;
259
+
260
+ if (mode === "system") {
261
+ // System mode: tenantId is a default the handler can override —
262
+ // e.g. to write a cross-tenant row under SYSTEM_TENANT_ID, or to
263
+ // target a foreign tenant's projection from a SystemAdmin action.
264
+ return { tenantId, ...data };
265
+ }
266
+
267
+ // Tenant mode: tenantId is forced, handler cannot override
268
+ return { ...data, tenantId };
269
+ }
270
+
271
+ // --- Select wrapper (lazy filter + chainable) ---
272
+
273
+ function wrapSelect(
274
+ // biome-ignore lint/suspicious/noExplicitAny: Drizzle internal query type
275
+ query: any,
276
+ table: Table,
277
+ filtered: boolean,
278
+ ): TenantSelectQuery {
279
+ function ensureFiltered() {
280
+ if (filtered) return query;
281
+ const filter = readFilter(table);
282
+ return filter ? query.where(filter) : query;
283
+ }
284
+
285
+ return {
286
+ where(condition: SQL) {
287
+ const filter = readFilter(table, condition);
288
+ return wrapSelect(filter ? query.where(filter) : query.where(condition), table, true);
289
+ },
290
+ limit(n: number) {
291
+ return wrapSelect(ensureFiltered().limit(n), table, true);
292
+ },
293
+ offset(n: number) {
294
+ return wrapSelect(ensureFiltered().offset(n), table, true);
295
+ },
296
+ orderBy(...columns: SQL[]) {
297
+ return wrapSelect(ensureFiltered().orderBy(...columns), table, true);
298
+ },
299
+ for(strength: RowLockStrength) {
300
+ return wrapSelect(ensureFiltered().for(strength), table, true);
301
+ },
302
+ // biome-ignore lint/suspicious/noThenProperty: thenable for await
303
+ then(
304
+ resolve: ((value: Record<string, unknown>[]) => void) | null,
305
+ reject: ((reason: unknown) => void) | null,
306
+ ) {
307
+ return withDbSpan<Record<string, unknown>[]>("select", table, () => ensureFiltered()).then(
308
+ (rows) => resolve?.(rows),
309
+ reject ?? undefined,
310
+ );
311
+ },
312
+ } as TenantSelectQuery;
313
+ }
314
+
315
+ // --- Where helper for update/delete ---
316
+
317
+ function whereClause(table: Table, condition: SQL): SQL {
318
+ const filter = writeFilter(table, condition);
319
+ return filter ?? condition;
320
+ }
321
+
322
+ return {
323
+ tenantId,
324
+ mode,
325
+ raw: db,
326
+
327
+ select(columns?: ColumnSelection) {
328
+ return {
329
+ from(table: Table) {
330
+ const baseQuery = columns ? db.select(columns).from(table) : db.select().from(table);
331
+ return wrapSelect(baseQuery, table, false);
332
+ },
333
+ };
334
+ },
335
+
336
+ insert(table: Table) {
337
+ return {
338
+ values(data: Record<string, unknown>) {
339
+ const q = db.insert(table).values(insertValues(table, data));
340
+ return {
341
+ returning() {
342
+ return withDbSpan<Record<string, unknown>[]>(
343
+ "insert",
344
+ table,
345
+ () => q.returning() as PromiseLike<Record<string, unknown>[]>,
346
+ );
347
+ },
348
+ onConflictDoUpdate(spec: ConflictUpdate) {
349
+ return withDbSpan<void>("insert", table, () =>
350
+ (
351
+ q as unknown as {
352
+ onConflictDoUpdate: (s: ConflictUpdate) => PromiseLike<void>;
353
+ }
354
+ ).onConflictDoUpdate(spec),
355
+ );
356
+ },
357
+ onConflictDoNothing(spec?: { target: ConflictTarget }) {
358
+ return withDbSpan<void>("insert", table, () =>
359
+ (
360
+ q as unknown as {
361
+ onConflictDoNothing: (s?: { target: ConflictTarget }) => PromiseLike<void>;
362
+ }
363
+ ).onConflictDoNothing(spec),
364
+ );
365
+ },
366
+ // biome-ignore lint/suspicious/noThenProperty: thenable for await
367
+ then(resolve, reject) {
368
+ return withDbSpan<void>("insert", table, () => asDrizzleThenable<void>(q)).then(
369
+ resolve,
370
+ reject,
371
+ );
372
+ },
373
+ } as TenantInsertValues;
374
+ },
375
+ };
376
+ },
377
+
378
+ update(table: Table) {
379
+ return {
380
+ set(data: Record<string, unknown>) {
381
+ const q = db.update(table).set(data);
382
+ return {
383
+ where(condition: SQL) {
384
+ const wq = q.where(whereClause(table, condition));
385
+ return {
386
+ returning() {
387
+ return withDbSpan<Record<string, unknown>[]>(
388
+ "update",
389
+ table,
390
+ () => wq.returning() as PromiseLike<Record<string, unknown>[]>,
391
+ );
392
+ },
393
+ // biome-ignore lint/suspicious/noThenProperty: thenable for await
394
+ then(resolve, reject) {
395
+ return withDbSpan<void>("update", table, () => asDrizzleThenable<void>(wq)).then(
396
+ resolve,
397
+ reject,
398
+ );
399
+ },
400
+ } as TenantUpdateWhere;
401
+ },
402
+ returning(): PromiseLike<Record<string, unknown>[]> {
403
+ return Promise.reject(
404
+ new Error(
405
+ "TenantDb.update().set().returning() without .where() would mass-update all tenant rows. " +
406
+ "Add .where(...) first, or call .set(...).where(...).returning().",
407
+ ),
408
+ );
409
+ },
410
+ // biome-ignore lint/suspicious/noThenProperty: thenable for await
411
+ then(resolve, reject) {
412
+ return Promise.reject(
413
+ new Error(
414
+ "TenantDb.update().set() awaited without .where() would mass-update all tenant rows. " +
415
+ "Add .where(...) before awaiting.",
416
+ ),
417
+ ).then(resolve, reject);
418
+ },
419
+ } as TenantUpdateSet;
420
+ },
421
+ };
422
+ },
423
+
424
+ delete(table: Table) {
425
+ return {
426
+ where(condition: SQL) {
427
+ return withDbSpan<void>("delete", table, () =>
428
+ asDrizzleThenable<void>(db.delete(table).where(whereClause(table, condition))),
429
+ );
430
+ },
431
+ };
432
+ },
433
+ };
434
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { createRegistry, defineFeature } from "../index";
3
+
4
+ describe("r.authClaims() — registrar collection", () => {
5
+ test("feature without authClaims has an empty hooks list", () => {
6
+ const feature = defineFeature("empty", () => {});
7
+ expect(feature.authClaimsHooks).toEqual([]);
8
+ });
9
+
10
+ test("single authClaims call stores the fn on the feature definition", () => {
11
+ const hook = async () => ({ teamId: "t-1" });
12
+ const feature = defineFeature("drivers", (r) => {
13
+ r.authClaims(hook);
14
+ });
15
+ expect(feature.authClaimsHooks).toHaveLength(1);
16
+ expect(feature.authClaimsHooks[0]).toBe(hook);
17
+ });
18
+
19
+ test("multiple authClaims calls inside one feature are all retained (last-wins is a merge concern, not a storage concern)", () => {
20
+ const feature = defineFeature("billing", (r) => {
21
+ r.authClaims(async () => ({ plan: "free" }));
22
+ r.authClaims(async () => ({ plan: "pro" }));
23
+ });
24
+ expect(feature.authClaimsHooks).toHaveLength(2);
25
+ });
26
+ });
27
+
28
+ describe("Registry.getAuthClaimsHooks — aggregation across features", () => {
29
+ test("empty registry has no hooks", () => {
30
+ const reg = createRegistry([]);
31
+ expect(reg.getAuthClaimsHooks()).toEqual([]);
32
+ });
33
+
34
+ test("aggregates hooks from multiple features with feature name tagged", () => {
35
+ const driversFeature = defineFeature("drivers", (r) => {
36
+ r.authClaims(async () => ({ teamId: "t-1" }));
37
+ });
38
+ const billingFeature = defineFeature("billing", (r) => {
39
+ r.authClaims(async () => ({ plan: "pro" }));
40
+ });
41
+ const reg = createRegistry([driversFeature, billingFeature]);
42
+
43
+ const hooks = reg.getAuthClaimsHooks();
44
+ expect(hooks).toHaveLength(2);
45
+
46
+ const names = hooks.map((h) => h.featureName).sort();
47
+ expect(names).toEqual(["billing", "drivers"]);
48
+ });
49
+
50
+ test("preserves registration order within a feature", () => {
51
+ const feature = defineFeature("billing", (r) => {
52
+ r.authClaims(async () => ({ x: 1 }));
53
+ r.authClaims(async () => ({ x: 2 }));
54
+ });
55
+ const reg = createRegistry([feature]);
56
+
57
+ const hooks = reg.getAuthClaimsHooks();
58
+ expect(hooks).toHaveLength(2);
59
+ // Both carry the same featureName; the resolver decides the merge policy.
60
+ expect(hooks.every((h) => h.featureName === "billing")).toBe(true);
61
+ });
62
+
63
+ test("features without r.authClaims contribute nothing", () => {
64
+ const plain = defineFeature("plain", () => {});
65
+ const withClaims = defineFeature("drivers", (r) => {
66
+ r.authClaims(async () => ({ teamId: "t-1" }));
67
+ });
68
+ const reg = createRegistry([plain, withClaims]);
69
+
70
+ const hooks = reg.getAuthClaimsHooks();
71
+ expect(hooks).toHaveLength(1);
72
+ expect(hooks[0]?.featureName).toBe("drivers");
73
+ });
74
+ });
@@ -0,0 +1,108 @@
1
+ // Boot-Validator-Tests für locatedBy-Markers.
2
+ //
3
+ // Ein Timestamp-Feld mit `locatedBy: "X"` muss ein bestehendes tz-Feld "X"
4
+ // in derselben Entity referenzieren. Sonst silent data loss — wir wollen
5
+ // fail-fast beim Boot.
6
+
7
+ import { describe, expect, test } from "vitest";
8
+ import { validateBoot } from "../boot-validator";
9
+ import { defineFeature } from "../define-feature";
10
+ import { createEntity, createTimestampField, createTzField, locatedTimestamp } from "../factories";
11
+
12
+ describe("validateBoot — locatedBy markers", () => {
13
+ test("locatedTimestamp(name) Helper-Pair passiert validiert (positive case)", () => {
14
+ const feature = defineFeature("test", (r) => {
15
+ r.entity(
16
+ "order",
17
+ createEntity({
18
+ fields: {
19
+ ...locatedTimestamp("pickup"),
20
+ ...locatedTimestamp("delivery"),
21
+ },
22
+ }),
23
+ );
24
+ });
25
+
26
+ expect(() => validateBoot([feature])).not.toThrow();
27
+ });
28
+
29
+ test("manuelle Konstruktion mit korrektem Pair passiert (positive case)", () => {
30
+ const feature = defineFeature("test", (r) => {
31
+ r.entity(
32
+ "order",
33
+ createEntity({
34
+ fields: {
35
+ customAt: createTimestampField({ locatedBy: "customTz" }),
36
+ customTz: createTzField(),
37
+ },
38
+ }),
39
+ );
40
+ });
41
+
42
+ expect(() => validateBoot([feature])).not.toThrow();
43
+ });
44
+
45
+ test("locatedBy zeigt auf nicht-existierendes Feld → Fehler beim Boot", () => {
46
+ const feature = defineFeature("test", (r) => {
47
+ r.entity(
48
+ "order",
49
+ createEntity({
50
+ fields: {
51
+ pickupAt: createTimestampField({ locatedBy: "pickupTz" }),
52
+ // pickupTz fehlt komplett!
53
+ },
54
+ }),
55
+ );
56
+ });
57
+
58
+ expect(() => validateBoot([feature])).toThrow(/no field with that name exists/);
59
+ });
60
+
61
+ test("locatedBy zeigt auf falschen Feld-Typ → Fehler beim Boot", () => {
62
+ const feature = defineFeature("test", (r) => {
63
+ r.entity(
64
+ "order",
65
+ createEntity({
66
+ fields: {
67
+ pickupAt: createTimestampField({ locatedBy: "pickupTz" }),
68
+ // text statt tz — typo-Falle die der Validator fängt
69
+ pickupTz: { type: "text", maxLength: 100 },
70
+ },
71
+ }),
72
+ );
73
+ });
74
+
75
+ expect(() => validateBoot([feature])).toThrow(/expected "tz"/);
76
+ });
77
+
78
+ test("Fehlermeldung verweist auf locatedTimestamp-Helper als Fix", () => {
79
+ const feature = defineFeature("test", (r) => {
80
+ r.entity(
81
+ "order",
82
+ createEntity({
83
+ fields: {
84
+ pickupAt: createTimestampField({ locatedBy: "pickupTz" }),
85
+ },
86
+ }),
87
+ );
88
+ });
89
+
90
+ expect(() => validateBoot([feature])).toThrow(/locatedTimestamp\("pickup"\)/);
91
+ });
92
+
93
+ test("Timestamp ohne locatedBy ist OK (reiner UTC-Instant)", () => {
94
+ const feature = defineFeature("test", (r) => {
95
+ r.entity(
96
+ "order",
97
+ createEntity({
98
+ fields: {
99
+ createdAt: createTimestampField(),
100
+ actualPickupAt: createTimestampField({ required: true }),
101
+ },
102
+ }),
103
+ );
104
+ });
105
+
106
+ expect(() => validateBoot([feature])).not.toThrow();
107
+ });
108
+ });