@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
@@ -2,16 +2,22 @@ import SQLite from "better-sqlite3";
2
2
  import { SqliteDialect } from "kysely";
3
3
  import { beforeAll, describe, expect, it } from "vitest";
4
4
  import { instantiate } from "@fragno-dev/core";
5
- import { internalFragmentDef, internalSchema, SETTINGS_NAMESPACE } from "./internal-fragment";
5
+ import {
6
+ internalFragmentDef,
7
+ internalSchema,
8
+ SETTINGS_NAMESPACE,
9
+ getSchemaVersionFromDatabase,
10
+ } from "./internal-fragment";
6
11
  import type { FragnoPublicConfigWithDatabase } from "../db-fragment-definition-builder";
7
- import { DrizzleAdapter } from "../adapters/drizzle/drizzle-adapter";
12
+ import { SqlAdapter } from "../adapters/generic-sql/generic-sql-adapter";
8
13
  import { BetterSQLite3DriverConfig } from "../adapters/generic-sql/driver-config";
9
14
  import { ExponentialBackoffRetryPolicy, NoRetryPolicy } from "../query/unit-of-work/retry-policy";
15
+ import { ConcurrencyConflictError } from "../query/unit-of-work/execute-unit-of-work";
10
16
  import type { FragnoId } from "../schema/create";
11
17
 
12
18
  describe("Internal Fragment", () => {
13
19
  let sqliteDatabase: SQLite.Database;
14
- let adapter: DrizzleAdapter;
20
+ let adapter: SqlAdapter;
15
21
  let fragment: ReturnType<typeof instantiateFragment>;
16
22
 
17
23
  function instantiateFragment(options: FragnoPublicConfigWithDatabase) {
@@ -25,19 +31,20 @@ describe("Internal Fragment", () => {
25
31
  database: sqliteDatabase,
26
32
  });
27
33
 
28
- adapter = new DrizzleAdapter({
34
+ adapter = new SqlAdapter({
29
35
  dialect,
30
36
  driverConfig: new BetterSQLite3DriverConfig(),
31
37
  });
32
38
 
33
39
  {
34
- const migrations = adapter.prepareMigrations(internalSchema, "");
40
+ const migrations = adapter.prepareMigrations(internalSchema, null);
35
41
  await migrations.executeWithDriver(adapter.driver, 0);
36
42
  }
37
43
 
38
44
  // Instantiate fragment with shared database adapter
39
45
  const options: FragnoPublicConfigWithDatabase = {
40
46
  databaseAdapter: adapter,
47
+ databaseNamespace: null,
41
48
  };
42
49
 
43
50
  fragment = instantiateFragment(options);
@@ -49,11 +56,12 @@ describe("Internal Fragment", () => {
49
56
 
50
57
  it("should get undefined for non-existent key", async () => {
51
58
  const result = await fragment.inContext(async function () {
52
- return await this.uow(async ({ executeRetrieve }) => {
53
- const valuePromise = fragment.services.settingsService.get(SETTINGS_NAMESPACE, "test-key");
54
- await executeRetrieve();
55
- return await valuePromise;
56
- });
59
+ return await this.handlerTx()
60
+ .withServiceCalls(
61
+ () => [fragment.services.settingsService.get(SETTINGS_NAMESPACE, "test-key")] as const,
62
+ )
63
+ .transform(({ serviceResult: [value] }) => value)
64
+ .execute();
57
65
  });
58
66
 
59
67
  expect(result).toBeUndefined();
@@ -61,23 +69,20 @@ describe("Internal Fragment", () => {
61
69
 
62
70
  it("should set and get a value", async () => {
63
71
  await fragment.inContext(async function () {
64
- return await this.uow(async ({ executeMutate }) => {
65
- const setPromise = fragment.services.settingsService.set(
66
- SETTINGS_NAMESPACE,
67
- "test-key",
68
- "test-value",
69
- );
70
- await executeMutate();
71
- await setPromise;
72
- });
72
+ await this.handlerTx()
73
+ .withServiceCalls(() => [
74
+ fragment.services.settingsService.set(SETTINGS_NAMESPACE, "test-key", "test-value"),
75
+ ])
76
+ .execute();
73
77
  });
74
78
 
75
79
  const result = await fragment.inContext(async function () {
76
- return await this.uow(async ({ executeRetrieve }) => {
77
- const valuePromise = fragment.services.settingsService.get(SETTINGS_NAMESPACE, "test-key");
78
- await executeRetrieve();
79
- return await valuePromise;
80
- });
80
+ return await this.handlerTx()
81
+ .withServiceCalls(
82
+ () => [fragment.services.settingsService.get(SETTINGS_NAMESPACE, "test-key")] as const,
83
+ )
84
+ .transform(({ serviceResult: [value] }) => value)
85
+ .execute();
81
86
  });
82
87
 
83
88
  expect(result).toMatchObject({
@@ -88,23 +93,27 @@ describe("Internal Fragment", () => {
88
93
 
89
94
  it("should update an existing value", async () => {
90
95
  await fragment.inContext(async function () {
91
- return await this.uow(async ({ executeMutate }) => {
92
- const setPromise = fragment.services.settingsService.set(
93
- SETTINGS_NAMESPACE,
94
- "test-key",
95
- "updated-value",
96
- );
97
- await executeMutate();
98
- await setPromise;
99
- });
96
+ await this.handlerTx()
97
+ .withServiceCalls(
98
+ () =>
99
+ [
100
+ fragment.services.settingsService.set(
101
+ SETTINGS_NAMESPACE,
102
+ "test-key",
103
+ "updated-value",
104
+ ),
105
+ ] as const,
106
+ )
107
+ .execute();
100
108
  });
101
109
 
102
110
  const result = await fragment.inContext(async function () {
103
- return await this.uow(async ({ executeRetrieve }) => {
104
- const valuePromise = fragment.services.settingsService.get(SETTINGS_NAMESPACE, "test-key");
105
- await executeRetrieve();
106
- return await valuePromise;
107
- });
111
+ return await this.handlerTx()
112
+ .withServiceCalls(
113
+ () => [fragment.services.settingsService.get(SETTINGS_NAMESPACE, "test-key")] as const,
114
+ )
115
+ .transform(({ serviceResult: [value] }) => value)
116
+ .execute();
108
117
  });
109
118
 
110
119
  expect(result).toMatchObject({
@@ -116,31 +125,31 @@ describe("Internal Fragment", () => {
116
125
  it("should delete a value", async () => {
117
126
  // First get the ID
118
127
  const setting = await fragment.inContext(async function () {
119
- return await this.uow(async ({ executeRetrieve }) => {
120
- const valuePromise = fragment.services.settingsService.get(SETTINGS_NAMESPACE, "test-key");
121
- await executeRetrieve();
122
- return await valuePromise;
123
- });
128
+ return await this.handlerTx()
129
+ .withServiceCalls(
130
+ () => [fragment.services.settingsService.get(SETTINGS_NAMESPACE, "test-key")] as const,
131
+ )
132
+ .transform(({ serviceResult: [value] }) => value)
133
+ .execute();
124
134
  });
125
135
 
126
136
  expect(setting).toBeDefined();
127
137
 
128
138
  // Delete it
129
139
  await fragment.inContext(async function () {
130
- return await this.uow(async ({ executeMutate }) => {
131
- const deletePromise = fragment.services.settingsService.delete(setting!.id);
132
- await executeMutate();
133
- await deletePromise;
134
- });
140
+ await this.handlerTx()
141
+ .withServiceCalls(() => [fragment.services.settingsService.delete(setting!.id)] as const)
142
+ .execute();
135
143
  });
136
144
 
137
145
  // Verify it's gone
138
146
  const result = await fragment.inContext(async function () {
139
- return await this.uow(async ({ executeRetrieve }) => {
140
- const valuePromise = fragment.services.settingsService.get(SETTINGS_NAMESPACE, "test-key");
141
- await executeRetrieve();
142
- return await valuePromise;
143
- });
147
+ return await this.handlerTx()
148
+ .withServiceCalls(
149
+ () => [fragment.services.settingsService.get(SETTINGS_NAMESPACE, "test-key")] as const,
150
+ )
151
+ .transform(({ serviceResult: [value] }) => value)
152
+ .execute();
144
153
  });
145
154
 
146
155
  expect(result).toBeUndefined();
@@ -149,7 +158,7 @@ describe("Internal Fragment", () => {
149
158
 
150
159
  describe("Hook Service", () => {
151
160
  let sqliteDatabase: SQLite.Database;
152
- let adapter: DrizzleAdapter;
161
+ let adapter: SqlAdapter;
153
162
  let fragment: ReturnType<typeof instantiateFragment>;
154
163
 
155
164
  function instantiateFragment(options: FragnoPublicConfigWithDatabase) {
@@ -163,18 +172,19 @@ describe("Hook Service", () => {
163
172
  database: sqliteDatabase,
164
173
  });
165
174
 
166
- adapter = new DrizzleAdapter({
175
+ adapter = new SqlAdapter({
167
176
  dialect,
168
177
  driverConfig: new BetterSQLite3DriverConfig(),
169
178
  });
170
179
 
171
180
  {
172
- const migrations = adapter.prepareMigrations(internalSchema, "");
181
+ const migrations = adapter.prepareMigrations(internalSchema, null);
173
182
  await migrations.executeWithDriver(adapter.driver, 0);
174
183
  }
175
184
 
176
185
  const options: FragnoPublicConfigWithDatabase = {
177
186
  databaseAdapter: adapter,
187
+ databaseNamespace: null,
178
188
  };
179
189
 
180
190
  fragment = instantiateFragment(options);
@@ -188,42 +198,44 @@ describe("Hook Service", () => {
188
198
  const nonce = "test-nonce-1";
189
199
 
190
200
  await fragment.inContext(async function () {
191
- return await this.uow(async ({ forSchema, executeMutate }) => {
192
- const uow = forSchema(internalSchema);
193
- uow.create("fragno_hooks", {
194
- namespace: "test-namespace",
195
- hookName: "onTest",
196
- payload: { test: "data" },
197
- status: "pending",
198
- attempts: 0,
199
- maxAttempts: 5,
200
- lastAttemptAt: null,
201
- nextRetryAt: null,
202
- error: null,
203
- nonce,
204
- });
205
- uow.create("fragno_hooks", {
206
- namespace: "test-namespace",
207
- hookName: "onTest",
208
- payload: { test: "already-completed-data" },
209
- status: "completed",
210
- attempts: 0,
211
- maxAttempts: 5,
212
- lastAttemptAt: null,
213
- nextRetryAt: null,
214
- error: null,
215
- nonce,
216
- });
217
- await executeMutate();
218
- });
201
+ await this.handlerTx()
202
+ .mutate(({ forSchema }) => {
203
+ const uow = forSchema(internalSchema);
204
+ uow.create("fragno_hooks", {
205
+ namespace: "test-namespace",
206
+ hookName: "onTest",
207
+ payload: { test: "data" },
208
+ status: "pending",
209
+ attempts: 0,
210
+ maxAttempts: 5,
211
+ lastAttemptAt: null,
212
+ nextRetryAt: null,
213
+ error: null,
214
+ nonce,
215
+ });
216
+ uow.create("fragno_hooks", {
217
+ namespace: "test-namespace",
218
+ hookName: "onTest",
219
+ payload: { test: "already-completed-data" },
220
+ status: "completed",
221
+ attempts: 0,
222
+ maxAttempts: 5,
223
+ lastAttemptAt: null,
224
+ nextRetryAt: null,
225
+ error: null,
226
+ nonce,
227
+ });
228
+ })
229
+ .execute();
219
230
  });
220
231
 
221
232
  const events = await fragment.inContext(async function () {
222
- return await this.uow(async ({ executeRetrieve }) => {
223
- const eventsPromise = fragment.services.hookService.getPendingHookEvents("test-namespace");
224
- await executeRetrieve();
225
- return await eventsPromise;
226
- });
233
+ return await this.handlerTx()
234
+ .withServiceCalls(
235
+ () => [fragment.services.hookService.getPendingHookEvents("test-namespace")] as const,
236
+ )
237
+ .transform(({ serviceResult: [result] }) => result)
238
+ .execute();
227
239
  });
228
240
 
229
241
  expect(events).toHaveLength(1);
@@ -232,7 +244,7 @@ describe("Hook Service", () => {
232
244
  payload: { test: "data" },
233
245
  attempts: 0,
234
246
  maxAttempts: 5,
235
- nonce,
247
+ idempotencyKey: nonce,
236
248
  });
237
249
  });
238
250
 
@@ -241,41 +253,36 @@ describe("Hook Service", () => {
241
253
  let eventId: FragnoId;
242
254
 
243
255
  await fragment.inContext(async function () {
244
- return await this.uow(async ({ forSchema, executeMutate }) => {
245
- const uow = forSchema(internalSchema);
246
- eventId = uow.create("fragno_hooks", {
247
- namespace: "test-namespace",
248
- hookName: "onComplete",
249
- payload: { test: "data" },
250
- status: "pending",
251
- attempts: 0,
252
- maxAttempts: 5,
253
- lastAttemptAt: null,
254
- nextRetryAt: null,
255
- error: null,
256
- nonce,
257
- });
258
- await executeMutate();
259
- });
256
+ await this.handlerTx()
257
+ .mutate(({ forSchema }) => {
258
+ const uow = forSchema(internalSchema);
259
+ eventId = uow.create("fragno_hooks", {
260
+ namespace: "test-namespace",
261
+ hookName: "onComplete",
262
+ payload: { test: "data" },
263
+ status: "pending",
264
+ attempts: 0,
265
+ maxAttempts: 5,
266
+ lastAttemptAt: null,
267
+ nextRetryAt: null,
268
+ error: null,
269
+ nonce,
270
+ });
271
+ })
272
+ .execute();
260
273
  });
261
274
 
262
275
  await fragment.inContext(async function () {
263
- return await this.uow(async ({ executeMutate }) => {
264
- fragment.services.hookService.markHookCompleted(eventId);
265
- await executeMutate();
266
- });
276
+ await this.handlerTx()
277
+ .withServiceCalls(() => [fragment.services.hookService.markHookCompleted(eventId)] as const)
278
+ .execute();
267
279
  });
268
280
 
269
281
  const result = await fragment.inContext(async function () {
270
- return await this.uow(async ({ forSchema, executeRetrieve }) => {
271
- const uow = forSchema(internalSchema);
272
- const findUow = uow.find("fragno_hooks", (b) =>
273
- b.whereIndex("primary", (eb) => eb("id", "=", eventId)),
274
- );
275
- await executeRetrieve();
276
- const [events] = await findUow.retrievalPhase;
277
- return events?.[0];
278
- });
282
+ return await this.handlerTx()
283
+ .withServiceCalls(() => [fragment.services.hookService.getHookById(eventId)] as const)
284
+ .transform(({ serviceResult: [event] }) => event)
285
+ .execute();
279
286
  });
280
287
 
281
288
  expect(result).toBeDefined();
@@ -283,46 +290,88 @@ describe("Hook Service", () => {
283
290
  expect(result?.lastAttemptAt).toBeInstanceOf(Date);
284
291
  });
285
292
 
293
+ it("should reject marking completed with a stale id", async () => {
294
+ const namespace = "complete-stale";
295
+ const nonce = "test-nonce-complete-stale";
296
+ let staleId!: FragnoId;
297
+
298
+ await fragment.inContext(async function () {
299
+ const createdId = await this.handlerTx()
300
+ .mutate(({ forSchema }) => {
301
+ const uow = forSchema(internalSchema);
302
+ return uow.create("fragno_hooks", {
303
+ namespace,
304
+ hookName: "onCompleteStale",
305
+ payload: { test: "data" },
306
+ status: "pending",
307
+ attempts: 0,
308
+ maxAttempts: 5,
309
+ lastAttemptAt: null,
310
+ nextRetryAt: null,
311
+ error: null,
312
+ nonce,
313
+ });
314
+ })
315
+ .execute();
316
+ staleId = createdId;
317
+ });
318
+
319
+ await fragment.inContext(async function () {
320
+ await this.handlerTx()
321
+ .withServiceCalls(
322
+ () => [fragment.services.hookService.claimPendingHookEvents(namespace)] as const,
323
+ )
324
+ .execute();
325
+ });
326
+
327
+ await expect(
328
+ fragment.inContext(async function () {
329
+ await this.handlerTx()
330
+ .withServiceCalls(
331
+ () => [fragment.services.hookService.markHookCompleted(staleId)] as const,
332
+ )
333
+ .execute();
334
+ }),
335
+ ).rejects.toThrow(ConcurrencyConflictError);
336
+ });
337
+
286
338
  it("should mark a hook event as processing", async () => {
287
339
  const nonce = "test-nonce-3";
288
340
  let eventId: FragnoId;
289
341
 
290
342
  await fragment.inContext(async function () {
291
- return await this.uow(async ({ forSchema, executeMutate }) => {
292
- const uow = forSchema(internalSchema);
293
- eventId = uow.create("fragno_hooks", {
294
- namespace: "test-namespace",
295
- hookName: "onProcess",
296
- payload: { test: "data" },
297
- status: "pending",
298
- attempts: 0,
299
- maxAttempts: 5,
300
- lastAttemptAt: null,
301
- nextRetryAt: null,
302
- error: null,
303
- nonce,
304
- });
305
- await executeMutate();
306
- });
343
+ return this.handlerTx()
344
+ .mutate(({ forSchema }) => {
345
+ const uow = forSchema(internalSchema);
346
+ eventId = uow.create("fragno_hooks", {
347
+ namespace: "test-namespace",
348
+ hookName: "onProcess",
349
+ payload: { test: "data" },
350
+ status: "pending",
351
+ attempts: 0,
352
+ maxAttempts: 5,
353
+ lastAttemptAt: null,
354
+ nextRetryAt: null,
355
+ error: null,
356
+ nonce,
357
+ });
358
+ })
359
+ .execute();
307
360
  });
308
361
 
309
362
  await fragment.inContext(async function () {
310
- return await this.uow(async ({ executeMutate }) => {
311
- fragment.services.hookService.markHookProcessing(eventId);
312
- await executeMutate();
313
- });
363
+ await this.handlerTx()
364
+ .withServiceCalls(
365
+ () => [fragment.services.hookService.markHookProcessing(eventId)] as const,
366
+ )
367
+ .execute();
314
368
  });
315
369
 
316
370
  const result = await fragment.inContext(async function () {
317
- return await this.uow(async ({ forSchema, executeRetrieve }) => {
318
- const uow = forSchema(internalSchema);
319
- const findUow = uow.find("fragno_hooks", (b) =>
320
- b.whereIndex("primary", (eb) => eb("id", "=", eventId)),
321
- );
322
- await executeRetrieve();
323
- const [events] = await findUow.retrievalPhase;
324
- return events?.[0];
325
- });
371
+ return await this.handlerTx()
372
+ .withServiceCalls(() => [fragment.services.hookService.getHookById(eventId)] as const)
373
+ .transform(({ serviceResult: [event] }) => event)
374
+ .execute();
326
375
  });
327
376
 
328
377
  expect(result).toBeDefined();
@@ -335,43 +384,44 @@ describe("Hook Service", () => {
335
384
  let eventId: FragnoId;
336
385
 
337
386
  await fragment.inContext(async function () {
338
- return await this.uow(async ({ forSchema, executeMutate }) => {
339
- const uow = forSchema(internalSchema);
340
- eventId = uow.create("fragno_hooks", {
341
- namespace: "test-namespace",
342
- hookName: "onFail",
343
- payload: { test: "data" },
344
- status: "pending",
345
- attempts: 0,
346
- maxAttempts: 5,
347
- lastAttemptAt: null,
348
- nextRetryAt: null,
349
- error: null,
350
- nonce,
351
- });
352
- await executeMutate();
353
- });
387
+ const createdId = await this.handlerTx()
388
+ .mutate(({ forSchema }) => {
389
+ const uow = forSchema(internalSchema);
390
+ return uow.create("fragno_hooks", {
391
+ namespace: "test-namespace",
392
+ hookName: "onFail",
393
+ payload: { test: "data" },
394
+ status: "pending",
395
+ attempts: 0,
396
+ maxAttempts: 5,
397
+ lastAttemptAt: null,
398
+ nextRetryAt: null,
399
+ error: null,
400
+ nonce,
401
+ });
402
+ })
403
+ .execute();
404
+ eventId = createdId;
354
405
  });
355
406
 
356
407
  const retryPolicy = new ExponentialBackoffRetryPolicy({ maxRetries: 3 });
357
408
 
358
409
  await fragment.inContext(async function () {
359
- return await this.uow(async ({ executeMutate }) => {
360
- fragment.services.hookService.markHookFailed(eventId, "Test error", 0, retryPolicy);
361
- await executeMutate();
362
- });
410
+ await this.handlerTx()
411
+ .withServiceCalls(
412
+ () =>
413
+ [
414
+ fragment.services.hookService.markHookFailed(eventId, "Test error", 0, retryPolicy),
415
+ ] as const,
416
+ )
417
+ .execute();
363
418
  });
364
419
 
365
420
  const result = await fragment.inContext(async function () {
366
- return await this.uow(async ({ forSchema, executeRetrieve }) => {
367
- const uow = forSchema(internalSchema);
368
- const findUow = uow.find("fragno_hooks", (b) =>
369
- b.whereIndex("primary", (eb) => eb("id", "=", eventId)),
370
- );
371
- await executeRetrieve();
372
- const [events] = await findUow.retrievalPhase;
373
- return events?.[0];
374
- });
421
+ return await this.handlerTx()
422
+ .withServiceCalls(() => [fragment.services.hookService.getHookById(eventId)] as const)
423
+ .transform(({ serviceResult: [event] }) => event)
424
+ .execute();
375
425
  });
376
426
 
377
427
  expect(result).toBeDefined();
@@ -387,48 +437,49 @@ describe("Hook Service", () => {
387
437
  let eventId: FragnoId;
388
438
 
389
439
  await fragment.inContext(async function () {
390
- return await this.uow(async ({ forSchema, executeMutate }) => {
391
- const uow = forSchema(internalSchema);
392
- eventId = uow.create("fragno_hooks", {
393
- namespace: "test-namespace",
394
- hookName: "onMaxFail",
395
- payload: { test: "data" },
396
- status: "pending",
397
- attempts: 0,
398
- maxAttempts: 1,
399
- lastAttemptAt: null,
400
- nextRetryAt: null,
401
- error: null,
402
- nonce,
403
- });
404
- await executeMutate();
405
- });
440
+ const createdId = await this.handlerTx()
441
+ .mutate(({ forSchema }) => {
442
+ const uow = forSchema(internalSchema);
443
+ return uow.create("fragno_hooks", {
444
+ namespace: "test-namespace",
445
+ hookName: "onMaxFail",
446
+ payload: { test: "data" },
447
+ status: "pending",
448
+ attempts: 0,
449
+ maxAttempts: 1,
450
+ lastAttemptAt: null,
451
+ nextRetryAt: null,
452
+ error: null,
453
+ nonce,
454
+ });
455
+ })
456
+ .execute();
457
+ eventId = createdId;
406
458
  });
407
459
 
408
460
  const retryPolicy = new NoRetryPolicy();
409
461
 
410
462
  await fragment.inContext(async function () {
411
- return await this.uow(async ({ executeMutate }) => {
412
- fragment.services.hookService.markHookFailed(
413
- eventId,
414
- "Max attempts reached",
415
- 0,
416
- retryPolicy,
417
- );
418
- await executeMutate();
419
- });
463
+ await this.handlerTx()
464
+ .withServiceCalls(
465
+ () =>
466
+ [
467
+ fragment.services.hookService.markHookFailed(
468
+ eventId,
469
+ "Max attempts reached",
470
+ 0,
471
+ retryPolicy,
472
+ ),
473
+ ] as const,
474
+ )
475
+ .execute();
420
476
  });
421
477
 
422
478
  const result = await fragment.inContext(async function () {
423
- return await this.uow(async ({ forSchema, executeRetrieve }) => {
424
- const uow = forSchema(internalSchema);
425
- const findUow = uow.find("fragno_hooks", (b) =>
426
- b.whereIndex("primary", (eb) => eb("id", "=", eventId)),
427
- );
428
- await executeRetrieve();
429
- const [events] = await findUow.retrievalPhase;
430
- return events?.[0];
431
- });
479
+ return await this.handlerTx()
480
+ .withServiceCalls(() => [fragment.services.hookService.getHookById(eventId)] as const)
481
+ .transform(({ serviceResult: [event] }) => event)
482
+ .execute();
432
483
  });
433
484
 
434
485
  expect(result).toBeDefined();
@@ -444,30 +495,33 @@ describe("Hook Service", () => {
444
495
  const pastTime = new Date(Date.now() - 10000);
445
496
 
446
497
  await fragment.inContext(async function () {
447
- return await this.uow(async ({ forSchema, executeMutate }) => {
448
- const uow = forSchema(internalSchema);
449
- eventId = uow.create("fragno_hooks", {
450
- namespace: "test-namespace",
451
- hookName: "onStale",
452
- payload: { test: "stale" },
453
- status: "pending",
454
- attempts: 1,
455
- maxAttempts: 5,
456
- lastAttemptAt: pastTime,
457
- nextRetryAt: pastTime,
458
- error: "Previous error",
459
- nonce,
460
- });
461
- await executeMutate();
462
- });
498
+ const createdId = await this.handlerTx()
499
+ .mutate(({ forSchema }) => {
500
+ const uow = forSchema(internalSchema);
501
+ return uow.create("fragno_hooks", {
502
+ namespace: "test-namespace",
503
+ hookName: "onStale",
504
+ payload: { test: "stale" },
505
+ status: "pending",
506
+ attempts: 1,
507
+ maxAttempts: 5,
508
+ lastAttemptAt: pastTime,
509
+ nextRetryAt: pastTime,
510
+ error: "Previous error",
511
+ nonce,
512
+ });
513
+ })
514
+ .execute();
515
+ eventId = createdId;
463
516
  });
464
517
 
465
518
  const events = await fragment.inContext(async function () {
466
- return await this.uow(async ({ executeRetrieve }) => {
467
- const eventsPromise = fragment.services.hookService.getPendingHookEvents("test-namespace");
468
- await executeRetrieve();
469
- return await eventsPromise;
470
- });
519
+ return await this.handlerTx()
520
+ .withServiceCalls(
521
+ () => [fragment.services.hookService.getPendingHookEvents("test-namespace")] as const,
522
+ )
523
+ .transform(({ serviceResult: [result] }) => result)
524
+ .execute();
471
525
  });
472
526
 
473
527
  const staleEvent = events.find((e) => e.id.externalId === eventId.externalId);
@@ -476,34 +530,79 @@ describe("Hook Service", () => {
476
530
  expect(staleEvent?.attempts).toBe(1);
477
531
  });
478
532
 
533
+ it("should detect conflicts when requeueing after another update in the same transaction", async () => {
534
+ const namespace = "requeue-conflict";
535
+ const nonce = "test-nonce-requeue-conflict";
536
+ let eventId!: FragnoId;
537
+
538
+ const staleBefore = new Date(Date.now() - 60_000);
539
+ const lastAttemptAt = new Date(Date.now() - 120_000);
540
+
541
+ await fragment.inContext(async function () {
542
+ eventId = await this.handlerTx()
543
+ .mutate(({ forSchema }) => {
544
+ const uow = forSchema(internalSchema);
545
+ return uow.create("fragno_hooks", {
546
+ namespace,
547
+ hookName: "onRequeueConflict",
548
+ payload: { test: "requeue" },
549
+ status: "processing",
550
+ attempts: 0,
551
+ maxAttempts: 5,
552
+ lastAttemptAt,
553
+ nextRetryAt: null,
554
+ error: null,
555
+ nonce,
556
+ });
557
+ })
558
+ .execute();
559
+ });
560
+
561
+ await expect(
562
+ fragment.inContext(async function () {
563
+ await this.handlerTx({ retryPolicy: new NoRetryPolicy() })
564
+ .withServiceCalls(
565
+ () =>
566
+ [
567
+ fragment.services.hookService.markHookProcessing(eventId),
568
+ fragment.services.hookService.requeueStuckProcessingHooks(namespace, staleBefore),
569
+ ] as const,
570
+ )
571
+ .execute();
572
+ }),
573
+ ).rejects.toThrow(ConcurrencyConflictError);
574
+ });
575
+
479
576
  it("should not retrieve events from different namespace", async () => {
480
577
  const nonce = "test-nonce-7";
481
578
 
482
579
  await fragment.inContext(async function () {
483
- return await this.uow(async ({ forSchema, executeMutate }) => {
484
- const uow = forSchema(internalSchema);
485
- uow.create("fragno_hooks", {
486
- namespace: "other-namespace",
487
- hookName: "onOther",
488
- payload: { test: "other" },
489
- status: "pending",
490
- attempts: 0,
491
- maxAttempts: 5,
492
- lastAttemptAt: null,
493
- nextRetryAt: null,
494
- error: null,
495
- nonce,
496
- });
497
- await executeMutate();
498
- });
580
+ await this.handlerTx()
581
+ .mutate(({ forSchema }) => {
582
+ const uow = forSchema(internalSchema);
583
+ uow.create("fragno_hooks", {
584
+ namespace: "other-namespace",
585
+ hookName: "onOther",
586
+ payload: { test: "other" },
587
+ status: "pending",
588
+ attempts: 0,
589
+ maxAttempts: 5,
590
+ lastAttemptAt: null,
591
+ nextRetryAt: null,
592
+ error: null,
593
+ nonce,
594
+ });
595
+ })
596
+ .execute();
499
597
  });
500
598
 
501
599
  const events = await fragment.inContext(async function () {
502
- return await this.uow(async ({ executeRetrieve }) => {
503
- const eventsPromise = fragment.services.hookService.getPendingHookEvents("test-namespace");
504
- await executeRetrieve();
505
- return await eventsPromise;
506
- });
600
+ return await this.handlerTx()
601
+ .withServiceCalls(
602
+ () => [fragment.services.hookService.getPendingHookEvents("test-namespace")] as const,
603
+ )
604
+ .transform(({ serviceResult: [result] }) => result)
605
+ .execute();
507
606
  });
508
607
 
509
608
  const otherEvent = events.find((e) => e.hookName === "onOther");
@@ -517,33 +616,390 @@ describe("Hook Service", () => {
517
616
  const futureTime = new Date(Date.now() + 60000);
518
617
 
519
618
  await fragment.inContext(async function () {
520
- return await this.uow(async ({ forSchema, executeMutate }) => {
521
- const uow = forSchema(internalSchema);
522
- eventId = uow.create("fragno_hooks", {
523
- namespace: "test-namespace",
524
- hookName: "onFuture",
525
- payload: { test: "future" },
526
- status: "pending",
527
- attempts: 1,
528
- maxAttempts: 5,
529
- lastAttemptAt: new Date(),
530
- nextRetryAt: futureTime,
531
- error: "Previous error",
532
- nonce,
533
- });
534
- await executeMutate();
535
- });
619
+ const createdId = await this.handlerTx()
620
+ .mutate(({ forSchema }) => {
621
+ const uow = forSchema(internalSchema);
622
+ return uow.create("fragno_hooks", {
623
+ namespace: "test-namespace",
624
+ hookName: "onFuture",
625
+ payload: { test: "future" },
626
+ status: "pending",
627
+ attempts: 1,
628
+ maxAttempts: 5,
629
+ lastAttemptAt: new Date(),
630
+ nextRetryAt: futureTime,
631
+ error: "Previous error",
632
+ nonce,
633
+ });
634
+ })
635
+ .execute();
636
+ eventId = createdId;
536
637
  });
537
638
 
538
639
  const events = await fragment.inContext(async function () {
539
- return await this.uow(async ({ executeRetrieve }) => {
540
- const eventsPromise = fragment.services.hookService.getPendingHookEvents("test-namespace");
541
- await executeRetrieve();
542
- return await eventsPromise;
543
- });
640
+ return await this.handlerTx()
641
+ .withServiceCalls(
642
+ () => [fragment.services.hookService.getPendingHookEvents("test-namespace")] as const,
643
+ )
644
+ .transform(({ serviceResult: [result] }) => result)
645
+ .execute();
544
646
  });
545
647
 
546
648
  const futureEvent = events.find((e) => e.id.externalId === eventId.externalId);
547
649
  expect(futureEvent).toBeUndefined();
548
650
  });
651
+
652
+ it("should claim only ready pending events and mark them processing", async () => {
653
+ const namespace = "claim-ready";
654
+ const pastTime = new Date(Date.now() - 10000);
655
+ const futureTime = new Date(Date.now() + 60000);
656
+
657
+ let nullRetryId!: FragnoId;
658
+ let pastRetryId!: FragnoId;
659
+ let futureRetryId!: FragnoId;
660
+
661
+ await fragment.inContext(async function () {
662
+ await this.handlerTx()
663
+ .mutate(({ forSchema }) => {
664
+ const uow = forSchema(internalSchema);
665
+ nullRetryId = uow.create("fragno_hooks", {
666
+ namespace,
667
+ hookName: "onNullRetry",
668
+ payload: { test: "null" },
669
+ status: "pending",
670
+ attempts: 0,
671
+ maxAttempts: 5,
672
+ lastAttemptAt: null,
673
+ nextRetryAt: null,
674
+ error: null,
675
+ nonce: "test-nonce-claim-null",
676
+ });
677
+ pastRetryId = uow.create("fragno_hooks", {
678
+ namespace,
679
+ hookName: "onPastRetry",
680
+ payload: { test: "past" },
681
+ status: "pending",
682
+ attempts: 1,
683
+ maxAttempts: 5,
684
+ lastAttemptAt: pastTime,
685
+ nextRetryAt: pastTime,
686
+ error: "Previous error",
687
+ nonce: "test-nonce-claim-past",
688
+ });
689
+ futureRetryId = uow.create("fragno_hooks", {
690
+ namespace,
691
+ hookName: "onFutureRetry",
692
+ payload: { test: "future" },
693
+ status: "pending",
694
+ attempts: 1,
695
+ maxAttempts: 5,
696
+ lastAttemptAt: new Date(),
697
+ nextRetryAt: futureTime,
698
+ error: "Previous error",
699
+ nonce: "test-nonce-claim-future",
700
+ });
701
+ })
702
+ .execute();
703
+ });
704
+
705
+ const claimed = await fragment.inContext(async function () {
706
+ return await this.handlerTx()
707
+ .withServiceCalls(
708
+ () => [fragment.services.hookService.claimPendingHookEvents(namespace)] as const,
709
+ )
710
+ .transform(({ serviceResult: [result] }) => result)
711
+ .execute();
712
+ });
713
+
714
+ expect(claimed).toHaveLength(2);
715
+ const claimedIds = new Set(claimed.map((event) => event.id.externalId));
716
+ expect(claimedIds.has(nullRetryId.externalId)).toBe(true);
717
+ expect(claimedIds.has(pastRetryId.externalId)).toBe(true);
718
+ expect(claimedIds.has(futureRetryId.externalId)).toBe(false);
719
+
720
+ const [nullEvent, pastEvent, futureEvent] = await fragment.inContext(async function () {
721
+ return await this.handlerTx()
722
+ .withServiceCalls(
723
+ () =>
724
+ [
725
+ fragment.services.hookService.getHookById(nullRetryId),
726
+ fragment.services.hookService.getHookById(pastRetryId),
727
+ fragment.services.hookService.getHookById(futureRetryId),
728
+ ] as const,
729
+ )
730
+ .transform(({ serviceResult: [nullResult, pastResult, futureResult] }) => [
731
+ nullResult,
732
+ pastResult,
733
+ futureResult,
734
+ ])
735
+ .execute();
736
+ });
737
+
738
+ expect(nullEvent?.status).toBe("processing");
739
+ expect(nullEvent?.lastAttemptAt).toBeInstanceOf(Date);
740
+ expect(pastEvent?.status).toBe("processing");
741
+ expect(pastEvent?.lastAttemptAt).toBeInstanceOf(Date);
742
+ expect(futureEvent?.status).toBe("pending");
743
+ });
744
+
745
+ it("should return claimed ids with incremented versions", async () => {
746
+ const namespace = "claim-version";
747
+ const nonce = "test-nonce-claim-version";
748
+ let createdId!: FragnoId;
749
+
750
+ await fragment.inContext(async function () {
751
+ createdId = await this.handlerTx()
752
+ .mutate(({ forSchema }) => {
753
+ const uow = forSchema(internalSchema);
754
+ return uow.create("fragno_hooks", {
755
+ namespace,
756
+ hookName: "onClaimVersion",
757
+ payload: { test: "version" },
758
+ status: "pending",
759
+ attempts: 0,
760
+ maxAttempts: 5,
761
+ lastAttemptAt: null,
762
+ nextRetryAt: null,
763
+ error: null,
764
+ nonce,
765
+ });
766
+ })
767
+ .execute();
768
+ });
769
+
770
+ const claimed = await fragment.inContext(async function () {
771
+ return await this.handlerTx()
772
+ .withServiceCalls(
773
+ () => [fragment.services.hookService.claimPendingHookEvents(namespace)] as const,
774
+ )
775
+ .transform(({ serviceResult: [result] }) => result)
776
+ .execute();
777
+ });
778
+
779
+ expect(claimed).toHaveLength(1);
780
+ expect(claimed[0]?.id.externalId).toBe(createdId.externalId);
781
+ expect(claimed[0]?.id.version).toBe(createdId.version + 1);
782
+ });
783
+
784
+ it("should return now when pending hooks have no nextRetryAt", async () => {
785
+ const namespace = "wake-now";
786
+
787
+ await fragment.inContext(async function () {
788
+ await this.handlerTx()
789
+ .mutate(({ forSchema }) => {
790
+ const uow = forSchema(internalSchema);
791
+ uow.create("fragno_hooks", {
792
+ namespace,
793
+ hookName: "onImmediate",
794
+ payload: { test: "now" },
795
+ status: "pending",
796
+ attempts: 0,
797
+ maxAttempts: 5,
798
+ lastAttemptAt: null,
799
+ nextRetryAt: null,
800
+ error: null,
801
+ nonce: "test-nonce-now",
802
+ });
803
+ })
804
+ .execute();
805
+ });
806
+
807
+ const wakeAt = await fragment.inContext(async function () {
808
+ return await this.handlerTx()
809
+ .withServiceCalls(
810
+ () => [fragment.services.hookService.getNextHookWakeAt(namespace)] as const,
811
+ )
812
+ .transform(({ serviceResult: [result] }) => result)
813
+ .execute();
814
+ });
815
+
816
+ expect(wakeAt).toBeInstanceOf(Date);
817
+ expect(Math.abs((wakeAt as Date).getTime() - Date.now())).toBeLessThan(5000);
818
+ });
819
+
820
+ it("should return earliest scheduled hook time", async () => {
821
+ const namespace = "wake-future";
822
+ const soon = new Date(Date.now() + 10000);
823
+ const later = new Date(Date.now() + 60000);
824
+
825
+ await fragment.inContext(async function () {
826
+ await this.handlerTx()
827
+ .mutate(({ forSchema }) => {
828
+ const uow = forSchema(internalSchema);
829
+ uow.create("fragno_hooks", {
830
+ namespace,
831
+ hookName: "onSoon",
832
+ payload: { test: "soon" },
833
+ status: "pending",
834
+ attempts: 0,
835
+ maxAttempts: 5,
836
+ lastAttemptAt: null,
837
+ nextRetryAt: soon,
838
+ error: null,
839
+ nonce: "test-nonce-soon",
840
+ });
841
+ uow.create("fragno_hooks", {
842
+ namespace,
843
+ hookName: "onLater",
844
+ payload: { test: "later" },
845
+ status: "pending",
846
+ attempts: 0,
847
+ maxAttempts: 5,
848
+ lastAttemptAt: null,
849
+ nextRetryAt: later,
850
+ error: null,
851
+ nonce: "test-nonce-later",
852
+ });
853
+ })
854
+ .execute();
855
+ });
856
+
857
+ const wakeAt = await fragment.inContext(async function () {
858
+ return await this.handlerTx()
859
+ .withServiceCalls(
860
+ () => [fragment.services.hookService.getNextHookWakeAt(namespace)] as const,
861
+ )
862
+ .transform(({ serviceResult: [result] }) => result)
863
+ .execute();
864
+ });
865
+
866
+ expect(wakeAt).toEqual(soon);
867
+ });
868
+
869
+ it("should return null when no pending hooks exist", async () => {
870
+ const namespace = "wake-none";
871
+ const wakeAt = await fragment.inContext(async function () {
872
+ return await this.handlerTx()
873
+ .withServiceCalls(
874
+ () => [fragment.services.hookService.getNextHookWakeAt(namespace)] as const,
875
+ )
876
+ .transform(({ serviceResult: [result] }) => result)
877
+ .execute();
878
+ });
879
+
880
+ expect(wakeAt).toBeNull();
881
+ });
882
+ });
883
+
884
+ describe("getSchemaVersionFromDatabase", () => {
885
+ function createTestSetup() {
886
+ const sqliteDatabase = new SQLite(":memory:");
887
+ const dialect = new SqliteDialect({ database: sqliteDatabase });
888
+ const adapter = new SqlAdapter({
889
+ dialect,
890
+ driverConfig: new BetterSQLite3DriverConfig(),
891
+ });
892
+
893
+ function instantiateFragment(options: FragnoPublicConfigWithDatabase) {
894
+ return instantiate(internalFragmentDef).withConfig({}).withOptions(options).build();
895
+ }
896
+
897
+ return { sqliteDatabase, adapter, instantiateFragment };
898
+ }
899
+
900
+ async function setupAndMigrate() {
901
+ const { sqliteDatabase, adapter, instantiateFragment } = createTestSetup();
902
+ // Create tables without writing a version record, so tests control version state
903
+ const migrations = adapter.prepareMigrations(internalSchema, "");
904
+ await migrations.executeWithDriver(adapter.driver, 0, undefined, {
905
+ updateVersionInMigration: false,
906
+ });
907
+ const fragment = instantiateFragment({
908
+ databaseAdapter: adapter,
909
+ databaseNamespace: null,
910
+ });
911
+ return { sqliteDatabase, adapter, fragment };
912
+ }
913
+
914
+ it("should return 0 when no version exists", async () => {
915
+ const { fragment } = await setupAndMigrate();
916
+
917
+ const version = await getSchemaVersionFromDatabase(fragment, "nonexistent");
918
+ expect(version).toBe(0);
919
+ });
920
+
921
+ it("should find version stored under empty-string namespace", async () => {
922
+ const { fragment } = await setupAndMigrate();
923
+
924
+ // Write version under empty-string namespace (key = ".schema_version")
925
+ await fragment.inContext(async function () {
926
+ await this.handlerTx()
927
+ .withServiceCalls(() => [fragment.services.settingsService.set("", "schema_version", "5")])
928
+ .execute();
929
+ });
930
+
931
+ const version = await getSchemaVersionFromDatabase(fragment, "");
932
+ expect(version).toBe(5);
933
+ });
934
+
935
+ it("should find version via back-compat when stored under internalSchema.name but read with empty string", async () => {
936
+ const { fragment } = await setupAndMigrate();
937
+
938
+ // Write version under "fragno_internal" namespace (legacy key from buggy code)
939
+ await fragment.inContext(async function () {
940
+ await this.handlerTx()
941
+ .withServiceCalls(() => [
942
+ fragment.services.settingsService.set(internalSchema.name, "schema_version", "3"),
943
+ ])
944
+ .execute();
945
+ });
946
+
947
+ // Reading with "" should find it via back-compat fallback
948
+ const version = await getSchemaVersionFromDatabase(fragment, "");
949
+ expect(version).toBe(3);
950
+ });
951
+
952
+ it("should find version via back-compat when stored under empty string but read with internalSchema.name", async () => {
953
+ const { fragment } = await setupAndMigrate();
954
+
955
+ // Write version under empty-string namespace
956
+ await fragment.inContext(async function () {
957
+ await this.handlerTx()
958
+ .withServiceCalls(() => [fragment.services.settingsService.set("", "schema_version", "7")])
959
+ .execute();
960
+ });
961
+
962
+ // Reading with internalSchema.name should find it via back-compat fallback
963
+ const version = await getSchemaVersionFromDatabase(fragment, internalSchema.name);
964
+ expect(version).toBe(7);
965
+ });
966
+
967
+ it("should prefer primary namespace over back-compat fallback", async () => {
968
+ const { fragment } = await setupAndMigrate();
969
+
970
+ // Write version under BOTH namespaces with different values
971
+ await fragment.inContext(async function () {
972
+ await this.handlerTx()
973
+ .withServiceCalls(() => [
974
+ fragment.services.settingsService.set("", "schema_version", "10"),
975
+ fragment.services.settingsService.set(internalSchema.name, "schema_version", "20"),
976
+ ])
977
+ .execute();
978
+ });
979
+
980
+ // Reading with "" should find 10 (primary), not 20 (back-compat)
981
+ const versionEmpty = await getSchemaVersionFromDatabase(fragment, "");
982
+ expect(versionEmpty).toBe(10);
983
+
984
+ // Reading with internalSchema.name should find 20 (primary), not 10 (back-compat)
985
+ const versionNamed = await getSchemaVersionFromDatabase(fragment, internalSchema.name);
986
+ expect(versionNamed).toBe(20);
987
+ });
988
+
989
+ it("should not use back-compat for non-internal namespaces", async () => {
990
+ const { fragment } = await setupAndMigrate();
991
+
992
+ // Write version under "some-fragment"
993
+ await fragment.inContext(async function () {
994
+ await this.handlerTx()
995
+ .withServiceCalls(() => [
996
+ fragment.services.settingsService.set("some-fragment", "schema_version", "4"),
997
+ ])
998
+ .execute();
999
+ });
1000
+
1001
+ // Reading with a different non-internal namespace should NOT find it
1002
+ const version = await getSchemaVersionFromDatabase(fragment, "other-fragment");
1003
+ expect(version).toBe(0);
1004
+ });
549
1005
  });