@reventlessdev/reventless-aws 3.0.0-alpha.209 → 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 +9 -0
- package/package.json +7 -7
- package/src/Platform.res +445 -71
- package/src/Platform.res.mjs +318 -151
- package/src/components/Api/AppSync_Adapter.res +144 -10
- package/src/components/Api/AppSync_Adapter.res.mjs +62 -7
- 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/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
|
@@ -157,6 +157,77 @@ let getClient = () =>
|
|
|
157
157
|
c
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// ── GetSourceApiAssociation — merged-API association status poll ──────────
|
|
161
|
+
// Merged-API deploys must fail loudly on MERGE_FAILED (plan
|
|
162
|
+
// merged-api-push-free-composition, Phase 0 finding: a failed merge silently
|
|
163
|
+
// keeps the last-good merged schema serving). After creating a
|
|
164
|
+
// SourceApiAssociation, poll until the initial merge lands.
|
|
165
|
+
type getSourceApiAssociationInput = {
|
|
166
|
+
associationId: string,
|
|
167
|
+
mergedApiIdentifier: string,
|
|
168
|
+
}
|
|
169
|
+
type getSourceApiAssociationCommand
|
|
170
|
+
type sourceApiAssociationSummary = {
|
|
171
|
+
sourceApiAssociationStatus: option<string>,
|
|
172
|
+
sourceApiAssociationStatusDetail: option<string>,
|
|
173
|
+
}
|
|
174
|
+
type getSourceApiAssociationResult = {sourceApiAssociation: option<sourceApiAssociationSummary>}
|
|
175
|
+
|
|
176
|
+
@module("@aws-sdk/client-appsync") @new
|
|
177
|
+
external makeGetSourceApiAssociationCommand: getSourceApiAssociationInput => getSourceApiAssociationCommand =
|
|
178
|
+
"GetSourceApiAssociationCommand"
|
|
179
|
+
|
|
180
|
+
@send
|
|
181
|
+
external sendGetSourceApiAssociation: (
|
|
182
|
+
appSyncClient,
|
|
183
|
+
getSourceApiAssociationCommand,
|
|
184
|
+
) => promise<getSourceApiAssociationResult> = "send"
|
|
185
|
+
|
|
186
|
+
// Poll until the association reports MERGE_SUCCESS; throw with the AWS status
|
|
187
|
+
// detail on MERGE_FAILED / AUTO_MERGE_SCHEDULE_FAILED. Auto-merge lands in
|
|
188
|
+
// ~12 s (spike-measured), so 60 × 2 s bounds the wait at two minutes.
|
|
189
|
+
let rec waitForMergeSuccess = async (
|
|
190
|
+
client: appSyncClient,
|
|
191
|
+
~associationId: string,
|
|
192
|
+
~mergedApiIdentifier: string,
|
|
193
|
+
~maxAttempts=60,
|
|
194
|
+
~attempt=0,
|
|
195
|
+
~delayMs=2000,
|
|
196
|
+
) => {
|
|
197
|
+
let result = await client->sendGetSourceApiAssociation(
|
|
198
|
+
{associationId, mergedApiIdentifier}->makeGetSourceApiAssociationCommand,
|
|
199
|
+
)
|
|
200
|
+
let status =
|
|
201
|
+
result.sourceApiAssociation
|
|
202
|
+
->Option.flatMap(a => a.sourceApiAssociationStatus)
|
|
203
|
+
->Option.getOr("(no status)")
|
|
204
|
+
let detail =
|
|
205
|
+
result.sourceApiAssociation
|
|
206
|
+
->Option.flatMap(a => a.sourceApiAssociationStatusDetail)
|
|
207
|
+
->Option.getOr("(no details)")
|
|
208
|
+
switch status {
|
|
209
|
+
| "MERGE_SUCCESS" => ()
|
|
210
|
+
| "MERGE_FAILED" | "AUTO_MERGE_SCHEDULE_FAILED" =>
|
|
211
|
+
JsError.throwWithMessage(
|
|
212
|
+
`Source API association ${associationId} on ${mergedApiIdentifier} failed to merge (${status}): ${detail}`,
|
|
213
|
+
)
|
|
214
|
+
| _ if attempt >= maxAttempts =>
|
|
215
|
+
JsError.throwWithMessage(
|
|
216
|
+
`Source API association ${associationId} merge timed out after ${maxAttempts->Int.toString} attempts (status: ${status})`,
|
|
217
|
+
)
|
|
218
|
+
| _ =>
|
|
219
|
+
await Promise.make((resolve, _) => setTimeout(resolve, delayMs)->ignore)
|
|
220
|
+
await waitForMergeSuccess(
|
|
221
|
+
client,
|
|
222
|
+
~associationId,
|
|
223
|
+
~mergedApiIdentifier,
|
|
224
|
+
~maxAttempts,
|
|
225
|
+
~attempt=attempt + 1,
|
|
226
|
+
~delayMs,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
160
231
|
// ── @aws_auth directive injection ─────────────────────────────────────────
|
|
161
232
|
// Injects @aws_auth(cognito_groups: [...]) directives into SDL field strings
|
|
162
233
|
// based on authorization metadata from schema entries.
|
|
@@ -433,13 +504,40 @@ let stitchWithAwsDirectives = (
|
|
|
433
504
|
->stampSharedIamTypes
|
|
434
505
|
}
|
|
435
506
|
|
|
507
|
+
/**
|
|
508
|
+
Merged-mode plugin subgraph document: one fragment rendered standalone
|
|
509
|
+
(relay base types included, global `node` omitted — only the platform's
|
|
510
|
+
canonical base document carries `node`), with the same AppSync dialect as
|
|
511
|
+
`stitchWithAwsDirectives`. No `@canonical` stamps — plugin subgraphs stay
|
|
512
|
+
unstamped; the admin source's canonical definitions win on merge.
|
|
513
|
+
*/
|
|
514
|
+
let stitchStandaloneWithAwsDirectives = (
|
|
515
|
+
~fragment: Reventless.Plugin.apiSchemaFragment,
|
|
516
|
+
): string => {
|
|
517
|
+
let sources = ReventlessCore.GraphQL_Stitcher.collectSubscriptionSources(
|
|
518
|
+
~baseFragment=fragment,
|
|
519
|
+
~pluginFragments=[],
|
|
520
|
+
)
|
|
521
|
+
ReventlessCore.GraphQL_Stitcher.stitchStandalone(~fragment)
|
|
522
|
+
->AppSync_SdlDecorate.injectAwsSubscribe(~sources)
|
|
523
|
+
->stampSharedIamTypes
|
|
524
|
+
}
|
|
525
|
+
|
|
436
526
|
// ── Provider implementation ────────────────────────────────────────────────
|
|
437
527
|
|
|
438
528
|
type api = AppSync.GraphQLApi.t
|
|
439
529
|
type role = IAM.Role.t
|
|
440
530
|
|
|
441
|
-
|
|
531
|
+
// Primary authentication mode every platform-created AppSync API uses. A
|
|
532
|
+
// merged API and its source APIs must share this primary mode — exported via
|
|
533
|
+
// StackReference (`mergedApiPrimaryAuth`) and asserted where associations are
|
|
534
|
+
// created (AppSync_MergedApi.assertCompatiblePrimaryAuth).
|
|
535
|
+
let primaryAuthenticationType = AppSync.GraphQLApi.AMAZON_COGNITO_USER_POOLS
|
|
536
|
+
|
|
537
|
+
let _makeApiResourceWith = (
|
|
442
538
|
~name: string,
|
|
539
|
+
~schema: option<string>,
|
|
540
|
+
~userPoolConfig: option<Pulumi.Output.t<AppSync.GraphQLApi.userPoolConfig>>,
|
|
443
541
|
~opts: Pulumi.ComponentResource.options,
|
|
444
542
|
): (Pulumi.Output.t<api>, Pulumi.Output.t<role>) => {
|
|
445
543
|
let customOpts: Pulumi.CustomResourceOptions.t = {
|
|
@@ -456,13 +554,16 @@ let makeApiResource = (
|
|
|
456
554
|
~opts=Some(customOpts),
|
|
457
555
|
)
|
|
458
556
|
|
|
459
|
-
// Resolve the Cognito UserPool —
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
557
|
+
// Resolve the Cognito UserPool — either supplied by the caller (plugin-stack
|
|
558
|
+
// source APIs read it from the platform's StackReference exports so they
|
|
559
|
+
// never provision pool/client resources of their own) or resolved via
|
|
560
|
+
// Auth_Cognito (cached inside Platform_Stack, so calling from each API call
|
|
561
|
+
// site — DomainApi, PlatformApi — is safe). A single Output yielding the
|
|
562
|
+
// {userPoolId, awsRegion, defaultAction} record AppSync expects.
|
|
563
|
+
let userPoolConfigOut = switch userPoolConfig {
|
|
564
|
+
| Some(config) => config
|
|
565
|
+
| None =>
|
|
566
|
+
Auth_Cognito.make(~name=`${name}-auth`)->Pulumi.Output.apply((c: Auth_Cognito.authConfig) =>
|
|
466
567
|
(
|
|
467
568
|
{
|
|
468
569
|
userPoolId: c.userPoolId,
|
|
@@ -471,13 +572,13 @@ let makeApiResource = (
|
|
|
471
572
|
}: AppSync.GraphQLApi.userPoolConfig
|
|
472
573
|
)
|
|
473
574
|
)
|
|
575
|
+
}
|
|
474
576
|
|
|
475
577
|
// Cognito as primary auth, AWS_IAM as additional provider for
|
|
476
578
|
// server-to-server lambdas (heartbeat, Plugin_Connected emission) signed via
|
|
477
579
|
// the existing IAM role.
|
|
478
580
|
let apiArgs: AppSync.GraphQLApi.args = {
|
|
479
|
-
authenticationType:
|
|
480
|
-
.AMAZON_COGNITO_USER_POOLS->Pulumi.Input.make,
|
|
581
|
+
authenticationType: primaryAuthenticationType->Pulumi.Input.make,
|
|
481
582
|
userPoolConfig: userPoolConfigOut->Pulumi.Output.asInput,
|
|
482
583
|
additionalAuthenticationProviders: [
|
|
483
584
|
(
|
|
@@ -486,12 +587,45 @@ let makeApiResource = (
|
|
|
486
587
|
}: AppSync.GraphQLApi.additionalAuthenticationProvider
|
|
487
588
|
)->Pulumi.Input.make,
|
|
488
589
|
]->Pulumi.Input.make,
|
|
590
|
+
schema: ?(schema->Option.map(Pulumi.Input.make)),
|
|
489
591
|
}
|
|
490
592
|
let graphQLApi = AppSync.GraphQLApi.make(~name, ~args=apiArgs, ~opts=Some(customOpts))
|
|
491
593
|
|
|
492
594
|
(graphQLApi->Pulumi.Output.make, iamRole->Pulumi.Output.make)
|
|
493
595
|
}
|
|
494
596
|
|
|
597
|
+
let makeApiResource = (
|
|
598
|
+
~name: string,
|
|
599
|
+
~opts: Pulumi.ComponentResource.options,
|
|
600
|
+
): (Pulumi.Output.t<api>, Pulumi.Output.t<role>) =>
|
|
601
|
+
_makeApiResourceWith(~name, ~schema=None, ~userPoolConfig=None, ~opts)
|
|
602
|
+
|
|
603
|
+
// Merged-mode source API: same auth shape as makeApiResource but with a
|
|
604
|
+
// DECLARATIVE inline schema — the provider runs StartSchemaCreation + poll
|
|
605
|
+
// before the resource resolves, so resolvers chained on the API are ordered
|
|
606
|
+
// after the schema is ACTIVE without the push-path hook machinery. Not part
|
|
607
|
+
// of the Api_Adapter.Provider interface (Platform.res calls it directly on
|
|
608
|
+
// the merge path).
|
|
609
|
+
let makeSourceApiResource = (
|
|
610
|
+
~name: string,
|
|
611
|
+
~schema: string,
|
|
612
|
+
~opts: Pulumi.ComponentResource.options,
|
|
613
|
+
): (Pulumi.Output.t<api>, Pulumi.Output.t<role>) =>
|
|
614
|
+
_makeApiResourceWith(~name, ~schema=Some(schema), ~userPoolConfig=None, ~opts)
|
|
615
|
+
|
|
616
|
+
// Merged-mode PLUGIN source API: schema-less at creation (the plugin's
|
|
617
|
+
// standalone subgraph document is only computable during P.make(), so
|
|
618
|
+
// preResolversSchemaHook pushes it — the plugin's own API is a single writer
|
|
619
|
+
// by construction). The user pool comes from the platform's StackReference
|
|
620
|
+
// exports so the merged endpoint's Cognito primary auth matches across every
|
|
621
|
+
// source API without the plugin stack provisioning pool/client resources.
|
|
622
|
+
let makePluginSourceApiResource = (
|
|
623
|
+
~name: string,
|
|
624
|
+
~userPoolConfig: Pulumi.Output.t<AppSync.GraphQLApi.userPoolConfig>,
|
|
625
|
+
~opts: Pulumi.ComponentResource.options,
|
|
626
|
+
): (Pulumi.Output.t<api>, Pulumi.Output.t<role>) =>
|
|
627
|
+
_makeApiResourceWith(~name, ~schema=None, ~userPoolConfig=Some(userPoolConfig), ~opts)
|
|
628
|
+
|
|
495
629
|
let generateFragment = (
|
|
496
630
|
~mutationEntries: array<ReventlessInfra.Api.mutationSchemaEntry>,
|
|
497
631
|
~queryEntries: array<ReventlessInfra.Api.querySchemaEntry>,
|
|
@@ -101,6 +101,35 @@ function getClient() {
|
|
|
101
101
|
return c$1;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
async function waitForMergeSuccess(client, associationId, mergedApiIdentifier, maxAttemptsOpt, attemptOpt, delayMsOpt) {
|
|
105
|
+
let maxAttempts = maxAttemptsOpt !== undefined ? maxAttemptsOpt : 60;
|
|
106
|
+
let attempt = attemptOpt !== undefined ? attemptOpt : 0;
|
|
107
|
+
let delayMs = delayMsOpt !== undefined ? delayMsOpt : 2000;
|
|
108
|
+
let result = await client.send(new ClientAppsync.GetSourceApiAssociationCommand({
|
|
109
|
+
associationId: associationId,
|
|
110
|
+
mergedApiIdentifier: mergedApiIdentifier
|
|
111
|
+
}));
|
|
112
|
+
let status = Stdlib_Option.getOr(Stdlib_Option.flatMap(result.sourceApiAssociation, a => a.sourceApiAssociationStatus), "(no status)");
|
|
113
|
+
let detail = Stdlib_Option.getOr(Stdlib_Option.flatMap(result.sourceApiAssociation, a => a.sourceApiAssociationStatusDetail), "(no details)");
|
|
114
|
+
switch (status) {
|
|
115
|
+
case "AUTO_MERGE_SCHEDULE_FAILED" :
|
|
116
|
+
case "MERGE_FAILED" :
|
|
117
|
+
break;
|
|
118
|
+
case "MERGE_SUCCESS" :
|
|
119
|
+
return;
|
|
120
|
+
default:
|
|
121
|
+
if (attempt >= maxAttempts) {
|
|
122
|
+
return Stdlib_JsError.throwWithMessage(`Source API association ` + associationId + ` merge timed out after ` + maxAttempts.toString() + ` attempts (status: ` + status + `)`);
|
|
123
|
+
} else {
|
|
124
|
+
await new Promise((resolve, param) => {
|
|
125
|
+
setTimeout(resolve, delayMs);
|
|
126
|
+
});
|
|
127
|
+
return await waitForMergeSuccess(client, associationId, mergedApiIdentifier, maxAttempts, attempt + 1 | 0, delayMs);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return Stdlib_JsError.throwWithMessage(`Source API association ` + associationId + ` on ` + mergedApiIdentifier + ` failed to merge (` + status + `): ` + detail);
|
|
131
|
+
}
|
|
132
|
+
|
|
104
133
|
function _permissionToCognitoGroups(permission) {
|
|
105
134
|
if (typeof permission !== "object") {
|
|
106
135
|
switch (permission) {
|
|
@@ -268,7 +297,12 @@ function stitchWithAwsDirectives(baseFragment, pluginFragments) {
|
|
|
268
297
|
return AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, pluginFragments), sources));
|
|
269
298
|
}
|
|
270
299
|
|
|
271
|
-
function
|
|
300
|
+
function stitchStandaloneWithAwsDirectives(fragment) {
|
|
301
|
+
let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(fragment, []);
|
|
302
|
+
return AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitchStandalone(fragment), sources));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
|
|
272
306
|
let customOpts_parent = opts.parent;
|
|
273
307
|
let customOpts = {
|
|
274
308
|
parent: customOpts_parent
|
|
@@ -276,18 +310,19 @@ function makeApiResource(name, opts) {
|
|
|
276
310
|
let iamRole = new (Aws.iam.Role)(name + `-appsync-role`, {
|
|
277
311
|
assumeRolePolicy: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"appsync.amazonaws.com"},"Action":"sts:AssumeRole"}]}`
|
|
278
312
|
}, customOpts);
|
|
279
|
-
let
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
313
|
+
let userPoolConfigOut = userPoolConfig !== undefined ? userPoolConfig : Auth_Cognito$ReventlessAws.make(name + `-auth`, undefined).apply(c => ({
|
|
314
|
+
userPoolId: c.userPoolId,
|
|
315
|
+
defaultAction: "ALLOW",
|
|
316
|
+
awsRegion: c.region
|
|
317
|
+
}));
|
|
318
|
+
let apiArgs_schema = Stdlib_Option.map(schema, prim => prim);
|
|
285
319
|
let apiArgs_userPoolConfig = userPoolConfigOut;
|
|
286
320
|
let apiArgs_additionalAuthenticationProviders = [{
|
|
287
321
|
authenticationType: "AWS_IAM"
|
|
288
322
|
}];
|
|
289
323
|
let apiArgs = {
|
|
290
324
|
authenticationType: "AMAZON_COGNITO_USER_POOLS",
|
|
325
|
+
schema: apiArgs_schema,
|
|
291
326
|
userPoolConfig: apiArgs_userPoolConfig,
|
|
292
327
|
additionalAuthenticationProviders: apiArgs_additionalAuthenticationProviders
|
|
293
328
|
};
|
|
@@ -298,6 +333,18 @@ function makeApiResource(name, opts) {
|
|
|
298
333
|
];
|
|
299
334
|
}
|
|
300
335
|
|
|
336
|
+
function makeApiResource(name, opts) {
|
|
337
|
+
return _makeApiResourceWith(name, undefined, undefined, opts);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function makeSourceApiResource(name, schema, opts) {
|
|
341
|
+
return _makeApiResourceWith(name, schema, undefined, opts);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function makePluginSourceApiResource(name, userPoolConfig, opts) {
|
|
345
|
+
return _makeApiResourceWith(name, undefined, userPoolConfig, opts);
|
|
346
|
+
}
|
|
347
|
+
|
|
301
348
|
function generateFragment(mutationEntries, queryEntries) {
|
|
302
349
|
let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate(mutationEntries, queryEntries);
|
|
303
350
|
return injectAwsAuth(fragment, mutationEntries, queryEntries);
|
|
@@ -319,6 +366,8 @@ function updateSchema(api, baseFragment, pluginFragments) {
|
|
|
319
366
|
|
|
320
367
|
let stampSharedIamTypes = AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes;
|
|
321
368
|
|
|
369
|
+
let primaryAuthenticationType = "AMAZON_COGNITO_USER_POOLS";
|
|
370
|
+
|
|
322
371
|
export {
|
|
323
372
|
log,
|
|
324
373
|
sha256Hex,
|
|
@@ -329,6 +378,7 @@ export {
|
|
|
329
378
|
deploySchemaWithRetry,
|
|
330
379
|
_client,
|
|
331
380
|
getClient,
|
|
381
|
+
waitForMergeSuccess,
|
|
332
382
|
_permissionToCognitoGroups,
|
|
333
383
|
_formatGroupsDirective,
|
|
334
384
|
_formatDualAuthDirective,
|
|
@@ -338,7 +388,12 @@ export {
|
|
|
338
388
|
injectAwsAuth,
|
|
339
389
|
injectAwsAuthAll,
|
|
340
390
|
stitchWithAwsDirectives,
|
|
391
|
+
stitchStandaloneWithAwsDirectives,
|
|
392
|
+
primaryAuthenticationType,
|
|
393
|
+
_makeApiResourceWith,
|
|
341
394
|
makeApiResource,
|
|
395
|
+
makeSourceApiResource,
|
|
396
|
+
makePluginSourceApiResource,
|
|
342
397
|
generateFragment,
|
|
343
398
|
updateSchema,
|
|
344
399
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// AppSync_MergedApi — merged-API construction for the push-free composition
|
|
2
|
+
// path (docs/plans/merged-api-push-free-composition.md, Phase 3).
|
|
3
|
+
//
|
|
4
|
+
// A merged API carries no schema of its own: source APIs contribute theirs via
|
|
5
|
+
// SourceApiAssociation and AWS composes the merged endpoint. The platform
|
|
6
|
+
// stack creates the merged API(s) plus the association(s) for its own
|
|
7
|
+
// source(s) (the admin/base canonical source); plugin stacks associate their
|
|
8
|
+
// source APIs against the exported merged-API ARN (Phase 4).
|
|
9
|
+
|
|
10
|
+
open PulumiAws
|
|
11
|
+
|
|
12
|
+
// ── Primary-auth contract ───────────────────────────────────────────────────
|
|
13
|
+
// A merged API and every associated source API must share the same PRIMARY
|
|
14
|
+
// authentication mode (AWS rejects incompatible associations). The platform
|
|
15
|
+
// exports the merged API's mode via StackReference (`mergedApiPrimaryAuth`);
|
|
16
|
+
// stacks creating a source API assert against it before associating.
|
|
17
|
+
|
|
18
|
+
let authenticationTypeName = (t: AppSync.GraphQLApi.authenticationType): string =>
|
|
19
|
+
switch t {
|
|
20
|
+
| API_KEY => "API_KEY"
|
|
21
|
+
| AWS_IAM => "AWS_IAM"
|
|
22
|
+
| AMAZON_COGNITO_USER_POOLS => "AMAZON_COGNITO_USER_POOLS"
|
|
23
|
+
| OPENID_CONNECT => "OPENID_CONNECT"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The merged APIs share the platform-wide primary mode (Cognito primary,
|
|
27
|
+
// IAM secondary) — same shape the Phase-0 spike validated end-to-end.
|
|
28
|
+
let primaryAuthMode = authenticationTypeName(AppSync_Adapter.primaryAuthenticationType)
|
|
29
|
+
|
|
30
|
+
let assertCompatiblePrimaryAuth = (~sourceMode: string, ~mergedMode: string): unit =>
|
|
31
|
+
if sourceMode != mergedMode {
|
|
32
|
+
failwith(
|
|
33
|
+
`Merged-API primary-auth mismatch: the source API uses ${sourceMode} but the merged API ` ++
|
|
34
|
+
`expects ${mergedMode}. A source API must share the merged API's primary authentication ` ++
|
|
35
|
+
`mode — align the source API's authenticationType with the platform's ` ++ `\`mergedApiPrimaryAuth\` export before associating.`,
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ── Merged API + execution role ─────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
type t = {
|
|
42
|
+
api: Pulumi.Output.t<AppSync.GraphQLApi.t>,
|
|
43
|
+
executionRole: IAM.Role.t,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let make = (~name: string, ~opts: Pulumi.ComponentResource.options): t => {
|
|
47
|
+
let customOpts: Pulumi.CustomResourceOptions.t = {
|
|
48
|
+
parent: ?opts.parent,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Execution role AWS assumes to proxy merged-endpoint requests to the source
|
|
52
|
+
// APIs (appsync:SourceGraphQL) and to run schema merges
|
|
53
|
+
// (appsync:StartSchemaMerge — required for MANUAL_MERGE, harmless under
|
|
54
|
+
// AUTO_MERGE). Source APIs associate later from independent plugin stacks,
|
|
55
|
+
// so their ARNs cannot be enumerated here — the policy stays unscoped.
|
|
56
|
+
let executionRole = IAM.Role.make(
|
|
57
|
+
~name=`${name}-merge-exec-role`,
|
|
58
|
+
~args={
|
|
59
|
+
assumeRolePolicy: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"appsync.amazonaws.com"},"Action":"sts:AssumeRole"}]}`->Pulumi.Input.make,
|
|
60
|
+
},
|
|
61
|
+
~opts=Some(customOpts),
|
|
62
|
+
)
|
|
63
|
+
let _ = {
|
|
64
|
+
open PolicyDocument
|
|
65
|
+
IAM.RolePolicy.make(
|
|
66
|
+
~name=`${name}-merge-exec-policy`,
|
|
67
|
+
~args={
|
|
68
|
+
policy: PolicyDocument.make(
|
|
69
|
+
~id=`${name}-merge-exec-policy`,
|
|
70
|
+
~statements=[
|
|
71
|
+
{
|
|
72
|
+
sid: "AllowSourceGraphQLAndMerge",
|
|
73
|
+
effect: Allow,
|
|
74
|
+
actions: Actions(["appsync:SourceGraphQL", "appsync:StartSchemaMerge"]),
|
|
75
|
+
resources: AllResources,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
)
|
|
79
|
+
->PolicyDocument.toJsonString
|
|
80
|
+
->Pulumi.Input.make,
|
|
81
|
+
role: executionRole.id->Pulumi.Output.asInput,
|
|
82
|
+
},
|
|
83
|
+
~opts=customOpts,
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Same auth shape as the source APIs (AppSync_Adapter.makeApiResource):
|
|
88
|
+
// Cognito primary + AWS_IAM secondary. Per-field multi-auth directives on
|
|
89
|
+
// the source schemas survive the merge verbatim (Phase-0 finding 4).
|
|
90
|
+
let authConfigOut = Auth_Cognito.make(~name=`${name}-auth`)
|
|
91
|
+
let userPoolConfigOut =
|
|
92
|
+
authConfigOut->Pulumi.Output.apply((
|
|
93
|
+
c: Auth_Cognito.authConfig,
|
|
94
|
+
): AppSync.GraphQLApi.userPoolConfig => {
|
|
95
|
+
userPoolId: c.userPoolId,
|
|
96
|
+
awsRegion: c.region,
|
|
97
|
+
defaultAction: AppSync.GraphQLApi.ALLOW,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
let mergedApi = AppSync.GraphQLApi.make(
|
|
101
|
+
~name,
|
|
102
|
+
~args={
|
|
103
|
+
authenticationType: AppSync_Adapter.primaryAuthenticationType->Pulumi.Input.make,
|
|
104
|
+
userPoolConfig: userPoolConfigOut->Pulumi.Output.asInput,
|
|
105
|
+
additionalAuthenticationProviders: [
|
|
106
|
+
(
|
|
107
|
+
{
|
|
108
|
+
authenticationType: AppSync.GraphQLApi.AWS_IAM->Pulumi.Input.make,
|
|
109
|
+
}: AppSync.GraphQLApi.additionalAuthenticationProvider
|
|
110
|
+
)->Pulumi.Input.make,
|
|
111
|
+
]->Pulumi.Input.make,
|
|
112
|
+
apiType: AppSync.GraphQLApi.MERGED->Pulumi.Input.make,
|
|
113
|
+
mergedApiExecutionRoleArn: executionRole.arn->Pulumi.Output.asInput,
|
|
114
|
+
},
|
|
115
|
+
~opts=Some(customOpts),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
{api: mergedApi->Pulumi.Output.make, executionRole}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Source association ──────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/** Associate a source API with the merged API under AUTO_MERGE. AWS
|
|
124
|
+
serializes association CREATES per merged API (409
|
|
125
|
+
ConcurrentModificationException) — the platform stack's own associations
|
|
126
|
+
are inherently sequential; concurrent first-time plugin-stack deploys need
|
|
127
|
+
retry-with-backoff (Phase 4). */
|
|
128
|
+
let associateSource = (
|
|
129
|
+
~name: string,
|
|
130
|
+
~mergedApi: t,
|
|
131
|
+
~sourceApi: Pulumi.Output.t<AppSync.GraphQLApi.t>,
|
|
132
|
+
~opts: Pulumi.ComponentResource.options,
|
|
133
|
+
): AppSync.SourceApiAssociation.t => {
|
|
134
|
+
let customOpts: Pulumi.CustomResourceOptions.t = {
|
|
135
|
+
parent: ?opts.parent,
|
|
136
|
+
}
|
|
137
|
+
AppSync.SourceApiAssociation.make(
|
|
138
|
+
~name,
|
|
139
|
+
~args={
|
|
140
|
+
mergedApiId: mergedApi.api
|
|
141
|
+
->Pulumi.Output.flatMap((a: AppSync.GraphQLApi.t) => a.id)
|
|
142
|
+
->Pulumi.Output.asInput,
|
|
143
|
+
sourceApiId: sourceApi
|
|
144
|
+
->Pulumi.Output.flatMap((a: AppSync.GraphQLApi.t) => a.id)
|
|
145
|
+
->Pulumi.Output.asInput,
|
|
146
|
+
sourceApiAssociationConfigs: [
|
|
147
|
+
(
|
|
148
|
+
{
|
|
149
|
+
mergeType: AppSync.SourceApiAssociation.AUTO_MERGE->Pulumi.Input.make,
|
|
150
|
+
}: AppSync.SourceApiAssociation.sourceApiAssociationConfig
|
|
151
|
+
)->Pulumi.Input.make,
|
|
152
|
+
]->Pulumi.Input.make,
|
|
153
|
+
},
|
|
154
|
+
~opts=Some(customOpts),
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Associate a source API against a merged API referenced by ARN — the
|
|
159
|
+
plugin-stack form, where the merged API is not a resource in this stack
|
|
160
|
+
but the platform's `domainMergedApiArn` / `platformMergedApiArn`
|
|
161
|
+
StackReference export. Same AUTO_MERGE + create-time 409 caveat as
|
|
162
|
+
`associateSource`. */
|
|
163
|
+
let associateSourceWithMergedArn = (
|
|
164
|
+
~name: string,
|
|
165
|
+
~mergedApiArn: Pulumi.Output.t<string>,
|
|
166
|
+
~sourceApi: Pulumi.Output.t<AppSync.GraphQLApi.t>,
|
|
167
|
+
~opts: Pulumi.ComponentResource.options,
|
|
168
|
+
): AppSync.SourceApiAssociation.t => {
|
|
169
|
+
let customOpts: Pulumi.CustomResourceOptions.t = {
|
|
170
|
+
parent: ?opts.parent,
|
|
171
|
+
}
|
|
172
|
+
AppSync.SourceApiAssociation.make(
|
|
173
|
+
~name,
|
|
174
|
+
~args={
|
|
175
|
+
mergedApiArn: mergedApiArn->Pulumi.Output.asInput,
|
|
176
|
+
sourceApiId: sourceApi
|
|
177
|
+
->Pulumi.Output.flatMap((a: AppSync.GraphQLApi.t) => a.id)
|
|
178
|
+
->Pulumi.Output.asInput,
|
|
179
|
+
sourceApiAssociationConfigs: [
|
|
180
|
+
(
|
|
181
|
+
{
|
|
182
|
+
mergeType: AppSync.SourceApiAssociation.AUTO_MERGE->Pulumi.Input.make,
|
|
183
|
+
}: AppSync.SourceApiAssociation.sourceApiAssociationConfig
|
|
184
|
+
)->Pulumi.Input.make,
|
|
185
|
+
]->Pulumi.Input.make,
|
|
186
|
+
},
|
|
187
|
+
~opts=Some(customOpts),
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Deploy-time merge gate: resolves once the association reports
|
|
192
|
+
MERGE_SUCCESS, fails the deploy loudly (with AWS's status detail) on
|
|
193
|
+
MERGE_FAILED. Fold the returned Output into an exported value so it is
|
|
194
|
+
consumed — a MERGE_FAILED then fails `pulumi up` instead of silently
|
|
195
|
+
serving the last-good merged schema (Phase-0 operational finding).
|
|
196
|
+
`mergedApiIdentifier` accepts the merged API's id or ARN. */
|
|
197
|
+
let mergeStatusGateWith = (
|
|
198
|
+
~mergedApiIdentifier: Pulumi.Output.t<string>,
|
|
199
|
+
~association: AppSync.SourceApiAssociation.t,
|
|
200
|
+
): Pulumi.Output.t<unit> =>
|
|
201
|
+
(mergedApiIdentifier, association.associationId)
|
|
202
|
+
->Pulumi.Output.all2
|
|
203
|
+
->Pulumi.Output.flatMap(((identifier, associationId)) =>
|
|
204
|
+
AppSync_Adapter.waitForMergeSuccess(
|
|
205
|
+
AppSync_Adapter.getClient(),
|
|
206
|
+
~associationId,
|
|
207
|
+
~mergedApiIdentifier=identifier,
|
|
208
|
+
)->Pulumi.Output.fromPromise
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
let mergeStatusGate = (
|
|
212
|
+
~mergedApi: t,
|
|
213
|
+
~association: AppSync.SourceApiAssociation.t,
|
|
214
|
+
): Pulumi.Output.t<unit> =>
|
|
215
|
+
mergeStatusGateWith(
|
|
216
|
+
~mergedApiIdentifier=mergedApi.api->Pulumi.Output.flatMap((a: AppSync.GraphQLApi.t) => a.id),
|
|
217
|
+
~association,
|
|
218
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Pervasives from "@rescript/runtime/lib/es6/Pervasives.js";
|
|
4
|
+
import * as Aws from "@pulumi/aws";
|
|
5
|
+
import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
6
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
7
|
+
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
8
|
+
import * as Auth_Cognito$ReventlessAws from "../../adapter/Auth/Auth_Cognito.res.mjs";
|
|
9
|
+
import * as AppSync_Adapter$ReventlessAws from "./AppSync_Adapter.res.mjs";
|
|
10
|
+
|
|
11
|
+
function authenticationTypeName(t) {
|
|
12
|
+
switch (t) {
|
|
13
|
+
case "API_KEY" :
|
|
14
|
+
return "API_KEY";
|
|
15
|
+
case "AWS_IAM" :
|
|
16
|
+
return "AWS_IAM";
|
|
17
|
+
case "AMAZON_COGNITO_USER_POOLS" :
|
|
18
|
+
return "AMAZON_COGNITO_USER_POOLS";
|
|
19
|
+
case "OPENID_CONNECT" :
|
|
20
|
+
return "OPENID_CONNECT";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let primaryAuthMode = authenticationTypeName(AppSync_Adapter$ReventlessAws.primaryAuthenticationType);
|
|
25
|
+
|
|
26
|
+
function assertCompatiblePrimaryAuth(sourceMode, mergedMode) {
|
|
27
|
+
if (sourceMode !== mergedMode) {
|
|
28
|
+
return Pervasives.failwith(`Merged-API primary-auth mismatch: the source API uses ` + sourceMode + ` but the merged API ` + (`expects ` + mergedMode + `. A source API must share the merged API's primary authentication `) + `mode — align the source API's authenticationType with the platform's \`mergedApiPrimaryAuth\` export before associating.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function make(name, opts) {
|
|
33
|
+
let customOpts_parent = opts.parent;
|
|
34
|
+
let customOpts = {
|
|
35
|
+
parent: customOpts_parent
|
|
36
|
+
};
|
|
37
|
+
let executionRole = new (Aws.iam.Role)(name + `-merge-exec-role`, {
|
|
38
|
+
assumeRolePolicy: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"appsync.amazonaws.com"},"Action":"sts:AssumeRole"}]}`
|
|
39
|
+
}, customOpts);
|
|
40
|
+
new (Aws.iam.RolePolicy)(name + `-merge-exec-policy`, {
|
|
41
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `-merge-exec-policy`, [{
|
|
42
|
+
Sid: "AllowSourceGraphQLAndMerge",
|
|
43
|
+
Effect: "Allow",
|
|
44
|
+
Action: [
|
|
45
|
+
"appsync:SourceGraphQL",
|
|
46
|
+
"appsync:StartSchemaMerge"
|
|
47
|
+
],
|
|
48
|
+
Resource: "*"
|
|
49
|
+
}])),
|
|
50
|
+
role: executionRole.id
|
|
51
|
+
}, customOpts);
|
|
52
|
+
let authConfigOut = Auth_Cognito$ReventlessAws.make(name + `-auth`, undefined);
|
|
53
|
+
let userPoolConfigOut = authConfigOut.apply(c => ({
|
|
54
|
+
userPoolId: c.userPoolId,
|
|
55
|
+
defaultAction: "ALLOW",
|
|
56
|
+
awsRegion: c.region
|
|
57
|
+
}));
|
|
58
|
+
let mergedApi = new (Aws.appsync.GraphQLApi)(name, {
|
|
59
|
+
authenticationType: AppSync_Adapter$ReventlessAws.primaryAuthenticationType,
|
|
60
|
+
userPoolConfig: userPoolConfigOut,
|
|
61
|
+
additionalAuthenticationProviders: [{
|
|
62
|
+
authenticationType: "AWS_IAM"
|
|
63
|
+
}],
|
|
64
|
+
apiType: "MERGED",
|
|
65
|
+
mergedApiExecutionRoleArn: executionRole.arn
|
|
66
|
+
}, customOpts);
|
|
67
|
+
return {
|
|
68
|
+
api: Pulumi.output(mergedApi),
|
|
69
|
+
executionRole: executionRole
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function associateSource(name, mergedApi, sourceApi, opts) {
|
|
74
|
+
let customOpts_parent = opts.parent;
|
|
75
|
+
let customOpts = {
|
|
76
|
+
parent: customOpts_parent
|
|
77
|
+
};
|
|
78
|
+
return new (Aws.appsync.SourceApiAssociation)(name, {
|
|
79
|
+
mergedApiId: Output$Pulumi.flatMap(mergedApi.api, a => a.id),
|
|
80
|
+
sourceApiId: Output$Pulumi.flatMap(sourceApi, a => a.id),
|
|
81
|
+
sourceApiAssociationConfigs: [{
|
|
82
|
+
mergeType: "AUTO_MERGE"
|
|
83
|
+
}]
|
|
84
|
+
}, customOpts);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function associateSourceWithMergedArn(name, mergedApiArn, sourceApi, opts) {
|
|
88
|
+
let customOpts_parent = opts.parent;
|
|
89
|
+
let customOpts = {
|
|
90
|
+
parent: customOpts_parent
|
|
91
|
+
};
|
|
92
|
+
return new (Aws.appsync.SourceApiAssociation)(name, {
|
|
93
|
+
mergedApiArn: mergedApiArn,
|
|
94
|
+
sourceApiId: Output$Pulumi.flatMap(sourceApi, a => a.id),
|
|
95
|
+
sourceApiAssociationConfigs: [{
|
|
96
|
+
mergeType: "AUTO_MERGE"
|
|
97
|
+
}]
|
|
98
|
+
}, customOpts);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function mergeStatusGateWith(mergedApiIdentifier, association) {
|
|
102
|
+
return Output$Pulumi.flatMap(Pulumi.all([
|
|
103
|
+
mergedApiIdentifier,
|
|
104
|
+
association.associationId
|
|
105
|
+
]), param => AppSync_Adapter$ReventlessAws.waitForMergeSuccess(AppSync_Adapter$ReventlessAws.getClient(), param[1], param[0], undefined, undefined, undefined));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function mergeStatusGate(mergedApi, association) {
|
|
109
|
+
return mergeStatusGateWith(Output$Pulumi.flatMap(mergedApi.api, a => a.id), association);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
authenticationTypeName,
|
|
114
|
+
primaryAuthMode,
|
|
115
|
+
assertCompatiblePrimaryAuth,
|
|
116
|
+
make,
|
|
117
|
+
associateSource,
|
|
118
|
+
associateSourceWithMergedArn,
|
|
119
|
+
mergeStatusGateWith,
|
|
120
|
+
mergeStatusGate,
|
|
121
|
+
}
|
|
122
|
+
/* primaryAuthMode Not a pure module */
|