@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,18 +1,18 @@
1
1
  import { SQLocalKysely } from "sqlocal/kysely";
2
2
  import { assert, beforeAll, describe, expect, it } from "vitest";
3
3
  import { z } from "zod";
4
- import { KyselyAdapter } from "./adapters/kysely/kysely-adapter";
4
+ import { SqlAdapter } from "./adapters/generic-sql/generic-sql-adapter";
5
5
  import { column, idColumn, referenceColumn, schema, type FragnoId } from "./schema/create";
6
6
  import { defineFragment, instantiate } from "@fragno-dev/core";
7
7
  import { defineRoutes } from "@fragno-dev/core/route";
8
8
  import { withDatabase } from "./with-database";
9
9
  import type { FragnoPublicConfigWithDatabase } from "./db-fragment-definition-builder";
10
- import { ConcurrencyConflictError } from "./query/unit-of-work/execute-unit-of-work";
10
+ import { ConcurrencyConflictError, type TxResult } from "./query/unit-of-work/execute-unit-of-work";
11
11
  import { SQLocalDriverConfig } from "./adapters/generic-sql/driver-config";
12
12
 
13
13
  describe.sequential("Database Fragment Integration", () => {
14
14
  // Schema 1: Users fragment
15
- const usersSchema = schema((s) => {
15
+ const usersSchema = schema("users", (s) => {
16
16
  return s
17
17
  .addTable("users", (t) => {
18
18
  return t
@@ -36,7 +36,7 @@ describe.sequential("Database Fragment Integration", () => {
36
36
  });
37
37
 
38
38
  // Schema 2: Orders fragment
39
- const ordersSchema = schema((s) => {
39
+ const ordersSchema = schema("orders", (s) => {
40
40
  return s.addTable("orders", (t) => {
41
41
  return t
42
42
  .addColumn("id", idColumn())
@@ -48,106 +48,124 @@ describe.sequential("Database Fragment Integration", () => {
48
48
  });
49
49
  });
50
50
 
51
- // Define Users Fragment
51
+ // Define Users Fragment using the new unified serviceTx API
52
52
  const usersFragmentDef = defineFragment("users-fragment")
53
- .extend(withDatabase(usersSchema, "users"))
53
+ .extend(withDatabase(usersSchema))
54
54
  .providesService("userService", ({ defineService }) => {
55
55
  return defineService({
56
- createUser(name: string, email: string): FragnoId {
57
- const uow = this.uow(usersSchema);
58
- return uow.create("users", { name, email });
56
+ // Creates a user - returns TxResult<FragnoId>
57
+ createUser(name: string, email: string) {
58
+ return this.serviceTx(usersSchema)
59
+ .mutate(({ uow }) => uow.create("users", { name, email }))
60
+ .build();
59
61
  },
60
- async getUserById(userId: FragnoId | string) {
61
- const uow = this.uow(usersSchema).find("users", (b) =>
62
- b.whereIndex("primary", (eb) => eb("id", "=", userId)),
63
- );
64
- // Note: executeRetrieve() should be called by the caller before awaiting retrievalPhase
65
- const [users] = await uow.retrievalPhase;
66
- return users?.[0] ?? null;
62
+ // Gets a user by ID - returns TxResult<User | null>
63
+ getUserById(userId: FragnoId | string) {
64
+ return this.serviceTx(usersSchema)
65
+ .retrieve((uow) =>
66
+ uow.find("users", (b) => b.whereIndex("primary", (eb) => eb("id", "=", userId))),
67
+ )
68
+ .transformRetrieve(
69
+ ([users]): { id: FragnoId; name: string; email: string } | null => users[0] ?? null,
70
+ )
71
+ .build();
67
72
  },
68
- createProfile(userId: FragnoId | string, bio: string): FragnoId {
69
- const uow = this.uow(usersSchema);
70
- return uow.create("profiles", {
71
- user_id: userId,
72
- bio,
73
- });
73
+ // Creates a profile - returns TxResult<FragnoId>
74
+ createProfile(userId: FragnoId | string, bio: string) {
75
+ return this.serviceTx(usersSchema)
76
+ .mutate(({ uow }) =>
77
+ uow.create("profiles", {
78
+ user_id: userId,
79
+ bio,
80
+ }),
81
+ )
82
+ .build();
74
83
  },
75
84
  });
76
85
  })
77
86
  .build();
78
87
 
79
- // Define routes for Users Fragment
80
- const usersRoutes = defineRoutes(usersFragmentDef).create(({ services, defineRoute }) => [
88
+ // Define routes for Users Fragment using new handlerTx API
89
+ const usersRoutes = defineRoutes(usersFragmentDef).create(({ defineRoute }) => [
81
90
  defineRoute({
82
91
  method: "POST",
83
92
  path: "/users",
84
93
  outputSchema: z.object({ userId: z.string(), profileId: z.string() }),
85
94
  handler: async function (_input, { json }) {
86
- const { userId, profileId } = await this.uow(async ({ executeMutate }) => {
87
- const userId = services.userService.createUser("John Doe", "john@example.com");
88
- const profileId = services.userService.createProfile(userId, "Software engineer");
89
- await executeMutate();
90
- return { userId, profileId };
91
- });
95
+ // Use handlerTx with mutate to create both user and profile atomically
96
+ const result = await this.handlerTx()
97
+ .mutate(({ forSchema }) => {
98
+ const uow = forSchema(usersSchema);
99
+ const userId = uow.create("users", { name: "John Doe", email: "john@example.com" });
100
+ const profileId = uow.create("profiles", {
101
+ user_id: userId,
102
+ bio: "Software engineer",
103
+ });
104
+ return { userId, profileId };
105
+ })
106
+ .execute();
92
107
 
93
108
  return json(
94
- { userId: userId.externalId, profileId: profileId.externalId },
109
+ { userId: result.userId.externalId, profileId: result.profileId.externalId },
95
110
  { status: 201 },
96
111
  );
97
112
  },
98
113
  }),
99
114
  ]);
100
115
 
101
- // Define Orders Fragment with cross-fragment service dependency
116
+ // User type for service dependency
117
+ type User = { id: FragnoId; name: string; email: string };
118
+
119
+ // Define Orders Fragment with cross-fragment service dependency using new serviceTx API
102
120
  const ordersFragmentDef = defineFragment("orders-fragment")
103
- .extend(withDatabase(ordersSchema, "orders"))
121
+ .extend(withDatabase(ordersSchema))
104
122
  .usesService<
105
123
  "userService",
106
124
  {
107
- getUserById: (
108
- userId: FragnoId | string,
109
- ) => Promise<{ id: FragnoId; name: string; email: string } | null>;
125
+ // Service methods now return TxResult instead of Promise
126
+ // TxResult<T> defaults to TxResult<T, T> (deps receive same type as final result)
127
+ getUserById: (userId: FragnoId | string) => TxResult<User | null>;
110
128
  }
111
129
  >("userService")
112
130
  .providesService("orderService", ({ defineService, serviceDeps }) => {
113
131
  return defineService({
114
- async createOrder(
115
- userExternalId: string,
116
- productName: string,
117
- quantity: number,
118
- total: number,
119
- ) {
120
- // Verify user exists by calling the userService from the other fragment
121
- const user = await serviceDeps.userService.getUserById(userExternalId);
122
-
123
- if (!user) {
124
- throw new Error("User not found");
125
- }
126
-
127
- const uow = this.uow(ordersSchema);
128
- const orderId = uow.create("orders", {
129
- user_external_id: userExternalId,
130
- product_name: productName,
131
- quantity,
132
- total,
133
- });
134
-
135
- await uow.mutationPhase;
136
- return orderId;
132
+ createOrder(userExternalId: string, productName: string, quantity: number, total: number) {
133
+ return this.serviceTx(ordersSchema)
134
+ .withServiceCalls(() => [serviceDeps.userService.getUserById(userExternalId)] as const)
135
+ .mutate(({ uow, serviceIntermediateResult: [user] }) => {
136
+ if (!user) {
137
+ throw new Error("User not found");
138
+ }
139
+
140
+ expect(user.id.externalId).toBe(userExternalId);
141
+
142
+ return uow.create("orders", {
143
+ user_external_id: userExternalId,
144
+ product_name: productName,
145
+ quantity,
146
+ total,
147
+ });
148
+ })
149
+ .build();
137
150
  },
138
- async getOrdersByUser(userExternalId: string) {
139
- const uow = this.uow(ordersSchema).find("orders", (b) =>
140
- b.whereIndex("orders_user_idx", (eb) => eb("user_external_id", "=", userExternalId)),
141
- );
142
- // Note: executeRetrieve() should be called by the caller before awaiting retrievalPhase
143
- const [orders] = await uow.retrievalPhase;
144
- return orders;
151
+ // Gets orders by user - returns TxResult<Order[]>
152
+ getOrdersByUser(userExternalId: string) {
153
+ return this.serviceTx(ordersSchema)
154
+ .retrieve((uow) =>
155
+ uow.find("orders", (b) =>
156
+ b.whereIndex("orders_user_idx", (eb) =>
157
+ eb("user_external_id", "=", userExternalId),
158
+ ),
159
+ ),
160
+ )
161
+ .transformRetrieve(([orders]) => orders)
162
+ .build();
145
163
  },
146
164
  });
147
165
  })
148
166
  .build();
149
167
 
150
- // Define routes for Orders Fragment
168
+ // Define routes for Orders Fragment using new handlerTx API
151
169
  const ordersRoutes = defineRoutes(ordersFragmentDef).create(({ services, defineRoute }) => [
152
170
  defineRoute({
153
171
  method: "POST",
@@ -159,28 +177,39 @@ describe.sequential("Database Fragment Integration", () => {
159
177
  total: z.number(),
160
178
  }),
161
179
  outputSchema: z.object({ orderId: z.string() }),
162
- handler: async function ({ input }, { json }) {
180
+ handler: async function ({ input }, { json, error }) {
163
181
  const body = await input.valid();
164
182
 
165
- const { orderId } = await this.uow(async ({ executeMutate }) => {
166
- const orderIdPromise = services.orderService.createOrder(
167
- body.userId,
168
- body.productName,
169
- body.quantity,
170
- body.total,
171
- );
172
-
173
- await executeMutate();
174
-
175
- return { orderId: await orderIdPromise };
176
- });
177
-
178
- return json({ orderId: orderId.externalId }, { status: 201 });
183
+ try {
184
+ // Use handlerTx with withServiceCalls to execute the service TxResult
185
+ // createOrder validates that the user exists
186
+ const result = await this.handlerTx()
187
+ .withServiceCalls(
188
+ () =>
189
+ [
190
+ services.orderService.createOrder(
191
+ body.userId,
192
+ body.productName,
193
+ body.quantity,
194
+ body.total,
195
+ ),
196
+ ] as const,
197
+ )
198
+ .transform(({ serviceResult: [orderId] }) => ({ orderId }))
199
+ .execute();
200
+
201
+ return json({ orderId: result.orderId.externalId }, { status: 201 });
202
+ } catch (e) {
203
+ if (e instanceof Error && e.message === "User not found") {
204
+ return error({ message: "User not found", code: "USER_NOT_FOUND" }, { status: 404 });
205
+ }
206
+ throw e;
207
+ }
179
208
  },
180
209
  }),
181
210
  ]);
182
211
 
183
- let adapter: KyselyAdapter;
212
+ let adapter: SqlAdapter;
184
213
  let usersFragment: ReturnType<typeof instantiateUsersFragment>;
185
214
  let ordersFragment: ReturnType<typeof instantiateOrdersFragment>;
186
215
 
@@ -210,7 +239,7 @@ describe.sequential("Database Fragment Integration", () => {
210
239
  beforeAll(async () => {
211
240
  // Create in-memory SQLite database with Kysely
212
241
  const { dialect } = new SQLocalKysely(":memory:");
213
- adapter = new KyselyAdapter({
242
+ adapter = new SqlAdapter({
214
243
  dialect,
215
244
  driverConfig: new SQLocalDriverConfig(),
216
245
  });
@@ -244,11 +273,12 @@ describe.sequential("Database Fragment Integration", () => {
244
273
 
245
274
  it("should verify user was created with profile", async () => {
246
275
  const user = await usersFragment.inContext(async function () {
247
- return await this.uow(async ({ executeRetrieve }) => {
248
- const userPromise = usersFragment.services.userService.getUserById(userId);
249
- await executeRetrieve();
250
- return await userPromise;
251
- });
276
+ // Use handlerTx with withServiceCalls to execute the service TxResult
277
+ const result = await this.handlerTx()
278
+ .withServiceCalls(() => [usersFragment.services.userService.getUserById(userId)] as const)
279
+ .transform(({ serviceResult: [user] }) => user)
280
+ .execute();
281
+ return result;
252
282
  });
253
283
 
254
284
  expect(user).toMatchObject({
@@ -274,15 +304,18 @@ describe.sequential("Database Fragment Integration", () => {
274
304
  `createOrderResponse.type !== json: ${createOrderResponse.type}`,
275
305
  );
276
306
  orderId = createOrderResponse.data.orderId;
277
- });
307
+ }, 500);
278
308
 
279
309
  it("should verify order was created with correct user reference", async () => {
280
310
  const orders = await ordersFragment.inContext(async function () {
281
- return await this.uow(async ({ executeRetrieve }) => {
282
- const ordersPromise = ordersFragment.services.orderService.getOrdersByUser(userId);
283
- await executeRetrieve();
284
- return await ordersPromise;
285
- });
311
+ // Use handlerTx with withServiceCalls to execute the service TxResult
312
+ const result = await this.handlerTx()
313
+ .withServiceCalls(
314
+ () => [ordersFragment.services.orderService.getOrdersByUser(userId)] as const,
315
+ )
316
+ .transform(({ serviceResult: [orders] }) => orders)
317
+ .execute();
318
+ return result;
286
319
  });
287
320
  expect(orders).toHaveLength(1);
288
321
  expect(orders[0]).toMatchObject({
@@ -299,20 +332,25 @@ describe.sequential("Database Fragment Integration", () => {
299
332
  it("should verify cross-fragment service integration works bidirectionally", async () => {
300
333
  // Orders service should be able to query users via the shared userService
301
334
  const ordersByUser = await ordersFragment.inContext(async function () {
302
- return await this.uow(async ({ executeRetrieve }) => {
303
- const ordersPromise = ordersFragment.services.orderService.getOrdersByUser(userId);
304
- await executeRetrieve();
305
- return await ordersPromise;
306
- });
335
+ const result = await this.handlerTx()
336
+ .withServiceCalls(
337
+ () => [ordersFragment.services.orderService.getOrdersByUser(userId)] as const,
338
+ )
339
+ .transform(({ serviceResult: [orders] }) => orders)
340
+ .execute();
341
+ return result;
307
342
  });
308
343
  const userFromOrdersContext = await usersFragment.inContext(async function () {
309
- return await this.uow(async ({ executeRetrieve }) => {
310
- const userPromise = usersFragment.services.userService.getUserById(
311
- ordersByUser[0].user_external_id,
312
- );
313
- await executeRetrieve();
314
- return await userPromise;
315
- });
344
+ const result = await this.handlerTx()
345
+ .withServiceCalls(
346
+ () =>
347
+ [
348
+ usersFragment.services.userService.getUserById(ordersByUser[0].user_external_id),
349
+ ] as const,
350
+ )
351
+ .transform(({ serviceResult: [user] }) => user)
352
+ .execute();
353
+ return result;
316
354
  });
317
355
 
318
356
  expect(userFromOrdersContext).toMatchObject({
@@ -334,27 +372,36 @@ describe.sequential("Database Fragment Integration", () => {
334
372
  },
335
373
  });
336
374
 
337
- // Should return error because user doesn't exist
338
- expect(invalidOrderResponse.type).toBe("error");
375
+ // Should return 404 error because user doesn't exist
376
+ expect(invalidOrderResponse.status).toBe(404);
339
377
  });
340
378
 
341
379
  it("should be able to use inContext to call a service", async () => {
342
- const [user, order] = await usersFragment.inContext(async function () {
343
- return await this.uow(async ({ executeRetrieve }) => {
344
- const user = usersFragment.services.userService.getUserById(userId);
345
- const orders = ordersFragment.services.orderService.getOrdersByUser(userId);
346
- await executeRetrieve();
347
- return [user, orders];
348
- });
380
+ const result = await usersFragment.inContext(async function () {
381
+ // Use handlerTx with multiple deps
382
+ return await this.handlerTx()
383
+ .withServiceCalls(
384
+ () =>
385
+ [
386
+ usersFragment.services.userService.getUserById(userId),
387
+ ordersFragment.services.orderService.getOrdersByUser(userId),
388
+ ] as const,
389
+ )
390
+ .transform(({ serviceResult: [userResult, ordersResult] }) => ({
391
+ user: userResult,
392
+ orders: ordersResult,
393
+ }))
394
+ .execute();
349
395
  });
350
- expect(user).toMatchObject({
396
+
397
+ expect(result.user).toMatchObject({
351
398
  id: expect.objectContaining({
352
399
  externalId: userId,
353
400
  }),
354
401
  });
355
402
 
356
- expect(order).toHaveLength(1);
357
- expect(order[0]).toMatchObject({
403
+ expect(result.orders).toHaveLength(1);
404
+ expect(result.orders[0]).toMatchObject({
358
405
  id: expect.objectContaining({
359
406
  externalId: orderId,
360
407
  }),
@@ -362,45 +409,150 @@ describe.sequential("Database Fragment Integration", () => {
362
409
  });
363
410
  });
364
411
 
365
- it("should provide nonce and currentAttempt in the UOW context", async () => {
366
- let firstNonce: string;
412
+ it("should provide idempotencyKey and currentAttempt in the handlerTx context", async () => {
413
+ let firstIdempotencyKey: string;
367
414
 
368
415
  const result = await usersFragment.inContext(async function () {
369
- return await this.uow(async ({ executeRetrieve, nonce, currentAttempt }) => {
370
- const user = usersFragment.services.userService.getUserById(userId);
371
- await executeRetrieve();
372
-
373
- if (currentAttempt === 0) {
374
- firstNonce = nonce;
375
- // Trigger a conflict by throwing the specific conflict error
376
- throw new ConcurrencyConflictError();
377
- }
416
+ return await this.handlerTx()
417
+ // Add a retrieve op so retry is allowed for the forced conflict below.
418
+ .retrieve(({ forSchema }) => forSchema(usersSchema).find("users"))
419
+ .mutate(({ forSchema, idempotencyKey, currentAttempt }) => {
420
+ if (currentAttempt === 0) {
421
+ firstIdempotencyKey = idempotencyKey;
422
+ // Trigger a conflict by throwing the specific conflict error
423
+ throw new ConcurrencyConflictError();
424
+ }
378
425
 
379
- expect(nonce).toBe(firstNonce);
426
+ expect(idempotencyKey).toBe(firstIdempotencyKey);
380
427
 
381
- // Return context data along with user data
382
- return {
383
- user,
384
- nonce,
385
- currentAttempt,
386
- };
387
- });
428
+ // Create something to verify the mutation works
429
+ const newUserId = forSchema(usersSchema).create("users", {
430
+ name: "Nonce Test User",
431
+ email: `nonce-test-${Date.now()}@example.com`,
432
+ });
433
+
434
+ // Return context data
435
+ return {
436
+ newUserId,
437
+ idempotencyKey,
438
+ currentAttempt,
439
+ };
440
+ })
441
+ .execute();
388
442
  });
389
443
 
390
- // Verify nonce is a string UUID
391
- expect(result.nonce).toBeDefined();
392
- expect(typeof result.nonce).toBe("string");
393
- expect(result.nonce).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
444
+ // Verify idempotencyKey is a string UUID
445
+ expect(result.idempotencyKey).toBeDefined();
446
+ expect(typeof result.idempotencyKey).toBe("string");
447
+ expect(result.idempotencyKey).toMatch(
448
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
449
+ );
394
450
 
395
451
  expect(result.currentAttempt).toBe(1);
396
452
 
397
- // Verify user data is still correct
398
- expect(result.user).toMatchObject({
399
- id: expect.objectContaining({
400
- externalId: userId,
401
- }),
402
- name: "John Doe",
403
- email: "john@example.com",
453
+ // Verify user was created
454
+ expect(result.newUserId).toBeDefined();
455
+ expect(result.newUserId.externalId).toBeDefined();
456
+ });
457
+
458
+ describe("Unified Tx Builder API (serviceTx/handlerTx)", () => {
459
+ it("should use handlerTx with mutate to create records", async () => {
460
+ const result = await usersFragment.inContext(async function () {
461
+ return await this.handlerTx()
462
+ .mutate(({ forSchema }) => {
463
+ const newUserId = forSchema(usersSchema).create("users", {
464
+ name: "Unified API User",
465
+ email: "unified@example.com",
466
+ });
467
+ return { newUserId };
468
+ })
469
+ .execute();
470
+ });
471
+
472
+ expect(result.newUserId).toBeDefined();
473
+ expect(result.newUserId.externalId).toBeDefined();
474
+ });
475
+
476
+ it("should use handlerTx with retrieve callback to query data", async () => {
477
+ const result = await usersFragment.inContext(async function () {
478
+ return await this.handlerTx()
479
+ .retrieve(({ forSchema }) =>
480
+ forSchema(usersSchema).find("users", (b) =>
481
+ b.whereIndex("primary", (eb) => eb("id", "=", userId)),
482
+ ),
483
+ )
484
+ .transformRetrieve(([users]) => {
485
+ return { retrieved: true, user: users[0] };
486
+ })
487
+ .execute();
488
+ });
489
+
490
+ expect(result.retrieved).toBe(true);
491
+ expect(result.user).toMatchObject({
492
+ id: expect.objectContaining({ externalId: userId }),
493
+ name: "John Doe",
494
+ });
495
+ });
496
+
497
+ it("should use handlerTx with mutate and transform callbacks", async () => {
498
+ const result = await usersFragment.inContext(async function () {
499
+ return await this.handlerTx()
500
+ .mutate(({ forSchema }) => {
501
+ const newUserId = forSchema(usersSchema).create("users", {
502
+ name: "Success Callback User",
503
+ email: "success-callback@example.com",
504
+ });
505
+ return { newUserId, createdInMutate: true };
506
+ })
507
+ .transform(({ mutateResult }) => {
508
+ return {
509
+ userId: mutateResult.newUserId,
510
+ wasCreatedInMutate: mutateResult.createdInMutate,
511
+ processedAt: new Date().toISOString(),
512
+ };
513
+ })
514
+ .execute();
515
+ });
516
+
517
+ expect(result.userId).toBeDefined();
518
+ expect(result.wasCreatedInMutate).toBe(true);
519
+ expect(result.processedAt).toBeDefined();
520
+ });
521
+
522
+ it("should use handlerTx with retrieve, mutate, and transform callbacks", async () => {
523
+ const result = await ordersFragment.inContext(async function () {
524
+ return await this.handlerTx()
525
+ .retrieve(({ forSchema }) =>
526
+ forSchema(ordersSchema).find("orders", (b) =>
527
+ b.whereIndex("orders_user_idx", (eb) => eb("user_external_id", "=", userId)),
528
+ ),
529
+ )
530
+ .transformRetrieve(([orders]) => {
531
+ return { existingOrders: orders };
532
+ })
533
+ .mutate(({ forSchema, retrieveResult }) => {
534
+ expect(retrieveResult.existingOrders.length).toBeGreaterThan(0);
535
+ const orderId = forSchema(ordersSchema).create("orders", {
536
+ user_external_id: userId,
537
+ product_name: "Full Flow Product",
538
+ quantity: 3,
539
+ total: 3000,
540
+ });
541
+ return { orderId };
542
+ })
543
+ .transform(({ mutateResult, retrieveResult }) => {
544
+ return {
545
+ orderId: mutateResult.orderId,
546
+ hadRetrieve: retrieveResult.existingOrders.length > 0,
547
+ completedAt: new Date().toISOString(),
548
+ };
549
+ })
550
+ .execute();
551
+ });
552
+
553
+ expect(result.orderId).toBeDefined();
554
+ expect(result.hadRetrieve).toBe(true);
555
+ expect(result.completedAt).toBeDefined();
404
556
  });
405
557
  });
406
558
  });
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createDurableHooksDispatcherDurableObject } from "./index";
3
+
4
+ describe("createDurableHooksDispatcherDurableObject", () => {
5
+ it("should schedule an initial alarm on creation", async () => {
6
+ const process = vi.fn().mockResolvedValue(0);
7
+ const getNextWakeAt = vi.fn().mockResolvedValue(new Date());
8
+ const drain = vi.fn().mockResolvedValue(undefined);
9
+ const setAlarm = vi.fn().mockResolvedValue(undefined);
10
+
11
+ const handlerFactory = createDurableHooksDispatcherDurableObject({
12
+ createProcessor: () => ({ process, getNextWakeAt, drain, namespace: "test" }),
13
+ });
14
+
15
+ handlerFactory({ storage: { setAlarm } }, {});
16
+
17
+ await Promise.resolve();
18
+ expect(setAlarm).toHaveBeenCalledTimes(1);
19
+ expect(process).not.toHaveBeenCalled();
20
+ });
21
+
22
+ it("should delete the alarm when no pending hooks exist", async () => {
23
+ const process = vi.fn().mockResolvedValue(0);
24
+ const getNextWakeAt = vi.fn().mockResolvedValue(null);
25
+ const drain = vi.fn().mockResolvedValue(undefined);
26
+ const setAlarm = vi.fn().mockResolvedValue(undefined);
27
+ const deleteAlarm = vi.fn().mockResolvedValue(undefined);
28
+
29
+ const handlerFactory = createDurableHooksDispatcherDurableObject({
30
+ createProcessor: () => ({ process, getNextWakeAt, drain, namespace: "test" }),
31
+ });
32
+
33
+ const handler = handlerFactory({ storage: { setAlarm, deleteAlarm } }, {});
34
+
35
+ await Promise.resolve();
36
+ expect(getNextWakeAt).toHaveBeenCalledTimes(1);
37
+
38
+ await handler.alarm?.();
39
+
40
+ expect(process).toHaveBeenCalledTimes(1);
41
+ expect(deleteAlarm).toHaveBeenCalledTimes(2);
42
+ expect(setAlarm).not.toHaveBeenCalled();
43
+ });
44
+
45
+ it("should schedule alarm using max(nextWakeAt, now)", async () => {
46
+ vi.useFakeTimers();
47
+ const now = new Date("2024-01-01T00:00:00Z");
48
+ vi.setSystemTime(now);
49
+
50
+ const process = vi.fn().mockResolvedValue(0);
51
+ const getNextWakeAt = vi.fn().mockResolvedValue(new Date(now.getTime() - 10000));
52
+ const drain = vi.fn().mockResolvedValue(undefined);
53
+ const setAlarm = vi.fn().mockResolvedValue(undefined);
54
+ const deleteAlarm = vi.fn().mockResolvedValue(undefined);
55
+
56
+ const handlerFactory = createDurableHooksDispatcherDurableObject({
57
+ createProcessor: () => ({ process, getNextWakeAt, drain, namespace: "test" }),
58
+ });
59
+
60
+ const handler = handlerFactory({ storage: { setAlarm, deleteAlarm } }, {});
61
+
62
+ await Promise.resolve();
63
+ expect(setAlarm).toHaveBeenCalledTimes(1);
64
+
65
+ await handler.alarm?.();
66
+
67
+ expect(setAlarm).toHaveBeenCalledTimes(2);
68
+ for (const [scheduledAt] of setAlarm.mock.calls) {
69
+ expect(scheduledAt.getTime()).toBeGreaterThanOrEqual(now.getTime());
70
+ }
71
+ vi.useRealTimers();
72
+ });
73
+ });