@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.
- package/CHANGELOG.md +17 -0
- package/package.json +7 -7
- package/src/Platform.res +478 -545
- package/src/Platform.res.mjs +393 -717
- package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +7 -382
- package/src/adapter/Runtime/ApiSchemaPush_Runtime.mjs +8 -1
- package/src/components/Api/AppSync_Adapter.res +144 -101
- package/src/components/Api/AppSync_Adapter.res.mjs +62 -91
- package/src/components/Api/AppSync_MergedApi.res +218 -0
- package/src/components/Api/AppSync_MergedApi.res.mjs +122 -0
- package/src/components/Api/AppSync_SdlDecorate.res +47 -0
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +39 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res +0 -139
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +4 -80
- package/tests/AppSync_AdapterTest.res +176 -0
- package/tests/AppSync_AdapterTest.res.mjs +134 -0
- package/tests/AppSync_SdlDecorateTest.res +27 -0
- package/tests/AppSync_SdlDecorateTest.res.mjs +20 -0
- package/tests/MCP_LambdaTest.res +6 -4
- package/tests/MCP_LambdaTest.res.mjs +2 -2
|
@@ -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,
|
|
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,
|
package/tests/MCP_LambdaTest.res
CHANGED
|
@@ -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
|
|
11
|
-
|
|
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,
|
|
21
|
-
|
|
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(
|
|
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(
|
|
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);
|