@reventlessdev/reventless-aws 3.0.0-alpha.209 → 3.0.0-alpha.211
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/package.json +8 -8
- package/src/Platform.res +348 -707
- package/src/Platform.res.mjs +271 -739
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res +4 -47
- package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +1 -30
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +0 -11
- package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +0 -5
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +6 -7
- package/src/components/Api/AppSync_Adapter.res +143 -101
- package/src/components/Api/AppSync_Adapter.res.mjs +60 -57
- package/src/components/Api/AppSync_MergedApi.res +218 -0
- package/src/components/Api/AppSync_MergedApi.res.mjs +122 -0
- package/src/components/Api/AppSync_SdlDecorate.res +49 -61
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +38 -29
- package/src/components/Plugin.res.mjs +1 -2
- package/tests/AppSync_AdapterTest.res +184 -0
- package/tests/AppSync_AdapterTest.res.mjs +136 -0
- package/tests/AppSync_SdlDecorateTest.res +33 -86
- package/tests/AppSync_SdlDecorateTest.res.mjs +26 -71
- package/tests/MCP_LambdaTest.res +4 -6
- package/tests/MCP_LambdaTest.res.mjs +2 -2
- package/src/adapter/Api/ApiFragmentDeregistration.res +0 -138
- package/src/adapter/Api/ApiFragmentDeregistration.res.mjs +0 -108
- package/src/adapter/Api/ApiSchemaPush.res +0 -79
- package/src/adapter/Api/ApiSchemaPush.res.mjs +0 -64
- package/src/adapter/Api/Platform_ApiFragments_Lambda.res +0 -222
- package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +0 -202
- package/src/adapter/QueryDb/NodeResolver_AppSync.res +0 -71
- package/src/adapter/QueryDb/NodeResolver_AppSync.res.mjs +0 -44
- package/src/adapter/Runtime/ApiSchemaPush_Runtime.mjs +0 -204
- package/tests/ApiSchemaPushTest.res +0 -32
- package/tests/ApiSchemaPushTest.res.mjs +0 -20
|
@@ -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
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
|
424
|
-
~
|
|
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.
|
|
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
|
-
|
|
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 —
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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:
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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.)
|