@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
@@ -0,0 +1,842 @@
1
+ // Typed Query-API über Bun.sql. KEIN drizzle intern. Identische Signatur
2
+ // zur legacy `db/query-api.ts` — App-code-migration ist Import-Pfad-Wechsel,
3
+ // kein call-site-Refactor.
4
+ //
5
+ // API:
6
+ // selectMany<T>(db, table, where?, opts?) → readonly T[]
7
+ // fetchOne<T>(db, table, where) → T | undefined
8
+ // insertOne<T>(db, table, values) → T | undefined
9
+ // updateMany<T>(db, table, set, where) → readonly T[]
10
+ // deleteMany(db, table, where) → void
11
+ // countWhere(db, table, where?) → number
12
+ // upsertByPk / upsertOnConflict / incrementCounter
13
+ // deleteManyBatched(db, table, where, { limit }) → { deleted, batches }
14
+ // transaction<T>(db, fn) → T
15
+ //
16
+ // `table` kann sein:
17
+ // - EntityTableMeta (preferred, plain data)
18
+ // - drizzle pgTable (legacy, hat Symbol-based tableName) — extracted via
19
+ // drizzle's getTableName + getTableColumns (drizzle weiterhin als type-
20
+ // reference, NICHT als runtime-API-call)
21
+
22
+ import type { EntityTableMeta } from "../db/entity-table-meta";
23
+ import { toSnakeCase } from "../db/table-builder";
24
+ import { camelCase as envCamelCase } from "../env";
25
+ import { parseJsonSafe } from "../utils/safe-json";
26
+
27
+ // Idempotent snake_case → camelCase. `env.camelCase` always lowercases first
28
+ // (designed for SHOUT_CASE input) — for already-camelCase keys (mock rows
29
+ // in tests, projection-aliased columns) it would silently produce "tenantid"
30
+ // instead of leaving "tenantId" alone. Guard with an underscore check so
31
+ // the conversion only fires when the key is actually snake-shaped.
32
+ function snakeToCamel(key: string): string {
33
+ if (!key.includes("_")) return key;
34
+ return envCamelCase(key);
35
+ }
36
+
37
+ import type { BunDbRunner } from "./connection";
38
+
39
+ // Drizzle-pgTable-Inspection via raw Symbol-access (kein drizzle-orm import).
40
+ // drizzle stores the table name unter `Symbol.for("kumiko:schema:Name")` und die
41
+ // column-map unter `Symbol.for("kumiko:schema:Columns")`.
42
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
43
+ const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
44
+
45
+ function getTableName(table: unknown): string | null {
46
+ if (typeof table !== "object" || table === null) return null;
47
+ const name = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
48
+ return typeof name === "string" ? name : null;
49
+ }
50
+
51
+ function extractDrizzleColumns(table: unknown): Map<string, { name: string; sqlType?: string }> {
52
+ const out = new Map<string, { name: string; sqlType?: string }>();
53
+ if (typeof table !== "object" || table === null) return out;
54
+ const cols = (table as Record<symbol, unknown>)[KUMIKO_COLUMNS_SYMBOL];
55
+ if (typeof cols !== "object" || cols === null) return out;
56
+ for (const [key, val] of Object.entries(cols as Record<string, unknown>)) {
57
+ if (typeof val !== "object" || val === null) continue;
58
+ const colObj = val as { name?: unknown; getSQLType?: () => string };
59
+ const colName = colObj.name;
60
+ if (typeof colName !== "string") continue;
61
+ // Drizzle's getSQLType uses `this` — call as method on colObj.
62
+ const sqlType = typeof colObj.getSQLType === "function" ? colObj.getSQLType() : undefined;
63
+ out.set(key, { name: colName, ...(sqlType !== undefined && { sqlType }) });
64
+ }
65
+ return out;
66
+ }
67
+
68
+ // `db` Input akzeptiert drei Shapes:
69
+ // 1. Bun.SQL connection (BunDbRunner) — neue Welt, native .unsafe + .begin
70
+ // 2. drizzle DbConnection (postgres-js-Wrapper) — legacy compat,
71
+ // raw postgres-js client liegt unter `db.$client.unsafe / .begin`
72
+ // 3. drizzle tx-handle — client liegt unter `tx.session.client`
73
+ // Shim extrahiert in allen Fällen ein `{unsafe, begin}`-Surface.
74
+ type RawClient = {
75
+ unsafe: <TRow = unknown>(sql: string, params?: readonly unknown[]) => Promise<readonly TRow[]>;
76
+ begin: <T>(fn: (tx: unknown) => Promise<T>) => Promise<T>;
77
+ };
78
+
79
+ export function asRawClient(db: unknown): RawClient {
80
+ const dbAny = db as Record<string, unknown>;
81
+ // Direct: Bun.SQL / postgres-js Sql / postgres-js TransactionSql. All three
82
+ // expose `.unsafe`; only Sql/Bun.SQL have `.begin` — TransactionSql uses
83
+ // `.savepoint` for nested-tx. We only require `.unsafe` here; callers that
84
+ // need `.begin` (e.g. `transaction()`) verify it themselves.
85
+ if (typeof dbAny["unsafe"] === "function") {
86
+ return dbAny as unknown as RawClient;
87
+ }
88
+ // TenantDb-shape: framework wrapper exposing the underlying runner as `.raw`.
89
+ // Callers that pass `ctx.db` instead of `ctx.db.raw` land here — unwrap once.
90
+ const raw = dbAny["raw"];
91
+ if (raw && typeof (raw as Record<string, unknown>)["unsafe"] === "function") {
92
+ return raw as unknown as RawClient;
93
+ }
94
+ // Drizzle DbConnection (legacy compat): $client = postgres-js Sql.
95
+ const $client = dbAny["$client"];
96
+ if ($client && typeof ($client as Record<string, unknown>)["unsafe"] === "function") {
97
+ return $client as unknown as RawClient;
98
+ }
99
+ // Drizzle pg-transaction (legacy compat): session.client = postgres-js Sql.
100
+ const session = dbAny["session"] as Record<string, unknown> | undefined;
101
+ const sessionClient = session?.["client"];
102
+ if (sessionClient && typeof (sessionClient as Record<string, unknown>)["unsafe"] === "function") {
103
+ return sessionClient as unknown as RawClient;
104
+ }
105
+ throw new Error(
106
+ "bun-db: db argument has no .unsafe() — pass Bun.SQL, postgres-js Sql, TenantDb, or a transaction handle.",
107
+ );
108
+ }
109
+
110
+ export type AnyDb = BunDbRunner | unknown;
111
+
112
+ // WhereValue: primitive für eq, array für IN, null für IS NULL, oder
113
+ // operator-object für range/comparisons.
114
+ export type WhereOperator = {
115
+ readonly gt?: unknown;
116
+ readonly gte?: unknown;
117
+ readonly lt?: unknown;
118
+ readonly lte?: unknown;
119
+ readonly ne?: unknown;
120
+ readonly in?: readonly unknown[];
121
+ readonly like?: string;
122
+ };
123
+ export type WhereValue = unknown | WhereOperator;
124
+ export type WhereObject = Record<string, WhereValue>;
125
+
126
+ function isWhereOperator(v: unknown): v is WhereOperator {
127
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
128
+ // Don't false-positive on Date / Temporal / other domain-objects.
129
+ // WhereOperator is plain object literal with at most these keys.
130
+ const keys = Object.keys(v);
131
+ if (keys.length === 0) return false;
132
+ const opKeys = ["gt", "gte", "lt", "lte", "ne", "in", "like"];
133
+ return keys.every((k) => opKeys.includes(k));
134
+ }
135
+ export type OrderByClause = {
136
+ readonly col: string;
137
+ readonly direction?: "asc" | "desc";
138
+ };
139
+
140
+ export type SelectOptions = {
141
+ readonly limit?: number;
142
+ // Single column or array for multi-column tie-breaks (e.g.
143
+ // [{col: "createdAt"}, {col: "id"}] for chronological-with-stable-id).
144
+ readonly orderBy?: OrderByClause | readonly OrderByClause[];
145
+ };
146
+
147
+ // Akzeptiert EITHER. Beide haben einen tableName und field→column-mapping.
148
+ // biome-ignore lint/suspicious/noExplicitAny: legacy drizzle pgTable surface
149
+ type TableLike = EntityTableMeta | any;
150
+
151
+ export type TableInfo = {
152
+ readonly name: string;
153
+ // field-name (camelCase oder snake_case) → snake_case column-name
154
+ readonly columnOf: (field: string) => string;
155
+ // pgType per column-name, for jsonb-cast detection
156
+ readonly pgTypeOf: (column: string) => string | undefined;
157
+ // Inverse of columnOf — snake_case DB column → JS field-name (camelCase).
158
+ // Used at the result boundary to rename row keys back to the API shape
159
+ // that callers consume (`row.aggregateId` instead of `row.aggregate_id`).
160
+ readonly fieldOf: (column: string) => string;
161
+ // Check if a field name or column name is known to this table
162
+ readonly hasColumn: (fieldOrColumn: string) => boolean;
163
+ };
164
+
165
+ export function extractTableInfo(table: TableLike): TableInfo {
166
+ // EntityTableMeta discriminator: hat source-property "managed" | "unmanaged"
167
+ if (
168
+ table !== null &&
169
+ typeof table === "object" &&
170
+ "source" in table &&
171
+ (table.source === "managed" || table.source === "unmanaged")
172
+ ) {
173
+ const meta = table as EntityTableMeta;
174
+ const colByField = new Map<string, string>();
175
+ const fieldByCol = new Map<string, string>();
176
+ const typeByCol = new Map<string, string>();
177
+ for (const c of meta.columns) {
178
+ typeByCol.set(c.name, c.pgType);
179
+ // EntityTableMeta column names are snake_case. Map snake → snake AND
180
+ // derive a camelCase JS field-name so result rows can be renamed back
181
+ // to the API shape (`aggregate_id` → `aggregateId`).
182
+ colByField.set(c.name, c.name);
183
+ const camel = snakeToCamel(c.name);
184
+ if (camel !== c.name) colByField.set(camel, c.name);
185
+ fieldByCol.set(c.name, camel === c.name ? c.name : camel);
186
+ }
187
+ return {
188
+ name: meta.tableName,
189
+ columnOf: (field) => colByField.get(field) ?? toSnakeCase(field),
190
+ pgTypeOf: (col) => typeByCol.get(col),
191
+ fieldOf: (col) => fieldByCol.get(col) ?? snakeToCamel(col),
192
+ hasColumn: (fieldOrColumn) => colByField.has(fieldOrColumn) || fieldByCol.has(fieldOrColumn),
193
+ };
194
+ }
195
+ // drizzle pgTable: tableName via Symbol.for("kumiko:schema:Name"), columns via
196
+ // enumerable properties (jeder col-object hat .name + .getSQLType()).
197
+ // Wir lesen Beide via raw Symbol/Property-access — kein drizzle-orm import.
198
+ const name = getTableName(table);
199
+ if (!name) {
200
+ throw new Error(
201
+ "bun-db.extractTableInfo: table-Argument ist weder EntityTableMeta noch drizzle pgTable",
202
+ );
203
+ }
204
+ const cols = extractDrizzleColumns(table);
205
+ const colByField = new Map<string, string>();
206
+ const fieldByCol = new Map<string, string>();
207
+ const typeByCol = new Map<string, string>();
208
+ for (const [field, { name: colName, sqlType }] of cols) {
209
+ colByField.set(field, colName);
210
+ fieldByCol.set(colName, field);
211
+ if (sqlType) typeByCol.set(colName, sqlType);
212
+ }
213
+ return {
214
+ name,
215
+ columnOf: (field) => colByField.get(field) ?? toSnakeCase(field),
216
+ pgTypeOf: (col) => typeByCol.get(col),
217
+ fieldOf: (col) => fieldByCol.get(col) ?? snakeToCamel(col),
218
+ hasColumn: (fieldOrColumn) => colByField.has(fieldOrColumn) || fieldByCol.has(fieldOrColumn),
219
+ };
220
+ }
221
+
222
+ function quoteIdent(name: string): string {
223
+ return `"${name.replace(/"/g, '""')}"`;
224
+ }
225
+
226
+ // --- Value coercion at the driver boundary ------------------------------
227
+ //
228
+ // postgres-js returns timestamptz as JS Date (or ISO string depending on
229
+ // driver config). Framework contract says timestamptz surfaces as
230
+ // Temporal.Instant — without coercion every read hands callers a Date and
231
+ // downstream code that does .epochMilliseconds / .add(...) crashes.
232
+ //
233
+ // Symmetric on write: Temporal.Instant doesn't bind directly into postgres-js
234
+ // params — convert to ISO string when the column pgType is timestamptz.
235
+
236
+ function isTemporalInstant(v: unknown): boolean {
237
+ return (
238
+ typeof v === "object" &&
239
+ v !== null &&
240
+ typeof (v as { epochNanoseconds?: unknown }).epochNanoseconds === "bigint"
241
+ );
242
+ }
243
+
244
+ function instantFromDriver(value: unknown): Temporal.Instant | null {
245
+ if (value === null || value === undefined) return null;
246
+ if (isTemporalInstant(value)) return value as Temporal.Instant;
247
+ // Number-coercion via +date — equivalent to .getTime() ohne Date-API-Methode
248
+ // (guard-no-date-api). Date → epochMilliseconds, dann zu Temporal.Instant.
249
+ if (value instanceof Date) return Temporal.Instant.fromEpochMilliseconds(+value);
250
+ if (typeof value === "string") {
251
+ try {
252
+ return Temporal.Instant.from(value);
253
+ } catch {
254
+ return null;
255
+ }
256
+ }
257
+ if (typeof value === "number") return Temporal.Instant.fromEpochMilliseconds(value);
258
+ return null;
259
+ }
260
+
261
+ // Walk the driver-row, applying three boundary-conversions per known column:
262
+ // - rename key snake_case → camelCase JS field-name (drizzle did this
263
+ // invisibly via its column-mapper; native dialect rebuild lost it)
264
+ // - parse jsonb string → object (postgres-js returns jsonb as text by
265
+ // default, not parsed JSON)
266
+ // - coerce timestamptz Date/string → Temporal.Instant
267
+ //
268
+ // Unknown columns (computed/aliased) pass through unchanged.
269
+ export function coerceRow<T extends Record<string, unknown>>(row: T, info: TableInfo): T {
270
+ const out: Record<string, unknown> = {};
271
+ let changed = false;
272
+ for (const key of Object.keys(row)) {
273
+ const pgType = info.pgTypeOf(key);
274
+ const value = row[key];
275
+ let coerced: unknown = value;
276
+ if (pgType === "timestamptz" || pgType === "timestamptz(3)") {
277
+ const t = instantFromDriver(value);
278
+ if (t !== null) coerced = t;
279
+ } else if (pgType === "jsonb" && typeof value === "string") {
280
+ coerced = parseJsonSafe(value, value);
281
+ } else if ((pgType === "bigint" || pgType === "bigserial") && typeof value === "string") {
282
+ // postgres-js returns BIGINT as string to avoid JS-Number precision
283
+ // loss past 2^53. Framework contract: bigint columns surface as
284
+ // JS `bigint`. Drizzle's bigint customType did this conversion
285
+ // invisibly; the native dialect rebuild needs it explicit.
286
+ try {
287
+ coerced = BigInt(value);
288
+ } catch {
289
+ // leave as string on parse error
290
+ }
291
+ }
292
+ const fieldName = info.fieldOf(key);
293
+ if (fieldName !== key) changed = true;
294
+ if (coerced !== value) changed = true;
295
+ out[fieldName] = coerced;
296
+ }
297
+ return (changed ? out : row) as T;
298
+ }
299
+
300
+ function coerceRows<T extends Record<string, unknown>>(
301
+ rows: readonly T[],
302
+ info: TableInfo,
303
+ ): readonly T[] {
304
+ return rows.map((r) => coerceRow(r, info));
305
+ }
306
+
307
+ // Helper für jsonb-Werte: Bun.sql kann arrays/objects nicht direkt als
308
+ // jsonb binden — wir JSON.stringify + ::jsonb cast.
309
+ // Plus Temporal.Instant → ISO string coercion for timestamptz columns.
310
+ // SqlExpression (sql`now()`, sql`gen_random_uuid()`) wird als kind:"literal"
311
+ // returned — Caller embedded das inline statt einen $N-placeholder zu setzen.
312
+ type PreparedValue =
313
+ | { readonly kind: "param"; readonly sql: string; readonly bound: unknown }
314
+ | { readonly kind: "literal"; readonly literal: string };
315
+
316
+ function isSqlExpression(v: unknown): v is { kind: "sql-expr"; text: string } {
317
+ return typeof v === "object" && v !== null && (v as { kind?: unknown }).kind === "sql-expr";
318
+ }
319
+
320
+ function prepareValue(value: unknown, pgType: string | undefined): PreparedValue {
321
+ if (isSqlExpression(value)) {
322
+ return { kind: "literal", literal: value.text };
323
+ }
324
+ if (
325
+ pgType === "jsonb" &&
326
+ value !== null &&
327
+ typeof value === "object" &&
328
+ !isTemporalInstant(value)
329
+ ) {
330
+ return { kind: "param", sql: "::jsonb", bound: JSON.stringify(value) };
331
+ }
332
+ if ((pgType === "timestamptz" || pgType === "timestamptz(3)") && isTemporalInstant(value)) {
333
+ return { kind: "param", sql: "", bound: (value as Temporal.Instant).toString() };
334
+ }
335
+ return { kind: "param", sql: "", bound: value };
336
+ }
337
+
338
+ function buildWhereClause(
339
+ info: TableInfo,
340
+ where: WhereObject,
341
+ startIndex: number,
342
+ ): { sqlText: string; values: unknown[] } {
343
+ const conditions: string[] = [];
344
+ const values: unknown[] = [];
345
+ let idx = startIndex;
346
+ for (const [field, value] of Object.entries(where)) {
347
+ const col = info.columnOf(field);
348
+ const pgType = info.pgTypeOf(col);
349
+ if (value === null) {
350
+ conditions.push(`${quoteIdent(col)} IS NULL`);
351
+ } else if (Array.isArray(value)) {
352
+ if (value.length === 0) {
353
+ conditions.push("FALSE");
354
+ } else {
355
+ const parts: string[] = [];
356
+ for (const v of value) {
357
+ const p = prepareValue(v, pgType);
358
+ if (p.kind === "literal") {
359
+ parts.push(p.literal);
360
+ } else {
361
+ parts.push(`$${idx++}${p.sql}`);
362
+ values.push(p.bound);
363
+ }
364
+ }
365
+ conditions.push(`${quoteIdent(col)} IN (${parts.join(", ")})`);
366
+ }
367
+ } else if (isWhereOperator(value)) {
368
+ const opMap: Record<string, string> = {
369
+ gt: ">",
370
+ gte: ">=",
371
+ lt: "<",
372
+ lte: "<=",
373
+ ne: "<>",
374
+ like: "LIKE",
375
+ };
376
+ for (const [opKey, opSym] of Object.entries(opMap)) {
377
+ const opVal = (value as Record<string, unknown>)[opKey];
378
+ if (opVal === undefined) continue;
379
+ const p = prepareValue(opVal, pgType);
380
+ if (p.kind === "literal") {
381
+ conditions.push(`${quoteIdent(col)} ${opSym} ${p.literal}`);
382
+ } else {
383
+ conditions.push(`${quoteIdent(col)} ${opSym} $${idx++}${p.sql}`);
384
+ values.push(p.bound);
385
+ }
386
+ }
387
+ const inVal = (value as Record<string, unknown>)["in"];
388
+ if (Array.isArray(inVal)) {
389
+ if (inVal.length === 0) {
390
+ conditions.push("FALSE");
391
+ } else {
392
+ const parts: string[] = [];
393
+ for (const v of inVal) {
394
+ const p = prepareValue(v, pgType);
395
+ if (p.kind === "literal") {
396
+ parts.push(p.literal);
397
+ } else {
398
+ parts.push(`$${idx++}${p.sql}`);
399
+ values.push(p.bound);
400
+ }
401
+ }
402
+ conditions.push(`${quoteIdent(col)} IN (${parts.join(", ")})`);
403
+ }
404
+ }
405
+ } else {
406
+ const p = prepareValue(value, pgType);
407
+ if (p.kind === "literal") {
408
+ conditions.push(`${quoteIdent(col)} = ${p.literal}`);
409
+ } else {
410
+ conditions.push(`${quoteIdent(col)} = $${idx++}${p.sql}`);
411
+ values.push(p.bound);
412
+ }
413
+ }
414
+ }
415
+ return { sqlText: conditions.join(" AND "), values };
416
+ }
417
+
418
+ // biome-ignore lint/suspicious/noExplicitAny: opt-in default loosens row type for unannotated test fixtures
419
+ export async function selectMany<TRow = any>(
420
+ db: AnyDb,
421
+ table: TableLike,
422
+ where?: WhereObject,
423
+ options?: SelectOptions,
424
+ ): Promise<readonly TRow[]> {
425
+ const info = extractTableInfo(table);
426
+ let sqlText = `SELECT * FROM ${quoteIdent(info.name)}`;
427
+ let values: unknown[] = [];
428
+ if (where && Object.keys(where).length > 0) {
429
+ const w = buildWhereClause(info, where, 1);
430
+ sqlText += ` WHERE ${w.sqlText}`;
431
+ values = w.values;
432
+ }
433
+ if (options?.orderBy) {
434
+ const clauses = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
435
+ const parts = clauses.map((c) => {
436
+ const col = info.columnOf(c.col);
437
+ const dir = c.direction === "desc" ? "DESC" : "ASC";
438
+ return `${quoteIdent(col)} ${dir}`;
439
+ });
440
+ if (parts.length > 0) sqlText += ` ORDER BY ${parts.join(", ")}`;
441
+ }
442
+ if (options?.limit !== undefined) {
443
+ sqlText += ` LIMIT ${options.limit}`;
444
+ }
445
+ const raw = (await asRawClient(db).unsafe(sqlText, values)) as readonly Record<string, unknown>[];
446
+ return coerceRows(raw, info) as readonly TRow[];
447
+ }
448
+
449
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
450
+ export async function fetchOne<TRow = any>(
451
+ db: AnyDb,
452
+ table: TableLike,
453
+ where: WhereObject,
454
+ ): Promise<TRow | undefined> {
455
+ const rows = await selectMany<TRow>(db, table, where, { limit: 1 });
456
+ return rows[0];
457
+ }
458
+
459
+ // Bulk INSERT — same shape as insertOne but takes an array of rows and
460
+ // produces one multi-VALUES statement. Mirrors drizzle's
461
+ // `db.insert(t).values(rows[])`. Empty input is a no-op.
462
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
463
+ export async function insertMany<TRow = any>(
464
+ db: AnyDb,
465
+ table: TableLike,
466
+ rows: ReadonlyArray<Record<string, unknown>>,
467
+ ): Promise<readonly TRow[]> {
468
+ if (rows.length === 0) return [];
469
+ const info = extractTableInfo(table);
470
+ // Use the column-set from the first row; assume all rows share keys.
471
+ const firstRow = rows[0];
472
+ if (firstRow === undefined) return [];
473
+ // Filter out fields that don't exist as columns in the table (like drizzle did implicitly)
474
+ const fields = Object.keys(firstRow).filter((k) => info.hasColumn(k));
475
+ if (fields.length === 0) throw new Error("insertMany: empty row object");
476
+ const cols = fields.map((k) => quoteIdent(info.columnOf(k))).join(", ");
477
+ const params: unknown[] = [];
478
+ const valuesClauses: string[] = [];
479
+ for (const row of rows) {
480
+ const placeholders: string[] = [];
481
+ for (const f of fields) {
482
+ const col = info.columnOf(f);
483
+ const pgType = info.pgTypeOf(col);
484
+ const p = prepareValue(row[f], pgType);
485
+ if (p.kind === "literal") {
486
+ placeholders.push(p.literal);
487
+ } else {
488
+ params.push(p.bound);
489
+ placeholders.push(`$${params.length}${p.sql}`);
490
+ }
491
+ }
492
+ valuesClauses.push(`(${placeholders.join(", ")})`);
493
+ }
494
+ const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES ${valuesClauses.join(", ")} RETURNING *`;
495
+ const raw = (await asRawClient(db).unsafe(sqlText, params)) as readonly Record<string, unknown>[];
496
+ return coerceRows(raw, info) as readonly TRow[];
497
+ }
498
+
499
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
500
+ export async function insertOne<TRow = any>(
501
+ db: AnyDb,
502
+ table: TableLike,
503
+ values: Record<string, unknown>,
504
+ ): Promise<TRow | undefined> {
505
+ const info = extractTableInfo(table);
506
+ const entries = Object.entries(values)
507
+ .filter(([k]) => info.hasColumn(k))
508
+ .map(([k, v]) => {
509
+ const col = info.columnOf(k);
510
+ const pgType = info.pgTypeOf(col);
511
+ return { col, prepared: prepareValue(v, pgType) };
512
+ });
513
+ if (entries.length === 0) throw new Error("insertOne: empty values object");
514
+ const cols = entries.map((e) => quoteIdent(e.col)).join(", ");
515
+ const params: unknown[] = [];
516
+ const placeholders = entries
517
+ .map((e) => {
518
+ if (e.prepared.kind === "literal") return e.prepared.literal;
519
+ params.push(e.prepared.bound);
520
+ return `$${params.length}${e.prepared.sql}`;
521
+ })
522
+ .join(", ");
523
+ const sqlText = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES (${placeholders}) RETURNING *`;
524
+ const rows = (await asRawClient(db).unsafe(sqlText, params)) as readonly Record<
525
+ string,
526
+ unknown
527
+ >[];
528
+ const first = rows[0];
529
+ if (!first) return undefined;
530
+ return coerceRow(first, info) as TRow;
531
+ }
532
+
533
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
534
+ export async function updateMany<TRow = any>(
535
+ db: AnyDb,
536
+ table: TableLike,
537
+ set: Record<string, unknown>,
538
+ where: WhereObject,
539
+ ): Promise<readonly TRow[]> {
540
+ const info = extractTableInfo(table);
541
+ const setEntries = Object.entries(set).map(([k, v]) => {
542
+ const col = info.columnOf(k);
543
+ const pgType = info.pgTypeOf(col);
544
+ return { col, prepared: prepareValue(v, pgType) };
545
+ });
546
+ if (setEntries.length === 0) throw new Error("updateMany: empty set object");
547
+ const values: unknown[] = [];
548
+ let idx = 1;
549
+ const setParts: string[] = [];
550
+ for (const e of setEntries) {
551
+ if (e.prepared.kind === "literal") {
552
+ setParts.push(`${quoteIdent(e.col)} = ${e.prepared.literal}`);
553
+ } else {
554
+ setParts.push(`${quoteIdent(e.col)} = $${idx++}${e.prepared.sql}`);
555
+ values.push(e.prepared.bound);
556
+ }
557
+ }
558
+ const w = buildWhereClause(info, where, idx);
559
+ let sqlText = `UPDATE ${quoteIdent(info.name)} SET ${setParts.join(", ")}`;
560
+ if (w.sqlText) {
561
+ sqlText += ` WHERE ${w.sqlText}`;
562
+ for (const v of w.values) values.push(v);
563
+ }
564
+ sqlText += " RETURNING *";
565
+ const raw = (await asRawClient(db).unsafe(sqlText, values)) as readonly Record<string, unknown>[];
566
+ return coerceRows(raw, info) as readonly TRow[];
567
+ }
568
+
569
+ export async function deleteMany(db: AnyDb, table: TableLike, where: WhereObject): Promise<void> {
570
+ const info = extractTableInfo(table);
571
+ const w = buildWhereClause(info, where, 1);
572
+ let sqlText = `DELETE FROM ${quoteIdent(info.name)}`;
573
+ if (w.sqlText) sqlText += ` WHERE ${w.sqlText}`;
574
+ await asRawClient(db).unsafe(sqlText, w.values);
575
+ }
576
+
577
+ type InsertEntry = {
578
+ readonly field: string;
579
+ readonly col: string;
580
+ readonly prepared: PreparedValue;
581
+ };
582
+
583
+ function insertEntries(info: TableInfo, values: Record<string, unknown>): InsertEntry[] {
584
+ return Object.entries(values)
585
+ .filter(([k]) => info.hasColumn(k))
586
+ .map(([k, v]) => {
587
+ const col = info.columnOf(k);
588
+ return { field: k, col, prepared: prepareValue(v, info.pgTypeOf(col)) };
589
+ });
590
+ }
591
+
592
+ function isEntityTableMeta(table: unknown): table is EntityTableMeta {
593
+ return (
594
+ typeof table === "object" &&
595
+ table !== null &&
596
+ "source" in table &&
597
+ (table.source === "managed" || table.source === "unmanaged") &&
598
+ "columns" in table
599
+ );
600
+ }
601
+
602
+ function resolveConflictColumns(
603
+ table: TableLike,
604
+ info: TableInfo,
605
+ conflictKeys: readonly string[] | undefined,
606
+ ): readonly string[] {
607
+ if (conflictKeys !== undefined && conflictKeys.length > 0) {
608
+ return conflictKeys.map((field) => info.columnOf(field));
609
+ }
610
+ if (isEntityTableMeta(table)) {
611
+ const pks = table.columns.filter((c) => c.primaryKey).map((c) => c.name);
612
+ if (pks.length > 0) return pks;
613
+ }
614
+ if (info.hasColumn("id")) return [info.columnOf("id")];
615
+ throw new Error("upsert: cannot infer conflict keys — pass conflictKeys explicitly");
616
+ }
617
+
618
+ function buildInsertSql(
619
+ info: TableInfo,
620
+ entries: readonly InsertEntry[],
621
+ ): { sqlPrefix: string; params: unknown[] } {
622
+ if (entries.length === 0) throw new Error("insert: empty values object");
623
+ const cols = entries.map((e) => quoteIdent(e.col)).join(", ");
624
+ const params: unknown[] = [];
625
+ const placeholders = entries
626
+ .map((e) => {
627
+ if (e.prepared.kind === "literal") return e.prepared.literal;
628
+ params.push(e.prepared.bound);
629
+ return `$${params.length}${e.prepared.sql}`;
630
+ })
631
+ .join(", ");
632
+ const sqlPrefix = `INSERT INTO ${quoteIdent(info.name)} (${cols}) VALUES (${placeholders})`;
633
+ return { sqlPrefix, params };
634
+ }
635
+
636
+ export type UpsertOnConflictOptions = {
637
+ readonly conflictKeys: readonly string[];
638
+ /** Columns to set on conflict. Default: EXCLUDED for every inserted non-key column. */
639
+ readonly update?: Record<string, unknown>;
640
+ };
641
+
642
+ export type DeleteManyBatchedOptions = {
643
+ readonly limit: number;
644
+ readonly maxBatches?: number;
645
+ /** Row id field for the LIMIT subquery. Default `id`. */
646
+ readonly idColumn?: string;
647
+ };
648
+
649
+ export type DeleteManyBatchedResult = {
650
+ readonly deleted: number;
651
+ readonly batches: number;
652
+ };
653
+
654
+ export async function countWhere(
655
+ db: AnyDb,
656
+ table: TableLike,
657
+ where: WhereObject = {},
658
+ ): Promise<number> {
659
+ const info = extractTableInfo(table);
660
+ let sqlText = `SELECT COUNT(*)::int AS count FROM ${quoteIdent(info.name)}`;
661
+ let values: unknown[] = [];
662
+ if (Object.keys(where).length > 0) {
663
+ const w = buildWhereClause(info, where, 1);
664
+ sqlText += ` WHERE ${w.sqlText}`;
665
+ values = w.values;
666
+ }
667
+ const rows = (await asRawClient(db).unsafe(sqlText, values)) as readonly { count: number }[];
668
+ return rows[0]?.count ?? 0;
669
+ }
670
+
671
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
672
+ export async function upsertOnConflict<TRow = any>(
673
+ db: AnyDb,
674
+ table: TableLike,
675
+ values: Record<string, unknown>,
676
+ options: UpsertOnConflictOptions,
677
+ ): Promise<TRow | undefined> {
678
+ const info = extractTableInfo(table);
679
+ const entries = insertEntries(info, values);
680
+ const conflictCols = resolveConflictColumns(table, info, options.conflictKeys);
681
+ const conflictSet = new Set(conflictCols);
682
+ const { sqlPrefix, params } = buildInsertSql(info, entries);
683
+
684
+ const updateParts: string[] = [];
685
+ let idx = params.length + 1;
686
+
687
+ if (options.update !== undefined) {
688
+ for (const [field, value] of Object.entries(options.update)) {
689
+ const col = info.columnOf(field);
690
+ const p = prepareValue(value, info.pgTypeOf(col));
691
+ if (p.kind === "literal") {
692
+ updateParts.push(`${quoteIdent(col)} = ${p.literal}`);
693
+ } else {
694
+ updateParts.push(`${quoteIdent(col)} = $${idx++}${p.sql}`);
695
+ params.push(p.bound);
696
+ }
697
+ }
698
+ } else {
699
+ for (const e of entries) {
700
+ if (conflictSet.has(e.col)) continue;
701
+ updateParts.push(`${quoteIdent(e.col)} = EXCLUDED.${quoteIdent(e.col)}`);
702
+ }
703
+ }
704
+
705
+ if (updateParts.length === 0) {
706
+ throw new Error("upsertOnConflict: nothing to update on conflict");
707
+ }
708
+
709
+ const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
710
+ const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
711
+ const rows = (await asRawClient(db).unsafe(sqlText, params)) as readonly Record<
712
+ string,
713
+ unknown
714
+ >[];
715
+ const first = rows[0];
716
+ if (!first) return undefined;
717
+ return coerceRow(first, info) as TRow;
718
+ }
719
+
720
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
721
+ export async function upsertByPk<TRow = any>(
722
+ db: AnyDb,
723
+ table: TableLike,
724
+ values: Record<string, unknown>,
725
+ updateOnConflict?: Record<string, unknown>,
726
+ ): Promise<TRow | undefined> {
727
+ const info = extractTableInfo(table);
728
+ const conflictKeys = resolveConflictColumns(table, info, undefined);
729
+ const fieldKeys = conflictKeys.map((col) => info.fieldOf(col));
730
+ return upsertOnConflict<TRow>(db, table, values, {
731
+ conflictKeys: fieldKeys,
732
+ ...(updateOnConflict !== undefined ? { update: updateOnConflict } : {}),
733
+ });
734
+ }
735
+
736
+ export type IncrementCounterOptions = {
737
+ readonly conflictKeys?: readonly string[];
738
+ /** Extra SET clauses on conflict (e.g. lastUpdatedAt: sql\`NOW()\`). */
739
+ readonly set?: Record<string, unknown>;
740
+ };
741
+
742
+ // biome-ignore lint/suspicious/noExplicitAny: see selectMany default
743
+ export async function incrementCounter<TRow = any>(
744
+ db: AnyDb,
745
+ table: TableLike,
746
+ values: Record<string, unknown>,
747
+ increments: Record<string, number>,
748
+ options: IncrementCounterOptions = {},
749
+ ): Promise<TRow | undefined> {
750
+ const info = extractTableInfo(table);
751
+ const entries = insertEntries(info, values);
752
+ const conflictCols = resolveConflictColumns(table, info, options.conflictKeys);
753
+ const conflictSet = new Set(conflictCols);
754
+ const { sqlPrefix, params } = buildInsertSql(info, entries);
755
+ const tableQ = quoteIdent(info.name);
756
+
757
+ const updateParts: string[] = [];
758
+ let idx = params.length + 1;
759
+
760
+ for (const [field, delta] of Object.entries(increments)) {
761
+ const col = info.columnOf(field);
762
+ updateParts.push(`${quoteIdent(col)} = ${tableQ}.${quoteIdent(col)} + $${idx++}`);
763
+ params.push(delta);
764
+ }
765
+
766
+ if (options.set !== undefined) {
767
+ for (const [field, value] of Object.entries(options.set)) {
768
+ const col = info.columnOf(field);
769
+ const p = prepareValue(value, info.pgTypeOf(col));
770
+ if (p.kind === "literal") {
771
+ updateParts.push(`${quoteIdent(col)} = ${p.literal}`);
772
+ } else {
773
+ updateParts.push(`${quoteIdent(col)} = $${idx++}${p.sql}`);
774
+ params.push(p.bound);
775
+ }
776
+ }
777
+ }
778
+
779
+ for (const e of entries) {
780
+ if (conflictSet.has(e.col)) continue;
781
+ if (Object.hasOwn(increments, e.field)) continue;
782
+ if (options.set !== undefined && Object.hasOwn(options.set, e.field)) continue;
783
+ updateParts.push(`${quoteIdent(e.col)} = EXCLUDED.${quoteIdent(e.col)}`);
784
+ }
785
+
786
+ if (updateParts.length === 0) {
787
+ throw new Error("incrementCounter: nothing to update on conflict");
788
+ }
789
+
790
+ const conflictList = conflictCols.map((c) => quoteIdent(c)).join(", ");
791
+ const sqlText = `${sqlPrefix} ON CONFLICT (${conflictList}) DO UPDATE SET ${updateParts.join(", ")} RETURNING *`;
792
+ const rows = (await asRawClient(db).unsafe(sqlText, params)) as readonly Record<
793
+ string,
794
+ unknown
795
+ >[];
796
+ const first = rows[0];
797
+ if (!first) return undefined;
798
+ return coerceRow(first, info) as TRow;
799
+ }
800
+
801
+ export async function deleteManyBatched(
802
+ db: AnyDb,
803
+ table: TableLike,
804
+ where: WhereObject,
805
+ options: DeleteManyBatchedOptions,
806
+ ): Promise<DeleteManyBatchedResult> {
807
+ const info = extractTableInfo(table);
808
+ const w = buildWhereClause(info, where, 1);
809
+ if (!w.sqlText) {
810
+ throw new Error("deleteManyBatched: where clause required — refusing unbounded batch delete");
811
+ }
812
+ const limit = options.limit;
813
+ if (!Number.isFinite(limit) || limit <= 0) {
814
+ throw new Error("deleteManyBatched: limit must be a positive number");
815
+ }
816
+ const idField = options.idColumn ?? "id";
817
+ const idCol = info.columnOf(idField);
818
+ const tableQ = quoteIdent(info.name);
819
+ const idQ = quoteIdent(idCol);
820
+ const maxBatches = options.maxBatches ?? Number.POSITIVE_INFINITY;
821
+
822
+ let deleted = 0;
823
+ let batches = 0;
824
+
825
+ while (batches < maxBatches) {
826
+ const limitIdx = w.values.length + 1;
827
+ const params = [...w.values, limit];
828
+ const sqlText = `DELETE FROM ${tableQ} WHERE ${idQ} IN (
829
+ SELECT ${idQ} FROM ${tableQ} WHERE ${w.sqlText} LIMIT $${limitIdx}
830
+ ) RETURNING ${idQ}`;
831
+ const rows = (await asRawClient(db).unsafe(sqlText, params)) as readonly unknown[];
832
+ batches++;
833
+ deleted += rows.length;
834
+ if (rows.length < limit) break;
835
+ }
836
+
837
+ return { deleted, batches };
838
+ }
839
+
840
+ export async function transaction<T>(db: AnyDb, fn: (tx: BunDbRunner) => Promise<T>): Promise<T> {
841
+ return (await asRawClient(db).begin(async (tx) => fn(tx as BunDbRunner))) as T;
842
+ }