@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.
Files changed (33) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res +348 -707
  4. package/src/Platform.res.mjs +271 -739
  5. package/src/adapter/QueryDb/PgQueryResolver_Builder.res +4 -47
  6. package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +1 -30
  7. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +0 -11
  8. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +0 -5
  9. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +6 -7
  10. package/src/components/Api/AppSync_Adapter.res +143 -101
  11. package/src/components/Api/AppSync_Adapter.res.mjs +60 -57
  12. package/src/components/Api/AppSync_MergedApi.res +218 -0
  13. package/src/components/Api/AppSync_MergedApi.res.mjs +122 -0
  14. package/src/components/Api/AppSync_SdlDecorate.res +49 -61
  15. package/src/components/Api/AppSync_SdlDecorate.res.mjs +38 -29
  16. package/src/components/Plugin.res.mjs +1 -2
  17. package/tests/AppSync_AdapterTest.res +184 -0
  18. package/tests/AppSync_AdapterTest.res.mjs +136 -0
  19. package/tests/AppSync_SdlDecorateTest.res +33 -86
  20. package/tests/AppSync_SdlDecorateTest.res.mjs +26 -71
  21. package/tests/MCP_LambdaTest.res +4 -6
  22. package/tests/MCP_LambdaTest.res.mjs +2 -2
  23. package/src/adapter/Api/ApiFragmentDeregistration.res +0 -138
  24. package/src/adapter/Api/ApiFragmentDeregistration.res.mjs +0 -108
  25. package/src/adapter/Api/ApiSchemaPush.res +0 -79
  26. package/src/adapter/Api/ApiSchemaPush.res.mjs +0 -64
  27. package/src/adapter/Api/Platform_ApiFragments_Lambda.res +0 -222
  28. package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +0 -202
  29. package/src/adapter/QueryDb/NodeResolver_AppSync.res +0 -71
  30. package/src/adapter/QueryDb/NodeResolver_AppSync.res.mjs +0 -44
  31. package/src/adapter/Runtime/ApiSchemaPush_Runtime.mjs +0 -204
  32. package/tests/ApiSchemaPushTest.res +0 -32
  33. 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 stitchWithAwsDirectives(baseFragment, pluginFragments) {
267
- let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(baseFragment, pluginFragments);
268
- return AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, pluginFragments), sources));
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 makeApiResource(name, opts) {
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 authConfigOut = Auth_Cognito$ReventlessAws.make(name + `-auth`, undefined);
280
- let userPoolConfigOut = authConfigOut.apply(c => ({
281
- userPoolId: c.userPoolId,
282
- defaultAction: "ALLOW",
283
- awsRegion: c.region
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
- stitchWithAwsDirectives,
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), the `CommandResult` members (mutation
123
- // returns, deduped across fragments by the stitcher), and `Platform_ApiFragmentEntry`
124
- // (the return type of the IAM-callable `Platform_ApiFragments` status query the deploy
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
- // ── Reactive push planner (runtime-pure) ─────────────────────────────────────
143
- // The AWS-decorated counterpart of core `GraphQL_PushPlanner.planPushes`: given
144
- // the fragments currently in the ApiFragmentRegistry (each tagged with its target
145
- // API, as the bare string "Domain" / "Platform"), it produces one fully
146
- // AppSync-decorated SDL push plan per API. It reuses the core planner for the
147
- // neutral base-selection + stitch, then applies the AppSync dialect exactly as the
148
- // deploy path's `stitchWithAwsDirectives`:
149
- // - the admin base is auth-decorated (group "Admin" + dual-auth on the system-
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
- type awsPushPlan = {
165
- // "PlatformApi" | "DomainApi" the bare-string runtime form of GraphQL_PushPlanner.planTarget.
166
- api: string,
167
- sdl: string,
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 planAwsPushes = (
171
- ~rawAdminBase: Reventless.Plugin.apiSchemaFragment,
172
- ~iamFieldNames: array<string>,
173
- ~fragments: array<targetedFragmentInput>,
174
- ~splitApi: bool,
175
- ): array<awsPushPlan> => {
176
- let authBase = injectAwsAuthAll(rawAdminBase, ~group="Admin", ~iamFieldNames)
177
- let targeted = fragments->Array.map((f): ReventlessCore.GraphQL_PushPlanner.targetedFragment => {
178
- fragment: {encoded: f.encoded, protocol: f.protocol},
179
- target: switch f.target {
180
- | "Platform" => Platform
181
- | _ => Domain
182
- },
183
- })
184
- let allFrags = targeted->Array.map(t => t.fragment)
185
- let sources = ReventlessCore.GraphQL_Stitcher.collectSubscriptionSources(
186
- ~baseFragment=rawAdminBase,
187
- ~pluginFragments=allFrags,
188
- )
189
- ReventlessCore.GraphQL_PushPlanner.planPushes(~adminBase=authBase, ~fragments=targeted, ~splitApi)
190
- ->Array.map(plan => {
191
- let sdl = plan.sdl->injectAwsSubscribe(~sources)->stampSharedIamTypes
192
- let api = switch plan.api {
193
- | PlatformApi => "PlatformApi"
194
- | DomainApi => "DomainApi"
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.)