@fragno-dev/db 0.2.1 → 0.3.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 (362) hide show
  1. package/.turbo/turbo-build.log +206 -140
  2. package/CHANGELOG.md +67 -0
  3. package/README.md +30 -9
  4. package/dist/adapters/adapters.d.ts +23 -21
  5. package/dist/adapters/adapters.d.ts.map +1 -1
  6. package/dist/adapters/adapters.js.map +1 -1
  7. package/dist/adapters/generic-sql/driver-config.d.ts +16 -1
  8. package/dist/adapters/generic-sql/driver-config.d.ts.map +1 -1
  9. package/dist/adapters/generic-sql/driver-config.js +23 -1
  10. package/dist/adapters/generic-sql/driver-config.js.map +1 -1
  11. package/dist/adapters/generic-sql/generic-sql-adapter.d.ts +27 -9
  12. package/dist/adapters/generic-sql/generic-sql-adapter.d.ts.map +1 -1
  13. package/dist/adapters/generic-sql/generic-sql-adapter.js +55 -16
  14. package/dist/adapters/generic-sql/generic-sql-adapter.js.map +1 -1
  15. package/dist/adapters/generic-sql/generic-sql-uow-executor.js +129 -3
  16. package/dist/adapters/generic-sql/generic-sql-uow-executor.js.map +1 -1
  17. package/dist/adapters/generic-sql/migration/dialect/mysql.js +24 -5
  18. package/dist/adapters/generic-sql/migration/dialect/mysql.js.map +1 -1
  19. package/dist/adapters/generic-sql/migration/dialect/postgres.js +6 -5
  20. package/dist/adapters/generic-sql/migration/dialect/postgres.js.map +1 -1
  21. package/dist/adapters/generic-sql/migration/dialect/sqlite.js +21 -10
  22. package/dist/adapters/generic-sql/migration/dialect/sqlite.js.map +1 -1
  23. package/dist/adapters/generic-sql/migration/prepared-migrations.d.ts.map +1 -1
  24. package/dist/adapters/generic-sql/migration/prepared-migrations.js +8 -8
  25. package/dist/adapters/generic-sql/migration/prepared-migrations.js.map +1 -1
  26. package/dist/adapters/generic-sql/migration/sql-generator.js +74 -51
  27. package/dist/adapters/generic-sql/migration/sql-generator.js.map +1 -1
  28. package/dist/adapters/generic-sql/query/create-sql-query-compiler.js +6 -5
  29. package/dist/adapters/generic-sql/query/create-sql-query-compiler.js.map +1 -1
  30. package/dist/adapters/generic-sql/query/cursor-utils.js +42 -4
  31. package/dist/adapters/generic-sql/query/cursor-utils.js.map +1 -1
  32. package/dist/adapters/generic-sql/query/generic-sql-uow-operation-compiler.js +25 -17
  33. package/dist/adapters/generic-sql/query/generic-sql-uow-operation-compiler.js.map +1 -1
  34. package/dist/adapters/generic-sql/query/select-builder.js +5 -3
  35. package/dist/adapters/generic-sql/query/select-builder.js.map +1 -1
  36. package/dist/adapters/generic-sql/query/sql-query-compiler.js +15 -12
  37. package/dist/adapters/generic-sql/query/sql-query-compiler.js.map +1 -1
  38. package/dist/adapters/generic-sql/query/where-builder.js +38 -28
  39. package/dist/adapters/generic-sql/query/where-builder.js.map +1 -1
  40. package/dist/adapters/generic-sql/sqlite-storage.d.ts +13 -0
  41. package/dist/adapters/generic-sql/sqlite-storage.d.ts.map +1 -0
  42. package/dist/adapters/generic-sql/sqlite-storage.js +15 -0
  43. package/dist/adapters/generic-sql/sqlite-storage.js.map +1 -0
  44. package/dist/adapters/generic-sql/uow-decoder.js +7 -3
  45. package/dist/adapters/generic-sql/uow-decoder.js.map +1 -1
  46. package/dist/adapters/generic-sql/uow-encoder.js +28 -8
  47. package/dist/adapters/generic-sql/uow-encoder.js.map +1 -1
  48. package/dist/adapters/in-memory/condition-evaluator.js +131 -0
  49. package/dist/adapters/in-memory/condition-evaluator.js.map +1 -0
  50. package/dist/adapters/in-memory/errors.d.ts +13 -0
  51. package/dist/adapters/in-memory/errors.d.ts.map +1 -0
  52. package/dist/adapters/in-memory/errors.js +23 -0
  53. package/dist/adapters/in-memory/errors.js.map +1 -0
  54. package/dist/adapters/in-memory/in-memory-adapter.d.ts +27 -0
  55. package/dist/adapters/in-memory/in-memory-adapter.d.ts.map +1 -0
  56. package/dist/adapters/in-memory/in-memory-adapter.js +176 -0
  57. package/dist/adapters/in-memory/in-memory-adapter.js.map +1 -0
  58. package/dist/adapters/in-memory/in-memory-uow.js +648 -0
  59. package/dist/adapters/in-memory/in-memory-uow.js.map +1 -0
  60. package/dist/adapters/in-memory/index.d.ts +4 -0
  61. package/dist/adapters/in-memory/index.js +4 -0
  62. package/dist/adapters/in-memory/options.d.ts +28 -0
  63. package/dist/adapters/in-memory/options.d.ts.map +1 -0
  64. package/dist/adapters/in-memory/options.js +61 -0
  65. package/dist/adapters/in-memory/options.js.map +1 -0
  66. package/dist/adapters/in-memory/reference-resolution.js +26 -0
  67. package/dist/adapters/in-memory/reference-resolution.js.map +1 -0
  68. package/dist/adapters/in-memory/sorted-array-index.js +129 -0
  69. package/dist/adapters/in-memory/sorted-array-index.js.map +1 -0
  70. package/dist/adapters/in-memory/store.js +71 -0
  71. package/dist/adapters/in-memory/store.js.map +1 -0
  72. package/dist/adapters/in-memory/value-comparison.js +28 -0
  73. package/dist/adapters/in-memory/value-comparison.js.map +1 -0
  74. package/dist/adapters/shared/from-unit-of-work-compiler.js.map +1 -1
  75. package/dist/adapters/shared/uow-operation-compiler.js +11 -11
  76. package/dist/adapters/shared/uow-operation-compiler.js.map +1 -1
  77. package/dist/adapters/sql/index.d.ts +5 -0
  78. package/dist/adapters/sql/index.js +4 -0
  79. package/dist/db-fragment-definition-builder.d.ts +45 -96
  80. package/dist/db-fragment-definition-builder.d.ts.map +1 -1
  81. package/dist/db-fragment-definition-builder.js +121 -99
  82. package/dist/db-fragment-definition-builder.js.map +1 -1
  83. package/dist/dispatchers/cloudflare-do/index.d.ts +26 -0
  84. package/dist/dispatchers/cloudflare-do/index.d.ts.map +1 -0
  85. package/dist/dispatchers/cloudflare-do/index.js +63 -0
  86. package/dist/dispatchers/cloudflare-do/index.js.map +1 -0
  87. package/dist/dispatchers/node/index.d.ts +17 -0
  88. package/dist/dispatchers/node/index.d.ts.map +1 -0
  89. package/dist/dispatchers/node/index.js +59 -0
  90. package/dist/dispatchers/node/index.js.map +1 -0
  91. package/dist/fragments/internal-fragment.d.ts +172 -9
  92. package/dist/fragments/internal-fragment.d.ts.map +1 -1
  93. package/dist/fragments/internal-fragment.js +193 -74
  94. package/dist/fragments/internal-fragment.js.map +1 -1
  95. package/dist/fragments/internal-fragment.routes.js +29 -0
  96. package/dist/fragments/internal-fragment.routes.js.map +1 -0
  97. package/dist/fragments/internal-fragment.schema.d.ts +9 -0
  98. package/dist/fragments/internal-fragment.schema.d.ts.map +1 -0
  99. package/dist/fragments/internal-fragment.schema.js +22 -0
  100. package/dist/fragments/internal-fragment.schema.js.map +1 -0
  101. package/dist/hooks/durable-hooks-processor.d.ts +14 -0
  102. package/dist/hooks/durable-hooks-processor.d.ts.map +1 -0
  103. package/dist/hooks/durable-hooks-processor.js +32 -0
  104. package/dist/hooks/durable-hooks-processor.js.map +1 -0
  105. package/dist/hooks/hooks.d.ts +47 -4
  106. package/dist/hooks/hooks.d.ts.map +1 -1
  107. package/dist/hooks/hooks.js +106 -39
  108. package/dist/hooks/hooks.js.map +1 -1
  109. package/dist/migration-engine/auto-from-schema.js +14 -11
  110. package/dist/migration-engine/auto-from-schema.js.map +1 -1
  111. package/dist/migration-engine/generation-engine.d.ts +16 -10
  112. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  113. package/dist/migration-engine/generation-engine.js +72 -33
  114. package/dist/migration-engine/generation-engine.js.map +1 -1
  115. package/dist/migration-engine/shared.js.map +1 -1
  116. package/dist/mod.d.ts +17 -10
  117. package/dist/mod.d.ts.map +1 -1
  118. package/dist/mod.js +14 -8
  119. package/dist/mod.js.map +1 -1
  120. package/dist/naming/sql-naming.d.ts +19 -0
  121. package/dist/naming/sql-naming.d.ts.map +1 -0
  122. package/dist/naming/sql-naming.js +116 -0
  123. package/dist/naming/sql-naming.js.map +1 -0
  124. package/dist/node_modules/.pnpm/{rou3@0.7.10 → rou3@0.7.12}/node_modules/rou3/dist/index.js +8 -5
  125. package/dist/node_modules/.pnpm/rou3@0.7.12/node_modules/rou3/dist/index.js.map +1 -0
  126. package/dist/outbox/outbox-builder.js +156 -0
  127. package/dist/outbox/outbox-builder.js.map +1 -0
  128. package/dist/outbox/outbox.d.ts +52 -0
  129. package/dist/outbox/outbox.d.ts.map +1 -0
  130. package/dist/outbox/outbox.js +37 -0
  131. package/dist/outbox/outbox.js.map +1 -0
  132. package/dist/packages/fragno/dist/api/fragment-definition-builder.js +3 -2
  133. package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -1
  134. package/dist/packages/fragno/dist/api/fragment-instantiator.js +164 -20
  135. package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -1
  136. package/dist/packages/fragno/dist/api/request-input-context.js +67 -0
  137. package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -1
  138. package/dist/packages/fragno/dist/api/route.js +14 -1
  139. package/dist/packages/fragno/dist/api/route.js.map +1 -1
  140. package/dist/packages/fragno/dist/internal/trace-context.js +12 -0
  141. package/dist/packages/fragno/dist/internal/trace-context.js.map +1 -0
  142. package/dist/query/column-defaults.js +20 -4
  143. package/dist/query/column-defaults.js.map +1 -1
  144. package/dist/query/cursor.d.ts +3 -1
  145. package/dist/query/cursor.d.ts.map +1 -1
  146. package/dist/query/cursor.js +45 -14
  147. package/dist/query/cursor.js.map +1 -1
  148. package/dist/query/db-now.d.ts +8 -0
  149. package/dist/query/db-now.d.ts.map +1 -0
  150. package/dist/query/db-now.js +7 -0
  151. package/dist/query/db-now.js.map +1 -0
  152. package/dist/query/serialize/create-sql-serializer.js +3 -2
  153. package/dist/query/serialize/create-sql-serializer.js.map +1 -1
  154. package/dist/query/serialize/dialect/mysql-serializer.js +12 -6
  155. package/dist/query/serialize/dialect/mysql-serializer.js.map +1 -1
  156. package/dist/query/serialize/dialect/postgres-serializer.js +25 -7
  157. package/dist/query/serialize/dialect/postgres-serializer.js.map +1 -1
  158. package/dist/query/serialize/dialect/sqlite-serializer.js +55 -11
  159. package/dist/query/serialize/dialect/sqlite-serializer.js.map +1 -1
  160. package/dist/query/serialize/sql-serializer.js +2 -2
  161. package/dist/query/serialize/sql-serializer.js.map +1 -1
  162. package/dist/query/simple-query-interface.d.ts +6 -1
  163. package/dist/query/simple-query-interface.d.ts.map +1 -1
  164. package/dist/query/unit-of-work/execute-unit-of-work.d.ts +351 -100
  165. package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -1
  166. package/dist/query/unit-of-work/execute-unit-of-work.js +440 -267
  167. package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
  168. package/dist/query/unit-of-work/unit-of-work.d.ts +67 -22
  169. package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
  170. package/dist/query/unit-of-work/unit-of-work.js +110 -13
  171. package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
  172. package/dist/query/value-decoding.js +8 -5
  173. package/dist/query/value-decoding.js.map +1 -1
  174. package/dist/query/value-encoding.js +29 -9
  175. package/dist/query/value-encoding.js.map +1 -1
  176. package/dist/schema/create.d.ts +40 -14
  177. package/dist/schema/create.d.ts.map +1 -1
  178. package/dist/schema/create.js +82 -42
  179. package/dist/schema/create.js.map +1 -1
  180. package/dist/schema/generate-id.d.ts +20 -0
  181. package/dist/schema/generate-id.d.ts.map +1 -0
  182. package/dist/schema/generate-id.js +28 -0
  183. package/dist/schema/generate-id.js.map +1 -0
  184. package/dist/schema/type-conversion/create-sql-type-mapper.js +3 -2
  185. package/dist/schema/type-conversion/create-sql-type-mapper.js.map +1 -1
  186. package/dist/schema/type-conversion/dialect/sqlite.js +9 -0
  187. package/dist/schema/type-conversion/dialect/sqlite.js.map +1 -1
  188. package/dist/schema/validator.d.ts +10 -0
  189. package/dist/schema/validator.d.ts.map +1 -0
  190. package/dist/schema/validator.js +123 -0
  191. package/dist/schema/validator.js.map +1 -0
  192. package/dist/schema-output/drizzle.d.ts +30 -0
  193. package/dist/schema-output/drizzle.d.ts.map +1 -0
  194. package/dist/{adapters/drizzle/generate.js → schema-output/drizzle.js} +82 -56
  195. package/dist/schema-output/drizzle.js.map +1 -0
  196. package/dist/schema-output/prisma.d.ts +17 -0
  197. package/dist/schema-output/prisma.d.ts.map +1 -0
  198. package/dist/schema-output/prisma.js +296 -0
  199. package/dist/schema-output/prisma.js.map +1 -0
  200. package/dist/util/default-database-adapter.js +61 -0
  201. package/dist/util/default-database-adapter.js.map +1 -0
  202. package/dist/with-database.d.ts +1 -1
  203. package/dist/with-database.d.ts.map +1 -1
  204. package/dist/with-database.js +12 -3
  205. package/dist/with-database.js.map +1 -1
  206. package/package.json +43 -28
  207. package/src/adapters/adapters.ts +30 -24
  208. package/src/adapters/drizzle/migrate-drizzle.test.ts +54 -33
  209. package/src/adapters/drizzle/migration-parity-drizzle-kit.test.ts +599 -0
  210. package/src/adapters/drizzle/test-utils.ts +12 -8
  211. package/src/adapters/generic-sql/driver-config.ts +38 -0
  212. package/src/adapters/generic-sql/generic-sql-adapter.test.ts +5 -5
  213. package/src/adapters/generic-sql/generic-sql-adapter.ts +110 -24
  214. package/src/adapters/generic-sql/generic-sql-uow-executor.test.ts +54 -0
  215. package/src/adapters/generic-sql/generic-sql-uow-executor.ts +231 -3
  216. package/src/adapters/generic-sql/migration/adapter-migration-parity.test.ts +118 -0
  217. package/src/adapters/generic-sql/migration/dialect/mysql.test.ts +26 -8
  218. package/src/adapters/generic-sql/migration/dialect/mysql.ts +46 -8
  219. package/src/adapters/generic-sql/migration/dialect/postgres.test.ts +25 -7
  220. package/src/adapters/generic-sql/migration/dialect/postgres.ts +8 -4
  221. package/src/adapters/generic-sql/migration/dialect/sqlite.test.ts +47 -8
  222. package/src/adapters/generic-sql/migration/dialect/sqlite.ts +27 -12
  223. package/src/adapters/generic-sql/migration/prepared-migrations.test.ts +128 -39
  224. package/src/adapters/generic-sql/migration/prepared-migrations.ts +15 -8
  225. package/src/adapters/generic-sql/migration/sql-generator.ts +142 -65
  226. package/src/adapters/generic-sql/query/create-sql-query-compiler.ts +9 -6
  227. package/src/adapters/generic-sql/query/cursor-utils.test.ts +271 -0
  228. package/src/adapters/generic-sql/query/cursor-utils.ts +41 -6
  229. package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.test.ts +27 -27
  230. package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.ts +38 -24
  231. package/src/adapters/generic-sql/query/select-builder.test.ts +15 -11
  232. package/src/adapters/generic-sql/query/select-builder.ts +6 -2
  233. package/src/adapters/generic-sql/query/sql-query-compiler.test.ts +52 -2
  234. package/src/adapters/generic-sql/query/sql-query-compiler.ts +50 -15
  235. package/src/adapters/generic-sql/query/where-builder.test.ts +91 -17
  236. package/src/adapters/generic-sql/query/where-builder.ts +90 -38
  237. package/src/adapters/{kysely/kysely-adapter-pglite.test.ts → generic-sql/sql-adapter-pglite-migrations.test.ts} +6 -6
  238. package/src/adapters/generic-sql/sql-adapter-pglite-pagination.test.ts +806 -0
  239. package/src/adapters/{drizzle/drizzle-adapter-pglite.test.ts → generic-sql/sql-adapter-pglite-queries.test.ts} +11 -11
  240. package/src/adapters/generic-sql/{test/generic-drizzle-adapter-sqlite3.test.ts → sql-adapter-sqlite3-driver.test.ts} +49 -35
  241. package/src/adapters/{drizzle/drizzle-adapter-sqlite3.test.ts → generic-sql/sql-adapter-sqlite3-uow.test.ts} +48 -32
  242. package/src/adapters/{kysely/kysely-adapter-sqlocal.test.ts → generic-sql/sql-adapter-sqlocal.test.ts} +6 -6
  243. package/src/adapters/generic-sql/sqlite-storage.ts +20 -0
  244. package/src/adapters/generic-sql/uow-decoder.test.ts +1 -1
  245. package/src/adapters/generic-sql/uow-decoder.ts +21 -3
  246. package/src/adapters/generic-sql/uow-encoder.test.ts +33 -2
  247. package/src/adapters/generic-sql/uow-encoder.ts +50 -11
  248. package/src/adapters/in-memory/condition-evaluator.test.ts +193 -0
  249. package/src/adapters/in-memory/condition-evaluator.ts +275 -0
  250. package/src/adapters/in-memory/errors.ts +20 -0
  251. package/src/adapters/in-memory/in-memory-adapter.ts +277 -0
  252. package/src/adapters/in-memory/in-memory-uow.mutations.test.ts +296 -0
  253. package/src/adapters/in-memory/in-memory-uow.retrieval.test.ts +100 -0
  254. package/src/adapters/in-memory/in-memory-uow.ts +1348 -0
  255. package/src/adapters/in-memory/index.ts +3 -0
  256. package/src/adapters/in-memory/options.test.ts +41 -0
  257. package/src/adapters/in-memory/options.ts +87 -0
  258. package/src/adapters/in-memory/reference-resolution.test.ts +50 -0
  259. package/src/adapters/in-memory/reference-resolution.ts +67 -0
  260. package/src/adapters/in-memory/sorted-array-index.test.ts +123 -0
  261. package/src/adapters/in-memory/sorted-array-index.ts +228 -0
  262. package/src/adapters/in-memory/store.test.ts +68 -0
  263. package/src/adapters/in-memory/store.ts +145 -0
  264. package/src/adapters/in-memory/value-comparison.ts +53 -0
  265. package/src/adapters/in-memory/value-normalization.test.ts +57 -0
  266. package/src/adapters/prisma/prisma-adapter-sqlite3.test.ts +1163 -0
  267. package/src/adapters/shared/from-unit-of-work-compiler.ts +3 -1
  268. package/src/adapters/shared/uow-operation-compiler.ts +26 -16
  269. package/src/adapters/sql/index.ts +12 -0
  270. package/src/db-fragment-definition-builder.test.ts +88 -54
  271. package/src/db-fragment-definition-builder.ts +201 -322
  272. package/src/db-fragment-instantiator.test.ts +169 -101
  273. package/src/db-fragment-integration.test.ts +301 -149
  274. package/src/dispatchers/cloudflare-do/index.test.ts +73 -0
  275. package/src/dispatchers/cloudflare-do/index.ts +104 -0
  276. package/src/dispatchers/node/index.test.ts +91 -0
  277. package/src/dispatchers/node/index.ts +87 -0
  278. package/src/fragments/internal-fragment.routes.ts +42 -0
  279. package/src/fragments/internal-fragment.schema.ts +51 -0
  280. package/src/fragments/internal-fragment.test.ts +730 -274
  281. package/src/fragments/internal-fragment.ts +447 -154
  282. package/src/hooks/durable-hooks-processor.test.ts +117 -0
  283. package/src/hooks/durable-hooks-processor.ts +67 -0
  284. package/src/hooks/hooks.test.ts +411 -259
  285. package/src/hooks/hooks.ts +265 -66
  286. package/src/migration-engine/auto-from-schema.test.ts +14 -14
  287. package/src/migration-engine/auto-from-schema.ts +5 -2
  288. package/src/migration-engine/create.test.ts +2 -2
  289. package/src/migration-engine/generation-engine.test.ts +229 -104
  290. package/src/migration-engine/generation-engine.ts +94 -64
  291. package/src/migration-engine/shared.ts +1 -0
  292. package/src/mod.ts +78 -30
  293. package/src/naming/sql-naming.ts +180 -0
  294. package/src/outbox/outbox-builder.ts +241 -0
  295. package/src/outbox/outbox.test.ts +253 -0
  296. package/src/outbox/outbox.ts +137 -0
  297. package/src/query/column-defaults.ts +41 -3
  298. package/src/query/condition-builder.test.ts +3 -3
  299. package/src/query/cursor.test.ts +116 -18
  300. package/src/query/cursor.ts +75 -26
  301. package/src/query/db-now.ts +6 -0
  302. package/src/query/query-type.test.ts +2 -2
  303. package/src/query/serialize/create-sql-serializer.ts +7 -2
  304. package/src/query/serialize/dialect/mysql-serializer.ts +12 -4
  305. package/src/query/serialize/dialect/postgres-serializer.ts +34 -4
  306. package/src/query/serialize/dialect/sqlite-serializer.test.ts +51 -1
  307. package/src/query/serialize/dialect/sqlite-serializer.ts +92 -9
  308. package/src/query/serialize/sql-serializer.ts +4 -4
  309. package/src/query/simple-query-interface.ts +5 -0
  310. package/src/query/unit-of-work/execute-unit-of-work.test.ts +1512 -1458
  311. package/src/query/unit-of-work/execute-unit-of-work.ts +1708 -596
  312. package/src/query/unit-of-work/tx-builder.test.ts +1041 -0
  313. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +32 -32
  314. package/src/query/unit-of-work/unit-of-work-types.test.ts +1 -1
  315. package/src/query/unit-of-work/unit-of-work.test.ts +231 -36
  316. package/src/query/unit-of-work/unit-of-work.ts +229 -31
  317. package/src/query/value-decoding.test.ts +13 -2
  318. package/src/query/value-decoding.ts +17 -4
  319. package/src/query/value-encoding.test.ts +85 -2
  320. package/src/query/value-encoding.ts +56 -6
  321. package/src/schema/create.test.ts +129 -42
  322. package/src/schema/create.ts +187 -47
  323. package/src/schema/generate-id.test.ts +57 -0
  324. package/src/schema/generate-id.ts +38 -0
  325. package/src/schema/serialize.test.ts +14 -2
  326. package/src/schema/type-conversion/create-sql-type-mapper.ts +7 -2
  327. package/src/schema/type-conversion/dialect/sqlite.ts +18 -0
  328. package/src/schema/type-conversion/type-mapping.test.ts +25 -1
  329. package/src/schema/validator.test.ts +197 -0
  330. package/src/schema/validator.ts +231 -0
  331. package/src/{adapters/drizzle/generate.test.ts → schema-output/drizzle.test.ts} +179 -129
  332. package/src/{adapters/drizzle/generate.ts → schema-output/drizzle.ts} +143 -93
  333. package/src/schema-output/prisma.test.ts +536 -0
  334. package/src/schema-output/prisma.ts +573 -0
  335. package/src/util/default-database-adapter.ts +106 -0
  336. package/src/with-database.ts +22 -3
  337. package/tsdown.config.ts +6 -4
  338. package/dist/adapters/drizzle/drizzle-adapter.d.ts +0 -20
  339. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +0 -1
  340. package/dist/adapters/drizzle/drizzle-adapter.js +0 -27
  341. package/dist/adapters/drizzle/drizzle-adapter.js.map +0 -1
  342. package/dist/adapters/drizzle/generate.d.ts +0 -30
  343. package/dist/adapters/drizzle/generate.d.ts.map +0 -1
  344. package/dist/adapters/drizzle/generate.js.map +0 -1
  345. package/dist/adapters/kysely/kysely-adapter.d.ts +0 -19
  346. package/dist/adapters/kysely/kysely-adapter.d.ts.map +0 -1
  347. package/dist/adapters/kysely/kysely-adapter.js +0 -17
  348. package/dist/adapters/kysely/kysely-adapter.js.map +0 -1
  349. package/dist/adapters/shared/table-name-mapper.d.ts +0 -12
  350. package/dist/adapters/shared/table-name-mapper.d.ts.map +0 -1
  351. package/dist/adapters/shared/table-name-mapper.js +0 -43
  352. package/dist/adapters/shared/table-name-mapper.js.map +0 -1
  353. package/dist/node_modules/.pnpm/rou3@0.7.10/node_modules/rou3/dist/index.js.map +0 -1
  354. package/dist/schema-generator/schema-generator.d.ts +0 -15
  355. package/dist/schema-generator/schema-generator.d.ts.map +0 -1
  356. package/src/adapters/drizzle/drizzle-adapter.ts +0 -39
  357. package/src/adapters/kysely/kysely-adapter.ts +0 -27
  358. package/src/adapters/shared/table-name-mapper.ts +0 -50
  359. package/src/schema-generator/schema-generator.ts +0 -12
  360. package/src/shared/config.ts +0 -10
  361. package/src/shared/connection-pool.ts +0 -24
  362. package/src/shared/prisma.ts +0 -45
@@ -1,331 +1,481 @@
1
1
  import type { AnySchema } from "../../schema/create";
2
2
  import type { TypedUnitOfWork, IUnitOfWork } from "./unit-of-work";
3
3
  import type { HooksMap } from "../../hooks/hooks";
4
- import { NoRetryPolicy, ExponentialBackoffRetryPolicy, type RetryPolicy } from "./retry-policy";
5
- import type { FragnoId } from "../../schema/create";
4
+ import { ExponentialBackoffRetryPolicy, NoRetryPolicy, type RetryPolicy } from "./retry-policy";
6
5
 
7
6
  /**
8
- * Error thrown when a Unit of Work execution fails due to optimistic concurrency conflict.
9
- * This error triggers automatic retry behavior in executeRestrictedUnitOfWork.
7
+ * Symbol to identify TxResult objects
10
8
  */
11
- export class ConcurrencyConflictError extends Error {
12
- constructor(message = "Optimistic concurrency conflict detected") {
13
- super(message);
14
- this.name = "ConcurrencyConflictError";
15
- }
9
+ const TX_RESULT_BRAND = Symbol("TxResult");
10
+
11
+ /**
12
+ * Check if a value is a TxResult
13
+ */
14
+ export function isTxResult(value: unknown): value is TxResult<unknown> {
15
+ return (
16
+ value !== null &&
17
+ typeof value === "object" &&
18
+ TX_RESULT_BRAND in value &&
19
+ (value as Record<symbol, boolean>)[TX_RESULT_BRAND] === true
20
+ );
16
21
  }
17
22
 
18
23
  /**
19
- * Type utility that unwraps promises 1 level deep in objects, arrays, or direct promises
20
- * Handles tuples, arrays, objects, and direct promises
24
+ * Extract the retrieve success result type from a TxResult.
25
+ * If the TxResult has retrieveSuccess, returns its return type.
26
+ * Otherwise returns the raw retrieve results type.
27
+ * Handles undefined (for optional service patterns like optionalService?.method()).
21
28
  */
22
- export type AwaitedPromisesInObject<T> =
23
- // First check if it's a Promise
24
- T extends Promise<infer U>
25
- ? Awaited<U>
26
- : // Check for arrays with known length (tuples) - preserves tuple structure
27
- T extends readonly [unknown, ...unknown[]]
28
- ? { [K in keyof T]: AwaitedPromisesInObject<T[K]> }
29
- : T extends [unknown, ...unknown[]]
30
- ? { [K in keyof T]: AwaitedPromisesInObject<T[K]> }
31
- : // Check for regular arrays (unknown length)
32
- T extends (infer U)[]
33
- ? Awaited<U>[]
34
- : T extends readonly (infer U)[]
35
- ? readonly Awaited<U>[]
36
- : // Check for objects
37
- T extends Record<string, unknown>
38
- ? {
39
- [K in keyof T]: T[K] extends Promise<infer U> ? Awaited<U> : T[K];
40
- }
41
- : // Otherwise return as-is
42
- T;
29
+ export type ExtractTxRetrieveSuccessResult<T> = T extends undefined
30
+ ? undefined
31
+ : T extends TxResult<unknown, infer R>
32
+ ? R
33
+ : never;
43
34
 
44
35
  /**
45
- * Await promises in an object 1 level deep
36
+ * Extract the final result type from a TxResult.
37
+ * Handles undefined (for optional service patterns like optionalService?.method()).
46
38
  */
47
- async function awaitPromisesInObject<T>(obj: T): Promise<AwaitedPromisesInObject<T>> {
48
- if (obj === null || obj === undefined) {
49
- return obj as AwaitedPromisesInObject<T>;
50
- }
39
+ export type ExtractTxFinalResult<T> = T extends undefined
40
+ ? undefined
41
+ : T extends TxResult<infer R, infer _>
42
+ ? R
43
+ : Awaited<T>;
51
44
 
52
- if (typeof obj !== "object") {
53
- return obj as AwaitedPromisesInObject<T>;
54
- }
45
+ /**
46
+ * Map over service calls array to extract retrieve success results from each service call.
47
+ * Preserves tuple structure while extracting the retrieve success result type from each element.
48
+ */
49
+ export type ExtractServiceRetrieveResults<T extends readonly unknown[]> = {
50
+ [K in keyof T]: ExtractTxRetrieveSuccessResult<T[K]>;
51
+ };
55
52
 
56
- // Check if it's a Promise
57
- if (obj instanceof Promise) {
58
- return (await obj) as AwaitedPromisesInObject<T>;
59
- }
53
+ /**
54
+ * Map over service calls array to extract final results from each service call.
55
+ * Preserves tuple structure while extracting the final result type from each element.
56
+ */
57
+ export type ExtractServiceFinalResults<T extends readonly unknown[]> = {
58
+ [K in keyof T]: ExtractTxFinalResult<T[K]>;
59
+ };
60
60
 
61
- // Check if it's an array
62
- if (Array.isArray(obj)) {
63
- const awaited = await Promise.all(
64
- obj.map((item) => (item instanceof Promise ? item : Promise.resolve(item))),
65
- );
66
- return awaited as AwaitedPromisesInObject<T>;
67
- }
61
+ /**
62
+ * Context passed to mutate callback for service methods
63
+ */
64
+ export interface ServiceTxMutateContext<
65
+ TSchema extends AnySchema,
66
+ TRetrieveSuccessResult,
67
+ TServiceRetrieveResults extends readonly unknown[],
68
+ THooks extends HooksMap,
69
+ > {
70
+ /** Unit of work for scheduling mutations */
71
+ uow: TypedUnitOfWork<TSchema, [], unknown, THooks>;
72
+ /** Result from retrieveSuccess callback (or raw retrieve results if no retrieveSuccess) */
73
+ retrieveResult: TRetrieveSuccessResult;
74
+ /** Array of retrieve success results from service calls (intermediate results, not final) */
75
+ serviceIntermediateResult: TServiceRetrieveResults;
76
+ }
68
77
 
69
- // It's a plain object - await promises in each property
70
- const result = {} as T;
71
- const entries = Object.entries(obj as Record<string, unknown>);
72
- const awaitedEntries = await Promise.all(
73
- entries.map(async ([key, value]) => {
74
- const awaitedValue = value instanceof Promise ? await value : value;
75
- return [key, awaitedValue] as const;
76
- }),
77
- );
78
+ /**
79
+ * Context passed to handler-level callbacks
80
+ */
81
+ export interface HandlerTxContext<THooks extends HooksMap> {
82
+ /** Get a typed Unit of Work for the given schema */
83
+ forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
84
+ schema: S,
85
+ hooks?: H,
86
+ ) => TypedUnitOfWork<S, [], unknown, H>;
87
+ /** Unique key for this transaction attempt (for idempotency/deduplication) */
88
+ idempotencyKey: string;
89
+ /** Current attempt number (0-based) */
90
+ currentAttempt: number;
91
+ }
78
92
 
79
- for (const [key, value] of awaitedEntries) {
80
- (result as Record<string, unknown>)[key] = value;
81
- }
93
+ /**
94
+ * Context passed to handler mutate callback
95
+ */
96
+ export interface HandlerTxMutateContext<
97
+ TRetrieveSuccessResult,
98
+ TServiceRetrieveResults extends readonly unknown[],
99
+ THooks extends HooksMap,
100
+ > extends HandlerTxContext<THooks> {
101
+ /** Result from retrieveSuccess callback (or raw retrieve results if no retrieveSuccess) */
102
+ retrieveResult: TRetrieveSuccessResult;
103
+ /** Array of retrieve success results from service calls (intermediate results, not final) */
104
+ serviceIntermediateResult: TServiceRetrieveResults;
105
+ }
82
106
 
83
- return result as AwaitedPromisesInObject<T>;
107
+ /**
108
+ * Context passed to success callback when mutate IS provided
109
+ */
110
+ export interface TxSuccessContextWithMutate<
111
+ TRetrieveSuccessResult,
112
+ TMutateResult,
113
+ TServiceFinalResults extends readonly unknown[],
114
+ TServiceRetrieveResults extends readonly unknown[],
115
+ > {
116
+ /** Result from retrieveSuccess callback (or raw retrieve results if no retrieveSuccess) */
117
+ retrieveResult: TRetrieveSuccessResult;
118
+ /** Result from mutate callback */
119
+ mutateResult: TMutateResult;
120
+ /** Array of final results from service calls */
121
+ serviceResult: TServiceFinalResults;
122
+ /** Array of retrieve success results from service calls (same as what mutate receives) */
123
+ serviceIntermediateResult: TServiceRetrieveResults;
84
124
  }
85
125
 
86
126
  /**
87
- * Result of executing a Unit of Work with retry support
88
- * Promises in mutationResult are unwrapped 1 level deep
127
+ * Context passed to success callback when mutate is NOT provided
89
128
  */
90
- export type ExecuteUnitOfWorkResult<TRetrievalResults, TMutationResult> =
91
- | {
92
- success: true;
93
- results: TRetrievalResults;
94
- mutationResult: AwaitedPromisesInObject<TMutationResult>;
95
- createdIds: FragnoId[];
96
- nonce: string;
97
- }
98
- | {
99
- success: false;
100
- reason: "conflict";
101
- }
102
- | {
103
- success: false;
104
- reason: "aborted";
105
- }
106
- | {
107
- success: false;
108
- reason: "error";
109
- error: unknown;
110
- };
129
+ export interface TxSuccessContextWithoutMutate<
130
+ TRetrieveSuccessResult,
131
+ TServiceFinalResults extends readonly unknown[],
132
+ TServiceRetrieveResults extends readonly unknown[],
133
+ > {
134
+ /** Result from retrieveSuccess callback (or raw retrieve results if no retrieveSuccess) */
135
+ retrieveResult: TRetrieveSuccessResult;
136
+ /** No mutate callback was provided */
137
+ mutateResult: undefined;
138
+ /** Array of final results from service calls */
139
+ serviceResult: TServiceFinalResults;
140
+ /** Array of retrieve success results from service calls (same as what mutate receives) */
141
+ serviceIntermediateResult: TServiceRetrieveResults;
142
+ }
143
+
144
+ /**
145
+ * Context passed to success callback.
146
+ * Union of TxSuccessContextWithMutate and TxSuccessContextWithoutMutate to handle
147
+ * both cases in a single callback signature.
148
+ */
149
+ export type TxSuccessContext<
150
+ TRetrieveSuccessResult,
151
+ TMutateResult,
152
+ TServiceFinalResults extends readonly unknown[],
153
+ TServiceRetrieveResults extends readonly unknown[] = readonly unknown[],
154
+ > =
155
+ | TxSuccessContextWithMutate<
156
+ TRetrieveSuccessResult,
157
+ TMutateResult,
158
+ TServiceFinalResults,
159
+ TServiceRetrieveResults
160
+ >
161
+ | TxSuccessContextWithoutMutate<
162
+ TRetrieveSuccessResult,
163
+ TServiceFinalResults,
164
+ TServiceRetrieveResults
165
+ >;
111
166
 
112
167
  /**
113
- * Callbacks for executing a Unit of Work
168
+ * Callbacks for service-level TxResult.
169
+ *
170
+ * Return type priority:
171
+ * 1. If success exists: ReturnType<success>
172
+ * 2. Else if mutate exists: ReturnType<mutate>
173
+ * 3. Else if retrieveSuccess exists: ReturnType<retrieveSuccess>
174
+ * 4. Else if retrieve exists: TRetrieveResults
175
+ * 5. Else: serviceResult array type
114
176
  */
115
- export interface ExecuteUnitOfWorkCallbacks<
177
+ export interface ServiceTxCallbacks<
116
178
  TSchema extends AnySchema,
117
- TRetrievalResults extends unknown[],
118
- TMutationResult,
119
- TRawInput,
179
+ TRetrieveResults extends unknown[],
180
+ TRetrieveSuccessResult,
181
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
182
+ TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
183
+ TMutateResult,
184
+ TSuccessResult,
185
+ THooks extends HooksMap,
120
186
  > {
121
187
  /**
122
- * Retrieval phase callback - adds retrieval operations to the UOW
188
+ * Service calls - other TxResults to execute first.
189
+ */
190
+ serviceCalls?: () => TServiceCalls;
191
+
192
+ /**
193
+ * Retrieval phase callback - schedules retrieval operations.
123
194
  */
124
195
  retrieve?: (
125
- uow: TypedUnitOfWork<TSchema, [], TRawInput>,
126
- ) => TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>;
196
+ uow: TypedUnitOfWork<TSchema, [], unknown, THooks>,
197
+ ) => TypedUnitOfWork<TSchema, TRetrieveResults, unknown, THooks>;
127
198
 
128
199
  /**
129
- * Mutation phase callback - receives UOW and retrieval results, adds mutation operations
200
+ * Transform retrieve results before passing to mutate.
201
+ */
202
+ retrieveSuccess?: (
203
+ retrieveResult: TRetrieveResults,
204
+ serviceResult: ExtractServiceRetrieveResults<TServiceCalls>,
205
+ ) => TRetrieveSuccessResult;
206
+
207
+ /**
208
+ * Mutation phase callback - schedules mutations based on retrieve results.
130
209
  */
131
210
  mutate?: (
132
- uow: TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>,
133
- results: TRetrievalResults,
134
- ) => TMutationResult | Promise<TMutationResult>;
211
+ ctx: ServiceTxMutateContext<
212
+ TSchema,
213
+ TRetrieveSuccessResult,
214
+ ExtractServiceRetrieveResults<TServiceCalls>,
215
+ THooks
216
+ >,
217
+ ) => TMutateResult;
135
218
 
136
219
  /**
137
- * Success callback - invoked after successful execution
138
- * Promises in mutationResult are already unwrapped 1 level deep
220
+ * Success callback - final transformation after mutations complete.
139
221
  */
140
- onSuccess?: (result: {
141
- results: TRetrievalResults;
142
- mutationResult: AwaitedPromisesInObject<TMutationResult>;
143
- createdIds: FragnoId[];
144
- nonce: string;
145
- }) => void | Promise<void>;
222
+ success?: (
223
+ ctx: TxSuccessContext<
224
+ TRetrieveSuccessResult,
225
+ TMutateResult,
226
+ ExtractServiceFinalResults<TServiceCalls>,
227
+ ExtractServiceRetrieveResults<TServiceCalls>
228
+ >,
229
+ ) => TSuccessResult;
146
230
  }
147
231
 
148
232
  /**
149
- * Options for executing a Unit of Work
233
+ * Callbacks for handler-level executeTx.
234
+ * Uses context-based callbacks that provide forSchema() method.
150
235
  */
151
- export interface ExecuteUnitOfWorkOptions<TSchema extends AnySchema, TRawInput> {
236
+ export interface HandlerTxCallbacks<
237
+ TRetrieveResults extends unknown[],
238
+ TRetrieveSuccessResult,
239
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
240
+ TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
241
+ TMutateResult,
242
+ TSuccessResult,
243
+ THooks extends HooksMap,
244
+ > {
152
245
  /**
153
- * Factory function that creates or resets a UOW instance for each attempt
246
+ * Service calls - other TxResults to execute first.
154
247
  */
155
- createUnitOfWork: () => TypedUnitOfWork<TSchema, [], TRawInput>;
248
+ serviceCalls?: () => TServiceCalls;
156
249
 
157
250
  /**
158
- * Retry policy for handling optimistic concurrency conflicts
251
+ * Retrieval phase callback - schedules retrieval operations using context.forSchema().
252
+ * Return a TypedUnitOfWork to get typed results, or void for no retrieval.
159
253
  */
160
- retryPolicy?: RetryPolicy;
254
+ retrieve?: (
255
+ context: HandlerTxContext<THooks>,
256
+ ) => TypedUnitOfWork<AnySchema, TRetrieveResults, unknown, HooksMap> | void;
161
257
 
162
258
  /**
163
- * Abort signal to cancel execution
259
+ * Transform retrieve results before passing to mutate.
164
260
  */
165
- signal?: AbortSignal;
261
+ retrieveSuccess?: (
262
+ retrieveResult: TRetrieveResults,
263
+ serviceResult: ExtractServiceRetrieveResults<TServiceCalls>,
264
+ ) => TRetrieveSuccessResult;
265
+
266
+ /**
267
+ * Mutation phase callback - schedules mutations based on retrieve results.
268
+ */
269
+ mutate?: (
270
+ ctx: HandlerTxMutateContext<
271
+ TRetrieveSuccessResult,
272
+ ExtractServiceRetrieveResults<TServiceCalls>,
273
+ THooks
274
+ >,
275
+ ) => TMutateResult;
276
+
277
+ /**
278
+ * Success callback - final transformation after mutations complete.
279
+ */
280
+ success?: (
281
+ ctx: TxSuccessContext<
282
+ TRetrieveSuccessResult,
283
+ TMutateResult,
284
+ ExtractServiceFinalResults<TServiceCalls>,
285
+ ExtractServiceRetrieveResults<TServiceCalls>
286
+ >,
287
+ ) => TSuccessResult;
166
288
  }
167
289
 
168
290
  /**
169
- * Create a bound version of executeUnitOfWork with a pre-configured UOW factory.
170
- * This is useful for handler contexts where the factory is already known.
171
- *
172
- * @param createUnitOfWork - Factory function that creates a fresh UOW instance
173
- * @returns A bound executeUnitOfWork function that doesn't require the factory parameter
174
- *
175
- * @example
176
- * ```ts
177
- * const boundExecute = createExecuteUnitOfWork(() => db.createUnitOfWork());
178
- * const result = await boundExecute({
179
- * retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
180
- * mutate: (uow, [users]) => {
181
- * uow.update("users", users[0].id, (b) => b.set({ balance: newBalance }));
182
- * }
183
- * });
184
- * ```
291
+ * Internal structure storing TxResult callbacks and state.
185
292
  */
186
- export function createExecuteUnitOfWork<TSchema extends AnySchema, TRawInput>(
187
- createUnitOfWork: () => TypedUnitOfWork<TSchema, [], TRawInput>,
188
- ) {
189
- return async function <TRetrievalResults extends unknown[], TMutationResult = void>(
190
- callbacks: ExecuteUnitOfWorkCallbacks<TSchema, TRetrievalResults, TMutationResult, TRawInput>,
191
- options?: Omit<ExecuteUnitOfWorkOptions<TSchema, TRawInput>, "createUnitOfWork">,
192
- ): Promise<ExecuteUnitOfWorkResult<TRetrievalResults, TMutationResult>> {
193
- return executeUnitOfWork(callbacks, { ...options, createUnitOfWork });
194
- };
293
+ interface TxResultInternal<
294
+ TSchema extends AnySchema,
295
+ TRetrieveResults extends unknown[],
296
+ TRetrieveSuccessResult,
297
+ TServiceCalls extends readonly (TxResult<unknown> | undefined)[],
298
+ TMutateResult,
299
+ TSuccessResult,
300
+ THooks extends HooksMap,
301
+ > {
302
+ schema: TSchema | undefined;
303
+ callbacks: ServiceTxCallbacks<
304
+ TSchema,
305
+ TRetrieveResults,
306
+ TRetrieveSuccessResult,
307
+ TServiceCalls,
308
+ TMutateResult,
309
+ TSuccessResult,
310
+ THooks
311
+ >;
312
+ /** The typed UOW created during retrieve callback */
313
+ typedUow: TypedUnitOfWork<TSchema, TRetrieveResults, unknown, THooks> | undefined;
314
+ /** The restricted UOW for signaling (used when typedUow is undefined) */
315
+ restrictedUow: IUnitOfWork;
316
+ /** Promise that resolves when retrieve phase is complete */
317
+ retrievePhase: Promise<TRetrieveResults>;
318
+ /** Resolve function for retrievePhase */
319
+ resolveRetrievePhase: (results: TRetrieveResults) => void;
320
+ /** Reject function for retrievePhase */
321
+ rejectRetrievePhase: (error: unknown) => void;
322
+ /** Computed retrieve success result (set after retrieveSuccess runs) */
323
+ retrieveSuccessResult: TRetrieveSuccessResult | undefined;
324
+ /** Computed mutate result (set after mutate runs) */
325
+ mutateResult: TMutateResult | undefined;
326
+ /** Computed final result (set after success runs or defaults) */
327
+ finalResult: TSuccessResult | undefined;
328
+ /** Service calls resolved */
329
+ serviceCalls: TServiceCalls | undefined;
195
330
  }
196
331
 
197
332
  /**
198
- * Execute a Unit of Work with automatic retry support for optimistic concurrency conflicts.
199
- *
200
- * This function orchestrates the two-phase execution (retrieval + mutation) with retry logic.
201
- * It creates fresh UOW instances for each attempt.
333
+ * TxResult represents a transaction definition (not yet executed).
334
+ * It describes the work to be done: retrieve operations, transformations, and mutations.
202
335
  *
203
- * @param callbacks - Object containing retrieve, mutate, and onSuccess callbacks
204
- * @param options - Configuration including UOW factory, retry policy, and abort signal
205
- * @returns Promise resolving to the execution result
336
+ * Service methods return TxResult objects, and the handler's executeTx function
337
+ * orchestrates their execution with retry support.
206
338
  *
207
- * @example
208
- * ```ts
209
- * const result = await executeUnitOfWork(
210
- * {
211
- * retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
212
- * mutate: (uow, [users]) => {
213
- * const user = users[0];
214
- * uow.update("users", user.id, (b) => b.set({ balance: newBalance }));
215
- * },
216
- * onSuccess: async ({ results, mutationResult }) => {
217
- * console.log("Update successful!");
218
- * }
219
- * },
220
- * {
221
- * createUnitOfWork: () => queryEngine.createUnitOfWork(),
222
- * retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3 })
223
- * }
224
- * );
225
- * ```
339
+ * @template TResult - The final result type (determined by return type priority)
340
+ * @template TRetrieveSuccessResult - The retrieve success result type (what serviceCalls receive).
341
+ * Defaults to TResult, meaning serviceCalls receive the same type as the final result.
226
342
  */
227
- export async function executeUnitOfWork<
343
+ export interface TxResult<TResult, TRetrieveSuccessResult = TResult> {
344
+ /** Brand to identify TxResult objects */
345
+ readonly [TX_RESULT_BRAND]: true;
346
+
347
+ /** Internal structure - do not access directly */
348
+ readonly _internal: TxResultInternal<
349
+ AnySchema,
350
+ unknown[],
351
+ TRetrieveSuccessResult,
352
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
353
+ readonly TxResult<any, any>[],
354
+ unknown,
355
+ TResult,
356
+ HooksMap
357
+ >;
358
+ }
359
+
360
+ /**
361
+ * Create a TxResult for service context.
362
+ * Schedules retrieve operations on the baseUow and returns a TxResult with callbacks stored.
363
+ * @internal Used by ServiceTxBuilder.build()
364
+ */
365
+ function createServiceTx<
228
366
  TSchema extends AnySchema,
229
- TRetrievalResults extends unknown[],
230
- TMutationResult = void,
231
- TRawInput = unknown,
367
+ TRetrieveResults extends unknown[],
368
+ TRetrieveSuccessResult,
369
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
370
+ TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
371
+ TMutateResult,
372
+ TSuccessResult,
373
+ THooks extends HooksMap = {},
232
374
  >(
233
- callbacks: ExecuteUnitOfWorkCallbacks<TSchema, TRetrievalResults, TMutationResult, TRawInput>,
234
- options: ExecuteUnitOfWorkOptions<TSchema, TRawInput>,
235
- ): Promise<ExecuteUnitOfWorkResult<TRetrievalResults, TMutationResult>> {
236
- // Validate that at least one of retrieve or mutate is provided
237
- if (!callbacks.retrieve && !callbacks.mutate) {
238
- throw new Error("At least one of 'retrieve' or 'mutate' callbacks must be provided");
375
+ schema: TSchema | undefined,
376
+ callbacks: ServiceTxCallbacks<
377
+ TSchema,
378
+ TRetrieveResults,
379
+ TRetrieveSuccessResult,
380
+ TServiceCalls,
381
+ TMutateResult,
382
+ TSuccessResult,
383
+ THooks
384
+ >,
385
+ baseUow: IUnitOfWork,
386
+ ): TxResult<unknown, unknown> {
387
+ // Create deferred promise for retrieve phase
388
+ const {
389
+ promise: retrievePhase,
390
+ resolve: resolveRetrievePhase,
391
+ reject: rejectRetrievePhase,
392
+ } = Promise.withResolvers<TRetrieveResults>();
393
+
394
+ // Get a restricted view that signals readiness
395
+ const restrictedUow = baseUow.restrict({ readyFor: "none" });
396
+
397
+ // Call serviceCalls factory if provided - this invokes other services which schedule their operations
398
+ let serviceCalls: TServiceCalls | undefined;
399
+ try {
400
+ if (callbacks.serviceCalls) {
401
+ serviceCalls = callbacks.serviceCalls();
402
+ }
403
+ } catch (error) {
404
+ restrictedUow.signalReadyForRetrieval();
405
+ restrictedUow.signalReadyForMutation();
406
+ retrievePhase.catch(() => {});
407
+ rejectRetrievePhase(error);
408
+ throw error;
239
409
  }
240
-
241
- const retryPolicy = options.retryPolicy ?? new NoRetryPolicy();
242
- const signal = options.signal;
243
- let attempt = 0;
244
-
245
- while (true) {
246
- // Check if aborted before starting attempt
247
- if (signal?.aborted) {
248
- return { success: false, reason: "aborted" };
410
+ let typedUow: TypedUnitOfWork<TSchema, TRetrieveResults, unknown, THooks> | undefined;
411
+ try {
412
+ if (schema && callbacks.retrieve) {
413
+ const emptyUow = restrictedUow.forSchema<TSchema, THooks>(schema);
414
+ typedUow = callbacks.retrieve(emptyUow);
249
415
  }
416
+ } catch (error) {
417
+ restrictedUow.signalReadyForRetrieval();
418
+ restrictedUow.signalReadyForMutation();
419
+ retrievePhase.catch(() => {});
420
+ rejectRetrievePhase(error);
421
+ throw error;
422
+ }
423
+ restrictedUow.signalReadyForRetrieval();
250
424
 
251
- try {
252
- // Create a fresh UOW for this attempt
253
- const uow = options.createUnitOfWork();
254
-
255
- // Apply retrieval phase if provided
256
- let retrievalUow: TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>;
257
- if (callbacks.retrieve) {
258
- retrievalUow = callbacks.retrieve(uow);
259
- } else {
260
- // No retrieval phase, use empty UOW with type cast
261
- // This is safe because when there's no retrieve, TRetrievalResults should be []
262
- retrievalUow = uow as unknown as TypedUnitOfWork<TSchema, TRetrievalResults, TRawInput>;
263
- }
264
-
265
- // Execute retrieval phase
266
- const results = (await retrievalUow.executeRetrieve()) as TRetrievalResults;
267
-
268
- // Invoke mutation phase callback if provided
269
- let mutationResult: TMutationResult;
270
- if (callbacks.mutate) {
271
- mutationResult = await callbacks.mutate(retrievalUow, results);
272
- } else {
273
- mutationResult = undefined as TMutationResult;
274
- }
275
-
276
- // Execute mutation phase
277
- const { success } = await retrievalUow.executeMutations();
278
-
279
- if (success) {
280
- // Success! Get created IDs and nonce, then invoke onSuccess if provided
281
- const createdIds = retrievalUow.getCreatedIds();
282
- const nonce = retrievalUow.nonce;
283
-
284
- // Await promises in mutationResult (1 level deep)
285
- const awaitedMutationResult = await awaitPromisesInObject(mutationResult);
286
-
287
- if (callbacks.onSuccess) {
288
- await callbacks.onSuccess({
289
- results,
290
- mutationResult: awaitedMutationResult,
291
- createdIds,
292
- nonce,
293
- });
294
- }
295
-
296
- return {
297
- success: true,
298
- results,
299
- mutationResult: awaitedMutationResult,
300
- createdIds,
301
- nonce,
302
- };
303
- }
304
-
305
- // Failed - check if we should retry
306
- // attempt represents the number of attempts completed so far
307
- if (!retryPolicy.shouldRetry(attempt, undefined, signal)) {
308
- // No more retries
309
- return { success: false, reason: "conflict" };
310
- }
425
+ // Set up the retrieve phase promise to resolve when the handler executes retrieve
426
+ if (typedUow) {
427
+ typedUow.retrievalPhase.then(
428
+ (results) => resolveRetrievePhase(results as TRetrieveResults),
429
+ (error) => rejectRetrievePhase(error),
430
+ );
431
+ } else if (!callbacks.retrieve) {
432
+ // No retrieve callback - resolve immediately with empty array
433
+ resolveRetrievePhase([] as unknown as TRetrieveResults);
434
+ }
311
435
 
312
- const delayMs = retryPolicy.getDelayMs(attempt);
313
- if (delayMs > 0) {
314
- await new Promise((resolve) => setTimeout(resolve, delayMs));
315
- }
436
+ const internal: TxResultInternal<
437
+ TSchema,
438
+ TRetrieveResults,
439
+ TRetrieveSuccessResult,
440
+ TServiceCalls,
441
+ TMutateResult,
442
+ TSuccessResult,
443
+ THooks
444
+ > = {
445
+ schema,
446
+ callbacks,
447
+ typedUow,
448
+ restrictedUow,
449
+ retrievePhase,
450
+ resolveRetrievePhase,
451
+ rejectRetrievePhase,
452
+ retrieveSuccessResult: undefined,
453
+ mutateResult: undefined,
454
+ finalResult: undefined,
455
+ serviceCalls,
456
+ };
316
457
 
317
- attempt++;
318
- } catch (error) {
319
- // An error was thrown during execution
320
- return { success: false, reason: "error", error };
321
- }
322
- }
458
+ return {
459
+ [TX_RESULT_BRAND]: true as const,
460
+ // Cast through unknown to avoid type incompatibility issues with generic constraints
461
+ _internal: internal as unknown as TxResultInternal<
462
+ AnySchema,
463
+ unknown[],
464
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
465
+ any,
466
+ readonly TxResult<unknown>[],
467
+ unknown,
468
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
469
+ any,
470
+ HooksMap
471
+ >,
472
+ };
323
473
  }
324
474
 
325
475
  /**
326
- * Options for executing a Unit of Work with restricted access
476
+ * Options for executing transactions
327
477
  */
328
- export interface ExecuteRestrictedUnitOfWorkOptions {
478
+ export interface ExecuteTxOptions {
329
479
  /**
330
480
  * Factory function that creates or resets a UOW instance for each attempt
331
481
  */
@@ -351,272 +501,396 @@ export interface ExecuteRestrictedUnitOfWorkOptions {
351
501
  * Callback invoked after successful mutation phase.
352
502
  * Use this for post-mutation processing like hook execution.
353
503
  */
354
- onSuccess?: (uow: IUnitOfWork) => Promise<void>;
504
+ onAfterMutate?: (uow: IUnitOfWork) => Promise<void>;
355
505
  }
356
506
 
357
507
  /**
358
- * Context provided to handler tx callbacks
508
+ * Recursively collect all TxResults from a service call tree.
509
+ * Returns them in a flat array in dependency order (serviceCalls before their dependents).
510
+ * Skips undefined values (which can occur with optional service patterns like
511
+ * optionalService?.method()).
359
512
  */
360
- export interface TxPhaseContext<THooks extends HooksMap> {
361
- /**
362
- * Get a typed Unit of Work for the given schema
363
- */
364
- forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
365
- schema: S,
366
- hooks?: H,
367
- ) => TypedUnitOfWork<S, [], unknown, H>;
513
+ function collectAllTxResults(
514
+ txResults: readonly (TxResult<unknown> | undefined)[],
515
+ ): TxResult<unknown>[] {
516
+ const collected: TxResult<unknown>[] = [];
517
+ const seen = new Set<TxResult<unknown>>();
518
+
519
+ function collect(txResult: TxResult<unknown> | undefined) {
520
+ if (txResult === undefined) {
521
+ return;
522
+ }
523
+
524
+ if (seen.has(txResult)) {
525
+ return;
526
+ }
527
+ seen.add(txResult);
528
+
529
+ // First collect serviceCalls (so they come before this TxResult)
530
+ const serviceCalls = txResult._internal.serviceCalls;
531
+ if (serviceCalls) {
532
+ for (const serviceCall of serviceCalls) {
533
+ collect(serviceCall);
534
+ }
535
+ }
536
+
537
+ collected.push(txResult);
538
+ }
539
+
540
+ for (const txResult of txResults) {
541
+ collect(txResult);
542
+ }
543
+
544
+ return collected;
368
545
  }
369
546
 
370
547
  /**
371
- * Handler callbacks for tx() - SYNCHRONOUS ONLY (no Promise return allowed)
372
- * This prevents accidentally awaiting services in the wrong place
548
+ * Execute a single TxResult's callbacks after retrieve phase completes.
549
+ * This processes retrieveSuccess, mutate, and success callbacks in order.
373
550
  */
374
- export interface HandlerTxCallbacks<TRetrieveResult, TMutationResult, THooks extends HooksMap> {
375
- /**
376
- * Retrieval phase callback - schedules retrievals and optionally calls services
377
- * Must be synchronous - cannot await promises
378
- */
379
- retrieve?: (context: TxPhaseContext<THooks>) => TRetrieveResult;
380
- /**
381
- * Mutation phase callback - receives retrieve result, schedules mutations
382
- * Must be synchronous - cannot await promises (but may return a promise to be awaited)
383
- */
384
- mutate?: (context: TxPhaseContext<THooks>, retrieveResult: TRetrieveResult) => TMutationResult;
551
+ async function processTxResultAfterRetrieve<T>(
552
+ txResult: TxResult<T>,
553
+ baseUow: IUnitOfWork,
554
+ ): Promise<void> {
555
+ const internal = txResult._internal;
556
+ const callbacks = internal.callbacks;
557
+
558
+ // Wait for retrieve phase to complete
559
+ const retrieveResults = await internal.retrievePhase;
560
+
561
+ // Collect serviceCalls' retrieve success results (or mutate results if no retrieve was provided)
562
+ // When a serviceCall has no retrieve/retrieveSuccess but has mutate, its mutate has already run
563
+ // (due to service call execution order), so we use its mutate result as the "retrieve success result".
564
+ const serviceResults: unknown[] = [];
565
+ if (internal.serviceCalls) {
566
+ for (const serviceCall of internal.serviceCalls) {
567
+ if (serviceCall === undefined) {
568
+ serviceResults.push(undefined);
569
+ continue;
570
+ }
571
+
572
+ const serviceCallInternal = serviceCall._internal;
573
+ // Check if this is a mutate-only service call (empty array sentinel with mutate callback)
574
+ // In that case, prefer mutateResult over the empty array retrieveSuccessResult
575
+ if (
576
+ serviceCallInternal.retrieveSuccessResult !== undefined &&
577
+ !(
578
+ Array.isArray(serviceCallInternal.retrieveSuccessResult) &&
579
+ serviceCallInternal.retrieveSuccessResult.length === 0 &&
580
+ serviceCallInternal.callbacks.mutate
581
+ )
582
+ ) {
583
+ serviceResults.push(serviceCallInternal.retrieveSuccessResult);
584
+ } else if (serviceCallInternal.mutateResult !== undefined) {
585
+ serviceResults.push(serviceCallInternal.mutateResult);
586
+ } else {
587
+ serviceResults.push(serviceCallInternal.retrieveSuccessResult);
588
+ }
589
+ }
590
+ }
591
+
592
+ if (callbacks.retrieveSuccess) {
593
+ internal.retrieveSuccessResult = callbacks.retrieveSuccess(
594
+ retrieveResults,
595
+ serviceResults as ExtractServiceRetrieveResults<readonly TxResult<unknown>[]>,
596
+ );
597
+ } else {
598
+ internal.retrieveSuccessResult = retrieveResults as typeof internal.retrieveSuccessResult;
599
+ }
600
+
601
+ if (callbacks.mutate) {
602
+ const mutateCtx = {
603
+ uow: internal.schema
604
+ ? baseUow.forSchema(internal.schema)
605
+ : (undefined as unknown as TypedUnitOfWork<AnySchema, [], unknown, HooksMap>),
606
+ // At this point retrieveSuccessResult has been set (either by retrieveSuccess
607
+ // callback or defaulted to retrieveResults)
608
+ retrieveResult: internal.retrieveSuccessResult as NonNullable<
609
+ typeof internal.retrieveSuccessResult
610
+ >,
611
+ serviceIntermediateResult: serviceResults as ExtractServiceRetrieveResults<
612
+ readonly TxResult<unknown>[]
613
+ >,
614
+ };
615
+ internal.mutateResult = callbacks.mutate(mutateCtx);
616
+ }
617
+
618
+ if (internal.typedUow) {
619
+ internal.typedUow.signalReadyForMutation();
620
+ } else {
621
+ // For TxResults without retrieve callback, signal via the restricted UOW
622
+ internal.restrictedUow.signalReadyForMutation();
623
+ }
385
624
  }
386
625
 
387
- export interface ServiceTxCallbacks<
388
- TSchema extends AnySchema,
389
- TRetrievalResults extends unknown[],
390
- TMutationResult,
391
- THooks extends HooksMap,
392
- > {
393
- /**
394
- * Retrieval phase callback - schedules retrievals, returns typed UOW
395
- */
396
- retrieve?: (
397
- uow: TypedUnitOfWork<TSchema, [], unknown, THooks>,
398
- ) => TypedUnitOfWork<TSchema, TRetrievalResults, unknown, THooks>;
399
- /**
400
- * Mutation phase callback - receives retrieval results, schedules mutations and hooks
401
- */
402
- mutate?: (
403
- uow: TypedUnitOfWork<TSchema, TRetrievalResults, unknown, THooks>,
404
- results: TRetrievalResults,
405
- ) => TMutationResult | Promise<TMutationResult>;
626
+ /**
627
+ * Execute a single TxResult's success callback after mutations complete.
628
+ */
629
+ async function processTxResultAfterMutate<T>(txResult: TxResult<T>): Promise<T> {
630
+ const internal = txResult._internal;
631
+ const callbacks = internal.callbacks;
632
+
633
+ const serviceIntermediateResults: unknown[] = [];
634
+ const serviceFinalResults: unknown[] = [];
635
+ if (internal.serviceCalls) {
636
+ for (const serviceCall of internal.serviceCalls) {
637
+ if (serviceCall === undefined) {
638
+ serviceIntermediateResults.push(undefined);
639
+ serviceFinalResults.push(undefined);
640
+ continue;
641
+ }
642
+
643
+ // Mirror the logic from processTxResultAfterRetrieve/executeTx:
644
+ // For mutate-only serviceCalls (no retrieve phase, just mutations), use mutateResult instead of retrieveSuccessResult
645
+ const serviceCallInternal = serviceCall._internal;
646
+ // Check if this is a mutate-only service call (empty array sentinel with mutate callback)
647
+ // In that case, prefer mutateResult over the empty array retrieveSuccessResult
648
+ if (
649
+ serviceCallInternal.retrieveSuccessResult !== undefined &&
650
+ !(
651
+ Array.isArray(serviceCallInternal.retrieveSuccessResult) &&
652
+ serviceCallInternal.retrieveSuccessResult.length === 0 &&
653
+ serviceCallInternal.callbacks.mutate
654
+ )
655
+ ) {
656
+ serviceIntermediateResults.push(serviceCallInternal.retrieveSuccessResult);
657
+ } else if (serviceCallInternal.mutateResult !== undefined) {
658
+ serviceIntermediateResults.push(serviceCallInternal.mutateResult);
659
+ } else {
660
+ serviceIntermediateResults.push(serviceCallInternal.retrieveSuccessResult);
661
+ }
662
+ serviceFinalResults.push(serviceCallInternal.finalResult);
663
+ }
664
+ }
665
+
666
+ if (callbacks.success) {
667
+ const successCtx = {
668
+ retrieveResult: internal.retrieveSuccessResult as NonNullable<
669
+ typeof internal.retrieveSuccessResult
670
+ >,
671
+ mutateResult: internal.mutateResult,
672
+ serviceResult: serviceFinalResults as ExtractServiceFinalResults<
673
+ readonly TxResult<unknown>[]
674
+ >,
675
+ serviceIntermediateResult: serviceIntermediateResults as ExtractServiceRetrieveResults<
676
+ readonly TxResult<unknown>[]
677
+ >,
678
+ };
679
+ internal.finalResult = callbacks.success(successCtx) as T;
680
+ } else if (callbacks.mutate) {
681
+ internal.finalResult = (await awaitPromisesInObject(internal.mutateResult)) as T;
682
+ } else if (callbacks.retrieveSuccess || callbacks.retrieve) {
683
+ internal.finalResult = internal.retrieveSuccessResult as T;
684
+ } else {
685
+ internal.finalResult = serviceFinalResults as T;
686
+ }
687
+
688
+ return internal.finalResult as T;
406
689
  }
407
690
 
408
691
  /**
409
- * Execute a Unit of Work with explicit phase control and automatic retry support.
692
+ * Execute a transaction with the unified TxResult pattern.
410
693
  *
411
- * This function provides an alternative API where users write a single callback that receives
412
- * a context object with forSchema, executeRetrieve, and executeMutate methods. The user can
413
- * create schema-specific UOWs via forSchema, then call executeRetrieve() and executeMutate()
414
- * to execute the retrieval and mutation phases. The entire callback is re-executed on optimistic
415
- * concurrency conflicts, ensuring retries work properly.
694
+ * This is the handler-level function that actually executes TxResults with retry support.
416
695
  *
417
- * @param callback - Async function that receives a context with forSchema, executeRetrieve, executeMutate, nonce, and currentAttempt
696
+ * @param callbacks - Transaction callbacks (serviceCalls, retrieve, retrieveSuccess, mutate, success)
418
697
  * @param options - Configuration including UOW factory, retry policy, and abort signal
419
- * @returns Promise resolving to the callback's return value
420
- * @throws Error if retries are exhausted or callback throws an error
698
+ * @returns Promise resolving to the result determined by return type priority
421
699
  *
422
700
  * @example
423
701
  * ```ts
424
- * const { userId, profileId } = await executeRestrictedUnitOfWork(
425
- * async ({ forSchema, executeRetrieve, executeMutate, nonce, currentAttempt }) => {
426
- * const uow = forSchema(schema);
427
- * const userId = uow.create("users", { name: "John" });
428
- *
429
- * // Execute retrieval phase
430
- * await executeRetrieve();
431
- *
432
- * const profileId = uow.create("profiles", { userId });
433
- *
434
- * // Execute mutation phase
435
- * await executeMutate();
436
- *
437
- * return { userId, profileId };
438
- * },
439
- * {
440
- * createUnitOfWork: () => db.createUnitOfWork(),
441
- * retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 5 })
442
- * }
443
- * );
444
- * ```
702
+ * // Simple retrieve + transform
703
+ * const user = await executeTx({
704
+ * retrieve: (ctx) => ctx.forSchema(usersSchema).find("users", ...),
705
+ * retrieveSuccess: ([users]) => users[0] ?? null,
706
+ * }, { createUnitOfWork });
707
+ * @internal Used by HandlerTxBuilder.execute()
445
708
  */
446
- export async function executeRestrictedUnitOfWork<TResult, THooks extends HooksMap = {}>(
447
- callback: (context: {
448
- forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
449
- schema: S,
450
- hooks?: H,
451
- ) => TypedUnitOfWork<S, [], unknown, H>;
452
- executeRetrieve: () => Promise<void>;
453
- executeMutate: () => Promise<void>;
454
- nonce: string;
455
- currentAttempt: number;
456
- }) => Promise<TResult>,
457
- options: ExecuteRestrictedUnitOfWorkOptions,
458
- ): Promise<AwaitedPromisesInObject<TResult>> {
459
- // Default retry policy with small, fast retries for optimistic concurrency
460
- const retryPolicy =
461
- options.retryPolicy ??
462
- new ExponentialBackoffRetryPolicy({
463
- maxRetries: 5,
464
- initialDelayMs: 10,
465
- maxDelayMs: 100,
466
- });
709
+ async function executeTx(
710
+ callbacks: HandlerTxCallbacks<
711
+ unknown[],
712
+ unknown,
713
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
714
+ readonly (TxResult<any, any> | undefined)[],
715
+ unknown,
716
+ unknown,
717
+ HooksMap
718
+ >,
719
+ options: ExecuteTxOptions,
720
+ ): Promise<unknown> {
721
+ type TRetrieveResults = unknown[];
722
+ type TRetrieveSuccessResult = unknown;
723
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
724
+ type TServiceCalls = readonly (TxResult<any, any> | undefined)[];
725
+ type TMutateResult = unknown;
726
+ type THooks = HooksMap;
467
727
  const signal = options.signal;
468
728
  let attempt = 0;
469
729
 
470
730
  while (true) {
471
731
  // Check if aborted before starting attempt
472
732
  if (signal?.aborted) {
473
- throw new Error("Unit of Work execution aborted");
733
+ throw new Error("Transaction execution aborted");
474
734
  }
475
735
 
736
+ let retryPolicy: RetryPolicy | undefined;
737
+
476
738
  try {
477
739
  // Create a fresh UOW for this attempt
478
740
  const baseUow = options.createUnitOfWork();
479
741
 
480
- const context = {
742
+ // Create handler context
743
+ const context: HandlerTxContext<THooks> = {
481
744
  forSchema: <S extends AnySchema, H extends HooksMap = THooks>(schema: S, hooks?: H) => {
482
745
  return baseUow.forSchema(schema, hooks);
483
746
  },
484
- executeRetrieve: async () => {
485
- await baseUow.executeRetrieve();
486
- },
487
- executeMutate: async () => {
488
- if (baseUow.state === "executed") {
489
- return;
490
- }
491
-
492
- if (baseUow.state === "building-retrieval") {
493
- await baseUow.executeRetrieve();
494
- }
495
-
496
- // Add hook mutations before executing
497
- if (options.onBeforeMutate) {
498
- options.onBeforeMutate(baseUow);
499
- }
500
-
501
- const result = await baseUow.executeMutations();
502
- if (!result.success) {
503
- throw new ConcurrencyConflictError();
504
- }
505
-
506
- if (options.onSuccess) {
507
- await options.onSuccess(baseUow);
508
- }
509
- },
510
- nonce: baseUow.nonce,
747
+ idempotencyKey: baseUow.idempotencyKey,
511
748
  currentAttempt: attempt,
512
749
  };
513
750
 
514
- // Execute the callback which will call executeRetrieve and executeMutate
515
- const result = await callback(context);
516
-
517
- // Await promises in the result object (1 level deep)
518
- const awaitedResult = await awaitPromisesInObject(result);
519
-
520
- // Return the awaited result
521
- return awaitedResult;
522
- } catch (error) {
523
- if (signal?.aborted) {
524
- throw new Error("Unit of Work execution aborted");
751
+ // Call serviceCalls factory if provided - this creates TxResults that schedule operations
752
+ let serviceCalls: TServiceCalls | undefined;
753
+ if (callbacks.serviceCalls) {
754
+ serviceCalls = callbacks.serviceCalls();
525
755
  }
526
756
 
527
- // Only retry concurrency conflicts, not other errors
528
- if (!(error instanceof ConcurrencyConflictError)) {
529
- // Not a concurrency conflict - throw immediately without retry
530
- throw error;
531
- }
757
+ // Call retrieve callback - it returns a TypedUnitOfWork with scheduled operations or void
758
+ const typedUowFromRetrieve = callbacks.retrieve?.(context);
532
759
 
533
- if (!retryPolicy.shouldRetry(attempt, error, signal)) {
534
- // No more retries - check again if aborted or throw conflict error
535
- if (signal?.aborted) {
536
- throw new Error("Unit of Work execution aborted");
760
+ const allServiceCallTxResults = serviceCalls ? collectAllTxResults([...serviceCalls]) : [];
761
+
762
+ const hasRetrieveOps = baseUow.getRetrievalOperations().length > 0;
763
+ if (!hasRetrieveOps) {
764
+ if (options.retryPolicy) {
765
+ throw new Error(
766
+ "Retry policy is only supported when the transaction includes retrieve operations.",
767
+ );
537
768
  }
538
- throw new Error("Unit of Work execution failed: optimistic concurrency conflict", {
539
- cause: error,
540
- });
769
+ retryPolicy = new NoRetryPolicy();
770
+ } else {
771
+ retryPolicy =
772
+ options.retryPolicy ??
773
+ new ExponentialBackoffRetryPolicy({
774
+ maxRetries: 5,
775
+ initialDelayMs: 10,
776
+ maxDelayMs: 100,
777
+ });
541
778
  }
542
779
 
543
- const delayMs = retryPolicy.getDelayMs(attempt);
544
- if (delayMs > 0) {
545
- await new Promise((resolve) => setTimeout(resolve, delayMs));
546
- }
780
+ await baseUow.executeRetrieve();
547
781
 
548
- attempt++;
549
- }
550
- }
551
- }
782
+ // Get retrieve results from TypedUnitOfWork's retrievalPhase or default to empty array
783
+ const retrieveResult: TRetrieveResults = typedUowFromRetrieve
784
+ ? await typedUowFromRetrieve.retrievalPhase
785
+ : ([] as unknown as TRetrieveResults);
552
786
 
553
- /**
554
- * Execute a transaction with array syntax (handler context).
555
- * Takes a factory function that creates an array of service promises, enabling proper retry support.
556
- *
557
- * @param servicesFactory - Function that creates an array of service promises
558
- * @param options - Configuration including UOW factory, retry policy, and abort signal
559
- * @returns Promise resolving to array of awaited service results
560
- *
561
- * @example
562
- * ```ts
563
- * const [result1, result2] = await executeTxArray(
564
- * () => [
565
- * executeServiceTx(schema, callbacks1, uow),
566
- * executeServiceTx(schema, callbacks2, uow)
567
- * ],
568
- * { createUnitOfWork }
569
- * );
570
- * ```
571
- */
572
- export async function executeTxArray<T extends readonly unknown[]>(
573
- servicesFactory: () => readonly [...{ [K in keyof T]: Promise<T[K]> }],
574
- options: ExecuteRestrictedUnitOfWorkOptions,
575
- ): Promise<{ [K in keyof T]: T[K] }> {
576
- const retryPolicy =
577
- options.retryPolicy ??
578
- new ExponentialBackoffRetryPolicy({
579
- maxRetries: 5,
580
- initialDelayMs: 10,
581
- maxDelayMs: 100,
582
- });
583
- const signal = options.signal;
584
- let attempt = 0;
585
-
586
- while (true) {
587
- // Check if aborted before starting attempt
588
- if (signal?.aborted) {
589
- throw new Error("Unit of Work execution aborted");
590
- }
787
+ for (const txResult of allServiceCallTxResults) {
788
+ await processTxResultAfterRetrieve(txResult, baseUow);
789
+ }
591
790
 
592
- try {
593
- // Create a fresh UOW for this attempt
594
- const baseUow = options.createUnitOfWork();
791
+ const serviceResults: unknown[] = [];
792
+ if (serviceCalls) {
793
+ for (const serviceCall of serviceCalls) {
794
+ if (serviceCall === undefined) {
795
+ serviceResults.push(undefined);
796
+ continue;
797
+ }
798
+ const serviceCallInternal = serviceCall._internal;
799
+ // Check if this is a mutate-only service call (empty array sentinel with mutate callback)
800
+ // In that case, prefer mutateResult over the empty array retrieveSuccessResult
801
+ if (
802
+ serviceCallInternal.retrieveSuccessResult !== undefined &&
803
+ !(
804
+ Array.isArray(serviceCallInternal.retrieveSuccessResult) &&
805
+ serviceCallInternal.retrieveSuccessResult.length === 0 &&
806
+ serviceCallInternal.callbacks.mutate
807
+ )
808
+ ) {
809
+ serviceResults.push(serviceCallInternal.retrieveSuccessResult);
810
+ } else if (serviceCallInternal.mutateResult !== undefined) {
811
+ serviceResults.push(serviceCallInternal.mutateResult);
812
+ } else {
813
+ serviceResults.push(serviceCallInternal.retrieveSuccessResult);
814
+ }
815
+ }
816
+ }
595
817
 
596
- // Call factory to create fresh service promises for this attempt
597
- const services = servicesFactory();
818
+ // Call retrieveSuccess if provided
819
+ let retrieveSuccessResult: TRetrieveSuccessResult;
820
+ if (callbacks.retrieveSuccess) {
821
+ retrieveSuccessResult = callbacks.retrieveSuccess(
822
+ retrieveResult,
823
+ serviceResults as ExtractServiceRetrieveResults<TServiceCalls>,
824
+ );
825
+ } else {
826
+ retrieveSuccessResult = retrieveResult as unknown as TRetrieveSuccessResult;
827
+ }
598
828
 
599
- await baseUow.executeRetrieve();
829
+ let mutateResult: TMutateResult | undefined;
830
+ if (callbacks.mutate) {
831
+ const mutateCtx: HandlerTxMutateContext<
832
+ TRetrieveSuccessResult,
833
+ ExtractServiceRetrieveResults<TServiceCalls>,
834
+ THooks
835
+ > = {
836
+ ...context,
837
+ retrieveResult: retrieveSuccessResult,
838
+ serviceIntermediateResult: serviceResults as ExtractServiceRetrieveResults<TServiceCalls>,
839
+ };
840
+ mutateResult = callbacks.mutate(mutateCtx);
841
+ }
600
842
 
601
843
  if (options.onBeforeMutate) {
602
844
  options.onBeforeMutate(baseUow);
603
845
  }
604
-
605
846
  const result = await baseUow.executeMutations();
606
847
  if (!result.success) {
607
848
  throw new ConcurrencyConflictError();
608
849
  }
609
850
 
610
- if (options.onSuccess) {
611
- await options.onSuccess(baseUow);
851
+ // Process each serviceCall TxResult's success callback
852
+ for (const txResult of allServiceCallTxResults) {
853
+ await processTxResultAfterMutate(txResult);
854
+ }
855
+
856
+ const serviceFinalResults: unknown[] = [];
857
+ if (serviceCalls) {
858
+ for (const serviceCall of serviceCalls) {
859
+ if (serviceCall === undefined) {
860
+ serviceFinalResults.push(undefined);
861
+ continue;
862
+ }
863
+ serviceFinalResults.push(serviceCall._internal.finalResult);
864
+ }
865
+ }
866
+
867
+ let finalResult: unknown;
868
+ if (callbacks.success) {
869
+ // The success context type is determined by the overload - we construct it at runtime
870
+ // and the type safety is guaranteed by the discriminated overloads
871
+ const successCtx = {
872
+ retrieveResult: retrieveSuccessResult,
873
+ mutateResult,
874
+ serviceResult: serviceFinalResults as ExtractServiceFinalResults<TServiceCalls>,
875
+ serviceIntermediateResult: serviceResults as ExtractServiceRetrieveResults<TServiceCalls>,
876
+ } as Parameters<NonNullable<typeof callbacks.success>>[0];
877
+ finalResult = callbacks.success(successCtx);
878
+ } else if (callbacks.mutate) {
879
+ finalResult = await awaitPromisesInObject(mutateResult);
880
+ } else if (callbacks.retrieveSuccess || callbacks.retrieve) {
881
+ finalResult = retrieveSuccessResult;
882
+ } else {
883
+ finalResult = serviceFinalResults;
884
+ }
885
+
886
+ if (options.onAfterMutate) {
887
+ await options.onAfterMutate(baseUow);
612
888
  }
613
889
 
614
- // Now await all service promises - they should all resolve now that mutations executed
615
- const results = await Promise.all(services);
616
- return results as { [K in keyof T]: T[K] };
890
+ return await awaitPromisesInObject(finalResult);
617
891
  } catch (error) {
618
892
  if (signal?.aborted) {
619
- throw new Error("Unit of Work execution aborted");
893
+ throw new Error("Transaction execution aborted");
620
894
  }
621
895
 
622
896
  // Only retry concurrency conflicts, not other errors
@@ -624,13 +898,15 @@ export async function executeTxArray<T extends readonly unknown[]>(
624
898
  throw error;
625
899
  }
626
900
 
901
+ if (!retryPolicy) {
902
+ throw error;
903
+ }
904
+
627
905
  if (!retryPolicy.shouldRetry(attempt, error, signal)) {
628
906
  if (signal?.aborted) {
629
- throw new Error("Unit of Work execution aborted");
907
+ throw new Error("Transaction execution aborted");
630
908
  }
631
- throw new Error("Unit of Work execution failed: optimistic concurrency conflict", {
632
- cause: error,
633
- });
909
+ throw new ConcurrencyConflictError();
634
910
  }
635
911
 
636
912
  const delayMs = retryPolicy.getDelayMs(attempt);
@@ -644,169 +920,1005 @@ export async function executeTxArray<T extends readonly unknown[]>(
644
920
  }
645
921
 
646
922
  /**
647
- * Execute a transaction with callback syntax (handler context).
648
- * Callbacks are synchronous only to prevent accidentally awaiting services in wrong place.
649
- *
650
- * @param callbacks - Object containing retrieve and mutate callbacks
651
- * @param options - Configuration including UOW factory, retry policy, and abort signal
652
- * @returns Promise resolving to the mutation result with promises awaited 1 level deep
923
+ * Error thrown when a Unit of Work execution fails due to optimistic concurrency conflict.
924
+ * This error triggers automatic retry behavior in executeTx.
653
925
  */
654
- export async function executeTxCallbacks<
655
- TRetrieveResult,
656
- TMutationResult,
657
- THooks extends HooksMap = {},
658
- >(
659
- callbacks: HandlerTxCallbacks<TRetrieveResult, TMutationResult, THooks>,
660
- options: ExecuteRestrictedUnitOfWorkOptions,
661
- ): Promise<AwaitedPromisesInObject<TMutationResult>> {
662
- const retryPolicy =
663
- options.retryPolicy ??
664
- new ExponentialBackoffRetryPolicy({
665
- maxRetries: 5,
666
- initialDelayMs: 10,
667
- maxDelayMs: 100,
668
- });
669
- const signal = options.signal;
670
- let attempt = 0;
926
+ export class ConcurrencyConflictError extends Error {
927
+ constructor(message = "Optimistic concurrency conflict detected") {
928
+ super(message);
929
+ this.name = "ConcurrencyConflictError";
930
+ }
931
+ }
671
932
 
672
- while (true) {
673
- // Check if aborted before starting attempt
674
- if (signal?.aborted) {
675
- throw new Error("Unit of Work execution aborted");
676
- }
933
+ /**
934
+ * Type utility that unwraps promises 1 level deep in objects, arrays, or direct promises
935
+ * Handles tuples, arrays, objects, and direct promises
936
+ */
937
+ export type AwaitedPromisesInObject<T> =
938
+ // First check if it's a Promise
939
+ T extends Promise<infer U>
940
+ ? Awaited<U>
941
+ : // Check for arrays with known length (tuples) - preserves tuple structure
942
+ T extends readonly [unknown, ...unknown[]]
943
+ ? { [K in keyof T]: AwaitedPromisesInObject<T[K]> }
944
+ : T extends [unknown, ...unknown[]]
945
+ ? { [K in keyof T]: AwaitedPromisesInObject<T[K]> }
946
+ : // Check for regular arrays (unknown length)
947
+ T extends (infer U)[]
948
+ ? Awaited<U>[]
949
+ : T extends readonly (infer U)[]
950
+ ? readonly Awaited<U>[]
951
+ : // Check for objects
952
+ T extends Record<string, unknown>
953
+ ? {
954
+ [K in keyof T]: T[K] extends Promise<infer U> ? Awaited<U> : T[K];
955
+ }
956
+ : // Otherwise return as-is
957
+ T;
677
958
 
678
- try {
679
- // Create a fresh UOW for this attempt
680
- const baseUow = options.createUnitOfWork();
959
+ /**
960
+ * Await promises in an object 1 level deep
961
+ */
962
+ async function awaitPromisesInObject<T>(obj: T): Promise<AwaitedPromisesInObject<T>> {
963
+ if (obj === null || obj === undefined) {
964
+ return obj as AwaitedPromisesInObject<T>;
965
+ }
681
966
 
682
- const context: TxPhaseContext<THooks> = {
683
- forSchema: <S extends AnySchema, H extends HooksMap = THooks>(schema: S, hooks?: H) => {
684
- return baseUow.forSchema(schema, hooks);
685
- },
686
- };
967
+ if (typeof obj !== "object") {
968
+ return obj as AwaitedPromisesInObject<T>;
969
+ }
687
970
 
688
- let retrieveResult: TRetrieveResult;
689
- if (callbacks.retrieve) {
690
- retrieveResult = callbacks.retrieve(context);
691
- } else {
692
- retrieveResult = undefined as TRetrieveResult;
693
- }
971
+ // Check if it's a Promise
972
+ if (obj instanceof Promise) {
973
+ return (await obj) as AwaitedPromisesInObject<T>;
974
+ }
694
975
 
695
- await baseUow.executeRetrieve();
976
+ // Check if it's an array
977
+ if (Array.isArray(obj)) {
978
+ const awaited = await Promise.all(
979
+ obj.map((item) => (item instanceof Promise ? item : Promise.resolve(item))),
980
+ );
981
+ return awaited as AwaitedPromisesInObject<T>;
982
+ }
696
983
 
697
- let mutationResult: TMutationResult;
698
- if (callbacks.mutate) {
699
- mutationResult = callbacks.mutate(context, retrieveResult);
700
- } else {
701
- mutationResult = retrieveResult as unknown as TMutationResult;
702
- }
984
+ if (obj.constructor !== Object) {
985
+ return obj as AwaitedPromisesInObject<T>;
986
+ }
987
+ const result = {} as T;
988
+ const entries = Object.entries(obj as Record<string, unknown>);
989
+ const awaitedEntries = await Promise.all(
990
+ entries.map(async ([key, value]) => {
991
+ const awaitedValue = value instanceof Promise ? await value : value;
992
+ return [key, awaitedValue] as const;
993
+ }),
994
+ );
703
995
 
704
- const awaitedMutationResult = await awaitPromisesInObject(mutationResult);
996
+ for (const [key, value] of awaitedEntries) {
997
+ (result as Record<string, unknown>)[key] = value;
998
+ }
705
999
 
706
- if (options.onBeforeMutate) {
707
- options.onBeforeMutate(baseUow);
708
- }
1000
+ return result as AwaitedPromisesInObject<T>;
1001
+ }
709
1002
 
710
- const result = await baseUow.executeMutations();
711
- if (!result.success) {
712
- throw new ConcurrencyConflictError();
713
- }
1003
+ // ============================================================================
1004
+ // Builder Pattern Types and Classes
1005
+ // ============================================================================
714
1006
 
715
- if (options.onSuccess) {
716
- await options.onSuccess(baseUow);
717
- }
1007
+ /**
1008
+ * Context passed to service-level mutate callback in builder pattern.
1009
+ */
1010
+ export interface ServiceBuilderMutateContext<
1011
+ TSchema extends AnySchema,
1012
+ TRetrieveSuccessResult,
1013
+ TServiceResult extends readonly unknown[],
1014
+ THooks extends HooksMap,
1015
+ > {
1016
+ /** Unit of work for scheduling mutations */
1017
+ uow: TypedUnitOfWork<TSchema, [], unknown, THooks>;
1018
+ /** Result from transformRetrieve callback (or raw retrieve results if no transformRetrieve) */
1019
+ retrieveResult: TRetrieveSuccessResult;
1020
+ /** Array of retrieve success results from service calls (intermediate results, not final: retrieve results if service has retrieve, mutate result if service only mutates) */
1021
+ serviceIntermediateResult: TServiceResult;
1022
+ }
718
1023
 
719
- return awaitedMutationResult;
720
- } catch (error) {
721
- if (signal?.aborted) {
722
- throw new Error("Unit of Work execution aborted");
723
- }
1024
+ /**
1025
+ * Context passed to handler-level mutate callback in builder pattern.
1026
+ */
1027
+ export interface HandlerBuilderMutateContext<
1028
+ TRetrieveSuccessResult,
1029
+ TServiceResult extends readonly unknown[],
1030
+ THooks extends HooksMap,
1031
+ > {
1032
+ /** Get a typed Unit of Work for the given schema */
1033
+ forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
1034
+ schema: S,
1035
+ hooks?: H,
1036
+ ) => TypedUnitOfWork<S, [], unknown, H>;
1037
+ /** Unique key for this transaction (for idempotency/deduplication) */
1038
+ idempotencyKey: string;
1039
+ /** Current attempt number (0-based) */
1040
+ currentAttempt: number;
1041
+ /** Result from transformRetrieve callback (or raw retrieve results if no transformRetrieve) */
1042
+ retrieveResult: TRetrieveSuccessResult;
1043
+ /** Array of retrieve success results from service calls (intermediate results, not final: retrieve results if service has retrieve, mutate result if service only mutates) */
1044
+ serviceIntermediateResult: TServiceResult;
1045
+ }
724
1046
 
725
- // Only retry concurrency conflicts, not other errors
726
- if (!(error instanceof ConcurrencyConflictError)) {
727
- throw error;
728
- }
1047
+ /**
1048
+ * Context passed to transform callback when mutate IS provided.
1049
+ */
1050
+ export interface BuilderTransformContextWithMutate<
1051
+ TRetrieveSuccessResult,
1052
+ TMutateResult,
1053
+ TServiceFinalResult extends readonly unknown[],
1054
+ TServiceIntermediateResult extends readonly unknown[],
1055
+ > {
1056
+ /** Result from transformRetrieve callback (or raw retrieve results if no transformRetrieve) */
1057
+ retrieveResult: TRetrieveSuccessResult;
1058
+ /** Result from mutate callback */
1059
+ mutateResult: TMutateResult;
1060
+ /** Array of final results from service calls (after success/transform callbacks) */
1061
+ serviceResult: TServiceFinalResult;
1062
+ /** Array of retrieve success results from service calls (same as what mutate receives: retrieve results if service has retrieve, mutate result if service only mutates) */
1063
+ serviceIntermediateResult: TServiceIntermediateResult;
1064
+ }
729
1065
 
730
- if (!retryPolicy.shouldRetry(attempt, error, signal)) {
731
- if (signal?.aborted) {
732
- throw new Error("Unit of Work execution aborted");
733
- }
734
- throw new Error("Unit of Work execution failed: optimistic concurrency conflict", {
735
- cause: error,
736
- });
737
- }
1066
+ /**
1067
+ * Context passed to transform callback when mutate is NOT provided.
1068
+ */
1069
+ export interface BuilderTransformContextWithoutMutate<
1070
+ TRetrieveSuccessResult,
1071
+ TServiceFinalResult extends readonly unknown[],
1072
+ TServiceIntermediateResult extends readonly unknown[],
1073
+ > {
1074
+ /** Result from transformRetrieve callback (or raw retrieve results if no transformRetrieve) */
1075
+ retrieveResult: TRetrieveSuccessResult;
1076
+ /** No mutate callback was provided */
1077
+ mutateResult: undefined;
1078
+ /** Array of final results from service calls (after success/transform callbacks) */
1079
+ serviceResult: TServiceFinalResult;
1080
+ /** Array of retrieve success results from service calls (same as what mutate receives: retrieve results if service has retrieve, mutate result if service only mutates) */
1081
+ serviceIntermediateResult: TServiceIntermediateResult;
1082
+ }
738
1083
 
739
- const delayMs = retryPolicy.getDelayMs(attempt);
740
- if (delayMs > 0) {
741
- await new Promise((resolve) => setTimeout(resolve, delayMs));
742
- }
1084
+ /**
1085
+ * Infer the final result type from builder state:
1086
+ * 1. transform TTransformResult
1087
+ * 2. mutate → AwaitedPromisesInObject<TMutateResult>
1088
+ * 3. transformRetrieve → TRetrieveSuccessResult
1089
+ * 4. retrieve → TRetrieveResults
1090
+ * 5. withServiceCalls → ExtractServiceFinalResults<TServiceCalls>
1091
+ */
1092
+ export type InferBuilderResultType<
1093
+ TRetrieveResults extends unknown[],
1094
+ TRetrieveSuccessResult,
1095
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1096
+ TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
1097
+ TMutateResult,
1098
+ TTransformResult,
1099
+ HasTransform extends boolean,
1100
+ HasMutate extends boolean,
1101
+ HasTransformRetrieve extends boolean,
1102
+ HasRetrieve extends boolean,
1103
+ > = HasTransform extends true
1104
+ ? TTransformResult
1105
+ : HasMutate extends true
1106
+ ? AwaitedPromisesInObject<TMutateResult>
1107
+ : HasTransformRetrieve extends true
1108
+ ? TRetrieveSuccessResult
1109
+ : HasRetrieve extends true
1110
+ ? TRetrieveResults
1111
+ : ExtractServiceFinalResults<TServiceCalls>;
743
1112
 
744
- attempt++;
745
- }
746
- }
1113
+ /**
1114
+ * Infer the retrieve success result type for the builder:
1115
+ * - If transformRetrieve exists: TRetrieveSuccessResult
1116
+ * - Else if retrieve exists: TRetrieveResults (raw retrieve results)
1117
+ * - Else if mutate exists: AwaitedPromisesInObject<TMutateResult>
1118
+ * (mutate result becomes retrieve result for dependents)
1119
+ * - Else: TRetrieveResults (raw retrieve results, typically [])
1120
+ */
1121
+ export type InferBuilderRetrieveSuccessResult<
1122
+ TRetrieveResults extends unknown[],
1123
+ TRetrieveSuccessResult,
1124
+ TMutateResult,
1125
+ HasTransformRetrieve extends boolean,
1126
+ HasRetrieve extends boolean,
1127
+ HasMutate extends boolean,
1128
+ > = HasTransformRetrieve extends true
1129
+ ? TRetrieveSuccessResult
1130
+ : HasRetrieve extends true
1131
+ ? TRetrieveResults
1132
+ : HasMutate extends true
1133
+ ? AwaitedPromisesInObject<TMutateResult>
1134
+ : TRetrieveResults;
1135
+
1136
+ /**
1137
+ * Internal state for ServiceTxBuilder
1138
+ */
1139
+ interface ServiceTxBuilderState<
1140
+ TSchema extends AnySchema,
1141
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1142
+ TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
1143
+ TRetrieveResults extends unknown[],
1144
+ TRetrieveSuccessResult,
1145
+ TMutateResult,
1146
+ TTransformResult,
1147
+ THooks extends HooksMap,
1148
+ > {
1149
+ schema: TSchema;
1150
+ baseUow: IUnitOfWork;
1151
+ hooks?: THooks;
1152
+ withServiceCallsFn?: () => TServiceCalls;
1153
+ retrieveFn?: (
1154
+ uow: TypedUnitOfWork<TSchema, [], unknown, THooks>,
1155
+ ) => TypedUnitOfWork<TSchema, TRetrieveResults, unknown, THooks>;
1156
+ transformRetrieveFn?: (
1157
+ retrieveResult: TRetrieveResults,
1158
+ serviceRetrieveResult: ExtractServiceRetrieveResults<TServiceCalls>,
1159
+ ) => TRetrieveSuccessResult;
1160
+ mutateFn?: (
1161
+ ctx: ServiceBuilderMutateContext<
1162
+ TSchema,
1163
+ TRetrieveSuccessResult,
1164
+ ExtractServiceRetrieveResults<TServiceCalls>,
1165
+ THooks
1166
+ >,
1167
+ ) => TMutateResult;
1168
+ transformFn?: (
1169
+ ctx:
1170
+ | BuilderTransformContextWithMutate<
1171
+ TRetrieveSuccessResult,
1172
+ TMutateResult,
1173
+ ExtractServiceFinalResults<TServiceCalls>,
1174
+ ExtractServiceRetrieveResults<TServiceCalls>
1175
+ >
1176
+ | BuilderTransformContextWithoutMutate<
1177
+ TRetrieveSuccessResult,
1178
+ ExtractServiceFinalResults<TServiceCalls>,
1179
+ ExtractServiceRetrieveResults<TServiceCalls>
1180
+ >,
1181
+ ) => TTransformResult;
747
1182
  }
748
1183
 
749
1184
  /**
750
- * Execute a transaction for service context.
751
- * Service callbacks can be async for ergonomic async work.
1185
+ * Builder for service-level transactions.
1186
+ * Uses a fluent API to build up transaction callbacks with proper type inference.
752
1187
  *
753
- * @param schema - Schema to use for the transaction
754
- * @param callbacks - Object containing retrieve and mutate callbacks
755
- * @param baseUow - Base Unit of Work (restricted) to use
756
- * @returns Promise resolving to the mutation result with promises awaited 1 level deep
1188
+ * @example
1189
+ * ```ts
1190
+ * return serviceTx(schema)
1191
+ * .withServiceCalls(() => [otherService.getData()])
1192
+ * .retrieve((uow) => uow.find("users", ...))
1193
+ * .transformRetrieve(([users], serviceResult) => users[0])
1194
+ * .mutate(({ uow, retrieveResult, serviceIntermediateResult }) =>
1195
+ * uow.create("records", { ... })
1196
+ * )
1197
+ * .transform(({ mutateResult, serviceResult, serviceIntermediateResult }) => ({ id: mutateResult }))
1198
+ * .build();
1199
+ * ```
757
1200
  */
758
- export async function executeServiceTx<
1201
+ export class ServiceTxBuilder<
759
1202
  TSchema extends AnySchema,
760
- TRetrievalResults extends unknown[],
761
- TMutationResult,
1203
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1204
+ TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
1205
+ TRetrieveResults extends unknown[],
1206
+ TRetrieveSuccessResult,
1207
+ TMutateResult,
1208
+ TTransformResult,
1209
+ HasRetrieve extends boolean,
1210
+ HasTransformRetrieve extends boolean,
1211
+ HasMutate extends boolean,
1212
+ HasTransform extends boolean,
762
1213
  THooks extends HooksMap,
763
- >(
764
- schema: TSchema,
765
- callbacks: ServiceTxCallbacks<TSchema, TRetrievalResults, TMutationResult, THooks>,
766
- baseUow: IUnitOfWork,
767
- ): Promise<AwaitedPromisesInObject<TMutationResult>> {
768
- const typedUow = baseUow.restrict({ readyFor: "none" }).forSchema<TSchema, THooks>(schema);
1214
+ > {
1215
+ readonly #state: ServiceTxBuilderState<
1216
+ TSchema,
1217
+ TServiceCalls,
1218
+ TRetrieveResults,
1219
+ TRetrieveSuccessResult,
1220
+ TMutateResult,
1221
+ TTransformResult,
1222
+ THooks
1223
+ >;
1224
+
1225
+ constructor(
1226
+ state: ServiceTxBuilderState<
1227
+ TSchema,
1228
+ TServiceCalls,
1229
+ TRetrieveResults,
1230
+ TRetrieveSuccessResult,
1231
+ TMutateResult,
1232
+ TTransformResult,
1233
+ THooks
1234
+ >,
1235
+ ) {
1236
+ this.#state = state;
1237
+ }
769
1238
 
770
- let retrievalUow: TypedUnitOfWork<TSchema, TRetrievalResults, unknown, THooks>;
771
- try {
772
- if (callbacks.retrieve) {
773
- retrievalUow = callbacks.retrieve(typedUow);
774
- } else {
775
- // Safe cast: when there's no retrieve callback, TRetrievalResults should be []
776
- retrievalUow = typedUow as unknown as TypedUnitOfWork<
1239
+ /**
1240
+ * Add dependencies to execute before this transaction.
1241
+ */
1242
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1243
+ withServiceCalls<TNewDeps extends readonly (TxResult<any, any> | undefined)[]>(
1244
+ fn: () => TNewDeps,
1245
+ ): ServiceTxBuilder<
1246
+ TSchema,
1247
+ TNewDeps,
1248
+ TRetrieveResults,
1249
+ TRetrieveSuccessResult,
1250
+ TMutateResult,
1251
+ TTransformResult,
1252
+ HasRetrieve,
1253
+ HasTransformRetrieve,
1254
+ HasMutate,
1255
+ HasTransform,
1256
+ THooks
1257
+ > {
1258
+ return new ServiceTxBuilder({
1259
+ ...this.#state,
1260
+ withServiceCallsFn: fn,
1261
+ } as ServiceTxBuilderState<
1262
+ TSchema,
1263
+ TNewDeps,
1264
+ TRetrieveResults,
1265
+ TRetrieveSuccessResult,
1266
+ TMutateResult,
1267
+ TTransformResult,
1268
+ THooks
1269
+ >);
1270
+ }
1271
+
1272
+ /**
1273
+ * Add retrieval operations to the transaction.
1274
+ */
1275
+ retrieve<TNewRetrieveResults extends unknown[]>(
1276
+ fn: (
1277
+ uow: TypedUnitOfWork<TSchema, [], unknown, THooks>,
1278
+ ) => TypedUnitOfWork<TSchema, TNewRetrieveResults, unknown, THooks>,
1279
+ ): ServiceTxBuilder<
1280
+ TSchema,
1281
+ TServiceCalls,
1282
+ TNewRetrieveResults,
1283
+ TNewRetrieveResults, // Default TRetrieveSuccessResult to TNewRetrieveResults
1284
+ TMutateResult,
1285
+ TTransformResult,
1286
+ true, // HasRetrieve = true
1287
+ false, // Reset HasTransformRetrieve since retrieve results changed
1288
+ HasMutate,
1289
+ HasTransform,
1290
+ THooks
1291
+ > {
1292
+ return new ServiceTxBuilder({
1293
+ ...this.#state,
1294
+ retrieveFn: fn,
1295
+ transformRetrieveFn: undefined, // Clear any existing transformRetrieve since results shape changed
1296
+ } as unknown as ServiceTxBuilderState<
1297
+ TSchema,
1298
+ TServiceCalls,
1299
+ TNewRetrieveResults,
1300
+ TNewRetrieveResults,
1301
+ TMutateResult,
1302
+ TTransformResult,
1303
+ THooks
1304
+ >);
1305
+ }
1306
+
1307
+ /**
1308
+ * Transform retrieve results before passing to mutate.
1309
+ */
1310
+ transformRetrieve<TNewRetrieveSuccessResult>(
1311
+ fn: (
1312
+ retrieveResult: TRetrieveResults,
1313
+ serviceResult: ExtractServiceRetrieveResults<TServiceCalls>,
1314
+ ) => TNewRetrieveSuccessResult,
1315
+ ): ServiceTxBuilder<
1316
+ TSchema,
1317
+ TServiceCalls,
1318
+ TRetrieveResults,
1319
+ TNewRetrieveSuccessResult,
1320
+ TMutateResult,
1321
+ TTransformResult,
1322
+ HasRetrieve,
1323
+ true, // HasTransformRetrieve = true
1324
+ HasMutate,
1325
+ HasTransform,
1326
+ THooks
1327
+ > {
1328
+ return new ServiceTxBuilder({
1329
+ ...this.#state,
1330
+ transformRetrieveFn: fn,
1331
+ } as unknown as ServiceTxBuilderState<
1332
+ TSchema,
1333
+ TServiceCalls,
1334
+ TRetrieveResults,
1335
+ TNewRetrieveSuccessResult,
1336
+ TMutateResult,
1337
+ TTransformResult,
1338
+ THooks
1339
+ >);
1340
+ }
1341
+
1342
+ /**
1343
+ * Add mutation operations based on retrieve results.
1344
+ */
1345
+ mutate<TNewMutateResult>(
1346
+ fn: (
1347
+ ctx: ServiceBuilderMutateContext<
777
1348
  TSchema,
778
- TRetrievalResults,
779
- unknown,
1349
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1350
+ ExtractServiceRetrieveResults<TServiceCalls>,
780
1351
  THooks
781
- >;
782
- }
783
- } catch (error) {
784
- typedUow.signalReadyForRetrieval();
785
- typedUow.signalReadyForMutation();
786
- throw error;
1352
+ >,
1353
+ ) => TNewMutateResult,
1354
+ ): ServiceTxBuilder<
1355
+ TSchema,
1356
+ TServiceCalls,
1357
+ TRetrieveResults,
1358
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1359
+ TNewMutateResult,
1360
+ TTransformResult,
1361
+ HasRetrieve,
1362
+ HasTransformRetrieve,
1363
+ true, // HasMutate = true
1364
+ HasTransform,
1365
+ THooks
1366
+ > {
1367
+ return new ServiceTxBuilder({
1368
+ ...this.#state,
1369
+ mutateFn: fn,
1370
+ } as unknown as ServiceTxBuilderState<
1371
+ TSchema,
1372
+ TServiceCalls,
1373
+ TRetrieveResults,
1374
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1375
+ TNewMutateResult,
1376
+ TTransformResult,
1377
+ THooks
1378
+ >);
787
1379
  }
788
1380
 
789
- typedUow.signalReadyForRetrieval();
1381
+ /**
1382
+ * Add final transformation after mutations complete.
1383
+ */
1384
+ transform<TNewTransformResult>(
1385
+ fn: (
1386
+ ctx: HasMutate extends true
1387
+ ? BuilderTransformContextWithMutate<
1388
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1389
+ TMutateResult,
1390
+ ExtractServiceFinalResults<TServiceCalls>,
1391
+ ExtractServiceRetrieveResults<TServiceCalls>
1392
+ >
1393
+ : BuilderTransformContextWithoutMutate<
1394
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1395
+ ExtractServiceFinalResults<TServiceCalls>,
1396
+ ExtractServiceRetrieveResults<TServiceCalls>
1397
+ >,
1398
+ ) => TNewTransformResult,
1399
+ ): ServiceTxBuilder<
1400
+ TSchema,
1401
+ TServiceCalls,
1402
+ TRetrieveResults,
1403
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1404
+ TMutateResult,
1405
+ TNewTransformResult,
1406
+ HasRetrieve,
1407
+ HasTransformRetrieve,
1408
+ HasMutate,
1409
+ true, // HasTransform = true
1410
+ THooks
1411
+ > {
1412
+ return new ServiceTxBuilder({
1413
+ ...this.#state,
1414
+ transformFn: fn,
1415
+ } as unknown as ServiceTxBuilderState<
1416
+ TSchema,
1417
+ TServiceCalls,
1418
+ TRetrieveResults,
1419
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1420
+ TMutateResult,
1421
+ TNewTransformResult,
1422
+ THooks
1423
+ >);
1424
+ }
790
1425
 
791
- // Safe cast: retrievalPhase returns the correct type based on the UOW's type parameters
792
- const results = (await retrievalUow.retrievalPhase) as TRetrievalResults;
1426
+ /**
1427
+ * Build and return the TxResult.
1428
+ */
1429
+ build(): TxResult<
1430
+ InferBuilderResultType<
1431
+ TRetrieveResults,
1432
+ TRetrieveSuccessResult,
1433
+ TServiceCalls,
1434
+ TMutateResult,
1435
+ TTransformResult,
1436
+ HasTransform,
1437
+ HasMutate,
1438
+ HasTransformRetrieve,
1439
+ HasRetrieve
1440
+ >,
1441
+ InferBuilderRetrieveSuccessResult<
1442
+ TRetrieveResults,
1443
+ TRetrieveSuccessResult,
1444
+ TMutateResult,
1445
+ HasTransformRetrieve,
1446
+ HasRetrieve,
1447
+ HasMutate
1448
+ >
1449
+ > {
1450
+ const state = this.#state;
1451
+
1452
+ // Convert builder state to legacy callbacks format
1453
+ const callbacks: ServiceTxCallbacks<
1454
+ TSchema,
1455
+ TRetrieveResults,
1456
+ TRetrieveSuccessResult,
1457
+ TServiceCalls,
1458
+ TMutateResult,
1459
+ TTransformResult,
1460
+ THooks
1461
+ > = {
1462
+ serviceCalls: state.withServiceCallsFn,
1463
+ retrieve: state.retrieveFn,
1464
+ retrieveSuccess: state.transformRetrieveFn,
1465
+ mutate: state.mutateFn
1466
+ ? (ctx) => {
1467
+ return state.mutateFn!({
1468
+ uow: ctx.uow,
1469
+ retrieveResult: ctx.retrieveResult,
1470
+ serviceIntermediateResult: ctx.serviceIntermediateResult,
1471
+ });
1472
+ }
1473
+ : undefined,
1474
+ success: state.transformFn
1475
+ ? (ctx) => {
1476
+ return state.transformFn!({
1477
+ retrieveResult: ctx.retrieveResult,
1478
+ mutateResult: ctx.mutateResult,
1479
+ serviceResult: ctx.serviceResult,
1480
+ serviceIntermediateResult: ctx.serviceIntermediateResult,
1481
+ } as BuilderTransformContextWithMutate<
1482
+ TRetrieveSuccessResult,
1483
+ TMutateResult,
1484
+ ExtractServiceFinalResults<TServiceCalls>,
1485
+ ExtractServiceRetrieveResults<TServiceCalls>
1486
+ >);
1487
+ }
1488
+ : undefined,
1489
+ };
793
1490
 
794
- let mutationResult: TMutationResult;
795
- try {
796
- if (callbacks.mutate) {
797
- mutationResult = await callbacks.mutate(retrievalUow, results);
798
- } else {
799
- // Safe cast: when there's no mutate callback, TMutationResult should be void
800
- mutationResult = undefined as TMutationResult;
801
- }
802
- } catch (error) {
803
- typedUow.signalReadyForMutation();
804
- throw error;
1491
+ // Use the existing createServiceTx implementation
1492
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1493
+ return createServiceTx(state.schema, callbacks as any, state.baseUow) as unknown as TxResult<
1494
+ InferBuilderResultType<
1495
+ TRetrieveResults,
1496
+ TRetrieveSuccessResult,
1497
+ TServiceCalls,
1498
+ TMutateResult,
1499
+ TTransformResult,
1500
+ HasTransform,
1501
+ HasMutate,
1502
+ HasTransformRetrieve,
1503
+ HasRetrieve
1504
+ >,
1505
+ InferBuilderRetrieveSuccessResult<
1506
+ TRetrieveResults,
1507
+ TRetrieveSuccessResult,
1508
+ TMutateResult,
1509
+ HasTransformRetrieve,
1510
+ HasRetrieve,
1511
+ HasMutate
1512
+ >
1513
+ >;
1514
+ }
1515
+ }
1516
+
1517
+ /**
1518
+ * Create a new ServiceTxBuilder for the given schema.
1519
+ */
1520
+ export function createServiceTxBuilder<TSchema extends AnySchema, THooks extends HooksMap = {}>(
1521
+ schema: TSchema,
1522
+ baseUow: IUnitOfWork,
1523
+ hooks?: THooks,
1524
+ ): ServiceTxBuilder<
1525
+ TSchema,
1526
+ readonly [],
1527
+ [],
1528
+ [],
1529
+ unknown,
1530
+ unknown,
1531
+ false,
1532
+ false,
1533
+ false,
1534
+ false,
1535
+ THooks
1536
+ > {
1537
+ return new ServiceTxBuilder({
1538
+ schema,
1539
+ baseUow,
1540
+ hooks,
1541
+ });
1542
+ }
1543
+
1544
+ /**
1545
+ * Internal state for HandlerTxBuilder
1546
+ */
1547
+ interface HandlerTxBuilderState<
1548
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1549
+ TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
1550
+ TRetrieveResults extends unknown[],
1551
+ TRetrieveSuccessResult,
1552
+ TMutateResult,
1553
+ TTransformResult,
1554
+ THooks extends HooksMap,
1555
+ > {
1556
+ options: ExecuteTxOptions;
1557
+ hooks?: THooks;
1558
+ withServiceCallsFn?: () => TServiceCalls;
1559
+ retrieveFn?: (context: {
1560
+ forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
1561
+ schema: S,
1562
+ hooks?: H,
1563
+ ) => TypedUnitOfWork<S, [], unknown, H>;
1564
+ idempotencyKey: string;
1565
+ currentAttempt: number;
1566
+ }) => TypedUnitOfWork<AnySchema, TRetrieveResults, unknown, HooksMap> | void;
1567
+ transformRetrieveFn?: (
1568
+ retrieveResult: TRetrieveResults,
1569
+ serviceResult: ExtractServiceRetrieveResults<TServiceCalls>,
1570
+ ) => TRetrieveSuccessResult;
1571
+ mutateFn?: (
1572
+ ctx: HandlerBuilderMutateContext<
1573
+ TRetrieveSuccessResult,
1574
+ ExtractServiceRetrieveResults<TServiceCalls>,
1575
+ THooks
1576
+ >,
1577
+ ) => TMutateResult;
1578
+ transformFn?: (
1579
+ ctx:
1580
+ | BuilderTransformContextWithMutate<
1581
+ TRetrieveSuccessResult,
1582
+ TMutateResult,
1583
+ ExtractServiceFinalResults<TServiceCalls>,
1584
+ ExtractServiceRetrieveResults<TServiceCalls>
1585
+ >
1586
+ | BuilderTransformContextWithoutMutate<
1587
+ TRetrieveSuccessResult,
1588
+ ExtractServiceFinalResults<TServiceCalls>,
1589
+ ExtractServiceRetrieveResults<TServiceCalls>
1590
+ >,
1591
+ ) => TTransformResult;
1592
+ }
1593
+
1594
+ /**
1595
+ * Builder for handler-level transactions.
1596
+ * Uses a fluent API to build up transaction callbacks with proper type inference.
1597
+ *
1598
+ * @example
1599
+ * ```ts
1600
+ * const result = await handlerTx()
1601
+ * .withServiceCalls(() => [userService.getUser(id)])
1602
+ * .mutate(({ forSchema, idempotencyKey, currentAttempt, serviceIntermediateResult }) => {
1603
+ * return forSchema(ordersSchema).create("orders", { ... });
1604
+ * })
1605
+ * .transform(({ mutateResult, serviceResult }) => ({ ... }))
1606
+ * .execute();
1607
+ * ```
1608
+ */
1609
+ export class HandlerTxBuilder<
1610
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1611
+ TServiceCalls extends readonly (TxResult<any, any> | undefined)[],
1612
+ TRetrieveResults extends unknown[],
1613
+ TRetrieveSuccessResult,
1614
+ TMutateResult,
1615
+ TTransformResult,
1616
+ HasRetrieve extends boolean,
1617
+ HasTransformRetrieve extends boolean,
1618
+ HasMutate extends boolean,
1619
+ HasTransform extends boolean,
1620
+ THooks extends HooksMap,
1621
+ > {
1622
+ readonly #state: HandlerTxBuilderState<
1623
+ TServiceCalls,
1624
+ TRetrieveResults,
1625
+ TRetrieveSuccessResult,
1626
+ TMutateResult,
1627
+ TTransformResult,
1628
+ THooks
1629
+ >;
1630
+
1631
+ constructor(
1632
+ state: HandlerTxBuilderState<
1633
+ TServiceCalls,
1634
+ TRetrieveResults,
1635
+ TRetrieveSuccessResult,
1636
+ TMutateResult,
1637
+ TTransformResult,
1638
+ THooks
1639
+ >,
1640
+ ) {
1641
+ this.#state = state;
805
1642
  }
806
1643
 
807
- typedUow.signalReadyForMutation();
1644
+ /**
1645
+ * Add dependencies to execute before this transaction.
1646
+ */
1647
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1648
+ withServiceCalls<TNewDeps extends readonly (TxResult<any, any> | undefined)[]>(
1649
+ fn: () => TNewDeps,
1650
+ ): HandlerTxBuilder<
1651
+ TNewDeps,
1652
+ TRetrieveResults,
1653
+ TRetrieveSuccessResult,
1654
+ TMutateResult,
1655
+ TTransformResult,
1656
+ HasRetrieve,
1657
+ HasTransformRetrieve,
1658
+ HasMutate,
1659
+ HasTransform,
1660
+ THooks
1661
+ > {
1662
+ return new HandlerTxBuilder({
1663
+ ...this.#state,
1664
+ withServiceCallsFn: fn,
1665
+ } as HandlerTxBuilderState<
1666
+ TNewDeps,
1667
+ TRetrieveResults,
1668
+ TRetrieveSuccessResult,
1669
+ TMutateResult,
1670
+ TTransformResult,
1671
+ THooks
1672
+ >);
1673
+ }
1674
+
1675
+ /**
1676
+ * Add retrieval operations to the transaction.
1677
+ * Return a TypedUnitOfWork from forSchema().find() to get typed results.
1678
+ */
1679
+ retrieve<TNewRetrieveResults extends unknown[]>(
1680
+ fn: (context: {
1681
+ forSchema: <S extends AnySchema, H extends HooksMap = THooks>(
1682
+ schema: S,
1683
+ hooks?: H,
1684
+ ) => TypedUnitOfWork<S, [], unknown, H>;
1685
+ idempotencyKey: string;
1686
+ currentAttempt: number;
1687
+ }) => TypedUnitOfWork<AnySchema, TNewRetrieveResults, unknown, HooksMap> | void,
1688
+ ): HandlerTxBuilder<
1689
+ TServiceCalls,
1690
+ TNewRetrieveResults,
1691
+ TNewRetrieveResults, // Default TRetrieveSuccessResult to TNewRetrieveResults
1692
+ TMutateResult,
1693
+ TTransformResult,
1694
+ true, // HasRetrieve = true
1695
+ false, // Reset HasTransformRetrieve since retrieve results changed
1696
+ HasMutate,
1697
+ HasTransform,
1698
+ THooks
1699
+ > {
1700
+ return new HandlerTxBuilder({
1701
+ ...this.#state,
1702
+ retrieveFn: fn,
1703
+ transformRetrieveFn: undefined, // Clear any existing transformRetrieve since results shape changed
1704
+ } as unknown as HandlerTxBuilderState<
1705
+ TServiceCalls,
1706
+ TNewRetrieveResults,
1707
+ TNewRetrieveResults,
1708
+ TMutateResult,
1709
+ TTransformResult,
1710
+ THooks
1711
+ >);
1712
+ }
808
1713
 
809
- await retrievalUow.mutationPhase;
1714
+ /**
1715
+ * Transform retrieve results before passing to mutate.
1716
+ */
1717
+ transformRetrieve<TNewRetrieveSuccessResult>(
1718
+ fn: (
1719
+ retrieveResult: TRetrieveResults,
1720
+ serviceResult: ExtractServiceRetrieveResults<TServiceCalls>,
1721
+ ) => TNewRetrieveSuccessResult,
1722
+ ): HandlerTxBuilder<
1723
+ TServiceCalls,
1724
+ TRetrieveResults,
1725
+ TNewRetrieveSuccessResult,
1726
+ TMutateResult,
1727
+ TTransformResult,
1728
+ HasRetrieve,
1729
+ true, // HasTransformRetrieve = true
1730
+ HasMutate,
1731
+ HasTransform,
1732
+ THooks
1733
+ > {
1734
+ return new HandlerTxBuilder({
1735
+ ...this.#state,
1736
+ transformRetrieveFn: fn,
1737
+ } as unknown as HandlerTxBuilderState<
1738
+ TServiceCalls,
1739
+ TRetrieveResults,
1740
+ TNewRetrieveSuccessResult,
1741
+ TMutateResult,
1742
+ TTransformResult,
1743
+ THooks
1744
+ >);
1745
+ }
810
1746
 
811
- return await awaitPromisesInObject(mutationResult);
1747
+ /**
1748
+ * Add mutation operations based on retrieve results.
1749
+ */
1750
+ mutate<TNewMutateResult>(
1751
+ fn: (
1752
+ ctx: HandlerBuilderMutateContext<
1753
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1754
+ ExtractServiceRetrieveResults<TServiceCalls>,
1755
+ THooks
1756
+ >,
1757
+ ) => TNewMutateResult,
1758
+ ): HandlerTxBuilder<
1759
+ TServiceCalls,
1760
+ TRetrieveResults,
1761
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1762
+ TNewMutateResult,
1763
+ TTransformResult,
1764
+ HasRetrieve,
1765
+ HasTransformRetrieve,
1766
+ true, // HasMutate = true
1767
+ HasTransform,
1768
+ THooks
1769
+ > {
1770
+ return new HandlerTxBuilder({
1771
+ ...this.#state,
1772
+ mutateFn: fn,
1773
+ } as unknown as HandlerTxBuilderState<
1774
+ TServiceCalls,
1775
+ TRetrieveResults,
1776
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1777
+ TNewMutateResult,
1778
+ TTransformResult,
1779
+ THooks
1780
+ >);
1781
+ }
1782
+
1783
+ /**
1784
+ * Add final transformation after mutations complete.
1785
+ */
1786
+ transform<TNewTransformResult>(
1787
+ fn: (
1788
+ ctx: HasMutate extends true
1789
+ ? BuilderTransformContextWithMutate<
1790
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1791
+ TMutateResult,
1792
+ ExtractServiceFinalResults<TServiceCalls>,
1793
+ ExtractServiceRetrieveResults<TServiceCalls>
1794
+ >
1795
+ : BuilderTransformContextWithoutMutate<
1796
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1797
+ ExtractServiceFinalResults<TServiceCalls>,
1798
+ ExtractServiceRetrieveResults<TServiceCalls>
1799
+ >,
1800
+ ) => TNewTransformResult,
1801
+ ): HandlerTxBuilder<
1802
+ TServiceCalls,
1803
+ TRetrieveResults,
1804
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1805
+ TMutateResult,
1806
+ TNewTransformResult,
1807
+ HasRetrieve,
1808
+ HasTransformRetrieve,
1809
+ HasMutate,
1810
+ true, // HasTransform = true
1811
+ THooks
1812
+ > {
1813
+ return new HandlerTxBuilder({
1814
+ ...this.#state,
1815
+ transformFn: fn,
1816
+ } as unknown as HandlerTxBuilderState<
1817
+ TServiceCalls,
1818
+ TRetrieveResults,
1819
+ HasTransformRetrieve extends true ? TRetrieveSuccessResult : TRetrieveResults,
1820
+ TMutateResult,
1821
+ TNewTransformResult,
1822
+ THooks
1823
+ >);
1824
+ }
1825
+
1826
+ /**
1827
+ * Execute the transaction and return the result.
1828
+ */
1829
+ execute(): Promise<
1830
+ AwaitedPromisesInObject<
1831
+ InferBuilderResultType<
1832
+ TRetrieveResults,
1833
+ TRetrieveSuccessResult,
1834
+ TServiceCalls,
1835
+ TMutateResult,
1836
+ TTransformResult,
1837
+ HasTransform,
1838
+ HasMutate,
1839
+ HasTransformRetrieve,
1840
+ HasRetrieve
1841
+ >
1842
+ >
1843
+ > {
1844
+ const state = this.#state;
1845
+
1846
+ // Convert builder state to legacy callbacks format
1847
+ const callbacks: HandlerTxCallbacks<
1848
+ TRetrieveResults,
1849
+ TRetrieveSuccessResult,
1850
+ TServiceCalls,
1851
+ TMutateResult,
1852
+ TTransformResult,
1853
+ THooks
1854
+ > = {
1855
+ serviceCalls: state.withServiceCallsFn,
1856
+ retrieve: state.retrieveFn
1857
+ ? (context) => {
1858
+ return state.retrieveFn!({
1859
+ forSchema: context.forSchema,
1860
+ idempotencyKey: context.idempotencyKey,
1861
+ currentAttempt: context.currentAttempt,
1862
+ });
1863
+ }
1864
+ : undefined,
1865
+ retrieveSuccess: state.transformRetrieveFn,
1866
+ mutate: state.mutateFn
1867
+ ? (ctx) => {
1868
+ return state.mutateFn!({
1869
+ forSchema: ctx.forSchema,
1870
+ idempotencyKey: ctx.idempotencyKey,
1871
+ currentAttempt: ctx.currentAttempt,
1872
+ retrieveResult: ctx.retrieveResult,
1873
+ serviceIntermediateResult: ctx.serviceIntermediateResult,
1874
+ });
1875
+ }
1876
+ : undefined,
1877
+ success: state.transformFn
1878
+ ? (ctx) => {
1879
+ return state.transformFn!({
1880
+ retrieveResult: ctx.retrieveResult,
1881
+ mutateResult: ctx.mutateResult,
1882
+ serviceResult: ctx.serviceResult,
1883
+ serviceIntermediateResult: ctx.serviceIntermediateResult,
1884
+ } as BuilderTransformContextWithMutate<
1885
+ TRetrieveSuccessResult,
1886
+ TMutateResult,
1887
+ ExtractServiceFinalResults<TServiceCalls>,
1888
+ ExtractServiceRetrieveResults<TServiceCalls>
1889
+ >);
1890
+ }
1891
+ : undefined,
1892
+ };
1893
+
1894
+ // Use the existing executeTx implementation
1895
+ return executeTx(callbacks as Parameters<typeof executeTx>[0], state.options) as Promise<
1896
+ AwaitedPromisesInObject<
1897
+ InferBuilderResultType<
1898
+ TRetrieveResults,
1899
+ TRetrieveSuccessResult,
1900
+ TServiceCalls,
1901
+ TMutateResult,
1902
+ TTransformResult,
1903
+ HasTransform,
1904
+ HasMutate,
1905
+ HasTransformRetrieve,
1906
+ HasRetrieve
1907
+ >
1908
+ >
1909
+ >;
1910
+ }
1911
+ }
1912
+
1913
+ /**
1914
+ * Create a new HandlerTxBuilder with the given options.
1915
+ */
1916
+ export function createHandlerTxBuilder<THooks extends HooksMap = {}>(
1917
+ options: ExecuteTxOptions,
1918
+ hooks?: THooks,
1919
+ ): HandlerTxBuilder<readonly [], [], [], unknown, unknown, false, false, false, false, THooks> {
1920
+ return new HandlerTxBuilder({
1921
+ options,
1922
+ hooks,
1923
+ });
812
1924
  }