@effect-app/infra 0.117.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.
Files changed (85) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/_cjs/filter.cjs +13 -13
  3. package/_cjs/filter.cjs.map +1 -1
  4. package/_cjs/services/Repository.cjs +4 -38
  5. package/_cjs/services/Repository.cjs.map +1 -1
  6. package/_cjs/services/RepositoryBase.cjs +39 -36
  7. package/_cjs/services/RepositoryBase.cjs.map +1 -1
  8. package/_cjs/services/Store/Cosmos/query.cjs +291 -0
  9. package/_cjs/services/Store/Cosmos/query.cjs.map +1 -0
  10. package/_cjs/services/Store/Cosmos.cjs +38 -183
  11. package/_cjs/services/Store/Cosmos.cjs.map +1 -1
  12. package/_cjs/services/Store/Cosmos.test.cjs +7 -7
  13. package/_cjs/services/Store/Cosmos.test.cjs.map +1 -1
  14. package/_cjs/services/Store/Memory.cjs +20 -17
  15. package/_cjs/services/Store/Memory.cjs.map +1 -1
  16. package/_cjs/services/Store/Redis.cjs +1 -1
  17. package/_cjs/services/Store/Redis.cjs.map +1 -1
  18. package/_cjs/services/Store/codeFilter.cjs +151 -0
  19. package/_cjs/services/Store/codeFilter.cjs.map +1 -0
  20. package/_cjs/services/Store/codeFilter.test.cjs +8 -8
  21. package/_cjs/services/Store/codeFilter.test.cjs.map +1 -1
  22. package/_cjs/services/Store/filterApi/proxy.cjs +40 -0
  23. package/_cjs/services/Store/filterApi/proxy.cjs.map +1 -0
  24. package/_cjs/services/Store/filterApi/query.cjs +173 -0
  25. package/_cjs/services/Store/filterApi/query.cjs.map +1 -0
  26. package/_cjs/services/Store/filterApi.test.cjs +164 -0
  27. package/_cjs/services/Store/filterApi.test.cjs.map +1 -0
  28. package/_cjs/services/Store/service.cjs.map +1 -1
  29. package/_cjs/services/Store/utils.cjs +8 -1
  30. package/_cjs/services/Store/utils.cjs.map +1 -1
  31. package/_src/filter.ts +13 -13
  32. package/_src/services/Repository.ts +124 -70
  33. package/_src/services/RepositoryBase.ts +26 -12
  34. package/_src/services/Store/Cosmos/query.ts +390 -0
  35. package/_src/services/Store/Cosmos.test.ts +6 -6
  36. package/_src/services/Store/Cosmos.ts +39 -245
  37. package/_src/services/Store/Memory.ts +21 -18
  38. package/_src/services/Store/Redis.ts +2 -3
  39. package/_src/services/Store/codeFilter.test.ts +8 -8
  40. package/_src/services/Store/codeFilter.ts +150 -0
  41. package/_src/services/Store/filterApi/proxy.ts +109 -0
  42. package/_src/services/Store/filterApi/query.ts +256 -0
  43. package/_src/services/Store/filterApi.test.ts +263 -0
  44. package/_src/services/Store/service.ts +17 -5
  45. package/_src/services/Store/utils.ts +10 -7
  46. package/dist/filter.d.ts +13 -13
  47. package/dist/filter.js +14 -14
  48. package/dist/services/Repository.d.ts +44 -104
  49. package/dist/services/Repository.d.ts.map +1 -1
  50. package/dist/services/Repository.js +6 -30
  51. package/dist/services/RepositoryBase.d.ts +11 -21
  52. package/dist/services/RepositoryBase.d.ts.map +1 -1
  53. package/dist/services/RepositoryBase.js +41 -37
  54. package/dist/services/Store/Cosmos/query.d.ts +61 -0
  55. package/dist/services/Store/Cosmos/query.d.ts.map +1 -0
  56. package/dist/services/Store/Cosmos/query.js +302 -0
  57. package/dist/services/Store/Cosmos.d.ts +3 -47
  58. package/dist/services/Store/Cosmos.d.ts.map +1 -1
  59. package/dist/services/Store/Cosmos.js +39 -193
  60. package/dist/services/Store/Memory.d.ts +2 -7
  61. package/dist/services/Store/Memory.d.ts.map +1 -1
  62. package/dist/services/Store/Memory.js +17 -15
  63. package/dist/services/Store/Redis.d.ts.map +1 -1
  64. package/dist/services/Store/Redis.js +2 -2
  65. package/dist/services/Store/codeFilter.d.ts +5 -0
  66. package/dist/services/Store/codeFilter.d.ts.map +1 -0
  67. package/dist/services/Store/codeFilter.js +140 -0
  68. package/dist/services/Store/filterApi/proxy.d.ts +41 -0
  69. package/dist/services/Store/filterApi/proxy.d.ts.map +1 -0
  70. package/dist/services/Store/filterApi/proxy.js +41 -0
  71. package/dist/services/Store/filterApi/query.d.ts +64 -0
  72. package/dist/services/Store/filterApi/query.d.ts.map +1 -0
  73. package/dist/services/Store/filterApi/query.js +134 -0
  74. package/dist/services/Store/filterApi.test.d.ts.map +1 -0
  75. package/dist/services/Store/service.d.ts +12 -5
  76. package/dist/services/Store/service.d.ts.map +1 -1
  77. package/dist/services/Store/service.js +1 -1
  78. package/dist/services/Store/utils.d.ts +7 -1
  79. package/dist/services/Store/utils.d.ts.map +1 -1
  80. package/dist/services/Store/utils.js +118 -115
  81. package/dist/services/simpledb/cosmosdb.d.ts +2 -2
  82. package/dist/services/simpledb/diskdb.d.ts +1 -1
  83. package/dist/services/simpledb/redisdb.d.ts +2 -2
  84. package/dist/services/simpledb/simpledb.d.ts +2 -2
  85. 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
- Filter,
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
- // TODO: Retry operation when running into RU limit.
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: (filter: Filter<PM>, cursor?: { skip?: number; limit?: number }) => {
247
- const skip = cursor?.skip
248
- const limit = cursor?.limit
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<PM>(q)
275
+ .query<M>(q)
263
276
  .fetchAll()
264
277
  .then(({ resources }) => resources)
265
278
  )
266
279
  )
267
280
  )
268
281
  .map((_) => _.flatMap((_) => _))
269
- : Effect(buildCosmosQuery(filter, name, importedMarkerId, skip, limit))
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<{ f: PM }>(
276
- q
277
- )
299
+ .query<M>(q)
278
300
  .fetchAll()
279
- .then(({ resources }) => resources.map((_) => _.f))
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 { Filter, FilterJoinSelect, PersistenceModelType, Store, StoreConfig } from "./service.js"
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 { id: string }>(filter: Filter<T>, cursor?: { skip?: number; limit?: number }) {
9
- return ((c: T[]): T[] => {
10
- const skip = cursor?.skip
11
- const limit = cursor?.limit
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 c.findFirstMap(codeFilter(filter)).map(ReadonlyArray.make).getOrElse(() => [])
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
- return r
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(filter: any, cursor: any) {
41
+ function logQuery(f: FilterArgs<any, any>) {
34
42
  return Effect
35
43
  .logDebug("mem query")
36
44
  .pipe(Effect.annotateLogs({
37
- query: JSON.stringify(filter, undefined, 2),
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: (filter: Filter<PM>, cursor?: { skip?: number; limit?: number }) =>
97
+ filter: (f) =>
95
98
  all
96
- .tap(() => logQuery(filter, cursor))
97
- .map(memFilter(filter, cursor))
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 { Filter, FilterJoinSelect, PersistenceModelType, StorageConfig, Store, StoreConfig } from "./service.js"
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: (filter: Filter<PM>, cursor?: { skip?: number; limit?: number }) =>
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))),
@@ -55,7 +55,7 @@ test("works", () => {
55
55
  expect(
56
56
  somethings
57
57
  .filterMap(codeFilter(
58
- somethingsWhere((_) => _("b", Filters.$contains("b")))
58
+ somethingsWhere((_) => _("b", Filters.contains("b")))
59
59
  ))
60
60
  .toArray
61
61
  )
@@ -64,7 +64,7 @@ test("works", () => {
64
64
  expect(
65
65
  somethings
66
66
  .filterMap(codeFilter(
67
- somethingsWhere((_) => _("b", Filters.$contains("2")))
67
+ somethingsWhere((_) => _("b", Filters.contains("2")))
68
68
  ))
69
69
  .toArray
70
70
  )
@@ -73,7 +73,7 @@ test("works", () => {
73
73
  expect(
74
74
  somethings
75
75
  .filterMap(codeFilter(
76
- somethingsWhere((_) => _("b", Filters.$endsWith("b")))
76
+ somethingsWhere((_) => _("b", Filters.endsWith("b")))
77
77
  ))
78
78
  .toArray
79
79
  )
@@ -82,7 +82,7 @@ test("works", () => {
82
82
  expect(
83
83
  somethings
84
84
  .filterMap(codeFilter(
85
- somethingsWhere((_) => _("b", Filters.$endsWith("2")))
85
+ somethingsWhere((_) => _("b", Filters.endsWith("2")))
86
86
  ))
87
87
  .toArray
88
88
  )
@@ -91,7 +91,7 @@ test("works", () => {
91
91
  expect(
92
92
  somethings
93
93
  .filterMap(codeFilter(
94
- somethingsWhere((_) => _("b", Filters.$startsWith("b")))
94
+ somethingsWhere((_) => _("b", Filters.startsWith("b")))
95
95
  ))
96
96
  .toArray
97
97
  )
@@ -100,7 +100,7 @@ test("works", () => {
100
100
  expect(
101
101
  somethings
102
102
  .filterMap(codeFilter(
103
- somethingsWhere((_) => _("b", Filters.$startsWith("2")))
103
+ somethingsWhere((_) => _("b", Filters.startsWith("2")))
104
104
  ))
105
105
  .toArray
106
106
  )
@@ -118,7 +118,7 @@ test("works", () => {
118
118
  expect(
119
119
  somethings
120
120
  .filterMap(codeFilter(
121
- somethingsWhere((_) => _("c", Filters.$includes("c")))
121
+ somethingsWhere((_) => _("c", Filters.includes("c")))
122
122
  ))
123
123
  .toArray
124
124
  )
@@ -145,7 +145,7 @@ test("works", () => {
145
145
  expect(
146
146
  somethings
147
147
  .filterMap(codeFilter(
148
- somethingsWhere((_) => _("d.-1.a", Filters.$isnt("a")))
148
+ somethingsWhere((_) => _("d.-1.a", Filters.isnt("a")))
149
149
  ))
150
150
  .toArray
151
151
  )
@@ -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
+ }