@effect-app/infra 4.0.0-beta.316 → 4.0.0-beta.318
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 +41 -0
- package/dist/ClusterCosmos.d.ts +5 -1
- package/dist/ClusterCosmos.d.ts.map +1 -1
- package/dist/ClusterCosmos.js +42 -13
- package/dist/ClusterServiceBus.d.ts.map +1 -1
- package/dist/ClusterServiceBus.js +3 -1
- 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 +1 -1
- package/examples/query.ts +4 -4
- package/package.json +6 -6
- package/src/ClusterCosmos.ts +56 -20
- package/src/ClusterServiceBus.ts +2 -0
- 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/Cosmos.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { DatabaseError, OptimisticConcurrencyException } from "../errors.ts"
|
|
|
19
19
|
import { InfraLogger } from "../logger.ts"
|
|
20
20
|
import { annotateCosmosResponse, annotateDb } from "../otel.ts"
|
|
21
21
|
import { buildWhereCosmosQuery3, logQuery } from "./Cosmos/query.ts"
|
|
22
|
+
import { makeJsonDocumentCodec } from "./jsonDocument.ts"
|
|
23
|
+
import { makeJsonLower } from "./utils.ts"
|
|
22
24
|
|
|
23
25
|
const makeMapId =
|
|
24
26
|
<IdKey extends keyof Encoded, Encoded extends FieldValues>(idKey: IdKey) => ({ [idKey]: id, ...e }: Encoded) => ({
|
|
@@ -96,6 +98,10 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
96
98
|
) {
|
|
97
99
|
const mapId = makeMapId<IdKey, Encoded>(idKey)
|
|
98
100
|
const mapReverseId = makeReverseMapId<IdKey, Encoded>(idKey)
|
|
101
|
+
const codec = makeJsonDocumentCodec<Encoded>(config?.schema)
|
|
102
|
+
const json = makeJsonLower(config)
|
|
103
|
+
const defaultValues = json.toJson(config?.defaultValues ?? {}) as Partial<Encoded>
|
|
104
|
+
const fromStored = (raw: Encoded) => codec.decode({ ...defaultValues, ...mapReverseId(raw as any) })
|
|
99
105
|
type PM = PersistenceModelType<Encoded>
|
|
100
106
|
type PMCosmos = PersistenceModelType<Omit<Encoded, IdKey> & { id: string }>
|
|
101
107
|
const containerId = `${prefix}${name}`
|
|
@@ -128,7 +134,6 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
128
134
|
return namespace
|
|
129
135
|
}))
|
|
130
136
|
|
|
131
|
-
const defaultValues = config?.defaultValues ?? {}
|
|
132
137
|
const container = db.container(containerId)
|
|
133
138
|
const bulk = container.items.bulk.bind(container.items)
|
|
134
139
|
const execBatch = container.items.batch.bind(container.items)
|
|
@@ -205,7 +210,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
205
210
|
dropUndefinedT({
|
|
206
211
|
operationType: "Create" as const,
|
|
207
212
|
resourceBody: {
|
|
208
|
-
...Struct.omit(x, ["_etag", idKey]),
|
|
213
|
+
...Struct.omit(codec.encode(x), ["_etag", idKey]),
|
|
209
214
|
id: x[idKey],
|
|
210
215
|
_partitionKey: nsPartitionValue(ns, x)
|
|
211
216
|
}
|
|
@@ -217,7 +222,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
217
222
|
operationType: "Replace" as const,
|
|
218
223
|
id: x[idKey],
|
|
219
224
|
resourceBody: {
|
|
220
|
-
...Struct.omit(x, ["_etag", idKey]),
|
|
225
|
+
...Struct.omit(codec.encode(x), ["_etag", idKey]),
|
|
221
226
|
id: x[idKey],
|
|
222
227
|
_partitionKey: nsPartitionValue(ns, x)
|
|
223
228
|
},
|
|
@@ -314,7 +319,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
314
319
|
onNone: () => ({
|
|
315
320
|
operationType: "Create" as const,
|
|
316
321
|
resourceBody: {
|
|
317
|
-
...Struct.omit(x, ["_etag", idKey]),
|
|
322
|
+
...Struct.omit(codec.encode(x), ["_etag", idKey]),
|
|
318
323
|
id: x[idKey],
|
|
319
324
|
_partitionKey: nsPartitionValue(ns, x)
|
|
320
325
|
}
|
|
@@ -325,7 +330,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
325
330
|
operationType: "Replace" as const,
|
|
326
331
|
id: x[idKey],
|
|
327
332
|
resourceBody: {
|
|
328
|
-
...Struct.omit(x, ["_etag", idKey]),
|
|
333
|
+
...Struct.omit(codec.encode(x), ["_etag", idKey]),
|
|
329
334
|
id: x[idKey],
|
|
330
335
|
_partitionKey: nsPartitionValue(ns, x)
|
|
331
336
|
},
|
|
@@ -447,7 +452,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
447
452
|
container.items.query<PMCosmos>(q, { partitionKey: nsBasePartitionKey(ns) }).fetchAll()
|
|
448
453
|
)
|
|
449
454
|
yield* annotateFeed(response)
|
|
450
|
-
return response.resources.map((_) => (
|
|
455
|
+
return response.resources.map((_) => fromStored(_ as unknown as Encoded))
|
|
451
456
|
})
|
|
452
457
|
.pipe(
|
|
453
458
|
annotateDb({
|
|
@@ -492,7 +497,8 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
492
497
|
| undefined,
|
|
493
498
|
f.order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
|
|
494
499
|
skip,
|
|
495
|
-
limit
|
|
500
|
+
limit,
|
|
501
|
+
json
|
|
496
502
|
)
|
|
497
503
|
),
|
|
498
504
|
ns: resolveNamespace
|
|
@@ -520,7 +526,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
520
526
|
container.items.query<{ f: M }>(q, { partitionKey: nsBasePartitionKey(ns) }).fetchAll()
|
|
521
527
|
)
|
|
522
528
|
yield* annotateFeed(response)
|
|
523
|
-
return response.resources.map(({ f }) => (
|
|
529
|
+
return response.resources.map(({ f }) => fromStored(f as Encoded) as any)
|
|
524
530
|
})
|
|
525
531
|
.pipe(
|
|
526
532
|
annotateDb({
|
|
@@ -546,7 +552,7 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
546
552
|
)
|
|
547
553
|
yield* annotateItem(response)
|
|
548
554
|
return Option.fromNullishOr(response.resource).pipe(
|
|
549
|
-
Option.map((_) => (
|
|
555
|
+
Option.map((_) => fromStored(_))
|
|
550
556
|
)
|
|
551
557
|
})
|
|
552
558
|
.pipe(annotateDb({
|
|
@@ -573,12 +579,12 @@ const makeCosmosStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
573
579
|
{
|
|
574
580
|
onNone: () =>
|
|
575
581
|
container.items.create({
|
|
576
|
-
...mapId(e),
|
|
582
|
+
...mapId(codec.encode(e)),
|
|
577
583
|
_partitionKey: nsPartitionValue(ns, e)
|
|
578
584
|
}),
|
|
579
585
|
onSome: (eTag) =>
|
|
580
586
|
container.item(e[idKey], nsPartitionValue(ns, e)).replace(
|
|
581
|
-
{ ...mapId(e), _partitionKey: nsPartitionValue(ns, e) },
|
|
587
|
+
{ ...mapId(codec.encode(e)), _partitionKey: nsPartitionValue(ns, e) },
|
|
582
588
|
{
|
|
583
589
|
accessCondition: {
|
|
584
590
|
type: "IfMatch",
|
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)
|