@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,71 @@
1
+ import { generateId } from "../utils";
2
+
3
+ export type SseClient = {
4
+ id: string;
5
+ send: (event: SseEvent) => void;
6
+ close: () => void;
7
+ };
8
+
9
+ export type SseEvent = {
10
+ type: string;
11
+ data: Record<string, unknown>;
12
+ };
13
+
14
+ export type SseBroker = {
15
+ addClient(channel: string, send: (event: SseEvent) => void, close: () => void): string;
16
+ removeClient(channel: string, clientId: string): void;
17
+ pushToChannel(channel: string, event: SseEvent): void;
18
+ getClientCount(channel: string): number;
19
+ getTotalClientCount(): number;
20
+ };
21
+
22
+ export function createSseBroker(): SseBroker {
23
+ const channels = new Map<string, Map<string, SseClient>>();
24
+
25
+ function getOrCreateChannel(channel: string): Map<string, SseClient> {
26
+ let clients = channels.get(channel);
27
+ if (!clients) {
28
+ clients = new Map();
29
+ channels.set(channel, clients);
30
+ }
31
+ return clients;
32
+ }
33
+
34
+ return {
35
+ addClient(channel, send, close) {
36
+ const clientId = generateId();
37
+ const clients = getOrCreateChannel(channel);
38
+ clients.set(clientId, { id: clientId, send, close });
39
+ return clientId;
40
+ },
41
+
42
+ removeClient(channel, clientId) {
43
+ const clients = channels.get(channel);
44
+ // skip: channel was never registered or already cleaned up
45
+ if (!clients) return;
46
+ clients.delete(clientId);
47
+ if (clients.size === 0) channels.delete(channel);
48
+ },
49
+
50
+ pushToChannel(channel, event) {
51
+ const clients = channels.get(channel);
52
+ // skip: no listeners on this channel, event has no audience
53
+ if (!clients) return;
54
+ for (const client of clients.values()) {
55
+ client.send(event);
56
+ }
57
+ },
58
+
59
+ getClientCount(channel) {
60
+ return channels.get(channel)?.size ?? 0;
61
+ },
62
+
63
+ getTotalClientCount() {
64
+ let total = 0;
65
+ for (const clients of channels.values()) {
66
+ total += clients.size;
67
+ }
68
+ return total;
69
+ },
70
+ };
71
+ }
@@ -0,0 +1,62 @@
1
+ import { Hono } from "hono";
2
+ import { streamSSE } from "hono/streaming";
3
+ import { tenantChannel } from "../engine/constants";
4
+ import { Routes } from "./api-constants";
5
+ import { getUser } from "./auth-middleware";
6
+ import type { SseBroker } from "./sse-broker";
7
+
8
+ /**
9
+ * Heartbeat-Cadence für SSE-Streams.
10
+ *
11
+ * Spec: muss UNTER jedem realistischen Idle-Timeout der Hop-by-Hop-Layer
12
+ * liegen, sonst killt einer davon die Connection und der Browser sieht
13
+ * ERR_HTTP2_PROTOCOL_ERROR. Bekannte Limits:
14
+ * - Bun.serve default: 10 s (lokal disabled via idleTimeout: 0,
15
+ * aber Spec-konform auch ohne Override)
16
+ * - Caddy reverse_proxy: kein default-Timeout für SSE (auto-detect
17
+ * via Content-Type), aber langlebige idle Streams können von
18
+ * Connection-Tracking dichtgemacht werden
19
+ * - Cloudflare Edge: 100 s
20
+ * - AWS ALB: 60 s
21
+ *
22
+ * 15 s liegt komfortabel unter allen davon. Server-side Cost ist
23
+ * marginal (1 Frame pro Client alle 15 s).
24
+ *
25
+ * Spec-Test in __tests__/sse-route-spec.test.ts pinst diesen Wert
26
+ * gegen versehentliches Hochsetzen.
27
+ */
28
+ export const SSE_HEARTBEAT_INTERVAL_MS = 15_000;
29
+
30
+ export function createSseRoute(broker: SseBroker) {
31
+ const route = new Hono();
32
+
33
+ route.get(Routes.sse, async (c) => {
34
+ const user = getUser(c);
35
+ // Channel is server-derived from authenticated user — never trust client input.
36
+ // Allowing ?channel=... would let any authenticated user subscribe to other tenants' feeds.
37
+ const channel = tenantChannel(user.tenantId);
38
+
39
+ return streamSSE(c, async (stream) => {
40
+ const clientId = broker.addClient(
41
+ channel,
42
+ (event) => {
43
+ stream.writeSSE({ event: event.type, data: JSON.stringify(event.data) });
44
+ },
45
+ () => stream.close(),
46
+ );
47
+
48
+ stream.onAbort(() => {
49
+ broker.removeClient(channel, clientId);
50
+ });
51
+
52
+ // Keep connection alive with heartbeat — siehe SSE_HEARTBEAT_INTERVAL_MS
53
+ // header für die Layer-für-Layer-Begründung der 15s-Cadence.
54
+ while (true) {
55
+ await stream.writeSSE({ event: "ping", data: "" });
56
+ await stream.sleep(SSE_HEARTBEAT_INTERVAL_MS);
57
+ }
58
+ });
59
+ });
60
+
61
+ return route;
62
+ }
@@ -0,0 +1,16 @@
1
+ import { randomBytes } from "node:crypto";
2
+
3
+ // Security tokens (CSRF cookie, one-shot correlation tokens that must
4
+ // not be guessable from wall-clock time). Backend-only — imports
5
+ // `node:crypto` and must not be pulled into a Metro/Expo-Web bundle.
6
+ //
7
+ // Unlike `generateId` (v7), this must not leak the creation timestamp:
8
+ // a CSRF value whose first 6 bytes are "the millisecond the login
9
+ // happened" is predictable. Every bit here is unpredictable.
10
+ //
11
+ // 32 bytes = 256 bits — session-class strength. base64url = 43 chars,
12
+ // cookie- and URL-safe, matches the encoding of JWT segments / OAuth
13
+ // state / WebAuthn challenges.
14
+ export function generateToken(): string {
15
+ return randomBytes(32).toString("base64url");
16
+ }
@@ -0,0 +1,159 @@
1
+ // Direkte Tests für applyEntityEvent's tenantId-Defaulting. Zwei
2
+ // Branches (siehe apply-entity-event.ts:created):
3
+ //
4
+ // - payload.tenantId GESETZT → wins (z.B. seedTenantMembership-Pfad
5
+ // wo Operator tenantId und Ziel-tenantId divergieren)
6
+ // - payload.tenantId NICHT gesetzt → Fallback auf event.tenantId
7
+ // (Replay-Fall für entity-Tabellen ohne tenantId-Feld)
8
+ //
9
+ // Beide Branches werden indirekt durch andere Tests berührt
10
+ // (seedTenantMembership-Integration für A, Live==Rebuild für B), aber
11
+ // kein expliziter Test pinst das exakte Verhalten von applyEntityEvent.
12
+ // Wenn jemand die Spread-Reihenfolge im values()-Object umdreht
13
+ // (`...event.payload, tenantId: event.tenantId` statt
14
+ // `tenantId: event.tenantId, ...event.payload`), würde Branch A still
15
+ // zerbrechen — der bestehende seed-Test wäre der einzige Catcher, und
16
+ // der lief vor dem Refactor durch Zufall grün.
17
+
18
+ import { eq, sql } from "drizzle-orm";
19
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
20
+ import { createEntity, createTextField } from "../../engine/factories";
21
+ import type { TenantId } from "../../engine/types";
22
+ import type { StoredEvent } from "../../event-store";
23
+ import { createEventsTable } from "../../event-store";
24
+ import { createTestDb, type TestDb } from "../../stack";
25
+ import { applyEntityEvent } from "../apply-entity-event";
26
+ import { buildDrizzleTable } from "../table-builder";
27
+
28
+ const entity = createEntity({
29
+ table: "read_apply_tenant_check",
30
+ fields: {
31
+ name: createTextField({ required: true }),
32
+ },
33
+ });
34
+ const table = buildDrizzleTable("apply-tenant-check", entity);
35
+
36
+ let testDb: TestDb;
37
+
38
+ beforeAll(async () => {
39
+ testDb = await createTestDb();
40
+ await createEventsTable(testDb.db);
41
+ await testDb.db.execute(sql`
42
+ CREATE TABLE read_apply_tenant_check (
43
+ id uuid PRIMARY KEY,
44
+ tenant_id uuid NOT NULL,
45
+ version integer NOT NULL DEFAULT 1,
46
+ inserted_at timestamptz NOT NULL DEFAULT now(),
47
+ modified_at timestamptz,
48
+ inserted_by_id text,
49
+ modified_by_id text,
50
+ name text NOT NULL
51
+ )
52
+ `);
53
+ });
54
+
55
+ afterAll(async () => {
56
+ await testDb.cleanup();
57
+ });
58
+
59
+ beforeEach(async () => {
60
+ await testDb.db.execute(sql`TRUNCATE read_apply_tenant_check`);
61
+ });
62
+
63
+ const TENANT_OPERATOR = "11111111-1111-1111-1111-111111111111" as TenantId;
64
+ const TENANT_TARGET = "22222222-2222-2222-2222-222222222222" as TenantId;
65
+
66
+ function syntheticCreateEvent(payload: Record<string, unknown>): StoredEvent {
67
+ return {
68
+ id: "evt-1",
69
+ aggregateId: "33333333-3333-3333-3333-333333333333",
70
+ aggregateType: "apply-tenant-check",
71
+ tenantId: TENANT_OPERATOR,
72
+ version: 1,
73
+ type: "apply-tenant-check.created",
74
+ eventVersion: 1,
75
+ payload,
76
+ metadata: { userId: "u-1" },
77
+ createdAt: { toString: () => "2026-04-27T00:00:00Z" } as never,
78
+ createdBy: "u-1",
79
+ };
80
+ }
81
+
82
+ describe("applyEntityEvent — tenantId-Defaulting", () => {
83
+ test("payload OHNE tenantId → row.tenantId fällt auf event.tenantId zurück (Replay-Default)", async () => {
84
+ const event = syntheticCreateEvent({ name: "without-tenantId-in-payload" });
85
+ const result = await applyEntityEvent(event, table, entity, testDb.db);
86
+ expect(result.kind).toBe("applied");
87
+
88
+ const [row] = await testDb.db.select().from(table).where(eq(table["id"], event.aggregateId));
89
+ expect(row?.["tenantId"]).toBe(TENANT_OPERATOR);
90
+ expect(row?.["name"]).toBe("without-tenantId-in-payload");
91
+ });
92
+
93
+ test("payload MIT tenantId überschreibt event.tenantId (seed-Override-Case)", async () => {
94
+ // seedTenantMembership-Realität: Operator (event.tenantId =
95
+ // OPERATOR) schreibt eine Membership in den Ziel-Tenant (payload
96
+ // .tenantId = TARGET). Der Row gehört in den Ziel-Tenant.
97
+ const event = syntheticCreateEvent({
98
+ name: "tenantId-override",
99
+ tenantId: TENANT_TARGET,
100
+ });
101
+ const result = await applyEntityEvent(event, table, entity, testDb.db);
102
+ expect(result.kind).toBe("applied");
103
+
104
+ const [row] = await testDb.db.select().from(table).where(eq(table["id"], event.aggregateId));
105
+ expect(row?.["tenantId"]).toBe(TENANT_TARGET);
106
+ expect(row?.["tenantId"]).not.toBe(TENANT_OPERATOR);
107
+ });
108
+
109
+ test("payload.tenantId === '' (empty string) → wirft (fail-loud, kein silent fallback)", async () => {
110
+ // Tenant-isolation-kritisch: silent fallback auf event.tenantId
111
+ // würde eine Bug-payload (Form-Input ohne Trim-Check, defekter
112
+ // Hook etc.) in den Operator-Tenant schreiben, obwohl der
113
+ // Caller-Code die Row in irgendeinen Ziel-Tenant routen wollte.
114
+ // Cross-Tenant-Drift. Fail-loud ist die einzige Wahl.
115
+ const event = syntheticCreateEvent({
116
+ name: "empty-tenantId",
117
+ tenantId: "",
118
+ });
119
+ await expect(applyEntityEvent(event, table, entity, testDb.db)).rejects.toThrow(
120
+ /payload\.tenantId set but invalid/,
121
+ );
122
+ });
123
+
124
+ test("payload.tenantId === null → wirft (fail-loud)", async () => {
125
+ // Spiegel-Case. JSON-Payload kann literal null tragen (Hook der
126
+ // einen Wert auf null gesetzt hat statt zu unsetten). Auch hier
127
+ // kein silent fallback — tenant-isolation-kritisch.
128
+ const event = syntheticCreateEvent({
129
+ name: "null-tenantId",
130
+ tenantId: null,
131
+ });
132
+ await expect(applyEntityEvent(event, table, entity, testDb.db)).rejects.toThrow(
133
+ /payload\.tenantId set but invalid/,
134
+ );
135
+ });
136
+
137
+ test("Spread-Reihenfolge: payload-Felder bleiben erhalten, framework-Defaults nicht überschrieben", async () => {
138
+ // Negative-Anchor: id/version/insertedAt/insertedById dürfen NICHT
139
+ // aus dem payload kommen (kommen vom event). Wenn jemand die
140
+ // Reihenfolge im values() umstellt, würde dieser Test fangen.
141
+ const event = syntheticCreateEvent({
142
+ name: "spread-order-check",
143
+ // Diese Felder im payload müssen vom framework überschrieben werden:
144
+ id: "00000000-0000-0000-0000-000000000000",
145
+ version: 999,
146
+ insertedById: "fake-user",
147
+ });
148
+ const result = await applyEntityEvent(event, table, entity, testDb.db);
149
+ expect(result.kind).toBe("applied");
150
+
151
+ const [row] = await testDb.db.select().from(table).where(eq(table["id"], event.aggregateId));
152
+ // event.aggregateId wins, nicht payload.id
153
+ expect(row?.["id"]).toBe(event.aggregateId);
154
+ // event.version wins, nicht payload.version
155
+ expect(row?.["version"]).toBe(event.version);
156
+ // event.createdBy wins, nicht payload.insertedById
157
+ expect(row?.["insertedById"]).toBe(event.createdBy);
158
+ });
159
+ });
@@ -0,0 +1,114 @@
1
+ // Tests für die Compound-Type-Pipeline.
2
+ // Garantien die wir prüfen:
3
+ // - Identity bei leerem Payload / keinen Compound-Feldern
4
+ // - Beide Konverter werden gerufen wenn beide Field-Types vorkommen
5
+ // - Reihenfolge ist deterministisch + Konverter überlappen nicht
6
+ // - Pure (kein input-mutate)
7
+
8
+ import { describe, expect, test } from "vitest";
9
+ import {
10
+ createEntity,
11
+ createLocatedTimestampField,
12
+ createMoneyField,
13
+ createTextField,
14
+ } from "../../engine";
15
+ import type { EntityDefinition } from "../../engine/types";
16
+ import { flattenCompoundTypes, rehydrateCompoundTypes } from "../compound-types";
17
+
18
+ const mixedEntity: EntityDefinition = createEntity({
19
+ defaultCurrency: "EUR",
20
+ fields: {
21
+ label: createTextField(),
22
+ pickup: createLocatedTimestampField(),
23
+ buyingPrice: createMoneyField(),
24
+ },
25
+ });
26
+
27
+ describe("flattenCompoundTypes — Pipeline", () => {
28
+ test("identity bei Payload ohne Compound-Felder", () => {
29
+ const payload = { label: "ACME-001" };
30
+ const flat = flattenCompoundTypes(payload, mixedEntity);
31
+ expect(flat).toEqual({ label: "ACME-001" });
32
+ });
33
+
34
+ test("identity bei leerem Payload", () => {
35
+ expect(flattenCompoundTypes({}, mixedEntity)).toEqual({});
36
+ });
37
+
38
+ test("alle Konverter laufen wenn alle Compound-Types im Payload sind", () => {
39
+ const flat = flattenCompoundTypes(
40
+ {
41
+ label: "ACME",
42
+ pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon" },
43
+ buyingPrice: { amount: 45_000, currency: "EUR" },
44
+ },
45
+ mixedEntity,
46
+ );
47
+ // Beide Compound-Konverter müssen gefeuert haben.
48
+ // Sprint F: pickupUtc ist jetzt Temporal.Instant — vergleichen via .toString().
49
+ expect(flat["label"]).toBe("ACME");
50
+ expect((flat["pickupUtc"] as Temporal.Instant).toString()).toBe("2026-04-15T09:00:00Z");
51
+ expect(flat["pickupTz"]).toBe("Europe/Lisbon");
52
+ expect(flat["buyingPrice"]).toBe(45_000);
53
+ expect(flat["buyingPriceCurrency"]).toBe("EUR");
54
+ });
55
+
56
+ test("ist pure — input wird nicht mutiert", () => {
57
+ const input = {
58
+ pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon" },
59
+ buyingPrice: { amount: 100, currency: "EUR" },
60
+ };
61
+ const before = JSON.stringify(input);
62
+ flattenCompoundTypes(input, mixedEntity);
63
+ expect(JSON.stringify(input)).toBe(before);
64
+ });
65
+ });
66
+
67
+ describe("rehydrateCompoundTypes — Pipeline", () => {
68
+ test("identity bei Row ohne Compound-Felder", () => {
69
+ expect(rehydrateCompoundTypes({ label: "ACME" }, mixedEntity)).toEqual({ label: "ACME" });
70
+ });
71
+
72
+ test("alle Konverter rehydraten parallel", () => {
73
+ const out = rehydrateCompoundTypes(
74
+ {
75
+ label: "ACME",
76
+ pickupUtc: "2026-04-15T09:00:00Z",
77
+ pickupTz: "Europe/Lisbon",
78
+ buyingPrice: 45_000,
79
+ buyingPriceCurrency: "EUR",
80
+ },
81
+ mixedEntity,
82
+ );
83
+ expect(out).toEqual({
84
+ label: "ACME",
85
+ pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon", utc: "2026-04-15T09:00:00Z" },
86
+ buyingPrice: { amount: 45_000, currency: "EUR" },
87
+ });
88
+ });
89
+
90
+ test("Round-Trip: flatten → rehydrate ergibt identisches API-Object (utc wird im Read addiert)", () => {
91
+ const original = {
92
+ label: "ACME",
93
+ pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon" },
94
+ buyingPrice: { amount: 100, currency: "EUR" },
95
+ };
96
+ const round = rehydrateCompoundTypes(flattenCompoundTypes(original, mixedEntity), mixedEntity);
97
+ // pickup bekommt utc dazu beim Read (war beim Insert nicht gesetzt)
98
+ expect((round["pickup"] as { utc: string }).utc).toBe("2026-04-15T09:00:00Z");
99
+ expect(round["buyingPrice"]).toEqual({ amount: 100, currency: "EUR" });
100
+ expect(round["label"]).toBe("ACME");
101
+ });
102
+
103
+ test("ist pure — input wird nicht mutiert", () => {
104
+ const input = {
105
+ pickupUtc: "2026-04-15T09:00:00Z",
106
+ pickupTz: "Europe/Lisbon",
107
+ buyingPrice: 100,
108
+ buyingPriceCurrency: "EUR",
109
+ };
110
+ const before = JSON.stringify(input);
111
+ rehydrateCompoundTypes(input, mixedEntity);
112
+ expect(JSON.stringify(input)).toBe(before);
113
+ });
114
+ });
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { dbConnectionOptionsFromEnv } from "../connection";
3
+
4
+ // createDbConnection itself opens a real postgres.js socket, so it's
5
+ // exercised in the DB-integration suite. The env-parsing + validation
6
+ // logic is pure and worth pinning as a unit — misconfig at boot is the
7
+ // whole point of parsing strictly.
8
+
9
+ describe("dbConnectionOptionsFromEnv", () => {
10
+ test("empty env → empty options (falls back to postgres.js defaults)", () => {
11
+ expect(dbConnectionOptionsFromEnv({})).toEqual({});
12
+ });
13
+
14
+ test("reads all three supported keys", () => {
15
+ const opts = dbConnectionOptionsFromEnv({
16
+ DATABASE_POOL_MAX: "25",
17
+ DATABASE_POOL_IDLE_TIMEOUT: "60",
18
+ DATABASE_POOL_CONNECT_TIMEOUT: "5",
19
+ });
20
+ expect(opts).toEqual({
21
+ maxConnections: 25,
22
+ idleTimeoutSeconds: 60,
23
+ connectTimeoutSeconds: 5,
24
+ });
25
+ });
26
+
27
+ test("empty string is treated as unset", () => {
28
+ const opts = dbConnectionOptionsFromEnv({
29
+ DATABASE_POOL_MAX: "",
30
+ DATABASE_POOL_IDLE_TIMEOUT: "30",
31
+ });
32
+ expect(opts).toEqual({ idleTimeoutSeconds: 30 });
33
+ });
34
+
35
+ test("zero is allowed (idle_timeout=0 disables idle eviction in postgres.js)", () => {
36
+ const opts = dbConnectionOptionsFromEnv({
37
+ DATABASE_POOL_IDLE_TIMEOUT: "0",
38
+ });
39
+ expect(opts).toEqual({ idleTimeoutSeconds: 0 });
40
+ });
41
+
42
+ test("negative number → throws (misconfig catches at boot)", () => {
43
+ expect(() => dbConnectionOptionsFromEnv({ DATABASE_POOL_MAX: "-5" })).toThrow(
44
+ /DATABASE_POOL_MAX="-5".*non-negative/i,
45
+ );
46
+ });
47
+
48
+ test("non-numeric → throws", () => {
49
+ expect(() => dbConnectionOptionsFromEnv({ DATABASE_POOL_CONNECT_TIMEOUT: "five" })).toThrow(
50
+ /DATABASE_POOL_CONNECT_TIMEOUT="five"/,
51
+ );
52
+ });
53
+
54
+ test("decimal → throws (postgres.js expects integer seconds)", () => {
55
+ expect(() => dbConnectionOptionsFromEnv({ DATABASE_POOL_IDLE_TIMEOUT: "1.5" })).toThrow(
56
+ /DATABASE_POOL_IDLE_TIMEOUT="1.5".*integer/i,
57
+ );
58
+ });
59
+
60
+ test("unrelated env vars are ignored", () => {
61
+ const opts = dbConnectionOptionsFromEnv({
62
+ HOME: "/home/user",
63
+ PATH: "/usr/bin",
64
+ DATABASE_POOL_MAX: "10",
65
+ });
66
+ expect(opts).toEqual({ maxConnections: 10 });
67
+ });
68
+ });
@@ -0,0 +1,41 @@
1
+ // Cursor encoding pinst dass UUID + Integer-IDs beide den gleichen
2
+ // Pfad nehmen — vorher hat decodeCursor mit Number.parseInt versucht
3
+ // einen UUID zu number zu casten und zurück NaN gegeben, der DB-WHERE-
4
+ // Clause hat dann auf "id > NaN" gequeryt → Postgres-Crash. UUIDs
5
+ // sind aktuell der Default (Sprint F idType=uuid), also war cursor-
6
+ // Pagination strukturell broken vor diesem Fix.
7
+
8
+ import { describe, expect, test } from "vitest";
9
+ import { decodeCursor, encodeCursor } from "../cursor";
10
+
11
+ describe("encodeCursor + decodeCursor", () => {
12
+ test("UUIDv7-Roundtrip: encoded → decoded gibt denselben UUID-String", () => {
13
+ const uuid = "019dcd94-d6b9-742c-9a3c-43d7972f6243";
14
+ expect(decodeCursor(encodeCursor(uuid))).toBe(uuid);
15
+ });
16
+
17
+ test("Integer-Roundtrip: encoded → decoded gibt String-Form (kompatibel)", () => {
18
+ // String/number als Input erlaubt; output ist immer String. PG
19
+ // castet beim WHERE id > '42' selbst auf integer-Spalten korrekt.
20
+ expect(decodeCursor(encodeCursor(42))).toBe("42");
21
+ });
22
+
23
+ test("encoded String ist URL-safe base64 (kein /, +, =)", () => {
24
+ const encoded = encodeCursor("019dcd94-d6b9-742c-9a3c-43d7972f6243");
25
+ expect(encoded).not.toMatch(/[/+=]/);
26
+ });
27
+
28
+ test("Leerer Cursor (corrupted base64): wirft Invalid-cursor-Error", () => {
29
+ expect(() => decodeCursor("")).toThrow(/Invalid cursor/);
30
+ });
31
+
32
+ test("UUIDs sind lexikografisch sort-stabil (UUIDv7-Voraussetzung)", () => {
33
+ // Cursor-Pagination erwartet dass `gt(id, last-id)` die nächste Seite
34
+ // liefert. Bei UUIDv7 ist das time-ordered, also lexikografisch
35
+ // monoton mit Insert-Reihenfolge. Hier nur eine String-Compare-
36
+ // Sanity-Check — die DB-Seite glaubt das default-mäßig.
37
+ const a = "019dcd94-0000-742c-0000-000000000001";
38
+ const b = "019dcd95-0000-742c-0000-000000000002";
39
+ expect(b > a).toBe(true);
40
+ });
41
+ });