@reventlessdev/reventless-aws 3.0.0-alpha.209 → 3.0.0-alpha.211
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/package.json +8 -8
- package/src/Platform.res +348 -707
- package/src/Platform.res.mjs +271 -739
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res +4 -47
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +1 -30
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +0 -11
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +0 -5
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +6 -7
- package/src/components/Api/AppSync_Adapter.res +143 -101
- package/src/components/Api/AppSync_Adapter.res.mjs +60 -57
- 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 +49 -61
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +38 -29
- package/src/components/Plugin.res.mjs +1 -2
- package/tests/AppSync_AdapterTest.res +184 -0
- package/tests/AppSync_AdapterTest.res.mjs +136 -0
- package/tests/AppSync_SdlDecorateTest.res +33 -86
- package/tests/AppSync_SdlDecorateTest.res.mjs +26 -71
- package/tests/MCP_LambdaTest.res +4 -6
- package/tests/MCP_LambdaTest.res.mjs +2 -2
- package/src/adapter/Api/ApiFragmentDeregistration.res +0 -138
- package/src/adapter/Api/ApiFragmentDeregistration.res.mjs +0 -108
- package/src/adapter/Api/ApiSchemaPush.res +0 -79
- package/src/adapter/Api/ApiSchemaPush.res.mjs +0 -64
- package/src/adapter/Api/Platform_ApiFragments_Lambda.res +0 -222
- package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +0 -202
- package/src/adapter/QueryDb/NodeResolver_AppSync.res +0 -71
- package/src/adapter/QueryDb/NodeResolver_AppSync.res.mjs +0 -44
- package/src/adapter/Runtime/ApiSchemaPush_Runtime.mjs +0 -204
- package/tests/ApiSchemaPushTest.res +0 -32
- package/tests/ApiSchemaPushTest.res.mjs +0 -20
|
@@ -4,14 +4,12 @@ import * as Effect from "@reventlessdev/rescript-effect/src/Effect.res.mjs";
|
|
|
4
4
|
import * as Aws from "@pulumi/aws";
|
|
5
5
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
6
6
|
import * as Nodecrypto from "node:crypto";
|
|
7
|
-
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
8
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
9
8
|
import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
|
|
10
9
|
import * as Effect$1 from "effect/Effect";
|
|
11
10
|
import * as Pulumi from "@pulumi/pulumi";
|
|
12
11
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
13
12
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
14
|
-
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
15
13
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
16
14
|
import * as ClientAppsync from "@aws-sdk/client-appsync";
|
|
17
15
|
import * as Auth_Cognito$ReventlessAws from "../../adapter/Auth/Auth_Cognito.res.mjs";
|
|
@@ -60,33 +58,6 @@ async function waitForSchemaActive(client, apiId, maxAttemptsOpt, attemptOpt) {
|
|
|
60
58
|
}
|
|
61
59
|
}
|
|
62
60
|
|
|
63
|
-
async function getIntrospectionSdl(client, apiId) {
|
|
64
|
-
try {
|
|
65
|
-
let resp = await client.send(new ClientAppsync.GetIntrospectionSchemaCommand({
|
|
66
|
-
apiId: apiId,
|
|
67
|
-
format: "SDL"
|
|
68
|
-
}));
|
|
69
|
-
let blob = resp.schema;
|
|
70
|
-
if (blob !== undefined) {
|
|
71
|
-
return Buffer.from(Primitive_option.valFromOption(blob)).toString("utf-8");
|
|
72
|
-
} else {
|
|
73
|
-
return "";
|
|
74
|
-
}
|
|
75
|
-
} catch (raw_exn) {
|
|
76
|
-
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
77
|
-
let msg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
|
|
78
|
-
log.warn("AppSync_Adapter", undefined, `getIntrospectionSdl failed for ` + apiId + `: ` + msg);
|
|
79
|
-
return "";
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function deploySchemaWithRetry(client, apiId, definition) {
|
|
84
|
-
return Effect$1.retry(Effect.tryPromise(AppSync_Error$ReventlessAws.classify, () => client.send(new ClientAppsync.StartSchemaCreationCommand({
|
|
85
|
-
apiId: apiId,
|
|
86
|
-
definition: definition
|
|
87
|
-
})).then(param => Promise.resolve())), AppSync_Error$ReventlessAws.retrySchedule);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
61
|
let _client = {
|
|
91
62
|
contents: undefined
|
|
92
63
|
};
|
|
@@ -101,6 +72,35 @@ function getClient() {
|
|
|
101
72
|
return c$1;
|
|
102
73
|
}
|
|
103
74
|
|
|
75
|
+
async function waitForMergeSuccess(client, associationId, mergedApiIdentifier, maxAttemptsOpt, attemptOpt, delayMsOpt) {
|
|
76
|
+
let maxAttempts = maxAttemptsOpt !== undefined ? maxAttemptsOpt : 60;
|
|
77
|
+
let attempt = attemptOpt !== undefined ? attemptOpt : 0;
|
|
78
|
+
let delayMs = delayMsOpt !== undefined ? delayMsOpt : 2000;
|
|
79
|
+
let result = await client.send(new ClientAppsync.GetSourceApiAssociationCommand({
|
|
80
|
+
associationId: associationId,
|
|
81
|
+
mergedApiIdentifier: mergedApiIdentifier
|
|
82
|
+
}));
|
|
83
|
+
let status = Stdlib_Option.getOr(Stdlib_Option.flatMap(result.sourceApiAssociation, a => a.sourceApiAssociationStatus), "(no status)");
|
|
84
|
+
let detail = Stdlib_Option.getOr(Stdlib_Option.flatMap(result.sourceApiAssociation, a => a.sourceApiAssociationStatusDetail), "(no details)");
|
|
85
|
+
switch (status) {
|
|
86
|
+
case "AUTO_MERGE_SCHEDULE_FAILED" :
|
|
87
|
+
case "MERGE_FAILED" :
|
|
88
|
+
break;
|
|
89
|
+
case "MERGE_SUCCESS" :
|
|
90
|
+
return;
|
|
91
|
+
default:
|
|
92
|
+
if (attempt >= maxAttempts) {
|
|
93
|
+
return Stdlib_JsError.throwWithMessage(`Source API association ` + associationId + ` merge timed out after ` + maxAttempts.toString() + ` attempts (status: ` + status + `)`);
|
|
94
|
+
} else {
|
|
95
|
+
await new Promise((resolve, param) => {
|
|
96
|
+
setTimeout(resolve, delayMs);
|
|
97
|
+
});
|
|
98
|
+
return await waitForMergeSuccess(client, associationId, mergedApiIdentifier, maxAttempts, attempt + 1 | 0, delayMs);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return Stdlib_JsError.throwWithMessage(`Source API association ` + associationId + ` on ` + mergedApiIdentifier + ` failed to merge (` + status + `): ` + detail);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
104
|
function _permissionToCognitoGroups(permission) {
|
|
105
105
|
if (typeof permission !== "object") {
|
|
106
106
|
switch (permission) {
|
|
@@ -263,12 +263,12 @@ function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
|
|
|
263
263
|
return AppSync_SdlDecorate$ReventlessAws.injectAwsAuthAll(fragment, group, iamFieldNames);
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
function
|
|
267
|
-
let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(
|
|
268
|
-
return AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.
|
|
266
|
+
function stitchStandaloneWithAwsDirectives(fragment) {
|
|
267
|
+
let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(fragment, []);
|
|
268
|
+
return AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitchStandalone(fragment), sources));
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
function
|
|
271
|
+
function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
|
|
272
272
|
let customOpts_parent = opts.parent;
|
|
273
273
|
let customOpts = {
|
|
274
274
|
parent: customOpts_parent
|
|
@@ -276,18 +276,19 @@ function makeApiResource(name, opts) {
|
|
|
276
276
|
let iamRole = new (Aws.iam.Role)(name + `-appsync-role`, {
|
|
277
277
|
assumeRolePolicy: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"appsync.amazonaws.com"},"Action":"sts:AssumeRole"}]}`
|
|
278
278
|
}, customOpts);
|
|
279
|
-
let
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
279
|
+
let userPoolConfigOut = userPoolConfig !== undefined ? userPoolConfig : Auth_Cognito$ReventlessAws.make(name + `-auth`, undefined).apply(c => ({
|
|
280
|
+
userPoolId: c.userPoolId,
|
|
281
|
+
defaultAction: "ALLOW",
|
|
282
|
+
awsRegion: c.region
|
|
283
|
+
}));
|
|
284
|
+
let apiArgs_schema = Stdlib_Option.map(schema, prim => prim);
|
|
285
285
|
let apiArgs_userPoolConfig = userPoolConfigOut;
|
|
286
286
|
let apiArgs_additionalAuthenticationProviders = [{
|
|
287
287
|
authenticationType: "AWS_IAM"
|
|
288
288
|
}];
|
|
289
289
|
let apiArgs = {
|
|
290
290
|
authenticationType: "AMAZON_COGNITO_USER_POOLS",
|
|
291
|
+
schema: apiArgs_schema,
|
|
291
292
|
userPoolConfig: apiArgs_userPoolConfig,
|
|
292
293
|
additionalAuthenticationProviders: apiArgs_additionalAuthenticationProviders
|
|
293
294
|
};
|
|
@@ -298,37 +299,36 @@ function makeApiResource(name, opts) {
|
|
|
298
299
|
];
|
|
299
300
|
}
|
|
300
301
|
|
|
302
|
+
function makeApiResource(name, opts) {
|
|
303
|
+
return _makeApiResourceWith(name, undefined, undefined, opts);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function makeSourceApiResource(name, schema, opts) {
|
|
307
|
+
return _makeApiResourceWith(name, schema, undefined, opts);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function makePluginSourceApiResource(name, userPoolConfig, opts) {
|
|
311
|
+
return _makeApiResourceWith(name, undefined, userPoolConfig, opts);
|
|
312
|
+
}
|
|
313
|
+
|
|
301
314
|
function generateFragment(mutationEntries, queryEntries) {
|
|
302
315
|
let fragment = GraphQL_FragmentGenerator$ReventlessCore.generate(mutationEntries, queryEntries);
|
|
303
316
|
return injectAwsAuth(fragment, mutationEntries, queryEntries);
|
|
304
317
|
}
|
|
305
318
|
|
|
306
|
-
function updateSchema(api, baseFragment, pluginFragments) {
|
|
307
|
-
let augmentedBaseFragment = injectAwsAuthAll(baseFragment, "Admin", undefined);
|
|
308
|
-
let sdl = stitchWithAwsDirectives(augmentedBaseFragment, pluginFragments);
|
|
309
|
-
let resultPromise = {
|
|
310
|
-
contents: Promise.resolve()
|
|
311
|
-
};
|
|
312
|
-
api.apply(graphQLApi => graphQLApi.id.apply(apiId => {
|
|
313
|
-
let effect = deploySchemaWithRetry(getClient(), apiId, sdl);
|
|
314
|
-
let p = Effect$1.runPromise(effect);
|
|
315
|
-
resultPromise.contents = p;
|
|
316
|
-
}));
|
|
317
|
-
return resultPromise.contents;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
319
|
let stampSharedIamTypes = AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes;
|
|
321
320
|
|
|
321
|
+
let primaryAuthenticationType = "AMAZON_COGNITO_USER_POOLS";
|
|
322
|
+
|
|
322
323
|
export {
|
|
323
324
|
log,
|
|
324
325
|
sha256Hex,
|
|
325
326
|
startSchemaCreation,
|
|
326
327
|
startSchemaCreationRetrying,
|
|
327
328
|
waitForSchemaActive,
|
|
328
|
-
getIntrospectionSdl,
|
|
329
|
-
deploySchemaWithRetry,
|
|
330
329
|
_client,
|
|
331
330
|
getClient,
|
|
331
|
+
waitForMergeSuccess,
|
|
332
332
|
_permissionToCognitoGroups,
|
|
333
333
|
_formatGroupsDirective,
|
|
334
334
|
_formatDualAuthDirective,
|
|
@@ -337,9 +337,12 @@ export {
|
|
|
337
337
|
stampSharedIamTypes,
|
|
338
338
|
injectAwsAuth,
|
|
339
339
|
injectAwsAuthAll,
|
|
340
|
-
|
|
340
|
+
stitchStandaloneWithAwsDirectives,
|
|
341
|
+
primaryAuthenticationType,
|
|
342
|
+
_makeApiResourceWith,
|
|
341
343
|
makeApiResource,
|
|
344
|
+
makeSourceApiResource,
|
|
345
|
+
makePluginSourceApiResource,
|
|
342
346
|
generateFragment,
|
|
343
|
-
updateSchema,
|
|
344
347
|
}
|
|
345
348
|
/* log Not a pure module */
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// AppSync_MergedApi — merged-API construction for the push-free composition
|
|
2
|
+
// path (docs/plans/done/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 */
|
|
@@ -119,19 +119,14 @@ let injectAwsAuthAll = (
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
// Shared traversal types every callable surface reaches — `PageInfo` (relay
|
|
122
|
-
// connections, injected by the stitcher)
|
|
123
|
-
// returns
|
|
124
|
-
// (
|
|
125
|
-
// waiter polls via SigV4 — without the type-level `@aws_iam` the SigV4 caller reaches the
|
|
126
|
-
// query field but is "Not Authorized to access <field> on type Platform_ApiFragmentEntry").
|
|
127
|
-
// Stamped once on the ASSEMBLED SDL: per-fragment stamping would race the stitcher's
|
|
128
|
-
// first-wins dedupe against unstamped sibling copies.
|
|
122
|
+
// connections, injected by the stitcher) and the `CommandResult` members
|
|
123
|
+
// (mutation returns). Stamped once on the ASSEMBLED SDL so an IAM system
|
|
124
|
+
// caller (`@@reventless.systemCallable` fields) can traverse them.
|
|
129
125
|
let sharedIamTypeNames = [
|
|
130
126
|
"PageInfo",
|
|
131
127
|
"CommandAccepted",
|
|
132
128
|
"CommandRejected",
|
|
133
129
|
"CommandPending",
|
|
134
|
-
"Platform_ApiFragmentEntry",
|
|
135
130
|
]
|
|
136
131
|
|
|
137
132
|
let stampSharedIamTypes = (sdl: string): string =>
|
|
@@ -139,60 +134,53 @@ let stampSharedIamTypes = (sdl: string): string =>
|
|
|
139
134
|
acc->String.replace(`type ${name} {`, `type ${name} @aws_cognito_user_pools @aws_iam {`)
|
|
140
135
|
)
|
|
141
136
|
|
|
142
|
-
// ──
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
// caller field names) BEFORE stitching, so the Platform/unified base carries
|
|
151
|
-
// @aws_auth/@aws_iam (the empty Domain-split base needs none);
|
|
152
|
-
// - @aws_subscribe is injected from the neutral subscriptionSources metadata;
|
|
153
|
-
// - the shared traversal types are stamped once on the assembled SDL.
|
|
154
|
-
// Plugin fragments already carry their own per-field @aws_auth (baked at
|
|
155
|
-
// generateFragment time), so they stitch in as-is. `api` comes back as the bare
|
|
156
|
-
// string "PlatformApi" / "DomainApi" for the mjs to map onto the AppSync ids.
|
|
157
|
-
type targetedFragmentInput = {
|
|
158
|
-
encoded: string,
|
|
159
|
-
protocol: string,
|
|
160
|
-
// "Domain" | "Platform" — the bare-string runtime form of Reventless.Plugin.apiTarget.
|
|
161
|
-
target: string,
|
|
162
|
-
}
|
|
137
|
+
// ── Merged-API canonical stamping ─────────────────────────────────────────────
|
|
138
|
+
// Under AppSync Merged APIs the admin source API owns the shared traversal
|
|
139
|
+
// types; `@canonical` makes its definition win over every plugin source's copy
|
|
140
|
+
// (plugin copies must still exist — a source schema has to be valid standalone).
|
|
141
|
+
// Spike-validated (plan Phase 0): a divergent non-canonical copy is SHADOWED by
|
|
142
|
+
// the canonical definition, not a MERGE_FAILED — so this stamp is what keeps
|
|
143
|
+
// shared-type evolution single-owner. Applied only to the ADMIN source SDL on
|
|
144
|
+
// the merge path; plugin subgraph documents stay unstamped.
|
|
163
145
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
146
|
+
// Object types the admin source owns canonically. `interface Node` and
|
|
147
|
+
// `union CommandResult` are handled structurally below (their def lines don't
|
|
148
|
+
// start with `type `).
|
|
149
|
+
let canonicalTypeNames = ["PageInfo", "CommandAccepted", "CommandRejected", "CommandPending"]
|
|
169
150
|
|
|
170
|
-
let
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
151
|
+
let stampCanonicalTypes = (sdl: string): string =>
|
|
152
|
+
sdl
|
|
153
|
+
->String.split("\n")
|
|
154
|
+
->Array.map(line => {
|
|
155
|
+
let isObjectDef = canonicalTypeNames->Array.some(name => line->String.startsWith(`type ${name} `))
|
|
156
|
+
let isNodeDef = line->String.startsWith("interface Node ") || line->String.startsWith("interface Node{")
|
|
157
|
+
let isUnionDef = line->String.startsWith("union CommandResult ") || line->String.startsWith("union CommandResult=")
|
|
158
|
+
if line->String.includes("@canonical") {
|
|
159
|
+
line
|
|
160
|
+
} else if isObjectDef || isNodeDef {
|
|
161
|
+
// Insert before the opening brace so it composes with earlier stamps
|
|
162
|
+
// (e.g. `type PageInfo @aws_cognito_user_pools @aws_iam {`).
|
|
163
|
+
switch line->String.indexOfOpt("{") {
|
|
164
|
+
| Some(braceIdx) =>
|
|
165
|
+
let head = line->String.slice(~start=0, ~end=braceIdx)->String.trimEnd
|
|
166
|
+
let tail = line->String.slice(~start=braceIdx)
|
|
167
|
+
`${head} @canonical ${tail}`
|
|
168
|
+
| None => line
|
|
169
|
+
}
|
|
170
|
+
} else if isUnionDef {
|
|
171
|
+
switch line->String.indexOfOpt("=") {
|
|
172
|
+
| Some(eqIdx) =>
|
|
173
|
+
let head = line->String.slice(~start=0, ~end=eqIdx)->String.trimEnd
|
|
174
|
+
let tail = line->String.slice(~start=eqIdx)
|
|
175
|
+
`${head} @canonical ${tail}`
|
|
176
|
+
| None => line
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
line
|
|
195
180
|
}
|
|
196
|
-
{api, sdl}
|
|
197
181
|
})
|
|
198
|
-
|
|
182
|
+
->Array.join("\n")
|
|
183
|
+
|
|
184
|
+
// (The reactive push planner — planAwsPushes, the AWS counterpart of core
|
|
185
|
+
// GraphQL_PushPlanner — was retired with the merged-API cutover: source APIs
|
|
186
|
+
// are single writers, AWS composes the merged endpoint, nothing re-stitches.)
|