@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/Store/Cosmos/query.d.ts +2 -1
  3. package/dist/Store/Cosmos/query.d.ts.map +1 -1
  4. package/dist/Store/Cosmos/query.js +60 -2
  5. package/dist/Store/Cosmos.d.ts.map +1 -1
  6. package/dist/Store/Cosmos.js +17 -12
  7. package/dist/Store/Disk.d.ts.map +1 -1
  8. package/dist/Store/Disk.js +12 -7
  9. package/dist/Store/Memory.d.ts +2 -1
  10. package/dist/Store/Memory.d.ts.map +1 -1
  11. package/dist/Store/Memory.js +28 -15
  12. package/dist/Store/SQL/Pg.d.ts.map +1 -1
  13. package/dist/Store/SQL/Pg.js +15 -11
  14. package/dist/Store/SQL/query.d.ts +5 -1
  15. package/dist/Store/SQL/query.d.ts.map +1 -1
  16. package/dist/Store/SQL/query.js +131 -2
  17. package/dist/Store/SQL.d.ts +1 -1
  18. package/dist/Store/SQL.d.ts.map +1 -1
  19. package/dist/Store/SQL.js +24 -18
  20. package/dist/Store/codeFilter.d.ts.map +1 -1
  21. package/dist/Store/codeFilter.js +67 -20
  22. package/dist/Store/jsonDocument.d.ts +13 -0
  23. package/dist/Store/jsonDocument.d.ts.map +1 -0
  24. package/dist/Store/jsonDocument.js +31 -0
  25. package/dist/Store/utils.d.ts +16 -0
  26. package/dist/Store/utils.d.ts.map +1 -1
  27. package/dist/Store/utils.js +245 -6
  28. package/dist/logger.d.ts +5 -5
  29. package/examples/query.ts +4 -4
  30. package/package.json +3 -3
  31. package/src/Store/Cosmos/query.ts +65 -9
  32. package/src/Store/Cosmos.ts +17 -11
  33. package/src/Store/Disk.ts +28 -7
  34. package/src/Store/Memory.ts +40 -17
  35. package/src/Store/SQL/Pg.ts +23 -10
  36. package/src/Store/SQL/query.ts +152 -7
  37. package/src/Store/SQL.ts +37 -17
  38. package/src/Store/codeFilter.ts +71 -23
  39. package/src/Store/jsonDocument.ts +43 -0
  40. package/src/Store/utils.ts +275 -5
  41. package/test/cosmos-query.test.ts +100 -5
  42. package/test/dist/json-lower.test.d.ts.map +1 -0
  43. package/test/dist/query.test.d.ts.map +1 -1
  44. package/test/json-lower.test.ts +115 -0
  45. package/test/query.test.ts +261 -12
  46. 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 as any
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[\.\["]/i)
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 as any)
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 as any)
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 as any)
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;;;;;;;;;;;;;;;;;AAgBtC,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"}
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
+ })
@@ -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") // TODO: work with To type translation, so Date?
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") // TODO: work with To type translation, so Date?
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: string
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().toISOString())),
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().toISOString())),
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 as unknown
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 as unknown
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 as any) as any[]
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 as any) as any[]
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 as any) as any[]
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)