@cosmicdrift/kumiko-framework 0.13.0 → 0.15.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 (314) hide show
  1. package/package.json +7 -7
  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/readiness.ts +2 -2
  24. package/src/auth/__tests__/roles.test.ts +2 -2
  25. package/src/bun-db/__tests__/PATTERN.md +73 -0
  26. package/src/bun-db/__tests__/_helpers.ts +103 -0
  27. package/src/bun-db/__tests__/batch-methods.integration.test.ts +143 -0
  28. package/src/bun-db/__tests__/batch-methods.test.ts +20 -0
  29. package/src/bun-db/__tests__/bun-test-db.ts +19 -0
  30. package/src/bun-db/__tests__/bun-test-stack.ts +6 -0
  31. package/src/bun-db/__tests__/column-types.integration.test.ts +132 -0
  32. package/src/bun-db/__tests__/compound-types.integration.test.ts +134 -0
  33. package/src/bun-db/__tests__/jsonb-edge-cases.integration.test.ts +235 -0
  34. package/src/bun-db/__tests__/smoke.integration.test.ts +43 -0
  35. package/src/bun-db/__tests__/sql-methods.integration.test.ts +231 -0
  36. package/src/bun-db/__tests__/where-patterns.integration.test.ts +185 -0
  37. package/src/bun-db/connection.ts +84 -0
  38. package/src/bun-db/index.ts +31 -0
  39. package/src/bun-db/query.ts +845 -0
  40. package/src/compliance/__tests__/duration-spec.test.ts +1 -1
  41. package/src/compliance/__tests__/profiles.test.ts +1 -1
  42. package/src/compliance/__tests__/sub-processors.test.ts +1 -1
  43. package/src/db/__tests__/{apply-entity-event-tenant.integration.ts → apply-entity-event-tenant.integration.test.ts} +13 -11
  44. package/src/db/__tests__/big-int-field.test.ts +15 -14
  45. package/src/db/__tests__/column-ddl.integration.test.ts +113 -0
  46. package/src/db/__tests__/compound-types.test.ts +1 -1
  47. package/src/db/__tests__/{config-seed.integration.ts → config-seed.integration.test.ts} +32 -27
  48. package/src/db/__tests__/connection-options.test.ts +1 -1
  49. package/src/db/__tests__/dialect-instant.test.ts +1 -1
  50. package/src/db/__tests__/encryption.test.ts +1 -1
  51. package/src/db/__tests__/{drizzle-table-types.test.ts → entity-table-types.test.ts} +16 -16
  52. package/src/db/__tests__/{event-store-executor-list.integration.ts → event-store-executor-list.integration.test.ts} +12 -7
  53. package/src/db/__tests__/{event-store-executor.integration.ts → event-store-executor.integration.test.ts} +19 -12
  54. package/src/db/__tests__/{implicit-projection-equivalence.integration.ts → implicit-projection-equivalence.integration.test.ts} +35 -29
  55. package/src/db/__tests__/located-timestamp.test.ts +1 -1
  56. package/src/db/__tests__/money.test.ts +1 -1
  57. package/src/db/__tests__/{multi-row-insert.integration.ts → multi-row-insert.integration.test.ts} +18 -11
  58. package/src/db/__tests__/parse-auto-verb.test.ts +1 -1
  59. package/src/db/__tests__/{required-not-null-migration-safety.integration.ts → required-not-null-migration-safety.integration.test.ts} +28 -24
  60. package/src/db/__tests__/{schema-migration.integration.ts → schema-migration.integration.test.ts} +32 -28
  61. package/src/db/__tests__/sql-inventory.test.ts +56 -0
  62. package/src/db/__tests__/table-builder-indexes.test.ts +30 -11
  63. package/src/db/__tests__/table-builder-required.test.ts +20 -22
  64. package/src/db/__tests__/{tenant-db.integration.ts → tenant-db.integration.test.ts} +106 -144
  65. package/src/db/__tests__/{unique-violation-mapping.integration.ts → unique-violation-mapping.integration.test.ts} +13 -8
  66. package/src/db/api.ts +46 -0
  67. package/src/db/apply-entity-event.ts +45 -36
  68. package/src/db/assert-exists-in.ts +5 -16
  69. package/src/db/bun-provider.ts +37 -0
  70. package/src/db/config-seed.ts +4 -4
  71. package/src/db/connection.ts +14 -57
  72. package/src/db/cursor.ts +5 -56
  73. package/src/db/dialect.ts +472 -99
  74. package/src/db/eagerload.ts +5 -12
  75. package/src/db/entity-table-meta.ts +390 -0
  76. package/src/db/event-store-executor.ts +158 -100
  77. package/src/db/index.ts +33 -5
  78. package/src/db/migrate-generator.ts +350 -0
  79. package/src/db/migrate-runner.ts +206 -0
  80. package/src/db/postgres-provider.ts +25 -0
  81. package/src/db/queries/entity-read.ts +15 -0
  82. package/src/db/queries/es-ops.ts +17 -0
  83. package/src/db/queries/event-consumer.ts +170 -0
  84. package/src/db/queries/event-store-admin.ts +127 -0
  85. package/src/db/queries/event-store.ts +155 -0
  86. package/src/db/queries/projection-rebuild.ts +59 -0
  87. package/src/db/queries/raw-sql.ts +15 -0
  88. package/src/db/queries/schema-drift.ts +35 -0
  89. package/src/db/queries/seed-context.ts +58 -0
  90. package/src/db/queries/table-ops.ts +11 -0
  91. package/src/db/queries/test-stack.ts +56 -0
  92. package/src/db/query-api.ts +22 -0
  93. package/src/db/query.ts +30 -0
  94. package/src/db/reference-data.ts +19 -22
  95. package/src/db/render-ddl.ts +57 -0
  96. package/src/db/row-helpers.ts +3 -52
  97. package/src/db/schema-inspection.ts +17 -4
  98. package/src/db/sql-inventory.ts +208 -0
  99. package/src/db/table-builder.ts +48 -40
  100. package/src/db/tenant-db.ts +105 -326
  101. package/src/engine/__tests__/auth-claims-registrar.test.ts +1 -1
  102. package/src/engine/__tests__/boot-validator-api-exposure.test.ts +3 -3
  103. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +1 -1
  104. package/src/engine/__tests__/boot-validator-pii-retention.test.ts +5 -5
  105. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +3 -3
  106. package/src/engine/__tests__/boot-validator.test.ts +4 -3
  107. package/src/engine/__tests__/build-app-schema.test.ts +1 -1
  108. package/src/engine/__tests__/build-target.test.ts +1 -1
  109. package/src/engine/__tests__/claim-keys.test.ts +1 -1
  110. package/src/engine/__tests__/codemod-pipeline.test.ts +3 -3
  111. package/src/engine/__tests__/config-helpers.test.ts +1 -1
  112. package/src/engine/__tests__/effective-features.test.ts +1 -1
  113. package/src/engine/__tests__/engine.test.ts +1 -1
  114. package/src/engine/__tests__/entity-handlers.test.ts +3 -3
  115. package/src/engine/__tests__/event-helpers.test.ts +3 -3
  116. package/src/engine/__tests__/extends-registrar.test.ts +4 -4
  117. package/src/engine/__tests__/factories-long-text.test.ts +1 -1
  118. package/src/engine/__tests__/factories-time.test.ts +1 -1
  119. package/src/engine/__tests__/field-predicates.test.ts +1 -1
  120. package/src/engine/__tests__/hook-phases.test.ts +1 -1
  121. package/src/engine/__tests__/identifiers.test.ts +1 -1
  122. package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
  123. package/src/engine/__tests__/nav.test.ts +1 -1
  124. package/src/engine/__tests__/ownership.test.ts +10 -11
  125. package/src/engine/__tests__/parse-ref-target.test.ts +1 -1
  126. package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
  127. package/src/engine/__tests__/{pipeline-handler.integration.ts → pipeline-handler.integration.test.ts} +38 -52
  128. package/src/engine/__tests__/{pipeline-observability.integration.ts → pipeline-observability.integration.test.ts} +1 -1
  129. package/src/engine/__tests__/{pipeline-performance.integration.ts → pipeline-performance.integration.test.ts} +1 -1
  130. package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
  131. package/src/engine/__tests__/post-query-hook.test.ts +1 -1
  132. package/src/engine/__tests__/projection-helpers.test.ts +25 -17
  133. package/src/engine/__tests__/projection.test.ts +4 -4
  134. package/src/engine/__tests__/qualified-name.test.ts +1 -1
  135. package/src/engine/__tests__/raw-table.test.ts +9 -8
  136. package/src/engine/__tests__/resolve-config-or-param.test.ts +5 -5
  137. package/src/engine/__tests__/run-in.test.ts +1 -1
  138. package/src/engine/__tests__/schema-builder.test.ts +1 -1
  139. package/src/engine/__tests__/screen.test.ts +1 -1
  140. package/src/engine/__tests__/search-payload-extension.test.ts +3 -3
  141. package/src/engine/__tests__/state-machine.test.ts +1 -1
  142. package/src/engine/__tests__/steps-aggregate-append-event.test.ts +7 -7
  143. package/src/engine/__tests__/steps-aggregate-create.test.ts +4 -4
  144. package/src/engine/__tests__/steps-aggregate-update.test.ts +3 -3
  145. package/src/engine/__tests__/steps-call-feature.test.ts +5 -5
  146. package/src/engine/__tests__/steps-mail-send.test.ts +7 -7
  147. package/src/engine/__tests__/steps-read.test.ts +34 -40
  148. package/src/engine/__tests__/steps-resolver-utils.test.ts +6 -6
  149. package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +24 -19
  150. package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +28 -17
  151. package/src/engine/__tests__/steps-webhook-send.test.ts +6 -6
  152. package/src/engine/__tests__/steps-workflow.test.ts +7 -7
  153. package/src/engine/__tests__/system-user.test.ts +1 -1
  154. package/src/engine/__tests__/validate-projection-allowlist.test.ts +4 -5
  155. package/src/engine/__tests__/validation-hooks.test.ts +1 -1
  156. package/src/engine/__tests__/visual-tree-patterns.test.ts +1 -1
  157. package/src/engine/boot-validator/entity-handler.ts +3 -3
  158. package/src/engine/boot-validator/ownership.ts +1 -1
  159. package/src/engine/define-feature.ts +1 -2
  160. package/src/engine/entity-handlers.ts +5 -5
  161. package/src/engine/factories.ts +1 -1
  162. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +1 -1
  163. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -1
  164. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +2 -2
  165. package/src/engine/feature-ast/__tests__/parse.test.ts +1 -1
  166. package/src/engine/feature-ast/__tests__/patch.test.ts +1 -1
  167. package/src/engine/feature-ast/__tests__/patcher.test.ts +1 -1
  168. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +1 -1
  169. package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +1 -1
  170. package/src/engine/ownership.ts +113 -41
  171. package/src/engine/pattern-library/__tests__/library.test.ts +2 -2
  172. package/src/engine/projection-helpers.ts +2 -11
  173. package/src/engine/registry.ts +2 -2
  174. package/src/engine/steps/read-find-many.ts +13 -13
  175. package/src/engine/steps/read-find-one.ts +7 -9
  176. package/src/engine/steps/unsafe-projection-delete.ts +4 -5
  177. package/src/engine/steps/unsafe-projection-upsert.ts +63 -31
  178. package/src/engine/types/feature.ts +7 -2
  179. package/src/engine/types/fields.ts +4 -5
  180. package/src/engine/types/step.ts +10 -10
  181. package/src/engine/validate-projection-allowlist.ts +23 -3
  182. package/src/entrypoint/__tests__/{entrypoint-job-wiring.integration.ts → entrypoint-job-wiring.integration.test.ts} +4 -3
  183. package/src/entrypoint/__tests__/{split-deploy.integration.ts → split-deploy.integration.test.ts} +4 -3
  184. package/src/env/__tests__/compose-env-schema.test.ts +1 -1
  185. package/src/env/__tests__/dry-run.test.ts +1 -1
  186. package/src/errors/__tests__/classes.test.ts +1 -1
  187. package/src/errors/__tests__/write-failures.test.ts +1 -1
  188. package/src/es-ops/__tests__/{context.integration.ts → context.integration.test.ts} +43 -29
  189. package/src/es-ops/__tests__/{runner.integration.ts → runner.integration.test.ts} +25 -23
  190. package/src/es-ops/__tests__/runner.test.ts +29 -19
  191. package/src/es-ops/context.ts +9 -43
  192. package/src/es-ops/operations-schema.ts +2 -2
  193. package/src/es-ops/runner.ts +12 -26
  194. package/src/event-store/__tests__/{admin-api.integration.ts → admin-api.integration.test.ts} +71 -45
  195. package/src/event-store/__tests__/{event-store.integration.ts → event-store.integration.test.ts} +7 -5
  196. package/src/event-store/__tests__/{get-stream-version-perf.integration.ts → get-stream-version-perf.integration.test.ts} +5 -3
  197. package/src/event-store/__tests__/{perf.integration.ts → perf.integration.test.ts} +24 -16
  198. package/src/event-store/__tests__/{snapshot.integration.ts → snapshot.integration.test.ts} +34 -28
  199. package/src/event-store/__tests__/{upcaster-dead-letter.integration.ts → upcaster-dead-letter.integration.test.ts} +11 -12
  200. package/src/event-store/__tests__/{upcaster.integration.ts → upcaster.integration.test.ts} +19 -32
  201. package/src/event-store/admin-api.ts +55 -83
  202. package/src/event-store/archive.ts +15 -39
  203. package/src/event-store/event-store.ts +92 -86
  204. package/src/event-store/events-schema.ts +2 -1
  205. package/src/event-store/index.ts +1 -0
  206. package/src/event-store/snapshot.ts +26 -24
  207. package/src/event-store/upcaster-dead-letter.ts +19 -18
  208. package/src/files/__tests__/content-disposition.test.ts +1 -1
  209. package/src/files/__tests__/{file-field-pipeline.integration.ts → file-field-pipeline.integration.test.ts} +8 -5
  210. package/src/files/__tests__/file-handle.test.ts +1 -1
  211. package/src/files/__tests__/{files.integration.ts → files.integration.test.ts} +32 -17
  212. package/src/files/__tests__/read-stream.test.ts +1 -1
  213. package/src/files/__tests__/{storage-tracking.integration.ts → storage-tracking.integration.test.ts} +26 -30
  214. package/src/files/__tests__/write-stream.test.ts +1 -1
  215. package/src/files/__tests__/zip-stream.test.ts +1 -1
  216. package/src/files/file-ref-table.ts +2 -2
  217. package/src/files/file-routes.ts +7 -9
  218. package/src/files/storage-tracking.ts +9 -17
  219. package/src/i18n/__tests__/i18n.test.ts +1 -1
  220. package/src/jobs/__tests__/{job-event-trigger.integration.ts → job-event-trigger.integration.test.ts} +6 -3
  221. package/src/jobs/__tests__/{job-multi-trigger.integration.ts → job-multi-trigger.integration.test.ts} +6 -3
  222. package/src/jobs/__tests__/{jobs.integration.ts → jobs.integration.test.ts} +5 -7
  223. package/src/lifecycle/__tests__/{lifecycle-server.integration.ts → lifecycle-server.integration.test.ts} +1 -1
  224. package/src/lifecycle/__tests__/lifecycle.test.ts +6 -6
  225. package/src/lifecycle/__tests__/signal-handlers.test.ts +6 -6
  226. package/src/logging/__tests__/pino-trace-bridge.test.ts +1 -1
  227. package/src/migrations/__tests__/compare-snapshots.test.ts +1 -1
  228. package/src/migrations/__tests__/{detect-drift.integration.ts → detect-drift.integration.test.ts} +34 -26
  229. package/src/migrations/__tests__/{detect-projections-to-rebuild.integration.ts → detect-projections-to-rebuild.integration.test.ts} +1 -1
  230. package/src/migrations/__tests__/rebuild-marker.test.ts +1 -1
  231. package/src/migrations/projection-detection.ts +12 -1
  232. package/src/migrations/schema-drift.ts +7 -23
  233. package/src/observability/__tests__/console-provider.test.ts +1 -1
  234. package/src/observability/__tests__/metric-validator.test.ts +1 -1
  235. package/src/observability/__tests__/noop-provider.test.ts +1 -1
  236. package/src/observability/__tests__/{observability.integration.ts → observability.integration.test.ts} +5 -8
  237. package/src/observability/__tests__/prometheus-meter.test.ts +1 -1
  238. package/src/observability/__tests__/recording-meter.test.ts +1 -1
  239. package/src/observability/__tests__/recording-tracer.test.ts +1 -1
  240. package/src/observability/__tests__/sensitive-filter.test.ts +1 -1
  241. package/src/pipeline/__tests__/{archive-stream.integration.ts → archive-stream.integration.test.ts} +3 -3
  242. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +9 -9
  243. package/src/pipeline/__tests__/{cascade-handler.integration.ts → cascade-handler.integration.test.ts} +18 -15
  244. package/src/pipeline/__tests__/cascade-handler.test.ts +1 -1
  245. package/src/pipeline/__tests__/{causation-chain.integration.ts → causation-chain.integration.test.ts} +12 -13
  246. package/src/pipeline/__tests__/{ctx-bridge.integration.ts → ctx-bridge.integration.test.ts} +12 -11
  247. package/src/pipeline/__tests__/dispatcher.test.ts +2 -2
  248. package/src/pipeline/__tests__/{distributed-lock.integration.ts → distributed-lock.integration.test.ts} +1 -1
  249. package/src/pipeline/__tests__/{domain-events-projections.integration.ts → domain-events-projections.integration.test.ts} +13 -15
  250. package/src/pipeline/__tests__/{event-dedup.integration.ts → event-dedup.integration.test.ts} +1 -1
  251. package/src/pipeline/__tests__/{event-define-event-strict.integration.ts → event-define-event-strict.integration.test.ts} +6 -16
  252. package/src/pipeline/__tests__/{event-dispatcher-lifecycle.integration.ts → event-dispatcher-lifecycle.integration.test.ts} +1 -1
  253. package/src/pipeline/__tests__/{event-dispatcher-multi-instance.integration.ts → event-dispatcher-multi-instance.integration.test.ts} +3 -2
  254. package/src/pipeline/__tests__/{event-dispatcher-pg-listen.integration.ts → event-dispatcher-pg-listen.integration.test.ts} +1 -1
  255. package/src/pipeline/__tests__/{event-dispatcher-recovery.integration.ts → event-dispatcher-recovery.integration.test.ts} +2 -2
  256. package/src/pipeline/__tests__/{event-dispatcher-second-audit.integration.ts → event-dispatcher-second-audit.integration.test.ts} +17 -16
  257. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +14 -12
  258. package/src/pipeline/__tests__/{event-dispatcher.integration.ts → event-dispatcher.integration.test.ts} +8 -15
  259. package/src/pipeline/__tests__/{event-retention.integration.ts → event-retention.integration.test.ts} +28 -25
  260. package/src/pipeline/__tests__/{fetch-for-writing.integration.ts → fetch-for-writing.integration.test.ts} +6 -6
  261. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +4 -4
  262. package/src/pipeline/__tests__/{load-aggregate-query.integration.ts → load-aggregate-query.integration.test.ts} +9 -5
  263. package/src/pipeline/__tests__/{msp-error-mode.integration.ts → msp-error-mode.integration.test.ts} +1 -1
  264. package/src/pipeline/__tests__/{msp-multi-hop.integration.ts → msp-multi-hop.integration.test.ts} +9 -8
  265. package/src/pipeline/__tests__/{msp-rebuild.integration.ts → msp-rebuild.integration.test.ts} +47 -55
  266. package/src/pipeline/__tests__/{multi-stream-projection.integration.ts → multi-stream-projection.integration.test.ts} +19 -53
  267. package/src/pipeline/__tests__/{perf-rebuild.integration.ts → perf-rebuild.integration.test.ts} +36 -34
  268. package/src/pipeline/__tests__/{post-query-hook.integration.ts → post-query-hook.integration.test.ts} +1 -1
  269. package/src/pipeline/__tests__/{projection-rebuild.integration.ts → projection-rebuild.integration.test.ts} +21 -30
  270. package/src/pipeline/__tests__/{query-projection.integration.ts → query-projection.integration.test.ts} +6 -5
  271. package/src/pipeline/__tests__/{redis-pipeline.integration.ts → redis-pipeline.integration.test.ts} +3 -1
  272. package/src/pipeline/cascade-handler.ts +13 -21
  273. package/src/pipeline/dispatcher.ts +43 -48
  274. package/src/pipeline/event-consumer-state.ts +11 -2
  275. package/src/pipeline/event-dispatcher.ts +86 -146
  276. package/src/pipeline/event-retention.ts +14 -24
  277. package/src/pipeline/msp-rebuild.ts +54 -78
  278. package/src/pipeline/projection-rebuild.ts +65 -67
  279. package/src/pipeline/projection-state.ts +2 -2
  280. package/src/random/__tests__/generate.test.ts +13 -13
  281. package/src/rate-limit/__tests__/{dispatcher-l3.integration.ts → dispatcher-l3.integration.test.ts} +1 -1
  282. package/src/rate-limit/__tests__/{middleware.integration.ts → middleware.integration.test.ts} +1 -1
  283. package/src/rate-limit/__tests__/{resolver.integration.ts → resolver.integration.test.ts} +1 -1
  284. package/src/redis/__tests__/redis-options.test.ts +1 -1
  285. package/src/search/__tests__/{meilisearch-adapter.integration.ts → meilisearch-adapter.integration.test.ts} +1 -1
  286. package/src/search/__tests__/search-adapter.test.ts +1 -1
  287. package/src/secrets/__tests__/dek-cache.test.ts +1 -3
  288. package/src/secrets/__tests__/env-master-key-provider.test.ts +1 -1
  289. package/src/secrets/__tests__/envelope.test.ts +1 -1
  290. package/src/secrets/__tests__/leak-guard.test.ts +1 -1
  291. package/src/secrets/__tests__/rotation.test.ts +1 -1
  292. package/src/stack/db.ts +25 -48
  293. package/src/stack/push-entity-projection-tables.ts +2 -4
  294. package/src/stack/table-helpers.ts +98 -61
  295. package/src/stack/test-stack.ts +8 -7
  296. package/src/testing/__tests__/db-cleanup.test.ts +40 -0
  297. package/src/testing/__tests__/e2e-generator.test.ts +1 -1
  298. package/src/testing/__tests__/{ensure-entity-table.integration.ts → ensure-entity-table.integration.test.ts} +7 -14
  299. package/src/testing/db-cleanup.ts +44 -0
  300. package/src/testing/expect-error.ts +1 -1
  301. package/src/testing/index.ts +2 -0
  302. package/src/testing/multipart-helper.ts +94 -0
  303. package/src/testing/shared-entities.ts +5 -5
  304. package/src/time/__tests__/polyfill.test.ts +1 -1
  305. package/src/time/__tests__/tz-context.test.ts +1 -1
  306. package/src/utils/__tests__/assert.test.ts +1 -1
  307. package/src/utils/__tests__/env-parse.test.ts +1 -1
  308. package/CHANGELOG.md +0 -472
  309. package/src/db/__tests__/cursor.test.ts +0 -41
  310. package/src/db/__tests__/db-helpers.test.ts +0 -369
  311. package/src/db/__tests__/drizzle-helpers.integration.ts +0 -186
  312. package/src/db/__tests__/row-helpers.test.ts +0 -59
  313. package/src/engine/steps/_drizzle-boundary.ts +0 -19
  314. package/src/files/__tests__/file-field-column.integration.ts +0 -103
@@ -12,14 +12,22 @@
12
12
  // { from: "claim:<featureQn>",
13
13
  // column?: "..." } → row[column ?? claim.shortName] === user.claims[claim.qn]
14
14
  // (string[] claim → inArray)
15
- // { where: (user, table) => SQL } → escape hatch, arbitrary Drizzle predicate
15
+ // { where: (user, ctx) => SqlFragment } → escape hatch, raw parameterised SQL
16
16
  //
17
17
  // Construction: use the `from(ref, column?)` helper. It returns a FromRule
18
18
  // ready to drop into an access map.
19
19
 
20
- import { eq, inArray, or, type SQL, sql } from "drizzle-orm";
20
+ import { toSnakeCase } from "../db/table-builder";
21
21
  import type { SessionUser } from "./types";
22
22
 
23
+ // Parameterised SQL fragment — produced by buildOwnershipClause + by the
24
+ // WhereRule escape-hatch. Caller weaves `sqlText` into a larger statement,
25
+ // renumbering placeholders if needed (FragmentBuilder below).
26
+ export type SqlFragment = {
27
+ readonly sqlText: string;
28
+ readonly params: readonly unknown[];
29
+ };
30
+
23
31
  // Reference spec supported by `from()`:
24
32
  // "user:id" → user.id
25
33
  // "user:tenantId" → user.tenantId (rarely needed — TenantDb scopes anyway)
@@ -49,9 +57,18 @@ export type FromRule = {
49
57
  readonly column: string;
50
58
  };
51
59
 
60
+ // Context passed to a WhereRule escape-hatch. The author returns a SqlFragment
61
+ // whose placeholders start at `paramStart` ($N, $N+1, ...); the framework
62
+ // concatenates the fragment into the larger query.
63
+ export type WhereRuleContext<TTable = unknown> = {
64
+ readonly table: TTable;
65
+ readonly tableName: string;
66
+ readonly paramStart: number;
67
+ };
68
+
52
69
  export type WhereRule<TTable = unknown> = {
53
70
  readonly kind: "where";
54
- readonly where: (user: SessionUser, table: TTable) => SQL;
71
+ readonly where: (user: SessionUser, ctx: WhereRuleContext<TTable>) => SqlFragment;
55
72
  };
56
73
 
57
74
  // "all" collapses to a primitive so map authors can write `Admin: "all"`
@@ -244,51 +261,94 @@ export function userCanCreateFieldRow(
244
261
  // "empty" → user has a role mapped but no rule accepts any row (missing
245
262
  // claim, empty array, role not in map). Skip the DB call entirely
246
263
  // — returning [] is equivalent and avoids a pointless roundtrip.
247
- // "sql" → apply `.sql` as an AND to the query's where clause.
264
+ // "sql" → apply the parameterised fragment as an AND on the query.
265
+ // Caller is responsible for renumbering placeholders when
266
+ // concatenating with other fragments (see `shiftParams` below).
248
267
  //
249
268
  // "empty" vs. "pass" is the critical distinction for a safe default:
250
- // undefined/pass = allow, empty = deny-by-construction. Mixing them up was
251
- // the exact leak direction advisor flagged; the disjoint type prevents it.
269
+ // undefined/pass = allow, empty = deny-by-construction.
252
270
  export type OwnershipClause =
253
271
  | { readonly kind: "pass" }
254
272
  | { readonly kind: "empty" }
255
- | { readonly kind: "sql"; readonly sql: SQL };
273
+ | { readonly kind: "sql"; readonly sqlText: string; readonly params: readonly unknown[] };
256
274
 
257
275
  const PASS_CLAUSE: OwnershipClause = { kind: "pass" };
258
276
  const EMPTY_CLAUSE: OwnershipClause = { kind: "empty" };
259
277
 
260
- // Build an ownership clause for entity-level READ access. The caller
261
- // translates the result to its query layer (see above).
278
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
279
+ const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
280
+
281
+ function tableNameOf(table: unknown): string {
282
+ if (table !== null && typeof table === "object") {
283
+ const sym = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
284
+ if (typeof sym === "string") return sym;
285
+ }
286
+ return "<unknown>";
287
+ }
288
+
289
+ // Resolve a JS-field name on the table to its underlying SQL column name.
290
+ // Drizzle tables carry the mapping under Symbol.for("kumiko:schema:Columns");
291
+ // we read it without importing drizzle-orm at runtime.
292
+ function columnSqlName(table: unknown, field: string): string | null {
293
+ if (table === null || typeof table !== "object") return null;
294
+ const cols = (table as Record<symbol, unknown>)[KUMIKO_COLUMNS_SYMBOL];
295
+ if (cols && typeof cols === "object") {
296
+ const col = (cols as Record<string, unknown>)[field];
297
+ if (col && typeof col === "object") {
298
+ const nameVal = (col as Record<string, unknown>)["name"];
299
+ if (typeof nameVal === "string") return nameVal;
300
+ }
301
+ }
302
+ // Field may already be the SQL column name on plain objects (tests, etc.).
303
+ if ((table as Record<string, unknown>)[field] !== undefined) {
304
+ return toSnakeCase(field);
305
+ }
306
+ return null;
307
+ }
308
+
309
+ function quoteIdent(name: string): string {
310
+ return `"${name.replace(/"/g, '""')}"`;
311
+ }
312
+
313
+ // Shift `$N` placeholder numbers in an embedded fragment so they line up
314
+ // with the outer query's param array.
315
+ export function shiftParams(fragment: SqlFragment, shift: number): SqlFragment {
316
+ if (shift === 0) return fragment;
317
+ const sqlText = fragment.sqlText.replace(/\$(\d+)/g, (_, num) => `$${Number(num) + shift}`);
318
+ return { sqlText, params: fragment.params };
319
+ }
320
+
321
+ // Build an ownership clause for entity-level READ access. Caller weaves
322
+ // the result into a raw-SQL WHERE (see event-store-executor list/getById).
262
323
  //
263
- // `table` is the Drizzle table with column objects. Unknown column on a
264
- // from-rule is a boot-time misconfiguration; at request time we treat it
265
- // as empty (safe default) rather than passing silently.
324
+ // `table` is the (drizzle or compatible) table object; we extract column
325
+ // SQL names via the kumiko:schema:Columns symbol. Unknown column on a from-rule
326
+ // is a boot-time misconfiguration; at request time we treat it as empty
327
+ // (safe default) rather than passing silently.
266
328
  export function buildOwnershipClause(
267
329
  user: SessionUser,
268
330
  accessMap: OwnershipMap | undefined,
269
- // biome-ignore lint/suspicious/noExplicitAny: Drizzle tables carry schema-dependent column shapes
270
- table: any,
331
+ table: unknown,
332
+ paramStart = 1,
271
333
  ): OwnershipClause {
272
334
  if (!accessMap || Object.keys(accessMap).length === 0) return PASS_CLAUSE;
273
335
 
274
- const clauses: SQL[] = [];
336
+ const clauses: SqlFragment[] = [];
275
337
  let anyRoleMatched = false;
276
338
  let everyRuleCollapsedToEmpty = true;
339
+ let nextParamIdx = paramStart;
277
340
 
278
341
  for (const role of user.roles) {
279
342
  const rule = accessMap[role];
280
343
  if (!rule) continue;
281
344
  anyRoleMatched = true;
282
- // "all" = no filter at all for this role; short-circuit.
283
345
  if (rule === "all") return PASS_CLAUSE;
284
- const resolved = ruleToClause(rule, user, table);
346
+ const resolved = ruleToFragment(rule, user, table, nextParamIdx);
285
347
  if (resolved.kind === "sql") {
286
- clauses.push(resolved.sql);
348
+ clauses.push({ sqlText: resolved.sqlText, params: resolved.params });
349
+ nextParamIdx += resolved.params.length;
287
350
  everyRuleCollapsedToEmpty = false;
288
351
  }
289
- // "empty" contribution from one role doesn't short-circuit: another
290
- // role might still contribute an OR-branch. But if ALL branches are
291
- // empty, the result is empty.
292
352
  }
293
353
 
294
354
  if (!anyRoleMatched) return EMPTY_CLAUSE;
@@ -296,42 +356,54 @@ export function buildOwnershipClause(
296
356
  if (clauses.length === 1) {
297
357
  const only = clauses[0];
298
358
  if (!only) return EMPTY_CLAUSE;
299
- return { kind: "sql", sql: only };
359
+ return { kind: "sql", sqlText: `(${only.sqlText})`, params: only.params };
300
360
  }
301
- // @cast-boundary db-operator drizzle or() widened signature
302
- // biome-ignore lint/suspicious/noExplicitAny: same reason as above
303
- const combined = or(...(clauses as any)) as SQL;
304
- return { kind: "sql", sql: combined };
361
+ const sqlText = clauses.map((c) => `(${c.sqlText})`).join(" OR ");
362
+ const params: unknown[] = [];
363
+ for (const c of clauses) for (const p of c.params) params.push(p);
364
+ return { kind: "sql", sqlText: `(${sqlText})`, params };
305
365
  }
306
366
 
307
- type RuleClauseResult = { readonly kind: "empty" } | { readonly kind: "sql"; readonly sql: SQL };
367
+ type RuleFragmentResult =
368
+ | { readonly kind: "empty" }
369
+ | { readonly kind: "sql"; readonly sqlText: string; readonly params: readonly unknown[] };
308
370
 
309
- function ruleToClause(
371
+ function ruleToFragment(
310
372
  rule: OwnershipRule,
311
373
  user: SessionUser,
312
- // biome-ignore lint/suspicious/noExplicitAny: Drizzle tables carry schema-dependent column shapes
313
- table: any,
314
- ): RuleClauseResult {
374
+ table: unknown,
375
+ paramStart: number,
376
+ ): RuleFragmentResult {
315
377
  if (rule === "all") {
316
- // Caller handles "all" by short-circuit before reaching here; defensive
317
- // fallback.
318
- return { kind: "sql", sql: sql`true` };
378
+ return { kind: "sql", sqlText: "TRUE", params: [] };
319
379
  }
320
380
  if (rule.kind === "where") {
321
- return { kind: "sql", sql: rule.where(user, table) };
381
+ const frag = rule.where(user, {
382
+ table,
383
+ tableName: tableNameOf(table),
384
+ paramStart,
385
+ });
386
+ return { kind: "sql", sqlText: frag.sqlText, params: frag.params };
322
387
  }
323
388
  // FromRule
324
- const column = table[rule.column];
325
- // Unknown column boot validator should have caught this, but at request
326
- // time we treat as empty (fail-closed).
327
- if (!column) return { kind: "empty" };
389
+ const colName = columnSqlName(table, rule.column);
390
+ if (!colName) return { kind: "empty" };
328
391
 
329
392
  const value = resolveUserValue(rule, user);
330
393
  if (value === undefined || value === null) return { kind: "empty" };
331
394
 
332
395
  if (Array.isArray(value)) {
333
396
  if (value.length === 0) return { kind: "empty" };
334
- return { kind: "sql", sql: inArray(column, value) };
397
+ const placeholders = value.map((_, i) => `$${paramStart + i}`).join(", ");
398
+ return {
399
+ kind: "sql",
400
+ sqlText: `${quoteIdent(colName)} IN (${placeholders})`,
401
+ params: value,
402
+ };
335
403
  }
336
- return { kind: "sql", sql: eq(column, value) };
404
+ return {
405
+ kind: "sql",
406
+ sqlText: `${quoteIdent(colName)} = $${paramStart}`,
407
+ params: [value],
408
+ };
337
409
  }
@@ -7,7 +7,7 @@
7
7
  // sample pattern (sanity check that entity-fields-editor points at
8
8
  // `definition.fields`, not `fields`).
9
9
 
10
- import { describe, expect, expectTypeOf, test } from "vitest";
10
+ import { describe, expect, expectTypeOf, test } from "bun:test";
11
11
  import {
12
12
  type FeaturePattern,
13
13
  type FeaturePatternKind,
@@ -24,7 +24,7 @@ import {
24
24
  // All FeaturePatternKind discriminator values, hand-listed so the test
25
25
  // fails CI when a new pattern is added without a library entry. Match
26
26
  // against the FeaturePattern union via type-test below.
27
- const ALL_KINDS: readonly FeaturePatternKind[] = [
27
+ const ALL_KINDS: FeaturePatternKind[] = [
28
28
  "requires",
29
29
  "optionalRequires",
30
30
  "readsConfig",
@@ -1,5 +1,5 @@
1
- import { eq } from "drizzle-orm";
2
1
  import type { DbRunner } from "../db/connection";
2
+ import { updateMany } from "../db/query";
3
3
  import type { StoredEvent } from "../event-store/event-store";
4
4
  import type { MultiStreamApplyContext } from "../pipeline/multi-stream-apply-context";
5
5
  import type { MultiStreamApplyFn, ProjectionTable, SingleStreamApplyFn } from "./types/projection";
@@ -71,15 +71,6 @@ export function setFields(
71
71
  }
72
72
  return async (event, tx) => {
73
73
  const values = typeof fields === "function" ? fields(event) : fields;
74
- // ProjectionTable erases its column shape on purpose (the framework
75
- // does not know user table shapes). Drizzle's tx.update().set() is
76
- // strict about the concrete row, so we feed it the erased value; the
77
- // type-safety guarantee for `values` lives at the setFields call-site.
78
- // biome-ignore lint/suspicious/noExplicitAny: see note above.
79
- const set = values as any; // @cast-boundary engine-bridge
80
- await tx
81
- .update(table)
82
- .set(set)
83
- .where(eq(idCol as never, event.aggregateId)); // @cast-boundary db-operator
74
+ await updateMany(tx, table, values, { id: event.aggregateId });
84
75
  };
85
76
  }
@@ -1,5 +1,5 @@
1
1
  import { applyEntityEvent } from "../db/apply-entity-event";
2
- import { buildDrizzleTable } from "../db/table-builder";
2
+ import { buildEntityTable } from "../db/table-builder";
3
3
  import { buildMetricName, validateMetricName } from "../observability";
4
4
  import { type QnType, qualifyEntityName } from "./qualified-name";
5
5
  import type {
@@ -67,7 +67,7 @@ function buildImplicitProjection(
67
67
  qualify: typeof qualifyEntityName,
68
68
  ): ProjectionDefinition {
69
69
  const name = qualify(featureName, "projection", `${entityName}${IMPLICIT_PROJECTION_SUFFIX}`);
70
- const drizzleTable = buildDrizzleTable(entityName, entity);
70
+ const drizzleTable = buildEntityTable(entityName, entity);
71
71
  // applyEntityEvent gibt ApplyResult zurück; SingleStreamApplyFn erwartet
72
72
  // Promise<void>. Im rebuild-Pfad ist die Row irrelevant — wir discarden.
73
73
  const handler = async (
@@ -1,24 +1,22 @@
1
1
  // r.step.read.findMany — load multiple rows from a projection table.
2
2
  //
3
- // Sibling to read.findOne — same tenant-filter caveat (caller-owned),
4
- // same drizzle-boundary cast. Resolves to a row-array (possibly empty),
5
- // landed under steps.<name>.
3
+ // Sibling to read.findOne — same tenant-filter caveat (caller-owned).
4
+ // Resolves to a row-array (possibly empty), landed under steps.<name>.
6
5
  //
7
6
  // Optional `limit` — defaults to no-limit (caller-chosen, NOT a
8
7
  // guard-rail). Most legitimate uses iterate via r.step.forEach (M.1.6)
9
8
  // over the result, where unbounded arrays would be the bug. Set
10
9
  // `limit` explicitly when the row-count could grow without bound.
11
10
 
12
- import type { SQL, Table } from "drizzle-orm";
11
+ import { selectMany, type WhereObject } from "../../db/query";
13
12
  import { defineStep } from "../define-step";
14
13
  import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
15
- import { asQueryTarget } from "./_drizzle-boundary";
16
14
  import { resolveOptional } from "./_resolver-utils";
17
15
 
18
16
  type ReadFindManyArgs = {
19
17
  readonly name: string;
20
- readonly table: Table;
21
- readonly where?: StepResolver<SQL | undefined>;
18
+ readonly table: unknown;
19
+ readonly where?: StepResolver<WhereObject | undefined>;
22
20
  readonly limit?: number;
23
21
  };
24
22
 
@@ -28,10 +26,12 @@ defineStep<ReadFindManyArgs, readonly Record<string, unknown>[]>({
28
26
  resultKey: (args) => args.name,
29
27
  run: async (args, ctx: PipelineCtx) => {
30
28
  const where = resolveOptional(args.where, ctx);
31
- const baseQuery = ctx.db.select().from(asQueryTarget(args.table));
32
- const filteredQuery = where === undefined ? baseQuery : baseQuery.where(where);
33
- const finalQuery = args.limit === undefined ? filteredQuery : filteredQuery.limit(args.limit);
34
- const rows = await finalQuery;
29
+ const rows = await selectMany(
30
+ ctx.db.raw,
31
+ args.table,
32
+ where,
33
+ args.limit !== undefined ? { limit: args.limit } : undefined,
34
+ );
35
35
  return rows as readonly Record<string, unknown>[];
36
36
  },
37
37
  });
@@ -39,8 +39,8 @@ defineStep<ReadFindManyArgs, readonly Record<string, unknown>[]>({
39
39
  export function buildReadFindManyStep(
40
40
  name: string,
41
41
  opts: {
42
- readonly table: Table;
43
- readonly where?: StepResolver<SQL | undefined>;
42
+ readonly table: unknown;
43
+ readonly where?: StepResolver<WhereObject | undefined>;
44
44
  readonly limit?: number;
45
45
  },
46
46
  ): StepInstance {
@@ -1,6 +1,6 @@
1
1
  // r.step.read.findOne — load a single row from a projection table.
2
2
  //
3
- // Thin wrapper on ctx.db.select().from(table).where(where).limit(1).
3
+ // Thin wrapper on selectMany(db, table, where, { limit: 1 }) (bun-db).
4
4
  // Resolves to the first row or null. Tenant-isolation: the caller's
5
5
  // `where` clause is responsible for any tenantId filter — read.findOne
6
6
  // does NOT auto-inject one (different from ctx.queryProjection which
@@ -20,16 +20,15 @@
20
20
  // fine for "find by uuid", a footgun for "find by tenantId". No
21
21
  // runtime check; reviewer responsibility.
22
22
 
23
- import type { SQL, Table } from "drizzle-orm";
23
+ import { selectMany, type WhereObject } from "../../db/query";
24
24
  import { defineStep } from "../define-step";
25
25
  import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
26
- import { asQueryTarget } from "./_drizzle-boundary";
27
26
  import { resolveRequired } from "./_resolver-utils";
28
27
 
29
28
  type ReadFindOneArgs = {
30
29
  readonly name: string;
31
- readonly table: Table;
32
- readonly where: StepResolver<SQL | undefined>;
30
+ readonly table: unknown;
31
+ readonly where: StepResolver<WhereObject | undefined>;
33
32
  };
34
33
 
35
34
  defineStep<ReadFindOneArgs, Record<string, unknown> | null>({
@@ -38,8 +37,7 @@ defineStep<ReadFindOneArgs, Record<string, unknown> | null>({
38
37
  resultKey: (args) => args.name,
39
38
  run: async (args, ctx: PipelineCtx) => {
40
39
  const where = resolveRequired(args.where, ctx);
41
- const query = ctx.db.select().from(asQueryTarget(args.table));
42
- const rows = where === undefined ? await query.limit(1) : await query.where(where).limit(1);
40
+ const rows = await selectMany(ctx.db.raw, args.table, where, { limit: 1 });
43
41
  return (rows[0] as Record<string, unknown> | undefined) ?? null;
44
42
  },
45
43
  });
@@ -47,8 +45,8 @@ defineStep<ReadFindOneArgs, Record<string, unknown> | null>({
47
45
  export function buildReadFindOneStep(
48
46
  name: string,
49
47
  opts: {
50
- readonly table: Table;
51
- readonly where: StepResolver<SQL | undefined>;
48
+ readonly table: unknown;
49
+ readonly where: StepResolver<WhereObject | undefined>;
52
50
  },
53
51
  ): StepInstance {
54
52
  return {
@@ -16,10 +16,9 @@
16
16
  // must commit in the same TX as the aggregate-mutation that triggered
17
17
  // it (stronger consistency than an async projection). Reviewer judges.
18
18
 
19
- import type { SQL, Table } from "drizzle-orm";
19
+ import { deleteMany, type WhereObject } from "../../db/query";
20
20
  import { defineStep } from "../define-step";
21
21
  import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
22
- import { asQueryTarget } from "./_drizzle-boundary";
23
22
  import { resolveRequired } from "./_resolver-utils";
24
23
 
25
24
  // `where` is REQUIRED — table-wide DELETE without a clause is a TRUNCATE
@@ -28,8 +27,8 @@ import { resolveRequired } from "./_resolver-utils";
28
27
  // `r.step.unsafeProjectionTruncate` step rather than loosening this
29
28
  // type to `SQL | undefined`.
30
29
  type UnsafeProjectionDeleteArgs = {
31
- readonly table: Table;
32
- readonly where: StepResolver<SQL>;
30
+ readonly table: unknown;
31
+ readonly where: StepResolver<WhereObject>;
33
32
  };
34
33
 
35
34
  defineStep<UnsafeProjectionDeleteArgs, void>({
@@ -37,7 +36,7 @@ defineStep<UnsafeProjectionDeleteArgs, void>({
37
36
  defaultFailureStrategy: "throw",
38
37
  run: async (args, ctx: PipelineCtx) => {
39
38
  const where = resolveRequired(args.where, ctx);
40
- await ctx.db.delete(asQueryTarget(args.table)).where(where);
39
+ await deleteMany(ctx.db.raw, args.table, where);
41
40
  },
42
41
  });
43
42
 
@@ -11,56 +11,88 @@
11
11
  // rejected by boot-validation — domain mutation MUST go through
12
12
  // r.step.aggregate.*.
13
13
 
14
- import { getTableColumns, type Table } from "drizzle-orm";
15
- import type { PgColumn } from "drizzle-orm/pg-core";
14
+ import { executeRawQuery } from "../../db/queries/raw-sql";
16
15
  import { defineStep } from "../define-step";
17
16
  import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
18
- import { asQueryTarget } from "./_drizzle-boundary";
19
17
  import { resolveRequired } from "./_resolver-utils";
20
18
 
21
19
  type UnsafeProjectionUpsertArgs = {
22
- readonly table: Table;
20
+ readonly table: unknown;
23
21
  readonly on: readonly string[];
24
22
  readonly row: StepResolver<Record<string, unknown>>;
25
23
  };
26
24
 
25
+ // @cast-boundary drizzle-bridge — reads table name + column snake_case
26
+ // names from drizzle Symbol-based metadata without importing drizzle-orm.
27
+ const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
28
+ const KUMIKO_COLUMNS_SYMBOL = Symbol.for("kumiko:schema:Columns");
29
+
30
+ function resolveTableName(table: unknown): string {
31
+ if (typeof table !== "object" || table === null) {
32
+ throw new Error("unsafeProjectionUpsert: table is not an object");
33
+ }
34
+ const name = (table as Record<symbol, unknown>)[KUMIKO_NAME_SYMBOL];
35
+ if (typeof name !== "string") {
36
+ throw new Error("unsafeProjectionUpsert: table has no kumiko:schema:Name symbol");
37
+ }
38
+ return name;
39
+ }
40
+
41
+ function resolveColumnName(table: unknown, field: string): string {
42
+ if (typeof table !== "object" || table === null) return field;
43
+ const cols = (table as Record<symbol, unknown>)[KUMIKO_COLUMNS_SYMBOL];
44
+ if (typeof cols !== "object" || cols === null) return field;
45
+ const col = (cols as Record<string, unknown>)[field];
46
+ if (typeof col === "object" && col !== null) {
47
+ const nameVal = (col as Record<string, unknown>)["name"];
48
+ if (typeof nameVal === "string") return nameVal;
49
+ }
50
+ return field;
51
+ }
52
+
53
+ function quoteIdent(name: string): string {
54
+ return `"${name.replace(/"/g, '""')}"`;
55
+ }
56
+
27
57
  defineStep<UnsafeProjectionUpsertArgs, void>({
28
58
  kind: "unsafeProjectionUpsert",
29
59
  defaultFailureStrategy: "throw",
30
60
  run: async (args, ctx: PipelineCtx) => {
31
61
  const resolvedRow = resolveRequired(args.row, ctx);
32
62
 
33
- const columns = getTableColumns(args.table) as Record<string, unknown>;
34
- const conflictTargets = args.on.map((key) => {
35
- const col = columns[key];
36
- if (!col) {
37
- throw new Error(`unsafeProjectionUpsert: column "${key}" not found on target table`);
63
+ // Validate conflict-key columns exist in the row.
64
+ for (const key of args.on) {
65
+ if (!(key in resolvedRow)) {
66
+ throw new Error(`unsafeProjectionUpsert: column "${key}" not found in row`);
38
67
  }
39
- return col;
40
- });
68
+ }
69
+
70
+ const tableName = resolveTableName(args.table);
71
+ const entries = Object.entries(resolvedRow);
72
+ const params: unknown[] = [];
41
73
 
42
- // SET clause is the same row minus the conflict-key columns —
43
- // updating a key to itself is harmless but verbose.
44
- const updateSet: Record<string, unknown> = {};
45
- for (const [k, v] of Object.entries(resolvedRow)) {
46
- if (!args.on.includes(k)) updateSet[k] = v;
74
+ const colNames = entries.map(([k]) => quoteIdent(resolveColumnName(args.table, k)));
75
+ const placeholders = entries.map((_, i) => `$${i + 1}`);
76
+ for (const [, v] of entries) params.push(v);
77
+
78
+ const conflictCols = args.on
79
+ .map((k) => quoteIdent(resolveColumnName(args.table, k)))
80
+ .join(", ");
81
+
82
+ // SET clause excludes conflict-key columns.
83
+ const setClauses: string[] = [];
84
+ let paramIdx = entries.length + 1;
85
+ for (const [k, v] of entries) {
86
+ if (args.on.includes(k)) continue;
87
+ setClauses.push(`${quoteIdent(resolveColumnName(args.table, k))} = $${paramIdx++}`);
88
+ params.push(v);
47
89
  }
48
90
 
49
- // @cast-boundary drizzle-bridge — The values + set + target casts
50
- // cross the drizzle type-boundary for the same reason as
51
- // asQueryTarget: resolvedRow is Record<string, unknown> by design
52
- // (M.1 phantom-typing limit), drizzle's typed-builder expects
53
- // table-specific shapes. Step-author owns shape correctness.
54
- // `as never` (not `as any`) — never is contravariantly assignable to
55
- // every drizzle Insert-shape; explicit "this bypass cannot be made
56
- // type-safe without lifting <TTable extends Table>" marker.
57
- await ctx.db
58
- .insert(asQueryTarget(args.table))
59
- .values(resolvedRow as never)
60
- .onConflictDoUpdate({
61
- target: conflictTargets as unknown as PgColumn[],
62
- set: updateSet as never,
63
- });
91
+ const sqlText =
92
+ `INSERT INTO ${quoteIdent(tableName)} (${colNames.join(", ")}) VALUES (${placeholders.join(", ")}) ` +
93
+ `ON CONFLICT (${conflictCols}) DO UPDATE SET ${setClauses.join(", ")}`;
94
+
95
+ await executeRawQuery(ctx.db.raw, sqlText, params);
64
96
  },
65
97
  });
66
98
 
@@ -1,5 +1,10 @@
1
- import type { PgTable } from "drizzle-orm/pg-core";
2
1
  import type { ZodType, z } from "zod";
2
+
3
+ // PgTable historically came from drizzle-orm/pg-core; the native dialect
4
+ // no longer carries drizzle internal class types. Every caller really
5
+ // needs "an opaque table-object with Symbol-based introspection".
6
+ type PgTable = unknown;
7
+
3
8
  import type { QueryHandlerDefinition, WriteHandlerDefinition } from "../define-handler";
4
9
  import type {
5
10
  ConfigKeyDefinition,
@@ -176,7 +181,7 @@ export type FeatureDefinition = {
176
181
  // F3 search-payload-extension — per-entity contributors that add flat fields
177
182
  // to the search-index payload during indexing. Keyed by entityName. Wrapped
178
183
  // in OwnedFn for feature-toggle filtering (consistent with postQuery-Hooks).
179
- readonly searchPayloadExtensions: Readonly<
184
+ readonly searchPayloadExtensions?: Readonly<
180
185
  Record<string, readonly OwnedFn<SearchPayloadContributorFn>[]>
181
186
  >;
182
187
  readonly configKeys: Readonly<Record<string, ConfigKeyDefinition>>;
@@ -5,7 +5,6 @@
5
5
  // accepted at the type layer during migration: features that pass an
6
6
  // array are auto-normalized to { [role]: "all" } at registry build.
7
7
  // Long-term: string[] disappears.
8
- import type { SQL } from "drizzle-orm";
9
8
  import type { OwnershipMap } from "../ownership";
10
9
 
11
10
  export type FieldAccess = {
@@ -475,7 +474,7 @@ export function isFileField(field: FieldDefinition | undefined): field is AnyFil
475
474
  export type TransitionMap = Readonly<Record<string, readonly string[]>>;
476
475
 
477
476
  /** Composite-Index auf einer Entity. Spalten werden via field-Name
478
- * referenziert (camelCase). buildDrizzleTable mapped sie auf snake_case-
477
+ * referenziert (camelCase). buildEntityTable mapped sie auf snake_case-
479
478
  * Spaltennamen und benennt den Index nach Convention:
480
479
  *
481
480
  * <table>_<col1>_<col2>_idx (non-unique)
@@ -484,7 +483,7 @@ export type TransitionMap = Readonly<Record<string, readonly string[]>>;
484
483
  * Eine `name`-Override ist erlaubt — Convention-Bruch in Bestandscode
485
484
  * vermeidet Migration-Churn beim Refactor.
486
485
  *
487
- * Single-column indices über `tenantId` sind redundant (buildDrizzleTable
486
+ * Single-column indices über `tenantId` sind redundant (buildEntityTable
488
487
  * legt die immer automatisch an); die Boot-Validation warnt. */
489
488
  export type EntityIndexDef = {
490
489
  readonly columns: readonly [string, ...string[]];
@@ -503,7 +502,7 @@ export type EntityIndexDef = {
503
502
  * man z.B. fuer scharfe BTREE-Indexes nur auf einer Status-Teilmenge
504
503
  * statt voller Tabelle).
505
504
  */
506
- readonly where?: SQL;
505
+ readonly where?: unknown;
507
506
  };
508
507
 
509
508
  export type FieldsMap = Readonly<Record<string, FieldDefinition>>;
@@ -517,7 +516,7 @@ export type EntityDefinition<F extends FieldsMap = FieldsMap> = {
517
516
  /** Allowed state transitions per field. Boot validates against select options. */
518
517
  readonly transitions?: Readonly<Record<string, TransitionMap>>;
519
518
  /** Composite-Indices über mehrere Felder. Single-column FK-Indices und
520
- * der tenant_id-Index werden weiterhin automatisch von buildDrizzleTable
519
+ * der tenant_id-Index werden weiterhin automatisch von buildEntityTable
521
520
  * angelegt — diese Liste ist nur für Custom-Indices die der Author
522
521
  * explizit deklariert (z.B. `{ unique: true, columns: ["key", "tenantId", "userId"] }`). */
523
522
  readonly indexes?: readonly EntityIndexDef[];