@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.
@@ -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
+ })
@@ -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*() {