@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.
@@ -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