@effect-app/infra 4.0.0-beta.316 → 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.
- package/CHANGELOG.md +24 -0
- package/dist/Store/Cosmos/query.d.ts +2 -1
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +60 -2
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +17 -12
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +12 -7
- package/dist/Store/Memory.d.ts +2 -1
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +28 -15
- package/dist/Store/SQL/Pg.d.ts.map +1 -1
- package/dist/Store/SQL/Pg.js +15 -11
- package/dist/Store/SQL/query.d.ts +5 -1
- package/dist/Store/SQL/query.d.ts.map +1 -1
- package/dist/Store/SQL/query.js +131 -2
- package/dist/Store/SQL.d.ts +1 -1
- package/dist/Store/SQL.d.ts.map +1 -1
- package/dist/Store/SQL.js +24 -18
- package/dist/Store/codeFilter.d.ts.map +1 -1
- package/dist/Store/codeFilter.js +67 -20
- package/dist/Store/jsonDocument.d.ts +13 -0
- package/dist/Store/jsonDocument.d.ts.map +1 -0
- package/dist/Store/jsonDocument.js +31 -0
- package/dist/Store/utils.d.ts +16 -0
- package/dist/Store/utils.d.ts.map +1 -1
- package/dist/Store/utils.js +245 -6
- package/dist/logger.d.ts +5 -5
- package/examples/query.ts +4 -4
- package/package.json +3 -3
- package/src/Store/Cosmos/query.ts +65 -9
- package/src/Store/Cosmos.ts +17 -11
- package/src/Store/Disk.ts +28 -7
- package/src/Store/Memory.ts +40 -17
- package/src/Store/SQL/Pg.ts +23 -10
- package/src/Store/SQL/query.ts +152 -7
- package/src/Store/SQL.ts +37 -17
- package/src/Store/codeFilter.ts +71 -23
- package/src/Store/jsonDocument.ts +43 -0
- package/src/Store/utils.ts +275 -5
- package/test/cosmos-query.test.ts +100 -5
- package/test/dist/json-lower.test.d.ts.map +1 -0
- package/test/dist/query.test.d.ts.map +1 -1
- package/test/json-lower.test.ts +115 -0
- package/test/query.test.ts +261 -12
- 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
|
-
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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
|
),
|
package/src/Store/codeFilter.ts
CHANGED
|
@@ -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:
|
|
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
|
|
29
|
+
return (v as unknown[]).includes(k)
|
|
18
30
|
case "notIn":
|
|
19
|
-
return !
|
|
31
|
+
return !(v as unknown[]).includes(k)
|
|
20
32
|
case "lt":
|
|
21
|
-
return lowerThan(k,
|
|
33
|
+
return lowerThan(k as any, v as any)
|
|
22
34
|
case "lte":
|
|
23
|
-
return lowerThanExclusive(k,
|
|
35
|
+
return lowerThanExclusive(k as any, v as any)
|
|
24
36
|
case "gt":
|
|
25
|
-
return greaterThan(k,
|
|
37
|
+
return greaterThan(k as any, v as any)
|
|
26
38
|
case "gte":
|
|
27
|
-
return greaterThanExclusive(k,
|
|
39
|
+
return greaterThanExclusive(k as any, v as any)
|
|
28
40
|
case "includes":
|
|
29
|
-
return (k as Array<
|
|
41
|
+
return (k as Array<unknown>).includes(v)
|
|
30
42
|
case "notIncludes":
|
|
31
|
-
return !(k as Array<
|
|
43
|
+
return !(k as Array<unknown>).includes(v)
|
|
32
44
|
case "includes-any":
|
|
33
|
-
return (vAsArr(p.value)).some((_) => (k as Array<
|
|
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<
|
|
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<
|
|
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<
|
|
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(
|
|
89
|
+
return (k as string).toLowerCase().includes((v as string).toLowerCase())
|
|
42
90
|
case "endsWith":
|
|
43
|
-
return (k as string).toLowerCase().endsWith(
|
|
91
|
+
return (k as string).toLowerCase().endsWith((v as string).toLowerCase())
|
|
44
92
|
case "startsWith":
|
|
45
|
-
return (k as string).toLowerCase().startsWith(
|
|
93
|
+
return (k as string).toLowerCase().startsWith((v as string).toLowerCase())
|
|
46
94
|
case "notContains":
|
|
47
|
-
return !(k as string).toLowerCase().includes(
|
|
95
|
+
return !(k as string).toLowerCase().includes((v as string).toLowerCase())
|
|
48
96
|
case "notEndsWith":
|
|
49
|
-
return !(k as string).toLowerCase().endsWith(
|
|
97
|
+
return !(k as string).toLowerCase().endsWith((v as string).toLowerCase())
|
|
50
98
|
case "notStartsWith":
|
|
51
|
-
return !(k as string).toLowerCase().startsWith(
|
|
99
|
+
return !(k as string).toLowerCase().startsWith((v as string).toLowerCase())
|
|
52
100
|
case "neq":
|
|
53
|
-
return !compare(k,
|
|
101
|
+
return !compare(k, v)
|
|
54
102
|
case "eq":
|
|
55
103
|
case undefined:
|
|
56
|
-
return compare(k,
|
|
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
|
+
}
|
package/src/Store/utils.ts
CHANGED
|
@@ -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
|
}
|