@effect-app/infra 4.0.0-beta.225 → 4.0.0-beta.226
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 +16 -0
- package/dist/Emailer/Sendgrid.d.ts.map +1 -1
- package/dist/Emailer/Sendgrid.js +2 -2
- package/dist/Emailer/fake.js +2 -2
- package/dist/Model/query/dsl.d.ts +5 -1
- package/dist/Model/query/dsl.d.ts.map +1 -1
- package/dist/Model/query/dsl.js +5 -1
- package/dist/Model/query/new-kid-interpreter.d.ts +4 -1
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.js +5 -2
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +7 -4
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +6 -1
- package/dist/Store/SQL/query.d.ts.map +1 -1
- package/dist/Store/SQL/query.js +10 -5
- package/package.json +2 -2
- package/src/Emailer/Sendgrid.ts +45 -43
- package/src/Emailer/fake.ts +1 -1
- package/src/Model/query/dsl.ts +8 -0
- package/src/Model/query/new-kid-interpreter.ts +8 -1
- package/src/Store/Cosmos/query.ts +8 -3
- package/src/Store/Memory.ts +5 -0
- package/src/Store/SQL/query.ts +9 -4
- package/test/cosmos-query.test.ts +41 -0
- package/test/dist/cosmos-query.test.d.ts.map +1 -0
- package/test/query.test.ts +4 -4
- package/test/rawQuery.test.ts +26 -0
|
@@ -88,6 +88,10 @@ export type ComputedProjectionIrExpression =
|
|
|
88
88
|
readonly distinct: boolean
|
|
89
89
|
readonly filter: readonly FilterResult[]
|
|
90
90
|
}
|
|
91
|
+
| {
|
|
92
|
+
readonly _tag: "relation-length"
|
|
93
|
+
readonly path: string
|
|
94
|
+
}
|
|
91
95
|
|
|
92
96
|
type Result<TFieldValues extends FieldValues, A = TFieldValues, R = never> = {
|
|
93
97
|
filter: FilterResult[]
|
|
@@ -230,7 +234,8 @@ const interpret = <
|
|
|
230
234
|
? Object.fromEntries(
|
|
231
235
|
Object.entries(v.computed).map(([key, expression]) => {
|
|
232
236
|
const e = expression
|
|
233
|
-
const
|
|
237
|
+
const op = "operation" in e ? e.operation : undefined
|
|
238
|
+
const filter = op ? interpret(op(make())).filter.map(applyPath(e.path)) : []
|
|
234
239
|
switch (e._tag) {
|
|
235
240
|
case "relation-count":
|
|
236
241
|
case "relation-any":
|
|
@@ -293,6 +298,8 @@ const interpret = <
|
|
|
293
298
|
filter
|
|
294
299
|
} as ComputedProjectionIrExpression
|
|
295
300
|
]
|
|
301
|
+
case "relation-length":
|
|
302
|
+
return [key, { _tag: e._tag, path: e.path } as ComputedProjectionIrExpression]
|
|
296
303
|
}
|
|
297
304
|
})
|
|
298
305
|
)
|
|
@@ -289,7 +289,9 @@ export function buildWhereCosmosQuery3(
|
|
|
289
289
|
: [_]
|
|
290
290
|
)
|
|
291
291
|
const computedFilters = select
|
|
292
|
-
? select.flatMap((_) =>
|
|
292
|
+
? select.flatMap((_) =>
|
|
293
|
+
typeof _ === "object" && "computed" in _ && "filter" in _.computed ? getValues(_.computed.filter) : []
|
|
294
|
+
)
|
|
293
295
|
: []
|
|
294
296
|
const values = [...computedFilters, ...getValues(filter)]
|
|
295
297
|
|
|
@@ -314,8 +316,9 @@ export function buildWhereCosmosQuery3(
|
|
|
314
316
|
`IIF(${unitExpr} = ${JSON.stringify(toBase)}, 1, 0)`
|
|
315
317
|
)
|
|
316
318
|
}
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
+
const filter = "filter" in computed ? computed.filter : []
|
|
320
|
+
const where = filter.length > 0
|
|
321
|
+
? ` WHERE ${print(filter, relationPath, false)}`
|
|
319
322
|
: ""
|
|
320
323
|
switch (computed._tag) {
|
|
321
324
|
case "relation-count":
|
|
@@ -360,6 +363,8 @@ export function buildWhereCosmosQuery3(
|
|
|
360
363
|
}
|
|
361
364
|
return `ARRAY(SELECT VALUE ${fieldRef} FROM ${relationAlias} IN ${relationSource}${where}) AS ${key}`
|
|
362
365
|
}
|
|
366
|
+
case "relation-length":
|
|
367
|
+
return `ARRAY_LENGTH(${relationSource}) AS ${key}`
|
|
363
368
|
case "relation-collect-fields": {
|
|
364
369
|
const subqueries = computed.fields.map((field) => {
|
|
365
370
|
const fieldRef = dottedToAccess(`${relationAlias}.${field}`)
|
package/src/Store/Memory.ts
CHANGED
|
@@ -60,6 +60,8 @@ const emptyValueFor = (tag: ComputedProjectionIrExpression["_tag"]) => {
|
|
|
60
60
|
return [] as unknown[]
|
|
61
61
|
case "relation-collect-fields":
|
|
62
62
|
return [] as unknown[]
|
|
63
|
+
case "relation-length":
|
|
64
|
+
return 0
|
|
63
65
|
default:
|
|
64
66
|
return assertUnreachable(tag)
|
|
65
67
|
}
|
|
@@ -73,6 +75,9 @@ const computeProjectionValue = (
|
|
|
73
75
|
if (!Array.isArray(relation)) {
|
|
74
76
|
return emptyValueFor(computed._tag)
|
|
75
77
|
}
|
|
78
|
+
if (computed._tag === "relation-length") {
|
|
79
|
+
return relation.length
|
|
80
|
+
}
|
|
76
81
|
const filter = stripRelationFilterPaths(computed.filter, computed.path)
|
|
77
82
|
// empty filter = unconditional match (codeFilter3_ uses eval on a built
|
|
78
83
|
// string and chokes on `( )`, so short-circuit before invoking it).
|
package/src/Store/SQL/query.ts
CHANGED
|
@@ -412,9 +412,10 @@ export function buildWhereSQLQuery(
|
|
|
412
412
|
const cases = entries.map(([unit, factor]) => ` WHEN ${sqlStringLiteral(unit)} THEN ${factor}`).join("")
|
|
413
413
|
return `CASE ${unitExpr} WHEN ${sqlStringLiteral(toBase)} THEN 1${cases} ELSE NULL END`
|
|
414
414
|
}
|
|
415
|
+
const filter = "filter" in computed ? computed.filter : []
|
|
415
416
|
const whereClause = () =>
|
|
416
|
-
|
|
417
|
-
? ` WHERE ${print(
|
|
417
|
+
filter.length > 0
|
|
418
|
+
? ` WHERE ${print(filter, computed.path, false)}`
|
|
418
419
|
: ""
|
|
419
420
|
const boolExpr = (sqlExpr: string) =>
|
|
420
421
|
dialect.jsonColumnType === "JSON"
|
|
@@ -428,9 +429,9 @@ export function buildWhereSQLQuery(
|
|
|
428
429
|
case "relation-every":
|
|
429
430
|
// ∀x.P(x) ≡ ¬∃x.¬P(x). When no filter, no element exists that violates ⊤ → true.
|
|
430
431
|
return boolExpr(
|
|
431
|
-
|
|
432
|
+
filter.length === 0
|
|
432
433
|
? `1=1`
|
|
433
|
-
: `NOT EXISTS(SELECT 1 FROM ${relationFrom} WHERE NOT (${print(
|
|
434
|
+
: `NOT EXISTS(SELECT 1 FROM ${relationFrom} WHERE NOT (${print(filter, computed.path, false)}))`
|
|
434
435
|
)
|
|
435
436
|
case "relation-distinct-count": {
|
|
436
437
|
const fieldExtract = dialect.jsonExtractElement(relationAlias, computed.field)
|
|
@@ -470,6 +471,10 @@ export function buildWhereSQLQuery(
|
|
|
470
471
|
const aggArg = computed.distinct ? `DISTINCT ${fieldExtract}` : fieldExtract
|
|
471
472
|
return `(SELECT COALESCE(jsonb_agg(${aggArg}), '[]'::jsonb) FROM ${relationFrom}${whereClause()}) AS "${key}"`
|
|
472
473
|
}
|
|
474
|
+
case "relation-length": {
|
|
475
|
+
const arrPath = dottedToJsonPath(computed.path)
|
|
476
|
+
return `${dialect.arrayLength(arrPath)} AS "${key}"`
|
|
477
|
+
}
|
|
473
478
|
case "relation-collect-fields": {
|
|
474
479
|
const branches = computed.fields.map((field) => {
|
|
475
480
|
const fieldExtract = dialect.jsonExtractElement(relationAlias, field)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import * as S from "effect-app/Schema"
|
|
3
|
+
import { describe, expect, it } from "vitest"
|
|
4
|
+
import { computed, make, projectComputed, relation, toFilter } from "../src/Model/query.js"
|
|
5
|
+
import { buildWhereCosmosQuery3 } from "../src/Store/Cosmos/query.js"
|
|
6
|
+
|
|
7
|
+
class Order extends S.Class<Order>("Order")({
|
|
8
|
+
id: S.String,
|
|
9
|
+
packages: S.Array(S.Struct({ id: S.String, weight: S.Finite }))
|
|
10
|
+
}) {}
|
|
11
|
+
|
|
12
|
+
type OrderEnc = S.Codec.Encoded<typeof Order>
|
|
13
|
+
|
|
14
|
+
// Length projection via `relation(...).length()` should emit a scalar
|
|
15
|
+
// ARRAY_LENGTH expression rather than pulling (or reshaping) the array.
|
|
16
|
+
describe("cosmos query projection: array length", () => {
|
|
17
|
+
it("projects packages length via ARRAY_LENGTH", () => {
|
|
18
|
+
const q = make<OrderEnc>().pipe(
|
|
19
|
+
projectComputed(
|
|
20
|
+
S.Struct({ id: S.String, packageCount: S.NonNegativeInt }),
|
|
21
|
+
computed({ packageCount: relation<OrderEnc>("packages").length() })
|
|
22
|
+
)
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
const ir = toFilter(q as any, Order as any)
|
|
26
|
+
|
|
27
|
+
const result = buildWhereCosmosQuery3(
|
|
28
|
+
"id",
|
|
29
|
+
ir.filter ?? [],
|
|
30
|
+
"Orders",
|
|
31
|
+
{},
|
|
32
|
+
ir.select as any
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(result.query).toMatch(/ARRAY_LENGTH\(f(?:\.packages|\["packages"\])\)/)
|
|
36
|
+
expect(result.query).toContain("AS packageCount")
|
|
37
|
+
// 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)
|
|
39
|
+
expect(result.query).not.toMatch(/SELECT VALUE COUNT/)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cosmos-query.test.d.ts","sourceRoot":"","sources":["../cosmos-query.test.ts"],"names":[],"mappings":""}
|
package/test/query.test.ts
CHANGED
|
@@ -614,7 +614,7 @@ it("projectComputed sets computed IR and forces project mode", () => {
|
|
|
614
614
|
])
|
|
615
615
|
expect(interpreted.computed?.["pickedCount"]?._tag).toBe("relation-count")
|
|
616
616
|
expect(interpreted.computed?.["pickedCount"]?.path).toBe("items")
|
|
617
|
-
expect(interpreted.computed?.["pickedCount"]?.filter).toEqual([
|
|
617
|
+
expect((interpreted.computed?.["pickedCount"] as { filter: unknown } | undefined)?.filter).toEqual([
|
|
618
618
|
{ t: "where", path: "items.-1.state._tag", op: "eq", value: "Picked" }
|
|
619
619
|
])
|
|
620
620
|
})
|
|
@@ -662,7 +662,7 @@ it("projectComputed.every emits relation-every IR", () => {
|
|
|
662
662
|
const interpreted = toFilter(query, baseSchema)
|
|
663
663
|
expect(interpreted.computed?.["allPicked"]?._tag).toBe("relation-every")
|
|
664
664
|
expect(interpreted.computed?.["allPicked"]?.path).toBe("items")
|
|
665
|
-
expect(interpreted.computed?.["allPicked"]?.filter).toEqual([
|
|
665
|
+
expect((interpreted.computed?.["allPicked"] as { filter: unknown } | undefined)?.filter).toEqual([
|
|
666
666
|
{ t: "where", path: "items.-1.state._tag", op: "eq", value: "Picked" }
|
|
667
667
|
])
|
|
668
668
|
})
|
|
@@ -687,7 +687,7 @@ it("projectComputed.distinctCount emits relation-distinct-count IR with field",
|
|
|
687
687
|
const ir = interpreted.computed?.["positionCount"]
|
|
688
688
|
expect(ir?._tag).toBe("relation-distinct-count")
|
|
689
689
|
expect((ir as { field: string } | undefined)?.field).toBe("rowId")
|
|
690
|
-
expect(ir?.filter).toEqual([
|
|
690
|
+
expect((ir as { filter: unknown } | undefined)?.filter).toEqual([
|
|
691
691
|
{ t: "where", path: "items.-1.state._tag", op: "neq", value: "cancelled" }
|
|
692
692
|
])
|
|
693
693
|
})
|
|
@@ -707,7 +707,7 @@ it("projectComputed.sum emits relation-sum IR with field", () => {
|
|
|
707
707
|
const ir = interpreted.computed?.["totalWeight"]
|
|
708
708
|
expect(ir?._tag).toBe("relation-sum")
|
|
709
709
|
expect((ir as { field: string } | undefined)?.field).toBe("weight")
|
|
710
|
-
expect(ir?.filter).toEqual([])
|
|
710
|
+
expect((ir as { filter: unknown } | undefined)?.filter).toEqual([])
|
|
711
711
|
})
|
|
712
712
|
|
|
713
713
|
it("projectComputed.collect / collectDistinct emit relation-collect IR", () => {
|
package/test/rawQuery.test.ts
CHANGED
|
@@ -369,6 +369,32 @@ describe("multi-level", () => {
|
|
|
369
369
|
.pipe(Effect.provide(SomethingRepo.Test), rt.runPromise))
|
|
370
370
|
})
|
|
371
371
|
|
|
372
|
+
describe("array length projection", () => {
|
|
373
|
+
const test = Effect
|
|
374
|
+
.gen(function*() {
|
|
375
|
+
const repo = yield* SomethingRepo
|
|
376
|
+
const result = yield* repo.query(
|
|
377
|
+
projectComputed(
|
|
378
|
+
S.Struct({ id: S.String, itemCount: S.NonNegativeInt }),
|
|
379
|
+
computed({ itemCount: relation<S.Codec.Encoded<typeof Something>>("items").length() })
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
expect(result).toStrictEqual([
|
|
383
|
+
{ id: "1", itemCount: 2 },
|
|
384
|
+
{ id: "2", itemCount: 2 }
|
|
385
|
+
])
|
|
386
|
+
})
|
|
387
|
+
.pipe(setupRequestContextFromCurrent())
|
|
388
|
+
|
|
389
|
+
it.skipIf(!process.env["STORAGE_URL"])("works well in CosmosDB", () =>
|
|
390
|
+
test
|
|
391
|
+
.pipe(Effect.provide(SomethingRepo.TestCosmos), rt.runPromise))
|
|
392
|
+
|
|
393
|
+
it("works well in Memory", () =>
|
|
394
|
+
test
|
|
395
|
+
.pipe(Effect.provide(SomethingRepo.Test), rt.runPromise))
|
|
396
|
+
})
|
|
397
|
+
|
|
372
398
|
describe("computed projections", () => {
|
|
373
399
|
const test = Effect
|
|
374
400
|
.gen(function*() {
|