@effect-app/infra 4.0.0-beta.99 → 4.0.1

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 (486) hide show
  1. package/CHANGELOG.md +2901 -0
  2. package/_check.sh +1 -1
  3. package/dist/CUPS.d.ts +28 -9
  4. package/dist/CUPS.d.ts.map +1 -1
  5. package/dist/CUPS.js +35 -14
  6. package/dist/ClusterCosmos.d.ts +67 -0
  7. package/dist/ClusterCosmos.d.ts.map +1 -0
  8. package/dist/ClusterCosmos.js +588 -0
  9. package/dist/ClusterServiceBus.d.ts +67 -0
  10. package/dist/ClusterServiceBus.d.ts.map +1 -0
  11. package/dist/ClusterServiceBus.js +84 -0
  12. package/dist/{api/ContextProvider.d.ts → ContextProvider.d.ts} +7 -4
  13. package/dist/ContextProvider.d.ts.map +1 -0
  14. package/dist/ContextProvider.js +40 -0
  15. package/dist/Emailer/Sendgrid.d.ts +110 -146
  16. package/dist/Emailer/Sendgrid.d.ts.map +1 -1
  17. package/dist/Emailer/Sendgrid.js +24 -19
  18. package/dist/Emailer/fake.d.ts +1 -1
  19. package/dist/Emailer/fake.d.ts.map +1 -1
  20. package/dist/Emailer/fake.js +4 -4
  21. package/dist/MainFiberSet.d.ts +11 -7
  22. package/dist/MainFiberSet.d.ts.map +1 -1
  23. package/dist/MainFiberSet.js +22 -6
  24. package/dist/QueueMaker/errors.d.ts +3 -1
  25. package/dist/QueueMaker/errors.d.ts.map +1 -1
  26. package/dist/QueueMaker/errors.js +4 -2
  27. package/dist/{adapters/SQL → SQL}/Model.d.ts +34 -45
  28. package/dist/SQL/Model.d.ts.map +1 -0
  29. package/dist/SQL/Model.js +469 -0
  30. package/dist/SQL.d.ts.map +1 -0
  31. package/dist/{adapters/SQL.js → SQL.js} +1 -1
  32. package/dist/Store/Cosmos/query.d.ts +16 -4
  33. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  34. package/dist/Store/Cosmos/query.js +240 -44
  35. package/dist/Store/Cosmos.d.ts +2 -2
  36. package/dist/Store/Cosmos.d.ts.map +1 -1
  37. package/dist/Store/Cosmos.js +394 -320
  38. package/dist/Store/Disk.d.ts +4 -4
  39. package/dist/Store/Disk.d.ts.map +1 -1
  40. package/dist/Store/Disk.js +91 -39
  41. package/dist/Store/Memory.d.ts +7 -10
  42. package/dist/Store/Memory.d.ts.map +1 -1
  43. package/dist/Store/Memory.js +347 -78
  44. package/dist/Store/SQL/Pg.d.ts +2 -2
  45. package/dist/Store/SQL/Pg.d.ts.map +1 -1
  46. package/dist/Store/SQL/Pg.js +230 -155
  47. package/dist/Store/SQL/query.d.ts +19 -3
  48. package/dist/Store/SQL/query.d.ts.map +1 -1
  49. package/dist/Store/SQL/query.js +308 -14
  50. package/dist/Store/SQL.d.ts +22 -5
  51. package/dist/Store/SQL.d.ts.map +1 -1
  52. package/dist/Store/SQL.js +370 -99
  53. package/dist/Store/codeFilter.d.ts +5 -5
  54. package/dist/Store/codeFilter.d.ts.map +1 -1
  55. package/dist/Store/codeFilter.js +72 -23
  56. package/dist/Store/index.d.ts +6 -4
  57. package/dist/Store/index.d.ts.map +1 -1
  58. package/dist/Store/index.js +9 -6
  59. package/dist/Store/jsonDocument.d.ts +67 -0
  60. package/dist/Store/jsonDocument.d.ts.map +1 -0
  61. package/dist/Store/jsonDocument.js +107 -0
  62. package/dist/Store/utils.d.ts +21 -2
  63. package/dist/Store/utils.d.ts.map +1 -1
  64. package/dist/Store/utils.js +253 -10
  65. package/dist/WorkflowEngineCosmos.d.ts +30 -0
  66. package/dist/WorkflowEngineCosmos.d.ts.map +1 -0
  67. package/dist/WorkflowEngineCosmos.js +542 -0
  68. package/dist/WorkflowEngineSqlite.d.ts +24 -0
  69. package/dist/WorkflowEngineSqlite.d.ts.map +1 -0
  70. package/dist/WorkflowEngineSqlite.js +550 -0
  71. package/dist/arbs.d.ts +6 -3
  72. package/dist/arbs.d.ts.map +1 -1
  73. package/dist/arbs.js +20 -4
  74. package/dist/codec.d.ts.map +1 -0
  75. package/dist/codec.js +5 -0
  76. package/dist/cosmos-client.d.ts +40 -0
  77. package/dist/cosmos-client.d.ts.map +1 -0
  78. package/dist/cosmos-client.js +41 -0
  79. package/dist/errorReporter.d.ts +6 -4
  80. package/dist/errorReporter.d.ts.map +1 -1
  81. package/dist/errorReporter.js +23 -27
  82. package/dist/fileUtil.d.ts +1 -1
  83. package/dist/fileUtil.d.ts.map +1 -1
  84. package/dist/fileUtil.js +2 -2
  85. package/dist/index.d.ts +2 -1
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +3 -2
  88. package/dist/internal/RequestContextMiddleware.d.ts +5 -0
  89. package/dist/internal/RequestContextMiddleware.d.ts.map +1 -0
  90. package/dist/internal/RequestContextMiddleware.js +45 -0
  91. package/dist/internal/auth.d.ts +53 -0
  92. package/dist/internal/auth.d.ts.map +1 -0
  93. package/dist/internal/auth.js +180 -0
  94. package/dist/internal/events.d.ts +11 -0
  95. package/dist/internal/events.d.ts.map +1 -0
  96. package/dist/internal/events.js +49 -0
  97. package/dist/{api/internal → internal}/health.d.ts +1 -1
  98. package/dist/internal/health.d.ts.map +1 -0
  99. package/dist/internal/health.js +5 -0
  100. package/dist/{api/layerUtils.d.ts → layerUtils.d.ts} +10 -2
  101. package/dist/layerUtils.d.ts.map +1 -0
  102. package/dist/layerUtils.js +17 -0
  103. package/dist/logger/jsonLogger.d.ts +1 -1
  104. package/dist/logger/jsonLogger.d.ts.map +1 -1
  105. package/dist/logger/jsonLogger.js +5 -3
  106. package/dist/logger/logFmtLogger.d.ts +1 -1
  107. package/dist/logger/logFmtLogger.d.ts.map +1 -1
  108. package/dist/logger/logFmtLogger.js +3 -3
  109. package/dist/logger/shared.d.ts +2 -2
  110. package/dist/logger/shared.d.ts.map +1 -1
  111. package/dist/logger/shared.js +6 -6
  112. package/dist/logger.d.ts.map +1 -1
  113. package/dist/middlewares.d.ts.map +1 -0
  114. package/dist/{api/middlewares.js → middlewares.js} +1 -1
  115. package/dist/{adapters/mongo-client.d.ts → mongo-client.d.ts} +2 -1
  116. package/dist/mongo-client.d.ts.map +1 -0
  117. package/dist/mongo-client.js +15 -0
  118. package/dist/otel.d.ts +75 -0
  119. package/dist/otel.d.ts.map +1 -0
  120. package/dist/otel.js +84 -0
  121. package/dist/rateLimit.d.ts +11 -3
  122. package/dist/rateLimit.d.ts.map +1 -1
  123. package/dist/rateLimit.js +7 -12
  124. package/dist/{adapters/redis-client.d.ts → redis-client.d.ts} +4 -1
  125. package/dist/redis-client.d.ts.map +1 -0
  126. package/dist/redis-client.js +98 -0
  127. package/dist/{api/reportError.d.ts → reportError.d.ts} +1 -1
  128. package/dist/reportError.d.ts.map +1 -0
  129. package/dist/reportError.js +28 -0
  130. package/dist/{api/routing → routing}/middleware/RouterMiddleware.d.ts +2 -1
  131. package/dist/routing/middleware/RouterMiddleware.d.ts.map +1 -0
  132. package/dist/{api/routing → routing}/middleware/RouterMiddleware.js +1 -1
  133. package/dist/{api/routing → routing}/middleware/middleware.d.ts +11 -6
  134. package/dist/routing/middleware/middleware.d.ts.map +1 -0
  135. package/dist/routing/middleware/middleware.js +152 -0
  136. package/dist/{api/routing → routing}/middleware.d.ts +0 -1
  137. package/dist/routing/middleware.d.ts.map +1 -0
  138. package/dist/{api/routing → routing}/middleware.js +1 -2
  139. package/dist/routing/schema/jwt.d.ts.map +1 -0
  140. package/dist/routing/schema/jwt.js +12 -0
  141. package/dist/routing/tsort.d.ts.map +1 -0
  142. package/dist/routing/tsort.js +51 -0
  143. package/dist/{api/routing → routing}/utils.d.ts +3 -3
  144. package/dist/routing/utils.d.ts.map +1 -0
  145. package/dist/routing/utils.js +45 -0
  146. package/dist/routing.d.ts +182 -0
  147. package/dist/routing.d.ts.map +1 -0
  148. package/dist/routing.js +257 -0
  149. package/dist/test.d.ts +2 -2
  150. package/dist/test.d.ts.map +1 -1
  151. package/dist/test.js +5 -5
  152. package/dist/util.d.ts.map +1 -0
  153. package/dist/util.js +14 -0
  154. package/docs/cluster-storage.md +26 -0
  155. package/docs/workflow-engine.md +262 -0
  156. package/examples/query.ts +23 -19
  157. package/package.json +36 -364
  158. package/src/CUPS.ts +53 -14
  159. package/src/ClusterCosmos.ts +1170 -0
  160. package/src/ClusterServiceBus.ts +244 -0
  161. package/src/{api/ContextProvider.ts → ContextProvider.ts} +10 -7
  162. package/src/Emailer/Sendgrid.ts +83 -60
  163. package/src/Emailer/fake.ts +4 -4
  164. package/src/MainFiberSet.ts +31 -12
  165. package/src/QueueMaker/errors.ts +3 -1
  166. package/src/{adapters/SQL → SQL}/Model.ts +48 -47
  167. package/src/SQL.ts +1 -0
  168. package/src/Store/Cosmos/query.ts +277 -57
  169. package/src/Store/Cosmos.ts +597 -512
  170. package/src/Store/Disk.ts +145 -71
  171. package/src/Store/Memory.ts +419 -115
  172. package/src/Store/SQL/Pg.ts +352 -269
  173. package/src/Store/SQL/query.ts +368 -22
  174. package/src/Store/SQL.ts +742 -294
  175. package/src/Store/codeFilter.ts +79 -29
  176. package/src/Store/index.ts +18 -13
  177. package/src/Store/jsonDocument.ts +143 -0
  178. package/src/Store/utils.ts +307 -30
  179. package/src/WorkflowEngineCosmos.ts +749 -0
  180. package/src/WorkflowEngineSqlite.ts +813 -0
  181. package/src/arbs.ts +25 -5
  182. package/src/cosmos-client.ts +72 -0
  183. package/src/errorReporter.ts +70 -78
  184. package/src/fileUtil.ts +1 -1
  185. package/src/index.ts +2 -1
  186. package/src/{api/internal → internal}/RequestContextMiddleware.ts +23 -6
  187. package/src/internal/auth.ts +272 -0
  188. package/src/{api/internal → internal}/events.ts +18 -9
  189. package/src/{api/layerUtils.ts → layerUtils.ts} +7 -3
  190. package/src/logger/jsonLogger.ts +5 -3
  191. package/src/logger/logFmtLogger.ts +3 -3
  192. package/src/logger/shared.ts +6 -5
  193. package/src/middlewares.ts +10 -0
  194. package/src/{adapters/mongo-client.ts → mongo-client.ts} +3 -1
  195. package/src/otel.ts +177 -0
  196. package/src/rateLimit.ts +35 -24
  197. package/src/{adapters/redis-client.ts → redis-client.ts} +6 -2
  198. package/src/{api/reportError.ts → reportError.ts} +3 -2
  199. package/src/{api/routing → routing}/middleware/RouterMiddleware.ts +2 -1
  200. package/src/{api/routing → routing}/middleware/middleware.ts +75 -50
  201. package/src/routing/middleware.ts +4 -0
  202. package/src/{api/routing → routing}/schema/jwt.ts +2 -2
  203. package/src/{api/routing → routing}/utils.ts +2 -1
  204. package/src/routing.ts +827 -0
  205. package/src/test.ts +6 -6
  206. package/src/vitest.ts +1 -1
  207. package/test/arbs-magnitude.test.ts +33 -0
  208. package/test/arbs.test.ts +41 -0
  209. package/test/auth.test.ts +101 -0
  210. package/test/cluster-cosmos.test.ts +634 -0
  211. package/test/cluster-servicebus.test.ts +180 -0
  212. package/test/cluster-sqlite.test.ts +207 -0
  213. package/test/contextProvider.test.ts +11 -8
  214. package/test/controller.test.ts +23 -28
  215. package/test/cosmos-container-throughput.test.ts +80 -0
  216. package/test/cosmos-query.test.ts +368 -0
  217. package/test/dist/arbs-magnitude.test.d.ts.map +1 -0
  218. package/test/dist/arbs.test.d.ts.map +1 -0
  219. package/test/dist/auth.test.d.ts.map +1 -0
  220. package/test/dist/cluster-cosmos.test.d.ts.map +1 -0
  221. package/test/dist/cluster-servicebus.test.d.ts.map +1 -0
  222. package/test/dist/cluster-sqlite.test.d.ts.map +1 -0
  223. package/test/dist/contextProvider.test.d.ts.map +1 -1
  224. package/test/dist/controller.test.d.ts.map +1 -1
  225. package/test/dist/cosmos-container-throughput.test.d.ts.map +1 -0
  226. package/test/dist/cosmos-query.test.d.ts.map +1 -0
  227. package/test/dist/fixtures.d.ts +17 -9
  228. package/test/dist/fixtures.d.ts.map +1 -1
  229. package/test/dist/fixtures.js +8 -3
  230. package/test/dist/json-lower.test.d.ts.map +1 -0
  231. package/test/dist/query.test.d.ts.map +1 -1
  232. package/test/dist/rawQuery.test.d.ts.map +1 -1
  233. package/test/dist/repository-ext.test.d.ts.map +1 -0
  234. package/test/dist/repository-legacy-document.test.d.ts.map +1 -0
  235. package/test/dist/requires.test.d.ts.map +1 -1
  236. package/test/dist/router-generator.test.d.ts.map +1 -0
  237. package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
  238. package/test/dist/rpc-context-map-streaming.test.d.ts.map +1 -0
  239. package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
  240. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  241. package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
  242. package/test/dist/workflow-engine-cosmos.test.d.ts.map +1 -0
  243. package/test/dist/workflow-engine-sqlite.test.d.ts.map +1 -0
  244. package/test/fixtures.ts +7 -2
  245. package/test/json-lower.test.ts +129 -0
  246. package/test/layerUtils.test.ts +2 -2
  247. package/test/query.test.ts +1232 -42
  248. package/test/rawQuery.test.ts +432 -7
  249. package/test/repository-ext.test.ts +456 -0
  250. package/test/repository-legacy-document.test.ts +394 -0
  251. package/test/requires.test.ts +7 -3
  252. package/test/router-generator.test.ts +187 -0
  253. package/test/routing-interruptibility.test.ts +66 -0
  254. package/test/rpc-context-map-streaming.test.ts +265 -0
  255. package/test/rpc-e2e-invalidation.test.ts +421 -0
  256. package/test/rpc-multi-middleware.test.ts +17 -11
  257. package/test/rpc-stream-fullstack.test.ts +307 -0
  258. package/test/sql-store.test.ts +1202 -47
  259. package/test/validateSample.test.ts +70 -42
  260. package/test/workflow-engine-cosmos.test.ts +345 -0
  261. package/test/workflow-engine-sqlite.test.ts +297 -0
  262. package/tsconfig.examples.json +1 -1
  263. package/tsconfig.json +9 -1
  264. package/tsconfig.src.json +35 -35
  265. package/tsconfig.test.json +2 -2
  266. package/dist/Emailer/service.d.ts +0 -55
  267. package/dist/Emailer/service.d.ts.map +0 -1
  268. package/dist/Emailer/service.js +0 -6
  269. package/dist/Emailer.d.ts +0 -2
  270. package/dist/Emailer.d.ts.map +0 -1
  271. package/dist/Emailer.js +0 -2
  272. package/dist/Model/Repository/ext.d.ts +0 -41
  273. package/dist/Model/Repository/ext.d.ts.map +0 -1
  274. package/dist/Model/Repository/ext.js +0 -65
  275. package/dist/Model/Repository/internal/internal.d.ts +0 -59
  276. package/dist/Model/Repository/internal/internal.d.ts.map +0 -1
  277. package/dist/Model/Repository/internal/internal.js +0 -320
  278. package/dist/Model/Repository/legacy.d.ts +0 -19
  279. package/dist/Model/Repository/legacy.d.ts.map +0 -1
  280. package/dist/Model/Repository/legacy.js +0 -2
  281. package/dist/Model/Repository/makeRepo.d.ts +0 -49
  282. package/dist/Model/Repository/makeRepo.d.ts.map +0 -1
  283. package/dist/Model/Repository/makeRepo.js +0 -24
  284. package/dist/Model/Repository/service.d.ts +0 -89
  285. package/dist/Model/Repository/service.d.ts.map +0 -1
  286. package/dist/Model/Repository/service.js +0 -2
  287. package/dist/Model/Repository/validation.d.ts +0 -43
  288. package/dist/Model/Repository/validation.d.ts.map +0 -1
  289. package/dist/Model/Repository/validation.js +0 -32
  290. package/dist/Model/Repository.d.ts +0 -6
  291. package/dist/Model/Repository.d.ts.map +0 -1
  292. package/dist/Model/Repository.js +0 -6
  293. package/dist/Model/dsl.d.ts +0 -32
  294. package/dist/Model/dsl.d.ts.map +0 -1
  295. package/dist/Model/dsl.js +0 -44
  296. package/dist/Model/filter/filterApi.d.ts +0 -30
  297. package/dist/Model/filter/filterApi.d.ts.map +0 -1
  298. package/dist/Model/filter/filterApi.js +0 -2
  299. package/dist/Model/filter/types/errors.d.ts +0 -29
  300. package/dist/Model/filter/types/errors.d.ts.map +0 -1
  301. package/dist/Model/filter/types/errors.js +0 -2
  302. package/dist/Model/filter/types/fields.d.ts +0 -15
  303. package/dist/Model/filter/types/fields.d.ts.map +0 -1
  304. package/dist/Model/filter/types/fields.js +0 -2
  305. package/dist/Model/filter/types/path/common.d.ts +0 -316
  306. package/dist/Model/filter/types/path/common.d.ts.map +0 -1
  307. package/dist/Model/filter/types/path/common.js +0 -2
  308. package/dist/Model/filter/types/path/eager.d.ts +0 -95
  309. package/dist/Model/filter/types/path/eager.d.ts.map +0 -1
  310. package/dist/Model/filter/types/path/eager.js +0 -31
  311. package/dist/Model/filter/types/path/index.d.ts +0 -4
  312. package/dist/Model/filter/types/path/index.d.ts.map +0 -1
  313. package/dist/Model/filter/types/path/index.js +0 -3
  314. package/dist/Model/filter/types/utils.d.ts +0 -79
  315. package/dist/Model/filter/types/utils.d.ts.map +0 -1
  316. package/dist/Model/filter/types/utils.js +0 -2
  317. package/dist/Model/filter/types/validator.d.ts +0 -30
  318. package/dist/Model/filter/types/validator.d.ts.map +0 -1
  319. package/dist/Model/filter/types/validator.js +0 -2
  320. package/dist/Model/filter/types.d.ts +0 -5
  321. package/dist/Model/filter/types.d.ts.map +0 -1
  322. package/dist/Model/filter/types.js +0 -7
  323. package/dist/Model/query/dsl.d.ts +0 -248
  324. package/dist/Model/query/dsl.d.ts.map +0 -1
  325. package/dist/Model/query/dsl.js +0 -104
  326. package/dist/Model/query/new-kid-interpreter.d.ts +0 -28
  327. package/dist/Model/query/new-kid-interpreter.d.ts.map +0 -1
  328. package/dist/Model/query/new-kid-interpreter.js +0 -165
  329. package/dist/Model/query.d.ts +0 -15
  330. package/dist/Model/query.d.ts.map +0 -1
  331. package/dist/Model/query.js +0 -3
  332. package/dist/Model.d.ts +0 -4
  333. package/dist/Model.d.ts.map +0 -1
  334. package/dist/Model.js +0 -4
  335. package/dist/Operations.d.ts +0 -55
  336. package/dist/Operations.d.ts.map +0 -1
  337. package/dist/Operations.js +0 -102
  338. package/dist/OperationsRepo.d.ts +0 -41
  339. package/dist/OperationsRepo.d.ts.map +0 -1
  340. package/dist/OperationsRepo.js +0 -14
  341. package/dist/QueueMaker/SQLQueue.d.ts +0 -14
  342. package/dist/QueueMaker/SQLQueue.d.ts.map +0 -1
  343. package/dist/QueueMaker/SQLQueue.js +0 -132
  344. package/dist/QueueMaker/memQueue.d.ts +0 -11
  345. package/dist/QueueMaker/memQueue.d.ts.map +0 -1
  346. package/dist/QueueMaker/memQueue.js +0 -76
  347. package/dist/QueueMaker/sbqueue.d.ts +0 -12
  348. package/dist/QueueMaker/sbqueue.d.ts.map +0 -1
  349. package/dist/QueueMaker/sbqueue.js +0 -91
  350. package/dist/QueueMaker/service.d.ts +0 -11
  351. package/dist/QueueMaker/service.d.ts.map +0 -1
  352. package/dist/QueueMaker/service.js +0 -4
  353. package/dist/RequestContext.d.ts +0 -68
  354. package/dist/RequestContext.d.ts.map +0 -1
  355. package/dist/RequestContext.js +0 -49
  356. package/dist/RequestFiberSet.d.ts +0 -36
  357. package/dist/RequestFiberSet.d.ts.map +0 -1
  358. package/dist/RequestFiberSet.js +0 -78
  359. package/dist/Store/ContextMapContainer.d.ts +0 -25
  360. package/dist/Store/ContextMapContainer.d.ts.map +0 -1
  361. package/dist/Store/ContextMapContainer.js +0 -77
  362. package/dist/Store/service.d.ts +0 -111
  363. package/dist/Store/service.d.ts.map +0 -1
  364. package/dist/Store/service.js +0 -79
  365. package/dist/Store.d.ts +0 -2
  366. package/dist/Store.d.ts.map +0 -1
  367. package/dist/Store.js +0 -2
  368. package/dist/adapters/SQL/Model.d.ts.map +0 -1
  369. package/dist/adapters/SQL/Model.js +0 -478
  370. package/dist/adapters/SQL.d.ts.map +0 -1
  371. package/dist/adapters/ServiceBus.d.ts +0 -58
  372. package/dist/adapters/ServiceBus.d.ts.map +0 -1
  373. package/dist/adapters/ServiceBus.js +0 -99
  374. package/dist/adapters/cosmos-client.d.ts +0 -14
  375. package/dist/adapters/cosmos-client.d.ts.map +0 -1
  376. package/dist/adapters/cosmos-client.js +0 -9
  377. package/dist/adapters/index.d.ts +0 -2
  378. package/dist/adapters/index.d.ts.map +0 -1
  379. package/dist/adapters/index.js +0 -2
  380. package/dist/adapters/logger.d.ts +0 -9
  381. package/dist/adapters/logger.d.ts.map +0 -1
  382. package/dist/adapters/logger.js +0 -3
  383. package/dist/adapters/memQueue.d.ts +0 -13
  384. package/dist/adapters/memQueue.d.ts.map +0 -1
  385. package/dist/adapters/memQueue.js +0 -20
  386. package/dist/adapters/mongo-client.d.ts.map +0 -1
  387. package/dist/adapters/mongo-client.js +0 -13
  388. package/dist/adapters/redis-client.d.ts.map +0 -1
  389. package/dist/adapters/redis-client.js +0 -94
  390. package/dist/api/ContextProvider.d.ts.map +0 -1
  391. package/dist/api/ContextProvider.js +0 -38
  392. package/dist/api/codec.d.ts.map +0 -1
  393. package/dist/api/codec.js +0 -5
  394. package/dist/api/internal/RequestContextMiddleware.d.ts +0 -5
  395. package/dist/api/internal/RequestContextMiddleware.d.ts.map +0 -1
  396. package/dist/api/internal/RequestContextMiddleware.js +0 -35
  397. package/dist/api/internal/auth.d.ts +0 -15
  398. package/dist/api/internal/auth.d.ts.map +0 -1
  399. package/dist/api/internal/auth.js +0 -47
  400. package/dist/api/internal/events.d.ts +0 -9
  401. package/dist/api/internal/events.d.ts.map +0 -1
  402. package/dist/api/internal/events.js +0 -42
  403. package/dist/api/internal/health.d.ts.map +0 -1
  404. package/dist/api/internal/health.js +0 -5
  405. package/dist/api/layerUtils.d.ts.map +0 -1
  406. package/dist/api/layerUtils.js +0 -16
  407. package/dist/api/middlewares.d.ts.map +0 -1
  408. package/dist/api/reportError.d.ts.map +0 -1
  409. package/dist/api/reportError.js +0 -27
  410. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +0 -1
  411. package/dist/api/routing/middleware/middleware.d.ts.map +0 -1
  412. package/dist/api/routing/middleware/middleware.js +0 -130
  413. package/dist/api/routing/middleware.d.ts.map +0 -1
  414. package/dist/api/routing/schema/jwt.d.ts.map +0 -1
  415. package/dist/api/routing/schema/jwt.js +0 -12
  416. package/dist/api/routing/tsort.d.ts.map +0 -1
  417. package/dist/api/routing/tsort.js +0 -51
  418. package/dist/api/routing/utils.d.ts.map +0 -1
  419. package/dist/api/routing/utils.js +0 -44
  420. package/dist/api/routing.d.ts +0 -134
  421. package/dist/api/routing.d.ts.map +0 -1
  422. package/dist/api/routing.js +0 -167
  423. package/dist/api/setupRequest.d.ts +0 -15
  424. package/dist/api/setupRequest.d.ts.map +0 -1
  425. package/dist/api/setupRequest.js +0 -49
  426. package/dist/api/util.d.ts.map +0 -1
  427. package/dist/api/util.js +0 -14
  428. package/eslint.config.mjs +0 -24
  429. package/src/Emailer/service.ts +0 -52
  430. package/src/Emailer.ts +0 -1
  431. package/src/Model/Repository/ext.ts +0 -283
  432. package/src/Model/Repository/internal/internal.ts +0 -580
  433. package/src/Model/Repository/legacy.ts +0 -27
  434. package/src/Model/Repository/makeRepo.ts +0 -139
  435. package/src/Model/Repository/service.ts +0 -627
  436. package/src/Model/Repository/validation.ts +0 -31
  437. package/src/Model/Repository.ts +0 -5
  438. package/src/Model/dsl.ts +0 -128
  439. package/src/Model/filter/filterApi.ts +0 -60
  440. package/src/Model/filter/types/errors.ts +0 -47
  441. package/src/Model/filter/types/fields.ts +0 -50
  442. package/src/Model/filter/types/path/common.ts +0 -404
  443. package/src/Model/filter/types/path/eager.ts +0 -298
  444. package/src/Model/filter/types/path/index.ts +0 -4
  445. package/src/Model/filter/types/utils.ts +0 -128
  446. package/src/Model/filter/types/validator.ts +0 -46
  447. package/src/Model/filter/types.ts +0 -6
  448. package/src/Model/query/dsl.ts +0 -2110
  449. package/src/Model/query/new-kid-interpreter.ts +0 -210
  450. package/src/Model/query.ts +0 -13
  451. package/src/Model.ts +0 -3
  452. package/src/Operations.ts +0 -235
  453. package/src/OperationsRepo.ts +0 -16
  454. package/src/QueueMaker/SQLQueue.ts +0 -190
  455. package/src/QueueMaker/memQueue.ts +0 -130
  456. package/src/QueueMaker/sbqueue.ts +0 -137
  457. package/src/QueueMaker/service.ts +0 -17
  458. package/src/RequestContext.ts +0 -63
  459. package/src/RequestFiberSet.ts +0 -105
  460. package/src/Store/ContextMapContainer.ts +0 -116
  461. package/src/Store/index.test.ts.bak +0 -112
  462. package/src/Store/service.ts +0 -193
  463. package/src/Store.ts +0 -1
  464. package/src/adapters/SQL.ts +0 -1
  465. package/src/adapters/ServiceBus.ts +0 -209
  466. package/src/adapters/cosmos-client.ts +0 -16
  467. package/src/adapters/index.ts +0 -0
  468. package/src/adapters/logger.ts +0 -3
  469. package/src/adapters/memQueue.ts +0 -21
  470. package/src/api/internal/auth.ts +0 -68
  471. package/src/api/middlewares.ts +0 -10
  472. package/src/api/routing/middleware.ts +0 -6
  473. package/src/api/routing.ts +0 -598
  474. package/src/api/setupRequest.ts +0 -104
  475. package/test/dist/date-query.test.d.ts.map +0 -1
  476. package/tsconfig.json.bak +0 -12
  477. /package/dist/{adapters/SQL.d.ts → SQL.d.ts} +0 -0
  478. /package/dist/{api/codec.d.ts → codec.d.ts} +0 -0
  479. /package/dist/{api/middlewares.d.ts → middlewares.d.ts} +0 -0
  480. /package/dist/{api/routing → routing}/schema/jwt.d.ts +0 -0
  481. /package/dist/{api/routing → routing}/tsort.d.ts +0 -0
  482. /package/dist/{api/util.d.ts → util.d.ts} +0 -0
  483. /package/src/{api/codec.ts → codec.ts} +0 -0
  484. /package/src/{api/internal → internal}/health.ts +0 -0
  485. /package/src/{api/routing → routing}/tsort.ts +0 -0
  486. /package/src/{api/util.ts → util.ts} +0 -0
@@ -1,10 +1,11 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  import type Sqlite from "better-sqlite3"
3
3
  import BetterSqlite from "better-sqlite3"
4
+ import * as S from "effect-app/Schema"
4
5
  import { describe, expect, it } from "vitest"
5
6
  import { parseRow } from "../src/Store/SQL.js"
6
7
  import { buildWhereSQLQuery, pgDialect, sqliteDialect } from "../src/Store/SQL/query.js"
7
- import { makeETag } from "../src/Store/utils.js"
8
+ import { makeETag, makeJsonLower } from "../src/Store/utils.js"
8
9
 
9
10
  const query = (db: Sqlite.Database, sql: string, params: unknown[] = []) =>
10
11
  db.prepare(sql).all(...params as any[]) as any[]
@@ -24,11 +25,182 @@ describe("SQL query builder (SQLite dialect)", () => {
24
25
  expect(result.params).toContain("John")
25
26
  })
26
27
 
28
+ it("where eq Date binds ISO string", () => {
29
+ const result = buildWhereSQLQuery(
30
+ sqliteDialect,
31
+ "id",
32
+ [{ t: "where", path: "n", op: "eq", value: new Date("2024-01-01T00:00:00.000Z") }],
33
+ "users",
34
+ {}
35
+ )
36
+ expect(result.params).toContain("2024-01-01T00:00:00.000Z")
37
+ })
38
+
39
+ it("where eq app native Encoded binds via JsonLower", () => {
40
+ class Day {
41
+ readonly ymd: string
42
+ constructor(ymd: string) {
43
+ this.ymd = ymd
44
+ }
45
+ }
46
+ const day = new Day("2024-06-01")
47
+ const json = {
48
+ toJson: (v: unknown) => v instanceof Day ? v.ymd : v,
49
+ jsonifyFilter: (filter: readonly { t: string; path?: string; op?: string; value?: unknown }[]) =>
50
+ filter.map((r) => r.t === "where" ? { ...r, value: r.value instanceof Day ? r.value.ymd : r.value } : r)
51
+ }
52
+ const result = buildWhereSQLQuery(
53
+ sqliteDialect,
54
+ "id",
55
+ [{ t: "where", path: "day", op: "eq", value: day }],
56
+ "users",
57
+ {},
58
+ undefined,
59
+ undefined,
60
+ undefined,
61
+ undefined,
62
+ undefined,
63
+ json as never
64
+ )
65
+ expect(result.params).toContain("2024-06-01")
66
+ })
67
+
68
+ it("does not re-encode store-lowered Date defaultValues", () => {
69
+ class Item extends S.Class<Item>("SqlDefaultValuesItem")({
70
+ id: S.String,
71
+ at: S.Date
72
+ }) {}
73
+ const json = makeJsonLower({ schema: Item })
74
+ const at = new Date("2024-06-01T00:00:00.000Z")
75
+ const lowered = json.toJson({ at }) as Record<string, unknown>
76
+ const result = buildWhereSQLQuery(
77
+ sqliteDialect,
78
+ "id",
79
+ [{ t: "where", path: "at", op: "eq", value: at }],
80
+ "items",
81
+ lowered,
82
+ undefined,
83
+ undefined,
84
+ undefined,
85
+ undefined,
86
+ undefined,
87
+ json
88
+ )
89
+ expect(result.sql).toContain("COALESCE(")
90
+ expect(result.params).toContain("2024-06-01T00:00:00.000Z")
91
+ })
92
+
93
+ it("where in Set binds array values", () => {
94
+ const result = buildWhereSQLQuery(
95
+ sqliteDialect,
96
+ "id",
97
+ [{ t: "where", path: "tags", op: "in", value: new Set(["a", "b"]) }],
98
+ "users",
99
+ {}
100
+ )
101
+ expect(result.params).toEqual(expect.arrayContaining(["a", "b"]))
102
+ })
103
+
104
+ it("where includes Date binds ISO string", () => {
105
+ const result = buildWhereSQLQuery(
106
+ sqliteDialect,
107
+ "id",
108
+ [{ t: "where", path: "dates", op: "includes", value: new Date("2024-01-01T00:00:00.000Z") }],
109
+ "users",
110
+ {}
111
+ )
112
+ expect(result.params).toContain("2024-01-01T00:00:00.000Z")
113
+ })
114
+
115
+ it("where hasKey / hasValue / hasKeyValue on Map JSON tuples", () => {
116
+ const key = buildWhereSQLQuery(
117
+ sqliteDialect,
118
+ "id",
119
+ [{ t: "where", path: "meta", op: "hasKey", value: "n" }],
120
+ "users",
121
+ {}
122
+ )
123
+ expect(key.sql).toContain("json_extract(value, '$[0]')")
124
+ expect(key.params).toContain("n")
125
+
126
+ const value = buildWhereSQLQuery(
127
+ sqliteDialect,
128
+ "id",
129
+ [{ t: "where", path: "meta", op: "hasValue", value: 1 }],
130
+ "users",
131
+ {}
132
+ )
133
+ expect(value.sql).toContain("json_extract(value, '$[1]')")
134
+ expect(value.params).toContain(1)
135
+
136
+ const pair = buildWhereSQLQuery(
137
+ sqliteDialect,
138
+ "id",
139
+ [{ t: "where", path: "meta", op: "hasKeyValue", value: ["n", 1] }],
140
+ "users",
141
+ {}
142
+ )
143
+ expect(pair.sql).toContain("json_each")
144
+ expect(pair.params).toContain(JSON.stringify(["n", 1]))
145
+
146
+ const anyKeys = buildWhereSQLQuery(
147
+ sqliteDialect,
148
+ "id",
149
+ [{ t: "where", path: "meta", op: "hasKey-any", value: ["n", "x"] }],
150
+ "users",
151
+ {}
152
+ )
153
+ expect(anyKeys.sql).toContain(" OR ")
154
+ expect(anyKeys.params).toEqual(expect.arrayContaining(["n", "x"]))
155
+ })
156
+
157
+ it("pg where hasKey / hasKeyValue uses jsonb tuple elements", () => {
158
+ const key = buildWhereSQLQuery(
159
+ pgDialect,
160
+ "id",
161
+ [{ t: "where", path: "meta", op: "hasKey", value: "n" }],
162
+ "users",
163
+ {}
164
+ )
165
+ expect(key.sql).toContain("jsonb_array_elements")
166
+ expect(key.sql).toContain("e->0")
167
+ expect(key.params).toContain(JSON.stringify("n"))
168
+
169
+ const pair = buildWhereSQLQuery(
170
+ pgDialect,
171
+ "id",
172
+ [{ t: "where", path: "meta", op: "hasKeyValue", value: ["n", 1] }],
173
+ "users",
174
+ {}
175
+ )
176
+ expect(pair.sql).toContain("@>")
177
+ expect(pair.sql).toContain("jsonb_build_array")
178
+ expect(pair.params).toContain(JSON.stringify(["n", 1]))
179
+ })
180
+
181
+ it("where includes-any Date[] binds ISO strings", () => {
182
+ const result = buildWhereSQLQuery(
183
+ sqliteDialect,
184
+ "id",
185
+ [{
186
+ t: "where",
187
+ path: "dates",
188
+ op: "includes-any",
189
+ value: [new Date("2024-01-01T00:00:00.000Z"), new Date("2024-06-01T00:00:00.000Z")]
190
+ }],
191
+ "users",
192
+ {}
193
+ )
194
+ expect(result.params).toEqual(
195
+ expect.arrayContaining(["2024-01-01T00:00:00.000Z", "2024-06-01T00:00:00.000Z"])
196
+ )
197
+ })
198
+
27
199
  it("where eq number", () => {
28
200
  const result = buildWhereSQLQuery(
29
201
  sqliteDialect,
30
202
  "id",
31
- [{ t: "where", path: "age", op: "eq", value: 25 as any }],
203
+ [{ t: "where", path: "age", op: "eq", value: 25 }],
32
204
  "users",
33
205
  {}
34
206
  )
@@ -40,7 +212,7 @@ describe("SQL query builder (SQLite dialect)", () => {
40
212
  const result = buildWhereSQLQuery(
41
213
  sqliteDialect,
42
214
  "id",
43
- [{ t: "where", path: "age", op: "gt", value: 18 as any }],
215
+ [{ t: "where", path: "age", op: "gt", value: 18 }],
44
216
  "users",
45
217
  {}
46
218
  )
@@ -70,7 +242,7 @@ describe("SQL query builder (SQLite dialect)", () => {
70
242
  "id",
71
243
  [
72
244
  { t: "where", path: "name", op: "eq", value: "Alice" },
73
- { t: "and", path: "age", op: "gt", value: 18 as any }
245
+ { t: "and", path: "age", op: "gt", value: 18 }
74
246
  ],
75
247
  "users",
76
248
  {}
@@ -83,7 +255,7 @@ describe("SQL query builder (SQLite dialect)", () => {
83
255
  const result = buildWhereSQLQuery(
84
256
  sqliteDialect,
85
257
  "id",
86
- [{ t: "where", path: "id", op: "in", value: ["a", "b", "c"] as any }],
258
+ [{ t: "where", path: "id", op: "in", value: ["a", "b", "c"] }],
87
259
  "users",
88
260
  {}
89
261
  )
@@ -167,7 +339,7 @@ describe("SQL query builder (SQLite dialect)", () => {
167
339
  const result = buildWhereSQLQuery(
168
340
  sqliteDialect,
169
341
  "id",
170
- [{ t: "where", path: "tags", op: "includes-any", value: ["admin", "user"] as any }],
342
+ [{ t: "where", path: "tags", op: "includes-any", value: ["admin", "user"] }],
171
343
  "users",
172
344
  {}
173
345
  )
@@ -228,6 +400,241 @@ describe("SQL query builder (SQLite dialect)", () => {
228
400
  expect(result.sql).toContain("LIMIT")
229
401
  expect(result.sql).toContain("OFFSET")
230
402
  })
403
+
404
+ it("computed relation count projection", () => {
405
+ const result = buildWhereSQLQuery(
406
+ sqliteDialect,
407
+ "id",
408
+ [],
409
+ "users",
410
+ {},
411
+ [{
412
+ key: "pickedCount",
413
+ computed: {
414
+ _tag: "relation-count",
415
+ path: "items",
416
+ filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
417
+ }
418
+ }]
419
+ )
420
+ expect(result.sql).toContain(`SELECT COUNT(1) FROM json_each(data, '$.items') AS _items`)
421
+ expect(result.sql).toContain(`AS "pickedCount"`)
422
+ expect(result.params).toContain("%picked%")
423
+ })
424
+
425
+ it("computed relation any projection (sqlite bool encoding)", () => {
426
+ const result = buildWhereSQLQuery(
427
+ sqliteDialect,
428
+ "id",
429
+ [],
430
+ "users",
431
+ {},
432
+ [{
433
+ key: "hasPicked",
434
+ computed: {
435
+ _tag: "relation-any",
436
+ path: "items",
437
+ filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
438
+ }
439
+ }]
440
+ )
441
+ expect(result.sql).toContain("CASE WHEN EXISTS(")
442
+ expect(result.sql).toContain(`AS "hasPicked"`)
443
+ })
444
+
445
+ it("computed relation every projection (sqlite emits NOT EXISTS NOT)", () => {
446
+ const result = buildWhereSQLQuery(
447
+ sqliteDialect,
448
+ "id",
449
+ [],
450
+ "users",
451
+ {},
452
+ [{
453
+ key: "allPicked",
454
+ computed: {
455
+ _tag: "relation-every",
456
+ path: "items",
457
+ filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
458
+ }
459
+ }]
460
+ )
461
+ expect(result.sql).toContain(`NOT EXISTS(SELECT 1 FROM json_each(data, '$.items') AS _items WHERE NOT (`)
462
+ expect(result.sql).toContain(`AS "allPicked"`)
463
+ expect(result.params).toContain("picked")
464
+ })
465
+
466
+ it("computed relation-every with no filter degenerates to true", () => {
467
+ const result = buildWhereSQLQuery(
468
+ sqliteDialect,
469
+ "id",
470
+ [],
471
+ "users",
472
+ {},
473
+ [{
474
+ key: "allPicked",
475
+ computed: { _tag: "relation-every", path: "items", filter: [] }
476
+ }]
477
+ )
478
+ expect(result.sql).toContain("CASE WHEN 1=1")
479
+ expect(result.sql).toContain(`AS "allPicked"`)
480
+ })
481
+
482
+ it("computed relation-distinct-count projection (sqlite)", () => {
483
+ const result = buildWhereSQLQuery(
484
+ sqliteDialect,
485
+ "id",
486
+ [],
487
+ "users",
488
+ {},
489
+ [{
490
+ key: "positionCount",
491
+ computed: {
492
+ _tag: "relation-distinct-count",
493
+ path: "items",
494
+ field: "rowId",
495
+ filter: [{ t: "where", path: "items.-1.state._tag", op: "neq", value: "cancelled" }]
496
+ }
497
+ }]
498
+ )
499
+ expect(result.sql).toContain(`SELECT COUNT(DISTINCT json_extract(_items.value, '$.rowId'))`)
500
+ expect(result.sql).toContain(`AS "positionCount"`)
501
+ expect(result.params).toContain("cancelled")
502
+ })
503
+
504
+ it("computed relation-sum projection (sqlite casts to REAL)", () => {
505
+ const result = buildWhereSQLQuery(
506
+ sqliteDialect,
507
+ "id",
508
+ [],
509
+ "users",
510
+ {},
511
+ [{
512
+ key: "totalWeight",
513
+ computed: { _tag: "relation-sum", path: "items", field: "weight", filter: [] }
514
+ }]
515
+ )
516
+ expect(result.sql).toContain(`SELECT COALESCE(SUM(CAST(json_extract(_items.value, '$.weight') AS REAL)), 0)`)
517
+ expect(result.sql).toContain(`AS "totalWeight"`)
518
+ })
519
+
520
+ it("computed relation-sum-expr projection (sqlite)", () => {
521
+ const result = buildWhereSQLQuery(
522
+ sqliteDialect,
523
+ "id",
524
+ [],
525
+ "users",
526
+ {},
527
+ [{
528
+ key: "totalWeighted",
529
+ computed: {
530
+ _tag: "relation-sum-expr",
531
+ path: "items",
532
+ expression: {
533
+ _tag: "mul",
534
+ left: { _tag: "field", field: "weight" },
535
+ right: { _tag: "field", field: "tradeUnit.amount" }
536
+ },
537
+ filter: []
538
+ }
539
+ }]
540
+ )
541
+ expect(result.sql).toContain(
542
+ `COALESCE(SUM((CAST(json_extract(_items.value, '$.weight') AS REAL) * CAST(json_extract(_items.value, '$.tradeUnit.amount') AS REAL))), 0)`
543
+ )
544
+ expect(result.sql).toContain(`AS "totalWeighted"`)
545
+ })
546
+
547
+ it("computed relation-sum-expr-by projection (sqlite)", () => {
548
+ const result = buildWhereSQLQuery(
549
+ sqliteDialect,
550
+ "id",
551
+ [],
552
+ "users",
553
+ {},
554
+ [{
555
+ key: "totalsByUnit",
556
+ computed: {
557
+ _tag: "relation-sum-expr-by",
558
+ path: "items",
559
+ expression: {
560
+ _tag: "mul",
561
+ left: { _tag: "field", field: "weight" },
562
+ right: { _tag: "field", field: "tradeUnit.amount" }
563
+ },
564
+ unit: "tradeUnit.unit",
565
+ filter: []
566
+ }
567
+ }]
568
+ )
569
+ expect(result.sql).toContain(`json_group_array(json_object('unit', __unit, 'total', __total))`)
570
+ expect(result.sql).toContain(`GROUP BY json_extract(_items.value, '$.tradeUnit.unit')`)
571
+ expect(result.sql).toContain(`AS "totalsByUnit"`)
572
+ })
573
+
574
+ it("computed relation-sum-expr-normalized projection (sqlite)", () => {
575
+ const result = buildWhereSQLQuery(
576
+ sqliteDialect,
577
+ "id",
578
+ [],
579
+ "users",
580
+ {},
581
+ [{
582
+ key: "totalKg",
583
+ computed: {
584
+ _tag: "relation-sum-expr-normalized",
585
+ path: "items",
586
+ expression: {
587
+ _tag: "mul",
588
+ left: { _tag: "field", field: "weight" },
589
+ right: { _tag: "field", field: "tradeUnit.amount" }
590
+ },
591
+ unit: "tradeUnit.unit",
592
+ toBase: "kg",
593
+ factors: { g: 0.001 },
594
+ filter: []
595
+ }
596
+ }]
597
+ )
598
+ expect(result.sql).toContain(
599
+ `CASE json_extract(_items.value, '$.tradeUnit.unit') WHEN 'kg' THEN 1 WHEN 'g' THEN 0.001 ELSE NULL END`
600
+ )
601
+ expect(result.sql).toContain(`AS "totalKg"`)
602
+ })
603
+
604
+ it("computed relation-collect (non-distinct) projection (sqlite)", () => {
605
+ const result = buildWhereSQLQuery(
606
+ sqliteDialect,
607
+ "id",
608
+ [],
609
+ "users",
610
+ {},
611
+ [{
612
+ key: "tags",
613
+ computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: false, filter: [] }
614
+ }]
615
+ )
616
+ expect(result.sql).toContain(
617
+ `SELECT COALESCE(json_group_array(json_extract(_items.value, '$.articleId')), json_array())`
618
+ )
619
+ expect(result.sql).toContain(`AS "tags"`)
620
+ })
621
+
622
+ it("computed relation-collect (distinct) emits inner DISTINCT subquery (sqlite)", () => {
623
+ const result = buildWhereSQLQuery(
624
+ sqliteDialect,
625
+ "id",
626
+ [],
627
+ "users",
628
+ {},
629
+ [{
630
+ key: "tags",
631
+ computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: true, filter: [] }
632
+ }]
633
+ )
634
+ expect(result.sql).toContain(`json_group_array(__v)`)
635
+ expect(result.sql).toContain(`SELECT DISTINCT json_extract(_items.value, '$.articleId') AS __v`)
636
+ expect(result.sql).toContain(`AS "tags"`)
637
+ })
231
638
  })
232
639
 
233
640
  describe("SQL query builder (PostgreSQL dialect)", () => {
@@ -260,7 +667,7 @@ describe("SQL query builder (PostgreSQL dialect)", () => {
260
667
  const result = buildWhereSQLQuery(
261
668
  pgDialect,
262
669
  "id",
263
- [{ t: "where", path: "status", op: "in", value: ["active", "pending"] as any }],
670
+ [{ t: "where", path: "status", op: "in", value: ["active", "pending"] }],
264
671
  "users",
265
672
  {}
266
673
  )
@@ -291,33 +698,218 @@ describe("SQL query builder (PostgreSQL dialect)", () => {
291
698
  )
292
699
  expect(result.sql).toContain("data->'address'->>'city'")
293
700
  })
294
- })
295
701
 
296
- // --- Integration tests with in-memory SQLite (direct, no Effect SQL client) ---
702
+ it("computed relation any projection", () => {
703
+ const result = buildWhereSQLQuery(
704
+ pgDialect,
705
+ "id",
706
+ [],
707
+ "users",
708
+ {},
709
+ [{
710
+ key: "hasPicked",
711
+ computed: {
712
+ _tag: "relation-any",
713
+ path: "items",
714
+ filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
715
+ }
716
+ }]
717
+ )
718
+ expect(result.sql).toContain("EXISTS(SELECT 1 FROM jsonb_array_elements(data->'items') AS _items")
719
+ expect(result.sql).toContain(`AS "hasPicked"`)
720
+ })
297
721
 
298
- describe("SQL Store (SQLite integration)", () => {
299
- const withDb = (fn: (db: Sqlite.Database) => void) => {
300
- const db = new BetterSqlite(":memory:")
301
- db.pragma("journal_mode = WAL")
302
- try {
303
- fn(db)
304
- } finally {
305
- db.close()
306
- }
307
- }
722
+ it("computed relation-every (pg)", () => {
723
+ const result = buildWhereSQLQuery(
724
+ pgDialect,
725
+ "id",
726
+ [],
727
+ "users",
728
+ {},
729
+ [{
730
+ key: "allPicked",
731
+ computed: {
732
+ _tag: "relation-every",
733
+ path: "items",
734
+ filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
735
+ }
736
+ }]
737
+ )
738
+ expect(result.sql).toContain(`NOT EXISTS(SELECT 1 FROM jsonb_array_elements(data->'items') AS _items WHERE NOT (`)
739
+ expect(result.sql).toContain(`AS "allPicked"`)
740
+ })
308
741
 
309
- it("creates table and seeds data", () =>
310
- withDb((db) => {
311
- db.exec(
312
- `CREATE TABLE IF NOT EXISTS "test_items" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
313
- )
314
- db
315
- .prepare(`INSERT INTO "test_items" (id, _etag, data) VALUES (?, ?, ?)`)
316
- .run("1", "etag1", JSON.stringify({ name: "Alice", age: 30 }))
742
+ it("computed relation-distinct-count (pg)", () => {
743
+ const result = buildWhereSQLQuery(
744
+ pgDialect,
745
+ "id",
746
+ [],
747
+ "users",
748
+ {},
749
+ [{
750
+ key: "positions",
751
+ computed: { _tag: "relation-distinct-count", path: "items", field: "rowId", filter: [] }
752
+ }]
753
+ )
754
+ expect(result.sql).toContain(`COUNT(DISTINCT _items->>'rowId')`)
755
+ expect(result.sql).toContain(`AS "positions"`)
756
+ })
317
757
 
318
- const rows = db.prepare(`SELECT * FROM "test_items"`).all()
319
- expect(rows.length).toBe(1)
320
- expect((rows[0] as any).id).toBe("1")
758
+ it("computed relation-sum (pg casts via ::numeric)", () => {
759
+ const result = buildWhereSQLQuery(
760
+ pgDialect,
761
+ "id",
762
+ [],
763
+ "users",
764
+ {},
765
+ [{
766
+ key: "totalWeight",
767
+ computed: { _tag: "relation-sum", path: "items", field: "weight", filter: [] }
768
+ }]
769
+ )
770
+ expect(result.sql).toContain(`COALESCE(SUM((_items->>'weight')::numeric), 0)`)
771
+ expect(result.sql).toContain(`AS "totalWeight"`)
772
+ })
773
+
774
+ it("computed relation-sum-expr (pg)", () => {
775
+ const result = buildWhereSQLQuery(
776
+ pgDialect,
777
+ "id",
778
+ [],
779
+ "users",
780
+ {},
781
+ [{
782
+ key: "totalWeighted",
783
+ computed: {
784
+ _tag: "relation-sum-expr",
785
+ path: "items",
786
+ expression: {
787
+ _tag: "mul",
788
+ left: { _tag: "field", field: "weight" },
789
+ right: { _tag: "field", field: "tradeUnit.amount" }
790
+ },
791
+ filter: []
792
+ }
793
+ }]
794
+ )
795
+ expect(result.sql).toContain(
796
+ `COALESCE(SUM(((_items->>'weight')::numeric * (_items->'tradeUnit'->>'amount')::numeric)), 0)`
797
+ )
798
+ expect(result.sql).toContain(`AS "totalWeighted"`)
799
+ })
800
+
801
+ it("computed relation-sum-expr-by (pg)", () => {
802
+ const result = buildWhereSQLQuery(
803
+ pgDialect,
804
+ "id",
805
+ [],
806
+ "users",
807
+ {},
808
+ [{
809
+ key: "totalsByUnit",
810
+ computed: {
811
+ _tag: "relation-sum-expr-by",
812
+ path: "items",
813
+ expression: {
814
+ _tag: "mul",
815
+ left: { _tag: "field", field: "weight" },
816
+ right: { _tag: "field", field: "tradeUnit.amount" }
817
+ },
818
+ unit: "tradeUnit.unit",
819
+ filter: []
820
+ }
821
+ }]
822
+ )
823
+ expect(result.sql).toContain(`jsonb_agg(jsonb_build_object('unit', __unit, 'total', __total))`)
824
+ expect(result.sql).toContain(`GROUP BY _items->'tradeUnit'->>'unit'`)
825
+ expect(result.sql).toContain(`AS "totalsByUnit"`)
826
+ })
827
+
828
+ it("computed relation-sum-expr-normalized (pg)", () => {
829
+ const result = buildWhereSQLQuery(
830
+ pgDialect,
831
+ "id",
832
+ [],
833
+ "users",
834
+ {},
835
+ [{
836
+ key: "totalKg",
837
+ computed: {
838
+ _tag: "relation-sum-expr-normalized",
839
+ path: "items",
840
+ expression: {
841
+ _tag: "mul",
842
+ left: { _tag: "field", field: "weight" },
843
+ right: { _tag: "field", field: "tradeUnit.amount" }
844
+ },
845
+ unit: "tradeUnit.unit",
846
+ toBase: "kg",
847
+ factors: { g: 0.001 },
848
+ filter: []
849
+ }
850
+ }]
851
+ )
852
+ expect(result.sql).toContain(`CASE _items->'tradeUnit'->>'unit' WHEN 'kg' THEN 1 WHEN 'g' THEN 0.001 ELSE NULL END`)
853
+ expect(result.sql).toContain(`AS "totalKg"`)
854
+ })
855
+
856
+ it("computed relation-collect (pg jsonb_agg)", () => {
857
+ const result = buildWhereSQLQuery(
858
+ pgDialect,
859
+ "id",
860
+ [],
861
+ "users",
862
+ {},
863
+ [{
864
+ key: "ids",
865
+ computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: false, filter: [] }
866
+ }]
867
+ )
868
+ expect(result.sql).toContain(`COALESCE(jsonb_agg(_items->>'articleId'), '[]'::jsonb)`)
869
+ expect(result.sql).toContain(`AS "ids"`)
870
+ })
871
+
872
+ it("computed relation-collect distinct (pg jsonb_agg DISTINCT)", () => {
873
+ const result = buildWhereSQLQuery(
874
+ pgDialect,
875
+ "id",
876
+ [],
877
+ "users",
878
+ {},
879
+ [{
880
+ key: "ids",
881
+ computed: { _tag: "relation-collect", path: "items", field: "articleId", distinct: true, filter: [] }
882
+ }]
883
+ )
884
+ expect(result.sql).toContain(`COALESCE(jsonb_agg(DISTINCT _items->>'articleId'), '[]'::jsonb)`)
885
+ })
886
+ })
887
+
888
+ // --- Integration tests with in-memory SQLite (direct, no Effect SQL client) ---
889
+
890
+ describe("SQL Store (SQLite integration)", () => {
891
+ const withDb = (fn: (db: Sqlite.Database) => void) => {
892
+ const db = new BetterSqlite(":memory:")
893
+ db.pragma("journal_mode = WAL")
894
+ try {
895
+ fn(db)
896
+ } finally {
897
+ db.close()
898
+ }
899
+ }
900
+
901
+ it("creates table and seeds data", () =>
902
+ withDb((db) => {
903
+ db.exec(
904
+ `CREATE TABLE IF NOT EXISTS "test_items" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`
905
+ )
906
+ db
907
+ .prepare(`INSERT INTO "test_items" (id, _etag, data) VALUES (?, ?, ?)`)
908
+ .run("1", "etag1", JSON.stringify({ name: "Alice", age: 30 }))
909
+
910
+ const rows = db.prepare(`SELECT * FROM "test_items"`).all()
911
+ expect(rows.length).toBe(1)
912
+ expect((rows[0] as any).id).toBe("1")
321
913
  }))
322
914
 
323
915
  it("data column should not contain _etag or id", () =>
@@ -367,7 +959,7 @@ describe("SQL Store (SQLite integration)", () => {
367
959
  )
368
960
  const r1 = query(db, q1.sql, q1.params)
369
961
  expect(r1.length).toBe(1)
370
- expect((r1[0] as any).id).toBe("1")
962
+ expect(r1[0].id).toBe("1")
371
963
 
372
964
  const q2 = buildWhereSQLQuery(
373
965
  sqliteDialect,
@@ -378,13 +970,13 @@ describe("SQL Store (SQLite integration)", () => {
378
970
  )
379
971
  const r2 = query(db, q2.sql, q2.params)
380
972
  expect(r2.length).toBe(1)
381
- expect((r2[0] as any).id).toBe("2")
973
+ expect(r2[0].id).toBe("2")
382
974
 
383
975
  // Both queryable by id column
384
976
  const q3 = buildWhereSQLQuery(
385
977
  sqliteDialect,
386
978
  "id",
387
- [{ t: "where", path: "id", op: "in", value: ["1", "2"] as any }],
979
+ [{ t: "where", path: "id", op: "in", value: ["1", "2"] }],
388
980
  "test_compat",
389
981
  {}
390
982
  )
@@ -410,7 +1002,7 @@ describe("SQL Store (SQLite integration)", () => {
410
1002
  const q1 = buildWhereSQLQuery(
411
1003
  sqliteDialect,
412
1004
  "id",
413
- [{ t: "where", path: "age", op: "gt", value: 28 as any }],
1005
+ [{ t: "where", path: "age", op: "gt", value: 28 }],
414
1006
  "test_noid",
415
1007
  {}
416
1008
  )
@@ -426,8 +1018,8 @@ describe("SQL Store (SQLite integration)", () => {
426
1018
  )
427
1019
  const r2 = query(db, q2.sql, q2.params)
428
1020
  expect(r2.length).toBe(1)
429
- expect((r2[0] as any).id).toBe("2")
430
- expect((JSON.parse((r2[0] as any).data) as any).name).toBe("Bob")
1021
+ expect(r2[0].id).toBe("2")
1022
+ expect((JSON.parse(r2[0].data) as any).name).toBe("Bob")
431
1023
 
432
1024
  // Order + limit still works
433
1025
  const q3 = buildWhereSQLQuery(
@@ -443,7 +1035,7 @@ describe("SQL Store (SQLite integration)", () => {
443
1035
  )
444
1036
  const r3 = query(db, q3.sql, q3.params)
445
1037
  expect(r3.length).toBe(2)
446
- expect((JSON.parse((r3[0] as any).data) as any).name).toBe("Bob") // youngest first
1038
+ expect((JSON.parse(r3[0].data) as any).name).toBe("Bob") // youngest first
447
1039
  }))
448
1040
 
449
1041
  it("query builder generates valid SQL for SQLite", () =>
@@ -476,13 +1068,13 @@ describe("SQL Store (SQLite integration)", () => {
476
1068
  {}
477
1069
  )
478
1070
  expect(query(db, q1.sql, q1.params).length).toBe(1)
479
- expect((JSON.parse((query(db, q1.sql, q1.params)[0] as any).data) as any).name).toBe("Alice")
1071
+ expect((JSON.parse(query(db, q1.sql, q1.params)[0].data) as any).name).toBe("Alice")
480
1072
 
481
1073
  // Test gt
482
1074
  const q2 = buildWhereSQLQuery(
483
1075
  sqliteDialect,
484
1076
  "id",
485
- [{ t: "where", path: "age", op: "gt", value: 28 as any }],
1077
+ [{ t: "where", path: "age", op: "gt", value: 28 }],
486
1078
  "test_people",
487
1079
  {}
488
1080
  )
@@ -507,20 +1099,20 @@ describe("SQL Store (SQLite integration)", () => {
507
1099
  "id",
508
1100
  [
509
1101
  { t: "where", path: "name", op: "eq", value: "Alice" },
510
- { t: "and", path: "age", op: "gt", value: 25 as any }
1102
+ { t: "and", path: "age", op: "gt", value: 25 }
511
1103
  ],
512
1104
  "test_people",
513
1105
  {}
514
1106
  )
515
1107
  const r4 = query(db, q4.sql, q4.params)
516
1108
  expect(r4.length).toBe(1)
517
- expect((JSON.parse((r4[0] as any).data) as any).name).toBe("Alice")
1109
+ expect((JSON.parse(r4[0].data) as any).name).toBe("Alice")
518
1110
 
519
1111
  // Test IN
520
1112
  const q5 = buildWhereSQLQuery(
521
1113
  sqliteDialect,
522
1114
  "id",
523
- [{ t: "where", path: "id", op: "in", value: ["1", "3"] as any }],
1115
+ [{ t: "where", path: "id", op: "in", value: ["1", "3"] }],
524
1116
  "test_people",
525
1117
  {}
526
1118
  )
@@ -546,7 +1138,7 @@ describe("SQL Store (SQLite integration)", () => {
546
1138
  )
547
1139
  const r7 = query(db, q7.sql, q7.params)
548
1140
  expect(r7.length).toBe(1)
549
- expect((JSON.parse((r7[0] as any).data) as any).name).toBe("Alice")
1141
+ expect((JSON.parse(r7[0].data) as any).name).toBe("Alice")
550
1142
 
551
1143
  // Test includes (array)
552
1144
  const q8 = buildWhereSQLQuery(
@@ -567,7 +1159,7 @@ describe("SQL Store (SQLite integration)", () => {
567
1159
  {
568
1160
  t: "or-scope",
569
1161
  result: [
570
- { t: "where", path: "age", op: "gt", value: 30 as any },
1162
+ { t: "where", path: "age", op: "gt", value: 30 },
571
1163
  { t: "and", path: "name", op: "contains", value: "ar" }
572
1164
  ],
573
1165
  relation: "some"
@@ -592,7 +1184,115 @@ describe("SQL Store (SQLite integration)", () => {
592
1184
  )
593
1185
  const r10 = query(db, q10.sql, q10.params)
594
1186
  expect(r10.length).toBe(2)
595
- expect((JSON.parse((r10[0] as any).data) as any).name).toBe("Charlie") // oldest first
1187
+ expect((JSON.parse(r10[0].data) as any).name).toBe("Charlie") // oldest first
1188
+ }))
1189
+
1190
+ it("computed relation-every / distinct-count / sum / collect run on SQLite", () =>
1191
+ withDb((db) => {
1192
+ db.exec(`CREATE TABLE "test_orders" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1193
+ const orders = [
1194
+ {
1195
+ id: "o1",
1196
+ items: [
1197
+ { rowId: "r1", articleId: "A", weight: 1.5, state: { _tag: "picked" } },
1198
+ { rowId: "r2", articleId: "A", weight: 2.5, state: { _tag: "picked" } },
1199
+ { rowId: "r2", articleId: "B", weight: 0.25, state: { _tag: "picking" } }
1200
+ ]
1201
+ },
1202
+ {
1203
+ id: "o2",
1204
+ items: [
1205
+ { rowId: "r9", articleId: "Z", weight: 10, state: { _tag: "packed" } }
1206
+ ]
1207
+ }
1208
+ ]
1209
+ const insert = db.prepare(`INSERT INTO "test_orders" (id, _etag, data) VALUES (?, ?, ?)`)
1210
+ for (const o of orders) {
1211
+ const { id, ...data } = o
1212
+ insert.run(id, `etag_${id}`, JSON.stringify(data))
1213
+ }
1214
+
1215
+ const q = buildWhereSQLQuery(
1216
+ sqliteDialect,
1217
+ "id",
1218
+ [],
1219
+ "test_orders",
1220
+ {},
1221
+ [
1222
+ {
1223
+ key: "allPicked",
1224
+ computed: {
1225
+ _tag: "relation-every",
1226
+ path: "items",
1227
+ filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
1228
+ }
1229
+ },
1230
+ {
1231
+ key: "positionCount",
1232
+ computed: { _tag: "relation-distinct-count", path: "items", field: "rowId", filter: [] }
1233
+ },
1234
+ {
1235
+ key: "totalWeight",
1236
+ computed: { _tag: "relation-sum", path: "items", field: "weight", filter: [] }
1237
+ },
1238
+ {
1239
+ key: "articleIds",
1240
+ computed: {
1241
+ _tag: "relation-collect",
1242
+ path: "items",
1243
+ field: "articleId",
1244
+ distinct: true,
1245
+ filter: []
1246
+ }
1247
+ }
1248
+ ] as any,
1249
+ [{ key: "id", direction: "ASC" }] as any
1250
+ )
1251
+ const rows = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1252
+ expect(rows.length).toBe(2)
1253
+ // o1: not all picked (one is "picking")
1254
+ expect(JSON.parse(rows[0]!["allPicked"] as string)).toBe(false)
1255
+ expect(rows[0]!["positionCount"]).toBe(2)
1256
+ expect(rows[0]!["totalWeight"]).toBeCloseTo(4.25)
1257
+ expect((JSON.parse(rows[0]!["articleIds"] as string) as string[]).sort()).toEqual(["A", "B"])
1258
+ // o2: all packed (so not "picked"), allPicked = !exists(NOT picked) = false
1259
+ expect(JSON.parse(rows[1]!["allPicked"] as string)).toBe(false)
1260
+ expect(rows[1]!["positionCount"]).toBe(1)
1261
+ expect(rows[1]!["totalWeight"]).toBeCloseTo(10)
1262
+ expect(JSON.parse(rows[1]!["articleIds"] as string)).toEqual(["Z"])
1263
+ }))
1264
+
1265
+ it("computed relation-every is true when all items match filter", () =>
1266
+ withDb((db) => {
1267
+ db.exec(`CREATE TABLE "test_every" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1268
+ db.prepare(`INSERT INTO "test_every" (id, _etag, data) VALUES (?, ?, ?)`).run(
1269
+ "1",
1270
+ "e",
1271
+ JSON.stringify({
1272
+ items: [
1273
+ { state: { _tag: "picked" } },
1274
+ { state: { _tag: "picked" } }
1275
+ ]
1276
+ })
1277
+ )
1278
+
1279
+ const q = buildWhereSQLQuery(
1280
+ sqliteDialect,
1281
+ "id",
1282
+ [],
1283
+ "test_every",
1284
+ {},
1285
+ [{
1286
+ key: "allPicked",
1287
+ computed: {
1288
+ _tag: "relation-every",
1289
+ path: "items",
1290
+ filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "picked" }]
1291
+ }
1292
+ }]
1293
+ )
1294
+ const rows = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1295
+ expect(JSON.parse(rows[0]!["allPicked"] as string)).toBe(true)
596
1296
  }))
597
1297
 
598
1298
  it("namespace param is in correct position for SQLite positional placeholders", () =>
@@ -626,9 +1326,454 @@ describe("SQL Store (SQLite integration)", () => {
626
1326
  const results = query(db, nsSql, params)
627
1327
  // Should only get Alice and Bob (primary namespace), not Charlie (other namespace)
628
1328
  expect(results.length).toBe(2)
629
- const names = results.map((r) => (JSON.parse((r as any).data) as any).name).sort()
1329
+ const names = results.map((r) => (JSON.parse(r.data) as any).name).sort()
630
1330
  expect(names).toEqual(["Alice", "Bob"])
631
1331
  }))
1332
+
1333
+ it("aggregate: agg-count-when with GROUP BY on aliased path field (SQLite)", () =>
1334
+ withDb((db) => {
1335
+ db.exec(`CREATE TABLE "test_agg" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1336
+ const rows = [
1337
+ { city: "NYC", state: "active" },
1338
+ { city: "NYC", state: "inactive" },
1339
+ { city: "NYC", state: "active" },
1340
+ { city: "LA", state: "active" }
1341
+ ]
1342
+ const insert = db.prepare(`INSERT INTO "test_agg" (id, _etag, data) VALUES (?, ?, ?)`)
1343
+ rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
1344
+
1345
+ const q = buildWhereSQLQuery(
1346
+ sqliteDialect,
1347
+ "id",
1348
+ [],
1349
+ "test_agg",
1350
+ {},
1351
+ [
1352
+ { key: "city", path: "city" },
1353
+ {
1354
+ key: "activeCount",
1355
+ aggregate: {
1356
+ _tag: "agg-count-when",
1357
+ filter: [{ t: "where", path: "state", op: "eq", value: "active" }]
1358
+ }
1359
+ },
1360
+ { key: "total", aggregate: { _tag: "agg-count" } }
1361
+ ] as any,
1362
+ [{ key: "city", direction: "ASC" }] as any
1363
+ )
1364
+
1365
+ expect(q.sql).toContain("GROUP BY")
1366
+ expect(q.sql).toContain("COUNT(CASE WHEN")
1367
+
1368
+ const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1369
+ expect(result.length).toBe(2)
1370
+
1371
+ const la = result.find((r) => r["city"] === "LA")!
1372
+ expect(la["activeCount"]).toBe(1)
1373
+ expect(la["total"]).toBe(1)
1374
+
1375
+ const nyc = result.find((r) => r["city"] === "NYC")!
1376
+ expect(nyc["activeCount"]).toBe(2)
1377
+ expect(nyc["total"]).toBe(3)
1378
+ }))
1379
+
1380
+ it("aggregate: agg-sum and agg-min / agg-max (SQLite)", () =>
1381
+ withDb((db) => {
1382
+ db.exec(`CREATE TABLE "test_agg2" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1383
+ const rows = [
1384
+ { dept: "eng", salary: 100 },
1385
+ { dept: "eng", salary: 200 },
1386
+ { dept: "hr", salary: 50 }
1387
+ ]
1388
+ const insert = db.prepare(`INSERT INTO "test_agg2" (id, _etag, data) VALUES (?, ?, ?)`)
1389
+ rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
1390
+
1391
+ const q = buildWhereSQLQuery(
1392
+ sqliteDialect,
1393
+ "id",
1394
+ [],
1395
+ "test_agg2",
1396
+ {},
1397
+ [
1398
+ { key: "dept", path: "dept" },
1399
+ { key: "totalSalary", aggregate: { _tag: "agg-sum", field: "salary" } },
1400
+ { key: "minSalary", aggregate: { _tag: "agg-min", field: "salary" } },
1401
+ { key: "maxSalary", aggregate: { _tag: "agg-max", field: "salary" } }
1402
+ ] as any,
1403
+ [{ key: "dept", direction: "ASC" }] as any
1404
+ )
1405
+
1406
+ const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1407
+ expect(result.length).toBe(2)
1408
+
1409
+ const eng = result.find((r) => r["dept"] === "eng")!
1410
+ expect(eng["totalSalary"]).toBeCloseTo(300)
1411
+ expect(eng["minSalary"]).toBeCloseTo(100)
1412
+ expect(eng["maxSalary"]).toBeCloseTo(200)
1413
+
1414
+ const hr = result.find((r) => r["dept"] === "hr")!
1415
+ expect(hr["totalSalary"]).toBeCloseTo(50)
1416
+ }))
1417
+
1418
+ it("aggregate: nested path field grouping (SQLite)", () =>
1419
+ withDb((db) => {
1420
+ db.exec(`CREATE TABLE "test_agg3" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1421
+ const rows = [
1422
+ { address: { city: "NYC" }, status: "open" },
1423
+ { address: { city: "NYC" }, status: "closed" },
1424
+ { address: { city: "LA" }, status: "open" }
1425
+ ]
1426
+ const insert = db.prepare(`INSERT INTO "test_agg3" (id, _etag, data) VALUES (?, ?, ?)`)
1427
+ rows.forEach((r, i) => insert.run(String(i + 1), `e${i + 1}`, JSON.stringify(r)))
1428
+
1429
+ const q = buildWhereSQLQuery(
1430
+ sqliteDialect,
1431
+ "id",
1432
+ [],
1433
+ "test_agg3",
1434
+ {},
1435
+ [
1436
+ { key: "city", path: "address.city" },
1437
+ { key: "total", aggregate: { _tag: "agg-count" } }
1438
+ ] as any,
1439
+ [{ key: "city", direction: "ASC" }] as any
1440
+ )
1441
+
1442
+ expect(q.sql).toContain("GROUP BY")
1443
+
1444
+ const result = query(db, q.sql, q.params) as Array<Record<string, unknown>>
1445
+ expect(result.length).toBe(2)
1446
+ const la = result.find((r) => r["city"] === "LA")!
1447
+ expect(la["total"]).toBe(1)
1448
+ const nyc = result.find((r) => r["city"] === "NYC")!
1449
+ expect(nyc["total"]).toBe(2)
1450
+ }))
1451
+ })
1452
+
1453
+ // --- boolean WHERE clause tests (regression: where("x", true) must work per dialect) ---
1454
+
1455
+ describe("boolean WHERE clauses — query builder", () => {
1456
+ it("sqlite: eq true serializes bool → 1 integer", () => {
1457
+ const result = buildWhereSQLQuery(
1458
+ sqliteDialect,
1459
+ "id",
1460
+ [{ t: "where", path: "flag", op: "eq", value: true }],
1461
+ "t",
1462
+ {}
1463
+ )
1464
+ expect(result.sql).toContain("json_extract(data, '$.flag') = ?")
1465
+ expect(result.params).toEqual([1])
1466
+ })
1467
+
1468
+ it("sqlite: eq false serializes bool → 0 integer", () => {
1469
+ const result = buildWhereSQLQuery(
1470
+ sqliteDialect,
1471
+ "id",
1472
+ [{ t: "where", path: "flag", op: "eq", value: false }],
1473
+ "t",
1474
+ {}
1475
+ )
1476
+ expect(result.params).toEqual([0])
1477
+ })
1478
+
1479
+ it("sqlite: neq/in also serialize booleans", () => {
1480
+ const r1 = buildWhereSQLQuery(
1481
+ sqliteDialect,
1482
+ "id",
1483
+ [{ t: "where", path: "flag", op: "neq", value: true }],
1484
+ "t",
1485
+ {}
1486
+ )
1487
+ expect(r1.params).toEqual([1])
1488
+
1489
+ const r2 = buildWhereSQLQuery(
1490
+ sqliteDialect,
1491
+ "id",
1492
+ [{ t: "where", path: "flag", op: "in", value: [true, false] }],
1493
+ "t",
1494
+ {}
1495
+ )
1496
+ expect(r2.params).toEqual([1, 0])
1497
+ })
1498
+
1499
+ it("pg: eq true serializes bool → 'true' text (matches ->> output)", () => {
1500
+ const result = buildWhereSQLQuery(
1501
+ pgDialect,
1502
+ "id",
1503
+ [{ t: "where", path: "flag", op: "eq", value: true }],
1504
+ "t",
1505
+ {}
1506
+ )
1507
+ expect(result.sql).toContain("data->>'flag' = $1")
1508
+ expect(result.params).toEqual(["true"])
1509
+ })
1510
+
1511
+ it("pg: eq false serializes bool → 'false' text", () => {
1512
+ const result = buildWhereSQLQuery(
1513
+ pgDialect,
1514
+ "id",
1515
+ [{ t: "where", path: "flag", op: "eq", value: false }],
1516
+ "t",
1517
+ {}
1518
+ )
1519
+ expect(result.params).toEqual(["false"])
1520
+ })
1521
+
1522
+ it("pg: in with booleans serializes as text list", () => {
1523
+ const result = buildWhereSQLQuery(
1524
+ pgDialect,
1525
+ "id",
1526
+ [{ t: "where", path: "flag", op: "in", value: [true, false] }],
1527
+ "t",
1528
+ {}
1529
+ )
1530
+ expect(result.params).toEqual(["true", "false"])
1531
+ })
1532
+
1533
+ it("non-boolean scalars pass through unchanged (sqlite)", () => {
1534
+ const result = buildWhereSQLQuery(
1535
+ sqliteDialect,
1536
+ "id",
1537
+ [{ t: "where", path: "name", op: "eq", value: "Alice" }],
1538
+ "t",
1539
+ {}
1540
+ )
1541
+ expect(result.params).toEqual(["Alice"])
1542
+ })
1543
+
1544
+ it("non-boolean scalars pass through unchanged (pg)", () => {
1545
+ const result = buildWhereSQLQuery(
1546
+ pgDialect,
1547
+ "id",
1548
+ [{ t: "where", path: "age", op: "gt", value: 18 }],
1549
+ "t",
1550
+ {}
1551
+ )
1552
+ expect(result.params).toEqual([18])
1553
+ })
1554
+ })
1555
+
1556
+ describe("boolean WHERE clauses — SQLite integration (end-to-end)", () => {
1557
+ const withDb = (fn: (db: Sqlite.Database) => void) => {
1558
+ const db = new BetterSqlite(":memory:")
1559
+ try {
1560
+ fn(db)
1561
+ } finally {
1562
+ db.close()
1563
+ }
1564
+ }
1565
+
1566
+ it("where flag = true matches only true rows", () =>
1567
+ withDb((db) => {
1568
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1569
+ db
1570
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1571
+ .run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
1572
+ db
1573
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1574
+ .run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
1575
+
1576
+ const q = buildWhereSQLQuery(
1577
+ sqliteDialect,
1578
+ "id",
1579
+ [{ t: "where", path: "flag", op: "eq", value: true }],
1580
+ "t",
1581
+ {}
1582
+ )
1583
+ const rows = query(db, q.sql, q.params)
1584
+ expect(rows.length).toBe(1)
1585
+ expect((JSON.parse(rows[0].data) as any).name).toBe("Alice")
1586
+ }))
1587
+
1588
+ it("where flag = false matches only false rows", () =>
1589
+ withDb((db) => {
1590
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1591
+ db
1592
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1593
+ .run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
1594
+ db
1595
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1596
+ .run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
1597
+
1598
+ const q = buildWhereSQLQuery(
1599
+ sqliteDialect,
1600
+ "id",
1601
+ [{ t: "where", path: "flag", op: "eq", value: false }],
1602
+ "t",
1603
+ {}
1604
+ )
1605
+ const rows = query(db, q.sql, q.params)
1606
+ expect(rows.length).toBe(1)
1607
+ expect((JSON.parse(rows[0].data) as any).name).toBe("Bob")
1608
+ }))
1609
+
1610
+ it("where nested boolean path works", () =>
1611
+ withDb((db) => {
1612
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1613
+ db
1614
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1615
+ .run("1", "e", JSON.stringify({ meta: { active: true }, name: "Alice" }))
1616
+ db
1617
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1618
+ .run("2", "e", JSON.stringify({ meta: { active: false }, name: "Bob" }))
1619
+
1620
+ const q = buildWhereSQLQuery(
1621
+ sqliteDialect,
1622
+ "id",
1623
+ [{ t: "where", path: "meta.active", op: "eq", value: true }],
1624
+ "t",
1625
+ {}
1626
+ )
1627
+ const rows = query(db, q.sql, q.params)
1628
+ expect(rows.length).toBe(1)
1629
+ expect((JSON.parse(rows[0].data) as any).name).toBe("Alice")
1630
+ }))
1631
+
1632
+ it("where hasKey / hasValue / hasKeyValue match Map tuple JSON", () =>
1633
+ withDb((db) => {
1634
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1635
+ db
1636
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1637
+ .run("1", "e", JSON.stringify({ meta: [["n", 1], ["x", 2]] }))
1638
+ db
1639
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1640
+ .run("2", "e", JSON.stringify({ meta: [["n", 9]] }))
1641
+
1642
+ const byKey = buildWhereSQLQuery(
1643
+ sqliteDialect,
1644
+ "id",
1645
+ [{ t: "where", path: "meta", op: "hasKey", value: "x" }],
1646
+ "t",
1647
+ {}
1648
+ )
1649
+ expect(query(db, byKey.sql, byKey.params).map((r) => r.id)).toEqual(["1"])
1650
+
1651
+ const byValue = buildWhereSQLQuery(
1652
+ sqliteDialect,
1653
+ "id",
1654
+ [{ t: "where", path: "meta", op: "hasValue", value: 9 }],
1655
+ "t",
1656
+ {}
1657
+ )
1658
+ expect(query(db, byValue.sql, byValue.params).map((r) => r.id)).toEqual(["2"])
1659
+
1660
+ const byPair = buildWhereSQLQuery(
1661
+ sqliteDialect,
1662
+ "id",
1663
+ [{ t: "where", path: "meta", op: "hasKeyValue", value: ["n", 1] }],
1664
+ "t",
1665
+ {}
1666
+ )
1667
+ expect(query(db, byPair.sql, byPair.params).map((r) => r.id)).toEqual(["1"])
1668
+ }))
1669
+
1670
+ it("where neq boolean works", () =>
1671
+ withDb((db) => {
1672
+ db.exec(`CREATE TABLE "t" (id TEXT PRIMARY KEY, _etag TEXT, data JSON NOT NULL)`)
1673
+ db
1674
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1675
+ .run("1", "e", JSON.stringify({ flag: true, name: "Alice" }))
1676
+ db
1677
+ .prepare(`INSERT INTO "t" (id, _etag, data) VALUES (?, ?, ?)`)
1678
+ .run("2", "e", JSON.stringify({ flag: false, name: "Bob" }))
1679
+
1680
+ const q = buildWhereSQLQuery(
1681
+ sqliteDialect,
1682
+ "id",
1683
+ [{ t: "where", path: "flag", op: "neq", value: true }],
1684
+ "t",
1685
+ {}
1686
+ )
1687
+ const rows = query(db, q.sql, q.params)
1688
+ expect(rows.length).toBe(1)
1689
+ expect((JSON.parse(rows[0].data) as any).name).toBe("Bob")
1690
+ }))
1691
+ })
1692
+
1693
+ // --- jsonExtractJson dialect tests (regression: booleans must survive round-trip) ---
1694
+
1695
+ describe("sqliteDialect.jsonExtractJson boolean round-trip", () => {
1696
+ const withDb = (fn: (db: Sqlite.Database) => void) => {
1697
+ const db = new BetterSqlite(":memory:")
1698
+ try {
1699
+ fn(db)
1700
+ } finally {
1701
+ db.close()
1702
+ }
1703
+ }
1704
+
1705
+ it("SQL uses json_type CASE to preserve booleans", () => {
1706
+ const sql = sqliteDialect.jsonExtractJson("flag")
1707
+ expect(sql).toContain("json_type(data, '$.flag')")
1708
+ expect(sql).toContain("'true'")
1709
+ expect(sql).toContain("'false'")
1710
+ expect(sql).toContain("json_quote(json_extract(data, '$.flag'))")
1711
+ })
1712
+
1713
+ it("boolean false round-trips as false (not 0)", () =>
1714
+ withDb((db) => {
1715
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
1716
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ flag: false }))
1717
+
1718
+ const row = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("flag")} AS v FROM "t"`).get() as any
1719
+ const parsed = JSON.parse(row.v)
1720
+ expect(parsed).toBe(false)
1721
+ expect(typeof parsed).toBe("boolean")
1722
+ }))
1723
+
1724
+ it("boolean true round-trips as true (not 1)", () =>
1725
+ withDb((db) => {
1726
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
1727
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ flag: true }))
1728
+
1729
+ const row = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("flag")} AS v FROM "t"`).get() as any
1730
+ const parsed = JSON.parse(row.v)
1731
+ expect(parsed).toBe(true)
1732
+ expect(typeof parsed).toBe("boolean")
1733
+ }))
1734
+
1735
+ it("nested boolean round-trips correctly", () =>
1736
+ withDb((db) => {
1737
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
1738
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(
1739
+ JSON.stringify({ nested: { flag: false } })
1740
+ )
1741
+
1742
+ const row = db
1743
+ .prepare(`SELECT ${sqliteDialect.jsonExtractJson("nested.flag")} AS v FROM "t"`)
1744
+ .get() as any
1745
+ const parsed = JSON.parse(row.v)
1746
+ expect(parsed).toBe(false)
1747
+ expect(typeof parsed).toBe("boolean")
1748
+ }))
1749
+
1750
+ it("numbers 0 and 1 still parse as numbers (not booleans)", () =>
1751
+ withDb((db) => {
1752
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
1753
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(JSON.stringify({ zero: 0, one: 1 }))
1754
+
1755
+ const zRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("zero")} AS v FROM "t"`).get() as any
1756
+ const oRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("one")} AS v FROM "t"`).get() as any
1757
+ expect(JSON.parse(zRow.v)).toBe(0)
1758
+ expect(typeof JSON.parse(zRow.v)).toBe("number")
1759
+ expect(JSON.parse(oRow.v)).toBe(1)
1760
+ expect(typeof JSON.parse(oRow.v)).toBe("number")
1761
+ }))
1762
+
1763
+ it("strings, arrays, and null still round-trip correctly", () =>
1764
+ withDb((db) => {
1765
+ db.exec(`CREATE TABLE "t" (data JSON NOT NULL)`)
1766
+ db.prepare(`INSERT INTO "t" (data) VALUES (?)`).run(
1767
+ JSON.stringify({ s: "hello", arr: [1, 2, 3], nil: null })
1768
+ )
1769
+
1770
+ const sRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("s")} AS v FROM "t"`).get() as any
1771
+ const aRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("arr")} AS v FROM "t"`).get() as any
1772
+ const nRow = db.prepare(`SELECT ${sqliteDialect.jsonExtractJson("nil")} AS v FROM "t"`).get() as any
1773
+ expect(JSON.parse(sRow.v)).toBe("hello")
1774
+ expect(JSON.parse(aRow.v)).toEqual([1, 2, 3])
1775
+ expect(JSON.parse(nRow.v)).toBe(null)
1776
+ }))
632
1777
  })
633
1778
 
634
1779
  // --- toRow stripping and parseRow reconstruction tests ---
@@ -638,7 +1783,7 @@ describe("toRow strips _etag and id from data", () => {
638
1783
  const toRow = <IdKey extends PropertyKey>(e: any, idKey: IdKey) => {
639
1784
  const newE = makeETag(e)
640
1785
  const id = newE[idKey] as string
641
- const { _etag, [idKey]: _id, ...rest } = newE as any
1786
+ const { _etag, [idKey]: _id, ...rest } = newE
642
1787
  const data = JSON.stringify(rest)
643
1788
  return { id, _etag: newE._etag!, data, item: newE }
644
1789
  }
@@ -773,4 +1918,14 @@ describe("parseRow reconstructs full object from row", () => {
773
1918
  expect(reconstructed.tags).toEqual(["admin"])
774
1919
  expect(reconstructed._etag).toBe(newE._etag)
775
1920
  })
1921
+
1922
+ it("lowers Date defaultValues to ISO before merge", () => {
1923
+ const result: any = parseRow(
1924
+ { id: "1", _etag: "e1", data: JSON.stringify({ name: "Alice" }) },
1925
+ "id",
1926
+ { at: new Date("2024-06-01T00:00:00.000Z") }
1927
+ )
1928
+ expect(result.at).toBe("2024-06-01T00:00:00.000Z")
1929
+ expect(result.name).toBe("Alice")
1930
+ })
776
1931
  })