@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
@@ -53,30 +53,10 @@ let registerNodeType = (~typeName: string, ~readModelName: string): unit =>
53
53
  // -- Provision --------------------------------------------------------------
54
54
  let bool = b => b ? "true" : "false"
55
55
 
56
- // One `node(id: ID!)` resolver on the shared data source → Invoke {kind:"node"}.
57
- let nodeResolverCode =
58
- `import { util } from '@aws-appsync/utils';
59
- export function request(ctx) {
60
- const id = ctx.identity;
61
- return {
62
- operation: 'Invoke',
63
- payload: {
64
- readModelName: '',
65
- kind: 'node',
66
- arguments: ctx.args,
67
- identity: id != null && id.sub != null
68
- ? { userId: id.sub, username: id.username, groups: id.claims?.['cognito:groups'] ?? [], claims: id.claims, provider: 'Cognito' }
69
- : id != null
70
- ? { userArn: id.userArn ?? null, accountId: id.accountId ?? null, username: id.username ?? null, provider: 'IAM' }
71
- : null
72
- }
73
- };
74
- }
75
- export function response(ctx) {
76
- if (ctx.error) util.error(ctx.error.message, ctx.error.type);
77
- return ctx.result;
78
- }
79
- `->Pulumi.Input.make
56
+ // (The shared `node(id: ID!)` resolver was removed with the Relay-node
57
+ // resolution of the merged-api plan: the root field is no longer emitted on
58
+ // AWS. The Pg Lambda's runtime `handleNode` + the nodeTypes env plumbing stay
59
+ // inert pending a future consumer.)
80
60
 
81
61
  // Serialize the shared connection config into the env-config JSON (same shape
82
62
  // EventCollectorRuntime_Builder_Single bakes into HANDLER_CONFIG).
@@ -87,13 +67,6 @@ let provision = (
87
67
  ~api: Pulumi.Output.t<AppSync.GraphQLApi.t>,
88
68
  ~selection: QueryDbBackend.selection,
89
69
  ~opts: Pulumi.ComponentResource.options,
90
- // The shared node(id) resolver is a single resolver on the API's Query.node
91
- // field. In plugin-stack mode each plugin stack deploys independently onto one
92
- // shared API, so only ONE stack may own that field — and a per-plugin node
93
- // resolver only knows its own plugin's types anyway. So node is provisioned in
94
- // monolithic mode only; plugin stacks pass false. (Node is not wired on the
95
- // DynamoDB path either — it stays a monolithic-only capability for now.)
96
- ~createNodeResolver: bool=true,
97
70
  ): unit => {
98
71
  // Join the resolver-binding registry (labelField/includeIdParam) with the
99
72
  // ReadModel runtime registry (specModulePath, pgBacked). Only read models
@@ -257,22 +230,6 @@ let provision = (
257
230
  ~opts=Some(customOpts),
258
231
  )
259
232
 
260
- // Shared node(id) resolver (B3.2c) — one for the whole API, dispatched by
261
- // typeName in the Lambda. Only when some entity registered a node type
262
- // (else the `node` field isn't in the schema) AND in monolithic mode
263
- // (plugin stacks would collide on the single Query.node field).
264
- if createNodeResolver && nodeTypes->Dict.toArray->Array.length > 0 {
265
- let _nodeResolver = AppSync_Resolver_Retrying.makeUnitJsResolver(
266
- ~name=name ++ "NodeResolver",
267
- ~api,
268
- ~dataSourceName=dataSource.name->Pulumi.Output.asInput,
269
- ~type_="Query"->Pulumi.Input.make,
270
- ~field="node"->Pulumi.Input.make,
271
- ~code=nodeResolverCode,
272
- ~opts=customOpts,
273
- )
274
- }
275
-
276
233
  // Fulfil the deferred name the Postgres storage maker handed to resolvers.
277
234
  let _ = dataSource.name->Pulumi.Output.apply(n => resolveDataSourceName.contents(n))
278
235
  log.info(~comp="PgQueryResolver_Builder", `provisioned for ${handlers->Array.length->Int.toString} read model(s)`)
@@ -13,7 +13,6 @@ import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/sr
13
13
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
14
14
  import * as PgConnection$ReventlessAws from "../Postgres/PgConnection.res.mjs";
15
15
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
16
- import * as AppSync_Resolver_Retrying$ReventlessAws from "../Api/AppSync_Resolver_Retrying.res.mjs";
17
16
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "../Runtime/RuntimeEnvironment_Lambda.res.mjs";
18
17
  import * as EventCollectorRuntime_Builder_Single$ReventlessAws from "../Runtime/EventCollectorRuntime_Builder_Single.res.mjs";
19
18
 
@@ -47,35 +46,11 @@ function bool(b) {
47
46
  }
48
47
  }
49
48
 
50
- let nodeResolverCode = `import { util } from '@aws-appsync/utils';
51
- export function request(ctx) {
52
- const id = ctx.identity;
53
- return {
54
- operation: 'Invoke',
55
- payload: {
56
- readModelName: '',
57
- kind: 'node',
58
- arguments: ctx.args,
59
- identity: id != null && id.sub != null
60
- ? { userId: id.sub, username: id.username, groups: id.claims?.['cognito:groups'] ?? [], claims: id.claims, provider: 'Cognito' }
61
- : id != null
62
- ? { userArn: id.userArn ?? null, accountId: id.accountId ?? null, username: id.username ?? null, provider: 'IAM' }
63
- : null
64
- }
65
- };
66
- }
67
- export function response(ctx) {
68
- if (ctx.error) util.error(ctx.error.message, ctx.error.type);
69
- return ctx.result;
70
- }
71
- `;
72
-
73
49
  function pgConnectionJson(cc) {
74
50
  return JSON.stringify(PgConnection$ReventlessAws.connectionConfigToJson(undefined, cc));
75
51
  }
76
52
 
77
- function provision(api, selection, opts, createNodeResolverOpt) {
78
- let createNodeResolver = createNodeResolverOpt !== undefined ? createNodeResolverOpt : true;
53
+ function provision(api, selection, opts) {
79
54
  let handlers = Stdlib_Array.filterMap(Object.values(entries), entry => {
80
55
  let info = EventCollectorRuntime_Builder_Single$ReventlessAws.readModelInfos[entry.readModelName];
81
56
  if (info !== undefined && info.pgBacked) {
@@ -147,9 +122,6 @@ function provision(api, selection, opts, createNodeResolverOpt) {
147
122
  },
148
123
  serviceRoleArn: dataSourceRole.arn
149
124
  }, customOpts);
150
- if (createNodeResolver && Object.entries(nodeTypes).length !== 0) {
151
- AppSync_Resolver_Retrying$ReventlessAws.makeUnitJsResolver(name + "NodeResolver", api, dataSource.name, "Query", "node", nodeResolverCode, customOpts);
152
- }
153
125
  dataSource.name.apply(n => resolveDataSourceName.contents(n));
154
126
  log.info("PgQueryResolver_Builder", undefined, `provisioned for ` + handlers.length.toString() + ` read model(s)`);
155
127
  }
@@ -166,7 +138,6 @@ export {
166
138
  nodeTypes,
167
139
  registerNodeType,
168
140
  bool,
169
- nodeResolverCode,
170
141
  pgConnectionJson,
171
142
  provision,
172
143
  }
@@ -105,17 +105,6 @@ let make: ReventlessCore.QueryDb_Adapter.resolversMaker<api, role> = (
105
105
  | None => true
106
106
  }
107
107
 
108
- // Resolve returnTypeName for Relay Node type registry
109
- let returnTypeName = switch registryEntry {
110
- | Some({returnTypeName: rt}) => rt
111
- | None => name
112
- }
113
-
114
- // Register entity type in the Relay Node type registry for node(id: ID!) resolution
115
- if includeIdParam {
116
- NodeResolver_AppSync.registerNodeType(~typeName=returnTypeName, ~dataSourceName)
117
- }
118
-
119
108
  // Creates either a unit resolver (no interceptor) or a pipeline resolver
120
109
  // (interceptor Lambda → DynamoDB query) depending on queryInterceptorConfig.
121
110
  let makeQueryResolver = (~resolverName, ~field, ~code) =>
@@ -11,7 +11,6 @@ import * as Util_DynamoDb$ReventlessAws from "../../util/Util_DynamoDb.res.mjs";
11
11
  import * as Util_QueryDb$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_QueryDb.res.mjs";
12
12
  import * as AppSync_DataSource$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_DataSource.res.mjs";
13
13
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
14
- import * as NodeResolver_AppSync$ReventlessAws from "./NodeResolver_AppSync.res.mjs";
15
14
  import * as AppSync_Resolver_Functions$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/AppSync/AppSync_Resolver_Functions.res.mjs";
16
15
  import * as AppSync_Resolver_Retrying$ReventlessAws from "../Api/AppSync_Resolver_Retrying.res.mjs";
17
16
  import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
@@ -70,10 +69,6 @@ function make(name, api, apiRole, dataSourceName, indexes, subIdField, idResolve
70
69
  let fieldNameForSingle = registryEntry !== undefined ? registryEntry.singleFieldName : AppSync_Resolver_Functions$PulumiAws.uncapitalize(name$1);
71
70
  let includeIdParam = registryEntry !== undefined ? registryEntry.includeIdParam : true;
72
71
  let connectionSpec = registryEntry !== undefined ? registryEntry.connectionSpec : true;
73
- let returnTypeName = registryEntry !== undefined ? registryEntry.returnTypeName : name$1;
74
- if (includeIdParam) {
75
- NodeResolver_AppSync$ReventlessAws.registerNodeType(returnTypeName, dataSourceName);
76
- }
77
72
  let makeQueryResolver = (resolverName, field, code) => {
78
73
  let match = queryInterceptorConfig.contents;
79
74
  if (match === undefined) {
@@ -15,7 +15,7 @@ let sideEffectInfos: dict<sideEffectInfo> = Dict.make()
15
15
  let registerSideEffectHandler = (~sideEffectHandlerName, ~sideEffectModulePaths) =>
16
16
  sideEffectInfos->Dict.set(sideEffectHandlerName, {sideEffectModulePaths: sideEffectModulePaths})
17
17
 
18
- // Extra Lambda env vars contributed by bespoke side effects (e.g. admin ApiSchemaPush).
18
+ // Extra Lambda env vars contributed by bespoke side effects.
19
19
  // The shared "AllSideEffectHandlers" Lambda is built once in finish(); all registered
20
20
  // entries are merged onto its env there. Deploy-derived config only — never overrides
21
21
  // HANDLER_CONFIG.
@@ -176,12 +176,11 @@ let finish = () =>
176
176
  ~opts,
177
177
  )
178
178
 
179
- // The admin ApiSchemaPush side effect (the only extra-env contributor) pushes the
180
- // stitched schema to AppSync via StartSchemaCreation and reads the live schema for
181
- // the shrink guard. Grant those AppSync perms to the shared side-effect-handler
182
- // Lambda role ONLY when a bespoke side effect registered config — Task-only
183
- // deployments keep the narrow default perimeter. Mirrors the admin EventCollector's
184
- // AllowAdminStartSchemaCreation grant (PluginRuntime_Builder).
179
+ // Grant AppSync schema perms to the shared side-effect-handler Lambda
180
+ // role ONLY when a bespoke side effect registered extra-env config
181
+ // Task-only deployments keep the narrow default perimeter. (The last
182
+ // such contributor, the admin ApiSchemaPush, was retired with the
183
+ // merged-API cutover; the mechanism stays for future bespoke effects.)
185
184
  if extraEnvVarsAll->Dict.keysToArray->Array.length > 0 {
186
185
  let _ = PulumiAws.IAM.RolePolicy.make(
187
186
  ~name="AllSideEffectHandlers-appsyncSchemaPush",
@@ -94,58 +94,6 @@ let rec waitForSchemaActive = async (client, apiId, ~maxAttempts=30, ~attempt=0)
94
94
  }
95
95
  }
96
96
 
97
- // GetIntrospectionSchema — fetch the live schema as an SDL string. Used by the
98
- // deploy-time drift check in preResolversSchemaHook to detect a live schema that
99
- // was clobbered out-of-band by a runtime re-stitch (the stored deploy hash does
100
- // not reflect such clobbers). Returns "" when the API has no schema or when
101
- // introspection fails — the caller decides how to treat an empty result.
102
- type getIntrospectionSchemaInput = {apiId: string, format: string}
103
- type getIntrospectionSchemaCommand
104
- type schemaBlob
105
- type getIntrospectionSchemaResult = {schema: option<schemaBlob>}
106
-
107
- @module("@aws-sdk/client-appsync") @new
108
- external makeGetIntrospectionSchemaCommand: getIntrospectionSchemaInput => getIntrospectionSchemaCommand =
109
- "GetIntrospectionSchemaCommand"
110
-
111
- @send
112
- external sendGetIntrospection: (
113
- appSyncClient,
114
- getIntrospectionSchemaCommand,
115
- ) => promise<getIntrospectionSchemaResult> = "send"
116
-
117
- // resp.schema is a Uint8Array of the SDL text; decode it to UTF-8.
118
- type nodeBuffer
119
- @val @scope("Buffer") external bufferFrom: schemaBlob => nodeBuffer = "from"
120
- @send external bufferToString: (nodeBuffer, string) => string = "toString"
121
-
122
- let getIntrospectionSdl = async (client: appSyncClient, apiId: string): string => {
123
- try {
124
- let resp = await client->sendGetIntrospection(
125
- {apiId, format: "SDL"}->makeGetIntrospectionSchemaCommand,
126
- )
127
- switch resp.schema {
128
- | Some(blob) => bufferFrom(blob)->bufferToString("utf-8")
129
- | None => ""
130
- }
131
- } catch {
132
- | exn =>
133
- let msg = exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown")
134
- log.warn(~comp="AppSync_Adapter", `getIntrospectionSdl failed for ${apiId}: ${msg}`)
135
- ""
136
- }
137
- }
138
-
139
- let deploySchemaWithRetry = (
140
- client: appSyncClient,
141
- apiId: string,
142
- definition: string,
143
- ): Effect.t<unit, AppSync_Error.t, unit> =>
144
- Effect.tryPromise(
145
- ~catch=AppSync_Error.classify,
146
- () => startSchemaCreation(client, {apiId, definition})->Promise.then(_ => Promise.resolve()),
147
- )->Effect.retry(AppSync_Error.retrySchedule)
148
-
149
97
  // Lazy singleton AppSync client (runtime only)
150
98
  let _client: ref<option<appSyncClient>> = ref(None)
151
99
  let getClient = () =>
@@ -157,6 +105,77 @@ let getClient = () =>
157
105
  c
158
106
  }
159
107
 
108
+ // ── GetSourceApiAssociation — merged-API association status poll ──────────
109
+ // Merged-API deploys must fail loudly on MERGE_FAILED (plan
110
+ // merged-api-push-free-composition, Phase 0 finding: a failed merge silently
111
+ // keeps the last-good merged schema serving). After creating a
112
+ // SourceApiAssociation, poll until the initial merge lands.
113
+ type getSourceApiAssociationInput = {
114
+ associationId: string,
115
+ mergedApiIdentifier: string,
116
+ }
117
+ type getSourceApiAssociationCommand
118
+ type sourceApiAssociationSummary = {
119
+ sourceApiAssociationStatus: option<string>,
120
+ sourceApiAssociationStatusDetail: option<string>,
121
+ }
122
+ type getSourceApiAssociationResult = {sourceApiAssociation: option<sourceApiAssociationSummary>}
123
+
124
+ @module("@aws-sdk/client-appsync") @new
125
+ external makeGetSourceApiAssociationCommand: getSourceApiAssociationInput => getSourceApiAssociationCommand =
126
+ "GetSourceApiAssociationCommand"
127
+
128
+ @send
129
+ external sendGetSourceApiAssociation: (
130
+ appSyncClient,
131
+ getSourceApiAssociationCommand,
132
+ ) => promise<getSourceApiAssociationResult> = "send"
133
+
134
+ // Poll until the association reports MERGE_SUCCESS; throw with the AWS status
135
+ // detail on MERGE_FAILED / AUTO_MERGE_SCHEDULE_FAILED. Auto-merge lands in
136
+ // ~12 s (spike-measured), so 60 × 2 s bounds the wait at two minutes.
137
+ let rec waitForMergeSuccess = async (
138
+ client: appSyncClient,
139
+ ~associationId: string,
140
+ ~mergedApiIdentifier: string,
141
+ ~maxAttempts=60,
142
+ ~attempt=0,
143
+ ~delayMs=2000,
144
+ ) => {
145
+ let result = await client->sendGetSourceApiAssociation(
146
+ {associationId, mergedApiIdentifier}->makeGetSourceApiAssociationCommand,
147
+ )
148
+ let status =
149
+ result.sourceApiAssociation
150
+ ->Option.flatMap(a => a.sourceApiAssociationStatus)
151
+ ->Option.getOr("(no status)")
152
+ let detail =
153
+ result.sourceApiAssociation
154
+ ->Option.flatMap(a => a.sourceApiAssociationStatusDetail)
155
+ ->Option.getOr("(no details)")
156
+ switch status {
157
+ | "MERGE_SUCCESS" => ()
158
+ | "MERGE_FAILED" | "AUTO_MERGE_SCHEDULE_FAILED" =>
159
+ JsError.throwWithMessage(
160
+ `Source API association ${associationId} on ${mergedApiIdentifier} failed to merge (${status}): ${detail}`,
161
+ )
162
+ | _ if attempt >= maxAttempts =>
163
+ JsError.throwWithMessage(
164
+ `Source API association ${associationId} merge timed out after ${maxAttempts->Int.toString} attempts (status: ${status})`,
165
+ )
166
+ | _ =>
167
+ await Promise.make((resolve, _) => setTimeout(resolve, delayMs)->ignore)
168
+ await waitForMergeSuccess(
169
+ client,
170
+ ~associationId,
171
+ ~mergedApiIdentifier,
172
+ ~maxAttempts,
173
+ ~attempt=attempt + 1,
174
+ ~delayMs,
175
+ )
176
+ }
177
+ }
178
+
160
179
  // ── @aws_auth directive injection ─────────────────────────────────────────
161
180
  // Injects @aws_auth(cognito_groups: [...]) directives into SDL field strings
162
181
  // based on authorization metadata from schema entries.
@@ -413,22 +432,25 @@ let injectAwsAuthAll = (
413
432
  AppSync_SdlDecorate.injectAwsAuthAll(fragment, ~group, ~iamFieldNames)
414
433
 
415
434
  /**
416
- Stitch base + plugin fragments and decorate the assembled SDL with the AppSync
417
- dialect: `@aws_subscribe` on mutation-sourced subscription fields (from the
418
- fragments' neutral `subscriptionSources` metadata core no longer emits the
419
- directive) and `@aws_cognito_user_pools @aws_iam` on the shared traversal
420
- types. Every AWS schema push assembles its SDL through here so the dialect is
421
- applied uniformly.
435
+ One source-API document rendered standalone (relay base types included; the
436
+ global `node` query is not emitted on AWS see the merged-api plan's "Relay
437
+ node resolution" section), decorated with the AppSync dialect:
438
+ `@aws_subscribe` on mutation-sourced subscription fields (from the fragment's
439
+ neutral `subscriptionSources` metadata) and `@aws_cognito_user_pools @aws_iam`
440
+ on the shared traversal types. Every AWS source-API document — the platform's
441
+ canonical documents and each plugin's subgraph — assembles through here so the
442
+ dialect is applied uniformly. No `@canonical` stamps here — the platform adds
443
+ them to its canonical documents only; plugin subgraphs stay unstamped and the
444
+ canonical definitions win on merge.
422
445
  */
423
- let stitchWithAwsDirectives = (
424
- ~baseFragment: Reventless.Plugin.apiSchemaFragment,
425
- ~pluginFragments: array<Reventless.Plugin.apiSchemaFragment>,
446
+ let stitchStandaloneWithAwsDirectives = (
447
+ ~fragment: Reventless.Plugin.apiSchemaFragment,
426
448
  ): string => {
427
449
  let sources = ReventlessCore.GraphQL_Stitcher.collectSubscriptionSources(
428
- ~baseFragment,
429
- ~pluginFragments,
450
+ ~baseFragment=fragment,
451
+ ~pluginFragments=[],
430
452
  )
431
- ReventlessCore.GraphQL_Stitcher.stitch(~baseFragment, ~pluginFragments)
453
+ ReventlessCore.GraphQL_Stitcher.stitchStandalone(~fragment)
432
454
  ->AppSync_SdlDecorate.injectAwsSubscribe(~sources)
433
455
  ->stampSharedIamTypes
434
456
  }
@@ -438,8 +460,16 @@ let stitchWithAwsDirectives = (
438
460
  type api = AppSync.GraphQLApi.t
439
461
  type role = IAM.Role.t
440
462
 
441
- let makeApiResource = (
463
+ // Primary authentication mode every platform-created AppSync API uses. A
464
+ // merged API and its source APIs must share this primary mode — exported via
465
+ // StackReference (`mergedApiPrimaryAuth`) and asserted where associations are
466
+ // created (AppSync_MergedApi.assertCompatiblePrimaryAuth).
467
+ let primaryAuthenticationType = AppSync.GraphQLApi.AMAZON_COGNITO_USER_POOLS
468
+
469
+ let _makeApiResourceWith = (
442
470
  ~name: string,
471
+ ~schema: option<string>,
472
+ ~userPoolConfig: option<Pulumi.Output.t<AppSync.GraphQLApi.userPoolConfig>>,
443
473
  ~opts: Pulumi.ComponentResource.options,
444
474
  ): (Pulumi.Output.t<api>, Pulumi.Output.t<role>) => {
445
475
  let customOpts: Pulumi.CustomResourceOptions.t = {
@@ -456,13 +486,16 @@ let makeApiResource = (
456
486
  ~opts=Some(customOpts),
457
487
  )
458
488
 
459
- // Resolve the Cognito UserPool — cached inside Platform_Stack, so calling
460
- // from each API call site (DomainApi, PlatformApi) is safe. Returned as a
461
- // single Output that yields the {userPoolId, awsRegion, defaultAction}
462
- // record AppSync expects.
463
- let authConfigOut = Auth_Cognito.make(~name=`${name}-auth`)
464
- let userPoolConfigOut =
465
- authConfigOut->Pulumi.Output.apply((c: Auth_Cognito.authConfig) =>
489
+ // Resolve the Cognito UserPool — either supplied by the caller (plugin-stack
490
+ // source APIs read it from the platform's StackReference exports so they
491
+ // never provision pool/client resources of their own) or resolved via
492
+ // Auth_Cognito (cached inside Platform_Stack, so calling from each API call
493
+ // site DomainApi, PlatformApi — is safe). A single Output yielding the
494
+ // {userPoolId, awsRegion, defaultAction} record AppSync expects.
495
+ let userPoolConfigOut = switch userPoolConfig {
496
+ | Some(config) => config
497
+ | None =>
498
+ Auth_Cognito.make(~name=`${name}-auth`)->Pulumi.Output.apply((c: Auth_Cognito.authConfig) =>
466
499
  (
467
500
  {
468
501
  userPoolId: c.userPoolId,
@@ -471,13 +504,13 @@ let makeApiResource = (
471
504
  }: AppSync.GraphQLApi.userPoolConfig
472
505
  )
473
506
  )
507
+ }
474
508
 
475
509
  // Cognito as primary auth, AWS_IAM as additional provider for
476
510
  // server-to-server lambdas (heartbeat, Plugin_Connected emission) signed via
477
511
  // the existing IAM role.
478
512
  let apiArgs: AppSync.GraphQLApi.args = {
479
- authenticationType: AppSync.GraphQLApi
480
- .AMAZON_COGNITO_USER_POOLS->Pulumi.Input.make,
513
+ authenticationType: primaryAuthenticationType->Pulumi.Input.make,
481
514
  userPoolConfig: userPoolConfigOut->Pulumi.Output.asInput,
482
515
  additionalAuthenticationProviders: [
483
516
  (
@@ -486,12 +519,45 @@ let makeApiResource = (
486
519
  }: AppSync.GraphQLApi.additionalAuthenticationProvider
487
520
  )->Pulumi.Input.make,
488
521
  ]->Pulumi.Input.make,
522
+ schema: ?(schema->Option.map(Pulumi.Input.make)),
489
523
  }
490
524
  let graphQLApi = AppSync.GraphQLApi.make(~name, ~args=apiArgs, ~opts=Some(customOpts))
491
525
 
492
526
  (graphQLApi->Pulumi.Output.make, iamRole->Pulumi.Output.make)
493
527
  }
494
528
 
529
+ let makeApiResource = (
530
+ ~name: string,
531
+ ~opts: Pulumi.ComponentResource.options,
532
+ ): (Pulumi.Output.t<api>, Pulumi.Output.t<role>) =>
533
+ _makeApiResourceWith(~name, ~schema=None, ~userPoolConfig=None, ~opts)
534
+
535
+ // Merged-mode source API: same auth shape as makeApiResource but with a
536
+ // DECLARATIVE inline schema — the provider runs StartSchemaCreation + poll
537
+ // before the resource resolves, so resolvers chained on the API are ordered
538
+ // after the schema is ACTIVE without the push-path hook machinery. Not part
539
+ // of the Api_Adapter.Provider interface (Platform.res calls it directly on
540
+ // the merge path).
541
+ let makeSourceApiResource = (
542
+ ~name: string,
543
+ ~schema: string,
544
+ ~opts: Pulumi.ComponentResource.options,
545
+ ): (Pulumi.Output.t<api>, Pulumi.Output.t<role>) =>
546
+ _makeApiResourceWith(~name, ~schema=Some(schema), ~userPoolConfig=None, ~opts)
547
+
548
+ // Merged-mode PLUGIN source API: schema-less at creation (the plugin's
549
+ // standalone subgraph document is only computable during P.make(), so
550
+ // preResolversSchemaHook pushes it — the plugin's own API is a single writer
551
+ // by construction). The user pool comes from the platform's StackReference
552
+ // exports so the merged endpoint's Cognito primary auth matches across every
553
+ // source API without the plugin stack provisioning pool/client resources.
554
+ let makePluginSourceApiResource = (
555
+ ~name: string,
556
+ ~userPoolConfig: Pulumi.Output.t<AppSync.GraphQLApi.userPoolConfig>,
557
+ ~opts: Pulumi.ComponentResource.options,
558
+ ): (Pulumi.Output.t<api>, Pulumi.Output.t<role>) =>
559
+ _makeApiResourceWith(~name, ~schema=None, ~userPoolConfig=Some(userPoolConfig), ~opts)
560
+
495
561
  let generateFragment = (
496
562
  ~mutationEntries: array<ReventlessInfra.Api.mutationSchemaEntry>,
497
563
  ~queryEntries: array<ReventlessInfra.Api.querySchemaEntry>,
@@ -500,30 +566,6 @@ let generateFragment = (
500
566
  injectAwsAuth(fragment, ~mutationEntries, ~queryEntries)
501
567
  }
502
568
 
503
- let updateSchema = (
504
- ~api: Pulumi.Output.t<api>,
505
- ~baseFragment: Reventless.Plugin.apiSchemaFragment,
506
- ~pluginFragments: array<Reventless.Plugin.apiSchemaFragment>,
507
- ): promise<unit> => {
508
- // Inject @aws_auth(cognito_groups: ["Admin"]) into all base fragment fields.
509
- // The base fragment contains core Plugin aggregate queries/mutations — all Admin-only.
510
- // Plugin fragments already have @aws_auth injected via generateFragment.
511
- let augmentedBaseFragment = injectAwsAuthAll(baseFragment, ~group="Admin")
512
- // Shared traversal types + @aws_subscribe are stamped once on the assembled
513
- // SDL (post-stitch, post-dedupe) — see stitchWithAwsDirectives.
514
- let sdl = stitchWithAwsDirectives(~baseFragment=augmentedBaseFragment, ~pluginFragments)
515
- // Resolve the API ID from the Output chain. In mock mode (tests) and in Lambda runtime
516
- // (where the Output is backed by already-known values), this completes synchronously.
517
- // The resulting promise wraps the AppSync SDK call.
518
- let resultPromise: ref<promise<unit>> = ref(Promise.resolve())
519
- let _ =
520
- api
521
- ->Pulumi.Output.apply(graphQLApi =>
522
- graphQLApi.id->Pulumi.Output.apply(apiId => {
523
- let effect = deploySchemaWithRetry(getClient(), apiId, sdl)
524
- let p = effect->Effect.runPromise
525
- resultPromise.contents = p
526
- })
527
- )
528
- resultPromise.contents
529
- }
569
+ // (updateSchema the whole-replace stitched-schema push — was retired with
570
+ // the merged-API cutover; every source API owns its schema declaratively or
571
+ // via its own single-writer subgraph push in preResolversSchemaHook.)