@effect-app/infra 4.0.0-beta.10 → 4.0.0-beta.100

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