@reventlessdev/reventless-aws 3.0.0-alpha.197 → 3.0.0-alpha.199

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 (28) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/package.json +7 -7
  3. package/src/Platform.res +294 -44
  4. package/src/Platform.res.mjs +420 -94
  5. package/src/adapter/Api/ApiFragmentDeregistration.res +137 -0
  6. package/src/adapter/Api/ApiFragmentDeregistration.res.mjs +108 -0
  7. package/src/adapter/Api/CommandSubscriptionResolvers_AppSync.res +4 -3
  8. package/src/adapter/Api/Platform_ApiFragments_Lambda.res +222 -0
  9. package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +202 -0
  10. package/src/adapter/Api/Platform_UIFragments_Lambda.res +27 -17
  11. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +13 -13
  12. package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +2 -1
  13. package/src/adapter/QueryDb/QueryDbBackend.res +1 -1
  14. package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +235 -9
  15. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +4 -0
  16. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +5 -0
  17. package/src/components/Api/AppSync_Adapter.res +36 -59
  18. package/src/components/Api/AppSync_Adapter.res.mjs +18 -74
  19. package/src/components/Api/AppSync_SdlDecorate.res +189 -0
  20. package/src/components/Api/AppSync_SdlDecorate.res.mjs +125 -0
  21. package/src/plugin/runtime/PluginRuntime_Builder.res +107 -2
  22. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +70 -7
  23. package/src/util/Util_AppSync_Caller.res +12 -4
  24. package/src/util/Util_AppSync_Caller.res.mjs +9 -3
  25. package/tests/AppSync_AdapterTest.res +6 -0
  26. package/tests/AppSync_AdapterTest.res.mjs +12 -6
  27. package/tests/AppSync_SdlDecorateTest.res +157 -0
  28. package/tests/AppSync_SdlDecorateTest.res.mjs +147 -0
@@ -4,7 +4,6 @@ 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_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
8
7
  import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
9
8
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
10
9
  import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
@@ -21,6 +20,7 @@ import * as Auth_Cognito$ReventlessAws from "../../adapter/Auth/Auth_Cognito.res
21
20
  import * as AppSync_Error$ReventlessAws from "../../errors/AppSync_Error.res.mjs";
22
21
  import * as DynamoDb_DocumentClient$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/DynamoDb_DocumentClient.res.mjs";
23
22
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
23
+ import * as AppSync_SdlDecorate$ReventlessAws from "./AppSync_SdlDecorate.res.mjs";
24
24
  import * as GraphQL_FragmentGenerator$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_FragmentGenerator.res.mjs";
25
25
 
26
26
  let log = Logger$ReventlessCore.fromEnv();
@@ -236,17 +236,6 @@ function _stampTypeDualAuth(decl) {
236
236
  }
237
237
  }
238
238
 
239
- let sharedIamTypeNames = [
240
- "PageInfo",
241
- "CommandAccepted",
242
- "CommandRejected",
243
- "CommandPending"
244
- ];
245
-
246
- function stampSharedIamTypes(sdl) {
247
- return Stdlib_Array.reduce(sharedIamTypeNames, sdl, (acc, name) => acc.replace(`type ` + name + ` {`, `type ` + name + ` @aws_cognito_user_pools @aws_iam {`));
248
- }
249
-
250
239
  function injectAwsAuth(fragment, mutationEntries, queryEntries) {
251
240
  let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
252
241
  let iamFields = {};
@@ -341,70 +330,23 @@ function injectAwsAuth(fragment, mutationEntries, queryEntries) {
341
330
  return decl;
342
331
  }
343
332
  });
344
- let encoded = JSON.stringify(Object.fromEntries([
345
- [
346
- "types",
347
- augmentedTypes.map(prim => prim)
348
- ],
349
- [
350
- "mutations",
351
- augmentedMutations.map(prim => prim)
352
- ],
353
- [
354
- "queries",
355
- augmentedQueries.map(prim => prim)
356
- ],
357
- [
358
- "subscriptions",
359
- parts.subscriptions.map(prim => prim)
360
- ]
361
- ]));
362
- return {
363
- encoded: encoded,
364
- protocol: "graphql"
365
- };
333
+ return GraphQL_Stitcher$ReventlessCore.encode({
334
+ types: augmentedTypes,
335
+ mutations: augmentedMutations,
336
+ queries: augmentedQueries,
337
+ subscriptions: parts.subscriptions,
338
+ subscriptionSources: parts.subscriptionSources
339
+ });
366
340
  }
367
341
 
368
342
  function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
369
343
  let iamFieldNames = iamFieldNamesOpt !== undefined ? iamFieldNamesOpt : [];
370
- let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
371
- let augmentedMutations = parts.mutations.map(field => {
372
- if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
373
- return field + `\n ` + _formatDualAuthDirective([group]);
374
- } else {
375
- return field + `\n @aws_auth(cognito_groups: ["` + group + `"])`;
376
- }
377
- });
378
- let augmentedQueries = parts.queries.map(field => {
379
- if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
380
- return field + ` ` + _formatDualAuthDirective([group]);
381
- } else {
382
- return field + ` @aws_auth(cognito_groups: ["` + group + `"])`;
383
- }
384
- });
385
- let augmentedSubscriptions = parts.subscriptions.map(field => field + `\n @aws_auth(cognito_groups: ["` + group + `"])`);
386
- let encoded = JSON.stringify(Object.fromEntries([
387
- [
388
- "types",
389
- parts.types.map(prim => prim)
390
- ],
391
- [
392
- "mutations",
393
- augmentedMutations.map(prim => prim)
394
- ],
395
- [
396
- "queries",
397
- augmentedQueries.map(prim => prim)
398
- ],
399
- [
400
- "subscriptions",
401
- augmentedSubscriptions.map(prim => prim)
402
- ]
403
- ]));
404
- return {
405
- encoded: encoded,
406
- protocol: "graphql"
407
- };
344
+ return AppSync_SdlDecorate$ReventlessAws.injectAwsAuthAll(fragment, group, iamFieldNames);
345
+ }
346
+
347
+ function stitchWithAwsDirectives(baseFragment, pluginFragments) {
348
+ let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(baseFragment, pluginFragments);
349
+ return AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes(AppSync_SdlDecorate$ReventlessAws.injectAwsSubscribe(GraphQL_Stitcher$ReventlessCore.stitch(baseFragment, pluginFragments), sources));
408
350
  }
409
351
 
410
352
  function makeApiResource(name, opts) {
@@ -444,7 +386,7 @@ function generateFragment(mutationEntries, queryEntries) {
444
386
 
445
387
  function updateSchema(api, baseFragment, pluginFragments) {
446
388
  let augmentedBaseFragment = injectAwsAuthAll(baseFragment, "Admin", undefined);
447
- let sdl = stampSharedIamTypes(GraphQL_Stitcher$ReventlessCore.stitch(augmentedBaseFragment, pluginFragments));
389
+ let sdl = stitchWithAwsDirectives(augmentedBaseFragment, pluginFragments);
448
390
  let resultPromise = {
449
391
  contents: Promise.resolve()
450
392
  };
@@ -456,6 +398,8 @@ function updateSchema(api, baseFragment, pluginFragments) {
456
398
  return resultPromise.contents;
457
399
  }
458
400
 
401
+ let stampSharedIamTypes = AppSync_SdlDecorate$ReventlessAws.stampSharedIamTypes;
402
+
459
403
  export {
460
404
  log,
461
405
  sha256Hex,
@@ -474,10 +418,10 @@ export {
474
418
  _formatDualAuthDirective,
475
419
  _typeDeclName,
476
420
  _stampTypeDualAuth,
477
- sharedIamTypeNames,
478
421
  stampSharedIamTypes,
479
422
  injectAwsAuth,
480
423
  injectAwsAuthAll,
424
+ stitchWithAwsDirectives,
481
425
  makeApiResource,
482
426
  generateFragment,
483
427
  updateSchema,
@@ -0,0 +1,189 @@
1
+ // AppSync_SdlDecorate.res
2
+ // Pure SDL decoration for the AppSync dialect — no AWS SDK, no Pulumi.
3
+ //
4
+ // Core emits provider-neutral SDL plus structured `subscriptionSource`
5
+ // metadata (which mutation(s) feed each subscription field). This module adds
6
+ // the AppSync-specific `@aws_subscribe(mutations: [...])` directive onto the
7
+ // STITCHED schema at push time — the additive counterpart of the neutral
8
+ // emission, mirroring how `injectAwsAuthAll`/`stampSharedIamTypes` decorate
9
+ // auth. Runtime-pure so the bundled Lambda entry points
10
+ // (AdminEventCollectorEntryPoint.mjs) can import it without dragging
11
+ // deploy-time dependencies into the runtime module graph.
12
+
13
+ /**
14
+ Append `@aws_subscribe(mutations: [...])` to every field of the SDL's
15
+ `type Subscription { … }` block that has a source mapping. Fields without a
16
+ mapping (e.g. Source A event-stream fields, pushed via the Events API) are
17
+ left untouched. No-op when the SDL has no Subscription block or `sources` is
18
+ empty.
19
+ */
20
+ let injectAwsSubscribe = (
21
+ sdl: string,
22
+ ~sources: array<ReventlessCore.GraphQL_Stitcher.subscriptionSource>,
23
+ ): string => {
24
+ if sources->Array.length == 0 {
25
+ sdl
26
+ } else {
27
+ let sourceByField: Dict.t<array<string>> = Dict.make()
28
+ sources->Array.forEach(source => sourceByField->Dict.set(source.field, source.mutations))
29
+ switch sdl->String.indexOfOpt("type Subscription") {
30
+ | None => sdl
31
+ | Some(blockStart) =>
32
+ let before = sdl->String.slice(~start=0, ~end=blockStart)
33
+ let rest = sdl->String.slice(~start=blockStart)
34
+ switch rest->String.indexOfOpt("}") {
35
+ | None => sdl
36
+ | Some(closeIdx) =>
37
+ let block = rest->String.slice(~start=0, ~end=closeIdx)
38
+ let after = rest->String.slice(~start=closeIdx)
39
+ let decorated =
40
+ block
41
+ ->String.split("\n")
42
+ ->Array.map(line => {
43
+ // `name(args): T` → the arg list is stripped; `name: T` (no args)
44
+ // leaves a trailing colon on the token — drop it (same rule as
45
+ // GraphQL_Stitcher.rootTypeFieldNames).
46
+ let name = ReventlessCore.GraphQL_Stitcher.extractLeadingName(line)
47
+ let name = name->String.endsWith(":")
48
+ ? name->String.slice(~start=0, ~end=name->String.length - 1)
49
+ : name
50
+ switch sourceByField->Dict.get(name) {
51
+ | Some(mutations) =>
52
+ let list = mutations->Array.map(m => `"${m}"`)->Array.join(", ")
53
+ `${line}\n @aws_subscribe(mutations: [${list}])`
54
+ | None => line
55
+ }
56
+ })
57
+ ->Array.join("\n")
58
+ before ++ decorated ++ after
59
+ }
60
+ }
61
+ }
62
+ }
63
+
64
+ // ── Auth decoration (runtime-pure) ───────────────────────────────────────────
65
+ // Moved here from AppSync_Adapter so the reactive single-writer push (the
66
+ // bundled AdminEventCollector Lambda) can decorate the admin base identically to
67
+ // the deploy path without dragging Pulumi into the runtime graph. AppSync_Adapter
68
+ // delegates its `injectAwsAuthAll` / `stampSharedIamTypes` to these definitions.
69
+
70
+ // Multi-auth directive for a field/type that must accept BOTH Cognito and IAM.
71
+ // `groups=Some([...])` preserves Cognito group gating; `groups=None` keeps it
72
+ // open to any authenticated Cognito user. `@aws_iam` admits the deploy-time
73
+ // SigV4 system caller.
74
+ let formatDualAuthDirective = (groups: option<array<string>>): string => {
75
+ let cognito = switch groups {
76
+ | Some(g) =>
77
+ let quoted = g->Array.map(x => `"${x}"`)->Array.join(", ")
78
+ `@aws_cognito_user_pools(cognito_groups: [${quoted}])`
79
+ | None => `@aws_cognito_user_pools`
80
+ }
81
+ `${cognito} @aws_iam`
82
+ }
83
+
84
+ // Injects @aws_auth with the given group on ALL mutation, query, and subscription
85
+ // fields in a fragment. `~iamFieldNames` opts the named mutation/query fields into
86
+ // deploy-time IAM (dual-auth): those fields emit
87
+ // `@aws_cognito_user_pools(cognito_groups: ["<group>"]) @aws_iam` instead of the
88
+ // single-mode `@aws_auth(...)`, keeping the same Cognito gating while also admitting
89
+ // the SigV4 system caller. Subscriptions are never IAM-marked.
90
+ let injectAwsAuthAll = (
91
+ fragment: Reventless.Plugin.apiSchemaFragment,
92
+ ~group: string,
93
+ ~iamFieldNames: array<string>=[],
94
+ ): Reventless.Plugin.apiSchemaFragment => {
95
+ let parts = ReventlessCore.GraphQL_Stitcher.decode(fragment)
96
+ let isIam = (field: string): bool =>
97
+ iamFieldNames->Array.includes(ReventlessCore.GraphQL_Stitcher.extractLeadingName(field))
98
+
99
+ let augmentedMutations = parts.mutations->Array.map(field =>
100
+ isIam(field)
101
+ ? `${field}\n ${formatDualAuthDirective(Some([group]))}`
102
+ : `${field}\n @aws_auth(cognito_groups: ["${group}"])`
103
+ )
104
+ let augmentedQueries = parts.queries->Array.map(field =>
105
+ isIam(field)
106
+ ? `${field} ${formatDualAuthDirective(Some([group]))}`
107
+ : `${field} @aws_auth(cognito_groups: ["${group}"])`
108
+ )
109
+ let augmentedSubscriptions = parts.subscriptions->Array.map(field =>
110
+ `${field}\n @aws_auth(cognito_groups: ["${group}"])`
111
+ )
112
+
113
+ ReventlessCore.GraphQL_Stitcher.encode({
114
+ ...parts,
115
+ mutations: augmentedMutations,
116
+ queries: augmentedQueries,
117
+ subscriptions: augmentedSubscriptions,
118
+ })
119
+ }
120
+
121
+ // Shared traversal types every callable surface reaches — `PageInfo` (relay
122
+ // connections, injected by the stitcher) and the `CommandResult` members
123
+ // (mutation returns, deduped across fragments by the stitcher). Stamped once on
124
+ // the ASSEMBLED SDL: per-fragment stamping would race the stitcher's first-wins
125
+ // dedupe against unstamped sibling copies.
126
+ let sharedIamTypeNames = ["PageInfo", "CommandAccepted", "CommandRejected", "CommandPending"]
127
+
128
+ let stampSharedIamTypes = (sdl: string): string =>
129
+ sharedIamTypeNames->Array.reduce(sdl, (acc, name) =>
130
+ acc->String.replace(`type ${name} {`, `type ${name} @aws_cognito_user_pools @aws_iam {`)
131
+ )
132
+
133
+ // ── Reactive push planner (runtime-pure) ─────────────────────────────────────
134
+ // The AWS-decorated counterpart of core `GraphQL_PushPlanner.planPushes`: given
135
+ // the fragments currently in the ApiFragmentRegistry (each tagged with its target
136
+ // API, as the bare string "Domain" / "Platform"), it produces one fully
137
+ // AppSync-decorated SDL push plan per API. It reuses the core planner for the
138
+ // neutral base-selection + stitch, then applies the AppSync dialect exactly as the
139
+ // deploy path's `stitchWithAwsDirectives`:
140
+ // - the admin base is auth-decorated (group "Admin" + dual-auth on the system-
141
+ // caller field names) BEFORE stitching, so the Platform/unified base carries
142
+ // @aws_auth/@aws_iam (the empty Domain-split base needs none);
143
+ // - @aws_subscribe is injected from the neutral subscriptionSources metadata;
144
+ // - the shared traversal types are stamped once on the assembled SDL.
145
+ // Plugin fragments already carry their own per-field @aws_auth (baked at
146
+ // generateFragment time), so they stitch in as-is. `api` comes back as the bare
147
+ // string "PlatformApi" / "DomainApi" for the mjs to map onto the AppSync ids.
148
+ type targetedFragmentInput = {
149
+ encoded: string,
150
+ protocol: string,
151
+ // "Domain" | "Platform" — the bare-string runtime form of Reventless.Plugin.apiTarget.
152
+ target: string,
153
+ }
154
+
155
+ type awsPushPlan = {
156
+ // "PlatformApi" | "DomainApi" — the bare-string runtime form of GraphQL_PushPlanner.planTarget.
157
+ api: string,
158
+ sdl: string,
159
+ }
160
+
161
+ let planAwsPushes = (
162
+ ~rawAdminBase: Reventless.Plugin.apiSchemaFragment,
163
+ ~iamFieldNames: array<string>,
164
+ ~fragments: array<targetedFragmentInput>,
165
+ ~splitApi: bool,
166
+ ): array<awsPushPlan> => {
167
+ let authBase = injectAwsAuthAll(rawAdminBase, ~group="Admin", ~iamFieldNames)
168
+ let targeted = fragments->Array.map((f): ReventlessCore.GraphQL_PushPlanner.targetedFragment => {
169
+ fragment: {encoded: f.encoded, protocol: f.protocol},
170
+ target: switch f.target {
171
+ | "Platform" => Platform
172
+ | _ => Domain
173
+ },
174
+ })
175
+ let allFrags = targeted->Array.map(t => t.fragment)
176
+ let sources = ReventlessCore.GraphQL_Stitcher.collectSubscriptionSources(
177
+ ~baseFragment=rawAdminBase,
178
+ ~pluginFragments=allFrags,
179
+ )
180
+ ReventlessCore.GraphQL_PushPlanner.planPushes(~adminBase=authBase, ~fragments=targeted, ~splitApi)
181
+ ->Array.map(plan => {
182
+ let sdl = plan.sdl->injectAwsSubscribe(~sources)->stampSharedIamTypes
183
+ let api = switch plan.api {
184
+ | PlatformApi => "PlatformApi"
185
+ | DomainApi => "DomainApi"
186
+ }
187
+ {api, sdl}
188
+ })
189
+ }
@@ -0,0 +1,125 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
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
+
8
+ function injectAwsSubscribe(sdl, sources) {
9
+ if (sources.length === 0) {
10
+ return sdl;
11
+ }
12
+ let sourceByField = {};
13
+ sources.forEach(source => {
14
+ sourceByField[source.field] = source.mutations;
15
+ });
16
+ let blockStart = Stdlib_String.indexOfOpt(sdl, "type Subscription");
17
+ if (blockStart === undefined) {
18
+ return sdl;
19
+ }
20
+ let before = sdl.slice(0, blockStart);
21
+ let rest = sdl.slice(blockStart);
22
+ let closeIdx = Stdlib_String.indexOfOpt(rest, "}");
23
+ if (closeIdx === undefined) {
24
+ return sdl;
25
+ }
26
+ let block = rest.slice(0, closeIdx);
27
+ let after = rest.slice(closeIdx);
28
+ let decorated = block.split("\n").map(line => {
29
+ let name = GraphQL_Stitcher$ReventlessCore.extractLeadingName(line);
30
+ let name$1 = name.endsWith(":") ? name.slice(0, name.length - 1 | 0) : name;
31
+ let mutations = sourceByField[name$1];
32
+ if (mutations === undefined) {
33
+ return line;
34
+ }
35
+ let list = mutations.map(m => `"` + m + `"`).join(", ");
36
+ return line + `\n @aws_subscribe(mutations: [` + list + `])`;
37
+ }).join("\n");
38
+ return before + decorated + after;
39
+ }
40
+
41
+ function formatDualAuthDirective(groups) {
42
+ let cognito;
43
+ if (groups !== undefined) {
44
+ let quoted = groups.map(x => `"` + x + `"`).join(", ");
45
+ cognito = `@aws_cognito_user_pools(cognito_groups: [` + quoted + `])`;
46
+ } else {
47
+ cognito = `@aws_cognito_user_pools`;
48
+ }
49
+ return cognito + ` @aws_iam`;
50
+ }
51
+
52
+ function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
53
+ let iamFieldNames = iamFieldNamesOpt !== undefined ? iamFieldNamesOpt : [];
54
+ let parts = GraphQL_Stitcher$ReventlessCore.decode(fragment);
55
+ let augmentedMutations = parts.mutations.map(field => {
56
+ if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
57
+ return field + `\n ` + formatDualAuthDirective([group]);
58
+ } else {
59
+ return field + `\n @aws_auth(cognito_groups: ["` + group + `"])`;
60
+ }
61
+ });
62
+ let augmentedQueries = parts.queries.map(field => {
63
+ if (iamFieldNames.includes(GraphQL_Stitcher$ReventlessCore.extractLeadingName(field))) {
64
+ return field + ` ` + formatDualAuthDirective([group]);
65
+ } else {
66
+ return field + ` @aws_auth(cognito_groups: ["` + group + `"])`;
67
+ }
68
+ });
69
+ let augmentedSubscriptions = parts.subscriptions.map(field => field + `\n @aws_auth(cognito_groups: ["` + group + `"])`);
70
+ return GraphQL_Stitcher$ReventlessCore.encode({
71
+ types: parts.types,
72
+ mutations: augmentedMutations,
73
+ queries: augmentedQueries,
74
+ subscriptions: augmentedSubscriptions,
75
+ subscriptionSources: parts.subscriptionSources
76
+ });
77
+ }
78
+
79
+ let sharedIamTypeNames = [
80
+ "PageInfo",
81
+ "CommandAccepted",
82
+ "CommandRejected",
83
+ "CommandPending"
84
+ ];
85
+
86
+ function stampSharedIamTypes(sdl) {
87
+ return Stdlib_Array.reduce(sharedIamTypeNames, sdl, (acc, name) => acc.replace(`type ` + name + ` {`, `type ` + name + ` @aws_cognito_user_pools @aws_iam {`));
88
+ }
89
+
90
+ function planAwsPushes(rawAdminBase, iamFieldNames, fragments, splitApi) {
91
+ let authBase = injectAwsAuthAll(rawAdminBase, "Admin", iamFieldNames);
92
+ let targeted = fragments.map(f => {
93
+ let match = f.target;
94
+ let tmp = match === "Platform" ? "Platform" : "Domain";
95
+ return {
96
+ fragment: {
97
+ encoded: f.encoded,
98
+ protocol: f.protocol
99
+ },
100
+ target: tmp
101
+ };
102
+ });
103
+ let allFrags = targeted.map(t => t.fragment);
104
+ let sources = GraphQL_Stitcher$ReventlessCore.collectSubscriptionSources(rawAdminBase, allFrags);
105
+ return GraphQL_PushPlanner$ReventlessCore.planPushes(authBase, targeted, splitApi).map(plan => {
106
+ let sdl = stampSharedIamTypes(injectAwsSubscribe(plan.sdl, sources));
107
+ let match = plan.api;
108
+ let api;
109
+ api = match === "DomainApi" ? "DomainApi" : "PlatformApi";
110
+ return {
111
+ api: api,
112
+ sdl: sdl
113
+ };
114
+ });
115
+ }
116
+
117
+ export {
118
+ injectAwsSubscribe,
119
+ formatDualAuthDirective,
120
+ injectAwsAuthAll,
121
+ sharedIamTypeNames,
122
+ stampSharedIamTypes,
123
+ planAwsPushes,
124
+ }
125
+ /* GraphQL_Stitcher-ReventlessCore Not a pure module */
@@ -12,6 +12,14 @@ type adminConfig = {
12
12
  schedulerQueueName: option<Pulumi.Output.t<string>>,
13
13
  appSyncApiId: option<Pulumi.Output.t<string>>,
14
14
  clonerEnabled: bool,
15
+ // Reactive ApiFragmentRegistry single writer (2e), admin EventCollector only:
16
+ // the Platform AppSync id (split mode; unified → same as appSyncApiId), the
17
+ // ApiFragments StateViewSlice table it scans, the admin DCB command-topic FIFO
18
+ // URL it dispatches RecordApiFragmentPush to, and the split/unified mode flag.
19
+ platformApiId: option<Pulumi.Output.t<string>>,
20
+ apiFragmentRegistryTableName: option<Pulumi.Output.t<string>>,
21
+ adminDcbCmdTopicUrl: option<Pulumi.Output.t<string>>,
22
+ splitApi: bool,
15
23
  }
16
24
 
17
25
  let configRef: ref<adminConfig> = ref({
@@ -23,6 +31,10 @@ let configRef: ref<adminConfig> = ref({
23
31
  schedulerQueueName: None,
24
32
  appSyncApiId: None,
25
33
  clonerEnabled: false,
34
+ platformApiId: None,
35
+ apiFragmentRegistryTableName: None,
36
+ adminDcbCmdTopicUrl: None,
37
+ splitApi: false,
26
38
  })
27
39
 
28
40
  type sliceModulePaths = {
@@ -105,6 +117,10 @@ let registerConfig = (
105
117
  ~schedulerQueueName=?,
106
118
  ~appSyncApiId=?,
107
119
  ~clonerEnabled=false,
120
+ ~platformApiId=?,
121
+ ~apiFragmentRegistryTableName=?,
122
+ ~adminDcbCmdTopicUrl=?,
123
+ ~splitApi=false,
108
124
  (),
109
125
  ) =>
110
126
  configRef := {
@@ -116,6 +132,10 @@ let registerConfig = (
116
132
  schedulerQueueName,
117
133
  appSyncApiId,
118
134
  clonerEnabled,
135
+ platformApiId,
136
+ apiFragmentRegistryTableName,
137
+ adminDcbCmdTopicUrl,
138
+ splitApi,
119
139
  }
120
140
 
121
141
  // PluginRuntime_Builder is a functor so that the caller can inject the EventCollectorChannel
@@ -155,13 +175,15 @@ module Make = (
155
175
  // Stable JSON literal — matches Reventless.Plugin.pluginDefinitionSchema
156
176
  // (optional fields encoded as null via js_nullable).
157
177
  let fakePluginDefinitionJson =
158
- `{"id":"Admin@INTERNAL","name":"Admin","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"uiFragments":null,"structure":null}`->Pulumi.Output.make
178
+ `{"id":"Admin@INTERNAL","name":"Admin","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"structure":null}`->Pulumi.Output.make
159
179
  let adminEpEventTopicArn = switch config.eventTopicArn {
160
180
  | Some(arn) => arn
161
181
  | None => Pulumi.Output.make("NOT_AVAILABLE")
162
182
  }
163
183
  {
164
184
  pluginDefinitionJson: fakePluginDefinitionJson,
185
+ // Admin ships no UI-fragment manifest.
186
+ uiFragmentsJson: "null"->Pulumi.Output.make,
165
187
  extensionPoints: [
166
188
  {
167
189
  specModule: ReventlessCore.Plugin_Helpers.adminPluginExtensionPointSpecModule,
@@ -297,6 +319,9 @@ module Make = (
297
319
  config.appSyncApiId->outputOrPlaceholder->Obj.magic,
298
320
  epEventTopicArnsOutput->Pulumi.Output.asInput->Obj.magic,
299
321
  config.pluginSchemaPersistenceTableName->outputOrPlaceholder->Obj.magic,
322
+ config.platformApiId->outputOrPlaceholder->Obj.magic,
323
+ config.apiFragmentRegistryTableName->outputOrPlaceholder->Obj.magic,
324
+ config.adminDcbCmdTopicUrl->outputOrPlaceholder->Obj.magic,
300
325
  ])
301
326
  ->Pulumi.Output.apply(values => {
302
327
  let queueUrl = values->Array.getUnsafe(0)
@@ -309,6 +334,9 @@ module Make = (
309
334
  let appSyncApiId = values->Array.getUnsafe(7)
310
335
  let epEventTopicArns: array<string> = Obj.magic(values->Array.getUnsafe(8))
311
336
  let schemaPersistenceTable = values->Array.getUnsafe(9)
337
+ let platformApiId = values->Array.getUnsafe(10)
338
+ let apiFragmentRegistryTable = values->Array.getUnsafe(11)
339
+ let adminDcbCmdTopicUrl = values->Array.getUnsafe(12)
312
340
 
313
341
  let dict = Dict.make()
314
342
  dict->Dict.set("queueUrl", JSON.Encode.string(queueUrl))
@@ -320,6 +348,12 @@ module Make = (
320
348
  JSON.Encode.string(schemaPersistenceTable),
321
349
  )
322
350
  dict->Dict.set("appSyncApiId", JSON.Encode.string(appSyncApiId))
351
+ // Reactive ApiFragmentRegistry single writer (2e) — placeholders for
352
+ // plugin ECs / all-at-once platforms disable it in the mjs.
353
+ dict->Dict.set("platformApiId", JSON.Encode.string(platformApiId))
354
+ dict->Dict.set("apiFragmentRegistryTableName", JSON.Encode.string(apiFragmentRegistryTable))
355
+ dict->Dict.set("adminDcbCmdTopicUrl", JSON.Encode.string(adminDcbCmdTopicUrl))
356
+ dict->Dict.set("splitApi", JSON.Encode.bool(config.splitApi))
323
357
  dict->Dict.set("clonerEnabled", JSON.Encode.bool(config.clonerEnabled))
324
358
  dict->Dict.set("schedulerRoleArn", JSON.Encode.string(schedRoleArn))
325
359
  dict->Dict.set("schedulerQueueArn", JSON.Encode.string(schedQueueArn))
@@ -480,9 +514,15 @@ module Make = (
480
514
  // is identity at runtime, so the Output unwraps correctly inside the
481
515
  // Lambda Function args.
482
516
  let bundleOutput =
483
- context.pluginDefinitionJson->Pulumi.Output.apply(pluginDefinitionJson => {
517
+ (context.pluginDefinitionJson, context.uiFragmentsJson)
518
+ ->Pulumi.Output.all2
519
+ ->Pulumi.Output.apply(((pluginDefinitionJson, uiFragmentsJson)) => {
484
520
  let extraStringAssets = Dict.make()
485
521
  extraStringAssets->Dict.set("pluginDefinition.json", pluginDefinitionJson)
522
+ // The UI-fragment manifest no longer rides pluginDefinition — ship it as
523
+ // its own asset ("null" for plugins without a UI) so the bundled Connect
524
+ // extension can emit RegisterUiFragment in the handshake answer.
525
+ extraStringAssets->Dict.set("uiFragments.json", uiFragmentsJson)
486
526
  Util_Bundle.buildCodeArchive(
487
527
  ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs",
488
528
  ~packageDirs,
@@ -632,6 +672,71 @@ module Make = (
632
672
  )
633
673
  | None => ()
634
674
  }
675
+
676
+ // 2e: the reactive ApiFragmentRegistry single writer scans the ApiFragments
677
+ // StateViewSlice table to re-fold the registry on each ApiFragment* event.
678
+ // The table is owned by the admin DcbBuilder, so grant Scan explicitly.
679
+ switch config.apiFragmentRegistryTableName {
680
+ | Some(tableOutput) =>
681
+ let policyJson =
682
+ tableOutput->Pulumi.Output.apply(tableName =>
683
+ PulumiAws.PolicyDocument.make(
684
+ ~id=`${name}ApiFragmentsScanPolicy`,
685
+ ~statements=[
686
+ {
687
+ sid: "AllowAdminScanApiFragments",
688
+ effect: Allow,
689
+ actions: Actions(["dynamodb:Scan"]),
690
+ resources: Resource("arn:aws:dynamodb:*:*:table/" ++ tableName),
691
+ },
692
+ ],
693
+ )->PulumiAws.PolicyDocument.toJsonString
694
+ )
695
+ let _ = PulumiAws.IAM.RolePolicy.make(
696
+ ~name=`${name}-apiFragmentsScan`,
697
+ ~args={
698
+ policy: policyJson->Pulumi.Output.asInput,
699
+ role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
700
+ },
701
+ )
702
+ | None => ()
703
+ }
704
+
705
+ // 2e: the reactive push writes the outcome back with RecordApiFragmentPush,
706
+ // dispatched onto the admin DCB command-topic FIFO queue — grant
707
+ // sqs:SendMessage on it (queue URL → ARN, same derivation as
708
+ // publishToAggregates below).
709
+ switch config.adminDcbCmdTopicUrl {
710
+ | Some(urlOutput) =>
711
+ let policyJson =
712
+ urlOutput->Pulumi.Output.apply(url => {
713
+ let arn = switch url->String.split("/") {
714
+ | [_, _, host, acct, qn] =>
715
+ let region = host->String.split(".")->Array.get(1)->Option.getOr("eu-west-1")
716
+ `arn:aws:sqs:${region}:${acct}:${qn}`
717
+ | _ => url
718
+ }
719
+ PulumiAws.PolicyDocument.make(
720
+ ~id=`${name}AdminDcbSendPolicy`,
721
+ ~statements=[
722
+ {
723
+ sid: "AllowAdminDispatchRecordApiFragmentPush",
724
+ effect: Allow,
725
+ actions: Actions(["sqs:SendMessage"]),
726
+ resources: Resource(arn),
727
+ },
728
+ ],
729
+ )->PulumiAws.PolicyDocument.toJsonString
730
+ })
731
+ let _ = PulumiAws.IAM.RolePolicy.make(
732
+ ~name=`${name}-adminDcbSend`,
733
+ ~args={
734
+ policy: policyJson->Pulumi.Output.asInput,
735
+ role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
736
+ },
737
+ )
738
+ | None => ()
739
+ }
635
740
  }
636
741
 
637
742
  // Plugin EC sqs:SendMessage grants on the aggregate / StateChangeSlice