@effect-app/infra 4.0.0-beta.213 → 4.0.0-beta.215
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/Model/query/dsl.d.ts +26 -1
- package/dist/Model/query/dsl.d.ts.map +1 -1
- package/dist/Model/query/dsl.js +58 -1
- package/dist/Model/query/new-kid-interpreter.d.ts +21 -1
- package/dist/Model/query/new-kid-interpreter.d.ts.map +1 -1
- package/dist/Model/query/new-kid-interpreter.js +24 -2
- package/dist/Store/Cosmos/query.d.ts.map +1 -1
- package/dist/Store/Cosmos/query.js +23 -1
- package/dist/Store/Memory.d.ts.map +1 -1
- package/dist/Store/Memory.js +61 -4
- package/dist/Store/SQL/query.d.ts.map +1 -1
- package/dist/Store/SQL/query.js +33 -7
- package/package.json +2 -2
- package/src/Model/query/dsl.ts +85 -0
- package/src/Model/query/new-kid-interpreter.ts +47 -1
- package/src/Store/Cosmos/query.ts +23 -0
- package/src/Store/Memory.ts +60 -13
- package/src/Store/SQL/query.ts +38 -8
- package/test/dist/rawQuery.test.d.ts.map +1 -1
- package/test/query.test.ts +423 -0
- package/test/rawQuery.test.ts +223 -1
- package/test/sql-store.test.ts +301 -0
package/test/query.test.ts
CHANGED
|
@@ -640,6 +640,96 @@ it("projection schema with computed fields fails without computed map", () => {
|
|
|
640
640
|
expect(() => toFilter(query, baseSchema)).toThrowError("Missing computed projections for schema keys")
|
|
641
641
|
})
|
|
642
642
|
|
|
643
|
+
it("projectComputed.every emits relation-every IR", () => {
|
|
644
|
+
const baseSchema = S.Struct({
|
|
645
|
+
id: S.String,
|
|
646
|
+
items: S.Array(S.Struct({ state: S.Struct({ _tag: S.String }) }))
|
|
647
|
+
})
|
|
648
|
+
const query = make<S.Codec.Encoded<typeof baseSchema>>().pipe(
|
|
649
|
+
projectComputed(
|
|
650
|
+
S.Struct({ allPicked: S.Boolean }),
|
|
651
|
+
computed({
|
|
652
|
+
allPicked: relation<S.Codec.Encoded<typeof baseSchema>>("items").every(where("state._tag", "Picked"))
|
|
653
|
+
})
|
|
654
|
+
)
|
|
655
|
+
)
|
|
656
|
+
const interpreted = toFilter(query, baseSchema)
|
|
657
|
+
expect(interpreted.computed?.["allPicked"]?._tag).toBe("relation-every")
|
|
658
|
+
expect(interpreted.computed?.["allPicked"]?.path).toBe("items")
|
|
659
|
+
expect(interpreted.computed?.["allPicked"]?.filter).toEqual([
|
|
660
|
+
{ t: "where", path: "items.-1.state._tag", op: "eq", value: "Picked" }
|
|
661
|
+
])
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it("projectComputed.distinctCount emits relation-distinct-count IR with field", () => {
|
|
665
|
+
const baseSchema = S.Struct({
|
|
666
|
+
id: S.String,
|
|
667
|
+
items: S.Array(S.Struct({ rowId: S.String, state: S.Struct({ _tag: S.String }) }))
|
|
668
|
+
})
|
|
669
|
+
const query = make<S.Codec.Encoded<typeof baseSchema>>().pipe(
|
|
670
|
+
projectComputed(
|
|
671
|
+
S.Struct({ positionCount: S.NonNegativeInt }),
|
|
672
|
+
computed({
|
|
673
|
+
positionCount: relation<S.Codec.Encoded<typeof baseSchema>>("items").distinctCount(
|
|
674
|
+
"rowId",
|
|
675
|
+
where("state._tag", "neq", "cancelled")
|
|
676
|
+
)
|
|
677
|
+
})
|
|
678
|
+
)
|
|
679
|
+
)
|
|
680
|
+
const interpreted = toFilter(query, baseSchema)
|
|
681
|
+
const ir = interpreted.computed?.["positionCount"]
|
|
682
|
+
expect(ir?._tag).toBe("relation-distinct-count")
|
|
683
|
+
expect((ir as { field: string } | undefined)?.field).toBe("rowId")
|
|
684
|
+
expect(ir?.filter).toEqual([
|
|
685
|
+
{ t: "where", path: "items.-1.state._tag", op: "neq", value: "cancelled" }
|
|
686
|
+
])
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
it("projectComputed.sum emits relation-sum IR with field", () => {
|
|
690
|
+
const baseSchema = S.Struct({
|
|
691
|
+
id: S.String,
|
|
692
|
+
items: S.Array(S.Struct({ weight: S.Number }))
|
|
693
|
+
})
|
|
694
|
+
const query = make<S.Codec.Encoded<typeof baseSchema>>().pipe(
|
|
695
|
+
projectComputed(
|
|
696
|
+
S.Struct({ totalWeight: S.Number }),
|
|
697
|
+
computed({ totalWeight: relation<S.Codec.Encoded<typeof baseSchema>>("items").sum("weight") })
|
|
698
|
+
)
|
|
699
|
+
)
|
|
700
|
+
const interpreted = toFilter(query, baseSchema)
|
|
701
|
+
const ir = interpreted.computed?.["totalWeight"]
|
|
702
|
+
expect(ir?._tag).toBe("relation-sum")
|
|
703
|
+
expect((ir as { field: string } | undefined)?.field).toBe("weight")
|
|
704
|
+
expect(ir?.filter).toEqual([])
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
it("projectComputed.collect / collectDistinct emit relation-collect IR", () => {
|
|
708
|
+
const baseSchema = S.Struct({
|
|
709
|
+
id: S.String,
|
|
710
|
+
items: S.Array(S.Struct({ articleId: S.String }))
|
|
711
|
+
})
|
|
712
|
+
const query = make<S.Codec.Encoded<typeof baseSchema>>().pipe(
|
|
713
|
+
projectComputed(
|
|
714
|
+
S.Struct({
|
|
715
|
+
all: S.Array(S.String),
|
|
716
|
+
distinct: S.Array(S.String)
|
|
717
|
+
}),
|
|
718
|
+
computed({
|
|
719
|
+
all: relation<S.Codec.Encoded<typeof baseSchema>>("items").collect("articleId"),
|
|
720
|
+
distinct: relation<S.Codec.Encoded<typeof baseSchema>>("items").collectDistinct("articleId")
|
|
721
|
+
})
|
|
722
|
+
)
|
|
723
|
+
)
|
|
724
|
+
const interpreted = toFilter(query, baseSchema)
|
|
725
|
+
const all = interpreted.computed?.["all"]
|
|
726
|
+
const distinct = interpreted.computed?.["distinct"]
|
|
727
|
+
expect(all?._tag).toBe("relation-collect")
|
|
728
|
+
expect((all as { distinct: boolean } | undefined)?.distinct).toBe(false)
|
|
729
|
+
expect(distinct?._tag).toBe("relation-collect")
|
|
730
|
+
expect((distinct as { distinct: boolean } | undefined)?.distinct).toBe(true)
|
|
731
|
+
})
|
|
732
|
+
|
|
643
733
|
it(
|
|
644
734
|
"doesn't mess when refining fields",
|
|
645
735
|
() =>
|
|
@@ -1343,3 +1433,336 @@ it("refine union with nested union", () =>
|
|
|
1343
1433
|
>()
|
|
1344
1434
|
})
|
|
1345
1435
|
.pipe(Effect.provide(TestStoreLive), setupRequestContextFromCurrent(), Effect.runPromise))
|
|
1436
|
+
|
|
1437
|
+
// ---------------------------------------------------------------------------
|
|
1438
|
+
// memFilter: computed projection execution (in-memory) and code filter coverage
|
|
1439
|
+
// ---------------------------------------------------------------------------
|
|
1440
|
+
|
|
1441
|
+
const computedBaseSchema = S.Struct({
|
|
1442
|
+
id: S.String,
|
|
1443
|
+
status: S.Literals(["active", "archived"]),
|
|
1444
|
+
items: S.Array(S.Struct({
|
|
1445
|
+
id: S.String,
|
|
1446
|
+
tag: S.Literals(["a", "b", "c"]),
|
|
1447
|
+
qty: S.Finite,
|
|
1448
|
+
note: S.String
|
|
1449
|
+
}))
|
|
1450
|
+
})
|
|
1451
|
+
type ComputedBase = S.Codec.Encoded<typeof computedBaseSchema>
|
|
1452
|
+
|
|
1453
|
+
const computedRows: ComputedBase[] = [
|
|
1454
|
+
{
|
|
1455
|
+
id: "r1",
|
|
1456
|
+
status: "active",
|
|
1457
|
+
items: [
|
|
1458
|
+
{ id: "i1", tag: "a", qty: 10, note: "alpha" },
|
|
1459
|
+
{ id: "i2", tag: "a", qty: 20, note: "alpha" },
|
|
1460
|
+
{ id: "i3", tag: "b", qty: 5, note: "beta" }
|
|
1461
|
+
]
|
|
1462
|
+
},
|
|
1463
|
+
{ id: "r2", status: "active", items: [] },
|
|
1464
|
+
{
|
|
1465
|
+
id: "r3",
|
|
1466
|
+
status: "archived",
|
|
1467
|
+
items: [
|
|
1468
|
+
{ id: "i4", tag: "b", qty: 7, note: "gamma" },
|
|
1469
|
+
{ id: "i5", tag: "b", qty: 7, note: "gamma" },
|
|
1470
|
+
{ id: "i6", tag: "c", qty: 3, note: "delta" }
|
|
1471
|
+
]
|
|
1472
|
+
}
|
|
1473
|
+
]
|
|
1474
|
+
|
|
1475
|
+
it("memFilter: relation-count with filter", () => {
|
|
1476
|
+
const q = make<ComputedBase>().pipe(
|
|
1477
|
+
projectComputed(
|
|
1478
|
+
S.Struct({ id: S.String, aCount: S.NonNegativeInt }),
|
|
1479
|
+
computed({
|
|
1480
|
+
aCount: relation<ComputedBase>("items").count(where("tag", "a"))
|
|
1481
|
+
})
|
|
1482
|
+
)
|
|
1483
|
+
)
|
|
1484
|
+
expect(memFilter(toFilter(q, computedBaseSchema))(computedRows)).toEqual([
|
|
1485
|
+
{ id: "r1", aCount: 2 },
|
|
1486
|
+
{ id: "r2", aCount: 0 },
|
|
1487
|
+
{ id: "r3", aCount: 0 }
|
|
1488
|
+
])
|
|
1489
|
+
})
|
|
1490
|
+
|
|
1491
|
+
it("memFilter: relation-any / every with filter", () => {
|
|
1492
|
+
const q = make<ComputedBase>().pipe(
|
|
1493
|
+
projectComputed(
|
|
1494
|
+
S.Struct({
|
|
1495
|
+
id: S.String,
|
|
1496
|
+
hasA: S.Boolean,
|
|
1497
|
+
allB: S.Boolean
|
|
1498
|
+
}),
|
|
1499
|
+
computed({
|
|
1500
|
+
hasA: relation<ComputedBase>("items").any(where("tag", "a")),
|
|
1501
|
+
allB: relation<ComputedBase>("items").every(where("tag", "b"))
|
|
1502
|
+
})
|
|
1503
|
+
)
|
|
1504
|
+
)
|
|
1505
|
+
expect(memFilter(toFilter(q, computedBaseSchema))(computedRows)).toEqual([
|
|
1506
|
+
{ id: "r1", hasA: true, allB: false },
|
|
1507
|
+
// empty array: any → false, every → true (JS Array.every on [] is true)
|
|
1508
|
+
{ id: "r2", hasA: false, allB: true },
|
|
1509
|
+
{ id: "r3", hasA: false, allB: false }
|
|
1510
|
+
])
|
|
1511
|
+
})
|
|
1512
|
+
|
|
1513
|
+
it("memFilter: relation-distinct-count with filter", () => {
|
|
1514
|
+
const q = make<ComputedBase>().pipe(
|
|
1515
|
+
projectComputed(
|
|
1516
|
+
S.Struct({ id: S.String, distinctNotes: S.NonNegativeInt }),
|
|
1517
|
+
computed({
|
|
1518
|
+
distinctNotes: relation<ComputedBase>("items").distinctCount("note", where("tag", "neq", "c"))
|
|
1519
|
+
})
|
|
1520
|
+
)
|
|
1521
|
+
)
|
|
1522
|
+
expect(memFilter(toFilter(q, computedBaseSchema))(computedRows)).toEqual([
|
|
1523
|
+
{ id: "r1", distinctNotes: 2 }, // alpha, beta
|
|
1524
|
+
{ id: "r2", distinctNotes: 0 },
|
|
1525
|
+
{ id: "r3", distinctNotes: 1 } // gamma (delta filtered out)
|
|
1526
|
+
])
|
|
1527
|
+
})
|
|
1528
|
+
|
|
1529
|
+
it("memFilter: relation-sum with filter", () => {
|
|
1530
|
+
const q = make<ComputedBase>().pipe(
|
|
1531
|
+
projectComputed(
|
|
1532
|
+
S.Struct({ id: S.String, totalQty: S.Finite }),
|
|
1533
|
+
computed({
|
|
1534
|
+
totalQty: relation<ComputedBase>("items").sum("qty", where("tag", "neq", "c"))
|
|
1535
|
+
})
|
|
1536
|
+
)
|
|
1537
|
+
)
|
|
1538
|
+
expect(memFilter(toFilter(q, computedBaseSchema))(computedRows)).toEqual([
|
|
1539
|
+
{ id: "r1", totalQty: 35 },
|
|
1540
|
+
{ id: "r2", totalQty: 0 },
|
|
1541
|
+
{ id: "r3", totalQty: 14 }
|
|
1542
|
+
])
|
|
1543
|
+
})
|
|
1544
|
+
|
|
1545
|
+
it("memFilter: relation-collect / collectDistinct with filter", () => {
|
|
1546
|
+
const q = make<ComputedBase>().pipe(
|
|
1547
|
+
projectComputed(
|
|
1548
|
+
S.Struct({
|
|
1549
|
+
id: S.String,
|
|
1550
|
+
notes: S.Array(S.String),
|
|
1551
|
+
distinctNotes: S.Array(S.String)
|
|
1552
|
+
}),
|
|
1553
|
+
computed({
|
|
1554
|
+
notes: relation<ComputedBase>("items").collect("note", where("tag", "neq", "c")),
|
|
1555
|
+
distinctNotes: relation<ComputedBase>("items").collectDistinct("note", where("tag", "neq", "c"))
|
|
1556
|
+
})
|
|
1557
|
+
)
|
|
1558
|
+
)
|
|
1559
|
+
expect(memFilter(toFilter(q, computedBaseSchema))(computedRows)).toEqual([
|
|
1560
|
+
{ id: "r1", notes: ["alpha", "alpha", "beta"], distinctNotes: ["alpha", "beta"] },
|
|
1561
|
+
{ id: "r2", notes: [], distinctNotes: [] },
|
|
1562
|
+
{ id: "r3", notes: ["gamma", "gamma"], distinctNotes: ["gamma"] }
|
|
1563
|
+
])
|
|
1564
|
+
})
|
|
1565
|
+
|
|
1566
|
+
it("memFilter: computed projection with multi-statement relation filter", () => {
|
|
1567
|
+
const q = make<ComputedBase>().pipe(
|
|
1568
|
+
projectComputed(
|
|
1569
|
+
S.Struct({ id: S.String, hits: S.NonNegativeInt }),
|
|
1570
|
+
computed({
|
|
1571
|
+
hits: relation<ComputedBase>("items").count(
|
|
1572
|
+
flow(
|
|
1573
|
+
where("tag", "a"),
|
|
1574
|
+
and("qty", "gt", 10)
|
|
1575
|
+
)
|
|
1576
|
+
)
|
|
1577
|
+
})
|
|
1578
|
+
)
|
|
1579
|
+
)
|
|
1580
|
+
expect(memFilter(toFilter(q, computedBaseSchema))(computedRows)).toEqual([
|
|
1581
|
+
{ id: "r1", hits: 1 }, // only i2 (a, qty 20)
|
|
1582
|
+
{ id: "r2", hits: 0 },
|
|
1583
|
+
{ id: "r3", hits: 0 }
|
|
1584
|
+
])
|
|
1585
|
+
})
|
|
1586
|
+
|
|
1587
|
+
it("memFilter: computed projection combined with root where filter", () => {
|
|
1588
|
+
const q = make<ComputedBase>().pipe(
|
|
1589
|
+
where("id", "neq", "r3"),
|
|
1590
|
+
projectComputed(
|
|
1591
|
+
S.Struct({ id: S.String, totalQty: S.Finite }),
|
|
1592
|
+
computed({
|
|
1593
|
+
totalQty: relation<ComputedBase>("items").sum("qty", where("tag", "neq", "c"))
|
|
1594
|
+
})
|
|
1595
|
+
)
|
|
1596
|
+
)
|
|
1597
|
+
expect(memFilter(toFilter(q, computedBaseSchema))(computedRows)).toEqual([
|
|
1598
|
+
{ id: "r1", totalQty: 35 },
|
|
1599
|
+
{ id: "r2", totalQty: 0 }
|
|
1600
|
+
])
|
|
1601
|
+
})
|
|
1602
|
+
|
|
1603
|
+
it("memFilter: computed projection with order/limit/skip applied to base rows", () => {
|
|
1604
|
+
const q = make<ComputedBase>().pipe(
|
|
1605
|
+
order("id", "DESC"),
|
|
1606
|
+
page({ skip: 1, take: 1 }),
|
|
1607
|
+
projectComputed(
|
|
1608
|
+
S.Struct({ id: S.String, total: S.NonNegativeInt }),
|
|
1609
|
+
computed({
|
|
1610
|
+
total: relation<ComputedBase>("items").count(where("qty", "gte", 0))
|
|
1611
|
+
})
|
|
1612
|
+
)
|
|
1613
|
+
)
|
|
1614
|
+
expect(memFilter(toFilter(q, computedBaseSchema))(computedRows)).toEqual([
|
|
1615
|
+
{ id: "r2", total: 0 }
|
|
1616
|
+
])
|
|
1617
|
+
})
|
|
1618
|
+
|
|
1619
|
+
it("memFilter: computed projection - relation missing on row returns empty value", () => {
|
|
1620
|
+
const partial: ComputedBase[] = [
|
|
1621
|
+
{ id: "x1" } as ComputedBase,
|
|
1622
|
+
{ id: "x2", status: "active", items: undefined as unknown as ComputedBase["items"] }
|
|
1623
|
+
]
|
|
1624
|
+
const q = make<ComputedBase>().pipe(
|
|
1625
|
+
projectComputed(
|
|
1626
|
+
S.Struct({
|
|
1627
|
+
id: S.String,
|
|
1628
|
+
c: S.NonNegativeInt,
|
|
1629
|
+
s: S.Finite,
|
|
1630
|
+
any_: S.Boolean,
|
|
1631
|
+
every_: S.Boolean,
|
|
1632
|
+
coll: S.Array(S.String)
|
|
1633
|
+
}),
|
|
1634
|
+
computed({
|
|
1635
|
+
c: relation<ComputedBase>("items").count(where("tag", "a")),
|
|
1636
|
+
s: relation<ComputedBase>("items").sum("qty", where("tag", "a")),
|
|
1637
|
+
any_: relation<ComputedBase>("items").any(where("tag", "a")),
|
|
1638
|
+
every_: relation<ComputedBase>("items").every(where("tag", "a")),
|
|
1639
|
+
coll: relation<ComputedBase>("items").collect("note", where("tag", "a"))
|
|
1640
|
+
})
|
|
1641
|
+
)
|
|
1642
|
+
)
|
|
1643
|
+
expect(memFilter(toFilter(q, computedBaseSchema))(partial)).toEqual([
|
|
1644
|
+
{ id: "x1", c: 0, s: 0, any_: false, every_: true, coll: [] },
|
|
1645
|
+
{ id: "x2", c: 0, s: 0, any_: false, every_: true, coll: [] }
|
|
1646
|
+
])
|
|
1647
|
+
})
|
|
1648
|
+
|
|
1649
|
+
it("memFilter: rejects extra computed keys not in projection schema", () => {
|
|
1650
|
+
const q = make<ComputedBase>().pipe(
|
|
1651
|
+
projectComputed(
|
|
1652
|
+
S.Struct({ id: S.String }),
|
|
1653
|
+
computed({
|
|
1654
|
+
bogus: relation<ComputedBase>("items").count(where("tag", "a"))
|
|
1655
|
+
})
|
|
1656
|
+
)
|
|
1657
|
+
)
|
|
1658
|
+
expect(() => toFilter(q, computedBaseSchema)).toThrowError(
|
|
1659
|
+
"Computed projection keys must exist in projection schema"
|
|
1660
|
+
)
|
|
1661
|
+
})
|
|
1662
|
+
|
|
1663
|
+
// ---------------------------------------------------------------------------
|
|
1664
|
+
// memFilter: code filter (where/and/or/scopes) execution coverage
|
|
1665
|
+
// ---------------------------------------------------------------------------
|
|
1666
|
+
|
|
1667
|
+
type CFRow = {
|
|
1668
|
+
readonly id: string
|
|
1669
|
+
readonly tag: "x" | "y" | "z"
|
|
1670
|
+
readonly qty: number
|
|
1671
|
+
readonly desc: string
|
|
1672
|
+
readonly tags: ReadonlyArray<string>
|
|
1673
|
+
readonly nested: { readonly kind: "k1" | "k2"; readonly v: number }
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
const cfRows: CFRow[] = [
|
|
1677
|
+
{ id: "1", tag: "x", qty: 10, desc: "Hello World", tags: ["red", "green"], nested: { kind: "k1", v: 1 } },
|
|
1678
|
+
{ id: "2", tag: "y", qty: 20, desc: "Goodbye", tags: ["blue"], nested: { kind: "k2", v: 5 } },
|
|
1679
|
+
{ id: "3", tag: "z", qty: 0, desc: "Hello again", tags: ["red", "blue", "green"], nested: { kind: "k1", v: 9 } },
|
|
1680
|
+
{ id: "4", tag: "y", qty: 30, desc: "World cup", tags: [], nested: { kind: "k2", v: 0 } }
|
|
1681
|
+
]
|
|
1682
|
+
|
|
1683
|
+
const runCF = (q: any) =>
|
|
1684
|
+
(memFilter(toFilter(q))(cfRows) as unknown as readonly CFRow[]).map((_) => _.id)
|
|
1685
|
+
|
|
1686
|
+
it("codeFilter: where + and chain", () => {
|
|
1687
|
+
const q = make<CFRow>().pipe(
|
|
1688
|
+
where("tag", "y"),
|
|
1689
|
+
and("qty", "gt", 25)
|
|
1690
|
+
)
|
|
1691
|
+
expect(runCF(q)).toEqual(["4"])
|
|
1692
|
+
})
|
|
1693
|
+
|
|
1694
|
+
it("codeFilter: where + or chain", () => {
|
|
1695
|
+
const q = make<CFRow>().pipe(
|
|
1696
|
+
where("tag", "x"),
|
|
1697
|
+
or("tag", "z")
|
|
1698
|
+
)
|
|
1699
|
+
expect(runCF(q).sort()).toEqual(["1", "3"])
|
|
1700
|
+
})
|
|
1701
|
+
|
|
1702
|
+
it("codeFilter: nested scope precedence (a AND (b OR c))", () => {
|
|
1703
|
+
const q = make<CFRow>().pipe(
|
|
1704
|
+
where("tag", "y"),
|
|
1705
|
+
and(
|
|
1706
|
+
where("qty", "gt", 25),
|
|
1707
|
+
or("desc", "contains", "good")
|
|
1708
|
+
)
|
|
1709
|
+
)
|
|
1710
|
+
// tag=y AND (qty>25 OR desc contains "good") → row 2 (Goodbye) and row 4 (qty 30)
|
|
1711
|
+
expect(runCF(q).sort()).toEqual(["2", "4"])
|
|
1712
|
+
})
|
|
1713
|
+
|
|
1714
|
+
it("codeFilter: contains/startsWith/endsWith are case-insensitive", () => {
|
|
1715
|
+
expect(runCF(make<CFRow>().pipe(where("desc", "contains", "WORLD"))).sort()).toEqual(["1", "4"])
|
|
1716
|
+
expect(runCF(make<CFRow>().pipe(where("desc", "startsWith", "hello"))).sort()).toEqual(["1", "3"])
|
|
1717
|
+
expect(runCF(make<CFRow>().pipe(where("desc", "endsWith", "AGAIN")))).toEqual(["3"])
|
|
1718
|
+
})
|
|
1719
|
+
|
|
1720
|
+
it("codeFilter: array includes / includes-any / includes-all", () => {
|
|
1721
|
+
expect(runCF(make<CFRow>().pipe(where("tags", "includes", "red"))).sort()).toEqual(["1", "3"])
|
|
1722
|
+
expect(runCF(make<CFRow>().pipe(where("tags", "includes-any", ["blue", "green"]))).sort()).toEqual([
|
|
1723
|
+
"1",
|
|
1724
|
+
"2",
|
|
1725
|
+
"3"
|
|
1726
|
+
])
|
|
1727
|
+
expect(runCF(make<CFRow>().pipe(where("tags", "includes-all", ["red", "blue"])))).toEqual(["3"])
|
|
1728
|
+
})
|
|
1729
|
+
|
|
1730
|
+
it("codeFilter: in / notIn", () => {
|
|
1731
|
+
expect(runCF(make<CFRow>().pipe(where("tag", "in", ["x", "z"]))).sort()).toEqual(["1", "3"])
|
|
1732
|
+
expect(runCF(make<CFRow>().pipe(where("tag", "notIn", ["x", "z"]))).sort()).toEqual(["2", "4"])
|
|
1733
|
+
})
|
|
1734
|
+
|
|
1735
|
+
it("codeFilter: gt / gte / lt / lte / neq", () => {
|
|
1736
|
+
expect(runCF(make<CFRow>().pipe(where("qty", "gt", 10))).sort()).toEqual(["2", "4"])
|
|
1737
|
+
expect(runCF(make<CFRow>().pipe(where("qty", "gte", 10))).sort()).toEqual(["1", "2", "4"])
|
|
1738
|
+
expect(runCF(make<CFRow>().pipe(where("qty", "lt", 10)))).toEqual(["3"])
|
|
1739
|
+
expect(runCF(make<CFRow>().pipe(where("qty", "lte", 10))).sort()).toEqual(["1", "3"])
|
|
1740
|
+
expect(runCF(make<CFRow>().pipe(where("qty", "neq", 0))).sort()).toEqual(["1", "2", "4"])
|
|
1741
|
+
})
|
|
1742
|
+
|
|
1743
|
+
it("codeFilter: nested path access through dot notation", () => {
|
|
1744
|
+
expect(runCF(make<CFRow>().pipe(where("nested.kind", "k1"))).sort()).toEqual(["1", "3"])
|
|
1745
|
+
expect(
|
|
1746
|
+
runCF(
|
|
1747
|
+
make<CFRow>().pipe(
|
|
1748
|
+
where("nested.kind", "k2"),
|
|
1749
|
+
and("nested.v", "gt", 0)
|
|
1750
|
+
)
|
|
1751
|
+
)
|
|
1752
|
+
)
|
|
1753
|
+
.toEqual(["2"])
|
|
1754
|
+
})
|
|
1755
|
+
|
|
1756
|
+
it("codeFilter: array length predicates", () => {
|
|
1757
|
+
expect(runCF(make<CFRow>().pipe(where("tags.length", 0)))).toEqual(["4"])
|
|
1758
|
+
expect(runCF(make<CFRow>().pipe(where("tags.length", "gte", 2))).sort()).toEqual(["1", "3"])
|
|
1759
|
+
})
|
|
1760
|
+
|
|
1761
|
+
it("codeFilter: order + skip + limit applied after filter", () => {
|
|
1762
|
+
const q = make<CFRow>().pipe(
|
|
1763
|
+
where("tag", "neq", "z"),
|
|
1764
|
+
order("qty", "DESC"),
|
|
1765
|
+
page({ skip: 1, take: 2 })
|
|
1766
|
+
)
|
|
1767
|
+
expect(runCF(q)).toEqual(["2", "1"])
|
|
1768
|
+
})
|
package/test/rawQuery.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "@effect/vitest"
|
|
2
|
-
import { Array, Config, Context, Effect, flow, Layer, ManagedRuntime, Redacted, References, Result, S } from "effect-app"
|
|
2
|
+
import { Array, Config, Context, Effect, flow, Layer, ManagedRuntime, Redacted, References, Result, S, Struct } from "effect-app"
|
|
3
3
|
import { LogLevels } from "effect-app/utils"
|
|
4
4
|
import { setupRequestContextFromCurrent } from "../src/api/setupRequest.js"
|
|
5
5
|
import { and, computed, or, project, projectComputed, relation, where, whereEvery, whereSome } from "../src/Model/query.js"
|
|
@@ -408,6 +408,228 @@ describe("computed projections", () => {
|
|
|
408
408
|
))
|
|
409
409
|
*/
|
|
410
410
|
|
|
411
|
+
// Mimic scanner MultiPick/EasyLife AllPickList shape:
|
|
412
|
+
// - parent has tagged-state with `at` timestamp
|
|
413
|
+
// - items is NonEmptyArray with state._tag + articleId/articleGTIN
|
|
414
|
+
// - controller filters `state.at gte X` and `state._tag neq closed`
|
|
415
|
+
// then projectComputed with: count, any(initial/picking/packed),
|
|
416
|
+
// every(picked/packed), collectDistinct(articleId)
|
|
417
|
+
const itemStateSchema = S.Union([
|
|
418
|
+
S.TaggedStruct("initial", { at: S.String }),
|
|
419
|
+
S.TaggedStruct("picking", { at: S.String }),
|
|
420
|
+
S.TaggedStruct("picked", { at: S.String }),
|
|
421
|
+
S.TaggedStruct("packed", { at: S.String })
|
|
422
|
+
])
|
|
423
|
+
|
|
424
|
+
class ArticleLineItem extends S.Class<ArticleLineItem>("ArticleLineItem")({
|
|
425
|
+
articleId: S.String,
|
|
426
|
+
articleGTIN: S.String,
|
|
427
|
+
state: itemStateSchema
|
|
428
|
+
}) {}
|
|
429
|
+
|
|
430
|
+
const stOrderState = S.Union([
|
|
431
|
+
S.TaggedStruct("initial", { at: S.String }),
|
|
432
|
+
S.TaggedStruct("packed", { at: S.String }),
|
|
433
|
+
S.TaggedStruct("closed", { at: S.String })
|
|
434
|
+
])
|
|
435
|
+
|
|
436
|
+
class Order extends S.Class<Order>("Order")({
|
|
437
|
+
id: S.String,
|
|
438
|
+
state: stOrderState,
|
|
439
|
+
items: S.NonEmptyArray(ArticleLineItem)
|
|
440
|
+
}) {}
|
|
441
|
+
|
|
442
|
+
const orderItems = [
|
|
443
|
+
new Order({
|
|
444
|
+
id: "o-open-1",
|
|
445
|
+
state: { _tag: "initial", at: "2026-05-08T08:00:00Z" },
|
|
446
|
+
items: [
|
|
447
|
+
new ArticleLineItem({
|
|
448
|
+
articleId: "A1",
|
|
449
|
+
articleGTIN: "G1",
|
|
450
|
+
state: { _tag: "picking", at: "2026-05-08T08:01:00Z" }
|
|
451
|
+
}),
|
|
452
|
+
new ArticleLineItem({
|
|
453
|
+
articleId: "A1",
|
|
454
|
+
articleGTIN: "G1",
|
|
455
|
+
state: { _tag: "picked", at: "2026-05-08T08:02:00Z" }
|
|
456
|
+
}),
|
|
457
|
+
new ArticleLineItem({
|
|
458
|
+
articleId: "A2",
|
|
459
|
+
articleGTIN: "G2",
|
|
460
|
+
state: { _tag: "initial", at: "2026-05-08T08:00:00Z" }
|
|
461
|
+
})
|
|
462
|
+
]
|
|
463
|
+
}),
|
|
464
|
+
new Order({
|
|
465
|
+
id: "o-allpicked-2",
|
|
466
|
+
state: { _tag: "packed", at: "2026-05-07T10:00:00Z" },
|
|
467
|
+
items: [
|
|
468
|
+
new ArticleLineItem({
|
|
469
|
+
articleId: "B1",
|
|
470
|
+
articleGTIN: "GB1",
|
|
471
|
+
state: { _tag: "picked", at: "2026-05-07T09:50:00Z" }
|
|
472
|
+
}),
|
|
473
|
+
new ArticleLineItem({
|
|
474
|
+
articleId: "B2",
|
|
475
|
+
articleGTIN: "GB2",
|
|
476
|
+
state: { _tag: "picked", at: "2026-05-07T09:55:00Z" }
|
|
477
|
+
})
|
|
478
|
+
]
|
|
479
|
+
}),
|
|
480
|
+
new Order({
|
|
481
|
+
id: "o-closed-3",
|
|
482
|
+
state: { _tag: "closed", at: "2026-05-04T10:00:00Z" },
|
|
483
|
+
items: [
|
|
484
|
+
new ArticleLineItem({
|
|
485
|
+
articleId: "C1",
|
|
486
|
+
articleGTIN: "GC1",
|
|
487
|
+
state: { _tag: "packed", at: "2026-05-04T09:00:00Z" }
|
|
488
|
+
})
|
|
489
|
+
]
|
|
490
|
+
})
|
|
491
|
+
]
|
|
492
|
+
|
|
493
|
+
// @effect-diagnostics-next-line missingEffectServiceDependency:off
|
|
494
|
+
class OrderRepo extends Context.Service<OrderRepo>()(
|
|
495
|
+
"OrderRepo",
|
|
496
|
+
{
|
|
497
|
+
make: Effect.gen(function*() {
|
|
498
|
+
const partitionKey = "orders-" + new Date().getTime()
|
|
499
|
+
return yield* makeRepo("Order", Order, { config: { partitionValue: () => partitionKey } })
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
) {
|
|
503
|
+
static readonly layer = Layer
|
|
504
|
+
.effect(
|
|
505
|
+
OrderRepo,
|
|
506
|
+
Effect.gen(function*() {
|
|
507
|
+
const partitionKey = "orders-" + new Date().getTime()
|
|
508
|
+
const repo = OrderRepo.of(
|
|
509
|
+
yield* makeRepo("Order", Order, {
|
|
510
|
+
config: { partitionValue: () => partitionKey }
|
|
511
|
+
})
|
|
512
|
+
)
|
|
513
|
+
yield* repo.saveAndPublish(orderItems).pipe(setupRequestContextFromCurrent("init"))
|
|
514
|
+
return repo
|
|
515
|
+
})
|
|
516
|
+
)
|
|
517
|
+
static readonly Test = this
|
|
518
|
+
.layer
|
|
519
|
+
.pipe(Layer.provide(Layer.merge(MemoryStoreLive, RepositoryRegistryLive)))
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
describe("scanner-style AllPickList computed projections", () => {
|
|
523
|
+
const test = Effect
|
|
524
|
+
.gen(function*() {
|
|
525
|
+
const repo = yield* OrderRepo
|
|
526
|
+
type OrderEnc = S.Codec.Encoded<typeof Order>
|
|
527
|
+
|
|
528
|
+
const projection = S.Struct({
|
|
529
|
+
id: S.String,
|
|
530
|
+
state: stOrderState,
|
|
531
|
+
articleCount: S.NonNegativeInt,
|
|
532
|
+
hasInitialItem: S.Boolean,
|
|
533
|
+
hasPickingItem: S.Boolean,
|
|
534
|
+
hasPackedItem: S.Boolean,
|
|
535
|
+
allItemsPicked: S.Boolean,
|
|
536
|
+
allItemsPacked: S.Boolean,
|
|
537
|
+
articleIds: S.Array(S.String)
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
const result = yield* repo.query(
|
|
541
|
+
where("state.at", "gte", "2026-05-05T00:00:00Z"),
|
|
542
|
+
and("state._tag", "neq", "closed"),
|
|
543
|
+
projectComputed(
|
|
544
|
+
projection,
|
|
545
|
+
computed({
|
|
546
|
+
articleCount: relation<OrderEnc>("items").count(),
|
|
547
|
+
hasInitialItem: relation<OrderEnc>("items").any(where("state._tag", "initial")),
|
|
548
|
+
hasPickingItem: relation<OrderEnc>("items").any(where("state._tag", "picking")),
|
|
549
|
+
hasPackedItem: relation<OrderEnc>("items").any(where("state._tag", "packed")),
|
|
550
|
+
allItemsPicked: relation<OrderEnc>("items").every(where("state._tag", "picked")),
|
|
551
|
+
allItemsPacked: relation<OrderEnc>("items").every(where("state._tag", "packed")),
|
|
552
|
+
articleIds: relation<OrderEnc>("items").collectDistinct("articleId")
|
|
553
|
+
})
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
const byId = Object.fromEntries(result.map((r) => [r.id, r]))
|
|
558
|
+
|
|
559
|
+
expect(Object.keys(byId).sort()).toEqual(["o-allpicked-2", "o-open-1"])
|
|
560
|
+
|
|
561
|
+
const open = byId["o-open-1"]!
|
|
562
|
+
expect(open.articleCount).toBe(3)
|
|
563
|
+
expect(open.hasInitialItem).toBe(true)
|
|
564
|
+
expect(open.hasPickingItem).toBe(true)
|
|
565
|
+
expect(open.hasPackedItem).toBe(false)
|
|
566
|
+
expect(open.allItemsPicked).toBe(false)
|
|
567
|
+
expect(open.allItemsPacked).toBe(false)
|
|
568
|
+
expect([...open.articleIds].sort()).toEqual(["A1", "A2"])
|
|
569
|
+
|
|
570
|
+
const allp = byId["o-allpicked-2"]!
|
|
571
|
+
expect(allp.articleCount).toBe(2)
|
|
572
|
+
expect(allp.hasInitialItem).toBe(false)
|
|
573
|
+
expect(allp.hasPickingItem).toBe(false)
|
|
574
|
+
expect(allp.hasPackedItem).toBe(false)
|
|
575
|
+
expect(allp.allItemsPicked).toBe(true)
|
|
576
|
+
expect(allp.allItemsPacked).toBe(false)
|
|
577
|
+
expect([...allp.articleIds].sort()).toEqual(["B1", "B2"])
|
|
578
|
+
})
|
|
579
|
+
.pipe(setupRequestContextFromCurrent())
|
|
580
|
+
|
|
581
|
+
it("works well in Memory", () =>
|
|
582
|
+
test.pipe(Effect.provide(OrderRepo.Test), rt.runPromise))
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
// Same but mimics the FULL controller projection: includes `items` array
|
|
586
|
+
// (NonEmptyArray) alongside the computed scalars. This tests the
|
|
587
|
+
// memory-side select pipeline that combines subKeys (items) with
|
|
588
|
+
// computedKeys in one Project node.
|
|
589
|
+
describe("scanner-style AllPickList — items + computed combined", () => {
|
|
590
|
+
const test = Effect
|
|
591
|
+
.gen(function*() {
|
|
592
|
+
const repo = yield* OrderRepo
|
|
593
|
+
type OrderEnc = S.Codec.Encoded<typeof Order>
|
|
594
|
+
|
|
595
|
+
const projection = S.Struct({
|
|
596
|
+
id: S.String,
|
|
597
|
+
items: S.NonEmptyArray(ArticleLineItem.mapFields(Struct.pick(["articleId", "articleGTIN"]))),
|
|
598
|
+
articleCount: S.NonNegativeInt,
|
|
599
|
+
allItemsPicked: S.Boolean,
|
|
600
|
+
articleIds: S.Array(S.String)
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
const result = yield* repo.query(
|
|
604
|
+
where("state.at", "gte", "2026-05-05T00:00:00Z"),
|
|
605
|
+
and("state._tag", "neq", "closed"),
|
|
606
|
+
projectComputed(
|
|
607
|
+
projection,
|
|
608
|
+
computed({
|
|
609
|
+
articleCount: relation<OrderEnc>("items").count(),
|
|
610
|
+
allItemsPicked: relation<OrderEnc>("items").every(where("state._tag", "picked")),
|
|
611
|
+
articleIds: relation<OrderEnc>("items").collectDistinct("articleId")
|
|
612
|
+
})
|
|
613
|
+
)
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
expect(result.length).toBe(2)
|
|
617
|
+
const byId = Object.fromEntries(result.map((r) => [r.id, r]))
|
|
618
|
+
const open = byId["o-open-1"]!
|
|
619
|
+
expect(open.items.length).toBe(3)
|
|
620
|
+
expect(open.items[0]).toHaveProperty("articleId")
|
|
621
|
+
expect(open.items[0]).toHaveProperty("articleGTIN")
|
|
622
|
+
expect(open.allItemsPicked).toBe(false)
|
|
623
|
+
const allp = byId["o-allpicked-2"]!
|
|
624
|
+
expect(allp.items.length).toBe(2)
|
|
625
|
+
expect(allp.allItemsPicked).toBe(true)
|
|
626
|
+
})
|
|
627
|
+
.pipe(setupRequestContextFromCurrent())
|
|
628
|
+
|
|
629
|
+
it("works well in Memory", () =>
|
|
630
|
+
test.pipe(Effect.provide(OrderRepo.Test), rt.runPromise))
|
|
631
|
+
})
|
|
632
|
+
|
|
411
633
|
describe("removeByIds", () => {
|
|
412
634
|
const test = Effect
|
|
413
635
|
.gen(function*() {
|