@effect-app/infra 0.118.0 → 0.120.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 (88) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/_cjs/services/Emailer/Sendgrid.cjs +1 -1
  3. package/_cjs/services/Emailer/Sendgrid.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/Disk.cjs +1 -1
  15. package/_cjs/services/Store/Disk.cjs.map +1 -1
  16. package/_cjs/services/Store/Memory.cjs +20 -17
  17. package/_cjs/services/Store/Memory.cjs.map +1 -1
  18. package/_cjs/services/Store/Redis.cjs +2 -2
  19. package/_cjs/services/Store/Redis.cjs.map +1 -1
  20. package/_cjs/services/Store/codeFilter.cjs +151 -0
  21. package/_cjs/services/Store/codeFilter.cjs.map +1 -0
  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/index.cjs +1 -1
  29. package/_cjs/services/Store/index.cjs.map +1 -1
  30. package/_cjs/services/Store/service.cjs.map +1 -1
  31. package/_cjs/services/Store/utils.cjs +8 -1
  32. package/_cjs/services/Store/utils.cjs.map +1 -1
  33. package/_src/services/Emailer/Sendgrid.ts +0 -1
  34. package/_src/services/Repository.ts +124 -70
  35. package/_src/services/RepositoryBase.ts +26 -12
  36. package/_src/services/Store/Cosmos/query.ts +390 -0
  37. package/_src/services/Store/Cosmos.test.ts +5 -5
  38. package/_src/services/Store/Cosmos.ts +39 -245
  39. package/_src/services/Store/Disk.ts +0 -1
  40. package/_src/services/Store/Memory.ts +21 -18
  41. package/_src/services/Store/Redis.ts +3 -4
  42. package/_src/services/Store/codeFilter.ts +150 -0
  43. package/_src/services/Store/filterApi/proxy.ts +109 -0
  44. package/_src/services/Store/filterApi/query.ts +256 -0
  45. package/_src/services/Store/filterApi.test.ts +263 -0
  46. package/_src/services/Store/index.ts +0 -1
  47. package/_src/services/Store/service.ts +17 -5
  48. package/_src/services/Store/utils.ts +10 -7
  49. package/dist/services/Emailer/Sendgrid.d.ts.map +1 -1
  50. package/dist/services/Emailer/Sendgrid.js +2 -2
  51. package/dist/services/Repository.d.ts +44 -104
  52. package/dist/services/Repository.d.ts.map +1 -1
  53. package/dist/services/Repository.js +6 -30
  54. package/dist/services/RepositoryBase.d.ts +11 -21
  55. package/dist/services/RepositoryBase.d.ts.map +1 -1
  56. package/dist/services/RepositoryBase.js +41 -37
  57. package/dist/services/Store/Cosmos/query.d.ts +61 -0
  58. package/dist/services/Store/Cosmos/query.d.ts.map +1 -0
  59. package/dist/services/Store/Cosmos/query.js +302 -0
  60. package/dist/services/Store/Cosmos.d.ts +3 -47
  61. package/dist/services/Store/Cosmos.d.ts.map +1 -1
  62. package/dist/services/Store/Cosmos.js +39 -193
  63. package/dist/services/Store/Disk.d.ts.map +1 -1
  64. package/dist/services/Store/Disk.js +2 -2
  65. package/dist/services/Store/Memory.d.ts +2 -7
  66. package/dist/services/Store/Memory.d.ts.map +1 -1
  67. package/dist/services/Store/Memory.js +17 -15
  68. package/dist/services/Store/Redis.d.ts.map +1 -1
  69. package/dist/services/Store/Redis.js +3 -3
  70. package/dist/services/Store/codeFilter.d.ts +5 -0
  71. package/dist/services/Store/codeFilter.d.ts.map +1 -0
  72. package/dist/services/Store/codeFilter.js +140 -0
  73. package/dist/services/Store/filterApi/proxy.d.ts +41 -0
  74. package/dist/services/Store/filterApi/proxy.d.ts.map +1 -0
  75. package/dist/services/Store/filterApi/proxy.js +41 -0
  76. package/dist/services/Store/filterApi/query.d.ts +64 -0
  77. package/dist/services/Store/filterApi/query.d.ts.map +1 -0
  78. package/dist/services/Store/filterApi/query.js +134 -0
  79. package/dist/services/Store/filterApi.test.d.ts.map +1 -0
  80. package/dist/services/Store/index.d.ts.map +1 -1
  81. package/dist/services/Store/index.js +2 -2
  82. package/dist/services/Store/service.d.ts +12 -5
  83. package/dist/services/Store/service.d.ts.map +1 -1
  84. package/dist/services/Store/service.js +1 -1
  85. package/dist/services/Store/utils.d.ts +7 -1
  86. package/dist/services/Store/utils.d.ts.map +1 -1
  87. package/dist/services/Store/utils.js +118 -115
  88. package/package.json +48 -8
@@ -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.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
- }
@@ -138,7 +138,6 @@ export function makeDiskStore({ prefix }: StorageConfig, dir: string) {
138
138
 
139
139
  export function DiskStoreLive(config: Config<StorageConfig>, dir: string) {
140
140
  return config
141
- .config
142
141
  .flatMap((_) => makeDiskStore(_, dir))
143
142
  .toLayer(StoreMaker)
144
143
  }
@@ -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))),
@@ -92,5 +91,5 @@ export function makeRedisStore({ prefix }: StorageConfig) {
92
91
  })
93
92
  }
94
93
  export function RedisStoreLive(config: Config<StorageConfig>) {
95
- return config.config.flatMap(makeRedisStore).toLayer(StoreMaker)
94
+ return config.flatMap(makeRedisStore).toLayer(StoreMaker)
96
95
  }
@@ -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
+ )