@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/SQL.ts CHANGED
@@ -15,8 +15,9 @@ import { SqlClient } from "effect/unstable/sql"
15
15
  import { DatabaseError, OptimisticConcurrencyException } from "../errors.ts"
16
16
  import { InfraLogger } from "../logger.ts"
17
17
  import { annotateDb, type DbSystem } from "../otel.ts"
18
+ import { makeJsonDocumentCodec } from "./jsonDocument.ts"
18
19
  import { buildWhereSQLQuery, logQuery, type SQLDialect, sqliteDialect } from "./SQL/query.ts"
19
- import { makeETag } from "./utils.ts"
20
+ import { makeETag, makeJsonLower, toJsonQueryValue } from "./utils.ts"
20
21
 
21
22
  const sqlErrorMessage = (e: unknown) => (e as any)?.message ? String((e as any).message) : String(e)
22
23
  const sqlIsTransient = (e: unknown) =>
@@ -46,10 +47,14 @@ export class WithNsTransaction
46
47
  export const parseRow = <Encoded extends FieldValues>(
47
48
  row: { id: string; _etag: string | null; data: string },
48
49
  idKey: PropertyKey,
49
- defaultValues: Partial<Encoded>
50
+ defaultValues: Partial<Encoded>,
51
+ decode: (doc: PersistenceModelType<Encoded>) => PersistenceModelType<Encoded> = (doc) => doc
50
52
  ): PersistenceModelType<Encoded> => {
51
53
  const data = (typeof row.data === "string" ? JSON.parse(row.data) : row.data) as object
52
- return { ...defaultValues, ...data, [idKey]: row.id, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
54
+ const jsonDefaults = toJsonQueryValue(defaultValues) as Partial<Encoded>
55
+ return decode(
56
+ { ...jsonDefaults, ...data, [idKey]: row.id, _etag: row._etag ?? undefined } as PersistenceModelType<Encoded>
57
+ )
53
58
  }
54
59
 
55
60
  const parseSelectRow = (
@@ -86,7 +91,9 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
86
91
  ) {
87
92
  type PM = PersistenceModelType<Encoded>
88
93
  const tableName = `${prefix}${name}`
89
- const defaultValues = config?.defaultValues ?? {}
94
+ const json = makeJsonLower(config)
95
+ const defaultValues = json.toJson(config?.defaultValues ?? {}) as Partial<Encoded>
96
+ const codec = makeJsonDocumentCodec<Encoded>(config?.schema)
90
97
 
91
98
  const resolveNamespace = !config?.allowNamespace
92
99
  ? Effect.succeed("primary")
@@ -112,11 +119,11 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
112
119
  )
113
120
 
114
121
  const toRow = (e: PM) => {
115
- const newE = makeETag(e)
122
+ const newE = makeETag(codec.encode(e))
116
123
  const id = newE[idKey] as string
117
124
  const { _etag, [idKey]: _id, ...rest } = newE as any
118
125
  const data = JSON.stringify(rest)
119
- return { id, _etag: newE._etag!, data, item: newE }
126
+ return { id, _etag: newE._etag!, data, item: codec.decode(newE) }
120
127
  }
121
128
 
122
129
  const exec = (query: string, params?: readonly unknown[]) =>
@@ -209,7 +216,9 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
209
216
  const sqlText = `SELECT id, _etag, data FROM "${tableName}" WHERE _namespace = ?`
210
217
  return exec(sqlText, [ns])
211
218
  .pipe(
212
- Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
219
+ Effect.map((rows) =>
220
+ (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues, codec.decode))
221
+ ),
213
222
  annotateDb({
214
223
  operation: "all",
215
224
  system,
@@ -231,7 +240,7 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
231
240
  Effect.map((rows) => {
232
241
  const row = (rows as any[])[0]
233
242
  return row
234
- ? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
243
+ ? Option.some(parseRow<Encoded>(row, idKey, defaultValues, codec.decode))
235
244
  : Option.none()
236
245
  }),
237
246
  annotateDb({
@@ -281,7 +290,8 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
281
290
  .skip,
282
291
  f
283
292
  .limit,
284
- ns
293
+ ns,
294
+ json
285
295
  )
286
296
  })
287
297
  .pipe(
@@ -303,7 +313,9 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
303
313
  } as M
304
314
  })
305
315
  }
306
- return (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues) as any as M)
316
+ return (rows as any[]).map((r) =>
317
+ parseRow<Encoded>(r, idKey, defaultValues, codec.decode) as any as M
318
+ )
307
319
  })
308
320
  )
309
321
  ),
@@ -418,7 +430,9 @@ function makeSQLiteStorePerNs(
418
430
  ) {
419
431
  type PM = PersistenceModelType<Encoded>
420
432
  const tableName = `${prefix}${name}`
421
- const defaultValues = config?.defaultValues ?? {}
433
+ const json = makeJsonLower(config)
434
+ const defaultValues = json.toJson(config?.defaultValues ?? {}) as Partial<Encoded>
435
+ const codec = makeJsonDocumentCodec<Encoded>(config?.schema)
422
436
 
423
437
  const resolveNamespace = !config?.allowNamespace
424
438
  ? Effect.succeed("primary")
@@ -430,11 +444,11 @@ function makeSQLiteStorePerNs(
430
444
  }))
431
445
 
432
446
  const toRow = (e: PM) => {
433
- const newE = makeETag(e)
447
+ const newE = makeETag(codec.encode(e))
434
448
  const id = newE[idKey] as string
435
449
  const { _etag, [idKey]: _id, ...rest } = newE as any
436
450
  const data = JSON.stringify(rest)
437
- return { id, _etag: newE._etag!, data, item: newE }
451
+ return { id, _etag: newE._etag!, data, item: codec.decode(newE) }
438
452
  }
439
453
 
440
454
  const exec = (ns: string, query: string, params?: readonly unknown[]) =>
@@ -549,7 +563,9 @@ function makeSQLiteStorePerNs(
549
563
  const sqlText = `SELECT id, _etag, data FROM "${tableName}"`
550
564
  return exec(ns, sqlText)
551
565
  .pipe(
552
- Effect.map((rows) => (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues))),
566
+ Effect.map((rows) =>
567
+ (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues, codec.decode))
568
+ ),
553
569
  annotateDb({
554
570
  operation: "all",
555
571
  system: "sqlite",
@@ -570,7 +586,7 @@ function makeSQLiteStorePerNs(
570
586
  Effect.map((rows) => {
571
587
  const row = (rows as any[])[0]
572
588
  return row
573
- ? Option.some(parseRow<Encoded>(row, idKey, defaultValues))
589
+ ? Option.some(parseRow<Encoded>(row, idKey, defaultValues, codec.decode))
574
590
  : Option.none()
575
591
  }),
576
592
  annotateDb({
@@ -619,7 +635,9 @@ function makeSQLiteStorePerNs(
619
635
  f
620
636
  .skip,
621
637
  f
622
- .limit
638
+ .limit,
639
+ undefined,
640
+ json
623
641
  )
624
642
  )
625
643
  .pipe(
@@ -641,7 +659,9 @@ function makeSQLiteStorePerNs(
641
659
  } as M
642
660
  })
643
661
  }
644
- return (rows as any[]).map((r) => parseRow<Encoded>(r, idKey, defaultValues) as any as M)
662
+ return (rows as any[]).map((r) =>
663
+ parseRow<Encoded>(r, idKey, defaultValues, codec.decode) as any as M
664
+ )
645
665
  })
646
666
  )
647
667
  ),
@@ -6,54 +6,102 @@ import type { FieldValues } from "effect-app/Model/filter/types"
6
6
  import * as Option from "effect-app/Option"
7
7
  import type { Filter } from "effect-app/Store"
8
8
  import { assertUnreachable } from "effect-app/utils"
9
- import { compare, get, greaterThan, greaterThanExclusive, lowerThan, lowerThanExclusive } from "./utils.ts"
9
+ import { compare, get, greaterThan, greaterThanExclusive, lowerThan, lowerThanExclusive, toJsonQueryValue } from "./utils.ts"
10
10
 
11
- const vAsArr = (v: string) => v as unknown as any[]
11
+ const vAsArr = (v: unknown) => toJsonQueryValue(v) as any[]
12
+
13
+ const mapEntries = (value: unknown): readonly [unknown, unknown][] => {
14
+ const json = toJsonQueryValue(value)
15
+ if (!Array.isArray(json)) return []
16
+ return json.filter((entry): entry is [unknown, unknown] => Array.isArray(entry) && entry.length >= 2)
17
+ }
18
+
19
+ const pairEq = (entry: readonly [unknown, unknown], pair: unknown) => {
20
+ const json = toJsonQueryValue(pair)
21
+ return Array.isArray(json) && json.length >= 2 && compare(entry[0], json[0]) && compare(entry[1], json[1])
22
+ }
12
23
 
13
24
  const filterStatement = (x: any, p: FilterR) => {
14
- const k = get(x, p.path)
25
+ const k = toJsonQueryValue(get(x, p.path))
26
+ const v = toJsonQueryValue(p.value)
15
27
  switch (p.op) {
16
28
  case "in":
17
- return p.value.includes(k)
29
+ return (v as unknown[]).includes(k)
18
30
  case "notIn":
19
- return !p.value.includes(k)
31
+ return !(v as unknown[]).includes(k)
20
32
  case "lt":
21
- return lowerThan(k, p.value)
33
+ return lowerThan(k as any, v as any)
22
34
  case "lte":
23
- return lowerThanExclusive(k, p.value)
35
+ return lowerThanExclusive(k as any, v as any)
24
36
  case "gt":
25
- return greaterThan(k, p.value)
37
+ return greaterThan(k as any, v as any)
26
38
  case "gte":
27
- return greaterThanExclusive(k, p.value)
39
+ return greaterThanExclusive(k as any, v as any)
28
40
  case "includes":
29
- return (k as Array<string>).includes(p.value)
41
+ return (k as Array<unknown>).includes(v)
30
42
  case "notIncludes":
31
- return !(k as Array<string>).includes(p.value)
43
+ return !(k as Array<unknown>).includes(v)
32
44
  case "includes-any":
33
- return (vAsArr(p.value)).some((_) => (k as Array<string>)?.includes(_))
45
+ return (vAsArr(p.value)).some((_) => (k as Array<unknown>)?.includes(_))
34
46
  case "notIncludes-any":
35
- return !(vAsArr(p.value)).some((_) => (k as Array<string>)?.includes(_))
47
+ return !(vAsArr(p.value)).some((_) => (k as Array<unknown>)?.includes(_))
36
48
  case "includes-all":
37
- return (vAsArr(p.value)).every((_) => (k as Array<string>)?.includes(_))
49
+ return (vAsArr(p.value)).every((_) => (k as Array<unknown>)?.includes(_))
38
50
  case "notIncludes-all":
39
- return !(vAsArr(p.value)).every((_) => (k as Array<string>)?.includes(_))
51
+ return !(vAsArr(p.value)).every((_) => (k as Array<unknown>)?.includes(_))
52
+ case "hasKey":
53
+ return mapEntries(k).some(([key]) => compare(key, v))
54
+ case "notHasKey":
55
+ return !mapEntries(k).some(([key]) => compare(key, v))
56
+ case "hasValue":
57
+ return mapEntries(k).some(([, val]) => compare(val, v))
58
+ case "notHasValue":
59
+ return !mapEntries(k).some(([, val]) => compare(val, v))
60
+ case "hasKeyValue":
61
+ return mapEntries(k).some((entry) => pairEq(entry, v))
62
+ case "notHasKeyValue":
63
+ return !mapEntries(k).some((entry) => pairEq(entry, v))
64
+ case "hasKey-any":
65
+ return vAsArr(p.value).some((key) => mapEntries(k).some(([k0]) => compare(k0, key)))
66
+ case "notHasKey-any":
67
+ return !vAsArr(p.value).some((key) => mapEntries(k).some(([k0]) => compare(k0, key)))
68
+ case "hasKey-all":
69
+ return vAsArr(p.value).every((key) => mapEntries(k).some(([k0]) => compare(k0, key)))
70
+ case "notHasKey-all":
71
+ return !vAsArr(p.value).every((key) => mapEntries(k).some(([k0]) => compare(k0, key)))
72
+ case "hasValue-any":
73
+ return vAsArr(p.value).some((val) => mapEntries(k).some(([, v0]) => compare(v0, val)))
74
+ case "notHasValue-any":
75
+ return !vAsArr(p.value).some((val) => mapEntries(k).some(([, v0]) => compare(v0, val)))
76
+ case "hasValue-all":
77
+ return vAsArr(p.value).every((val) => mapEntries(k).some(([, v0]) => compare(v0, val)))
78
+ case "notHasValue-all":
79
+ return !vAsArr(p.value).every((val) => mapEntries(k).some(([, v0]) => compare(v0, val)))
80
+ case "hasKeyValue-any":
81
+ return vAsArr(p.value).some((pair) => mapEntries(k).some((entry) => pairEq(entry, pair)))
82
+ case "notHasKeyValue-any":
83
+ return !vAsArr(p.value).some((pair) => mapEntries(k).some((entry) => pairEq(entry, pair)))
84
+ case "hasKeyValue-all":
85
+ return vAsArr(p.value).every((pair) => mapEntries(k).some((entry) => pairEq(entry, pair)))
86
+ case "notHasKeyValue-all":
87
+ return !vAsArr(p.value).every((pair) => mapEntries(k).some((entry) => pairEq(entry, pair)))
40
88
  case "contains":
41
- return (k as string).toLowerCase().includes(p.value.toLowerCase())
89
+ return (k as string).toLowerCase().includes((v as string).toLowerCase())
42
90
  case "endsWith":
43
- return (k as string).toLowerCase().endsWith(p.value.toLowerCase())
91
+ return (k as string).toLowerCase().endsWith((v as string).toLowerCase())
44
92
  case "startsWith":
45
- return (k as string).toLowerCase().startsWith(p.value.toLowerCase())
93
+ return (k as string).toLowerCase().startsWith((v as string).toLowerCase())
46
94
  case "notContains":
47
- return !(k as string).toLowerCase().includes(p.value.toLowerCase())
95
+ return !(k as string).toLowerCase().includes((v as string).toLowerCase())
48
96
  case "notEndsWith":
49
- return !(k as string).toLowerCase().endsWith(p.value.toLowerCase())
97
+ return !(k as string).toLowerCase().endsWith((v as string).toLowerCase())
50
98
  case "notStartsWith":
51
- return !(k as string).toLowerCase().startsWith(p.value.toLowerCase())
99
+ return !(k as string).toLowerCase().startsWith((v as string).toLowerCase())
52
100
  case "neq":
53
- return !compare(k, p.value)
101
+ return !compare(k, v)
54
102
  case "eq":
55
103
  case undefined:
56
- return compare(k, p.value)
104
+ return compare(k, v)
57
105
  default: {
58
106
  return assertUnreachable(p.op)
59
107
  }
@@ -0,0 +1,43 @@
1
+ import type { FieldValues } from "effect-app/Model/filter/types"
2
+ import * as S from "effect-app/Schema"
3
+ import type { PersistenceModelType } from "effect-app/Store"
4
+ import { toJsonQueryValue } from "./utils.ts"
5
+
6
+ export interface JsonDocumentCodec<E extends FieldValues> {
7
+ readonly encode: (doc: PersistenceModelType<E>) => PersistenceModelType<E>
8
+ readonly decode: (doc: PersistenceModelType<E>) => PersistenceModelType<E>
9
+ }
10
+
11
+ const splitEtag = <E extends FieldValues>(doc: PersistenceModelType<E>) => {
12
+ const { _etag, ...rest } = doc
13
+ return { rest: rest as E, _etag }
14
+ }
15
+
16
+ const joinEtag = <E extends FieldValues>(
17
+ rest: E,
18
+ _etag: string | undefined
19
+ ): PersistenceModelType<E> => (_etag === undefined ? rest : { ...rest, _etag })
20
+
21
+ /**
22
+ * Encoded document ↔ JSON document. Prefer `Schema.toCodecJson(toEncoded(schema))`
23
+ * when the store has a schema; otherwise lower Date/Map/Set structurally.
24
+ */
25
+ export const makeJsonDocumentCodec = <E extends FieldValues>(schema?: S.Top): JsonDocumentCodec<E> => {
26
+ if (schema) {
27
+ const codec = S.toCodecJson(S.toEncoded(schema)) as S.Codec<E, S.Json>
28
+ return {
29
+ encode: (doc) => {
30
+ const { rest, _etag } = splitEtag(doc)
31
+ return joinEtag(S.encodeSync(codec)(rest) as E, _etag)
32
+ },
33
+ decode: (doc) => {
34
+ const { rest, _etag } = splitEtag(doc)
35
+ return joinEtag(S.decodeSync(codec)(rest as S.Json), _etag)
36
+ }
37
+ }
38
+ }
39
+ return {
40
+ encode: (doc) => toJsonQueryValue(doc) as PersistenceModelType<E>,
41
+ decode: (doc) => doc
42
+ }
43
+ }
@@ -1,9 +1,279 @@
1
1
  import crypto from "crypto"
2
2
  import * as Effect from "effect-app/Effect"
3
+ import type { FilterResult, Ops } from "effect-app/Model/filter/filterApi"
3
4
  import * as Option from "effect-app/Option"
5
+ import * as S from "effect-app/Schema"
6
+ import * as SchemaAST from "effect-app/SchemaAST"
4
7
  import type { PersistenceModelType, SupportedValues2 } from "effect-app/Store"
5
8
  import { OptimisticConcurrencyException } from "../errors.ts"
6
9
 
10
+ const dateJson = S.toCodecJson(S.Date)
11
+
12
+ const isPlainObject = (value: unknown): value is Record<string, unknown> => {
13
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false
14
+ if (value instanceof Date || value instanceof Map || value instanceof Set) return false
15
+ const proto = Object.getPrototypeOf(value)
16
+ return proto === Object.prototype || proto === null
17
+ }
18
+
19
+ const unwrapAst = (ast: SchemaAST.AST): SchemaAST.AST => SchemaAST.isSuspend(ast) ? unwrapAst(ast.thunk()) : ast
20
+
21
+ const unionAst = (hits: readonly SchemaAST.AST[]): SchemaAST.AST | undefined => {
22
+ if (hits.length === 0) return undefined
23
+ if (hits.length === 1) return hits[0]
24
+ return S.Union(hits.map((hit) => S.make(hit)) as [S.Top, S.Top, ...Array<S.Top>]).ast
25
+ }
26
+
27
+ const encodedObjects = (ast: SchemaAST.AST): SchemaAST.Objects | undefined => {
28
+ const current = unwrapAst(ast)
29
+ if (SchemaAST.isObjects(current)) return current
30
+ if (SchemaAST.isDeclaration(current)) {
31
+ const encoded = unwrapAst(SchemaAST.toEncoded(current))
32
+ if (SchemaAST.isObjects(encoded)) return encoded
33
+ }
34
+ return undefined
35
+ }
36
+
37
+ const astAtPath = (ast: SchemaAST.AST | undefined, path: readonly string[]): SchemaAST.AST | undefined => {
38
+ if (ast === undefined) return undefined
39
+ if (path.length === 0) return unwrapAst(ast)
40
+ const current = unwrapAst(ast)
41
+ const [head, ...tail] = path
42
+ if (head === undefined) return current
43
+ if (SchemaAST.isUnion(current)) {
44
+ return unionAst(
45
+ current.types.flatMap((member) => {
46
+ const hit = astAtPath(member, path)
47
+ return hit === undefined ? [] : [hit]
48
+ })
49
+ )
50
+ }
51
+ if (head === "-1" || /^\d+$/.test(head)) {
52
+ if (SchemaAST.isArrays(current)) {
53
+ const element = current.rest[0] ?? current.elements[Number(head)] ?? current.elements[0]
54
+ return astAtPath(element, tail)
55
+ }
56
+ if (SchemaAST.isDeclaration(current) && current.typeParameters.length === 1) {
57
+ return astAtPath(current.typeParameters[0], tail)
58
+ }
59
+ return undefined
60
+ }
61
+ const objects = encodedObjects(current)
62
+ if (objects !== undefined) {
63
+ const property = objects.propertySignatures.find((p) => p.name === head)
64
+ return property === undefined ? undefined : astAtPath(property.type, tail)
65
+ }
66
+ return undefined
67
+ }
68
+
69
+ const elementAst = (ast: SchemaAST.AST | undefined): SchemaAST.AST | undefined => {
70
+ if (ast === undefined) return undefined
71
+ const current = unwrapAst(ast)
72
+ if (SchemaAST.isUnion(current)) {
73
+ return unionAst(
74
+ current.types.flatMap((member) => {
75
+ const hit = elementAst(member)
76
+ return hit === undefined ? [] : [hit]
77
+ })
78
+ )
79
+ }
80
+ if (SchemaAST.isArrays(current)) return unwrapAst(current.rest[0] ?? current.elements[0] ?? current)
81
+ if (SchemaAST.isDeclaration(current) && current.typeParameters.length === 1) {
82
+ return unwrapAst(current.typeParameters[0]!)
83
+ }
84
+ return current
85
+ }
86
+
87
+ const mapKeyAst = (ast: SchemaAST.AST | undefined): SchemaAST.AST | undefined => {
88
+ if (ast === undefined) return undefined
89
+ const current = unwrapAst(ast)
90
+ if (SchemaAST.isUnion(current)) {
91
+ return unionAst(
92
+ current.types.flatMap((member) => {
93
+ const hit = mapKeyAst(member)
94
+ return hit === undefined ? [] : [hit]
95
+ })
96
+ )
97
+ }
98
+ if (SchemaAST.isDeclaration(current) && current.typeParameters.length >= 2) {
99
+ return unwrapAst(current.typeParameters[0]!)
100
+ }
101
+ return current
102
+ }
103
+
104
+ const mapValueAst = (ast: SchemaAST.AST | undefined): SchemaAST.AST | undefined => {
105
+ if (ast === undefined) return undefined
106
+ const current = unwrapAst(ast)
107
+ if (SchemaAST.isUnion(current)) {
108
+ return unionAst(
109
+ current.types.flatMap((member) => {
110
+ const hit = mapValueAst(member)
111
+ return hit === undefined ? [] : [hit]
112
+ })
113
+ )
114
+ }
115
+ if (SchemaAST.isDeclaration(current) && current.typeParameters.length >= 2) {
116
+ return unwrapAst(current.typeParameters[1]!)
117
+ }
118
+ return current
119
+ }
120
+
121
+ const asArray = (value: unknown): readonly unknown[] =>
122
+ Array.isArray(value) ? value : value instanceof Set ? [...value] : [value]
123
+
124
+ const encodeJson = (ast: SchemaAST.AST | undefined, value: unknown): unknown => {
125
+ if (ast === undefined) return toJsonQueryValue(value)
126
+ const current = unwrapAst(ast)
127
+ if (isPlainObject(value)) {
128
+ const out: Record<string, unknown> = {}
129
+ for (const [key, child] of Object.entries(value)) {
130
+ out[key] = encodeJson(astAtPath(current, [key]), child)
131
+ }
132
+ return out
133
+ }
134
+ if (Array.isArray(value)) {
135
+ const element = SchemaAST.isArrays(current) || SchemaAST.isUnion(current)
136
+ ? elementAst(current)
137
+ : current
138
+ return value.map((item) => encodeJson(element, item))
139
+ }
140
+ if (value instanceof Set) {
141
+ return [...value].map((item) => encodeJson(elementAst(current), item))
142
+ }
143
+ if (value instanceof Map) {
144
+ return [...value.entries()].map(([k, v]) => [
145
+ encodeJson(mapKeyAst(current), k),
146
+ encodeJson(mapValueAst(current), v)
147
+ ])
148
+ }
149
+ return Effect.runSync(
150
+ S.encodeUnknownEffect(S.toCodecJson(S.make(current)))(value) as Effect.Effect<S.Json>
151
+ )
152
+ }
153
+
154
+ const encodeFilterValue = (fieldAst: SchemaAST.AST | undefined, op: Ops, value: unknown): unknown => {
155
+ if (fieldAst === undefined) return toJsonQueryValue(value)
156
+ if (op === "in" || op === "notIn") {
157
+ return asArray(value).map((item) => encodeJson(fieldAst, item))
158
+ }
159
+ if (
160
+ op === "includes"
161
+ || op === "notIncludes"
162
+ || op === "includes-any"
163
+ || op === "notIncludes-any"
164
+ || op === "includes-all"
165
+ || op === "notIncludes-all"
166
+ ) {
167
+ const element = elementAst(fieldAst)
168
+ return op === "includes" || op === "notIncludes"
169
+ ? encodeJson(element, value)
170
+ : asArray(value).map((item) => encodeJson(element, item))
171
+ }
172
+ if (
173
+ op === "hasKeyValue"
174
+ || op === "notHasKeyValue"
175
+ || op === "hasKeyValue-any"
176
+ || op === "notHasKeyValue-any"
177
+ || op === "hasKeyValue-all"
178
+ || op === "notHasKeyValue-all"
179
+ ) {
180
+ const keyAst = mapKeyAst(fieldAst)
181
+ const valueAst = mapValueAst(fieldAst)
182
+ const pair = (item: unknown) =>
183
+ Array.isArray(item) && item.length >= 2
184
+ ? [encodeJson(keyAst, item[0]), encodeJson(valueAst, item[1])]
185
+ : toJsonQueryValue(item)
186
+ return op === "hasKeyValue" || op === "notHasKeyValue" ? pair(value) : asArray(value).map(pair)
187
+ }
188
+ if (
189
+ op === "hasKey"
190
+ || op === "notHasKey"
191
+ || op === "hasKey-any"
192
+ || op === "notHasKey-any"
193
+ || op === "hasKey-all"
194
+ || op === "notHasKey-all"
195
+ ) {
196
+ const keyAst = mapKeyAst(fieldAst)
197
+ return op === "hasKey" || op === "notHasKey"
198
+ ? encodeJson(keyAst, value)
199
+ : asArray(value).map((item) => encodeJson(keyAst, item))
200
+ }
201
+ if (
202
+ op === "hasValue"
203
+ || op === "notHasValue"
204
+ || op === "hasValue-any"
205
+ || op === "notHasValue-any"
206
+ || op === "hasValue-all"
207
+ || op === "notHasValue-all"
208
+ ) {
209
+ const valueAst = mapValueAst(fieldAst)
210
+ return op === "hasValue" || op === "notHasValue"
211
+ ? encodeJson(valueAst, value)
212
+ : asArray(value).map((item) => encodeJson(valueAst, item))
213
+ }
214
+ return encodeJson(fieldAst, value)
215
+ }
216
+
217
+ /**
218
+ * Lower Date / Map / Set to JSON when no field schema is available.
219
+ * Prefer {@link encodeWithSchema} / {@link jsonifyFilter} with the store schema.
220
+ */
221
+ export function toJsonQueryValue(value: unknown): unknown {
222
+ if (value instanceof Date) {
223
+ return S.encodeSync(dateJson)(value)
224
+ }
225
+ if (value instanceof Map) {
226
+ return [...value.entries()].map(([k, v]) => [toJsonQueryValue(k), toJsonQueryValue(v)])
227
+ }
228
+ if (value instanceof Set) {
229
+ return [...value].map((v) => toJsonQueryValue(v))
230
+ }
231
+ if (Array.isArray(value)) {
232
+ return value.map((v) => toJsonQueryValue(v))
233
+ }
234
+ if (isPlainObject(value)) {
235
+ const out: Record<string, unknown> = {}
236
+ for (const [k, v] of Object.entries(value)) {
237
+ out[k] = toJsonQueryValue(v)
238
+ }
239
+ return out
240
+ }
241
+ if (value !== null && typeof value === "object") {
242
+ const toJSON = (value as { toJSON?: () => unknown }).toJSON
243
+ if (typeof toJSON === "function") {
244
+ return toJsonQueryValue(toJSON.call(value))
245
+ }
246
+ }
247
+ return value
248
+ }
249
+
250
+ export function encodeWithSchema(schema: S.Top | undefined, value: unknown): unknown {
251
+ if (schema === undefined) return toJsonQueryValue(value)
252
+ return encodeJson(SchemaAST.toEncoded(schema.ast), value)
253
+ }
254
+
255
+ export function jsonifyFilter(
256
+ filter: readonly FilterResult[],
257
+ schema?: S.Top
258
+ ): FilterResult[] {
259
+ const ast = schema === undefined ? undefined : SchemaAST.toEncoded(schema.ast)
260
+ return filter.map((r) =>
261
+ r.t === "and-scope" || r.t === "or-scope" || r.t === "where-scope"
262
+ ? { ...r, result: jsonifyFilter(r.result, schema) }
263
+ : { ...r, value: encodeFilterValue(astAtPath(ast, r.path.split(".")), r.op, r.value) }
264
+ )
265
+ }
266
+
267
+ export type JsonLower = {
268
+ readonly toJson: (value: unknown) => unknown
269
+ readonly jsonifyFilter: (filter: readonly FilterResult[]) => FilterResult[]
270
+ }
271
+
272
+ export const makeJsonLower = (config?: { readonly schema?: S.Top }): JsonLower => ({
273
+ toJson: (value) => encodeWithSchema(config?.schema, value),
274
+ jsonifyFilter: (filter) => jsonifyFilter(filter, config?.schema)
275
+ })
276
+
7
277
  /** Traverse an object by a dot-separated path string, e.g. `"a.b.c"`. */
8
278
  export function get(obj: any, path: string): any {
9
279
  return path.split(".").reduce((res: any, key: string) => (res != null ? res[key] : res), obj)
@@ -55,21 +325,21 @@ export function lowercaseIfString<T>(val: T) {
55
325
  }
56
326
 
57
327
  export function compare(valA: unknown, valB: unknown) {
58
- return valA === valB
328
+ return toJsonQueryValue(valA) === toJsonQueryValue(valB)
59
329
  }
60
330
 
61
331
  export function lowerThan(valA: SupportedValues2, valB: SupportedValues2) {
62
- return valA < valB
332
+ return (toJsonQueryValue(valA) as SupportedValues2) < (toJsonQueryValue(valB) as SupportedValues2)
63
333
  }
64
334
 
65
335
  export function lowerThanExclusive(valA: SupportedValues2, valB: SupportedValues2) {
66
- return valA <= valB
336
+ return (toJsonQueryValue(valA) as SupportedValues2) <= (toJsonQueryValue(valB) as SupportedValues2)
67
337
  }
68
338
 
69
339
  export function greaterThan(valA: SupportedValues2, valB: SupportedValues2) {
70
- return valA > valB
340
+ return (toJsonQueryValue(valA) as SupportedValues2) > (toJsonQueryValue(valB) as SupportedValues2)
71
341
  }
72
342
 
73
343
  export function greaterThanExclusive(valA: SupportedValues2, valB: SupportedValues2) {
74
- return valA >= valB
344
+ return (toJsonQueryValue(valA) as SupportedValues2) >= (toJsonQueryValue(valB) as SupportedValues2)
75
345
  }