@reventlessdev/reventless-aws 3.0.0-alpha.208 → 3.0.0-alpha.210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/package.json +7 -7
- package/src/Platform.res +478 -545
- package/src/Platform.res.mjs +393 -717
- package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +7 -382
- package/src/adapter/Runtime/ApiSchemaPush_Runtime.mjs +8 -1
- package/src/components/Api/AppSync_Adapter.res +144 -101
- package/src/components/Api/AppSync_Adapter.res.mjs +62 -91
- package/src/components/Api/AppSync_MergedApi.res +218 -0
- package/src/components/Api/AppSync_MergedApi.res.mjs +122 -0
- package/src/components/Api/AppSync_SdlDecorate.res +47 -0
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +39 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res +0 -139
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +4 -80
- package/tests/AppSync_AdapterTest.res +176 -0
- package/tests/AppSync_AdapterTest.res.mjs +134 -0
- package/tests/AppSync_SdlDecorateTest.res +27 -0
- package/tests/AppSync_SdlDecorateTest.res.mjs +20 -0
- package/tests/MCP_LambdaTest.res +6 -4
- package/tests/MCP_LambdaTest.res.mjs +2 -2
|
@@ -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 */
|
|
@@ -139,6 +139,53 @@ let stampSharedIamTypes = (sdl: string): string =>
|
|
|
139
139
|
acc->String.replace(`type ${name} {`, `type ${name} @aws_cognito_user_pools @aws_iam {`)
|
|
140
140
|
)
|
|
141
141
|
|
|
142
|
+
// ── Merged-API canonical stamping ─────────────────────────────────────────────
|
|
143
|
+
// Under AppSync Merged APIs the admin source API owns the shared traversal
|
|
144
|
+
// types; `@canonical` makes its definition win over every plugin source's copy
|
|
145
|
+
// (plugin copies must still exist — a source schema has to be valid standalone).
|
|
146
|
+
// Spike-validated (plan Phase 0): a divergent non-canonical copy is SHADOWED by
|
|
147
|
+
// the canonical definition, not a MERGE_FAILED — so this stamp is what keeps
|
|
148
|
+
// shared-type evolution single-owner. Applied only to the ADMIN source SDL on
|
|
149
|
+
// the merge path; plugin subgraph documents stay unstamped.
|
|
150
|
+
|
|
151
|
+
// Object types the admin source owns canonically. `interface Node` and
|
|
152
|
+
// `union CommandResult` are handled structurally below (their def lines don't
|
|
153
|
+
// start with `type `).
|
|
154
|
+
let canonicalTypeNames = ["PageInfo", "CommandAccepted", "CommandRejected", "CommandPending"]
|
|
155
|
+
|
|
156
|
+
let stampCanonicalTypes = (sdl: string): string =>
|
|
157
|
+
sdl
|
|
158
|
+
->String.split("\n")
|
|
159
|
+
->Array.map(line => {
|
|
160
|
+
let isObjectDef = canonicalTypeNames->Array.some(name => line->String.startsWith(`type ${name} `))
|
|
161
|
+
let isNodeDef = line->String.startsWith("interface Node ") || line->String.startsWith("interface Node{")
|
|
162
|
+
let isUnionDef = line->String.startsWith("union CommandResult ") || line->String.startsWith("union CommandResult=")
|
|
163
|
+
if line->String.includes("@canonical") {
|
|
164
|
+
line
|
|
165
|
+
} else if isObjectDef || isNodeDef {
|
|
166
|
+
// Insert before the opening brace so it composes with earlier stamps
|
|
167
|
+
// (e.g. `type PageInfo @aws_cognito_user_pools @aws_iam {`).
|
|
168
|
+
switch line->String.indexOfOpt("{") {
|
|
169
|
+
| Some(braceIdx) =>
|
|
170
|
+
let head = line->String.slice(~start=0, ~end=braceIdx)->String.trimEnd
|
|
171
|
+
let tail = line->String.slice(~start=braceIdx)
|
|
172
|
+
`${head} @canonical ${tail}`
|
|
173
|
+
| None => line
|
|
174
|
+
}
|
|
175
|
+
} else if isUnionDef {
|
|
176
|
+
switch line->String.indexOfOpt("=") {
|
|
177
|
+
| Some(eqIdx) =>
|
|
178
|
+
let head = line->String.slice(~start=0, ~end=eqIdx)->String.trimEnd
|
|
179
|
+
let tail = line->String.slice(~start=eqIdx)
|
|
180
|
+
`${head} @canonical ${tail}`
|
|
181
|
+
| None => line
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
line
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
->Array.join("\n")
|
|
188
|
+
|
|
142
189
|
// ── Reactive push planner (runtime-pure) ─────────────────────────────────────
|
|
143
190
|
// The AWS-decorated counterpart of core `GraphQL_PushPlanner.planPushes`: given
|
|
144
191
|
// the fragments currently in the ApiFragmentRegistry (each tagged with its target
|
|
@@ -88,6 +88,43 @@ function stampSharedIamTypes(sdl) {
|
|
|
88
88
|
return Stdlib_Array.reduce(sharedIamTypeNames, sdl, (acc, name) => acc.replace(`type ` + name + ` {`, `type ` + name + ` @aws_cognito_user_pools @aws_iam {`));
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
let canonicalTypeNames = [
|
|
92
|
+
"PageInfo",
|
|
93
|
+
"CommandAccepted",
|
|
94
|
+
"CommandRejected",
|
|
95
|
+
"CommandPending"
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
function stampCanonicalTypes(sdl) {
|
|
99
|
+
return sdl.split("\n").map(line => {
|
|
100
|
+
let isObjectDef = canonicalTypeNames.some(name => line.startsWith(`type ` + name + ` `));
|
|
101
|
+
let isNodeDef = line.startsWith("interface Node ") || line.startsWith("interface Node{");
|
|
102
|
+
let isUnionDef = line.startsWith("union CommandResult ") || line.startsWith("union CommandResult=");
|
|
103
|
+
if (line.includes("@canonical")) {
|
|
104
|
+
return line;
|
|
105
|
+
}
|
|
106
|
+
if (isObjectDef || isNodeDef) {
|
|
107
|
+
let braceIdx = Stdlib_String.indexOfOpt(line, "{");
|
|
108
|
+
if (braceIdx === undefined) {
|
|
109
|
+
return line;
|
|
110
|
+
}
|
|
111
|
+
let head = line.slice(0, braceIdx).trimEnd();
|
|
112
|
+
let tail = line.slice(braceIdx);
|
|
113
|
+
return head + ` @canonical ` + tail;
|
|
114
|
+
}
|
|
115
|
+
if (!isUnionDef) {
|
|
116
|
+
return line;
|
|
117
|
+
}
|
|
118
|
+
let eqIdx = Stdlib_String.indexOfOpt(line, "=");
|
|
119
|
+
if (eqIdx === undefined) {
|
|
120
|
+
return line;
|
|
121
|
+
}
|
|
122
|
+
let head$1 = line.slice(0, eqIdx).trimEnd();
|
|
123
|
+
let tail$1 = line.slice(eqIdx);
|
|
124
|
+
return head$1 + ` @canonical ` + tail$1;
|
|
125
|
+
}).join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
91
128
|
function planAwsPushes(rawAdminBase, iamFieldNames, fragments, splitApi) {
|
|
92
129
|
let authBase = injectAwsAuthAll(rawAdminBase, "Admin", iamFieldNames);
|
|
93
130
|
let targeted = fragments.map(f => {
|
|
@@ -121,6 +158,8 @@ export {
|
|
|
121
158
|
injectAwsAuthAll,
|
|
122
159
|
sharedIamTypeNames,
|
|
123
160
|
stampSharedIamTypes,
|
|
161
|
+
canonicalTypeNames,
|
|
162
|
+
stampCanonicalTypes,
|
|
124
163
|
planAwsPushes,
|
|
125
164
|
}
|
|
126
165
|
/* GraphQL_Stitcher-ReventlessCore Not a pure module */
|
|
@@ -3,38 +3,21 @@ let log = ReventlessCore.Logger.fromEnv()
|
|
|
3
3
|
type adminConfig = {
|
|
4
4
|
eventTopicArn: option<Pulumi.Output.t<string>>,
|
|
5
5
|
pluginReadModelTableName: option<Pulumi.Output.t<string>>,
|
|
6
|
-
// Dedicated PluginSchemaPersistence table holding deploy-time SDL fragments
|
|
7
|
-
// (rows keyed "deploy-schema:<name>"). The runtime schema stitch reads this
|
|
8
|
-
// durable source instead of the lifecycle-volatile Plugin RM Connected rows.
|
|
9
|
-
pluginSchemaPersistenceTableName: option<Pulumi.Output.t<string>>,
|
|
10
6
|
schedulerRoleArn: option<Pulumi.Output.t<string>>,
|
|
11
7
|
schedulerQueueArn: option<Pulumi.Output.t<string>>,
|
|
12
8
|
schedulerQueueName: option<Pulumi.Output.t<string>>,
|
|
13
9
|
appSyncApiId: option<Pulumi.Output.t<string>>,
|
|
14
10
|
clonerEnabled: bool,
|
|
15
|
-
// Reactive ApiFragmentRegistry single writer (2e), admin EventCollector only:
|
|
16
|
-
// the Platform AppSync id (split mode; unified → same as appSyncApiId), the
|
|
17
|
-
// ApiFragments StateViewSlice table it scans, the admin DCB command-topic FIFO
|
|
18
|
-
// URL it dispatches RecordApiFragmentPush to, and the split/unified mode flag.
|
|
19
|
-
platformApiId: option<Pulumi.Output.t<string>>,
|
|
20
|
-
apiFragmentRegistryTableName: option<Pulumi.Output.t<string>>,
|
|
21
|
-
adminDcbCmdTopicUrl: option<Pulumi.Output.t<string>>,
|
|
22
|
-
splitApi: bool,
|
|
23
11
|
}
|
|
24
12
|
|
|
25
13
|
let configRef: ref<adminConfig> = ref({
|
|
26
14
|
eventTopicArn: None,
|
|
27
15
|
pluginReadModelTableName: None,
|
|
28
|
-
pluginSchemaPersistenceTableName: None,
|
|
29
16
|
schedulerRoleArn: None,
|
|
30
17
|
schedulerQueueArn: None,
|
|
31
18
|
schedulerQueueName: None,
|
|
32
19
|
appSyncApiId: None,
|
|
33
20
|
clonerEnabled: false,
|
|
34
|
-
platformApiId: None,
|
|
35
|
-
apiFragmentRegistryTableName: None,
|
|
36
|
-
adminDcbCmdTopicUrl: None,
|
|
37
|
-
splitApi: false,
|
|
38
21
|
})
|
|
39
22
|
|
|
40
23
|
type sliceModulePaths = {
|
|
@@ -111,31 +94,21 @@ let setStateChangesConfig = (~sync=?, ~async=?, ()) => {
|
|
|
111
94
|
let registerConfig = (
|
|
112
95
|
~eventTopicArn=?,
|
|
113
96
|
~pluginReadModelTableName=?,
|
|
114
|
-
~pluginSchemaPersistenceTableName=?,
|
|
115
97
|
~schedulerRoleArn=?,
|
|
116
98
|
~schedulerQueueArn=?,
|
|
117
99
|
~schedulerQueueName=?,
|
|
118
100
|
~appSyncApiId=?,
|
|
119
101
|
~clonerEnabled=false,
|
|
120
|
-
~platformApiId=?,
|
|
121
|
-
~apiFragmentRegistryTableName=?,
|
|
122
|
-
~adminDcbCmdTopicUrl=?,
|
|
123
|
-
~splitApi=false,
|
|
124
102
|
(),
|
|
125
103
|
) =>
|
|
126
104
|
configRef := {
|
|
127
105
|
eventTopicArn,
|
|
128
106
|
pluginReadModelTableName,
|
|
129
|
-
pluginSchemaPersistenceTableName,
|
|
130
107
|
schedulerRoleArn,
|
|
131
108
|
schedulerQueueArn,
|
|
132
109
|
schedulerQueueName,
|
|
133
110
|
appSyncApiId,
|
|
134
111
|
clonerEnabled,
|
|
135
|
-
platformApiId,
|
|
136
|
-
apiFragmentRegistryTableName,
|
|
137
|
-
adminDcbCmdTopicUrl,
|
|
138
|
-
splitApi,
|
|
139
112
|
}
|
|
140
113
|
|
|
141
114
|
// PluginRuntime_Builder is a functor so that the caller can inject the EventCollectorChannel
|
|
@@ -318,10 +291,6 @@ module Make = (
|
|
|
318
291
|
config.schedulerQueueName->outputOrPlaceholder->Obj.magic,
|
|
319
292
|
config.appSyncApiId->outputOrPlaceholder->Obj.magic,
|
|
320
293
|
epEventTopicArnsOutput->Pulumi.Output.asInput->Obj.magic,
|
|
321
|
-
config.pluginSchemaPersistenceTableName->outputOrPlaceholder->Obj.magic,
|
|
322
|
-
config.platformApiId->outputOrPlaceholder->Obj.magic,
|
|
323
|
-
config.apiFragmentRegistryTableName->outputOrPlaceholder->Obj.magic,
|
|
324
|
-
config.adminDcbCmdTopicUrl->outputOrPlaceholder->Obj.magic,
|
|
325
294
|
])
|
|
326
295
|
->Pulumi.Output.apply(values => {
|
|
327
296
|
let queueUrl = values->Array.getUnsafe(0)
|
|
@@ -333,27 +302,13 @@ module Make = (
|
|
|
333
302
|
let schedQueueName = values->Array.getUnsafe(6)
|
|
334
303
|
let appSyncApiId = values->Array.getUnsafe(7)
|
|
335
304
|
let epEventTopicArns: array<string> = Obj.magic(values->Array.getUnsafe(8))
|
|
336
|
-
let schemaPersistenceTable = values->Array.getUnsafe(9)
|
|
337
|
-
let platformApiId = values->Array.getUnsafe(10)
|
|
338
|
-
let apiFragmentRegistryTable = values->Array.getUnsafe(11)
|
|
339
|
-
let adminDcbCmdTopicUrl = values->Array.getUnsafe(12)
|
|
340
305
|
|
|
341
306
|
let dict = Dict.make()
|
|
342
307
|
dict->Dict.set("queueUrl", JSON.Encode.string(queueUrl))
|
|
343
308
|
dict->Dict.set("pluginExtensionPointCmdTopicUrl", JSON.Encode.string(pluginEpCmdTopicUrl))
|
|
344
309
|
dict->Dict.set("eventTopicArn", JSON.Encode.string(topLevelEventTopicArn))
|
|
345
310
|
dict->Dict.set("pluginReadModelTableName", JSON.Encode.string(rmTable))
|
|
346
|
-
dict->Dict.set(
|
|
347
|
-
"pluginSchemaPersistenceTableName",
|
|
348
|
-
JSON.Encode.string(schemaPersistenceTable),
|
|
349
|
-
)
|
|
350
311
|
dict->Dict.set("appSyncApiId", JSON.Encode.string(appSyncApiId))
|
|
351
|
-
// Reactive ApiFragmentRegistry single writer (2e) — placeholders for
|
|
352
|
-
// plugin ECs / all-at-once platforms disable it in the mjs.
|
|
353
|
-
dict->Dict.set("platformApiId", JSON.Encode.string(platformApiId))
|
|
354
|
-
dict->Dict.set("apiFragmentRegistryTableName", JSON.Encode.string(apiFragmentRegistryTable))
|
|
355
|
-
dict->Dict.set("adminDcbCmdTopicUrl", JSON.Encode.string(adminDcbCmdTopicUrl))
|
|
356
|
-
dict->Dict.set("splitApi", JSON.Encode.bool(config.splitApi))
|
|
357
312
|
dict->Dict.set("clonerEnabled", JSON.Encode.bool(config.clonerEnabled))
|
|
358
313
|
dict->Dict.set("schedulerRoleArn", JSON.Encode.string(schedRoleArn))
|
|
359
314
|
dict->Dict.set("schedulerQueueArn", JSON.Encode.string(schedQueueArn))
|
|
@@ -643,100 +598,6 @@ module Make = (
|
|
|
643
598
|
| None => ()
|
|
644
599
|
}
|
|
645
600
|
|
|
646
|
-
// mkUpdateApiSchema reads each plugin's deploy-time SDL fragment from the
|
|
647
|
-
// dedicated PluginSchemaPersistence table (deploy-schema:<name> rows) to
|
|
648
|
-
// re-stitch the live schema. Grant Scan on that table too — it is owned by
|
|
649
|
-
// Platform.res, so the admin EC's default policy includes no perms on it.
|
|
650
|
-
switch config.pluginSchemaPersistenceTableName {
|
|
651
|
-
| Some(schemaTableOutput) =>
|
|
652
|
-
let policyJson =
|
|
653
|
-
schemaTableOutput->Pulumi.Output.apply(tableName =>
|
|
654
|
-
PulumiAws.PolicyDocument.make(
|
|
655
|
-
~id=`${name}PluginSchemaScanPolicy`,
|
|
656
|
-
~statements=[
|
|
657
|
-
{
|
|
658
|
-
sid: "AllowAdminScanPluginSchemaPersistence",
|
|
659
|
-
effect: Allow,
|
|
660
|
-
actions: Actions(["dynamodb:Scan"]),
|
|
661
|
-
resources: Resource("arn:aws:dynamodb:*:*:table/" ++ tableName),
|
|
662
|
-
},
|
|
663
|
-
],
|
|
664
|
-
)->PulumiAws.PolicyDocument.toJsonString
|
|
665
|
-
)
|
|
666
|
-
let _ = PulumiAws.IAM.RolePolicy.make(
|
|
667
|
-
~name=`${name}-pluginSchemaScan`,
|
|
668
|
-
~args={
|
|
669
|
-
policy: policyJson->Pulumi.Output.asInput,
|
|
670
|
-
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
671
|
-
},
|
|
672
|
-
)
|
|
673
|
-
| None => ()
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// 2e: the reactive ApiFragmentRegistry single writer scans the ApiFragments
|
|
677
|
-
// StateViewSlice table to re-fold the registry on each ApiFragment* event.
|
|
678
|
-
// The table is owned by the admin DcbBuilder, so grant Scan explicitly.
|
|
679
|
-
switch config.apiFragmentRegistryTableName {
|
|
680
|
-
| Some(tableOutput) =>
|
|
681
|
-
let policyJson =
|
|
682
|
-
tableOutput->Pulumi.Output.apply(tableName =>
|
|
683
|
-
PulumiAws.PolicyDocument.make(
|
|
684
|
-
~id=`${name}ApiFragmentsScanPolicy`,
|
|
685
|
-
~statements=[
|
|
686
|
-
{
|
|
687
|
-
sid: "AllowAdminScanApiFragments",
|
|
688
|
-
effect: Allow,
|
|
689
|
-
actions: Actions(["dynamodb:Scan"]),
|
|
690
|
-
resources: Resource("arn:aws:dynamodb:*:*:table/" ++ tableName),
|
|
691
|
-
},
|
|
692
|
-
],
|
|
693
|
-
)->PulumiAws.PolicyDocument.toJsonString
|
|
694
|
-
)
|
|
695
|
-
let _ = PulumiAws.IAM.RolePolicy.make(
|
|
696
|
-
~name=`${name}-apiFragmentsScan`,
|
|
697
|
-
~args={
|
|
698
|
-
policy: policyJson->Pulumi.Output.asInput,
|
|
699
|
-
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
700
|
-
},
|
|
701
|
-
)
|
|
702
|
-
| None => ()
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
// 2e: the reactive push writes the outcome back with RecordApiFragmentPush,
|
|
706
|
-
// dispatched onto the admin DCB command-topic FIFO queue — grant
|
|
707
|
-
// sqs:SendMessage on it (queue URL → ARN, same derivation as
|
|
708
|
-
// publishToAggregates below).
|
|
709
|
-
switch config.adminDcbCmdTopicUrl {
|
|
710
|
-
| Some(urlOutput) =>
|
|
711
|
-
let policyJson =
|
|
712
|
-
urlOutput->Pulumi.Output.apply(url => {
|
|
713
|
-
let arn = switch url->String.split("/") {
|
|
714
|
-
| [_, _, host, acct, qn] =>
|
|
715
|
-
let region = host->String.split(".")->Array.get(1)->Option.getOr("eu-west-1")
|
|
716
|
-
`arn:aws:sqs:${region}:${acct}:${qn}`
|
|
717
|
-
| _ => url
|
|
718
|
-
}
|
|
719
|
-
PulumiAws.PolicyDocument.make(
|
|
720
|
-
~id=`${name}AdminDcbSendPolicy`,
|
|
721
|
-
~statements=[
|
|
722
|
-
{
|
|
723
|
-
sid: "AllowAdminDispatchRecordApiFragmentPush",
|
|
724
|
-
effect: Allow,
|
|
725
|
-
actions: Actions(["sqs:SendMessage"]),
|
|
726
|
-
resources: Resource(arn),
|
|
727
|
-
},
|
|
728
|
-
],
|
|
729
|
-
)->PulumiAws.PolicyDocument.toJsonString
|
|
730
|
-
})
|
|
731
|
-
let _ = PulumiAws.IAM.RolePolicy.make(
|
|
732
|
-
~name=`${name}-adminDcbSend`,
|
|
733
|
-
~args={
|
|
734
|
-
policy: policyJson->Pulumi.Output.asInput,
|
|
735
|
-
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
736
|
-
},
|
|
737
|
-
)
|
|
738
|
-
| None => ()
|
|
739
|
-
}
|
|
740
601
|
}
|
|
741
602
|
|
|
742
603
|
// Plugin EC sqs:SendMessage grants on the aggregate / StateChangeSlice
|