@cosmicdrift/kumiko-framework 0.14.0 → 0.16.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 (342) hide show
  1. package/package.json +6 -6
  2. package/src/__tests__/{anonymous-access.integration.ts → anonymous-access.integration.test.ts} +12 -9
  3. package/src/__tests__/{error-contract.integration.ts → error-contract.integration.test.ts} +5 -4
  4. package/src/__tests__/{field-access.integration.ts → field-access.integration.test.ts} +3 -3
  5. package/src/__tests__/{full-stack.integration.ts → full-stack.integration.test.ts} +7 -16
  6. package/src/__tests__/{ownership.integration.ts → ownership.integration.test.ts} +3 -2
  7. package/src/__tests__/{raw-table.integration.ts → raw-table.integration.test.ts} +18 -30
  8. package/src/__tests__/{reference-data.integration.ts → reference-data.integration.test.ts} +24 -11
  9. package/src/__tests__/{transition-guard.integration.ts → transition-guard.integration.test.ts} +12 -10
  10. package/src/api/__tests__/api.test.ts +1 -1
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +1 -1
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +1 -1
  13. package/src/api/__tests__/{batch.integration.ts → batch.integration.test.ts} +30 -30
  14. package/src/api/__tests__/body-limit.test.ts +1 -1
  15. package/src/api/__tests__/csrf-middleware.test.ts +1 -1
  16. package/src/api/__tests__/{dispatcher-live.integration.ts → dispatcher-live.integration.test.ts} +10 -9
  17. package/src/api/__tests__/metrics-endpoint.test.ts +1 -1
  18. package/src/api/__tests__/{nested-write.integration.ts → nested-write.integration.test.ts} +13 -16
  19. package/src/api/__tests__/readiness.test.ts +1 -1
  20. package/src/api/__tests__/request-id-middleware.test.ts +1 -1
  21. package/src/api/__tests__/sse-broker.test.ts +12 -12
  22. package/src/api/__tests__/sse-route.test.ts +1 -1
  23. package/src/api/auth-routes.ts +2 -5
  24. package/src/api/readiness.ts +2 -2
  25. package/src/auth/__tests__/roles.test.ts +2 -2
  26. package/src/bun-db/__tests__/PATTERN.md +73 -0
  27. package/src/bun-db/__tests__/_helpers.ts +103 -0
  28. package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
  29. package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
  30. package/src/bun-db/__tests__/bun-test-db.ts +19 -0
  31. package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
  32. package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
  33. package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
  34. package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
  35. package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
  36. package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
  37. package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
  38. package/src/bun-db/connection.ts +84 -0
  39. package/src/bun-db/index.ts +31 -0
  40. package/src/bun-db/query.ts +842 -0
  41. package/src/compliance/__tests__/duration-spec.test.ts +1 -1
  42. package/src/compliance/__tests__/profiles.test.ts +1 -1
  43. package/src/compliance/__tests__/sub-processors.test.ts +1 -1
  44. package/src/compliance/profiles.ts +1 -4
  45. package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
  46. package/src/db/__tests__/big-int-field.test.ts +15 -14
  47. package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
  48. package/src/db/__tests__/compound-types.test.ts +1 -1
  49. package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
  50. package/src/db/__tests__/connection-options.test.ts +1 -1
  51. package/src/db/__tests__/cursor.test.ts +8 -32
  52. package/src/db/__tests__/dialect-instant.test.ts +1 -1
  53. package/src/db/__tests__/encryption.test.ts +1 -1
  54. package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
  55. package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
  56. package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
  57. package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
  58. package/src/db/__tests__/located-timestamp.test.ts +1 -1
  59. package/src/db/__tests__/migrate-generator.test.ts +71 -0
  60. package/src/db/__tests__/migrate-runner.test.ts +19 -0
  61. package/src/db/__tests__/money.test.ts +1 -1
  62. package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
  63. package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
  64. package/src/db/__tests__/pg-error.test.ts +43 -0
  65. package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
  66. package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
  67. package/src/db/__tests__/sql-inventory.test.ts +56 -0
  68. package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
  69. package/src/db/__tests__/table-builder-required.test.ts +20 -22
  70. package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
  71. package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
  72. package/src/db/api.ts +46 -0
  73. package/src/db/apply-entity-event.ts +45 -36
  74. package/src/db/assert-exists-in.ts +5 -16
  75. package/src/db/bun-provider.ts +37 -0
  76. package/src/db/config-seed.ts +4 -4
  77. package/src/db/connection.ts +14 -57
  78. package/src/db/cursor.ts +5 -56
  79. package/src/db/dialect.ts +472 -99
  80. package/src/db/eagerload.ts +5 -12
  81. package/src/db/entity-table-meta.ts +390 -0
  82. package/src/db/event-store-executor.ts +158 -100
  83. package/src/db/index.ts +33 -5
  84. package/src/db/migrate-generator.ts +350 -0
  85. package/src/db/migrate-runner.ts +206 -0
  86. package/src/db/postgres-provider.ts +25 -0
  87. package/src/db/queries/entity-read.ts +15 -0
  88. package/src/db/queries/es-ops.ts +17 -0
  89. package/src/db/queries/event-consumer.ts +170 -0
  90. package/src/db/queries/event-store-admin.ts +127 -0
  91. package/src/db/queries/event-store.ts +155 -0
  92. package/src/db/queries/projection-rebuild.ts +59 -0
  93. package/src/db/queries/raw-sql.ts +15 -0
  94. package/src/db/queries/schema-drift.ts +35 -0
  95. package/src/db/queries/seed-context.ts +58 -0
  96. package/src/db/queries/table-ops.ts +11 -0
  97. package/src/db/queries/test-stack.ts +56 -0
  98. package/src/db/query-api.ts +22 -0
  99. package/src/db/query.ts +30 -0
  100. package/src/db/reference-data.ts +19 -22
  101. package/src/db/render-ddl.ts +57 -0
  102. package/src/db/row-helpers.ts +3 -52
  103. package/src/db/schema-inspection.ts +17 -4
  104. package/src/db/sql-inventory.ts +208 -0
  105. package/src/db/table-builder.ts +54 -46
  106. package/src/db/tenant-db.ts +105 -326
  107. package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
  108. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
  109. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
  110. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
  111. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
  112. package/src/engine/__tests__/boot-validator.test.ts +4 -3
  113. package/src/engine/__tests__/build-app-schema.test.ts +1 -1
  114. package/src/engine/__tests__/build-target.test.ts +1 -1
  115. package/src/engine/__tests__/claim-keys.test.ts +1 -1
  116. package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
  117. package/src/engine/__tests__/config-helpers.test.ts +1 -1
  118. package/src/engine/__tests__/duration-utils.test.ts +16 -0
  119. package/src/engine/__tests__/effective-features.test.ts +1 -1
  120. package/src/engine/__tests__/engine.test.ts +1 -1
  121. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  122. package/src/engine/__tests__/event-helpers.test.ts +3 -3
  123. package/src/engine/__tests__/extends-registrar.test.ts +4 -4
  124. package/src/engine/__tests__/factories-long-text.test.ts +1 -1
  125. package/src/engine/__tests__/factories-time.test.ts +1 -1
  126. package/src/engine/__tests__/field-access.test.ts +38 -0
  127. package/src/engine/__tests__/field-predicates.test.ts +1 -1
  128. package/src/engine/__tests__/hook-phases.test.ts +1 -1
  129. package/src/engine/__tests__/identifiers.test.ts +1 -1
  130. package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
  131. package/src/engine/__tests__/nav.test.ts +1 -1
  132. package/src/engine/__tests__/no-return-guard.test.ts +17 -0
  133. package/src/engine/__tests__/ownership.test.ts +10 -11
  134. package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
  135. package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
  136. package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
  137. package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
  138. package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
  139. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
  140. package/src/engine/__tests__/post-query-hook.test.ts +1 -1
  141. package/src/engine/__tests__/projection-helpers.test.ts +25 -17
  142. package/src/engine/__tests__/projection.test.ts +4 -4
  143. package/src/engine/__tests__/qualified-name.test.ts +1 -1
  144. package/src/engine/__tests__/raw-table.test.ts +9 -8
  145. package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
  146. package/src/engine/__tests__/run-in.test.ts +1 -1
  147. package/src/engine/__tests__/schema-builder.test.ts +1 -1
  148. package/src/engine/__tests__/screen.test.ts +1 -1
  149. package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
  150. package/src/engine/__tests__/state-machine.test.ts +1 -1
  151. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
  152. package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
  153. package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
  154. package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
  155. package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
  156. package/src/engine/__tests__/steps-read.test.ts +34 -40
  157. package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
  158. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
  159. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
  160. package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
  161. package/src/engine/__tests__/steps-workflow.test.ts +7 -7
  162. package/src/engine/__tests__/system-user.test.ts +1 -1
  163. package/src/engine/__tests__/unmanaged-table.test.ts +98 -0
  164. package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
  165. package/src/engine/__tests__/validation-hooks.test.ts +1 -1
  166. package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
  167. package/src/engine/boot-validator/entity-handler.ts +3 -3
  168. package/src/engine/boot-validator/ownership.ts +1 -1
  169. package/src/engine/define-feature.ts +37 -2
  170. package/src/engine/entity-handlers.ts +5 -5
  171. package/src/engine/factories.ts +1 -1
  172. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
  173. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
  174. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
  175. package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
  176. package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
  177. package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
  178. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
  179. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
  180. package/src/engine/feature-ast/extractors/shared.ts +2 -3
  181. package/src/engine/ownership.ts +113 -41
  182. package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
  183. package/src/engine/projection-helpers.ts +2 -11
  184. package/src/engine/registry.ts +21 -2
  185. package/src/engine/steps/read-find-many.ts +13 -13
  186. package/src/engine/steps/read-find-one.ts +7 -9
  187. package/src/engine/steps/unsafe-projection-delete.ts +4 -5
  188. package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
  189. package/src/engine/types/feature.ts +47 -2
  190. package/src/engine/types/fields.ts +4 -5
  191. package/src/engine/types/index.ts +2 -0
  192. package/src/engine/types/step.ts +10 -10
  193. package/src/engine/validate-projection-allowlist.ts +23 -3
  194. package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
  195. package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
  196. package/src/env/__tests__/compose-env-schema.test.ts +1 -1
  197. package/src/env/__tests__/dry-run.test.ts +1 -1
  198. package/src/errors/__tests__/classes.test.ts +1 -1
  199. package/src/errors/__tests__/error-helpers.test.ts +44 -0
  200. package/src/errors/__tests__/field-issue-compat.test.ts +16 -0
  201. package/src/errors/__tests__/write-failures.test.ts +1 -1
  202. package/src/errors/classes.ts +5 -19
  203. package/src/errors/field-issue.ts +11 -0
  204. package/src/errors/index.ts +1 -0
  205. package/src/errors/zod-bridge.ts +3 -2
  206. package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
  207. package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
  208. package/src/es-ops/__tests__/runner.test.ts +29 -19
  209. package/src/es-ops/context.ts +11 -56
  210. package/src/es-ops/operations-schema.ts +2 -2
  211. package/src/es-ops/runner.ts +12 -26
  212. package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
  213. package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
  214. package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
  215. package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
  216. package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
  217. package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
  218. package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
  219. package/src/event-store/admin-api.ts +55 -83
  220. package/src/event-store/archive.ts +15 -39
  221. package/src/event-store/event-store.ts +92 -86
  222. package/src/event-store/events-schema.ts +2 -1
  223. package/src/event-store/index.ts +1 -0
  224. package/src/event-store/snapshot.ts +26 -24
  225. package/src/event-store/upcaster-dead-letter.ts +19 -18
  226. package/src/files/__tests__/content-disposition.test.ts +1 -1
  227. package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
  228. package/src/files/__tests__/file-handle.test.ts +1 -1
  229. package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
  230. package/src/files/__tests__/read-stream.test.ts +1 -1
  231. package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
  232. package/src/files/__tests__/write-stream.test.ts +1 -1
  233. package/src/files/__tests__/zip-stream.test.ts +1 -1
  234. package/src/files/file-ref-table.ts +2 -2
  235. package/src/files/file-routes.ts +7 -9
  236. package/src/files/storage-tracking.ts +9 -17
  237. package/src/i18n/__tests__/i18n.test.ts +1 -1
  238. package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
  239. package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
  240. package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
  241. package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
  242. package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
  243. package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
  244. package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
  245. package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
  246. package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
  247. package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
  248. package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
  249. package/src/migrations/projection-detection.ts +12 -1
  250. package/src/migrations/schema-drift.ts +7 -23
  251. package/src/observability/__tests__/console-provider.test.ts +1 -1
  252. package/src/observability/__tests__/metric-validator.test.ts +1 -1
  253. package/src/observability/__tests__/noop-provider.test.ts +1 -1
  254. package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
  255. package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
  256. package/src/observability/__tests__/recording-meter.test.ts +1 -1
  257. package/src/observability/__tests__/recording-tracer.test.ts +1 -1
  258. package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
  259. package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
  260. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
  261. package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
  262. package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
  263. package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
  264. package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
  265. package/src/pipeline/__tests__/dispatcher-utils.test.ts +107 -0
  266. package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
  267. package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
  268. package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
  269. package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
  270. package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
  271. package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
  272. package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
  273. package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
  274. package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
  275. package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
  276. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
  277. package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
  278. package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
  279. package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
  280. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
  281. package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
  282. package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
  283. package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
  284. package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
  285. package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
  286. package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
  287. package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
  288. package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
  289. package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
  290. package/src/pipeline/__tests__/redis-keys.test.ts +12 -0
  291. package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
  292. package/src/pipeline/cascade-handler.ts +13 -21
  293. package/src/pipeline/dispatcher-utils.ts +8 -7
  294. package/src/pipeline/dispatcher.ts +43 -48
  295. package/src/pipeline/event-consumer-state.ts +11 -2
  296. package/src/pipeline/event-dispatcher.ts +86 -146
  297. package/src/pipeline/event-retention.ts +14 -24
  298. package/src/pipeline/msp-rebuild.ts +54 -78
  299. package/src/pipeline/projection-rebuild.ts +65 -67
  300. package/src/pipeline/projection-state.ts +2 -2
  301. package/src/random/__tests__/generate.test.ts +13 -13
  302. package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
  303. package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
  304. package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
  305. package/src/redis/__tests__/redis-options.test.ts +1 -1
  306. package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
  307. package/src/search/__tests__/search-adapter.test.ts +1 -1
  308. package/src/secrets/__tests__/dek-cache.test.ts +1 -3
  309. package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
  310. package/src/secrets/__tests__/envelope.test.ts +1 -1
  311. package/src/secrets/__tests__/leak-guard.test.ts +1 -1
  312. package/src/secrets/__tests__/rotation.test.ts +1 -1
  313. package/src/stack/db.ts +25 -48
  314. package/src/stack/push-entity-projection-tables.ts +2 -4
  315. package/src/stack/table-helpers.ts +98 -61
  316. package/src/stack/test-stack.ts +10 -9
  317. package/src/testing/__tests__/db-cleanup.test.ts +40 -0
  318. package/src/testing/__tests__/e2e-generator.test.ts +1 -1
  319. package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
  320. package/src/testing/db-cleanup.ts +44 -0
  321. package/src/testing/expect-error.ts +1 -1
  322. package/src/testing/index.ts +2 -0
  323. package/src/testing/multipart-helper.ts +94 -0
  324. package/src/testing/shared-entities.ts +5 -5
  325. package/src/time/__tests__/polyfill.test.ts +1 -1
  326. package/src/time/__tests__/tz-context.test.ts +1 -1
  327. package/src/utils/__tests__/assert.test.ts +1 -1
  328. package/src/utils/__tests__/case.test.ts +16 -0
  329. package/src/utils/__tests__/env-parse.test.ts +1 -1
  330. package/src/utils/__tests__/is-plain-object.test.ts +16 -0
  331. package/src/utils/__tests__/parse-string-array-json.test.ts +16 -0
  332. package/src/utils/__tests__/safe-json.test.ts +22 -0
  333. package/src/utils/case.ts +6 -0
  334. package/src/utils/index.ts +3 -0
  335. package/src/utils/is-plain-object.ts +4 -0
  336. package/src/utils/parse-string-array-json.ts +14 -0
  337. package/CHANGELOG.md +0 -474
  338. package/src/db/__tests__/db-helpers.test.ts +0 -369
  339. package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
  340. package/src/db/__tests__/row-helpers.test.ts +0 -59
  341. package/src/engine/steps/_drizzle-boundary.ts +0 -19
  342. package/src/files/__tests__/file-field-column.integration.ts +0 -103
package/src/stack/db.ts CHANGED
@@ -1,5 +1,10 @@
1
- import { drizzle } from "drizzle-orm/postgres-js";
2
- import postgres from "postgres";
1
+ // Test-DB Factory: CREATE DATABASE → connect → createEventsTable.
2
+ // Provider-agnostic via createConnection (DB_PROVIDER env).
3
+ // postgres-js = default. DB_PROVIDER=bun = Bun.SQL (experimentell).
4
+
5
+ import { createConnection } from "../db/api";
6
+ import { createDatabase, databaseExists, dropDatabaseIfExists } from "../db/queries/test-stack";
7
+ import { ensureTemporalPolyfill } from "../time/polyfill";
3
8
  import { generateId } from "../utils";
4
9
 
5
10
  function requireEnv(name: string): string {
@@ -13,88 +18,60 @@ function requireEnv(name: string): string {
13
18
  }
14
19
 
15
20
  export type TestDb = {
16
- db: ReturnType<typeof drizzle>;
17
- client: ReturnType<typeof postgres>;
21
+ db: unknown;
22
+ client: unknown;
18
23
  dbName: string;
19
24
  cleanup: () => Promise<void>;
20
25
  };
21
26
 
22
27
  export type CreateTestDbOptions = {
23
- /** Override TEST_DATABASE_URL. Rare — mostly for tests that want a
24
- * non-default Postgres (e.g. a read-replica probe). */
25
28
  readonly baseUrl?: string;
26
- /** Use a specific DB name instead of the default
27
- * `kumiko_test_<8chars>`. Combined with `persistent: true`, lets a
28
- * dev server keep state across restarts. Must be a legal Postgres
29
- * identifier — the caller is responsible for matching the usual
30
- * [a-z_0-9]+ shape. */
31
29
  readonly dbName?: string;
32
- /** When true, cleanup() is a no-op and the DB survives. Also
33
- * changes CREATE DATABASE to IF-NOT-EXISTS semantics so restarts
34
- * reuse the same storage. Default false (test contract: fresh DB
35
- * per call, dropped on cleanup). */
36
30
  readonly persistent?: boolean;
37
31
  };
38
32
 
39
33
  /**
40
- * Accepts a baseUrl string (legacy shorthand used by most tests) OR an
41
- * options object. The string form is kept because thousands of tests
42
- * call `createTestDb()` with no args; only dev-server and niche tests
43
- * need the options form.
34
+ * Provider-agnostische Test-DB. createConnection liest DB_PROVIDER.
35
+ * Für Bun.SQL: DB_PROVIDER=bun setzen (experimentell siehe db/bun-provider.ts).
44
36
  */
45
37
  export async function createTestDb(arg?: string | CreateTestDbOptions): Promise<TestDb> {
38
+ await ensureTemporalPolyfill();
46
39
  const opts: CreateTestDbOptions = typeof arg === "string" ? { baseUrl: arg } : (arg ?? {});
47
40
  const url = opts.baseUrl ?? requireEnv("TEST_DATABASE_URL");
48
- // slice(-8) — the last 8 hex chars of a UUIDv7 are pure random (the
49
- // front 48 bits are a timestamp, which would collide across workers
50
- // that start within the same millisecond).
51
41
  const dbName = opts.dbName ?? `kumiko_test_${generateId().slice(-8)}`;
52
42
  const adminUrl = url.replace(/\/[^/]+$/, "/postgres");
53
43
 
54
- const adminClient = postgres(adminUrl);
44
+ const admin = await createConnection(adminUrl, { maxConnections: 1 });
55
45
  try {
56
46
  if (opts.persistent) {
57
- // Postgres has no CREATE DATABASE IF NOT EXISTS; emulate with a
58
- // catalog probe so restarts are idempotent.
59
- const existing = await adminClient<{ exists: boolean }[]>`
60
- SELECT EXISTS (SELECT 1 FROM pg_database WHERE datname = ${dbName}) AS exists
61
- `;
62
- if (!existing[0]?.exists) {
63
- await adminClient.unsafe(`CREATE DATABASE "${dbName}"`);
47
+ if (!(await databaseExists(admin.db, dbName))) {
48
+ await createDatabase(admin.db, dbName);
64
49
  }
65
50
  } else {
66
- await adminClient.unsafe(`CREATE DATABASE "${dbName}"`);
51
+ await createDatabase(admin.db, dbName);
67
52
  }
68
53
  } finally {
69
- await adminClient.end();
54
+ await admin.close();
70
55
  }
71
56
 
72
57
  const testUrl = url.replace(/\/[^/]+$/, `/${dbName}`);
73
- const client = postgres(testUrl);
74
- const db = drizzle(client);
58
+ const conn = await createConnection(testUrl);
75
59
 
76
- // Every ES-entity writes events; auto-create the events table so tests that
77
- // go straight to createTestDb (not setupTestStack) also work out of the box.
78
- // In persistent mode this is idempotent: createEventsTable emits IF NOT
79
- // EXISTS so a second boot is a no-op.
80
60
  const { createEventsTable } = await import("../event-store");
81
- await createEventsTable(db);
61
+ await createEventsTable(conn.db);
82
62
 
83
63
  return {
84
- db,
85
- client,
64
+ db: conn.db,
65
+ client: conn.client,
86
66
  dbName,
87
67
  cleanup: async () => {
88
- await client.end();
89
- // Persistent mode: dev-server owns the DB lifecycle — don't drop
90
- // on process exit. `yarn kumiko clean-test-dbs` is the escape
91
- // hatch when you really want to start over.
68
+ await conn.close();
92
69
  if (!opts.persistent) {
93
- const admin = postgres(adminUrl);
70
+ const admin2 = await createConnection(adminUrl, { maxConnections: 1 });
94
71
  try {
95
- await admin.unsafe(`DROP DATABASE IF EXISTS "${dbName}"`);
72
+ await dropDatabaseIfExists(admin2.db, dbName);
96
73
  } finally {
97
- await admin.end();
74
+ await admin2.close();
98
75
  }
99
76
  }
100
77
  },
@@ -1,4 +1,3 @@
1
- import { getTableName } from "drizzle-orm";
2
1
  import { tableExists } from "../db/schema-inspection";
3
2
  import type { Registry } from "../engine/types";
4
3
  import { unsafePushTables } from "./table-helpers";
@@ -35,9 +34,8 @@ export async function pushEntityProjectionTables(
35
34
  if (!proj.isImplicit) continue;
36
35
  if (seen.has(proj.table)) continue;
37
36
  seen.add(proj.table);
38
- // @cast-boundary drizzle-bridge ProjectionTable + PgTable both round-trip
39
- // through getTableName at runtime; the type system can't unify them.
40
- const physical = getTableName(proj.table as Parameters<typeof getTableName>[0]);
37
+ const tableRec = proj.table as unknown as Record<symbol, unknown>;
38
+ const physical = tableRec[Symbol.for("kumiko:schema:Name")] as string;
41
39
  if (await tableExists(stack.db, `public.${physical}`)) {
42
40
  logInfo(`[kumiko-stack] table ${physical} already exists — skipping create`);
43
41
  continue;
@@ -1,42 +1,46 @@
1
- import { generateDrizzleJson, generateMigration } from "drizzle-kit/api";
2
- import { getTableName, sql } from "drizzle-orm";
3
- import type { PgTable } from "drizzle-orm/pg-core";
4
- import type { drizzle } from "drizzle-orm/postgres-js";
1
+ import type { DbConnection } from "../db/connection";
2
+ import { pgTypeToSqlType } from "../db/dialect";
3
+ import type { ColumnMeta, EntityTableMeta } from "../db/entity-table-meta";
4
+ import {
5
+ alterTableAddColumn,
6
+ createIndexIfNotExists,
7
+ executeDdlStatement,
8
+ truncateTablesRestartIdentity,
9
+ } from "../db/queries/test-stack";
10
+ import { renderTableDdl } from "../db/render-ddl";
5
11
  import { tableExists } from "../db/schema-inspection";
6
- import { buildDrizzleTable, toTableName } from "../db/table-builder";
7
- import type { TestStack } from "./test-stack";
12
+ import { buildEntityTable, toTableName } from "../db/table-builder";
13
+ import type { EventDispatcher } from "../pipeline";
14
+
15
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
16
+ function tableNameOf(table: unknown): string {
17
+ if (typeof table !== "object" || table === null) {
18
+ throw new Error("table-helpers: table is not a SchemaTable object");
19
+ }
20
+ const rec = table as Record<string | symbol, unknown>;
21
+ if (typeof rec[KUMIKO_NAME_SYMBOL] === "string") return rec[KUMIKO_NAME_SYMBOL] as string;
22
+ if (typeof (rec as { tableName?: unknown }).tableName === "string") {
23
+ return (rec as { tableName: string }).tableName;
24
+ }
25
+ throw new Error("table-helpers: table has no name");
26
+ }
8
27
 
9
28
  /**
10
- * Bypass: creates a Drizzle table directly, without registering it as
11
- * a projection of the event-sourcing engine. Apps should declare data
12
- * via `r.entity(...)` and get tables, migrations, snapshots and audit
13
- * for free — this helper is reserved for framework-internal meta-tables
14
- * (event-store, snapshots, projection-state) and test setup.
15
- *
16
- * Strict: raises a postgres `relation already exists` (42P07) error if
17
- * the table is already there. Use `unsafeEnsureEntityTable` for the
18
- * idempotent boot-path variant.
29
+ * Bypass: creates an entity-table directly without going through the
30
+ * full registry. Reserved for framework-internal meta-tables and
31
+ * test setup — apps declare data via `r.entity(...)`.
19
32
  */
20
33
  export async function unsafeCreateEntityTable(
21
- db: ReturnType<typeof drizzle>,
34
+ db: DbConnection,
22
35
  entity: import("../engine/types").EntityDefinition,
23
36
  entityName?: string,
24
37
  ): Promise<void> {
25
- const table = buildDrizzleTable(entityName ?? "entity", entity);
38
+ const table = buildEntityTable(entityName ?? "entity", entity);
26
39
  await unsafePushTables(db, { [entityName ?? "entity"]: table });
27
40
  }
28
41
 
29
- /**
30
- * Bypass (idempotent): same caveat as `unsafeCreateEntityTable` —
31
- * apps declare data via `r.entity(...)`. Checks whether the entity's
32
- * table already exists and skips creation if so. Schema-drift is *not*
33
- * detected: if the table is there but has the wrong columns, that's
34
- * the caller's problem (the dev-server contract is "drop the DB by
35
- * hand when you change the schema"). Tests should use
36
- * `unsafeCreateEntityTable` instead, since they rely on fresh DBs.
37
- */
38
42
  export async function unsafeEnsureEntityTable(
39
- db: ReturnType<typeof drizzle>,
43
+ db: DbConnection,
40
44
  entity: import("../engine/types").EntityDefinition,
41
45
  entityName?: string,
42
46
  ): Promise<boolean> {
@@ -46,54 +50,88 @@ export async function unsafeEnsureEntityTable(
46
50
  return true;
47
51
  }
48
52
 
53
+ // Tables produced by the native dialect already carry EntityTableMeta-shape
54
+ // (source/columns/indexes). renderTableDdl converts that to CREATE TABLE +
55
+ // CREATE INDEX statements executed via db/queries/test-stack.
56
+ function tableToMeta(table: unknown): EntityTableMeta {
57
+ if (
58
+ typeof table === "object" &&
59
+ table !== null &&
60
+ "tableName" in table &&
61
+ "columns" in table &&
62
+ "indexes" in table &&
63
+ "source" in table
64
+ ) {
65
+ return table as EntityTableMeta;
66
+ }
67
+ throw new Error("unsafePushTables: argument is not a SchemaTable / EntityTableMeta");
68
+ }
69
+
49
70
  /**
50
- * Bypass: pushes Drizzle table definitions to the database directly.
51
- * Uses drizzle-kit's generateDrizzleJson + generateMigration to produce SQL,
52
- * then executes it. Same SQL that `drizzle-kit push` would generate.
53
- * Reserved for framework-internal meta-tables (event-store, projections,
54
- * consumer-state) and test setup — apps declare data via `r.entity(...)`.
71
+ * Bypass: pushes table definitions to the database directly. Produces
72
+ * CREATE TABLE IF NOT EXISTS + CREATE INDEX statements via renderTableDdl
73
+ * and executes them via db/queries/test-stack. Idempotent re-runs are safe.
55
74
  *
56
- * @param prevTables - Previous table definitions (for ALTER TABLE scenarios).
57
- * If omitted, assumes empty DB (CREATE TABLE).
75
+ * Reserved for framework-internal meta-tables + test setup. App-defined
76
+ * entities go through `kumiko schema apply` (committed SQL files).
58
77
  */
59
78
  export async function unsafePushTables(
60
- db: ReturnType<typeof drizzle>,
79
+ db: DbConnection,
61
80
  tables: Record<string, unknown>,
62
81
  prevTables?: Record<string, unknown>,
63
82
  ): Promise<void> {
64
- const prevJson = generateDrizzleJson(prevTables ?? {});
65
- const targetJson = generateDrizzleJson(tables);
66
- const statements = await generateMigration(prevJson, targetJson);
67
- for (const stmt of statements) {
68
- await db.execute(sql.raw(stmt));
83
+ const prevMetas = new Map<string, EntityTableMeta>();
84
+ if (prevTables) {
85
+ for (const [key, table] of Object.entries(prevTables)) {
86
+ const meta = tableToMeta(table);
87
+ prevMetas.set(key, meta);
88
+ }
89
+ }
90
+
91
+ for (const [key, table] of Object.entries(tables)) {
92
+ const meta = tableToMeta(table);
93
+ const prev = prevMetas.get(key);
94
+
95
+ if (prev) {
96
+ const prevCols = new Set(prev.columns.map((c) => c.name));
97
+ for (const col of meta.columns) {
98
+ if (!prevCols.has(col.name)) {
99
+ const type = renderColumnType(col);
100
+ const notNull = col.notNull && !col.primaryKey ? " NOT NULL" : "";
101
+ const defaultClause = col.defaultSql !== undefined ? ` DEFAULT ${col.defaultSql}` : "";
102
+ await alterTableAddColumn(db, meta.tableName, col.name, type, defaultClause, notNull);
103
+ }
104
+ }
105
+ const prevIdxNames = new Set(prev.indexes.map((i) => i.name));
106
+ for (const idx of meta.indexes) {
107
+ if (!prevIdxNames.has(idx.name)) {
108
+ const kind = idx.unique ? "UNIQUE INDEX" : "INDEX";
109
+ const colList = idx.columns.map((c) => `"${c}"`).join(", ");
110
+ await createIndexIfNotExists(db, kind, idx.name, meta.tableName, colList);
111
+ }
112
+ }
113
+ } else {
114
+ const statements = renderTableDdl(meta);
115
+ for (const stmt of statements) {
116
+ await executeDdlStatement(db, stmt);
117
+ }
118
+ }
69
119
  }
70
120
  }
71
121
 
122
+ function renderColumnType(col: ColumnMeta): string {
123
+ return pgTypeToSqlType(col.pgType);
124
+ }
125
+
72
126
  /**
73
127
  * Wipes event store + framework-state + the given feature read-models in
74
128
  * one TRUNCATE, then re-registers the event-consumer state rows. Used in
75
129
  * test beforeEach-hooks to return the stack to a clean slate without
76
130
  * rebuilding it.
77
- *
78
- * Fixed list of framework tables (kumiko_events, kumiko_event_consumers,
79
- * kumiko_archived_streams, kumiko_snapshots, kumiko_projections) is always
80
- * included — any event-sourced test setup needs those cleared. The
81
- * `extraTables` arg covers the feature's own read-model tables that would
82
- * otherwise accumulate rows across tests.
83
- *
84
- * Accepts either a Drizzle PgTable (for locally-defined tables: getTableName
85
- * extracts the SQL name) or a plain string (for SQL names whose Drizzle
86
- * reference lives in another module and importing it for the TRUNCATE
87
- * alone would be overkill). Both round-trip to the same TRUNCATE list.
88
- *
89
- * Pre-existing code duplicates this block 30+ times, each with its own
90
- * list of extras. The helper collapses that to a one-liner per test and
91
- * lets a future change to the framework-table set (e.g. adding a new
92
- * consumer-state table) ripple through without touching every suite.
93
131
  */
94
132
  export async function resetEventStore(
95
- stack: TestStack,
96
- extraTables: readonly (PgTable | string)[] = [],
133
+ stack: { db: unknown; eventDispatcher?: EventDispatcher },
134
+ extraTables: readonly (unknown | string)[] = [],
97
135
  ): Promise<void> {
98
136
  const frameworkTables = [
99
137
  "kumiko_events",
@@ -102,9 +140,8 @@ export async function resetEventStore(
102
140
  "kumiko_snapshots",
103
141
  "kumiko_projections",
104
142
  ];
105
- const extraNames = extraTables.map((t) => (typeof t === "string" ? t : getTableName(t)));
106
- const allTables = [...frameworkTables, ...extraNames];
107
- await stack.db.execute(sql.raw(`TRUNCATE ${allTables.join(", ")} RESTART IDENTITY CASCADE`));
143
+ const extraNames = extraTables.map((t) => (typeof t === "string" ? t : tableNameOf(t)));
144
+ await truncateTablesRestartIdentity(stack.db, [...frameworkTables, ...extraNames]);
108
145
  if (stack.eventDispatcher) {
109
146
  await stack.eventDispatcher.ensureRegistered();
110
147
  }
@@ -3,7 +3,8 @@ import type { AuthRoutesConfig } from "../api/auth-routes";
3
3
  import type { JwtHelper } from "../api/jwt";
4
4
  import { buildServer } from "../api/server";
5
5
  import { createSseBroker } from "../api/sse-broker";
6
- import type { DbConnection } from "../db/connection";
6
+ import type { PgClient } from "../db/connection";
7
+ import { extractTableInfo } from "../db/query";
7
8
  import { createRegistry } from "../engine/registry";
8
9
  import type { FeatureDefinition, Registry, TenantId } from "../engine/types";
9
10
  import { createArchivedStreamsTable, createEventsTable } from "../event-store";
@@ -23,9 +24,8 @@ export type TestStack = {
23
24
  app: Hono;
24
25
  jwt: JwtHelper;
25
26
  registry: Registry;
26
- /** Drizzle connection the test DB's lifecycle (name, raw pg client,
27
- * drop) lives inside setupTestStack and is released via stack.cleanup(). */
28
- db: DbConnection;
27
+ // biome-ignore lint/suspicious/noExplicitAny: cross-provider connection
28
+ db: any;
29
29
  redis: TestRedis;
30
30
  search: SearchAdapter;
31
31
  events: EventCollector;
@@ -58,7 +58,8 @@ export type TestStackOptions = {
58
58
  | Record<string, unknown>
59
59
  | ((deps: {
60
60
  registry: Registry;
61
- db: import("../db/connection").DbConnection;
61
+ // biome-ignore lint/suspicious/noExplicitAny: cross-provider connection
62
+ db: any;
62
63
  sseBroker: import("../api/sse-broker").SseBroker;
63
64
  redis: import("ioredis").default;
64
65
  }) => Record<string, unknown>);
@@ -110,7 +111,8 @@ export type TestStackOptions = {
110
111
  | import("../api/server").ServerOptions["anonymousAccess"]
111
112
  | ((deps: {
112
113
  registry: Registry;
113
- db: import("../db/connection").DbConnection;
114
+ // biome-ignore lint/suspicious/noExplicitAny: cross-provider connection
115
+ db: any;
114
116
  sseBroker: import("../api/sse-broker").SseBroker;
115
117
  redis: import("ioredis").default;
116
118
  }) => import("../api/server").ServerOptions["anonymousAccess"]);
@@ -206,10 +208,9 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
206
208
  // exist; drizzle-kit's diff machinery would otherwise emit CREATE for
207
209
  // them again.
208
210
  const { tableExists } = await import("../db/schema-inspection");
209
- const { getTableName } = await import("drizzle-orm");
210
211
  const missing: Record<string, unknown> = {};
211
212
  for (const [key, tbl] of Object.entries(projectionTables)) {
212
- const physical = getTableName(tbl as Parameters<typeof getTableName>[0]); // @cast-boundary drizzle-bridge
213
+ const physical = extractTableInfo(tbl).name;
213
214
  if (await tableExists(testDb.db, `public.${physical}`)) continue;
214
215
  missing[key] = tbl;
215
216
  }
@@ -294,7 +295,7 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
294
295
  // post-commit latency (Sprint E.4).
295
296
  eventDispatcher: {
296
297
  pollIntervalMs: 50,
297
- pgClient: testDb.client,
298
+ pgClient: testDb.client as PgClient | undefined,
298
299
  systemConsumers: {
299
300
  sse: enabledHooks.includes("sse"),
300
301
  search: enabledHooks.includes("search"),
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { clearTables } from "../db-cleanup";
3
+
4
+ describe("db-cleanup", () => {
5
+ test("clearTables issues DELETE without WHERE per table via deleteMany", async () => {
6
+ const sqlLog: string[] = [];
7
+ const mockDb = {
8
+ unsafe: async (sql: string) => {
9
+ sqlLog.push(sql);
10
+ return [];
11
+ },
12
+ };
13
+
14
+ await clearTables(mockDb, ["read_users", "kumiko_events"]);
15
+
16
+ expect(sqlLog).toHaveLength(2);
17
+ expect(sqlLog[0]).toBe('DELETE FROM "read_users"');
18
+ expect(sqlLog[1]).toBe('DELETE FROM "kumiko_events"');
19
+ });
20
+
21
+ test("clearTables accepts EntityTableMeta-shaped tables", async () => {
22
+ const sqlLog: string[] = [];
23
+ const mockDb = {
24
+ unsafe: async (sql: string) => {
25
+ sqlLog.push(sql);
26
+ return [];
27
+ },
28
+ };
29
+
30
+ const userTable = {
31
+ source: "managed" as const,
32
+ tableName: "read_users",
33
+ columns: [{ name: "id", pgType: "uuid", notNull: true, primaryKey: true }],
34
+ indexes: [],
35
+ };
36
+
37
+ await clearTables(mockDb, [userTable]);
38
+ expect(sqlLog[0]).toBe('DELETE FROM "read_users"');
39
+ });
40
+ });
@@ -1,4 +1,4 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { describe, expect, test } from "bun:test";
2
2
  import { z } from "zod";
3
3
  import {
4
4
  createBooleanField,
@@ -1,5 +1,5 @@
1
- import { sql } from "drizzle-orm";
2
- import { afterAll, beforeAll, describe, expect, test } from "vitest";
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { asRawClient } from "../../db/query";
3
3
  import type { EntityDefinition } from "../../engine/types";
4
4
  import {
5
5
  createTestDb,
@@ -34,8 +34,8 @@ describe("unsafeEnsureEntityTable", () => {
34
34
  test("legt die Tabelle beim ersten Aufruf an (returnt true)", async () => {
35
35
  const created = await unsafeEnsureEntityTable(db.db, tenantEntity, "probe");
36
36
  expect(created).toBe(true);
37
- const rows = await db.db.execute<{ exists: boolean }>(
38
- sql`SELECT to_regclass('public.ensure_entity_table_probe') IS NOT NULL AS exists`,
37
+ const rows = await asRawClient(db.db).unsafe<{ exists: boolean }>(
38
+ `SELECT to_regclass('public.ensure_entity_table_probe') IS NOT NULL AS exists`,
39
39
  );
40
40
  expect(rows[0]?.exists).toBe(true);
41
41
  });
@@ -45,15 +45,8 @@ describe("unsafeEnsureEntityTable", () => {
45
45
  expect(created).toBe(false);
46
46
  });
47
47
 
48
- test("unsafeCreateEntityTable bleibt strict — wirft bei existierender Tabelle", async () => {
49
- // Gleiche Entity zweimal via unsafeCreateEntityTable postgres 42P07
50
- // (relation already exists). Drizzle wrappt den PG-Error in
51
- // DrizzleQueryError; der echte Code steckt in .cause. Sicherstellt,
52
- // dass unsafeEnsureEntityTable nicht versehentlich das strict-Verhalten
53
- // verändert.
54
- await expect(unsafeCreateEntityTable(db.db, tenantEntity, "probe")).rejects.toSatisfy((err) => {
55
- const cause = (err as { cause?: { code?: string } }).cause;
56
- return cause?.code === "42P07";
57
- });
48
+ test("unsafeCreateEntityTable ist idempotentzweiter Push wirft nicht (CREATE IF NOT EXISTS)", async () => {
49
+ // CREATE TABLE IF NOT EXISTS idempotent by design.
50
+ await expect(unsafeCreateEntityTable(db.db, tenantEntity, "probe")).resolves.toBeUndefined();
58
51
  });
59
52
  });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Integration-test DB cleanup — replaces copy-pasted `DELETE FROM …` in
3
+ * beforeEach hooks. All table clears go through typed `deleteMany` (empty
4
+ * where = full table wipe). Raw SQL stays out of test files.
5
+ */
6
+ import { type AnyDb, deleteMany } from "../db/query";
7
+
8
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
9
+ const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
10
+
11
+ /** EntityTableMeta, drizzle pgTable, or plain table name string. */
12
+ export type ClearableTable = string | { readonly tableName?: string } | unknown;
13
+
14
+ function tableFromName(name: string): unknown {
15
+ return {
16
+ [KUMIKO_NAME_SYMBOL]: name,
17
+ [KUMIKO_COLUMNS_SYMBOL]: {},
18
+ };
19
+ }
20
+
21
+ function resolveClearableTable(table: ClearableTable): unknown {
22
+ if (typeof table === "string") return tableFromName(table);
23
+ if (
24
+ typeof table === "object" &&
25
+ table !== null &&
26
+ "tableName" in table &&
27
+ typeof (table as { tableName?: unknown }).tableName === "string"
28
+ ) {
29
+ return table;
30
+ }
31
+ return table;
32
+ }
33
+
34
+ /** Delete all rows from each table (order preserved — FK-sensitive callers order explicitly). */
35
+ export async function clearTables(db: AnyDb, tables: readonly ClearableTable[]): Promise<void> {
36
+ for (const table of tables) {
37
+ await deleteMany(db, resolveClearableTable(table), {});
38
+ }
39
+ }
40
+
41
+ /** Alias — same as clearTables, reads better in beforeEach. */
42
+ export async function resetTestTables(db: AnyDb, tables: readonly ClearableTable[]): Promise<void> {
43
+ await clearTables(db, tables);
44
+ }
@@ -1,4 +1,4 @@
1
- import { expect } from "vitest";
1
+ import { expect } from "bun:test";
2
2
  import type { WriteErrorInfo } from "../errors";
3
3
 
4
4
  // Vitest's toContain doesn't operate on plain objects, so after the move from
@@ -5,6 +5,7 @@
5
5
 
6
6
  export { rolesOf } from "./access-assertions";
7
7
  export { expectError, expectSuccess } from "./assertions";
8
+ export { type ClearableTable, clearTables, resetTestTables } from "./db-cleanup";
8
9
  export {
9
10
  type E2EGeneratorOptions,
10
11
  type E2ETestSpec,
@@ -21,6 +22,7 @@ export {
21
22
  type ParsedSetCookie,
22
23
  } from "./http-cookies";
23
24
  export { createLateBoundHolder, type LateBoundHolder } from "./late-bound";
25
+ export { buildMultipartBody, patchFileInstanceofForBunTest } from "./multipart-helper";
24
26
  export {
25
27
  createMutableMasterKeyProvider,
26
28
  type MutableMasterKeyProvider,
@@ -0,0 +1,94 @@
1
+ // Workarounds for Bun v1.3.x bun:test limitations with multipart/form-data.
2
+ //
3
+ // Two bugs combine to break file upload tests in bun:test:
4
+ //
5
+ // 1. Content-Type omission: both app.request({body: formData}) and
6
+ // fetch(url, {body: formData}) stringify FormData via .toString() instead
7
+ // of serializing it as multipart, so no Content-Type header is set.
8
+ // Fix: serialize FormData manually via buildMultipartBody().
9
+ //
10
+ // 2. Cross-realm instanceof: Hono's multipart parser creates Blob objects from
11
+ // a different JS realm than the test globals. In bun:test this means
12
+ // `parsedValue instanceof File` is always false even when the value has all
13
+ // File properties. Fix: patchFilInstanceofForBunTest().
14
+ //
15
+ // Both fixes are test-only — production code and real HTTP clients are unaffected.
16
+
17
+ /**
18
+ * Serializes a FormData instance to multipart/form-data bytes.
19
+ *
20
+ * Returns the encoded body and the Content-Type header value (including the
21
+ * generated boundary). Pass both directly to app.request or fetch.
22
+ */
23
+ export async function buildMultipartBody(
24
+ fd: FormData,
25
+ ): Promise<{ body: BodyInit; contentType: string }> {
26
+ const boundary = `KumikoBnd${Date.now()}${Math.random().toString(36).slice(2, 8)}`;
27
+ const enc = new TextEncoder();
28
+ const parts: Uint8Array[] = [];
29
+
30
+ for (const [name, value] of fd.entries()) {
31
+ const v = value as unknown as {
32
+ name?: string;
33
+ type?: string;
34
+ arrayBuffer?: () => Promise<ArrayBuffer>;
35
+ };
36
+ if (typeof v === "object" && v !== null && typeof v.arrayBuffer === "function") {
37
+ parts.push(
38
+ enc.encode(
39
+ `--${boundary}\r\nContent-Disposition: form-data; name="${name}"; filename="${v.name ?? "blob"}"\r\nContent-Type: ${v.type || "application/octet-stream"}\r\n\r\n`,
40
+ ),
41
+ );
42
+ parts.push(new Uint8Array(await v.arrayBuffer()));
43
+ parts.push(enc.encode("\r\n"));
44
+ } else {
45
+ parts.push(
46
+ enc.encode(
47
+ `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${String(value)}\r\n`,
48
+ ),
49
+ );
50
+ }
51
+ }
52
+ parts.push(enc.encode(`--${boundary}--\r\n`));
53
+
54
+ const total = parts.reduce((s, p) => s + p.length, 0);
55
+ const buf = new Uint8Array(total);
56
+ let off = 0;
57
+ for (const p of parts) {
58
+ buf.set(p, off);
59
+ off += p.length;
60
+ }
61
+ return {
62
+ body: buf as unknown as BodyInit,
63
+ contentType: `multipart/form-data; boundary=${boundary}`,
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Patches File[Symbol.hasInstance] so that cross-realm Blob objects returned
69
+ * by Hono's parseBody() pass `instanceof File` checks in bun:test.
70
+ *
71
+ * In bun:test the multipart parser runs in a different JS realm than the test
72
+ * globals, so the Blob/File constructors differ. The patch replaces the
73
+ * prototype-chain check with a duck-type check: an object with string `.name`,
74
+ * number `.size`, and function `.arrayBuffer` is treated as a File.
75
+ *
76
+ * Safe to call multiple times (idempotent via the `_patched` marker).
77
+ */
78
+ export function patchFileInstanceofForBunTest(): void {
79
+ // skip: idempotent re-patch — already installed, nothing to do
80
+ if ((File as unknown as { _kumikoPatched?: boolean })._kumikoPatched) return;
81
+ Object.defineProperty(File, Symbol.hasInstance, {
82
+ value(instance: unknown): boolean {
83
+ if (typeof instance !== "object" || instance === null) return false;
84
+ const f = instance as Record<string, unknown>;
85
+ return (
86
+ typeof f["name"] === "string" &&
87
+ typeof f["size"] === "number" &&
88
+ typeof f["arrayBuffer"] === "function"
89
+ );
90
+ },
91
+ configurable: true,
92
+ });
93
+ (File as unknown as { _kumikoPatched?: boolean })._kumikoPatched = true;
94
+ }