@reventlessdev/reventless-aws 3.0.0-alpha.208 → 3.0.0-alpha.210

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.
@@ -25,16 +25,11 @@ let configRef = {
25
25
  contents: {
26
26
  eventTopicArn: undefined,
27
27
  pluginReadModelTableName: undefined,
28
- pluginSchemaPersistenceTableName: undefined,
29
28
  schedulerRoleArn: undefined,
30
29
  schedulerQueueArn: undefined,
31
30
  schedulerQueueName: undefined,
32
31
  appSyncApiId: undefined,
33
- clonerEnabled: false,
34
- platformApiId: undefined,
35
- apiFragmentRegistryTableName: undefined,
36
- adminDcbCmdTopicUrl: undefined,
37
- splitApi: false
32
+ clonerEnabled: false
38
33
  }
39
34
  };
40
35
 
@@ -108,22 +103,16 @@ function setStateChangesConfig(sync, async, param) {
108
103
  });
109
104
  }
110
105
 
111
- function registerConfig(eventTopicArn, pluginReadModelTableName, pluginSchemaPersistenceTableName, schedulerRoleArn, schedulerQueueArn, schedulerQueueName, appSyncApiId, clonerEnabledOpt, platformApiId, apiFragmentRegistryTableName, adminDcbCmdTopicUrl, splitApiOpt, param) {
106
+ function registerConfig(eventTopicArn, pluginReadModelTableName, schedulerRoleArn, schedulerQueueArn, schedulerQueueName, appSyncApiId, clonerEnabledOpt, param) {
112
107
  let clonerEnabled = clonerEnabledOpt !== undefined ? clonerEnabledOpt : false;
113
- let splitApi = splitApiOpt !== undefined ? splitApiOpt : false;
114
108
  configRef.contents = {
115
109
  eventTopicArn: eventTopicArn,
116
110
  pluginReadModelTableName: pluginReadModelTableName,
117
- pluginSchemaPersistenceTableName: pluginSchemaPersistenceTableName,
118
111
  schedulerRoleArn: schedulerRoleArn,
119
112
  schedulerQueueArn: schedulerQueueArn,
120
113
  schedulerQueueName: schedulerQueueName,
121
114
  appSyncApiId: appSyncApiId,
122
- clonerEnabled: clonerEnabled,
123
- platformApiId: platformApiId,
124
- apiFragmentRegistryTableName: apiFragmentRegistryTableName,
125
- adminDcbCmdTopicUrl: adminDcbCmdTopicUrl,
126
- splitApi: splitApi
115
+ clonerEnabled: clonerEnabled
127
116
  };
128
117
  }
129
118
 
@@ -209,11 +198,7 @@ function Make(EventCollectorChannel) {
209
198
  outputOrPlaceholder(config.schedulerQueueArn),
210
199
  outputOrPlaceholder(config.schedulerQueueName),
211
200
  outputOrPlaceholder(config.appSyncApiId),
212
- epEventTopicArnsOutput,
213
- outputOrPlaceholder(config.pluginSchemaPersistenceTableName),
214
- outputOrPlaceholder(config.platformApiId),
215
- outputOrPlaceholder(config.apiFragmentRegistryTableName),
216
- outputOrPlaceholder(config.adminDcbCmdTopicUrl)
201
+ epEventTopicArnsOutput
217
202
  ]).apply(values => {
218
203
  let queueUrl = values[0];
219
204
  let pluginEpCmdTopicUrl = values[1];
@@ -224,21 +209,12 @@ function Make(EventCollectorChannel) {
224
209
  let schedQueueName = values[6];
225
210
  let appSyncApiId = values[7];
226
211
  let epEventTopicArns = values[8];
227
- let schemaPersistenceTable = values[9];
228
- let platformApiId = values[10];
229
- let apiFragmentRegistryTable = values[11];
230
- let adminDcbCmdTopicUrl = values[12];
231
212
  let dict = {};
232
213
  dict["queueUrl"] = queueUrl;
233
214
  dict["pluginExtensionPointCmdTopicUrl"] = pluginEpCmdTopicUrl;
234
215
  dict["eventTopicArn"] = topLevelEventTopicArn;
235
216
  dict["pluginReadModelTableName"] = rmTable;
236
- dict["pluginSchemaPersistenceTableName"] = schemaPersistenceTable;
237
217
  dict["appSyncApiId"] = appSyncApiId;
238
- dict["platformApiId"] = platformApiId;
239
- dict["apiFragmentRegistryTableName"] = apiFragmentRegistryTable;
240
- dict["adminDcbCmdTopicUrl"] = adminDcbCmdTopicUrl;
241
- dict["splitApi"] = config.splitApi;
242
218
  dict["clonerEnabled"] = config.clonerEnabled;
243
219
  dict["schedulerRoleArn"] = schedRoleArn;
244
220
  dict["schedulerQueueArn"] = schedQueueArn;
@@ -383,58 +359,6 @@ function Make(EventCollectorChannel) {
383
359
  role: runtime.parts.lambdaRole.id
384
360
  });
385
361
  }
386
- let schemaTableOutput = config.pluginSchemaPersistenceTableName;
387
- if (schemaTableOutput !== undefined) {
388
- let policyJson$1 = schemaTableOutput.apply(tableName => PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `PluginSchemaScanPolicy`, [{
389
- Sid: "AllowAdminScanPluginSchemaPersistence",
390
- Effect: "Allow",
391
- Action: ["dynamodb:Scan"],
392
- Resource: "arn:aws:dynamodb:*:*:table/" + tableName
393
- }])));
394
- new (Aws.iam.RolePolicy)(name + `-pluginSchemaScan`, {
395
- policy: policyJson$1,
396
- role: runtime.parts.lambdaRole.id
397
- });
398
- }
399
- let tableOutput = config.apiFragmentRegistryTableName;
400
- if (tableOutput !== undefined) {
401
- let policyJson$2 = tableOutput.apply(tableName => PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `ApiFragmentsScanPolicy`, [{
402
- Sid: "AllowAdminScanApiFragments",
403
- Effect: "Allow",
404
- Action: ["dynamodb:Scan"],
405
- Resource: "arn:aws:dynamodb:*:*:table/" + tableName
406
- }])));
407
- new (Aws.iam.RolePolicy)(name + `-apiFragmentsScan`, {
408
- policy: policyJson$2,
409
- role: runtime.parts.lambdaRole.id
410
- });
411
- }
412
- let urlOutput = config.adminDcbCmdTopicUrl;
413
- if (urlOutput !== undefined) {
414
- let policyJson$3 = urlOutput.apply(url => {
415
- let match = url.split("/");
416
- let arn;
417
- if (match.length !== 5) {
418
- arn = url;
419
- } else {
420
- let host = match[2];
421
- let acct = match[3];
422
- let qn = match[4];
423
- let region = Stdlib_Option.getOr(host.split(".")[1], "eu-west-1");
424
- arn = `arn:aws:sqs:` + region + `:` + acct + `:` + qn;
425
- }
426
- return PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `AdminDcbSendPolicy`, [{
427
- Sid: "AllowAdminDispatchRecordApiFragmentPush",
428
- Effect: "Allow",
429
- Action: ["sqs:SendMessage"],
430
- Resource: arn
431
- }]));
432
- });
433
- new (Aws.iam.RolePolicy)(name + `-adminDcbSend`, {
434
- policy: policyJson$3,
435
- role: runtime.parts.lambdaRole.id
436
- });
437
- }
438
362
  }
439
363
  let aggregateNameSet = {};
440
364
  context.extensions.forEach(ext => {
@@ -652,3 +652,179 @@ 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.stitchWithAwsDirectives(~baseFragment, ~pluginFragments=[])
667
+ ->AppSync_SdlDecorate.stampCanonicalTypes
668
+
669
+ let emptyBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
670
+ types: [],
671
+ mutations: [],
672
+ queries: [],
673
+ subscriptions: [],
674
+ subscriptionSources: [],
675
+ })
676
+
677
+ let adminSourceSdl = assembleCanonicalSourceSdl(
678
+ ~baseFragment=AppSync_Adapter.injectAwsAuthAll(
679
+ ReventlessCore.AdminApi.baseFragment(~cloner=false),
680
+ ~group="Admin",
681
+ ~iamFieldNames=ReventlessCore.AdminApi.systemCallerFieldNames,
682
+ ),
683
+ )
684
+
685
+ let relayBaseSourceSdl = assembleCanonicalSourceSdl(~baseFragment=emptyBaseFragment)
686
+
687
+ testSync("admin source document carries the global node query", () => {
688
+ expect(adminSourceSdl)->toContain("node(id: ID!): Node")
689
+ })
690
+
691
+ testSync("admin source document stamps @canonical on the shared traversal types", () => {
692
+ expect(adminSourceSdl)->toContain("type PageInfo @aws_cognito_user_pools @aws_iam @canonical {")
693
+ expect(adminSourceSdl)->toContain("interface Node @canonical {")
694
+ expect(adminSourceSdl)->toContain("union CommandResult @canonical =")
695
+ expect(adminSourceSdl)->toContain(
696
+ "type CommandAccepted @aws_cognito_user_pools @aws_iam @canonical {",
697
+ )
698
+ })
699
+
700
+ testSync("admin source document keeps admin fields and dual-auth stamps", () => {
701
+ expect(adminSourceSdl)->toContain("Platform_Plugin")
702
+ expect(adminSourceSdl)->toContain("@aws_iam")
703
+ })
704
+
705
+ testSync("relay-base source document (split-mode Domain source) is node + relay types only", () => {
706
+ expect(relayBaseSourceSdl)->toContain("node(id: ID!): Node")
707
+ expect(relayBaseSourceSdl)->toContain("interface Node @canonical {")
708
+ expect(relayBaseSourceSdl)->toContain("type PageInfo")
709
+ // No admin or plugin fields on the split-mode Domain source.
710
+ expect(relayBaseSourceSdl)->not_->toContain("Platform_Plugin")
711
+ // No mutations → no CommandResult family on this source.
712
+ expect(relayBaseSourceSdl)->not_->toContain("union CommandResult")
713
+ })
714
+
715
+ // Phase 4: the plugin-stack subgraph document pushed to the plugin's own
716
+ // source API.
717
+ let pluginFragment = ReventlessCore.GraphQL_Stitcher.encode({
718
+ types: [
719
+ "type MyPlugin_Item implements Node {\n id: ID!\n name: String!\n}",
720
+ "type CommandAccepted {\n msgId: ID!\n eventCount: Int!\n}",
721
+ ],
722
+ mutations: ["MyPlugin_Item_Create(name: String!): CommandResult"],
723
+ queries: ["MyPlugin_Item(id: ID!): MyPlugin_Item"],
724
+ subscriptions: ["onMyPlugin_Item_Create(id: ID): CommandResult"],
725
+ subscriptionSources: [
726
+ {field: "onMyPlugin_Item_Create", mutations: ["MyPlugin_Item_Create"]},
727
+ ],
728
+ })
729
+
730
+ testSync("plugin subgraph document is standalone: relay types included, node omitted", () => {
731
+ let sdl = AppSync_Adapter.stitchStandaloneWithAwsDirectives(~fragment=pluginFragment)
732
+ expect(sdl)->toContain("interface Node")
733
+ expect(sdl)->toContain("type PageInfo")
734
+ expect(sdl)->toContain("MyPlugin_Item_Create")
735
+ expect(sdl)->not_->toContain("node(id: ID!): Node")
736
+ })
737
+
738
+ testSync("plugin subgraph document carries @aws_subscribe + shared-type IAM stamps, no @canonical", () => {
739
+ let sdl = AppSync_Adapter.stitchStandaloneWithAwsDirectives(~fragment=pluginFragment)
740
+ expect(sdl)->toContain(`@aws_subscribe(mutations: ["MyPlugin_Item_Create"])`)
741
+ expect(sdl)->toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam {")
742
+ // Plugin subgraphs stay unstamped — the admin source's canonical defs win.
743
+ expect(sdl)->not_->toContain("@canonical")
744
+ })
745
+ })
746
+
747
+ // ── waitForMergeSuccess — association merge-status poll ─────────────────────
748
+
749
+ describe("AppSync_Adapter.waitForMergeSuccess", () => {
750
+ // Fake AppSync client: `send` yields the canned responses in order and
751
+ // repeats the last one when the poll outruns the script.
752
+ let makeFakeClient = (
753
+ responses: array<AppSync_Adapter.getSourceApiAssociationResult>,
754
+ ): AppSync_Adapter.appSyncClient => {
755
+ let i = ref(0)
756
+ let fake = {
757
+ "send": _cmd => {
758
+ let idx = Math.Int.min(i.contents, responses->Array.length - 1)
759
+ i := i.contents + 1
760
+ Promise.resolve(responses->Array.getUnsafe(idx))
761
+ },
762
+ }
763
+ fake->Obj.magic
764
+ }
765
+
766
+ let response = (~status, ~detail=?): AppSync_Adapter.getSourceApiAssociationResult => {
767
+ sourceApiAssociation: Some({
768
+ sourceApiAssociationStatus: Some(status),
769
+ sourceApiAssociationStatusDetail: detail,
770
+ }),
771
+ }
772
+
773
+ test("resolves once the association reports MERGE_SUCCESS", async () => {
774
+ let client = makeFakeClient([
775
+ response(~status="MERGE_SCHEDULED"),
776
+ response(~status="MERGE_IN_PROGRESS"),
777
+ response(~status="MERGE_SUCCESS"),
778
+ ])
779
+ await AppSync_Adapter.waitForMergeSuccess(
780
+ client,
781
+ ~associationId="assoc-1",
782
+ ~mergedApiIdentifier="merged-1",
783
+ ~delayMs=1,
784
+ )
785
+ })
786
+
787
+ test("throws with the AWS status detail on MERGE_FAILED", async () => {
788
+ let client = makeFakeClient([
789
+ response(
790
+ ~status="MERGE_FAILED",
791
+ ~detail="Unable to resolve conflict on object with name SharedThing.x",
792
+ ),
793
+ ])
794
+ let message = ref("")
795
+ try {
796
+ await AppSync_Adapter.waitForMergeSuccess(
797
+ client,
798
+ ~associationId="assoc-1",
799
+ ~mergedApiIdentifier="merged-1",
800
+ ~delayMs=1,
801
+ )
802
+ } catch {
803
+ | exn =>
804
+ message :=
805
+ exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("(no message)")
806
+ }
807
+ expect(message.contents)->toContain("MERGE_FAILED")
808
+ expect(message.contents)->toContain("SharedThing.x")
809
+ })
810
+
811
+ test("times out after maxAttempts on a status that never settles", async () => {
812
+ let client = makeFakeClient([response(~status="MERGE_IN_PROGRESS")])
813
+ let message = ref("")
814
+ try {
815
+ await AppSync_Adapter.waitForMergeSuccess(
816
+ client,
817
+ ~associationId="assoc-1",
818
+ ~mergedApiIdentifier="merged-1",
819
+ ~maxAttempts=2,
820
+ ~delayMs=1,
821
+ )
822
+ } catch {
823
+ | exn =>
824
+ message :=
825
+ exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("(no message)")
826
+ }
827
+ expect(message.contents)->toContain("timed out")
828
+ expect(message.contents)->toContain("MERGE_IN_PROGRESS")
829
+ })
830
+ })
@@ -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,137 @@ globalThis.describe("Split mode — empty base fragment", () => {
603
606
  });
604
607
  });
605
608
 
609
+ globalThis.describe("Merged mode — canonical source documents", () => {
610
+ let emptyBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
611
+ types: [],
612
+ mutations: [],
613
+ queries: [],
614
+ subscriptions: [],
615
+ subscriptionSources: []
616
+ });
617
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin", AdminApi$ReventlessCore.systemCallerFieldNames);
618
+ let adminSourceSdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchWithAwsDirectives(baseFragment, []));
619
+ let relayBaseSourceSdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchWithAwsDirectives(emptyBaseFragment, []));
620
+ globalThis.test("admin source document carries the global node query", () => {
621
+ globalThis.expect(adminSourceSdl).toContain("node(id: ID!): Node");
622
+ });
623
+ globalThis.test("admin source document stamps @canonical on the shared traversal types", () => {
624
+ globalThis.expect(adminSourceSdl).toContain("type PageInfo @aws_cognito_user_pools @aws_iam @canonical {");
625
+ globalThis.expect(adminSourceSdl).toContain("interface Node @canonical {");
626
+ globalThis.expect(adminSourceSdl).toContain("union CommandResult @canonical =");
627
+ globalThis.expect(adminSourceSdl).toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam @canonical {");
628
+ });
629
+ globalThis.test("admin source document keeps admin fields and dual-auth stamps", () => {
630
+ globalThis.expect(adminSourceSdl).toContain("Platform_Plugin");
631
+ globalThis.expect(adminSourceSdl).toContain("@aws_iam");
632
+ });
633
+ globalThis.test("relay-base source document (split-mode Domain source) is node + relay types only", () => {
634
+ globalThis.expect(relayBaseSourceSdl).toContain("node(id: ID!): Node");
635
+ globalThis.expect(relayBaseSourceSdl).toContain("interface Node @canonical {");
636
+ globalThis.expect(relayBaseSourceSdl).toContain("type PageInfo");
637
+ globalThis.expect(relayBaseSourceSdl).not.toContain("Platform_Plugin");
638
+ globalThis.expect(relayBaseSourceSdl).not.toContain("union CommandResult");
639
+ });
640
+ let pluginFragment = GraphQL_Stitcher$ReventlessCore.encode({
641
+ types: [
642
+ "type MyPlugin_Item implements Node {\n id: ID!\n name: String!\n}",
643
+ "type CommandAccepted {\n msgId: ID!\n eventCount: Int!\n}"
644
+ ],
645
+ mutations: ["MyPlugin_Item_Create(name: String!): CommandResult"],
646
+ queries: ["MyPlugin_Item(id: ID!): MyPlugin_Item"],
647
+ subscriptions: ["onMyPlugin_Item_Create(id: ID): CommandResult"],
648
+ subscriptionSources: [{
649
+ field: "onMyPlugin_Item_Create",
650
+ mutations: ["MyPlugin_Item_Create"]
651
+ }]
652
+ });
653
+ globalThis.test("plugin subgraph document is standalone: relay types included, node omitted", () => {
654
+ let sdl = AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(pluginFragment);
655
+ globalThis.expect(sdl).toContain("interface Node");
656
+ globalThis.expect(sdl).toContain("type PageInfo");
657
+ globalThis.expect(sdl).toContain("MyPlugin_Item_Create");
658
+ globalThis.expect(sdl).not.toContain("node(id: ID!): Node");
659
+ });
660
+ globalThis.test("plugin subgraph document carries @aws_subscribe + shared-type IAM stamps, no @canonical", () => {
661
+ let sdl = AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(pluginFragment);
662
+ globalThis.expect(sdl).toContain(`@aws_subscribe(mutations: ["MyPlugin_Item_Create"])`);
663
+ globalThis.expect(sdl).toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam {");
664
+ globalThis.expect(sdl).not.toContain("@canonical");
665
+ });
666
+ });
667
+
668
+ globalThis.describe("AppSync_Adapter.waitForMergeSuccess", () => {
669
+ let makeFakeClient = responses => {
670
+ let i = {
671
+ contents: 0
672
+ };
673
+ return {
674
+ send: _cmd => {
675
+ let idx = Math.min(i.contents, responses.length - 1 | 0);
676
+ i.contents = i.contents + 1 | 0;
677
+ return Promise.resolve(responses[idx]);
678
+ }
679
+ };
680
+ };
681
+ globalThis.test("resolves once the association reports MERGE_SUCCESS", async () => {
682
+ let client = makeFakeClient([
683
+ {
684
+ sourceApiAssociation: {
685
+ sourceApiAssociationStatus: "MERGE_SCHEDULED",
686
+ sourceApiAssociationStatusDetail: undefined
687
+ }
688
+ },
689
+ {
690
+ sourceApiAssociation: {
691
+ sourceApiAssociationStatus: "MERGE_IN_PROGRESS",
692
+ sourceApiAssociationStatusDetail: undefined
693
+ }
694
+ },
695
+ {
696
+ sourceApiAssociation: {
697
+ sourceApiAssociationStatus: "MERGE_SUCCESS",
698
+ sourceApiAssociationStatusDetail: undefined
699
+ }
700
+ }
701
+ ]);
702
+ return await AppSync_Adapter$ReventlessAws.waitForMergeSuccess(client, "assoc-1", "merged-1", undefined, undefined, 1);
703
+ });
704
+ globalThis.test("throws with the AWS status detail on MERGE_FAILED", async () => {
705
+ let client = makeFakeClient([{
706
+ sourceApiAssociation: {
707
+ sourceApiAssociationStatus: "MERGE_FAILED",
708
+ sourceApiAssociationStatusDetail: "Unable to resolve conflict on object with name SharedThing.x"
709
+ }
710
+ }]);
711
+ let message = "";
712
+ try {
713
+ await AppSync_Adapter$ReventlessAws.waitForMergeSuccess(client, "assoc-1", "merged-1", undefined, undefined, 1);
714
+ } catch (raw_exn) {
715
+ let exn = Primitive_exceptions.internalToException(raw_exn);
716
+ message = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "(no message)");
717
+ }
718
+ globalThis.expect(message).toContain("MERGE_FAILED");
719
+ globalThis.expect(message).toContain("SharedThing.x");
720
+ });
721
+ globalThis.test("times out after maxAttempts on a status that never settles", async () => {
722
+ let client = makeFakeClient([{
723
+ sourceApiAssociation: {
724
+ sourceApiAssociationStatus: "MERGE_IN_PROGRESS",
725
+ sourceApiAssociationStatusDetail: undefined
726
+ }
727
+ }]);
728
+ let message = "";
729
+ try {
730
+ await AppSync_Adapter$ReventlessAws.waitForMergeSuccess(client, "assoc-1", "merged-1", 2, undefined, 1);
731
+ } catch (raw_exn) {
732
+ let exn = Primitive_exceptions.internalToException(raw_exn);
733
+ message = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "(no message)");
734
+ }
735
+ globalThis.expect(message).toContain("timed out");
736
+ globalThis.expect(message).toContain("MERGE_IN_PROGRESS");
737
+ });
738
+ });
739
+
606
740
  export {
607
741
  decodeFragment,
608
742
  }
@@ -205,3 +205,30 @@ describe("AppSync_SdlDecorate.stampSharedIamTypes", () => {
205
205
  expect(sdl)->toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam {")
206
206
  })
207
207
  })
208
+
209
+ describe("AppSync_SdlDecorate.stampCanonicalTypes", () => {
210
+ testSync("stamps shared object types, the Node interface, and the CommandResult union", () => {
211
+ let sdl = AppSync_SdlDecorate.stampCanonicalTypes(
212
+ "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}",
213
+ )
214
+ expect(sdl)->toContain("interface Node @canonical {")
215
+ expect(sdl)->toContain("type PageInfo @canonical {")
216
+ expect(sdl)->toContain("union CommandResult @canonical = CommandAccepted")
217
+ expect(sdl)->toContain("type CommandAccepted @canonical {")
218
+ })
219
+
220
+ testSync("composes after stampSharedIamTypes and leaves other types alone", () => {
221
+ let sdl =
222
+ AppSync_SdlDecorate.stampSharedIamTypes(
223
+ "type PageInfo {\n hasNextPage: Boolean!\n}\n\ntype Product {\n id: ID!\n}",
224
+ )->AppSync_SdlDecorate.stampCanonicalTypes
225
+ expect(sdl)->toContain("type PageInfo @aws_cognito_user_pools @aws_iam @canonical {")
226
+ expect(sdl)->toContain("type Product {")
227
+ expect(sdl->String.includes("type Product @canonical"))->toBe(false)
228
+ })
229
+
230
+ testSync("is idempotent", () => {
231
+ let once = AppSync_SdlDecorate.stampCanonicalTypes("type PageInfo {\n x: Int\n}")
232
+ expect(AppSync_SdlDecorate.stampCanonicalTypes(once))->toBe(once)
233
+ })
234
+ })
@@ -177,6 +177,26 @@ globalThis.describe("AppSync_SdlDecorate.stampSharedIamTypes", () => {
177
177
  });
178
178
  });
179
179
 
180
+ globalThis.describe("AppSync_SdlDecorate.stampCanonicalTypes", () => {
181
+ globalThis.test("stamps shared object types, the Node interface, and the CommandResult union", () => {
182
+ let sdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes("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}");
183
+ globalThis.expect(sdl).toContain("interface Node @canonical {");
184
+ globalThis.expect(sdl).toContain("type PageInfo @canonical {");
185
+ globalThis.expect(sdl).toContain("union CommandResult @canonical = CommandAccepted");
186
+ globalThis.expect(sdl).toContain("type CommandAccepted @canonical {");
187
+ });
188
+ globalThis.test("composes after stampSharedIamTypes and leaves other types alone", () => {
189
+ let sdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes("type PageInfo {\n hasNextPage: Boolean!\n}\n\ntype Product {\n id: ID!\n}"));
190
+ globalThis.expect(sdl).toContain("type PageInfo @aws_cognito_user_pools @aws_iam @canonical {");
191
+ globalThis.expect(sdl).toContain("type Product {");
192
+ globalThis.expect(sdl.includes("type Product @canonical")).toBe(false);
193
+ });
194
+ globalThis.test("is idempotent", () => {
195
+ let once = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes("type PageInfo {\n x: Int\n}");
196
+ globalThis.expect(AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(once)).toBe(once);
197
+ });
198
+ });
199
+
180
200
  export {
181
201
  sources,
182
202
  stitchedSdl,
@@ -7,8 +7,9 @@ describe("MCP_Lambda.generateAdminConfig", () => {
7
7
  ~serverVersion="1.0.0",
8
8
  ~cloner=false,
9
9
  )
10
- // Admin has Plugin_Activate, Plugin_Deactivate, Plugin_Retire mutations
11
- expect(config.tools)->toHaveLength(3)
10
+ // Admin has Plugin_Activate, Plugin_Deactivate, Plugin_Retire +
11
+ // ApiFragmentRegistry_Register/DeregisterApiFragment mutations
12
+ expect(config.tools)->toHaveLength(5)
12
13
  })
13
14
 
14
15
  testSync("produces additional clone tool when cloner=true", () => {
@@ -17,8 +18,9 @@ describe("MCP_Lambda.generateAdminConfig", () => {
17
18
  ~serverVersion="1.0.0",
18
19
  ~cloner=true,
19
20
  )
20
- // Plugin_Activate, Plugin_Deactivate, Plugin_Retire, Clone
21
- expect(config.tools)->toHaveLength(4)
21
+ // Plugin_Activate, Plugin_Deactivate, Plugin_Retire,
22
+ // ApiFragmentRegistry_Register/DeregisterApiFragment, Clone
23
+ expect(config.tools)->toHaveLength(6)
22
24
  })
23
25
 
24
26
  testSync("produces resources from admin query entries", () => {
@@ -6,11 +6,11 @@ import * as MCP_Lambda$ReventlessAws from "../src/adapter/Mcp/MCP_Lambda.res.mjs
6
6
  globalThis.describe("MCP_Lambda.generateAdminConfig", () => {
7
7
  globalThis.test("produces tools from admin mutation entries (no cloner)", () => {
8
8
  let config = MCP_Lambda$ReventlessAws.generateAdminConfig("test", "1.0.0", false, undefined, undefined);
9
- globalThis.expect(config.tools).toHaveLength(3);
9
+ globalThis.expect(config.tools).toHaveLength(5);
10
10
  });
11
11
  globalThis.test("produces additional clone tool when cloner=true", () => {
12
12
  let config = MCP_Lambda$ReventlessAws.generateAdminConfig("test", "1.0.0", true, undefined, undefined);
13
- globalThis.expect(config.tools).toHaveLength(4);
13
+ globalThis.expect(config.tools).toHaveLength(6);
14
14
  });
15
15
  globalThis.test("produces resources from admin query entries", () => {
16
16
  let config = MCP_Lambda$ReventlessAws.generateAdminConfig("test", "1.0.0", undefined, undefined, undefined);