@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
@@ -1,3 +1,6 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
1
4
  import { describe, it, expect, vi, assert } from "vitest";
2
5
  import { instantiate, defineFragment } from "@fragno-dev/core";
3
6
  import { defineRoutes } from "@fragno-dev/core/route";
@@ -7,14 +10,22 @@ import type { DatabaseAdapter } from "./adapters/adapters";
7
10
  import type { SimpleQueryInterface } from "./query/simple-query-interface";
8
11
  import { RequestContextStorage } from "@fragno-dev/core/internal/request-context-storage";
9
12
  import { z } from "zod";
13
+ import { suffixNamingStrategy } from "./naming/sql-naming";
10
14
 
11
15
  // Create a test schema
12
- const testSchema = schema((s) => {
16
+ const testSchema = schema("test", (s) => {
13
17
  return s.addTable("users", (t) => {
14
18
  return t.addColumn("id", idColumn()).addColumn("name", column("string"));
15
19
  });
16
20
  });
17
21
 
22
+ // Schema with dashes in the name (used to test namespace sanitization)
23
+ const dashedSchema = schema("my-fragment", (s) => {
24
+ return s.addTable("items", (t) => {
25
+ return t.addColumn("id", idColumn()).addColumn("label", column("string"));
26
+ });
27
+ });
28
+
18
29
  type TestSchema = typeof testSchema;
19
30
 
20
31
  // Mock database adapter
@@ -35,6 +46,8 @@ function createMockAdapter(): DatabaseAdapter {
35
46
  mutationPhase: Promise.resolve(),
36
47
  })),
37
48
  restrict: vi.fn(() => createMockRestrictedUow()),
49
+ getRetrievalOperations: vi.fn(() => []),
50
+ getMutationOperations: vi.fn(() => []),
38
51
  table: vi.fn(() => ({
39
52
  findMany: vi.fn(),
40
53
  })),
@@ -61,7 +74,11 @@ function createMockAdapter(): DatabaseAdapter {
61
74
  executeMutations: vi.fn(async () => ({ success: true })),
62
75
  commit: vi.fn(),
63
76
  rollback: vi.fn(),
77
+ registerSchema: vi.fn(),
64
78
  reset: vi.fn(),
79
+ getRetrievalOperations: vi.fn(() => []),
80
+ getMutationOperations: vi.fn(() => []),
81
+ getCreatedIds: vi.fn(() => []),
65
82
  table: vi.fn(() => ({
66
83
  findMany: vi.fn(),
67
84
  })),
@@ -74,10 +91,11 @@ function createMockAdapter(): DatabaseAdapter {
74
91
 
75
92
  return {
76
93
  createQueryEngine: vi.fn(() => mockdb),
77
- migrate: vi.fn(),
94
+ getSchemaVersion: vi.fn(async () => undefined),
78
95
  close: vi.fn(),
79
96
  type: "mock",
80
97
  contextStorage: new RequestContextStorage(),
98
+ namingStrategy: suffixNamingStrategy,
81
99
  } as unknown as DatabaseAdapter;
82
100
  }
83
101
 
@@ -206,7 +224,7 @@ describe("db-fragment-instantiator", () => {
206
224
 
207
225
  describe("database operations with UOW", () => {
208
226
  it("should allow accessing schema-typed UOW in handlers via handlerTx", async () => {
209
- const testSchemaWithCounter = schema((s) => {
227
+ const testSchemaWithCounter = schema("testschemawithcounter", (s) => {
210
228
  return s.addTable("counters", (t) => {
211
229
  return t.addColumn("id", idColumn()).addColumn("value", column("integer"));
212
230
  });
@@ -447,7 +465,73 @@ describe("db-fragment-instantiator", () => {
447
465
  .build();
448
466
 
449
467
  expect(fragment.$internal.deps.schema).toBe(testSchema);
450
- expect(fragment.$internal.deps.namespace).toBe("test-db-fragment");
468
+ expect(fragment.$internal.deps.namespace).toBe("test");
469
+ });
470
+
471
+ it("should use databaseNamespace override when provided", () => {
472
+ const definition = defineFragment("test-db-fragment")
473
+ .extend(withDatabase(testSchema))
474
+ .build();
475
+
476
+ const mockAdapter = createMockAdapter();
477
+ const fragment = instantiate(definition)
478
+ .withOptions({
479
+ mountRoute: "/api",
480
+ databaseAdapter: mockAdapter,
481
+ databaseNamespace: "custom-namespace",
482
+ })
483
+ .build();
484
+
485
+ expect(fragment.$internal.deps.namespace).toBe("custom-namespace");
486
+ });
487
+
488
+ it("should allow explicit null namespace override", () => {
489
+ const definition = defineFragment("test-db-fragment")
490
+ .extend(withDatabase(testSchema))
491
+ .build();
492
+
493
+ const mockAdapter = createMockAdapter();
494
+ const fragment = instantiate(definition)
495
+ .withOptions({
496
+ mountRoute: "/api",
497
+ databaseAdapter: mockAdapter,
498
+ databaseNamespace: null,
499
+ })
500
+ .build();
501
+
502
+ expect(fragment.$internal.deps.namespace).toBeNull();
503
+ });
504
+
505
+ it("should sanitize dashes in schema.name when used as default namespace", () => {
506
+ const definition = defineFragment("test-dashed-fragment")
507
+ .extend(withDatabase(dashedSchema))
508
+ .build();
509
+
510
+ const mockAdapter = createMockAdapter();
511
+ const fragment = instantiate(definition)
512
+ .withOptions({ mountRoute: "/api", databaseAdapter: mockAdapter })
513
+ .build();
514
+
515
+ // schema.name is "my-fragment", but default namespace should be sanitized to "my_fragment"
516
+ expect(fragment.$internal.deps.namespace).toBe("my_fragment");
517
+ });
518
+
519
+ it("should NOT sanitize explicit databaseNamespace even when it contains dashes", () => {
520
+ const definition = defineFragment("test-dashed-fragment")
521
+ .extend(withDatabase(dashedSchema))
522
+ .build();
523
+
524
+ const mockAdapter = createMockAdapter();
525
+ const fragment = instantiate(definition)
526
+ .withOptions({
527
+ mountRoute: "/api",
528
+ databaseAdapter: mockAdapter,
529
+ databaseNamespace: "my-fragment",
530
+ })
531
+ .build();
532
+
533
+ // Explicit override should be used as-is, dashes preserved
534
+ expect(fragment.$internal.deps.namespace).toBe("my-fragment");
451
535
  });
452
536
 
453
537
  it("should populate $internal when using providesBaseService without withDependencies", () => {
@@ -478,18 +562,26 @@ describe("db-fragment-instantiator", () => {
478
562
  });
479
563
  });
480
564
 
481
- describe("error handling", () => {
482
- it("should throw when databaseAdapter is not provided", () => {
565
+ describe("default adapter", () => {
566
+ it("should default to sqlite adapter when databaseAdapter is not provided", () => {
483
567
  const definition = defineFragment("test-db-fragment")
484
568
  .extend(withDatabase(testSchema))
485
569
  .build();
486
-
487
- expect(() => {
488
- instantiate(definition)
489
- // @ts-expect-error - Test case
490
- .withOptions({})
491
- .build();
492
- }).toThrow("Database fragment requires a database adapter");
570
+ const previous = process.env["FRAGNO_DATA_DIR"];
571
+ const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "fragno-db-default-"));
572
+ process.env["FRAGNO_DATA_DIR"] = dataDir;
573
+
574
+ try {
575
+ const fragment = instantiate(definition).withOptions({}).build();
576
+ expect(fragment.$internal.options.databaseAdapter).toBeDefined();
577
+ expect(fragment.$internal.deps.db).toBeDefined();
578
+ } finally {
579
+ if (previous === undefined) {
580
+ delete process.env["FRAGNO_DATA_DIR"];
581
+ } else {
582
+ process.env["FRAGNO_DATA_DIR"] = previous;
583
+ }
584
+ }
493
585
  });
494
586
 
495
587
  it("should throw when serviceTx called outside request context", () => {
@@ -1,7 +1,7 @@
1
1
  import { SQLocalKysely } from "sqlocal/kysely";
2
2
  import { assert, beforeAll, describe, expect, it } from "vitest";
3
3
  import { z } from "zod";
4
- import { KyselyAdapter } from "./adapters/kysely/kysely-adapter";
4
+ import { SqlAdapter } from "./adapters/generic-sql/generic-sql-adapter";
5
5
  import { column, idColumn, referenceColumn, schema, type FragnoId } from "./schema/create";
6
6
  import { defineFragment, instantiate } from "@fragno-dev/core";
7
7
  import { defineRoutes } from "@fragno-dev/core/route";
@@ -12,7 +12,7 @@ import { SQLocalDriverConfig } from "./adapters/generic-sql/driver-config";
12
12
 
13
13
  describe.sequential("Database Fragment Integration", () => {
14
14
  // Schema 1: Users fragment
15
- const usersSchema = schema((s) => {
15
+ const usersSchema = schema("users", (s) => {
16
16
  return s
17
17
  .addTable("users", (t) => {
18
18
  return t
@@ -36,7 +36,7 @@ describe.sequential("Database Fragment Integration", () => {
36
36
  });
37
37
 
38
38
  // Schema 2: Orders fragment
39
- const ordersSchema = schema((s) => {
39
+ const ordersSchema = schema("orders", (s) => {
40
40
  return s.addTable("orders", (t) => {
41
41
  return t
42
42
  .addColumn("id", idColumn())
@@ -50,7 +50,7 @@ describe.sequential("Database Fragment Integration", () => {
50
50
 
51
51
  // Define Users Fragment using the new unified serviceTx API
52
52
  const usersFragmentDef = defineFragment("users-fragment")
53
- .extend(withDatabase(usersSchema, "users"))
53
+ .extend(withDatabase(usersSchema))
54
54
  .providesService("userService", ({ defineService }) => {
55
55
  return defineService({
56
56
  // Creates a user - returns TxResult<FragnoId>
@@ -118,7 +118,7 @@ describe.sequential("Database Fragment Integration", () => {
118
118
 
119
119
  // Define Orders Fragment with cross-fragment service dependency using new serviceTx API
120
120
  const ordersFragmentDef = defineFragment("orders-fragment")
121
- .extend(withDatabase(ordersSchema, "orders"))
121
+ .extend(withDatabase(ordersSchema))
122
122
  .usesService<
123
123
  "userService",
124
124
  {
@@ -209,7 +209,7 @@ describe.sequential("Database Fragment Integration", () => {
209
209
  }),
210
210
  ]);
211
211
 
212
- let adapter: KyselyAdapter;
212
+ let adapter: SqlAdapter;
213
213
  let usersFragment: ReturnType<typeof instantiateUsersFragment>;
214
214
  let ordersFragment: ReturnType<typeof instantiateOrdersFragment>;
215
215
 
@@ -239,7 +239,7 @@ describe.sequential("Database Fragment Integration", () => {
239
239
  beforeAll(async () => {
240
240
  // Create in-memory SQLite database with Kysely
241
241
  const { dialect } = new SQLocalKysely(":memory:");
242
- adapter = new KyselyAdapter({
242
+ adapter = new SqlAdapter({
243
243
  dialect,
244
244
  driverConfig: new SQLocalDriverConfig(),
245
245
  });
@@ -414,6 +414,8 @@ describe.sequential("Database Fragment Integration", () => {
414
414
 
415
415
  const result = await usersFragment.inContext(async function () {
416
416
  return await this.handlerTx()
417
+ // Add a retrieve op so retry is allowed for the forced conflict below.
418
+ .retrieve(({ forSchema }) => forSchema(usersSchema).find("users"))
417
419
  .mutate(({ forSchema, idempotencyKey, currentAttempt }) => {
418
420
  if (currentAttempt === 0) {
419
421
  firstIdempotencyKey = idempotencyKey;
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createDurableHooksDispatcherDurableObject } from "./index";
3
+
4
+ describe("createDurableHooksDispatcherDurableObject", () => {
5
+ it("should schedule an initial alarm on creation", async () => {
6
+ const process = vi.fn().mockResolvedValue(0);
7
+ const getNextWakeAt = vi.fn().mockResolvedValue(new Date());
8
+ const drain = vi.fn().mockResolvedValue(undefined);
9
+ const setAlarm = vi.fn().mockResolvedValue(undefined);
10
+
11
+ const handlerFactory = createDurableHooksDispatcherDurableObject({
12
+ createProcessor: () => ({ process, getNextWakeAt, drain, namespace: "test" }),
13
+ });
14
+
15
+ handlerFactory({ storage: { setAlarm } }, {});
16
+
17
+ await Promise.resolve();
18
+ expect(setAlarm).toHaveBeenCalledTimes(1);
19
+ expect(process).not.toHaveBeenCalled();
20
+ });
21
+
22
+ it("should delete the alarm when no pending hooks exist", async () => {
23
+ const process = vi.fn().mockResolvedValue(0);
24
+ const getNextWakeAt = vi.fn().mockResolvedValue(null);
25
+ const drain = vi.fn().mockResolvedValue(undefined);
26
+ const setAlarm = vi.fn().mockResolvedValue(undefined);
27
+ const deleteAlarm = vi.fn().mockResolvedValue(undefined);
28
+
29
+ const handlerFactory = createDurableHooksDispatcherDurableObject({
30
+ createProcessor: () => ({ process, getNextWakeAt, drain, namespace: "test" }),
31
+ });
32
+
33
+ const handler = handlerFactory({ storage: { setAlarm, deleteAlarm } }, {});
34
+
35
+ await Promise.resolve();
36
+ expect(getNextWakeAt).toHaveBeenCalledTimes(1);
37
+
38
+ await handler.alarm?.();
39
+
40
+ expect(process).toHaveBeenCalledTimes(1);
41
+ expect(deleteAlarm).toHaveBeenCalledTimes(2);
42
+ expect(setAlarm).not.toHaveBeenCalled();
43
+ });
44
+
45
+ it("should schedule alarm using max(nextWakeAt, now)", async () => {
46
+ vi.useFakeTimers();
47
+ const now = new Date("2024-01-01T00:00:00Z");
48
+ vi.setSystemTime(now);
49
+
50
+ const process = vi.fn().mockResolvedValue(0);
51
+ const getNextWakeAt = vi.fn().mockResolvedValue(new Date(now.getTime() - 10000));
52
+ const drain = vi.fn().mockResolvedValue(undefined);
53
+ const setAlarm = vi.fn().mockResolvedValue(undefined);
54
+ const deleteAlarm = vi.fn().mockResolvedValue(undefined);
55
+
56
+ const handlerFactory = createDurableHooksDispatcherDurableObject({
57
+ createProcessor: () => ({ process, getNextWakeAt, drain, namespace: "test" }),
58
+ });
59
+
60
+ const handler = handlerFactory({ storage: { setAlarm, deleteAlarm } }, {});
61
+
62
+ await Promise.resolve();
63
+ expect(setAlarm).toHaveBeenCalledTimes(1);
64
+
65
+ await handler.alarm?.();
66
+
67
+ expect(setAlarm).toHaveBeenCalledTimes(2);
68
+ for (const [scheduledAt] of setAlarm.mock.calls) {
69
+ expect(scheduledAt.getTime()).toBeGreaterThanOrEqual(now.getTime());
70
+ }
71
+ vi.useRealTimers();
72
+ });
73
+ });
@@ -0,0 +1,104 @@
1
+ import type { DurableHooksProcessor } from "../../hooks/durable-hooks-processor";
2
+
3
+ type AlarmStorage = {
4
+ setAlarm?: (timestamp: number | Date) => Promise<void>;
5
+ deleteAlarm?: () => Promise<void>;
6
+ };
7
+
8
+ export type DurableHooksDispatcherDurableObjectState = {
9
+ readonly storage: AlarmStorage;
10
+ };
11
+
12
+ export type DurableHooksDispatcherDurableObjectHandler = {
13
+ fetch?: (request: Request) => Promise<Response>;
14
+ alarm?: () => Promise<void>;
15
+ };
16
+
17
+ export type DurableHooksDispatcherDurableObjectFactory<TEnv = unknown> = (
18
+ state: DurableHooksDispatcherDurableObjectState,
19
+ env: TEnv,
20
+ ) => DurableHooksDispatcherDurableObjectHandler;
21
+
22
+ export type DurableHooksDispatcherDurableObjectOptions<TEnv = unknown> = {
23
+ createProcessor: (context: {
24
+ state: DurableHooksDispatcherDurableObjectState;
25
+ env: TEnv;
26
+ }) => DurableHooksProcessor;
27
+ onProcessError?: (error: unknown) => void;
28
+ };
29
+
30
+ export function createDurableHooksDispatcherDurableObject<TEnv>(
31
+ options: DurableHooksDispatcherDurableObjectOptions<TEnv>,
32
+ ): DurableHooksDispatcherDurableObjectFactory<TEnv> {
33
+ return (state, env) => {
34
+ const processor = options.createProcessor({ state, env });
35
+ const onProcessError =
36
+ options.onProcessError ??
37
+ ((error: unknown) => {
38
+ console.error("Durable hooks dispatcher error", error);
39
+ });
40
+ const rawSetAlarm = state.storage.setAlarm;
41
+ const rawDeleteAlarm = state.storage.deleteAlarm;
42
+
43
+ if (!rawSetAlarm) {
44
+ throw new Error(
45
+ "Durable hooks dispatcher requires state.storage.setAlarm to schedule alarms.",
46
+ );
47
+ }
48
+ const setAlarm = rawSetAlarm.bind(state.storage);
49
+ const deleteAlarm = rawDeleteAlarm?.bind(state.storage);
50
+
51
+ let processing = false;
52
+ let queued = false;
53
+ let currentPromise: Promise<void> | undefined;
54
+
55
+ const runProcess = () => {
56
+ if (processing) {
57
+ queued = true;
58
+ return currentPromise ?? Promise.resolve();
59
+ }
60
+
61
+ processing = true;
62
+ currentPromise = (async () => {
63
+ do {
64
+ queued = false;
65
+ try {
66
+ await processor.process();
67
+ } catch (error) {
68
+ onProcessError(error);
69
+ }
70
+ } while (queued);
71
+ processing = false;
72
+ })();
73
+
74
+ return currentPromise;
75
+ };
76
+
77
+ const scheduleNextAlarm = async () => {
78
+ const nextWakeAt = await processor.getNextWakeAt();
79
+ if (!nextWakeAt) {
80
+ await deleteAlarm?.();
81
+ return;
82
+ }
83
+
84
+ const now = Date.now();
85
+ const scheduledAt = new Date(Math.max(nextWakeAt.getTime(), now));
86
+ await setAlarm(scheduledAt);
87
+ };
88
+
89
+ void scheduleNextAlarm().catch((error) => {
90
+ onProcessError(error);
91
+ });
92
+
93
+ return {
94
+ alarm: async () => {
95
+ try {
96
+ await runProcess();
97
+ await scheduleNextAlarm();
98
+ } catch (error) {
99
+ onProcessError(error);
100
+ }
101
+ },
102
+ };
103
+ };
104
+ }
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { createDurableHooksDispatcher } from "./index";
3
+
4
+ describe("createDurableHooksDispatcher", () => {
5
+ it("should wake and process hooks", async () => {
6
+ const process = vi.fn().mockResolvedValue(0);
7
+ const getNextWakeAt = vi.fn().mockResolvedValue(null);
8
+ const drain = vi.fn().mockResolvedValue(undefined);
9
+
10
+ const dispatcher = createDurableHooksDispatcher({
11
+ processor: { process, getNextWakeAt, drain, namespace: "test" },
12
+ });
13
+
14
+ await dispatcher.wake();
15
+
16
+ expect(process).toHaveBeenCalledTimes(1);
17
+ });
18
+
19
+ it("should coalesce overlapping wake calls", async () => {
20
+ let resolveFirst!: (value: number) => void;
21
+ const firstPromise = new Promise<number>((resolve) => {
22
+ resolveFirst = resolve;
23
+ });
24
+ const process = vi.fn().mockReturnValueOnce(firstPromise).mockResolvedValue(0);
25
+ const drain = vi.fn().mockResolvedValue(undefined);
26
+
27
+ const dispatcher = createDurableHooksDispatcher({
28
+ processor: {
29
+ process,
30
+ getNextWakeAt: vi.fn().mockResolvedValue(null),
31
+ drain,
32
+ namespace: "test",
33
+ },
34
+ });
35
+
36
+ const first = dispatcher.wake();
37
+ const second = dispatcher.wake();
38
+
39
+ expect(process).toHaveBeenCalledTimes(1);
40
+
41
+ resolveFirst(0);
42
+ await first;
43
+ await second;
44
+
45
+ expect(process).toHaveBeenCalledTimes(2);
46
+ });
47
+
48
+ it("should poll and process when due", async () => {
49
+ vi.useFakeTimers();
50
+ const now = new Date("2024-01-01T00:00:00Z");
51
+ vi.setSystemTime(now);
52
+
53
+ const process = vi.fn().mockResolvedValue(0);
54
+ const getNextWakeAt = vi.fn().mockResolvedValue(new Date(now.getTime() - 1000));
55
+ const drain = vi.fn().mockResolvedValue(undefined);
56
+
57
+ const dispatcher = createDurableHooksDispatcher({
58
+ processor: { process, getNextWakeAt, drain, namespace: "test" },
59
+ pollIntervalMs: 1000,
60
+ });
61
+
62
+ dispatcher.startPolling();
63
+ await vi.advanceTimersByTimeAsync(1000);
64
+ dispatcher.stopPolling();
65
+
66
+ expect(process).toHaveBeenCalledTimes(1);
67
+ vi.useRealTimers();
68
+ });
69
+
70
+ it("should skip polling when next wake is in the future", async () => {
71
+ vi.useFakeTimers();
72
+ const now = new Date("2024-01-01T00:00:00Z");
73
+ vi.setSystemTime(now);
74
+
75
+ const process = vi.fn().mockResolvedValue(0);
76
+ const getNextWakeAt = vi.fn().mockResolvedValue(new Date(now.getTime() + 60000));
77
+ const drain = vi.fn().mockResolvedValue(undefined);
78
+
79
+ const dispatcher = createDurableHooksDispatcher({
80
+ processor: { process, getNextWakeAt, drain, namespace: "test" },
81
+ pollIntervalMs: 1000,
82
+ });
83
+
84
+ dispatcher.startPolling();
85
+ await vi.advanceTimersByTimeAsync(1000);
86
+ dispatcher.stopPolling();
87
+
88
+ expect(process).not.toHaveBeenCalled();
89
+ vi.useRealTimers();
90
+ });
91
+ });
@@ -0,0 +1,87 @@
1
+ import type { DurableHooksProcessor } from "../../hooks/durable-hooks-processor";
2
+
3
+ export type DurableHooksDispatcher = {
4
+ wake: () => Promise<void>;
5
+ startPolling: () => void;
6
+ stopPolling: () => void;
7
+ };
8
+
9
+ export type DurableHooksDispatcherOptions = {
10
+ processor: DurableHooksProcessor;
11
+ pollIntervalMs?: number;
12
+ onError?: (error: unknown) => void;
13
+ };
14
+
15
+ export function createDurableHooksDispatcher(
16
+ options: DurableHooksDispatcherOptions,
17
+ ): DurableHooksDispatcher {
18
+ const pollIntervalMs = options.pollIntervalMs ?? 5000;
19
+ const onError =
20
+ options.onError ??
21
+ ((error: unknown) => {
22
+ console.error("Durable hooks dispatcher error", error);
23
+ });
24
+ let timer: ReturnType<typeof setInterval> | undefined;
25
+ let processing = false;
26
+ let queued = false;
27
+ let currentPromise: Promise<void> | undefined;
28
+
29
+ const runProcess = () => {
30
+ if (processing) {
31
+ queued = true;
32
+ return currentPromise ?? Promise.resolve();
33
+ }
34
+
35
+ processing = true;
36
+ currentPromise = (async () => {
37
+ do {
38
+ queued = false;
39
+ try {
40
+ await options.processor.process();
41
+ } catch (error) {
42
+ onError(error);
43
+ }
44
+ } while (queued);
45
+ processing = false;
46
+ })();
47
+
48
+ return currentPromise;
49
+ };
50
+
51
+ const poll = async () => {
52
+ try {
53
+ const nextWakeAt = await options.processor.getNextWakeAt();
54
+ if (!nextWakeAt) {
55
+ return;
56
+ }
57
+ if (Date.now() >= nextWakeAt.getTime()) {
58
+ await runProcess();
59
+ }
60
+ } catch (error) {
61
+ onError(error);
62
+ }
63
+ };
64
+
65
+ return {
66
+ wake: async () => {
67
+ await runProcess();
68
+ },
69
+ startPolling: () => {
70
+ if (timer) {
71
+ return;
72
+ }
73
+
74
+ timer = setInterval(() => {
75
+ void poll();
76
+ }, pollIntervalMs);
77
+ },
78
+ stopPolling: () => {
79
+ if (!timer) {
80
+ return;
81
+ }
82
+
83
+ clearInterval(timer);
84
+ timer = undefined;
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,42 @@
1
+ import { defineRoutes } from "@fragno-dev/core";
2
+ import { internalFragmentDef } from "./internal-fragment";
3
+
4
+ export const internalFragmentRoutes = defineRoutes(internalFragmentDef).create(
5
+ ({ defineRoute, services }) => [
6
+ defineRoute({
7
+ method: "GET",
8
+ path: "/outbox",
9
+ handler: async function (input, { json }) {
10
+ // We intentionally skip input/output schemas here to keep the internal route lightweight.
11
+ // Query params are validated manually and the response shape is stable (OutboxEntry[]),
12
+ // while the public API surface is still gated behind adapter config.
13
+ const afterVersionstamp = input.query.get("afterVersionstamp") ?? undefined;
14
+ const limitValue = input.query.get("limit");
15
+ let limit: number | undefined;
16
+
17
+ if (limitValue !== null) {
18
+ const parsed = Number.parseInt(limitValue, 10);
19
+ if (!Number.isFinite(parsed) || parsed < 1) {
20
+ return json(
21
+ {
22
+ error: "Invalid limit query parameter.",
23
+ code: "INVALID_LIMIT",
24
+ },
25
+ { status: 400 },
26
+ );
27
+ }
28
+ limit = parsed;
29
+ }
30
+
31
+ const entries = await this.handlerTx()
32
+ .withServiceCalls(
33
+ () => [services.outboxService.list({ afterVersionstamp, limit })] as const,
34
+ )
35
+ .transform(({ serviceResult: [result] }) => result)
36
+ .execute();
37
+
38
+ return json(entries);
39
+ },
40
+ }),
41
+ ],
42
+ );