@fragno-dev/db 0.1.14 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (445) hide show
  1. package/.turbo/turbo-build.log +242 -139
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +123 -8
  4. package/dist/adapters/adapters.d.ts +19 -5
  5. package/dist/adapters/adapters.d.ts.map +1 -1
  6. package/dist/adapters/adapters.js.map +1 -1
  7. package/dist/adapters/drizzle/drizzle-adapter.d.ts +6 -19
  8. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +1 -1
  9. package/dist/adapters/drizzle/drizzle-adapter.js +7 -47
  10. package/dist/adapters/drizzle/drizzle-adapter.js.map +1 -1
  11. package/dist/adapters/drizzle/generate.d.ts +7 -1
  12. package/dist/adapters/drizzle/generate.d.ts.map +1 -1
  13. package/dist/adapters/drizzle/generate.js +46 -45
  14. package/dist/adapters/drizzle/generate.js.map +1 -1
  15. package/dist/adapters/generic-sql/driver-config.d.ts +74 -0
  16. package/dist/adapters/generic-sql/driver-config.d.ts.map +1 -0
  17. package/dist/adapters/generic-sql/driver-config.js +94 -0
  18. package/dist/adapters/generic-sql/driver-config.js.map +1 -0
  19. package/dist/adapters/generic-sql/generic-sql-adapter.d.ts +43 -0
  20. package/dist/adapters/generic-sql/generic-sql-adapter.d.ts.map +1 -0
  21. package/dist/adapters/generic-sql/generic-sql-adapter.js +87 -0
  22. package/dist/adapters/generic-sql/generic-sql-adapter.js.map +1 -0
  23. package/dist/adapters/generic-sql/generic-sql-uow-executor.js +67 -0
  24. package/dist/adapters/generic-sql/generic-sql-uow-executor.js.map +1 -0
  25. package/dist/adapters/generic-sql/migration/cold-kysely.js +33 -0
  26. package/dist/adapters/generic-sql/migration/cold-kysely.js.map +1 -0
  27. package/dist/adapters/generic-sql/migration/dialect/mysql.js +60 -0
  28. package/dist/adapters/generic-sql/migration/dialect/mysql.js.map +1 -0
  29. package/dist/adapters/generic-sql/migration/dialect/postgres.js +59 -0
  30. package/dist/adapters/generic-sql/migration/dialect/postgres.js.map +1 -0
  31. package/dist/adapters/generic-sql/migration/dialect/sqlite.js +96 -0
  32. package/dist/adapters/generic-sql/migration/dialect/sqlite.js.map +1 -0
  33. package/dist/adapters/generic-sql/migration/executor.d.ts +15 -0
  34. package/dist/adapters/generic-sql/migration/executor.d.ts.map +1 -0
  35. package/dist/adapters/generic-sql/migration/executor.js +18 -0
  36. package/dist/adapters/generic-sql/migration/executor.js.map +1 -0
  37. package/dist/adapters/generic-sql/migration/prepared-migrations.d.ts +66 -0
  38. package/dist/adapters/generic-sql/migration/prepared-migrations.d.ts.map +1 -0
  39. package/dist/adapters/generic-sql/migration/prepared-migrations.js +68 -0
  40. package/dist/adapters/generic-sql/migration/prepared-migrations.js.map +1 -0
  41. package/dist/adapters/generic-sql/migration/sql-generator.js +212 -0
  42. package/dist/adapters/generic-sql/migration/sql-generator.js.map +1 -0
  43. package/dist/adapters/generic-sql/query/create-sql-query-compiler.js +32 -0
  44. package/dist/adapters/generic-sql/query/create-sql-query-compiler.js.map +1 -0
  45. package/dist/adapters/generic-sql/query/cursor-utils.js +37 -0
  46. package/dist/adapters/generic-sql/query/cursor-utils.js.map +1 -0
  47. package/dist/adapters/generic-sql/query/dialect/mysql.js +33 -0
  48. package/dist/adapters/generic-sql/query/dialect/mysql.js.map +1 -0
  49. package/dist/adapters/generic-sql/query/dialect/postgres.js +32 -0
  50. package/dist/adapters/generic-sql/query/dialect/postgres.js.map +1 -0
  51. package/dist/adapters/generic-sql/query/dialect/sqlite.js +32 -0
  52. package/dist/adapters/generic-sql/query/dialect/sqlite.js.map +1 -0
  53. package/dist/adapters/generic-sql/query/generic-sql-uow-operation-compiler.js +152 -0
  54. package/dist/adapters/generic-sql/query/generic-sql-uow-operation-compiler.js.map +1 -0
  55. package/dist/adapters/generic-sql/query/select-builder.js +69 -0
  56. package/dist/adapters/generic-sql/query/select-builder.js.map +1 -0
  57. package/dist/adapters/generic-sql/query/sql-query-compiler.js +145 -0
  58. package/dist/adapters/generic-sql/query/sql-query-compiler.js.map +1 -0
  59. package/dist/adapters/generic-sql/query/where-builder.js +129 -0
  60. package/dist/adapters/generic-sql/query/where-builder.js.map +1 -0
  61. package/dist/adapters/generic-sql/result-interpreter.js +74 -0
  62. package/dist/adapters/generic-sql/result-interpreter.js.map +1 -0
  63. package/dist/adapters/generic-sql/uow-decoder.js +105 -0
  64. package/dist/adapters/generic-sql/uow-decoder.js.map +1 -0
  65. package/dist/adapters/generic-sql/uow-encoder.js +93 -0
  66. package/dist/adapters/generic-sql/uow-encoder.js.map +1 -0
  67. package/dist/adapters/kysely/kysely-adapter.d.ts +5 -16
  68. package/dist/adapters/kysely/kysely-adapter.d.ts.map +1 -1
  69. package/dist/adapters/kysely/kysely-adapter.js +6 -159
  70. package/dist/adapters/kysely/kysely-adapter.js.map +1 -1
  71. package/dist/adapters/{drizzle/drizzle-query.js → shared/from-unit-of-work-compiler.js} +48 -62
  72. package/dist/adapters/shared/from-unit-of-work-compiler.js.map +1 -0
  73. package/dist/adapters/{kysely/kysely-shared.d.ts → shared/table-name-mapper.d.ts} +3 -2
  74. package/dist/adapters/shared/table-name-mapper.d.ts.map +1 -0
  75. package/dist/adapters/shared/table-name-mapper.js +43 -0
  76. package/dist/adapters/shared/table-name-mapper.js.map +1 -0
  77. package/dist/adapters/shared/uow-operation-compiler.js +105 -0
  78. package/dist/adapters/shared/uow-operation-compiler.js.map +1 -0
  79. package/dist/db-fragment-definition-builder.d.ts +186 -0
  80. package/dist/db-fragment-definition-builder.d.ts.map +1 -0
  81. package/dist/db-fragment-definition-builder.js +207 -0
  82. package/dist/db-fragment-definition-builder.js.map +1 -0
  83. package/dist/fragments/internal-fragment.d.ts +53 -0
  84. package/dist/fragments/internal-fragment.d.ts.map +1 -0
  85. package/dist/fragments/internal-fragment.js +111 -0
  86. package/dist/fragments/internal-fragment.js.map +1 -0
  87. package/dist/hooks/hooks.d.ts +51 -0
  88. package/dist/hooks/hooks.d.ts.map +1 -0
  89. package/dist/hooks/hooks.js +88 -0
  90. package/dist/hooks/hooks.js.map +1 -0
  91. package/dist/migration-engine/generation-engine.d.ts +0 -2
  92. package/dist/migration-engine/generation-engine.d.ts.map +1 -1
  93. package/dist/migration-engine/generation-engine.js +38 -56
  94. package/dist/migration-engine/generation-engine.js.map +1 -1
  95. package/dist/mod.d.ts +35 -23
  96. package/dist/mod.d.ts.map +1 -1
  97. package/dist/mod.js +48 -45
  98. package/dist/mod.js.map +1 -1
  99. package/dist/node_modules/.pnpm/rou3@0.7.10/node_modules/rou3/dist/index.js +165 -0
  100. package/dist/node_modules/.pnpm/rou3@0.7.10/node_modules/rou3/dist/index.js.map +1 -0
  101. package/dist/packages/fragno/dist/api/bind-services.js +20 -0
  102. package/dist/packages/fragno/dist/api/bind-services.js.map +1 -0
  103. package/dist/packages/fragno/dist/api/error.js +48 -0
  104. package/dist/packages/fragno/dist/api/error.js.map +1 -0
  105. package/dist/packages/fragno/dist/api/fragment-definition-builder.js +320 -0
  106. package/dist/packages/fragno/dist/api/fragment-definition-builder.js.map +1 -0
  107. package/dist/packages/fragno/dist/api/fragment-instantiator.js +525 -0
  108. package/dist/packages/fragno/dist/api/fragment-instantiator.js.map +1 -0
  109. package/dist/packages/fragno/dist/api/fragno-response.js +73 -0
  110. package/dist/packages/fragno/dist/api/fragno-response.js.map +1 -0
  111. package/dist/packages/fragno/dist/api/internal/response-stream.js +81 -0
  112. package/dist/packages/fragno/dist/api/internal/response-stream.js.map +1 -0
  113. package/dist/packages/fragno/dist/api/internal/route.js +10 -0
  114. package/dist/packages/fragno/dist/api/internal/route.js.map +1 -0
  115. package/dist/packages/fragno/dist/api/mutable-request-state.js +97 -0
  116. package/dist/packages/fragno/dist/api/mutable-request-state.js.map +1 -0
  117. package/dist/packages/fragno/dist/api/request-context-storage.js +43 -0
  118. package/dist/packages/fragno/dist/api/request-context-storage.js.map +1 -0
  119. package/dist/packages/fragno/dist/api/request-input-context.js +118 -0
  120. package/dist/packages/fragno/dist/api/request-input-context.js.map +1 -0
  121. package/dist/packages/fragno/dist/api/request-middleware.js +83 -0
  122. package/dist/packages/fragno/dist/api/request-middleware.js.map +1 -0
  123. package/dist/packages/fragno/dist/api/request-output-context.js +119 -0
  124. package/dist/packages/fragno/dist/api/request-output-context.js.map +1 -0
  125. package/dist/packages/fragno/dist/api/route.js +17 -0
  126. package/dist/packages/fragno/dist/api/route.js.map +1 -0
  127. package/dist/packages/fragno/dist/internal/symbols.js +10 -0
  128. package/dist/packages/fragno/dist/internal/symbols.js.map +1 -0
  129. package/dist/query/column-defaults.js +27 -0
  130. package/dist/query/column-defaults.js.map +1 -0
  131. package/dist/query/cursor.d.ts +14 -6
  132. package/dist/query/cursor.d.ts.map +1 -1
  133. package/dist/query/cursor.js +16 -7
  134. package/dist/query/cursor.js.map +1 -1
  135. package/dist/query/orm/orm.d.ts +1 -1
  136. package/dist/query/orm/orm.js.map +1 -1
  137. package/dist/query/serialize/create-sql-serializer.js +30 -0
  138. package/dist/query/serialize/create-sql-serializer.js.map +1 -0
  139. package/dist/query/serialize/dialect/mysql-serializer.js +87 -0
  140. package/dist/query/serialize/dialect/mysql-serializer.js.map +1 -0
  141. package/dist/query/serialize/dialect/postgres-serializer.js +80 -0
  142. package/dist/query/serialize/dialect/postgres-serializer.js.map +1 -0
  143. package/dist/query/serialize/dialect/sqlite-serializer.js +93 -0
  144. package/dist/query/serialize/dialect/sqlite-serializer.js.map +1 -0
  145. package/dist/query/serialize/sql-serializer.js +67 -0
  146. package/dist/query/serialize/sql-serializer.js.map +1 -0
  147. package/dist/query/{query.d.ts → simple-query-interface.d.ts} +6 -6
  148. package/dist/query/simple-query-interface.d.ts.map +1 -0
  149. package/dist/query/unit-of-work/execute-unit-of-work.d.ts +133 -0
  150. package/dist/query/unit-of-work/execute-unit-of-work.d.ts.map +1 -0
  151. package/dist/query/unit-of-work/execute-unit-of-work.js +197 -0
  152. package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -0
  153. package/dist/query/unit-of-work/retry-policy.d.ts +88 -0
  154. package/dist/query/unit-of-work/retry-policy.d.ts.map +1 -0
  155. package/dist/query/unit-of-work/retry-policy.js +61 -0
  156. package/dist/query/unit-of-work/retry-policy.js.map +1 -0
  157. package/dist/query/{unit-of-work.d.ts → unit-of-work/unit-of-work.d.ts} +145 -58
  158. package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -0
  159. package/dist/query/{unit-of-work.js → unit-of-work/unit-of-work.js} +435 -198
  160. package/dist/query/unit-of-work/unit-of-work.js.map +1 -0
  161. package/dist/query/value-decoding.js +71 -0
  162. package/dist/query/value-decoding.js.map +1 -0
  163. package/dist/query/value-encoding.js +124 -0
  164. package/dist/query/value-encoding.js.map +1 -0
  165. package/dist/schema/create.d.ts +3 -0
  166. package/dist/schema/create.d.ts.map +1 -1
  167. package/dist/schema/create.js +4 -0
  168. package/dist/schema/create.js.map +1 -1
  169. package/dist/schema/type-conversion/create-sql-type-mapper.js +29 -0
  170. package/dist/schema/type-conversion/create-sql-type-mapper.js.map +1 -0
  171. package/dist/schema/type-conversion/dialect/mysql.js +57 -0
  172. package/dist/schema/type-conversion/dialect/mysql.js.map +1 -0
  173. package/dist/schema/type-conversion/dialect/postgres.js +56 -0
  174. package/dist/schema/type-conversion/dialect/postgres.js.map +1 -0
  175. package/dist/schema/type-conversion/dialect/sqlite.js +52 -0
  176. package/dist/schema/type-conversion/dialect/sqlite.js.map +1 -0
  177. package/dist/schema/type-conversion/type-mapping.js +63 -0
  178. package/dist/schema/type-conversion/type-mapping.js.map +1 -0
  179. package/dist/sql-driver/connection/connection-provider.d.ts +13 -0
  180. package/dist/sql-driver/connection/connection-provider.d.ts.map +1 -0
  181. package/dist/sql-driver/connection/connection-provider.js +19 -0
  182. package/dist/sql-driver/connection/connection-provider.js.map +1 -0
  183. package/dist/sql-driver/connection/single-connection-provider.js +23 -0
  184. package/dist/sql-driver/connection/single-connection-provider.js.map +1 -0
  185. package/dist/sql-driver/dialect-adapter/dialect-adapter.d.ts +7 -0
  186. package/dist/sql-driver/dialect-adapter/dialect-adapter.d.ts.map +1 -0
  187. package/dist/sql-driver/dialects/dialects.d.ts +2 -0
  188. package/dist/sql-driver/dialects/dialects.js +3 -0
  189. package/dist/sql-driver/dialects/durable-object-dialect.d.ts +72 -0
  190. package/dist/sql-driver/dialects/durable-object-dialect.d.ts.map +1 -0
  191. package/dist/sql-driver/dialects/durable-object-dialect.js +130 -0
  192. package/dist/sql-driver/dialects/durable-object-dialect.js.map +1 -0
  193. package/dist/sql-driver/driver/runtime-driver.d.ts +23 -0
  194. package/dist/sql-driver/driver/runtime-driver.d.ts.map +1 -0
  195. package/dist/sql-driver/driver/runtime-driver.js +56 -0
  196. package/dist/sql-driver/driver/runtime-driver.js.map +1 -0
  197. package/dist/sql-driver/query-executor/default-query-executor.js +26 -0
  198. package/dist/sql-driver/query-executor/default-query-executor.js.map +1 -0
  199. package/dist/sql-driver/query-executor/plugin.d.ts +17 -0
  200. package/dist/sql-driver/query-executor/plugin.d.ts.map +1 -0
  201. package/dist/sql-driver/query-executor/query-executor-base.js +25 -0
  202. package/dist/sql-driver/query-executor/query-executor-base.js.map +1 -0
  203. package/dist/sql-driver/query-executor/query-executor.d.ts +36 -0
  204. package/dist/sql-driver/query-executor/query-executor.d.ts.map +1 -0
  205. package/dist/sql-driver/sql-driver-adapter.d.ts +29 -0
  206. package/dist/sql-driver/sql-driver-adapter.d.ts.map +1 -0
  207. package/dist/sql-driver/sql-driver-adapter.js +68 -0
  208. package/dist/sql-driver/sql-driver-adapter.js.map +1 -0
  209. package/dist/sql-driver/sql-driver.d.ts +38 -0
  210. package/dist/sql-driver/sql-driver.d.ts.map +1 -0
  211. package/dist/sql-driver/sql-driver.js +1 -0
  212. package/dist/sql-driver/sql.js +50 -0
  213. package/dist/sql-driver/sql.js.map +1 -0
  214. package/dist/with-database.d.ts +32 -0
  215. package/dist/with-database.d.ts.map +1 -0
  216. package/dist/with-database.js +34 -0
  217. package/dist/with-database.js.map +1 -0
  218. package/package.json +43 -9
  219. package/src/adapters/adapters.ts +23 -4
  220. package/src/adapters/drizzle/drizzle-adapter-pglite.test.ts +140 -185
  221. package/src/adapters/drizzle/{drizzle-adapter-sqlite.test.ts → drizzle-adapter-sqlite3.test.ts} +187 -55
  222. package/src/adapters/drizzle/drizzle-adapter.ts +14 -93
  223. package/src/adapters/drizzle/generate.test.ts +102 -269
  224. package/src/adapters/drizzle/generate.ts +89 -63
  225. package/src/adapters/drizzle/migrate-drizzle.test.ts +19 -0
  226. package/src/adapters/drizzle/shared.ts +0 -34
  227. package/src/adapters/drizzle/test-utils.ts +36 -5
  228. package/src/adapters/generic-sql/README.md +14 -0
  229. package/src/adapters/generic-sql/driver-config.ts +144 -0
  230. package/src/adapters/generic-sql/generic-sql-adapter.test.ts +50 -0
  231. package/src/adapters/generic-sql/generic-sql-adapter.ts +146 -0
  232. package/src/adapters/generic-sql/generic-sql-uow-executor.ts +130 -0
  233. package/src/adapters/generic-sql/migration/cold-kysely.ts +55 -0
  234. package/src/adapters/{kysely/migration/execute-mysql.test.ts → generic-sql/migration/dialect/mysql.test.ts} +342 -484
  235. package/src/adapters/generic-sql/migration/dialect/mysql.ts +104 -0
  236. package/src/adapters/generic-sql/migration/dialect/postgres.test.ts +1008 -0
  237. package/src/adapters/generic-sql/migration/dialect/postgres.ts +113 -0
  238. package/src/adapters/{kysely/migration/execute-sqlite.test.ts → generic-sql/migration/dialect/sqlite.test.ts} +307 -510
  239. package/src/adapters/generic-sql/migration/dialect/sqlite.ts +189 -0
  240. package/src/adapters/generic-sql/migration/executor.ts +33 -0
  241. package/src/adapters/generic-sql/migration/prepared-migrations.test.ts +661 -0
  242. package/src/adapters/generic-sql/migration/prepared-migrations.ts +214 -0
  243. package/src/adapters/generic-sql/migration/sql-generator.ts +413 -0
  244. package/src/adapters/generic-sql/query/create-sql-query-compiler.ts +36 -0
  245. package/src/adapters/generic-sql/query/cursor-utils.ts +56 -0
  246. package/src/adapters/generic-sql/query/dialect/mysql.ts +34 -0
  247. package/src/adapters/generic-sql/query/dialect/postgres.ts +32 -0
  248. package/src/adapters/generic-sql/query/dialect/sqlite.ts +32 -0
  249. package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.test.ts +1568 -0
  250. package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.ts +314 -0
  251. package/src/adapters/generic-sql/query/select-builder.test.ts +256 -0
  252. package/src/adapters/generic-sql/query/select-builder.ts +137 -0
  253. package/src/adapters/generic-sql/query/sql-query-compiler.test.ts +195 -0
  254. package/src/adapters/generic-sql/query/sql-query-compiler.ts +367 -0
  255. package/src/adapters/generic-sql/query/where-builder.test.ts +744 -0
  256. package/src/adapters/generic-sql/query/where-builder.ts +211 -0
  257. package/src/adapters/generic-sql/result-interpreter.ts +102 -0
  258. package/src/adapters/generic-sql/test/generic-drizzle-adapter-sqlite3.test.ts +899 -0
  259. package/src/adapters/generic-sql/uow-decoder.test.ts +399 -0
  260. package/src/adapters/generic-sql/uow-decoder.ts +152 -0
  261. package/src/adapters/generic-sql/uow-encoder.test.ts +183 -0
  262. package/src/adapters/generic-sql/uow-encoder.ts +131 -0
  263. package/src/adapters/kysely/kysely-adapter-pglite.test.ts +90 -96
  264. package/src/adapters/kysely/kysely-adapter-sqlocal.test.ts +215 -0
  265. package/src/adapters/kysely/kysely-adapter.ts +10 -242
  266. package/src/adapters/{drizzle/drizzle-query.ts → shared/from-unit-of-work-compiler.ts} +111 -106
  267. package/src/adapters/shared/table-name-mapper.ts +50 -0
  268. package/src/adapters/shared/uow-operation-compiler.ts +211 -0
  269. package/src/db-fragment-definition-builder.test.ts +887 -0
  270. package/src/db-fragment-definition-builder.ts +737 -0
  271. package/src/db-fragment-instantiator.test.ts +543 -0
  272. package/src/db-fragment-integration.test.ts +406 -0
  273. package/src/fragments/internal-fragment.test.ts +549 -0
  274. package/src/fragments/internal-fragment.ts +249 -0
  275. package/src/hooks/hooks.test.ts +575 -0
  276. package/src/hooks/hooks.ts +179 -0
  277. package/src/migration-engine/generation-engine.test.ts +60 -27
  278. package/src/migration-engine/generation-engine.ts +99 -92
  279. package/src/mod.ts +139 -78
  280. package/src/query/column-defaults.ts +49 -0
  281. package/src/query/cursor.test.ts +147 -3
  282. package/src/query/cursor.ts +25 -8
  283. package/src/query/orm/orm.ts +1 -1
  284. package/src/query/query-type.test.ts +9 -9
  285. package/src/query/serialize/create-sql-serializer.ts +34 -0
  286. package/src/query/serialize/dialect/mysql-serializer.ts +142 -0
  287. package/src/query/serialize/dialect/postgres-serializer.ts +129 -0
  288. package/src/query/serialize/dialect/sqlite-serializer.test.ts +251 -0
  289. package/src/query/serialize/dialect/sqlite-serializer.ts +156 -0
  290. package/src/query/serialize/sql-serializer.ts +143 -0
  291. package/src/query/{query.ts → simple-query-interface.ts} +4 -4
  292. package/src/query/unit-of-work/execute-unit-of-work.test.ts +1310 -0
  293. package/src/query/unit-of-work/execute-unit-of-work.ts +504 -0
  294. package/src/query/unit-of-work/retry-policy.test.ts +217 -0
  295. package/src/query/unit-of-work/retry-policy.ts +141 -0
  296. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +831 -0
  297. package/src/query/{unit-of-work-types.test.ts → unit-of-work/unit-of-work-types.test.ts} +7 -5
  298. package/src/query/unit-of-work/unit-of-work.test.ts +1716 -0
  299. package/src/query/{unit-of-work.ts → unit-of-work/unit-of-work.ts} +716 -420
  300. package/src/query/{result-transform.test.ts → value-decoding.test.ts} +45 -298
  301. package/src/query/value-decoding.ts +113 -0
  302. package/src/query/value-encoding.test.ts +390 -0
  303. package/src/query/value-encoding.ts +168 -0
  304. package/src/schema/create.test.ts +5 -1
  305. package/src/schema/create.ts +5 -0
  306. package/src/schema/serialize.test.ts +165 -407
  307. package/src/schema/type-conversion/create-sql-type-mapper.ts +28 -0
  308. package/src/schema/type-conversion/dialect/mysql.ts +64 -0
  309. package/src/schema/type-conversion/dialect/postgres.ts +62 -0
  310. package/src/schema/type-conversion/dialect/sqlite.ts +63 -0
  311. package/src/schema/type-conversion/type-mapping.test.ts +137 -0
  312. package/src/schema/type-conversion/type-mapping.ts +153 -0
  313. package/src/shared/connection-pool.ts +5 -5
  314. package/src/sql-driver/better-sqlite3.test.ts +126 -0
  315. package/src/sql-driver/connection/connection-provider.ts +27 -0
  316. package/src/sql-driver/connection/single-connection-provider.ts +42 -0
  317. package/src/sql-driver/dialect-adapter/dialect-adapter.ts +9 -0
  318. package/src/sql-driver/dialect-adapter/sqlite-dialect-adapter.ts +7 -0
  319. package/src/sql-driver/dialects/dialects.ts +1 -0
  320. package/src/sql-driver/dialects/durable-object-dialect.ts +260 -0
  321. package/src/sql-driver/driver/runtime-driver.ts +91 -0
  322. package/src/sql-driver/query-executor/default-query-executor.ts +38 -0
  323. package/src/sql-driver/query-executor/plugin.ts +22 -0
  324. package/src/sql-driver/query-executor/query-executor-base.ts +53 -0
  325. package/src/sql-driver/query-executor/query-executor.ts +44 -0
  326. package/src/sql-driver/sql-driver-adapter.ts +96 -0
  327. package/src/sql-driver/sql-driver.ts +53 -0
  328. package/src/sql-driver/sql.ts +57 -0
  329. package/src/sql-driver/sqlocal.test.ts +117 -0
  330. package/src/with-database.ts +152 -0
  331. package/tsdown.config.ts +8 -2
  332. package/dist/adapters/drizzle/drizzle-connection-pool.js +0 -40
  333. package/dist/adapters/drizzle/drizzle-connection-pool.js.map +0 -1
  334. package/dist/adapters/drizzle/drizzle-query.d.ts +0 -23
  335. package/dist/adapters/drizzle/drizzle-query.d.ts.map +0 -1
  336. package/dist/adapters/drizzle/drizzle-query.js.map +0 -1
  337. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts +0 -10
  338. package/dist/adapters/drizzle/drizzle-uow-compiler.d.ts.map +0 -1
  339. package/dist/adapters/drizzle/drizzle-uow-compiler.js +0 -315
  340. package/dist/adapters/drizzle/drizzle-uow-compiler.js.map +0 -1
  341. package/dist/adapters/drizzle/drizzle-uow-decoder.js +0 -116
  342. package/dist/adapters/drizzle/drizzle-uow-decoder.js.map +0 -1
  343. package/dist/adapters/drizzle/drizzle-uow-executor.js +0 -149
  344. package/dist/adapters/drizzle/drizzle-uow-executor.js.map +0 -1
  345. package/dist/adapters/drizzle/join-column-utils.js +0 -28
  346. package/dist/adapters/drizzle/join-column-utils.js.map +0 -1
  347. package/dist/adapters/drizzle/shared.d.ts +0 -14
  348. package/dist/adapters/drizzle/shared.d.ts.map +0 -1
  349. package/dist/adapters/drizzle/shared.js +0 -35
  350. package/dist/adapters/drizzle/shared.js.map +0 -1
  351. package/dist/adapters/kysely/kysely-connection-pool.js +0 -41
  352. package/dist/adapters/kysely/kysely-connection-pool.js.map +0 -1
  353. package/dist/adapters/kysely/kysely-query-builder.js +0 -321
  354. package/dist/adapters/kysely/kysely-query-builder.js.map +0 -1
  355. package/dist/adapters/kysely/kysely-query-compiler.js +0 -66
  356. package/dist/adapters/kysely/kysely-query-compiler.js.map +0 -1
  357. package/dist/adapters/kysely/kysely-query.d.ts +0 -22
  358. package/dist/adapters/kysely/kysely-query.d.ts.map +0 -1
  359. package/dist/adapters/kysely/kysely-query.js +0 -223
  360. package/dist/adapters/kysely/kysely-query.js.map +0 -1
  361. package/dist/adapters/kysely/kysely-shared.d.ts.map +0 -1
  362. package/dist/adapters/kysely/kysely-shared.js +0 -18
  363. package/dist/adapters/kysely/kysely-shared.js.map +0 -1
  364. package/dist/adapters/kysely/kysely-uow-compiler.js +0 -170
  365. package/dist/adapters/kysely/kysely-uow-compiler.js.map +0 -1
  366. package/dist/adapters/kysely/kysely-uow-executor.js +0 -89
  367. package/dist/adapters/kysely/kysely-uow-executor.js.map +0 -1
  368. package/dist/adapters/kysely/migration/execute-base.js +0 -128
  369. package/dist/adapters/kysely/migration/execute-base.js.map +0 -1
  370. package/dist/adapters/kysely/migration/execute-factory.js +0 -34
  371. package/dist/adapters/kysely/migration/execute-factory.js.map +0 -1
  372. package/dist/adapters/kysely/migration/execute-mssql.js +0 -112
  373. package/dist/adapters/kysely/migration/execute-mssql.js.map +0 -1
  374. package/dist/adapters/kysely/migration/execute-mysql.js +0 -93
  375. package/dist/adapters/kysely/migration/execute-mysql.js.map +0 -1
  376. package/dist/adapters/kysely/migration/execute-postgres.js +0 -104
  377. package/dist/adapters/kysely/migration/execute-postgres.js.map +0 -1
  378. package/dist/adapters/kysely/migration/execute-sqlite.js +0 -123
  379. package/dist/adapters/kysely/migration/execute-sqlite.js.map +0 -1
  380. package/dist/adapters/kysely/migration/execute.js +0 -34
  381. package/dist/adapters/kysely/migration/execute.js.map +0 -1
  382. package/dist/bind-services.d.ts +0 -7
  383. package/dist/bind-services.d.ts.map +0 -1
  384. package/dist/bind-services.js +0 -14
  385. package/dist/bind-services.js.map +0 -1
  386. package/dist/fragment.d.ts +0 -173
  387. package/dist/fragment.d.ts.map +0 -1
  388. package/dist/fragment.js +0 -191
  389. package/dist/fragment.js.map +0 -1
  390. package/dist/migration-engine/create.d.ts +0 -37
  391. package/dist/migration-engine/create.d.ts.map +0 -1
  392. package/dist/migration-engine/create.js +0 -58
  393. package/dist/migration-engine/create.js.map +0 -1
  394. package/dist/migration-engine/shared.d.ts +0 -112
  395. package/dist/migration-engine/shared.d.ts.map +0 -1
  396. package/dist/query/query.d.ts.map +0 -1
  397. package/dist/query/result-transform.js +0 -168
  398. package/dist/query/result-transform.js.map +0 -1
  399. package/dist/query/unit-of-work.d.ts.map +0 -1
  400. package/dist/query/unit-of-work.js.map +0 -1
  401. package/dist/schema/serialize.js +0 -106
  402. package/dist/schema/serialize.js.map +0 -1
  403. package/dist/shared/settings-schema.js +0 -36
  404. package/dist/shared/settings-schema.js.map +0 -1
  405. package/src/adapters/drizzle/drizzle-adapter.test.ts +0 -170
  406. package/src/adapters/drizzle/drizzle-connection-pool.ts +0 -66
  407. package/src/adapters/drizzle/drizzle-query.test.ts +0 -499
  408. package/src/adapters/drizzle/drizzle-uow-compiler.test.ts +0 -1383
  409. package/src/adapters/drizzle/drizzle-uow-compiler.ts +0 -636
  410. package/src/adapters/drizzle/drizzle-uow-decoder.ts +0 -218
  411. package/src/adapters/drizzle/drizzle-uow-executor.ts +0 -276
  412. package/src/adapters/drizzle/join-column-utils.test.ts +0 -79
  413. package/src/adapters/drizzle/join-column-utils.ts +0 -39
  414. package/src/adapters/kysely/kysely-connection-pool.ts +0 -70
  415. package/src/adapters/kysely/kysely-query-builder.test.ts +0 -1344
  416. package/src/adapters/kysely/kysely-query-builder.ts +0 -666
  417. package/src/adapters/kysely/kysely-query-compiler.ts +0 -132
  418. package/src/adapters/kysely/kysely-query.test.ts +0 -498
  419. package/src/adapters/kysely/kysely-query.ts +0 -390
  420. package/src/adapters/kysely/kysely-shared.ts +0 -23
  421. package/src/adapters/kysely/kysely-uow-compiler.test.ts +0 -998
  422. package/src/adapters/kysely/kysely-uow-compiler.ts +0 -318
  423. package/src/adapters/kysely/kysely-uow-executor.ts +0 -145
  424. package/src/adapters/kysely/kysely-uow-joins.test.ts +0 -811
  425. package/src/adapters/kysely/migration/execute-base.ts +0 -256
  426. package/src/adapters/kysely/migration/execute-factory.ts +0 -53
  427. package/src/adapters/kysely/migration/execute-mssql.ts +0 -250
  428. package/src/adapters/kysely/migration/execute-mysql.ts +0 -211
  429. package/src/adapters/kysely/migration/execute-postgres.test.ts +0 -2657
  430. package/src/adapters/kysely/migration/execute-postgres.ts +0 -234
  431. package/src/adapters/kysely/migration/execute-sqlite.ts +0 -247
  432. package/src/adapters/kysely/migration/execute.ts +0 -50
  433. package/src/adapters/kysely/migration/kysely-migrator.test.ts +0 -261
  434. package/src/bind-services.test.ts +0 -214
  435. package/src/bind-services.ts +0 -37
  436. package/src/db-fragment.test.ts +0 -800
  437. package/src/fragment.ts +0 -727
  438. package/src/query/result-transform.ts +0 -271
  439. package/src/query/unit-of-work-multi-schema.test.ts +0 -64
  440. package/src/query/unit-of-work.test.ts +0 -943
  441. package/src/schema/serialize.ts +0 -396
  442. package/src/shared/settings-schema.ts +0 -61
  443. package/src/uow-context-integration.test.ts +0 -102
  444. package/src/uow-context.test.ts +0 -182
  445. /package/dist/query/{query.js → simple-query-interface.js} +0 -0
@@ -0,0 +1,1310 @@
1
+ import { describe, it, expect, vi, assert, expectTypeOf } from "vitest";
2
+ import { schema, idColumn, FragnoId } from "../../schema/create";
3
+ import {
4
+ createUnitOfWork,
5
+ type TypedUnitOfWork,
6
+ type UOWCompiler,
7
+ type UOWDecoder,
8
+ type UOWExecutor,
9
+ } from "./unit-of-work";
10
+ import { executeUnitOfWork, executeRestrictedUnitOfWork } from "./execute-unit-of-work";
11
+ import {
12
+ ExponentialBackoffRetryPolicy,
13
+ LinearBackoffRetryPolicy,
14
+ NoRetryPolicy,
15
+ } from "./retry-policy";
16
+ import type { AwaitedPromisesInObject } from "./execute-unit-of-work";
17
+
18
+ // Create test schema
19
+ const testSchema = schema((s) =>
20
+ s.addTable("users", (t) =>
21
+ t
22
+ .addColumn("id", idColumn())
23
+ .addColumn("email", "string")
24
+ .addColumn("name", "string")
25
+ .addColumn("balance", "integer")
26
+ .createIndex("idx_email", ["email"], { unique: true }),
27
+ ),
28
+ );
29
+
30
+ // Type tests for AwaitedPromisesInObject
31
+ describe("AwaitedPromisesInObject type tests", () => {
32
+ it("should unwrap promises in objects", () => {
33
+ type Input = { a: Promise<string>; b: number };
34
+ type Expected = { a: string; b: number };
35
+ type Actual = AwaitedPromisesInObject<Input>;
36
+ expectTypeOf<Actual>().toMatchObjectType<Expected>();
37
+ });
38
+
39
+ it("should unwrap promises in arrays", () => {
40
+ type Input = Promise<string>[];
41
+ type Expected = string[];
42
+ type Actual = AwaitedPromisesInObject<Input>;
43
+ expectTypeOf<Actual>().toEqualTypeOf<Expected>();
44
+ });
45
+
46
+ it("should unwrap direct promises", () => {
47
+ type Input = Promise<{ value: number }>;
48
+ type Expected = { value: number };
49
+ type Actual = AwaitedPromisesInObject<Input>;
50
+ expectTypeOf<Actual>().toEqualTypeOf<Expected>();
51
+ });
52
+
53
+ it("should handle tuples correctly", () => {
54
+ type Input = [Promise<string>, Promise<number>];
55
+ type Actual = AwaitedPromisesInObject<Input>;
56
+
57
+ // Should preserve tuple structure - check first and second elements
58
+ expectTypeOf<Actual[0]>().toEqualTypeOf<string>();
59
+ expectTypeOf<Actual[1]>().toEqualTypeOf<number>();
60
+
61
+ // Verify it's actually a tuple with length 2
62
+ expectTypeOf<Actual["length"]>().toEqualTypeOf<2>();
63
+ });
64
+
65
+ it("should preserve tuple structure with Promise.all pattern", () => {
66
+ type User = { id: string; name: string };
67
+ type Order = { id: string; total: number };
68
+
69
+ type Input = [Promise<User>, Promise<Order[]>];
70
+ type Actual = AwaitedPromisesInObject<Input>;
71
+
72
+ // Check individual elements
73
+ expectTypeOf<Actual[0]>().toMatchObjectType<User>();
74
+ expectTypeOf<Actual[1]>().toEqualTypeOf<Order[]>();
75
+
76
+ // Verify length
77
+ expectTypeOf<Actual["length"]>().toEqualTypeOf<2>();
78
+ });
79
+
80
+ it("should handle readonly tuples", () => {
81
+ type Input = readonly [Promise<string>, Promise<number>];
82
+ type Actual = AwaitedPromisesInObject<Input>;
83
+
84
+ // Check elements
85
+ expectTypeOf<Actual[0]>().toEqualTypeOf<string>();
86
+ expectTypeOf<Actual[1]>().toEqualTypeOf<number>();
87
+ });
88
+
89
+ it("should handle tuples with more than 2 elements", () => {
90
+ type Input = [Promise<string>, Promise<number>, Promise<boolean>];
91
+ type Actual = AwaitedPromisesInObject<Input>;
92
+
93
+ // Check all three elements
94
+ expectTypeOf<Actual[0]>().toEqualTypeOf<string>();
95
+ expectTypeOf<Actual[1]>().toEqualTypeOf<number>();
96
+ expectTypeOf<Actual[2]>().toEqualTypeOf<boolean>();
97
+ expectTypeOf<Actual["length"]>().toEqualTypeOf<3>();
98
+ });
99
+
100
+ it("should handle tuples with mixed promise and non-promise types", () => {
101
+ type Input = [Promise<string>, number, Promise<boolean>];
102
+ type Actual = AwaitedPromisesInObject<Input>;
103
+
104
+ // Non-promises should be preserved as-is
105
+ expectTypeOf<Actual[0]>().toEqualTypeOf<string>();
106
+ expectTypeOf<Actual[1]>().toEqualTypeOf<number>();
107
+ expectTypeOf<Actual[2]>().toEqualTypeOf<boolean>();
108
+ });
109
+ });
110
+
111
+ // Mock compiler that returns null for all operations
112
+ function createMockCompiler(): UOWCompiler<unknown> {
113
+ return {
114
+ compileRetrievalOperation: () => null,
115
+ compileMutationOperation: () => null,
116
+ };
117
+ }
118
+
119
+ // Mock decoder that returns raw results as-is
120
+ function createMockDecoder(): UOWDecoder {
121
+ return {
122
+ decode(rawResults) {
123
+ return rawResults;
124
+ },
125
+ };
126
+ }
127
+
128
+ // Helper to create a UOW factory that tracks how many times it's called
129
+ function createMockUOWFactory(mutationResults: Array<{ success: boolean }>) {
130
+ const callCount = { value: 0 };
131
+ // Share callIndex across all UOW instances
132
+ let callIndex = 0;
133
+
134
+ const factory = () => {
135
+ callCount.value++;
136
+
137
+ // Create executor that uses shared callIndex
138
+ const executor: UOWExecutor<unknown, unknown> = {
139
+ executeRetrievalPhase: async () => {
140
+ return [
141
+ [
142
+ {
143
+ id: FragnoId.fromExternal("user-1", 1),
144
+ email: "test@example.com",
145
+ name: "Test User",
146
+ balance: 100,
147
+ },
148
+ ],
149
+ ];
150
+ },
151
+ executeMutationPhase: async () => {
152
+ const result = mutationResults[callIndex] || { success: false };
153
+ callIndex++;
154
+ return { ...result, createdInternalIds: [] };
155
+ },
156
+ };
157
+
158
+ return createUnitOfWork(createMockCompiler(), executor, createMockDecoder()).forSchema(
159
+ testSchema,
160
+ );
161
+ };
162
+ return { factory, callCount };
163
+ }
164
+
165
+ describe("executeUnitOfWork", () => {
166
+ describe("validation", () => {
167
+ it("should throw error when neither retrieve nor mutate is provided", async () => {
168
+ const { factory } = createMockUOWFactory([{ success: true }]);
169
+
170
+ await expect(executeUnitOfWork({}, { createUnitOfWork: factory })).rejects.toThrow(
171
+ "At least one of 'retrieve' or 'mutate' callbacks must be provided",
172
+ );
173
+ });
174
+ });
175
+
176
+ describe("success scenarios", () => {
177
+ it("should succeed on first attempt without retries", async () => {
178
+ const { factory } = createMockUOWFactory([{ success: true }]);
179
+ const onSuccess = vi.fn();
180
+
181
+ const result = await executeUnitOfWork(
182
+ {
183
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
184
+ mutate: (uow, [users]) => {
185
+ const newBalance = users[0].balance + 100;
186
+ uow.update("users", users[0].id, (b) => b.set({ balance: newBalance }).check());
187
+ return { newBalance };
188
+ },
189
+ onSuccess,
190
+ },
191
+ { createUnitOfWork: factory },
192
+ );
193
+
194
+ assert(result.success);
195
+ expect(result.mutationResult).toEqual({ newBalance: 200 });
196
+ expect(onSuccess).toHaveBeenCalledExactlyOnceWith({
197
+ results: expect.any(Array),
198
+ mutationResult: { newBalance: 200 },
199
+ createdIds: [],
200
+ nonce: expect.any(String),
201
+ });
202
+ });
203
+ });
204
+
205
+ describe("retry scenarios", () => {
206
+ it("should retry on conflict with eventual success", async () => {
207
+ const { factory, callCount } = createMockUOWFactory([
208
+ { success: false },
209
+ { success: false },
210
+ { success: true },
211
+ ]);
212
+
213
+ const result = await executeUnitOfWork(
214
+ {
215
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
216
+ mutate: async (uow, [users]) => {
217
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }).check());
218
+ },
219
+ },
220
+ {
221
+ createUnitOfWork: factory,
222
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
223
+ },
224
+ );
225
+
226
+ expect(result.success).toBe(true);
227
+ expect(callCount.value).toBe(3); // Initial + 2 retries
228
+ });
229
+
230
+ it("should fail when max retries exceeded", async () => {
231
+ const { factory, callCount } = createMockUOWFactory([
232
+ { success: false },
233
+ { success: false },
234
+ { success: false },
235
+ { success: false },
236
+ ]);
237
+
238
+ const result = await executeUnitOfWork(
239
+ {
240
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
241
+ mutate: async (uow, [users]) => {
242
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
243
+ },
244
+ },
245
+ {
246
+ createUnitOfWork: factory,
247
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 2, initialDelayMs: 1 }),
248
+ },
249
+ );
250
+
251
+ assert(!result.success);
252
+ expect(result.reason).toBe("conflict");
253
+ expect(callCount.value).toBe(3); // Initial + 2 retries
254
+ });
255
+
256
+ it("should create fresh UOW on each retry attempt", async () => {
257
+ const { factory, callCount } = createMockUOWFactory([
258
+ { success: false },
259
+ { success: false },
260
+ { success: true },
261
+ ]);
262
+
263
+ await executeUnitOfWork(
264
+ {
265
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
266
+ mutate: async (uow, [users]) => {
267
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
268
+ },
269
+ },
270
+ {
271
+ createUnitOfWork: factory,
272
+ retryPolicy: new LinearBackoffRetryPolicy({ maxRetries: 3, delayMs: 1 }),
273
+ },
274
+ );
275
+
276
+ expect(callCount.value).toBe(3); // Each attempt creates a new UOW
277
+ });
278
+ });
279
+
280
+ describe("AbortSignal handling", () => {
281
+ it("should abort when signal is aborted before execution", async () => {
282
+ const { factory } = createMockUOWFactory([{ success: false }]);
283
+ const controller = new AbortController();
284
+ controller.abort();
285
+
286
+ const result = await executeUnitOfWork(
287
+ {
288
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
289
+ mutate: async (uow, [users]) => {
290
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
291
+ },
292
+ },
293
+ {
294
+ createUnitOfWork: factory,
295
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 5, initialDelayMs: 1 }),
296
+ signal: controller.signal,
297
+ },
298
+ );
299
+
300
+ assert(!result.success);
301
+ expect(result.reason).toBe("aborted");
302
+ });
303
+
304
+ it("should abort when signal is aborted during retry", async () => {
305
+ const { factory } = createMockUOWFactory([{ success: false }, { success: false }]);
306
+ const controller = new AbortController();
307
+
308
+ // Abort after first attempt
309
+ setTimeout(() => controller.abort(), 50);
310
+
311
+ const result = await executeUnitOfWork(
312
+ {
313
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
314
+ mutate: async (uow, [users]) => {
315
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
316
+ },
317
+ },
318
+ {
319
+ createUnitOfWork: factory,
320
+ retryPolicy: new LinearBackoffRetryPolicy({ maxRetries: 5, delayMs: 100 }),
321
+ signal: controller.signal,
322
+ },
323
+ );
324
+
325
+ assert(!result.success);
326
+ expect(result.reason).toBe("aborted");
327
+ });
328
+ });
329
+
330
+ describe("onSuccess callback", () => {
331
+ it("should pass mutation result to onSuccess callback", async () => {
332
+ const { factory } = createMockUOWFactory([{ success: true }]);
333
+ const onSuccess = vi.fn();
334
+
335
+ await executeUnitOfWork(
336
+ {
337
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
338
+ mutate: async () => {
339
+ return { updatedCount: 5 };
340
+ },
341
+ onSuccess,
342
+ },
343
+ { createUnitOfWork: factory },
344
+ );
345
+
346
+ expect(onSuccess).toHaveBeenCalledTimes(1);
347
+ expect(onSuccess).toHaveBeenCalledWith({
348
+ results: expect.any(Array),
349
+ mutationResult: { updatedCount: 5 },
350
+ createdIds: [],
351
+ nonce: expect.any(String),
352
+ });
353
+ });
354
+
355
+ it("should only execute onSuccess callback on success", async () => {
356
+ const { factory } = createMockUOWFactory([{ success: false }]);
357
+ const onSuccess = vi.fn();
358
+
359
+ const result = await executeUnitOfWork(
360
+ {
361
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
362
+ mutate: async (uow, [users]) => {
363
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
364
+ },
365
+ onSuccess,
366
+ },
367
+ {
368
+ createUnitOfWork: factory,
369
+ retryPolicy: new NoRetryPolicy(),
370
+ },
371
+ );
372
+
373
+ assert(!result.success);
374
+ expect(result.reason).toBe("conflict");
375
+ expect(onSuccess).not.toHaveBeenCalled();
376
+ });
377
+
378
+ it("should execute onSuccess only once even after retries", async () => {
379
+ const { factory } = createMockUOWFactory([
380
+ { success: false },
381
+ { success: false },
382
+ { success: true },
383
+ ]);
384
+ const onSuccess = vi.fn();
385
+
386
+ await executeUnitOfWork(
387
+ {
388
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
389
+ mutate: async (uow, [users]) => {
390
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
391
+ },
392
+ onSuccess,
393
+ },
394
+ {
395
+ createUnitOfWork: factory,
396
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 3, initialDelayMs: 1 }),
397
+ },
398
+ );
399
+
400
+ expect(onSuccess).toHaveBeenCalledTimes(1);
401
+ });
402
+
403
+ it("should handle async onSuccess callback", async () => {
404
+ const { factory } = createMockUOWFactory([{ success: true }]);
405
+ const onSuccess = vi.fn(async () => {
406
+ await new Promise((resolve) => setTimeout(resolve, 10));
407
+ });
408
+
409
+ await executeUnitOfWork(
410
+ {
411
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
412
+ mutate: async (uow, [users]) => {
413
+ uow.update("users", users[0].id, (b) => b.set({ balance: 200 }));
414
+ },
415
+ onSuccess,
416
+ },
417
+ { createUnitOfWork: factory },
418
+ );
419
+
420
+ expect(onSuccess).toHaveBeenCalledTimes(1);
421
+ });
422
+ });
423
+
424
+ describe("error handling", () => {
425
+ it("should return error result when retrieve callback throws", async () => {
426
+ const { factory } = createMockUOWFactory([{ success: true }]);
427
+ const testError = new Error("Retrieve failed");
428
+
429
+ const result = await executeUnitOfWork(
430
+ {
431
+ retrieve: () => {
432
+ throw testError;
433
+ },
434
+ mutate: async () => {},
435
+ },
436
+ { createUnitOfWork: factory },
437
+ );
438
+
439
+ assert(!result.success);
440
+ assert(result.reason === "error");
441
+ expect(result.error).toBe(testError);
442
+ });
443
+
444
+ it("should return error result when mutate callback throws", async () => {
445
+ const { factory } = createMockUOWFactory([{ success: true }]);
446
+ const testError = new Error("Mutate failed");
447
+
448
+ const result = await executeUnitOfWork(
449
+ {
450
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
451
+ mutate: async () => {
452
+ throw testError;
453
+ },
454
+ },
455
+ { createUnitOfWork: factory },
456
+ );
457
+
458
+ assert(!result.success);
459
+ assert(result.reason === "error");
460
+ expect(result.error).toBe(testError);
461
+ });
462
+
463
+ it("should return error result when onSuccess callback throws", async () => {
464
+ const { factory } = createMockUOWFactory([{ success: true }]);
465
+ const testError = new Error("onSuccess failed");
466
+
467
+ const result = await executeUnitOfWork(
468
+ {
469
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
470
+ mutate: async () => {},
471
+ onSuccess: async () => {
472
+ throw testError;
473
+ },
474
+ },
475
+ { createUnitOfWork: factory },
476
+ );
477
+
478
+ assert(!result.success);
479
+ assert(result.reason === "error");
480
+ expect(result.error).toBe(testError);
481
+ });
482
+
483
+ it("should capture non-Error thrown values", async () => {
484
+ const { factory } = createMockUOWFactory([{ success: true }]);
485
+
486
+ const result = await executeUnitOfWork(
487
+ {
488
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
489
+ mutate: async () => {
490
+ throw "string error";
491
+ },
492
+ },
493
+ { createUnitOfWork: factory },
494
+ );
495
+
496
+ assert(!result.success);
497
+ assert(result.reason === "error");
498
+ expect(result.error).toBe("string error");
499
+ });
500
+ });
501
+
502
+ describe("retrieval results", () => {
503
+ it("should pass retrieval results to mutation phase", async () => {
504
+ const { factory } = createMockUOWFactory([{ success: true }]);
505
+ const mutationPhase = vi.fn(async (_uow: unknown, _results: unknown) => {});
506
+
507
+ await executeUnitOfWork(
508
+ {
509
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
510
+ mutate: mutationPhase,
511
+ },
512
+ { createUnitOfWork: factory },
513
+ );
514
+
515
+ expect(mutationPhase).toHaveBeenCalledTimes(1);
516
+ const call = mutationPhase.mock.calls[0];
517
+ assert(call);
518
+ const [_uow, results] = call;
519
+ expect(results).toBeInstanceOf(Array);
520
+ expect(results as unknown[]).toHaveLength(1);
521
+ expect((results as unknown[])[0]).toBeInstanceOf(Array);
522
+ });
523
+
524
+ it("should return retrieval results in the result object", async () => {
525
+ const { factory } = createMockUOWFactory([{ success: true }]);
526
+
527
+ const result = await executeUnitOfWork(
528
+ {
529
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
530
+ mutate: async () => {},
531
+ },
532
+ { createUnitOfWork: factory },
533
+ );
534
+
535
+ assert(result.success);
536
+ expect(result.results).toBeInstanceOf(Array);
537
+ expect(result.results).toHaveLength(1);
538
+ });
539
+ });
540
+
541
+ describe("promise awaiting in mutation result", () => {
542
+ it("should await promises in mutation result object (1 level deep)", async () => {
543
+ const { factory } = createMockUOWFactory([{ success: true }]);
544
+
545
+ const result = await executeUnitOfWork(
546
+ {
547
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
548
+ mutate: async () => {
549
+ return {
550
+ userId: Promise.resolve("user-123"),
551
+ count: Promise.resolve(42),
552
+ data: "plain-value",
553
+ };
554
+ },
555
+ },
556
+ { createUnitOfWork: factory },
557
+ );
558
+
559
+ assert(result.success);
560
+ expect(result.mutationResult).toEqual({
561
+ userId: "user-123",
562
+ count: 42,
563
+ data: "plain-value",
564
+ });
565
+ });
566
+
567
+ it("should await promises in mutation result array", async () => {
568
+ const { factory } = createMockUOWFactory([{ success: true }]);
569
+
570
+ const result = await executeUnitOfWork(
571
+ {
572
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
573
+ mutate: async () => {
574
+ return [Promise.resolve("a"), Promise.resolve("b"), "c"];
575
+ },
576
+ },
577
+ { createUnitOfWork: factory },
578
+ );
579
+
580
+ assert(result.success);
581
+ expect(result.mutationResult).toEqual(["a", "b", "c"]);
582
+ });
583
+
584
+ it("should await direct promise mutation result", async () => {
585
+ const { factory } = createMockUOWFactory([{ success: true }]);
586
+
587
+ const result = await executeUnitOfWork(
588
+ {
589
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
590
+ mutate: async () => {
591
+ return Promise.resolve({ value: "resolved" });
592
+ },
593
+ },
594
+ { createUnitOfWork: factory },
595
+ );
596
+
597
+ assert(result.success);
598
+ expect(result.mutationResult).toEqual({ value: "resolved" });
599
+ });
600
+
601
+ it("should NOT await nested promises (only 1 level deep)", async () => {
602
+ const { factory } = createMockUOWFactory([{ success: true }]);
603
+
604
+ const result = await executeUnitOfWork(
605
+ {
606
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
607
+ mutate: async () => {
608
+ return {
609
+ nested: { promise: Promise.resolve("still-a-promise") },
610
+ };
611
+ },
612
+ },
613
+ { createUnitOfWork: factory },
614
+ );
615
+
616
+ assert(result.success);
617
+ // The nested promise should still be a promise
618
+ expect(result.mutationResult.nested.promise).toBeInstanceOf(Promise);
619
+ });
620
+
621
+ it("should handle mixed types in mutation result", async () => {
622
+ const { factory } = createMockUOWFactory([{ success: true }]);
623
+
624
+ const result = await executeUnitOfWork(
625
+ {
626
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
627
+ mutate: async () => {
628
+ return {
629
+ promise: Promise.resolve(100),
630
+ number: 42,
631
+ string: "test",
632
+ null: null,
633
+ undefined: undefined,
634
+ nested: { value: "nested" },
635
+ };
636
+ },
637
+ },
638
+ { createUnitOfWork: factory },
639
+ );
640
+
641
+ assert(result.success);
642
+ expect(result.mutationResult).toEqual({
643
+ promise: 100,
644
+ number: 42,
645
+ string: "test",
646
+ null: null,
647
+ undefined: undefined,
648
+ nested: { value: "nested" },
649
+ });
650
+ });
651
+
652
+ it("should pass awaited mutation result to onSuccess callback", async () => {
653
+ const { factory } = createMockUOWFactory([{ success: true }]);
654
+ const onSuccess = vi.fn();
655
+
656
+ await executeUnitOfWork(
657
+ {
658
+ retrieve: (uow) => uow.find("users", (b) => b.whereIndex("primary")),
659
+ mutate: async () => {
660
+ return {
661
+ userId: Promise.resolve("user-456"),
662
+ status: Promise.resolve("active"),
663
+ };
664
+ },
665
+ onSuccess,
666
+ },
667
+ { createUnitOfWork: factory },
668
+ );
669
+
670
+ expect(onSuccess).toHaveBeenCalledExactlyOnceWith({
671
+ results: expect.any(Array),
672
+ mutationResult: {
673
+ userId: "user-456",
674
+ status: "active",
675
+ },
676
+ createdIds: [],
677
+ nonce: expect.any(String),
678
+ });
679
+ });
680
+ });
681
+ });
682
+
683
+ describe("executeRestrictedUnitOfWork", () => {
684
+ describe("basic success cases", () => {
685
+ it("should execute a simple mutation-only workflow", async () => {
686
+ const { factory, callCount } = createMockUOWFactory([{ success: true }]);
687
+
688
+ const result = await executeRestrictedUnitOfWork(
689
+ async ({ forSchema, executeMutate }) => {
690
+ const uow = forSchema(testSchema);
691
+ const userId = uow.create("users", {
692
+ id: "user-1",
693
+ email: "test@example.com",
694
+ name: "Test User",
695
+ balance: 100,
696
+ });
697
+
698
+ await executeMutate();
699
+
700
+ return { userId: userId.externalId };
701
+ },
702
+ { createUnitOfWork: factory },
703
+ );
704
+
705
+ expect(result).toEqual({ userId: "user-1" });
706
+ expect(callCount.value).toBe(1);
707
+ });
708
+
709
+ it("should execute retrieval and mutation phases", async () => {
710
+ const { factory, callCount } = createMockUOWFactory([{ success: true }]);
711
+
712
+ const result = await executeRestrictedUnitOfWork(
713
+ async ({ forSchema, executeRetrieve, executeMutate }) => {
714
+ const uow = forSchema(testSchema).find("users", (b) => b.whereIndex("primary"));
715
+ await executeRetrieve();
716
+ const [[user]] = await uow.retrievalPhase;
717
+
718
+ uow.update("users", user.id, (b) => b.set({ balance: user.balance + 50 }).check());
719
+ await executeMutate();
720
+
721
+ return { newBalance: user.balance + 50 };
722
+ },
723
+ { createUnitOfWork: factory },
724
+ );
725
+
726
+ expect(result).toEqual({ newBalance: 150 });
727
+ expect(callCount.value).toBe(1);
728
+ });
729
+
730
+ it("should return callback result directly", async () => {
731
+ const { factory } = createMockUOWFactory([{ success: true }]);
732
+
733
+ const result = await executeRestrictedUnitOfWork(
734
+ async () => {
735
+ return { data: "test", count: 42, nested: { value: true } };
736
+ },
737
+ { createUnitOfWork: factory },
738
+ );
739
+
740
+ expect(result).toEqual({ data: "test", count: 42, nested: { value: true } });
741
+ });
742
+ });
743
+
744
+ describe("retry behavior", () => {
745
+ it("should retry on conflict and eventually succeed", async () => {
746
+ const { factory, callCount } = createMockUOWFactory([
747
+ { success: false }, // First attempt fails
748
+ { success: false }, // Second attempt fails
749
+ { success: true }, // Third attempt succeeds
750
+ ]);
751
+
752
+ const callbackExecutions = { count: 0 };
753
+
754
+ const result = await executeRestrictedUnitOfWork(
755
+ async ({ forSchema, executeMutate }) => {
756
+ callbackExecutions.count++;
757
+ const uow = forSchema(testSchema);
758
+
759
+ uow.create("users", {
760
+ id: "user-1",
761
+ email: "test@example.com",
762
+ name: "Test User",
763
+ balance: 100,
764
+ });
765
+
766
+ await executeMutate();
767
+
768
+ return { attempt: callbackExecutions.count };
769
+ },
770
+ { createUnitOfWork: factory },
771
+ );
772
+
773
+ expect(result.attempt).toBe(3);
774
+ expect(callCount.value).toBe(3);
775
+ expect(callbackExecutions.count).toBe(3);
776
+ });
777
+
778
+ it("should throw error when retries are exhausted", async () => {
779
+ const { factory, callCount } = createMockUOWFactory([
780
+ { success: false }, // First attempt fails
781
+ { success: false }, // Second attempt fails
782
+ { success: false }, // Third attempt fails
783
+ { success: false }, // Fourth attempt fails (exceeds default maxRetries: 3)
784
+ ]);
785
+
786
+ await expect(
787
+ executeRestrictedUnitOfWork(
788
+ async ({ executeMutate }) => {
789
+ await executeMutate();
790
+ return { hello: "world" };
791
+ },
792
+ { createUnitOfWork: factory },
793
+ ),
794
+ ).rejects.toThrow("Unit of Work execution failed: optimistic concurrency conflict");
795
+
796
+ // Default policy has maxRetries: 5, so we make 6 attempts (initial + 5 retries)
797
+ expect(callCount.value).toBe(6);
798
+ });
799
+
800
+ it("should respect custom retry policy", async () => {
801
+ const { factory, callCount } = createMockUOWFactory([
802
+ { success: false },
803
+ { success: false },
804
+ { success: false },
805
+ { success: false },
806
+ { success: false },
807
+ { success: true },
808
+ ]);
809
+
810
+ const result = await executeRestrictedUnitOfWork(
811
+ async ({ executeMutate }) => {
812
+ await executeMutate();
813
+ return { done: true };
814
+ },
815
+ {
816
+ createUnitOfWork: factory,
817
+ retryPolicy: new ExponentialBackoffRetryPolicy({ maxRetries: 5, initialDelayMs: 1 }),
818
+ },
819
+ );
820
+
821
+ expect(result).toEqual({ done: true });
822
+ expect(callCount.value).toBe(6); // Initial + 5 retries
823
+ });
824
+
825
+ it("should use default ExponentialBackoffRetryPolicy with small delays", async () => {
826
+ const { factory } = createMockUOWFactory([{ success: false }, { success: true }]);
827
+
828
+ const startTime = Date.now();
829
+ await executeRestrictedUnitOfWork(
830
+ async ({ executeMutate }) => {
831
+ await executeMutate();
832
+ return {};
833
+ },
834
+ { createUnitOfWork: factory },
835
+ );
836
+ const elapsed = Date.now() - startTime;
837
+
838
+ // Default policy has initialDelayMs: 10, maxDelayMs: 100
839
+ // First retry delay should be around 10ms
840
+ expect(elapsed).toBeLessThan(200); // Allow some margin
841
+ });
842
+ });
843
+
844
+ describe("error handling", () => {
845
+ it("should throw error from callback immediately without retry", async () => {
846
+ const { factory, callCount } = createMockUOWFactory([{ success: true }]);
847
+
848
+ await expect(
849
+ executeRestrictedUnitOfWork(
850
+ async () => {
851
+ throw new Error("Callback error");
852
+ },
853
+ { createUnitOfWork: factory },
854
+ ),
855
+ ).rejects.toThrow("Callback error");
856
+
857
+ // Should NOT retry non-conflict errors
858
+ expect(callCount.value).toBe(1); // Only initial attempt
859
+ });
860
+
861
+ it("should throw callback error directly", async () => {
862
+ const { factory } = createMockUOWFactory([{ success: true }]);
863
+ const originalError = new Error("Original error");
864
+
865
+ try {
866
+ await executeRestrictedUnitOfWork(
867
+ async () => {
868
+ throw originalError;
869
+ },
870
+ {
871
+ createUnitOfWork: factory,
872
+ retryPolicy: new NoRetryPolicy(), // Don't retry
873
+ },
874
+ );
875
+ expect.fail("Should have thrown");
876
+ } catch (error) {
877
+ // Error should be thrown directly, not wrapped
878
+ expect(error).toBe(originalError);
879
+ }
880
+ });
881
+ });
882
+
883
+ describe("abort signal", () => {
884
+ it("should throw when aborted before execution", async () => {
885
+ const { factory } = createMockUOWFactory([{ success: true }]);
886
+ const controller = new AbortController();
887
+ controller.abort();
888
+
889
+ await expect(
890
+ executeRestrictedUnitOfWork(
891
+ async () => {
892
+ return {};
893
+ },
894
+ { createUnitOfWork: factory, signal: controller.signal },
895
+ ),
896
+ ).rejects.toThrow("Unit of Work execution aborted");
897
+ });
898
+
899
+ it("should stop retrying when aborted during retry", async () => {
900
+ const { factory, callCount } = createMockUOWFactory([
901
+ { success: false },
902
+ { success: false },
903
+ { success: true },
904
+ ]);
905
+ const controller = new AbortController();
906
+
907
+ const promise = executeRestrictedUnitOfWork(
908
+ async ({ executeMutate }) => {
909
+ if (callCount.value === 2) {
910
+ controller.abort();
911
+ }
912
+ await executeMutate();
913
+ return {};
914
+ },
915
+ { createUnitOfWork: factory, signal: controller.signal },
916
+ );
917
+
918
+ await expect(promise).rejects.toThrow("Unit of Work execution aborted");
919
+ expect(callCount.value).toBeLessThanOrEqual(2);
920
+ });
921
+ });
922
+
923
+ describe("restricted UOW interface", () => {
924
+ it("should provide access to forSchema", async () => {
925
+ const { factory } = createMockUOWFactory([{ success: true }]);
926
+
927
+ await executeRestrictedUnitOfWork(
928
+ async ({ forSchema }) => {
929
+ const uow = forSchema(testSchema);
930
+ expect(uow).toBeDefined();
931
+ expect(uow.schema).toBe(testSchema);
932
+ return {};
933
+ },
934
+ { createUnitOfWork: factory },
935
+ );
936
+ });
937
+
938
+ it("should allow creating entities via forSchema", async () => {
939
+ const { factory } = createMockUOWFactory([{ success: true }]);
940
+
941
+ const result = await executeRestrictedUnitOfWork(
942
+ async ({ forSchema, executeRetrieve, executeMutate }) => {
943
+ const uow = forSchema(testSchema);
944
+ await executeRetrieve();
945
+
946
+ const userId = uow.create("users", {
947
+ id: "user-123",
948
+ email: "test@example.com",
949
+ name: "Test",
950
+ balance: 0,
951
+ });
952
+
953
+ await executeMutate();
954
+
955
+ return { userId };
956
+ },
957
+ { createUnitOfWork: factory },
958
+ );
959
+
960
+ expect(result.userId).toBeInstanceOf(FragnoId);
961
+ expect(result.userId.externalId).toBe("user-123");
962
+ });
963
+ });
964
+
965
+ describe("promise awaiting in callback result", () => {
966
+ it("should await promises in result object (1 level deep)", async () => {
967
+ const { factory } = createMockUOWFactory([{ success: true }]);
968
+
969
+ const result = await executeRestrictedUnitOfWork(
970
+ async ({ executeMutate }) => {
971
+ await executeMutate();
972
+ return {
973
+ userId: Promise.resolve("user-123"),
974
+ profileId: Promise.resolve("profile-456"),
975
+ status: "completed",
976
+ };
977
+ },
978
+ { createUnitOfWork: factory },
979
+ );
980
+
981
+ expect(result).toEqual({
982
+ userId: "user-123",
983
+ profileId: "profile-456",
984
+ status: "completed",
985
+ });
986
+ });
987
+
988
+ it("should await promises in result array", async () => {
989
+ const { factory } = createMockUOWFactory([{ success: true }]);
990
+
991
+ const result = await executeRestrictedUnitOfWork(
992
+ async ({ executeMutate }) => {
993
+ await executeMutate();
994
+ return [Promise.resolve(1), Promise.resolve(2), 3];
995
+ },
996
+ { createUnitOfWork: factory },
997
+ );
998
+
999
+ expect(result).toEqual([1, 2, 3]);
1000
+ });
1001
+
1002
+ it("should await direct promise result", async () => {
1003
+ const { factory } = createMockUOWFactory([{ success: true }]);
1004
+
1005
+ const result = await executeRestrictedUnitOfWork(
1006
+ async ({ executeMutate }) => {
1007
+ await executeMutate();
1008
+ return Promise.resolve({ data: "test" });
1009
+ },
1010
+ { createUnitOfWork: factory },
1011
+ );
1012
+
1013
+ expect(result).toEqual({ data: "test" });
1014
+ });
1015
+
1016
+ it("should NOT await nested promises (only 1 level deep)", async () => {
1017
+ const { factory } = createMockUOWFactory([{ success: true }]);
1018
+
1019
+ const result = await executeRestrictedUnitOfWork(
1020
+ async ({ executeMutate }) => {
1021
+ await executeMutate();
1022
+ return {
1023
+ nested: {
1024
+ promise: Promise.resolve("still-a-promise"),
1025
+ },
1026
+ };
1027
+ },
1028
+ { createUnitOfWork: factory },
1029
+ );
1030
+
1031
+ // The nested promise should still be a promise
1032
+ expect(result.nested.promise).toBeInstanceOf(Promise);
1033
+ });
1034
+
1035
+ it("should handle mixed types in result", async () => {
1036
+ const { factory } = createMockUOWFactory([{ success: true }]);
1037
+
1038
+ const result = await executeRestrictedUnitOfWork(
1039
+ async ({ executeMutate }) => {
1040
+ await executeMutate();
1041
+ return {
1042
+ promise: Promise.resolve("resolved"),
1043
+ number: 42,
1044
+ string: "test",
1045
+ boolean: true,
1046
+ null: null,
1047
+ undefined: undefined,
1048
+ object: { nested: "value" },
1049
+ };
1050
+ },
1051
+ { createUnitOfWork: factory },
1052
+ );
1053
+
1054
+ expect(result).toEqual({
1055
+ promise: "resolved",
1056
+ number: 42,
1057
+ string: "test",
1058
+ boolean: true,
1059
+ null: null,
1060
+ undefined: undefined,
1061
+ object: { nested: "value" },
1062
+ });
1063
+ });
1064
+
1065
+ it("should await promises even after retries", async () => {
1066
+ const { factory, callCount } = createMockUOWFactory([{ success: false }, { success: true }]);
1067
+
1068
+ const result = await executeRestrictedUnitOfWork(
1069
+ async ({ executeMutate }) => {
1070
+ await executeMutate();
1071
+ return {
1072
+ attempt: callCount.value,
1073
+ data: Promise.resolve("final-result"),
1074
+ };
1075
+ },
1076
+ { createUnitOfWork: factory },
1077
+ );
1078
+
1079
+ expect(result).toEqual({
1080
+ attempt: 2,
1081
+ data: "final-result",
1082
+ });
1083
+ });
1084
+
1085
+ it("should handle complex objects with multiple promises at top level", async () => {
1086
+ const { factory } = createMockUOWFactory([{ success: true }]);
1087
+
1088
+ const result = await executeRestrictedUnitOfWork(
1089
+ async ({ executeMutate }) => {
1090
+ await executeMutate();
1091
+ return {
1092
+ userId: Promise.resolve("user-1"),
1093
+ email: Promise.resolve("test@example.com"),
1094
+ count: Promise.resolve(100),
1095
+ active: Promise.resolve(true),
1096
+ metadata: {
1097
+ timestamp: Date.now(),
1098
+ version: 1,
1099
+ },
1100
+ };
1101
+ },
1102
+ { createUnitOfWork: factory },
1103
+ );
1104
+
1105
+ expect(typeof result.userId).toBe("string");
1106
+ expect(result.userId).toBe("user-1");
1107
+ expect(typeof result.email).toBe("string");
1108
+ expect(result.email).toBe("test@example.com");
1109
+ expect(typeof result.count).toBe("number");
1110
+ expect(result.count).toBe(100);
1111
+ expect(typeof result.active).toBe("boolean");
1112
+ expect(result.active).toBe(true);
1113
+ expect(typeof result.metadata.timestamp).toBe("number");
1114
+ expect(result.metadata.version).toBe(1);
1115
+ });
1116
+
1117
+ it("should handle empty object result", async () => {
1118
+ const { factory } = createMockUOWFactory([{ success: true }]);
1119
+
1120
+ const result = await executeRestrictedUnitOfWork(
1121
+ async ({ executeMutate }) => {
1122
+ await executeMutate();
1123
+ return {};
1124
+ },
1125
+ { createUnitOfWork: factory },
1126
+ );
1127
+
1128
+ expect(result).toEqual({});
1129
+ });
1130
+
1131
+ it("should handle primitive result types", async () => {
1132
+ const { factory } = createMockUOWFactory([{ success: true }]);
1133
+
1134
+ const stringResult = await executeRestrictedUnitOfWork(
1135
+ async ({ executeMutate }) => {
1136
+ await executeMutate();
1137
+ return "test-string";
1138
+ },
1139
+ { createUnitOfWork: factory },
1140
+ );
1141
+
1142
+ expect(stringResult).toBe("test-string");
1143
+
1144
+ const { factory: factory2 } = createMockUOWFactory([{ success: true }]);
1145
+ const numberResult = await executeRestrictedUnitOfWork(
1146
+ async ({ executeMutate }) => {
1147
+ await executeMutate();
1148
+ return 42;
1149
+ },
1150
+ { createUnitOfWork: factory2 },
1151
+ );
1152
+
1153
+ expect(numberResult).toBe(42);
1154
+ });
1155
+ });
1156
+
1157
+ describe("tuple return types", () => {
1158
+ it("should await promises in tuple and preserve tuple structure", async () => {
1159
+ const { factory } = createMockUOWFactory([{ success: true }]);
1160
+
1161
+ const result = await executeRestrictedUnitOfWork(
1162
+ async ({ executeMutate }) => {
1163
+ await executeMutate();
1164
+ // Return a tuple with promises
1165
+ return [Promise.resolve("user-123"), Promise.resolve(42)] as const;
1166
+ },
1167
+ { createUnitOfWork: factory },
1168
+ );
1169
+
1170
+ // Runtime behavior: promises should be awaited
1171
+ expect(result).toEqual(["user-123", 42]);
1172
+ expect(result[0]).toBe("user-123");
1173
+ expect(result[1]).toBe(42);
1174
+ });
1175
+
1176
+ it("should handle tuple with mixed promise and non-promise values", async () => {
1177
+ const { factory } = createMockUOWFactory([{ success: true }]);
1178
+
1179
+ const result = await executeRestrictedUnitOfWork(
1180
+ async ({ executeMutate }) => {
1181
+ await executeMutate();
1182
+ // Tuple with mixed types
1183
+ return [Promise.resolve("first"), "second", Promise.resolve(3)] as const;
1184
+ },
1185
+ { createUnitOfWork: factory },
1186
+ );
1187
+
1188
+ expect(result).toEqual(["first", "second", 3]);
1189
+ expect(result[0]).toBe("first");
1190
+ expect(result[1]).toBe("second");
1191
+ expect(result[2]).toBe(3);
1192
+ });
1193
+
1194
+ it("should handle Promise.all pattern with tuple", async () => {
1195
+ const { factory } = createMockUOWFactory([{ success: true }]);
1196
+
1197
+ const result = await executeRestrictedUnitOfWork(
1198
+ async ({ executeMutate }) => {
1199
+ await executeMutate();
1200
+ // Simulate the pattern from db-fragment-integration.test.ts
1201
+ const userPromise = Promise.resolve({ id: "user-1", name: "John" });
1202
+ const ordersPromise = Promise.resolve([
1203
+ { id: "order-1", total: 100 },
1204
+ { id: "order-2", total: 200 },
1205
+ ]);
1206
+ return await Promise.all([userPromise, ordersPromise]);
1207
+ },
1208
+ { createUnitOfWork: factory },
1209
+ );
1210
+
1211
+ // Runtime behavior
1212
+ expect(result).toHaveLength(2);
1213
+ expect(result[0]).toEqual({ id: "user-1", name: "John" });
1214
+ expect(result[1]).toEqual([
1215
+ { id: "order-1", total: 100 },
1216
+ { id: "order-2", total: 200 },
1217
+ ]);
1218
+
1219
+ // Type check: result should be [{ id: string; name: string }, { id: string; total: number }[]]
1220
+ // But with current implementation, it's incorrectly typed as an array union
1221
+ const [user, orders] = result;
1222
+ expect(user).toBeDefined();
1223
+ expect(orders).toBeDefined();
1224
+ });
1225
+
1226
+ it("should handle array (not tuple) with promises", async () => {
1227
+ const { factory } = createMockUOWFactory([{ success: true }]);
1228
+
1229
+ const result = await executeRestrictedUnitOfWork(
1230
+ async ({ executeMutate }) => {
1231
+ await executeMutate();
1232
+ // Regular array (not a tuple)
1233
+ const items = [Promise.resolve(1), Promise.resolve(2), Promise.resolve(3)];
1234
+ return items;
1235
+ },
1236
+ { createUnitOfWork: factory },
1237
+ );
1238
+
1239
+ expect(result).toEqual([1, 2, 3]);
1240
+ expect(result).toHaveLength(3);
1241
+ });
1242
+ });
1243
+
1244
+ describe("unhandled rejection handling", () => {
1245
+ it("should not cause unhandled rejection when service method awaits retrievalPhase and executeRetrieve fails", async () => {
1246
+ const settingsSchema = schema((s) =>
1247
+ s.addTable("settings", (t) =>
1248
+ t
1249
+ .addColumn("id", idColumn())
1250
+ .addColumn("key", "string")
1251
+ .addColumn("value", "string")
1252
+ .createIndex("unique_key", ["key"], { unique: true }),
1253
+ ),
1254
+ );
1255
+
1256
+ // Create executor that throws "table does not exist" error
1257
+ const failingExecutor: UOWExecutor<unknown, unknown> = {
1258
+ executeRetrievalPhase: async () => {
1259
+ throw new Error('relation "settings" does not exist');
1260
+ },
1261
+ executeMutationPhase: async () => ({ success: true, createdInternalIds: [] }),
1262
+ };
1263
+
1264
+ const factory = () =>
1265
+ createUnitOfWork(createMockCompiler(), failingExecutor, createMockDecoder());
1266
+
1267
+ const deferred = Promise.withResolvers<string>();
1268
+
1269
+ // Service method that awaits retrievalPhase (simulating settingsService.get())
1270
+ const getSettingValue = async (typedUow: TypedUnitOfWork<typeof settingsSchema>) => {
1271
+ const uow = typedUow.find("settings", (b) =>
1272
+ b.whereIndex("unique_key", (eb) => eb("key", "=", "version")),
1273
+ );
1274
+ const [results] = await uow.retrievalPhase;
1275
+ return results?.[0];
1276
+ };
1277
+
1278
+ // Execute with executeRestrictedUnitOfWork
1279
+ try {
1280
+ await executeRestrictedUnitOfWork(
1281
+ async ({ forSchema, executeRetrieve }) => {
1282
+ const uow = forSchema(settingsSchema);
1283
+
1284
+ const settingPromise = getSettingValue(uow);
1285
+
1286
+ // Execute retrieval - this will fail
1287
+ await executeRetrieve();
1288
+
1289
+ // Won't reach here
1290
+ return await settingPromise;
1291
+ },
1292
+ {
1293
+ createUnitOfWork: factory,
1294
+ retryPolicy: new NoRetryPolicy(),
1295
+ },
1296
+ );
1297
+ expect.fail("Should have thrown an error");
1298
+ } catch (error) {
1299
+ // The error should be thrown directly (not wrapped) since it's not a concurrency conflict
1300
+ expect(error).toBeInstanceOf(Error);
1301
+ expect((error as Error).message).toContain('relation "settings" does not exist');
1302
+ deferred.resolve((error as Error).message);
1303
+ }
1304
+
1305
+ // Verify no unhandled rejection occurred
1306
+ // If the test completes without throwing, the promise rejection was properly handled
1307
+ expect(await deferred.promise).toContain('relation "settings" does not exist');
1308
+ });
1309
+ });
1310
+ });