@cosmicdrift/kumiko-framework 0.387.0 → 0.389.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 (375) hide show
  1. package/AGENTS.md +9 -1
  2. package/agents/examples/anonymous-access.md +1 -1
  3. package/agents/examples/apex-landing.md +204 -0
  4. package/agents/examples/api-worker-split.md +64 -0
  5. package/agents/examples/app-config.md +14 -0
  6. package/agents/examples/auth-mfa.md +115 -0
  7. package/agents/examples/env-composition.md +28 -1
  8. package/agents/examples/golden-path.md +13 -1
  9. package/agents/examples/legal-pages.md +1 -1
  10. package/agents/examples/lifecycle-hooks.md +55 -2
  11. package/agents/examples/list-table-options.md +466 -0
  12. package/agents/examples/pdf-audit-export.md +4 -2
  13. package/agents/examples/prod-boot-options.md +16 -0
  14. package/agents/examples/rate-limiting.md +48 -0
  15. package/agents/examples/record-detail-layout.md +692 -0
  16. package/agents/examples/seed-migration.md +212 -0
  17. package/agents/examples/state-machine.md +3 -2
  18. package/agents/examples/tenant-isolation.md +73 -1
  19. package/agents/examples/wizard-form.md +941 -0
  20. package/agents/examples/writeform-section.md +548 -0
  21. package/agents/examples.md +1 -0
  22. package/agents/features.md +19 -2
  23. package/agents/widgets.md +2 -0
  24. package/dist/api/api-constants.d.ts +1 -0
  25. package/dist/api/api-constants.js +2 -0
  26. package/dist/api/auth-routes-shared.d.ts +2 -0
  27. package/dist/api/auth-routes-shared.js +5 -0
  28. package/dist/api/client-error-route.js +10 -5
  29. package/dist/api/entity-read-access.d.ts +2 -1
  30. package/dist/api/entity-read-access.js +14 -0
  31. package/dist/api/export/export-cells-reference.d.ts +9 -0
  32. package/dist/api/export/export-cells-reference.js +21 -0
  33. package/dist/api/export/export-cells.d.ts +11 -0
  34. package/dist/api/export/export-cells.js +136 -0
  35. package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
  36. package/dist/api/export/export-ciphertext-guard.js +27 -0
  37. package/dist/api/export/export-columns.d.ts +29 -0
  38. package/dist/api/export/export-columns.js +85 -0
  39. package/dist/api/export/export-errors.d.ts +4 -0
  40. package/dist/api/export/export-errors.js +14 -0
  41. package/dist/api/export/export-route.d.ts +4 -0
  42. package/dist/api/export/export-route.js +74 -0
  43. package/dist/api/export/export-rows.d.ts +13 -0
  44. package/dist/api/export/export-rows.js +62 -0
  45. package/dist/api/export/export-screen.d.ts +12 -0
  46. package/dist/api/export/export-screen.js +47 -0
  47. package/dist/api/export/export-xlsx-writer.d.ts +4 -0
  48. package/dist/api/export/export-xlsx-writer.js +21 -0
  49. package/dist/api/extra-route.d.ts +4 -0
  50. package/dist/api/index.d.ts +3 -1
  51. package/dist/api/index.js +3 -1
  52. package/dist/api/pii-leak-guard.d.ts +3 -0
  53. package/dist/api/pii-leak-guard.js +3 -2
  54. package/dist/api/request-context.d.ts +1 -1
  55. package/dist/api/request-context.js +1 -1
  56. package/dist/api/routes.d.ts +16 -0
  57. package/dist/api/routes.js +2 -2
  58. package/dist/api/server-boot-checks.d.ts +10 -0
  59. package/dist/api/server-boot-checks.js +157 -0
  60. package/dist/api/server-event-dispatcher.d.ts +26 -0
  61. package/dist/api/server-event-dispatcher.js +208 -0
  62. package/dist/api/server-extra-routes.d.ts +11 -0
  63. package/dist/api/server-extra-routes.js +206 -0
  64. package/dist/api/server-middleware.d.ts +20 -0
  65. package/dist/api/server-middleware.js +211 -0
  66. package/dist/api/server-routes.d.ts +19 -0
  67. package/dist/api/server-routes.js +299 -0
  68. package/dist/api/server-websocket-routes.d.ts +4 -0
  69. package/dist/api/server-websocket-routes.js +108 -0
  70. package/dist/api/server-wiring.d.ts +32 -0
  71. package/dist/api/server-wiring.js +238 -0
  72. package/dist/api/server.d.ts +10 -15
  73. package/dist/api/server.js +48 -1251
  74. package/dist/api/session-cookies.js +4 -4
  75. package/dist/api/sse-route.d.ts +2 -1
  76. package/dist/api/sse-route.js +27 -13
  77. package/dist/config/index.d.ts +3 -1
  78. package/dist/crypto/owner-transition-rekey.d.ts +5 -1
  79. package/dist/crypto/owner-transition-rekey.js +1 -1
  80. package/dist/crypto/pg-kms-adapter.d.ts +1 -0
  81. package/dist/crypto/pg-kms-adapter.js +12 -18
  82. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  83. package/dist/crypto/pii-field-encryption.js +3 -1
  84. package/dist/db/aggregate-sql.d.ts +23 -0
  85. package/dist/db/aggregate-sql.js +165 -0
  86. package/dist/db/closed-connection.d.ts +9 -0
  87. package/dist/db/closed-connection.js +88 -0
  88. package/dist/db/connection.d.ts +4 -5
  89. package/dist/db/connection.js +29 -20
  90. package/dist/db/dialect.js +1 -1
  91. package/dist/db/distinct-on.d.ts +1 -1
  92. package/dist/db/driver-row-coercion.d.ts +2 -0
  93. package/dist/db/driver-row-coercion.js +186 -0
  94. package/dist/db/driver-value-preparation.d.ts +16 -0
  95. package/dist/db/driver-value-preparation.js +101 -0
  96. package/dist/db/entity-field-encryption.d.ts +1 -0
  97. package/dist/db/entity-field-encryption.js +1 -1
  98. package/dist/db/entity-table-meta.js +103 -110
  99. package/dist/db/event-store-executor-context.d.ts +2 -1
  100. package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
  101. package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
  102. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  103. package/dist/db/event-store-executor-read-aggregate.js +143 -0
  104. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  105. package/dist/db/event-store-executor-read-filter.js +206 -0
  106. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  107. package/dist/db/event-store-executor-read-paging.js +99 -0
  108. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  109. package/dist/db/event-store-executor-read-rows.js +32 -0
  110. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  111. package/dist/db/event-store-executor-read-search.js +221 -0
  112. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  113. package/dist/db/event-store-executor-read-sort.js +130 -0
  114. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  115. package/dist/db/event-store-executor-read-where.js +81 -0
  116. package/dist/db/event-store-executor-read.d.ts +2 -9
  117. package/dist/db/event-store-executor-read.js +48 -701
  118. package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
  119. package/dist/db/event-store-executor-write-atomic.js +41 -0
  120. package/dist/db/event-store-executor-write-steps.d.ts +17 -0
  121. package/dist/db/event-store-executor-write-steps.js +95 -0
  122. package/dist/db/event-store-executor-write.js +405 -509
  123. package/dist/db/event-store-executor.js +2 -1
  124. package/dist/db/executor-managed-tables.js +1 -1
  125. package/dist/db/index.d.ts +7 -3
  126. package/dist/db/index.js +6 -2
  127. package/dist/db/list-aggregate-labels.d.ts +14 -0
  128. package/dist/db/list-aggregate-labels.js +26 -0
  129. package/dist/db/migrate-generator.js +71 -86
  130. package/dist/db/migrate-runner.js +7 -4
  131. package/dist/db/pg-error.js +26 -21
  132. package/dist/db/queries/backfill-pii.js +3 -6
  133. package/dist/db/queries/database-now.js +2 -1
  134. package/dist/db/queries/shadow-swap.js +2 -3
  135. package/dist/db/query.d.ts +16 -15
  136. package/dist/db/query.js +76 -804
  137. package/dist/db/reference-data.js +19 -2
  138. package/dist/db/statement-timeout.js +2 -1
  139. package/dist/db/table-builder.js +113 -166
  140. package/dist/db/table-info.d.ts +10 -0
  141. package/dist/db/table-info.js +45 -0
  142. package/dist/db/temporal-value-guards.d.ts +6 -0
  143. package/dist/db/temporal-value-guards.js +15 -0
  144. package/dist/db/tenant-db-runner.d.ts +1 -0
  145. package/dist/db/tenant-db-runner.js +3 -0
  146. package/dist/db/tenant-db-upsert.d.ts +5 -0
  147. package/dist/db/tenant-db-upsert.js +91 -0
  148. package/dist/db/tenant-db.js +50 -10
  149. package/dist/db/where-clause.d.ts +6 -0
  150. package/dist/db/where-clause.js +236 -0
  151. package/dist/derivatives/delete-stored-file.js +2 -1
  152. package/dist/engine/boot-validator/boot-check.js +1 -1
  153. package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
  154. package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
  155. package/dist/engine/boot-validator/detail-screens.js +2 -2
  156. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  157. package/dist/engine/boot-validator/entity-handler.js +17 -0
  158. package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
  159. package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
  160. package/dist/engine/boot-validator/form-derivations.js +35 -1
  161. package/dist/engine/boot-validator/i18n-keys.js +5 -3
  162. package/dist/engine/boot-validator/index.js +2 -0
  163. package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
  164. package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
  165. package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
  166. package/dist/engine/boot-validator/list-column-layout.js +41 -0
  167. package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
  168. package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
  169. package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
  170. package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
  171. package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
  172. package/dist/engine/boot-validator/list-table-export-options.js +27 -0
  173. package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
  174. package/dist/engine/boot-validator/list-table-options.js +77 -0
  175. package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
  176. package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
  177. package/dist/engine/boot-validator/pii-retention.js +233 -155
  178. package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
  179. package/dist/engine/boot-validator/projection-list-screens.js +3 -13
  180. package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
  181. package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
  182. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  183. package/dist/engine/boot-validator/screens-actions.js +8 -1
  184. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  185. package/dist/engine/boot-validator/screens-forms.js +2 -6
  186. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  187. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  188. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  189. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  190. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  191. package/dist/engine/boot-validator/screens-kind-entity-list.js +235 -0
  192. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  193. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  194. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  195. package/dist/engine/boot-validator/screens-kind-projection-list.js +56 -0
  196. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  197. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  198. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  199. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  200. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  201. package/dist/engine/boot-validator/screens-lists.js +24 -11
  202. package/dist/engine/boot-validator/screens.d.ts +0 -2
  203. package/dist/engine/boot-validator/screens.js +99 -860
  204. package/dist/engine/build-app-schema.d.ts +2 -1
  205. package/dist/engine/build-app-schema.js +65 -97
  206. package/dist/engine/build-config-feature-schema.js +6 -1
  207. package/dist/engine/define-feature.js +13 -2
  208. package/dist/engine/define-handler.js +1 -0
  209. package/dist/engine/entity-handlers.d.ts +13 -0
  210. package/dist/engine/entity-handlers.js +10 -21
  211. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  212. package/dist/engine/entity-list-runtime-options.js +18 -0
  213. package/dist/engine/feature-ast/extractors/handlers.js +1 -0
  214. package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
  215. package/dist/engine/feature-ast/extractors/index.js +1 -1
  216. package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
  217. package/dist/engine/feature-ast/extractors/round1.js +6 -0
  218. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  219. package/dist/engine/feature-ast/index.d.ts +1 -1
  220. package/dist/engine/feature-ast/parse.js +3 -1
  221. package/dist/engine/feature-ast/patch.d.ts +2 -0
  222. package/dist/engine/feature-ast/patch.js +5 -182
  223. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
  224. package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
  225. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  226. package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
  227. package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
  228. package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
  229. package/dist/engine/feature-ast/patterns.d.ts +14 -1
  230. package/dist/engine/feature-ast/patterns.js +44 -51
  231. package/dist/engine/feature-ast/render.js +52 -87
  232. package/dist/engine/feature-builder-state.d.ts +1 -0
  233. package/dist/engine/feature-builder-state.js +1 -0
  234. package/dist/engine/feature-entity-handlers.js +3 -0
  235. package/dist/engine/feature-manifest.d.ts +1 -0
  236. package/dist/engine/feature-manifest.js +1 -0
  237. package/dist/engine/field-access.d.ts +1 -0
  238. package/dist/engine/field-access.js +12 -0
  239. package/dist/engine/form-derivations.js +18 -7
  240. package/dist/engine/handler-field-references.js +1 -1
  241. package/dist/engine/index.d.ts +2 -2
  242. package/dist/engine/index.js +1 -1
  243. package/dist/engine/pattern-library/library.js +2 -1
  244. package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
  245. package/dist/engine/pattern-library/static-schemas.js +9 -0
  246. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  247. package/dist/engine/project-app-schema-for-roles.js +6 -4
  248. package/dist/engine/registry-ingest.js +1 -1
  249. package/dist/engine/schema-builder.js +228 -212
  250. package/dist/engine/screen-filter-ops.js +2 -0
  251. package/dist/engine/screen-helpers.js +1 -1
  252. package/dist/engine/screen-kind.d.ts +7 -0
  253. package/dist/engine/screen-kind.js +1 -0
  254. package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
  255. package/dist/engine/types/feature.d.ts +1 -0
  256. package/dist/engine/types/index.d.ts +4 -3
  257. package/dist/engine/types/screen.d.ts +1 -1
  258. package/dist/engine/types/screen.js +1 -1
  259. package/dist/engine/validate-projection-allowlist.js +2 -1
  260. package/dist/entrypoint/index.d.ts +6 -1
  261. package/dist/entrypoint/index.js +10 -1
  262. package/dist/errors/docs-url.d.ts +1 -1
  263. package/dist/errors/docs-url.js +22 -2
  264. package/dist/es-ops/context.js +1 -1
  265. package/dist/event-store/admin-api.js +1 -1
  266. package/dist/event-store/event-store.d.ts +1 -0
  267. package/dist/event-store/event-store.js +17 -8
  268. package/dist/event-store/provenance-append.js +1 -1
  269. package/dist/files/bind-pending-file-refs.js +3 -3
  270. package/dist/files/create-file-ref-from-bytes.js +7 -1
  271. package/dist/files/create-file-ref.d.ts +3 -0
  272. package/dist/files/create-file-ref.js +11 -2
  273. package/dist/files/file-handle.js +2 -3
  274. package/dist/files/file-routes.d.ts +2 -0
  275. package/dist/files/file-routes.js +40 -10
  276. package/dist/files/provider-resolver.d.ts +1 -0
  277. package/dist/files/provider-resolver.js +1 -1
  278. package/dist/files/record-write-check.d.ts +6 -0
  279. package/dist/files/record-write-check.js +13 -0
  280. package/dist/files/resolve-content-type.js +43 -0
  281. package/dist/files/upload-ledger.d.ts +2 -2
  282. package/dist/files/upload-ledger.js +11 -4
  283. package/dist/i18n/required-surface-keys.js +189 -208
  284. package/dist/internal/stack-support.d.ts +2 -1
  285. package/dist/internal/stack-support.js +1 -1
  286. package/dist/internal/testing.js +1 -1
  287. package/dist/jobs/job-runner-context.d.ts +32 -0
  288. package/dist/jobs/job-runner-context.js +305 -0
  289. package/dist/jobs/job-runner-execution.d.ts +7 -0
  290. package/dist/jobs/job-runner-execution.js +160 -0
  291. package/dist/jobs/job-runner-gates.d.ts +19 -0
  292. package/dist/jobs/job-runner-gates.js +198 -0
  293. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  294. package/dist/jobs/job-runner-helpers.js +61 -2
  295. package/dist/jobs/job-runner-scope.d.ts +44 -0
  296. package/dist/jobs/job-runner-scope.js +102 -0
  297. package/dist/jobs/job-runner-types.d.ts +12 -0
  298. package/dist/jobs/job-runner.d.ts +0 -1
  299. package/dist/jobs/job-runner.js +50 -657
  300. package/dist/observability/escape-hatch-report.d.ts +3 -0
  301. package/dist/observability/escape-hatch-report.js +21 -3
  302. package/dist/pipeline/after-rollback-sink.d.ts +6 -0
  303. package/dist/pipeline/after-rollback-sink.js +32 -0
  304. package/dist/pipeline/cascade-handler.js +165 -138
  305. package/dist/pipeline/dispatch-batch.js +8 -1
  306. package/dist/pipeline/dispatch-query.js +10 -3
  307. package/dist/pipeline/dispatch-shared.d.ts +13 -3
  308. package/dist/pipeline/dispatch-shared.js +62 -710
  309. package/dist/pipeline/dispatch-write.js +296 -189
  310. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  311. package/dist/pipeline/event-dispatcher-delivery.js +6 -6
  312. package/dist/pipeline/event-dispatcher.d.ts +1 -0
  313. package/dist/pipeline/event-dispatcher.js +6 -9
  314. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  315. package/dist/pipeline/handler-context-auth.js +54 -0
  316. package/dist/pipeline/handler-context-config.d.ts +14 -0
  317. package/dist/pipeline/handler-context-config.js +113 -0
  318. package/dist/pipeline/handler-context-db.d.ts +24 -0
  319. package/dist/pipeline/handler-context-db.js +149 -0
  320. package/dist/pipeline/handler-context-dispatch.d.ts +23 -0
  321. package/dist/pipeline/handler-context-dispatch.js +101 -0
  322. package/dist/pipeline/handler-context-events.d.ts +41 -0
  323. package/dist/pipeline/handler-context-events.js +239 -0
  324. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  325. package/dist/pipeline/handler-context-notify.js +55 -0
  326. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  327. package/dist/pipeline/handler-context-observability.js +21 -0
  328. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  329. package/dist/pipeline/handler-context-scope.js +88 -0
  330. package/dist/pipeline/notify-scope.js +12 -1
  331. package/dist/pipeline/pre-handler-gates.js +20 -4
  332. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  333. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  334. package/dist/pipeline/projection-rebuild.js +2 -1
  335. package/dist/pipeline/system-hooks.d.ts +3 -1
  336. package/dist/pipeline/system-hooks.js +55 -6
  337. package/dist/pipeline/write-result-masking.js +13 -10
  338. package/dist/schema-cli/apply-command.d.ts +2 -0
  339. package/dist/schema-cli/apply-command.js +97 -0
  340. package/dist/schema-cli/baseline-command.d.ts +2 -0
  341. package/dist/schema-cli/baseline-command.js +33 -0
  342. package/dist/schema-cli/generate-command.d.ts +2 -0
  343. package/dist/schema-cli/generate-command.js +90 -0
  344. package/dist/schema-cli/shared.d.ts +39 -0
  345. package/dist/schema-cli/shared.js +95 -0
  346. package/dist/schema-cli/status-command.d.ts +2 -0
  347. package/dist/schema-cli/status-command.js +73 -0
  348. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  349. package/dist/schema-cli/validate-boot-input.js +130 -0
  350. package/dist/schema-cli/validate-command.d.ts +2 -0
  351. package/dist/schema-cli/validate-command.js +130 -0
  352. package/dist/schema-cli.d.ts +2 -18
  353. package/dist/schema-cli.js +47 -604
  354. package/dist/search/meilisearch-adapter.js +2 -1
  355. package/dist/search/reindex-entity.js +15 -5
  356. package/dist/ui-types/index.d.ts +2 -2
  357. package/dist/ui-types/index.js +1 -1
  358. package/dist/upgrade-changelog.d.ts +2 -0
  359. package/dist/upgrade-changelog.js +14 -0
  360. package/dist/upgrade-cli.d.ts +3 -12
  361. package/dist/upgrade-cli.js +171 -197
  362. package/dist/upgrade-marker-parse.d.ts +32 -0
  363. package/dist/upgrade-marker-parse.js +73 -0
  364. package/package.json +9 -6
  365. package/src/changes.json +555 -0
  366. package/src/scripts/codemod/codemod-support.ts +1 -1
  367. package/src/scripts/codemod/db-factory.ts +1 -1
  368. package/src/scripts/codemod/feature-layout.ts +10 -2
  369. package/src/scripts/codemod/object-form.ts +41 -4
  370. package/src/scripts/codemod/package-merge.ts +58 -8
  371. package/src/scripts/codemod/pii-personal-migration.ts +175 -145
  372. package/src/scripts/codemod/testing-injection.ts +122 -42
  373. package/src/scripts/codemod/testing-move.ts +29 -7
  374. package/dist/engine/provider-plugin-context.d.ts +0 -3
  375. package/dist/engine/provider-plugin-context.js +0 -28
@@ -2,8 +2,10 @@ import { fetchOne } from "../db/query.js";
2
2
  import { createSystemUser } from "../engine/system-user.js";
3
3
  import { SYSTEM_TENANT_ID } from "../engine/types/index.js";
4
4
  import { reraiseAsKumikoError } from "../errors/index.js";
5
+ import { getStreamVersion } from "../event-store/event-store.js";
5
6
  import { entityTableFromRegistry } from "./entity-table-from-registry.js";
6
7
  import { createEventStoreExecutor } from "./event-store-executor.js";
8
+ import { deleteRowsWithoutReplayableStream } from "./queries/event-store-orphan-rows.js";
7
9
  import { createTenantDb } from "./tenant-db.js";
8
10
  /**
9
11
  * Seed reference data at boot time.
@@ -37,7 +39,22 @@ export async function seedReferenceData(defs, registry, db) {
37
39
  const keyValue = row[upsertKey];
38
40
  if (keyValue === undefined)
39
41
  continue;
40
- const existing = await fetchOne(db, table, { [upsertKey]: keyValue });
42
+ // Soft-deleted rows are invisible to executor.update, so treat them as missing and re-create.
43
+ const existingRow = await fetchOne(db, table, {
44
+ [upsertKey]: keyValue,
45
+ tenantId: SYSTEM_TENANT_ID,
46
+ ...(entity.softDelete && { isDeleted: false }),
47
+ });
48
+ // Rows seeded before reference data went through the executor have no events: update would
49
+ // hit a stream-version conflict and a rebuild would drop them. Drop the row and re-create
50
+ // it under the same id so its stream starts now.
51
+ let existing = existingRow;
52
+ let recreateId;
53
+ if (existingRow && (await getStreamVersion(db, existingRow.id, SYSTEM_TENANT_ID)) === 0) {
54
+ await deleteRowsWithoutReplayableStream(tdb, { table, ids: [existingRow.id] });
55
+ existing = undefined;
56
+ recreateId = existingRow.id;
57
+ }
41
58
  if (existing) {
42
59
  const changes = {};
43
60
  for (const [field, value] of Object.entries(row)) {
@@ -55,7 +72,7 @@ export async function seedReferenceData(defs, registry, db) {
55
72
  }
56
73
  }
57
74
  else {
58
- const result = await executor.create({ ...row, tenantId: SYSTEM_TENANT_ID }, by, tdb);
75
+ const result = await executor.create({ ...row, ...(recreateId && { id: recreateId }), tenantId: SYSTEM_TENANT_ID }, by, tdb);
59
76
  if (!result.isSuccess)
60
77
  throw reraiseAsKumikoError(result.error);
61
78
  inserted++;
@@ -4,7 +4,8 @@ const UNBOUNDED_POOL_CLOSE_TIMEOUT_SECONDS = 5;
4
4
  // PII backfill and projection rebuilds legitimately run longer. The cap is a connect-time session
5
5
  // setting, so the run gets its own short-lived pool with the same options minus the cap: one shared
6
6
  // connection would deadlock code that holds a lock in a tx while its DDL runs on another connection.
7
- // Handles not created by createDbConnection (tx, savepoint, siblings) are passed through.
7
+ // Handles not created by createDbConnection (tx, savepoint, siblings, gated runner proxies) are passed through
8
+ // unchanged and keep the cap; a caller holding such a runner lifts it itself (SET LOCAL in its own transaction).
8
9
  export async function withoutStatementTimeout(db, run) {
9
10
  const unbounded = openUnboundedSiblingPool(db);
10
11
  if (unbounded === undefined)
@@ -1,6 +1,5 @@
1
1
  import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
2
2
  import { toSnakeCase } from "../utils/case.js";
3
- import { assertUnreachable } from "../utils/index.js";
4
3
  import { markExecutorManagedTable } from "./executor-managed-tables.js";
5
4
  import { columnHandleName } from "./table-introspection.js";
6
5
  export { toSnakeCase } from "../utils/case.js";
@@ -13,172 +12,120 @@ export const SOFT_DELETE_LIVE_ROW_PREDICATE = `"is_deleted" = false`;
13
12
  export function physicalColumnName(table, field) {
14
13
  return columnHandleName(table, field) ?? toSnakeCase(field);
15
14
  }
16
- // Returns column(s) for a field. Most fields return a single entry,
17
- // money returns two (amount + currency).
18
- //
19
- // `required: true` on a FieldDefinition always maps to .notNull() in the
20
- // database column. Previously this applied only to reference fields, while
21
- // text/select/number/etc. remained silently nullable even when the API
22
- // validator enforced required. That forced hand-written PgTable definitions
23
- // to add .notNull(), causing duplicate definitions and schema drift. Now
24
- // r.entity is the single source of truth.
25
- function fieldToColumns(name, field, entity) {
26
- const snakeName = toSnakeCase(name);
27
- switch (field.type) {
28
- case "text":
29
- case "longText": {
30
- // Both map to PG `text` (unbounded). The difference exists only in the
31
- // type layer: longText has no sortable/searchable/filterable support.
32
- // The order default() before notNull() matters: the column builder
33
- // chains both; without default(), the generated SQL has no DEFAULT
34
- // clause, which breaks ALTER TABLE ADD COLUMN on existing rows.
35
- // longText currently has no default field in its type, but checking
36
- // `field.default !== undefined` is defensive.
37
- const base = text(snakeName);
38
- const withDefault = "default" in field && field.default !== undefined ? base.default(field.default) : base;
39
- const mainCol = { [name]: field.required ? withDefault.notNull() : withDefault };
40
- // lookupable → nullable HMAC blind-index column (kept in lockstep with
41
- // fieldToColumnMeta in entity-table-meta.ts).
42
- if (field.type === "text" && field.lookupable === true) {
43
- return { ...mainCol, [`${name}Bidx`]: text(`${snakeName}_bidx`) };
44
- }
45
- return mainCol;
46
- }
47
- case "boolean":
48
- return {
49
- [name]: field.default !== undefined
50
- ? boolean(snakeName).default(field.default).notNull()
51
- : field.required
52
- ? boolean(snakeName).notNull()
53
- : boolean(snakeName),
54
- };
55
- case "select": {
56
- // Pass default() through because entity-table-meta renders declared
57
- // defaults. This builder must stay in lockstep; otherwise the metadata
58
- // on the buildEntityTable object describes a different column than the
59
- // migration.
60
- const base = text(snakeName);
61
- const col = field.default !== undefined ? base.default(field.default) : base;
62
- return { [name]: field.required ? col.notNull() : col };
63
- }
64
- case "multiSelect":
65
- // jsonb string array with default `[]`, always NOT NULL.
66
- //
67
- // The `required` flag on MultiSelectFieldDef is intentionally ignored:
68
- // with default `[]`, the field is structurally never null (an insert
69
- // without a value produces an empty array, not NULL). Read-side code
70
- // therefore needs no null check; this is an API guarantee. Code that
71
- // needs actual null (meaning the field has never been set) should use
72
- // a separate status field or an optional-typed reference instead of a
73
- // multi-select.
74
- return { [name]: jsonb(snakeName).default([]).notNull() };
75
- case "number": {
76
- const base = field.integer === true ? integer(snakeName) : doublePrecision(snakeName);
77
- const col = field.default !== undefined ? base.default(field.default) : base;
78
- return { [name]: field.required ? col.notNull() : col };
79
- }
80
- case "decimal": {
81
- const base = decimalColumn(snakeName, field.precision, field.scale);
82
- const col = field.default !== undefined ? base.default(field.default) : base;
83
- return { [name]: field.required ? col.notNull() : col };
84
- }
85
- case "bigInt": {
86
- // 64-bit integer for audit counters, byte sizes, and cumulative sums.
87
- // mode:"number" produces a JS `number` (safe up to 2^53 ≈ 9 PB) rather
88
- // than JS `bigint`, making it JSON-serializable and frontend-friendly.
89
- // Values above 2^53 should use a text field with a custom codec.
90
- const base = bigint(snakeName, { mode: "number" });
91
- const col = field.default !== undefined ? base.default(field.default) : base;
92
- return { [name]: field.required ? col.notNull() : col };
93
- }
94
- case "reference":
95
- // FK-style UUID column. Multi-mode stores UUIDs as a jsonb string array;
96
- // single-mode remains a conventional UUID column (NOT NULL only when
97
- // required).
98
- if (field.multiple === true) {
99
- return { [name]: jsonb(snakeName).default([]).notNull() };
100
- }
101
- return {
102
- [name]: field.required ? uuid(snakeName).notNull() : uuid(snakeName),
103
- };
104
- case "money":
105
- // BIGINT stores the integer minor unit (cents for EUR, yen for JPY —
106
- // the currency column identifies which). INTEGER would cap at about
107
- // 21 M EUR, which is too low for B2B invoices, property values, or
108
- // balance aggregates. BIGINT safely handles up to about 90 trillion
109
- // EUR in JS.
110
- // Currency always has a default, so it is structurally .notNull().
111
- return {
112
- [name]: field.required ? moneyAmount(snakeName).notNull() : moneyAmount(snakeName),
113
- [`${name}Currency`]: text(`${snakeName}_currency`)
114
- .default(entity.defaultCurrency ?? "EUR")
115
- .notNull(),
116
- };
117
- case "embedded":
118
- // jsonb with default `{}`, always NOT NULL, analogous to multiSelect.
119
- // `required` is intentionally ignored because the default makes the
120
- // field structurally never null. To make embedded data optional (so
121
- // the field can be omitted entirely), model it with a wrapper field
122
- // and boolean flag or a discriminated union.
123
- // multiple → the value is a list of rows, so the empty default is `[]`.
124
- return field.multiple === true
125
- ? { [name]: jsonb(snakeName).default([]).notNull() }
126
- : { [name]: jsonb(snakeName).default({}).notNull() };
127
- case "jsonb":
128
- // Free-form jsonb — keys are not schema-validated. Default `{}`, NOT NULL
129
- // (analogous to embedded). Use case: the host entity's custom-fields
130
- // bundle in the `customFields` column, whose keys are tenant-defined.
131
- return { [name]: jsonb(snakeName).default({}).notNull() };
132
- case "date": {
133
- // Real PG `date` — no time-of-day, no timezone. Temporal.PlainDate
134
- // end-to-end (see plainDate() in dialect.ts). Was aliased on instant()
135
- // = TIMESTAMPTZ (TZ-dependent round-trip).
136
- const col = plainDate(snakeName);
137
- return { [name]: field.required ? col.notNull() : col };
138
- }
139
- case "timestamp": {
140
- // UTC instant — stored as TIMESTAMPTZ in PG and read/written as
141
- // Temporal.Instant through the instant() customType (see dialect.ts).
142
- // Callers use only Temporal.Instant, never JS Date. Comparisons
143
- // (lte/gt/orderBy) also accept Instants directly, so no .toString()
144
- // cast is needed.
145
- const col = instant(snakeName);
146
- return { [name]: field.required ? col.notNull() : col };
147
- }
148
- case "tz": {
149
- // IANA time-zone name as TEXT — validated through the Zod schema in the
150
- // validator step. Snake-case convention: `pickup_tz`.
151
- const col = text(snakeName);
152
- return { [name]: field.required ? col.notNull() : col };
153
- }
154
- case "locatedTimestamp": {
155
- // Two columns form an atomic pair: <name>_utc TIMESTAMPTZ + <name>_tz TEXT.
156
- // _utc is instant() (Temporal.Instant), and _tz is text (IANA name).
157
- // The executor wires automatic conversion (at+tz → utc on insert;
158
- // utc+tz → at on read). required propagates to both columns.
159
- const utc = instant(`${snakeName}_utc`);
160
- const tz = text(`${snakeName}_tz`);
161
- return {
162
- [`${name}Utc`]: field.required ? utc.notNull() : utc,
163
- [`${name}Tz`]: field.required ? tz.notNull() : tz,
164
- };
165
- }
166
- case "file":
167
- case "image": {
168
- // Single file: stores fileRefId as UUID — must match fileRefsTable.id
169
- // (uuid column). Anything narrower (integer, text length-limited) would
170
- // silently truncate or type-coerce at INSERT time and the FK reference
171
- // would be unusable.
172
- const col = uuid(snakeName);
173
- return { [name]: field.required ? col.notNull() : col };
174
- }
175
- case "files":
176
- case "images":
177
- // Ordered fileRef UUID array, same shape as multiSelect: never null.
178
- return { [name]: jsonb(snakeName).default([]).notNull() };
179
- default:
180
- assertUnreachable(field, "field type");
15
+ // `required: true` maps to NOT NULL for every field type, so r.entity stays the
16
+ // single source of truth and hand-written tables never add .notNull() on top.
17
+ function notNullWhenRequired(column, required) {
18
+ return required ? column.notNull() : column;
19
+ }
20
+ // Without a DEFAULT clause ALTER TABLE ADD COLUMN breaks on existing rows, and
21
+ // entity-table-meta renders declared defaults too, so both builders stay in lockstep.
22
+ function withDeclaredDefault(column, value) {
23
+ return value !== undefined ? column.default(value) : column;
24
+ }
25
+ // longText differs from text only in the type layer (no sortable/searchable/filterable).
26
+ function textColumns(name, snakeName, field) {
27
+ const declaredDefault = "default" in field ? field.default : undefined;
28
+ const mainCol = {
29
+ [name]: notNullWhenRequired(withDeclaredDefault(text(snakeName), declaredDefault), field.required),
30
+ };
31
+ // Nullable HMAC blind-index column, in lockstep with fieldToColumnMeta in entity-table-meta.ts.
32
+ if (field.type === "text" && field.lookupable === true) {
33
+ return { ...mainCol, [`${name}Bidx`]: text(`${snakeName}_bidx`) };
181
34
  }
35
+ return mainCol;
36
+ }
37
+ function booleanColumns(name, snakeName, field) {
38
+ if (field.default !== undefined) {
39
+ return { [name]: boolean(snakeName).default(field.default).notNull() };
40
+ }
41
+ return { [name]: notNullWhenRequired(boolean(snakeName), field.required) };
42
+ }
43
+ function numberColumns(name, snakeName, field) {
44
+ const base = field.integer === true ? integer(snakeName) : doublePrecision(snakeName);
45
+ return { [name]: notNullWhenRequired(withDeclaredDefault(base, field.default), field.required) };
46
+ }
47
+ function referenceColumns(name, snakeName, field) {
48
+ if (field.multiple === true)
49
+ return { [name]: emptyJsonbArray(snakeName) };
50
+ return { [name]: notNullWhenRequired(uuid(snakeName), field.required) };
51
+ }
52
+ // BIGINT minor units: INTEGER would cap at about 21 M EUR. The currency column
53
+ // always has a default, so it is structurally NOT NULL.
54
+ function moneyColumns(name, snakeName, field, entity) {
55
+ return {
56
+ [name]: notNullWhenRequired(moneyAmount(snakeName), field.required),
57
+ [`${name}Currency`]: text(`${snakeName}_currency`)
58
+ .default(entity.defaultCurrency ?? "EUR")
59
+ .notNull(),
60
+ };
61
+ }
62
+ // <name>_utc + <name>_tz form an atomic pair the executor converts on write and
63
+ // read, so `required` propagates to both columns.
64
+ function locatedTimestampColumns(name, snakeName, field) {
65
+ return {
66
+ [`${name}Utc`]: notNullWhenRequired(instant(`${snakeName}_utc`), field.required),
67
+ [`${name}Tz`]: notNullWhenRequired(text(`${snakeName}_tz`), field.required),
68
+ };
69
+ }
70
+ // The `[]` default makes the column structurally never null, so `required` is
71
+ // intentionally ignored and read-side code needs no null check.
72
+ function emptyJsonbArray(snakeName) {
73
+ return jsonb(snakeName).default([]).notNull();
74
+ }
75
+ // Same reasoning as emptyJsonbArray; optional embedded data needs a wrapper field.
76
+ function emptyJsonbObject(snakeName) {
77
+ return jsonb(snakeName).default({}).notNull();
78
+ }
79
+ function singleColumnNotNullWhenRequired(column) {
80
+ return (name, snakeName, field) => ({
81
+ [name]: notNullWhenRequired(column(snakeName), field.required),
82
+ });
83
+ }
84
+ // Each entry has a type-level twin in ColumnsForField and a meta-level twin in
85
+ // fieldToColumnMeta (entity-table-meta.ts).
86
+ const COLUMN_EMITTERS = {
87
+ text: textColumns,
88
+ longText: textColumns,
89
+ boolean: booleanColumns,
90
+ select: (name, snakeName, field) => ({
91
+ [name]: notNullWhenRequired(withDeclaredDefault(text(snakeName), field.default), field.required),
92
+ }),
93
+ multiSelect: (name, snakeName) => ({ [name]: emptyJsonbArray(snakeName) }),
94
+ number: numberColumns,
95
+ decimal: (name, snakeName, field) => ({
96
+ [name]: notNullWhenRequired(withDeclaredDefault(decimalColumn(snakeName, field.precision, field.scale), field.default), field.required),
97
+ }),
98
+ // mode "number" keeps the value JSON-serializable (safe up to 2^53).
99
+ bigInt: (name, snakeName, field) => ({
100
+ [name]: notNullWhenRequired(withDeclaredDefault(bigint(snakeName, { mode: "number" }), field.default), field.required),
101
+ }),
102
+ reference: referenceColumns,
103
+ money: moneyColumns,
104
+ embedded: (name, snakeName, field) => ({
105
+ [name]: field.multiple === true ? emptyJsonbArray(snakeName) : emptyJsonbObject(snakeName),
106
+ }),
107
+ jsonb: (name, snakeName) => ({ [name]: emptyJsonbObject(snakeName) }),
108
+ date: singleColumnNotNullWhenRequired(plainDate),
109
+ timestamp: singleColumnNotNullWhenRequired(instant),
110
+ tz: singleColumnNotNullWhenRequired(text),
111
+ locatedTimestamp: locatedTimestampColumns,
112
+ // Must match fileRefsTable.id (uuid); anything narrower breaks the reference.
113
+ file: singleColumnNotNullWhenRequired(uuid),
114
+ image: singleColumnNotNullWhenRequired(uuid),
115
+ files: (name, snakeName) => ({ [name]: emptyJsonbArray(snakeName) }),
116
+ images: (name, snakeName) => ({ [name]: emptyJsonbArray(snakeName) }),
117
+ };
118
+ function emitFieldColumns(type, name, field, entity) {
119
+ // A type from untyped input must not reach an Object.prototype member.
120
+ if (!Object.hasOwn(COLUMN_EMITTERS, type)) {
121
+ throw new Error(`[Kumiko] unhandled field type: ${String(field)}`);
122
+ }
123
+ const emit = COLUMN_EMITTERS[type];
124
+ return emit(name, toSnakeCase(name), field, entity);
125
+ }
126
+ // Most fields return one column; money and locatedTimestamp return two.
127
+ function fieldToColumns(name, field, entity) {
128
+ return emitFieldColumns(field.type, name, field, entity);
182
129
  }
183
130
  // Accepts both camelCase (`tenantMembership`) and kebab-case (`tenant-membership`)
184
131
  // entity / field names. Implementation lives in utils/case — re-exported here
@@ -0,0 +1,10 @@
1
+ import type { TableLike } from "./query.ts";
2
+ export type TableInfo = {
3
+ readonly name: string;
4
+ readonly columnOf: (field: string) => string;
5
+ readonly pgTypeOf: (column: string) => string | undefined;
6
+ readonly bigintJsModeOf: (column: string) => "number" | "bigint" | undefined;
7
+ readonly fieldOf: (column: string) => string;
8
+ readonly hasColumn: (fieldOrColumn: string) => boolean;
9
+ };
10
+ export declare function extractTableInfo(table: TableLike): TableInfo;
@@ -0,0 +1,45 @@
1
+ import { camelCase as envCamelCase } from "../env/index.js";
2
+ import { toSnakeCase } from "./table-builder.js";
3
+ import { asEntityTableMeta } from "./table-introspection.js";
4
+ // Idempotent snake_case → camelCase. `env.camelCase` always lowercases first
5
+ // (designed for SHOUT_CASE input) — for already-camelCase keys (mock rows
6
+ // in tests, projection-aliased columns) it would silently produce "tenantid"
7
+ // instead of leaving "tenantId" alone. Guard with an underscore check so
8
+ // the conversion only fires when the key is actually snake-shaped.
9
+ function snakeToCamel(key) {
10
+ if (!key.includes("_"))
11
+ return key;
12
+ return envCamelCase(key);
13
+ }
14
+ export function extractTableInfo(table) {
15
+ const meta = asEntityTableMeta(table);
16
+ if (!meta) {
17
+ throw new Error("extractTableInfo: table is not a kumiko EntityTableMeta — " +
18
+ "build it via buildEntityTable / deriveEntityTableMeta / table().");
19
+ }
20
+ const colByField = new Map();
21
+ const fieldByCol = new Map();
22
+ const typeByCol = new Map();
23
+ const bigintModeByCol = new Map();
24
+ for (const c of meta.columns) {
25
+ typeByCol.set(c.name, c.pgType);
26
+ if (c.bigintJsMode !== undefined)
27
+ bigintModeByCol.set(c.name, c.bigintJsMode);
28
+ // EntityTableMeta column names are snake_case. Map snake → snake AND
29
+ // derive a camelCase JS field-name so result rows can be renamed back
30
+ // to the API shape (`aggregate_id` → `aggregateId`).
31
+ colByField.set(c.name, c.name);
32
+ const camel = snakeToCamel(c.name);
33
+ if (camel !== c.name)
34
+ colByField.set(camel, c.name);
35
+ fieldByCol.set(c.name, camel === c.name ? c.name : camel);
36
+ }
37
+ return {
38
+ name: meta.tableName,
39
+ columnOf: (field) => colByField.get(field) ?? toSnakeCase(field),
40
+ pgTypeOf: (col) => typeByCol.get(col),
41
+ bigintJsModeOf: (col) => bigintModeByCol.get(col),
42
+ fieldOf: (col) => fieldByCol.get(col) ?? snakeToCamel(col),
43
+ hasColumn: (fieldOrColumn) => colByField.has(fieldOrColumn) || fieldByCol.has(fieldOrColumn),
44
+ };
45
+ }
@@ -0,0 +1,6 @@
1
+ import type { Temporal } from "@cosmicdrift/kumiko-types/temporal";
2
+ export declare function isTemporalInstant(v: unknown): v is Temporal.Instant;
3
+ export declare function isTemporalPlainDate(v: unknown): v is Temporal.PlainDate;
4
+ export declare function isTemporalPlainDateTimeLike(v: unknown): v is {
5
+ toPlainDate(): Temporal.PlainDate;
6
+ };
@@ -0,0 +1,15 @@
1
+ import { propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
2
+ export function isTemporalInstant(v) {
3
+ return propertyOf(v, Symbol.toStringTag) === "Temporal.Instant";
4
+ }
5
+ export function isTemporalPlainDate(v) {
6
+ return propertyOf(v, Symbol.toStringTag) === "Temporal.PlainDate";
7
+ }
8
+ export function isTemporalPlainDateTimeLike(v) {
9
+ const tag = propertyOf(v, Symbol.toStringTag);
10
+ return ((tag === "Temporal.PlainDateTime" || tag === "Temporal.ZonedDateTime") &&
11
+ typeof v === "object" &&
12
+ v !== null &&
13
+ "toPlainDate" in v &&
14
+ typeof v.toPlainDate === "function");
15
+ }
@@ -6,5 +6,6 @@ export declare function isDbRunner(value: unknown): value is DbRunner;
6
6
  export declare function bindTenantDbRunner(tenantDb: TenantDb, runner: DbRunner): void;
7
7
  export declare function tenantDbRunner(tenantDb: TenantDb): DbRunner;
8
8
  export declare function denyTenantDbWrites(tenantDb: TenantDb, reason: string): void;
9
+ export declare function tenantDbWritesDeniedReason(tenantDb: TenantDb): string | undefined;
9
10
  export declare function tenantDbWritesDeniedError(operation: string, reason: string): InternalError;
10
11
  export declare function tenantDbWriteRunner(tenantDb: TenantDb, operation: string): DbRunner;
@@ -26,6 +26,9 @@ const writesDeniedReasons = new WeakMap();
26
26
  export function denyTenantDbWrites(tenantDb, reason) {
27
27
  writesDeniedReasons.set(tenantDb, reason);
28
28
  }
29
+ export function tenantDbWritesDeniedReason(tenantDb) {
30
+ return writesDeniedReasons.get(tenantDb);
31
+ }
29
32
  export function tenantDbWritesDeniedError(operation, reason) {
30
33
  return new InternalError({
31
34
  message: `${operation}: writes are not available ${reason}; run the write through ctx.write so it gets its own transaction`,
@@ -0,0 +1,5 @@
1
+ import type { TenantDbMode, TenantDbUpsertOptions } from "@cosmicdrift/kumiko-types/tenant-db-types";
2
+ import { AccessDeniedError } from "../errors/index.ts";
3
+ import { type AnyDb, type TableLike } from "./query.ts";
4
+ export declare function upsertTableDenied(table: TableLike, mode: TenantDbMode): AccessDeniedError | undefined;
5
+ export declare function upsertTenantRow<TRow>(db: AnyDb, table: TableLike, values: Record<string, unknown>, options: TenantDbUpsertOptions): Promise<TRow | undefined>;
@@ -0,0 +1,91 @@
1
+ import { quoteIdent } from "../crypto/ciphertext-pattern.js";
2
+ import { AccessDeniedError } from "../errors/index.js";
3
+ import { isExecutorManagedTable } from "./executor-managed-tables.js";
4
+ import { buildInsertSql, coerceRow, conflictUpdateParts, insertEntries, unsafeLoggingClosedConnection, } from "./query.js";
5
+ import { extractTableInfo } from "./table-info.js";
6
+ import { asEntityTableMeta, tableNameOf } from "./table-introspection.js";
7
+ const TENANT_COLUMN = "tenant_id";
8
+ // Upsert is never allowed on event-fed tables (even with the write capability) and has no tenant-mode meaning for "global" ones.
9
+ export function upsertTableDenied(table, mode) {
10
+ if (isExecutorManagedTable(table)) {
11
+ return new AccessDeniedError({
12
+ message: `ctx.db.upsert(${tableNameOf(table)}): table is fed by events; upsert is not supported on it.`,
13
+ });
14
+ }
15
+ if (mode === "tenant" && asEntityTableMeta(table)?.tenancy === "global") {
16
+ return new AccessDeniedError({
17
+ message: `ctx.db.upsert(${tableNameOf(table)}): "global" table rows must carry SYSTEM_TENANT_ID; ` +
18
+ "use db.global(table) with escapeHatch instead.",
19
+ });
20
+ }
21
+ return undefined;
22
+ }
23
+ function sameColumnSet(a, b) {
24
+ return a.length === b.length && new Set(a).size === a.length && a.every((c) => b.includes(c));
25
+ }
26
+ function declaredUniqueColumnSets(meta) {
27
+ const sets = meta.indexes
28
+ .filter((i) => i.unique === true && i.whereSql === undefined && i.needsManualWhere !== true)
29
+ .map((i) => i.columns);
30
+ if (meta.compositePrimaryKey)
31
+ sets.push(meta.compositePrimaryKey.columns);
32
+ const primaryKeyColumns = meta.columns.filter((c) => c.primaryKey).map((c) => c.name);
33
+ if (primaryKeyColumns.length > 0)
34
+ sets.push(primaryKeyColumns);
35
+ return sets;
36
+ }
37
+ function primaryKeyColumnsOf(meta) {
38
+ return [
39
+ ...meta.columns.filter((c) => c.primaryKey).map((c) => c.name),
40
+ ...(meta.compositePrimaryKey?.columns ?? []),
41
+ ];
42
+ }
43
+ function assertTenantSafeConflictTarget(tableName, meta, conflictCols) {
44
+ if (conflictCols.length === 0 || !conflictCols.includes(TENANT_COLUMN)) {
45
+ throw new Error(`tenantDb.upsert(${tableName}): conflictTarget must include "tenantId" — a target without it ` +
46
+ "could match, and overwrite, another tenant's row.");
47
+ }
48
+ if (!declaredUniqueColumnSets(meta).some((set) => sameColumnSet(set, conflictCols))) {
49
+ throw new Error(`tenantDb.upsert(${tableName}): conflictTarget (${conflictCols.join(", ")}) matches no ` +
50
+ "declared unique index (without a WHERE clause) or primary key.");
51
+ }
52
+ }
53
+ // Handles a tenant's own row only: values carry the tenant already, and the DO UPDATE guard
54
+ // compares against the tenant being inserted, so a conflict on another tenant's row updates nothing.
55
+ export async function upsertTenantRow(db, table, values, options) {
56
+ const info = extractTableInfo(table);
57
+ const meta = asEntityTableMeta(table);
58
+ if (!info.hasColumn("tenantId")) {
59
+ throw new Error(`tenantDb.upsert(${info.name}): table has no tenant column — use the free upsertOnConflict ` +
60
+ "helper on an unsafeRaw runner instead.");
61
+ }
62
+ if (!meta) {
63
+ throw new Error(`tenantDb.upsert(${info.name}): needs an EntityTableMeta table (declared unique indexes and primary key), got a table without meta.`);
64
+ }
65
+ const conflictCols = options.conflictTarget.map((field) => info.columnOf(field));
66
+ assertTenantSafeConflictTarget(info.name, meta, conflictCols);
67
+ if (options.update &&
68
+ Object.keys(options.update).some((f) => info.columnOf(f) === TENANT_COLUMN)) {
69
+ throw new Error(`tenantDb.upsert(${info.name}): update must not set "tenantId" — it would move the row into another tenant.`);
70
+ }
71
+ const entries = insertEntries(info, values);
72
+ if (!entries.some((e) => e.col === TENANT_COLUMN)) {
73
+ throw new Error(`tenantDb.upsert(${info.name}): values carry no tenantId.`);
74
+ }
75
+ const { sqlPrefix, params } = buildInsertSql(info, entries);
76
+ const skipCols = new Set([...conflictCols, ...primaryKeyColumnsOf(meta), TENANT_COLUMN]);
77
+ const setParts = conflictUpdateParts(info, entries, options.update, skipCols, params);
78
+ if (setParts.length === 0) {
79
+ throw new Error(`tenantDb.upsert(${info.name}): nothing to update on conflict`);
80
+ }
81
+ const tenantRef = quoteIdent(TENANT_COLUMN);
82
+ const sqlText = `${sqlPrefix} ON CONFLICT (${conflictCols.map((c) => quoteIdent(c)).join(", ")}) ` +
83
+ `DO UPDATE SET ${setParts.join(", ")} ` +
84
+ `WHERE ${quoteIdent(info.name)}.${tenantRef} = EXCLUDED.${tenantRef} RETURNING *`;
85
+ const rows = await unsafeLoggingClosedConnection(db, sqlText, params, {
86
+ operation: "tenantDb.upsert",
87
+ table: info.name,
88
+ });
89
+ const first = rows[0];
90
+ return first ? coerceRow(first, info) : undefined;
91
+ }