@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.
- package/CHANGELOG.md +22 -0
- package/package.json +7 -7
- package/src/Platform.res +294 -44
- package/src/Platform.res.mjs +420 -94
- package/src/adapter/Api/ApiFragmentDeregistration.res +137 -0
- package/src/adapter/Api/ApiFragmentDeregistration.res.mjs +108 -0
- package/src/adapter/Api/CommandSubscriptionResolvers_AppSync.res +4 -3
- package/src/adapter/Api/Platform_ApiFragments_Lambda.res +222 -0
- package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +202 -0
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +27 -17
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +13 -13
- package/src/adapter/CommandGenerator/CommandGeneratorResolvers_AppSync.res +2 -1
- package/src/adapter/QueryDb/QueryDbBackend.res +1 -1
- package/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs +235 -9
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +4 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +5 -0
- package/src/components/Api/AppSync_Adapter.res +36 -59
- package/src/components/Api/AppSync_Adapter.res.mjs +18 -74
- package/src/components/Api/AppSync_SdlDecorate.res +189 -0
- package/src/components/Api/AppSync_SdlDecorate.res.mjs +125 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res +107 -2
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +70 -7
- package/src/util/Util_AppSync_Caller.res +12 -4
- package/src/util/Util_AppSync_Caller.res.mjs +9 -3
- package/tests/AppSync_AdapterTest.res +6 -0
- package/tests/AppSync_AdapterTest.res.mjs +12 -6
- package/tests/AppSync_SdlDecorateTest.res +157 -0
- 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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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 =
|
|
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,"
|
|
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
|
|
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
|