@effect-app/infra 4.0.0-beta.9 → 4.0.0-beta.90

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