@reventlessdev/reventless-aws 3.0.0-alpha.197 → 3.0.0-alpha.199
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 +22 -0
- package/package.json +7 -7
- package/src/Platform.res +294 -44
- package/src/Platform.res.mjs +420 -94
- package/src/adapter/Api/ApiFragmentDeregistration.res +137 -0
- package/src/adapter/Api/ApiFragmentDeregistration.res.mjs +108 -0
- package/src/adapter/Api/CommandSubscriptionResolvers_AppSync.res +4 -3
- package/src/adapter/Api/Platform_ApiFragments_Lambda.res +222 -0
- package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +202 -0
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +27 -17
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +13 -13
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +2 -1
- package/src/adapter/QueryDb/QueryDbBackend.res +1 -1
- package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +235 -9
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +4 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +5 -0
- package/src/components/Api/AppSync_Adapter.res +36 -59
- package/src/components/Api/AppSync_Adapter.res.mjs +18 -74
- package/src/components/Api/AppSync_SdlDecorate.res +189 -0
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +125 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res +107 -2
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +70 -7
- package/src/util/Util_AppSync_Caller.res +12 -4
- package/src/util/Util_AppSync_Caller.res.mjs +9 -3
- package/tests/AppSync_AdapterTest.res +6 -0
- package/tests/AppSync_AdapterTest.res.mjs +12 -6
- package/tests/AppSync_SdlDecorateTest.res +157 -0
- package/tests/AppSync_SdlDecorateTest.res.mjs +147 -0
|
@@ -30,7 +30,11 @@ let configRef = {
|
|
|
30
30
|
schedulerQueueArn: undefined,
|
|
31
31
|
schedulerQueueName: undefined,
|
|
32
32
|
appSyncApiId: undefined,
|
|
33
|
-
clonerEnabled: false
|
|
33
|
+
clonerEnabled: false,
|
|
34
|
+
platformApiId: undefined,
|
|
35
|
+
apiFragmentRegistryTableName: undefined,
|
|
36
|
+
adminDcbCmdTopicUrl: undefined,
|
|
37
|
+
splitApi: false
|
|
34
38
|
}
|
|
35
39
|
};
|
|
36
40
|
|
|
@@ -104,8 +108,9 @@ function setStateChangesConfig(sync, async, param) {
|
|
|
104
108
|
});
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
function registerConfig(eventTopicArn, pluginReadModelTableName, pluginSchemaPersistenceTableName, schedulerRoleArn, schedulerQueueArn, schedulerQueueName, appSyncApiId, clonerEnabledOpt, param) {
|
|
111
|
+
function registerConfig(eventTopicArn, pluginReadModelTableName, pluginSchemaPersistenceTableName, schedulerRoleArn, schedulerQueueArn, schedulerQueueName, appSyncApiId, clonerEnabledOpt, platformApiId, apiFragmentRegistryTableName, adminDcbCmdTopicUrl, splitApiOpt, param) {
|
|
108
112
|
let clonerEnabled = clonerEnabledOpt !== undefined ? clonerEnabledOpt : false;
|
|
113
|
+
let splitApi = splitApiOpt !== undefined ? splitApiOpt : false;
|
|
109
114
|
configRef.contents = {
|
|
110
115
|
eventTopicArn: eventTopicArn,
|
|
111
116
|
pluginReadModelTableName: pluginReadModelTableName,
|
|
@@ -114,7 +119,11 @@ function registerConfig(eventTopicArn, pluginReadModelTableName, pluginSchemaPer
|
|
|
114
119
|
schedulerQueueArn: schedulerQueueArn,
|
|
115
120
|
schedulerQueueName: schedulerQueueName,
|
|
116
121
|
appSyncApiId: appSyncApiId,
|
|
117
|
-
clonerEnabled: clonerEnabled
|
|
122
|
+
clonerEnabled: clonerEnabled,
|
|
123
|
+
platformApiId: platformApiId,
|
|
124
|
+
apiFragmentRegistryTableName: apiFragmentRegistryTableName,
|
|
125
|
+
adminDcbCmdTopicUrl: adminDcbCmdTopicUrl,
|
|
126
|
+
splitApi: splitApi
|
|
118
127
|
};
|
|
119
128
|
}
|
|
120
129
|
|
|
@@ -136,11 +145,12 @@ function Make(EventCollectorChannel) {
|
|
|
136
145
|
};
|
|
137
146
|
let synthesizeAdminContext = () => {
|
|
138
147
|
let config = configRef.contents;
|
|
139
|
-
let fakePluginDefinitionJson = Pulumi.output(`{"id":"Admin@INTERNAL","name":"Admin","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"
|
|
148
|
+
let fakePluginDefinitionJson = Pulumi.output(`{"id":"Admin@INTERNAL","name":"Admin","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"structure":null}`);
|
|
140
149
|
let arn = config.eventTopicArn;
|
|
141
150
|
let adminEpEventTopicArn = arn !== undefined ? arn : Pulumi.output("NOT_AVAILABLE");
|
|
142
151
|
return {
|
|
143
152
|
pluginDefinitionJson: fakePluginDefinitionJson,
|
|
153
|
+
uiFragmentsJson: Pulumi.output("null"),
|
|
144
154
|
extensionPoints: [{
|
|
145
155
|
specModule: Plugin_Helpers$ReventlessCore.adminPluginExtensionPointSpecModule,
|
|
146
156
|
mappingsModule: Plugin_Helpers$ReventlessCore.adminPluginExtensionPointMappingsModule,
|
|
@@ -200,7 +210,10 @@ function Make(EventCollectorChannel) {
|
|
|
200
210
|
outputOrPlaceholder(config.schedulerQueueName),
|
|
201
211
|
outputOrPlaceholder(config.appSyncApiId),
|
|
202
212
|
epEventTopicArnsOutput,
|
|
203
|
-
outputOrPlaceholder(config.pluginSchemaPersistenceTableName)
|
|
213
|
+
outputOrPlaceholder(config.pluginSchemaPersistenceTableName),
|
|
214
|
+
outputOrPlaceholder(config.platformApiId),
|
|
215
|
+
outputOrPlaceholder(config.apiFragmentRegistryTableName),
|
|
216
|
+
outputOrPlaceholder(config.adminDcbCmdTopicUrl)
|
|
204
217
|
]).apply(values => {
|
|
205
218
|
let queueUrl = values[0];
|
|
206
219
|
let pluginEpCmdTopicUrl = values[1];
|
|
@@ -212,6 +225,9 @@ function Make(EventCollectorChannel) {
|
|
|
212
225
|
let appSyncApiId = values[7];
|
|
213
226
|
let epEventTopicArns = values[8];
|
|
214
227
|
let schemaPersistenceTable = values[9];
|
|
228
|
+
let platformApiId = values[10];
|
|
229
|
+
let apiFragmentRegistryTable = values[11];
|
|
230
|
+
let adminDcbCmdTopicUrl = values[12];
|
|
215
231
|
let dict = {};
|
|
216
232
|
dict["queueUrl"] = queueUrl;
|
|
217
233
|
dict["pluginExtensionPointCmdTopicUrl"] = pluginEpCmdTopicUrl;
|
|
@@ -219,6 +235,10 @@ function Make(EventCollectorChannel) {
|
|
|
219
235
|
dict["pluginReadModelTableName"] = rmTable;
|
|
220
236
|
dict["pluginSchemaPersistenceTableName"] = schemaPersistenceTable;
|
|
221
237
|
dict["appSyncApiId"] = appSyncApiId;
|
|
238
|
+
dict["platformApiId"] = platformApiId;
|
|
239
|
+
dict["apiFragmentRegistryTableName"] = apiFragmentRegistryTable;
|
|
240
|
+
dict["adminDcbCmdTopicUrl"] = adminDcbCmdTopicUrl;
|
|
241
|
+
dict["splitApi"] = config.splitApi;
|
|
222
242
|
dict["clonerEnabled"] = config.clonerEnabled;
|
|
223
243
|
dict["schedulerRoleArn"] = schedRoleArn;
|
|
224
244
|
dict["schedulerQueueArn"] = schedQueueArn;
|
|
@@ -305,9 +325,13 @@ function Make(EventCollectorChannel) {
|
|
|
305
325
|
}
|
|
306
326
|
packageDirs["@reventlessdev/reventless-aws"] = Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-aws");
|
|
307
327
|
packageDirs["@reventlessdev/reventless-core"] = Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-core");
|
|
308
|
-
let bundleOutput =
|
|
328
|
+
let bundleOutput = Pulumi.all([
|
|
329
|
+
context.pluginDefinitionJson,
|
|
330
|
+
context.uiFragmentsJson
|
|
331
|
+
]).apply(param => {
|
|
309
332
|
let extraStringAssets = {};
|
|
310
|
-
extraStringAssets["pluginDefinition.json"] =
|
|
333
|
+
extraStringAssets["pluginDefinition.json"] = param[0];
|
|
334
|
+
extraStringAssets["uiFragments.json"] = param[1];
|
|
311
335
|
return Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs", packageDirs, extraStringAssets);
|
|
312
336
|
});
|
|
313
337
|
let codeOutput = bundleOutput.apply(b => b.code);
|
|
@@ -372,6 +396,45 @@ function Make(EventCollectorChannel) {
|
|
|
372
396
|
role: runtime.parts.lambdaRole.id
|
|
373
397
|
});
|
|
374
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
|
+
}
|
|
375
438
|
}
|
|
376
439
|
let aggregateNameSet = {};
|
|
377
440
|
context.extensions.forEach(ext => {
|
|
@@ -225,9 +225,17 @@ let sendMutation = async (
|
|
|
225
225
|
let response = await fetch(endpoint, {method: "POST", headers: signed.headers, body: signed.body})
|
|
226
226
|
|
|
227
227
|
let json = await response->responseJson
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
228
|
+
// Returns the `data` object (or None on GraphQL errors) so a caller can inspect
|
|
229
|
+
// the result — e.g. the deploy-time RegisterApiFragment caller reads the
|
|
230
|
+
// CommandResult __typename / eventCount to decide whether to wait for a push.
|
|
231
|
+
switch json->JSON.Decode.object {
|
|
232
|
+
| Some(d) =>
|
|
233
|
+
switch d->Dict.get("errors") {
|
|
234
|
+
| Some(errors) =>
|
|
235
|
+
log.error(~comp="Util_AppSync_Caller", ~data=errors, `${mutation} errors`)
|
|
236
|
+
None
|
|
237
|
+
| None => d->Dict.get("data")
|
|
238
|
+
}
|
|
239
|
+
| None => None
|
|
232
240
|
}
|
|
233
241
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
|
-
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
4
|
import * as HashNode from "@smithy/hash-node";
|
|
6
5
|
import * as SignatureV4 from "@smithy/signature-v4";
|
|
7
6
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
@@ -155,9 +154,16 @@ async function sendMutation(endpoint, region, mutation, selectionOpt, variables)
|
|
|
155
154
|
body: signed.body
|
|
156
155
|
});
|
|
157
156
|
let json = await response.json();
|
|
158
|
-
let
|
|
157
|
+
let d$1 = Stdlib_JSON.Decode.object(json);
|
|
158
|
+
if (d$1 === undefined) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let errors = d$1["errors"];
|
|
159
162
|
if (errors !== undefined) {
|
|
160
|
-
|
|
163
|
+
log.error("Util_AppSync_Caller", errors, mutation + ` errors`);
|
|
164
|
+
return;
|
|
165
|
+
} else {
|
|
166
|
+
return d$1["data"];
|
|
161
167
|
}
|
|
162
168
|
}
|
|
163
169
|
|
|
@@ -140,6 +140,7 @@ describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifting", () =>
|
|
|
140
140
|
mutations: mutationFields,
|
|
141
141
|
queries: queryFields,
|
|
142
142
|
subscriptions: [],
|
|
143
|
+
subscriptionSources: [],
|
|
143
144
|
})
|
|
144
145
|
|
|
145
146
|
let mutationEntry = (
|
|
@@ -361,6 +362,7 @@ describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth", () => {
|
|
|
361
362
|
mutations: mutationFields,
|
|
362
363
|
queries: queryFields,
|
|
363
364
|
subscriptions: [],
|
|
365
|
+
subscriptionSources: [],
|
|
364
366
|
})
|
|
365
367
|
|
|
366
368
|
testSync("systemCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
|
|
@@ -500,6 +502,7 @@ describe("AppSync_Adapter — type-level dual-auth", () => {
|
|
|
500
502
|
mutations: [],
|
|
501
503
|
queries: queryFields,
|
|
502
504
|
subscriptions: [],
|
|
505
|
+
subscriptionSources: [],
|
|
503
506
|
})
|
|
504
507
|
|
|
505
508
|
let callableEntry: ReventlessInfra.Api.querySchemaEntry = {
|
|
@@ -606,6 +609,7 @@ describe("Split mode — empty base fragment", () => {
|
|
|
606
609
|
mutations: [],
|
|
607
610
|
queries: [],
|
|
608
611
|
subscriptions: [],
|
|
612
|
+
subscriptionSources: [],
|
|
609
613
|
})
|
|
610
614
|
let parts = decodeFragment(emptyFragment)
|
|
611
615
|
expect(parts.types)->toHaveLength(0)
|
|
@@ -619,12 +623,14 @@ describe("Split mode — empty base fragment", () => {
|
|
|
619
623
|
mutations: [],
|
|
620
624
|
queries: [],
|
|
621
625
|
subscriptions: [],
|
|
626
|
+
subscriptionSources: [],
|
|
622
627
|
})
|
|
623
628
|
let pluginFragment = ReventlessCore.GraphQL_Stitcher.encode({
|
|
624
629
|
types: [`type MyPlugin_Item { id: ID! }`],
|
|
625
630
|
mutations: [`MyPlugin_Item_Create(id: ID!): String`],
|
|
626
631
|
queries: [`MyPlugin_Item(id: ID!): MyPlugin_Item`],
|
|
627
632
|
subscriptions: [],
|
|
633
|
+
subscriptionSources: [],
|
|
628
634
|
})
|
|
629
635
|
let sdl = ReventlessCore.GraphQL_Stitcher.stitch(
|
|
630
636
|
~baseFragment=emptyBase,
|
|
@@ -103,7 +103,8 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — Stage E2 permission lifti
|
|
|
103
103
|
types: [],
|
|
104
104
|
mutations: mutationFields,
|
|
105
105
|
queries: queryFields,
|
|
106
|
-
subscriptions: []
|
|
106
|
+
subscriptions: [],
|
|
107
|
+
subscriptionSources: []
|
|
107
108
|
});
|
|
108
109
|
let mutationEntry = (fieldNames, fieldPermissions) => ({
|
|
109
110
|
fieldNames: fieldNames,
|
|
@@ -287,7 +288,8 @@ globalThis.describe("AppSync_Adapter.injectAwsAuth — systemCallable dual-auth"
|
|
|
287
288
|
types: [],
|
|
288
289
|
mutations: mutationFields,
|
|
289
290
|
queries: queryFields,
|
|
290
|
-
subscriptions: []
|
|
291
|
+
subscriptions: [],
|
|
292
|
+
subscriptionSources: []
|
|
291
293
|
});
|
|
292
294
|
globalThis.test("systemCallable mutation with a group emits @aws_cognito_user_pools + @aws_iam", () => {
|
|
293
295
|
let entry_fieldNames = ["p_Sync"];
|
|
@@ -464,7 +466,8 @@ globalThis.describe("AppSync_Adapter — type-level dual-auth", () => {
|
|
|
464
466
|
types: types,
|
|
465
467
|
mutations: [],
|
|
466
468
|
queries: queryFields,
|
|
467
|
-
subscriptions: []
|
|
469
|
+
subscriptions: [],
|
|
470
|
+
subscriptionSources: []
|
|
468
471
|
});
|
|
469
472
|
let callableEntry_permission = {
|
|
470
473
|
TAG: "AllowGroups",
|
|
@@ -565,7 +568,8 @@ globalThis.describe("Split mode — empty base fragment", () => {
|
|
|
565
568
|
types: [],
|
|
566
569
|
mutations: [],
|
|
567
570
|
queries: [],
|
|
568
|
-
subscriptions: []
|
|
571
|
+
subscriptions: [],
|
|
572
|
+
subscriptionSources: []
|
|
569
573
|
});
|
|
570
574
|
let parts = GraphQL_Stitcher$ReventlessCore.decode(emptyFragment);
|
|
571
575
|
globalThis.expect(parts.types).toHaveLength(0);
|
|
@@ -577,13 +581,15 @@ globalThis.describe("Split mode — empty base fragment", () => {
|
|
|
577
581
|
types: [],
|
|
578
582
|
mutations: [],
|
|
579
583
|
queries: [],
|
|
580
|
-
subscriptions: []
|
|
584
|
+
subscriptions: [],
|
|
585
|
+
subscriptionSources: []
|
|
581
586
|
});
|
|
582
587
|
let pluginFragment = GraphQL_Stitcher$ReventlessCore.encode({
|
|
583
588
|
types: [`type MyPlugin_Item { id: ID! }`],
|
|
584
589
|
mutations: [`MyPlugin_Item_Create(id: ID!): String`],
|
|
585
590
|
queries: [`MyPlugin_Item(id: ID!): MyPlugin_Item`],
|
|
586
|
-
subscriptions: []
|
|
591
|
+
subscriptions: [],
|
|
592
|
+
subscriptionSources: []
|
|
587
593
|
});
|
|
588
594
|
let sdl = GraphQL_Stitcher$ReventlessCore.stitch(emptyBase, [pluginFragment]);
|
|
589
595
|
globalThis.expect(sdl).toContain("MyPlugin_Item_Create");
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// AppSync_SdlDecorate — push-time @aws_subscribe injection from the neutral
|
|
2
|
+
// subscription→mutation source metadata. Core emits directive-free SDL; the
|
|
3
|
+
// AWS provider appends its dialect onto the STITCHED schema here.
|
|
4
|
+
|
|
5
|
+
open JestGlobals
|
|
6
|
+
|
|
7
|
+
let sources: array<ReventlessCore.GraphQL_Stitcher.subscriptionSource> = [
|
|
8
|
+
{field: "onCatalog_AddProduct", mutations: ["Catalog_AddProduct"]},
|
|
9
|
+
{
|
|
10
|
+
field: "onUIFragmentChange",
|
|
11
|
+
mutations: [
|
|
12
|
+
"Platform_UIFragmentRegistered",
|
|
13
|
+
"Platform_UIFragmentUpdated",
|
|
14
|
+
"Platform_UIFragmentDeregistered",
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
let stitchedSdl = `type UIFragmentChangeEvent {
|
|
20
|
+
pluginId: ID!
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type Query {
|
|
24
|
+
node(id: ID!): Node
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Mutation {
|
|
28
|
+
Catalog_AddProduct(input: AddInput): CommandResult
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type Subscription {
|
|
32
|
+
onCatalog_AddProduct(id: ID): CommandResult
|
|
33
|
+
onUIFragmentChange: UIFragmentChangeEvent
|
|
34
|
+
@aws_auth(cognito_groups: ["Admin"])
|
|
35
|
+
onCatalogEventLog_eventAppended: CatalogEventLogEvent
|
|
36
|
+
}`
|
|
37
|
+
|
|
38
|
+
describe("AppSync_SdlDecorate.injectAwsSubscribe", () => {
|
|
39
|
+
testSync("appends @aws_subscribe on a 1:1 mutation-sourced field (with args)", () => {
|
|
40
|
+
let sdl = AppSync_SdlDecorate.injectAwsSubscribe(stitchedSdl, ~sources)
|
|
41
|
+
expect(sdl)->toContain(
|
|
42
|
+
`onCatalog_AddProduct(id: ID): CommandResult\n @aws_subscribe(mutations: ["Catalog_AddProduct"])`,
|
|
43
|
+
)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
testSync("appends the many-mutations fan-in on a no-arg field, keeping @aws_auth", () => {
|
|
47
|
+
let sdl = AppSync_SdlDecorate.injectAwsSubscribe(stitchedSdl, ~sources)
|
|
48
|
+
expect(sdl)->toContain(
|
|
49
|
+
`onUIFragmentChange: UIFragmentChangeEvent\n @aws_subscribe(mutations: ["Platform_UIFragmentRegistered", "Platform_UIFragmentUpdated", "Platform_UIFragmentDeregistered"])`,
|
|
50
|
+
)
|
|
51
|
+
// The auth directive line appended by injectAwsAuthAll survives.
|
|
52
|
+
expect(sdl)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
testSync("leaves unmapped fields (Source A) and non-Subscription blocks untouched", () => {
|
|
56
|
+
let sdl = AppSync_SdlDecorate.injectAwsSubscribe(stitchedSdl, ~sources)
|
|
57
|
+
expect(sdl)->toContain(`onCatalogEventLog_eventAppended: CatalogEventLogEvent\n}`)
|
|
58
|
+
// The Mutation field shares its leading name with a source's mutation —
|
|
59
|
+
// it must not be decorated (only the Subscription block is).
|
|
60
|
+
expect(sdl)->toContain(`Catalog_AddProduct(input: AddInput): CommandResult\n}`)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
testSync("no-op on empty sources or an SDL without a Subscription block", () => {
|
|
64
|
+
expect(AppSync_SdlDecorate.injectAwsSubscribe(stitchedSdl, ~sources=[]))->toBe(stitchedSdl)
|
|
65
|
+
let noSubs = `type Query {\n node(id: ID!): Node\n}`
|
|
66
|
+
expect(AppSync_SdlDecorate.injectAwsSubscribe(noSubs, ~sources))->toBe(noSubs)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe("AppSync_Adapter.stitchWithAwsDirectives", () => {
|
|
71
|
+
testSync("assembles neutral fragments into AWS-dialect SDL", () => {
|
|
72
|
+
let baseFragment = ReventlessCore.GraphQL_Stitcher.encode({
|
|
73
|
+
types: [],
|
|
74
|
+
mutations: [` Platform_PluginStatusChanged(pluginId: ID!, status: PluginStatus!): PluginStatusChangeEvent`],
|
|
75
|
+
queries: [],
|
|
76
|
+
subscriptions: [` onPluginStatusChange: PluginStatusChangeEvent`],
|
|
77
|
+
subscriptionSources: [
|
|
78
|
+
{field: "onPluginStatusChange", mutations: ["Platform_PluginStatusChanged"]},
|
|
79
|
+
],
|
|
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
|
+
)
|
|
92
|
+
expect(sdl)->toContain(`@aws_subscribe(mutations: ["Platform_PluginStatusChanged"])`)
|
|
93
|
+
expect(sdl)->toContain(`@aws_subscribe(mutations: ["Catalog_AddProduct"])`)
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
describe("AppSync_SdlDecorate.planAwsPushes", () => {
|
|
98
|
+
// Neutral admin base: a system-callable mutation + a shared traversal type.
|
|
99
|
+
let rawAdminBase = ReventlessCore.GraphQL_Stitcher.encode({
|
|
100
|
+
types: [`type CommandAccepted {\n id: ID!\n}`],
|
|
101
|
+
mutations: [` Platform_RegisterApiFragment(input: ApiFragmentInput): CommandResult`],
|
|
102
|
+
queries: [],
|
|
103
|
+
subscriptions: [],
|
|
104
|
+
subscriptionSources: [],
|
|
105
|
+
})
|
|
106
|
+
let iamFieldNames = ["Platform_RegisterApiFragment"]
|
|
107
|
+
let mkFrag = (~mutation, ~target): AppSync_SdlDecorate.targetedFragmentInput => {
|
|
108
|
+
encoded: ReventlessCore.GraphQL_Stitcher.encode({
|
|
109
|
+
types: [],
|
|
110
|
+
mutations: [mutation],
|
|
111
|
+
queries: [],
|
|
112
|
+
subscriptions: [],
|
|
113
|
+
subscriptionSources: [],
|
|
114
|
+
}).encoded,
|
|
115
|
+
protocol: "graphql",
|
|
116
|
+
target,
|
|
117
|
+
}
|
|
118
|
+
let platformFrag = mkFrag(~mutation=` Inspector_Ping: CommandResult`, ~target="Platform")
|
|
119
|
+
let domainFrag = mkFrag(~mutation=` Catalog_AddProduct(input: AddInput): CommandResult`, ~target="Domain")
|
|
120
|
+
|
|
121
|
+
testSync("split mode: Platform API carries admin base (dual-auth) + Platform frags; Domain API excludes the admin base", () => {
|
|
122
|
+
let plans = AppSync_SdlDecorate.planAwsPushes(
|
|
123
|
+
~rawAdminBase,
|
|
124
|
+
~iamFieldNames,
|
|
125
|
+
~fragments=[platformFrag, domainFrag],
|
|
126
|
+
~splitApi=true,
|
|
127
|
+
)
|
|
128
|
+
expect(plans->Array.length)->toBe(2)
|
|
129
|
+
let platformPlan = plans->Array.find(p => p.api == "PlatformApi")->Option.getOrThrow
|
|
130
|
+
let domainPlan = plans->Array.find(p => p.api == "DomainApi")->Option.getOrThrow
|
|
131
|
+
// Admin base mutation lands on the Platform API with dual-auth (system caller).
|
|
132
|
+
expect(platformPlan.sdl)->toContain("Platform_RegisterApiFragment")
|
|
133
|
+
expect(platformPlan.sdl)->toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"]) @aws_iam`)
|
|
134
|
+
// Platform-target plugin field present on the Platform API.
|
|
135
|
+
expect(platformPlan.sdl)->toContain("Inspector_Ping")
|
|
136
|
+
// Shared traversal type stamped once on the assembled SDL.
|
|
137
|
+
expect(platformPlan.sdl)->toContain(`type CommandAccepted @aws_cognito_user_pools @aws_iam {`)
|
|
138
|
+
// Domain-target field on the Domain API; admin base absent (empty base in split mode).
|
|
139
|
+
expect(domainPlan.sdl)->toContain("Catalog_AddProduct")
|
|
140
|
+
expect(domainPlan.sdl->String.includes("Platform_RegisterApiFragment"))->toBe(false)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
testSync("unified mode: a single Domain API carries admin base + all frags", () => {
|
|
144
|
+
let plans = AppSync_SdlDecorate.planAwsPushes(
|
|
145
|
+
~rawAdminBase,
|
|
146
|
+
~iamFieldNames,
|
|
147
|
+
~fragments=[platformFrag, domainFrag],
|
|
148
|
+
~splitApi=false,
|
|
149
|
+
)
|
|
150
|
+
expect(plans->Array.length)->toBe(1)
|
|
151
|
+
let plan = plans->Array.getUnsafe(0)
|
|
152
|
+
expect(plan.api)->toBe("DomainApi")
|
|
153
|
+
expect(plan.sdl)->toContain("Platform_RegisterApiFragment")
|
|
154
|
+
expect(plan.sdl)->toContain("Inspector_Ping")
|
|
155
|
+
expect(plan.sdl)->toContain("Catalog_AddProduct")
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as AppSync_Adapter$ReventlessAws from "../src/components/Api/AppSync_Adapter.res.mjs";
|
|
5
|
+
import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
6
|
+
import * as AppSync_SdlDecorate$ReventlessAws from "../src/components/Api/AppSync_SdlDecorate.res.mjs";
|
|
7
|
+
|
|
8
|
+
let sources = [
|
|
9
|
+
{
|
|
10
|
+
field: "onCatalog_AddProduct",
|
|
11
|
+
mutations: ["Catalog_AddProduct"]
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
field: "onUIFragmentChange",
|
|
15
|
+
mutations: [
|
|
16
|
+
"Platform_UIFragmentRegistered",
|
|
17
|
+
"Platform_UIFragmentUpdated",
|
|
18
|
+
"Platform_UIFragmentDeregistered"
|
|
19
|
+
]
|
|
20
|
+
}
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
let stitchedSdl = `type UIFragmentChangeEvent {
|
|
24
|
+
pluginId: ID!
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type Query {
|
|
28
|
+
node(id: ID!): Node
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type Mutation {
|
|
32
|
+
Catalog_AddProduct(input: AddInput): CommandResult
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Subscription {
|
|
36
|
+
onCatalog_AddProduct(id: ID): CommandResult
|
|
37
|
+
onUIFragmentChange: UIFragmentChangeEvent
|
|
38
|
+
@aws_auth(cognito_groups: ["Admin"])
|
|
39
|
+
onCatalogEventLog_eventAppended: CatalogEventLogEvent
|
|
40
|
+
}`;
|
|
41
|
+
|
|
42
|
+
globalThis.describe("AppSync_SdlDecorate.injectAwsSubscribe", () => {
|
|
43
|
+
globalThis.test("appends @aws_subscribe on a 1:1 mutation-sourced field (with args)", () => {
|
|
44
|
+
let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
|
|
45
|
+
globalThis.expect(sdl).toContain(`onCatalog_AddProduct(id: ID): CommandResult\n @aws_subscribe(mutations: ["Catalog_AddProduct"])`);
|
|
46
|
+
});
|
|
47
|
+
globalThis.test("appends the many-mutations fan-in on a no-arg field, keeping @aws_auth", () => {
|
|
48
|
+
let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
|
|
49
|
+
globalThis.expect(sdl).toContain(`onUIFragmentChange: UIFragmentChangeEvent\n @aws_subscribe(mutations: ["Platform_UIFragmentRegistered", "Platform_UIFragmentUpdated", "Platform_UIFragmentDeregistered"])`);
|
|
50
|
+
globalThis.expect(sdl).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
|
|
51
|
+
});
|
|
52
|
+
globalThis.test("leaves unmapped fields (Source A) and non-Subscription blocks untouched", () => {
|
|
53
|
+
let sdl = AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, sources);
|
|
54
|
+
globalThis.expect(sdl).toContain(`onCatalogEventLog_eventAppended: CatalogEventLogEvent\n}`);
|
|
55
|
+
globalThis.expect(sdl).toContain(`Catalog_AddProduct(input: AddInput): CommandResult\n}`);
|
|
56
|
+
});
|
|
57
|
+
globalThis.test("no-op on empty sources or an SDL without a Subscription block", () => {
|
|
58
|
+
globalThis.expect(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(stitchedSdl, [])).toBe(stitchedSdl);
|
|
59
|
+
let noSubs = `type Query {\n node(id: ID!): Node\n}`;
|
|
60
|
+
globalThis.expect(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(noSubs, sources)).toBe(noSubs);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
globalThis.describe("AppSync_Adapter.stitchWithAwsDirectives", () => {
|
|
65
|
+
globalThis.test("assembles neutral fragments into AWS-dialect SDL", () => {
|
|
66
|
+
let baseFragment = GraphQL_Stitcher$ReventlessCore.encode({
|
|
67
|
+
types: [],
|
|
68
|
+
mutations: [` Platform_PluginStatusChanged(pluginId: ID!, status: PluginStatus!): PluginStatusChangeEvent`],
|
|
69
|
+
queries: [],
|
|
70
|
+
subscriptions: [` onPluginStatusChange: PluginStatusChangeEvent`],
|
|
71
|
+
subscriptionSources: [{
|
|
72
|
+
field: "onPluginStatusChange",
|
|
73
|
+
mutations: ["Platform_PluginStatusChanged"]
|
|
74
|
+
}]
|
|
75
|
+
});
|
|
76
|
+
let pluginFragment = GraphQL_Stitcher$ReventlessCore.encode({
|
|
77
|
+
types: [`type PluginStatusChangeEvent {\n pluginId: ID!\n}`],
|
|
78
|
+
mutations: [` Catalog_AddProduct(input: AddInput): CommandResult`],
|
|
79
|
+
queries: [],
|
|
80
|
+
subscriptions: [` onCatalog_AddProduct(id: ID): CommandResult`],
|
|
81
|
+
subscriptionSources: [{
|
|
82
|
+
field: "onCatalog_AddProduct",
|
|
83
|
+
mutations: ["Catalog_AddProduct"]
|
|
84
|
+
}]
|
|
85
|
+
});
|
|
86
|
+
let sdl = AppSync_Adapter$ReventlessAws.stitchWithAwsDirectives(baseFragment, [pluginFragment]);
|
|
87
|
+
globalThis.expect(sdl).toContain(`@aws_subscribe(mutations: ["Platform_PluginStatusChanged"])`);
|
|
88
|
+
globalThis.expect(sdl).toContain(`@aws_subscribe(mutations: ["Catalog_AddProduct"])`);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
globalThis.describe("AppSync_SdlDecorate.planAwsPushes", () => {
|
|
93
|
+
let rawAdminBase = GraphQL_Stitcher$ReventlessCore.encode({
|
|
94
|
+
types: [`type CommandAccepted {\n id: ID!\n}`],
|
|
95
|
+
mutations: [` Platform_RegisterApiFragment(input: ApiFragmentInput): CommandResult`],
|
|
96
|
+
queries: [],
|
|
97
|
+
subscriptions: [],
|
|
98
|
+
subscriptionSources: []
|
|
99
|
+
});
|
|
100
|
+
let iamFieldNames = ["Platform_RegisterApiFragment"];
|
|
101
|
+
let mkFrag = (mutation, target) => ({
|
|
102
|
+
encoded: GraphQL_Stitcher$ReventlessCore.encode({
|
|
103
|
+
types: [],
|
|
104
|
+
mutations: [mutation],
|
|
105
|
+
queries: [],
|
|
106
|
+
subscriptions: [],
|
|
107
|
+
subscriptionSources: []
|
|
108
|
+
}).encoded,
|
|
109
|
+
protocol: "graphql",
|
|
110
|
+
target: target
|
|
111
|
+
});
|
|
112
|
+
let platformFrag = mkFrag(` Inspector_Ping: CommandResult`, "Platform");
|
|
113
|
+
let domainFrag = mkFrag(` Catalog_AddProduct(input: AddInput): CommandResult`, "Domain");
|
|
114
|
+
globalThis.test("split mode: Platform API carries admin base (dual-auth) + Platform frags; Domain API excludes the admin base", () => {
|
|
115
|
+
let plans = AppSync_SdlDecorate$ReventlessAws.planAwsPushes(rawAdminBase, iamFieldNames, [
|
|
116
|
+
platformFrag,
|
|
117
|
+
domainFrag
|
|
118
|
+
], true);
|
|
119
|
+
globalThis.expect(plans.length).toBe(2);
|
|
120
|
+
let platformPlan = Stdlib_Option.getOrThrow(plans.find(p => p.api === "PlatformApi"), undefined);
|
|
121
|
+
let domainPlan = Stdlib_Option.getOrThrow(plans.find(p => p.api === "DomainApi"), undefined);
|
|
122
|
+
globalThis.expect(platformPlan.sdl).toContain("Platform_RegisterApiFragment");
|
|
123
|
+
globalThis.expect(platformPlan.sdl).toContain(`@aws_cognito_user_pools(cognito_groups: ["Admin"]) @aws_iam`);
|
|
124
|
+
globalThis.expect(platformPlan.sdl).toContain("Inspector_Ping");
|
|
125
|
+
globalThis.expect(platformPlan.sdl).toContain(`type CommandAccepted @aws_cognito_user_pools @aws_iam {`);
|
|
126
|
+
globalThis.expect(domainPlan.sdl).toContain("Catalog_AddProduct");
|
|
127
|
+
globalThis.expect(domainPlan.sdl.includes("Platform_RegisterApiFragment")).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
globalThis.test("unified mode: a single Domain API carries admin base + all frags", () => {
|
|
130
|
+
let plans = AppSync_SdlDecorate$ReventlessAws.planAwsPushes(rawAdminBase, iamFieldNames, [
|
|
131
|
+
platformFrag,
|
|
132
|
+
domainFrag
|
|
133
|
+
], false);
|
|
134
|
+
globalThis.expect(plans.length).toBe(1);
|
|
135
|
+
let plan = plans[0];
|
|
136
|
+
globalThis.expect(plan.api).toBe("DomainApi");
|
|
137
|
+
globalThis.expect(plan.sdl).toContain("Platform_RegisterApiFragment");
|
|
138
|
+
globalThis.expect(plan.sdl).toContain("Inspector_Ping");
|
|
139
|
+
globalThis.expect(plan.sdl).toContain("Catalog_AddProduct");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
export {
|
|
144
|
+
sources,
|
|
145
|
+
stitchedSdl,
|
|
146
|
+
}
|
|
147
|
+
/* Not a pure module */
|