@effect-app/infra 4.0.0-beta.316 → 4.0.0-beta.317
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 +24 -0
- package/dist/Store/Cosmos/query.d.ts +2 -1
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +60 -2
- package/dist/Store/Cosmos.d.ts.map +1 -1
- package/dist/Store/Cosmos.js +17 -12
- package/dist/Store/Disk.d.ts.map +1 -1
- package/dist/Store/Disk.js +12 -7
- package/dist/Store/Memory.d.ts +2 -1
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +28 -15
- package/dist/Store/SQL/Pg.d.ts.map +1 -1
- package/dist/Store/SQL/Pg.js +15 -11
- package/dist/Store/SQL/query.d.ts +5 -1
- package/dist/Store/SQL/query.d.ts.map +1 -1
- package/dist/Store/SQL/query.js +131 -2
- package/dist/Store/SQL.d.ts +1 -1
- package/dist/Store/SQL.d.ts.map +1 -1
- package/dist/Store/SQL.js +24 -18
- package/dist/Store/codeFilter.d.ts.map +1 -1
- package/dist/Store/codeFilter.js +67 -20
- package/dist/Store/jsonDocument.d.ts +13 -0
- package/dist/Store/jsonDocument.d.ts.map +1 -0
- package/dist/Store/jsonDocument.js +31 -0
- package/dist/Store/utils.d.ts +16 -0
- package/dist/Store/utils.d.ts.map +1 -1
- package/dist/Store/utils.js +245 -6
- package/dist/logger.d.ts +5 -5
- package/examples/query.ts +4 -4
- package/package.json +3 -3
- package/src/Store/Cosmos/query.ts +65 -9
- package/src/Store/Cosmos.ts +17 -11
- package/src/Store/Disk.ts +28 -7
- package/src/Store/Memory.ts +40 -17
- package/src/Store/SQL/Pg.ts +23 -10
- package/src/Store/SQL/query.ts +152 -7
- package/src/Store/SQL.ts +37 -17
- package/src/Store/codeFilter.ts +71 -23
- package/src/Store/jsonDocument.ts +43 -0
- package/src/Store/utils.ts +275 -5
- package/test/cosmos-query.test.ts +100 -5
- package/test/dist/json-lower.test.d.ts.map +1 -0
- package/test/dist/query.test.d.ts.map +1 -1
- package/test/json-lower.test.ts +115 -0
- package/test/query.test.ts +261 -12
- package/test/sql-store.test.ts +233 -39
|
@@ -13,6 +13,101 @@ type OrderEnc = S.Codec.Encoded<typeof Order>
|
|
|
13
13
|
|
|
14
14
|
// Length projection via `relation(...).length()` should emit a scalar
|
|
15
15
|
// ARRAY_LENGTH expression rather than pulling (or reshaping) the array.
|
|
16
|
+
describe("cosmos query filter: native Encoded values", () => {
|
|
17
|
+
it("binds Date as ISO string parameters", () => {
|
|
18
|
+
const result = buildWhereCosmosQuery3(
|
|
19
|
+
"id",
|
|
20
|
+
[{ t: "where", path: "n", op: "eq", value: new Date("2024-01-01T00:00:00.000Z") }],
|
|
21
|
+
"Orders",
|
|
22
|
+
{}
|
|
23
|
+
)
|
|
24
|
+
expect(result.parameters).toEqual(
|
|
25
|
+
expect.arrayContaining([{ name: "@v0", value: "2024-01-01T00:00:00.000Z" }])
|
|
26
|
+
)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("binds Map as array of tuples", () => {
|
|
30
|
+
const result = buildWhereCosmosQuery3(
|
|
31
|
+
"id",
|
|
32
|
+
[{ t: "where", path: "meta", op: "eq", value: new Map([["k", "v"]]) }],
|
|
33
|
+
"Orders",
|
|
34
|
+
{}
|
|
35
|
+
)
|
|
36
|
+
expect(result.parameters).toEqual(
|
|
37
|
+
expect.arrayContaining([{ name: "@v0", value: [["k", "v"]] }])
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("emits EXISTS over Map JSON tuples for hasKey / hasValue / hasKeyValue", () => {
|
|
42
|
+
const byKey = buildWhereCosmosQuery3(
|
|
43
|
+
"id",
|
|
44
|
+
[{ t: "where", path: "meta", op: "hasKey", value: "n" }],
|
|
45
|
+
"Orders",
|
|
46
|
+
{}
|
|
47
|
+
)
|
|
48
|
+
expect(byKey.query).toContain("EXISTS(SELECT VALUE p FROM p IN f[\"meta\"] WHERE p[0] = @v0)")
|
|
49
|
+
expect(byKey.parameters).toEqual(expect.arrayContaining([{ name: "@v0", value: "n" }]))
|
|
50
|
+
|
|
51
|
+
const byValue = buildWhereCosmosQuery3(
|
|
52
|
+
"id",
|
|
53
|
+
[{ t: "where", path: "meta", op: "hasValue", value: 1 }],
|
|
54
|
+
"Orders",
|
|
55
|
+
{}
|
|
56
|
+
)
|
|
57
|
+
expect(byValue.query).toContain("p[1] = @v0")
|
|
58
|
+
|
|
59
|
+
const byPair = buildWhereCosmosQuery3(
|
|
60
|
+
"id",
|
|
61
|
+
[{ t: "where", path: "meta", op: "hasKeyValue", value: ["n", 1] }],
|
|
62
|
+
"Orders",
|
|
63
|
+
{}
|
|
64
|
+
)
|
|
65
|
+
expect(byPair.query).toContain("ARRAY_CONTAINS")
|
|
66
|
+
expect(byPair.parameters).toEqual(expect.arrayContaining([{ name: "@v0", value: ["n", 1] }]))
|
|
67
|
+
|
|
68
|
+
const anyKey = buildWhereCosmosQuery3(
|
|
69
|
+
"id",
|
|
70
|
+
[{ t: "where", path: "meta", op: "hasKey-any", value: ["n", "x"] }],
|
|
71
|
+
"Orders",
|
|
72
|
+
{}
|
|
73
|
+
)
|
|
74
|
+
expect(anyKey.query).toContain("ARRAY_CONTAINS(@v0, p[0])")
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("binds includes Date as ISO string", () => {
|
|
78
|
+
const result = buildWhereCosmosQuery3(
|
|
79
|
+
"id",
|
|
80
|
+
[{ t: "where", path: "dates", op: "includes", value: new Date("2024-01-01T00:00:00.000Z") }],
|
|
81
|
+
"Orders",
|
|
82
|
+
{}
|
|
83
|
+
)
|
|
84
|
+
expect(result.query).toContain("ARRAY_CONTAINS")
|
|
85
|
+
expect(result.parameters).toEqual(
|
|
86
|
+
expect.arrayContaining([{ name: "@v0", value: "2024-01-01T00:00:00.000Z" }])
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("binds includes-any Date Set as ISO parameters", () => {
|
|
91
|
+
const result = buildWhereCosmosQuery3(
|
|
92
|
+
"id",
|
|
93
|
+
[{
|
|
94
|
+
t: "where",
|
|
95
|
+
path: "dates",
|
|
96
|
+
op: "includes-any",
|
|
97
|
+
value: new Set([new Date("2024-01-01T00:00:00.000Z")])
|
|
98
|
+
}],
|
|
99
|
+
"Orders",
|
|
100
|
+
{}
|
|
101
|
+
)
|
|
102
|
+
expect(result.parameters).toEqual(
|
|
103
|
+
expect.arrayContaining([
|
|
104
|
+
{ name: "@v0", value: ["2024-01-01T00:00:00.000Z"] },
|
|
105
|
+
{ name: "@v0__0", value: "2024-01-01T00:00:00.000Z" }
|
|
106
|
+
])
|
|
107
|
+
)
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
16
111
|
describe("cosmos query projection: array length", () => {
|
|
17
112
|
it("projects packages length via ARRAY_LENGTH", () => {
|
|
18
113
|
const q = make<OrderEnc>().pipe(
|
|
@@ -29,13 +124,13 @@ describe("cosmos query projection: array length", () => {
|
|
|
29
124
|
ir.filter ?? [],
|
|
30
125
|
"Orders",
|
|
31
126
|
{},
|
|
32
|
-
ir.select
|
|
127
|
+
ir.select
|
|
33
128
|
)
|
|
34
129
|
|
|
35
130
|
expect(result.query).toMatch(/ARRAY_LENGTH\(f(?:\.packages|\["packages"\])\)/)
|
|
36
131
|
expect(result.query).toContain("AS packageCount")
|
|
37
132
|
// Must not pull the full array nor reshape via subquery
|
|
38
|
-
expect(result.query).not.toMatch(/ARRAY\s*\(\s*SELECT[^)]*FROM\s+t\s+in\s+f[
|
|
133
|
+
expect(result.query).not.toMatch(/ARRAY\s*\(\s*SELECT[^)]*FROM\s+t\s+in\s+f[.["]/i)
|
|
39
134
|
expect(result.query).not.toMatch(/SELECT VALUE COUNT/)
|
|
40
135
|
})
|
|
41
136
|
})
|
|
@@ -83,7 +178,7 @@ describe("cosmos query projection: union array fields", () => {
|
|
|
83
178
|
const packageSelects = select.filter((item) =>
|
|
84
179
|
typeof item === "object" && item !== null && "key" in item && item.key === "packages"
|
|
85
180
|
)
|
|
86
|
-
const result = buildWhereCosmosQuery3("id", ir.filter ?? [], "Orders", {}, ir.select
|
|
181
|
+
const result = buildWhereCosmosQuery3("id", ir.filter ?? [], "Orders", {}, ir.select)
|
|
87
182
|
|
|
88
183
|
expect(packageSelects).toHaveLength(1)
|
|
89
184
|
expect(result.query.match(/\bAS\s+packages\b/g) ?? []).toHaveLength(1)
|
|
@@ -102,7 +197,7 @@ describe("cosmos query projection: union array fields", () => {
|
|
|
102
197
|
const packageSelects = select.filter((item) =>
|
|
103
198
|
typeof item === "object" && item !== null && "key" in item && item.key === "packages"
|
|
104
199
|
)
|
|
105
|
-
const result = buildWhereCosmosQuery3("id", ir.filter ?? [], "Orders", {}, ir.select
|
|
200
|
+
const result = buildWhereCosmosQuery3("id", ir.filter ?? [], "Orders", {}, ir.select)
|
|
106
201
|
|
|
107
202
|
expect(packageSelects).toHaveLength(1)
|
|
108
203
|
expect(result.query.match(/\bAS\s+packages\b/g) ?? []).toHaveLength(1)
|
|
@@ -146,7 +241,7 @@ describe("cosmos query projection: relation-every parameter binding", () => {
|
|
|
146
241
|
)
|
|
147
242
|
|
|
148
243
|
const ir = toFilter(q as any, DN as any)
|
|
149
|
-
const result = buildWhereCosmosQuery3("id", ir.filter ?? [], "DN", {}, ir.select
|
|
244
|
+
const result = buildWhereCosmosQuery3("id", ir.filter ?? [], "DN", {}, ir.select)
|
|
150
245
|
|
|
151
246
|
// Each filter element binds exactly one parameter: 2 every filters + 2 main filter = 4.
|
|
152
247
|
expect(result.parameters).toHaveLength(4)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-lower.test.d.ts","sourceRoot":"","sources":["../json-lower.test.ts"],"names":[],"mappings":""}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.test.d.ts","sourceRoot":"","sources":["../query.test.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,CAAC,MAAM,mBAAmB,CAAA;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"query.test.d.ts","sourceRoot":"","sources":["../query.test.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,CAAC,MAAM,mBAAmB,CAAA;;;;;;;;;;;;;;;;;AAsBtC,qBAAa,SAAU,SAAQ,cAM7B;CAAG;AACL,MAAM,CAAC,OAAO,WAAW,SAAS,CAAC;IAEjC,UAAiB,OAAQ,SAAQ,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,SAAS,CAAC;KAAG;CACtE"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Ops } from "effect-app/Model/filter/filterApi"
|
|
2
|
+
import * as S from "effect-app/Schema"
|
|
3
|
+
import * as Getter from "effect/SchemaGetter"
|
|
4
|
+
import { describe, expect, it } from "vitest"
|
|
5
|
+
import { jsonifyFilter } from "../src/Store/utils.js"
|
|
6
|
+
|
|
7
|
+
class Day {
|
|
8
|
+
readonly ymd: string
|
|
9
|
+
constructor(ymd: string) {
|
|
10
|
+
this.ymd = ymd
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DayFromSelf = S.declare((u): u is Day => u instanceof Day, {
|
|
15
|
+
expected: "Day",
|
|
16
|
+
toCodecJson: () =>
|
|
17
|
+
S.link<Day>()(
|
|
18
|
+
S.String,
|
|
19
|
+
{
|
|
20
|
+
decode: Getter.transform((s: string) => new Day(s)),
|
|
21
|
+
encode: Getter.transform((d: Day) => d.ymd)
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const where = (path: string, op: Ops, value: unknown) => ({
|
|
27
|
+
t: "where" as const,
|
|
28
|
+
path,
|
|
29
|
+
op,
|
|
30
|
+
value
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe("jsonifyFilter Encoded key paths", () => {
|
|
34
|
+
it("lowers encodeKeys-renamed native Encoded values via the Encoded field name", () => {
|
|
35
|
+
const schema = S
|
|
36
|
+
.Struct({
|
|
37
|
+
id: S.String,
|
|
38
|
+
day: DayFromSelf
|
|
39
|
+
})
|
|
40
|
+
.pipe(S.encodeKeys({ day: "the_day" }))
|
|
41
|
+
const day = new Day("2024-06-01")
|
|
42
|
+
expect(jsonifyFilter([where("the_day", "eq", day)], schema)).toEqual([
|
|
43
|
+
where("the_day", "eq", "2024-06-01")
|
|
44
|
+
])
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("lowers Class.pipe(encodeKeys) using Encoded names, not Type .fields", () => {
|
|
48
|
+
class Doc extends S.Class<Doc>("JsonLowerEncodeKeysDoc")({
|
|
49
|
+
id: S.String,
|
|
50
|
+
day: DayFromSelf,
|
|
51
|
+
tags: S.NonEmptyArray(S.String)
|
|
52
|
+
}) {}
|
|
53
|
+
const schema = Doc.pipe(S.encodeKeys({ day: "the_day" }))
|
|
54
|
+
const day = new Day("2024-06-01")
|
|
55
|
+
expect(jsonifyFilter([where("the_day", "eq", day)], schema)).toEqual([
|
|
56
|
+
where("the_day", "eq", "2024-06-01")
|
|
57
|
+
])
|
|
58
|
+
expect(jsonifyFilter([where("id", "in", ["d1"])], schema)).toEqual([
|
|
59
|
+
where("id", "in", ["d1"])
|
|
60
|
+
])
|
|
61
|
+
expect(jsonifyFilter([where("tags", "includes-any", ["a"])], schema)).toEqual([
|
|
62
|
+
where("tags", "includes-any", ["a"])
|
|
63
|
+
])
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("does not encode scalar `in` values through an array field renamed onto Encoded `id`", () => {
|
|
67
|
+
class Doc extends S.Class<Doc>("JsonLowerSwappedKeysDoc")({
|
|
68
|
+
id: S.String,
|
|
69
|
+
items: S.NonEmptyArray(S.String)
|
|
70
|
+
}) {}
|
|
71
|
+
const schema = Doc.pipe(S.encodeKeys({ items: "id", id: "item_id" }))
|
|
72
|
+
expect(jsonifyFilter([where("item_id", "in", ["d1"])], schema)).toEqual([
|
|
73
|
+
where("item_id", "in", ["d1"])
|
|
74
|
+
])
|
|
75
|
+
expect(jsonifyFilter([where("id", "includes-any", ["a"])], schema)).toEqual([
|
|
76
|
+
where("id", "includes-any", ["a"])
|
|
77
|
+
])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("lowers TaggedUnion Opaque `id in`", () => {
|
|
81
|
+
const identity = S.Struct({ id: S.String, layout: S.String })
|
|
82
|
+
class Available extends S.Opaque<Available>()(S.TaggedStruct("available", { ...identity.fields })) {}
|
|
83
|
+
class Reserved extends S.Opaque<Reserved>()(
|
|
84
|
+
S.TaggedStruct("reserved", { ...identity.fields, reservation: S.String })
|
|
85
|
+
) {}
|
|
86
|
+
const Cart = S.TaggedUnion([Available, Reserved])
|
|
87
|
+
expect(jsonifyFilter([where("id", "in", ["cart-1"])], Cart)).toEqual([
|
|
88
|
+
where("id", "in", ["cart-1"])
|
|
89
|
+
])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("lowers TaggedClass union array includes-any through the element schema", () => {
|
|
93
|
+
class Picking extends S.TaggedClass<Picking>()("picking", {
|
|
94
|
+
id: S.String,
|
|
95
|
+
cartIds: S.NonEmptyArray(S.String),
|
|
96
|
+
createdAt: S.Date
|
|
97
|
+
}) {}
|
|
98
|
+
class Assembling extends S.TaggedClass<Assembling>()("assembling", {
|
|
99
|
+
id: S.String,
|
|
100
|
+
cartIds: S.NonEmptyArray(S.String),
|
|
101
|
+
createdAt: S.Date
|
|
102
|
+
}) {}
|
|
103
|
+
const Batch = S.TaggedUnion([Picking, Assembling])
|
|
104
|
+
const at = new Date("2024-06-01T00:00:00.000Z")
|
|
105
|
+
expect(jsonifyFilter([where("id", "in", ["batch-1"])], Batch)).toEqual([
|
|
106
|
+
where("id", "in", ["batch-1"])
|
|
107
|
+
])
|
|
108
|
+
expect(jsonifyFilter([where("cartIds", "includes-any", ["cart-1"])], Batch)).toEqual([
|
|
109
|
+
where("cartIds", "includes-any", ["cart-1"])
|
|
110
|
+
])
|
|
111
|
+
expect(jsonifyFilter([where("createdAt", "eq", at)], Batch)).toEqual([
|
|
112
|
+
where("createdAt", "eq", at.toISOString())
|
|
113
|
+
])
|
|
114
|
+
})
|
|
115
|
+
})
|
package/test/query.test.ts
CHANGED
|
@@ -11,10 +11,16 @@ import * as Option from "effect-app/Option"
|
|
|
11
11
|
import * as S from "effect-app/Schema"
|
|
12
12
|
import { setupRequestContextFromCurrent } from "effect-app/setupRequest"
|
|
13
13
|
import { flow, pipe } from "effect/Function"
|
|
14
|
+
import * as Redacted from "effect/Redacted"
|
|
15
|
+
import * as Getter from "effect/SchemaGetter"
|
|
14
16
|
import * as SchemaTransformation from "effect/SchemaTransformation"
|
|
15
17
|
import * as Struct from "effect/Struct"
|
|
18
|
+
import * as fs from "fs"
|
|
19
|
+
import * as os from "os"
|
|
20
|
+
import * as path from "path"
|
|
16
21
|
import { inspect } from "util"
|
|
17
22
|
import { expect, expectTypeOf, it } from "vitest"
|
|
23
|
+
import { DiskStoreLayer } from "../src/Store/Disk.js"
|
|
18
24
|
import { memFilter, MemoryStoreLive } from "../src/Store/Memory.js"
|
|
19
25
|
import { SomeService } from "./fixtures.js"
|
|
20
26
|
|
|
@@ -41,7 +47,7 @@ const q = make<Something.Encoded>()
|
|
|
41
47
|
where("displayName", "Verona"),
|
|
42
48
|
or(
|
|
43
49
|
where("displayName", "Riley"),
|
|
44
|
-
and("n", "gt", "2021-01-01T00:00:00Z")
|
|
50
|
+
and("n", "gt", new Date("2021-01-01T00:00:00Z"))
|
|
45
51
|
),
|
|
46
52
|
order("displayName"),
|
|
47
53
|
page({ take: 10 }),
|
|
@@ -141,7 +147,7 @@ it("works with repo", () =>
|
|
|
141
147
|
where("displayName", "Verona"),
|
|
142
148
|
or(
|
|
143
149
|
where("displayName", "Riley"),
|
|
144
|
-
and("n", "gt", "2021-01-01T00:00:00Z")
|
|
150
|
+
and("n", "gt", new Date("2021-01-01T00:00:00Z"))
|
|
145
151
|
),
|
|
146
152
|
order("displayName"),
|
|
147
153
|
page({ take: 10 }),
|
|
@@ -168,6 +174,11 @@ it("works with repo", () =>
|
|
|
168
174
|
|
|
169
175
|
expect(q1).toEqual(items.slice(0, 2).toReversed().map(Struct.pick(["id", "displayName"])))
|
|
170
176
|
expect(q2).toEqual(items.slice(0, 2).toReversed().map(Struct.pick(["displayName"])))
|
|
177
|
+
|
|
178
|
+
const byDate = yield* somethingRepo.query(
|
|
179
|
+
where("n", new Date("2020-01-01T00:00:00.000Z"))
|
|
180
|
+
)
|
|
181
|
+
expect(byDate.map((_) => _.displayName)).toEqual(["Verona", "Riley"])
|
|
171
182
|
})
|
|
172
183
|
.pipe(
|
|
173
184
|
Effect.provide(Layer.mergeAll(SomethingRepo.Test, SomeService.Default)),
|
|
@@ -176,6 +187,150 @@ it("works with repo", () =>
|
|
|
176
187
|
Effect.runPromise
|
|
177
188
|
))
|
|
178
189
|
|
|
190
|
+
it("memory store round-trips Date/Set/Map via JSON codecs", () =>
|
|
191
|
+
Effect
|
|
192
|
+
.gen(function*() {
|
|
193
|
+
class Doc extends S.Class<Doc>("JsonCodecDoc")({
|
|
194
|
+
id: S.String,
|
|
195
|
+
at: S.Date,
|
|
196
|
+
tags: S.ReadonlySet(S.String),
|
|
197
|
+
meta: S.ReadonlyMap({ key: S.String, value: S.Finite })
|
|
198
|
+
}) {}
|
|
199
|
+
const at = new Date("2024-06-01T00:00:00.000Z")
|
|
200
|
+
const saved = new Doc({
|
|
201
|
+
id: "d1",
|
|
202
|
+
at,
|
|
203
|
+
tags: new Set(["a", "b"]),
|
|
204
|
+
meta: new Map([["n", 1]])
|
|
205
|
+
})
|
|
206
|
+
const repo = yield* makeRepo("JsonCodecDoc", Doc, { makeInitial: Effect.succeed([saved]) })
|
|
207
|
+
const found = yield* repo.find("d1")
|
|
208
|
+
expect(Option.isSome(found)).toBe(true)
|
|
209
|
+
if (Option.isSome(found)) {
|
|
210
|
+
expect(found.value.at.toISOString()).toBe(at.toISOString())
|
|
211
|
+
expect(found.value.tags).toEqual(new Set(["a", "b"]))
|
|
212
|
+
expect(found.value.meta).toEqual(new Map([["n", 1]]))
|
|
213
|
+
}
|
|
214
|
+
const byDate = yield* repo.query(where("at", at))
|
|
215
|
+
expect(byDate.map((_) => _.id)).toEqual(["d1"])
|
|
216
|
+
const byTag = yield* repo.query(where("tags", "includes", "b"))
|
|
217
|
+
expect(byTag.map((_) => _.id)).toEqual(["d1"])
|
|
218
|
+
const byKey = yield* repo.query(where("meta", "hasKey", "n"))
|
|
219
|
+
expect(byKey.map((_) => _.id)).toEqual(["d1"])
|
|
220
|
+
const byPair = yield* repo.query(where("meta", "hasKeyValue", ["n", 1]))
|
|
221
|
+
expect(byPair.map((_) => _.id)).toEqual(["d1"])
|
|
222
|
+
})
|
|
223
|
+
.pipe(Effect.provide(TestStoreLive), setupRequestContextFromCurrent(), Effect.scoped, Effect.runPromise))
|
|
224
|
+
|
|
225
|
+
class Day {
|
|
226
|
+
readonly ymd: string
|
|
227
|
+
constructor(ymd: string) {
|
|
228
|
+
this.ymd = ymd
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const DayFromSelf = S.declare((u): u is Day => u instanceof Day, {
|
|
233
|
+
expected: "Day",
|
|
234
|
+
toCodecJson: () =>
|
|
235
|
+
S.link<Day>()(
|
|
236
|
+
S.String,
|
|
237
|
+
{
|
|
238
|
+
decode: Getter.transform((s: string) => new Day(s)),
|
|
239
|
+
encode: Getter.transform((d: Day) => d.ymd)
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it("memory store round-trips app native Encoded values via the document schema", () =>
|
|
245
|
+
Effect
|
|
246
|
+
.gen(function*() {
|
|
247
|
+
class Doc extends S.Class<Doc>("JsonCodecDayDoc")({
|
|
248
|
+
id: S.String,
|
|
249
|
+
day: DayFromSelf
|
|
250
|
+
}) {}
|
|
251
|
+
const day = new Day("2024-06-01")
|
|
252
|
+
const saved = new Doc({ id: "d1", day })
|
|
253
|
+
const repo = yield* makeRepo("JsonCodecDayDoc", Doc, { makeInitial: Effect.succeed([saved]) })
|
|
254
|
+
const found = yield* repo.find("d1")
|
|
255
|
+
expect(Option.isSome(found)).toBe(true)
|
|
256
|
+
if (Option.isSome(found)) {
|
|
257
|
+
expect(found.value.day).toBeInstanceOf(Day)
|
|
258
|
+
expect(found.value.day.ymd).toBe("2024-06-01")
|
|
259
|
+
}
|
|
260
|
+
expect(S.encodeSync(DayFromSelf)(day)).toBe(day)
|
|
261
|
+
expect(S.encodeSync(S.toCodecJson(DayFromSelf))(day)).toBe("2024-06-01")
|
|
262
|
+
const byDay = yield* repo.query(where("day", day))
|
|
263
|
+
expect(byDay.map((_) => _.id)).toEqual(["d1"])
|
|
264
|
+
})
|
|
265
|
+
.pipe(Effect.provide(TestStoreLive), setupRequestContextFromCurrent(), Effect.scoped, Effect.runPromise))
|
|
266
|
+
|
|
267
|
+
it("memory store queries encodeKeys-renamed native Encoded fields", () =>
|
|
268
|
+
Effect
|
|
269
|
+
.gen(function*() {
|
|
270
|
+
class Doc extends S.Class<Doc>("JsonCodecEncodeKeysDayDoc")({
|
|
271
|
+
id: S.String,
|
|
272
|
+
day: DayFromSelf
|
|
273
|
+
}) {}
|
|
274
|
+
const schema = Doc.pipe(S.encodeKeys({ day: "the_day" }))
|
|
275
|
+
const day = new Day("2024-06-01")
|
|
276
|
+
const saved = new Doc({ id: "d1", day })
|
|
277
|
+
const repo = yield* makeRepo("JsonCodecEncodeKeysDayDoc", schema, { makeInitial: Effect.succeed([saved]) })
|
|
278
|
+
const byDay = yield* repo.query(where("the_day", day))
|
|
279
|
+
expect(byDay.map((_) => _.id)).toEqual(["d1"])
|
|
280
|
+
const byId = yield* repo.query(where("id", "in", ["d1"]))
|
|
281
|
+
expect(byId.map((_) => _.id)).toEqual(["d1"])
|
|
282
|
+
})
|
|
283
|
+
.pipe(Effect.provide(TestStoreLive), setupRequestContextFromCurrent(), Effect.scoped, Effect.runPromise))
|
|
284
|
+
|
|
285
|
+
it("disk store round-trips Date/Set/Map via JSON codecs", () => {
|
|
286
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "effect-app-disk-json-"))
|
|
287
|
+
const diskLive = Layer.merge(
|
|
288
|
+
DiskStoreLayer({ url: Redacted.make(`disk://${dir}`), prefix: "", dbName: "test" }, dir),
|
|
289
|
+
RepositoryRegistryLive
|
|
290
|
+
)
|
|
291
|
+
return Effect
|
|
292
|
+
.gen(function*() {
|
|
293
|
+
class Doc extends S.Class<Doc>("JsonCodecDiskDoc")({
|
|
294
|
+
id: S.String,
|
|
295
|
+
at: S.Date,
|
|
296
|
+
tags: S.ReadonlySet(S.String),
|
|
297
|
+
meta: S.ReadonlyMap({ key: S.String, value: S.Finite })
|
|
298
|
+
}) {}
|
|
299
|
+
const at = new Date("2024-06-01T00:00:00.000Z")
|
|
300
|
+
const saved = new Doc({
|
|
301
|
+
id: "d1",
|
|
302
|
+
at,
|
|
303
|
+
tags: new Set(["a", "b"]),
|
|
304
|
+
meta: new Map([["n", 1]])
|
|
305
|
+
})
|
|
306
|
+
const repo = yield* makeRepo("JsonCodecDiskDoc", Doc, { makeInitial: Effect.succeed([saved]) })
|
|
307
|
+
const found = yield* repo.find("d1")
|
|
308
|
+
expect(Option.isSome(found)).toBe(true)
|
|
309
|
+
if (Option.isSome(found)) {
|
|
310
|
+
expect(found.value.at.toISOString()).toBe(at.toISOString())
|
|
311
|
+
expect(found.value.tags).toEqual(new Set(["a", "b"]))
|
|
312
|
+
expect(found.value.meta).toEqual(new Map([["n", 1]]))
|
|
313
|
+
}
|
|
314
|
+
const jsonFile = fs.readdirSync(dir).find((f) => f.endsWith(".json"))
|
|
315
|
+
expect(jsonFile).toBeDefined()
|
|
316
|
+
const raw = JSON.parse(fs.readFileSync(path.join(dir, jsonFile!), "utf8")) as Array<{
|
|
317
|
+
at: unknown
|
|
318
|
+
tags: unknown
|
|
319
|
+
meta: unknown
|
|
320
|
+
}>
|
|
321
|
+
expect(raw[0]?.at).toBe(at.toISOString())
|
|
322
|
+
expect(raw[0]?.tags).toEqual(["a", "b"])
|
|
323
|
+
expect(raw[0]?.meta).toEqual([["n", 1]])
|
|
324
|
+
})
|
|
325
|
+
.pipe(
|
|
326
|
+
Effect.provide(diskLive),
|
|
327
|
+
setupRequestContextFromCurrent(),
|
|
328
|
+
Effect.scoped,
|
|
329
|
+
Effect.runPromise
|
|
330
|
+
)
|
|
331
|
+
.finally(() => fs.rmSync(dir, { recursive: true, force: true }))
|
|
332
|
+
})
|
|
333
|
+
|
|
179
334
|
it("collect", () =>
|
|
180
335
|
Effect
|
|
181
336
|
.gen(function*() {
|
|
@@ -196,8 +351,8 @@ it("collect", () =>
|
|
|
196
351
|
})),
|
|
197
352
|
S.toType(S.Option(S.String)),
|
|
198
353
|
(_) =>
|
|
199
|
-
_.displayName === "Riley" && _.n === "2020-01-01T00:00:00.000Z"
|
|
200
|
-
? Option.some(`${_.displayName}-${_.n}`)
|
|
354
|
+
_.displayName === "Riley" && _.n.toISOString() === "2020-01-01T00:00:00.000Z"
|
|
355
|
+
? Option.some(`${_.displayName}-${_.n.toISOString()}`)
|
|
201
356
|
: Option.none()
|
|
202
357
|
),
|
|
203
358
|
"collect"
|
|
@@ -215,7 +370,7 @@ it("collect", () =>
|
|
|
215
370
|
QueryEnd<{
|
|
216
371
|
readonly id: string
|
|
217
372
|
readonly displayName: string
|
|
218
|
-
readonly n:
|
|
373
|
+
readonly n: Date
|
|
219
374
|
readonly union: {
|
|
220
375
|
readonly _tag: "string"
|
|
221
376
|
readonly value: string
|
|
@@ -530,7 +685,7 @@ it(
|
|
|
530
685
|
const schema = S.Struct({
|
|
531
686
|
id: S.String,
|
|
532
687
|
createdAt: S.Date.pipe(
|
|
533
|
-
S.withDecodingDefault(Effect.sync(() => new Date()
|
|
688
|
+
S.withDecodingDefault(Effect.sync(() => new Date())),
|
|
534
689
|
S.withConstructorDefault(Effect.sync(() => new Date()))
|
|
535
690
|
)
|
|
536
691
|
})
|
|
@@ -543,7 +698,7 @@ it(
|
|
|
543
698
|
const outputSchema = S.Struct({
|
|
544
699
|
id: S.Literal("123"),
|
|
545
700
|
createdAt: S.Date.pipe(
|
|
546
|
-
S.withDecodingDefault(Effect.sync(() => new Date()
|
|
701
|
+
S.withDecodingDefault(Effect.sync(() => new Date())),
|
|
547
702
|
S.withConstructorDefault(Effect.sync(() => new Date()))
|
|
548
703
|
)
|
|
549
704
|
})
|
|
@@ -824,7 +979,7 @@ it("ProjectableFromDomain distributes over tagged union Encoded", () => {
|
|
|
824
979
|
type GoodCheck = ProjectableFromDomain<Good, DomainEnc, "n">
|
|
825
980
|
type BadCheck = ProjectableFromDomain<Bad, DomainEnc>
|
|
826
981
|
|
|
827
|
-
const _good: GoodCheck = undefined
|
|
982
|
+
const _good: GoodCheck = undefined
|
|
828
983
|
// @ts-expect-error cancelled branch requires activeRequest not present on domain cancelled
|
|
829
984
|
const _bad: BadCheck = undefined as unknown
|
|
830
985
|
void _good
|
|
@@ -852,7 +1007,7 @@ it("ProjectableFromDomain allows dual same-tag domain variants", () => {
|
|
|
852
1007
|
type GoodCheck = ProjectableFromDomain<Good, DomainEnc>
|
|
853
1008
|
type BadFlatCheck = ProjectableFromDomain<BadFlat, DomainEnc>
|
|
854
1009
|
|
|
855
|
-
const _good: GoodCheck = undefined
|
|
1010
|
+
const _good: GoodCheck = undefined
|
|
856
1011
|
// @ts-expect-error packages is not on domain initial; multi-tag flat intersection rejects it
|
|
857
1012
|
const _badFlat: BadFlatCheck = undefined as unknown
|
|
858
1013
|
void _good
|
|
@@ -1345,6 +1500,49 @@ it("does not allow string queries on arrays", () =>
|
|
|
1345
1500
|
expectTypeOf(good2).toEqualTypeOf<QueryWhere<Some, Some>>()
|
|
1346
1501
|
expectTypeOf(good3).toEqualTypeOf<QueryWhere<Some, Some>>()
|
|
1347
1502
|
expectTypeOf(good4).toEqualTypeOf<QueryWhere<Some, Some>>()
|
|
1503
|
+
|
|
1504
|
+
type Native = {
|
|
1505
|
+
readonly id: string
|
|
1506
|
+
readonly dates: Date[]
|
|
1507
|
+
readonly dateSet: ReadonlySet<Date>
|
|
1508
|
+
readonly tags: ReadonlySet<string>
|
|
1509
|
+
}
|
|
1510
|
+
const native = make<Native>()
|
|
1511
|
+
const d = new Date("2020-01-01T00:00:00.000Z")
|
|
1512
|
+
const n1 = native.pipe(where("dates", "includes", d))
|
|
1513
|
+
const n2 = native.pipe(where("dateSet", "includes", d))
|
|
1514
|
+
const n3 = native.pipe(where("tags", "includes", "a"))
|
|
1515
|
+
const n4 = native.pipe(where("dates", "includes-any", [d]))
|
|
1516
|
+
const n5 = native.pipe(where("dateSet", "includes-any", new Set([d])))
|
|
1517
|
+
const n6 = native.pipe(where("id", "in", new Set(["x"])))
|
|
1518
|
+
expectTypeOf(n1).toEqualTypeOf<QueryWhere<Native, Native>>()
|
|
1519
|
+
expectTypeOf(n2).toEqualTypeOf<QueryWhere<Native, Native>>()
|
|
1520
|
+
expectTypeOf(n3).toEqualTypeOf<QueryWhere<Native, Native>>()
|
|
1521
|
+
expectTypeOf(n4).toEqualTypeOf<QueryWhere<Native, Native>>()
|
|
1522
|
+
expectTypeOf(n5).toEqualTypeOf<QueryWhere<Native, Native>>()
|
|
1523
|
+
expectTypeOf(n6).toEqualTypeOf<QueryWhere<Native, Native>>()
|
|
1524
|
+
|
|
1525
|
+
type WithMap = {
|
|
1526
|
+
readonly id: string
|
|
1527
|
+
readonly meta: ReadonlyMap<string, number>
|
|
1528
|
+
}
|
|
1529
|
+
const mapped = make<WithMap>()
|
|
1530
|
+
const m1 = mapped.pipe(where("meta", "hasKey", "n"))
|
|
1531
|
+
const m2 = mapped.pipe(where("meta", "hasValue", 1))
|
|
1532
|
+
const m3 = mapped.pipe(where("meta", "hasKeyValue", ["n", 1] as const))
|
|
1533
|
+
const m4 = mapped.pipe(where("meta", "hasKey-any", ["n", "x"]))
|
|
1534
|
+
const m5 = mapped.pipe(where("meta", "hasValue-all", new Set([1, 2])))
|
|
1535
|
+
const m6 = mapped.pipe(where("meta", "hasKeyValue-any", [["n", 1] as const, ["x", 2] as const]))
|
|
1536
|
+
expectTypeOf(m1).toEqualTypeOf<QueryWhere<WithMap, WithMap>>()
|
|
1537
|
+
expectTypeOf(m2).toEqualTypeOf<QueryWhere<WithMap, WithMap>>()
|
|
1538
|
+
expectTypeOf(m3).toEqualTypeOf<QueryWhere<WithMap, WithMap>>()
|
|
1539
|
+
expectTypeOf(m4).toEqualTypeOf<QueryWhere<WithMap, WithMap>>()
|
|
1540
|
+
expectTypeOf(m5).toEqualTypeOf<QueryWhere<WithMap, WithMap>>()
|
|
1541
|
+
expectTypeOf(m6).toEqualTypeOf<QueryWhere<WithMap, WithMap>>()
|
|
1542
|
+
// @ts-expect-error cannot hasKey on a string field
|
|
1543
|
+
mapped.pipe(where("id", "hasKey", "n"))
|
|
1544
|
+
// @ts-expect-error hasKey value must be the map key type
|
|
1545
|
+
mapped.pipe(where("meta", "hasKey", 1))
|
|
1348
1546
|
})
|
|
1349
1547
|
.pipe(Effect.provide(TestStoreLive), setupRequestContextFromCurrent(), Effect.scoped, Effect.runPromise))
|
|
1350
1548
|
|
|
@@ -2064,6 +2262,57 @@ it("codeFilter: array includes / includes-any / includes-all", () => {
|
|
|
2064
2262
|
expect(runCF(make<CFRow>().pipe(where("tags", "includes-all", ["red", "blue"])))).toEqual(["3"])
|
|
2065
2263
|
})
|
|
2066
2264
|
|
|
2265
|
+
it("codeFilter: Date array / Set includes and in", () => {
|
|
2266
|
+
const d0 = new Date("2020-01-01T00:00:00.000Z")
|
|
2267
|
+
const d1 = new Date("2021-01-01T00:00:00.000Z")
|
|
2268
|
+
type DateRow = {
|
|
2269
|
+
readonly id: string
|
|
2270
|
+
readonly dates: Date[]
|
|
2271
|
+
readonly dateSet: ReadonlySet<Date>
|
|
2272
|
+
readonly tag: string
|
|
2273
|
+
}
|
|
2274
|
+
const rows: DateRow[] = [
|
|
2275
|
+
{ id: "1", dates: [d0], dateSet: new Set([d0]), tag: "a" },
|
|
2276
|
+
{ id: "2", dates: [d1, d0], dateSet: new Set([d1]), tag: "b" }
|
|
2277
|
+
]
|
|
2278
|
+
const run = (q: any) => (memFilter(toFilter(q))(rows) as DateRow[]).map((_) => _.id)
|
|
2279
|
+
expect(run(make<DateRow>().pipe(where("dates", "includes", d0))).sort()).toEqual(["1", "2"])
|
|
2280
|
+
expect(run(make<DateRow>().pipe(where("dates", "includes", d1)))).toEqual(["2"])
|
|
2281
|
+
expect(run(make<DateRow>().pipe(where("dateSet", "includes", d0)))).toEqual(["1"])
|
|
2282
|
+
expect(run(make<DateRow>().pipe(where("dates", "includes-any", [d1])))).toEqual(["2"])
|
|
2283
|
+
expect(run(make<DateRow>().pipe(where("dateSet", "includes-any", new Set([d0, d1])))).sort()).toEqual([
|
|
2284
|
+
"1",
|
|
2285
|
+
"2"
|
|
2286
|
+
])
|
|
2287
|
+
expect(run(make<DateRow>().pipe(where("tag", "in", new Set(["a"]))))).toEqual(["1"])
|
|
2288
|
+
})
|
|
2289
|
+
|
|
2290
|
+
it("codeFilter: Map hasKey / hasValue / hasKeyValue", () => {
|
|
2291
|
+
type MapRow = {
|
|
2292
|
+
readonly id: string
|
|
2293
|
+
readonly meta: ReadonlyMap<string, number>
|
|
2294
|
+
}
|
|
2295
|
+
const rows: MapRow[] = [
|
|
2296
|
+
{ id: "1", meta: new Map([["n", 1], ["x", 2]]) },
|
|
2297
|
+
{ id: "2", meta: new Map([["n", 9]]) },
|
|
2298
|
+
{ id: "3", meta: new Map([["z", 2]]) }
|
|
2299
|
+
]
|
|
2300
|
+
const run = (q: any) => (memFilter(toFilter(q))(rows) as MapRow[]).map((_) => _.id)
|
|
2301
|
+
expect(run(make<MapRow>().pipe(where("meta", "hasKey", "n"))).sort()).toEqual(["1", "2"])
|
|
2302
|
+
expect(run(make<MapRow>().pipe(where("meta", "notHasKey", "n")))).toEqual(["3"])
|
|
2303
|
+
expect(run(make<MapRow>().pipe(where("meta", "hasValue", 2))).sort()).toEqual(["1", "3"])
|
|
2304
|
+
expect(run(make<MapRow>().pipe(where("meta", "notHasValue", 2)))).toEqual(["2"])
|
|
2305
|
+
expect(run(make<MapRow>().pipe(where("meta", "hasKeyValue", ["n", 1])))).toEqual(["1"])
|
|
2306
|
+
expect(run(make<MapRow>().pipe(where("meta", "notHasKeyValue", ["n", 1]))).sort()).toEqual(["2", "3"])
|
|
2307
|
+
expect(run(make<MapRow>().pipe(where("meta", "hasKey-any", ["z", "missing"])))).toEqual(["3"])
|
|
2308
|
+
expect(run(make<MapRow>().pipe(where("meta", "hasKey-all", ["n", "x"])))).toEqual(["1"])
|
|
2309
|
+
expect(run(make<MapRow>().pipe(where("meta", "notHasKey-all", ["n", "x"]))).sort()).toEqual(["2", "3"])
|
|
2310
|
+
expect(run(make<MapRow>().pipe(where("meta", "hasValue-any", [9, 99])))).toEqual(["2"])
|
|
2311
|
+
expect(run(make<MapRow>().pipe(where("meta", "hasValue-all", [1, 2])))).toEqual(["1"])
|
|
2312
|
+
expect(run(make<MapRow>().pipe(where("meta", "hasKeyValue-any", [["n", 9], ["missing", 0]])))).toEqual(["2"])
|
|
2313
|
+
expect(run(make<MapRow>().pipe(where("meta", "hasKeyValue-all", [["n", 1], ["x", 2]])))).toEqual(["1"])
|
|
2314
|
+
})
|
|
2315
|
+
|
|
2067
2316
|
it("codeFilter: in / notIn", () => {
|
|
2068
2317
|
expect(runCF(make<CFRow>().pipe(where("tag", "in", ["x", "z"]))).sort()).toEqual(["1", "3"])
|
|
2069
2318
|
expect(runCF(make<CFRow>().pipe(where("tag", "notIn", ["x", "z"]))).sort()).toEqual(["2", "4"])
|
|
@@ -2129,7 +2378,7 @@ it("memFilter: agg-count-when groups rows and counts conditionally", () => {
|
|
|
2129
2378
|
},
|
|
2130
2379
|
{ key: "total", aggregate: { _tag: "agg-count" } }
|
|
2131
2380
|
] as any
|
|
2132
|
-
})(rows
|
|
2381
|
+
})(rows) as any[]
|
|
2133
2382
|
|
|
2134
2383
|
expect(result.length).toBe(2)
|
|
2135
2384
|
const nyc = result.find((r: any) => r.city === "NYC")!
|
|
@@ -2155,7 +2404,7 @@ it("memFilter: agg-sum / agg-min / agg-max aggregate numerics", () => {
|
|
|
2155
2404
|
{ key: "min", aggregate: { _tag: "agg-min", field: "salary" } },
|
|
2156
2405
|
{ key: "max", aggregate: { _tag: "agg-max", field: "salary" } }
|
|
2157
2406
|
] as any
|
|
2158
|
-
})(rows
|
|
2407
|
+
})(rows) as any[]
|
|
2159
2408
|
|
|
2160
2409
|
expect(result.length).toBe(2)
|
|
2161
2410
|
const eng = result.find((r: any) => r.dept === "eng")!
|
|
@@ -2179,7 +2428,7 @@ it("memFilter: aggregate with nested path grouping", () => {
|
|
|
2179
2428
|
{ key: "city", path: "address.city" },
|
|
2180
2429
|
{ key: "count", aggregate: { _tag: "agg-count" } }
|
|
2181
2430
|
] as any
|
|
2182
|
-
})(rows
|
|
2431
|
+
})(rows) as any[]
|
|
2183
2432
|
|
|
2184
2433
|
expect(result.length).toBe(2)
|
|
2185
2434
|
expect(result.find((r: any) => r.city === "NYC")!.count).toBe(2)
|