@reventlessdev/reventless-aws 3.0.0-alpha.291 → 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 +8 -0
- package/package.json +8 -8
- package/src/Platform.res +55 -1
- package/src/Platform.res.mjs +53 -2
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +10 -0
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +8 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +89 -12
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +55 -4
- package/src/plugin/runtime/PluginRuntime_Builder.res +16 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +6 -0
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res +110 -0
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res.mjs +87 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.292 (2026-08-13)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **aws:** re-detect once the Lambda that answers the handshake is up ([597e174](https://github.com/ReventlessDev/reventless-core/commit/597e174059e25461d66db35118c933d2ffce7e1f))
|
|
11
|
+
* **aws:** retain offloaded plugin structures so refs cannot dangle ([120a1a7](https://github.com/ReventlessDev/reventless-core/commit/120a1a7228087dd79f8bcfdd2fddc7b6dbdaf296))
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
# 3.0.0-alpha.291 (2026-08-13)
|
|
7
15
|
|
|
8
16
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.292",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -13,16 +13,16 @@
|
|
|
13
13
|
"sury": "11.0.0-alpha.4",
|
|
14
14
|
"uuid": "^13.0.0",
|
|
15
15
|
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.8",
|
|
16
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
17
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.73",
|
|
16
18
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
19
|
"@reventlessdev/rescript-node": "2.0.0-alpha.5",
|
|
18
|
-
"@reventlessdev/rescript-pulumi-
|
|
19
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
20
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
20
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
21
21
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
22
|
-
"@reventlessdev/reventless-
|
|
23
|
-
"@reventlessdev/reventless-
|
|
24
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.
|
|
25
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
22
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.231",
|
|
23
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.139",
|
|
24
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.31",
|
|
25
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.95",
|
|
26
26
|
"@reventlessdev/reventless-spec": "3.0.0-alpha.112"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
package/src/Platform.res
CHANGED
|
@@ -2296,11 +2296,34 @@ module MakeWithConfig = (
|
|
|
2296
2296
|
->Pulumi.Output.apply(o => o->Option.getOr("OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY"))
|
|
2297
2297
|
| None => Pulumi.Output.make("OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY")
|
|
2298
2298
|
}
|
|
2299
|
+
// The key this deploy wrote for the plugin's structure, exported below. The
|
|
2300
|
+
// Plugin read model is what the manifest bake reads and it is updated
|
|
2301
|
+
// asynchronously, so the bake needs to know which structure "current" means;
|
|
2302
|
+
// an equality check against the key the stack just wrote says that without
|
|
2303
|
+
// timestamps, and passes immediately for a plugin whose structure did not
|
|
2304
|
+
// change.
|
|
2305
|
+
let structureOffloadKey = ref(None)
|
|
2306
|
+
|
|
2299
2307
|
ReventlessCore.Plugin_Helpers.registerOffload((~store, ~bytes) => {
|
|
2300
2308
|
let hash = NodeCrypto.sha256Hex(bytes)
|
|
2301
2309
|
let key = "sha256/" ++ hash
|
|
2310
|
+
if store == "pluginStructures" {
|
|
2311
|
+
structureOffloadKey := Some(key)
|
|
2312
|
+
}
|
|
2302
2313
|
// Content-addressed: the name and key are the hash, so re-deploying an
|
|
2303
2314
|
// unchanged field writes the same object (idempotent, deduplicating).
|
|
2315
|
+
//
|
|
2316
|
+
// `retainOnDelete`: the only readers of these objects are the persisted
|
|
2317
|
+
// refs on the Plugin read model, and that row is updated asynchronously —
|
|
2318
|
+
// the deploy publishes a re-detect, the Plugin aggregate handles it, the
|
|
2319
|
+
// projection lands, minutes later. A changed field means a new hash, so
|
|
2320
|
+
// without this Pulumi DELETES the previous object as part of the same
|
|
2321
|
+
// update, and every reference still on the read model dangles until
|
|
2322
|
+
// registration catches up. S3 answers a GET for a deleted key with
|
|
2323
|
+
// AccessDenied (it masks the 404 unless the caller may ListBucket), so the
|
|
2324
|
+
// window shows up as an authorization failure in the manifest bake and in
|
|
2325
|
+
// the AutoUI definitions query. Content-addressed objects are immutable
|
|
2326
|
+
// and cost kilobytes; keeping them closes the window outright.
|
|
2304
2327
|
let _ = PulumiAws.S3.BucketObject.make(
|
|
2305
2328
|
~name="offload-" ++ hash,
|
|
2306
2329
|
~args={
|
|
@@ -2309,6 +2332,7 @@ module MakeWithConfig = (
|
|
|
2309
2332
|
content: Pulumi.Input.make(bytes),
|
|
2310
2333
|
contentType: Pulumi.Input.make("application/json"),
|
|
2311
2334
|
},
|
|
2335
|
+
~opts={retainOnDelete: true},
|
|
2312
2336
|
)
|
|
2313
2337
|
{Reventless.Offload.store, key, hash, bytes: bytes->String.length}
|
|
2314
2338
|
})
|
|
@@ -2361,6 +2385,23 @@ module MakeWithConfig = (
|
|
|
2361
2385
|
let pluginOutputs = pluginComponent->ReventlessCore.Component.outputs
|
|
2362
2386
|
ReventlessCore.Plugin_Helpers.exportPluginOutputs(pluginOutputs)
|
|
2363
2387
|
|
|
2388
|
+
// What the manifest bake compares the read model against. Keyed by the bare
|
|
2389
|
+
// plugin name because that is how the Plugin read model keys its rows — the
|
|
2390
|
+
// stack's own id carries the version, which the bake has no opinion about.
|
|
2391
|
+
switch structureOffloadKey.contents {
|
|
2392
|
+
| Some(key) =>
|
|
2393
|
+
Pulumi.Pulumi.export(
|
|
2394
|
+
"pluginStructureRef",
|
|
2395
|
+
pluginOutputs.id->Pulumi.Output.apply(id =>
|
|
2396
|
+
Dict.fromArray([
|
|
2397
|
+
("plugin", JSON.Encode.string(ReventlessCore.Plugin.name(id))),
|
|
2398
|
+
("key", JSON.Encode.string(key)),
|
|
2399
|
+
])->JSON.Encode.object
|
|
2400
|
+
),
|
|
2401
|
+
)
|
|
2402
|
+
| None => ()
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2364
2405
|
// ── Merged-API association (merged-api plan, Phase 4) ───────────────────
|
|
2365
2406
|
// Associate this plugin's source API with the platform's merged API —
|
|
2366
2407
|
// this replaces the SigV4 RegisterApiFragment handshake + reactive push +
|
|
@@ -2539,7 +2580,20 @@ module MakeWithConfig = (
|
|
|
2539
2580
|
// synchronously always saw the initial empty config, so the publish was skipped
|
|
2540
2581
|
// on every deploy. `pluginOutputs.heartbeat` derives from that same callback's
|
|
2541
2582
|
// output, so applying to it is the earliest point the config is populated.
|
|
2542
|
-
|
|
2583
|
+
//
|
|
2584
|
+
// The EventCollector Lambda is the other half of the gate because it is what
|
|
2585
|
+
// ANSWERS the handshake: the re-detect only asks the platform to re-run it,
|
|
2586
|
+
// and the definition that comes back is the one compiled into that Lambda
|
|
2587
|
+
// (PluginConnectExtension_Mapping's UnknownPluginDetected branch). Published
|
|
2588
|
+
// before it carries the new code, the plugin answers with the PREVIOUS
|
|
2589
|
+
// deploy's definition, `Connect` sees a definition it already holds and emits
|
|
2590
|
+
// nothing, and the row keeps the old structure — silently, and with no second
|
|
2591
|
+
// re-detect coming to correct it. Waiting on the heartbeat alone gated on a
|
|
2592
|
+
// Lambda that has no part in the answer.
|
|
2593
|
+
let redetectReady =
|
|
2594
|
+
(pluginOutputs.heartbeat, PluginRuntime_Builder.eventCollectorReadyRef.contents)
|
|
2595
|
+
->Pulumi.Output.all2
|
|
2596
|
+
let _ = redetectReady->Pulumi.Output.apply(_ => {
|
|
2543
2597
|
let hbConfig = PluginRuntime_Builder.heartbeatConfigRef.contents
|
|
2544
2598
|
switch (Pulumi.Pulumi.isDryRun(), hbConfig.epQueueUrl) {
|
|
2545
2599
|
| (true, _) => () // `pulumi preview` — no deploy-time side effects
|
package/src/Platform.res.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
|
16
16
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
17
17
|
import * as Plugin$ReventlessAws from "./components/Plugin.res.mjs";
|
|
18
18
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
19
|
+
import * as Plugin$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin.res.mjs";
|
|
19
20
|
import * as AWS_Tags$ReventlessAws from "./adapter/AWS_Tags.res.mjs";
|
|
20
21
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
21
22
|
import * as Scheduler$ReventlessAws from "./components/Scheduler.res.mjs";
|
|
@@ -1248,14 +1249,22 @@ function MakeWithConfig(Config) {
|
|
|
1248
1249
|
hooksApiRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApi);
|
|
1249
1250
|
hooksApiRoleRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApiRole);
|
|
1250
1251
|
let offloadBucketName = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("offloadBucket").apply(o => Stdlib_Option.getOr(o, "OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY")) : Pulumi.output("OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY");
|
|
1252
|
+
let structureOffloadKey = {
|
|
1253
|
+
contents: undefined
|
|
1254
|
+
};
|
|
1251
1255
|
Plugin_Helpers$ReventlessCore.registerOffload((store, bytes) => {
|
|
1252
1256
|
let hash = NodeCrypto.sha256Hex(bytes);
|
|
1253
1257
|
let key = "sha256/" + hash;
|
|
1258
|
+
if (store === "pluginStructures") {
|
|
1259
|
+
structureOffloadKey.contents = key;
|
|
1260
|
+
}
|
|
1254
1261
|
new (Aws.s3.BucketObject)("offload-" + hash, {
|
|
1255
1262
|
bucket: offloadBucketName,
|
|
1256
1263
|
key: key,
|
|
1257
1264
|
content: bytes,
|
|
1258
1265
|
contentType: "application/json"
|
|
1266
|
+
}, {
|
|
1267
|
+
retainOnDelete: true
|
|
1259
1268
|
});
|
|
1260
1269
|
return {
|
|
1261
1270
|
store: store,
|
|
@@ -1273,6 +1282,19 @@ function MakeWithConfig(Config) {
|
|
|
1273
1282
|
Pulumi$Pulumi.$$export("_interopMeta", Plugin_Helpers$ReventlessCore.getInteropMeta());
|
|
1274
1283
|
let pluginOutputs = Component$ReventlessCore.outputs(pluginComponent);
|
|
1275
1284
|
Plugin_Helpers$ReventlessCore.exportPluginOutputs(pluginOutputs);
|
|
1285
|
+
let key = structureOffloadKey.contents;
|
|
1286
|
+
if (key !== undefined) {
|
|
1287
|
+
Pulumi$Pulumi.$$export("pluginStructureRef", pluginOutputs.id.apply(id => Object.fromEntries([
|
|
1288
|
+
[
|
|
1289
|
+
"plugin",
|
|
1290
|
+
Plugin$ReventlessCore.name(id)
|
|
1291
|
+
],
|
|
1292
|
+
[
|
|
1293
|
+
"key",
|
|
1294
|
+
key
|
|
1295
|
+
]
|
|
1296
|
+
])));
|
|
1297
|
+
}
|
|
1276
1298
|
if (platformStackRef !== undefined) {
|
|
1277
1299
|
let stackRef = Primitive_option.valFromOption(platformStackRef);
|
|
1278
1300
|
let defaultOutput = stackRef.getOutput("default");
|
|
@@ -1335,7 +1357,11 @@ function MakeWithConfig(Config) {
|
|
|
1335
1357
|
if (sel !== undefined) {
|
|
1336
1358
|
PgQueryResolver_Builder$ReventlessAws.provision(domainApi, sel, {});
|
|
1337
1359
|
}
|
|
1338
|
-
|
|
1360
|
+
let redetectReady = Pulumi.all([
|
|
1361
|
+
pluginOutputs.heartbeat,
|
|
1362
|
+
PluginRuntime_Builder$ReventlessAws.eventCollectorReadyRef.contents
|
|
1363
|
+
]);
|
|
1364
|
+
redetectReady.apply(param => {
|
|
1339
1365
|
let hbConfig = PluginRuntime_Builder$ReventlessAws.heartbeatConfigRef.contents;
|
|
1340
1366
|
let match = IndexJs.isDryRun();
|
|
1341
1367
|
let match$1 = hbConfig.epQueueUrl;
|
|
@@ -2499,14 +2525,22 @@ function Make($star) {
|
|
|
2499
2525
|
hooksApiRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApi);
|
|
2500
2526
|
hooksApiRoleRef.contents = Platform_Casts$ReventlessAws.wrapHookedValue(targetApiRole);
|
|
2501
2527
|
let offloadBucketName = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("offloadBucket").apply(o => Stdlib_Option.getOr(o, "OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY")) : Pulumi.output("OFFLOAD_BUCKET_PENDING_PLATFORM_DEPLOY");
|
|
2528
|
+
let structureOffloadKey = {
|
|
2529
|
+
contents: undefined
|
|
2530
|
+
};
|
|
2502
2531
|
Plugin_Helpers$ReventlessCore.registerOffload((store, bytes) => {
|
|
2503
2532
|
let hash = NodeCrypto.sha256Hex(bytes);
|
|
2504
2533
|
let key = "sha256/" + hash;
|
|
2534
|
+
if (store === "pluginStructures") {
|
|
2535
|
+
structureOffloadKey.contents = key;
|
|
2536
|
+
}
|
|
2505
2537
|
new (Aws.s3.BucketObject)("offload-" + hash, {
|
|
2506
2538
|
bucket: offloadBucketName,
|
|
2507
2539
|
key: key,
|
|
2508
2540
|
content: bytes,
|
|
2509
2541
|
contentType: "application/json"
|
|
2542
|
+
}, {
|
|
2543
|
+
retainOnDelete: true
|
|
2510
2544
|
});
|
|
2511
2545
|
return {
|
|
2512
2546
|
store: store,
|
|
@@ -2524,6 +2558,19 @@ function Make($star) {
|
|
|
2524
2558
|
Pulumi$Pulumi.$$export("_interopMeta", Plugin_Helpers$ReventlessCore.getInteropMeta());
|
|
2525
2559
|
let pluginOutputs = Component$ReventlessCore.outputs(pluginComponent);
|
|
2526
2560
|
Plugin_Helpers$ReventlessCore.exportPluginOutputs(pluginOutputs);
|
|
2561
|
+
let key = structureOffloadKey.contents;
|
|
2562
|
+
if (key !== undefined) {
|
|
2563
|
+
Pulumi$Pulumi.$$export("pluginStructureRef", pluginOutputs.id.apply(id => Object.fromEntries([
|
|
2564
|
+
[
|
|
2565
|
+
"plugin",
|
|
2566
|
+
Plugin$ReventlessCore.name(id)
|
|
2567
|
+
],
|
|
2568
|
+
[
|
|
2569
|
+
"key",
|
|
2570
|
+
key
|
|
2571
|
+
]
|
|
2572
|
+
])));
|
|
2573
|
+
}
|
|
2527
2574
|
if (platformStackRef !== undefined) {
|
|
2528
2575
|
let stackRef = Primitive_option.valFromOption(platformStackRef);
|
|
2529
2576
|
let defaultOutput = stackRef.getOutput("default");
|
|
@@ -2586,7 +2633,11 @@ function Make($star) {
|
|
|
2586
2633
|
if (sel !== undefined) {
|
|
2587
2634
|
PgQueryResolver_Builder$ReventlessAws.provision(domainApi, sel, {});
|
|
2588
2635
|
}
|
|
2589
|
-
|
|
2636
|
+
let redetectReady = Pulumi.all([
|
|
2637
|
+
pluginOutputs.heartbeat,
|
|
2638
|
+
PluginRuntime_Builder$ReventlessAws.eventCollectorReadyRef.contents
|
|
2639
|
+
]);
|
|
2640
|
+
redetectReady.apply(param => {
|
|
2590
2641
|
let hbConfig = PluginRuntime_Builder$ReventlessAws.heartbeatConfigRef.contents;
|
|
2591
2642
|
let match = IndexJs.isDryRun();
|
|
2592
2643
|
let match$1 = hbConfig.epQueueUrl;
|
|
@@ -117,6 +117,16 @@ let make = (
|
|
|
117
117
|
actions: Actions(["s3:GetObject"]),
|
|
118
118
|
resources: Resource("arn:aws:s3:::" ++ offloadBucket ++ "/*"),
|
|
119
119
|
},
|
|
120
|
+
// Read-only on the bucket itself, purely so a key that is not there
|
|
121
|
+
// says so: S3 answers GET for a missing key with AccessDenied unless
|
|
122
|
+
// the caller may list the bucket, which reports a ref the handler
|
|
123
|
+
// cannot resolve as an IAM problem it does not have.
|
|
124
|
+
{
|
|
125
|
+
sid: "AllowOffloadList",
|
|
126
|
+
effect: Allow,
|
|
127
|
+
actions: Actions(["s3:ListBucket"]),
|
|
128
|
+
resources: Resource("arn:aws:s3:::" ++ offloadBucket),
|
|
129
|
+
},
|
|
120
130
|
],
|
|
121
131
|
)
|
|
122
132
|
->PolicyDocument.toJsonString
|
|
@@ -46,6 +46,7 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, bak
|
|
|
46
46
|
pluginReadModelTableName,
|
|
47
47
|
offloadBucketName
|
|
48
48
|
]).apply(param => {
|
|
49
|
+
let offloadBucket = param[1];
|
|
49
50
|
new (Aws.iam.RolePolicy)(name + "LambdaPolicy", {
|
|
50
51
|
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "LambdaPolicy", [
|
|
51
52
|
{
|
|
@@ -64,7 +65,13 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, bak
|
|
|
64
65
|
Sid: "AllowOffloadGet",
|
|
65
66
|
Effect: "Allow",
|
|
66
67
|
Action: ["s3:GetObject"],
|
|
67
|
-
Resource: "arn:aws:s3:::" +
|
|
68
|
+
Resource: "arn:aws:s3:::" + offloadBucket + "/*"
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
Sid: "AllowOffloadList",
|
|
72
|
+
Effect: "Allow",
|
|
73
|
+
Action: ["s3:ListBucket"],
|
|
74
|
+
Resource: "arn:aws:s3:::" + offloadBucket
|
|
68
75
|
}
|
|
69
76
|
])),
|
|
70
77
|
role: lambdaRole.id
|
|
@@ -216,7 +216,18 @@ let resolveStructure = (
|
|
|
216
216
|
->Option.flatMap(JSON.Decode.string) {
|
|
217
217
|
| None => Promise.resolve(item)
|
|
218
218
|
| Some(key) =>
|
|
219
|
-
fetch(key)
|
|
219
|
+
fetch(key)
|
|
220
|
+
// The bucket answers for the whole platform, so a failure here says which
|
|
221
|
+
// plugin's ref could not be read — otherwise the S3 error names the key
|
|
222
|
+
// and the bucket, and finding the row it came from is a table scan by hand.
|
|
223
|
+
->Promise.catch(e => {
|
|
224
|
+
let plugin = item->str("name")->Option.getOr("<unnamed>")
|
|
225
|
+
JsError.throwWithMessage(
|
|
226
|
+
`offloaded structure for plugin ${plugin} is unreadable at ${key}: ` ++
|
|
227
|
+
e->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown error"),
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
->Promise.then(bytes => {
|
|
220
231
|
let resolved = Dict.fromArray(item->Dict.toArray)
|
|
221
232
|
resolved->Dict.set("structure", JSON.parseOrThrow(bytes))
|
|
222
233
|
Promise.resolve(resolved)
|
|
@@ -337,6 +348,58 @@ let bakeTargetOf = (event: JSON.t): option<bakeTarget> =>
|
|
|
337
348
|
}
|
|
338
349
|
)
|
|
339
350
|
|
|
351
|
+
// ── Registration freshness ───────────────────────────────────────────────────
|
|
352
|
+
// The read model this bake scans is updated asynchronously: the plugin stack
|
|
353
|
+
// publishes a re-detect, the plugin answers with its definition, the projection
|
|
354
|
+
// lands. Invoked seconds after the last stack finished, the scan can still
|
|
355
|
+
// describe the deploy before it — and a manifest baked from that is wrong in the
|
|
356
|
+
// one way nothing downstream can detect, because it is a perfectly well-formed
|
|
357
|
+
// description of the wrong deployment.
|
|
358
|
+
//
|
|
359
|
+
// So the invocation carries the structure key each plugin stack just wrote. That
|
|
360
|
+
// is an equality check rather than an inference from timestamps, and it costs
|
|
361
|
+
// nothing for a plugin that was not redeployed: its key already matches. A caller
|
|
362
|
+
// that supplies no expectations bakes whatever is current — the query paths never
|
|
363
|
+
// send any, and a hand-run bake should not need to.
|
|
364
|
+
let bakeExpectations = (event: JSON.t): dict<string> =>
|
|
365
|
+
event
|
|
366
|
+
->JSON.Decode.object
|
|
367
|
+
->Option.flatMap(o => o->Dict.get("expect"))
|
|
368
|
+
->Option.flatMap(JSON.Decode.object)
|
|
369
|
+
->Option.mapOr(Dict.make(), o =>
|
|
370
|
+
o
|
|
371
|
+
->Dict.toArray
|
|
372
|
+
->Array.filterMap(((plugin, key)) => key->JSON.Decode.string->Option.map(k => (plugin, k)))
|
|
373
|
+
->Dict.fromArray
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
// The offload key a scanned row carries. None for a structure held inline, which
|
|
377
|
+
// on a deployed platform means the row predates offloading — it cannot match an
|
|
378
|
+
// expectation, and saying so beats baking it.
|
|
379
|
+
let structureRefKey = (item: dict<JSON.t>): option<string> =>
|
|
380
|
+
item
|
|
381
|
+
->Dict.get("structure")
|
|
382
|
+
->Option.flatMap(JSON.Decode.object)
|
|
383
|
+
->Option.flatMap(o => o->Dict.get(Reventless.Offload.sentinelKey))
|
|
384
|
+
->Option.flatMap(JSON.Decode.object)
|
|
385
|
+
->Option.flatMap(r => r->Dict.get("key"))
|
|
386
|
+
->Option.flatMap(JSON.Decode.string)
|
|
387
|
+
|
|
388
|
+
// Compared against the collapsed latest version per plugin, the same view the
|
|
389
|
+
// bake itself takes — an older version's row lingering on the table is not the
|
|
390
|
+
// registration anyone is waiting for.
|
|
391
|
+
let pendingRegistrations = (items: array<dict<JSON.t>>, ~expect: dict<string>): array<string> => {
|
|
392
|
+
let current =
|
|
393
|
+
Platform_AdminScan_Ops.latestByName(
|
|
394
|
+
items,
|
|
395
|
+
~nameVersionOf=item => item->str("name"),
|
|
396
|
+
~toEntry=(item, ~name) => item->structureRefKey->Option.map(key => (name, key)),
|
|
397
|
+
)->Dict.fromArray
|
|
398
|
+
expect
|
|
399
|
+
->Dict.toArray
|
|
400
|
+
->Array.filterMap(((plugin, key)) => current->Dict.get(plugin) == Some(key) ? None : Some(plugin))
|
|
401
|
+
}
|
|
402
|
+
|
|
340
403
|
// Every failure mode here is the deployment's own mistake — a name matching no
|
|
341
404
|
// component, a structure too old to read, a bucket the function may not write —
|
|
342
405
|
// and every one of them produces the same symptom if swallowed: a shop that
|
|
@@ -404,22 +467,36 @@ let handler = async (event: JSON.t): array<JSON.t> => {
|
|
|
404
467
|
let fetch = Reventless.Offload.cachedFetch(key =>
|
|
405
468
|
AwsSdk.S3.GetObjectCommand.getString(~bucket, ~key)
|
|
406
469
|
)
|
|
407
|
-
let
|
|
470
|
+
let resolveAll = () => Promise.all(rawItems->Array.map(item => resolveStructure(fetch, item)))
|
|
408
471
|
switch bakeTarget {
|
|
409
472
|
| Some(target) =>
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
473
|
+
// Checked on the raw rows: the refs are what the deploy can predict, and a
|
|
474
|
+
// row that is behind should not have its structure fetched at all.
|
|
475
|
+
switch pendingRegistrations(rawItems, ~expect=bakeExpectations(event)) {
|
|
476
|
+
| [] =>
|
|
477
|
+
// The built-in admin entry is deliberately absent: it never enters the
|
|
478
|
+
// Plugin read model, and the in-memory bake curates the composed plugins
|
|
479
|
+
// only. A deployment naming it gets `UnknownPlugin`, on both platforms.
|
|
480
|
+
let structures = Platform_AdminScan_Ops.latestByName(
|
|
481
|
+
await resolveAll(),
|
|
482
|
+
~nameVersionOf=item => item->str("name"),
|
|
483
|
+
~toEntry=structureOf,
|
|
484
|
+
)
|
|
485
|
+
await runBake(~target, ~structures)
|
|
486
|
+
| pending =>
|
|
487
|
+
// Not an error — the deploy just has not finished arriving. Reported so the
|
|
488
|
+
// caller can invoke again rather than bake the previous deployment.
|
|
489
|
+
[
|
|
490
|
+
Dict.fromArray([
|
|
491
|
+
("baked", JSON.Encode.bool(false)),
|
|
492
|
+
("pending", pending->Array.map(JSON.Encode.string)->JSON.Encode.array),
|
|
493
|
+
])->JSON.Encode.object,
|
|
494
|
+
]
|
|
495
|
+
}
|
|
419
496
|
| None =>
|
|
420
497
|
let userEntries =
|
|
421
498
|
Platform_AdminScan_Ops.latestByName(
|
|
422
|
-
|
|
499
|
+
await resolveAll(),
|
|
423
500
|
~nameVersionOf=item => item->str("name"),
|
|
424
501
|
~toEntry,
|
|
425
502
|
)
|
|
@@ -6,8 +6,11 @@ import * as S3$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/S3.res.mjs";
|
|
|
6
6
|
import * as Nodepath from "node:path";
|
|
7
7
|
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
8
8
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
9
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
9
10
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
10
11
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
12
|
+
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
13
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
11
14
|
import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
|
|
12
15
|
import * as ClientS3 from "@aws-sdk/client-s3";
|
|
13
16
|
import * as Offload$Reventless from "@reventlessdev/reventless-spec/src/semantic/Offload.res.mjs";
|
|
@@ -216,7 +219,10 @@ function resolveStructure(fetch, item) {
|
|
|
216
219
|
}
|
|
217
220
|
let key = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(refJson), r => r["key"]), Stdlib_JSON.Decode.string);
|
|
218
221
|
if (key !== undefined) {
|
|
219
|
-
return fetch(key)
|
|
222
|
+
return Stdlib_Promise.$$catch(fetch(key), e => {
|
|
223
|
+
let plugin = Stdlib_Option.getOr(str(item, "name"), "<unnamed>");
|
|
224
|
+
return Stdlib_JsError.throwWithMessage(`offloaded structure for plugin ` + plugin + ` is unreadable at ` + key + `: ` + Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(e), Stdlib_JsExn.message), "unknown error"));
|
|
225
|
+
}).then(bytes => {
|
|
220
226
|
let resolved = Object.fromEntries(Object.entries(item));
|
|
221
227
|
resolved["structure"] = JSON.parse(bytes);
|
|
222
228
|
return Promise.resolve(resolved);
|
|
@@ -295,6 +301,35 @@ function bakeTargetOf(event) {
|
|
|
295
301
|
});
|
|
296
302
|
}
|
|
297
303
|
|
|
304
|
+
function bakeExpectations(event) {
|
|
305
|
+
return Stdlib_Option.mapOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(event), o => o["expect"]), Stdlib_JSON.Decode.object), {}, o => Object.fromEntries(Stdlib_Array.filterMap(Object.entries(o), param => {
|
|
306
|
+
let plugin = param[0];
|
|
307
|
+
return Stdlib_Option.map(Stdlib_JSON.Decode.string(param[1]), k => [
|
|
308
|
+
plugin,
|
|
309
|
+
k
|
|
310
|
+
]);
|
|
311
|
+
})));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function structureRefKey(item) {
|
|
315
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(item["structure"], Stdlib_JSON.Decode.object), o => o[Offload$Reventless.sentinelKey]), Stdlib_JSON.Decode.object), r => r["key"]), Stdlib_JSON.Decode.string);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function pendingRegistrations(items, expect) {
|
|
319
|
+
let current = Object.fromEntries(Platform_AdminScan_Ops$ReventlessAws.latestByName(items, item => str(item, "name"), (item, name) => Stdlib_Option.map(structureRefKey(item), key => [
|
|
320
|
+
name,
|
|
321
|
+
key
|
|
322
|
+
])));
|
|
323
|
+
return Stdlib_Array.filterMap(Object.entries(expect), param => {
|
|
324
|
+
let plugin = param[0];
|
|
325
|
+
if (Primitive_object.equal(current[plugin], param[1])) {
|
|
326
|
+
return;
|
|
327
|
+
} else {
|
|
328
|
+
return plugin;
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
298
333
|
async function runBake(target, structures) {
|
|
299
334
|
let selections = bakeSelections();
|
|
300
335
|
if (selections.length === 0) {
|
|
@@ -351,12 +386,25 @@ async function handler(event) {
|
|
|
351
386
|
]]));
|
|
352
387
|
let bucket = Stdlib_Option.getOr(process.env["OFFLOAD_BUCKET"], "");
|
|
353
388
|
let fetch = Offload$Reventless.cachedFetch(key => S3$AwsSdk.GetObjectCommand.getString(bucket, key));
|
|
354
|
-
let
|
|
389
|
+
let resolveAll = () => Promise.all(rawItems.map(item => resolveStructure(fetch, item)));
|
|
355
390
|
if (bakeTarget !== undefined) {
|
|
356
|
-
let
|
|
391
|
+
let pending = pendingRegistrations(rawItems, bakeExpectations(event));
|
|
392
|
+
if (pending.length !== 0) {
|
|
393
|
+
return [Object.fromEntries([
|
|
394
|
+
[
|
|
395
|
+
"baked",
|
|
396
|
+
false
|
|
397
|
+
],
|
|
398
|
+
[
|
|
399
|
+
"pending",
|
|
400
|
+
pending.map(prim => prim)
|
|
401
|
+
]
|
|
402
|
+
])];
|
|
403
|
+
}
|
|
404
|
+
let structures = Platform_AdminScan_Ops$ReventlessAws.latestByName(await resolveAll(), item => str(item, "name"), structureOf);
|
|
357
405
|
return await runBake(bakeTarget, structures);
|
|
358
406
|
}
|
|
359
|
-
let userEntries = Platform_AdminScan_Ops$ReventlessAws.latestByName(
|
|
407
|
+
let userEntries = Platform_AdminScan_Ops$ReventlessAws.latestByName(await resolveAll(), item => str(item, "name"), toEntry);
|
|
360
408
|
return admin.concat(userEntries);
|
|
361
409
|
}
|
|
362
410
|
console.error("Platform_ComponentDefinitions: PLUGIN_RM_TABLE env var not set");
|
|
@@ -386,6 +434,9 @@ export {
|
|
|
386
434
|
bakeSelection,
|
|
387
435
|
bakeSelections,
|
|
388
436
|
bakeTargetOf,
|
|
437
|
+
bakeExpectations,
|
|
438
|
+
structureRefKey,
|
|
439
|
+
pendingRegistrations,
|
|
389
440
|
runBake,
|
|
390
441
|
handler,
|
|
391
442
|
}
|
|
@@ -186,6 +186,20 @@ let heartbeatConfigRef: ref<heartbeatConfig> = ref({
|
|
|
186
186
|
let registerHeartbeatConfig = (~pluginId, ~heartbeatTimeout, ~epQueueUrl=?, ()) =>
|
|
187
187
|
heartbeatConfigRef := {pluginId, heartbeatTimeout, epQueueUrl}
|
|
188
188
|
|
|
189
|
+
// Resolves once this plugin's EventCollector Lambda exists at its new code.
|
|
190
|
+
// `Platform.deployPlugin` gates the deploy-time re-detect on it: the re-detect
|
|
191
|
+
// only asks the platform to re-run the connect handshake, and this is the Lambda
|
|
192
|
+
// that ANSWERS it, with the `pluginDefinition` its deployed code carries. Fired
|
|
193
|
+
// before this Lambda is updated, the plugin answers with the previous deploy's
|
|
194
|
+
// definition, `Connect` sees a definition it already holds and emits nothing, and
|
|
195
|
+
// the row keeps the old structure — with no second re-detect to correct it.
|
|
196
|
+
//
|
|
197
|
+
// A bare `Output<unit>` rather than an `option<Output<_>>`: an Output is a
|
|
198
|
+
// JS Proxy, and wrapping one in a ReScript option produces the nested-Some
|
|
199
|
+
// sentinel instead of the value. Empty until `forPluginEventCollector` runs,
|
|
200
|
+
// which the gate reads only after `P.make()` has returned.
|
|
201
|
+
let eventCollectorReadyRef: ref<Pulumi.Output.t<unit>> = ref(Pulumi.Output.make())
|
|
202
|
+
|
|
189
203
|
// Per-flavor commandHandlerConfig override for the two DCB command-handler Lambdas
|
|
190
204
|
// (`<Plugin>DcbCmdHandler` sync / `<Plugin>DcbAsyncCmdHandler` async); populated by
|
|
191
205
|
// Platform.MakeWithConfig from `commandHandlerConfig.stateChanges.{sync,async}`.
|
|
@@ -624,6 +638,8 @@ module Make = (
|
|
|
624
638
|
~opts,
|
|
625
639
|
)
|
|
626
640
|
|
|
641
|
+
eventCollectorReadyRef := runtime.parts.lambda->Pulumi.Output.apply(_ => ())
|
|
642
|
+
|
|
627
643
|
// Admin-only cross-plugin SNS subscription permissions. The admin EC
|
|
628
644
|
// Lambda's manageSubscriptions hook (Phase 3 Step 1) creates SNS
|
|
629
645
|
// subscriptions at runtime as plugins connect/disconnect; that needs
|
|
@@ -149,6 +149,10 @@ function registerHeartbeatConfig(pluginId, heartbeatTimeout, epQueueUrl, param)
|
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
let eventCollectorReadyRef = {
|
|
153
|
+
contents: Pulumi.output()
|
|
154
|
+
};
|
|
155
|
+
|
|
152
156
|
let syncStateChangesConfigRef = {
|
|
153
157
|
contents: {}
|
|
154
158
|
};
|
|
@@ -381,6 +385,7 @@ function Make(EventCollectorChannel) {
|
|
|
381
385
|
eventTopics: eventTopics,
|
|
382
386
|
resources: resources
|
|
383
387
|
}], runtime, opts);
|
|
388
|
+
eventCollectorReadyRef.contents = runtime.parts.lambda.apply(param => {});
|
|
384
389
|
let isAdminEventCollector = Stdlib_Option.isNone(Plugin_Helpers$ReventlessCore.eventCollectorContextRef.contents[name]);
|
|
385
390
|
if (isAdminEventCollector) {
|
|
386
391
|
new (Aws.iam.RolePolicy)(name + `-snsManageSubs`, {
|
|
@@ -542,6 +547,7 @@ export {
|
|
|
542
547
|
registerDcbTableName,
|
|
543
548
|
heartbeatConfigRef,
|
|
544
549
|
registerHeartbeatConfig,
|
|
550
|
+
eventCollectorReadyRef,
|
|
545
551
|
syncStateChangesConfigRef,
|
|
546
552
|
asyncStateChangesConfigRef,
|
|
547
553
|
setStateChangesConfig,
|
|
@@ -240,3 +240,113 @@ describe("bakeSelection", () => {
|
|
|
240
240
|
expect(sel(`{"views": ["Orders"]}`)->Option.isNone)->toBe(true)
|
|
241
241
|
)
|
|
242
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) {
|
|
@@ -151,6 +154,90 @@ globalThis.describe("bakeSelection", () => {
|
|
|
151
154
|
});
|
|
152
155
|
});
|
|
153
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
|
+
|
|
154
241
|
export {
|
|
155
242
|
entry,
|
|
156
243
|
members,
|