@effect-app/infra 4.0.0-beta.212 → 4.0.0-beta.213
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 +11 -0
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
- package/dist/Model/Repository/internal/internal.js +6 -2
- package/dist/Model/Repository/validation.d.ts +8 -8
- package/dist/Model/query/dsl.d.ts +51 -1
- package/dist/Model/query/dsl.d.ts.map +1 -1
- package/dist/Model/query/dsl.js +54 -1
- package/dist/Model/query/new-kid-interpreter.d.ts +18 -2
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.js +58 -4
- package/dist/RequestContext.d.ts +12 -12
- package/dist/Store/Cosmos/query.d.ts +5 -1
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +41 -23
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +1 -1
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +33 -2
- package/dist/Store/SQL/Pg.d.ts.map +1 -1
- package/dist/Store/SQL/Pg.js +1 -1
- 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 +25 -1
- package/dist/Store/SQL.d.ts.map +1 -1
- package/dist/Store/SQL.js +1 -1
- package/dist/Store/service.d.ts +5 -2
- package/dist/Store/service.d.ts.map +1 -1
- package/dist/Store/service.js +1 -1
- package/package.json +2 -2
- package/src/Model/Repository/internal/internal.ts +5 -1
- package/src/Model/query/dsl.ts +106 -0
- package/src/Model/query/new-kid-interpreter.ts +78 -4
- package/src/Store/Cosmos/query.ts +57 -23
- package/src/Store/Cosmos.ts +10 -2
- package/src/Store/Memory.ts +53 -4
- package/src/Store/SQL/Pg.ts +10 -1
- package/src/Store/SQL/query.ts +35 -1
- package/src/Store/SQL.ts +19 -2
- package/src/Store/service.ts +9 -2
- package/test/query.test.ts +66 -1
- package/test/rawQuery.test.ts +36 -1
- package/test/sql-store.test.ts +61 -0
package/src/Store/Memory.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { Array, Context, Effect, flow, type NonEmptyReadonlyArray, Option, Order, pipe, Ref, Result, Semaphore, Struct } from "effect-app"
|
|
4
4
|
import { NonEmptyString255 } from "effect-app/Schema"
|
|
5
5
|
import { InfraLogger } from "../logger.js"
|
|
6
|
+
import type { FilterResult } from "../Model/filter/filterApi.js"
|
|
6
7
|
import type { FieldValues } from "../Model/filter/types.js"
|
|
7
8
|
import { annotateDb } from "../otel.js"
|
|
8
9
|
import { codeFilter, codeFilter3_ } from "./codeFilter.js"
|
|
@@ -14,6 +15,42 @@ export function get(obj: any, path: string): any {
|
|
|
14
15
|
return path.split(".").reduce((res: any, key: string) => (res != null ? res[key] : res), obj)
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
const stripRelationFilterPaths = (state: readonly FilterResult[], relationPath: string): readonly FilterResult[] => {
|
|
19
|
+
const prefix = `${relationPath}.-1.`
|
|
20
|
+
return state.map((entry) =>
|
|
21
|
+
"path" in entry
|
|
22
|
+
? {
|
|
23
|
+
...entry,
|
|
24
|
+
path: entry.path.startsWith(prefix) ? entry.path.slice(prefix.length) : entry.path
|
|
25
|
+
}
|
|
26
|
+
: {
|
|
27
|
+
...entry,
|
|
28
|
+
result: stripRelationFilterPaths(entry.result, relationPath)
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const computeProjectionValue = (
|
|
34
|
+
row: FieldValues,
|
|
35
|
+
computed: {
|
|
36
|
+
readonly _tag: "relation-count" | "relation-any"
|
|
37
|
+
readonly path: string
|
|
38
|
+
readonly filter: readonly FilterResult[]
|
|
39
|
+
}
|
|
40
|
+
) => {
|
|
41
|
+
const relation = get(row, computed.path)
|
|
42
|
+
if (!Array.isArray(relation)) {
|
|
43
|
+
return computed._tag === "relation-count" ? 0 : false
|
|
44
|
+
}
|
|
45
|
+
const filter = stripRelationFilterPaths(computed.filter, computed.path)
|
|
46
|
+
switch (computed._tag) {
|
|
47
|
+
case "relation-count":
|
|
48
|
+
return relation.reduce<number>((acc, value) => codeFilter3_(filter, value) ? acc + 1 : acc, 0)
|
|
49
|
+
case "relation-any":
|
|
50
|
+
return relation.some((value) => codeFilter3_(filter, value))
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
17
54
|
export function memFilter<T extends FieldValues, U extends keyof T = never>(f: FilterArgs<T, U>) {
|
|
18
55
|
type M = U extends undefined ? T : Pick<T, U>
|
|
19
56
|
return ((c: T[]): M[] => {
|
|
@@ -21,16 +58,28 @@ export function memFilter<T extends FieldValues, U extends keyof T = never>(f: F
|
|
|
21
58
|
const sel = f.select
|
|
22
59
|
if (!sel) return r as M[]
|
|
23
60
|
return r.map((i) => {
|
|
24
|
-
const [keys,
|
|
61
|
+
const [keys, entries] = pipe(
|
|
25
62
|
sel,
|
|
26
|
-
Array.partition((
|
|
27
|
-
|
|
28
|
-
|
|
63
|
+
Array.partition((entry) => typeof entry === "string" ? Result.fail(String(entry)) : Result.succeed(entry))
|
|
64
|
+
)
|
|
65
|
+
const subKeys = entries.filter((entry): entry is { key: string; subKeys: readonly string[] } =>
|
|
66
|
+
typeof entry === "object" && entry !== null && "subKeys" in entry
|
|
29
67
|
)
|
|
68
|
+
const computedKeys = entries.filter((entry): entry is {
|
|
69
|
+
key: string
|
|
70
|
+
computed: {
|
|
71
|
+
readonly _tag: "relation-count" | "relation-any"
|
|
72
|
+
readonly path: string
|
|
73
|
+
readonly filter: readonly FilterResult[]
|
|
74
|
+
}
|
|
75
|
+
} => typeof entry === "object" && entry !== null && "computed" in entry)
|
|
30
76
|
const n = Struct.pick(i, keys)
|
|
31
77
|
subKeys.forEach((subKey) => {
|
|
32
78
|
n[subKey.key] = i[subKey.key]!.map(Struct.pick(subKey.subKeys as never[]))
|
|
33
79
|
})
|
|
80
|
+
computedKeys.forEach((entry) => {
|
|
81
|
+
;(n as Record<string, unknown>)[entry.key] = computeProjectionValue(i, entry.computed)
|
|
82
|
+
})
|
|
34
83
|
return n as M
|
|
35
84
|
})
|
|
36
85
|
}
|
package/src/Store/SQL/Pg.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { SqlClient } from "effect/unstable/sql"
|
|
|
6
6
|
import { OptimisticConcurrencyException } from "../../errors.js"
|
|
7
7
|
import { InfraLogger } from "../../logger.js"
|
|
8
8
|
import type { FieldValues } from "../../Model/filter/types.js"
|
|
9
|
+
import type { ComputedProjectionIrExpression } from "../../Model/query.js"
|
|
9
10
|
import { annotateDb } from "../../otel.js"
|
|
10
11
|
import { storeId } from "../Memory.js"
|
|
11
12
|
import { type FilterArgs, type PersistenceModelType, type StorageConfig, type Store, type StoreConfig, StoreMaker } from "../service.js"
|
|
@@ -217,7 +218,15 @@ const makePgStore = Effect.fnUntraced(function*({ prefix }: StorageConfig) {
|
|
|
217
218
|
tableName,
|
|
218
219
|
defaultValues,
|
|
219
220
|
f.select as
|
|
220
|
-
| NonEmptyReadonlyArray<
|
|
221
|
+
| NonEmptyReadonlyArray<
|
|
222
|
+
string | {
|
|
223
|
+
key: string
|
|
224
|
+
subKeys: readonly string[]
|
|
225
|
+
} | {
|
|
226
|
+
key: string
|
|
227
|
+
computed: ComputedProjectionIrExpression
|
|
228
|
+
}
|
|
229
|
+
>
|
|
221
230
|
| undefined,
|
|
222
231
|
f.order,
|
|
223
232
|
f.skip,
|
package/src/Store/SQL/query.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Effect, type NonEmptyReadonlyArray } from "effect-app"
|
|
|
3
3
|
import { assertUnreachable } from "effect-app/utils"
|
|
4
4
|
import { InfraLogger } from "../../logger.js"
|
|
5
5
|
import type { FilterR, FilterResult } from "../../Model/filter/filterApi.js"
|
|
6
|
+
import type { ComputedProjectionIrExpression } from "../../Model/query.js"
|
|
6
7
|
import { isRelationCheck } from "../codeFilter.js"
|
|
7
8
|
|
|
8
9
|
export interface SQLDialect {
|
|
@@ -153,7 +154,15 @@ export function buildWhereSQLQuery(
|
|
|
153
154
|
filter: readonly FilterResult[],
|
|
154
155
|
tableName: string,
|
|
155
156
|
defaultValues: Record<string, unknown>,
|
|
156
|
-
select?: NonEmptyReadonlyArray<
|
|
157
|
+
select?: NonEmptyReadonlyArray<
|
|
158
|
+
string | {
|
|
159
|
+
key: string
|
|
160
|
+
subKeys: readonly string[]
|
|
161
|
+
} | {
|
|
162
|
+
key: string
|
|
163
|
+
computed: ComputedProjectionIrExpression
|
|
164
|
+
}
|
|
165
|
+
>,
|
|
157
166
|
order?: NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }>,
|
|
158
167
|
skip?: number,
|
|
159
168
|
limit?: number
|
|
@@ -378,6 +387,28 @@ export function buildWhereSQLQuery(
|
|
|
378
387
|
return s
|
|
379
388
|
}
|
|
380
389
|
|
|
390
|
+
const computedSelectExpr = (key: string, computed: ComputedProjectionIrExpression): string => {
|
|
391
|
+
const relationPath = dottedToJsonPath(computed.path)
|
|
392
|
+
const relationAlias = `_${computed.path}`
|
|
393
|
+
const relationFrom = dialect.jsonEachFrom(relationPath, relationAlias)
|
|
394
|
+
const where = computed.filter.length > 0
|
|
395
|
+
? ` WHERE ${print(computed.filter, computed.path, false)}`
|
|
396
|
+
: ""
|
|
397
|
+
switch (computed._tag) {
|
|
398
|
+
case "relation-count":
|
|
399
|
+
return `(SELECT COUNT(1) FROM ${relationFrom}${where}) AS "${key}"`
|
|
400
|
+
case "relation-any": {
|
|
401
|
+
const existsExpr = `EXISTS(SELECT 1 FROM ${relationFrom}${where})`
|
|
402
|
+
if (dialect.jsonColumnType === "JSON") {
|
|
403
|
+
return `CASE WHEN ${existsExpr} THEN 'true' ELSE 'false' END AS "${key}"`
|
|
404
|
+
}
|
|
405
|
+
return `${existsExpr} AS "${key}"`
|
|
406
|
+
}
|
|
407
|
+
default:
|
|
408
|
+
return assertUnreachable(computed)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
381
412
|
const getSelectExpr = (): string => {
|
|
382
413
|
if (!select) return "id, _etag, data"
|
|
383
414
|
const fields = select.map((s) => {
|
|
@@ -386,6 +417,9 @@ export function buildWhereSQLQuery(
|
|
|
386
417
|
if (s === "_etag") return `_etag`
|
|
387
418
|
return `${dialect.jsonExtractJson(s)} AS "${s}"`
|
|
388
419
|
}
|
|
420
|
+
if ("computed" in s) {
|
|
421
|
+
return computedSelectExpr(s.key, s.computed)
|
|
422
|
+
}
|
|
389
423
|
return `${dialect.jsonExtractJson(s.key)} AS "${s.key}"`
|
|
390
424
|
})
|
|
391
425
|
return fields.join(", ")
|
package/src/Store/SQL.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { SqlClient } from "effect/unstable/sql"
|
|
|
7
7
|
import { OptimisticConcurrencyException } from "../errors.js"
|
|
8
8
|
import { InfraLogger } from "../logger.js"
|
|
9
9
|
import type { FieldValues } from "../Model/filter/types.js"
|
|
10
|
+
import type { ComputedProjectionIrExpression } from "../Model/query.js"
|
|
10
11
|
import { annotateDb, type DbSystem } from "../otel.js"
|
|
11
12
|
import { storeId } from "./Memory.js"
|
|
12
13
|
import { type FilterArgs, type PersistenceModelType, type StorageConfig, type Store, type StoreConfig, StoreMaker } from "./service.js"
|
|
@@ -236,7 +237,15 @@ function makeSQLStoreInt(system: DbSystem, dialect: SQLDialect, jsonColumnType:
|
|
|
236
237
|
defaultValues,
|
|
237
238
|
f
|
|
238
239
|
.select as
|
|
239
|
-
| NonEmptyReadonlyArray<
|
|
240
|
+
| NonEmptyReadonlyArray<
|
|
241
|
+
string | {
|
|
242
|
+
key: string
|
|
243
|
+
subKeys: readonly string[]
|
|
244
|
+
} | {
|
|
245
|
+
key: string
|
|
246
|
+
computed: ComputedProjectionIrExpression
|
|
247
|
+
}
|
|
248
|
+
>
|
|
240
249
|
| undefined,
|
|
241
250
|
f
|
|
242
251
|
.order,
|
|
@@ -580,7 +589,15 @@ function makeSQLiteStorePerNs(
|
|
|
580
589
|
defaultValues,
|
|
581
590
|
f
|
|
582
591
|
.select as
|
|
583
|
-
| NonEmptyReadonlyArray<
|
|
592
|
+
| NonEmptyReadonlyArray<
|
|
593
|
+
string | {
|
|
594
|
+
key: string
|
|
595
|
+
subKeys: readonly string[]
|
|
596
|
+
} | {
|
|
597
|
+
key: string
|
|
598
|
+
computed: ComputedProjectionIrExpression
|
|
599
|
+
}
|
|
600
|
+
>
|
|
584
601
|
| undefined,
|
|
585
602
|
f
|
|
586
603
|
.order as NonEmptyReadonlyArray<{ key: string; direction: "ASC" | "DESC" }> | undefined,
|
package/src/Store/service.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { OptimisticConcurrencyException } from "../errors.js"
|
|
|
6
6
|
import type { FilterResult } from "../Model/filter/filterApi.js"
|
|
7
7
|
import type { FieldValues } from "../Model/filter/types.js"
|
|
8
8
|
import type { FieldPath } from "../Model/filter/types/path/index.js"
|
|
9
|
-
import {
|
|
9
|
+
import type { ComputedProjectionIrExpression, RawQuery } from "../Model/query.js"
|
|
10
10
|
|
|
11
11
|
export interface StoreConfig<E> {
|
|
12
12
|
partitionValue: (e?: E) => string
|
|
@@ -61,7 +61,14 @@ export interface O<TFieldValues extends FieldValues> {
|
|
|
61
61
|
export interface FilterArgs<Encoded extends FieldValues, U extends keyof Encoded = never> {
|
|
62
62
|
t: Encoded
|
|
63
63
|
filter?: Filter | undefined
|
|
64
|
-
select?:
|
|
64
|
+
select?:
|
|
65
|
+
| NonEmptyReadonlyArray<
|
|
66
|
+
U | { key: string; subKeys: readonly string[] } | {
|
|
67
|
+
key: string
|
|
68
|
+
computed: ComputedProjectionIrExpression
|
|
69
|
+
}
|
|
70
|
+
>
|
|
71
|
+
| undefined
|
|
65
72
|
order?: NonEmptyReadonlyArray<O<NoInfer<Encoded>>>
|
|
66
73
|
limit?: number | undefined
|
|
67
74
|
skip?: number | undefined
|
package/test/query.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Context, Effect, flow, Layer, Option, pipe, S, Struct } from "effect-ap
|
|
|
6
6
|
import { inspect } from "util"
|
|
7
7
|
import { expect, expectTypeOf, it } from "vitest"
|
|
8
8
|
import { setupRequestContextFromCurrent } from "../src/api/setupRequest.js"
|
|
9
|
-
import { and, count, make, one, or, order, page, project, type QueryEnd, type QueryProjection, type QueryWhere, toFilter, where } from "../src/Model/query.js"
|
|
9
|
+
import { and, computed, count, make, one, or, order, page, project, projectComputed, type QueryEnd, type QueryProjection, type QueryWhere, relation, toFilter, where } from "../src/Model/query.js"
|
|
10
10
|
import { makeRepo } from "../src/Model/Repository.js"
|
|
11
11
|
import { RepositoryRegistryLive } from "../src/Model/Repository/Registry.js"
|
|
12
12
|
import { memFilter, MemoryStoreLive } from "../src/Store/Memory.js"
|
|
@@ -575,6 +575,71 @@ it(
|
|
|
575
575
|
.pipe(Effect.provide(TestStoreLive), setupRequestContextFromCurrent(), Effect.runPromise)
|
|
576
576
|
)
|
|
577
577
|
|
|
578
|
+
it("projectComputed sets computed IR and forces project mode", () => {
|
|
579
|
+
const baseSchema = S.Struct({
|
|
580
|
+
id: S.String,
|
|
581
|
+
items: S.Array(S.Struct({
|
|
582
|
+
state: S.Struct({
|
|
583
|
+
_tag: S.String
|
|
584
|
+
})
|
|
585
|
+
}))
|
|
586
|
+
})
|
|
587
|
+
const query = make<S.Codec.Encoded<typeof baseSchema>>().pipe(
|
|
588
|
+
projectComputed(
|
|
589
|
+
S.Struct({
|
|
590
|
+
pickedCount: S.NonNegativeInt
|
|
591
|
+
}),
|
|
592
|
+
computed({
|
|
593
|
+
pickedCount: relation<S.Codec.Encoded<typeof baseSchema>>("items").count(where("state._tag", "Picked"))
|
|
594
|
+
})
|
|
595
|
+
)
|
|
596
|
+
)
|
|
597
|
+
const interpreted = toFilter(query, baseSchema)
|
|
598
|
+
expect(interpreted.mode).toBe("project")
|
|
599
|
+
expect(interpreted.select).toEqual([
|
|
600
|
+
{
|
|
601
|
+
key: "pickedCount",
|
|
602
|
+
computed: {
|
|
603
|
+
_tag: "relation-count",
|
|
604
|
+
path: "items",
|
|
605
|
+
filter: [{ t: "where", path: "items.-1.state._tag", op: "eq", value: "Picked" }]
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
])
|
|
609
|
+
expect(interpreted.computed?.["pickedCount"]?._tag).toBe("relation-count")
|
|
610
|
+
expect(interpreted.computed?.["pickedCount"]?.path).toBe("items")
|
|
611
|
+
expect(interpreted.computed?.["pickedCount"]?.filter).toEqual([
|
|
612
|
+
{ t: "where", path: "items.-1.state._tag", op: "eq", value: "Picked" }
|
|
613
|
+
])
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
it("projectComputed validates extra computed keys", () => {
|
|
617
|
+
const baseSchema = S.Struct({
|
|
618
|
+
id: S.String,
|
|
619
|
+
items: S.Array(S.Struct({ value: S.Number }))
|
|
620
|
+
})
|
|
621
|
+
const query = make<S.Codec.Encoded<typeof baseSchema>>().pipe(
|
|
622
|
+
projectComputed(
|
|
623
|
+
S.Struct({ id: S.String }),
|
|
624
|
+
computed({
|
|
625
|
+
pickedCount: relation<S.Codec.Encoded<typeof baseSchema>>("items").count()
|
|
626
|
+
})
|
|
627
|
+
)
|
|
628
|
+
)
|
|
629
|
+
expect(() => toFilter(query, baseSchema)).toThrowError("Computed projection keys must exist in projection schema")
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
it("projection schema with computed fields fails without computed map", () => {
|
|
633
|
+
const baseSchema = S.Struct({
|
|
634
|
+
id: S.String,
|
|
635
|
+
items: S.Array(S.Struct({ value: S.Number }))
|
|
636
|
+
})
|
|
637
|
+
const query = make<S.Codec.Encoded<typeof baseSchema>>().pipe(
|
|
638
|
+
projectComputed(S.Struct({ pickedCount: S.NonNegativeInt }), computed({}))
|
|
639
|
+
)
|
|
640
|
+
expect(() => toFilter(query, baseSchema)).toThrowError("Missing computed projections for schema keys")
|
|
641
|
+
})
|
|
642
|
+
|
|
578
643
|
it(
|
|
579
644
|
"doesn't mess when refining fields",
|
|
580
645
|
() =>
|
package/test/rawQuery.test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from "@effect/vitest"
|
|
|
2
2
|
import { Array, Config, Context, Effect, flow, Layer, ManagedRuntime, Redacted, References, Result, S } from "effect-app"
|
|
3
3
|
import { LogLevels } from "effect-app/utils"
|
|
4
4
|
import { setupRequestContextFromCurrent } from "../src/api/setupRequest.js"
|
|
5
|
-
import { and, or, project, where, whereEvery, whereSome } from "../src/Model/query.js"
|
|
5
|
+
import { and, computed, or, project, projectComputed, relation, where, whereEvery, whereSome } from "../src/Model/query.js"
|
|
6
6
|
import { makeRepo } from "../src/Model/Repository/makeRepo.js"
|
|
7
7
|
import { RepositoryRegistryLive } from "../src/Model/Repository/Registry.js"
|
|
8
8
|
import { CosmosStoreLayer } from "../src/Store/Cosmos.js"
|
|
@@ -356,6 +356,41 @@ describe("multi-level", () => {
|
|
|
356
356
|
.pipe(Effect.provide(SomethingRepo.Test), rt.runPromise))
|
|
357
357
|
})
|
|
358
358
|
|
|
359
|
+
describe("computed projections", () => {
|
|
360
|
+
const test = Effect
|
|
361
|
+
.gen(function*() {
|
|
362
|
+
const repo = yield* SomethingRepo
|
|
363
|
+
const output = S.Struct({
|
|
364
|
+
id: S.String,
|
|
365
|
+
pickedCount: S.NonNegativeInt,
|
|
366
|
+
hasPicked: S.Boolean
|
|
367
|
+
})
|
|
368
|
+
const pickedFilter = where("value", "gt", 20)
|
|
369
|
+
const items = yield* repo.query(
|
|
370
|
+
projectComputed(
|
|
371
|
+
output,
|
|
372
|
+
computed({
|
|
373
|
+
pickedCount: relation<S.Codec.Encoded<typeof Something>>("items").count(pickedFilter),
|
|
374
|
+
hasPicked: relation<S.Codec.Encoded<typeof Something>>("items").any(pickedFilter)
|
|
375
|
+
})
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
expect(items).toStrictEqual([
|
|
379
|
+
{ id: "1", pickedCount: 0, hasPicked: false },
|
|
380
|
+
{ id: "2", pickedCount: 2, hasPicked: true }
|
|
381
|
+
])
|
|
382
|
+
})
|
|
383
|
+
.pipe(setupRequestContextFromCurrent())
|
|
384
|
+
|
|
385
|
+
it.skipIf(!process.env["STORAGE_URL"])("works well in CosmosDB", () =>
|
|
386
|
+
test
|
|
387
|
+
.pipe(Effect.provide(SomethingRepo.TestCosmos), rt.runPromise))
|
|
388
|
+
|
|
389
|
+
it("works well in Memory", () =>
|
|
390
|
+
test
|
|
391
|
+
.pipe(Effect.provide(SomethingRepo.Test), rt.runPromise))
|
|
392
|
+
})
|
|
393
|
+
|
|
359
394
|
// FUTURE: we need something like this instead:
|
|
360
395
|
/*
|
|
361
396
|
const subQuery = <T extends FieldValues>() => <TKey extends keyof T>(key: TKey, type: "some" | "every" = "some") => make<T[TKey][number]>() // todo: mark that this is sub query on field "items"
|
package/test/sql-store.test.ts
CHANGED
|
@@ -228,6 +228,47 @@ describe("SQL query builder (SQLite dialect)", () => {
|
|
|
228
228
|
expect(result.sql).toContain("LIMIT")
|
|
229
229
|
expect(result.sql).toContain("OFFSET")
|
|
230
230
|
})
|
|
231
|
+
|
|
232
|
+
it("computed relation count projection", () => {
|
|
233
|
+
const result = buildWhereSQLQuery(
|
|
234
|
+
sqliteDialect,
|
|
235
|
+
"id",
|
|
236
|
+
[],
|
|
237
|
+
"users",
|
|
238
|
+
{},
|
|
239
|
+
[{
|
|
240
|
+
key: "pickedCount",
|
|
241
|
+
computed: {
|
|
242
|
+
_tag: "relation-count",
|
|
243
|
+
path: "items",
|
|
244
|
+
filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
|
|
245
|
+
}
|
|
246
|
+
}]
|
|
247
|
+
)
|
|
248
|
+
expect(result.sql).toContain(`SELECT COUNT(1) FROM json_each(data, '$.items') AS _items`)
|
|
249
|
+
expect(result.sql).toContain(`AS "pickedCount"`)
|
|
250
|
+
expect(result.params).toContain("%picked%")
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it("computed relation any projection (sqlite bool encoding)", () => {
|
|
254
|
+
const result = buildWhereSQLQuery(
|
|
255
|
+
sqliteDialect,
|
|
256
|
+
"id",
|
|
257
|
+
[],
|
|
258
|
+
"users",
|
|
259
|
+
{},
|
|
260
|
+
[{
|
|
261
|
+
key: "hasPicked",
|
|
262
|
+
computed: {
|
|
263
|
+
_tag: "relation-any",
|
|
264
|
+
path: "items",
|
|
265
|
+
filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
|
|
266
|
+
}
|
|
267
|
+
}]
|
|
268
|
+
)
|
|
269
|
+
expect(result.sql).toContain("CASE WHEN EXISTS(")
|
|
270
|
+
expect(result.sql).toContain(`AS "hasPicked"`)
|
|
271
|
+
})
|
|
231
272
|
})
|
|
232
273
|
|
|
233
274
|
describe("SQL query builder (PostgreSQL dialect)", () => {
|
|
@@ -291,6 +332,26 @@ describe("SQL query builder (PostgreSQL dialect)", () => {
|
|
|
291
332
|
)
|
|
292
333
|
expect(result.sql).toContain("data->'address'->>'city'")
|
|
293
334
|
})
|
|
335
|
+
|
|
336
|
+
it("computed relation any projection", () => {
|
|
337
|
+
const result = buildWhereSQLQuery(
|
|
338
|
+
pgDialect,
|
|
339
|
+
"id",
|
|
340
|
+
[],
|
|
341
|
+
"users",
|
|
342
|
+
{},
|
|
343
|
+
[{
|
|
344
|
+
key: "hasPicked",
|
|
345
|
+
computed: {
|
|
346
|
+
_tag: "relation-any",
|
|
347
|
+
path: "items",
|
|
348
|
+
filter: [{ t: "where", path: "items.-1.description", op: "contains", value: "picked" }]
|
|
349
|
+
}
|
|
350
|
+
}]
|
|
351
|
+
)
|
|
352
|
+
expect(result.sql).toContain("EXISTS(SELECT 1 FROM jsonb_array_elements(data->'items') AS _items")
|
|
353
|
+
expect(result.sql).toContain(`AS "hasPicked"`)
|
|
354
|
+
})
|
|
294
355
|
})
|
|
295
356
|
|
|
296
357
|
// --- Integration tests with in-memory SQLite (direct, no Effect SQL client) ---
|