@fragno-dev/db 0.2.2 → 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 (355) hide show
  1. package/.turbo/turbo-build.log +202 -140
  2. package/CHANGELOG.md +35 -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 +39 -29
  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 +18 -7
  80. package/dist/db-fragment-definition-builder.d.ts.map +1 -1
  81. package/dist/db-fragment-definition-builder.js +116 -54
  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 +79 -2
  92. package/dist/fragments/internal-fragment.d.ts.map +1 -1
  93. package/dist/fragments/internal-fragment.js +150 -32
  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 +42 -1
  106. package/dist/hooks/hooks.d.ts.map +1 -1
  107. package/dist/hooks/hooks.js +72 -6
  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 +15 -8
  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.map +1 -1
  165. package/dist/query/unit-of-work/execute-unit-of-work.js +11 -6
  166. package/dist/query/unit-of-work/execute-unit-of-work.js.map +1 -1
  167. package/dist/query/unit-of-work/unit-of-work.d.ts +50 -14
  168. package/dist/query/unit-of-work/unit-of-work.d.ts.map +1 -1
  169. package/dist/query/unit-of-work/unit-of-work.js +86 -5
  170. package/dist/query/unit-of-work/unit-of-work.js.map +1 -1
  171. package/dist/query/value-decoding.js +9 -6
  172. package/dist/query/value-decoding.js.map +1 -1
  173. package/dist/query/value-encoding.js +29 -9
  174. package/dist/query/value-encoding.js.map +1 -1
  175. package/dist/schema/create.d.ts +38 -14
  176. package/dist/schema/create.d.ts.map +1 -1
  177. package/dist/schema/create.js +81 -42
  178. package/dist/schema/create.js.map +1 -1
  179. package/dist/schema/generate-id.js +2 -2
  180. package/dist/schema/generate-id.js.map +1 -1
  181. package/dist/schema/type-conversion/create-sql-type-mapper.js +3 -2
  182. package/dist/schema/type-conversion/create-sql-type-mapper.js.map +1 -1
  183. package/dist/schema/type-conversion/dialect/sqlite.js +9 -0
  184. package/dist/schema/type-conversion/dialect/sqlite.js.map +1 -1
  185. package/dist/schema/validator.d.ts +10 -0
  186. package/dist/schema/validator.d.ts.map +1 -0
  187. package/dist/schema/validator.js +123 -0
  188. package/dist/schema/validator.js.map +1 -0
  189. package/dist/schema-output/drizzle.d.ts +30 -0
  190. package/dist/schema-output/drizzle.d.ts.map +1 -0
  191. package/dist/{adapters/drizzle/generate.js → schema-output/drizzle.js} +82 -56
  192. package/dist/schema-output/drizzle.js.map +1 -0
  193. package/dist/schema-output/prisma.d.ts +17 -0
  194. package/dist/schema-output/prisma.d.ts.map +1 -0
  195. package/dist/schema-output/prisma.js +296 -0
  196. package/dist/schema-output/prisma.js.map +1 -0
  197. package/dist/util/default-database-adapter.js +61 -0
  198. package/dist/util/default-database-adapter.js.map +1 -0
  199. package/dist/with-database.d.ts +1 -1
  200. package/dist/with-database.d.ts.map +1 -1
  201. package/dist/with-database.js +12 -3
  202. package/dist/with-database.js.map +1 -1
  203. package/package.json +43 -28
  204. package/src/adapters/adapters.ts +30 -24
  205. package/src/adapters/drizzle/migrate-drizzle.test.ts +54 -33
  206. package/src/adapters/drizzle/migration-parity-drizzle-kit.test.ts +599 -0
  207. package/src/adapters/drizzle/test-utils.ts +12 -8
  208. package/src/adapters/generic-sql/driver-config.ts +38 -0
  209. package/src/adapters/generic-sql/generic-sql-adapter.test.ts +5 -5
  210. package/src/adapters/generic-sql/generic-sql-adapter.ts +110 -24
  211. package/src/adapters/generic-sql/generic-sql-uow-executor.test.ts +54 -0
  212. package/src/adapters/generic-sql/generic-sql-uow-executor.ts +231 -3
  213. package/src/adapters/generic-sql/migration/adapter-migration-parity.test.ts +118 -0
  214. package/src/adapters/generic-sql/migration/dialect/mysql.test.ts +26 -8
  215. package/src/adapters/generic-sql/migration/dialect/mysql.ts +46 -8
  216. package/src/adapters/generic-sql/migration/dialect/postgres.test.ts +25 -7
  217. package/src/adapters/generic-sql/migration/dialect/postgres.ts +8 -4
  218. package/src/adapters/generic-sql/migration/dialect/sqlite.test.ts +47 -8
  219. package/src/adapters/generic-sql/migration/dialect/sqlite.ts +27 -12
  220. package/src/adapters/generic-sql/migration/prepared-migrations.test.ts +128 -39
  221. package/src/adapters/generic-sql/migration/prepared-migrations.ts +15 -8
  222. package/src/adapters/generic-sql/migration/sql-generator.ts +142 -65
  223. package/src/adapters/generic-sql/query/create-sql-query-compiler.ts +9 -6
  224. package/src/adapters/generic-sql/query/cursor-utils.test.ts +271 -0
  225. package/src/adapters/generic-sql/query/cursor-utils.ts +41 -6
  226. package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.test.ts +27 -27
  227. package/src/adapters/generic-sql/query/generic-sql-uow-operation-compiler.ts +38 -24
  228. package/src/adapters/generic-sql/query/select-builder.test.ts +15 -11
  229. package/src/adapters/generic-sql/query/select-builder.ts +6 -2
  230. package/src/adapters/generic-sql/query/sql-query-compiler.test.ts +52 -2
  231. package/src/adapters/generic-sql/query/sql-query-compiler.ts +50 -15
  232. package/src/adapters/generic-sql/query/where-builder.test.ts +91 -17
  233. package/src/adapters/generic-sql/query/where-builder.ts +90 -38
  234. package/src/adapters/{kysely/kysely-adapter-pglite.test.ts → generic-sql/sql-adapter-pglite-migrations.test.ts} +6 -6
  235. package/src/adapters/generic-sql/sql-adapter-pglite-pagination.test.ts +806 -0
  236. package/src/adapters/{drizzle/drizzle-adapter-pglite.test.ts → generic-sql/sql-adapter-pglite-queries.test.ts} +11 -11
  237. package/src/adapters/generic-sql/{test/generic-drizzle-adapter-sqlite3.test.ts → sql-adapter-sqlite3-driver.test.ts} +10 -10
  238. package/src/adapters/{drizzle/drizzle-adapter-sqlite3.test.ts → generic-sql/sql-adapter-sqlite3-uow.test.ts} +7 -7
  239. package/src/adapters/{kysely/kysely-adapter-sqlocal.test.ts → generic-sql/sql-adapter-sqlocal.test.ts} +6 -6
  240. package/src/adapters/generic-sql/sqlite-storage.ts +20 -0
  241. package/src/adapters/generic-sql/uow-decoder.test.ts +1 -1
  242. package/src/adapters/generic-sql/uow-decoder.ts +21 -3
  243. package/src/adapters/generic-sql/uow-encoder.test.ts +33 -2
  244. package/src/adapters/generic-sql/uow-encoder.ts +50 -11
  245. package/src/adapters/in-memory/condition-evaluator.test.ts +193 -0
  246. package/src/adapters/in-memory/condition-evaluator.ts +275 -0
  247. package/src/adapters/in-memory/errors.ts +20 -0
  248. package/src/adapters/in-memory/in-memory-adapter.ts +277 -0
  249. package/src/adapters/in-memory/in-memory-uow.mutations.test.ts +296 -0
  250. package/src/adapters/in-memory/in-memory-uow.retrieval.test.ts +100 -0
  251. package/src/adapters/in-memory/in-memory-uow.ts +1348 -0
  252. package/src/adapters/in-memory/index.ts +3 -0
  253. package/src/adapters/in-memory/options.test.ts +41 -0
  254. package/src/adapters/in-memory/options.ts +87 -0
  255. package/src/adapters/in-memory/reference-resolution.test.ts +50 -0
  256. package/src/adapters/in-memory/reference-resolution.ts +67 -0
  257. package/src/adapters/in-memory/sorted-array-index.test.ts +123 -0
  258. package/src/adapters/in-memory/sorted-array-index.ts +228 -0
  259. package/src/adapters/in-memory/store.test.ts +68 -0
  260. package/src/adapters/in-memory/store.ts +145 -0
  261. package/src/adapters/in-memory/value-comparison.ts +53 -0
  262. package/src/adapters/in-memory/value-normalization.test.ts +57 -0
  263. package/src/adapters/prisma/prisma-adapter-sqlite3.test.ts +1163 -0
  264. package/src/adapters/shared/from-unit-of-work-compiler.ts +3 -1
  265. package/src/adapters/shared/uow-operation-compiler.ts +26 -16
  266. package/src/adapters/sql/index.ts +12 -0
  267. package/src/db-fragment-definition-builder.test.ts +30 -12
  268. package/src/db-fragment-definition-builder.ts +142 -73
  269. package/src/db-fragment-instantiator.test.ts +105 -13
  270. package/src/db-fragment-integration.test.ts +9 -7
  271. package/src/dispatchers/cloudflare-do/index.test.ts +73 -0
  272. package/src/dispatchers/cloudflare-do/index.ts +104 -0
  273. package/src/dispatchers/node/index.test.ts +91 -0
  274. package/src/dispatchers/node/index.ts +87 -0
  275. package/src/fragments/internal-fragment.routes.ts +42 -0
  276. package/src/fragments/internal-fragment.schema.ts +51 -0
  277. package/src/fragments/internal-fragment.test.ts +458 -8
  278. package/src/fragments/internal-fragment.ts +322 -63
  279. package/src/hooks/durable-hooks-processor.test.ts +117 -0
  280. package/src/hooks/durable-hooks-processor.ts +67 -0
  281. package/src/hooks/hooks.test.ts +165 -5
  282. package/src/hooks/hooks.ts +197 -9
  283. package/src/migration-engine/auto-from-schema.test.ts +14 -14
  284. package/src/migration-engine/auto-from-schema.ts +5 -2
  285. package/src/migration-engine/create.test.ts +2 -2
  286. package/src/migration-engine/generation-engine.test.ts +229 -104
  287. package/src/migration-engine/generation-engine.ts +94 -64
  288. package/src/migration-engine/shared.ts +1 -0
  289. package/src/mod.ts +64 -26
  290. package/src/naming/sql-naming.ts +180 -0
  291. package/src/outbox/outbox-builder.ts +241 -0
  292. package/src/outbox/outbox.test.ts +253 -0
  293. package/src/outbox/outbox.ts +137 -0
  294. package/src/query/column-defaults.ts +41 -3
  295. package/src/query/condition-builder.test.ts +3 -3
  296. package/src/query/cursor.test.ts +116 -18
  297. package/src/query/cursor.ts +75 -26
  298. package/src/query/db-now.ts +6 -0
  299. package/src/query/query-type.test.ts +2 -2
  300. package/src/query/serialize/create-sql-serializer.ts +7 -2
  301. package/src/query/serialize/dialect/mysql-serializer.ts +12 -4
  302. package/src/query/serialize/dialect/postgres-serializer.ts +34 -4
  303. package/src/query/serialize/dialect/sqlite-serializer.test.ts +51 -1
  304. package/src/query/serialize/dialect/sqlite-serializer.ts +92 -9
  305. package/src/query/serialize/sql-serializer.ts +4 -4
  306. package/src/query/simple-query-interface.ts +5 -0
  307. package/src/query/unit-of-work/execute-unit-of-work.test.ts +25 -1
  308. package/src/query/unit-of-work/execute-unit-of-work.ts +25 -8
  309. package/src/query/unit-of-work/unit-of-work-coordinator.test.ts +12 -12
  310. package/src/query/unit-of-work/unit-of-work-types.test.ts +1 -1
  311. package/src/query/unit-of-work/unit-of-work.test.ts +168 -37
  312. package/src/query/unit-of-work/unit-of-work.ts +203 -18
  313. package/src/query/value-decoding.test.ts +13 -2
  314. package/src/query/value-decoding.ts +17 -4
  315. package/src/query/value-encoding.test.ts +85 -2
  316. package/src/query/value-encoding.ts +56 -6
  317. package/src/schema/create.test.ts +129 -42
  318. package/src/schema/create.ts +185 -47
  319. package/src/schema/generate-id.test.ts +2 -2
  320. package/src/schema/generate-id.ts +2 -2
  321. package/src/schema/serialize.test.ts +14 -2
  322. package/src/schema/type-conversion/create-sql-type-mapper.ts +7 -2
  323. package/src/schema/type-conversion/dialect/sqlite.ts +18 -0
  324. package/src/schema/type-conversion/type-mapping.test.ts +25 -1
  325. package/src/schema/validator.test.ts +197 -0
  326. package/src/schema/validator.ts +231 -0
  327. package/src/{adapters/drizzle/generate.test.ts → schema-output/drizzle.test.ts} +179 -129
  328. package/src/{adapters/drizzle/generate.ts → schema-output/drizzle.ts} +143 -93
  329. package/src/schema-output/prisma.test.ts +536 -0
  330. package/src/schema-output/prisma.ts +573 -0
  331. package/src/util/default-database-adapter.ts +106 -0
  332. package/src/with-database.ts +22 -3
  333. package/tsdown.config.ts +6 -4
  334. package/dist/adapters/drizzle/drizzle-adapter.d.ts +0 -20
  335. package/dist/adapters/drizzle/drizzle-adapter.d.ts.map +0 -1
  336. package/dist/adapters/drizzle/drizzle-adapter.js +0 -27
  337. package/dist/adapters/drizzle/drizzle-adapter.js.map +0 -1
  338. package/dist/adapters/drizzle/generate.d.ts +0 -30
  339. package/dist/adapters/drizzle/generate.d.ts.map +0 -1
  340. package/dist/adapters/drizzle/generate.js.map +0 -1
  341. package/dist/adapters/kysely/kysely-adapter.d.ts +0 -19
  342. package/dist/adapters/kysely/kysely-adapter.d.ts.map +0 -1
  343. package/dist/adapters/kysely/kysely-adapter.js +0 -17
  344. package/dist/adapters/kysely/kysely-adapter.js.map +0 -1
  345. package/dist/adapters/shared/table-name-mapper.d.ts +0 -12
  346. package/dist/adapters/shared/table-name-mapper.d.ts.map +0 -1
  347. package/dist/adapters/shared/table-name-mapper.js +0 -43
  348. package/dist/adapters/shared/table-name-mapper.js.map +0 -1
  349. package/dist/node_modules/.pnpm/rou3@0.7.10/node_modules/rou3/dist/index.js.map +0 -1
  350. package/dist/schema-generator/schema-generator.d.ts +0 -15
  351. package/dist/schema-generator/schema-generator.d.ts.map +0 -1
  352. package/src/adapters/drizzle/drizzle-adapter.ts +0 -39
  353. package/src/adapters/kysely/kysely-adapter.ts +0 -27
  354. package/src/adapters/shared/table-name-mapper.ts +0 -50
  355. package/src/schema-generator/schema-generator.ts +0 -12
@@ -8,45 +8,20 @@ import {
8
8
  type FragnoPublicConfigWithDatabase,
9
9
  type ImplicitDatabaseDependencies,
10
10
  } from "../db-fragment-definition-builder";
11
- import type { FragnoId } from "../schema/create";
12
- import { schema, idColumn, column } from "../schema/create";
11
+ import { FragnoId } from "../schema/create";
13
12
  import type { RetryPolicy } from "../query/unit-of-work/retry-policy";
13
+ import { dbNow } from "../query/db-now";
14
+ import {
15
+ internalSchema,
16
+ SETTINGS_NAMESPACE,
17
+ SETTINGS_TABLE_NAME,
18
+ } from "./internal-fragment.schema";
14
19
 
15
- // Constants for Fragno's internal settings table
16
- export const SETTINGS_TABLE_NAME = "fragno_db_settings" as const;
17
- // FIXME: In some places we simply use empty string "" as namespace, which is not correct.
18
- export const SETTINGS_NAMESPACE = "fragno-db-settings" as const;
19
-
20
- export const internalSchema = schema((s) => {
21
- return s
22
- .addTable(SETTINGS_TABLE_NAME, (t) => {
23
- return t
24
- .addColumn("id", idColumn())
25
- .addColumn("key", column("string"))
26
- .addColumn("value", column("string"))
27
- .createIndex("unique_key", ["key"], { unique: true });
28
- })
29
- .addTable("fragno_hooks", (t) => {
30
- return t
31
- .addColumn("id", idColumn())
32
- .addColumn("namespace", column("string"))
33
- .addColumn("hookName", column("string"))
34
- .addColumn("payload", column("json"))
35
- .addColumn("status", column("string")) // "pending" | "processing" | "completed" | "failed"
36
- .addColumn("attempts", column("integer").defaultTo(0))
37
- .addColumn("maxAttempts", column("integer").defaultTo(5))
38
- .addColumn("lastAttemptAt", column("timestamp").nullable())
39
- .addColumn("nextRetryAt", column("timestamp").nullable())
40
- .addColumn("error", column("string").nullable())
41
- .addColumn(
42
- "createdAt",
43
- column("timestamp").defaultTo((b) => b.now()),
44
- )
45
- .addColumn("nonce", column("string"))
46
- .createIndex("idx_namespace_status_retry", ["namespace", "status", "nextRetryAt"])
47
- .createIndex("idx_nonce", ["nonce"]);
48
- });
49
- });
20
+ export {
21
+ internalSchema,
22
+ SETTINGS_NAMESPACE,
23
+ SETTINGS_TABLE_NAME,
24
+ } from "./internal-fragment.schema";
50
25
 
51
26
  // This uses DatabaseFragmentDefinitionBuilder directly
52
27
  // to avoid circular dependency (it doesn't need to link to itself)
@@ -64,8 +39,15 @@ export const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(
64
39
  DatabaseRequestStorage
65
40
  >("$fragno-internal-fragment"),
66
41
  internalSchema,
67
- "", // intentionally blank namespace so there is no prefix
68
42
  )
43
+ .providesBaseService(({ deps }) => ({
44
+ getDbNow: async () => {
45
+ if (deps.db.now) {
46
+ return deps.db.now();
47
+ }
48
+ return new Date();
49
+ },
50
+ }))
69
51
  .providesService("settingsService", ({ defineService }) => {
70
52
  return defineService({
71
53
  /**
@@ -128,33 +110,240 @@ export const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(
128
110
  * Returns all pending events for the given namespace that are ready to be processed.
129
111
  */
130
112
  getPendingHookEvents(namespace: string) {
113
+ const now = dbNow();
131
114
  return this.serviceTx(internalSchema)
132
115
  .retrieve((uow) =>
133
116
  uow.find("fragno_hooks", (b) =>
134
117
  b.whereIndex("idx_namespace_status_retry", (eb) =>
135
- eb.and(eb("namespace", "=", namespace), eb("status", "=", "pending")),
118
+ eb.and(
119
+ eb("namespace", "=", namespace),
120
+ eb("status", "=", "pending"),
121
+ eb.or(eb.isNull("nextRetryAt"), eb("nextRetryAt", "<=", now)),
122
+ ),
136
123
  ),
137
124
  ),
138
125
  )
139
126
  .transformRetrieve(([events]) => {
140
- const now = new Date();
141
- // FIXME(Wilco): this should be handled by the database query, but there seems to be an issue.
142
- const ready = events.filter((event) => {
143
- if (!event.nextRetryAt) {
144
- return true; // Newly created events (nextRetryAt = null) are ready
127
+ return events.map((event) => ({
128
+ id: event.id,
129
+ hookName: event.hookName,
130
+ payload: event.payload as unknown,
131
+ attempts: event.attempts,
132
+ maxAttempts: event.maxAttempts,
133
+ idempotencyKey: event.nonce,
134
+ }));
135
+ })
136
+ .build();
137
+ },
138
+
139
+ /**
140
+ * Claim pending hook events for processing.
141
+ * Returns ready events and marks them as processing in the same transaction.
142
+ */
143
+ claimPendingHookEvents(namespace: string) {
144
+ const now = dbNow();
145
+ return this.serviceTx(internalSchema)
146
+ .retrieve((uow) =>
147
+ uow.find("fragno_hooks", (b) =>
148
+ b.whereIndex("idx_namespace_status_retry", (eb) =>
149
+ eb.and(
150
+ eb("namespace", "=", namespace),
151
+ eb("status", "=", "pending"),
152
+ eb.or(eb.isNull("nextRetryAt"), eb("nextRetryAt", "<=", now)),
153
+ ),
154
+ ),
155
+ ),
156
+ )
157
+ .transformRetrieve(([events]) => {
158
+ return events.map((event) => ({
159
+ id: event.id,
160
+ hookName: event.hookName,
161
+ payload: event.payload,
162
+ attempts: event.attempts,
163
+ maxAttempts: event.maxAttempts,
164
+ idempotencyKey: event.nonce,
165
+ }));
166
+ })
167
+ .mutate(({ uow, retrieveResult }) => {
168
+ if (retrieveResult.length === 0) {
169
+ return;
170
+ }
171
+ for (const event of retrieveResult) {
172
+ uow.update("fragno_hooks", event.id, (b) =>
173
+ b.set({ status: "processing", lastAttemptAt: now }).check(),
174
+ );
175
+ }
176
+ })
177
+ .transform(({ retrieveResult }) =>
178
+ retrieveResult.map((event) => ({
179
+ ...event,
180
+ id: new FragnoId({
181
+ externalId: event.id.externalId,
182
+ internalId: event.id.internalId,
183
+ version: event.id.version + 1,
184
+ }),
185
+ })),
186
+ )
187
+ .build();
188
+ },
189
+
190
+ /**
191
+ * Re-queue hook events that have been stuck in processing for too long.
192
+ */
193
+ requeueStuckProcessingHooks(namespace: string, staleBefore: Date) {
194
+ return this.serviceTx(internalSchema)
195
+ .retrieve((uow) =>
196
+ uow.find("fragno_hooks", (b) =>
197
+ b.whereIndex("idx_namespace_status_retry", (eb) =>
198
+ eb.and(eb("namespace", "=", namespace), eb("status", "=", "processing")),
199
+ ),
200
+ ),
201
+ )
202
+ .transformRetrieve(([events]) => {
203
+ const stuck = events.filter((event) => {
204
+ if (!event.lastAttemptAt) {
205
+ return true;
145
206
  }
146
- return event.nextRetryAt <= now; // Only include if retry time has passed
207
+ return event.lastAttemptAt <= staleBefore;
147
208
  });
148
209
 
149
- return ready.map((event) => ({
210
+ return stuck.map((event) => ({
150
211
  id: event.id,
151
212
  hookName: event.hookName,
152
- payload: event.payload as unknown,
153
213
  attempts: event.attempts,
154
214
  maxAttempts: event.maxAttempts,
155
- idempotencyKey: event.nonce,
215
+ lastAttemptAt: event.lastAttemptAt,
216
+ nextRetryAt: event.nextRetryAt,
156
217
  }));
157
218
  })
219
+ .mutate(({ uow, retrieveResult }) => {
220
+ for (const event of retrieveResult) {
221
+ uow.update("fragno_hooks", event.id, (b) =>
222
+ b.set({ status: "pending", nextRetryAt: null }).check(),
223
+ );
224
+ }
225
+ })
226
+ .transform(({ retrieveResult }) => retrieveResult)
227
+ .build();
228
+ },
229
+
230
+ /**
231
+ * Get the next time a processing hook becomes stale.
232
+ */
233
+ getNextProcessingStaleAt(namespace: string, timeoutMinutes: number, now?: Date) {
234
+ return this.serviceTx(internalSchema)
235
+ .retrieve((uow) =>
236
+ uow.find("fragno_hooks", (b) =>
237
+ b.whereIndex("idx_namespace_status_retry", (eb) =>
238
+ eb.and(eb("namespace", "=", namespace), eb("status", "=", "processing")),
239
+ ),
240
+ ),
241
+ )
242
+ .transformRetrieve(([events]) => {
243
+ if (events.length === 0) {
244
+ return null;
245
+ }
246
+
247
+ const baseNow = now ?? new Date();
248
+ const nowMs = baseNow.getTime();
249
+ const timeoutMs = timeoutMinutes * 60_000;
250
+ let earliestStaleAt: Date | null = null;
251
+
252
+ for (const event of events) {
253
+ if (!event.lastAttemptAt) {
254
+ return baseNow;
255
+ }
256
+
257
+ const staleAtMs = event.lastAttemptAt.getTime() + timeoutMs;
258
+ if (staleAtMs <= nowMs) {
259
+ return baseNow;
260
+ }
261
+
262
+ const staleAt = new Date(staleAtMs);
263
+ if (!earliestStaleAt || staleAt < earliestStaleAt) {
264
+ earliestStaleAt = staleAt;
265
+ }
266
+ }
267
+
268
+ return earliestStaleAt;
269
+ })
270
+ .build();
271
+ },
272
+
273
+ /**
274
+ * Get the earliest pending hook wake time for a namespace.
275
+ * Optionally considers processing hooks becoming stale when timeoutMinutes is provided.
276
+ */
277
+ getNextHookWakeAt(namespace: string, timeoutMinutes?: number | false, now?: Date) {
278
+ const baseNow = now ?? new Date();
279
+ const includeProcessing = typeof timeoutMinutes === "number" && timeoutMinutes > 0;
280
+ const timeoutMs = includeProcessing ? timeoutMinutes * 60_000 : 0;
281
+
282
+ return this.serviceTx(internalSchema)
283
+ .retrieve((uow) =>
284
+ uow.find("fragno_hooks", (b) =>
285
+ b
286
+ .whereIndex("idx_namespace_status_retry", (eb) => {
287
+ if (includeProcessing) {
288
+ return eb.and(
289
+ eb("namespace", "=", namespace),
290
+ eb.or(eb("status", "=", "pending"), eb("status", "=", "processing")),
291
+ );
292
+ }
293
+ return eb.and(eb("namespace", "=", namespace), eb("status", "=", "pending"));
294
+ })
295
+ .select(["status", "nextRetryAt", "lastAttemptAt"]),
296
+ ),
297
+ )
298
+ .transformRetrieve(([events]) => {
299
+ if (events.length === 0) {
300
+ return null;
301
+ }
302
+
303
+ const nowMs = baseNow.getTime();
304
+ let earliestPendingAt: Date | null = null;
305
+ let earliestStaleAt: Date | null = null;
306
+
307
+ for (const event of events) {
308
+ if (event.status === "pending") {
309
+ const nextRetryAt = event.nextRetryAt;
310
+ if (!nextRetryAt || nextRetryAt.getTime() <= nowMs) {
311
+ return baseNow;
312
+ }
313
+ if (!earliestPendingAt || nextRetryAt < earliestPendingAt) {
314
+ earliestPendingAt = nextRetryAt;
315
+ }
316
+ continue;
317
+ }
318
+
319
+ if (!includeProcessing || event.status !== "processing") {
320
+ continue;
321
+ }
322
+
323
+ const lastAttemptAt = event.lastAttemptAt;
324
+ if (!lastAttemptAt) {
325
+ return baseNow;
326
+ }
327
+
328
+ const staleAtMs = lastAttemptAt.getTime() + timeoutMs;
329
+ if (staleAtMs <= nowMs) {
330
+ return baseNow;
331
+ }
332
+
333
+ const staleAt = new Date(staleAtMs);
334
+ if (!earliestStaleAt || staleAt < earliestStaleAt) {
335
+ earliestStaleAt = staleAt;
336
+ }
337
+ }
338
+
339
+ if (!earliestPendingAt) {
340
+ return earliestStaleAt ?? null;
341
+ }
342
+ if (!earliestStaleAt) {
343
+ return earliestPendingAt;
344
+ }
345
+ return earliestPendingAt <= earliestStaleAt ? earliestPendingAt : earliestStaleAt;
346
+ })
158
347
  .build();
159
348
  },
160
349
 
@@ -165,7 +354,7 @@ export const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(
165
354
  return this.serviceTx(internalSchema)
166
355
  .mutate(({ uow }) =>
167
356
  uow.update("fragno_hooks", eventId, (b) =>
168
- b.set({ status: "completed", lastAttemptAt: new Date() }).check(),
357
+ b.set({ status: "completed", lastAttemptAt: dbNow() }).check(),
169
358
  ),
170
359
  )
171
360
  .build();
@@ -174,7 +363,13 @@ export const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(
174
363
  /**
175
364
  * Mark a hook event as failed and schedule next retry.
176
365
  */
177
- markHookFailed(eventId: FragnoId, error: string, attempts: number, retryPolicy: RetryPolicy) {
366
+ markHookFailed(
367
+ eventId: FragnoId,
368
+ error: string,
369
+ attempts: number,
370
+ retryPolicy: RetryPolicy,
371
+ now?: Date,
372
+ ) {
178
373
  const newAttempts = attempts + 1;
179
374
  const shouldRetry = retryPolicy.shouldRetry(newAttempts - 1);
180
375
 
@@ -182,13 +377,14 @@ export const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(
182
377
  .mutate(({ uow }) => {
183
378
  if (shouldRetry) {
184
379
  const delayMs = retryPolicy.getDelayMs(newAttempts - 1);
185
- const nextRetryAt = new Date(Date.now() + delayMs);
380
+ const baseNow = now ?? new Date();
381
+ const nextRetryAt = new Date(baseNow.getTime() + delayMs);
186
382
  uow.update("fragno_hooks", eventId, (b) =>
187
383
  b
188
384
  .set({
189
385
  status: "pending",
190
386
  attempts: newAttempts,
191
- lastAttemptAt: new Date(),
387
+ lastAttemptAt: dbNow(),
192
388
  nextRetryAt,
193
389
  error,
194
390
  })
@@ -200,7 +396,7 @@ export const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(
200
396
  .set({
201
397
  status: "failed",
202
398
  attempts: newAttempts,
203
- lastAttemptAt: new Date(),
399
+ lastAttemptAt: dbNow(),
204
400
  error,
205
401
  })
206
402
  .check(),
@@ -217,7 +413,7 @@ export const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(
217
413
  return this.serviceTx(internalSchema)
218
414
  .mutate(({ uow }) =>
219
415
  uow.update("fragno_hooks", eventId, (b) =>
220
- b.set({ status: "processing", lastAttemptAt: new Date() }).check(),
416
+ b.set({ status: "processing", lastAttemptAt: dbNow() }).check(),
221
417
  ),
222
418
  )
223
419
  .build();
@@ -252,6 +448,44 @@ export const internalFragmentDef = new DatabaseFragmentDefinitionBuilder(
252
448
  },
253
449
  });
254
450
  })
451
+ .providesService("outboxService", ({ defineService }) => {
452
+ return defineService({
453
+ /**
454
+ * List outbox entries ordered by versionstamp (ascending).
455
+ */
456
+ list({ afterVersionstamp, limit }: { afterVersionstamp?: string; limit?: number } = {}) {
457
+ const afterValue = afterVersionstamp?.toLowerCase();
458
+
459
+ return this.serviceTx(internalSchema)
460
+ .retrieve((uow) =>
461
+ uow.find("fragno_db_outbox", (b) => {
462
+ let builder = afterValue
463
+ ? b.whereIndex("idx_outbox_versionstamp", (eb) =>
464
+ eb("versionstamp", ">", afterValue),
465
+ )
466
+ : b.whereIndex("idx_outbox_versionstamp");
467
+
468
+ builder = builder.orderByIndex("idx_outbox_versionstamp", "asc");
469
+ if (limit !== undefined) {
470
+ builder = builder.pageSize(limit);
471
+ }
472
+ return builder;
473
+ }),
474
+ )
475
+ .transformRetrieve(([entries]) =>
476
+ entries.map((entry) => ({
477
+ id: entry.id,
478
+ versionstamp: entry.versionstamp,
479
+ uowId: entry.uowId,
480
+ payload: entry.payload,
481
+ refMap: entry.refMap ?? undefined,
482
+ createdAt: entry.createdAt,
483
+ })),
484
+ )
485
+ .build();
486
+ },
487
+ });
488
+ })
255
489
  .build();
256
490
 
257
491
  /**
@@ -267,15 +501,40 @@ export async function getSchemaVersionFromDatabase(
267
501
  namespace: string,
268
502
  ): Promise<number> {
269
503
  try {
270
- const setting = await fragment.inContext(async function () {
271
- return await this.handlerTx()
272
- .withServiceCalls(
273
- () => [fragment.services.settingsService.get(namespace, "schema_version")] as const,
274
- )
275
- .transform(({ serviceResult: [result] }) => result)
276
- .execute();
277
- });
278
- return setting ? parseInt(setting.value, 10) : 0;
504
+ const readSchemaVersion = async (targetNamespace: string): Promise<number | undefined> => {
505
+ const setting = await fragment.inContext(async function () {
506
+ return await this.handlerTx()
507
+ .withServiceCalls(
508
+ () =>
509
+ [fragment.services.settingsService.get(targetNamespace, "schema_version")] as const,
510
+ )
511
+ .transform(({ serviceResult: [result] }) => result)
512
+ .execute();
513
+ });
514
+ if (!setting) {
515
+ return undefined;
516
+ }
517
+ const parsed = parseInt(setting.value, 10);
518
+ return Number.isNaN(parsed) ? undefined : parsed;
519
+ };
520
+
521
+ const primary = await readSchemaVersion(namespace);
522
+ if (primary !== undefined) {
523
+ return primary;
524
+ }
525
+
526
+ // Back-compat: some installs stored internal schema version under a different namespace.
527
+ // Check the alternate key (empty string ↔ schema name) so we find the version either way.
528
+ const legacyNamespace =
529
+ namespace === "" ? internalSchema.name : namespace === internalSchema.name ? "" : null;
530
+ if (legacyNamespace !== null) {
531
+ const legacy = await readSchemaVersion(legacyNamespace);
532
+ if (legacy !== undefined) {
533
+ return legacy;
534
+ }
535
+ }
536
+
537
+ return 0;
279
538
  } catch {
280
539
  return 0;
281
540
  }
@@ -0,0 +1,117 @@
1
+ import SQLite from "better-sqlite3";
2
+ import { SqliteDialect } from "kysely";
3
+ import { beforeAll, describe, expect, it } from "vitest";
4
+ import { defineFragment, instantiate } from "@fragno-dev/core";
5
+ import { withDatabase } from "../with-database";
6
+ import { schema, column, idColumn } from "../schema/create";
7
+ import { SqlAdapter } from "../adapters/generic-sql/generic-sql-adapter";
8
+ import { BetterSQLite3DriverConfig } from "../adapters/generic-sql/driver-config";
9
+ import { internalSchema } from "../fragments/internal-fragment";
10
+ import { createDurableHooksProcessor } from "./durable-hooks-processor";
11
+
12
+ const testSchema = schema("test", (s) =>
13
+ s.addTable("items", (t) => t.addColumn("id", idColumn()).addColumn("name", column("string"))),
14
+ );
15
+
16
+ const testFragmentDefinition = defineFragment("test")
17
+ .extend(withDatabase(testSchema))
18
+ .provideHooks(({ defineHook }) => ({
19
+ onTest: defineHook(async function () {}),
20
+ }))
21
+ .build();
22
+
23
+ describe("createDurableHooksProcessor", () => {
24
+ let adapter: SqlAdapter;
25
+ let fragment: ReturnType<typeof instantiateFragment>;
26
+
27
+ function instantiateFragment(options: { databaseAdapter: SqlAdapter }) {
28
+ return instantiate(testFragmentDefinition).withConfig({}).withOptions(options).build();
29
+ }
30
+
31
+ beforeAll(async () => {
32
+ const sqliteDatabase = new SQLite(":memory:");
33
+ const dialect = new SqliteDialect({ database: sqliteDatabase });
34
+
35
+ adapter = new SqlAdapter({
36
+ dialect,
37
+ driverConfig: new BetterSQLite3DriverConfig(),
38
+ });
39
+
40
+ const internalMigrations = adapter.prepareMigrations(internalSchema, null);
41
+ await internalMigrations.executeWithDriver(adapter.driver, 0);
42
+
43
+ const testMigrations = adapter.prepareMigrations(testSchema, "test");
44
+ await testMigrations.executeWithDriver(adapter.driver, 0);
45
+
46
+ fragment = instantiateFragment({ databaseAdapter: adapter });
47
+
48
+ return async () => {
49
+ await adapter.close();
50
+ };
51
+ }, 12000);
52
+
53
+ it("should process pending hooks and return counts", async () => {
54
+ const processor = createDurableHooksProcessor(fragment);
55
+ expect(processor).not.toBeNull();
56
+
57
+ const internalFragment = fragment.$internal.linkedFragments._fragno_internal;
58
+ await internalFragment.inContext(async function () {
59
+ await this.handlerTx()
60
+ .mutate(({ forSchema }) => {
61
+ const uow = forSchema(internalSchema);
62
+ uow.create("fragno_hooks", {
63
+ namespace: "test",
64
+ hookName: "onTest",
65
+ payload: { ok: true },
66
+ status: "pending",
67
+ attempts: 0,
68
+ maxAttempts: 1,
69
+ lastAttemptAt: null,
70
+ nextRetryAt: null,
71
+ error: null,
72
+ nonce: "test-nonce",
73
+ });
74
+ })
75
+ .execute();
76
+ });
77
+
78
+ const wakeAt = await processor!.getNextWakeAt();
79
+ expect(wakeAt).toBeInstanceOf(Date);
80
+
81
+ const processed = await processor!.process();
82
+ expect(processed).toBe(1);
83
+ });
84
+
85
+ it("should wake for stale processing hooks", async () => {
86
+ const processor = createDurableHooksProcessor(fragment);
87
+ expect(processor).not.toBeNull();
88
+
89
+ const internalFragment = fragment.$internal.linkedFragments._fragno_internal;
90
+ const services = internalFragment.services as { getDbNow?: () => Promise<Date> };
91
+ const baseNow = services.getDbNow ? await services.getDbNow() : new Date();
92
+
93
+ await internalFragment.inContext(async function () {
94
+ await this.handlerTx()
95
+ .mutate(({ forSchema }) => {
96
+ const uow = forSchema(internalSchema);
97
+ uow.create("fragno_hooks", {
98
+ namespace: "test",
99
+ hookName: "onTest",
100
+ payload: { ok: true },
101
+ status: "processing",
102
+ attempts: 0,
103
+ maxAttempts: 1,
104
+ lastAttemptAt: new Date(baseNow.getTime() - 20 * 60_000),
105
+ nextRetryAt: null,
106
+ error: null,
107
+ nonce: "test-nonce-stuck",
108
+ });
109
+ })
110
+ .execute();
111
+ });
112
+
113
+ const wakeAt = await processor!.getNextWakeAt();
114
+ expect(wakeAt).toBeInstanceOf(Date);
115
+ expect(wakeAt!.getTime()).toBeLessThanOrEqual(baseNow.getTime());
116
+ });
117
+ });
@@ -0,0 +1,67 @@
1
+ import type { AnySchema } from "../schema/create";
2
+ import type { AnyFragnoInstantiatedDatabaseFragment } from "../mod";
3
+ import { createHookScheduler, type HookProcessorConfig } from "./hooks";
4
+
5
+ export type DurableHooksProcessor = {
6
+ process: () => Promise<number>;
7
+ getNextWakeAt: () => Promise<Date | null>;
8
+ drain: () => Promise<void>;
9
+ namespace: string;
10
+ };
11
+
12
+ type DurableHooksInternal = {
13
+ durableHooks?: HookProcessorConfig;
14
+ };
15
+
16
+ const DEFAULT_STUCK_PROCESSING_TIMEOUT_MINUTES = 10;
17
+
18
+ function resolveStuckProcessingTimeoutMinutes(value: number | false | undefined): number | false {
19
+ if (value === false) {
20
+ return false;
21
+ }
22
+ if (typeof value === "number") {
23
+ return value > 0 ? value : false;
24
+ }
25
+ return DEFAULT_STUCK_PROCESSING_TIMEOUT_MINUTES;
26
+ }
27
+
28
+ export function createDurableHooksProcessor<TSchema extends AnySchema>(
29
+ fragment: AnyFragnoInstantiatedDatabaseFragment<TSchema>,
30
+ ): DurableHooksProcessor | null {
31
+ const durableHooks = (fragment.$internal as DurableHooksInternal).durableHooks;
32
+ if (!durableHooks) {
33
+ return null;
34
+ }
35
+
36
+ const { namespace, internalFragment } = durableHooks;
37
+ const stuckProcessingTimeoutMinutes = resolveStuckProcessingTimeoutMinutes(
38
+ durableHooks.stuckProcessingTimeoutMinutes,
39
+ );
40
+ const scheduler =
41
+ durableHooks.scheduler ?? (durableHooks.scheduler = createHookScheduler(durableHooks));
42
+
43
+ return {
44
+ namespace,
45
+ process: async () => scheduler.schedule(),
46
+ drain: async () => scheduler.drain(),
47
+ getNextWakeAt: async () => {
48
+ const services = internalFragment.services as { getDbNow?: () => Promise<Date> };
49
+ const now = services.getDbNow ? await services.getDbNow() : new Date();
50
+ return await internalFragment.inContext(async function () {
51
+ return await this.handlerTx()
52
+ .withServiceCalls(
53
+ () =>
54
+ [
55
+ internalFragment.services.hookService.getNextHookWakeAt(
56
+ namespace,
57
+ stuckProcessingTimeoutMinutes,
58
+ now,
59
+ ),
60
+ ] as const,
61
+ )
62
+ .transform(({ serviceResult: [result] }) => result)
63
+ .execute();
64
+ });
65
+ },
66
+ };
67
+ }