@cosmicdrift/kumiko-framework 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +159 -0
  2. package/package.json +91 -0
  3. package/src/__tests__/anonymous-access.integration.ts +325 -0
  4. package/src/__tests__/error-contract.integration.ts +435 -0
  5. package/src/__tests__/field-access.integration.ts +269 -0
  6. package/src/__tests__/full-stack.integration.ts +914 -0
  7. package/src/__tests__/ownership.integration.ts +449 -0
  8. package/src/__tests__/reference-data.integration.ts +198 -0
  9. package/src/__tests__/transition-guard.integration.ts +340 -0
  10. package/src/api/__tests__/api.test.ts +337 -0
  11. package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
  12. package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
  13. package/src/api/__tests__/batch.integration.ts +404 -0
  14. package/src/api/__tests__/body-limit.test.ts +88 -0
  15. package/src/api/__tests__/csrf-middleware.test.ts +97 -0
  16. package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
  17. package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
  18. package/src/api/__tests__/nested-write.integration.ts +213 -0
  19. package/src/api/__tests__/readiness.test.ts +76 -0
  20. package/src/api/__tests__/request-id-middleware.test.ts +72 -0
  21. package/src/api/__tests__/sse-broker.test.ts +58 -0
  22. package/src/api/__tests__/sse-route.test.ts +112 -0
  23. package/src/api/anonymous-cookie.ts +60 -0
  24. package/src/api/api-constants.ts +64 -0
  25. package/src/api/auth-middleware.ts +418 -0
  26. package/src/api/auth-routes.ts +982 -0
  27. package/src/api/csrf-middleware.ts +77 -0
  28. package/src/api/index.ts +31 -0
  29. package/src/api/jwt.ts +66 -0
  30. package/src/api/observability-middleware.ts +89 -0
  31. package/src/api/readiness.ts +132 -0
  32. package/src/api/request-context.ts +49 -0
  33. package/src/api/request-id-middleware.ts +50 -0
  34. package/src/api/route-registrars.ts +195 -0
  35. package/src/api/routes.ts +135 -0
  36. package/src/api/server.ts +640 -0
  37. package/src/api/sse-broker.ts +71 -0
  38. package/src/api/sse-route.ts +62 -0
  39. package/src/api/tokens.ts +16 -0
  40. package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
  41. package/src/db/__tests__/compound-types.test.ts +114 -0
  42. package/src/db/__tests__/connection-options.test.ts +68 -0
  43. package/src/db/__tests__/cursor.test.ts +41 -0
  44. package/src/db/__tests__/db-helpers.test.ts +369 -0
  45. package/src/db/__tests__/dialect-instant.test.ts +50 -0
  46. package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
  47. package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
  48. package/src/db/__tests__/encryption.test.ts +39 -0
  49. package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
  50. package/src/db/__tests__/event-store-executor.integration.ts +235 -0
  51. package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
  52. package/src/db/__tests__/located-timestamp.test.ts +184 -0
  53. package/src/db/__tests__/money.test.ts +199 -0
  54. package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
  55. package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
  56. package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
  57. package/src/db/__tests__/row-helpers.test.ts +59 -0
  58. package/src/db/__tests__/schema-migration.integration.ts +273 -0
  59. package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
  60. package/src/db/__tests__/table-builder-required.test.ts +216 -0
  61. package/src/db/__tests__/tenant-db.integration.ts +606 -0
  62. package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
  63. package/src/db/apply-entity-event.ts +188 -0
  64. package/src/db/assert-exists-in.ts +59 -0
  65. package/src/db/compound-types.ts +47 -0
  66. package/src/db/connection.ts +104 -0
  67. package/src/db/cursor.ts +83 -0
  68. package/src/db/dialect.ts +109 -0
  69. package/src/db/eagerload.ts +174 -0
  70. package/src/db/encryption.ts +39 -0
  71. package/src/db/event-store-executor.ts +906 -0
  72. package/src/db/index.ts +55 -0
  73. package/src/db/located-timestamp.ts +114 -0
  74. package/src/db/money.ts +120 -0
  75. package/src/db/pg-error.ts +46 -0
  76. package/src/db/reference-data.ts +77 -0
  77. package/src/db/row-helpers.ts +53 -0
  78. package/src/db/schema-inspection.ts +25 -0
  79. package/src/db/table-builder.ts +475 -0
  80. package/src/db/tenant-db.ts +434 -0
  81. package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
  82. package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
  83. package/src/engine/__tests__/boot-validator.test.ts +1865 -0
  84. package/src/engine/__tests__/build-app-schema.test.ts +154 -0
  85. package/src/engine/__tests__/claim-keys.test.ts +274 -0
  86. package/src/engine/__tests__/config-helpers.test.ts +236 -0
  87. package/src/engine/__tests__/effective-features.test.ts +86 -0
  88. package/src/engine/__tests__/engine.test.ts +1461 -0
  89. package/src/engine/__tests__/entity-handlers.test.ts +274 -0
  90. package/src/engine/__tests__/event-helpers.test.ts +68 -0
  91. package/src/engine/__tests__/extends-registrar.test.ts +159 -0
  92. package/src/engine/__tests__/factories-long-text.test.ts +84 -0
  93. package/src/engine/__tests__/factories-time.test.ts +158 -0
  94. package/src/engine/__tests__/field-predicates.test.ts +48 -0
  95. package/src/engine/__tests__/hook-phases.test.ts +132 -0
  96. package/src/engine/__tests__/identifiers.test.ts +35 -0
  97. package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
  98. package/src/engine/__tests__/nav.test.ts +267 -0
  99. package/src/engine/__tests__/ownership.test.ts +421 -0
  100. package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
  101. package/src/engine/__tests__/projection-helpers.test.ts +62 -0
  102. package/src/engine/__tests__/projection.test.ts +191 -0
  103. package/src/engine/__tests__/qualified-name.test.ts +264 -0
  104. package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
  105. package/src/engine/__tests__/run-in.test.ts +38 -0
  106. package/src/engine/__tests__/schema-builder.test.ts +380 -0
  107. package/src/engine/__tests__/screen.test.ts +408 -0
  108. package/src/engine/__tests__/state-machine.test.ts +148 -0
  109. package/src/engine/__tests__/system-user.test.ts +57 -0
  110. package/src/engine/__tests__/validation-hooks.test.ts +71 -0
  111. package/src/engine/access.ts +23 -0
  112. package/src/engine/boot-validator.ts +1528 -0
  113. package/src/engine/build-app-schema.ts +125 -0
  114. package/src/engine/config-helpers.ts +115 -0
  115. package/src/engine/constants.ts +85 -0
  116. package/src/engine/create-app.ts +98 -0
  117. package/src/engine/define-feature.ts +702 -0
  118. package/src/engine/define-handler.ts +78 -0
  119. package/src/engine/define-roles.ts +19 -0
  120. package/src/engine/effective-features.ts +87 -0
  121. package/src/engine/entity-handlers.ts +364 -0
  122. package/src/engine/event-helpers.ts +73 -0
  123. package/src/engine/factories.ts +328 -0
  124. package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
  125. package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
  126. package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
  127. package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
  128. package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
  129. package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
  130. package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
  131. package/src/engine/feature-ast/extractors.ts +2562 -0
  132. package/src/engine/feature-ast/index.ts +105 -0
  133. package/src/engine/feature-ast/parse.ts +369 -0
  134. package/src/engine/feature-ast/patch.ts +525 -0
  135. package/src/engine/feature-ast/patcher.ts +518 -0
  136. package/src/engine/feature-ast/patterns.ts +434 -0
  137. package/src/engine/feature-ast/render.ts +602 -0
  138. package/src/engine/feature-ast/source-location.ts +45 -0
  139. package/src/engine/field-access.ts +120 -0
  140. package/src/engine/index.ts +254 -0
  141. package/src/engine/ownership.ts +337 -0
  142. package/src/engine/parse-ref-target.ts +22 -0
  143. package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
  144. package/src/engine/pattern-library/index.ts +24 -0
  145. package/src/engine/pattern-library/library.ts +1117 -0
  146. package/src/engine/pattern-library/types.ts +255 -0
  147. package/src/engine/projection-helpers.ts +85 -0
  148. package/src/engine/qualified-name.ts +122 -0
  149. package/src/engine/read-claim.ts +31 -0
  150. package/src/engine/registry.ts +1325 -0
  151. package/src/engine/resolve-config-or-param.ts +153 -0
  152. package/src/engine/run-in.ts +29 -0
  153. package/src/engine/schema-builder.ts +175 -0
  154. package/src/engine/screen-filter-ops.ts +51 -0
  155. package/src/engine/state-machine.ts +70 -0
  156. package/src/engine/system-user.ts +32 -0
  157. package/src/engine/types/config.ts +306 -0
  158. package/src/engine/types/event-type-map.ts +37 -0
  159. package/src/engine/types/feature.ts +574 -0
  160. package/src/engine/types/fields.ts +422 -0
  161. package/src/engine/types/handlers.ts +742 -0
  162. package/src/engine/types/hooks.ts +142 -0
  163. package/src/engine/types/http-route.ts +54 -0
  164. package/src/engine/types/identifiers.ts +47 -0
  165. package/src/engine/types/index.ts +208 -0
  166. package/src/engine/types/nav.ts +46 -0
  167. package/src/engine/types/projection.ts +132 -0
  168. package/src/engine/types/relations.ts +51 -0
  169. package/src/engine/types/screen.ts +452 -0
  170. package/src/engine/types/workspace.ts +42 -0
  171. package/src/engine/validation.ts +33 -0
  172. package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
  173. package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
  174. package/src/entrypoint/index.ts +442 -0
  175. package/src/errors/__tests__/classes.test.ts +371 -0
  176. package/src/errors/__tests__/write-failures.test.ts +109 -0
  177. package/src/errors/classes.ts +249 -0
  178. package/src/errors/i18n/de.yaml +83 -0
  179. package/src/errors/i18n/en.yaml +80 -0
  180. package/src/errors/index.ts +41 -0
  181. package/src/errors/kumiko-error.ts +67 -0
  182. package/src/errors/reasons.ts +36 -0
  183. package/src/errors/serialize.ts +136 -0
  184. package/src/errors/transition-details.ts +30 -0
  185. package/src/errors/write-error-info.ts +123 -0
  186. package/src/errors/zod-bridge.ts +49 -0
  187. package/src/event-store/__tests__/admin-api.integration.ts +361 -0
  188. package/src/event-store/__tests__/event-store.integration.ts +584 -0
  189. package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
  190. package/src/event-store/__tests__/perf.integration.ts +255 -0
  191. package/src/event-store/__tests__/snapshot.integration.ts +267 -0
  192. package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
  193. package/src/event-store/__tests__/upcaster.integration.ts +460 -0
  194. package/src/event-store/admin-api.ts +257 -0
  195. package/src/event-store/archive.ts +106 -0
  196. package/src/event-store/errors.ts +35 -0
  197. package/src/event-store/event-store.ts +405 -0
  198. package/src/event-store/events-schema.ts +90 -0
  199. package/src/event-store/index.ts +50 -0
  200. package/src/event-store/snapshot.ts +210 -0
  201. package/src/event-store/upcaster-dead-letter.ts +119 -0
  202. package/src/event-store/upcaster.ts +147 -0
  203. package/src/files/__tests__/content-disposition.test.ts +123 -0
  204. package/src/files/__tests__/file-field-column.integration.ts +103 -0
  205. package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
  206. package/src/files/__tests__/file-handle.test.ts +122 -0
  207. package/src/files/__tests__/files.integration.ts +830 -0
  208. package/src/files/__tests__/storage-tracking.integration.ts +153 -0
  209. package/src/files/content-disposition.ts +55 -0
  210. package/src/files/file-handle.ts +63 -0
  211. package/src/files/file-ref-table.ts +22 -0
  212. package/src/files/file-routes.ts +353 -0
  213. package/src/files/in-memory-provider.ts +62 -0
  214. package/src/files/index.ts +29 -0
  215. package/src/files/local-provider.ts +35 -0
  216. package/src/files/storage-tracking.ts +60 -0
  217. package/src/files/types.ts +118 -0
  218. package/src/i18n/__tests__/i18n.test.ts +72 -0
  219. package/src/i18n/index.ts +29 -0
  220. package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
  221. package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
  222. package/src/jobs/__tests__/jobs.integration.ts +566 -0
  223. package/src/jobs/index.ts +2 -0
  224. package/src/jobs/job-runner.ts +574 -0
  225. package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
  226. package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
  227. package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
  228. package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
  229. package/src/lifecycle/index.ts +13 -0
  230. package/src/lifecycle/lifecycle.ts +160 -0
  231. package/src/lifecycle/signal-handlers.ts +62 -0
  232. package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
  233. package/src/logging/index.ts +3 -0
  234. package/src/logging/pino-logger.ts +64 -0
  235. package/src/logging/types.ts +7 -0
  236. package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
  237. package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
  238. package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
  239. package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
  240. package/src/migrations/index.ts +28 -0
  241. package/src/migrations/projection-detection.ts +149 -0
  242. package/src/migrations/rebuild-marker.ts +64 -0
  243. package/src/migrations/schema-drift.ts +395 -0
  244. package/src/observability/__tests__/console-provider.test.ts +67 -0
  245. package/src/observability/__tests__/metric-validator.test.ts +87 -0
  246. package/src/observability/__tests__/noop-provider.test.ts +82 -0
  247. package/src/observability/__tests__/observability.integration.ts +559 -0
  248. package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
  249. package/src/observability/__tests__/recording-meter.test.ts +101 -0
  250. package/src/observability/__tests__/recording-tracer.test.ts +110 -0
  251. package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
  252. package/src/observability/console-provider.ts +130 -0
  253. package/src/observability/context.ts +26 -0
  254. package/src/observability/fallback.ts +34 -0
  255. package/src/observability/ids.ts +25 -0
  256. package/src/observability/index.ts +79 -0
  257. package/src/observability/metric-validator.ts +86 -0
  258. package/src/observability/metrics-handle.ts +56 -0
  259. package/src/observability/noop-provider.ts +146 -0
  260. package/src/observability/prometheus-meter.ts +284 -0
  261. package/src/observability/recording-meter.ts +156 -0
  262. package/src/observability/recording-tracer.ts +198 -0
  263. package/src/observability/redis-wrapper.ts +132 -0
  264. package/src/observability/sensitive-filter.ts +108 -0
  265. package/src/observability/standard-metrics.ts +213 -0
  266. package/src/observability/types/index.ts +29 -0
  267. package/src/observability/types/metric.ts +56 -0
  268. package/src/observability/types/provider.ts +32 -0
  269. package/src/observability/types/span.ts +64 -0
  270. package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
  271. package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
  272. package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
  273. package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
  274. package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
  275. package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
  276. package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
  277. package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
  278. package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
  279. package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
  280. package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
  281. package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
  282. package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
  283. package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
  284. package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
  285. package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
  286. package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
  287. package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
  288. package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
  289. package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
  290. package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
  291. package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
  292. package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
  293. package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
  294. package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
  295. package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
  296. package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
  297. package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
  298. package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
  299. package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
  300. package/src/pipeline/append-event-core.ts +117 -0
  301. package/src/pipeline/auth-claims-resolver.ts +103 -0
  302. package/src/pipeline/cascade-handler.ts +113 -0
  303. package/src/pipeline/dispatcher.ts +1585 -0
  304. package/src/pipeline/distributed-lock.ts +37 -0
  305. package/src/pipeline/entity-cache.ts +113 -0
  306. package/src/pipeline/event-consumer-state.ts +108 -0
  307. package/src/pipeline/event-dedup.ts +23 -0
  308. package/src/pipeline/event-dispatcher.ts +1016 -0
  309. package/src/pipeline/event-retention.ts +154 -0
  310. package/src/pipeline/idempotency.ts +76 -0
  311. package/src/pipeline/index.ts +66 -0
  312. package/src/pipeline/lifecycle-pipeline.ts +409 -0
  313. package/src/pipeline/msp-rebuild.ts +242 -0
  314. package/src/pipeline/multi-stream-apply-context.ts +115 -0
  315. package/src/pipeline/projection-rebuild.ts +334 -0
  316. package/src/pipeline/projection-state.ts +72 -0
  317. package/src/pipeline/projections-runner.ts +56 -0
  318. package/src/pipeline/redis-keys.ts +11 -0
  319. package/src/pipeline/system-hooks.ts +190 -0
  320. package/src/random/__tests__/generate.test.ts +149 -0
  321. package/src/random/generate.ts +141 -0
  322. package/src/random/index.ts +8 -0
  323. package/src/random/words.ts +392 -0
  324. package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
  325. package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
  326. package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
  327. package/src/rate-limit/bucket.ts +36 -0
  328. package/src/rate-limit/index.ts +14 -0
  329. package/src/rate-limit/middleware.ts +152 -0
  330. package/src/rate-limit/resolver.ts +267 -0
  331. package/src/redis/__tests__/redis-options.test.ts +54 -0
  332. package/src/redis/index.ts +74 -0
  333. package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
  334. package/src/search/__tests__/search-adapter.test.ts +256 -0
  335. package/src/search/in-memory-adapter.ts +123 -0
  336. package/src/search/index.ts +12 -0
  337. package/src/search/meilisearch-adapter.ts +106 -0
  338. package/src/search/types.ts +39 -0
  339. package/src/secrets/__tests__/dek-cache.test.ts +213 -0
  340. package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
  341. package/src/secrets/__tests__/envelope.test.ts +74 -0
  342. package/src/secrets/__tests__/leak-guard.test.ts +92 -0
  343. package/src/secrets/__tests__/rotation.test.ts +149 -0
  344. package/src/secrets/dek-cache.ts +116 -0
  345. package/src/secrets/env-master-key-provider.ts +162 -0
  346. package/src/secrets/envelope.ts +55 -0
  347. package/src/secrets/index.ts +19 -0
  348. package/src/secrets/leak-guard.ts +87 -0
  349. package/src/secrets/rotation.ts +34 -0
  350. package/src/secrets/types.ts +107 -0
  351. package/src/stack/db.ts +104 -0
  352. package/src/stack/event-collector.ts +23 -0
  353. package/src/stack/index.ts +32 -0
  354. package/src/stack/redis.ts +44 -0
  355. package/src/stack/request-helper.ts +168 -0
  356. package/src/stack/table-helpers.ts +104 -0
  357. package/src/stack/test-stack.ts +357 -0
  358. package/src/stack/test-users.ts +37 -0
  359. package/src/testing/__tests__/e2e-generator.test.ts +230 -0
  360. package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
  361. package/src/testing/access-assertions.ts +15 -0
  362. package/src/testing/assertions.ts +35 -0
  363. package/src/testing/e2e-generator.ts +465 -0
  364. package/src/testing/expect-error.ts +25 -0
  365. package/src/testing/handler-context.ts +125 -0
  366. package/src/testing/http-cookies.ts +52 -0
  367. package/src/testing/index.ts +41 -0
  368. package/src/testing/late-bound.ts +39 -0
  369. package/src/testing/mutable-master-key-provider.ts +31 -0
  370. package/src/testing/observability-recorder.ts +54 -0
  371. package/src/testing/shared-entities.ts +49 -0
  372. package/src/testing/utils.ts +1 -0
  373. package/src/testing/wait-for.ts +31 -0
  374. package/src/time/__tests__/polyfill.test.ts +73 -0
  375. package/src/time/__tests__/tz-context.test.ts +121 -0
  376. package/src/time/index.ts +21 -0
  377. package/src/time/polyfill.ts +70 -0
  378. package/src/time/tz-context.ts +107 -0
  379. package/src/ui-types/app-schema.ts +57 -0
  380. package/src/ui-types/index.ts +65 -0
  381. package/src/utils/__tests__/assert.test.ts +17 -0
  382. package/src/utils/__tests__/env-parse.test.ts +54 -0
  383. package/src/utils/assert.ts +18 -0
  384. package/src/utils/env-parse.ts +16 -0
  385. package/src/utils/ids.ts +16 -0
  386. package/src/utils/index.ts +5 -0
  387. package/src/utils/safe-json.ts +30 -0
  388. package/src/utils/serialization.ts +7 -0
@@ -0,0 +1,606 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
3
+ import { createBooleanField, createEntity, createTextField } from "../../engine";
4
+ import {
5
+ createEntityTable,
6
+ createTestDb,
7
+ pushTables,
8
+ type TestDb,
9
+ TestUsers,
10
+ testTenantId,
11
+ } from "../../stack";
12
+ import { table as pgTable, serial, text, timestamp } from "../dialect";
13
+ import { buildDrizzleTable } from "../table-builder";
14
+ import { createTenantDb } from "../tenant-db";
15
+
16
+ // --- Entity table (has tenantId via buildBaseColumns) ---
17
+
18
+ const entity = createEntity({
19
+ table: "tenant_db_items",
20
+ fields: {
21
+ name: createTextField({ required: true }),
22
+ status: createTextField({ default: "draft" }),
23
+ isActive: createBooleanField({ default: true }),
24
+ },
25
+ softDelete: true,
26
+ });
27
+
28
+ const table = buildDrizzleTable("tenantDbItem", entity);
29
+
30
+ // --- System table (no tenantId — like job_runs) ---
31
+
32
+ const systemTable = pgTable("tdb_system_entries", {
33
+ id: serial("id").primaryKey(),
34
+ label: text("label").notNull(),
35
+ createdAt: timestamp("created_at").defaultNow().notNull(),
36
+ });
37
+
38
+ let testDb: TestDb;
39
+ const tenant1 = TestUsers.admin; // tenantId: 1
40
+ const tenant2 = TestUsers.otherTenant; // tenantId: 2
41
+
42
+ beforeAll(async () => {
43
+ testDb = await createTestDb();
44
+ await createEntityTable(testDb.db, entity, "tenantDbItem");
45
+ await pushTables(testDb.db, { tdb_system_entries: systemTable });
46
+ });
47
+
48
+ afterAll(async () => {
49
+ await testDb.cleanup();
50
+ });
51
+
52
+ // =============================================================================
53
+ // MODE 1: Scoped (default) — tenant filter on reads, tenantId forced on insert
54
+ // =============================================================================
55
+
56
+ describe("scoped mode (default)", () => {
57
+ describe("insert", () => {
58
+ test("auto-injects tenantId into values", async () => {
59
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
60
+
61
+ const rows = await tdb.insert(table).values({ name: "Item 1" }).returning();
62
+ expect(rows[0]?.["tenantId"]).toBe(testTenantId(1));
63
+ expect(rows[0]?.["name"]).toBe("Item 1");
64
+ });
65
+
66
+ test("cannot override tenantId via values", async () => {
67
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
68
+
69
+ const rows = await tdb
70
+ .insert(table)
71
+ .values({ name: "Sneaky", tenantId: testTenantId(999) })
72
+ .returning();
73
+ expect(rows[0]?.["tenantId"]).toBe(testTenantId(1));
74
+ });
75
+ });
76
+
77
+ describe("select", () => {
78
+ test("only returns rows for own tenant", async () => {
79
+ const tdb1 = createTenantDb(testDb.db, tenant1.tenantId);
80
+ const tdb2 = createTenantDb(testDb.db, tenant2.tenantId);
81
+
82
+ await tdb1.insert(table).values({ name: "T1 Scoped" }).returning();
83
+ await tdb2.insert(table).values({ name: "T2 Scoped" }).returning();
84
+
85
+ const rows1 = await tdb1.select().from(table);
86
+ const rows2 = await tdb2.select().from(table);
87
+
88
+ expect(rows1.every((r) => r!["tenantId"] === testTenantId(1))).toBe(true);
89
+ expect(rows2.every((r) => r!["tenantId"] === testTenantId(2))).toBe(true);
90
+ });
91
+
92
+ test("additional where conditions combine with tenant filter", async () => {
93
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
94
+
95
+ await tdb.insert(table).values({ name: "findme", status: "active" }).returning();
96
+ await tdb.insert(table).values({ name: "notme", status: "draft" }).returning();
97
+
98
+ const rows = await tdb.select().from(table).where(eq(table["status"], "active"));
99
+
100
+ expect(rows.length).toBeGreaterThanOrEqual(1);
101
+ expect(
102
+ rows.every((r) => r!["tenantId"] === testTenantId(1) && r!["status"] === "active"),
103
+ ).toBe(true);
104
+ });
105
+
106
+ test("select with columns", async () => {
107
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
108
+
109
+ await tdb.insert(table).values({ name: "ColSelect" }).returning();
110
+
111
+ const rows = await tdb
112
+ .select({ id: table["id"], name: table["name"] })
113
+ .from(table)
114
+ .where(eq(table["name"], "ColSelect"));
115
+
116
+ expect(rows.length).toBeGreaterThanOrEqual(1);
117
+ const row = rows[0]!;
118
+ expect(row["name"]).toBe("ColSelect");
119
+ expect(row["id"]).toBeDefined();
120
+ expect(row["status"]).toBeUndefined();
121
+ });
122
+
123
+ test("select with limit", async () => {
124
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
125
+
126
+ for (let i = 0; i < 5; i++) {
127
+ await tdb
128
+ .insert(table)
129
+ .values({ name: `Limit${i}` })
130
+ .returning();
131
+ }
132
+
133
+ const rows = await tdb.select().from(table).limit(2);
134
+ expect(rows).toHaveLength(2);
135
+ });
136
+ });
137
+
138
+ describe("update", () => {
139
+ test("only updates rows for own tenant", async () => {
140
+ const tdb1 = createTenantDb(testDb.db, tenant1.tenantId);
141
+ const tdb2 = createTenantDb(testDb.db, tenant2.tenantId);
142
+
143
+ const [row] = await tdb1.insert(table).values({ name: "T1 Update" }).returning();
144
+ const id = row!["id"] as string;
145
+
146
+ const result = await tdb2
147
+ .update(table)
148
+ .set({ name: "Hacked" })
149
+ .where(eq(table["id"], id))
150
+ .returning();
151
+
152
+ expect(result).toHaveLength(0);
153
+
154
+ const [updated] = await tdb1
155
+ .update(table)
156
+ .set({ name: "Updated" })
157
+ .where(eq(table["id"], id))
158
+ .returning();
159
+
160
+ expect(updated!["name"]).toBe("Updated");
161
+ });
162
+
163
+ test("update without returning", async () => {
164
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
165
+
166
+ const [row] = await tdb.insert(table).values({ name: "NoReturn" }).returning();
167
+ const id = row!["id"] as string;
168
+
169
+ await tdb.update(table).set({ name: "NoReturnUpdated" }).where(eq(table["id"], id));
170
+
171
+ const [updated] = await tdb.select().from(table).where(eq(table["id"], id));
172
+ expect(updated!["name"]).toBe("NoReturnUpdated");
173
+ });
174
+ });
175
+
176
+ describe("delete", () => {
177
+ test("only deletes rows for own tenant", async () => {
178
+ const tdb1 = createTenantDb(testDb.db, tenant1.tenantId);
179
+ const tdb2 = createTenantDb(testDb.db, tenant2.tenantId);
180
+
181
+ const [row] = await tdb1.insert(table).values({ name: "T1 Delete" }).returning();
182
+ const id = row!["id"] as string;
183
+
184
+ await tdb2.delete(table).where(eq(table["id"], id));
185
+
186
+ const rows = await tdb1.select().from(table).where(eq(table["id"], id));
187
+ expect(rows).toHaveLength(1);
188
+ });
189
+ });
190
+
191
+ describe("cross-tenant isolation", () => {
192
+ test("tenant cannot see, update, or delete other tenant data", async () => {
193
+ const tdb1 = createTenantDb(testDb.db, tenant1.tenantId);
194
+ const tdb2 = createTenantDb(testDb.db, tenant2.tenantId);
195
+
196
+ const [created] = await tdb1.insert(table).values({ name: "Secret" }).returning();
197
+ const id = created!["id"] as string;
198
+
199
+ const seen = await tdb2.select().from(table).where(eq(table["id"], id));
200
+ expect(seen).toHaveLength(0);
201
+
202
+ const updated = await tdb2
203
+ .update(table)
204
+ .set({ name: "Hacked" })
205
+ .where(eq(table["id"], id))
206
+ .returning();
207
+ expect(updated).toHaveLength(0);
208
+
209
+ await tdb2.delete(table).where(eq(table["id"], id));
210
+ const stillThere = await tdb1.select().from(table).where(eq(table["id"], id));
211
+ expect(stillThere).toHaveLength(1);
212
+ });
213
+ });
214
+
215
+ describe("reference data (tenantId = 0)", () => {
216
+ test("scoped select includes rows with tenantId = 0", async () => {
217
+ // Seed reference data with tenantId = 0 (like seedReferenceData does)
218
+ await testDb.db.insert(table).values({
219
+ name: "GlobalRef",
220
+ status: "ref",
221
+ tenantId: "00000000-0000-4000-8000-000000000000",
222
+ version: 1,
223
+ insertedAt: Temporal.Now.instant(),
224
+ });
225
+
226
+ const tdb1 = createTenantDb(testDb.db, tenant1.tenantId);
227
+ const tdb2 = createTenantDb(testDb.db, tenant2.tenantId);
228
+
229
+ // Both tenants can see the global reference row
230
+ const rows1 = await tdb1.select().from(table).where(eq(table["name"], "GlobalRef"));
231
+ expect(rows1).toHaveLength(1);
232
+
233
+ const rows2 = await tdb2.select().from(table).where(eq(table["name"], "GlobalRef"));
234
+ expect(rows2).toHaveLength(1);
235
+ });
236
+
237
+ test("scoped update does NOT affect tenantId = 0 rows", async () => {
238
+ await testDb.db.insert(table).values({
239
+ name: "RefNoUpdate",
240
+ status: "ref",
241
+ tenantId: "00000000-0000-4000-8000-000000000000",
242
+ version: 1,
243
+ insertedAt: Temporal.Now.instant(),
244
+ });
245
+
246
+ const tdb1 = createTenantDb(testDb.db, tenant1.tenantId);
247
+
248
+ const result = await tdb1
249
+ .update(table)
250
+ .set({ name: "Hacked" })
251
+ .where(eq(table["name"], "RefNoUpdate"))
252
+ .returning();
253
+
254
+ // Writes from a tenant scope must never touch reference rows (tenantId = 0).
255
+ // Reading them is fine, modifying them is a cross-tenant integrity bug.
256
+ expect(result).toHaveLength(0);
257
+
258
+ const [untouched] = await testDb.db
259
+ .select()
260
+ .from(table)
261
+ .where(eq(table["name"], "RefNoUpdate"));
262
+ expect(untouched!["name"]).toBe("RefNoUpdate");
263
+ });
264
+
265
+ test("scoped delete does NOT affect tenantId = 0 rows", async () => {
266
+ await testDb.db.insert(table).values({
267
+ name: "RefNoDelete",
268
+ status: "ref",
269
+ tenantId: "00000000-0000-4000-8000-000000000000",
270
+ version: 1,
271
+ insertedAt: Temporal.Now.instant(),
272
+ });
273
+
274
+ const tdb1 = createTenantDb(testDb.db, tenant1.tenantId);
275
+ await tdb1.delete(table).where(eq(table["name"], "RefNoDelete"));
276
+
277
+ const [stillThere] = await testDb.db
278
+ .select()
279
+ .from(table)
280
+ .where(eq(table["name"], "RefNoDelete"));
281
+ expect(stillThere).toBeDefined();
282
+ });
283
+ });
284
+ });
285
+
286
+ // =============================================================================
287
+ // MODE 2: System (r.systemScope()) — no tenant filter, tenantId as default
288
+ // =============================================================================
289
+
290
+ describe("system mode (r.systemScope())", () => {
291
+ test("select returns rows from ALL tenants", async () => {
292
+ const scoped1 = createTenantDb(testDb.db, tenant1.tenantId);
293
+ const scoped2 = createTenantDb(testDb.db, tenant2.tenantId);
294
+
295
+ await scoped1.insert(table).values({ name: "System-T1" }).returning();
296
+ await scoped2.insert(table).values({ name: "System-T2" }).returning();
297
+
298
+ const systemDb = createTenantDb(testDb.db, tenant1.tenantId, "system");
299
+ const rows = await systemDb.select().from(table);
300
+
301
+ const tenantIds = new Set(rows.map((r) => r!["tenantId"]));
302
+ // Must see rows from at least 2 different tenants
303
+ expect(tenantIds.size).toBeGreaterThanOrEqual(2);
304
+ });
305
+
306
+ test("insert uses tenantId as default but handler can override", async () => {
307
+ const systemDb = createTenantDb(testDb.db, tenant1.tenantId, "system");
308
+
309
+ // Without explicit tenantId — uses the default (tenant1)
310
+ const [defaultRow] = await systemDb.insert(table).values({ name: "SystemDefault" }).returning();
311
+ expect(defaultRow!["tenantId"]).toBe(testTenantId(1));
312
+
313
+ // With explicit tenantId — handler's value wins
314
+ const [overrideRow] = await systemDb
315
+ .insert(table)
316
+ .values({ name: "SystemOverride", tenantId: testTenantId(99) })
317
+ .returning();
318
+ expect(overrideRow!["tenantId"]).toBe(testTenantId(99));
319
+ });
320
+
321
+ test("insert with tenantId null (system config pattern)", async () => {
322
+ // Config feature sets tenantId = null for system-scoped values
323
+ // This requires the column to allow NULL — using systemTable which has no tenantId col,
324
+ // but we can test the spread order logic directly:
325
+ const systemDb = createTenantDb(testDb.db, tenant1.tenantId, "system");
326
+
327
+ // In scoped mode, tenantId: 77 would be overridden to 1
328
+ const scopedDb = createTenantDb(testDb.db, tenant1.tenantId);
329
+ const [scopedRow] = await scopedDb
330
+ .insert(table)
331
+ .values({ name: "ScopedForce", tenantId: testTenantId(77) })
332
+ .returning();
333
+ expect(scopedRow!["tenantId"]).toBe(testTenantId(1)); // forced
334
+
335
+ // In unscoped mode, explicit tenantId wins
336
+ const [unscopedRow] = await systemDb
337
+ .insert(table)
338
+ .values({ name: "SystemExplicit", tenantId: testTenantId(77) })
339
+ .returning();
340
+ expect(unscopedRow!["tenantId"]).toBe(testTenantId(77)); // handler wins
341
+ });
342
+
343
+ test("update affects rows from any tenant", async () => {
344
+ const scoped2 = createTenantDb(testDb.db, tenant2.tenantId);
345
+ const [row] = await scoped2.insert(table).values({ name: "T2-System-Upd" }).returning();
346
+ const id = row!["id"] as string;
347
+
348
+ // Scoped tenant 1 cannot update tenant 2's row
349
+ const scoped1 = createTenantDb(testDb.db, tenant1.tenantId);
350
+ const scopedResult = await scoped1
351
+ .update(table)
352
+ .set({ name: "ScopedFail" })
353
+ .where(eq(table["id"], id))
354
+ .returning();
355
+ expect(scopedResult).toHaveLength(0);
356
+
357
+ // Unscoped CAN update tenant 2's row
358
+ const systemDb = createTenantDb(testDb.db, tenant1.tenantId, "system");
359
+ const [updated] = await systemDb
360
+ .update(table)
361
+ .set({ name: "SystemWin" })
362
+ .where(eq(table["id"], id))
363
+ .returning();
364
+ expect(updated!["name"]).toBe("SystemWin");
365
+ });
366
+
367
+ test("delete affects rows from any tenant", async () => {
368
+ const scoped2 = createTenantDb(testDb.db, tenant2.tenantId);
369
+ const [row] = await scoped2.insert(table).values({ name: "T2-System-Del" }).returning();
370
+ const id = row!["id"] as string;
371
+
372
+ // Unscoped can delete tenant 2's row from tenant 1 context
373
+ const systemDb = createTenantDb(testDb.db, tenant1.tenantId, "system");
374
+ await systemDb.delete(table).where(eq(table["id"], id));
375
+
376
+ // Verify it's gone
377
+ const remaining = await scoped2.select().from(table).where(eq(table["id"], id));
378
+ expect(remaining).toHaveLength(0);
379
+ });
380
+ });
381
+
382
+ // =============================================================================
383
+ // MODE 3: Tables without tenantId column — no filter, no injection
384
+ // =============================================================================
385
+
386
+ describe("tables without tenantId column", () => {
387
+ test("select returns all rows (no tenant filter)", async () => {
388
+ // Insert two rows via raw db
389
+ await testDb.db.insert(systemTable).values({ label: "System-A" });
390
+ await testDb.db.insert(systemTable).values({ label: "System-B" });
391
+
392
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
393
+ const rows = await tdb.select().from(systemTable);
394
+ expect(rows.length).toBeGreaterThanOrEqual(2);
395
+ });
396
+
397
+ test("insert does not inject tenantId", async () => {
398
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
399
+
400
+ const [row] = await tdb.insert(systemTable).values({ label: "NoTenantInjection" }).returning();
401
+ const data = row!;
402
+ expect(data["label"]).toBe("NoTenantInjection");
403
+ // No tenantId column at all — should not be in the result
404
+ expect(data["tenantId"]).toBeUndefined();
405
+ });
406
+
407
+ test("select with where works without tenant filter", async () => {
408
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
409
+
410
+ await tdb.insert(systemTable).values({ label: "FindThis" }).returning();
411
+
412
+ const rows = await tdb.select().from(systemTable).where(eq(systemTable["label"], "FindThis"));
413
+ expect(rows).toHaveLength(1);
414
+ });
415
+
416
+ test("update works without tenant filter", async () => {
417
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
418
+
419
+ const [row] = await tdb.insert(systemTable).values({ label: "BeforeUpd" }).returning();
420
+ const id = row!["id"] as number;
421
+
422
+ await tdb.update(systemTable).set({ label: "AfterUpd" }).where(eq(systemTable["id"], id));
423
+
424
+ const [updated] = await tdb.select().from(systemTable).where(eq(systemTable["id"], id));
425
+ expect(updated!["label"]).toBe("AfterUpd");
426
+ });
427
+
428
+ test("delete works without tenant filter", async () => {
429
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
430
+
431
+ const [row] = await tdb.insert(systemTable).values({ label: "ToDelete" }).returning();
432
+ const id = row!["id"] as number;
433
+
434
+ await tdb.delete(systemTable).where(eq(systemTable["id"], id));
435
+
436
+ const remaining = await tdb.select().from(systemTable).where(eq(systemTable["id"], id));
437
+ expect(remaining).toHaveLength(0);
438
+ });
439
+ });
440
+
441
+ // =============================================================================
442
+ // Misc
443
+ // =============================================================================
444
+
445
+ describe("tenantId property", () => {
446
+ test("exposes tenantId for use in cursor queries etc.", () => {
447
+ const tdb = createTenantDb(testDb.db, testTenantId(42));
448
+ expect(tdb.tenantId).toBe(testTenantId(42));
449
+ });
450
+ });
451
+
452
+ // =============================================================================
453
+ // Mass-update guard — update().set() without .where() must refuse.
454
+ // =============================================================================
455
+ //
456
+ // Rationale: without the guard, a handler that forgets the WHERE clause would
457
+ // overwrite every row for the current tenant. Drizzle itself doesn't flag this
458
+ // (plain SQL behaviour); TenantDb is the layer where we can notice and stop it.
459
+
460
+ describe("mass-update guard", () => {
461
+ test(".set().returning() without .where() rejects with a clear error", async () => {
462
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
463
+ await tdb.insert(table).values({ name: "MassUpdateVictim1" }).returning();
464
+ await tdb.insert(table).values({ name: "MassUpdateVictim2" }).returning();
465
+
466
+ await expect(tdb.update(table).set({ name: "Wiped" }).returning()).rejects.toThrow(
467
+ /without \.where\(\) would mass-update/,
468
+ );
469
+
470
+ // Rows must be untouched — the rejection happened before any SQL ran.
471
+ const untouched = await tdb.select().from(table);
472
+ const touched = untouched.filter((r) => r["name"] === "Wiped");
473
+ expect(touched).toHaveLength(0);
474
+ });
475
+
476
+ test(".set() awaited without .where() rejects too", async () => {
477
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
478
+ await tdb.insert(table).values({ name: "AwaitGuardVictim" }).returning();
479
+
480
+ const promise = tdb.update(table).set({ name: "WipedByAwait" }) as unknown as Promise<void>;
481
+ await expect(promise).rejects.toThrow(/awaited without \.where\(\) would mass-update/);
482
+
483
+ const untouched = await tdb.select().from(table);
484
+ const touched = untouched.filter((r) => r["name"] === "WipedByAwait");
485
+ expect(touched).toHaveLength(0);
486
+ });
487
+
488
+ test(".set().where(...).returning() still works (guard only triggers on missing where)", async () => {
489
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
490
+ const [row] = await tdb.insert(table).values({ name: "HappyPath" }).returning();
491
+ const id = row!["id"] as string;
492
+
493
+ const updated = await tdb
494
+ .update(table)
495
+ .set({ name: "HappyPathUpdated" })
496
+ .where(eq(table["id"], id))
497
+ .returning();
498
+ expect(updated[0]!["name"]).toBe("HappyPathUpdated");
499
+ });
500
+ });
501
+
502
+ describe("pre-flight signal cancellation", () => {
503
+ test("aborted signal: select throws AbortError before SQL is issued", async () => {
504
+ const controller = new AbortController();
505
+ controller.abort();
506
+ const tdb = createTenantDb(
507
+ testDb.db,
508
+ tenant1.tenantId,
509
+ "tenant",
510
+ undefined,
511
+ undefined,
512
+ controller.signal,
513
+ );
514
+
515
+ let thrown: unknown;
516
+ try {
517
+ await tdb.select().from(table);
518
+ } catch (e) {
519
+ thrown = e;
520
+ }
521
+ expect((thrown as Error).name).toBe("AbortError");
522
+ });
523
+
524
+ test("aborted signal: insert/update/delete all throw AbortError", async () => {
525
+ const controller = new AbortController();
526
+ controller.abort();
527
+ const tdb = createTenantDb(
528
+ testDb.db,
529
+ tenant1.tenantId,
530
+ "tenant",
531
+ undefined,
532
+ undefined,
533
+ controller.signal,
534
+ );
535
+
536
+ let insertThrown: unknown;
537
+ try {
538
+ await tdb.insert(table).values({ name: "x" }).returning();
539
+ } catch (e) {
540
+ insertThrown = e;
541
+ }
542
+ expect((insertThrown as Error).name).toBe("AbortError");
543
+
544
+ let updateThrown: unknown;
545
+ try {
546
+ await tdb
547
+ .update(table)
548
+ .set({ name: "y" })
549
+ .where(eq(table["id"], "00000000-0000-0000-0000-000000000001"));
550
+ } catch (e) {
551
+ updateThrown = e;
552
+ }
553
+ expect((updateThrown as Error).name).toBe("AbortError");
554
+
555
+ let deleteThrown: unknown;
556
+ try {
557
+ await tdb.delete(table).where(eq(table["id"], "00000000-0000-0000-0000-000000000001"));
558
+ } catch (e) {
559
+ deleteThrown = e;
560
+ }
561
+ expect((deleteThrown as Error).name).toBe("AbortError");
562
+ });
563
+
564
+ test("mid-chain abort: first query succeeds, abort, next query throws", async () => {
565
+ // Simulates a handler doing N sequential queries where the client
566
+ // disconnects after query 1. Without the pre-flight check, queries
567
+ // 2..N would all execute and waste DB-CPU. With it, the chain stops
568
+ // immediately.
569
+ const controller = new AbortController();
570
+ const tdb = createTenantDb(
571
+ testDb.db,
572
+ tenant1.tenantId,
573
+ "tenant",
574
+ undefined,
575
+ undefined,
576
+ controller.signal,
577
+ );
578
+
579
+ const [first] = await tdb.insert(table).values({ name: "preflight-first" }).returning();
580
+ expect(first).toBeDefined();
581
+
582
+ controller.abort();
583
+
584
+ let secondThrown: unknown;
585
+ try {
586
+ await tdb.insert(table).values({ name: "preflight-second" }).returning();
587
+ } catch (e) {
588
+ secondThrown = e;
589
+ }
590
+ expect((secondThrown as Error).name).toBe("AbortError");
591
+
592
+ // Proves the first row was actually committed and the second never
593
+ // made it — the abort prevented future work, didn't roll back done
594
+ // work.
595
+ const rows = await testDb.db.select().from(table);
596
+ const names = rows.map((r) => r["name"] as string);
597
+ expect(names).toContain("preflight-first");
598
+ expect(names).not.toContain("preflight-second");
599
+ });
600
+
601
+ test("no signal passed: queries run normally (signal is opt-in)", async () => {
602
+ const tdb = createTenantDb(testDb.db, tenant1.tenantId);
603
+ const result = await tdb.insert(table).values({ name: "no-signal" }).returning();
604
+ expect(result).toHaveLength(1);
605
+ });
606
+ });