@reventlessdev/reventless-aws 3.0.0-alpha.200 → 3.0.0-alpha.202

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,197 @@
1
+ // Runtime-pure push engine for the admin ApiSchemaPush SideEffect
2
+ // (docs/plans/event-sourced-fragment-registries.md § Reactive writer design).
3
+ //
4
+ // The ApiFragmentRegistry singleton aggregate's behaviour emits ApiSchemaComputed
5
+ // { snapshot } — the WHOLE per-plugin fragment set folded from the aggregate's own
6
+ // consistent single-partition state after each change. The ApiSchemaPush SideEffect
7
+ // (compiled from ApiSchemaPush.res) subscribes to that event via the aggregate's
8
+ // DynamoDB stream (single-shard for a singleton → naturally serialized, no concurrent
9
+ // StartSchemaCreation), and hands the snapshot here.
10
+ //
11
+ // This module stitches one AWS-decorated schema per target API from the snapshot
12
+ // (identical decoration to the deploy path via the runtime-pure
13
+ // AppSync_SdlDecorate.planAwsPushes), pushes each behind the catastrophic-shrink
14
+ // guard, and writes the outcome back with RecordApiFragmentPush onto the
15
+ // ApiFragmentRegistry aggregate's command topic so the deploy waiter can poll
16
+ // Platform_ApiFragments.
17
+ //
18
+ // Runtime-purity discipline: NO @pulumi imports (this loads in the SideEffectHandler
19
+ // Lambda — see reference_pulumi_leaks_into_lambda_runtime_graph). All config is read
20
+ // from env at invocation time; the AppSync push uses the AppSync SDK directly.
21
+
22
+ import { makeQueueRef, log } from "./HandlerFactoryHelpers.mjs";
23
+ import { publishJsons as sqsPublishJsons } from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
24
+ import { planAwsPushes } from "@reventlessdev/reventless-aws/src/components/Api/AppSync_SdlDecorate.res.mjs";
25
+ import {
26
+ countRootTypeFields,
27
+ isCatastrophicSchemaShrink,
28
+ } from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
29
+ import {
30
+ baseFragment as adminBaseFragment,
31
+ systemCallerFieldNames,
32
+ } from "@reventlessdev/reventless-core/src/admin/AdminApi.res.mjs";
33
+
34
+ const COMP = "apiSchemaPush";
35
+
36
+ // ── AppSync push primitives (mirror AdminEventCollectorEntryPoint.mjs) ──────────
37
+
38
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
39
+
40
+ // Push the SDL. Bounded retry on the AppSync API-level lock ("Schema is currently being
41
+ // altered" / ConcurrentModification) — the legacy connect-driven mkUpdateApiSchema still
42
+ // pushes until Plan Phase 4, and a standalone-service push can also contend. The lock
43
+ // clears in seconds, so this is NOT the plan's rejected lagging-read retry (that had no
44
+ // ceiling). The singleton aggregate's single-shard stream already serializes THIS writer
45
+ // against itself; this only covers cross-writer contention.
46
+ async function updateAppSyncSchema(apiId, sdl) {
47
+ const { AppSyncClient, StartSchemaCreationCommand } = await import("@aws-sdk/client-appsync");
48
+ const client = new AppSyncClient({});
49
+ const maxAttempts = 6;
50
+ for (let attempt = 1; ; attempt++) {
51
+ try {
52
+ await client.send(new StartSchemaCreationCommand({ apiId, definition: sdl }));
53
+ return;
54
+ } catch (e) {
55
+ const msg = (e && e.message) || String(e);
56
+ const locked = /being altered|ConcurrentModification|currently in the process/i.test(msg);
57
+ if (!locked || attempt >= maxAttempts) throw e;
58
+ const backoff = Math.min(1000 * 2 ** (attempt - 1), 8000);
59
+ log.warn(`schema push contended (${msg}) — retry ${attempt}/${maxAttempts} in ${backoff}ms`, { comp: COMP });
60
+ await sleep(backoff);
61
+ }
62
+ }
63
+ }
64
+
65
+ // Current live schema as SDL for the shrink guard. "" (not error) when the API has
66
+ // no schema yet or introspection fails — the caller treats "" as "no baseline".
67
+ async function getCurrentSchemaSdl(apiId) {
68
+ try {
69
+ const { AppSyncClient, GetIntrospectionSchemaCommand } = await import("@aws-sdk/client-appsync");
70
+ const client = new AppSyncClient({});
71
+ const resp = await client.send(new GetIntrospectionSchemaCommand({ apiId, format: "SDL" }));
72
+ if (!resp || !resp.schema) return "";
73
+ return Buffer.from(resp.schema).toString("utf-8");
74
+ } catch (e) {
75
+ log.warn(`could not introspect current schema (${(e && e.message) || e}) — skipping shrink guard`, { comp: COMP });
76
+ return "";
77
+ }
78
+ }
79
+
80
+ function parseShrinkThreshold(raw) {
81
+ const n = raw ? Number(raw) : NaN;
82
+ return Number.isFinite(n) && n > 0 && n < 1 ? n : 0.5;
83
+ }
84
+
85
+ // Emit the SchemaShrinkRejected CloudWatch metric via EMF (raw console.log — must
86
+ // stay unwrapped so CloudWatch's EMF auto-detect fires).
87
+ function emitShrinkRejectionMetric(apiId, currentRootFields, newRootFields) {
88
+ try {
89
+ // eslint-disable-next-line no-console
90
+ console.log(
91
+ JSON.stringify({
92
+ _aws: {
93
+ Timestamp: Date.now(),
94
+ CloudWatchMetrics: [
95
+ {
96
+ Namespace: "Reventless/Runtime",
97
+ Dimensions: [["ApiId"]],
98
+ Metrics: [{ Name: "SchemaShrinkRejected", Unit: "Count" }],
99
+ },
100
+ ],
101
+ },
102
+ ApiId: apiId,
103
+ SchemaShrinkRejected: 1,
104
+ currentRootFields,
105
+ newRootFields,
106
+ })
107
+ );
108
+ } catch {
109
+ // metric emission is best-effort
110
+ }
111
+ }
112
+
113
+ // ── Config (deploy-derived, injected as Lambda env by the admin SideEffectHandler) ──
114
+
115
+ function readConfig() {
116
+ const na = (v) => (v && v !== "NOT_AVAILABLE" ? v : "");
117
+ const domainApiId = na(process.env["API_SCHEMA_PUSH_DOMAIN_API_ID"]);
118
+ const platformApiId = na(process.env["API_SCHEMA_PUSH_PLATFORM_API_ID"]) || domainApiId;
119
+ return {
120
+ domainApiId,
121
+ platformApiId,
122
+ splitApi: process.env["API_SCHEMA_PUSH_SPLIT_API"] === "true",
123
+ clonerEnabled: process.env["API_SCHEMA_PUSH_CLONER"] === "true",
124
+ cmdTopicUrl: na(process.env["API_SCHEMA_PUSH_CMD_TOPIC_URL"]),
125
+ };
126
+ }
127
+
128
+ // Write RecordApiFragmentPush back per plugin onto the ApiFragmentRegistry aggregate's
129
+ // command topic (FIFO, singleton id="registry"). @noApi + idempotent — a no-op in the
130
+ // aggregate behaviour if the plugin was deregistered in the meantime.
131
+ async function recordPushOutcomes(cmdTopicUrl, pluginIds, ok, message) {
132
+ if (!cmdTopicUrl || pluginIds.length === 0) return;
133
+ const publisher = sqsPublishJsons(makeQueueRef(cmdTopicUrl), "SQS_FIFO");
134
+ const at = new Date().toISOString();
135
+ const commandJsons = pluginIds.map((pluginId) => ({
136
+ id: "registry",
137
+ meta: { service: "ApiFragmentRegistry", time: at, msgId: "pending", correlationId: pluginId },
138
+ commandJson: { TAG: "RecordApiFragmentPush", pluginId, ok, message, at },
139
+ }));
140
+ try {
141
+ await publisher(commandJsons);
142
+ } catch (e) {
143
+ log.error(`RecordApiFragmentPush dispatch failed: ${(e && e.message) || e}`, { comp: COMP });
144
+ }
145
+ }
146
+
147
+ // ── Entry point ────────────────────────────────────────────────────────────────
148
+
149
+ // snapshot: array of { pluginId, encoded, protocol, apiTarget } — the consistent
150
+ // registry contents after the triggering change, carried on the ApiSchemaComputed event.
151
+ export async function pushApiSchema(snapshot) {
152
+ const cfg = readConfig();
153
+ const entries = Array.isArray(snapshot) ? snapshot.filter((e) => e && typeof e.encoded === "string" && e.encoded) : [];
154
+ const pluginIds = [...new Set(entries.map((e) => e.pluginId).filter((p) => typeof p === "string"))];
155
+ if (!cfg.cmdTopicUrl) {
156
+ log.warn("ApiSchemaPush: no command-topic URL configured — skipping", { comp: COMP });
157
+ return;
158
+ }
159
+
160
+ // Fragments straight from the consistent snapshot — NO eventually-consistent read.
161
+ const fragments = entries.map((e) => ({
162
+ encoded: e.encoded,
163
+ protocol: typeof e.protocol === "string" ? e.protocol : "graphql",
164
+ target: e.apiTarget === "Platform" ? "Platform" : "Domain",
165
+ }));
166
+
167
+ const rawAdminBase = adminBaseFragment(cfg.clonerEnabled);
168
+ const plans = planAwsPushes(rawAdminBase, systemCallerFieldNames, fragments, cfg.splitApi);
169
+
170
+ let ok = true;
171
+ let message = "";
172
+ for (const plan of plans) {
173
+ const apiId = plan.api === "PlatformApi" ? cfg.platformApiId : cfg.domainApiId;
174
+ if (!apiId) continue;
175
+ const threshold = parseShrinkThreshold(process.env["RUNTIME_SCHEMA_SHRINK_THRESHOLD"]);
176
+ const currentSdl = await getCurrentSchemaSdl(apiId);
177
+ if (isCatastrophicSchemaShrink(currentSdl, plan.sdl, threshold)) {
178
+ const cur = countRootTypeFields(currentSdl, "Mutation") + countRootTypeFields(currentSdl, "Query");
179
+ const nw = countRootTypeFields(plan.sdl, "Mutation") + countRootTypeFields(plan.sdl, "Query");
180
+ log.error(`ABORTED schema push for ${plan.api} (${apiId}): ${nw} root field(s) vs ${cur} live (threshold ${threshold}).`, { comp: COMP });
181
+ emitShrinkRejectionMetric(apiId, cur, nw);
182
+ ok = false;
183
+ message = `shrink guard aborted push for ${plan.api}`;
184
+ continue;
185
+ }
186
+ try {
187
+ await updateAppSyncSchema(apiId, plan.sdl);
188
+ log.info(`schema push OK: ${plan.api} (${apiId})`, { comp: COMP });
189
+ } catch (e) {
190
+ ok = false;
191
+ message = (e && e.message) || String(e);
192
+ log.error(`schema push FAILED: ${plan.api} (${apiId}): ${message}`, { comp: COMP });
193
+ }
194
+ }
195
+
196
+ await recordPushOutcomes(cfg.cmdTopicUrl, pluginIds, ok, message);
197
+ }
@@ -15,6 +15,15 @@ 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).
19
+ // The shared "AllSideEffectHandlers" Lambda is built once in finish(); all registered
20
+ // entries are merged onto its env there. Deploy-derived config only — never overrides
21
+ // HANDLER_CONFIG.
22
+ let extraEnvVarsAll: dict<Pulumi.Input.t<string>> = Dict.make()
23
+
24
+ let registerExtraEnv = (~extraEnvVars: dict<Pulumi.Input.t<string>>) =>
25
+ extraEnvVars->Dict.forEachWithKey((v, k) => extraEnvVarsAll->Dict.set(k, v))
26
+
18
27
  type storedSpec = {
19
28
  componentName: string,
20
29
  parentResource: Pulumi.Resource.t,
@@ -143,6 +152,12 @@ let finish = () =>
143
152
 
144
153
  let envVars: dict<Pulumi.Input.t<string>> = Dict.make()
145
154
  envVars->Dict.set("HANDLER_CONFIG", handlerConfigOutput->Pulumi.Output.asInput)
155
+ // Merge bespoke side-effect config (never overrides HANDLER_CONFIG).
156
+ extraEnvVarsAll->Dict.forEachWithKey((v, k) =>
157
+ if k != "HANDLER_CONFIG" {
158
+ envVars->Dict.set(k, v)
159
+ }
160
+ )
146
161
 
147
162
  // Build AssetArchive: static re-export + user packages
148
163
  let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
@@ -161,6 +176,38 @@ let finish = () =>
161
176
  ~opts,
162
177
  )
163
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).
185
+ if extraEnvVarsAll->Dict.keysToArray->Array.length > 0 {
186
+ let _ = PulumiAws.IAM.RolePolicy.make(
187
+ ~name="AllSideEffectHandlers-appsyncSchemaPush",
188
+ ~args={
189
+ policy: PulumiAws.PolicyDocument.make(
190
+ ~id="AllSideEffectHandlersAppsyncSchemaPushPolicy",
191
+ ~statements=[
192
+ {
193
+ sid: "AllowSideEffectStartSchemaCreation",
194
+ effect: Allow,
195
+ actions: Actions([
196
+ "appsync:StartSchemaCreation",
197
+ "appsync:GetSchemaCreationStatus",
198
+ "appsync:GetIntrospectionSchema",
199
+ ]),
200
+ resources: AllResources,
201
+ },
202
+ ],
203
+ )
204
+ ->PulumiAws.PolicyDocument.toJsonString
205
+ ->Pulumi.Input.make,
206
+ role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
207
+ },
208
+ )
209
+ }
210
+
164
211
  let channelSpecs = storedSpecs->Array.map(({channelSpec}) => channelSpec)
165
212
  let _connectResources = EventCollectorChannel.connect(
166
213
  ~name="AllSideEffectHandlers",
@@ -1,11 +1,14 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Aws from "@pulumi/aws";
4
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
3
5
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
7
  import * as Pulumi from "@pulumi/pulumi";
6
8
  import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
9
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
8
10
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
11
+ import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
9
12
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
10
13
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
11
14
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
@@ -20,6 +23,14 @@ function registerSideEffectHandler(sideEffectHandlerName, sideEffectModulePaths)
20
23
  };
21
24
  }
22
25
 
26
+ let extraEnvVarsAll = {};
27
+
28
+ function registerExtraEnv(extraEnvVars) {
29
+ Stdlib_Dict.forEachWithKey(extraEnvVars, (v, k) => {
30
+ extraEnvVarsAll[k] = v;
31
+ });
32
+ }
33
+
23
34
  let storedSpecs = [];
24
35
 
25
36
  let grandParent = {
@@ -99,8 +110,29 @@ function finish() {
99
110
  let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
100
111
  let envVars = {};
101
112
  envVars["HANDLER_CONFIG"] = handlerConfigOutput;
113
+ Stdlib_Dict.forEachWithKey(extraEnvVarsAll, (v, k) => {
114
+ if (k !== "HANDLER_CONFIG") {
115
+ envVars[k] = v;
116
+ return;
117
+ }
118
+ });
102
119
  let match$1 = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/SideEffectEntryPoint.mjs", packageDirs, undefined);
103
120
  let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset("AllSideEffectHandlers", "Reactor", match$1.code, match$1.sourceCodeHash, envVars, match[0], match[1], undefined, undefined, undefined, undefined, undefined, opts);
121
+ if (Object.keys(extraEnvVarsAll).length !== 0) {
122
+ new (Aws.iam.RolePolicy)("AllSideEffectHandlers-appsyncSchemaPush", {
123
+ policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "AllSideEffectHandlersAppsyncSchemaPushPolicy", [{
124
+ Sid: "AllowSideEffectStartSchemaCreation",
125
+ Effect: "Allow",
126
+ Action: [
127
+ "appsync:StartSchemaCreation",
128
+ "appsync:GetSchemaCreationStatus",
129
+ "appsync:GetIntrospectionSchema"
130
+ ],
131
+ Resource: "*"
132
+ }])),
133
+ role: runtime.parts.lambdaRole.id
134
+ });
135
+ }
104
136
  let channelSpecs = storedSpecs.map(param => param.channelSpec);
105
137
  EventCollectorChannel_DynamoDbStream$ReventlessAws.connect("AllSideEffectHandlers", channelSpecs, runtime, opts);
106
138
  } else {
@@ -120,6 +152,8 @@ export {
120
152
  log,
121
153
  sideEffectInfos,
122
154
  registerSideEffectHandler,
155
+ extraEnvVarsAll,
156
+ registerExtraEnv,
123
157
  storedSpecs,
124
158
  grandParent,
125
159
  forEventCollector,
@@ -22,6 +22,7 @@ module Make = (): ReventlessCore.SideEffectHandler.T => {
22
22
  ~resourceNaming,
23
23
  ~memorySize=?,
24
24
  ~timeout=?,
25
+ ~extraEnvVars=?,
25
26
  ~opts=?,
26
27
  ) => {
27
28
  let component = Inner.make(
@@ -49,6 +50,13 @@ module Make = (): ReventlessCore.SideEffectHandler.T => {
49
50
  ~sideEffectModulePaths,
50
51
  )
51
52
 
53
+ // Bespoke side effects (e.g. admin ApiSchemaPush) inject deploy-derived config as
54
+ // extra Lambda env vars, merged onto the shared side-effect-handler Lambda in finish().
55
+ switch extraEnvVars {
56
+ | Some(env) => EventCollectorRuntimeBuilder.registerExtraEnv(~extraEnvVars=env)
57
+ | None => ()
58
+ }
59
+
52
60
  component
53
61
  }
54
62
  }
@@ -34,10 +34,13 @@ function Make($star) {
34
34
  forEventCollector: SideEffectHandlerRuntime_Builder_Single$ReventlessAws.forEventCollector,
35
35
  finish: SideEffectHandlerRuntime_Builder_Single$ReventlessAws.finish
36
36
  });
37
- let make = (name, sideEffects, allEventTopics, allCommandTopics, targets, queryEngine, scheduler, resourceNaming, memorySize, timeout, opts) => {
38
- let component = Inner.make(name, sideEffects, allEventTopics, allCommandTopics, targets, queryEngine, scheduler, resourceNaming, memorySize, timeout, opts);
37
+ let make = (name, sideEffects, allEventTopics, allCommandTopics, targets, queryEngine, scheduler, resourceNaming, memorySize, timeout, extraEnvVars, opts) => {
38
+ let component = Inner.make(name, sideEffects, allEventTopics, allCommandTopics, targets, queryEngine, scheduler, resourceNaming, memorySize, timeout, undefined, opts);
39
39
  let sideEffectModulePaths = sideEffects.map(SE => Util_Bundle$ReventlessAws.getModuleSpecifier(SE.moduleUrl));
40
40
  SideEffectHandlerRuntime_Builder_Single$ReventlessAws.registerSideEffectHandler(name, sideEffectModulePaths);
41
+ if (extraEnvVars !== undefined) {
42
+ SideEffectHandlerRuntime_Builder_Single$ReventlessAws.registerExtraEnv(extraEnvVars);
43
+ }
41
44
  return component;
42
45
  };
43
46
  return {
@@ -94,6 +94,38 @@ describe("AppSync_Adapter.stitchWithAwsDirectives", () => {
94
94
  })
95
95
  })
96
96
 
97
+ describe("AppSync_SdlDecorate.injectAwsAuthAll", () => {
98
+ // Regression: an ARG-LESS field named in iamFieldNames must still get @aws_iam.
99
+ // extractLeadingName used to return `Platform_ApiFragments:` (trailing colon) for
100
+ // arg-less fields, so isIam missed it and the field stayed Cognito-only — which
101
+ // 401'd the deploy waiter's SigV4 poll of Platform_ApiFragments on real AWS.
102
+ testSync("dual-auths an arg-less query field named in iamFieldNames", () => {
103
+ let base = ReventlessCore.GraphQL_Stitcher.encode({
104
+ types: [],
105
+ mutations: [` Platform_RegisterApiFragment(input: In): CommandResult`],
106
+ queries: [` Platform_ApiFragments: [Entry!]!`, ` Other_Query: Int`],
107
+ subscriptions: [],
108
+ subscriptionSources: [],
109
+ })
110
+ let decorated = AppSync_SdlDecorate.injectAwsAuthAll(
111
+ base,
112
+ ~group="Admin",
113
+ ~iamFieldNames=["Platform_RegisterApiFragment", "Platform_ApiFragments"],
114
+ )
115
+ let parts = ReventlessCore.GraphQL_Stitcher.decode(decorated)
116
+ let queryField = name =>
117
+ parts.queries->Array.find(q => q->String.includes(name))->Option.getOrThrow
118
+ // The arg-less IAM query gets dual-auth.
119
+ expect(queryField("Platform_ApiFragments"))->toContain("@aws_iam")
120
+ // The arg-full IAM mutation still does too.
121
+ expect(parts.mutations->Array.getUnsafe(0))->toContain("@aws_iam")
122
+ // A query NOT in iamFieldNames stays Cognito-only.
123
+ let other = queryField("Other_Query")
124
+ expect(other)->toContain(`@aws_auth(cognito_groups: ["Admin"])`)
125
+ expect(other->String.includes("@aws_iam"))->toBe(false)
126
+ })
127
+ })
128
+
97
129
  describe("AppSync_SdlDecorate.planAwsPushes", () => {
98
130
  // Neutral admin base: a system-callable mutation + a shared traversal type.
99
131
  let rawAdminBase = ReventlessCore.GraphQL_Stitcher.encode({
@@ -89,6 +89,32 @@ globalThis.describe("AppSync_Adapter.stitchWithAwsDirectives", () => {
89
89
  });
90
90
  });
91
91
 
92
+ globalThis.describe("AppSync_SdlDecorate.injectAwsAuthAll", () => {
93
+ globalThis.test("dual-auths an arg-less query field named in iamFieldNames", () => {
94
+ let base = GraphQL_Stitcher$ReventlessCore.encode({
95
+ types: [],
96
+ mutations: [` Platform_RegisterApiFragment(input: In): CommandResult`],
97
+ queries: [
98
+ ` Platform_ApiFragments: [Entry!]!`,
99
+ ` Other_Query: Int`
100
+ ],
101
+ subscriptions: [],
102
+ subscriptionSources: []
103
+ });
104
+ let decorated = AppSync_SdlDecorate$ReventlessAws.injectAwsAuthAll(base, "Admin", [
105
+ "Platform_RegisterApiFragment",
106
+ "Platform_ApiFragments"
107
+ ]);
108
+ let parts = GraphQL_Stitcher$ReventlessCore.decode(decorated);
109
+ let queryField = name => Stdlib_Option.getOrThrow(parts.queries.find(q => q.includes(name)), undefined);
110
+ globalThis.expect(queryField("Platform_ApiFragments")).toContain("@aws_iam");
111
+ globalThis.expect(parts.mutations[0]).toContain("@aws_iam");
112
+ let other = queryField("Other_Query");
113
+ globalThis.expect(other).toContain(`@aws_auth(cognito_groups: ["Admin"])`);
114
+ globalThis.expect(other.includes("@aws_iam")).toBe(false);
115
+ });
116
+ });
117
+
92
118
  globalThis.describe("AppSync_SdlDecorate.planAwsPushes", () => {
93
119
  let rawAdminBase = GraphQL_Stitcher$ReventlessCore.encode({
94
120
  types: [`type CommandAccepted {\n id: ID!\n}`],