@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/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
|
|
179
|
-
|
|
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,
|
package/src/Store/Memory.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
441
|
+
filter: <U extends keyof Encoded = never>(f: FilterArgs<Encoded, U>) =>
|
|
442
|
+
allStored
|
|
429
443
|
.pipe(
|
|
430
|
-
Effect.tap(() => logQuery(f,
|
|
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((
|
|
465
|
+
.tap((stored) =>
|
|
447
466
|
Ref.get(store).pipe(
|
|
448
|
-
Effect.map((_) => new Map([..._, [
|
|
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),
|
package/src/Store/SQL/Pg.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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
|
),
|
package/src/Store/SQL/query.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|