@fragno-dev/db 0.1.14 → 0.2.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 (445) hide show
  1. package/.turbo/turbo-build.log +242 -139
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +123 -8
  4. package/dist/adapters/adapters.d.ts +19 -5
  5. package/dist/adapters/adapters.d.ts.map +1 -1
  6. package/dist/adapters/adapters.js.map +1 -1
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts +6 -19
  8. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  9. package/dist/adapters/drizzle/drizzle-adapter.js +7 -47
  10. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
  11. package/dist/adapters/drizzle/generate.d.ts +7 -1
  12. package/dist/adapters/drizzle/generate.d.ts.map +1 -1
  13. package/dist/adapters/drizzle/generate.js +46 -45
  14. package/dist/adapters/drizzle/generate.js.map +1 -1
  15. package/dist/adapters/generic-sql/driver-config.d.ts +74 -0
  16. package/dist/adapters/generic-sql/driver-config.d.ts.map +1 -0
  17. package/dist/adapters/generic-sql/driver-config.js +94 -0
  18. package/dist/adapters/generic-sql/driver-config.js.map +1 -0
  19. package/dist/adapters/generic-sql/generic-sql-adapter.d.ts +43 -0
  20. package/dist/adapters/generic-sql/generic-sql-adapter.d.ts.map +1 -0
  21. package/dist/adapters/generic-sql/generic-sql-adapter.js +87 -0
  22. package/dist/adapters/generic-sql/generic-sql-adapter.js.map +1 -0
  23. package/dist/adapters/generic-sql/generic-sql-uow-executor.js +67 -0
  24. package/dist/adapters/generic-sql/generic-sql-uow-executor.js.map +1 -0
  25. package/dist/adapters/generic-sql/migration/cold-kysely.js +33 -0
  26. package/dist/adapters/generic-sql/migration/cold-kysely.js.map +1 -0
  27. package/dist/adapters/generic-sql/migration/dialect/mysql.js +60 -0
  28. package/dist/adapters/generic-sql/migration/dialect/mysql.js.map +1 -0
  29. package/dist/adapters/generic-sql/migration/dialect/postgres.js +59 -0
  30. package/dist/adapters/generic-sql/migration/dialect/postgres.js.map +1 -0
  31. package/dist/adapters/generic-sql/migration/dialect/sqlite.js +96 -0
  32. package/dist/adapters/generic-sql/migration/dialect/sqlite.js.map +1 -0
  33. package/dist/adapters/generic-sql/migration/executor.d.ts +15 -0
  34. package/dist/adapters/generic-sql/migration/executor.d.ts.map +1 -0
  35. package/dist/adapters/generic-sql/migration/executor.js +18 -0
  36. package/dist/adapters/generic-sql/migration/executor.js.map +1 -0
  37. package/dist/adapters/generic-sql/migration/prepared-migrations.d.ts +66 -0
  38. package/dist/adapters/generic-sql/migration/prepared-migrations.d.ts.map +1 -0
  39. package/dist/adapters/generic-sql/migration/prepared-migrations.js +68 -0
  40. package/dist/adapters/generic-sql/migration/prepared-migrations.js.map +1 -0
  41. package/dist/adapters/generic-sql/migration/sql-generator.js +212 -0
  42. package/dist/adapters/generic-sql/migration/sql-generator.js.map +1 -0
  43. package/dist/adapters/generic-sql/query/create-sql-query-compiler.js +32 -0
  44. package/dist/adapters/generic-sql/query/create-sql-query-compiler.js.map +1 -0
  45. package/dist/adapters/generic-sql/query/cursor-utils.js +37 -0
  46. package/dist/adapters/generic-sql/query/cursor-utils.js.map +1 -0
  47. package/dist/adapters/generic-sql/query/dialect/mysql.js +33 -0
  48. package/dist/adapters/generic-sql/query/dialect/mysql.js.map +1 -0
  49. package/dist/adapters/generic-sql/query/dialect/postgres.js +32 -0
  50. package/dist/adapters/generic-sql/query/dialect/postgres.js.map +1 -0
  51. package/dist/adapters/generic-sql/query/dialect/sqlite.js +32 -0
  52. package/dist/adapters/generic-sql/query/dialect/sqlite.js.map +1 -0
  53. package/dist/adapters/generic-sql/query/generic-sql-uow-operation-compiler.js +152 -0
  54. package/dist/adapters/generic-sql/query/generic-sql-uow-operation-compiler.js.map +1 -0
  55. package/dist/adapters/generic-sql/query/select-builder.js +69 -0
  56. package/dist/adapters/generic-sql/query/select-builder.js.map +1 -0
  57. package/dist/adapters/generic-sql/query/sql-query-compiler.js +145 -0
  58. package/dist/adapters/generic-sql/query/sql-query-compiler.js.map +1 -0
  59. package/dist/adapters/generic-sql/query/where-builder.js +129 -0
  60. package/dist/adapters/generic-sql/query/where-builder.js.map +1 -0
  61. package/dist/adapters/generic-sql/result-interpreter.js +74 -0
  62. package/dist/adapters/generic-sql/result-interpreter.js.map +1 -0
  63. package/dist/adapters/generic-sql/uow-decoder.js +105 -0
  64. package/dist/adapters/generic-sql/uow-decoder.js.map +1 -0
  65. package/dist/adapters/generic-sql/uow-encoder.js +93 -0
  66. package/dist/adapters/generic-sql/uow-encoder.js.map +1 -0
  67. package/dist/adapters/kysely/kysely-adapter.d.ts +5 -16
  68. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  69. package/dist/adapters/kysely/kysely-adapter.js +6 -159
  70. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  71. package/dist/adapters/{drizzle/drizzle-query.js → shared/from-unit-of-work-compiler.js} +48 -62
  72. package/dist/adapters/shared/from-unit-of-work-compiler.js.map +1 -0
  73. package/dist/adapters/{kysely/kysely-shared.d.ts → shared/table-name-mapper.d.ts} +3 -2
  74. package/dist/adapters/shared/table-name-mapper.d.ts.map +1 -0
  75. package/dist/adapters/shared/table-name-mapper.js +43 -0
  76. package/dist/adapters/shared/table-name-mapper.js.map +1 -0
  77. package/dist/adapters/shared/uow-operation-compiler.js +105 -0
  78. package/dist/adapters/shared/uow-operation-compiler.js.map +1 -0
  79. package/dist/db-fragment-definition-builder.d.ts +186 -0
  80. package/dist/db-fragment-definition-builder.d.ts.map +1 -0
  81. package/dist/db-fragment-definition-builder.js +207 -0
  82. package/dist/db-fragment-definition-builder.js.map +1 -0
  83. package/dist/fragments/internal-fragment.d.ts +53 -0
  84. package/dist/fragments/internal-fragment.d.ts.map +1 -0
  85. package/dist/fragments/internal-fragment.js +111 -0
  86. package/dist/fragments/internal-fragment.js.map +1 -0
  87. package/dist/hooks/hooks.d.ts +51 -0
  88. package/dist/hooks/hooks.d.ts.map +1 -0
  89. package/dist/hooks/hooks.js +88 -0
  90. package/dist/hooks/hooks.js.map +1 -0
  91. package/dist/migration-engine/generation-engine.d.ts +0 -2
  92. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  93. package/dist/migration-engine/generation-engine.js +38 -56
  94. package/dist/migration-engine/generation-engine.js.map +1 -1
  95. package/dist/mod.d.ts +35 -23
  96. package/dist/mod.d.ts.map +1 -1
  97. package/dist/mod.js +48 -45
  98. package/dist/mod.js.map +1 -1
  99. package/dist/node_modules/.pnpm/rou3@0.7.10/node_modules/rou3/dist/index.js +165 -0
  100. package/dist/node_modules/.pnpm/rou3@0.7.10/node_modules/rou3/dist/index.js.map +1 -0
  101. package/dist/packages/fragno/dist/api/bind-services.js +20 -0
  102. package/dist/packages/fragno/dist/api/bind-services.js.map +1 -0
  103. package/dist/packages/fragno/dist/api/error.js +48 -0
  104. package/dist/packages/fragno/dist/api/error.js.map +1 -0
  105. package/dist/packages/fragno/dist/api/fragment-definition-builder.js +320 -0
  106. package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -0
  107. package/dist/packages/fragno/dist/api/fragment-instantiator.js +525 -0
  108. package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -0
  109. package/dist/packages/fragno/dist/api/fragno-response.js +73 -0
  110. package/dist/packages/fragno/dist/api/fragno-response.js.map +1 -0
  111. package/dist/packages/fragno/dist/api/internal/response-stream.js +81 -0
  112. package/dist/packages/fragno/dist/api/internal/response-stream.js.map +1 -0
  113. package/dist/packages/fragno/dist/api/internal/route.js +10 -0
  114. package/dist/packages/fragno/dist/api/internal/route.js.map +1 -0
  115. package/dist/packages/fragno/dist/api/mutable-request-state.js +97 -0
  116. package/dist/packages/fragno/dist/api/mutable-request-state.js.map +1 -0
  117. package/dist/packages/fragno/dist/api/request-context-storage.js +43 -0
  118. package/dist/packages/fragno/dist/api/request-context-storage.js.map +1 -0
  119. package/dist/packages/fragno/dist/api/request-input-context.js +118 -0
  120. package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -0
  121. package/dist/packages/fragno/dist/api/request-middleware.js +83 -0
  122. package/dist/packages/fragno/dist/api/request-middleware.js.map +1 -0
  123. package/dist/packages/fragno/dist/api/request-output-context.js +119 -0
  124. package/dist/packages/fragno/dist/api/request-output-context.js.map +1 -0
  125. package/dist/packages/fragno/dist/api/route.js +17 -0
  126. package/dist/packages/fragno/dist/api/route.js.map +1 -0
  127. package/dist/packages/fragno/dist/internal/symbols.js +10 -0
  128. package/dist/packages/fragno/dist/internal/symbols.js.map +1 -0
  129. package/dist/query/column-defaults.js +27 -0
  130. package/dist/query/column-defaults.js.map +1 -0
  131. package/dist/query/cursor.d.ts +14 -6
  132. package/dist/query/cursor.d.ts.map +1 -1
  133. package/dist/query/cursor.js +16 -7
  134. package/dist/query/cursor.js.map +1 -1
  135. package/dist/query/orm/orm.d.ts +1 -1
  136. package/dist/query/orm/orm.js.map +1 -1
  137. package/dist/query/serialize/create-sql-serializer.js +30 -0
  138. package/dist/query/serialize/create-sql-serializer.js.map +1 -0
  139. package/dist/query/serialize/dialect/mysql-serializer.js +87 -0
  140. package/dist/query/serialize/dialect/mysql-serializer.js.map +1 -0
  141. package/dist/query/serialize/dialect/postgres-serializer.js +80 -0
  142. package/dist/query/serialize/dialect/postgres-serializer.js.map +1 -0
  143. package/dist/query/serialize/dialect/sqlite-serializer.js +93 -0
  144. package/dist/query/serialize/dialect/sqlite-serializer.js.map +1 -0
  145. package/dist/query/serialize/sql-serializer.js +67 -0
  146. package/dist/query/serialize/sql-serializer.js.map +1 -0
  147. package/dist/query/{query.d.ts → simple-query-interface.d.ts} +6 -6
  148. package/dist/query/simple-query-interface.d.ts.map +1 -0
  149. package/dist/query/unit-of-work/execute-unit-of-work.d.ts +133 -0
  150. package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -0
  151. package/dist/query/unit-of-work/execute-unit-of-work.js +197 -0
  152. package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -0
  153. package/dist/query/unit-of-work/retry-policy.d.ts +88 -0
  154. package/dist/query/unit-of-work/retry-policy.d.ts.map +1 -0
  155. package/dist/query/unit-of-work/retry-policy.js +61 -0
  156. package/dist/query/unit-of-work/retry-policy.js.map +1 -0
  157. package/dist/query/{unit-of-work.d.ts → unit-of-work/unit-of-work.d.ts} +145 -58
  158. package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -0
  159. package/dist/query/{unit-of-work.js → unit-of-work/unit-of-work.js} +435 -198
  160. package/dist/query/unit-of-work/unit-of-work.js.map +1 -0
  161. package/dist/query/value-decoding.js +71 -0
  162. package/dist/query/value-decoding.js.map +1 -0
  163. package/dist/query/value-encoding.js +124 -0
  164. package/dist/query/value-encoding.js.map +1 -0
  165. package/dist/schema/create.d.ts +3 -0
  166. package/dist/schema/create.d.ts.map +1 -1
  167. package/dist/schema/create.js +4 -0
  168. package/dist/schema/create.js.map +1 -1
  169. package/dist/schema/type-conversion/create-sql-type-mapper.js +29 -0
  170. package/dist/schema/type-conversion/create-sql-type-mapper.js.map +1 -0
  171. package/dist/schema/type-conversion/dialect/mysql.js +57 -0
  172. package/dist/schema/type-conversion/dialect/mysql.js.map +1 -0
  173. package/dist/schema/type-conversion/dialect/postgres.js +56 -0
  174. package/dist/schema/type-conversion/dialect/postgres.js.map +1 -0
  175. package/dist/schema/type-conversion/dialect/sqlite.js +52 -0
  176. package/dist/schema/type-conversion/dialect/sqlite.js.map +1 -0
  177. package/dist/schema/type-conversion/type-mapping.js +63 -0
  178. package/dist/schema/type-conversion/type-mapping.js.map +1 -0
  179. package/dist/sql-driver/connection/connection-provider.d.ts +13 -0
  180. package/dist/sql-driver/connection/connection-provider.d.ts.map +1 -0
  181. package/dist/sql-driver/connection/connection-provider.js +19 -0
  182. package/dist/sql-driver/connection/connection-provider.js.map +1 -0
  183. package/dist/sql-driver/connection/single-connection-provider.js +23 -0
  184. package/dist/sql-driver/connection/single-connection-provider.js.map +1 -0
  185. package/dist/sql-driver/dialect-adapter/dialect-adapter.d.ts +7 -0
  186. package/dist/sql-driver/dialect-adapter/dialect-adapter.d.ts.map +1 -0
  187. package/dist/sql-driver/dialects/dialects.d.ts +2 -0
  188. package/dist/sql-driver/dialects/dialects.js +3 -0
  189. package/dist/sql-driver/dialects/durable-object-dialect.d.ts +72 -0
  190. package/dist/sql-driver/dialects/durable-object-dialect.d.ts.map +1 -0
  191. package/dist/sql-driver/dialects/durable-object-dialect.js +130 -0
  192. package/dist/sql-driver/dialects/durable-object-dialect.js.map +1 -0
  193. package/dist/sql-driver/driver/runtime-driver.d.ts +23 -0
  194. package/dist/sql-driver/driver/runtime-driver.d.ts.map +1 -0
  195. package/dist/sql-driver/driver/runtime-driver.js +56 -0
  196. package/dist/sql-driver/driver/runtime-driver.js.map +1 -0
  197. package/dist/sql-driver/query-executor/default-query-executor.js +26 -0
  198. package/dist/sql-driver/query-executor/default-query-executor.js.map +1 -0
  199. package/dist/sql-driver/query-executor/plugin.d.ts +17 -0
  200. package/dist/sql-driver/query-executor/plugin.d.ts.map +1 -0
  201. package/dist/sql-driver/query-executor/query-executor-base.js +25 -0
  202. package/dist/sql-driver/query-executor/query-executor-base.js.map +1 -0
  203. package/dist/sql-driver/query-executor/query-executor.d.ts +36 -0
  204. package/dist/sql-driver/query-executor/query-executor.d.ts.map +1 -0
  205. package/dist/sql-driver/sql-driver-adapter.d.ts +29 -0
  206. package/dist/sql-driver/sql-driver-adapter.d.ts.map +1 -0
  207. package/dist/sql-driver/sql-driver-adapter.js +68 -0
  208. package/dist/sql-driver/sql-driver-adapter.js.map +1 -0
  209. package/dist/sql-driver/sql-driver.d.ts +38 -0
  210. package/dist/sql-driver/sql-driver.d.ts.map +1 -0
  211. package/dist/sql-driver/sql-driver.js +1 -0
  212. package/dist/sql-driver/sql.js +50 -0
  213. package/dist/sql-driver/sql.js.map +1 -0
  214. package/dist/with-database.d.ts +32 -0
  215. package/dist/with-database.d.ts.map +1 -0
  216. package/dist/with-database.js +34 -0
  217. package/dist/with-database.js.map +1 -0
  218. package/package.json +43 -9
  219. package/src/adapters/adapters.ts +23 -4
  220. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +140 -185
  221. package/src/adapters/drizzle/{drizzle-adapter-sqlite.test.ts → drizzle-adapter-sqlite3.test.ts} +187 -55
  222. package/src/adapters/drizzle/drizzle-adapter.ts +14 -93
  223. package/src/adapters/drizzle/generate.test.ts +102 -269
  224. package/src/adapters/drizzle/generate.ts +89 -63
  225. package/src/adapters/drizzle/migrate-drizzle.test.ts +19 -0
  226. package/src/adapters/drizzle/shared.ts +0 -34
  227. package/src/adapters/drizzle/test-utils.ts +36 -5
  228. package/src/adapters/generic-sql/README.md +14 -0
  229. package/src/adapters/generic-sql/driver-config.ts +144 -0
  230. package/src/adapters/generic-sql/generic-sql-adapter.test.ts +50 -0
  231. package/src/adapters/generic-sql/generic-sql-adapter.ts +146 -0
  232. package/src/adapters/generic-sql/generic-sql-uow-executor.ts +130 -0
  233. package/src/adapters/generic-sql/migration/cold-kysely.ts +55 -0
  234. package/src/adapters/{kysely/migration/execute-mysql.test.ts → generic-sql/migration/dialect/mysql.test.ts} +342 -484
  235. package/src/adapters/generic-sql/migration/dialect/mysql.ts +104 -0
  236. package/src/adapters/generic-sql/migration/dialect/postgres.test.ts +1008 -0
  237. package/src/adapters/generic-sql/migration/dialect/postgres.ts +113 -0
  238. package/src/adapters/{kysely/migration/execute-sqlite.test.ts → generic-sql/migration/dialect/sqlite.test.ts} +307 -510
  239. package/src/adapters/generic-sql/migration/dialect/sqlite.ts +189 -0
  240. package/src/adapters/generic-sql/migration/executor.ts +33 -0
  241. package/src/adapters/generic-sql/migration/prepared-migrations.test.ts +661 -0
  242. package/src/adapters/generic-sql/migration/prepared-migrations.ts +214 -0
  243. package/src/adapters/generic-sql/migration/sql-generator.ts +413 -0
  244. package/src/adapters/generic-sql/query/create-sql-query-compiler.ts +36 -0
  245. package/src/adapters/generic-sql/query/cursor-utils.ts +56 -0
  246. package/src/adapters/generic-sql/query/dialect/mysql.ts +34 -0
  247. package/src/adapters/generic-sql/query/dialect/postgres.ts +32 -0
  248. package/src/adapters/generic-sql/query/dialect/sqlite.ts +32 -0
  249. package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.test.ts +1568 -0
  250. package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.ts +314 -0
  251. package/src/adapters/generic-sql/query/select-builder.test.ts +256 -0
  252. package/src/adapters/generic-sql/query/select-builder.ts +137 -0
  253. package/src/adapters/generic-sql/query/sql-query-compiler.test.ts +195 -0
  254. package/src/adapters/generic-sql/query/sql-query-compiler.ts +367 -0
  255. package/src/adapters/generic-sql/query/where-builder.test.ts +744 -0
  256. package/src/adapters/generic-sql/query/where-builder.ts +211 -0
  257. package/src/adapters/generic-sql/result-interpreter.ts +102 -0
  258. package/src/adapters/generic-sql/test/generic-drizzle-adapter-sqlite3.test.ts +899 -0
  259. package/src/adapters/generic-sql/uow-decoder.test.ts +399 -0
  260. package/src/adapters/generic-sql/uow-decoder.ts +152 -0
  261. package/src/adapters/generic-sql/uow-encoder.test.ts +183 -0
  262. package/src/adapters/generic-sql/uow-encoder.ts +131 -0
  263. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +90 -96
  264. package/src/adapters/kysely/kysely-adapter-sqlocal.test.ts +215 -0
  265. package/src/adapters/kysely/kysely-adapter.ts +10 -242
  266. package/src/adapters/{drizzle/drizzle-query.ts → shared/from-unit-of-work-compiler.ts} +111 -106
  267. package/src/adapters/shared/table-name-mapper.ts +50 -0
  268. package/src/adapters/shared/uow-operation-compiler.ts +211 -0
  269. package/src/db-fragment-definition-builder.test.ts +887 -0
  270. package/src/db-fragment-definition-builder.ts +737 -0
  271. package/src/db-fragment-instantiator.test.ts +543 -0
  272. package/src/db-fragment-integration.test.ts +406 -0
  273. package/src/fragments/internal-fragment.test.ts +549 -0
  274. package/src/fragments/internal-fragment.ts +249 -0
  275. package/src/hooks/hooks.test.ts +575 -0
  276. package/src/hooks/hooks.ts +179 -0
  277. package/src/migration-engine/generation-engine.test.ts +60 -27
  278. package/src/migration-engine/generation-engine.ts +99 -92
  279. package/src/mod.ts +139 -78
  280. package/src/query/column-defaults.ts +49 -0
  281. package/src/query/cursor.test.ts +147 -3
  282. package/src/query/cursor.ts +25 -8
  283. package/src/query/orm/orm.ts +1 -1
  284. package/src/query/query-type.test.ts +9 -9
  285. package/src/query/serialize/create-sql-serializer.ts +34 -0
  286. package/src/query/serialize/dialect/mysql-serializer.ts +142 -0
  287. package/src/query/serialize/dialect/postgres-serializer.ts +129 -0
  288. package/src/query/serialize/dialect/sqlite-serializer.test.ts +251 -0
  289. package/src/query/serialize/dialect/sqlite-serializer.ts +156 -0
  290. package/src/query/serialize/sql-serializer.ts +143 -0
  291. package/src/query/{query.ts → simple-query-interface.ts} +4 -4
  292. package/src/query/unit-of-work/execute-unit-of-work.test.ts +1310 -0
  293. package/src/query/unit-of-work/execute-unit-of-work.ts +504 -0
  294. package/src/query/unit-of-work/retry-policy.test.ts +217 -0
  295. package/src/query/unit-of-work/retry-policy.ts +141 -0
  296. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +831 -0
  297. package/src/query/{unit-of-work-types.test.ts → unit-of-work/unit-of-work-types.test.ts} +7 -5
  298. package/src/query/unit-of-work/unit-of-work.test.ts +1716 -0
  299. package/src/query/{unit-of-work.ts → unit-of-work/unit-of-work.ts} +716 -420
  300. package/src/query/{result-transform.test.ts → value-decoding.test.ts} +45 -298
  301. package/src/query/value-decoding.ts +113 -0
  302. package/src/query/value-encoding.test.ts +390 -0
  303. package/src/query/value-encoding.ts +168 -0
  304. package/src/schema/create.test.ts +5 -1
  305. package/src/schema/create.ts +5 -0
  306. package/src/schema/serialize.test.ts +165 -407
  307. package/src/schema/type-conversion/create-sql-type-mapper.ts +28 -0
  308. package/src/schema/type-conversion/dialect/mysql.ts +64 -0
  309. package/src/schema/type-conversion/dialect/postgres.ts +62 -0
  310. package/src/schema/type-conversion/dialect/sqlite.ts +63 -0
  311. package/src/schema/type-conversion/type-mapping.test.ts +137 -0
  312. package/src/schema/type-conversion/type-mapping.ts +153 -0
  313. package/src/shared/connection-pool.ts +5 -5
  314. package/src/sql-driver/better-sqlite3.test.ts +126 -0
  315. package/src/sql-driver/connection/connection-provider.ts +27 -0
  316. package/src/sql-driver/connection/single-connection-provider.ts +42 -0
  317. package/src/sql-driver/dialect-adapter/dialect-adapter.ts +9 -0
  318. package/src/sql-driver/dialect-adapter/sqlite-dialect-adapter.ts +7 -0
  319. package/src/sql-driver/dialects/dialects.ts +1 -0
  320. package/src/sql-driver/dialects/durable-object-dialect.ts +260 -0
  321. package/src/sql-driver/driver/runtime-driver.ts +91 -0
  322. package/src/sql-driver/query-executor/default-query-executor.ts +38 -0
  323. package/src/sql-driver/query-executor/plugin.ts +22 -0
  324. package/src/sql-driver/query-executor/query-executor-base.ts +53 -0
  325. package/src/sql-driver/query-executor/query-executor.ts +44 -0
  326. package/src/sql-driver/sql-driver-adapter.ts +96 -0
  327. package/src/sql-driver/sql-driver.ts +53 -0
  328. package/src/sql-driver/sql.ts +57 -0
  329. package/src/sql-driver/sqlocal.test.ts +117 -0
  330. package/src/with-database.ts +152 -0
  331. package/tsdown.config.ts +8 -2
  332. package/dist/adapters/drizzle/drizzle-connection-pool.js +0 -40
  333. package/dist/adapters/drizzle/drizzle-connection-pool.js.map +0 -1
  334. package/dist/adapters/drizzle/drizzle-query.d.ts +0 -23
  335. package/dist/adapters/drizzle/drizzle-query.d.ts.map +0 -1
  336. package/dist/adapters/drizzle/drizzle-query.js.map +0 -1
  337. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -10
  338. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +0 -1
  339. package/dist/adapters/drizzle/drizzle-uow-compiler.js +0 -315
  340. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +0 -1
  341. package/dist/adapters/drizzle/drizzle-uow-decoder.js +0 -116
  342. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +0 -1
  343. package/dist/adapters/drizzle/drizzle-uow-executor.js +0 -149
  344. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +0 -1
  345. package/dist/adapters/drizzle/join-column-utils.js +0 -28
  346. package/dist/adapters/drizzle/join-column-utils.js.map +0 -1
  347. package/dist/adapters/drizzle/shared.d.ts +0 -14
  348. package/dist/adapters/drizzle/shared.d.ts.map +0 -1
  349. package/dist/adapters/drizzle/shared.js +0 -35
  350. package/dist/adapters/drizzle/shared.js.map +0 -1
  351. package/dist/adapters/kysely/kysely-connection-pool.js +0 -41
  352. package/dist/adapters/kysely/kysely-connection-pool.js.map +0 -1
  353. package/dist/adapters/kysely/kysely-query-builder.js +0 -321
  354. package/dist/adapters/kysely/kysely-query-builder.js.map +0 -1
  355. package/dist/adapters/kysely/kysely-query-compiler.js +0 -66
  356. package/dist/adapters/kysely/kysely-query-compiler.js.map +0 -1
  357. package/dist/adapters/kysely/kysely-query.d.ts +0 -22
  358. package/dist/adapters/kysely/kysely-query.d.ts.map +0 -1
  359. package/dist/adapters/kysely/kysely-query.js +0 -223
  360. package/dist/adapters/kysely/kysely-query.js.map +0 -1
  361. package/dist/adapters/kysely/kysely-shared.d.ts.map +0 -1
  362. package/dist/adapters/kysely/kysely-shared.js +0 -18
  363. package/dist/adapters/kysely/kysely-shared.js.map +0 -1
  364. package/dist/adapters/kysely/kysely-uow-compiler.js +0 -170
  365. package/dist/adapters/kysely/kysely-uow-compiler.js.map +0 -1
  366. package/dist/adapters/kysely/kysely-uow-executor.js +0 -89
  367. package/dist/adapters/kysely/kysely-uow-executor.js.map +0 -1
  368. package/dist/adapters/kysely/migration/execute-base.js +0 -128
  369. package/dist/adapters/kysely/migration/execute-base.js.map +0 -1
  370. package/dist/adapters/kysely/migration/execute-factory.js +0 -34
  371. package/dist/adapters/kysely/migration/execute-factory.js.map +0 -1
  372. package/dist/adapters/kysely/migration/execute-mssql.js +0 -112
  373. package/dist/adapters/kysely/migration/execute-mssql.js.map +0 -1
  374. package/dist/adapters/kysely/migration/execute-mysql.js +0 -93
  375. package/dist/adapters/kysely/migration/execute-mysql.js.map +0 -1
  376. package/dist/adapters/kysely/migration/execute-postgres.js +0 -104
  377. package/dist/adapters/kysely/migration/execute-postgres.js.map +0 -1
  378. package/dist/adapters/kysely/migration/execute-sqlite.js +0 -123
  379. package/dist/adapters/kysely/migration/execute-sqlite.js.map +0 -1
  380. package/dist/adapters/kysely/migration/execute.js +0 -34
  381. package/dist/adapters/kysely/migration/execute.js.map +0 -1
  382. package/dist/bind-services.d.ts +0 -7
  383. package/dist/bind-services.d.ts.map +0 -1
  384. package/dist/bind-services.js +0 -14
  385. package/dist/bind-services.js.map +0 -1
  386. package/dist/fragment.d.ts +0 -173
  387. package/dist/fragment.d.ts.map +0 -1
  388. package/dist/fragment.js +0 -191
  389. package/dist/fragment.js.map +0 -1
  390. package/dist/migration-engine/create.d.ts +0 -37
  391. package/dist/migration-engine/create.d.ts.map +0 -1
  392. package/dist/migration-engine/create.js +0 -58
  393. package/dist/migration-engine/create.js.map +0 -1
  394. package/dist/migration-engine/shared.d.ts +0 -112
  395. package/dist/migration-engine/shared.d.ts.map +0 -1
  396. package/dist/query/query.d.ts.map +0 -1
  397. package/dist/query/result-transform.js +0 -168
  398. package/dist/query/result-transform.js.map +0 -1
  399. package/dist/query/unit-of-work.d.ts.map +0 -1
  400. package/dist/query/unit-of-work.js.map +0 -1
  401. package/dist/schema/serialize.js +0 -106
  402. package/dist/schema/serialize.js.map +0 -1
  403. package/dist/shared/settings-schema.js +0 -36
  404. package/dist/shared/settings-schema.js.map +0 -1
  405. package/src/adapters/drizzle/drizzle-adapter.test.ts +0 -170
  406. package/src/adapters/drizzle/drizzle-connection-pool.ts +0 -66
  407. package/src/adapters/drizzle/drizzle-query.test.ts +0 -499
  408. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +0 -1383
  409. package/src/adapters/drizzle/drizzle-uow-compiler.ts +0 -636
  410. package/src/adapters/drizzle/drizzle-uow-decoder.ts +0 -218
  411. package/src/adapters/drizzle/drizzle-uow-executor.ts +0 -276
  412. package/src/adapters/drizzle/join-column-utils.test.ts +0 -79
  413. package/src/adapters/drizzle/join-column-utils.ts +0 -39
  414. package/src/adapters/kysely/kysely-connection-pool.ts +0 -70
  415. package/src/adapters/kysely/kysely-query-builder.test.ts +0 -1344
  416. package/src/adapters/kysely/kysely-query-builder.ts +0 -666
  417. package/src/adapters/kysely/kysely-query-compiler.ts +0 -132
  418. package/src/adapters/kysely/kysely-query.test.ts +0 -498
  419. package/src/adapters/kysely/kysely-query.ts +0 -390
  420. package/src/adapters/kysely/kysely-shared.ts +0 -23
  421. package/src/adapters/kysely/kysely-uow-compiler.test.ts +0 -998
  422. package/src/adapters/kysely/kysely-uow-compiler.ts +0 -318
  423. package/src/adapters/kysely/kysely-uow-executor.ts +0 -145
  424. package/src/adapters/kysely/kysely-uow-joins.test.ts +0 -811
  425. package/src/adapters/kysely/migration/execute-base.ts +0 -256
  426. package/src/adapters/kysely/migration/execute-factory.ts +0 -53
  427. package/src/adapters/kysely/migration/execute-mssql.ts +0 -250
  428. package/src/adapters/kysely/migration/execute-mysql.ts +0 -211
  429. package/src/adapters/kysely/migration/execute-postgres.test.ts +0 -2657
  430. package/src/adapters/kysely/migration/execute-postgres.ts +0 -234
  431. package/src/adapters/kysely/migration/execute-sqlite.ts +0 -247
  432. package/src/adapters/kysely/migration/execute.ts +0 -50
  433. package/src/adapters/kysely/migration/kysely-migrator.test.ts +0 -261
  434. package/src/bind-services.test.ts +0 -214
  435. package/src/bind-services.ts +0 -37
  436. package/src/db-fragment.test.ts +0 -800
  437. package/src/fragment.ts +0 -727
  438. package/src/query/result-transform.ts +0 -271
  439. package/src/query/unit-of-work-multi-schema.test.ts +0 -64
  440. package/src/query/unit-of-work.test.ts +0 -943
  441. package/src/schema/serialize.ts +0 -396
  442. package/src/shared/settings-schema.ts +0 -61
  443. package/src/uow-context-integration.test.ts +0 -102
  444. package/src/uow-context.test.ts +0 -182
  445. /package/dist/query/{query.js → simple-query-interface.js} +0 -0
@@ -1,6 +1,13 @@
1
- import type { AnySchema, AnyTable, Index, IdColumn, AnyColumn, Relation } from "../schema/create";
2
- import { FragnoId } from "../schema/create";
3
- import type { Condition, ConditionBuilder } from "./condition-builder";
1
+ import type {
2
+ AnySchema,
3
+ AnyTable,
4
+ Index,
5
+ IdColumn,
6
+ AnyColumn,
7
+ Relation,
8
+ } from "../../schema/create";
9
+ import { FragnoId } from "../../schema/create";
10
+ import type { Condition, ConditionBuilder } from "../condition-builder";
4
11
  import type {
5
12
  SelectClause,
6
13
  TableToInsertValues,
@@ -8,12 +15,13 @@ import type {
8
15
  SelectResult,
9
16
  ExtractSelect,
10
17
  ExtractJoinOut,
11
- } from "./query";
12
- import { buildCondition } from "./condition-builder";
13
- import type { CompiledJoin } from "./orm/orm";
14
- import type { CursorResult } from "./cursor";
15
- import { Cursor } from "./cursor";
16
- import type { Prettify } from "../util/types";
18
+ } from "../simple-query-interface";
19
+ import { buildCondition } from "../condition-builder";
20
+ import type { CompiledJoin } from "../orm/orm";
21
+ import type { CursorResult } from "../cursor";
22
+ import { Cursor } from "../cursor";
23
+ import type { Prettify } from "../../util/types";
24
+ import type { TriggeredHook, TriggerHookOptions, HooksMap, HookPayload } from "../../hooks/hooks";
17
25
 
18
26
  /**
19
27
  * Builder for updateMany operations that supports both whereIndex and set chaining
@@ -157,6 +165,7 @@ export type RetrievalOperation<
157
165
  indexName: string;
158
166
  options: FindOptions<TTable, SelectClause<TTable>>;
159
167
  withCursor?: boolean;
168
+ withSingleResult?: boolean;
160
169
  }
161
170
  | {
162
171
  type: "count";
@@ -198,6 +207,13 @@ export type MutationOperation<
198
207
  table: TTable["name"];
199
208
  id: FragnoId | string;
200
209
  checkVersion: boolean;
210
+ }
211
+ | {
212
+ type: "check";
213
+ schema: TSchema;
214
+ namespace?: string;
215
+ table: TTable["name"];
216
+ id: FragnoId;
201
217
  };
202
218
 
203
219
  /**
@@ -205,12 +221,22 @@ export type MutationOperation<
205
221
  */
206
222
  export interface CompiledMutation<TOutput> {
207
223
  query: TOutput;
224
+ /**
225
+ * The type of mutation operation (create, update, delete, or check).
226
+ */
227
+ op: "create" | "update" | "delete" | "check";
208
228
  /**
209
229
  * Number of rows this operation must affect for the transaction to succeed.
210
230
  * If actual affected rows doesn't match, it indicates a version conflict.
211
231
  * null means don't check affected rows (e.g., for create operations).
212
232
  */
213
- expectedAffectedRows: number | null;
233
+ expectedAffectedRows: bigint | null;
234
+ /**
235
+ * Number of rows this SELECT query must return for the transaction to succeed.
236
+ * Used for check operations to verify version without modifying data.
237
+ * null means this is not a SELECT query that needs row count validation.
238
+ */
239
+ expectedReturnedRows: number | null;
214
240
  }
215
241
 
216
242
  /**
@@ -263,7 +289,7 @@ export interface UOWDecoder<TRawInput = unknown> {
263
289
  * @param operations - Array of retrieval operations that produced these results
264
290
  * @returns Decoded results in application format
265
291
  */
266
- (rawResults: TRawInput[], operations: RetrievalOperation<AnySchema>[]): unknown[];
292
+ decode(rawResults: TRawInput[], operations: RetrievalOperation<AnySchema>[]): unknown[];
267
293
  }
268
294
 
269
295
  /**
@@ -405,8 +431,12 @@ export class FindBuilder<
405
431
 
406
432
  /**
407
433
  * Set the number of results per page
434
+ * @throws {RangeError} If size is not a positive integer
408
435
  */
409
436
  pageSize(size: number): this {
437
+ if (!Number.isInteger(size) || size <= 0) {
438
+ throw new RangeError(`pageSize must be a positive integer, received: ${size}`);
439
+ }
410
440
  this.#pageSizeValue = size;
411
441
  return this;
412
442
  }
@@ -705,8 +735,12 @@ export class JoinFindBuilder<
705
735
 
706
736
  /**
707
737
  * Set the number of results to return
738
+ * @throws {RangeError} If size is not a positive integer
708
739
  */
709
740
  pageSize(size: number): this {
741
+ if (!Number.isInteger(size) || size <= 0) {
742
+ throw new RangeError(`pageSize must be a positive integer, received: ${size}`);
743
+ }
710
744
  this.#pageSizeValue = size;
711
745
  return this;
712
746
  }
@@ -862,13 +896,14 @@ export function buildJoinIndexed<TTable extends AnyTable, TJoinOut>(
862
896
  }
863
897
 
864
898
  /**
865
- * Base interface for Unit of Work with schema-agnostic methods only.
866
- * This allows UOW instances to be passed between services that use different schemas.
899
+ * Full Unit of Work interface with all operations including execution.
900
+ * This allows UOW instances to be passed between different contexts that use different schemas.
867
901
  */
868
- export interface IUnitOfWorkBase {
902
+ export interface IUnitOfWork {
869
903
  // Getters (schema-agnostic)
870
904
  readonly state: UOWState;
871
905
  readonly name: string | undefined;
906
+ readonly nonce: string;
872
907
  readonly retrievalPhase: Promise<unknown[]>;
873
908
  readonly mutationPhase: Promise<void>;
874
909
 
@@ -881,30 +916,249 @@ export interface IUnitOfWorkBase {
881
916
  getMutationOperations(): ReadonlyArray<MutationOperation<AnySchema>>;
882
917
  getCreatedIds(): FragnoId[];
883
918
 
919
+ // Parent-child relationships
920
+ restrict(): IUnitOfWork;
921
+
922
+ // Reset for retry support
923
+ reset(): void;
924
+
884
925
  // Schema-specific view (for cross-schema operations)
885
- forSchema<TOtherSchema extends AnySchema>(
926
+ // The optional hooks parameter is for type inference only - pass your hooks map
927
+ // to get proper typing for triggerHook. The value is not used at runtime.
928
+ forSchema<TOtherSchema extends AnySchema, TOtherHooks extends HooksMap = {}>(
886
929
  schema: TOtherSchema,
930
+ hooks?: TOtherHooks,
887
931
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
888
- ): UnitOfWorkSchemaView<TOtherSchema, [], any>;
932
+ ): TypedUnitOfWork<TOtherSchema, [], any, TOtherHooks>;
933
+
934
+ // Schema registration (for cross-fragment operations like hooks)
935
+ registerSchema(schema: AnySchema, namespace: string): void;
936
+
937
+ // Hook triggering (schema-agnostic, string-based hook names)
938
+ triggerHook(hookName: string, payload: unknown, options?: TriggerHookOptions): void;
939
+
940
+ getTriggeredHooks(): readonly TriggeredHook[];
889
941
  }
890
942
 
891
- export function createUnitOfWork<
892
- const TSchema extends AnySchema,
893
- const TRetrievalResults extends unknown[] = [],
894
- const TRawInput = unknown,
895
- >(
896
- schema: TSchema,
943
+ /**
944
+ * Restricted UOW interface without execute methods.
945
+ * Useful when you want to allow building operations but not executing them,
946
+ * to prevent deadlocks or enforce execution control at a higher level.
947
+ *
948
+ * Note: This is just a marker interface. Restriction is enforced by the UnitOfWork class itself.
949
+ */
950
+ export interface IUnitOfWorkRestricted
951
+ extends Omit<IUnitOfWork, "executeRetrieve" | "executeMutations"> {}
952
+
953
+ export function createUnitOfWork(
897
954
  compiler: UOWCompiler<unknown>,
898
- executor: UOWExecutor<unknown, TRawInput>,
899
- decoder: UOWDecoder<TRawInput>,
955
+ executor: UOWExecutor<unknown, unknown>,
956
+ decoder: UOWDecoder<unknown>,
957
+ schemaNamespaceMap?: WeakMap<AnySchema, string>,
900
958
  name?: string,
901
- ): UnitOfWork<TSchema, TRetrievalResults, TRawInput> {
902
- return new UnitOfWork(schema, compiler, executor, decoder, name);
959
+ ): UnitOfWork {
960
+ return new UnitOfWork(compiler, executor, decoder, name, undefined, schemaNamespaceMap);
903
961
  }
904
962
 
905
963
  export interface UnitOfWorkConfig {
906
964
  dryRun?: boolean;
907
965
  onQuery?: (query: unknown) => void;
966
+ nonce?: string;
967
+ }
968
+
969
+ /**
970
+ * Encapsulates a promise with its resolver/rejecter functions.
971
+ * Simplifies management of deferred promises with built-in error handling.
972
+ */
973
+ class DeferredPromise<T> {
974
+ #resolve?: (value: T) => void;
975
+ #reject?: (error: Error) => void;
976
+ #promise: Promise<T>;
977
+
978
+ constructor() {
979
+ const { promise, resolve, reject } = Promise.withResolvers<T>();
980
+ this.#promise = promise;
981
+ this.#resolve = resolve;
982
+ this.#reject = reject;
983
+ // Attach no-op error handler to prevent unhandled rejection warnings
984
+ this.#promise.catch(() => {});
985
+ }
986
+
987
+ get promise(): Promise<T> {
988
+ return this.#promise;
989
+ }
990
+
991
+ resolve(value: T): void {
992
+ this.#resolve?.(value);
993
+ }
994
+
995
+ reject(error: Error): void {
996
+ this.#reject?.(error);
997
+ }
998
+
999
+ /**
1000
+ * Reset to a new promise
1001
+ */
1002
+ reset(): void {
1003
+ const { promise, resolve, reject } = Promise.withResolvers<T>();
1004
+ this.#promise = promise;
1005
+ this.#resolve = resolve;
1006
+ this.#reject = reject;
1007
+ // Attach no-op error handler to prevent unhandled rejection warnings
1008
+ this.#promise.catch(() => {});
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * Tracks readiness signals from a group of children.
1014
+ * Maintains a promise that resolves when all registered children have signaled.
1015
+ */
1016
+ class ReadinessTracker {
1017
+ #expectedCount = 0;
1018
+ #signalCount = 0;
1019
+ #resolve?: () => void;
1020
+ #promise: Promise<void> = Promise.resolve();
1021
+
1022
+ get promise(): Promise<void> {
1023
+ return this.#promise;
1024
+ }
1025
+
1026
+ /**
1027
+ * Register that we're expecting a signal from a child
1028
+ */
1029
+ registerChild(): void {
1030
+ if (this.#expectedCount === 0) {
1031
+ // First child - create new promise
1032
+ const { promise, resolve } = Promise.withResolvers<void>();
1033
+ this.#promise = promise;
1034
+ this.#resolve = resolve;
1035
+ }
1036
+ this.#expectedCount++;
1037
+ }
1038
+
1039
+ /**
1040
+ * Signal that one child is ready
1041
+ */
1042
+ signal(): void {
1043
+ this.#signalCount++;
1044
+ if (this.#signalCount >= this.#expectedCount && this.#resolve) {
1045
+ this.#resolve();
1046
+ }
1047
+ }
1048
+
1049
+ /**
1050
+ * Reset to initial state
1051
+ */
1052
+ reset(): void {
1053
+ this.#expectedCount = 0;
1054
+ this.#signalCount = 0;
1055
+ this.#resolve = undefined;
1056
+ this.#promise = Promise.resolve();
1057
+ }
1058
+ }
1059
+
1060
+ /**
1061
+ * Manages parent-child relationships and readiness coordination for Unit of Work instances.
1062
+ * This allows parent UOWs to wait for all child UOWs to signal readiness before executing phases.
1063
+ */
1064
+ class UOWChildCoordinator<TRawInput> {
1065
+ #parent: UnitOfWork<TRawInput> | null = null;
1066
+ #parentCoordinator: UOWChildCoordinator<TRawInput> | null = null;
1067
+ #children: Set<UnitOfWork<TRawInput>> = new Set();
1068
+ #isRestricted = false;
1069
+
1070
+ #retrievalTracker = new ReadinessTracker();
1071
+ #mutationTracker = new ReadinessTracker();
1072
+
1073
+ get isRestricted(): boolean {
1074
+ return this.#isRestricted;
1075
+ }
1076
+
1077
+ get parent(): UnitOfWork<TRawInput> | null {
1078
+ return this.#parent;
1079
+ }
1080
+
1081
+ get children(): ReadonlySet<UnitOfWork<TRawInput>> {
1082
+ return this.#children;
1083
+ }
1084
+
1085
+ get retrievalReadinessPromise(): Promise<void> {
1086
+ return this.#retrievalTracker.promise;
1087
+ }
1088
+
1089
+ get mutationReadinessPromise(): Promise<void> {
1090
+ return this.#mutationTracker.promise;
1091
+ }
1092
+
1093
+ /**
1094
+ * Mark this UOW as a restricted child of the given parent
1095
+ */
1096
+ setAsRestricted(
1097
+ parent: UnitOfWork<TRawInput>,
1098
+ parentCoordinator: UOWChildCoordinator<TRawInput>,
1099
+ ): void {
1100
+ this.#parent = parent;
1101
+ this.#parentCoordinator = parentCoordinator;
1102
+ this.#isRestricted = true;
1103
+ }
1104
+
1105
+ /**
1106
+ * Register a child UOW
1107
+ */
1108
+ addChild(child: UnitOfWork<TRawInput>): void {
1109
+ this.#children.add(child);
1110
+ this.#retrievalTracker.registerChild();
1111
+ this.#mutationTracker.registerChild();
1112
+ }
1113
+
1114
+ /**
1115
+ * Signal that this child is ready for retrieval phase execution.
1116
+ * Only valid for restricted (child) UOWs.
1117
+ */
1118
+ signalReadyForRetrieval(): void {
1119
+ if (!this.#parentCoordinator) {
1120
+ throw new Error("signalReadyForRetrieval() can only be called on restricted child UOWs");
1121
+ }
1122
+
1123
+ this.#parentCoordinator.notifyChildReadyForRetrieval();
1124
+ }
1125
+
1126
+ /**
1127
+ * Signal that this child is ready for mutation phase execution.
1128
+ * Only valid for restricted (child) UOWs.
1129
+ */
1130
+ signalReadyForMutation(): void {
1131
+ if (!this.#parentCoordinator) {
1132
+ throw new Error("signalReadyForMutation() can only be called on restricted child UOWs");
1133
+ }
1134
+
1135
+ this.#parentCoordinator.notifyChildReadyForMutation();
1136
+ }
1137
+
1138
+ /**
1139
+ * Notify this coordinator that a child is ready for retrieval (internal use).
1140
+ * Called by child UOWs when they signal readiness.
1141
+ */
1142
+ notifyChildReadyForRetrieval(): void {
1143
+ this.#retrievalTracker.signal();
1144
+ }
1145
+
1146
+ /**
1147
+ * Notify this coordinator that a child is ready for mutation (internal use).
1148
+ * Called by child UOWs when they signal readiness.
1149
+ */
1150
+ notifyChildReadyForMutation(): void {
1151
+ this.#mutationTracker.signal();
1152
+ }
1153
+
1154
+ /**
1155
+ * Reset coordination state for retry support
1156
+ */
1157
+ reset(): void {
1158
+ this.#children.clear();
1159
+ this.#retrievalTracker.reset();
1160
+ this.#mutationTracker.reset();
1161
+ }
908
1162
  }
909
1163
 
910
1164
  /**
@@ -914,19 +1168,22 @@ export interface UnitOfWorkConfig {
914
1168
  * 1. Retrieval phase: Read operations to fetch entities with their versions
915
1169
  * 2. Mutation phase: Write operations that check versions before committing
916
1170
  *
1171
+ * This is the untyped base storage. Use TypedUnitOfWork for type-safe operations.
1172
+ *
917
1173
  * @example
918
1174
  * ```ts
919
1175
  * const uow = queryEngine.createUnitOfWork("update-user-balance");
1176
+ * const typedUow = uow.forSchema(mySchema);
920
1177
  *
921
1178
  * // Retrieval phase
922
- * uow.find("users", (b) => b.where("primary", (eb) => eb("id", "=", userId)));
1179
+ * typedUow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId)));
923
1180
  *
924
1181
  * // Execute retrieval and transition to mutation phase
925
1182
  * const [users] = await uow.executeRetrieve();
926
1183
  *
927
1184
  * // Mutation phase with version check
928
1185
  * const user = users[0];
929
- * uow.update("users", user.id, (b) => b.set({ balance: newBalance }).check());
1186
+ * typedUow.update("users", user.id, (b) => b.set({ balance: newBalance }).check());
930
1187
  *
931
1188
  * // Execute mutations
932
1189
  * const { success } = await uow.executeMutations();
@@ -935,39 +1192,40 @@ export interface UnitOfWorkConfig {
935
1192
  * }
936
1193
  * ```
937
1194
  */
938
- export class UnitOfWork<
939
- const TSchema extends AnySchema,
940
- const TRetrievalResults extends unknown[] = [],
941
- const TRawInput = unknown,
942
- > implements IUnitOfWorkBase
943
- {
944
- #schema: TSchema;
945
-
1195
+ export class UnitOfWork<const TRawInput = unknown> implements IUnitOfWork {
946
1196
  #name?: string;
947
1197
  #config?: UnitOfWorkConfig;
1198
+ #nonce: string;
948
1199
 
949
1200
  #state: UOWState = "building-retrieval";
950
1201
 
951
- // Operations can now come from any schema
1202
+ // Operations can come from any schema
952
1203
  #retrievalOps: RetrievalOperation<AnySchema>[] = [];
953
1204
  #mutationOps: MutationOperation<AnySchema>[] = [];
954
1205
 
955
1206
  #compiler: UOWCompiler<unknown>;
956
1207
  #executor: UOWExecutor<unknown, TRawInput>;
957
1208
  #decoder: UOWDecoder<TRawInput>;
958
- #schemaNamespaceMap?: WeakMap<AnySchema, string>;
1209
+ #schemaNamespaceMap: WeakMap<AnySchema, string>;
959
1210
 
960
- #retrievalResults?: TRetrievalResults;
1211
+ #retrievalResults?: unknown[];
961
1212
  #createdInternalIds: (bigint | null)[] = [];
962
1213
 
963
1214
  // Phase coordination promises
964
- #retrievalPhaseResolve?: (value: TRetrievalResults) => void;
965
- #mutationPhaseResolve?: () => void;
966
- #retrievalPhasePromise: Promise<TRetrievalResults>;
967
- #mutationPhasePromise: Promise<void>;
1215
+ #retrievalPhaseDeferred = new DeferredPromise<unknown[]>();
1216
+ #mutationPhaseDeferred = new DeferredPromise<void>();
1217
+
1218
+ // Error tracking
1219
+ #retrievalError: Error | null = null;
1220
+ #mutationError: Error | null = null;
1221
+
1222
+ // Child coordination
1223
+ #coordinator: UOWChildCoordinator<TRawInput> = new UOWChildCoordinator();
1224
+
1225
+ // Hook triggers
1226
+ #triggeredHooks: TriggeredHook[] = [];
968
1227
 
969
1228
  constructor(
970
- schema: TSchema,
971
1229
  compiler: UOWCompiler<unknown>,
972
1230
  executor: UOWExecutor<unknown, TRawInput>,
973
1231
  decoder: UOWDecoder<TRawInput>,
@@ -975,381 +1233,233 @@ export class UnitOfWork<
975
1233
  config?: UnitOfWorkConfig,
976
1234
  schemaNamespaceMap?: WeakMap<AnySchema, string>,
977
1235
  ) {
978
- this.#schema = schema;
979
1236
  this.#compiler = compiler;
980
1237
  this.#executor = executor;
981
1238
  this.#decoder = decoder;
1239
+ this.#schemaNamespaceMap = schemaNamespaceMap ?? new WeakMap();
982
1240
  this.#name = name;
983
1241
  this.#config = config;
984
- this.#schemaNamespaceMap = schemaNamespaceMap;
985
-
986
- // Initialize phase coordination promises
987
- this.#retrievalPhasePromise = new Promise<TRetrievalResults>((resolve) => {
988
- this.#retrievalPhaseResolve = resolve;
989
- });
990
- this.#mutationPhasePromise = new Promise<void>((resolve) => {
991
- this.#mutationPhaseResolve = resolve;
992
- });
993
- }
994
-
995
- get schema(): TSchema {
996
- return this.#schema;
1242
+ this.#nonce = config?.nonce ?? crypto.randomUUID();
997
1243
  }
998
1244
 
999
- get $results(): Prettify<TRetrievalResults> {
1000
- throw new Error("type only");
1245
+ /**
1246
+ * Register a schema with its namespace for cross-fragment operations.
1247
+ * This is used for internal fragments like hooks that need to create
1248
+ * records in a different schema during the same transaction.
1249
+ */
1250
+ registerSchema(schema: AnySchema, namespace: string): void {
1251
+ this.#schemaNamespaceMap.set(schema, namespace);
1001
1252
  }
1002
1253
 
1003
1254
  /**
1004
- * Get a schema-specific view of this UOW for type-safe operations
1005
- * Returns a wrapper that uses a different schema for operations.
1255
+ * Get a schema-specific typed view of this UOW for type-safe operations.
1256
+ * Returns a wrapper that provides typed operations for the given schema.
1006
1257
  * The namespace is automatically resolved from the schema-namespace map.
1258
+ * The optional hooks parameter is for type inference only - pass your hooks map
1259
+ * to get proper typing for triggerHook. The value is not used at runtime.
1007
1260
  */
1008
- forSchema<TOtherSchema extends AnySchema>(
1261
+ forSchema<TOtherSchema extends AnySchema, TOtherHooks extends HooksMap = {}>(
1009
1262
  schema: TOtherSchema,
1010
- ): UnitOfWorkSchemaView<TOtherSchema, [], TRawInput> {
1011
- // Look up namespace from map
1012
- const resolvedNamespace = this.#schemaNamespaceMap?.get(schema);
1263
+ _hooks?: TOtherHooks,
1264
+ ): TypedUnitOfWork<TOtherSchema, [], TRawInput, TOtherHooks> {
1265
+ const resolvedNamespace = this.#schemaNamespaceMap.get(schema);
1013
1266
 
1014
- // Safe cast: UnitOfWorkSchemaView starts with empty result types
1015
- // As operations are added, the types will accumulate correctly
1016
- return new UnitOfWorkSchemaView(
1267
+ return new TypedUnitOfWork<TOtherSchema, [], TRawInput, TOtherHooks>(
1017
1268
  schema,
1018
1269
  resolvedNamespace,
1019
- this as unknown as UnitOfWork<AnySchema, unknown[], TRawInput>,
1270
+ this,
1020
1271
  );
1021
1272
  }
1022
1273
 
1023
- get state(): UOWState {
1024
- return this.#state;
1025
- }
1274
+ /**
1275
+ * Create a restricted child UOW that cannot execute phases.
1276
+ * The child shares the same operation storage but must signal readiness
1277
+ * before the parent can execute each phase.
1278
+ */
1279
+ restrict(): UnitOfWork<TRawInput> {
1280
+ const child = new UnitOfWork(
1281
+ this.#compiler,
1282
+ this.#executor,
1283
+ this.#decoder,
1284
+ this.#name,
1285
+ { ...this.#config, nonce: this.#nonce },
1286
+ this.#schemaNamespaceMap,
1287
+ );
1288
+ child.#coordinator.setAsRestricted(this, this.#coordinator);
1026
1289
 
1027
- get name(): string | undefined {
1028
- return this.#name;
1290
+ // Share state with parent
1291
+ child.#state = this.#state;
1292
+ child.#retrievalOps = this.#retrievalOps;
1293
+ child.#mutationOps = this.#mutationOps;
1294
+ child.#retrievalResults = this.#retrievalResults;
1295
+ child.#createdInternalIds = this.#createdInternalIds;
1296
+ child.#retrievalPhaseDeferred = this.#retrievalPhaseDeferred;
1297
+ child.#mutationPhaseDeferred = this.#mutationPhaseDeferred;
1298
+ child.#retrievalError = this.#retrievalError;
1299
+ child.#mutationError = this.#mutationError;
1300
+ child.#triggeredHooks = this.#triggeredHooks;
1301
+
1302
+ this.#coordinator.addChild(child);
1303
+
1304
+ // For synchronous usage (the common case), immediately signal readiness
1305
+ // This allows services called directly from handlers to work without explicit signaling
1306
+ child.signalReadyForRetrieval();
1307
+ child.signalReadyForMutation();
1308
+
1309
+ return child;
1029
1310
  }
1030
1311
 
1031
1312
  /**
1032
- * Promise that resolves when the retrieval phase is executed
1033
- * Service methods can await this to coordinate multi-phase logic
1313
+ * Signal that this child is ready for retrieval phase execution.
1314
+ * Only valid for restricted (child) UOWs.
1034
1315
  */
1035
- get retrievalPhase(): Promise<TRetrievalResults> {
1036
- return this.#retrievalPhasePromise;
1316
+ signalReadyForRetrieval(): void {
1317
+ this.#coordinator.signalReadyForRetrieval();
1037
1318
  }
1038
1319
 
1039
1320
  /**
1040
- * Promise that resolves when the mutation phase is executed
1041
- * Service methods can await this to coordinate multi-phase logic
1321
+ * Signal that this child is ready for mutation phase execution.
1322
+ * Only valid for restricted (child) UOWs.
1042
1323
  */
1043
- get mutationPhase(): Promise<void> {
1044
- return this.#mutationPhasePromise;
1324
+ signalReadyForMutation(): void {
1325
+ this.#coordinator.signalReadyForMutation();
1045
1326
  }
1046
1327
 
1047
1328
  /**
1048
- * Execute the retrieval phase and transition to mutation phase
1049
- * Returns all results from find operations
1329
+ * Reset the UOW to initial state for retry support.
1330
+ * Clears operations, resets state, and resets phase promises.
1050
1331
  */
1051
- async executeRetrieve(): Promise<TRetrievalResults> {
1052
- if (this.#state !== "building-retrieval") {
1053
- throw new Error(
1054
- `Cannot execute retrieval from state ${this.#state}. Must be in building-retrieval state.`,
1055
- );
1056
- }
1057
-
1058
- if (this.#retrievalOps.length === 0) {
1059
- this.#state = "building-mutation";
1060
- const emptyResults = [] as unknown as TRetrievalResults;
1061
- this.#retrievalPhaseResolve?.(emptyResults);
1062
- return emptyResults;
1063
- }
1064
-
1065
- // Compile retrieval operations using single compiler
1066
- const retrievalBatch: unknown[] = [];
1067
- for (const op of this.#retrievalOps) {
1068
- const compiled = this.#compiler.compileRetrievalOperation(op);
1069
- if (compiled !== null) {
1070
- this.#config?.onQuery?.(compiled);
1071
- retrievalBatch.push(compiled);
1072
- }
1332
+ reset(): void {
1333
+ if (this.#coordinator.isRestricted) {
1334
+ throw new Error("reset() cannot be called on restricted child UOWs");
1073
1335
  }
1074
1336
 
1075
- if (this.#config?.dryRun) {
1076
- this.#state = "executed";
1077
- return [] as unknown as TRetrievalResults;
1078
- }
1337
+ // Clear operations
1338
+ this.#retrievalOps = [];
1339
+ this.#mutationOps = [];
1340
+ this.#retrievalResults = undefined;
1341
+ this.#createdInternalIds = [];
1079
1342
 
1080
- // Execute all operations together (ideally in same transaction)
1081
- const rawResults = await this.#executor.executeRetrievalPhase(retrievalBatch);
1343
+ // Reset state
1344
+ this.#state = "building-retrieval";
1345
+ this.#retrievalError = null;
1346
+ this.#mutationError = null;
1082
1347
 
1083
- // Decode results using single decoder
1084
- const results = this.#decoder(rawResults, this.#retrievalOps);
1348
+ // Reset phase promises
1349
+ this.#retrievalPhaseDeferred.reset();
1350
+ this.#mutationPhaseDeferred.reset();
1085
1351
 
1086
- // Store results and transition to mutation phase
1087
- this.#retrievalResults = results as TRetrievalResults;
1088
- this.#state = "building-mutation";
1352
+ // Reset child coordination
1353
+ this.#coordinator.reset();
1089
1354
 
1090
- // Resolve the retrieval phase promise to unblock waiting service methods
1091
- this.#retrievalPhaseResolve?.(this.#retrievalResults);
1092
-
1093
- return this.#retrievalResults;
1355
+ // Reset hooks
1356
+ this.#triggeredHooks = [];
1094
1357
  }
1095
1358
 
1096
1359
  /**
1097
- * Add a find operation using a builder callback (retrieval phase only)
1360
+ * Trigger a hook to be executed after the transaction commits.
1098
1361
  */
1099
- find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1100
- tableName: TTableName,
1101
- builderFn: (
1102
- builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1103
- ) => TBuilderResult,
1104
- ): UnitOfWork<
1105
- TSchema,
1106
- [
1107
- ...TRetrievalResults,
1108
- SelectResult<
1109
- TSchema["tables"][TTableName],
1110
- ExtractJoinOut<TBuilderResult>,
1111
- Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1112
- >[],
1113
- ],
1114
- TRawInput
1115
- >;
1116
- find<TTableName extends keyof TSchema["tables"] & string>(
1117
- tableName: TTableName,
1118
- ): UnitOfWork<
1119
- TSchema,
1120
- [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], {}, true>[]],
1121
- TRawInput
1122
- >;
1123
- find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1124
- tableName: TTableName,
1125
- builderFn?: (
1126
- builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1127
- ) => TBuilderResult,
1128
- ): UnitOfWork<
1129
- TSchema,
1130
- [
1131
- ...TRetrievalResults,
1132
- SelectResult<
1133
- TSchema["tables"][TTableName],
1134
- ExtractJoinOut<TBuilderResult>,
1135
- Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1136
- >[],
1137
- ],
1138
- TRawInput
1139
- > {
1140
- if (this.#state !== "building-retrieval") {
1141
- throw new Error(
1142
- `find() can only be called during retrieval phase. Current state: ${this.#state}`,
1143
- );
1144
- }
1145
-
1146
- const table = this.#schema.tables[tableName];
1147
- if (!table) {
1148
- throw new Error(`Table ${tableName} not found in schema`);
1149
- }
1150
-
1151
- // Create builder, pass to callback (or use default), then extract configuration
1152
- const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1153
- if (builderFn) {
1154
- builderFn(builder);
1155
- } else {
1156
- // Default to primary index with no filter
1157
- builder.whereIndex("primary");
1158
- }
1159
- const { indexName, options, type } = builder.build();
1160
-
1161
- this.#retrievalOps.push({
1162
- type,
1163
- schema: this.#schema,
1164
- // Safe: we know the table is part of the schema from the find() method
1165
- table: table as TSchema["tables"][TTableName],
1166
- indexName,
1167
- // Safe: we're storing the options for later compilation
1168
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1169
- options: options as any,
1362
+ triggerHook(hookName: string, payload: unknown, options?: TriggerHookOptions): void {
1363
+ this.#triggeredHooks.push({
1364
+ hookName,
1365
+ payload,
1366
+ options,
1170
1367
  });
1171
-
1172
- // Safe: return type is correctly specified in the method signature
1173
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1174
- return this as any;
1175
1368
  }
1176
1369
 
1177
1370
  /**
1178
- * Add a find operation with cursor metadata (retrieval phase only)
1371
+ * Get all triggered hooks for this UOW.
1179
1372
  */
1180
- findWithCursor<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1181
- tableName: TTableName,
1182
- builderFn: (
1183
- // We omit "build" because we don't want to expose it to the user
1184
- builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1185
- ) => TBuilderResult,
1186
- ): UnitOfWork<
1187
- TSchema,
1188
- [
1189
- ...TRetrievalResults,
1190
- CursorResult<
1191
- SelectResult<
1192
- TSchema["tables"][TTableName],
1193
- ExtractJoinOut<TBuilderResult>,
1194
- Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1195
- >
1196
- >,
1197
- ],
1198
- TRawInput
1199
- > {
1200
- if (this.#state !== "building-retrieval") {
1201
- throw new Error(
1202
- `findWithCursor() can only be called during retrieval phase. Current state: ${this.#state}`,
1203
- );
1204
- }
1373
+ getTriggeredHooks(): ReadonlyArray<TriggeredHook> {
1374
+ return this.#triggeredHooks;
1375
+ }
1205
1376
 
1206
- const table = this.#schema.tables[tableName];
1207
- if (!table) {
1208
- throw new Error(`Table ${tableName} not found in schema`);
1209
- }
1377
+ get state(): UOWState {
1378
+ return this.#state;
1379
+ }
1210
1380
 
1211
- // Create builder and pass to callback
1212
- const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1213
- builderFn(builder);
1214
- const { indexName, options, type } = builder.build();
1381
+ get name(): string | undefined {
1382
+ return this.#name;
1383
+ }
1215
1384
 
1216
- this.#retrievalOps.push({
1217
- type,
1218
- schema: this.#schema,
1219
- // Safe: we know the table is part of the schema from the findWithCursor() method
1220
- table: table as TSchema["tables"][TTableName],
1221
- indexName,
1222
- // Safe: we're storing the options for later compilation
1223
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1224
- options: options as any,
1225
- withCursor: true,
1226
- });
1385
+ get nonce(): string {
1386
+ return this.#nonce;
1387
+ }
1227
1388
 
1228
- // Safe: return type is correctly specified in the method signature
1229
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1230
- return this as any;
1389
+ /**
1390
+ * Promise that resolves when the retrieval phase is executed
1391
+ * Service methods can await this to coordinate multi-phase logic
1392
+ */
1393
+ get retrievalPhase(): Promise<unknown[]> {
1394
+ return this.#retrievalPhaseDeferred.promise;
1231
1395
  }
1232
1396
 
1233
1397
  /**
1234
- * Add a create operation (mutation phase only)
1235
- * Returns a FragnoId with the external ID that can be used immediately in subsequent operations
1398
+ * Promise that resolves when the mutation phase is executed
1399
+ * Service methods can await this to coordinate multi-phase logic
1236
1400
  */
1237
- create<TableName extends keyof TSchema["tables"] & string>(
1238
- table: TableName,
1239
- values: TableToInsertValues<TSchema["tables"][TableName]>,
1240
- ): FragnoId {
1241
- if (this.#state === "executed") {
1242
- throw new Error(`create() can only be called during mutation phase.`);
1243
- }
1401
+ get mutationPhase(): Promise<void> {
1402
+ return this.#mutationPhaseDeferred.promise;
1403
+ }
1244
1404
 
1245
- const tableSchema = this.#schema.tables[table];
1246
- if (!tableSchema) {
1247
- throw new Error(`Table ${table} not found in schema`);
1405
+ /**
1406
+ * Execute the retrieval phase and transition to mutation phase
1407
+ * Returns all results from find operations
1408
+ */
1409
+ async executeRetrieve(): Promise<unknown[]> {
1410
+ if (this.#coordinator.isRestricted) {
1411
+ throw new Error("executeRetrieve() cannot be called on restricted child UOWs");
1248
1412
  }
1249
1413
 
1250
- const idColumn = tableSchema.getIdColumn();
1251
- let externalId: string;
1252
- let updatedValues = values;
1414
+ if (this.#state !== "building-retrieval") {
1415
+ throw new Error(
1416
+ `Cannot execute retrieval from state ${this.#state}. Must be in building-retrieval state.`,
1417
+ );
1418
+ }
1253
1419
 
1254
- // Check if ID value is provided in values
1255
- const providedIdValue = (values as Record<string, unknown>)[idColumn.ormName];
1420
+ try {
1421
+ // Wait for all children to signal readiness
1422
+ await this.#coordinator.retrievalReadinessPromise;
1256
1423
 
1257
- if (providedIdValue !== undefined) {
1258
- // Extract string from FragnoId or use string directly
1259
- if (
1260
- typeof providedIdValue === "object" &&
1261
- providedIdValue !== null &&
1262
- "externalId" in providedIdValue
1263
- ) {
1264
- externalId = (providedIdValue as FragnoId).externalId;
1265
- } else {
1266
- externalId = providedIdValue as string;
1267
- }
1268
- } else {
1269
- // Generate using the column's default configuration
1270
- const generated = idColumn.generateDefaultValue();
1271
- if (generated === undefined) {
1272
- throw new Error(
1273
- `No ID value provided and ID column ${idColumn.ormName} has no default generator`,
1274
- );
1424
+ if (this.#retrievalOps.length === 0) {
1425
+ this.#state = "building-mutation";
1426
+ const emptyResults: unknown[] = [];
1427
+ this.#retrievalPhaseDeferred.resolve(emptyResults);
1428
+ return emptyResults;
1275
1429
  }
1276
- externalId = generated as string;
1277
1430
 
1278
- // Add the generated ID to values so it's used in the insert
1279
- updatedValues = {
1280
- ...values,
1281
- [idColumn.ormName]: externalId,
1282
- } as TableToInsertValues<TSchema["tables"][TableName]>;
1283
- }
1431
+ // Compile retrieval operations using single compiler
1432
+ const retrievalBatch: unknown[] = [];
1433
+ for (const op of this.#retrievalOps) {
1434
+ const compiled = this.#compiler.compileRetrievalOperation(op);
1435
+ if (compiled !== null) {
1436
+ this.#config?.onQuery?.(compiled);
1437
+ retrievalBatch.push(compiled);
1438
+ }
1439
+ }
1284
1440
 
1285
- this.#mutationOps.push({
1286
- type: "create",
1287
- schema: this.#schema,
1288
- table,
1289
- values: updatedValues,
1290
- generatedExternalId: externalId,
1291
- });
1441
+ if (this.#config?.dryRun) {
1442
+ this.#state = "executed";
1443
+ const emptyResults: unknown[] = [];
1444
+ this.#retrievalPhaseDeferred.resolve(emptyResults);
1445
+ return emptyResults;
1446
+ }
1292
1447
 
1293
- return FragnoId.fromExternal(externalId, 0);
1294
- }
1448
+ const rawResults = await this.#executor.executeRetrievalPhase(retrievalBatch);
1295
1449
 
1296
- /**
1297
- * Add an update operation using a builder callback (mutation phase only)
1298
- */
1299
- update<TableName extends keyof TSchema["tables"] & string>(
1300
- table: TableName,
1301
- id: FragnoId | string,
1302
- builderFn: (
1303
- // We omit "build" because we don't want to expose it to the user
1304
- builder: Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build">,
1305
- ) => Omit<UpdateBuilder<TSchema["tables"][TableName]>, "build"> | void,
1306
- ): void {
1307
- if (this.#state === "executed") {
1308
- throw new Error(`update() can only be called during mutation phase.`);
1309
- }
1450
+ const results = this.#decoder.decode(rawResults, this.#retrievalOps);
1310
1451
 
1311
- // Create builder, pass to callback, then extract configuration
1312
- const builder = new UpdateBuilder<TSchema["tables"][TableName]>(table, id);
1313
- builderFn(builder);
1314
- const { id: opId, checkVersion, set } = builder.build();
1452
+ // Store results and transition to mutation phase
1453
+ this.#retrievalResults = results;
1454
+ this.#state = "building-mutation";
1315
1455
 
1316
- this.#mutationOps.push({
1317
- type: "update",
1318
- schema: this.#schema,
1319
- table,
1320
- id: opId,
1321
- checkVersion,
1322
- set,
1323
- });
1324
- }
1456
+ this.#retrievalPhaseDeferred.resolve(this.#retrievalResults);
1325
1457
 
1326
- /**
1327
- * Add a delete operation using a builder callback (mutation phase only)
1328
- */
1329
- delete<TableName extends keyof TSchema["tables"] & string>(
1330
- table: TableName,
1331
- id: FragnoId | string,
1332
- builderFn?: (
1333
- // We omit "build" because we don't want to expose it to the user
1334
- builder: Omit<DeleteBuilder, "build">,
1335
- ) => Omit<DeleteBuilder, "build"> | void,
1336
- ): void {
1337
- if (this.#state === "executed") {
1338
- throw new Error(`delete() can only be called during mutation phase.`);
1458
+ return this.#retrievalResults;
1459
+ } catch (error) {
1460
+ this.#retrievalError = error instanceof Error ? error : new Error(String(error));
1461
+ throw error;
1339
1462
  }
1340
-
1341
- // Create builder, optionally pass to callback, then extract configuration
1342
- const builder = new DeleteBuilder(table, id);
1343
- builderFn?.(builder);
1344
- const { id: opId, checkVersion } = builder.build();
1345
-
1346
- this.#mutationOps.push({
1347
- type: "delete",
1348
- schema: this.#schema,
1349
- table,
1350
- id: opId,
1351
- checkVersion,
1352
- });
1353
1463
  }
1354
1464
 
1355
1465
  /**
@@ -1357,41 +1467,54 @@ export class UnitOfWork<
1357
1467
  * Returns success flag indicating if mutations completed without conflicts
1358
1468
  */
1359
1469
  async executeMutations(): Promise<{ success: boolean }> {
1470
+ if (this.#coordinator.isRestricted) {
1471
+ throw new Error("executeMutations() cannot be called on restricted child UOWs");
1472
+ }
1473
+
1360
1474
  if (this.#state === "executed") {
1361
1475
  throw new Error(`Cannot execute mutations from state ${this.#state}.`);
1362
1476
  }
1363
1477
 
1364
- // Compile mutation operations using single compiler
1365
- const mutationBatch: CompiledMutation<unknown>[] = [];
1366
- for (const op of this.#mutationOps) {
1367
- const compiled = this.#compiler.compileMutationOperation(op);
1368
- if (compiled !== null) {
1369
- this.#config?.onQuery?.(compiled);
1370
- mutationBatch.push(compiled);
1478
+ try {
1479
+ // Wait for all children to signal readiness
1480
+ await this.#coordinator.mutationReadinessPromise;
1481
+
1482
+ // Compile mutation operations using single compiler
1483
+ const mutationBatch: CompiledMutation<unknown>[] = [];
1484
+ for (const op of this.#mutationOps) {
1485
+ const compiled = this.#compiler.compileMutationOperation(op);
1486
+ if (compiled !== null) {
1487
+ this.#config?.onQuery?.(compiled);
1488
+ mutationBatch.push(compiled);
1489
+ }
1371
1490
  }
1372
- }
1373
1491
 
1374
- if (this.#config?.dryRun) {
1375
- this.#state = "executed";
1376
- return {
1377
- success: true,
1378
- };
1379
- }
1492
+ if (this.#config?.dryRun) {
1493
+ this.#state = "executed";
1494
+ this.#mutationPhaseDeferred.resolve();
1495
+ return {
1496
+ success: true,
1497
+ };
1498
+ }
1380
1499
 
1381
- // Execute mutation phase
1382
- const result = await this.#executor.executeMutationPhase(mutationBatch);
1383
- this.#state = "executed";
1500
+ // Execute mutation phase
1501
+ const result = await this.#executor.executeMutationPhase(mutationBatch);
1502
+ this.#state = "executed";
1384
1503
 
1385
- if (result.success) {
1386
- this.#createdInternalIds = result.createdInternalIds;
1387
- }
1504
+ if (result.success) {
1505
+ this.#createdInternalIds = result.createdInternalIds;
1506
+ }
1388
1507
 
1389
- // Resolve the mutation phase promise to unblock waiting service methods
1390
- this.#mutationPhaseResolve?.();
1508
+ // Resolve the mutation phase promise to unblock waiting service methods
1509
+ this.#mutationPhaseDeferred.resolve();
1391
1510
 
1392
- return {
1393
- success: result.success,
1394
- };
1511
+ return {
1512
+ success: result.success,
1513
+ };
1514
+ } catch (error) {
1515
+ this.#mutationError = error instanceof Error ? error : new Error(String(error));
1516
+ throw error;
1517
+ }
1395
1518
  }
1396
1519
 
1397
1520
  /**
@@ -1410,18 +1533,26 @@ export class UnitOfWork<
1410
1533
 
1411
1534
  /**
1412
1535
  * @internal
1413
- * Add a retrieval operation (used by SchemaView)
1536
+ * Add a retrieval operation (used by TypedUnitOfWork)
1414
1537
  */
1415
1538
  addRetrievalOperation(op: RetrievalOperation<AnySchema>): number {
1539
+ if (this.#state !== "building-retrieval") {
1540
+ throw new Error(
1541
+ `Cannot add retrieval operation in state ${this.#state}. Must be in building-retrieval state.`,
1542
+ );
1543
+ }
1416
1544
  this.#retrievalOps.push(op);
1417
1545
  return this.#retrievalOps.length - 1;
1418
1546
  }
1419
1547
 
1420
1548
  /**
1421
1549
  * @internal
1422
- * Add a mutation operation (used by SchemaView)
1550
+ * Add a mutation operation (used by TypedUnitOfWork)
1423
1551
  */
1424
1552
  addMutationOperation(op: MutationOperation<AnySchema>): void {
1553
+ if (this.#state === "executed") {
1554
+ throw new Error(`Cannot add mutation operation in executed state.`);
1555
+ }
1425
1556
  this.#mutationOps.push(op);
1426
1557
  }
1427
1558
 
@@ -1494,28 +1625,27 @@ export class UnitOfWork<
1494
1625
  }
1495
1626
 
1496
1627
  /**
1497
- * A lightweight wrapper around a parent UOW that provides type-safe operations for a different schema.
1498
- * All operations are stored in the parent UOW, but this wrapper ensures the correct schema is used.
1628
+ * A typed facade around a UnitOfWork that provides type-safe operations for a specific schema.
1629
+ * All operations are stored in the underlying UOW, but this facade ensures type safety and
1630
+ * filters retrieval results to only include operations added through this facade.
1499
1631
  */
1500
- export class UnitOfWorkSchemaView<
1632
+ export class TypedUnitOfWork<
1501
1633
  const TSchema extends AnySchema,
1502
1634
  const TRetrievalResults extends unknown[] = [],
1503
1635
  const TRawInput = unknown,
1504
- > implements IUnitOfWorkBase
1636
+ const THooks extends HooksMap = {},
1637
+ > implements IUnitOfWork
1505
1638
  {
1506
1639
  #schema: TSchema;
1507
1640
  #namespace?: string;
1508
- #parent: UnitOfWork<AnySchema, unknown[], TRawInput>;
1641
+ #uow: UnitOfWork<TRawInput>;
1509
1642
  #operationIndices: number[] = [];
1643
+ #cachedRetrievalPhase?: Promise<TRetrievalResults>;
1510
1644
 
1511
- constructor(
1512
- schema: TSchema,
1513
- namespace: string | undefined,
1514
- parent: UnitOfWork<AnySchema, unknown[], TRawInput>,
1515
- ) {
1645
+ constructor(schema: TSchema, namespace: string | undefined, uow: UnitOfWork<TRawInput>) {
1516
1646
  this.#schema = schema;
1517
1647
  this.#namespace = namespace;
1518
- this.#parent = parent;
1648
+ this.#uow = uow;
1519
1649
  }
1520
1650
 
1521
1651
  get $results(): Prettify<TRetrievalResults> {
@@ -1527,43 +1657,86 @@ export class UnitOfWorkSchemaView<
1527
1657
  }
1528
1658
 
1529
1659
  get name(): string | undefined {
1530
- return this.#parent.name;
1660
+ return this.#uow.name;
1661
+ }
1662
+
1663
+ get nonce(): string {
1664
+ return this.#uow.nonce;
1531
1665
  }
1532
1666
 
1533
1667
  get state() {
1534
- return this.#parent.state;
1668
+ return this.#uow.state;
1535
1669
  }
1536
1670
 
1537
1671
  get retrievalPhase(): Promise<TRetrievalResults> {
1538
- // Filter parent's results to only include operations from this view
1539
- return this.#parent.retrievalPhase.then((allResults) => {
1540
- const filteredResults = this.#operationIndices.map((index) => allResults[index]);
1541
- return filteredResults as TRetrievalResults;
1542
- });
1672
+ // Cache the filtered promise to avoid recreating it on every access
1673
+ if (!this.#cachedRetrievalPhase) {
1674
+ this.#cachedRetrievalPhase = this.#uow.retrievalPhase.then((allResults) => {
1675
+ const allOperations = this.#uow.getRetrievalOperations();
1676
+ const filteredResults = this.#operationIndices.map((opIndex) => {
1677
+ const result = allResults[opIndex];
1678
+ const operation = allOperations[opIndex];
1679
+ // Transform array to single item for findFirst operations
1680
+ if (operation?.type === "find" && operation.withSingleResult) {
1681
+ return Array.isArray(result) ? (result[0] ?? null) : result;
1682
+ }
1683
+ return result;
1684
+ });
1685
+ return filteredResults as TRetrievalResults;
1686
+ });
1687
+ }
1688
+ return this.#cachedRetrievalPhase;
1543
1689
  }
1544
1690
 
1545
1691
  get mutationPhase(): Promise<void> {
1546
- return this.#parent.mutationPhase;
1692
+ return this.#uow.mutationPhase;
1547
1693
  }
1548
1694
 
1549
1695
  getRetrievalOperations() {
1550
- return this.#parent.getRetrievalOperations();
1696
+ return this.#uow.getRetrievalOperations();
1551
1697
  }
1552
1698
 
1553
1699
  getMutationOperations() {
1554
- return this.#parent.getMutationOperations();
1700
+ return this.#uow.getMutationOperations();
1555
1701
  }
1556
1702
 
1557
1703
  getCreatedIds() {
1558
- return this.#parent.getCreatedIds();
1704
+ return this.#uow.getCreatedIds();
1559
1705
  }
1560
1706
 
1561
- async executeRetrieve(): Promise<unknown[]> {
1562
- return this.#parent.executeRetrieve();
1707
+ async executeRetrieve(): Promise<TRetrievalResults> {
1708
+ return this.#uow.executeRetrieve() as Promise<TRetrievalResults>;
1563
1709
  }
1564
1710
 
1565
1711
  async executeMutations(): Promise<{ success: boolean }> {
1566
- return this.#parent.executeMutations();
1712
+ return this.#uow.executeMutations();
1713
+ }
1714
+
1715
+ restrict(): IUnitOfWork {
1716
+ return this.#uow.restrict();
1717
+ }
1718
+
1719
+ reset(): void {
1720
+ return this.#uow.reset();
1721
+ }
1722
+
1723
+ forSchema<TOtherSchema extends AnySchema, TOtherHooks extends HooksMap = {}>(
1724
+ schema: TOtherSchema,
1725
+ hooks?: TOtherHooks,
1726
+ ): TypedUnitOfWork<TOtherSchema, [], TRawInput, TOtherHooks> {
1727
+ return this.#uow.forSchema<TOtherSchema, TOtherHooks>(schema, hooks);
1728
+ }
1729
+
1730
+ registerSchema(schema: AnySchema, namespace: string): void {
1731
+ this.#uow.registerSchema(schema, namespace);
1732
+ }
1733
+
1734
+ compile<TOutput>(compiler: UOWCompiler<TOutput>): {
1735
+ name?: string;
1736
+ retrievalBatch: TOutput[];
1737
+ mutationBatch: CompiledMutation<TOutput>[];
1738
+ } {
1739
+ return this.#uow.compile(compiler);
1567
1740
  }
1568
1741
 
1569
1742
  find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
@@ -1571,7 +1744,7 @@ export class UnitOfWorkSchemaView<
1571
1744
  builderFn: (
1572
1745
  builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1573
1746
  ) => TBuilderResult,
1574
- ): UnitOfWorkSchemaView<
1747
+ ): TypedUnitOfWork<
1575
1748
  TSchema,
1576
1749
  [
1577
1750
  ...TRetrievalResults,
@@ -1581,21 +1754,23 @@ export class UnitOfWorkSchemaView<
1581
1754
  Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1582
1755
  >[],
1583
1756
  ],
1584
- TRawInput
1757
+ TRawInput,
1758
+ THooks
1585
1759
  >;
1586
1760
  find<TTableName extends keyof TSchema["tables"] & string>(
1587
1761
  tableName: TTableName,
1588
- ): UnitOfWorkSchemaView<
1762
+ ): TypedUnitOfWork<
1589
1763
  TSchema,
1590
1764
  [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], {}, true>[]],
1591
- TRawInput
1765
+ TRawInput,
1766
+ THooks
1592
1767
  >;
1593
1768
  find<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1594
1769
  tableName: TTableName,
1595
1770
  builderFn?: (
1596
1771
  builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1597
1772
  ) => TBuilderResult,
1598
- ): UnitOfWorkSchemaView<
1773
+ ): TypedUnitOfWork<
1599
1774
  TSchema,
1600
1775
  [
1601
1776
  ...TRetrievalResults,
@@ -1605,7 +1780,8 @@ export class UnitOfWorkSchemaView<
1605
1780
  Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1606
1781
  >[],
1607
1782
  ],
1608
- TRawInput
1783
+ TRawInput,
1784
+ THooks
1609
1785
  > {
1610
1786
  const table = this.#schema.tables[tableName];
1611
1787
  if (!table) {
@@ -1620,7 +1796,7 @@ export class UnitOfWorkSchemaView<
1620
1796
  }
1621
1797
  const { indexName, options, type } = builder.build();
1622
1798
 
1623
- const operationIndex = this.#parent.addRetrievalOperation({
1799
+ const operationIndex = this.#uow.addRetrievalOperation({
1624
1800
  type,
1625
1801
  schema: this.#schema,
1626
1802
  namespace: this.#namespace,
@@ -1638,12 +1814,90 @@ export class UnitOfWorkSchemaView<
1638
1814
  return this as any;
1639
1815
  }
1640
1816
 
1817
+ findFirst<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1818
+ tableName: TTableName,
1819
+ builderFn: (
1820
+ builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1821
+ ) => TBuilderResult,
1822
+ ): TypedUnitOfWork<
1823
+ TSchema,
1824
+ [
1825
+ ...TRetrievalResults,
1826
+ SelectResult<
1827
+ TSchema["tables"][TTableName],
1828
+ ExtractJoinOut<TBuilderResult>,
1829
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1830
+ > | null,
1831
+ ],
1832
+ TRawInput,
1833
+ THooks
1834
+ >;
1835
+ findFirst<TTableName extends keyof TSchema["tables"] & string>(
1836
+ tableName: TTableName,
1837
+ ): TypedUnitOfWork<
1838
+ TSchema,
1839
+ [...TRetrievalResults, SelectResult<TSchema["tables"][TTableName], {}, true> | null],
1840
+ TRawInput,
1841
+ THooks
1842
+ >;
1843
+ findFirst<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1844
+ tableName: TTableName,
1845
+ builderFn?: (
1846
+ builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1847
+ ) => TBuilderResult,
1848
+ ): TypedUnitOfWork<
1849
+ TSchema,
1850
+ [
1851
+ ...TRetrievalResults,
1852
+ SelectResult<
1853
+ TSchema["tables"][TTableName],
1854
+ ExtractJoinOut<TBuilderResult>,
1855
+ Extract<ExtractSelect<TBuilderResult>, SelectClause<TSchema["tables"][TTableName]>>
1856
+ > | null,
1857
+ ],
1858
+ TRawInput,
1859
+ THooks
1860
+ > {
1861
+ const table = this.#schema.tables[tableName];
1862
+ if (!table) {
1863
+ throw new Error(`Table ${tableName} not found in schema`);
1864
+ }
1865
+
1866
+ const builder = new FindBuilder(tableName, table as TSchema["tables"][TTableName]);
1867
+ if (builderFn) {
1868
+ builderFn(builder);
1869
+ } else {
1870
+ builder.whereIndex("primary");
1871
+ }
1872
+ // Automatically set pageSize to 1 for findFirst
1873
+ builder.pageSize(1);
1874
+ const { indexName, options, type } = builder.build();
1875
+
1876
+ const operationIndex = this.#uow.addRetrievalOperation({
1877
+ type,
1878
+ schema: this.#schema,
1879
+ namespace: this.#namespace,
1880
+ table: table as TSchema["tables"][TTableName],
1881
+ indexName,
1882
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1883
+ options: options as any,
1884
+ withSingleResult: true,
1885
+ });
1886
+
1887
+ // Track which operation index belongs to this view
1888
+ this.#operationIndices.push(operationIndex);
1889
+
1890
+ // Safe: return type is correctly specified in the method signature
1891
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1892
+ return this as any;
1893
+ }
1894
+
1641
1895
  findWithCursor<TTableName extends keyof TSchema["tables"] & string, const TBuilderResult>(
1642
1896
  tableName: TTableName,
1643
1897
  builderFn: (
1644
1898
  builder: Omit<FindBuilder<TSchema["tables"][TTableName]>, "build">,
1645
1899
  ) => TBuilderResult,
1646
- ): UnitOfWorkSchemaView<
1900
+ ): TypedUnitOfWork<
1647
1901
  TSchema,
1648
1902
  [
1649
1903
  ...TRetrievalResults,
@@ -1655,7 +1909,8 @@ export class UnitOfWorkSchemaView<
1655
1909
  >
1656
1910
  >,
1657
1911
  ],
1658
- TRawInput
1912
+ TRawInput,
1913
+ THooks
1659
1914
  > {
1660
1915
  const table = this.#schema.tables[tableName];
1661
1916
  if (!table) {
@@ -1666,7 +1921,7 @@ export class UnitOfWorkSchemaView<
1666
1921
  builderFn(builder);
1667
1922
  const { indexName, options, type } = builder.build();
1668
1923
 
1669
- const operationIndex = this.#parent.addRetrievalOperation({
1924
+ const operationIndex = this.#uow.addRetrievalOperation({
1670
1925
  type,
1671
1926
  schema: this.#schema,
1672
1927
  namespace: this.#namespace,
@@ -1729,7 +1984,7 @@ export class UnitOfWorkSchemaView<
1729
1984
  } as TableToInsertValues<TSchema["tables"][TableName]>;
1730
1985
  }
1731
1986
 
1732
- this.#parent.addMutationOperation({
1987
+ this.#uow.addMutationOperation({
1733
1988
  type: "create",
1734
1989
  schema: this.#schema,
1735
1990
  namespace: this.#namespace,
@@ -1752,7 +2007,7 @@ export class UnitOfWorkSchemaView<
1752
2007
  builderFn(builder);
1753
2008
  const { id: opId, checkVersion, set } = builder.build();
1754
2009
 
1755
- this.#parent.addMutationOperation({
2010
+ this.#uow.addMutationOperation({
1756
2011
  type: "update",
1757
2012
  schema: this.#schema,
1758
2013
  namespace: this.#namespace,
@@ -1772,7 +2027,7 @@ export class UnitOfWorkSchemaView<
1772
2027
  builderFn?.(builder);
1773
2028
  const { id: opId, checkVersion } = builder.build();
1774
2029
 
1775
- this.#parent.addMutationOperation({
2030
+ this.#uow.addMutationOperation({
1776
2031
  type: "delete",
1777
2032
  schema: this.#schema,
1778
2033
  namespace: this.#namespace,
@@ -1782,10 +2037,51 @@ export class UnitOfWorkSchemaView<
1782
2037
  });
1783
2038
  }
1784
2039
 
1785
- forSchema<TOtherSchema extends AnySchema>(
1786
- schema: TOtherSchema,
1787
- ): UnitOfWorkSchemaView<TOtherSchema, [], TRawInput> {
1788
- // Delegate to the parent's forSchema to create a new view
1789
- return this.#parent.forSchema(schema);
2040
+ /**
2041
+ * Check that a record's version hasn't changed since retrieval.
2042
+ * This is useful for ensuring related records remain unchanged during a transaction.
2043
+ *
2044
+ * @param tableName - The table name
2045
+ * @param id - The FragnoId with version information (string IDs are not allowed)
2046
+ * @throws Error if the ID is a string without version information
2047
+ *
2048
+ * @example
2049
+ * ```ts
2050
+ * // Ensure both accounts haven't changed before creating a transfer
2051
+ * uow.check("accounts", fromAccount.id);
2052
+ * uow.check("accounts", toAccount.id);
2053
+ * uow.create("transactions", { fromAccountId, toAccountId, amount });
2054
+ * ```
2055
+ */
2056
+ check<TableName extends keyof TSchema["tables"] & string>(
2057
+ tableName: TableName,
2058
+ id: FragnoId,
2059
+ ): void {
2060
+ this.#uow.addMutationOperation({
2061
+ type: "check",
2062
+ schema: this.#schema,
2063
+ namespace: this.#namespace,
2064
+ table: tableName,
2065
+ id,
2066
+ });
2067
+ }
2068
+
2069
+ get $hooks(): THooks {
2070
+ throw new Error("type only");
2071
+ }
2072
+
2073
+ /**
2074
+ * Trigger a hook to be executed after the transaction commits.
2075
+ */
2076
+ triggerHook<K extends keyof THooks & string>(
2077
+ hookName: K,
2078
+ payload: HookPayload<THooks[K]>,
2079
+ options?: TriggerHookOptions,
2080
+ ): void {
2081
+ this.#uow.triggerHook(hookName, payload, options);
2082
+ }
2083
+
2084
+ getTriggeredHooks(): ReadonlyArray<TriggeredHook> {
2085
+ return this.#uow.getTriggeredHooks();
1790
2086
  }
1791
2087
  }