@reventlessdev/reventless-aws 3.0.0-alpha.210 → 3.0.0-alpha.212

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 (39) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +6 -6
  3. package/src/Platform.res +116 -849
  4. package/src/Platform.res.mjs +75 -710
  5. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +16 -3
  6. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +11 -1
  7. package/src/adapter/QueryDb/PgQueryResolver_Builder.res +4 -47
  8. package/src/adapter/QueryDb/PgQueryResolver_Builder.res.mjs +1 -30
  9. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +0 -11
  10. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +0 -5
  11. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +6 -7
  12. package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +4 -1
  13. package/src/components/Api/AppSync_Adapter.res +32 -105
  14. package/src/components/Api/AppSync_Adapter.res.mjs +12 -53
  15. package/src/components/Api/AppSync_MergedApi.res +1 -1
  16. package/src/components/Api/AppSync_SdlDecorate.res +6 -65
  17. package/src/components/Api/AppSync_SdlDecorate.res.mjs +1 -31
  18. package/src/components/Plugin.res.mjs +1 -2
  19. package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res +40 -35
  20. package/src/plugin/heartbeat/HeartbeatRunner_CloudWatchEvents.res.mjs +14 -15
  21. package/src/plugin/runtime/PluginRuntime_Builder.res +58 -69
  22. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +31 -29
  23. package/tests/AppSync_AdapterTest.res +23 -15
  24. package/tests/AppSync_AdapterTest.res.mjs +18 -16
  25. package/tests/AppSync_SdlDecorateTest.res +9 -89
  26. package/tests/AppSync_SdlDecorateTest.res.mjs +7 -72
  27. package/tests/MCP_LambdaTest.res +4 -6
  28. package/tests/MCP_LambdaTest.res.mjs +2 -2
  29. package/src/adapter/Api/ApiFragmentDeregistration.res +0 -138
  30. package/src/adapter/Api/ApiFragmentDeregistration.res.mjs +0 -108
  31. package/src/adapter/Api/ApiSchemaPush.res +0 -79
  32. package/src/adapter/Api/ApiSchemaPush.res.mjs +0 -64
  33. package/src/adapter/Api/Platform_ApiFragments_Lambda.res +0 -222
  34. package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +0 -202
  35. package/src/adapter/QueryDb/NodeResolver_AppSync.res +0 -71
  36. package/src/adapter/QueryDb/NodeResolver_AppSync.res.mjs +0 -44
  37. package/src/adapter/Runtime/ApiSchemaPush_Runtime.mjs +0 -204
  38. package/tests/ApiSchemaPushTest.res +0 -32
  39. package/tests/ApiSchemaPushTest.res.mjs +0 -20
@@ -5,8 +5,11 @@
5
5
  // field. The persisted structure is sury-encoded with the exact same shape
6
6
  // the in-memory adapter's `Platform_ComponentDefinitionsApi.encodePluginStructureEntry`
7
7
  // produces (literal-string `commandLevel`, `null`-encoded options), so the
8
- // handler simply wraps each persisted structure with `pluginId` no decoding
9
- // or re-encoding needed.
8
+ // handler wraps each persisted structure with `pluginId` without decoding.
9
+ // One caveat: the persisted structure is PRE-filter — it still carries Internal
10
+ // ReadModels / StateViewSlices for developer tooling. The handler re-applies the
11
+ // `isPublicQueryable` filter (mirroring `encodePluginStructureEntry`) so Internal
12
+ // components stay out of the deployed AutoUI, matching the in-memory adapter.
10
13
 
11
14
  open PulumiAws
12
15
 
@@ -64,6 +67,16 @@ export async function handler() {
64
67
  }
65
68
  return 0;
66
69
  };
70
+ // The persisted structure carries Internal ReadModels / StateViewSlices for
71
+ // developer tooling; they must stay out of the deployed AutoUI. Mirror
72
+ // ReventlessCore.Platform_ComponentDefinitionsApi.isPublicQueryable here on the
73
+ // read path (the persisted structure is pre-filter — it is NOT re-encoded).
74
+ const isPublicQueryable = (q) => q?.visibility !== "Internal";
75
+ const filterStructure = (s) => ({
76
+ ...s,
77
+ readModels: (s.readModels ?? []).filter(isPublicQueryable),
78
+ stateViewSlices: (s.stateViewSlices ?? []).filter(isPublicQueryable),
79
+ });
67
80
  const latestByName = new Map();
68
81
  for (const item of items) {
69
82
  if (!item || !item.structure) continue;
@@ -71,7 +84,7 @@ export async function handler() {
71
84
  const version = String(item.name).split("@")[1] ?? "";
72
85
  const prev = latestByName.get(name);
73
86
  if (!prev || cmpVer(version, prev.version) > 0) {
74
- latestByName.set(name, { version, entry: { pluginId: name, ...item.structure } });
87
+ latestByName.set(name, { version, entry: { pluginId: name, ...filterStructure(item.structure) } });
75
88
  }
76
89
  }
77
90
  const userEntries = [...latestByName.values()].map(v => v.entry);
@@ -65,6 +65,16 @@ export async function handler() {
65
65
  }
66
66
  return 0;
67
67
  };
68
+ // The persisted structure carries Internal ReadModels / StateViewSlices for
69
+ // developer tooling; they must stay out of the deployed AutoUI. Mirror
70
+ // ReventlessCore.Platform_ComponentDefinitionsApi.isPublicQueryable here on the
71
+ // read path (the persisted structure is pre-filter — it is NOT re-encoded).
72
+ const isPublicQueryable = (q) => q?.visibility !== "Internal";
73
+ const filterStructure = (s) => ({
74
+ ...s,
75
+ readModels: (s.readModels ?? []).filter(isPublicQueryable),
76
+ stateViewSlices: (s.stateViewSlices ?? []).filter(isPublicQueryable),
77
+ });
68
78
  const latestByName = new Map();
69
79
  for (const item of items) {
70
80
  if (!item || !item.structure) continue;
@@ -72,7 +82,7 @@ export async function handler() {
72
82
  const version = String(item.name).split("@")[1] ?? "";
73
83
  const prev = latestByName.get(name);
74
84
  if (!prev || cmpVer(version, prev.version) > 0) {
75
- latestByName.set(name, { version, entry: { pluginId: name, ...item.structure } });
85
+ latestByName.set(name, { version, entry: { pluginId: name, ...filterStructure(item.structure) } });
76
86
  }
77
87
  }
78
88
  const userEntries = [...latestByName.values()].map(v => v.entry);
@@ -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",
@@ -91,7 +91,10 @@ export function buildJsonEventsHandler(specModule, projectionModule, queryDbTabl
91
91
  // every such action hit the `MissingSubIdConfig` guard and silently write nothing,
92
92
  // while the test-harness callback path (which threads it) stayed green. `undefined`
93
93
  // for slices without an `@subId` is correct — those never emit sub-id actions.
94
- Effect.promise(() => handleAction(action, queryDbOps, specModule.subIdConfig)),
94
+ // First positional arg is the compiled `~comp=` optional (attribution
95
+ // for Projection.handleAction's debug-lazy action log) — the slice's
96
+ // spec name. Compiled shape: (comp, action, operations, subIdConfig).
97
+ Effect.promise(() => handleAction(specModule.name, action, queryDbOps, specModule.subIdConfig)),
95
98
  _ => {}
96
99
  )
97
100
  );
@@ -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 = () =>
@@ -484,32 +432,16 @@ let injectAwsAuthAll = (
484
432
  AppSync_SdlDecorate.injectAwsAuthAll(fragment, ~group, ~iamFieldNames)
485
433
 
486
434
  /**
487
- Stitch base + plugin fragments and decorate the assembled SDL with the AppSync
488
- dialect: `@aws_subscribe` on mutation-sourced subscription fields (from the
489
- fragments' neutral `subscriptionSources` metadata core no longer emits the
490
- directive) and `@aws_cognito_user_pools @aws_iam` on the shared traversal
491
- types. Every AWS schema push assembles its SDL through here so the dialect is
492
- applied uniformly.
493
- */
494
- let stitchWithAwsDirectives = (
495
- ~baseFragment: Reventless.Plugin.apiSchemaFragment,
496
- ~pluginFragments: array<Reventless.Plugin.apiSchemaFragment>,
497
- ): string => {
498
- let sources = ReventlessCore.GraphQL_Stitcher.collectSubscriptionSources(
499
- ~baseFragment,
500
- ~pluginFragments,
501
- )
502
- ReventlessCore.GraphQL_Stitcher.stitch(~baseFragment, ~pluginFragments)
503
- ->AppSync_SdlDecorate.injectAwsSubscribe(~sources)
504
- ->stampSharedIamTypes
505
- }
506
-
507
- /**
508
- Merged-mode plugin subgraph document: one fragment rendered standalone
509
- (relay base types included, global `node` omitted — only the platform's
510
- canonical base document carries `node`), with the same AppSync dialect as
511
- `stitchWithAwsDirectives`. No `@canonical` stamps — plugin subgraphs stay
512
- unstamped; the admin source's canonical definitions win on merge.
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.
513
445
  */
514
446
  let stitchStandaloneWithAwsDirectives = (
515
447
  ~fragment: Reventless.Plugin.apiSchemaFragment,
@@ -554,6 +486,24 @@ let _makeApiResourceWith = (
554
486
  ~opts=Some(customOpts),
555
487
  )
556
488
 
489
+ // Let AppSync push field-resolver errors to CloudWatch. The API's own service
490
+ // role is already assumable by appsync.amazonaws.com, so attach the AWS-managed
491
+ // push policy to it and reuse it as the logging role. `fieldLogLevel = ERROR`
492
+ // (below) then captures resolver failures AppSync otherwise swallows into the
493
+ // client's `errors[]` — e.g. a non-null coercion on a stale read-model row.
494
+ let _appsyncCwLogsAttachment = IAM.RolePolicyAttachment.make(
495
+ ~name=`${name}-appsync-cwlogs`,
496
+ ~args={
497
+ role: iamRole.name->Pulumi.Output.asInput,
498
+ policyArn: "arn:aws:iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs"->Pulumi.Input.make,
499
+ },
500
+ ~opts=Some(customOpts),
501
+ )
502
+ let appsyncLogConfig: AppSync.GraphQLApi.logConfig = {
503
+ cloudwatchLogsRoleArn: iamRole.arn->Pulumi.Output.asInput,
504
+ fieldLogLevel: AppSync.GraphQLApi.ERROR->Pulumi.Input.make,
505
+ }
506
+
557
507
  // Resolve the Cognito UserPool — either supplied by the caller (plugin-stack
558
508
  // source APIs read it from the platform's StackReference exports so they
559
509
  // never provision pool/client resources of their own) or resolved via
@@ -588,6 +538,7 @@ let _makeApiResourceWith = (
588
538
  )->Pulumi.Input.make,
589
539
  ]->Pulumi.Input.make,
590
540
  schema: ?(schema->Option.map(Pulumi.Input.make)),
541
+ logConfig: appsyncLogConfig->Pulumi.Input.make,
591
542
  }
592
543
  let graphQLApi = AppSync.GraphQLApi.make(~name, ~args=apiArgs, ~opts=Some(customOpts))
593
544
 
@@ -634,30 +585,6 @@ let generateFragment = (
634
585
  injectAwsAuth(fragment, ~mutationEntries, ~queryEntries)
635
586
  }
636
587
 
637
- let updateSchema = (
638
- ~api: Pulumi.Output.t<api>,
639
- ~baseFragment: Reventless.Plugin.apiSchemaFragment,
640
- ~pluginFragments: array<Reventless.Plugin.apiSchemaFragment>,
641
- ): promise<unit> => {
642
- // Inject @aws_auth(cognito_groups: ["Admin"]) into all base fragment fields.
643
- // The base fragment contains core Plugin aggregate queries/mutations — all Admin-only.
644
- // Plugin fragments already have @aws_auth injected via generateFragment.
645
- let augmentedBaseFragment = injectAwsAuthAll(baseFragment, ~group="Admin")
646
- // Shared traversal types + @aws_subscribe are stamped once on the assembled
647
- // SDL (post-stitch, post-dedupe) — see stitchWithAwsDirectives.
648
- let sdl = stitchWithAwsDirectives(~baseFragment=augmentedBaseFragment, ~pluginFragments)
649
- // Resolve the API ID from the Output chain. In mock mode (tests) and in Lambda runtime
650
- // (where the Output is backed by already-known values), this completes synchronously.
651
- // The resulting promise wraps the AppSync SDK call.
652
- let resultPromise: ref<promise<unit>> = ref(Promise.resolve())
653
- let _ =
654
- api
655
- ->Pulumi.Output.apply(graphQLApi =>
656
- graphQLApi.id->Pulumi.Output.apply(apiId => {
657
- let effect = deploySchemaWithRetry(getClient(), apiId, sdl)
658
- let p = effect->Effect.runPromise
659
- resultPromise.contents = p
660
- })
661
- )
662
- resultPromise.contents
663
- }
588
+ // (updateSchema the whole-replace stitched-schema push — was retired with
589
+ // the merged-API cutover; every source API owns its schema declaratively or
590
+ // via its own single-writer subgraph push in preResolversSchemaHook.)
@@ -4,14 +4,12 @@ import * as Effect from "@reventlessdev/rescript-effect/src/Effect.res.mjs";
4
4
  import * as Aws from "@pulumi/aws";
5
5
  import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
6
6
  import * as Nodecrypto from "node:crypto";
7
- import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
8
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
8
  import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
10
9
  import * as Effect$1 from "effect/Effect";
11
10
  import * as Pulumi from "@pulumi/pulumi";
12
11
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
13
12
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
14
- import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
15
13
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
16
14
  import * as ClientAppsync from "@aws-sdk/client-appsync";
17
15
  import * as Auth_Cognito$ReventlessAws from "../../adapter/Auth/Auth_Cognito.res.mjs";
@@ -60,33 +58,6 @@ async function waitForSchemaActive(client, apiId, maxAttemptsOpt, attemptOpt) {
60
58
  }
61
59
  }
62
60
 
63
- async function getIntrospectionSdl(client, apiId) {
64
- try {
65
- let resp = await client.send(new ClientAppsync.GetIntrospectionSchemaCommand({
66
- apiId: apiId,
67
- format: "SDL"
68
- }));
69
- let blob = resp.schema;
70
- if (blob !== undefined) {
71
- return Buffer.from(Primitive_option.valFromOption(blob)).toString("utf-8");
72
- } else {
73
- return "";
74
- }
75
- } catch (raw_exn) {
76
- let exn = Primitive_exceptions.internalToException(raw_exn);
77
- let msg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
78
- log.warn("AppSync_Adapter", undefined, `getIntrospectionSdl failed for ` + apiId + `: ` + msg);
79
- return "";
80
- }
81
- }
82
-
83
- function deploySchemaWithRetry(client, apiId, definition) {
84
- return Effect$1.retry(Effect.tryPromise(AppSync_Error$ReventlessAws.classify, () => client.send(new ClientAppsync.StartSchemaCreationCommand({
85
- apiId: apiId,
86
- definition: definition
87
- })).then(param => Promise.resolve())), AppSync_Error$ReventlessAws.retrySchedule);
88
- }
89
-
90
61
  let _client = {
91
62
  contents: undefined
92
63
  };
@@ -292,11 +263,6 @@ function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
292
263
  return AppSync_SdlDecorate$ReventlessAws.injectAwsAuthAll(fragment, group, iamFieldNames);
293
264
  }
294
265
 
295
- function stitchWithAwsDirectives(baseFragment, pluginFragments) {
296
- let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(baseFragment, pluginFragments);
297
- return AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, pluginFragments), sources));
298
- }
299
-
300
266
  function stitchStandaloneWithAwsDirectives(fragment) {
301
267
  let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(fragment, []);
302
268
  return AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitchStandalone(fragment), sources));
@@ -310,6 +276,15 @@ function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
310
276
  let iamRole = new (Aws.iam.Role)(name + `-appsync-role`, {
311
277
  assumeRolePolicy: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"appsync.amazonaws.com"},"Action":"sts:AssumeRole"}]}`
312
278
  }, customOpts);
279
+ new (Aws.iam.RolePolicyAttachment)(name + `-appsync-cwlogs`, {
280
+ policyArn: "arn:aws:iam::aws:policy/service-role/AWSAppSyncPushToCloudWatchLogs",
281
+ role: iamRole.name
282
+ }, customOpts);
283
+ let appsyncLogConfig_cloudwatchLogsRoleArn = iamRole.arn;
284
+ let appsyncLogConfig = {
285
+ cloudwatchLogsRoleArn: appsyncLogConfig_cloudwatchLogsRoleArn,
286
+ fieldLogLevel: "ERROR"
287
+ };
313
288
  let userPoolConfigOut = userPoolConfig !== undefined ? userPoolConfig : Auth_Cognito$ReventlessAws.make(name + `-auth`, undefined).apply(c => ({
314
289
  userPoolId: c.userPoolId,
315
290
  defaultAction: "ALLOW",
@@ -320,11 +295,13 @@ function _makeApiResourceWith(name, schema, userPoolConfig, opts) {
320
295
  let apiArgs_additionalAuthenticationProviders = [{
321
296
  authenticationType: "AWS_IAM"
322
297
  }];
298
+ let apiArgs_logConfig = appsyncLogConfig;
323
299
  let apiArgs = {
324
300
  authenticationType: "AMAZON_COGNITO_USER_POOLS",
325
301
  schema: apiArgs_schema,
326
302
  userPoolConfig: apiArgs_userPoolConfig,
327
- additionalAuthenticationProviders: apiArgs_additionalAuthenticationProviders
303
+ additionalAuthenticationProviders: apiArgs_additionalAuthenticationProviders,
304
+ logConfig: apiArgs_logConfig
328
305
  };
329
306
  let graphQLApi = new (Aws.appsync.GraphQLApi)(name, apiArgs, customOpts);
330
307
  return [
@@ -350,20 +327,6 @@ function generateFragment(mutationEntries, queryEntries) {
350
327
  return injectAwsAuth(fragment, mutationEntries, queryEntries);
351
328
  }
352
329
 
353
- function updateSchema(api, baseFragment, pluginFragments) {
354
- let augmentedBaseFragment = injectAwsAuthAll(baseFragment, "Admin", undefined);
355
- let sdl = stitchWithAwsDirectives(augmentedBaseFragment, pluginFragments);
356
- let resultPromise = {
357
- contents: Promise.resolve()
358
- };
359
- api.apply(graphQLApi => graphQLApi.id.apply(apiId => {
360
- let effect = deploySchemaWithRetry(getClient(), apiId, sdl);
361
- let p = Effect$1.runPromise(effect);
362
- resultPromise.contents = p;
363
- }));
364
- return resultPromise.contents;
365
- }
366
-
367
330
  let stampSharedIamTypes = AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes;
368
331
 
369
332
  let primaryAuthenticationType = "AMAZON_COGNITO_USER_POOLS";
@@ -374,8 +337,6 @@ export {
374
337
  startSchemaCreation,
375
338
  startSchemaCreationRetrying,
376
339
  waitForSchemaActive,
377
- getIntrospectionSdl,
378
- deploySchemaWithRetry,
379
340
  _client,
380
341
  getClient,
381
342
  waitForMergeSuccess,
@@ -387,7 +348,6 @@ export {
387
348
  stampSharedIamTypes,
388
349
  injectAwsAuth,
389
350
  injectAwsAuthAll,
390
- stitchWithAwsDirectives,
391
351
  stitchStandaloneWithAwsDirectives,
392
352
  primaryAuthenticationType,
393
353
  _makeApiResourceWith,
@@ -395,6 +355,5 @@ export {
395
355
  makeSourceApiResource,
396
356
  makePluginSourceApiResource,
397
357
  generateFragment,
398
- updateSchema,
399
358
  }
400
359
  /* log Not a pure module */
@@ -1,5 +1,5 @@
1
1
  // AppSync_MergedApi — merged-API construction for the push-free composition
2
- // path (docs/plans/merged-api-push-free-composition.md, Phase 3).
2
+ // path (docs/plans/done/merged-api-push-free-composition.md, Phase 3).
3
3
  //
4
4
  // A merged API carries no schema of its own: source APIs contribute theirs via
5
5
  // SourceApiAssociation and AWS composes the merged endpoint. The platform