@effect-app/infra 4.0.0-beta.315 → 4.0.0-beta.317

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 (46) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/Store/Cosmos/query.d.ts +2 -1
  3. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  4. package/dist/Store/Cosmos/query.js +60 -2
  5. package/dist/Store/Cosmos.d.ts.map +1 -1
  6. package/dist/Store/Cosmos.js +17 -12
  7. package/dist/Store/Disk.d.ts.map +1 -1
  8. package/dist/Store/Disk.js +12 -7
  9. package/dist/Store/Memory.d.ts +2 -1
  10. package/dist/Store/Memory.d.ts.map +1 -1
  11. package/dist/Store/Memory.js +28 -15
  12. package/dist/Store/SQL/Pg.d.ts.map +1 -1
  13. package/dist/Store/SQL/Pg.js +15 -11
  14. package/dist/Store/SQL/query.d.ts +5 -1
  15. package/dist/Store/SQL/query.d.ts.map +1 -1
  16. package/dist/Store/SQL/query.js +131 -2
  17. package/dist/Store/SQL.d.ts +1 -1
  18. package/dist/Store/SQL.d.ts.map +1 -1
  19. package/dist/Store/SQL.js +24 -18
  20. package/dist/Store/codeFilter.d.ts.map +1 -1
  21. package/dist/Store/codeFilter.js +67 -20
  22. package/dist/Store/jsonDocument.d.ts +13 -0
  23. package/dist/Store/jsonDocument.d.ts.map +1 -0
  24. package/dist/Store/jsonDocument.js +31 -0
  25. package/dist/Store/utils.d.ts +16 -0
  26. package/dist/Store/utils.d.ts.map +1 -1
  27. package/dist/Store/utils.js +245 -6
  28. package/dist/logger.d.ts +5 -5
  29. package/examples/query.ts +4 -4
  30. package/package.json +3 -3
  31. package/src/Store/Cosmos/query.ts +65 -9
  32. package/src/Store/Cosmos.ts +17 -11
  33. package/src/Store/Disk.ts +28 -7
  34. package/src/Store/Memory.ts +40 -17
  35. package/src/Store/SQL/Pg.ts +23 -10
  36. package/src/Store/SQL/query.ts +152 -7
  37. package/src/Store/SQL.ts +37 -17
  38. package/src/Store/codeFilter.ts +71 -23
  39. package/src/Store/jsonDocument.ts +43 -0
  40. package/src/Store/utils.ts +275 -5
  41. package/test/cosmos-query.test.ts +100 -5
  42. package/test/dist/json-lower.test.d.ts.map +1 -0
  43. package/test/dist/query.test.d.ts.map +1 -1
  44. package/test/json-lower.test.ts +115 -0
  45. package/test/query.test.ts +261 -12
  46. package/test/sql-store.test.ts +233 -39
package/src/Store/Disk.ts CHANGED
@@ -10,7 +10,9 @@ import * as Console from "effect/Console"
10
10
  import { flow } from "effect/Function"
11
11
  import * as Semaphore from "effect/Semaphore"
12
12
  import { annotateDb } from "../otel.ts"
13
+ import { makeJsonDocumentCodec } from "./jsonDocument.ts"
13
14
  import { makeMemoryStoreInt } from "./Memory.ts"
15
+ import { type JsonLower, makeJsonLower } from "./utils.ts"
14
16
 
15
17
  function makeDiskStoreInt<IdKey extends keyof Encoded, Encoded extends FieldValues, R, E>(
16
18
  prefix: string,
@@ -19,9 +21,12 @@ function makeDiskStoreInt<IdKey extends keyof Encoded, Encoded extends FieldValu
19
21
  dir: string,
20
22
  name: string,
21
23
  seed?: Effect.Effect<Iterable<Encoded>, E, R>,
22
- defaultValues?: Partial<Encoded>
24
+ defaultValues?: Partial<Encoded>,
25
+ schema?: StoreConfig<Encoded>["schema"],
26
+ json?: JsonLower
23
27
  ) {
24
28
  type PM = PersistenceModelType<Encoded>
29
+ const codec = makeJsonDocumentCodec<Encoded>(schema)
25
30
  return Effect.gen(function*() {
26
31
  if (namespace !== "primary") {
27
32
  dir = dir + "/" + namespace
@@ -44,7 +49,7 @@ function makeDiskStoreInt<IdKey extends keyof Encoded, Encoded extends FieldValu
44
49
  extra: fileExtra
45
50
  }),
46
51
  Effect.flatMap((x) =>
47
- Effect.sync(() => JSON.parse(x) as PM[]).pipe(
52
+ Effect.sync(() => (JSON.parse(x) as PM[]).map((row) => codec.decode(row))).pipe(
48
53
  annotateDb({
49
54
  operation: "read.parse",
50
55
  system: "disk",
@@ -67,7 +72,7 @@ function makeDiskStoreInt<IdKey extends keyof Encoded, Encoded extends FieldValu
67
72
  ),
68
73
  setRaw: (v: Iterable<PM>) =>
69
74
  Effect
70
- .sync(() => JSON.stringify([...v], undefined, 2))
75
+ .sync(() => JSON.stringify([...v].map((row) => codec.encode(row)), undefined, 2))
71
76
  .pipe(
72
77
  annotateDb({
73
78
  operation: "stringify",
@@ -117,7 +122,9 @@ function makeDiskStoreInt<IdKey extends keyof Encoded, Encoded extends FieldValu
117
122
  shouldSeed
118
123
  ? seed
119
124
  : fsStore.get,
120
- defaultValues
125
+ defaultValues,
126
+ schema,
127
+ json
121
128
  )
122
129
  if (shouldSeed) {
123
130
  yield* store.all.pipe(Effect.flatMap(fsStore.setRaw))
@@ -175,9 +182,21 @@ export function makeDiskStore({ prefix }: StorageConfig, dir: string) {
175
182
  seed?: Effect.Effect<Iterable<Encoded>, E, R>,
176
183
  config?: StoreConfig<Encoded>
177
184
  ) {
178
- const primary = yield* makeDiskStoreInt(prefix, idKey, "primary", dir, name, seed, config?.defaultValues).pipe(
179
- Effect.orDie
185
+ const json = makeJsonLower(config)
186
+ const primary = yield* makeDiskStoreInt(
187
+ prefix,
188
+ idKey,
189
+ "primary",
190
+ dir,
191
+ name,
192
+ seed,
193
+ config?.defaultValues,
194
+ config?.schema,
195
+ json
180
196
  )
197
+ .pipe(
198
+ Effect.orDie
199
+ )
181
200
  const stores = new Map<string, Store<IdKey, Encoded>>([["primary", primary]])
182
201
  const ctx = yield* Effect.context<R>()
183
202
  const semaphores = new Map<string, Semaphore.Semaphore>()
@@ -204,7 +223,9 @@ export function makeDiskStore({ prefix }: StorageConfig, dir: string) {
204
223
  dir,
205
224
  name,
206
225
  seed,
207
- config?.defaultValues
226
+ config?.defaultValues,
227
+ config?.schema,
228
+ json
208
229
  )
209
230
  .pipe(
210
231
  Effect.orDie,
@@ -18,7 +18,8 @@ import * as Struct from "effect/Struct"
18
18
  import { InfraLogger } from "../logger.ts"
19
19
  import { annotateDb } from "../otel.ts"
20
20
  import { codeFilter, codeFilter3_ } from "./codeFilter.ts"
21
- import { get, makeUpdateETag } from "./utils.ts"
21
+ import { makeJsonDocumentCodec } from "./jsonDocument.ts"
22
+ import { get, jsonifyFilter, type JsonLower, makeJsonLower, makeUpdateETag, toJsonQueryValue } from "./utils.ts"
22
23
 
23
24
  export { get } from "./utils.ts"
24
25
 
@@ -332,25 +333,38 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
332
333
  idKey: IdKey,
333
334
  namespace: string,
334
335
  seed?: Effect.Effect<Iterable<Encoded>, E, R>,
335
- _defaultValues?: Partial<Encoded>
336
+ _defaultValues?: Partial<Encoded>,
337
+ schema?: StoreConfig<Encoded>["schema"],
338
+ json?: JsonLower
336
339
  ) {
337
340
  type PM = PersistenceModelType<Encoded>
338
341
  return Effect.gen(function*() {
339
342
  const updateETag = makeUpdateETag(modelName)
343
+ const codec = makeJsonDocumentCodec<Encoded>(schema)
344
+ const encodeDoc = (e: Encoded | PM): PM => codec.encode({ _etag: undefined, ...e })
345
+ const decodeDoc = (e: PM): PM => codec.decode(e)
340
346
  const items_ = yield* seed ?? Effect.sync(() => [])
341
- const defaultValues = _defaultValues ?? {}
347
+ const toJson = json?.toJson ?? toJsonQueryValue
348
+ const lowerFilter = json?.jsonifyFilter ?? jsonifyFilter
349
+ const encodedDefaults = toJson(_defaultValues ?? {}) as Partial<Encoded>
342
350
 
343
- const items = new Map([...items_].map((_) => [_[idKey], { _etag: undefined, ...defaultValues, ..._ }] as const))
351
+ const items = new Map(
352
+ [...items_].map((_) => {
353
+ const encoded = encodeDoc({ ...encodedDefaults, ..._ })
354
+ return [encoded[idKey], encoded] as const
355
+ })
356
+ )
344
357
  const store = Ref.makeUnsafe<ReadonlyMap<Encoded[IdKey], PM>>(items)
345
358
  const sem = Semaphore.makeUnsafe(1)
346
359
  const withPermit = sem.withPermits(1)
347
360
  const values = Effect.map(Ref.get(store), (s) => s.values())
348
361
 
349
- const all = Effect.map(values, Array.fromIterable)
362
+ const allStored = Effect.map(values, Array.fromIterable)
363
+ const all = Effect.map(allStored, (rows) => rows.map(decodeDoc))
350
364
 
351
365
  const batchSet = (items: NonEmptyReadonlyArray<PM>) =>
352
366
  Effect
353
- .forEach(items, (i) => Effect.flatMap(s.find(i[idKey]), (current) => updateETag(i, idKey, current)))
367
+ .forEach(items, (i) => Effect.flatMap(s.find(i[idKey]), (current) => updateETag(encodeDoc(i), idKey, current)))
354
368
  .pipe(
355
369
  Effect
356
370
  .tap((items) =>
@@ -368,7 +382,7 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
368
382
  )
369
383
  ),
370
384
  Effect
371
- .map((_) => _),
385
+ .map((items) => items.map(decodeDoc) as unknown as NonEmptyReadonlyArray<PM>),
372
386
  withPermit
373
387
  )
374
388
 
@@ -414,7 +428,7 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
414
428
  Ref
415
429
  .get(store)
416
430
  .pipe(
417
- Effect.map((_) => Option.fromNullishOr(_.get(id))),
431
+ Effect.map((_) => Option.fromNullishOr(_.get(id)).pipe(Option.map(decodeDoc))),
418
432
  annotateDb({
419
433
  operation: "find",
420
434
  system: "memory",
@@ -424,11 +438,16 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
424
438
  extra: { "app.entity.id": id }
425
439
  })
426
440
  ),
427
- filter: (f) =>
428
- all
441
+ filter: <U extends keyof Encoded = never>(f: FilterArgs<Encoded, U>) =>
442
+ allStored
429
443
  .pipe(
430
- Effect.tap(() => logQuery(f, defaultValues)),
431
- Effect.map(memFilter(f)),
444
+ Effect.tap(() => logQuery(f, encodedDefaults)),
445
+ Effect.map(memFilter({ ...f, filter: f.filter ? lowerFilter(f.filter) : f.filter })),
446
+ Effect.map((rows): (U extends undefined ? Encoded : Pick<Encoded, U>)[] =>
447
+ f.select
448
+ ? rows as (U extends undefined ? Encoded : Pick<Encoded, U>)[]
449
+ : rows.map(decodeDoc) as (U extends undefined ? Encoded : Pick<Encoded, U>)[]
450
+ ),
432
451
  annotateDb({
433
452
  operation: "filter",
434
453
  system: "memory",
@@ -441,14 +460,15 @@ export function makeMemoryStoreInt<IdKey extends keyof Encoded, Encoded extends
441
460
  s
442
461
  .find(e[idKey])
443
462
  .pipe(
444
- Effect.flatMap((current) => updateETag(e, idKey, current)),
463
+ Effect.flatMap((current) => updateETag(encodeDoc(e), idKey, current.pipe(Option.map(encodeDoc)))),
445
464
  Effect
446
- .tap((e) =>
465
+ .tap((stored) =>
447
466
  Ref.get(store).pipe(
448
- Effect.map((_) => new Map([..._, [e[idKey], e]])),
467
+ Effect.map((_) => new Map([..._, [stored[idKey], stored]])),
449
468
  Effect.flatMap((_) => Ref.set(store, _))
450
469
  )
451
470
  ),
471
+ Effect.map(decodeDoc),
452
472
  withPermit,
453
473
  annotateDb({
454
474
  operation: "set",
@@ -518,12 +538,15 @@ export const makeMemoryStore = () => ({
518
538
  seed?: Effect.Effect<Iterable<Encoded>, E, R>,
519
539
  config?: StoreConfig<Encoded>
520
540
  ) {
541
+ const json = makeJsonLower(config)
521
542
  const primary = yield* makeMemoryStoreInt<IdKey, Encoded, R, E>(
522
543
  modelName,
523
544
  idKey,
524
545
  "primary",
525
546
  seed,
526
- config?.defaultValues
547
+ config?.defaultValues,
548
+ config?.schema,
549
+ json
527
550
  )
528
551
  const ctx = yield* Effect.context<R>()
529
552
  const stores = new Map([["primary", primary]])
@@ -543,7 +566,7 @@ export const makeMemoryStore = () => ({
543
566
  if (config?.allowNamespace && !config.allowNamespace(namespace)) {
544
567
  throw new Error(`Namespace ${namespace} not allowed!`)
545
568
  }
546
- return makeMemoryStoreInt(modelName, idKey, namespace, seed, config?.defaultValues)
569
+ return makeMemoryStoreInt(modelName, idKey, namespace, seed, config?.defaultValues, config?.schema, json)
547
570
  .pipe(
548
571
  Effect.orDie,
549
572
  Effect.provide(ctx),
@@ -12,7 +12,8 @@ import { SqlClient } from "effect/unstable/sql"
12
12
  import { DatabaseError, OptimisticConcurrencyException } from "../../errors.ts"
13
13
  import { InfraLogger } from "../../logger.ts"
14
14
  import { annotateDb } from "../../otel.ts"
15
- import { makeETag } from "../utils.ts"
15
+ import { makeJsonDocumentCodec } from "../jsonDocument.ts"
16
+ import { makeETag, makeJsonLower, toJsonQueryValue } from "../utils.ts"
16
17
  import { buildWhereSQLQuery, logQuery, pgDialect } from "./query.ts"
17
18
 
18
19
  const sqlErrorMessage = (e: unknown) => (e as any)?.message ? String((e as any).message) : String(e)
@@ -36,10 +37,14 @@ const preserveStoreError = (e: unknown): DatabaseError | OptimisticConcurrencyEx
36
37
  const parseRow = <Encoded extends FieldValues>(
37
38
  row: { id: string; _etag: string | null; data: unknown },
38
39
  idKey: PropertyKey,
39
- defaultValues: Partial<Encoded>
40
+ defaultValues: Partial<Encoded>,
41
+ decode: (doc: PersistenceModelType<Encoded>) => PersistenceModelType<Encoded> = (doc) => doc
40
42
  ): PersistenceModelType<Encoded> => {
41
43
  const data = (typeof row.data === "string" ? JSON.parse(row.data) : row.data) as object
42
- return { ...defaultValues, ...data, [idKey]: row.id, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
44
+ const jsonDefaults = toJsonQueryValue(defaultValues) as Partial<Encoded>
45
+ return decode(
46
+ { ...jsonDefaults, ...data, [idKey]: row.id, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
47
+ )
43
48
  }
44
49
 
45
50
  const parseSelectRow = (
@@ -70,7 +75,9 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
70
75
  ) {
71
76
  type PM = PersistenceModelType<Encoded>
72
77
  const tableName = `${prefix}${name}`
73
- const defaultValues = config?.defaultValues ?? {}
78
+ const json = makeJsonLower(config)
79
+ const defaultValues = json.toJson(config?.defaultValues ?? {}) as Partial<Encoded>
80
+ const codec = makeJsonDocumentCodec<Encoded>(config?.schema)
74
81
 
75
82
  const resolveNamespace = !config?.allowNamespace
76
83
  ? Effect.succeed("primary")
@@ -96,11 +103,11 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
96
103
  )
97
104
 
98
105
  const toRow = (e: PM) => {
99
- const newE = makeETag(e)
106
+ const newE = makeETag(codec.encode(e))
100
107
  const id = newE[idKey] as string
101
108
  const { _etag, [idKey]: _id, ...rest } = newE as any
102
109
  const data = JSON.stringify(rest)
103
- return { id, _etag: newE._etag!, data, item: newE }
110
+ return { id, _etag: newE._etag!, data, item: codec.decode(newE) }
104
111
  }
105
112
 
106
113
  const exec = (query: string, params?: readonly unknown[]) =>
@@ -193,7 +200,9 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
193
200
  const sqlText = `SELECT id, _etag, data FROM "${tableName}" WHERE _namespace = $1`
194
201
  return exec(sqlText, [ns])
195
202
  .pipe(
196
- Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
203
+ Effect.map((rows) =>
204
+ (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues, codec.decode))
205
+ ),
197
206
  annotateDb({
198
207
  operation: "all",
199
208
  system: "postgresql",
@@ -215,7 +224,7 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
215
224
  Effect.map((rows) => {
216
225
  const row = (rows as any[])[0]
217
226
  return row
218
- ? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
227
+ ? Option.some(parseRow<Encoded>(row, idKey, defaultValues, codec.decode))
219
228
  : Option.none()
220
229
  }),
221
230
  annotateDb({
@@ -256,7 +265,9 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
256
265
  | undefined,
257
266
  f.order,
258
267
  f.skip,
259
- f.limit
268
+ f.limit,
269
+ ns,
270
+ json
260
271
  )
261
272
  const nsPlaceholder = pgDialect.placeholder(q.params.length + 1)
262
273
  const hasWhere = q.sql.includes("WHERE")
@@ -286,7 +297,9 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
286
297
  } as M
287
298
  })
288
299
  }
289
- return (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues) as any as M)
300
+ return (rows as any[]).map((r) =>
301
+ parseRow<Encoded>(r, idKey, defaultValues, codec.decode) as any as M
302
+ )
290
303
  })
291
304
  )
292
305
  ),
@@ -6,6 +6,7 @@ import type { AggregateIrExpression, ComputedProjectionIrExpression, ComputedPro
6
6
  import { assertUnreachable } from "effect-app/utils"
7
7
  import { InfraLogger } from "../../logger.ts"
8
8
  import { isRelationCheck } from "../codeFilter.ts"
9
+ import { jsonifyFilter, type JsonLower, toJsonQueryValue } from "../utils.ts"
9
10
 
10
11
  export interface SQLDialect {
11
12
  readonly jsonExtract: (path: string) => string
@@ -17,6 +18,9 @@ export interface SQLDialect {
17
18
  readonly jsonArrayNotContainsAny: (arrPath: string, valPlaceholders: readonly string[]) => string
18
19
  readonly jsonArrayContainsAll: (arrPath: string, valPlaceholders: readonly string[]) => string
19
20
  readonly jsonArrayNotContainsAll: (arrPath: string, valPlaceholders: readonly string[]) => string
21
+ readonly jsonMapHasKey: (arrPath: string, valPlaceholder: string) => string
22
+ readonly jsonMapHasValue: (arrPath: string, valPlaceholder: string) => string
23
+ readonly jsonMapHasPair: (arrPath: string, valPlaceholder: string) => string
20
24
  readonly caseInsensitiveLike: (expr: string, valPlaceholder: string) => string
21
25
  readonly caseInsensitiveNotLike: (expr: string, valPlaceholder: string) => string
22
26
  readonly jsonColumnType: "JSON" | "JSONB"
@@ -45,6 +49,11 @@ export const sqliteDialect: SQLDialect = {
45
49
  `NOT (${
46
50
  vals.map((v) => `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value = ${v})`).join(" AND ")
47
51
  })`,
52
+ jsonMapHasKey: (arrPath, val) =>
53
+ `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE json_extract(value, '$[0]') = ${val})`,
54
+ jsonMapHasValue: (arrPath, val) =>
55
+ `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE json_extract(value, '$[1]') = ${val})`,
56
+ jsonMapHasPair: (arrPath, val) => `EXISTS(SELECT 1 FROM json_each(data, '$.${arrPath}') WHERE value = ${val})`,
48
57
  caseInsensitiveLike: (expr, val) => `LOWER(${expr}) LIKE LOWER(${val})`,
49
58
  caseInsensitiveNotLike: (expr, val) => `LOWER(${expr}) NOT LIKE LOWER(${val})`,
50
59
  jsonColumnType: "JSON",
@@ -112,6 +121,27 @@ export const pgDialect: SQLDialect = {
112
121
  : `data${parts.map((p) => `->'${p}'`).join("")}`
113
122
  return `NOT (${vals.map((v) => `${jsonPath} @> ${v}::jsonb`).join(" AND ")})`
114
123
  },
124
+ jsonMapHasKey: (arrPath, val) => {
125
+ const parts = arrPath.split(".")
126
+ const jsonPath = parts.length === 1
127
+ ? `data->'${parts[0]}'`
128
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
129
+ return `EXISTS(SELECT 1 FROM jsonb_array_elements(${jsonPath}) e WHERE e->0 = ${val}::jsonb)`
130
+ },
131
+ jsonMapHasValue: (arrPath, val) => {
132
+ const parts = arrPath.split(".")
133
+ const jsonPath = parts.length === 1
134
+ ? `data->'${parts[0]}'`
135
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
136
+ return `EXISTS(SELECT 1 FROM jsonb_array_elements(${jsonPath}) e WHERE e->1 = ${val}::jsonb)`
137
+ },
138
+ jsonMapHasPair: (arrPath, val) => {
139
+ const parts = arrPath.split(".")
140
+ const jsonPath = parts.length === 1
141
+ ? `data->'${parts[0]}'`
142
+ : `data${parts.map((p) => `->'${p}'`).join("")}`
143
+ return `${jsonPath} @> jsonb_build_array(${val}::jsonb)`
144
+ },
115
145
  caseInsensitiveLike: (expr, val) => `${expr} ILIKE ${val}`,
116
146
  caseInsensitiveNotLike: (expr, val) => `${expr} NOT ILIKE ${val}`,
117
147
  jsonColumnType: "JSONB",
@@ -149,6 +179,14 @@ const dottedToJsonPath = (path: string) =>
149
179
  .filter((p) => p !== "-1")
150
180
  .join(".")
151
181
 
182
+ const mapItemParam = (dialect: SQLDialect, value: unknown) =>
183
+ dialect.jsonColumnType === "JSONB" ? dialect.serializeJsonValue(value) : value
184
+
185
+ const mapPairParam = (dialect: SQLDialect, value: unknown) => {
186
+ const encoded = dialect.serializeJsonValue(value)
187
+ return typeof encoded === "string" ? encoded : JSON.stringify(value)
188
+ }
189
+
152
190
  const sqlStringLiteral = (value: string) => `'${value.replaceAll("'", "''")}'`
153
191
 
154
192
  export function buildWhereSQLQuery(
@@ -175,8 +213,12 @@ export function buildWhereSQLQuery(
175
213
  order?: NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }>,
176
214
  skip?: number,
177
215
  limit?: number,
178
- namespace?: string
216
+ namespace?: string,
217
+ json?: JsonLower
179
218
  ) {
219
+ const toJson = json?.toJson ?? toJsonQueryValue
220
+ filter = (json?.jsonifyFilter ?? jsonifyFilter)(filter)
221
+ defaultValues = toJson(defaultValues) as Record<string, unknown>
180
222
  const params: unknown[] = []
181
223
  let paramIndex = 1
182
224
 
@@ -214,7 +256,7 @@ export function buildWhereSQLQuery(
214
256
 
215
257
  switch (x.op) {
216
258
  case "in": {
217
- const vals = x.value as unknown as readonly unknown[]
259
+ const vals = x.value as readonly unknown[]
218
260
  const hasNull = vals.some((v) => v == null)
219
261
  const nonNullVals = vals.filter((v) => v != null)
220
262
  const parts: string[] = []
@@ -226,7 +268,7 @@ export function buildWhereSQLQuery(
226
268
  return parts.length > 1 ? `(${parts.join(" OR ")})` : parts[0] ?? "1=0"
227
269
  }
228
270
  case "notIn": {
229
- const vals = x.value as unknown as readonly unknown[]
271
+ const vals = x.value as readonly unknown[]
230
272
  const hasNull = vals.some((v) => v == null)
231
273
  const nonNullVals = vals.filter((v) => v != null)
232
274
  const parts: string[] = []
@@ -251,30 +293,133 @@ export function buildWhereSQLQuery(
251
293
 
252
294
  case "includes-any": {
253
295
  const arrPath = dottedToJsonPath(resolvedPath)
254
- const vals = x.value as unknown as readonly unknown[]
296
+ const vals = x.value as readonly unknown[]
255
297
  const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
256
298
  return dialect.jsonArrayContainsAny(arrPath, placeholders)
257
299
  }
258
300
  case "notIncludes-any": {
259
301
  const arrPath = dottedToJsonPath(resolvedPath)
260
- const vals = x.value as unknown as readonly unknown[]
302
+ const vals = x.value as readonly unknown[]
261
303
  const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
262
304
  return dialect.jsonArrayNotContainsAny(arrPath, placeholders)
263
305
  }
264
306
 
265
307
  case "includes-all": {
266
308
  const arrPath = dottedToJsonPath(resolvedPath)
267
- const vals = x.value as unknown as readonly unknown[]
309
+ const vals = x.value as readonly unknown[]
268
310
  const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
269
311
  return dialect.jsonArrayContainsAll(arrPath, placeholders)
270
312
  }
271
313
  case "notIncludes-all": {
272
314
  const arrPath = dottedToJsonPath(resolvedPath)
273
- const vals = x.value as unknown as readonly unknown[]
315
+ const vals = x.value as readonly unknown[]
274
316
  const placeholders = vals.map((v) => addParam(dialect.serializeJsonValue(v)))
275
317
  return dialect.jsonArrayNotContainsAll(arrPath, placeholders)
276
318
  }
277
319
 
320
+ case "hasKey": {
321
+ const arrPath = dottedToJsonPath(resolvedPath)
322
+ const v = addParam(mapItemParam(dialect, x.value))
323
+ return dialect.jsonMapHasKey(arrPath, v)
324
+ }
325
+ case "notHasKey": {
326
+ const arrPath = dottedToJsonPath(resolvedPath)
327
+ const v = addParam(mapItemParam(dialect, x.value))
328
+ return `NOT (${dialect.jsonMapHasKey(arrPath, v)})`
329
+ }
330
+ case "hasValue": {
331
+ const arrPath = dottedToJsonPath(resolvedPath)
332
+ const v = addParam(mapItemParam(dialect, x.value))
333
+ return dialect.jsonMapHasValue(arrPath, v)
334
+ }
335
+ case "notHasValue": {
336
+ const arrPath = dottedToJsonPath(resolvedPath)
337
+ const v = addParam(mapItemParam(dialect, x.value))
338
+ return `NOT (${dialect.jsonMapHasValue(arrPath, v)})`
339
+ }
340
+ case "hasKeyValue": {
341
+ const arrPath = dottedToJsonPath(resolvedPath)
342
+ const v = addParam(mapPairParam(dialect, x.value))
343
+ return dialect.jsonMapHasPair(arrPath, v)
344
+ }
345
+ case "notHasKeyValue": {
346
+ const arrPath = dottedToJsonPath(resolvedPath)
347
+ const v = addParam(mapPairParam(dialect, x.value))
348
+ return `NOT (${dialect.jsonMapHasPair(arrPath, v)})`
349
+ }
350
+ case "hasKey-any": {
351
+ const arrPath = dottedToJsonPath(resolvedPath)
352
+ const vals = x.value as readonly unknown[]
353
+ const parts = vals.map((val) => dialect.jsonMapHasKey(arrPath, addParam(mapItemParam(dialect, val))))
354
+ return `(${parts.join(" OR ")})`
355
+ }
356
+ case "notHasKey-any": {
357
+ const arrPath = dottedToJsonPath(resolvedPath)
358
+ const vals = x.value as readonly unknown[]
359
+ const parts = vals.map((val) => dialect.jsonMapHasKey(arrPath, addParam(mapItemParam(dialect, val))))
360
+ return `NOT (${parts.join(" OR ")})`
361
+ }
362
+ case "hasKey-all": {
363
+ const arrPath = dottedToJsonPath(resolvedPath)
364
+ const vals = x.value as readonly unknown[]
365
+ return vals.map((val) => dialect.jsonMapHasKey(arrPath, addParam(mapItemParam(dialect, val)))).join(" AND ")
366
+ }
367
+ case "notHasKey-all": {
368
+ const arrPath = dottedToJsonPath(resolvedPath)
369
+ const vals = x.value as readonly unknown[]
370
+ return `NOT (${
371
+ vals.map((val) => dialect.jsonMapHasKey(arrPath, addParam(mapItemParam(dialect, val)))).join(" AND ")
372
+ })`
373
+ }
374
+ case "hasValue-any": {
375
+ const arrPath = dottedToJsonPath(resolvedPath)
376
+ const vals = x.value as readonly unknown[]
377
+ const parts = vals.map((val) => dialect.jsonMapHasValue(arrPath, addParam(mapItemParam(dialect, val))))
378
+ return `(${parts.join(" OR ")})`
379
+ }
380
+ case "notHasValue-any": {
381
+ const arrPath = dottedToJsonPath(resolvedPath)
382
+ const vals = x.value as readonly unknown[]
383
+ const parts = vals.map((val) => dialect.jsonMapHasValue(arrPath, addParam(mapItemParam(dialect, val))))
384
+ return `NOT (${parts.join(" OR ")})`
385
+ }
386
+ case "hasValue-all": {
387
+ const arrPath = dottedToJsonPath(resolvedPath)
388
+ const vals = x.value as readonly unknown[]
389
+ return vals.map((val) => dialect.jsonMapHasValue(arrPath, addParam(mapItemParam(dialect, val)))).join(" AND ")
390
+ }
391
+ case "notHasValue-all": {
392
+ const arrPath = dottedToJsonPath(resolvedPath)
393
+ const vals = x.value as readonly unknown[]
394
+ return `NOT (${
395
+ vals.map((val) => dialect.jsonMapHasValue(arrPath, addParam(mapItemParam(dialect, val)))).join(" AND ")
396
+ })`
397
+ }
398
+ case "hasKeyValue-any": {
399
+ const arrPath = dottedToJsonPath(resolvedPath)
400
+ const vals = x.value as readonly unknown[]
401
+ const parts = vals.map((val) => dialect.jsonMapHasPair(arrPath, addParam(mapPairParam(dialect, val))))
402
+ return `(${parts.join(" OR ")})`
403
+ }
404
+ case "notHasKeyValue-any": {
405
+ const arrPath = dottedToJsonPath(resolvedPath)
406
+ const vals = x.value as readonly unknown[]
407
+ const parts = vals.map((val) => dialect.jsonMapHasPair(arrPath, addParam(mapPairParam(dialect, val))))
408
+ return `NOT (${parts.join(" OR ")})`
409
+ }
410
+ case "hasKeyValue-all": {
411
+ const arrPath = dottedToJsonPath(resolvedPath)
412
+ const vals = x.value as readonly unknown[]
413
+ return vals.map((val) => dialect.jsonMapHasPair(arrPath, addParam(mapPairParam(dialect, val)))).join(" AND ")
414
+ }
415
+ case "notHasKeyValue-all": {
416
+ const arrPath = dottedToJsonPath(resolvedPath)
417
+ const vals = x.value as readonly unknown[]
418
+ return `NOT (${
419
+ vals.map((val) => dialect.jsonMapHasPair(arrPath, addParam(mapPairParam(dialect, val)))).join(" AND ")
420
+ })`
421
+ }
422
+
278
423
  case "contains": {
279
424
  const v = addParam(`%${x.value}%`)
280
425
  return dialect.caseInsensitiveLike(k, v)