@effect-app/infra 0.118.0 → 0.119.0
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 +6 -0
- package/_cjs/services/Repository.cjs +4 -38
- package/_cjs/services/Repository.cjs.map +1 -1
- package/_cjs/services/RepositoryBase.cjs +39 -36
- package/_cjs/services/RepositoryBase.cjs.map +1 -1
- package/_cjs/services/Store/Cosmos/query.cjs +291 -0
- package/_cjs/services/Store/Cosmos/query.cjs.map +1 -0
- package/_cjs/services/Store/Cosmos.cjs +38 -183
- package/_cjs/services/Store/Cosmos.cjs.map +1 -1
- package/_cjs/services/Store/Cosmos.test.cjs +7 -7
- package/_cjs/services/Store/Cosmos.test.cjs.map +1 -1
- package/_cjs/services/Store/Memory.cjs +20 -17
- package/_cjs/services/Store/Memory.cjs.map +1 -1
- package/_cjs/services/Store/Redis.cjs +1 -1
- package/_cjs/services/Store/Redis.cjs.map +1 -1
- package/_cjs/services/Store/codeFilter.cjs +151 -0
- package/_cjs/services/Store/codeFilter.cjs.map +1 -0
- package/_cjs/services/Store/filterApi/proxy.cjs +40 -0
- package/_cjs/services/Store/filterApi/proxy.cjs.map +1 -0
- package/_cjs/services/Store/filterApi/query.cjs +173 -0
- package/_cjs/services/Store/filterApi/query.cjs.map +1 -0
- package/_cjs/services/Store/filterApi.test.cjs +164 -0
- package/_cjs/services/Store/filterApi.test.cjs.map +1 -0
- package/_cjs/services/Store/service.cjs.map +1 -1
- package/_cjs/services/Store/utils.cjs +8 -1
- package/_cjs/services/Store/utils.cjs.map +1 -1
- package/_src/services/Repository.ts +124 -70
- package/_src/services/RepositoryBase.ts +26 -12
- package/_src/services/Store/Cosmos/query.ts +390 -0
- package/_src/services/Store/Cosmos.test.ts +5 -5
- package/_src/services/Store/Cosmos.ts +39 -245
- package/_src/services/Store/Memory.ts +21 -18
- package/_src/services/Store/Redis.ts +2 -3
- package/_src/services/Store/codeFilter.ts +150 -0
- package/_src/services/Store/filterApi/proxy.ts +109 -0
- package/_src/services/Store/filterApi/query.ts +256 -0
- package/_src/services/Store/filterApi.test.ts +263 -0
- package/_src/services/Store/service.ts +17 -5
- package/_src/services/Store/utils.ts +10 -7
- package/dist/services/Repository.d.ts +44 -104
- package/dist/services/Repository.d.ts.map +1 -1
- package/dist/services/Repository.js +6 -30
- package/dist/services/RepositoryBase.d.ts +11 -21
- package/dist/services/RepositoryBase.d.ts.map +1 -1
- package/dist/services/RepositoryBase.js +41 -37
- package/dist/services/Store/Cosmos/query.d.ts +61 -0
- package/dist/services/Store/Cosmos/query.d.ts.map +1 -0
- package/dist/services/Store/Cosmos/query.js +302 -0
- package/dist/services/Store/Cosmos.d.ts +3 -47
- package/dist/services/Store/Cosmos.d.ts.map +1 -1
- package/dist/services/Store/Cosmos.js +39 -193
- package/dist/services/Store/Memory.d.ts +2 -7
- package/dist/services/Store/Memory.d.ts.map +1 -1
- package/dist/services/Store/Memory.js +17 -15
- package/dist/services/Store/Redis.d.ts.map +1 -1
- package/dist/services/Store/Redis.js +2 -2
- package/dist/services/Store/codeFilter.d.ts +5 -0
- package/dist/services/Store/codeFilter.d.ts.map +1 -0
- package/dist/services/Store/codeFilter.js +140 -0
- package/dist/services/Store/filterApi/proxy.d.ts +41 -0
- package/dist/services/Store/filterApi/proxy.d.ts.map +1 -0
- package/dist/services/Store/filterApi/proxy.js +41 -0
- package/dist/services/Store/filterApi/query.d.ts +64 -0
- package/dist/services/Store/filterApi/query.d.ts.map +1 -0
- package/dist/services/Store/filterApi/query.js +134 -0
- package/dist/services/Store/filterApi.test.d.ts.map +1 -0
- package/dist/services/Store/service.d.ts +12 -5
- package/dist/services/Store/service.d.ts.map +1 -1
- package/dist/services/Store/service.js +1 -1
- package/dist/services/Store/utils.d.ts +7 -1
- package/dist/services/Store/utils.d.ts.map +1 -1
- package/dist/services/Store/utils.js +118 -115
- package/package.json +44 -4
|
@@ -3,21 +3,30 @@
|
|
|
3
3
|
import { CosmosClient } from "@effect-app/infra-adapters/cosmos-client"
|
|
4
4
|
import { omit } from "@effect-app/prelude/utils"
|
|
5
5
|
import { OptimisticConcurrencyException } from "../../errors.js"
|
|
6
|
+
import {
|
|
7
|
+
buildCosmosQuery,
|
|
8
|
+
buildFilterJoinSelectCosmosQuery,
|
|
9
|
+
buildFindJoinCosmosQuery,
|
|
10
|
+
buildWhereCosmosQuery3,
|
|
11
|
+
logQuery
|
|
12
|
+
} from "./Cosmos/query.js"
|
|
13
|
+
import { StoreMaker } from "./service.js"
|
|
6
14
|
import type {
|
|
7
|
-
|
|
15
|
+
FilterArgs,
|
|
8
16
|
FilterJoinSelect,
|
|
9
|
-
JoinFindFilter,
|
|
10
|
-
LegacyFilter,
|
|
11
17
|
PersistenceModelType,
|
|
12
18
|
StorageConfig,
|
|
13
19
|
Store,
|
|
14
|
-
StoreConfig
|
|
15
|
-
StoreWhereFilter,
|
|
16
|
-
SupportedValues
|
|
20
|
+
StoreConfig
|
|
17
21
|
} from "./service.js"
|
|
18
|
-
import { StoreMaker } from "./service.js"
|
|
19
22
|
|
|
20
|
-
|
|
23
|
+
export function CosmosStoreLive(config: Config<StorageConfig>) {
|
|
24
|
+
return config.config.flatMap(makeCosmosStore).toLayer(StoreMaker)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class CosmosDbOperationError {
|
|
28
|
+
constructor(readonly message: string) {}
|
|
29
|
+
} // TODO: Retry operation when running into RU limit.
|
|
21
30
|
export function makeCosmosStore({ prefix }: StorageConfig) {
|
|
22
31
|
return Effect.gen(function*($) {
|
|
23
32
|
const { db } = yield* $(CosmosClient)
|
|
@@ -243,9 +252,13 @@ export function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
243
252
|
/**
|
|
244
253
|
* May return duplicate results for "join_find", when matching more than once.
|
|
245
254
|
*/
|
|
246
|
-
filter:
|
|
247
|
-
|
|
248
|
-
|
|
255
|
+
filter: <U extends keyof PM = never>(
|
|
256
|
+
f: FilterArgs<PM, U>
|
|
257
|
+
) => {
|
|
258
|
+
const skip = f?.skip
|
|
259
|
+
const limit = f?.limit
|
|
260
|
+
const filter = f.filter ?? { type: "new-kid", build: () => [] }
|
|
261
|
+
type M = U extends undefined ? PM : Pick<PM, U>
|
|
249
262
|
return (filter.type === "join_find"
|
|
250
263
|
// This is a problem if one of the multiple joined arrays can be empty!
|
|
251
264
|
// https://stackoverflow.com/questions/60320780/azure-cosmosdb-sql-join-not-returning-results-when-the-child-contains-empty-arra
|
|
@@ -259,24 +272,33 @@ export function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
259
272
|
Effect.promise(() =>
|
|
260
273
|
container
|
|
261
274
|
.items
|
|
262
|
-
.query<
|
|
275
|
+
.query<M>(q)
|
|
263
276
|
.fetchAll()
|
|
264
277
|
.then(({ resources }) => resources)
|
|
265
278
|
)
|
|
266
279
|
)
|
|
267
280
|
)
|
|
268
281
|
.map((_) => _.flatMap((_) => _))
|
|
269
|
-
: Effect(
|
|
282
|
+
: Effect(
|
|
283
|
+
filter.type === "new-kid"
|
|
284
|
+
? buildWhereCosmosQuery3(
|
|
285
|
+
filter.build(),
|
|
286
|
+
name,
|
|
287
|
+
importedMarkerId,
|
|
288
|
+
f.select as string[] | undefined,
|
|
289
|
+
skip,
|
|
290
|
+
limit
|
|
291
|
+
)
|
|
292
|
+
: buildCosmosQuery(filter, name, importedMarkerId, skip, limit)
|
|
293
|
+
)
|
|
270
294
|
.tap((q) => logQuery(q))
|
|
271
295
|
.flatMap((q) =>
|
|
272
296
|
Effect.promise(() =>
|
|
273
297
|
container
|
|
274
298
|
.items
|
|
275
|
-
.query<
|
|
276
|
-
q
|
|
277
|
-
)
|
|
299
|
+
.query<M>(q)
|
|
278
300
|
.fetchAll()
|
|
279
|
-
.then(({ resources }) => resources
|
|
301
|
+
.then(({ resources }) => resources)
|
|
280
302
|
)
|
|
281
303
|
))
|
|
282
304
|
.withSpan("Cosmos.filter [effect-app/infra/Store]", {
|
|
@@ -389,231 +411,3 @@ export function makeCosmosStore({ prefix }: StorageConfig) {
|
|
|
389
411
|
}
|
|
390
412
|
})
|
|
391
413
|
}
|
|
392
|
-
|
|
393
|
-
function logQuery(q: {
|
|
394
|
-
query: string
|
|
395
|
-
parameters: {
|
|
396
|
-
name: string
|
|
397
|
-
value: SupportedValues | readonly SupportedValues[]
|
|
398
|
-
}[]
|
|
399
|
-
}) {
|
|
400
|
-
return Effect
|
|
401
|
-
.logDebug("cosmos query")
|
|
402
|
-
.pipe(Effect.annotateLogs({
|
|
403
|
-
query: q.query,
|
|
404
|
-
parameters: JSON.stringify(
|
|
405
|
-
q.parameters.reduce((acc, v) => {
|
|
406
|
-
acc[v.name] = v.value
|
|
407
|
-
return acc
|
|
408
|
-
}, {} as Record<string, SupportedValues | readonly SupportedValues[]>),
|
|
409
|
-
undefined,
|
|
410
|
-
2
|
|
411
|
-
)
|
|
412
|
-
}))
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* @deprecated: should build Select into Where query
|
|
417
|
-
*/
|
|
418
|
-
export function buildFilterJoinSelectCosmosQuery(
|
|
419
|
-
filter: FilterJoinSelect,
|
|
420
|
-
k: string,
|
|
421
|
-
name: string,
|
|
422
|
-
skip?: number,
|
|
423
|
-
limit?: number
|
|
424
|
-
) {
|
|
425
|
-
const lm = skip !== undefined || limit !== undefined ? `OFFSET ${skip ?? 0} LIMIT ${limit ?? 999999}` : ""
|
|
426
|
-
return {
|
|
427
|
-
query: `
|
|
428
|
-
SELECT r, f.id as _rootId
|
|
429
|
-
FROM ${name} f
|
|
430
|
-
JOIN r IN f.${k}
|
|
431
|
-
WHERE LOWER(r.${filter.valueKey}) = LOWER(@value)
|
|
432
|
-
${lm}
|
|
433
|
-
`,
|
|
434
|
-
parameters: [{ name: "@value", value: filter.value }]
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* @deprecated: is now part of Where query as k.-1.valueKey
|
|
440
|
-
*/
|
|
441
|
-
export function buildFindJoinCosmosQuery(
|
|
442
|
-
filter: JoinFindFilter,
|
|
443
|
-
k: string,
|
|
444
|
-
name: string,
|
|
445
|
-
skip?: number,
|
|
446
|
-
limit?: number
|
|
447
|
-
) {
|
|
448
|
-
const lm = skip !== undefined || limit !== undefined ? `OFFSET ${skip ?? 0} LIMIT ${limit ?? 999999}` : ""
|
|
449
|
-
return {
|
|
450
|
-
query: `
|
|
451
|
-
SELECT DISTINCT VALUE f
|
|
452
|
-
FROM ${name} f
|
|
453
|
-
JOIN r IN f.${k}
|
|
454
|
-
WHERE LOWER(r.${filter.valueKey}) = LOWER(@value)
|
|
455
|
-
${lm}`,
|
|
456
|
-
parameters: [{ name: "@value", value: filter.value }]
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* @deprecated: should build into Where query
|
|
462
|
-
*/
|
|
463
|
-
export function buildLegacyCosmosQuery<PM>(
|
|
464
|
-
filter: LegacyFilter<PM>,
|
|
465
|
-
name: string,
|
|
466
|
-
importedMarkerId: string,
|
|
467
|
-
skip?: number,
|
|
468
|
-
limit?: number
|
|
469
|
-
) {
|
|
470
|
-
const lm = skip !== undefined || limit !== undefined ? `OFFSET ${skip ?? 0} LIMIT ${limit ?? 999999}` : ""
|
|
471
|
-
return {
|
|
472
|
-
query: `
|
|
473
|
-
SELECT f
|
|
474
|
-
FROM ${name} AS f
|
|
475
|
-
WHERE f.id != @id AND f.${
|
|
476
|
-
String(
|
|
477
|
-
filter.by
|
|
478
|
-
)
|
|
479
|
-
} LIKE @filter
|
|
480
|
-
${lm}`,
|
|
481
|
-
parameters: [
|
|
482
|
-
{ name: "@id", value: importedMarkerId },
|
|
483
|
-
{
|
|
484
|
-
name: "@filter",
|
|
485
|
-
value: filter.type === "endsWith"
|
|
486
|
-
? `%${filter.value}`
|
|
487
|
-
: filter.type === "contains"
|
|
488
|
-
? `%${filter.value}%`
|
|
489
|
-
: `${filter.value}%`
|
|
490
|
-
}
|
|
491
|
-
]
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
export function buildWhereCosmosQuery(
|
|
496
|
-
filter: StoreWhereFilter,
|
|
497
|
-
name: string,
|
|
498
|
-
importedMarkerId: string,
|
|
499
|
-
skip?: number,
|
|
500
|
-
limit?: number
|
|
501
|
-
) {
|
|
502
|
-
const lm = skip !== undefined || limit !== undefined ? `OFFSET ${skip ?? 0} LIMIT ${limit ?? 999999}` : ""
|
|
503
|
-
return {
|
|
504
|
-
query: `
|
|
505
|
-
SELECT f
|
|
506
|
-
FROM ${name} AS f
|
|
507
|
-
${
|
|
508
|
-
filter
|
|
509
|
-
.where
|
|
510
|
-
.filter((_) => _.key.includes(".-1."))
|
|
511
|
-
.map((_) => _.key.split(".-1.")[0])
|
|
512
|
-
.map((_) => `JOIN ${_} IN f.${_}`)
|
|
513
|
-
.uniq(Equivalence.string)
|
|
514
|
-
.join("\n")
|
|
515
|
-
}
|
|
516
|
-
WHERE f.id != @id AND ${
|
|
517
|
-
filter
|
|
518
|
-
.where
|
|
519
|
-
.map((_) =>
|
|
520
|
-
_.key.includes(".-1.")
|
|
521
|
-
? { ..._, f: _.key.split(".-1.")[0], key: _.key.split(".-1.")[1]! }
|
|
522
|
-
: { ..._, f: "f" }
|
|
523
|
-
)
|
|
524
|
-
.map(
|
|
525
|
-
(x, i) => {
|
|
526
|
-
const k = `${x.f}.${x.key}`
|
|
527
|
-
const v = `@v${i}`
|
|
528
|
-
|
|
529
|
-
switch (x.t) {
|
|
530
|
-
case "in":
|
|
531
|
-
return `ARRAY_CONTAINS(${v}, ${k})`
|
|
532
|
-
case "not-in":
|
|
533
|
-
return `(NOT ARRAY_CONTAINS(${v}, ${k}))`
|
|
534
|
-
|
|
535
|
-
case "includes":
|
|
536
|
-
return `ARRAY_CONTAINS(${k}, ${v})`
|
|
537
|
-
case "not-includes":
|
|
538
|
-
return `(NOT ARRAY_CONTAINS(${k}, ${v}))`
|
|
539
|
-
case "contains":
|
|
540
|
-
return `CONTAINS(${k}, ${v}, true)`
|
|
541
|
-
case "starts-with":
|
|
542
|
-
return `STARTSWITH(${k}, ${v}, true)`
|
|
543
|
-
case "ends-with":
|
|
544
|
-
return `ENDSWITH(${k}, ${v}, true)`
|
|
545
|
-
case "not-contains":
|
|
546
|
-
return `NOT(CONTAINS(${k}, ${v}, true))`
|
|
547
|
-
case "not-starts-with":
|
|
548
|
-
return `NOT(STARTSWITH(${k}, ${v}, true))`
|
|
549
|
-
case "not-ends-with":
|
|
550
|
-
return `NOT(ENDSWITH(${k}, ${v}, true))`
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const lk = lowerIfNeeded(k, x.value)
|
|
554
|
-
const lv = lowerIfNeeded(v, x.value)
|
|
555
|
-
|
|
556
|
-
switch (x.t) {
|
|
557
|
-
case "lt":
|
|
558
|
-
return `${lk} < ${lv}`
|
|
559
|
-
case "lte":
|
|
560
|
-
return `${lk} <= ${lv}`
|
|
561
|
-
case "gt":
|
|
562
|
-
return `${lk} > ${lv}`
|
|
563
|
-
case "gte":
|
|
564
|
-
return `${lk} >= ${lv}`
|
|
565
|
-
case "not-eq":
|
|
566
|
-
return x.value === null
|
|
567
|
-
? `IS_NULL(${k}) = false`
|
|
568
|
-
: `${lk} <> ${lv}`
|
|
569
|
-
case undefined:
|
|
570
|
-
case "eq":
|
|
571
|
-
return x.value === null
|
|
572
|
-
? `IS_NULL(${k}) = true`
|
|
573
|
-
: `${lk} = ${lv}`
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
)
|
|
577
|
-
.join(filter.mode === "or" ? " OR " : " AND ")
|
|
578
|
-
}
|
|
579
|
-
${lm}`,
|
|
580
|
-
parameters: [
|
|
581
|
-
{ name: "@id", value: importedMarkerId },
|
|
582
|
-
...filter
|
|
583
|
-
.where
|
|
584
|
-
.map((x, i) => ({
|
|
585
|
-
name: `@v${i}`,
|
|
586
|
-
value: x.value
|
|
587
|
-
}))
|
|
588
|
-
]
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// function isArray(t: SupportedValues | readonly SupportedValues[]): t is readonly SupportedValues[] {
|
|
593
|
-
// return Array.isArray(t)
|
|
594
|
-
// }
|
|
595
|
-
|
|
596
|
-
function lowerIfNeeded(key: unknown, value: unknown) {
|
|
597
|
-
return typeof value === "string" ? `LOWER(${key})` : `${key}`
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
export function buildCosmosQuery<PM>(
|
|
601
|
-
filter: LegacyFilter<PM> | StoreWhereFilter,
|
|
602
|
-
name: string,
|
|
603
|
-
importedMarkerId: string,
|
|
604
|
-
skip?: number,
|
|
605
|
-
limit?: number
|
|
606
|
-
) {
|
|
607
|
-
return filter.type === "startsWith"
|
|
608
|
-
|| filter.type === "endsWith"
|
|
609
|
-
|| filter.type === "contains"
|
|
610
|
-
? buildLegacyCosmosQuery(filter, name, importedMarkerId, skip, limit)
|
|
611
|
-
: buildWhereCosmosQuery(filter, name, importedMarkerId, skip, limit)
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
class CosmosDbOperationError {
|
|
615
|
-
constructor(readonly message: string) {}
|
|
616
|
-
}
|
|
617
|
-
export function CosmosStoreLive(config: Config<StorageConfig>) {
|
|
618
|
-
return config.config.flatMap(makeCosmosStore).toLayer(StoreMaker)
|
|
619
|
-
}
|
|
@@ -1,25 +1,33 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
|
|
3
|
+
import { pick } from "@effect-app/prelude/utils"
|
|
3
4
|
import type { RequestContext } from "../../RequestContext.js"
|
|
4
|
-
import type {
|
|
5
|
+
import type { FilterArgs, FilterJoinSelect, PersistenceModelType, Store, StoreConfig } from "./service.js"
|
|
5
6
|
import { StoreMaker } from "./service.js"
|
|
6
7
|
import { codeFilter, codeFilterJoinSelect, makeUpdateETag } from "./utils.js"
|
|
7
8
|
|
|
8
|
-
export function memFilter<T extends
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
9
|
+
export function memFilter<T extends PersistenceModelType<string>, U extends keyof T = never>(f: FilterArgs<T, U>) {
|
|
10
|
+
type M = U extends undefined ? T : Pick<T, U>
|
|
11
|
+
return ((c: T[]): M[] => {
|
|
12
|
+
const select = (r: T[]): M[] => (f.select ? r.map((_) => pick(_, f.select!)) : r) as any
|
|
13
|
+
const skip = f?.skip
|
|
14
|
+
const limit = f?.limit
|
|
12
15
|
if (!skip && limit === 1) {
|
|
13
|
-
return
|
|
16
|
+
return select(
|
|
17
|
+
c.findFirstMap(f.filter ? codeFilter(f.filter) : (_) => Option.some(_)).map(ReadonlyArray.make).getOrElse(
|
|
18
|
+
() => []
|
|
19
|
+
)
|
|
20
|
+
)
|
|
14
21
|
}
|
|
15
|
-
let r = c.filterMap(codeFilter(filter))
|
|
22
|
+
let r = f.filter ? c.filterMap(codeFilter(f.filter)) : c
|
|
16
23
|
if (skip) {
|
|
17
24
|
r = r.drop(skip)
|
|
18
25
|
}
|
|
19
26
|
if (limit !== undefined) {
|
|
20
27
|
r = r.take(limit)
|
|
21
28
|
}
|
|
22
|
-
|
|
29
|
+
|
|
30
|
+
return select(r)
|
|
23
31
|
})
|
|
24
32
|
}
|
|
25
33
|
|
|
@@ -30,16 +38,11 @@ export const storeId = FiberRef.unsafeMake("primary")
|
|
|
30
38
|
*/
|
|
31
39
|
export const restoreFromRequestContext = (ctx: RequestContext) => storeId.set(ctx.namespace ?? "primary")
|
|
32
40
|
|
|
33
|
-
function logQuery(
|
|
41
|
+
function logQuery(f: FilterArgs<any, any>) {
|
|
34
42
|
return Effect
|
|
35
43
|
.logDebug("mem query")
|
|
36
44
|
.pipe(Effect.annotateLogs({
|
|
37
|
-
query:
|
|
38
|
-
cursor: JSON.stringify(
|
|
39
|
-
cursor,
|
|
40
|
-
undefined,
|
|
41
|
-
2
|
|
42
|
-
)
|
|
45
|
+
query: f
|
|
43
46
|
}))
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -91,10 +94,10 @@ export function makeMemoryStoreInt<Id extends string, PM extends PersistenceMode
|
|
|
91
94
|
namespace
|
|
92
95
|
}
|
|
93
96
|
}),
|
|
94
|
-
filter: (
|
|
97
|
+
filter: (f) =>
|
|
95
98
|
all
|
|
96
|
-
.tap(() => logQuery(
|
|
97
|
-
.map(memFilter(
|
|
99
|
+
.tap(() => logQuery(f))
|
|
100
|
+
.map(memFilter(f))
|
|
98
101
|
.withSpan("Memory.filter [effect-app/infra/Store]", {
|
|
99
102
|
attributes: { "repository.model_name": modelName, "repository.namespace": namespace }
|
|
100
103
|
}),
|
|
@@ -3,7 +3,7 @@ import { RedisClient } from "@effect-app/infra-adapters/redis-client"
|
|
|
3
3
|
import { NotFoundError } from "../../errors.js"
|
|
4
4
|
import { memFilter } from "./Memory.js"
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type { FilterJoinSelect, PersistenceModelType, StorageConfig, Store, StoreConfig } from "./service.js"
|
|
7
7
|
import { StoreMaker } from "./service.js"
|
|
8
8
|
import { codeFilterJoinSelect, makeETag, makeUpdateETag } from "./utils.js"
|
|
9
9
|
|
|
@@ -62,8 +62,7 @@ export function makeRedisStore({ prefix }: StorageConfig) {
|
|
|
62
62
|
.provideService(RedisClient, redis)
|
|
63
63
|
const s: Store<PM, Id> = {
|
|
64
64
|
all,
|
|
65
|
-
filter: (
|
|
66
|
-
all.map(memFilter(filter, cursor)),
|
|
65
|
+
filter: (f) => all.map(memFilter(f)),
|
|
67
66
|
filterJoinSelect: <T extends object>(filter: FilterJoinSelect) =>
|
|
68
67
|
all.map((c) => c.flatMap(codeFilterJoinSelect<PM, T>(filter))),
|
|
69
68
|
find: (id) => asMap.map((_) => Option.fromNullable(_.get(id))),
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { assertUnreachable, get } from "@effect-app/prelude/utils"
|
|
2
|
+
import type { FilterR, FilterResult } from "./filterApi/query.js"
|
|
3
|
+
import {
|
|
4
|
+
compareCaseInsensitive,
|
|
5
|
+
gtCaseInsensitive,
|
|
6
|
+
gteCaseInsensitive,
|
|
7
|
+
ltCaseInsensitive,
|
|
8
|
+
lteCaseInsensitive
|
|
9
|
+
} from "./utils.js"
|
|
10
|
+
|
|
11
|
+
export const codeFilterStatement = <E>(p: FilterR, x: E) => {
|
|
12
|
+
const k = get(x, p.path)
|
|
13
|
+
switch (p.op) {
|
|
14
|
+
case "in":
|
|
15
|
+
return p.value.includes(k)
|
|
16
|
+
case "notIn":
|
|
17
|
+
return !p.value.includes(k)
|
|
18
|
+
case "lt":
|
|
19
|
+
return ltCaseInsensitive(k, p.value)
|
|
20
|
+
case "lte":
|
|
21
|
+
return lteCaseInsensitive(k, p.value)
|
|
22
|
+
case "gt":
|
|
23
|
+
return gtCaseInsensitive(k, p.value)
|
|
24
|
+
case "gte":
|
|
25
|
+
return gteCaseInsensitive(k, p.value)
|
|
26
|
+
case "includes":
|
|
27
|
+
return (k as Array<string>).includes(p.value)
|
|
28
|
+
case "notIncludes":
|
|
29
|
+
return !(k as Array<string>).includes(p.value)
|
|
30
|
+
case "contains":
|
|
31
|
+
return (k as string).toLowerCase().includes(p.value.toLowerCase())
|
|
32
|
+
case "endsWith":
|
|
33
|
+
return (k as string).toLowerCase().endsWith(p.value.toLowerCase())
|
|
34
|
+
case "startsWith":
|
|
35
|
+
return (k as string).toLowerCase().startsWith(p.value.toLowerCase())
|
|
36
|
+
case "notContains":
|
|
37
|
+
return !(k as string).toLowerCase().includes(p.value.toLowerCase())
|
|
38
|
+
case "notEndsWith":
|
|
39
|
+
return !(k as string).toLowerCase().endsWith(p.value.toLowerCase())
|
|
40
|
+
case "notStartsWith":
|
|
41
|
+
return !(k as string).toLowerCase().startsWith(p.value.toLowerCase())
|
|
42
|
+
case "neq":
|
|
43
|
+
return p.path.includes(".-1.")
|
|
44
|
+
? (get(x, p.path.split(".-1.")[0]) as any[])
|
|
45
|
+
// TODO: or vs and
|
|
46
|
+
.every((_) => !compareCaseInsensitive(get(_, p.path.split(".-1.")[1]!), p.value))
|
|
47
|
+
: !compareCaseInsensitive(k, p.value)
|
|
48
|
+
case "eq":
|
|
49
|
+
case undefined:
|
|
50
|
+
return p.path.includes(".-1.")
|
|
51
|
+
? (get(x, p.path.split(".-1.")[0]) as any[])
|
|
52
|
+
// TODO: or vs and
|
|
53
|
+
.some((_) => compareCaseInsensitive(get(_, p.path.split(".-1.")[1]!), p.value))
|
|
54
|
+
: compareCaseInsensitive(k, p.value)
|
|
55
|
+
default: {
|
|
56
|
+
return assertUnreachable(p.op)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// const or = <E extends { id: string }, NE extends E>(filters: readonly FilterResult[], x: E): Option<NE> =>
|
|
62
|
+
// filters
|
|
63
|
+
// .some((p) => {
|
|
64
|
+
// if (p.t === "and-scope") {
|
|
65
|
+
// return and(p.result, x)
|
|
66
|
+
// }
|
|
67
|
+
// if (p.t === "or-scope") {
|
|
68
|
+
// return or(p.result, x)
|
|
69
|
+
// }
|
|
70
|
+
// if (p.t === "where-scope") {
|
|
71
|
+
// return codeFilter2(filters)(x)
|
|
72
|
+
// }
|
|
73
|
+
// return codeFilterStatement(p, x)
|
|
74
|
+
// })
|
|
75
|
+
// ? Option(x as unknown as NE)
|
|
76
|
+
// : Option.none
|
|
77
|
+
|
|
78
|
+
// export const and = <E extends { id: string }, NE extends E>(filters: readonly FilterResult[], x: E): Option<NE> =>
|
|
79
|
+
// filters
|
|
80
|
+
// .every((p) => {
|
|
81
|
+
// if (p.t === "and-scope") {
|
|
82
|
+
// return and(p.result, x)
|
|
83
|
+
// }
|
|
84
|
+
// if (p.t === "or-scope") {
|
|
85
|
+
// return or(p.result, x)
|
|
86
|
+
// }
|
|
87
|
+
// if (p.t === "where-scope") {
|
|
88
|
+
// return codeFilter2(filters)(x)
|
|
89
|
+
// }
|
|
90
|
+
// return codeFilterStatement(p, x)
|
|
91
|
+
// })
|
|
92
|
+
// ? Option(x as unknown as NE)
|
|
93
|
+
// : Option.none
|
|
94
|
+
|
|
95
|
+
// // TODO: how to handle and/or outside scopes.
|
|
96
|
+
// // TODO: the scopes are not about and every, or some.. they're about logical grouping. It's a logical group + and/or etc.
|
|
97
|
+
// // TODO: how to convert this to code? would we compile an actual function body instead?!
|
|
98
|
+
// export function codeFilter2<E extends { id: string }, NE extends E>(filters: readonly FilterResult[]) {
|
|
99
|
+
// // TODO: handle or, and, or-scope, and-scope, where-scope
|
|
100
|
+
// return (x: E) =>
|
|
101
|
+
// // AND
|
|
102
|
+
// and<E, NE>(filters, x)
|
|
103
|
+
// // OR
|
|
104
|
+
// // or<E, NE>(filters, x)
|
|
105
|
+
// }
|
|
106
|
+
|
|
107
|
+
export const codeFilter3 = <E>(state: readonly FilterResult[]) => (sut: E) => codeFilter3_(state, sut)
|
|
108
|
+
export const codeFilter3_ = <E>(state: readonly FilterResult[], sut: E): boolean => {
|
|
109
|
+
let s = ""
|
|
110
|
+
let l = 0
|
|
111
|
+
const printN = (n: number) => {
|
|
112
|
+
return n === 0 ? "" : ReadonlyArray.range(1, n).map(() => " ").join("")
|
|
113
|
+
}
|
|
114
|
+
// TODO: path str updates
|
|
115
|
+
|
|
116
|
+
const process = (e: FilterR) => codeFilterStatement(e, sut)
|
|
117
|
+
for (const e of state) {
|
|
118
|
+
switch (e.t) {
|
|
119
|
+
case "where":
|
|
120
|
+
s += process(e)
|
|
121
|
+
break
|
|
122
|
+
case "or":
|
|
123
|
+
s += " || " + process(e)
|
|
124
|
+
break
|
|
125
|
+
case "and":
|
|
126
|
+
s += " && " + process(e)
|
|
127
|
+
break
|
|
128
|
+
case "or-scope": {
|
|
129
|
+
;++l
|
|
130
|
+
s += ` || (\n${printN(l + 1)}${codeFilter3_(e.result, sut)}\n${printN(l)})`
|
|
131
|
+
;--l
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
case "and-scope": {
|
|
135
|
+
;++l
|
|
136
|
+
s += ` && (\n${printN(l + 1)}${codeFilter3_(e.result, sut)}\n${printN(l)})`
|
|
137
|
+
;--l
|
|
138
|
+
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
case "where-scope": {
|
|
142
|
+
// ;++l
|
|
143
|
+
s += `(\n${printN(l + 1)}${codeFilter3_(e.result, sut)}\n)`
|
|
144
|
+
// ;--l
|
|
145
|
+
break
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return eval(s)
|
|
150
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { FieldValues } from "../../../filter/types.js"
|
|
2
|
+
import type { FieldPath } from "../../../filter/types/path/eager.js"
|
|
3
|
+
|
|
4
|
+
type NullOrUndefined<T, Fallback> = null extends T ? null : undefined extends T ? null : Fallback
|
|
5
|
+
|
|
6
|
+
export type InOps =
|
|
7
|
+
| "in"
|
|
8
|
+
| "notIn"
|
|
9
|
+
|
|
10
|
+
export type OtherOps =
|
|
11
|
+
| "endsWith"
|
|
12
|
+
| "startsWith"
|
|
13
|
+
| "notEndsWith"
|
|
14
|
+
| "notStartsWith"
|
|
15
|
+
| "contains"
|
|
16
|
+
| "notContains"
|
|
17
|
+
| "includes"
|
|
18
|
+
| "notIncludes"
|
|
19
|
+
| "eq"
|
|
20
|
+
| "neq"
|
|
21
|
+
| "gt"
|
|
22
|
+
| "gte"
|
|
23
|
+
| "lt"
|
|
24
|
+
| "lte"
|
|
25
|
+
|
|
26
|
+
// TODO: includes | notIncludes
|
|
27
|
+
export type Ops = OtherOps | InOps
|
|
28
|
+
type F<T extends FieldValues> = {
|
|
29
|
+
path: FieldPath<T>
|
|
30
|
+
op: OtherOps
|
|
31
|
+
value: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type FIn<T extends FieldValues> = {
|
|
35
|
+
op: InOps
|
|
36
|
+
path: FieldPath<T>
|
|
37
|
+
value: readonly string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type G<T extends FieldValues, Val> = {
|
|
41
|
+
(value: Val): F<T>
|
|
42
|
+
startsWith: (value: string) => F<T>
|
|
43
|
+
notStartsWith: (value: string) => F<T>
|
|
44
|
+
endsWith: (value: string) => F<T>
|
|
45
|
+
notEndsWith: (value: string) => F<T>
|
|
46
|
+
contains: (value: string) => F<T>
|
|
47
|
+
notContains: (value: string) => F<T>
|
|
48
|
+
includes: (value: string) => F<T>
|
|
49
|
+
notIncludes: (value: string) => F<T>
|
|
50
|
+
in: (...value: readonly Val[]) => FIn<T>
|
|
51
|
+
notIn: (...value: readonly Val[]) => FIn<T>
|
|
52
|
+
eq: (value: Val) => F<T>
|
|
53
|
+
neq: (value: Val) => F<T>
|
|
54
|
+
gt: (value: Val) => F<T>
|
|
55
|
+
gte: (value: Val) => F<T>
|
|
56
|
+
lt: (value: Val) => F<T>
|
|
57
|
+
lte: (value: Val) => F<T>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type Filter<T extends FieldValues, Ext = never> = {
|
|
61
|
+
[K in keyof T]-?: [T[K]] extends [Record<any, any> | undefined | null]
|
|
62
|
+
? Filter<T[K], NullOrUndefined<T[K], Ext>> & G<T, T[K] | Ext>
|
|
63
|
+
: [T[K]] extends [Record<any, any>] ? Filter<T[K], NullOrUndefined<T[K], Ext>> & G<T, T[K] | Ext>
|
|
64
|
+
: G<T, T[K] | Ext>
|
|
65
|
+
}
|
|
66
|
+
const ops: Ops[] = [
|
|
67
|
+
"contains",
|
|
68
|
+
"startsWith",
|
|
69
|
+
"endsWith",
|
|
70
|
+
"notContains",
|
|
71
|
+
"notStartsWith",
|
|
72
|
+
"notEndsWith",
|
|
73
|
+
"in",
|
|
74
|
+
"notIn",
|
|
75
|
+
"eq",
|
|
76
|
+
"neq",
|
|
77
|
+
"gte",
|
|
78
|
+
"gt",
|
|
79
|
+
"lt",
|
|
80
|
+
"lte"
|
|
81
|
+
]
|
|
82
|
+
export const makeProxy = (parentProp?: string): any =>
|
|
83
|
+
new Proxy(
|
|
84
|
+
Object.assign(() => {}, {
|
|
85
|
+
_proxies: {} as Record<string, any>
|
|
86
|
+
}),
|
|
87
|
+
{
|
|
88
|
+
apply(_target, _thisArg, argArray) {
|
|
89
|
+
return ({ op: "eq", value: argArray[0], path: parentProp })
|
|
90
|
+
},
|
|
91
|
+
get(target, prop) {
|
|
92
|
+
if (typeof prop !== "string") return undefined
|
|
93
|
+
if (target._proxies[prop]) {
|
|
94
|
+
return target._proxies[prop]
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (ops.includes(prop as any)) {
|
|
98
|
+
return (value: any) => ({ op: prop, path: parentProp, value })
|
|
99
|
+
}
|
|
100
|
+
let fullProp = prop
|
|
101
|
+
if (parentProp) {
|
|
102
|
+
fullProp = `${parentProp}.${prop}`
|
|
103
|
+
}
|
|
104
|
+
const p = makeProxy(fullProp)
|
|
105
|
+
target._proxies[prop] = p
|
|
106
|
+
return p
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
)
|