@reventlessdev/reventless-aws 3.0.0-alpha.288 → 3.0.0-alpha.291
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 +15 -2
- package/package.json +8 -8
- package/src/Platform.res +60 -8
- package/src/Platform.res.mjs +48 -10
- package/src/adapter/Api/Platform_AdminScan_Ops.res +7 -3
- package/src/adapter/Api/Platform_AdminScan_Ops.res.mjs +2 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +51 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +21 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +169 -4
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +127 -6
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +7 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +2 -0
- package/src/util/Util_OwnerScopeEnv.res +48 -0
- package/src/util/Util_OwnerScopeEnv.res.mjs +31 -0
- package/src/util/Util_ShellConfig.res +18 -3
- package/src/util/Util_ShellConfig.res.mjs +5 -1
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res +64 -0
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res.mjs +43 -0
- package/tests/Util_OwnerScopeEnvTest.res +60 -0
- package/tests/Util_OwnerScopeEnvTest.res.mjs +62 -0
- package/tests/Util_ShellConfigTest.res +41 -0
- package/tests/Util_ShellConfigTest.res.mjs +51 -5
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
3
4
|
import * as Nodefs from "node:fs";
|
|
4
5
|
import * as S3$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/S3.res.mjs";
|
|
5
6
|
import * as Nodepath from "node:path";
|
|
6
7
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
8
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
7
9
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
10
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
11
|
+
import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
|
|
12
|
+
import * as ClientS3 from "@aws-sdk/client-s3";
|
|
8
13
|
import * as Offload$Reventless from "@reventlessdev/reventless-spec/src/semantic/Offload.res.mjs";
|
|
9
14
|
import * as Platform_AdminScan_Ops$ReventlessAws from "./Platform_AdminScan_Ops.res.mjs";
|
|
15
|
+
import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
|
|
10
16
|
|
|
11
17
|
function str(item, key) {
|
|
12
18
|
return Stdlib_Option.flatMap(item[key], Stdlib_JSON.Decode.string);
|
|
@@ -224,16 +230,118 @@ function isComplete(event) {
|
|
|
224
230
|
return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(event), o => o["complete"]), Stdlib_JSON.Decode.bool), false);
|
|
225
231
|
}
|
|
226
232
|
|
|
233
|
+
function structureOf(item, param) {
|
|
234
|
+
let match = str(item, "name");
|
|
235
|
+
let match$1 = Stdlib_Option.flatMap(item["structure"], Stdlib_JSON.Decode.object);
|
|
236
|
+
if (match === undefined) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (match$1 === undefined) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
let healed = healStructure(match$1);
|
|
243
|
+
let decoded;
|
|
244
|
+
try {
|
|
245
|
+
decoded = S.parseJsonOrThrow(healed, Plugin$Reventless.pluginStructureSchema);
|
|
246
|
+
} catch (exn) {
|
|
247
|
+
return Stdlib_JsError.throwWithMessage(`baked manifest: the structure persisted for "` + match + `" cannot be decoded — it was written by a framework version this platform can no longer read. Redeploy that plugin, or drop it from the include-list.`);
|
|
248
|
+
}
|
|
249
|
+
return [
|
|
250
|
+
match,
|
|
251
|
+
decoded
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function bakeSelection(json) {
|
|
256
|
+
let strings = (o, key) => Stdlib_Option.map(Stdlib_Option.flatMap(o[key], Stdlib_JSON.Decode.array), a => Stdlib_Array.filterMap(a, Stdlib_JSON.Decode.string));
|
|
257
|
+
return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => Stdlib_Option.map(Stdlib_Option.flatMap(o["plugin"], Stdlib_JSON.Decode.string), plugin => ({
|
|
258
|
+
plugin: plugin,
|
|
259
|
+
views: strings(o, "views"),
|
|
260
|
+
commands: strings(o, "commands")
|
|
261
|
+
})));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function bakeSelections() {
|
|
265
|
+
let raw = process.env["BAKE_SELECTIONS"];
|
|
266
|
+
if (raw === undefined) {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
if (raw === "") {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
let entries;
|
|
273
|
+
try {
|
|
274
|
+
entries = Stdlib_JSON.Decode.array(JSON.parse(raw));
|
|
275
|
+
} catch (exn) {
|
|
276
|
+
return Stdlib_JsError.throwWithMessage("baked manifest: BAKE_SELECTIONS is not a JSON array");
|
|
277
|
+
}
|
|
278
|
+
if (entries !== undefined) {
|
|
279
|
+
return Stdlib_Array.filterMap(entries, bakeSelection);
|
|
280
|
+
} else {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function bakeTargetOf(event) {
|
|
286
|
+
return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(event), o => {
|
|
287
|
+
let match = Stdlib_Option.flatMap(o["bake"], Stdlib_JSON.Decode.bool);
|
|
288
|
+
let match$1 = Stdlib_Option.flatMap(o["bucket"], Stdlib_JSON.Decode.string);
|
|
289
|
+
if (match !== undefined && match && match$1 !== undefined) {
|
|
290
|
+
return {
|
|
291
|
+
bucket: match$1,
|
|
292
|
+
key: Stdlib_Option.getOr(Stdlib_Option.flatMap(o["key"], Stdlib_JSON.Decode.string), Platform_BakedManifest$ReventlessCore.defaultKey)
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function runBake(target, structures) {
|
|
299
|
+
let selections = bakeSelections();
|
|
300
|
+
if (selections.length === 0) {
|
|
301
|
+
Stdlib_JsError.throwWithMessage("baked manifest: BAKE_SELECTIONS is empty — this platform declares no bake, so there is nothing to write and a shell pointed at the file would find none.");
|
|
302
|
+
}
|
|
303
|
+
let e = Platform_BakedManifest$ReventlessCore.curate(structures, selections);
|
|
304
|
+
if (e.TAG !== "Ok") {
|
|
305
|
+
return Stdlib_JsError.throwWithMessage(Platform_BakedManifest$ReventlessCore.describe(e._0));
|
|
306
|
+
}
|
|
307
|
+
let body = JSON.stringify(e._0, undefined, 2);
|
|
308
|
+
await S3$AwsSdk.PutObjectCommand.send(new ClientS3.PutObjectCommand({
|
|
309
|
+
Bucket: target.bucket,
|
|
310
|
+
Key: target.key,
|
|
311
|
+
Body: body,
|
|
312
|
+
ContentType: "application/json"
|
|
313
|
+
}));
|
|
314
|
+
return [Object.fromEntries([
|
|
315
|
+
[
|
|
316
|
+
"baked",
|
|
317
|
+
true
|
|
318
|
+
],
|
|
319
|
+
[
|
|
320
|
+
"bucket",
|
|
321
|
+
target.bucket
|
|
322
|
+
],
|
|
323
|
+
[
|
|
324
|
+
"key",
|
|
325
|
+
target.key
|
|
326
|
+
],
|
|
327
|
+
[
|
|
328
|
+
"plugins",
|
|
329
|
+
selections.length
|
|
330
|
+
],
|
|
331
|
+
[
|
|
332
|
+
"bytes",
|
|
333
|
+
body.length
|
|
334
|
+
]
|
|
335
|
+
])];
|
|
336
|
+
}
|
|
337
|
+
|
|
227
338
|
async function handler(event) {
|
|
228
339
|
let complete = isComplete(event);
|
|
340
|
+
let bakeTarget = bakeTargetOf(event);
|
|
229
341
|
let toEntry = (item, name) => toEntryWith(!complete, item, name);
|
|
230
342
|
let admin = Stdlib_Option.mapOr(adminEntry, [], e => [e]);
|
|
231
343
|
let table = process.env["PLUGIN_RM_TABLE"];
|
|
232
|
-
if (table !== undefined) {
|
|
233
|
-
if (table === "") {
|
|
234
|
-
console.error("Platform_ComponentDefinitions: PLUGIN_RM_TABLE env var not set");
|
|
235
|
-
return admin;
|
|
236
|
-
}
|
|
344
|
+
if (table !== undefined && table !== "") {
|
|
237
345
|
let rawItems = await Platform_AdminScan_Ops$ReventlessAws.scanAll(table, "contains(#status, :connected)", Object.fromEntries([[
|
|
238
346
|
"#status",
|
|
239
347
|
"status"
|
|
@@ -244,11 +352,19 @@ async function handler(event) {
|
|
|
244
352
|
let bucket = Stdlib_Option.getOr(process.env["OFFLOAD_BUCKET"], "");
|
|
245
353
|
let fetch = Offload$Reventless.cachedFetch(key => S3$AwsSdk.GetObjectCommand.getString(bucket, key));
|
|
246
354
|
let items = await Promise.all(rawItems.map(item => resolveStructure(fetch, item)));
|
|
355
|
+
if (bakeTarget !== undefined) {
|
|
356
|
+
let structures = Platform_AdminScan_Ops$ReventlessAws.latestByName(items, item => str(item, "name"), structureOf);
|
|
357
|
+
return await runBake(bakeTarget, structures);
|
|
358
|
+
}
|
|
247
359
|
let userEntries = Platform_AdminScan_Ops$ReventlessAws.latestByName(items, item => str(item, "name"), toEntry);
|
|
248
360
|
return admin.concat(userEntries);
|
|
249
361
|
}
|
|
250
362
|
console.error("Platform_ComponentDefinitions: PLUGIN_RM_TABLE env var not set");
|
|
251
|
-
|
|
363
|
+
if (bakeTarget !== undefined) {
|
|
364
|
+
return Stdlib_JsError.throwWithMessage("baked manifest: PLUGIN_RM_TABLE env var not set — the bake would write an empty shop rather than fail.");
|
|
365
|
+
} else {
|
|
366
|
+
return admin;
|
|
367
|
+
}
|
|
252
368
|
}
|
|
253
369
|
|
|
254
370
|
export {
|
|
@@ -266,6 +382,11 @@ export {
|
|
|
266
382
|
adminEntry,
|
|
267
383
|
resolveStructure,
|
|
268
384
|
isComplete,
|
|
385
|
+
structureOf,
|
|
386
|
+
bakeSelection,
|
|
387
|
+
bakeSelections,
|
|
388
|
+
bakeTargetOf,
|
|
389
|
+
runBake,
|
|
269
390
|
handler,
|
|
270
391
|
}
|
|
271
392
|
/* adminEntry Not a pure module */
|
|
@@ -237,6 +237,13 @@ let makeFromCodeAsset: (
|
|
|
237
237
|
// `Lambda.Function.make` builders apply the identical policy.
|
|
238
238
|
Util_LambdaLogging.applyLogLevelDefault(variables)
|
|
239
239
|
|
|
240
|
+
// Same terms, same reason it is here rather than at each builder: the groups
|
|
241
|
+
// exempt from owner scoping are decided by the deploy program, and the runtime
|
|
242
|
+
// that stamps a command has no other way to learn them. Without it a runtime
|
|
243
|
+
// concludes nobody is elevated and stamps an operator's on-behalf write with
|
|
244
|
+
// the operator's own id.
|
|
245
|
+
Util_OwnerScopeEnv.applyElevatedGroupsDefault(variables)
|
|
246
|
+
|
|
240
247
|
// ESM self-containment (Option C): every code archive built by
|
|
241
248
|
// Util_Bundle.buildCodeArchive ships register-hook.mjs + layer-resolver.mjs at
|
|
242
249
|
// /var/task. --import registers the resolve hook so the ESM entry point (and the
|
|
@@ -24,6 +24,7 @@ import * as ComponentType$ReventlessCore from "@reventlessdev/reventless-core/sr
|
|
|
24
24
|
import * as Util_DcbMetrics$ReventlessAws from "../../util/Util_DcbMetrics.res.mjs";
|
|
25
25
|
import * as RuntimeExtension$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/RuntimeExtension/RuntimeExtension.res.mjs";
|
|
26
26
|
import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
|
|
27
|
+
import * as Util_OwnerScopeEnv$ReventlessAws from "../../util/Util_OwnerScopeEnv.res.mjs";
|
|
27
28
|
import * as ResourceAttribution$ReventlessCore from "@reventlessdev/reventless-core/src/ResourceAttribution.res.mjs";
|
|
28
29
|
|
|
29
30
|
let additionalEnvVars = {};
|
|
@@ -102,6 +103,7 @@ function makeFromCodeAsset(name, unitKind, componentKind, code, sourceCodeHash,
|
|
|
102
103
|
variables[key] = value;
|
|
103
104
|
});
|
|
104
105
|
Util_LambdaLogging$ReventlessAws.applyLogLevelDefault(variables);
|
|
106
|
+
Util_OwnerScopeEnv$ReventlessAws.applyElevatedGroupsDefault(variables);
|
|
105
107
|
variables["NODE_OPTIONS"] = Util_Bundle$ReventlessAws.esmLoaderNodeOptions;
|
|
106
108
|
variables["ESM_FALLBACK_DIRS"] = Util_Bundle$ReventlessAws.esmFallbackDirs;
|
|
107
109
|
if (!RuntimeExtension$ReventlessCore.isEmpty()) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Carries the deployment's elevated groups into Lambda runtimes.
|
|
2
|
+
//
|
|
3
|
+
// A cloud deployment is two kinds of process and only one of them is the deploy
|
|
4
|
+
// program: it bakes the owner predicate into generated resolver source, while
|
|
5
|
+
// command stamping (`CommandGenerator_Callback`) and SQL-backed reads
|
|
6
|
+
// (`PgQueryResolver_Lambda`) run later, in function runtimes the deploy never
|
|
7
|
+
// enters. `Reventless.OwnerScope.elevatedGroups()` answers from an explicit
|
|
8
|
+
// `setElevatedGroups` or from the environment, so a value stated in the deploy
|
|
9
|
+
// program reaches those runtimes only if something puts it in their environment.
|
|
10
|
+
//
|
|
11
|
+
// Nothing did, and the two halves fail in opposite directions. A read that
|
|
12
|
+
// believes nobody is elevated shows an operator too little, which is safe and
|
|
13
|
+
// visible. A write that believes it stamps an operator's on-behalf order with
|
|
14
|
+
// the OPERATOR's id — silently, and the row is wrong from then on. The second is
|
|
15
|
+
// why this is a correctness fix rather than a parity one.
|
|
16
|
+
//
|
|
17
|
+
// Scoped deliberately to `RuntimeEnvironment_Lambda.makeFromCodeAsset`, which
|
|
18
|
+
// every command and query runtime is built through. The bespoke builders that
|
|
19
|
+
// append `Util_LambdaLogging.logLevelEntry()` for themselves — dead letters, the
|
|
20
|
+
// state-topic publisher, geocoding, the two upload Lambdas, the platform UI APIs
|
|
21
|
+
// — neither stamp a command nor serve an owner-scoped view, so they sit outside
|
|
22
|
+
// this policy for the same reason they sit outside several others. The
|
|
23
|
+
// serialized-closure `make` path is likewise outside it, and already outside the
|
|
24
|
+
// logging policy.
|
|
25
|
+
|
|
26
|
+
let key = "REVENTLESS_ELEVATED_GROUPS"
|
|
27
|
+
|
|
28
|
+
/** The `("REVENTLESS_ELEVATED_GROUPS", "Admin,Support")` entry, or `None` when
|
|
29
|
+
the deployment named no elevated groups.
|
|
30
|
+
|
|
31
|
+
Comma-separated because that is what `OwnerScope.parseElevatedGroups` reads,
|
|
32
|
+
which trims each part and drops the empties. Absent rather than empty for the
|
|
33
|
+
empty case: the two mean the same thing there, and writing `""` would suggest
|
|
34
|
+
a deployment had answered the question when it has not. */
|
|
35
|
+
let entry = (): option<(string, Pulumi.Input.t<string>)> =>
|
|
36
|
+
switch Reventless.OwnerScope.elevatedGroups() {
|
|
37
|
+
| [] => None
|
|
38
|
+
| groups => Some((key, groups->Array.join(",")->Pulumi.Input.make))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Default the elevated groups for a Lambda when the caller has not pinned them.
|
|
42
|
+
Mutates in place and must run after the caller's own env vars are set, so
|
|
43
|
+
theirs win — the same contract as `Util_LambdaLogging.applyLogLevelDefault`,
|
|
44
|
+
which it runs beside. */
|
|
45
|
+
let applyElevatedGroupsDefault = (variables: dict<Pulumi.Input.t<string>>) =>
|
|
46
|
+
if variables->Dict.get(key)->Option.isNone {
|
|
47
|
+
entry()->Option.forEach(((k, v)) => variables->Dict.set(k, v))
|
|
48
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
5
|
+
|
|
6
|
+
let key = "REVENTLESS_ELEVATED_GROUPS";
|
|
7
|
+
|
|
8
|
+
function entry() {
|
|
9
|
+
let groups = OwnerScope$Reventless.elevatedGroups();
|
|
10
|
+
if (groups.length !== 0) {
|
|
11
|
+
return [
|
|
12
|
+
key,
|
|
13
|
+
groups.join(",")
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function applyElevatedGroupsDefault(variables) {
|
|
19
|
+
if (Stdlib_Option.isNone(variables[key])) {
|
|
20
|
+
return Stdlib_Option.forEach(entry(), param => {
|
|
21
|
+
variables[param[0]] = param[1];
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
key,
|
|
28
|
+
entry,
|
|
29
|
+
applyElevatedGroupsDefault,
|
|
30
|
+
}
|
|
31
|
+
/* OwnerScope-Reventless Not a pure module */
|
|
@@ -32,18 +32,33 @@ The config.json field set.
|
|
|
32
32
|
|
|
33
33
|
`computed` arrives in wire order and keeps it. `viewModes` unset ⇒ no
|
|
34
34
|
`viewModes` key and no per-mode key, so a deployment that wants no optional mode
|
|
35
|
-
gets byte-identical output to before this input existed. `
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
gets byte-identical output to before this input existed. `bakedManifest` unset ⇒
|
|
36
|
+
no `manifestUrl`, and every caller keeps the admin discovery path. `shellConfig`
|
|
37
|
+
merges in last; a key it shares with one already present fails the deploy naming
|
|
38
|
+
it, because silently resolving the collision either way produces an app pointed
|
|
38
39
|
somewhere unintended with nothing in the diff to say so.
|
|
39
40
|
*/
|
|
40
41
|
let fields = (
|
|
41
42
|
~computed: array<(string, JSON.t)>,
|
|
42
43
|
~viewModes: option<array<Platform.viewMode>>=?,
|
|
44
|
+
~bakedManifest: option<Platform.bakedManifest>=?,
|
|
43
45
|
~shellConfig: option<dict<JSON.t>>=?,
|
|
44
46
|
): dict<JSON.t> => {
|
|
45
47
|
let out = computed->Dict.fromArray
|
|
46
48
|
|
|
49
|
+
// A declared bake is what turns the shell's non-elevated audience on: an
|
|
50
|
+
// operator keeps the admin queries, everyone else discovers from this file.
|
|
51
|
+
// Computed rather than left to `shellConfig` because the deploy is what
|
|
52
|
+
// decides where the file goes — a passthrough could point the shell at a key
|
|
53
|
+
// nothing writes, and a statically-discovered shell has no admin API behind it
|
|
54
|
+
// to notice.
|
|
55
|
+
bakedManifest->Option.forEach(bake =>
|
|
56
|
+
out->Dict.set(
|
|
57
|
+
"manifestUrl",
|
|
58
|
+
JSON.Encode.string(ReventlessCore.Platform_BakedManifest.urlForKey(bake.key)),
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
47
62
|
switch viewModes {
|
|
48
63
|
| Some(modes) =>
|
|
49
64
|
out->Dict.set(
|
|
@@ -4,6 +4,7 @@ import * as Pervasives from "@rescript/runtime/lib/es6/Pervasives.js";
|
|
|
4
4
|
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
5
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
6
|
import * as Platform$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/Platform.res.mjs";
|
|
7
|
+
import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
|
|
7
8
|
|
|
8
9
|
function modeOptions(mode) {
|
|
9
10
|
if (mode.TAG === "Map") {
|
|
@@ -28,8 +29,11 @@ function modeOptions(mode) {
|
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
function fields(computed, viewModes, shellConfig) {
|
|
32
|
+
function fields(computed, viewModes, bakedManifest, shellConfig) {
|
|
32
33
|
let out = Object.fromEntries(computed);
|
|
34
|
+
Stdlib_Option.forEach(bakedManifest, bake => {
|
|
35
|
+
out["manifestUrl"] = Platform_BakedManifest$ReventlessCore.urlForKey(bake.key);
|
|
36
|
+
});
|
|
33
37
|
if (viewModes !== undefined) {
|
|
34
38
|
out["viewModes"] = viewModes.map(Platform$ReventlessInfra.viewModeToString);
|
|
35
39
|
viewModes.forEach(m => {
|
|
@@ -176,3 +176,67 @@ describe("toEntryWith carries Internal queryables on the filtered field", () =>
|
|
|
176
176
|
expect(names)->toEqual([])
|
|
177
177
|
})
|
|
178
178
|
})
|
|
179
|
+
|
|
180
|
+
// ── Bake mode ────────────────────────────────────────────────────────────────
|
|
181
|
+
// The same scan, written out instead of answered with. Two inputs decide what a
|
|
182
|
+
// bake does and neither is checkable at deploy time: the include-list the
|
|
183
|
+
// platform put in the environment, and the target its caller supplied. Both are
|
|
184
|
+
// pure, so both are assertable without a Lambda.
|
|
185
|
+
|
|
186
|
+
describe("bakeTargetOf", () => {
|
|
187
|
+
let target = raw => Platform_ComponentDefinitions_Lambda_Ops.bakeTargetOf(JSON.parseOrThrow(raw))
|
|
188
|
+
|
|
189
|
+
// The read paths must stay read paths: an AppSync resolver invokes with
|
|
190
|
+
// `{}` or `{complete: true}`, and neither may write an object.
|
|
191
|
+
testSync("a query invocation is not a bake", () => {
|
|
192
|
+
expect(target(`{}`)->Option.isNone)->toBe(true)
|
|
193
|
+
expect(target(`{"complete": true}`)->Option.isNone)->toBe(true)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// No bucket, no default. Guessing one would write the manifest somewhere
|
|
197
|
+
// nothing serves it, which reads as a bake that worked.
|
|
198
|
+
testSync("a bake without a target is not a bake", () =>
|
|
199
|
+
expect(target(`{"bake": true}`)->Option.isNone)->toBe(true)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
testSync("defaults the key to the one the deploy tells the shell to fetch", () =>
|
|
203
|
+
expect(target(`{"bake": true, "bucket": "host-ui"}`)->Option.map(t => t.key))->toEqual(
|
|
204
|
+
Some("component-manifest.json"),
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
testSync("takes an explicit key", () =>
|
|
209
|
+
expect(
|
|
210
|
+
target(`{"bake": true, "bucket": "host-ui", "key": "storefront.json"}`)->Option.map(t => (
|
|
211
|
+
t.bucket,
|
|
212
|
+
t.key,
|
|
213
|
+
)),
|
|
214
|
+
)->toEqual(Some(("host-ui", "storefront.json")))
|
|
215
|
+
)
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
describe("bakeSelection", () => {
|
|
219
|
+
let sel = raw => Platform_ComponentDefinitions_Lambda_Ops.bakeSelection(JSON.parseOrThrow(raw))
|
|
220
|
+
|
|
221
|
+
// Absent is not empty. `views` absent means every public component of that
|
|
222
|
+
// plugin; `views: []` means none of them, and a decoder that collapsed the two
|
|
223
|
+
// would silently bake an empty section.
|
|
224
|
+
testSync("keeps absent and empty apart", () => {
|
|
225
|
+
expect(sel(`{"plugin": "Catalog"}`)->Option.flatMap(s => s.views))->toEqual(None)
|
|
226
|
+
expect(sel(`{"plugin": "Catalog", "views": []}`)->Option.flatMap(s => s.views))->toEqual(
|
|
227
|
+
Some([]),
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
testSync("reads the named components", () =>
|
|
232
|
+
expect(
|
|
233
|
+
sel(`{"plugin": "Ordering", "views": ["Orders"], "commands": ["PlaceOrder"]}`)->Option.map(
|
|
234
|
+
s => (s.plugin, s.views, s.commands),
|
|
235
|
+
),
|
|
236
|
+
)->toEqual(Some(("Ordering", Some(["Orders"]), Some(["PlaceOrder"]))))
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
testSync("refuses an entry naming no plugin", () =>
|
|
240
|
+
expect(sel(`{"views": ["Orders"]}`)->Option.isNone)->toBe(true)
|
|
241
|
+
)
|
|
242
|
+
})
|
|
@@ -108,6 +108,49 @@ globalThis.describe("toEntryWith carries Internal queryables on the filtered fie
|
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
globalThis.describe("bakeTargetOf", () => {
|
|
112
|
+
globalThis.test("a query invocation is not a bake", () => {
|
|
113
|
+
globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{}`)))).toBe(true);
|
|
114
|
+
globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{"complete": true}`)))).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
globalThis.test("a bake without a target is not a bake", () => {
|
|
117
|
+
globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{"bake": true}`)))).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
globalThis.test("defaults the key to the one the deploy tells the shell to fetch", () => {
|
|
120
|
+
globalThis.expect(Stdlib_Option.map(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{"bake": true, "bucket": "host-ui"}`)), t => t.key)).toEqual("component-manifest.json");
|
|
121
|
+
});
|
|
122
|
+
globalThis.test("takes an explicit key", () => {
|
|
123
|
+
globalThis.expect(Stdlib_Option.map(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{"bake": true, "bucket": "host-ui", "key": "storefront.json"}`)), t => [
|
|
124
|
+
t.bucket,
|
|
125
|
+
t.key
|
|
126
|
+
])).toEqual([
|
|
127
|
+
"host-ui",
|
|
128
|
+
"storefront.json"
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
globalThis.describe("bakeSelection", () => {
|
|
134
|
+
globalThis.test("keeps absent and empty apart", () => {
|
|
135
|
+
globalThis.expect(Stdlib_Option.flatMap(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"plugin": "Catalog"}`)), s => s.views)).toEqual(undefined);
|
|
136
|
+
globalThis.expect(Stdlib_Option.flatMap(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"plugin": "Catalog", "views": []}`)), s => s.views)).toEqual([]);
|
|
137
|
+
});
|
|
138
|
+
globalThis.test("reads the named components", () => {
|
|
139
|
+
globalThis.expect(Stdlib_Option.map(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"plugin": "Ordering", "views": ["Orders"], "commands": ["PlaceOrder"]}`)), s => [
|
|
140
|
+
s.plugin,
|
|
141
|
+
s.views,
|
|
142
|
+
s.commands
|
|
143
|
+
])).toEqual([
|
|
144
|
+
"Ordering",
|
|
145
|
+
["Orders"],
|
|
146
|
+
["PlaceOrder"]
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
globalThis.test("refuses an entry naming no plugin", () => {
|
|
150
|
+
globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"views": ["Orders"]}`)))).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
111
154
|
export {
|
|
112
155
|
entry,
|
|
113
156
|
members,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
open JestGlobals
|
|
2
|
+
|
|
3
|
+
// Guards the carrier that tells a Lambda runtime which groups are exempt from
|
|
4
|
+
// owner scoping. The deploy program bakes the read predicate itself, but
|
|
5
|
+
// stamping and SQL-backed reads run in function runtimes it never enters, so
|
|
6
|
+
// without this entry those runtimes conclude nobody is elevated — and an
|
|
7
|
+
// operator's on-behalf write is then stamped with the operator's own id.
|
|
8
|
+
//
|
|
9
|
+
// The encoding is load-bearing in both directions: what is written here is read
|
|
10
|
+
// back by `OwnerScope.parseElevatedGroups`, so the two are asserted together
|
|
11
|
+
// rather than each against a literal.
|
|
12
|
+
|
|
13
|
+
let withElevated = (groups, f) => {
|
|
14
|
+
Reventless.OwnerScope.setElevatedGroups(groups)
|
|
15
|
+
let result = f()
|
|
16
|
+
Reventless.OwnerScope.clearElevatedGroups()
|
|
17
|
+
result
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("Util_OwnerScopeEnv — the elevated-groups carrier", () => {
|
|
21
|
+
testSync("writes what OwnerScope reads back", () => {
|
|
22
|
+
let encoded = withElevated(["Admin", "Support"], () =>
|
|
23
|
+
switch Util_OwnerScopeEnv.entry() {
|
|
24
|
+
| Some((_, value)) => value->Obj.magic
|
|
25
|
+
| None => ""
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
// The round trip, not the spelling: a separator change on either side fails.
|
|
29
|
+
expect(Reventless.OwnerScope.parseElevatedGroups(encoded))->toEqual(["Admin", "Support"])
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
testSync("names the variable the runtime actually reads", () => {
|
|
33
|
+
expect(Util_OwnerScopeEnv.key)->toBe("REVENTLESS_ELEVATED_GROUPS")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
testSync("no elevated groups means no variable, not an empty one", () => {
|
|
37
|
+
let entry = withElevated([], () => Util_OwnerScopeEnv.entry())
|
|
38
|
+
expect(entry->Option.isNone)->toBe(true)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
testSync("defaults the variable when the caller pinned nothing", () => {
|
|
42
|
+
let variables = Dict.make()
|
|
43
|
+
withElevated(["Admin"], () => Util_OwnerScopeEnv.applyElevatedGroupsDefault(variables))
|
|
44
|
+
expect(variables->Dict.get(Util_OwnerScopeEnv.key)->Option.isSome)->toBe(true)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
testSync("a caller that pinned the variable wins", () => {
|
|
48
|
+
let variables = Dict.fromArray([(Util_OwnerScopeEnv.key, "Pinned"->Obj.magic)])
|
|
49
|
+
withElevated(["Admin"], () => Util_OwnerScopeEnv.applyElevatedGroupsDefault(variables))
|
|
50
|
+
expect(variables->Dict.get(Util_OwnerScopeEnv.key)->Option.getOr(""->Obj.magic)->Obj.magic)->toBe(
|
|
51
|
+
"Pinned",
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
testSync("adds nothing when the deployment named no elevated groups", () => {
|
|
56
|
+
let variables = Dict.make()
|
|
57
|
+
withElevated([], () => Util_OwnerScopeEnv.applyElevatedGroupsDefault(variables))
|
|
58
|
+
expect(variables->Dict.get(Util_OwnerScopeEnv.key)->Option.isNone)->toBe(true)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
|
|
5
|
+
import * as Util_OwnerScopeEnv$ReventlessAws from "../src/util/Util_OwnerScopeEnv.res.mjs";
|
|
6
|
+
|
|
7
|
+
function withElevated(groups, f) {
|
|
8
|
+
OwnerScope$Reventless.setElevatedGroups(groups);
|
|
9
|
+
let result = f();
|
|
10
|
+
OwnerScope$Reventless.clearElevatedGroups();
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
globalThis.describe("Util_OwnerScopeEnv — the elevated-groups carrier", () => {
|
|
15
|
+
globalThis.test("writes what OwnerScope reads back", () => {
|
|
16
|
+
let encoded = withElevated([
|
|
17
|
+
"Admin",
|
|
18
|
+
"Support"
|
|
19
|
+
], () => {
|
|
20
|
+
let match = Util_OwnerScopeEnv$ReventlessAws.entry();
|
|
21
|
+
if (match !== undefined) {
|
|
22
|
+
return match[1];
|
|
23
|
+
} else {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
globalThis.expect(OwnerScope$Reventless.parseElevatedGroups(encoded)).toEqual([
|
|
28
|
+
"Admin",
|
|
29
|
+
"Support"
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
32
|
+
globalThis.test("names the variable the runtime actually reads", () => {
|
|
33
|
+
globalThis.expect(Util_OwnerScopeEnv$ReventlessAws.key).toBe("REVENTLESS_ELEVATED_GROUPS");
|
|
34
|
+
});
|
|
35
|
+
globalThis.test("no elevated groups means no variable, not an empty one", () => {
|
|
36
|
+
let entry = withElevated([], () => Util_OwnerScopeEnv$ReventlessAws.entry());
|
|
37
|
+
globalThis.expect(Stdlib_Option.isNone(entry)).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
globalThis.test("defaults the variable when the caller pinned nothing", () => {
|
|
40
|
+
let variables = {};
|
|
41
|
+
withElevated(["Admin"], () => Util_OwnerScopeEnv$ReventlessAws.applyElevatedGroupsDefault(variables));
|
|
42
|
+
globalThis.expect(Stdlib_Option.isSome(variables[Util_OwnerScopeEnv$ReventlessAws.key])).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
globalThis.test("a caller that pinned the variable wins", () => {
|
|
45
|
+
let variables = Object.fromEntries([[
|
|
46
|
+
Util_OwnerScopeEnv$ReventlessAws.key,
|
|
47
|
+
"Pinned"
|
|
48
|
+
]]);
|
|
49
|
+
withElevated(["Admin"], () => Util_OwnerScopeEnv$ReventlessAws.applyElevatedGroupsDefault(variables));
|
|
50
|
+
globalThis.expect(Stdlib_Option.getOr(variables[Util_OwnerScopeEnv$ReventlessAws.key], "")).toBe("Pinned");
|
|
51
|
+
});
|
|
52
|
+
globalThis.test("adds nothing when the deployment named no elevated groups", () => {
|
|
53
|
+
let variables = {};
|
|
54
|
+
withElevated([], () => Util_OwnerScopeEnv$ReventlessAws.applyElevatedGroupsDefault(variables));
|
|
55
|
+
globalThis.expect(Stdlib_Option.isNone(variables[Util_OwnerScopeEnv$ReventlessAws.key])).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
withElevated,
|
|
61
|
+
}
|
|
62
|
+
/* Not a pure module */
|
|
@@ -40,6 +40,47 @@ describe("Util_ShellConfig.fields — viewModes", () => {
|
|
|
40
40
|
})
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
describe("Util_ShellConfig.fields — bakedManifest", () => {
|
|
44
|
+
let bake = (~key: option<string>=?): ReventlessInfra.Platform.bakedManifest => {
|
|
45
|
+
components: [{plugin: "Catalog", views: ["Products"], commands: []}],
|
|
46
|
+
key: ?key,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
testSync("unset ⇒ no manifestUrl, so every caller keeps the admin path", () => {
|
|
50
|
+
let out = Util_ShellConfig.fields(~computed)
|
|
51
|
+
expect(out->Dict.get("manifestUrl")->Option.isNone)->toBe(true)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
testSync("declared ⇒ the URL the deploy writes the file to", () => {
|
|
55
|
+
let out = Util_ShellConfig.fields(~computed, ~bakedManifest=bake())
|
|
56
|
+
expect(out->get("manifestUrl"))->toEqual(JSON.Encode.string("/component-manifest.json"))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// The key and the URL are one string; a renamed bake that kept the default
|
|
60
|
+
// URL would 404 on a shell with no admin API to fall back to.
|
|
61
|
+
testSync("follows a renamed bake", () => {
|
|
62
|
+
let out = Util_ShellConfig.fields(~computed, ~bakedManifest=bake(~key="storefront.json"))
|
|
63
|
+
expect(out->get("manifestUrl"))->toEqual(JSON.Encode.string("/storefront.json"))
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
testSync("a shellConfig manifestUrl fails the deploy rather than redirecting it", () => {
|
|
67
|
+
let failure = try {
|
|
68
|
+
let _ = Util_ShellConfig.fields(
|
|
69
|
+
~computed,
|
|
70
|
+
~bakedManifest=bake(),
|
|
71
|
+
~shellConfig=Dict.fromArray([("manifestUrl", JSON.Encode.string("/elsewhere.json"))]),
|
|
72
|
+
)
|
|
73
|
+
None
|
|
74
|
+
} catch {
|
|
75
|
+
| Failure(message) => Some(message)
|
|
76
|
+
}
|
|
77
|
+
switch failure {
|
|
78
|
+
| Some(message) => expect(message->String.includes("manifestUrl"))->toBe(true)
|
|
79
|
+
| None => fail("a shellConfig key redirecting the computed manifest must fail the deploy")
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
43
84
|
describe("Util_ShellConfig.fields — shellConfig passthrough", () => {
|
|
44
85
|
testSync("shell-owned keys land verbatim, under the computed ones", () => {
|
|
45
86
|
let out = Util_ShellConfig.fields(
|