@reventlessdev/reventless-aws 3.0.0-alpha.196 → 3.0.0-alpha.198
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/README.md +81 -54
- package/package.json +12 -12
- package/src/Platform.res +290 -44
- package/src/Platform.res.mjs +416 -90
- 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 +212 -0
- package/src/adapter/Api/Platform_ApiFragments_Lambda.res.mjs +202 -0
- package/src/adapter/Api/Platform_UIFragments_Lambda.res +8 -8
- package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +11 -11
- 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 +118 -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 = {
|
|
@@ -48,6 +60,17 @@ let registerStateChangeSliceSpec = (~specPath: string, ~behaviorPath: string) =>
|
|
|
48
60
|
let _ = registeredSliceModulePaths->Array.push({specPath, behaviorPath})
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
/**
|
|
64
|
+
Redundant with the seams that populate `dcbConfigRef` automatically:
|
|
65
|
+
`pluginName` is set by `registerPluginName` (called from `Plugin_Builder.make`
|
|
66
|
+
when the plugin builds), the DCB table name by `registerDcbTableName`, and slice
|
|
67
|
+
module paths by `registerStateChangeSliceSpec` (from each slice's `moduleUrl`).
|
|
68
|
+
No deploy program needs to call this. Kept only so any out-of-tree caller still
|
|
69
|
+
compiles; slated for removal.
|
|
70
|
+
*/
|
|
71
|
+
@deprecated(
|
|
72
|
+
"pluginName is auto-registered by Plugin_Builder.make; use registerDcbTableName / registerStateChangeSliceSpec for the other fields. This call is unnecessary and will be removed."
|
|
73
|
+
)
|
|
51
74
|
let registerDcbConfig = (~pluginName, ~dcbTableName=?, ~stateChangeSliceModulePaths=[], ()) => {
|
|
52
75
|
dcbConfigRef := {pluginName, dcbTableName, stateChangeSliceModulePaths}
|
|
53
76
|
stateChangeSliceModulePaths->Array.length
|
|
@@ -94,6 +117,10 @@ let registerConfig = (
|
|
|
94
117
|
~schedulerQueueName=?,
|
|
95
118
|
~appSyncApiId=?,
|
|
96
119
|
~clonerEnabled=false,
|
|
120
|
+
~platformApiId=?,
|
|
121
|
+
~apiFragmentRegistryTableName=?,
|
|
122
|
+
~adminDcbCmdTopicUrl=?,
|
|
123
|
+
~splitApi=false,
|
|
97
124
|
(),
|
|
98
125
|
) =>
|
|
99
126
|
configRef := {
|
|
@@ -105,6 +132,10 @@ let registerConfig = (
|
|
|
105
132
|
schedulerQueueName,
|
|
106
133
|
appSyncApiId,
|
|
107
134
|
clonerEnabled,
|
|
135
|
+
platformApiId,
|
|
136
|
+
apiFragmentRegistryTableName,
|
|
137
|
+
adminDcbCmdTopicUrl,
|
|
138
|
+
splitApi,
|
|
108
139
|
}
|
|
109
140
|
|
|
110
141
|
// PluginRuntime_Builder is a functor so that the caller can inject the EventCollectorChannel
|
|
@@ -144,13 +175,15 @@ module Make = (
|
|
|
144
175
|
// Stable JSON literal — matches Reventless.Plugin.pluginDefinitionSchema
|
|
145
176
|
// (optional fields encoded as null via js_nullable).
|
|
146
177
|
let fakePluginDefinitionJson =
|
|
147
|
-
`{"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
|
|
148
179
|
let adminEpEventTopicArn = switch config.eventTopicArn {
|
|
149
180
|
| Some(arn) => arn
|
|
150
181
|
| None => Pulumi.Output.make("NOT_AVAILABLE")
|
|
151
182
|
}
|
|
152
183
|
{
|
|
153
184
|
pluginDefinitionJson: fakePluginDefinitionJson,
|
|
185
|
+
// Admin ships no UI-fragment manifest.
|
|
186
|
+
uiFragmentsJson: "null"->Pulumi.Output.make,
|
|
154
187
|
extensionPoints: [
|
|
155
188
|
{
|
|
156
189
|
specModule: ReventlessCore.Plugin_Helpers.adminPluginExtensionPointSpecModule,
|
|
@@ -286,6 +319,9 @@ module Make = (
|
|
|
286
319
|
config.appSyncApiId->outputOrPlaceholder->Obj.magic,
|
|
287
320
|
epEventTopicArnsOutput->Pulumi.Output.asInput->Obj.magic,
|
|
288
321
|
config.pluginSchemaPersistenceTableName->outputOrPlaceholder->Obj.magic,
|
|
322
|
+
config.platformApiId->outputOrPlaceholder->Obj.magic,
|
|
323
|
+
config.apiFragmentRegistryTableName->outputOrPlaceholder->Obj.magic,
|
|
324
|
+
config.adminDcbCmdTopicUrl->outputOrPlaceholder->Obj.magic,
|
|
289
325
|
])
|
|
290
326
|
->Pulumi.Output.apply(values => {
|
|
291
327
|
let queueUrl = values->Array.getUnsafe(0)
|
|
@@ -298,6 +334,9 @@ module Make = (
|
|
|
298
334
|
let appSyncApiId = values->Array.getUnsafe(7)
|
|
299
335
|
let epEventTopicArns: array<string> = Obj.magic(values->Array.getUnsafe(8))
|
|
300
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)
|
|
301
340
|
|
|
302
341
|
let dict = Dict.make()
|
|
303
342
|
dict->Dict.set("queueUrl", JSON.Encode.string(queueUrl))
|
|
@@ -309,6 +348,12 @@ module Make = (
|
|
|
309
348
|
JSON.Encode.string(schemaPersistenceTable),
|
|
310
349
|
)
|
|
311
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))
|
|
312
357
|
dict->Dict.set("clonerEnabled", JSON.Encode.bool(config.clonerEnabled))
|
|
313
358
|
dict->Dict.set("schedulerRoleArn", JSON.Encode.string(schedRoleArn))
|
|
314
359
|
dict->Dict.set("schedulerQueueArn", JSON.Encode.string(schedQueueArn))
|
|
@@ -469,9 +514,15 @@ module Make = (
|
|
|
469
514
|
// is identity at runtime, so the Output unwraps correctly inside the
|
|
470
515
|
// Lambda Function args.
|
|
471
516
|
let bundleOutput =
|
|
472
|
-
context.pluginDefinitionJson
|
|
517
|
+
(context.pluginDefinitionJson, context.uiFragmentsJson)
|
|
518
|
+
->Pulumi.Output.all2
|
|
519
|
+
->Pulumi.Output.apply(((pluginDefinitionJson, uiFragmentsJson)) => {
|
|
473
520
|
let extraStringAssets = Dict.make()
|
|
474
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)
|
|
475
526
|
Util_Bundle.buildCodeArchive(
|
|
476
527
|
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/AdminEventCollectorEntryPoint.mjs",
|
|
477
528
|
~packageDirs,
|
|
@@ -621,6 +672,71 @@ module Make = (
|
|
|
621
672
|
)
|
|
622
673
|
| None => ()
|
|
623
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
|
+
}
|
|
624
740
|
}
|
|
625
741
|
|
|
626
742
|
// Plugin EC sqs:SendMessage grants on the aggregate / StateChangeSlice
|