@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
@@ -119,19 +119,14 @@ let injectAwsAuthAll = (
119
119
  }
120
120
 
121
121
  // Shared traversal types every callable surface reaches — `PageInfo` (relay
122
- // connections, injected by the stitcher), the `CommandResult` members (mutation
123
- // returns, deduped across fragments by the stitcher), and `Platform_ApiFragmentEntry`
124
- // (the return type of the IAM-callable `Platform_ApiFragments` status query the deploy
125
- // waiter polls via SigV4 — without the type-level `@aws_iam` the SigV4 caller reaches the
126
- // query field but is "Not Authorized to access <field> on type Platform_ApiFragmentEntry").
127
- // Stamped once on the ASSEMBLED SDL: per-fragment stamping would race the stitcher's
128
- // first-wins dedupe against unstamped sibling copies.
122
+ // connections, injected by the stitcher) and the `CommandResult` members
123
+ // (mutation returns). Stamped once on the ASSEMBLED SDL so an IAM system
124
+ // caller (`@@reventless.systemCallable` fields) can traverse them.
129
125
  let sharedIamTypeNames = [
130
126
  "PageInfo",
131
127
  "CommandAccepted",
132
128
  "CommandRejected",
133
129
  "CommandPending",
134
- "Platform_ApiFragmentEntry",
135
130
  ]
136
131
 
137
132
  let stampSharedIamTypes = (sdl: string): string =>
@@ -186,60 +181,6 @@ let stampCanonicalTypes = (sdl: string): string =>
186
181
  })
187
182
  ->Array.join("\n")
188
183
 
189
- // ── Reactive push planner (runtime-pure) ─────────────────────────────────────
190
- // The AWS-decorated counterpart of core `GraphQL_PushPlanner.planPushes`: given
191
- // the fragments currently in the ApiFragmentRegistry (each tagged with its target
192
- // API, as the bare string "Domain" / "Platform"), it produces one fully
193
- // AppSync-decorated SDL push plan per API. It reuses the core planner for the
194
- // neutral base-selection + stitch, then applies the AppSync dialect exactly as the
195
- // deploy path's `stitchWithAwsDirectives`:
196
- // - the admin base is auth-decorated (group "Admin" + dual-auth on the system-
197
- // caller field names) BEFORE stitching, so the Platform/unified base carries
198
- // @aws_auth/@aws_iam (the empty Domain-split base needs none);
199
- // - @aws_subscribe is injected from the neutral subscriptionSources metadata;
200
- // - the shared traversal types are stamped once on the assembled SDL.
201
- // Plugin fragments already carry their own per-field @aws_auth (baked at
202
- // generateFragment time), so they stitch in as-is. `api` comes back as the bare
203
- // string "PlatformApi" / "DomainApi" for the mjs to map onto the AppSync ids.
204
- type targetedFragmentInput = {
205
- encoded: string,
206
- protocol: string,
207
- // "Domain" | "Platform" — the bare-string runtime form of Reventless.Plugin.apiTarget.
208
- target: string,
209
- }
210
-
211
- type awsPushPlan = {
212
- // "PlatformApi" | "DomainApi" — the bare-string runtime form of GraphQL_PushPlanner.planTarget.
213
- api: string,
214
- sdl: string,
215
- }
216
-
217
- let planAwsPushes = (
218
- ~rawAdminBase: Reventless.Plugin.apiSchemaFragment,
219
- ~iamFieldNames: array<string>,
220
- ~fragments: array<targetedFragmentInput>,
221
- ~splitApi: bool,
222
- ): array<awsPushPlan> => {
223
- let authBase = injectAwsAuthAll(rawAdminBase, ~group="Admin", ~iamFieldNames)
224
- let targeted = fragments->Array.map((f): ReventlessCore.GraphQL_PushPlanner.targetedFragment => {
225
- fragment: {encoded: f.encoded, protocol: f.protocol},
226
- target: switch f.target {
227
- | "Platform" => Platform
228
- | _ => Domain
229
- },
230
- })
231
- let allFrags = targeted->Array.map(t => t.fragment)
232
- let sources = ReventlessCore.GraphQL_Stitcher.collectSubscriptionSources(
233
- ~baseFragment=rawAdminBase,
234
- ~pluginFragments=allFrags,
235
- )
236
- ReventlessCore.GraphQL_PushPlanner.planPushes(~adminBase=authBase, ~fragments=targeted, ~splitApi)
237
- ->Array.map(plan => {
238
- let sdl = plan.sdl->injectAwsSubscribe(~sources)->stampSharedIamTypes
239
- let api = switch plan.api {
240
- | PlatformApi => "PlatformApi"
241
- | DomainApi => "DomainApi"
242
- }
243
- {api, sdl}
244
- })
245
- }
184
+ // (The reactive push planner planAwsPushes, the AWS counterpart of core
185
+ // GraphQL_PushPlanner was retired with the merged-API cutover: source APIs
186
+ // are single writers, AWS composes the merged endpoint, nothing re-stitches.)
@@ -3,7 +3,6 @@
3
3
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
4
  import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
5
5
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
6
- import * as GraphQL_PushPlanner$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_PushPlanner.res.mjs";
7
6
 
8
7
  function injectAwsSubscribe(sdl, sources) {
9
8
  if (sources.length === 0) {
@@ -80,8 +79,7 @@ let sharedIamTypeNames = [
80
79
  "PageInfo",
81
80
  "CommandAccepted",
82
81
  "CommandRejected",
83
- "CommandPending",
84
- "Platform_ApiFragmentEntry"
82
+ "CommandPending"
85
83
  ];
86
84
 
87
85
  function stampSharedIamTypes(sdl) {
@@ -125,33 +123,6 @@ function stampCanonicalTypes(sdl) {
125
123
  }).join("\n");
126
124
  }
127
125
 
128
- function planAwsPushes(rawAdminBase, iamFieldNames, fragments, splitApi) {
129
- let authBase = injectAwsAuthAll(rawAdminBase, "Admin", iamFieldNames);
130
- let targeted = fragments.map(f => {
131
- let match = f.target;
132
- let tmp = match === "Platform" ? "Platform" : "Domain";
133
- return {
134
- fragment: {
135
- encoded: f.encoded,
136
- protocol: f.protocol
137
- },
138
- target: tmp
139
- };
140
- });
141
- let allFrags = targeted.map(t => t.fragment);
142
- let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(rawAdminBase, allFrags);
143
- return GraphQL_PushPlanner$ReventlessCore.planPushes(authBase, targeted, splitApi).map(plan => {
144
- let sdl = stampSharedIamTypes(injectAwsSubscribe(plan.sdl, sources));
145
- let match = plan.api;
146
- let api;
147
- api = match === "DomainApi" ? "DomainApi" : "PlatformApi";
148
- return {
149
- api: api,
150
- sdl: sdl
151
- };
152
- });
153
- }
154
-
155
126
  export {
156
127
  injectAwsSubscribe,
157
128
  formatDualAuthDirective,
@@ -160,6 +131,5 @@ export {
160
131
  stampSharedIamTypes,
161
132
  canonicalTypeNames,
162
133
  stampCanonicalTypes,
163
- planAwsPushes,
164
134
  }
165
135
  /* GraphQL_Stitcher-ReventlessCore Not a pure module */
@@ -28,8 +28,7 @@ function Make(HooksConfig) {
28
28
  hooks: HooksConfig.hooks
29
29
  })({})({
30
30
  makeApiResource: AppSync_Adapter$ReventlessAws.makeApiResource,
31
- generateFragment: AppSync_Adapter$ReventlessAws.generateFragment,
32
- updateSchema: AppSync_Adapter$ReventlessAws.updateSchema
31
+ generateFragment: AppSync_Adapter$ReventlessAws.generateFragment
33
32
  })({
34
33
  make: RuntimeEnvironment_Lambda$ReventlessAws.make,
35
34
  groupBySource: RuntimeEnvironment_Lambda$ReventlessAws.groupBySource,
@@ -25,16 +25,48 @@ let make: ReventlessCore.Heartbeat_Adapter.runnerMaker<runtimeParts> = (
25
25
  let lambdaRole = runtime.parts.lambdaRole
26
26
  let coreSqsQueue = remoteChannel.resources->Util_SQS.findResolvedResource
27
27
 
28
- let _attachPoliciesAndSetEventTarget =
29
- (
30
- lambda->Pulumi.Output.flatMap(lambda => lambda.arn),
31
- lambda->Pulumi.Output.flatMap(lambda => lambda.name),
32
- lambdaRole.id,
28
+ // Grant sqs:SendMessage on the Core Plugin ExtensionPoint command topic at
29
+ // TOP LEVEL. Previously this RolePolicy was created inside the
30
+ // `Pulumi.Output.apply` below — and a resource created inside an apply
31
+ // callback does not reliably register with the engine, which intermittently
32
+ // left the heartbeat Lambda without this grant (IAM AccessDenied:
33
+ // `sqs:sendmessage on CorePluginExtPointCmdTopic`). `coreSqsQueue.urn` is an
34
+ // already-resolved ARN string from the remote channel and `lambdaRole.id` an
35
+ // Output, so neither needs an apply. Mirrors the EC `pta` grant fix — see
36
+ // docs/analysis/ec-publish-to-aggregates-grant-broken.md.
37
+ let _attachHeartbeatLambdaPolicy = {
38
+ open PulumiAws.PolicyDocument
39
+ let heartbeatLambdaSendMessagePolicyDocument = PulumiAws.PolicyDocument.make(
40
+ ~statements=[
41
+ {
42
+ sid: "AllowLambdaToSendSQS",
43
+ effect: Allow,
44
+ actions: Action("sqs:SendMessage"),
45
+ resources: Resource(coreSqsQueue.urn),
46
+ },
47
+ ],
33
48
  )
34
- ->Pulumi.Output.all3
35
- ->Pulumi.Output.apply(((lambdaArn, lambdaName, heartbeatRoleId)) => {
36
- open PulumiAws.PolicyDocument
49
+ PulumiAws.IAM.RolePolicy.make(
50
+ ~name=name ++ "RolePolicy",
51
+ ~args={
52
+ policy: PulumiAws.PolicyDocument.mergePolicyDocuments(
53
+ name ++ "Policy",
54
+ [
55
+ PulumiAws.Lambda.defaultLoggingPolicyDocument,
56
+ heartbeatLambdaSendMessagePolicyDocument,
57
+ ],
58
+ )->Pulumi.Output.asInput,
59
+ role: lambdaRole.id->Pulumi.Output.asInput,
60
+ },
61
+ )
62
+ }
37
63
 
64
+ // The Lambda permission and CloudWatch event target genuinely need the
65
+ // Lambda's resolved arn/name, so they stay inside an apply.
66
+ let _permissionAndEventTarget =
67
+ (lambda->Pulumi.Output.flatMap(lambda => lambda.arn), lambda->Pulumi.Output.flatMap(lambda => lambda.name))
68
+ ->Pulumi.Output.all2
69
+ ->Pulumi.Output.apply(((lambdaArn, lambdaName)) => {
38
70
  let _addHeartbeatLambdaPermission = PulumiAws.Lambda.Permission.make(
39
71
  ~name,
40
72
  ~args={
@@ -45,33 +77,6 @@ let make: ReventlessCore.Heartbeat_Adapter.runnerMaker<runtimeParts> = (
45
77
  ~opts,
46
78
  )
47
79
 
48
- let heartbeatLambdaSendMessagePolicyDocument = {
49
- PulumiAws.PolicyDocument.make(
50
- ~statements=[
51
- {
52
- sid: "AllowLambdaToSendSQS",
53
- effect: Allow,
54
- actions: Action("sqs:SendMessage"),
55
- resources: Resource(coreSqsQueue.urn),
56
- },
57
- ],
58
- )
59
- }
60
-
61
- let _attachHeartbeatLambdaPolicy = PulumiAws.IAM.RolePolicy.make(
62
- ~name=name ++ "RolePolicy",
63
- ~args={
64
- policy: PulumiAws.PolicyDocument.mergePolicyDocuments(
65
- name ++ "Policy",
66
- [
67
- PulumiAws.Lambda.defaultLoggingPolicyDocument,
68
- heartbeatLambdaSendMessagePolicyDocument,
69
- ],
70
- )->Pulumi.Output.asInput,
71
- role: heartbeatRoleId->Pulumi.Input.make,
72
- },
73
- )
74
-
75
80
  let _cloudwatchEventTarget = {
76
81
  open PulumiAws.Cloudwatch
77
82
  EventTarget.make(
@@ -27,29 +27,28 @@ function make(name, remoteChannel, timeout, runtime, opts) {
27
27
  let lambda = runtime.parts.lambda;
28
28
  let lambdaRole = runtime.parts.lambdaRole;
29
29
  let coreSqsQueue = Util_SQS$ReventlessAws.findResolvedResource(remoteChannel.resources);
30
+ let heartbeatLambdaSendMessagePolicyDocument = PolicyDocument$PulumiAws.make(undefined, undefined, [{
31
+ Sid: "AllowLambdaToSendSQS",
32
+ Effect: "Allow",
33
+ Action: "sqs:SendMessage",
34
+ Resource: coreSqsQueue.urn
35
+ }]);
36
+ new (Aws.iam.RolePolicy)(name + "RolePolicy", {
37
+ policy: PolicyDocument$PulumiAws.mergePolicyDocuments(name + "Policy", [
38
+ Lambda$PulumiAws.defaultLoggingPolicyDocument,
39
+ heartbeatLambdaSendMessagePolicyDocument
40
+ ]),
41
+ role: lambdaRole.id
42
+ });
30
43
  Pulumi.all([
31
44
  Output$Pulumi.flatMap(lambda, lambda => lambda.arn),
32
- Output$Pulumi.flatMap(lambda, lambda => lambda.name),
33
- lambdaRole.id
45
+ Output$Pulumi.flatMap(lambda, lambda => lambda.name)
34
46
  ]).apply(param => {
35
47
  new (Aws.lambda.Permission)(name, {
36
48
  action: "lambda:InvokeFunction",
37
49
  function: param[1],
38
50
  principal: AWS$ReventlessAws.CloudwatchEventRule.principal
39
51
  }, opts$1);
40
- let heartbeatLambdaSendMessagePolicyDocument = PolicyDocument$PulumiAws.make(undefined, undefined, [{
41
- Sid: "AllowLambdaToSendSQS",
42
- Effect: "Allow",
43
- Action: "sqs:SendMessage",
44
- Resource: coreSqsQueue.urn
45
- }]);
46
- new (Aws.iam.RolePolicy)(name + "RolePolicy", {
47
- policy: PolicyDocument$PulumiAws.mergePolicyDocuments(name + "Policy", [
48
- Lambda$PulumiAws.defaultLoggingPolicyDocument,
49
- heartbeatLambdaSendMessagePolicyDocument
50
- ]),
51
- role: param[2]
52
- });
53
52
  new (Aws.cloudwatch.EventTarget)(name, {
54
53
  rule: Cloudwatch_EventTarget$PulumiAws.Rule.ofEventRule(cloudwatchEventRule),
55
54
  arn: param[0]
@@ -622,79 +622,68 @@ module Make = (
622
622
  aggregateNameSet->Dict.set(aggName, ())
623
623
  )
624
624
  )
625
- let queueUrlOutputs =
626
- aggregateNameSet
627
- ->Dict.keysToArray
628
- ->Array.filterMap(aggName => context.publishToAggregates->Dict.get(aggName))
629
- let _ = if queueUrlOutputs->Array.length > 0 {
630
- // The dict is declared as `dict<Pulumi.Output.t<string>>` but the
631
- // DCB slice path (Dcb_Builder.dcbCommandTopicQueueUrl) can leak the
632
- // `{BS_PRIVATE_NESTED_SOME_NONE: 0}` nested-option sentinel as a
633
- // value when the SQS Queue's .id is still unresolved during early
634
- // CustomResource construction. Pulumi.all happily wraps & resolves
635
- // it as Output<sentinel>, so .apply hands us a non-string. Drop
636
- // anything that isn't actually a stringan extension targeting
637
- // the affected slice will need a separate grant out-of-band until
638
- // the resource is fully provisioned and a subsequent preview/up
639
- // produces a real ARN. We build the policy document inline so an
640
- // empty filtered ARN array skips RolePolicy creation entirely
641
- // (PutRolePolicy rejects empty Resource arrays with
642
- // MalformedPolicyDocument).
643
- let policyJsonOutput =
644
- queueUrlOutputs
645
- ->Pulumi.Output.all
646
- ->Pulumi.Output.apply(urls => {
647
- let queueArns =
648
- urls
649
- ->Array.filter(url => typeof(url) === #string)
650
- ->Array.map(url =>
651
- // queue URL → ARN: https://sqs.<region>.amazonaws.com/<acct>/<name>
652
- // → arn:aws:sqs:<region>:<acct>:<name>
653
- switch url->String.split("/") {
654
- | [_, _, host, acct, name] =>
655
- let region =
656
- host->String.split(".")->Array.get(1)->Option.getOr("eu-west-1")
657
- `arn:aws:sqs:${region}:${acct}:${name}`
658
- | _ => url
659
- }
660
- )
661
- if queueArns->Array.length == 0 {
662
- None
625
+ // One RolePolicy per extension target, each derived through a SINGLE
626
+ // `.apply` on that target's queue-URL Output. `Pulumi.Output.all` is
627
+ // deliberately NOT used here: the DCB slice's URL is an `.apply`-lifted
628
+ // Output (Dcb_Builder reads the queue id via `operations.apply(_ =>
629
+ // resources[0].id)`), and `Pulumi.Output.all` mis-resolves such lifted
630
+ // Outputs to the `{BS_PRIVATE_NESTED_SOME_NONE: 0}` sentinel (both at
631
+ // preview and up) while a direct `.apply` on the same Output resolves the
632
+ // real URL verified by deploying both variants. RolePolicy creation
633
+ // happens at TOP LEVEL: calling `RolePolicy.make` inside a
634
+ // `Pulumi.Output.apply` callback does not reliably register the resource
635
+ // (the previous inside-apply variant existed on no collector role in the
636
+ // account). Dict values are heterogeneous at runtime plain resolved URL
637
+ // strings for aggregate targets, Outputs for DCB-slice targets; anything
638
+ // else is skipped. Inside the apply a non-string URL falls back to a
639
+ // benign placeholder ARN so the policy document stays valid (IAM rejects
640
+ // empty Resource entries with MalformedPolicyDocument).
641
+ // See docs/analysis/ec-publish-to-aggregates-grant-broken.md.
642
+ aggregateNameSet
643
+ ->Dict.keysToArray
644
+ ->Array.forEach(aggName => {
645
+ let urlOutput = switch context.publishToAggregates->Dict.get(aggName) {
646
+ | Some(o) if Pulumi.Output.isOutput(o) => Some(o)
647
+ | Some(o) if typeof(o) === #string => Some(Pulumi.Output.make((o->Obj.magic: string)))
648
+ | _ => None
649
+ }
650
+ switch urlOutput {
651
+ | Some(urlOutput) =>
652
+ let policyJson = urlOutput->Pulumi.Output.apply(url => {
653
+ let arn = if typeof(url) === #string {
654
+ // queue URL ARN: https://sqs.<region>.amazonaws.com/<acct>/<name>
655
+ // arn:aws:sqs:<region>:<acct>:<name>
656
+ switch url->String.split("/") {
657
+ | [_, _, host, acct, name] =>
658
+ let region = host->String.split(".")->Array.get(1)->Option.getOr("eu-west-1")
659
+ `arn:aws:sqs:${region}:${acct}:${name}`
660
+ | _ => url
661
+ }
663
662
  } else {
664
- Some(
665
- PulumiAws.PolicyDocument.make(
666
- ~id=`${name}PublishToAggregatesPolicy`,
667
- ~statements=[
668
- {
669
- sid: "AllowEcPublishToAggregateCmdTopics",
670
- effect: Allow,
671
- actions: Action("sqs:SendMessage"),
672
- resources: Resources(queueArns),
673
- },
674
- ],
675
- )->PulumiAws.PolicyDocument.toJsonString,
676
- )
663
+ "arn:aws:sqs:eu-west-1:000000000000:reventless-unresolved-cmd-topic"
677
664
  }
678
- })
679
- // Pulumi.all wraps a Some(string)/None and resolves to either the
680
- // policy JSON or undefined — the latter makes PutRolePolicy a no-op
681
- // by failing fast in the provider, so guard RolePolicy creation on
682
- // the resolved value being Some.
683
- let _ =
684
- policyJsonOutput->Pulumi.Output.apply(policyOpt =>
685
- switch policyOpt {
686
- | Some(policyJson) =>
687
- let _ = PulumiAws.IAM.RolePolicy.make(
688
- ~name=`${name}-publishToAggregates`,
689
- ~args={
690
- policy: policyJson->Pulumi.Input.make,
691
- role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
665
+ PulumiAws.PolicyDocument.make(
666
+ ~id=`${name}Pta${aggName}Policy`,
667
+ ~statements=[
668
+ {
669
+ sid: "AllowEcPublishToAggregateCmdTopic",
670
+ effect: Allow,
671
+ actions: Action("sqs:SendMessage"),
672
+ resources: Resource(arn),
692
673
  },
693
- )
694
- | None => ()
695
- }
674
+ ],
675
+ )->PulumiAws.PolicyDocument.toJsonString
676
+ })
677
+ let _ = PulumiAws.IAM.RolePolicy.make(
678
+ ~name=`${name}-pta-${aggName}`,
679
+ ~args={
680
+ policy: policyJson->Pulumi.Output.asInput,
681
+ role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
682
+ },
696
683
  )
697
- }
684
+ | None => ()
685
+ }
686
+ })
698
687
  }
699
688
 
700
689
  let forPluginHeartbeat: ReventlessCore.Runtime.forComponent<
@@ -1,7 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Aws from "@pulumi/aws";
4
- import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
5
  import * as Pulumi from "@pulumi/pulumi";
7
6
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
@@ -366,41 +365,44 @@ function Make(EventCollectorChannel) {
366
365
  aggregateNameSet[aggName] = undefined;
367
366
  });
368
367
  });
369
- let queueUrlOutputs = Stdlib_Array.filterMap(Object.keys(aggregateNameSet), aggName => context.publishToAggregates[aggName]);
370
- if (queueUrlOutputs.length !== 0) {
371
- let policyJsonOutput = Pulumi.all(queueUrlOutputs).apply(urls => {
372
- let queueArns = urls.filter(url => typeof url === "string").map(url => {
368
+ Object.keys(aggregateNameSet).forEach(aggName => {
369
+ let o = context.publishToAggregates[aggName];
370
+ let urlOutput = o !== undefined ? (
371
+ Pulumi.Output.isInstance(o) ? o : (
372
+ typeof o === "string" ? Pulumi.output(o) : undefined
373
+ )
374
+ ) : undefined;
375
+ if (urlOutput === undefined) {
376
+ return;
377
+ }
378
+ let policyJson = urlOutput.apply(url => {
379
+ let arn;
380
+ if (typeof url === "string") {
373
381
  let match = url.split("/");
374
382
  if (match.length !== 5) {
375
- return url;
383
+ arn = url;
384
+ } else {
385
+ let host = match[2];
386
+ let acct = match[3];
387
+ let name$1 = match[4];
388
+ let region = Stdlib_Option.getOr(host.split(".")[1], "eu-west-1");
389
+ arn = `arn:aws:sqs:` + region + `:` + acct + `:` + name$1;
376
390
  }
377
- let host = match[2];
378
- let acct = match[3];
379
- let name = match[4];
380
- let region = Stdlib_Option.getOr(host.split(".")[1], "eu-west-1");
381
- return `arn:aws:sqs:` + region + `:` + acct + `:` + name;
382
- });
383
- if (queueArns.length === 0) {
384
- return;
385
391
  } else {
386
- return PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `PublishToAggregatesPolicy`, [{
387
- Sid: "AllowEcPublishToAggregateCmdTopics",
388
- Effect: "Allow",
389
- Action: "sqs:SendMessage",
390
- Resource: queueArns
391
- }]));
392
+ arn = "arn:aws:sqs:eu-west-1:000000000000:reventless-unresolved-cmd-topic";
392
393
  }
394
+ return PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + `Pta` + aggName + `Policy`, [{
395
+ Sid: "AllowEcPublishToAggregateCmdTopic",
396
+ Effect: "Allow",
397
+ Action: "sqs:SendMessage",
398
+ Resource: arn
399
+ }]));
393
400
  });
394
- policyJsonOutput.apply(policyOpt => {
395
- if (policyOpt !== undefined) {
396
- new (Aws.iam.RolePolicy)(name + `-publishToAggregates`, {
397
- policy: policyOpt,
398
- role: runtime.parts.lambdaRole.id
399
- });
400
- return;
401
- }
401
+ new (Aws.iam.RolePolicy)(name + `-pta-` + aggName, {
402
+ policy: policyJson,
403
+ role: runtime.parts.lambdaRole.id
402
404
  });
403
- }
405
+ });
404
406
  };
405
407
  let forPluginHeartbeat = (param, connect, memorySizeOpt, timeoutOpt, heartbeat) => {
406
408
  let memorySize = memorySizeOpt !== undefined ? memorySizeOpt : 1024;
@@ -663,13 +663,15 @@ describe("Split mode — empty base fragment", () => {
663
663
 
664
664
  describe("Merged mode — canonical source documents", () => {
665
665
  let assembleCanonicalSourceSdl = (~baseFragment) =>
666
- AppSync_Adapter.stitchWithAwsDirectives(~baseFragment, ~pluginFragments=[])
666
+ AppSync_Adapter.stitchStandaloneWithAwsDirectives(~fragment=baseFragment)
667
667
  ->AppSync_SdlDecorate.stampCanonicalTypes
668
668
 
669
- let emptyBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
669
+ // Mirrors Platform.res's domainBaseFragment: no component fields, one
670
+ // Platform_ping so the Query type is non-empty.
671
+ let domainBaseFragment = ReventlessCore.GraphQL_Stitcher.encode({
670
672
  types: [],
671
673
  mutations: [],
672
- queries: [],
674
+ queries: [" Platform_ping: String"],
673
675
  subscriptions: [],
674
676
  subscriptionSources: [],
675
677
  })
@@ -678,14 +680,17 @@ describe("Merged mode — canonical source documents", () => {
678
680
  ~baseFragment=AppSync_Adapter.injectAwsAuthAll(
679
681
  ReventlessCore.AdminApi.baseFragment(~cloner=false),
680
682
  ~group="Admin",
681
- ~iamFieldNames=ReventlessCore.AdminApi.systemCallerFieldNames,
682
683
  ),
683
684
  )
684
685
 
685
- let relayBaseSourceSdl = assembleCanonicalSourceSdl(~baseFragment=emptyBaseFragment)
686
+ let domainBaseSourceSdl = assembleCanonicalSourceSdl(~baseFragment=domainBaseFragment)
686
687
 
687
- testSync("admin source document carries the global node query", () => {
688
- expect(adminSourceSdl)->toContain("node(id: ID!): Node")
688
+ testSync("no AWS source document carries the global node query (dropped — see plan)", () => {
689
+ expect(adminSourceSdl)->not_->toContain("node(id: ID!): Node")
690
+ expect(domainBaseSourceSdl)->not_->toContain("node(id: ID!): Node")
691
+ // The Node interface and global IDs stay — they are what makes a future
692
+ // node() possible without a schema migration.
693
+ expect(adminSourceSdl)->toContain("interface Node")
689
694
  })
690
695
 
691
696
  testSync("admin source document stamps @canonical on the shared traversal types", () => {
@@ -697,19 +702,22 @@ describe("Merged mode — canonical source documents", () => {
697
702
  )
698
703
  })
699
704
 
700
- testSync("admin source document keeps admin fields and dual-auth stamps", () => {
705
+ testSync("admin source document keeps admin fields and shared-type IAM stamps", () => {
701
706
  expect(adminSourceSdl)->toContain("Platform_Plugin")
702
- expect(adminSourceSdl)->toContain("@aws_iam")
707
+ // Type-level dual-auth on the shared traversal types (stampSharedIamTypes);
708
+ // no FIELD carries @aws_iam anymore — the SigV4 register/deregister surface
709
+ // died with the fragment registry.
710
+ expect(adminSourceSdl)->toContain("type PageInfo @aws_cognito_user_pools @aws_iam")
703
711
  })
704
712
 
705
- testSync("relay-base source document (split-mode Domain source) is node + relay types only", () => {
706
- expect(relayBaseSourceSdl)->toContain("node(id: ID!): Node")
707
- expect(relayBaseSourceSdl)->toContain("interface Node @canonical {")
708
- expect(relayBaseSourceSdl)->toContain("type PageInfo")
713
+ testSync("Domain base source document (split mode) is relay types + Platform_ping only", () => {
714
+ expect(domainBaseSourceSdl)->toContain("Platform_ping: String")
715
+ expect(domainBaseSourceSdl)->toContain("interface Node @canonical {")
716
+ expect(domainBaseSourceSdl)->toContain("type PageInfo")
709
717
  // No admin or plugin fields on the split-mode Domain source.
710
- expect(relayBaseSourceSdl)->not_->toContain("Platform_Plugin")
718
+ expect(domainBaseSourceSdl)->not_->toContain("Platform_Plugin")
711
719
  // No mutations → no CommandResult family on this source.
712
- expect(relayBaseSourceSdl)->not_->toContain("union CommandResult")
720
+ expect(domainBaseSourceSdl)->not_->toContain("union CommandResult")
713
721
  })
714
722
 
715
723
  // Phase 4: the plugin-stack subgraph document pushed to the plugin's own
@@ -607,18 +607,20 @@ globalThis.describe("Split mode — empty base fragment", () => {
607
607
  });
608
608
 
609
609
  globalThis.describe("Merged mode — canonical source documents", () => {
610
- let emptyBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
610
+ let domainBaseFragment = GraphQL_Stitcher$ReventlessCore.encode({
611
611
  types: [],
612
612
  mutations: [],
613
- queries: [],
613
+ queries: [" Platform_ping: String"],
614
614
  subscriptions: [],
615
615
  subscriptionSources: []
616
616
  });
617
- let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin", AdminApi$ReventlessCore.systemCallerFieldNames);
618
- let adminSourceSdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchWithAwsDirectives(baseFragment, []));
619
- let relayBaseSourceSdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchWithAwsDirectives(emptyBaseFragment, []));
620
- globalThis.test("admin source document carries the global node query", () => {
621
- globalThis.expect(adminSourceSdl).toContain("node(id: ID!): Node");
617
+ let baseFragment = AppSync_Adapter$ReventlessAws.injectAwsAuthAll(AdminApi$ReventlessCore.baseFragment(false), "Admin", undefined);
618
+ let adminSourceSdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(baseFragment));
619
+ let domainBaseSourceSdl = AppSync_SdlDecorate$ReventlessAws.stampCanonicalTypes(AppSync_Adapter$ReventlessAws.stitchStandaloneWithAwsDirectives(domainBaseFragment));
620
+ globalThis.test("no AWS source document carries the global node query (dropped — see plan)", () => {
621
+ globalThis.expect(adminSourceSdl).not.toContain("node(id: ID!): Node");
622
+ globalThis.expect(domainBaseSourceSdl).not.toContain("node(id: ID!): Node");
623
+ globalThis.expect(adminSourceSdl).toContain("interface Node");
622
624
  });
623
625
  globalThis.test("admin source document stamps @canonical on the shared traversal types", () => {
624
626
  globalThis.expect(adminSourceSdl).toContain("type PageInfo @aws_cognito_user_pools @aws_iam @canonical {");
@@ -626,16 +628,16 @@ globalThis.describe("Merged mode — canonical source documents", () => {
626
628
  globalThis.expect(adminSourceSdl).toContain("union CommandResult @canonical =");
627
629
  globalThis.expect(adminSourceSdl).toContain("type CommandAccepted @aws_cognito_user_pools @aws_iam @canonical {");
628
630
  });
629
- globalThis.test("admin source document keeps admin fields and dual-auth stamps", () => {
631
+ globalThis.test("admin source document keeps admin fields and shared-type IAM stamps", () => {
630
632
  globalThis.expect(adminSourceSdl).toContain("Platform_Plugin");
631
- globalThis.expect(adminSourceSdl).toContain("@aws_iam");
632
- });
633
- globalThis.test("relay-base source document (split-mode Domain source) is node + relay types only", () => {
634
- globalThis.expect(relayBaseSourceSdl).toContain("node(id: ID!): Node");
635
- globalThis.expect(relayBaseSourceSdl).toContain("interface Node @canonical {");
636
- globalThis.expect(relayBaseSourceSdl).toContain("type PageInfo");
637
- globalThis.expect(relayBaseSourceSdl).not.toContain("Platform_Plugin");
638
- globalThis.expect(relayBaseSourceSdl).not.toContain("union CommandResult");
633
+ globalThis.expect(adminSourceSdl).toContain("type PageInfo @aws_cognito_user_pools @aws_iam");
634
+ });
635
+ globalThis.test("Domain base source document (split mode) is relay types + Platform_ping only", () => {
636
+ globalThis.expect(domainBaseSourceSdl).toContain("Platform_ping: String");
637
+ globalThis.expect(domainBaseSourceSdl).toContain("interface Node @canonical {");
638
+ globalThis.expect(domainBaseSourceSdl).toContain("type PageInfo");
639
+ globalThis.expect(domainBaseSourceSdl).not.toContain("Platform_Plugin");
640
+ globalThis.expect(domainBaseSourceSdl).not.toContain("union CommandResult");
639
641
  });
640
642
  let pluginFragment = GraphQL_Stitcher$ReventlessCore.encode({
641
643
  types: [