@reventlessdev/reventless-aws 3.0.0-alpha.290 → 3.0.0-alpha.292
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 -0
- package/package.json +11 -11
- package/src/Platform.res +115 -9
- package/src/Platform.res.mjs +101 -12
- 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 +61 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +29 -2
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +248 -6
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +181 -9
- package/src/plugin/runtime/PluginRuntime_Builder.res +16 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +6 -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 +174 -0
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res.mjs +130 -0
- package/tests/Util_ShellConfigTest.res +41 -0
- package/tests/Util_ShellConfigTest.res.mjs +51 -5
|
@@ -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,177 @@ 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
|
+
})
|
|
243
|
+
|
|
244
|
+
// The bake reads a read model the deploy updates asynchronously, so "is this the
|
|
245
|
+
// deployment I was asked to bake" is a question it has to be able to answer. The
|
|
246
|
+
// answer is an equality check against the key each plugin stack just wrote.
|
|
247
|
+
describe("pendingRegistrations", () => {
|
|
248
|
+
let row = (~name, ~key=?, ()) => {
|
|
249
|
+
let item = Dict.fromArray([("name", JSON.Encode.string(name))])
|
|
250
|
+
key->Option.forEach(k =>
|
|
251
|
+
item->Dict.set(
|
|
252
|
+
"structure",
|
|
253
|
+
JSON.parseOrThrow(`{"$offload": {"store": "pluginStructures", "key": "${k}"}}`),
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
item
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let pending = (rows, expect) =>
|
|
260
|
+
Platform_ComponentDefinitions_Lambda_Ops.pendingRegistrations(
|
|
261
|
+
rows,
|
|
262
|
+
~expect=Dict.fromArray(expect),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
testSync("nothing is pending when every row carries the key the deploy wrote", () =>
|
|
266
|
+
expect(
|
|
267
|
+
pending(
|
|
268
|
+
[row(~name="Catalog", ~key="sha256/a", ()), row(~name="Ordering", ~key="sha256/b", ())],
|
|
269
|
+
[("Catalog", "sha256/a"), ("Ordering", "sha256/b")],
|
|
270
|
+
),
|
|
271
|
+
)->toEqual([])
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
// The window this exists for: the stack wrote a new structure, the row still
|
|
275
|
+
// points at the one before it.
|
|
276
|
+
testSync("names a plugin still carrying the previous structure", () =>
|
|
277
|
+
expect(
|
|
278
|
+
pending(
|
|
279
|
+
[row(~name="Catalog", ~key="sha256/a", ()), row(~name="Ordering", ~key="sha256/old", ())],
|
|
280
|
+
[("Catalog", "sha256/a"), ("Ordering", "sha256/new")],
|
|
281
|
+
),
|
|
282
|
+
)->toEqual(["Ordering"])
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
// A plugin that has never registered is behind, not absent — baking without it
|
|
286
|
+
// would ship a shop missing a section.
|
|
287
|
+
testSync("names a plugin with no row at all", () =>
|
|
288
|
+
expect(pending([], [("Ordering", "sha256/new")]))->toEqual(["Ordering"])
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
// Deployed before the stack exported its key, or invoked by hand: bake what is
|
|
292
|
+
// current rather than wait for an expectation nobody stated.
|
|
293
|
+
testSync("waits for nothing when the caller expects nothing", () =>
|
|
294
|
+
expect(pending([row(~name="Ordering", ~key="sha256/old", ())], []))->toEqual([])
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
testSync("reads the expectations off the invocation payload", () =>
|
|
298
|
+
expect(
|
|
299
|
+
Platform_ComponentDefinitions_Lambda_Ops.bakeExpectations(
|
|
300
|
+
JSON.parseOrThrow(`{"bake": true, "expect": {"Ordering": "sha256/b"}}`),
|
|
301
|
+
)->Dict.get("Ordering"),
|
|
302
|
+
)->toEqual(Some("sha256/b"))
|
|
303
|
+
)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
describe("resolveStructure", () => {
|
|
307
|
+
let item = (structure: string) =>
|
|
308
|
+
Dict.fromArray([
|
|
309
|
+
("name", JSON.Encode.string("Ordering")),
|
|
310
|
+
("structure", JSON.parseOrThrow(structure)),
|
|
311
|
+
])
|
|
312
|
+
|
|
313
|
+
let offloaded = `{"$offload": {"store": "pluginStructures", "key": "sha256/abc", "bytes": 4}}`
|
|
314
|
+
|
|
315
|
+
test("substitutes the fetched bytes for the reference", async () => {
|
|
316
|
+
let resolved = await Platform_ComponentDefinitions_Lambda_Ops.resolveStructure(
|
|
317
|
+
_ => Promise.resolve(`{"aggregates": []}`),
|
|
318
|
+
item(offloaded),
|
|
319
|
+
)
|
|
320
|
+
expect(resolved->Dict.get("structure")->Option.map(j => JSON.stringify(j)))->toEqual(
|
|
321
|
+
Some(`{"aggregates":[]}`),
|
|
322
|
+
)
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
// One offload bucket serves every plugin, so the S3 error names only the key —
|
|
326
|
+
// which plugin's row carries the unreadable reference is the part worth having.
|
|
327
|
+
// A missing object is the shape a deploy leaves behind while the read model
|
|
328
|
+
// still points at the previous structure.
|
|
329
|
+
test("names the plugin whose reference cannot be read", async () => {
|
|
330
|
+
let failed = await Platform_ComponentDefinitions_Lambda_Ops.resolveStructure(
|
|
331
|
+
_ => Promise.reject(JsExn.anyToExnInternal(JsError.make("AccessDenied"))),
|
|
332
|
+
item(offloaded),
|
|
333
|
+
)
|
|
334
|
+
->Promise.thenResolve(_ => None)
|
|
335
|
+
->Promise.catch(e =>
|
|
336
|
+
Promise.resolve(e->JsExn.fromException->Option.flatMap(JsExn.message))
|
|
337
|
+
)
|
|
338
|
+
expect(failed)->toEqual(
|
|
339
|
+
Some("offloaded structure for plugin Ordering is unreadable at sha256/abc: AccessDenied"),
|
|
340
|
+
)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
test("passes an inline structure through untouched", async () => {
|
|
344
|
+
let resolved = await Platform_ComponentDefinitions_Lambda_Ops.resolveStructure(
|
|
345
|
+
_ => Promise.reject(JsExn.anyToExnInternal(JsError.make("must not fetch"))),
|
|
346
|
+
item(`{"aggregates": []}`),
|
|
347
|
+
)
|
|
348
|
+
expect(resolved->Dict.get("structure")->Option.map(j => JSON.stringify(j)))->toEqual(
|
|
349
|
+
Some(`{"aggregates":[]}`),
|
|
350
|
+
)
|
|
351
|
+
})
|
|
352
|
+
})
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
8
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
6
9
|
import * as Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws from "../src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs";
|
|
7
10
|
|
|
8
11
|
function entry(filter, structure) {
|
|
@@ -108,6 +111,133 @@ globalThis.describe("toEntryWith carries Internal queryables on the filtered fie
|
|
|
108
111
|
});
|
|
109
112
|
});
|
|
110
113
|
|
|
114
|
+
globalThis.describe("bakeTargetOf", () => {
|
|
115
|
+
globalThis.test("a query invocation is not a bake", () => {
|
|
116
|
+
globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{}`)))).toBe(true);
|
|
117
|
+
globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{"complete": true}`)))).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
globalThis.test("a bake without a target is not a bake", () => {
|
|
120
|
+
globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{"bake": true}`)))).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
globalThis.test("defaults the key to the one the deploy tells the shell to fetch", () => {
|
|
123
|
+
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");
|
|
124
|
+
});
|
|
125
|
+
globalThis.test("takes an explicit key", () => {
|
|
126
|
+
globalThis.expect(Stdlib_Option.map(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeTargetOf(JSON.parse(`{"bake": true, "bucket": "host-ui", "key": "storefront.json"}`)), t => [
|
|
127
|
+
t.bucket,
|
|
128
|
+
t.key
|
|
129
|
+
])).toEqual([
|
|
130
|
+
"host-ui",
|
|
131
|
+
"storefront.json"
|
|
132
|
+
]);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
globalThis.describe("bakeSelection", () => {
|
|
137
|
+
globalThis.test("keeps absent and empty apart", () => {
|
|
138
|
+
globalThis.expect(Stdlib_Option.flatMap(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"plugin": "Catalog"}`)), s => s.views)).toEqual(undefined);
|
|
139
|
+
globalThis.expect(Stdlib_Option.flatMap(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"plugin": "Catalog", "views": []}`)), s => s.views)).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
globalThis.test("reads the named components", () => {
|
|
142
|
+
globalThis.expect(Stdlib_Option.map(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"plugin": "Ordering", "views": ["Orders"], "commands": ["PlaceOrder"]}`)), s => [
|
|
143
|
+
s.plugin,
|
|
144
|
+
s.views,
|
|
145
|
+
s.commands
|
|
146
|
+
])).toEqual([
|
|
147
|
+
"Ordering",
|
|
148
|
+
["Orders"],
|
|
149
|
+
["PlaceOrder"]
|
|
150
|
+
]);
|
|
151
|
+
});
|
|
152
|
+
globalThis.test("refuses an entry naming no plugin", () => {
|
|
153
|
+
globalThis.expect(Stdlib_Option.isNone(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeSelection(JSON.parse(`{"views": ["Orders"]}`)))).toBe(true);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
globalThis.describe("pendingRegistrations", () => {
|
|
158
|
+
let row = (name, key, param) => {
|
|
159
|
+
let item = Object.fromEntries([[
|
|
160
|
+
"name",
|
|
161
|
+
name
|
|
162
|
+
]]);
|
|
163
|
+
Stdlib_Option.forEach(key, k => {
|
|
164
|
+
item["structure"] = JSON.parse(`{"$offload": {"store": "pluginStructures", "key": "` + k + `"}}`);
|
|
165
|
+
});
|
|
166
|
+
return item;
|
|
167
|
+
};
|
|
168
|
+
globalThis.test("nothing is pending when every row carries the key the deploy wrote", () => {
|
|
169
|
+
let rows = [
|
|
170
|
+
row("Catalog", "sha256/a", undefined),
|
|
171
|
+
row("Ordering", "sha256/b", undefined)
|
|
172
|
+
];
|
|
173
|
+
globalThis.expect(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.pendingRegistrations(rows, Object.fromEntries([
|
|
174
|
+
[
|
|
175
|
+
"Catalog",
|
|
176
|
+
"sha256/a"
|
|
177
|
+
],
|
|
178
|
+
[
|
|
179
|
+
"Ordering",
|
|
180
|
+
"sha256/b"
|
|
181
|
+
]
|
|
182
|
+
]))).toEqual([]);
|
|
183
|
+
});
|
|
184
|
+
globalThis.test("names a plugin still carrying the previous structure", () => {
|
|
185
|
+
let rows = [
|
|
186
|
+
row("Catalog", "sha256/a", undefined),
|
|
187
|
+
row("Ordering", "sha256/old", undefined)
|
|
188
|
+
];
|
|
189
|
+
globalThis.expect(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.pendingRegistrations(rows, Object.fromEntries([
|
|
190
|
+
[
|
|
191
|
+
"Catalog",
|
|
192
|
+
"sha256/a"
|
|
193
|
+
],
|
|
194
|
+
[
|
|
195
|
+
"Ordering",
|
|
196
|
+
"sha256/new"
|
|
197
|
+
]
|
|
198
|
+
]))).toEqual(["Ordering"]);
|
|
199
|
+
});
|
|
200
|
+
globalThis.test("names a plugin with no row at all", () => {
|
|
201
|
+
globalThis.expect(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.pendingRegistrations([], Object.fromEntries([[
|
|
202
|
+
"Ordering",
|
|
203
|
+
"sha256/new"
|
|
204
|
+
]]))).toEqual(["Ordering"]);
|
|
205
|
+
});
|
|
206
|
+
globalThis.test("waits for nothing when the caller expects nothing", () => {
|
|
207
|
+
let rows = [row("Ordering", "sha256/old", undefined)];
|
|
208
|
+
globalThis.expect(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.pendingRegistrations(rows, Object.fromEntries([]))).toEqual([]);
|
|
209
|
+
});
|
|
210
|
+
globalThis.test("reads the expectations off the invocation payload", () => {
|
|
211
|
+
globalThis.expect(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.bakeExpectations(JSON.parse(`{"bake": true, "expect": {"Ordering": "sha256/b"}}`))["Ordering"]).toEqual("sha256/b");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
globalThis.describe("resolveStructure", () => {
|
|
216
|
+
let item = structure => Object.fromEntries([
|
|
217
|
+
[
|
|
218
|
+
"name",
|
|
219
|
+
"Ordering"
|
|
220
|
+
],
|
|
221
|
+
[
|
|
222
|
+
"structure",
|
|
223
|
+
JSON.parse(structure)
|
|
224
|
+
]
|
|
225
|
+
]);
|
|
226
|
+
let offloaded = `{"$offload": {"store": "pluginStructures", "key": "sha256/abc", "bytes": 4}}`;
|
|
227
|
+
globalThis.test("substitutes the fetched bytes for the reference", async () => {
|
|
228
|
+
let resolved = await Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.resolveStructure(param => Promise.resolve(`{"aggregates": []}`), item(offloaded));
|
|
229
|
+
globalThis.expect(Stdlib_Option.map(resolved["structure"], j => JSON.stringify(j))).toEqual(`{"aggregates":[]}`);
|
|
230
|
+
});
|
|
231
|
+
globalThis.test("names the plugin whose reference cannot be read", async () => {
|
|
232
|
+
let failed = await Stdlib_Promise.$$catch(Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.resolveStructure(param => Promise.reject(Primitive_exceptions.internalToException(new Error("AccessDenied"))), item(offloaded)).then(param => {}), e => Promise.resolve(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(e), Stdlib_JsExn.message)));
|
|
233
|
+
globalThis.expect(failed).toEqual("offloaded structure for plugin Ordering is unreadable at sha256/abc: AccessDenied");
|
|
234
|
+
});
|
|
235
|
+
globalThis.test("passes an inline structure through untouched", async () => {
|
|
236
|
+
let resolved = await Platform_ComponentDefinitions_Lambda_Ops$ReventlessAws.resolveStructure(param => Promise.reject(Primitive_exceptions.internalToException(new Error("must not fetch"))), item(`{"aggregates": []}`));
|
|
237
|
+
globalThis.expect(Stdlib_Option.map(resolved["structure"], j => JSON.stringify(j))).toEqual(`{"aggregates":[]}`);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
111
241
|
export {
|
|
112
242
|
entry,
|
|
113
243
|
members,
|
|
@@ -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(
|
|
@@ -22,7 +22,7 @@ function get(out, key) {
|
|
|
22
22
|
|
|
23
23
|
globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
|
|
24
24
|
globalThis.test("unset ⇒ no viewModes key and the computed set is untouched", () => {
|
|
25
|
-
let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined);
|
|
25
|
+
let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined);
|
|
26
26
|
globalThis.expect(Object.keys(out)).toEqual([
|
|
27
27
|
"apiEndpoint",
|
|
28
28
|
"region"
|
|
@@ -33,7 +33,7 @@ globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
|
|
|
33
33
|
let out = Util_ShellConfig$ReventlessAws.fields(computed, [{
|
|
34
34
|
TAG: "Map",
|
|
35
35
|
_0: {}
|
|
36
|
-
}], undefined);
|
|
36
|
+
}], undefined, undefined);
|
|
37
37
|
globalThis.expect(get(out, "viewModes")).toEqual(["map"]);
|
|
38
38
|
globalThis.expect(Stdlib_Option.isNone(out["mapStyle"])).toBe(true);
|
|
39
39
|
});
|
|
@@ -51,7 +51,7 @@ globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
|
|
|
51
51
|
layout: "dagre"
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
-
], undefined);
|
|
54
|
+
], undefined, undefined);
|
|
55
55
|
globalThis.expect(get(out, "viewModes")).toEqual([
|
|
56
56
|
"map",
|
|
57
57
|
"graph"
|
|
@@ -61,9 +61,55 @@ globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
globalThis.describe("Util_ShellConfig.fields — bakedManifest", () => {
|
|
65
|
+
let bake = key => ({
|
|
66
|
+
components: [{
|
|
67
|
+
plugin: "Catalog",
|
|
68
|
+
views: ["Products"],
|
|
69
|
+
commands: []
|
|
70
|
+
}],
|
|
71
|
+
key: key
|
|
72
|
+
});
|
|
73
|
+
globalThis.test("unset ⇒ no manifestUrl, so every caller keeps the admin path", () => {
|
|
74
|
+
let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, undefined);
|
|
75
|
+
globalThis.expect(Stdlib_Option.isNone(out["manifestUrl"])).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
globalThis.test("declared ⇒ the URL the deploy writes the file to", () => {
|
|
78
|
+
let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, bake(undefined), undefined);
|
|
79
|
+
globalThis.expect(get(out, "manifestUrl")).toEqual("/component-manifest.json");
|
|
80
|
+
});
|
|
81
|
+
globalThis.test("follows a renamed bake", () => {
|
|
82
|
+
let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, bake("storefront.json"), undefined);
|
|
83
|
+
globalThis.expect(get(out, "manifestUrl")).toEqual("/storefront.json");
|
|
84
|
+
});
|
|
85
|
+
globalThis.test("a shellConfig manifestUrl fails the deploy rather than redirecting it", () => {
|
|
86
|
+
let failure;
|
|
87
|
+
try {
|
|
88
|
+
Util_ShellConfig$ReventlessAws.fields(computed, undefined, bake(undefined), Object.fromEntries([[
|
|
89
|
+
"manifestUrl",
|
|
90
|
+
"/elsewhere.json"
|
|
91
|
+
]]));
|
|
92
|
+
failure = undefined;
|
|
93
|
+
} catch (raw_message) {
|
|
94
|
+
let message = Primitive_exceptions.internalToException(raw_message);
|
|
95
|
+
if (message.RE_EXN_ID === "Failure") {
|
|
96
|
+
failure = message._1;
|
|
97
|
+
} else {
|
|
98
|
+
throw message;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (failure !== undefined) {
|
|
102
|
+
globalThis.expect(failure.includes("manifestUrl")).toBe(true);
|
|
103
|
+
return;
|
|
104
|
+
} else {
|
|
105
|
+
return JestGlobals.fail("a shellConfig key redirecting the computed manifest must fail the deploy");
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
64
110
|
globalThis.describe("Util_ShellConfig.fields — shellConfig passthrough", () => {
|
|
65
111
|
globalThis.test("shell-owned keys land verbatim, under the computed ones", () => {
|
|
66
|
-
let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, Object.fromEntries([
|
|
112
|
+
let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, Object.fromEntries([
|
|
67
113
|
[
|
|
68
114
|
"platformName",
|
|
69
115
|
"Online Shop"
|
|
@@ -84,7 +130,7 @@ globalThis.describe("Util_ShellConfig.fields — shellConfig passthrough", () =>
|
|
|
84
130
|
globalThis.test("a key the deploy computes fails the deploy, naming it", () => {
|
|
85
131
|
let failure;
|
|
86
132
|
try {
|
|
87
|
-
Util_ShellConfig$ReventlessAws.fields(computed, undefined, Object.fromEntries([[
|
|
133
|
+
Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined, Object.fromEntries([[
|
|
88
134
|
"apiEndpoint",
|
|
89
135
|
"https://wrong.example/graphql"
|
|
90
136
|
]]));
|