@effect-app/infra 4.0.0-beta.21 → 4.0.0-beta.211

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 (309) hide show
  1. package/CHANGELOG.md +1556 -0
  2. package/_check.sh +1 -1
  3. package/dist/CUPS.d.ts +7 -7
  4. package/dist/CUPS.d.ts.map +1 -1
  5. package/dist/CUPS.js +10 -12
  6. package/dist/Emailer/Sendgrid.d.ts +14 -14
  7. package/dist/Emailer/Sendgrid.d.ts.map +1 -1
  8. package/dist/Emailer/Sendgrid.js +16 -15
  9. package/dist/Emailer/fake.d.ts +1 -1
  10. package/dist/Emailer/service.d.ts +10 -4
  11. package/dist/Emailer/service.d.ts.map +1 -1
  12. package/dist/Emailer/service.js +3 -3
  13. package/dist/Emailer.d.ts +1 -1
  14. package/dist/MainFiberSet.d.ts +9 -9
  15. package/dist/MainFiberSet.d.ts.map +1 -1
  16. package/dist/MainFiberSet.js +3 -3
  17. package/dist/Model/Repository/Registry.d.ts +20 -0
  18. package/dist/Model/Repository/Registry.d.ts.map +1 -0
  19. package/dist/Model/Repository/Registry.js +17 -0
  20. package/dist/Model/Repository/ext.d.ts +33 -15
  21. package/dist/Model/Repository/ext.d.ts.map +1 -1
  22. package/dist/Model/Repository/ext.js +54 -2
  23. package/dist/Model/Repository/internal/internal.d.ts +6 -6
  24. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  25. package/dist/Model/Repository/internal/internal.js +98 -50
  26. package/dist/Model/Repository/legacy.d.ts +1 -1
  27. package/dist/Model/Repository/makeRepo.d.ts +7 -6
  28. package/dist/Model/Repository/makeRepo.d.ts.map +1 -1
  29. package/dist/Model/Repository/makeRepo.js +5 -1
  30. package/dist/Model/Repository/service.d.ts +28 -23
  31. package/dist/Model/Repository/service.d.ts.map +1 -1
  32. package/dist/Model/Repository/validation.d.ts +46 -17
  33. package/dist/Model/Repository/validation.d.ts.map +1 -1
  34. package/dist/Model/Repository/validation.js +5 -5
  35. package/dist/Model/Repository.d.ts +2 -1
  36. package/dist/Model/Repository.d.ts.map +1 -1
  37. package/dist/Model/Repository.js +2 -1
  38. package/dist/Model/dsl.d.ts +4 -4
  39. package/dist/Model/dsl.d.ts.map +1 -1
  40. package/dist/Model/filter/filterApi.d.ts +5 -5
  41. package/dist/Model/filter/filterApi.d.ts.map +1 -1
  42. package/dist/Model/filter/types/errors.d.ts +1 -1
  43. package/dist/Model/filter/types/fields.d.ts +1 -1
  44. package/dist/Model/filter/types/path/common.d.ts +1 -1
  45. package/dist/Model/filter/types/path/eager.d.ts +1 -1
  46. package/dist/Model/filter/types/path/eager.d.ts.map +1 -1
  47. package/dist/Model/filter/types/path/eager.js +1 -1
  48. package/dist/Model/filter/types/path/index.d.ts +1 -1
  49. package/dist/Model/filter/types/utils.d.ts +1 -1
  50. package/dist/Model/filter/types/validator.d.ts +1 -1
  51. package/dist/Model/filter/types.d.ts +1 -1
  52. package/dist/Model/query/dsl.d.ts +16 -16
  53. package/dist/Model/query/dsl.d.ts.map +1 -1
  54. package/dist/Model/query/new-kid-interpreter.d.ts +6 -6
  55. package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
  56. package/dist/Model/query/new-kid-interpreter.js +3 -3
  57. package/dist/Model/query.d.ts +1 -1
  58. package/dist/Model.d.ts +2 -1
  59. package/dist/Model.d.ts.map +1 -1
  60. package/dist/Model.js +2 -1
  61. package/dist/QueueMaker/SQLQueue.d.ts +5 -7
  62. package/dist/QueueMaker/SQLQueue.d.ts.map +1 -1
  63. package/dist/QueueMaker/SQLQueue.js +130 -116
  64. package/dist/QueueMaker/errors.d.ts +2 -2
  65. package/dist/QueueMaker/errors.d.ts.map +1 -1
  66. package/dist/QueueMaker/memQueue.d.ts +7 -4
  67. package/dist/QueueMaker/memQueue.d.ts.map +1 -1
  68. package/dist/QueueMaker/memQueue.js +75 -63
  69. package/dist/QueueMaker/sbqueue.d.ts +6 -3
  70. package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
  71. package/dist/QueueMaker/sbqueue.js +52 -53
  72. package/dist/QueueMaker/service.d.ts +1 -1
  73. package/dist/RequestContext.d.ts +74 -35
  74. package/dist/RequestContext.d.ts.map +1 -1
  75. package/dist/RequestContext.js +13 -14
  76. package/dist/RequestFiberSet.d.ts +7 -7
  77. package/dist/RequestFiberSet.d.ts.map +1 -1
  78. package/dist/RequestFiberSet.js +3 -3
  79. package/dist/Store/ContextMapContainer.d.ts +19 -3
  80. package/dist/Store/ContextMapContainer.d.ts.map +1 -1
  81. package/dist/Store/ContextMapContainer.js +13 -3
  82. package/dist/Store/Cosmos/query.d.ts +1 -1
  83. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  84. package/dist/Store/Cosmos/query.js +10 -12
  85. package/dist/Store/Cosmos.d.ts +1 -1
  86. package/dist/Store/Cosmos.d.ts.map +1 -1
  87. package/dist/Store/Cosmos.js +335 -243
  88. package/dist/Store/Disk.d.ts +2 -2
  89. package/dist/Store/Disk.d.ts.map +1 -1
  90. package/dist/Store/Disk.js +72 -35
  91. package/dist/Store/Memory.d.ts +6 -4
  92. package/dist/Store/Memory.d.ts.map +1 -1
  93. package/dist/Store/Memory.js +90 -57
  94. package/dist/Store/SQL/Pg.d.ts +4 -0
  95. package/dist/Store/SQL/Pg.d.ts.map +1 -0
  96. package/dist/Store/SQL/Pg.js +231 -0
  97. package/dist/Store/SQL/query.d.ts +38 -0
  98. package/dist/Store/SQL/query.d.ts.map +1 -0
  99. package/dist/Store/SQL/query.js +367 -0
  100. package/dist/Store/SQL.d.ts +20 -0
  101. package/dist/Store/SQL.d.ts.map +1 -0
  102. package/dist/Store/SQL.js +464 -0
  103. package/dist/Store/codeFilter.d.ts +1 -1
  104. package/dist/Store/codeFilter.d.ts.map +1 -1
  105. package/dist/Store/codeFilter.js +4 -2
  106. package/dist/Store/index.d.ts +5 -2
  107. package/dist/Store/index.d.ts.map +1 -1
  108. package/dist/Store/index.js +15 -3
  109. package/dist/Store/service.d.ts +18 -7
  110. package/dist/Store/service.d.ts.map +1 -1
  111. package/dist/Store/service.js +24 -6
  112. package/dist/Store/utils.d.ts +1 -1
  113. package/dist/Store/utils.d.ts.map +1 -1
  114. package/dist/Store/utils.js +3 -4
  115. package/dist/Store.d.ts +1 -1
  116. package/dist/adapters/SQL/Model.d.ts +31 -42
  117. package/dist/adapters/SQL/Model.d.ts.map +1 -1
  118. package/dist/adapters/SQL/Model.js +29 -38
  119. package/dist/adapters/SQL.d.ts +1 -1
  120. package/dist/adapters/ServiceBus.d.ts +11 -11
  121. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  122. package/dist/adapters/ServiceBus.js +25 -21
  123. package/dist/adapters/cosmos-client.d.ts +3 -3
  124. package/dist/adapters/cosmos-client.d.ts.map +1 -1
  125. package/dist/adapters/cosmos-client.js +3 -3
  126. package/dist/adapters/index.d.ts +8 -2
  127. package/dist/adapters/index.d.ts.map +1 -1
  128. package/dist/adapters/index.js +8 -2
  129. package/dist/adapters/logger.d.ts +2 -2
  130. package/dist/adapters/logger.d.ts.map +1 -1
  131. package/dist/adapters/memQueue.d.ts +3 -3
  132. package/dist/adapters/memQueue.d.ts.map +1 -1
  133. package/dist/adapters/memQueue.js +3 -3
  134. package/dist/adapters/mongo-client.d.ts +3 -3
  135. package/dist/adapters/mongo-client.d.ts.map +1 -1
  136. package/dist/adapters/mongo-client.js +3 -3
  137. package/dist/adapters/redis-client.d.ts +3 -3
  138. package/dist/adapters/redis-client.d.ts.map +1 -1
  139. package/dist/adapters/redis-client.js +3 -3
  140. package/dist/api/ContextProvider.d.ts +8 -8
  141. package/dist/api/ContextProvider.d.ts.map +1 -1
  142. package/dist/api/ContextProvider.js +6 -6
  143. package/dist/api/codec.d.ts +1 -1
  144. package/dist/api/internal/RequestContextMiddleware.d.ts +2 -2
  145. package/dist/api/internal/RequestContextMiddleware.d.ts.map +1 -1
  146. package/dist/api/internal/RequestContextMiddleware.js +2 -2
  147. package/dist/api/internal/auth.d.ts +44 -6
  148. package/dist/api/internal/auth.d.ts.map +1 -1
  149. package/dist/api/internal/auth.js +160 -29
  150. package/dist/api/internal/events.d.ts +3 -3
  151. package/dist/api/internal/events.d.ts.map +1 -1
  152. package/dist/api/internal/events.js +10 -8
  153. package/dist/api/internal/health.d.ts +1 -1
  154. package/dist/api/layerUtils.d.ts +6 -6
  155. package/dist/api/layerUtils.d.ts.map +1 -1
  156. package/dist/api/layerUtils.js +5 -5
  157. package/dist/api/middlewares.d.ts +1 -1
  158. package/dist/api/reportError.d.ts +1 -1
  159. package/dist/api/routing/middleware/RouterMiddleware.d.ts +4 -4
  160. package/dist/api/routing/middleware/RouterMiddleware.d.ts.map +1 -1
  161. package/dist/api/routing/middleware/middleware.d.ts +39 -3
  162. package/dist/api/routing/middleware/middleware.d.ts.map +1 -1
  163. package/dist/api/routing/middleware/middleware.js +48 -16
  164. package/dist/api/routing/middleware.d.ts +1 -2
  165. package/dist/api/routing/middleware.d.ts.map +1 -1
  166. package/dist/api/routing/middleware.js +1 -2
  167. package/dist/api/routing/schema/jwt.d.ts +1 -1
  168. package/dist/api/routing/schema/jwt.d.ts.map +1 -1
  169. package/dist/api/routing/tsort.d.ts +1 -1
  170. package/dist/api/routing/tsort.d.ts.map +1 -1
  171. package/dist/api/routing/utils.d.ts +3 -3
  172. package/dist/api/routing/utils.d.ts.map +1 -1
  173. package/dist/api/routing.d.ts +80 -37
  174. package/dist/api/routing.d.ts.map +1 -1
  175. package/dist/api/routing.js +112 -40
  176. package/dist/api/setupRequest.d.ts +8 -5
  177. package/dist/api/setupRequest.d.ts.map +1 -1
  178. package/dist/api/setupRequest.js +12 -7
  179. package/dist/api/util.d.ts +1 -1
  180. package/dist/arbs.d.ts +1 -1
  181. package/dist/arbs.d.ts.map +1 -1
  182. package/dist/arbs.js +5 -3
  183. package/dist/errorReporter.d.ts +4 -4
  184. package/dist/errorReporter.d.ts.map +1 -1
  185. package/dist/errorReporter.js +20 -25
  186. package/dist/errors.d.ts +1 -1
  187. package/dist/fileUtil.d.ts +1 -1
  188. package/dist/fileUtil.d.ts.map +1 -1
  189. package/dist/index.d.ts +1 -1
  190. package/dist/logger/jsonLogger.d.ts +1 -1
  191. package/dist/logger/logFmtLogger.d.ts +1 -1
  192. package/dist/logger/shared.d.ts +1 -1
  193. package/dist/logger/shared.js +2 -2
  194. package/dist/logger.d.ts +1 -1
  195. package/dist/logger.d.ts.map +1 -1
  196. package/dist/otel.d.ts +75 -0
  197. package/dist/otel.d.ts.map +1 -0
  198. package/dist/otel.js +65 -0
  199. package/dist/rateLimit.d.ts +9 -3
  200. package/dist/rateLimit.d.ts.map +1 -1
  201. package/dist/rateLimit.js +5 -11
  202. package/dist/test.d.ts +2 -2
  203. package/dist/test.d.ts.map +1 -1
  204. package/dist/test.js +1 -1
  205. package/dist/vitest.d.ts +1 -1
  206. package/examples/query.ts +39 -35
  207. package/package.json +45 -37
  208. package/src/CUPS.ts +9 -11
  209. package/src/Emailer/Sendgrid.ts +17 -14
  210. package/src/Emailer/service.ts +9 -3
  211. package/src/MainFiberSet.ts +5 -6
  212. package/src/Model/Repository/Registry.ts +33 -0
  213. package/src/Model/Repository/ext.ts +96 -10
  214. package/src/Model/Repository/internal/internal.ts +213 -148
  215. package/src/Model/Repository/makeRepo.ts +12 -10
  216. package/src/Model/Repository/service.ts +31 -22
  217. package/src/Model/Repository/validation.ts +4 -4
  218. package/src/Model/Repository.ts +1 -0
  219. package/src/Model/dsl.ts +3 -3
  220. package/src/Model/filter/types/path/eager.ts +1 -2
  221. package/src/Model/query/dsl.ts +18 -18
  222. package/src/Model/query/new-kid-interpreter.ts +2 -2
  223. package/src/Model.ts +1 -0
  224. package/src/QueueMaker/SQLQueue.ts +144 -152
  225. package/src/QueueMaker/memQueue.ts +104 -103
  226. package/src/QueueMaker/sbqueue.ts +70 -86
  227. package/src/RequestContext.ts +14 -16
  228. package/src/RequestFiberSet.ts +2 -2
  229. package/src/Store/ContextMapContainer.ts +41 -2
  230. package/src/Store/Cosmos/query.ts +16 -20
  231. package/src/Store/Cosmos.ts +473 -348
  232. package/src/Store/Disk.ts +102 -65
  233. package/src/Store/Memory.ts +118 -83
  234. package/src/Store/SQL/Pg.ts +352 -0
  235. package/src/Store/SQL/query.ts +409 -0
  236. package/src/Store/SQL.ts +734 -0
  237. package/src/Store/codeFilter.ts +3 -1
  238. package/src/Store/index.ts +17 -2
  239. package/src/Store/service.ts +32 -8
  240. package/src/Store/utils.ts +23 -22
  241. package/src/adapters/SQL/Model.ts +41 -40
  242. package/src/adapters/ServiceBus.ts +125 -121
  243. package/src/adapters/cosmos-client.ts +2 -2
  244. package/src/adapters/index.ts +7 -0
  245. package/src/adapters/memQueue.ts +2 -2
  246. package/src/adapters/mongo-client.ts +2 -2
  247. package/src/adapters/redis-client.ts +2 -2
  248. package/src/api/ContextProvider.ts +12 -13
  249. package/src/api/internal/RequestContextMiddleware.ts +1 -1
  250. package/src/api/internal/auth.ts +246 -44
  251. package/src/api/internal/events.ts +13 -9
  252. package/src/api/layerUtils.ts +8 -8
  253. package/src/api/routing/middleware/RouterMiddleware.ts +4 -4
  254. package/src/api/routing/middleware/middleware.ts +55 -14
  255. package/src/api/routing/middleware.ts +0 -2
  256. package/src/api/routing.ts +298 -128
  257. package/src/api/setupRequest.ts +28 -8
  258. package/src/arbs.ts +4 -2
  259. package/src/errorReporter.ts +62 -74
  260. package/src/logger/shared.ts +1 -1
  261. package/src/otel.ts +152 -0
  262. package/src/rateLimit.ts +30 -22
  263. package/src/test.ts +1 -1
  264. package/test/auth.test.ts +101 -0
  265. package/test/contextProvider.test.ts +11 -11
  266. package/test/controller.test.ts +21 -30
  267. package/test/dist/auth.test.d.ts.map +1 -0
  268. package/test/dist/contextProvider.test.d.ts.map +1 -1
  269. package/test/dist/controller.test.d.ts.map +1 -1
  270. package/test/dist/date-query.test.d.ts.map +1 -0
  271. package/test/dist/fixtures.d.ts +26 -12
  272. package/test/dist/fixtures.d.ts.map +1 -1
  273. package/test/dist/fixtures.js +12 -10
  274. package/test/dist/query.test.d.ts.map +1 -1
  275. package/test/dist/rawQuery.test.d.ts.map +1 -1
  276. package/test/dist/repository-ext.test.d.ts.map +1 -0
  277. package/test/dist/requires.test.d.ts.map +1 -1
  278. package/test/dist/router-generator.test.d.ts.map +1 -0
  279. package/test/dist/routing-interruptibility.test.d.ts.map +1 -0
  280. package/test/dist/rpc-e2e-invalidation.test.d.ts.map +1 -0
  281. package/test/dist/rpc-multi-middleware.test.d.ts.map +1 -1
  282. package/test/dist/rpc-stream-fullstack.test.d.ts.map +1 -0
  283. package/test/dist/sql-store.test.d.ts.map +1 -0
  284. package/test/fixtures.ts +11 -9
  285. package/test/query.test.ts +216 -34
  286. package/test/rawQuery.test.ts +23 -19
  287. package/test/repository-ext.test.ts +60 -0
  288. package/test/requires.test.ts +6 -6
  289. package/test/router-generator.test.ts +183 -0
  290. package/test/routing-interruptibility.test.ts +63 -0
  291. package/test/rpc-e2e-invalidation.test.ts +251 -0
  292. package/test/rpc-multi-middleware.test.ts +78 -9
  293. package/test/rpc-stream-fullstack.test.ts +300 -0
  294. package/test/sql-store.test.ts +1064 -0
  295. package/test/validateSample.test.ts +15 -12
  296. package/tsconfig.examples.json +1 -1
  297. package/tsconfig.json +0 -1
  298. package/tsconfig.json.bak +2 -2
  299. package/tsconfig.src.json +35 -35
  300. package/tsconfig.test.json +2 -2
  301. package/dist/Operations.d.ts +0 -55
  302. package/dist/Operations.d.ts.map +0 -1
  303. package/dist/Operations.js +0 -102
  304. package/dist/OperationsRepo.d.ts +0 -41
  305. package/dist/OperationsRepo.d.ts.map +0 -1
  306. package/dist/OperationsRepo.js +0 -14
  307. package/eslint.config.mjs +0 -24
  308. package/src/Operations.ts +0 -235
  309. package/src/OperationsRepo.ts +0 -16
@@ -0,0 +1,409 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { Effect, type NonEmptyReadonlyArray } from "effect-app"
3
+ import { assertUnreachable } from "effect-app/utils"
4
+ import { InfraLogger } from "../../logger.js"
5
+ import type { FilterR, FilterResult } from "../../Model/filter/filterApi.js"
6
+ import { isRelationCheck } from "../codeFilter.js"
7
+
8
+ export interface SQLDialect {
9
+ readonly jsonExtract: (path: string) => string
10
+ readonly jsonExtractJson: (path: string) => string
11
+ readonly placeholder: (index: number) => string
12
+ readonly jsonArrayContains: (arrPath: string, valPlaceholder: string) => string
13
+ readonly jsonArrayNotContains: (arrPath: string, valPlaceholder: string) => string
14
+ readonly jsonArrayContainsAny: (arrPath: string, valPlaceholders: readonly string[]) => string
15
+ readonly jsonArrayNotContainsAny: (arrPath: string, valPlaceholders: readonly string[]) => string
16
+ readonly jsonArrayContainsAll: (arrPath: string, valPlaceholders: readonly string[]) => string
17
+ readonly jsonArrayNotContainsAll: (arrPath: string, valPlaceholders: readonly string[]) => string
18
+ readonly caseInsensitiveLike: (expr: string, valPlaceholder: string) => string
19
+ readonly caseInsensitiveNotLike: (expr: string, valPlaceholder: string) => string
20
+ readonly jsonColumnType: "JSON" | "JSONB"
21
+ readonly arrayLength: (path: string) => string
22
+ readonly jsonEachFrom: (arrPath: string, alias: string) => string
23
+ readonly jsonExtractElement: (alias: string, subPath: string) => string
24
+ readonly serializeJsonValue: (v: unknown) => unknown
25
+ readonly serializeScalar: (v: unknown) => unknown
26
+ }
27
+
28
+ export const sqliteDialect: SQLDialect = {
29
+ jsonExtract: (path) => `json_extract(data, '$.${path}')`,
30
+ jsonExtractJson: (path) =>
31
+ `CASE json_type(data, '$.${path}') WHEN 'true' THEN 'true' WHEN 'false' THEN 'false' ELSE json_quote(json_extract(data, '$.${path}')) END`,
32
+ placeholder: (_index) => "?",
33
+ jsonArrayContains: (arrPath, val) => `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value = ${val})`,
34
+ jsonArrayNotContains: (arrPath, val) =>
35
+ `NOT EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value = ${val})`,
36
+ jsonArrayContainsAny: (arrPath, vals) =>
37
+ `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value IN (${vals.join(", ")}))`,
38
+ jsonArrayNotContainsAny: (arrPath, vals) =>
39
+ `NOT EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value IN (${vals.join(", ")}))`,
40
+ jsonArrayContainsAll: (arrPath, vals) =>
41
+ vals.map((v) => `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value = ${v})`).join(" AND "),
42
+ jsonArrayNotContainsAll: (arrPath, vals) =>
43
+ `NOT (${
44
+ vals.map((v) => `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value = ${v})`).join(" AND ")
45
+ })`,
46
+ caseInsensitiveLike: (expr, val) => `LOWER(${expr}) LIKE LOWER(${val})`,
47
+ caseInsensitiveNotLike: (expr, val) => `LOWER(${expr}) NOT LIKE LOWER(${val})`,
48
+ jsonColumnType: "JSON",
49
+ arrayLength: (path) => `json_array_length(data, '$.${path}')`,
50
+ jsonEachFrom: (arrPath, alias) => `json_each(data, '$.${arrPath}') AS ${alias}`,
51
+ jsonExtractElement: (alias, subPath) => `json_extract(${alias}.value, '$.${subPath}')`,
52
+ serializeJsonValue: (v) => v,
53
+ // SQLite stores JSON booleans as integers (0/1) and better-sqlite3 refuses
54
+ // to bind JS booleans, so coerce them to integers for WHERE params.
55
+ serializeScalar: (v) => typeof v === "boolean" ? (v ? 1 : 0) : v
56
+ }
57
+
58
+ export const pgDialect: SQLDialect = {
59
+ jsonExtract: (path) => {
60
+ const parts = path.split(".")
61
+ if (parts.length === 1) return `data->>'${parts[0]}'`
62
+ const last = parts.pop()!
63
+ return `data${parts.map((p) => `->'${p}'`).join("")}->>'${last}'`
64
+ },
65
+ jsonExtractJson: (path) => {
66
+ const parts = path.split(".")
67
+ if (parts.length === 1) return `data->'${parts[0]}'`
68
+ return `data${parts.map((p) => `->'${p}'`).join("")}`
69
+ },
70
+ placeholder: (index) => `$${index}`,
71
+ jsonArrayContains: (arrPath, val) => {
72
+ const parts = arrPath.split(".")
73
+ const jsonPath = parts.length === 1
74
+ ? `data->'${parts[0]}'`
75
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
76
+ return `${jsonPath} @> ${val}::jsonb`
77
+ },
78
+ jsonArrayNotContains: (arrPath, val) => {
79
+ const parts = arrPath.split(".")
80
+ const jsonPath = parts.length === 1
81
+ ? `data->'${parts[0]}'`
82
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
83
+ return `NOT (${jsonPath} @> ${val}::jsonb)`
84
+ },
85
+ jsonArrayContainsAny: (arrPath, vals) => {
86
+ const parts = arrPath.split(".")
87
+ const jsonPath = parts.length === 1
88
+ ? `data->'${parts[0]}'`
89
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
90
+ return `(${vals.map((v) => `${jsonPath} @> ${v}::jsonb`).join(" OR ")})`
91
+ },
92
+ jsonArrayNotContainsAny: (arrPath, vals) => {
93
+ const parts = arrPath.split(".")
94
+ const jsonPath = parts.length === 1
95
+ ? `data->'${parts[0]}'`
96
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
97
+ return `NOT (${vals.map((v) => `${jsonPath} @> ${v}::jsonb`).join(" OR ")})`
98
+ },
99
+ jsonArrayContainsAll: (arrPath, vals) => {
100
+ const parts = arrPath.split(".")
101
+ const jsonPath = parts.length === 1
102
+ ? `data->'${parts[0]}'`
103
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
104
+ return vals.map((v) => `${jsonPath} @> ${v}::jsonb`).join(" AND ")
105
+ },
106
+ jsonArrayNotContainsAll: (arrPath, vals) => {
107
+ const parts = arrPath.split(".")
108
+ const jsonPath = parts.length === 1
109
+ ? `data->'${parts[0]}'`
110
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
111
+ return `NOT (${vals.map((v) => `${jsonPath} @> ${v}::jsonb`).join(" AND ")})`
112
+ },
113
+ caseInsensitiveLike: (expr, val) => `${expr} ILIKE ${val}`,
114
+ caseInsensitiveNotLike: (expr, val) => `${expr} NOT ILIKE ${val}`,
115
+ jsonColumnType: "JSONB",
116
+ arrayLength: (path) => `jsonb_array_length(data->'${path}')`,
117
+ jsonEachFrom: (arrPath, alias) => {
118
+ const parts = arrPath.split(".")
119
+ const jsonPath = parts.length === 1
120
+ ? `data->'${parts[0]}'`
121
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
122
+ return `jsonb_array_elements(${jsonPath}) AS ${alias}`
123
+ },
124
+ jsonExtractElement: (alias, subPath) => {
125
+ const parts = subPath.split(".")
126
+ if (parts.length === 1) return `${alias}->>'${parts[0]}'`
127
+ const last = parts.pop()!
128
+ return `${alias}${parts.map((p) => `->'${p}'`).join("")}->>'${last}'`
129
+ },
130
+ serializeJsonValue: (v) => JSON.stringify(v),
131
+ // PG's ->> operator yields text, so compare booleans as 'true'/'false' text.
132
+ serializeScalar: (v) => typeof v === "boolean" ? (v ? "true" : "false") : v
133
+ }
134
+
135
+ export function logQuery(q: { sql: string; params: unknown[] }) {
136
+ return InfraLogger
137
+ .logDebug("sql query")
138
+ .pipe(Effect.annotateLogs({
139
+ query: q.sql,
140
+ parameters: JSON.stringify(q.params, undefined, 2)
141
+ }))
142
+ }
143
+
144
+ const dottedToJsonPath = (path: string) =>
145
+ path
146
+ .split(".")
147
+ .filter((p) => p !== "-1")
148
+ .join(".")
149
+
150
+ export function buildWhereSQLQuery(
151
+ dialect: SQLDialect,
152
+ idKey: PropertyKey,
153
+ filter: readonly FilterResult[],
154
+ tableName: string,
155
+ defaultValues: Record<string, unknown>,
156
+ select?: NonEmptyReadonlyArray<string | { key: string; subKeys: readonly string[] }>,
157
+ order?: NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }>,
158
+ skip?: number,
159
+ limit?: number
160
+ ) {
161
+ const params: unknown[] = []
162
+ let paramIndex = 1
163
+
164
+ const addParam = (value: unknown): string => {
165
+ params.push(dialect.serializeScalar(value))
166
+ return dialect.placeholder(paramIndex++)
167
+ }
168
+
169
+ const fieldExpr = (path: string, relation?: string): string => {
170
+ if (path === idKey || path === "id") return "id"
171
+ if (relation && path.includes(".-1.")) {
172
+ const subPath = path.split(".-1.")[1]!
173
+ if (subPath.endsWith(".length")) {
174
+ // TODO: array length inside relation element
175
+ return dialect.jsonExtractElement(`_${relation}`, subPath.slice(0, -".length".length))
176
+ }
177
+ return dialect.jsonExtractElement(`_${relation}`, subPath)
178
+ }
179
+ if (path.endsWith(".length")) {
180
+ const arrPath = dottedToJsonPath(path.slice(0, -".length".length))
181
+ return dialect.arrayLength(arrPath)
182
+ }
183
+ const jsonPath = dottedToJsonPath(path)
184
+ const expr = dialect.jsonExtract(jsonPath)
185
+ const topKey = path.split(".")[0]
186
+ if (topKey in defaultValues) {
187
+ return `COALESCE(${expr}, ${addParam(defaultValues[topKey])})`
188
+ }
189
+ return expr
190
+ }
191
+
192
+ const statement = (x: FilterR, relation?: string): string => {
193
+ const resolvedPath = x.path === idKey ? "id" : x.path
194
+ const k = fieldExpr(resolvedPath, relation)
195
+
196
+ switch (x.op) {
197
+ case "in": {
198
+ const vals = x.value as unknown as readonly unknown[]
199
+ const hasNull = vals.some((v) => v == null)
200
+ const nonNullVals = vals.filter((v) => v != null)
201
+ const parts: string[] = []
202
+ if (nonNullVals.length > 0) {
203
+ const placeholders = nonNullVals.map((v) => addParam(v))
204
+ parts.push(`${k} IN (${placeholders.join(", ")})`)
205
+ }
206
+ if (hasNull) parts.push(`${k} IS NULL`)
207
+ return parts.length > 1 ? `(${parts.join(" OR ")})` : parts[0] ?? "1=0"
208
+ }
209
+ case "notIn": {
210
+ const vals = x.value as unknown as readonly unknown[]
211
+ const hasNull = vals.some((v) => v == null)
212
+ const nonNullVals = vals.filter((v) => v != null)
213
+ const parts: string[] = []
214
+ if (nonNullVals.length > 0) {
215
+ const placeholders = nonNullVals.map((v) => addParam(v))
216
+ parts.push(`${k} NOT IN (${placeholders.join(", ")})`)
217
+ }
218
+ if (hasNull) parts.push(`${k} IS NOT NULL`)
219
+ return parts.length > 1 ? `(${parts.join(" AND ")})` : parts[0] ?? "1=1"
220
+ }
221
+
222
+ case "includes": {
223
+ const arrPath = dottedToJsonPath(resolvedPath)
224
+ const v = addParam(x.value)
225
+ return dialect.jsonArrayContains(arrPath, v)
226
+ }
227
+ case "notIncludes": {
228
+ const arrPath = dottedToJsonPath(resolvedPath)
229
+ const v = addParam(x.value)
230
+ return dialect.jsonArrayNotContains(arrPath, v)
231
+ }
232
+
233
+ case "includes-any": {
234
+ const arrPath = dottedToJsonPath(resolvedPath)
235
+ const vals = x.value as unknown as readonly unknown[]
236
+ const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
237
+ return dialect.jsonArrayContainsAny(arrPath, placeholders)
238
+ }
239
+ case "notIncludes-any": {
240
+ const arrPath = dottedToJsonPath(resolvedPath)
241
+ const vals = x.value as unknown as readonly unknown[]
242
+ const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
243
+ return dialect.jsonArrayNotContainsAny(arrPath, placeholders)
244
+ }
245
+
246
+ case "includes-all": {
247
+ const arrPath = dottedToJsonPath(resolvedPath)
248
+ const vals = x.value as unknown as readonly unknown[]
249
+ const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
250
+ return dialect.jsonArrayContainsAll(arrPath, placeholders)
251
+ }
252
+ case "notIncludes-all": {
253
+ const arrPath = dottedToJsonPath(resolvedPath)
254
+ const vals = x.value as unknown as readonly unknown[]
255
+ const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
256
+ return dialect.jsonArrayNotContainsAll(arrPath, placeholders)
257
+ }
258
+
259
+ case "contains": {
260
+ const v = addParam(`%${x.value}%`)
261
+ return dialect.caseInsensitiveLike(k, v)
262
+ }
263
+ case "notContains": {
264
+ const v = addParam(`%${x.value}%`)
265
+ return dialect.caseInsensitiveNotLike(k, v)
266
+ }
267
+ case "startsWith": {
268
+ const v = addParam(`${x.value}%`)
269
+ return dialect.caseInsensitiveLike(k, v)
270
+ }
271
+ case "notStartsWith": {
272
+ const v = addParam(`${x.value}%`)
273
+ return dialect.caseInsensitiveNotLike(k, v)
274
+ }
275
+ case "endsWith": {
276
+ const v = addParam(`%${x.value}`)
277
+ return dialect.caseInsensitiveLike(k, v)
278
+ }
279
+ case "notEndsWith": {
280
+ const v = addParam(`%${x.value}`)
281
+ return dialect.caseInsensitiveNotLike(k, v)
282
+ }
283
+
284
+ case "lt": {
285
+ const v = addParam(x.value)
286
+ return `${k} < ${v}`
287
+ }
288
+ case "lte": {
289
+ const v = addParam(x.value)
290
+ return `${k} <= ${v}`
291
+ }
292
+ case "gt": {
293
+ const v = addParam(x.value)
294
+ return `${k} > ${v}`
295
+ }
296
+ case "gte": {
297
+ const v = addParam(x.value)
298
+ return `${k} >= ${v}`
299
+ }
300
+ case "neq": {
301
+ if (x.value === null) return `${k} IS NOT NULL`
302
+ const v = addParam(x.value)
303
+ return `${k} <> ${v}`
304
+ }
305
+ case undefined:
306
+ case "eq": {
307
+ if (x.value === null) return `${k} IS NULL`
308
+ const v = addParam(x.value)
309
+ return `${k} = ${v}`
310
+ }
311
+ default:
312
+ return assertUnreachable(x.op)
313
+ }
314
+ }
315
+
316
+ const wrapRelation = (rel: string, inner: string, every: boolean): string => {
317
+ // Optimize tautological/contradictory conditions
318
+ if (every && inner === "1=1") return "1=1"
319
+ if (!every && inner === "1=0") return "1=0"
320
+ const from = dialect.jsonEachFrom(rel, `_${rel}`)
321
+ // ∀x.P(x) ≡ ¬∃x.¬P(x), i.e. NOT EXISTS(... WHERE NOT P)
322
+ return every
323
+ ? `NOT EXISTS(SELECT 1 FROM ${from} WHERE NOT (${inner}))`
324
+ : `EXISTS(SELECT 1 FROM ${from} WHERE ${inner})`
325
+ }
326
+
327
+ const print = (state: readonly FilterResult[], isRelation: string | null, every: boolean): string => {
328
+ let s = ""
329
+ for (const e of state) {
330
+ switch (e.t) {
331
+ case "where":
332
+ s += statement(e, isRelation ?? undefined)
333
+ break
334
+ case "or":
335
+ s += ` OR ${statement(e, isRelation ?? undefined)}`
336
+ break
337
+ case "and":
338
+ s += ` AND ${statement(e, isRelation ?? undefined)}`
339
+ break
340
+ case "or-scope": {
341
+ if (!every) every = e.relation === "every"
342
+ const rel = isRelationCheck(e.result, isRelation)
343
+ if (rel) {
344
+ s += isRelation
345
+ ? ` OR (${print(e.result, rel, every)})`
346
+ : ` OR ${wrapRelation(rel, print(e.result, rel, every), every)}`
347
+ } else {
348
+ s += ` OR (${print(e.result, null, every)})`
349
+ }
350
+ break
351
+ }
352
+ case "and-scope": {
353
+ if (!every) every = e.relation === "every"
354
+ const rel = isRelationCheck(e.result, isRelation)
355
+ if (rel) {
356
+ s += isRelation
357
+ ? ` AND (${print(e.result, rel, every)})`
358
+ : ` AND ${wrapRelation(rel, print(e.result, rel, every), every)}`
359
+ } else {
360
+ s += ` AND (${print(e.result, null, every)})`
361
+ }
362
+ break
363
+ }
364
+ case "where-scope": {
365
+ if (!every) every = e.relation === "every"
366
+ const rel = isRelationCheck(e.result, isRelation)
367
+ if (rel) {
368
+ s += isRelation
369
+ ? `(${print(e.result, rel, every)})`
370
+ : wrapRelation(rel, print(e.result, rel, every), every)
371
+ } else {
372
+ s += `(${print(e.result, null, every)})`
373
+ }
374
+ break
375
+ }
376
+ }
377
+ }
378
+ return s
379
+ }
380
+
381
+ const getSelectExpr = (): string => {
382
+ if (!select) return "id, _etag, data"
383
+ const fields = select.map((s) => {
384
+ if (typeof s === "string") {
385
+ if (s === idKey || s === "id") return `id`
386
+ if (s === "_etag") return `_etag`
387
+ return `${dialect.jsonExtractJson(s)} AS "${s}"`
388
+ }
389
+ return `${dialect.jsonExtractJson(s.key)} AS "${s.key}"`
390
+ })
391
+ return fields.join(", ")
392
+ }
393
+
394
+ const whereClause = filter.length
395
+ ? `WHERE ${print([{ t: "where-scope", result: filter, relation: "some" }], null, false)}`
396
+ : ""
397
+
398
+ const orderClause = order
399
+ ? `ORDER BY ${order.map((_) => `${fieldExpr(_.key)} ${_.direction}`).join(", ")}`
400
+ : ""
401
+
402
+ const limitClause = limit !== undefined || skip !== undefined
403
+ ? `LIMIT ${addParam(limit ?? 999999)} OFFSET ${addParam(skip ?? 0)}`
404
+ : ""
405
+
406
+ const sql = `SELECT ${getSelectExpr()} FROM "${tableName}" ${whereClause} ${orderClause} ${limitClause}`.trim()
407
+
408
+ return { sql, params }
409
+ }