@reventlessdev/reventless-aws 3.0.0-alpha.209 → 3.0.0-alpha.211

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 (33) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res +348 -707
  4. package/src/Platform.res.mjs +271 -739
  5. package/src/adapter/QueryDb/PgQueryResolver_Builder.res +4 -47
  6. package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +1 -30
  7. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +0 -11
  8. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +0 -5
  9. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +6 -7
  10. package/src/components/Api/AppSync_Adapter.res +143 -101
  11. package/src/components/Api/AppSync_Adapter.res.mjs +60 -57
  12. package/src/components/Api/AppSync_MergedApi.res +218 -0
  13. package/src/components/Api/AppSync_MergedApi.res.mjs +122 -0
  14. package/src/components/Api/AppSync_SdlDecorate.res +49 -61
  15. package/src/components/Api/AppSync_SdlDecorate.res.mjs +38 -29
  16. package/src/components/Plugin.res.mjs +1 -2
  17. package/tests/AppSync_AdapterTest.res +184 -0
  18. package/tests/AppSync_AdapterTest.res.mjs +136 -0
  19. package/tests/AppSync_SdlDecorateTest.res +33 -86
  20. package/tests/AppSync_SdlDecorateTest.res.mjs +26 -71
  21. package/tests/MCP_LambdaTest.res +4 -6
  22. package/tests/MCP_LambdaTest.res.mjs +2 -2
  23. package/src/adapter/Api/ApiFragmentDeregistration.res +0 -138
  24. package/src/adapter/Api/ApiFragmentDeregistration.res.mjs +0 -108
  25. package/src/adapter/Api/ApiSchemaPush.res +0 -79
  26. package/src/adapter/Api/ApiSchemaPush.res.mjs +0 -64
  27. package/src/adapter/Api/Platform_ApiFragments_Lambda.res +0 -222
  28. package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +0 -202
  29. package/src/adapter/QueryDb/NodeResolver_AppSync.res +0 -71
  30. package/src/adapter/QueryDb/NodeResolver_AppSync.res.mjs +0 -44
  31. package/src/adapter/Runtime/ApiSchemaPush_Runtime.mjs +0 -204
  32. package/tests/ApiSchemaPushTest.res +0 -32
  33. package/tests/ApiSchemaPushTest.res.mjs +0 -20
@@ -3,7 +3,6 @@
3
3
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
4
  import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
5
5
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
6
- import * as GraphQL_PushPlanner$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_PushPlanner.res.mjs";
7
6
 
8
7
  function injectAwsSubscribe(sdl, sources) {
9
8
  if (sources.length === 0) {
@@ -80,39 +79,48 @@ let sharedIamTypeNames = [
80
79
  "PageInfo",
81
80
  "CommandAccepted",
82
81
  "CommandRejected",
83
- "CommandPending",
84
- "Platform_ApiFragmentEntry"
82
+ "CommandPending"
85
83
  ];
86
84
 
87
85
  function stampSharedIamTypes(sdl) {
88
86
  return Stdlib_Array.reduce(sharedIamTypeNames, sdl, (acc, name) => acc.replace(`type ` + name + ` {`, `type ` + name + ` @aws_cognito_user_pools @aws_iam {`));
89
87
  }
90
88
 
91
- function planAwsPushes(rawAdminBase, iamFieldNames, fragments, splitApi) {
92
- let authBase = injectAwsAuthAll(rawAdminBase, "Admin", iamFieldNames);
93
- let targeted = fragments.map(f => {
94
- let match = f.target;
95
- let tmp = match === "Platform" ? "Platform" : "Domain";
96
- return {
97
- fragment: {
98
- encoded: f.encoded,
99
- protocol: f.protocol
100
- },
101
- target: tmp
102
- };
103
- });
104
- let allFrags = targeted.map(t => t.fragment);
105
- let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(rawAdminBase, allFrags);
106
- return GraphQL_PushPlanner$ReventlessCore.planPushes(authBase, targeted, splitApi).map(plan => {
107
- let sdl = stampSharedIamTypes(injectAwsSubscribe(plan.sdl, sources));
108
- let match = plan.api;
109
- let api;
110
- api = match === "DomainApi" ? "DomainApi" : "PlatformApi";
111
- return {
112
- api: api,
113
- sdl: sdl
114
- };
115
- });
89
+ let canonicalTypeNames = [
90
+ "PageInfo",
91
+ "CommandAccepted",
92
+ "CommandRejected",
93
+ "CommandPending"
94
+ ];
95
+
96
+ function stampCanonicalTypes(sdl) {
97
+ return sdl.split("\n").map(line => {
98
+ let isObjectDef = canonicalTypeNames.some(name => line.startsWith(`type ` + name + ` `));
99
+ let isNodeDef = line.startsWith("interface Node ") || line.startsWith("interface Node{");
100
+ let isUnionDef = line.startsWith("union CommandResult ") || line.startsWith("union CommandResult=");
101
+ if (line.includes("@canonical")) {
102
+ return line;
103
+ }
104
+ if (isObjectDef || isNodeDef) {
105
+ let braceIdx = Stdlib_String.indexOfOpt(line, "{");
106
+ if (braceIdx === undefined) {
107
+ return line;
108
+ }
109
+ let head = line.slice(0, braceIdx).trimEnd();
110
+ let tail = line.slice(braceIdx);
111
+ return head + ` @canonical ` + tail;
112
+ }
113
+ if (!isUnionDef) {
114
+ return line;
115
+ }
116
+ let eqIdx = Stdlib_String.indexOfOpt(line, "=");
117
+ if (eqIdx === undefined) {
118
+ return line;
119
+ }
120
+ let head$1 = line.slice(0, eqIdx).trimEnd();
121
+ let tail$1 = line.slice(eqIdx);
122
+ return head$1 + ` @canonical ` + tail$1;
123
+ }).join("\n");
116
124
  }
117
125
 
118
126
  export {
@@ -121,6 +129,7 @@ export {
121
129
  injectAwsAuthAll,
122
130
  sharedIamTypeNames,
123
131
  stampSharedIamTypes,
124
- planAwsPushes,
132
+ canonicalTypeNames,
133
+ stampCanonicalTypes,
125
134
  }
126
135
  /* GraphQL_Stitcher-ReventlessCore Not a pure module */
@@ -28,8 +28,7 @@ function Make(HooksConfig) {
28
28
  hooks: HooksConfig.hooks
29
29
  })({})({
30
30
  makeApiResource: AppSync_Adapter$ReventlessAws.makeApiResource,
31
- generateFragment: AppSync_Adapter$ReventlessAws.generateFragment,
32
- updateSchema: AppSync_Adapter$ReventlessAws.updateSchema
31
+ generateFragment: AppSync_Adapter$ReventlessAws.generateFragment
33
32
  })({
34
33
  make: RuntimeEnvironment_Lambda$ReventlessAws.make,
35
34
  groupBySource: RuntimeEnvironment_Lambda$ReventlessAws.groupBySource,
@@ -652,3 +652,187 @@ describe("Split mode — empty base fragment", () => {
652
652
  expect(sdl)->toContain("Platform_Plugin")
653
653
  })
654
654
  })
655
+
656
+ // ── Merged mode — canonical source documents ─────────────────────────────────
657
+ //
658
+ // On the merge path (merged-api-push-free-composition, Phase 3) the platform-
659
+ // owned source APIs carry their schemas DECLARATIVELY: the admin base (or, in
660
+ // split mode, the bare relay base) stitched with an empty plugin list and
661
+ // stamped @canonical. These tests pin the assembled documents' load-bearing
662
+ // properties; Platform.res composes exactly this pipeline.
663
+
664
+ describe("Merged mode — canonical source documents", () => {
665
+ let assembleCanonicalSourceSdl = (~baseFragment) =>
666
+ AppSync_Adapter.stitchStandaloneWithAwsDirectives(~fragment=baseFragment)
667
+ ->AppSync_SdlDecorate.stampCanonicalTypes
668
+
669
+ // Mirrors Platform.res's domainBaseFragment: no component fields, one
670
+ // Platform_ping so the Query type is non-empty.
671
+ let domainBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
672
+ types: [],
673
+ mutations: [],
674
+ queries: [" Platform_ping: String"],
675
+ subscriptions: [],
676
+ subscriptionSources: [],
677
+ })
678
+
679
+ let adminSourceSdl = assembleCanonicalSourceSdl(
680
+ ~baseFragment=AppSync_Adapter.injectAwsAuthAll(
681
+ ReventlessCore.AdminApi.baseFragment(~cloner=false),
682
+ ~group="Admin",
683
+ ),
684
+ )
685
+
686
+ let domainBaseSourceSdl = assembleCanonicalSourceSdl(~baseFragment=domainBaseFragment)
687
+
688
+ testSync("no AWS source document carries the global node query (dropped — see plan)", () => {
689
+ expect(adminSourceSdl)->not_->toContain("node(id: ID!): Node")
690
+ expect(domainBaseSourceSdl)->not_->toContain("node(id: ID!): Node")
691
+ // The Node interface and global IDs stay — they are what makes a future
692
+ // node() possible without a schema migration.
693
+ expect(adminSourceSdl)->toContain("interface Node")
694
+ })
695
+
696
+ testSync("admin source document stamps @canonical on the shared traversal types", () => {
697
+ expect(adminSourceSdl)->toContain("type PageInfo @aws_cognito_user_pools @aws_iam @canonical {")
698
+ expect(adminSourceSdl)->toContain("interface Node @canonical {")
699
+ expect(adminSourceSdl)->toContain("union CommandResult @canonical =")
700
+ expect(adminSourceSdl)->toContain(
701
+ "type CommandAccepted @aws_cognito_user_pools @aws_iam @canonical {",
702
+ )
703
+ })
704
+
705
+ testSync("admin source document keeps admin fields and shared-type IAM stamps", () => {
706
+ expect(adminSourceSdl)->toContain("Platform_Plugin")
707
+ // Type-level dual-auth on the shared traversal types (stampSharedIamTypes);
708
+ // no FIELD carries @aws_iam anymore — the SigV4 register/deregister surface
709
+ // died with the fragment registry.
710
+ expect(adminSourceSdl)->toContain("type PageInfo @aws_cognito_user_pools @aws_iam")
711
+ })
712
+
713
+ testSync("Domain base source document (split mode) is relay types + Platform_ping only", () => {
714
+ expect(domainBaseSourceSdl)->toContain("Platform_ping: String")
715
+ expect(domainBaseSourceSdl)->toContain("interface Node @canonical {")
716
+ expect(domainBaseSourceSdl)->toContain("type PageInfo")
717
+ // No admin or plugin fields on the split-mode Domain source.
718
+ expect(domainBaseSourceSdl)->not_->toContain("Platform_Plugin")
719
+ // No mutations → no CommandResult family on this source.
720
+ expect(domainBaseSourceSdl)->not_->toContain("union CommandResult")
721
+ })
722
+
723
+ // Phase 4: the plugin-stack subgraph document pushed to the plugin's own
724
+ // source API.
725
+ let pluginFragment = ReventlessCore.GraphQL_Stitcher.encode({
726
+ types: [
727
+ "type MyPlugin_Item implements Node {\n id: ID!\n name: String!\n}",
728
+ "type CommandAccepted {\n msgId: ID!\n eventCount: Int!\n}",
729
+ ],
730
+ mutations: ["MyPlugin_Item_Create(name: String!): CommandResult"],
731
+ queries: ["MyPlugin_Item(id: ID!): MyPlugin_Item"],
732
+ subscriptions: ["onMyPlugin_Item_Create(id: ID): CommandResult"],
733
+ subscriptionSources: [
734
+ {field: "onMyPlugin_Item_Create", mutations: ["MyPlugin_Item_Create"]},
735
+ ],
736
+ })
737
+
738
+ testSync("plugin subgraph document is standalone: relay types included, node omitted", () => {
739
+ let sdl = AppSync_Adapter.stitchStandaloneWithAwsDirectives(~fragment=pluginFragment)
740
+ expect(sdl)->toContain("interface Node")
741
+ expect(sdl)->toContain("type PageInfo")
742
+ expect(sdl)->toContain("MyPlugin_Item_Create")
743
+ expect(sdl)->not_->toContain("node(id: ID!): Node")
744
+ })
745
+
746
+ testSync("plugin subgraph document carries @aws_subscribe + shared-type IAM stamps, no @canonical", () => {
747
+ let sdl = AppSync_Adapter.stitchStandaloneWithAwsDirectives(~fragment=pluginFragment)
748
+ expect(sdl)->toContain(`@aws_subscribe(mutations: ["MyPlugin_Item_Create"])`)
749
+ expect(sdl)->toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam {")
750
+ // Plugin subgraphs stay unstamped — the admin source's canonical defs win.
751
+ expect(sdl)->not_->toContain("@canonical")
752
+ })
753
+ })
754
+
755
+ // ── waitForMergeSuccess — association merge-status poll ─────────────────────
756
+
757
+ describe("AppSync_Adapter.waitForMergeSuccess", () => {
758
+ // Fake AppSync client: `send` yields the canned responses in order and
759
+ // repeats the last one when the poll outruns the script.
760
+ let makeFakeClient = (
761
+ responses: array<AppSync_Adapter.getSourceApiAssociationResult>,
762
+ ): AppSync_Adapter.appSyncClient => {
763
+ let i = ref(0)
764
+ let fake = {
765
+ "send": _cmd => {
766
+ let idx = Math.Int.min(i.contents, responses->Array.length - 1)
767
+ i := i.contents + 1
768
+ Promise.resolve(responses->Array.getUnsafe(idx))
769
+ },
770
+ }
771
+ fake->Obj.magic
772
+ }
773
+
774
+ let response = (~status, ~detail=?): AppSync_Adapter.getSourceApiAssociationResult => {
775
+ sourceApiAssociation: Some({
776
+ sourceApiAssociationStatus: Some(status),
777
+ sourceApiAssociationStatusDetail: detail,
778
+ }),
779
+ }
780
+
781
+ test("resolves once the association reports MERGE_SUCCESS", async () => {
782
+ let client = makeFakeClient([
783
+ response(~status="MERGE_SCHEDULED"),
784
+ response(~status="MERGE_IN_PROGRESS"),
785
+ response(~status="MERGE_SUCCESS"),
786
+ ])
787
+ await AppSync_Adapter.waitForMergeSuccess(
788
+ client,
789
+ ~associationId="assoc-1",
790
+ ~mergedApiIdentifier="merged-1",
791
+ ~delayMs=1,
792
+ )
793
+ })
794
+
795
+ test("throws with the AWS status detail on MERGE_FAILED", async () => {
796
+ let client = makeFakeClient([
797
+ response(
798
+ ~status="MERGE_FAILED",
799
+ ~detail="Unable to resolve conflict on object with name SharedThing.x",
800
+ ),
801
+ ])
802
+ let message = ref("")
803
+ try {
804
+ await AppSync_Adapter.waitForMergeSuccess(
805
+ client,
806
+ ~associationId="assoc-1",
807
+ ~mergedApiIdentifier="merged-1",
808
+ ~delayMs=1,
809
+ )
810
+ } catch {
811
+ | exn =>
812
+ message :=
813
+ exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("(no message)")
814
+ }
815
+ expect(message.contents)->toContain("MERGE_FAILED")
816
+ expect(message.contents)->toContain("SharedThing.x")
817
+ })
818
+
819
+ test("times out after maxAttempts on a status that never settles", async () => {
820
+ let client = makeFakeClient([response(~status="MERGE_IN_PROGRESS")])
821
+ let message = ref("")
822
+ try {
823
+ await AppSync_Adapter.waitForMergeSuccess(
824
+ client,
825
+ ~associationId="assoc-1",
826
+ ~mergedApiIdentifier="merged-1",
827
+ ~maxAttempts=2,
828
+ ~delayMs=1,
829
+ )
830
+ } catch {
831
+ | exn =>
832
+ message :=
833
+ exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("(no message)")
834
+ }
835
+ expect(message.contents)->toContain("timed out")
836
+ expect(message.contents)->toContain("MERGE_IN_PROGRESS")
837
+ })
838
+ })
@@ -1,11 +1,14 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as S from "sury/src/S.res.mjs";
4
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
6
8
  import * as AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/AdminApi.res.mjs";
7
9
  import * as AppSync_Adapter$ReventlessAws from "../src/components/Api/AppSync_Adapter.res.mjs";
8
10
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
11
+ import * as AppSync_SdlDecorate$ReventlessAws from "../src/components/Api/AppSync_SdlDecorate.res.mjs";
9
12
  import * as PluginBaseFragment$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/api/PluginBaseFragment.res.mjs";
10
13
 
11
14
  globalThis.describe("AppSync_Adapter.sha256Hex", () => {
@@ -603,6 +606,139 @@ globalThis.describe("Split mode — empty base fragment", () => {
603
606
  });
604
607
  });
605
608
 
609
+ globalThis.describe("Merged mode — canonical source documents", () => {
610
+ let domainBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
611
+ types: [],
612
+ mutations: [],
613
+ queries: [" Platform_ping: String"],
614
+ subscriptions: [],
615
+ subscriptionSources: []
616
+ });
617
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin", undefined);
618
+ let adminSourceSdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(baseFragment));
619
+ let domainBaseSourceSdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(domainBaseFragment));
620
+ globalThis.test("no AWS source document carries the global node query (dropped — see plan)", () => {
621
+ globalThis.expect(adminSourceSdl).not.toContain("node(id: ID!): Node");
622
+ globalThis.expect(domainBaseSourceSdl).not.toContain("node(id: ID!): Node");
623
+ globalThis.expect(adminSourceSdl).toContain("interface Node");
624
+ });
625
+ globalThis.test("admin source document stamps @canonical on the shared traversal types", () => {
626
+ globalThis.expect(adminSourceSdl).toContain("type PageInfo @aws_cognito_user_pools @aws_iam @canonical {");
627
+ globalThis.expect(adminSourceSdl).toContain("interface Node @canonical {");
628
+ globalThis.expect(adminSourceSdl).toContain("union CommandResult @canonical =");
629
+ globalThis.expect(adminSourceSdl).toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam @canonical {");
630
+ });
631
+ globalThis.test("admin source document keeps admin fields and shared-type IAM stamps", () => {
632
+ globalThis.expect(adminSourceSdl).toContain("Platform_Plugin");
633
+ globalThis.expect(adminSourceSdl).toContain("type PageInfo @aws_cognito_user_pools @aws_iam");
634
+ });
635
+ globalThis.test("Domain base source document (split mode) is relay types + Platform_ping only", () => {
636
+ globalThis.expect(domainBaseSourceSdl).toContain("Platform_ping: String");
637
+ globalThis.expect(domainBaseSourceSdl).toContain("interface Node @canonical {");
638
+ globalThis.expect(domainBaseSourceSdl).toContain("type PageInfo");
639
+ globalThis.expect(domainBaseSourceSdl).not.toContain("Platform_Plugin");
640
+ globalThis.expect(domainBaseSourceSdl).not.toContain("union CommandResult");
641
+ });
642
+ let pluginFragment = GraphQL_Stitcher$ReventlessCore.encode({
643
+ types: [
644
+ "type MyPlugin_Item implements Node {\n id: ID!\n name: String!\n}",
645
+ "type CommandAccepted {\n msgId: ID!\n eventCount: Int!\n}"
646
+ ],
647
+ mutations: ["MyPlugin_Item_Create(name: String!): CommandResult"],
648
+ queries: ["MyPlugin_Item(id: ID!): MyPlugin_Item"],
649
+ subscriptions: ["onMyPlugin_Item_Create(id: ID): CommandResult"],
650
+ subscriptionSources: [{
651
+ field: "onMyPlugin_Item_Create",
652
+ mutations: ["MyPlugin_Item_Create"]
653
+ }]
654
+ });
655
+ globalThis.test("plugin subgraph document is standalone: relay types included, node omitted", () => {
656
+ let sdl = AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(pluginFragment);
657
+ globalThis.expect(sdl).toContain("interface Node");
658
+ globalThis.expect(sdl).toContain("type PageInfo");
659
+ globalThis.expect(sdl).toContain("MyPlugin_Item_Create");
660
+ globalThis.expect(sdl).not.toContain("node(id: ID!): Node");
661
+ });
662
+ globalThis.test("plugin subgraph document carries @aws_subscribe + shared-type IAM stamps, no @canonical", () => {
663
+ let sdl = AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(pluginFragment);
664
+ globalThis.expect(sdl).toContain(`@aws_subscribe(mutations: ["MyPlugin_Item_Create"])`);
665
+ globalThis.expect(sdl).toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam {");
666
+ globalThis.expect(sdl).not.toContain("@canonical");
667
+ });
668
+ });
669
+
670
+ globalThis.describe("AppSync_Adapter.waitForMergeSuccess", () => {
671
+ let makeFakeClient = responses => {
672
+ let i = {
673
+ contents: 0
674
+ };
675
+ return {
676
+ send: _cmd => {
677
+ let idx = Math.min(i.contents, responses.length - 1 | 0);
678
+ i.contents = i.contents + 1 | 0;
679
+ return Promise.resolve(responses[idx]);
680
+ }
681
+ };
682
+ };
683
+ globalThis.test("resolves once the association reports MERGE_SUCCESS", async () => {
684
+ let client = makeFakeClient([
685
+ {
686
+ sourceApiAssociation: {
687
+ sourceApiAssociationStatus: "MERGE_SCHEDULED",
688
+ sourceApiAssociationStatusDetail: undefined
689
+ }
690
+ },
691
+ {
692
+ sourceApiAssociation: {
693
+ sourceApiAssociationStatus: "MERGE_IN_PROGRESS",
694
+ sourceApiAssociationStatusDetail: undefined
695
+ }
696
+ },
697
+ {
698
+ sourceApiAssociation: {
699
+ sourceApiAssociationStatus: "MERGE_SUCCESS",
700
+ sourceApiAssociationStatusDetail: undefined
701
+ }
702
+ }
703
+ ]);
704
+ return await AppSync_Adapter$ReventlessAws.waitForMergeSuccess(client, "assoc-1", "merged-1", undefined, undefined, 1);
705
+ });
706
+ globalThis.test("throws with the AWS status detail on MERGE_FAILED", async () => {
707
+ let client = makeFakeClient([{
708
+ sourceApiAssociation: {
709
+ sourceApiAssociationStatus: "MERGE_FAILED",
710
+ sourceApiAssociationStatusDetail: "Unable to resolve conflict on object with name SharedThing.x"
711
+ }
712
+ }]);
713
+ let message = "";
714
+ try {
715
+ await AppSync_Adapter$ReventlessAws.waitForMergeSuccess(client, "assoc-1", "merged-1", undefined, undefined, 1);
716
+ } catch (raw_exn) {
717
+ let exn = Primitive_exceptions.internalToException(raw_exn);
718
+ message = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "(no message)");
719
+ }
720
+ globalThis.expect(message).toContain("MERGE_FAILED");
721
+ globalThis.expect(message).toContain("SharedThing.x");
722
+ });
723
+ globalThis.test("times out after maxAttempts on a status that never settles", async () => {
724
+ let client = makeFakeClient([{
725
+ sourceApiAssociation: {
726
+ sourceApiAssociationStatus: "MERGE_IN_PROGRESS",
727
+ sourceApiAssociationStatusDetail: undefined
728
+ }
729
+ }]);
730
+ let message = "";
731
+ try {
732
+ await AppSync_Adapter$ReventlessAws.waitForMergeSuccess(client, "assoc-1", "merged-1", 2, undefined, 1);
733
+ } catch (raw_exn) {
734
+ let exn = Primitive_exceptions.internalToException(raw_exn);
735
+ message = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "(no message)");
736
+ }
737
+ globalThis.expect(message).toContain("timed out");
738
+ globalThis.expect(message).toContain("MERGE_IN_PROGRESS");
739
+ });
740
+ });
741
+
606
742
  export {
607
743
  decodeFragment,
608
744
  }
@@ -67,10 +67,10 @@ describe("AppSync_SdlDecorate.injectAwsSubscribe", () => {
67
67
  })
68
68
  })
69
69
 
70
- describe("AppSync_Adapter.stitchWithAwsDirectives", () => {
71
- testSync("assembles neutral fragments into AWS-dialect SDL", () => {
72
- let baseFragment = ReventlessCore.GraphQL_Stitcher.encode({
73
- types: [],
70
+ describe("AppSync_Adapter.stitchStandaloneWithAwsDirectives", () => {
71
+ testSync("assembles a neutral fragment into an AWS-dialect standalone document", () => {
72
+ let fragment = ReventlessCore.GraphQL_Stitcher.encode({
73
+ types: [`type PluginStatusChangeEvent {\n pluginId: ID!\n}`],
74
74
  mutations: [` Platform_PluginStatusChanged(pluginId: ID!, status: PluginStatus!): PluginStatusChangeEvent`],
75
75
  queries: [],
76
76
  subscriptions: [` onPluginStatusChange: PluginStatusChangeEvent`],
@@ -78,19 +78,11 @@ describe("AppSync_Adapter.stitchWithAwsDirectives", () => {
78
78
  {field: "onPluginStatusChange", mutations: ["Platform_PluginStatusChanged"]},
79
79
  ],
80
80
  })
81
- let pluginFragment = ReventlessCore.GraphQL_Stitcher.encode({
82
- types: [`type PluginStatusChangeEvent {\n pluginId: ID!\n}`],
83
- mutations: [` Catalog_AddProduct(input: AddInput): CommandResult`],
84
- queries: [],
85
- subscriptions: [` onCatalog_AddProduct(id: ID): CommandResult`],
86
- subscriptionSources: [{field: "onCatalog_AddProduct", mutations: ["Catalog_AddProduct"]}],
87
- })
88
- let sdl = AppSync_Adapter.stitchWithAwsDirectives(
89
- ~baseFragment,
90
- ~pluginFragments=[pluginFragment],
91
- )
81
+ let sdl = AppSync_Adapter.stitchStandaloneWithAwsDirectives(~fragment)
92
82
  expect(sdl)->toContain(`@aws_subscribe(mutations: ["Platform_PluginStatusChanged"])`)
93
- expect(sdl)->toContain(`@aws_subscribe(mutations: ["Catalog_AddProduct"])`)
83
+ // Standalone documents never carry the global node query (dropped on AWS —
84
+ // see the merged-api plan's Relay node resolution).
85
+ expect(sdl)->not_->toContain("node(id: ID!): Node")
94
86
  })
95
87
  })
96
88
 
@@ -126,82 +118,37 @@ describe("AppSync_SdlDecorate.injectAwsAuthAll", () => {
126
118
  })
127
119
  })
128
120
 
129
- describe("AppSync_SdlDecorate.planAwsPushes", () => {
130
- // Neutral admin base: a system-callable mutation + a shared traversal type.
131
- let rawAdminBase = ReventlessCore.GraphQL_Stitcher.encode({
132
- types: [`type CommandAccepted {\n id: ID!\n}`],
133
- mutations: [` Platform_RegisterApiFragment(input: ApiFragmentInput): CommandResult`],
134
- queries: [],
135
- subscriptions: [],
136
- subscriptionSources: [],
137
- })
138
- let iamFieldNames = ["Platform_RegisterApiFragment"]
139
- let mkFrag = (~mutation, ~target): AppSync_SdlDecorate.targetedFragmentInput => {
140
- encoded: ReventlessCore.GraphQL_Stitcher.encode({
141
- types: [],
142
- mutations: [mutation],
143
- queries: [],
144
- subscriptions: [],
145
- subscriptionSources: [],
146
- }).encoded,
147
- protocol: "graphql",
148
- target,
149
- }
150
- let platformFrag = mkFrag(~mutation=` Inspector_Ping: CommandResult`, ~target="Platform")
151
- let domainFrag = mkFrag(~mutation=` Catalog_AddProduct(input: AddInput): CommandResult`, ~target="Domain")
152
-
153
- testSync("split mode: Platform API carries admin base (dual-auth) + Platform frags; Domain API excludes the admin base", () => {
154
- let plans = AppSync_SdlDecorate.planAwsPushes(
155
- ~rawAdminBase,
156
- ~iamFieldNames,
157
- ~fragments=[platformFrag, domainFrag],
158
- ~splitApi=true,
159
- )
160
- expect(plans->Array.length)->toBe(2)
161
- let platformPlan = plans->Array.find(p => p.api == "PlatformApi")->Option.getOrThrow
162
- let domainPlan = plans->Array.find(p => p.api == "DomainApi")->Option.getOrThrow
163
- // Admin base mutation lands on the Platform API with dual-auth (system caller).
164
- expect(platformPlan.sdl)->toContain("Platform_RegisterApiFragment")
165
- expect(platformPlan.sdl)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"]) @aws_iam`)
166
- // Platform-target plugin field present on the Platform API.
167
- expect(platformPlan.sdl)->toContain("Inspector_Ping")
168
- // Shared traversal type stamped once on the assembled SDL.
169
- expect(platformPlan.sdl)->toContain(`type CommandAccepted @aws_cognito_user_pools @aws_iam {`)
170
- // Domain-target field on the Domain API; admin base absent (empty base in split mode).
171
- expect(domainPlan.sdl)->toContain("Catalog_AddProduct")
172
- expect(domainPlan.sdl->String.includes("Platform_RegisterApiFragment"))->toBe(false)
173
- })
174
121
 
175
- testSync("unified mode: a single Domain API carries admin base + all frags", () => {
176
- let plans = AppSync_SdlDecorate.planAwsPushes(
177
- ~rawAdminBase,
178
- ~iamFieldNames,
179
- ~fragments=[platformFrag, domainFrag],
180
- ~splitApi=false,
181
- )
182
- expect(plans->Array.length)->toBe(1)
183
- let plan = plans->Array.getUnsafe(0)
184
- expect(plan.api)->toBe("DomainApi")
185
- expect(plan.sdl)->toContain("Platform_RegisterApiFragment")
186
- expect(plan.sdl)->toContain("Inspector_Ping")
187
- expect(plan.sdl)->toContain("Catalog_AddProduct")
122
+ describe("AppSync_SdlDecorate.stampSharedIamTypes", () => {
123
+ testSync("stamps the CommandResult members", () => {
124
+ let sdl = AppSync_SdlDecorate.stampSharedIamTypes("type CommandAccepted {\n id: ID!\n}")
125
+ expect(sdl)->toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam {")
188
126
  })
189
127
  })
190
128
 
191
- describe("AppSync_SdlDecorate.stampSharedIamTypes", () => {
192
- // Regression: the deploy waiter polls the IAM-callable Platform_ApiFragments query via
193
- // SigV4; its return type Platform_ApiFragmentEntry must carry the type-level @aws_iam or
194
- // the SigV4 caller reaches the query field but gets "Not Authorized to access <field> on
195
- // type Platform_ApiFragmentEntry" (deploy validation #7).
196
- testSync("stamps Platform_ApiFragmentEntry with dual-auth", () => {
197
- let sdl = AppSync_SdlDecorate.stampSharedIamTypes(
198
- "type Platform_ApiFragmentEntry {\n pluginId: String!\n pushStatus: String!\n}",
129
+ describe("AppSync_SdlDecorate.stampCanonicalTypes", () => {
130
+ testSync("stamps shared object types, the Node interface, and the CommandResult union", () => {
131
+ let sdl = AppSync_SdlDecorate.stampCanonicalTypes(
132
+ "interface Node {\n id: ID!\n}\n\ntype PageInfo {\n hasNextPage: Boolean!\n}\n\nunion CommandResult = CommandAccepted | CommandRejected | CommandPending\n\ntype CommandAccepted {\n msgId: ID!\n}",
199
133
  )
200
- expect(sdl)->toContain("type Platform_ApiFragmentEntry @aws_cognito_user_pools @aws_iam {")
134
+ expect(sdl)->toContain("interface Node @canonical {")
135
+ expect(sdl)->toContain("type PageInfo @canonical {")
136
+ expect(sdl)->toContain("union CommandResult @canonical = CommandAccepted")
137
+ expect(sdl)->toContain("type CommandAccepted @canonical {")
201
138
  })
202
139
 
203
- testSync("still stamps the CommandResult members", () => {
204
- let sdl = AppSync_SdlDecorate.stampSharedIamTypes("type CommandAccepted {\n id: ID!\n}")
205
- expect(sdl)->toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam {")
140
+ testSync("composes after stampSharedIamTypes and leaves other types alone", () => {
141
+ let sdl =
142
+ AppSync_SdlDecorate.stampSharedIamTypes(
143
+ "type PageInfo {\n hasNextPage: Boolean!\n}\n\ntype Product {\n id: ID!\n}",
144
+ )->AppSync_SdlDecorate.stampCanonicalTypes
145
+ expect(sdl)->toContain("type PageInfo @aws_cognito_user_pools @aws_iam @canonical {")
146
+ expect(sdl)->toContain("type Product {")
147
+ expect(sdl->String.includes("type Product @canonical"))->toBe(false)
148
+ })
149
+
150
+ testSync("is idempotent", () => {
151
+ let once = AppSync_SdlDecorate.stampCanonicalTypes("type PageInfo {\n x: Int\n}")
152
+ expect(AppSync_SdlDecorate.stampCanonicalTypes(once))->toBe(once)
206
153
  })
207
154
  })