@reventlessdev/reventless-aws 3.0.0-alpha.318 → 3.0.0-alpha.319

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 CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.319 (2026-08-22)
7
+
8
+ * feat(core)!: a slice that has given up says so ([f5559ab](https://github.com/ReventlessDev/reventless-core/commit/f5559abede3a9cf2c9a48aadc69a666c46cc75bf))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * `onExhausted` is required on the Translation and Automation
13
+ module types. ReScript has no optional module-type fields, so the alternative
14
+ was a PPX-injected default — and defaulting this one to silence is the bug.
15
+ A slice now states what abandonment means for its domain, `None` included.
16
+ GeocodeCustomerAddress answers with MarkAddressUnresolvable; the others say
17
+ nothing, on purpose.
18
+
19
+
20
+
6
21
  # 3.0.0-alpha.318 (2026-08-21)
7
22
 
8
23
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.318",
3
+ "version": "3.0.0-alpha.319",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -15,15 +15,15 @@
15
15
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.12",
16
16
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
17
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
18
- "@reventlessdev/rescript-node": "2.0.0-alpha.8",
19
18
  "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.2",
19
+ "@reventlessdev/rescript-node": "2.0.0-alpha.8",
20
20
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
- "@reventlessdev/reventless-core": "3.0.0-alpha.246",
22
- "@reventlessdev/reventless-infra": "3.0.0-alpha.149",
21
+ "@reventlessdev/reventless-core": "3.0.0-alpha.247",
23
22
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
23
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.150",
24
24
  "@reventlessdev/reventless-interop": "3.0.0-alpha.33",
25
- "@reventlessdev/reventless-spec": "3.0.0-alpha.121",
26
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.110"
25
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.122",
26
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.111"
27
27
  },
28
28
  "devDependencies": {
29
29
  "rescript": "12.3.0",
@@ -148,9 +148,12 @@ let makeSyncTodoItems = (
148
148
  // by definition no fresher.
149
149
  //
150
150
  // Only `Pending` and `Failed` are read. `Completed` rows are the bulk of a
151
- // mature table and are never actionable. Retry-exhausted rows are not excluded
152
- // `maxRetries` is Spec-level and not known here but `phase2` filters them, so
153
- // they are inert rather than wrong.
151
+ // mature table and are never actionable, and neither are `Abandoned` ones a
152
+ // row whose retry budget is spent is terminal, so it is left in the table rather
153
+ // than carried into memory. That the status says so is what lets this filter
154
+ // decide it: `maxRetries` is Spec-level and still not known here, and before the
155
+ // status existed an exhausted row had to be loaded and then filtered out by
156
+ // `phase2`.
154
157
  let makeLoadTodoItems = (
155
158
  ~queryDbTableName: string,
156
159
  ~todoItems: dict<'row>,
@@ -612,3 +612,169 @@ describe("owner scoping", () => {
612
612
  expect(r->ids)->toEqual(["p-1", "p-3"])
613
613
  })
614
614
  })
615
+
616
+ // ── nested union member types ───────────────────────────────────────────────
617
+ //
618
+ // The `__typename` a union member is resolved from is written into the stored
619
+ // row, above the DynamoDB/Postgres branch (`withUnionMemberTypes` wraps the
620
+ // result of `makeQueryDbOps`), and this resolver stamps only the *top level* of
621
+ // a `node` answer. So the Postgres door's whole claim is that it hands a nested
622
+ // union back exactly as stored — which no test made, and which no deploy can
623
+ // make either: nothing runs the Postgres backend, so this door cannot be called
624
+ // against a deployment the way the AppSync ones were.
625
+ //
626
+ // Losing the nested key is not visible as an error: the member resolves to null
627
+ // and the null takes its non-nullable parent, so rows leave the answer.
628
+
629
+ let geoRow = (id, status) => {
630
+ let geo = Dict.make()
631
+ geo->Dict.set("__typename", JSON.Encode.string("GeolocationLocated"))
632
+ geo->Dict.set("lat", JSON.Encode.float(48.2082))
633
+ geo->Dict.set("lng", JSON.Encode.float(16.3738))
634
+ let o = Dict.make()
635
+ o->Dict.set("id", JSON.Encode.string(id))
636
+ o->Dict.set("status", JSON.Encode.string(status))
637
+ o->Dict.set("name", JSON.Encode.string("Geo " ++ id))
638
+ o->Dict.set("owner", JSON.Encode.string("u-a"))
639
+ o->Dict.set("geolocation", JSON.Encode.object(geo))
640
+ JSON.Encode.object(o)
641
+ }
642
+
643
+ let geoStore = Dict.fromArray([("g-1", geoRow("g-1", "active")), ("g-2", geoRow("g-2", "active"))])
644
+ let geoItems = () => geoStore->Dict.valuesToArray
645
+
646
+ let geoConnection = (): JSON.t =>
647
+ JSON.Encode.object(
648
+ Dict.fromArray([
649
+ (
650
+ "edges",
651
+ JSON.Encode.array(
652
+ geoItems()->Array.map(item =>
653
+ JSON.Encode.object(
654
+ Dict.fromArray([("cursor", JSON.Encode.string("c")), ("node", item)]),
655
+ )
656
+ ),
657
+ ),
658
+ ),
659
+ ]),
660
+ )
661
+
662
+ let geoBinding = (): PgQueryResolver_Lambda.binding => {
663
+ ops: {
664
+ ...ops,
665
+ load: async id => Ok(geoStore->Dict.get(id)->Option.mapOr([], i => [i])),
666
+ },
667
+ pushdowns: {
668
+ indexLookup: async (~readModelName as _, _field, _value) => geoItems(),
669
+ byIds: async (~readModelName as _, ids) => ids->Array.filterMap(id => geoStore->Dict.get(id)),
670
+ listPage: async (
671
+ ~readModelName as _,
672
+ ~argsDict as _,
673
+ ~capability as _,
674
+ ~labelField as _,
675
+ ~ownerScope as _=?,
676
+ ~retiredScope as _=?,
677
+ ) => Some(geoConnection()),
678
+ itemsPage: async (
679
+ ~readModelName as _,
680
+ ~subIdField as _,
681
+ ~id as _,
682
+ ~argsDict as _,
683
+ ~ownerScope as _=?,
684
+ ~retiredScope as _=?,
685
+ ) => geoConnection(),
686
+ scanAll: async (~readModelName as _) => geoItems(),
687
+ },
688
+ indexes: [
689
+ {index: "byStatus", type_: "S", idField: "status", projectionType: Reventless.ReadModel.ALL},
690
+ ],
691
+ subIdField: Some("lineNo"),
692
+ capability,
693
+ labelField: "name",
694
+ includeIdParam: true,
695
+ authorization: Reventless.Authorization.AllowAnonymous,
696
+ ownerField: None,
697
+ retiredField: None,
698
+ retiredValues: None,
699
+ namedWhenRetired: false,
700
+ }
701
+
702
+ // The member type of the first row a door answers with, whatever shape it took.
703
+ let memberOf = (r: JSON.t): option<string> => {
704
+ let fromRow = row =>
705
+ row->field("geolocation")->Option.flatMap(g => g->str("__typename"))
706
+ switch r->JSON.Decode.array {
707
+ | Some(rows) => rows->Array.get(0)->Option.flatMap(fromRow)
708
+ | None =>
709
+ switch r->field("edges")->Option.flatMap(JSON.Decode.array) {
710
+ | Some(edges) => edges->Array.get(0)->Option.flatMap(e => e->field("node"))->Option.flatMap(fromRow)
711
+ | None => fromRow(r)
712
+ }
713
+ }
714
+ }
715
+
716
+ describe("PgQueryResolver — a nested union survives every door", () => {
717
+ let doors = [
718
+ ("getById", mkPayload(~kind="getById", ~args=objArgs([("id", JSON.Encode.string("g-1"))]), ())),
719
+ (
720
+ "byIds",
721
+ mkPayload(~kind="byIds", ~args=objArgs([("ids", JSON.Encode.array([JSON.Encode.string("g-1")]))]), ()),
722
+ ),
723
+ (
724
+ "index",
725
+ mkPayload(~kind="index", ~index="byStatus", ~args=objArgs([("byStatus", JSON.Encode.string("active"))]), ()),
726
+ ),
727
+ ("list", mkPayload(~kind="list", ())),
728
+ ("items", mkPayload(~kind="items", ~args=objArgs([("id", JSON.Encode.string("g-1"))]), ())),
729
+ // The cross-table doors answer with the *target's* rows, so a union on the
730
+ // target travels through a field on some other view's type.
731
+ (
732
+ "resolveOne",
733
+ {
734
+ readModelName: "Geos",
735
+ kind: "resolveOne",
736
+ target: "Geos",
737
+ source: objArgs([("ref", JSON.Encode.string("g-1"))]),
738
+ sourceIdField: "ref",
739
+ multi: false,
740
+ arguments: objArgs([]),
741
+ identity: Reventless.Identity.anonymous,
742
+ },
743
+ ),
744
+ (
745
+ "resolveMany",
746
+ {
747
+ readModelName: "Geos",
748
+ kind: "resolveMany",
749
+ target: "Geos",
750
+ source: objArgs([("refs", JSON.Encode.array([JSON.Encode.string("g-1")]))]),
751
+ sourceIdsField: "refs",
752
+ arguments: objArgs([]),
753
+ identity: Reventless.Identity.anonymous,
754
+ },
755
+ ),
756
+ ]
757
+
758
+ doors->Array.forEach(((label, payload)) =>
759
+ testPromise(label ++ " hands the member type back as stored", async () => {
760
+ let r = await PgQueryResolver_Lambda.dispatch(~binding=geoBinding(), ~payload)
761
+ expect(memberOf(r))->toEqual(Some("GeolocationLocated"))
762
+ })
763
+ )
764
+
765
+ // `node` is the one door that writes a `__typename` of its own, at the top
766
+ // level. It must not reach down and overwrite the member's.
767
+ testPromise("node stamps the top level without disturbing the member", async () => {
768
+ PgQueryResolver_Lambda.register(~readModelName="Geos", geoBinding())
769
+ PgQueryResolver_Lambda.registerNodeType(~typeName="Geo", ~readModelName="Geos")
770
+ let payload: PgQueryResolver_Lambda.payload = {
771
+ readModelName: "",
772
+ kind: "node",
773
+ arguments: objArgs([("id", JSON.Encode.string(btoa("Geo:g-1")))]),
774
+ identity: Reventless.Identity.anonymous,
775
+ }
776
+ let r = await PgQueryResolver_Lambda.handler(payload, Obj.magic(Nullable.null))
777
+ expect(r->str("__typename"))->toEqual(Some("Geo"))
778
+ expect(memberOf(r))->toEqual(Some("GeolocationLocated"))
779
+ })
780
+ })
@@ -629,6 +629,196 @@ globalThis.describe("owner scoping", () => {
629
629
  });
630
630
  });
631
631
 
632
+ function geoRow(id, status) {
633
+ let geo = {};
634
+ geo["__typename"] = "GeolocationLocated";
635
+ geo["lat"] = 48.2082;
636
+ geo["lng"] = 16.3738;
637
+ let o = {};
638
+ o["id"] = id;
639
+ o["status"] = status;
640
+ o["name"] = "Geo " + id;
641
+ o["owner"] = "u-a";
642
+ o["geolocation"] = geo;
643
+ return o;
644
+ }
645
+
646
+ let geoStore = Object.fromEntries([
647
+ [
648
+ "g-1",
649
+ geoRow("g-1", "active")
650
+ ],
651
+ [
652
+ "g-2",
653
+ geoRow("g-2", "active")
654
+ ]
655
+ ]);
656
+
657
+ function geoItems() {
658
+ return Object.values(geoStore);
659
+ }
660
+
661
+ function geoConnection() {
662
+ return Object.fromEntries([[
663
+ "edges",
664
+ Object.values(geoStore).map(item => Object.fromEntries([
665
+ [
666
+ "cursor",
667
+ "c"
668
+ ],
669
+ [
670
+ "node",
671
+ item
672
+ ]
673
+ ]))
674
+ ]]);
675
+ }
676
+
677
+ function geoBinding() {
678
+ return {
679
+ ops: {
680
+ load: async id => ({
681
+ TAG: "Ok",
682
+ _0: Stdlib_Option.mapOr(geoStore[id], [], i => [i])
683
+ }),
684
+ loadStream: 0,
685
+ save: 0,
686
+ saveBatch: 0,
687
+ count: 0,
688
+ delete: 0,
689
+ deleteBatch: 0
690
+ },
691
+ pushdowns: {
692
+ indexLookup: async (param, _field, _value) => Object.values(geoStore),
693
+ byIds: async (param, ids) => Stdlib_Array.filterMap(ids, id => geoStore[id]),
694
+ listPage: async (param, param$1, param$2, param$3, param$4, param$5) => geoConnection(),
695
+ itemsPage: async (param, param$1, param$2, param$3, param$4, param$5) => geoConnection(),
696
+ scanAll: async param => Object.values(geoStore)
697
+ },
698
+ indexes: [{
699
+ index: "byStatus",
700
+ type_: "S",
701
+ idField: "status",
702
+ projectionType: "ALL"
703
+ }],
704
+ subIdField: "lineNo",
705
+ capability: capability,
706
+ labelField: "name",
707
+ includeIdParam: true,
708
+ authorization: "AllowAnonymous",
709
+ ownerField: undefined,
710
+ retiredField: undefined,
711
+ retiredValues: undefined,
712
+ namedWhenRetired: false
713
+ };
714
+ }
715
+
716
+ function memberOf(r) {
717
+ let fromRow = row => Stdlib_Option.flatMap(field(row, "geolocation"), g => str(g, "__typename"));
718
+ let rows = Stdlib_JSON.Decode.array(r);
719
+ if (rows !== undefined) {
720
+ return Stdlib_Option.flatMap(rows[0], fromRow);
721
+ }
722
+ let edges = Stdlib_Option.flatMap(field(r, "edges"), Stdlib_JSON.Decode.array);
723
+ if (edges !== undefined) {
724
+ return Stdlib_Option.flatMap(Stdlib_Option.flatMap(edges[0], e => field(e, "node")), fromRow);
725
+ } else {
726
+ return fromRow(r);
727
+ }
728
+ }
729
+
730
+ globalThis.describe("PgQueryResolver — a nested union survives every door", () => {
731
+ let doors = [
732
+ [
733
+ "getById",
734
+ mkPayload("getById", undefined, Object.fromEntries([[
735
+ "id",
736
+ "g-1"
737
+ ]]), undefined, undefined)
738
+ ],
739
+ [
740
+ "byIds",
741
+ mkPayload("byIds", undefined, Object.fromEntries([[
742
+ "ids",
743
+ ["g-1"]
744
+ ]]), undefined, undefined)
745
+ ],
746
+ [
747
+ "index",
748
+ mkPayload("index", "byStatus", Object.fromEntries([[
749
+ "byStatus",
750
+ "active"
751
+ ]]), undefined, undefined)
752
+ ],
753
+ [
754
+ "list",
755
+ mkPayload("list", undefined, undefined, undefined, undefined)
756
+ ],
757
+ [
758
+ "items",
759
+ mkPayload("items", undefined, Object.fromEntries([[
760
+ "id",
761
+ "g-1"
762
+ ]]), undefined, undefined)
763
+ ],
764
+ [
765
+ "resolveOne",
766
+ {
767
+ readModelName: "Geos",
768
+ kind: "resolveOne",
769
+ target: "Geos",
770
+ source: Object.fromEntries([[
771
+ "ref",
772
+ "g-1"
773
+ ]]),
774
+ sourceIdField: "ref",
775
+ multi: false,
776
+ arguments: Object.fromEntries([]),
777
+ identity: Identity$Reventless.anonymous
778
+ }
779
+ ],
780
+ [
781
+ "resolveMany",
782
+ {
783
+ readModelName: "Geos",
784
+ kind: "resolveMany",
785
+ target: "Geos",
786
+ source: Object.fromEntries([[
787
+ "refs",
788
+ ["g-1"]
789
+ ]]),
790
+ sourceIdsField: "refs",
791
+ arguments: Object.fromEntries([]),
792
+ identity: Identity$Reventless.anonymous
793
+ }
794
+ ]
795
+ ];
796
+ doors.forEach(param => {
797
+ let payload = param[1];
798
+ globalThis.test(param[0] + " hands the member type back as stored", async () => {
799
+ let r = await PgQueryResolver_Lambda$ReventlessAws.dispatch(geoBinding(), undefined, payload);
800
+ globalThis.expect(memberOf(r)).toEqual("GeolocationLocated");
801
+ });
802
+ });
803
+ globalThis.test("node stamps the top level without disturbing the member", async () => {
804
+ PgQueryResolver_Lambda$ReventlessAws.register("Geos", geoBinding());
805
+ PgQueryResolver_Lambda$ReventlessAws.registerNodeType("Geo", "Geos");
806
+ let payload_arguments = Object.fromEntries([[
807
+ "id",
808
+ btoa("Geo:g-1")
809
+ ]]);
810
+ let payload = {
811
+ readModelName: "",
812
+ kind: "node",
813
+ arguments: payload_arguments,
814
+ identity: Identity$Reventless.anonymous
815
+ };
816
+ let r = await PgQueryResolver_Lambda$ReventlessAws.handler(payload, null);
817
+ globalThis.expect(str(r, "__typename")).toEqual("Geo");
818
+ globalThis.expect(memberOf(r)).toEqual("GeolocationLocated");
819
+ });
820
+ });
821
+
632
822
  export {
633
823
  mk,
634
824
  store,
@@ -647,5 +837,11 @@ export {
647
837
  field,
648
838
  str,
649
839
  ids,
840
+ geoRow,
841
+ geoStore,
842
+ geoItems,
843
+ geoConnection,
844
+ geoBinding,
845
+ memberOf,
650
846
  }
651
847
  /* store Not a pure module */