@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.
@@ -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
- return admin;
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. `shellConfig` merges in
36
- last; a key it shares with one already present fails the deploy naming it,
37
- because silently resolving the collision either way produces an app pointed
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(