@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,103 @@
1
+ // Regression test for the file/image entity-column type: must be UUID to
2
+ // match fileRefsTable.id (uuid). Pre-fix the column was `integer`, which
3
+ // silently blocked any client from storing a fileRef reference on the
4
+ // entity — UUID → integer cast raised a PG error or truncated.
5
+ //
6
+ // Intentionally a minimal dedicated suite rather than a case in
7
+ // files.integration.ts because the bug is about *table generation*, not
8
+ // runtime behaviour of the upload route.
9
+
10
+ import { sql } from "drizzle-orm";
11
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
12
+ import { createEntity, createFileField, createImageField } from "../../engine";
13
+ import { createEntityTable, createTestDb, pushTables, type TestDb } from "../../stack";
14
+ import { generateId } from "../../utils";
15
+ import { fileRefsTable } from "../file-ref-table";
16
+
17
+ // Entity with BOTH singular file-field types exercised — the bug applied
18
+ // identically to `file` and `image` (same switch-case in table-builder).
19
+ const documentEntity = createEntity({
20
+ table: "regression_documents",
21
+ fields: {
22
+ attachment: createFileField(),
23
+ cover: createImageField(),
24
+ },
25
+ });
26
+
27
+ let testDb: TestDb;
28
+
29
+ beforeAll(async () => {
30
+ testDb = await createTestDb();
31
+ await pushTables(testDb.db, { fileRefsTable });
32
+ await createEntityTable(testDb.db, documentEntity);
33
+ });
34
+
35
+ afterAll(async () => {
36
+ await testDb.cleanup();
37
+ });
38
+
39
+ describe("file-field entity-column type", () => {
40
+ test("`file` and `image` fields generate UUID columns (not integer)", async () => {
41
+ // Pull the actual column type from information_schema. This is the
42
+ // load-bearing assertion: the type emitted by drizzle-kit during
43
+ // `createEntityTable` must be `uuid`. A regression to `integer` would
44
+ // fail here even if higher-level code happened to still work through
45
+ // implicit casts.
46
+ const rows = await testDb.db.execute<{ column_name: string; data_type: string }>(sql`
47
+ SELECT column_name, data_type
48
+ FROM information_schema.columns
49
+ WHERE table_name = 'regression_documents'
50
+ AND column_name IN ('attachment', 'cover')
51
+ ORDER BY column_name
52
+ `);
53
+ const arr = rows as unknown as Array<{ column_name: string; data_type: string }>;
54
+ expect(arr).toEqual([
55
+ { column_name: "attachment", data_type: "uuid" },
56
+ { column_name: "cover", data_type: "uuid" },
57
+ ]);
58
+ });
59
+
60
+ test("storing a fileRef UUID in a file-field column round-trips cleanly", async () => {
61
+ // Seed a fileRef row so we have a real UUID to reference. Full upload
62
+ // flow isn't exercised here — we're verifying the CRUD column contract.
63
+ const fileUuid = generateId();
64
+ const tenantId = generateId();
65
+ await testDb.db.execute(sql`
66
+ INSERT INTO file_refs (id, tenant_id, storage_key, file_name, mime_type, size)
67
+ VALUES (
68
+ ${fileUuid}::uuid, ${tenantId}::uuid, 'seed-key',
69
+ 'seed.pdf', 'application/pdf', 1024
70
+ )
71
+ `);
72
+
73
+ const docId = generateId();
74
+ await testDb.db.execute(sql`
75
+ INSERT INTO regression_documents (id, tenant_id, attachment, cover)
76
+ VALUES (
77
+ ${docId}::uuid, ${tenantId}::uuid,
78
+ ${fileUuid}::uuid, ${fileUuid}::uuid
79
+ )
80
+ `);
81
+
82
+ const read = await testDb.db.execute<{ attachment: string; cover: string }>(sql`
83
+ SELECT attachment, cover FROM regression_documents WHERE id = ${docId}::uuid
84
+ `);
85
+ const docArr = read as unknown as Array<{ attachment: string; cover: string }>;
86
+ expect(docArr[0]?.attachment).toBe(fileUuid);
87
+ expect(docArr[0]?.cover).toBe(fileUuid);
88
+ });
89
+
90
+ test("storing a non-UUID value in a file-field column rejects — proves strict typing", async () => {
91
+ // If the column were still `integer`, `'not-a-uuid'` would either
92
+ // truncate or coerce to 0. With uuid the insert raises
93
+ // invalid_text_representation (22P02) — the type is actually enforced.
94
+ const docId = generateId();
95
+ const tenantId = generateId();
96
+ await expect(
97
+ testDb.db.execute(sql`
98
+ INSERT INTO regression_documents (id, tenant_id, attachment)
99
+ VALUES (${docId}::uuid, ${tenantId}::uuid, 'not-a-uuid')
100
+ `),
101
+ ).rejects.toThrow();
102
+ });
103
+ });
@@ -0,0 +1,211 @@
1
+ // End-to-end regression for file/image-field wiring through the CRUD
2
+ // pipeline. The column-level fix in table-builder.ts is necessary but not
3
+ // sufficient: the Zod validation layer in schema-builder.ts also had a
4
+ // residual `z.number()` for file/image fields from an earlier era. Without
5
+ // that fix the pipeline would reject every valid UUID at the validation
6
+ // gate, before the column type ever mattered.
7
+ //
8
+ // This suite proves the whole path works end-to-end:
9
+ // POST /api/files → upload → receive file UUID
10
+ // POST /api/write → entity:create with file-field: <uuid>
11
+ // POST /api/query → entity:detail → UUID round-trips
12
+ // POST /api/write → entity:update with new file-UUID
13
+ // POST /api/query → entity:detail → new UUID persisted
14
+
15
+ import { mkdtemp, rm } from "node:fs/promises";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { sql } from "drizzle-orm";
19
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
20
+ import {
21
+ createEntity,
22
+ createFileField,
23
+ createFilesField,
24
+ createImageField,
25
+ createImagesField,
26
+ createTextField,
27
+ defineEntityCreateHandler,
28
+ defineEntityDetailHandler,
29
+ defineEntityUpdateHandler,
30
+ defineFeature,
31
+ } from "../../engine";
32
+ import {
33
+ createEntityTable,
34
+ createTestUser,
35
+ setupTestStack,
36
+ type TestStack,
37
+ testTenantId,
38
+ } from "../../stack";
39
+ import { createLocalProvider } from "../local-provider";
40
+
41
+ // Covers ALL four file-field variants: singular (file/image) stores a UUID in
42
+ // the entity column; plural (files/images) has no entity column — the array
43
+ // of UUIDs lives in the event payload only (resolved via fileRefs otherwise).
44
+ // Both shapes must validate + round-trip through the CRUD pipeline.
45
+ const documentEntity = createEntity({
46
+ table: "pipeline_documents",
47
+ fields: {
48
+ title: createTextField({ required: true }),
49
+ attachment: createFileField(),
50
+ cover: createImageField(),
51
+ photos: createImagesField(),
52
+ docs: createFilesField(),
53
+ },
54
+ });
55
+
56
+ const ROLES = { access: { roles: ["Admin", "User"] } } as const;
57
+
58
+ const documentFeature = defineFeature("pipeline-documents", (r) => {
59
+ r.entity("document", documentEntity);
60
+ r.writeHandler(defineEntityCreateHandler("document", documentEntity, ROLES));
61
+ r.writeHandler(defineEntityUpdateHandler("document", documentEntity, ROLES));
62
+ r.queryHandler(defineEntityDetailHandler("document", documentEntity, ROLES));
63
+ });
64
+
65
+ let stack: TestStack;
66
+ let storagePath: string;
67
+
68
+ const tenantId = testTenantId(1);
69
+ const user = createTestUser({ id: 1, tenantId, roles: ["Admin"] });
70
+
71
+ beforeAll(async () => {
72
+ storagePath = await mkdtemp(join(tmpdir(), "kumiko-file-field-pipeline-"));
73
+ stack = await setupTestStack({
74
+ features: [documentFeature],
75
+ files: { storageProvider: createLocalProvider(storagePath) },
76
+ });
77
+ await createEntityTable(stack.db, documentEntity);
78
+ });
79
+
80
+ afterAll(async () => {
81
+ await stack.cleanup();
82
+ await rm(storagePath, { recursive: true, force: true });
83
+ });
84
+
85
+ beforeEach(async () => {
86
+ await stack.db.execute(sql`TRUNCATE pipeline_documents`);
87
+ });
88
+
89
+ async function uploadFile(fileName: string, body: Uint8Array, mimeType: string): Promise<string> {
90
+ const token = await stack.jwt.sign(user);
91
+ const fd = new FormData();
92
+ fd.append("file", new File([Buffer.from(body)], fileName, { type: mimeType }));
93
+ const res = await stack.app.request("/api/files", {
94
+ method: "POST",
95
+ headers: { Authorization: `Bearer ${token}` },
96
+ body: fd,
97
+ });
98
+ // File-routes return 201 Created on successful upload.
99
+ expect(res.status).toBe(201);
100
+ const json = (await res.json()) as { id: string };
101
+ return json.id;
102
+ }
103
+
104
+ describe("file/image field through the CRUD pipeline", () => {
105
+ test("create entity with file-field UUID → detail round-trips the UUID", async () => {
106
+ const fileId = await uploadFile("doc.pdf", new Uint8Array([1, 2, 3]), "application/pdf");
107
+ const imageId = await uploadFile("cover.png", new Uint8Array([4, 5, 6]), "image/png");
108
+
109
+ // Create through the standard write pipeline — this is the path where the
110
+ // pre-fix validation (z.number() for file/image) would have rejected the
111
+ // UUID with a zod error. If we get past this, both schema + column agree.
112
+ const created = await stack.http.writeOk<{ id: string }>(
113
+ "pipeline-documents:write:document:create",
114
+ { title: "Annual report", attachment: fileId, cover: imageId },
115
+ user,
116
+ );
117
+
118
+ const detail = await stack.http.queryOk<{
119
+ id: string;
120
+ title: string;
121
+ attachment: string;
122
+ cover: string;
123
+ }>("pipeline-documents:query:document:detail", { id: created.id }, user);
124
+
125
+ expect(detail).toMatchObject({
126
+ id: created.id,
127
+ title: "Annual report",
128
+ attachment: fileId,
129
+ cover: imageId,
130
+ });
131
+ });
132
+
133
+ test("update entity swaps file-field UUIDs cleanly", async () => {
134
+ const oldFile = await uploadFile("v1.pdf", new Uint8Array([1]), "application/pdf");
135
+ const newFile = await uploadFile("v2.pdf", new Uint8Array([2]), "application/pdf");
136
+
137
+ const created = await stack.http.writeOk<{ id: string }>(
138
+ "pipeline-documents:write:document:create",
139
+ { title: "Swap target", attachment: oldFile },
140
+ user,
141
+ );
142
+
143
+ await stack.http.writeOk(
144
+ "pipeline-documents:write:document:update",
145
+ { id: created.id, version: 1, changes: { attachment: newFile } },
146
+ user,
147
+ );
148
+
149
+ const detail = await stack.http.queryOk<{ attachment: string }>(
150
+ "pipeline-documents:query:document:detail",
151
+ { id: created.id },
152
+ user,
153
+ );
154
+ expect(detail.attachment).toBe(newFile);
155
+ });
156
+
157
+ test("invalid UUID rejected by validation (code=validation_error, not some other failure)", async () => {
158
+ // With the pre-fix z.number() this would have returned "expected number".
159
+ // With z.uuid() we get a proper uuid-format error. Assertion has to pin
160
+ // the specific failure-class — otherwise a DB error, access-denied, or
161
+ // any other throw would silently satisfy `toBeDefined()` and we'd miss
162
+ // a regression where the validation layer stopped firing at all.
163
+ const err = await stack.http.writeErr(
164
+ "pipeline-documents:write:document:create",
165
+ { title: "Invalid", attachment: "not-a-uuid" },
166
+ user,
167
+ );
168
+ expect(err.code).toBe("validation_error");
169
+ });
170
+
171
+ test("plural files/images fields accept arrays of UUIDs end-to-end", async () => {
172
+ // Plural variants have NO entity-column (table-builder returns {} for
173
+ // files/images) — the array of UUIDs lives in the event payload. The
174
+ // pipeline still has to validate + accept it. Pre-fix this was
175
+ // z.array(z.number()) which would have rejected every UUID array.
176
+ const a = await uploadFile("a.jpg", new Uint8Array([1]), "image/jpeg");
177
+ const b = await uploadFile("b.jpg", new Uint8Array([2]), "image/jpeg");
178
+ const c = await uploadFile("notes.pdf", new Uint8Array([3]), "application/pdf");
179
+
180
+ const created = await stack.http.writeOk<{ id: string }>(
181
+ "pipeline-documents:write:document:create",
182
+ { title: "With arrays", photos: [a, b], docs: [c] },
183
+ user,
184
+ );
185
+ expect(created.id).toBeTruthy();
186
+
187
+ // Follow-up update: swap one photo out, add a second doc. Proves the
188
+ // update-path handles plural arrays too, not just create.
189
+ const d = await uploadFile("c.jpg", new Uint8Array([4]), "image/jpeg");
190
+ const e = await uploadFile("more.pdf", new Uint8Array([5]), "application/pdf");
191
+
192
+ const updated = await stack.http.writeOk<{ id: string }>(
193
+ "pipeline-documents:write:document:update",
194
+ { id: created.id, version: 1, changes: { photos: [a, d], docs: [c, e] } },
195
+ user,
196
+ );
197
+ // Version bumped by the CRUD executor → proves the write actually
198
+ // committed an event (not a silent no-op from validation-strip).
199
+ expect(updated.id).toBe(created.id);
200
+ });
201
+
202
+ test("plural files field rejects non-UUID element (schema validates EACH array element)", async () => {
203
+ const valid = await uploadFile("ok.pdf", new Uint8Array([1]), "application/pdf");
204
+ const err = await stack.http.writeErr(
205
+ "pipeline-documents:write:document:create",
206
+ { title: "Bad array", docs: [valid, "not-a-uuid"] },
207
+ user,
208
+ );
209
+ expect(err.code).toBe("validation_error");
210
+ });
211
+ });
@@ -0,0 +1,122 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { createFileContext, createFileHandle, deriveKey } from "../file-handle";
3
+ import { createInMemoryFileProvider } from "../in-memory-provider";
4
+
5
+ describe("deriveKey", () => {
6
+ test("inserts suffix before extension", () => {
7
+ expect(deriveKey("foo/bar.jpg", "medium")).toBe("foo/bar.medium.jpg");
8
+ });
9
+
10
+ test("handles keys without a slash", () => {
11
+ expect(deriveKey("bar.png", "thumb")).toBe("bar.thumb.png");
12
+ });
13
+
14
+ test("appends to keys without an extension", () => {
15
+ expect(deriveKey("foo/bar", "small")).toBe("foo/bar.small");
16
+ });
17
+
18
+ test("only splits on the last segment — earlier dots stay", () => {
19
+ expect(deriveKey("archive.v2/foo.jpg", "medium")).toBe("archive.v2/foo.medium.jpg");
20
+ });
21
+
22
+ test("handles multi-dot filenames — splits on the final extension", () => {
23
+ expect(deriveKey("tenant/my.photo.jpg", "thumb")).toBe("tenant/my.photo.thumb.jpg");
24
+ });
25
+ });
26
+
27
+ describe("FileHandle", () => {
28
+ test("read/write round-trip through the provider", async () => {
29
+ const provider = createInMemoryFileProvider();
30
+ const handle = createFileHandle("tenant/x.jpg", provider);
31
+
32
+ const payload = new Uint8Array([1, 2, 3, 4]);
33
+ await handle.write(payload, "image/jpeg");
34
+
35
+ const read = await handle.read();
36
+ expect(Array.from(read)).toEqual([1, 2, 3, 4]);
37
+ });
38
+
39
+ test("exists reflects write/delete state", async () => {
40
+ const provider = createInMemoryFileProvider();
41
+ const handle = createFileHandle("tenant/x.jpg", provider);
42
+
43
+ expect(await handle.exists()).toBe(false);
44
+ await handle.write(new Uint8Array([9]));
45
+ expect(await handle.exists()).toBe(true);
46
+ await handle.delete();
47
+ expect(await handle.exists()).toBe(false);
48
+ });
49
+
50
+ test("derive produces an independent handle at the derived key", async () => {
51
+ const provider = createInMemoryFileProvider();
52
+ const original = createFileHandle("tenant/photo.jpg", provider);
53
+ const thumb = original.derive("thumb");
54
+
55
+ expect(thumb.key).toBe("tenant/photo.thumb.jpg");
56
+
57
+ await original.write(new Uint8Array([1, 2]));
58
+ await thumb.write(new Uint8Array([9, 9, 9]));
59
+
60
+ expect(Array.from(await original.read())).toEqual([1, 2]);
61
+ expect(Array.from(await thumb.read())).toEqual([9, 9, 9]);
62
+ // Deleting the derived handle must not touch the original.
63
+ await thumb.delete();
64
+ expect(await original.exists()).toBe(true);
65
+ });
66
+
67
+ test("writes copy the buffer — caller mutations don't corrupt stored data", async () => {
68
+ const provider = createInMemoryFileProvider();
69
+ const handle = createFileHandle("tenant/x.bin", provider);
70
+
71
+ const buf = new Uint8Array([1, 2, 3]);
72
+ await handle.write(buf);
73
+ buf[0] = 99;
74
+
75
+ const read = await handle.read();
76
+ expect(Array.from(read)).toEqual([1, 2, 3]);
77
+ });
78
+ });
79
+
80
+ describe("createFileContext", () => {
81
+ test("ref returns a handle bound to the given key", async () => {
82
+ const provider = createInMemoryFileProvider();
83
+ const files = createFileContext(provider);
84
+
85
+ const h = files.ref("tenant/foo.pdf");
86
+ expect(h.key).toBe("tenant/foo.pdf");
87
+ await h.write(new Uint8Array([7]));
88
+
89
+ // Same key, new ref — should see the same stored bytes.
90
+ const h2 = files.ref("tenant/foo.pdf");
91
+ expect(Array.from(await h2.read())).toEqual([7]);
92
+ });
93
+ });
94
+
95
+ describe("InMemoryFileProvider", () => {
96
+ test("keys() lists every stored key", async () => {
97
+ const provider = createInMemoryFileProvider();
98
+ await provider.write("a/x.jpg", new Uint8Array([1]));
99
+ await provider.write("b/y.png", new Uint8Array([2]));
100
+ expect([...provider.keys()].sort()).toEqual(["a/x.jpg", "b/y.png"]);
101
+ });
102
+
103
+ test("read on a missing key throws with the key in the message", async () => {
104
+ const provider = createInMemoryFileProvider();
105
+ await expect(provider.read("nope.jpg")).rejects.toThrow(/nope\.jpg/);
106
+ });
107
+
108
+ test("clear() empties the store", async () => {
109
+ const provider = createInMemoryFileProvider();
110
+ await provider.write("a.jpg", new Uint8Array([1]));
111
+ provider.clear();
112
+ expect(provider.keys()).toEqual([]);
113
+ expect(await provider.exists("a.jpg")).toBe(false);
114
+ });
115
+
116
+ test("overwrite replaces bytes in place", async () => {
117
+ const provider = createInMemoryFileProvider();
118
+ await provider.write("a.jpg", new Uint8Array([1, 2]));
119
+ await provider.write("a.jpg", new Uint8Array([9, 9, 9]));
120
+ expect(Array.from(await provider.read("a.jpg"))).toEqual([9, 9, 9]);
121
+ });
122
+ });