@reventlessdev/reventless-aws 3.0.0-alpha.288 → 3.0.0-alpha.291
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -2
- package/package.json +8 -8
- package/src/Platform.res +60 -8
- package/src/Platform.res.mjs +48 -10
- package/src/adapter/Api/Platform_AdminScan_Ops.res +7 -3
- package/src/adapter/Api/Platform_AdminScan_Ops.res.mjs +2 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +51 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +21 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +169 -4
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +127 -6
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +7 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +2 -0
- package/src/util/Util_OwnerScopeEnv.res +48 -0
- package/src/util/Util_OwnerScopeEnv.res.mjs +31 -0
- package/src/util/Util_ShellConfig.res +18 -3
- package/src/util/Util_ShellConfig.res.mjs +5 -1
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res +64 -0
- package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res.mjs +43 -0
- package/tests/Util_OwnerScopeEnvTest.res +60 -0
- package/tests/Util_OwnerScopeEnvTest.res.mjs +62 -0
- package/tests/Util_ShellConfigTest.res +41 -0
- package/tests/Util_ShellConfigTest.res.mjs +51 -5
package/CHANGELOG.md
CHANGED
|
@@ -3,12 +3,25 @@
|
|
|
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.
|
|
6
|
+
# 3.0.0-alpha.291 (2026-08-13)
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **aws:** bake the component manifest after every plugin stack is up ([2fff09a](https://github.com/ReventlessDev/reventless-core/commit/2fff09aa32b77cc8e33a65414c58c12026ca009e))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.290 (2026-08-13)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** move past a version number that already names other content ([bffba4f](https://github.com/ReventlessDev/reventless-core/commit/bffba4f15eff4a9c30bbf2e28484bb549b865cfc))
|
|
9
18
|
|
|
10
19
|
|
|
20
|
+
# 3.0.0-alpha.288 (2026-08-12)
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
11
23
|
|
|
24
|
+
* **aws:** carry the elevated groups into every Lambda ([ced4d92](https://github.com/ReventlessDev/reventless-core/commit/ced4d9211c3cebe70702757ab331c8a1fa773fca))
|
|
12
25
|
|
|
13
26
|
|
|
14
27
|
# 3.0.0-alpha.287 (2026-08-12)
|
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.291",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -12,18 +12,18 @@
|
|
|
12
12
|
"@aws-sdk/s3-request-presigner": "3.970.0",
|
|
13
13
|
"sury": "11.0.0-alpha.4",
|
|
14
14
|
"uuid": "^13.0.0",
|
|
15
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.
|
|
15
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.8",
|
|
16
16
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.5",
|
|
18
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.72",
|
|
17
19
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
18
|
-
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.71",
|
|
19
|
-
"@reventlessdev/rescript-node": "2.0.0-alpha.4",
|
|
20
20
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
21
21
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
22
|
-
"@reventlessdev/reventless-
|
|
23
|
-
"@reventlessdev/reventless-
|
|
24
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.93",
|
|
22
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.138",
|
|
23
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.230",
|
|
25
24
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
26
|
-
"@reventlessdev/reventless-
|
|
25
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.94",
|
|
26
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.112"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -1454,17 +1454,24 @@ module MakeWithConfig = (
|
|
|
1454
1454
|
// Also register the Plugin RM table with the AllAggregates Lambda runtime
|
|
1455
1455
|
// so its in-Lambda plugin status gate can read plugin status at command
|
|
1456
1456
|
// dispatch time (Part 2.3 of the resolver plan).
|
|
1457
|
-
|
|
1457
|
+
// Handed back so the host-UI half of this deploy can grant the bake its write
|
|
1458
|
+
// into the bucket it creates, and so the function name can be exported for
|
|
1459
|
+
// the post-deploy step. `None` when there is no Plugin RM to scan — there is
|
|
1460
|
+
// then no function, and nothing downstream to grant.
|
|
1461
|
+
let componentDefinitions = switch pluginReadModelTableName {
|
|
1458
1462
|
| Some(tableName) =>
|
|
1459
1463
|
AggregateRuntime_Builder_Single.setPluginReadModelTable(~name=tableName)
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1464
|
+
Some(
|
|
1465
|
+
Platform_ComponentDefinitions_Lambda.make(
|
|
1466
|
+
~api=platformApi,
|
|
1467
|
+
~pluginReadModelTableName=tableName,
|
|
1468
|
+
~offloadBucketName,
|
|
1469
|
+
~schemaReady=admin.adminSchemaPushed,
|
|
1470
|
+
~bakedManifest=?hostUiBundle->Option.flatMap(cfg => cfg.bakedManifest),
|
|
1471
|
+
~opts={},
|
|
1472
|
+
),
|
|
1466
1473
|
)
|
|
1467
|
-
| None =>
|
|
1474
|
+
| None => None
|
|
1468
1475
|
}
|
|
1469
1476
|
|
|
1470
1477
|
// Mount the Platform_UIFragments Lambda resolver — scans the UiFragments
|
|
@@ -2163,6 +2170,7 @@ module MakeWithConfig = (
|
|
|
2163
2170
|
Util_ShellConfig.fields(
|
|
2164
2171
|
~computed=withEvents,
|
|
2165
2172
|
~viewModes=?cfg.viewModes,
|
|
2173
|
+
~bakedManifest=?cfg.bakedManifest,
|
|
2166
2174
|
~shellConfig=?cfg.shellConfig,
|
|
2167
2175
|
)
|
|
2168
2176
|
->JSON.Encode.object
|
|
@@ -2202,6 +2210,50 @@ module MakeWithConfig = (
|
|
|
2202
2210
|
)
|
|
2203
2211
|
}
|
|
2204
2212
|
|
|
2213
|
+
// The bake's write, granted here because here is where both facts are
|
|
2214
|
+
// known: which bucket serves the shell, and that this deployment declared a
|
|
2215
|
+
// manifest at all. Scoped to the one key the deploy told the shell to fetch
|
|
2216
|
+
// — a function that may rewrite the bundle would be a much larger grant
|
|
2217
|
+
// than "keep one generated file current".
|
|
2218
|
+
//
|
|
2219
|
+
// Exported alongside so the post-deploy step has the function and the
|
|
2220
|
+
// target without reading either out of the deploy program.
|
|
2221
|
+
switch (componentDefinitions, cfg.bakedManifest) {
|
|
2222
|
+
| (Some(fn), Some(bake)) =>
|
|
2223
|
+
let manifestKey = bake.key->Option.getOr(
|
|
2224
|
+
ReventlessCore.Platform_BakedManifest.defaultKey,
|
|
2225
|
+
)
|
|
2226
|
+
let _ =
|
|
2227
|
+
(fn.roleId, bucketName)
|
|
2228
|
+
->Pulumi.Output.all2
|
|
2229
|
+
->Pulumi.Output.apply(((roleId, bucket)) => {
|
|
2230
|
+
open PulumiAws.PolicyDocument
|
|
2231
|
+
let _ = PulumiAws.IAM.RolePolicy.make(
|
|
2232
|
+
~name="PlatformUIDefinitionsBake",
|
|
2233
|
+
~args={
|
|
2234
|
+
PulumiAws.IAM.RolePolicy.policy: PulumiAws.PolicyDocument.make(
|
|
2235
|
+
~id="PlatformUIDefinitionsBakePolicy",
|
|
2236
|
+
~statements=[
|
|
2237
|
+
{
|
|
2238
|
+
sid: "AllowWriteBakedManifest",
|
|
2239
|
+
effect: Allow,
|
|
2240
|
+
actions: Actions(["s3:PutObject"]),
|
|
2241
|
+
resources: Resource(`arn:aws:s3:::${bucket}/${manifestKey}`),
|
|
2242
|
+
},
|
|
2243
|
+
],
|
|
2244
|
+
)
|
|
2245
|
+
->PulumiAws.PolicyDocument.toJsonString
|
|
2246
|
+
->Pulumi.Input.make,
|
|
2247
|
+
role: roleId->Pulumi.Input.make,
|
|
2248
|
+
},
|
|
2249
|
+
)
|
|
2250
|
+
})
|
|
2251
|
+
Pulumi.Pulumi.export("bakedManifestFunction", fn.functionName)
|
|
2252
|
+
Pulumi.Pulumi.export("bakedManifestBucket", bucketName)
|
|
2253
|
+
Pulumi.Pulumi.export("bakedManifestKey", Pulumi.Output.make(manifestKey))
|
|
2254
|
+
| _ => ()
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2205
2257
|
Pulumi.Pulumi.export("hostShellUrl", distributionUrl)
|
|
2206
2258
|
}
|
|
2207
2259
|
|
package/src/Platform.res.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import * as Scheduler$ReventlessAws from "./components/Scheduler.res.mjs";
|
|
|
22
22
|
import * as Aggregate$ReventlessCore from "@reventlessdev/reventless-core/src/components/Aggregate/Aggregate.res.mjs";
|
|
23
23
|
import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
|
|
24
24
|
import * as DcbBackend$ReventlessAws from "./adapter/DcbEventLog/DcbBackend.res.mjs";
|
|
25
|
+
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
25
26
|
import * as ReadModel$ReventlessCore from "@reventlessdev/reventless-core/src/components/ReadModel/ReadModel.res.mjs";
|
|
26
27
|
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
27
28
|
import * as Util_Bundle$ReventlessAws from "./util/Util_Bundle.res.mjs";
|
|
@@ -70,6 +71,7 @@ import * as AutomationSlice_Builder$ReventlessAws from "./components/AutomationS
|
|
|
70
71
|
import * as EventTopicPublisher_SNS$ReventlessAws from "./adapter/EventTopic/EventTopicPublisher_SNS.res.mjs";
|
|
71
72
|
import * as ExtensionPointMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionPointMapping.res.mjs";
|
|
72
73
|
import * as PgQueryResolver_Builder$ReventlessAws from "./adapter/QueryDb/PgQueryResolver_Builder.res.mjs";
|
|
74
|
+
import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
|
|
73
75
|
import * as UiFragments_Projection$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments_Projection.res.mjs";
|
|
74
76
|
import * as Aggregate_Builder_Single$ReventlessAws from "./components/Aggregate_Builder_Single.res.mjs";
|
|
75
77
|
import * as ReadModel_Builder_Single$ReventlessAws from "./components/ReadModel_Builder_Single.res.mjs";
|
|
@@ -859,10 +861,7 @@ function MakeWithConfig(Config) {
|
|
|
859
861
|
restrictPublicBuckets: true
|
|
860
862
|
});
|
|
861
863
|
Pulumi$Pulumi.$$export("offloadBucket", offloadBucketName);
|
|
862
|
-
|
|
863
|
-
AggregateRuntime_Builder_Single$ReventlessAws.setPluginReadModelTable(pluginReadModelTableName);
|
|
864
|
-
Platform_ComponentDefinitions_Lambda$ReventlessAws.make(platformApi, pluginReadModelTableName, offloadBucketName, admin.adminSchemaPushed, {});
|
|
865
|
-
}
|
|
864
|
+
let componentDefinitions = pluginReadModelTableName !== undefined ? (AggregateRuntime_Builder_Single$ReventlessAws.setPluginReadModelTable(pluginReadModelTableName), Platform_ComponentDefinitions_Lambda$ReventlessAws.make(platformApi, pluginReadModelTableName, offloadBucketName, admin.adminSchemaPushed, Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), {})) : undefined;
|
|
866
865
|
let rm = admin.stateViewSlicesOutputs["UiFragments"];
|
|
867
866
|
if (rm !== undefined) {
|
|
868
867
|
let r$1 = rm.queryDb.resources[0];
|
|
@@ -1189,7 +1188,7 @@ function MakeWithConfig(Config) {
|
|
|
1189
1188
|
AppSync_EventsApi$ReventlessAws.clientNamespaceName
|
|
1190
1189
|
]
|
|
1191
1190
|
]) : computed;
|
|
1192
|
-
return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.shellConfig));
|
|
1191
|
+
return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.shellConfig));
|
|
1193
1192
|
});
|
|
1194
1193
|
new (Aws.s3.BucketObject)("host-ui-config-json", {
|
|
1195
1194
|
bucket: bucketName,
|
|
@@ -1207,6 +1206,27 @@ function MakeWithConfig(Config) {
|
|
|
1207
1206
|
contentType: "application/json"
|
|
1208
1207
|
});
|
|
1209
1208
|
}
|
|
1209
|
+
let match$5 = hostUiBundle.bakedManifest;
|
|
1210
|
+
if (componentDefinitions !== undefined && match$5 !== undefined) {
|
|
1211
|
+
let manifestKey = Stdlib_Option.getOr(match$5.key, Platform_BakedManifest$ReventlessCore.defaultKey);
|
|
1212
|
+
Pulumi.all([
|
|
1213
|
+
componentDefinitions.roleId,
|
|
1214
|
+
bucketName
|
|
1215
|
+
]).apply(param => {
|
|
1216
|
+
new (Aws.iam.RolePolicy)("PlatformUIDefinitionsBake", {
|
|
1217
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "PlatformUIDefinitionsBakePolicy", [{
|
|
1218
|
+
Sid: "AllowWriteBakedManifest",
|
|
1219
|
+
Effect: "Allow",
|
|
1220
|
+
Action: ["s3:PutObject"],
|
|
1221
|
+
Resource: `arn:aws:s3:::` + param[1] + `/` + manifestKey
|
|
1222
|
+
}])),
|
|
1223
|
+
role: param[0]
|
|
1224
|
+
});
|
|
1225
|
+
});
|
|
1226
|
+
Pulumi$Pulumi.$$export("bakedManifestFunction", componentDefinitions.functionName);
|
|
1227
|
+
Pulumi$Pulumi.$$export("bakedManifestBucket", bucketName);
|
|
1228
|
+
Pulumi$Pulumi.$$export("bakedManifestKey", Pulumi.output(manifestKey));
|
|
1229
|
+
}
|
|
1210
1230
|
Pulumi$Pulumi.$$export("hostShellUrl", match$4.distributionUrl);
|
|
1211
1231
|
}
|
|
1212
1232
|
return Pulumi$Pulumi.getOutputs();
|
|
@@ -2098,10 +2118,7 @@ function Make($star) {
|
|
|
2098
2118
|
restrictPublicBuckets: true
|
|
2099
2119
|
});
|
|
2100
2120
|
Pulumi$Pulumi.$$export("offloadBucket", offloadBucketName);
|
|
2101
|
-
|
|
2102
|
-
AggregateRuntime_Builder_Single$ReventlessAws.setPluginReadModelTable(pluginReadModelTableName);
|
|
2103
|
-
Platform_ComponentDefinitions_Lambda$ReventlessAws.make(platformApi, pluginReadModelTableName, offloadBucketName, admin.adminSchemaPushed, {});
|
|
2104
|
-
}
|
|
2121
|
+
let componentDefinitions = pluginReadModelTableName !== undefined ? (AggregateRuntime_Builder_Single$ReventlessAws.setPluginReadModelTable(pluginReadModelTableName), Platform_ComponentDefinitions_Lambda$ReventlessAws.make(platformApi, pluginReadModelTableName, offloadBucketName, admin.adminSchemaPushed, Stdlib_Option.flatMap(hostUiBundle, cfg => cfg.bakedManifest), {})) : undefined;
|
|
2105
2122
|
let rm = admin.stateViewSlicesOutputs["UiFragments"];
|
|
2106
2123
|
if (rm !== undefined) {
|
|
2107
2124
|
let r$1 = rm.queryDb.resources[0];
|
|
@@ -2422,7 +2439,7 @@ function Make($star) {
|
|
|
2422
2439
|
AppSync_EventsApi$ReventlessAws.clientNamespaceName
|
|
2423
2440
|
]
|
|
2424
2441
|
]) : computed;
|
|
2425
|
-
return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.shellConfig));
|
|
2442
|
+
return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.shellConfig));
|
|
2426
2443
|
});
|
|
2427
2444
|
new (Aws.s3.BucketObject)("host-ui-config-json", {
|
|
2428
2445
|
bucket: bucketName,
|
|
@@ -2440,6 +2457,27 @@ function Make($star) {
|
|
|
2440
2457
|
contentType: "application/json"
|
|
2441
2458
|
});
|
|
2442
2459
|
}
|
|
2460
|
+
let match$5 = hostUiBundle.bakedManifest;
|
|
2461
|
+
if (componentDefinitions !== undefined && match$5 !== undefined) {
|
|
2462
|
+
let manifestKey = Stdlib_Option.getOr(match$5.key, Platform_BakedManifest$ReventlessCore.defaultKey);
|
|
2463
|
+
Pulumi.all([
|
|
2464
|
+
componentDefinitions.roleId,
|
|
2465
|
+
bucketName
|
|
2466
|
+
]).apply(param => {
|
|
2467
|
+
new (Aws.iam.RolePolicy)("PlatformUIDefinitionsBake", {
|
|
2468
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "PlatformUIDefinitionsBakePolicy", [{
|
|
2469
|
+
Sid: "AllowWriteBakedManifest",
|
|
2470
|
+
Effect: "Allow",
|
|
2471
|
+
Action: ["s3:PutObject"],
|
|
2472
|
+
Resource: `arn:aws:s3:::` + param[1] + `/` + manifestKey
|
|
2473
|
+
}])),
|
|
2474
|
+
role: param[0]
|
|
2475
|
+
});
|
|
2476
|
+
});
|
|
2477
|
+
Pulumi$Pulumi.$$export("bakedManifestFunction", componentDefinitions.functionName);
|
|
2478
|
+
Pulumi$Pulumi.$$export("bakedManifestBucket", bucketName);
|
|
2479
|
+
Pulumi$Pulumi.$$export("bakedManifestKey", Pulumi.output(manifestKey));
|
|
2480
|
+
}
|
|
2443
2481
|
Pulumi$Pulumi.$$export("hostShellUrl", match$4.distributionUrl);
|
|
2444
2482
|
}
|
|
2445
2483
|
return Pulumi$Pulumi.getOutputs();
|
|
@@ -108,13 +108,17 @@ let compareVersions = (a: string, b: string): int => {
|
|
|
108
108
|
// `nameVersionOf` reads the row's `name@version` string; `toEntry` builds the
|
|
109
109
|
// output entry for a surviving row (returning None drops a malformed row). The
|
|
110
110
|
// name/version split mirrors ReventlessCore.Plugin.name / compareVersions.
|
|
111
|
+
// Generic in what an entry is: the GraphQL fields collapse to encoded JSON, the
|
|
112
|
+
// bake collapses to the decoded structure it curates. One version-collapse rule
|
|
113
|
+
// either way — a second copy would be a second chance to disagree about which
|
|
114
|
+
// version of a plugin is the deployed one.
|
|
111
115
|
let latestByName = (
|
|
112
116
|
items: array<dict<JSON.t>>,
|
|
113
117
|
~nameVersionOf: dict<JSON.t> => option<string>,
|
|
114
|
-
~toEntry: (dict<JSON.t>, ~name: string) => option<
|
|
115
|
-
): array<
|
|
118
|
+
~toEntry: (dict<JSON.t>, ~name: string) => option<'entry>,
|
|
119
|
+
): array<'entry> => {
|
|
116
120
|
// bare plugin name -> (version, entry); highest version wins.
|
|
117
|
-
let latest: dict<(string,
|
|
121
|
+
let latest: dict<(string, 'entry)> = Dict.make()
|
|
118
122
|
items->Array.forEach(item =>
|
|
119
123
|
switch item->nameVersionOf {
|
|
120
124
|
| None => ()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
4
5
|
import * as LibDynamodb from "@aws-sdk/lib-dynamodb";
|
|
5
6
|
import * as ClientDynamodb from "@aws-sdk/client-dynamodb";
|
|
6
7
|
|
|
@@ -83,7 +84,7 @@ function latestByName(items, nameVersionOf, toEntry) {
|
|
|
83
84
|
}
|
|
84
85
|
latest[name] = [
|
|
85
86
|
version,
|
|
86
|
-
entry
|
|
87
|
+
Primitive_option.valFromOption(entry)
|
|
87
88
|
];
|
|
88
89
|
});
|
|
89
90
|
return Object.values(latest).map(param => param[1]);
|
|
@@ -37,6 +37,22 @@ export function response(ctx) {
|
|
|
37
37
|
}
|
|
38
38
|
`->Pulumi.Input.make
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
What the platform needs back to finish wiring the bake.
|
|
42
|
+
|
|
43
|
+
The manifest is written into the host-UI bucket, and that bucket is created in a
|
|
44
|
+
later half of the same deploy — so the grant cannot be attached here. Handing the
|
|
45
|
+
role back is what lets the caller attach it at the point both facts are known,
|
|
46
|
+
rather than widening this function's write scope to "some bucket, later".
|
|
47
|
+
|
|
48
|
+
`functionName` is exported so the post-deploy step has something to invoke
|
|
49
|
+
without guessing at a generated name.
|
|
50
|
+
*/
|
|
51
|
+
type t = {
|
|
52
|
+
roleId: Pulumi.Output.t<string>,
|
|
53
|
+
functionName: Pulumi.Output.t<string>,
|
|
54
|
+
}
|
|
55
|
+
|
|
40
56
|
let make = (
|
|
41
57
|
~api: Pulumi.Output.t<AppSync.GraphQLApi.t>,
|
|
42
58
|
~pluginReadModelTableName: Pulumi.Output.t<string>,
|
|
@@ -47,8 +63,13 @@ let make = (
|
|
|
47
63
|
// first ships it. `Platform_ComponentDefinitions` predates the gate and is left
|
|
48
64
|
// ungated so this change adds no dependency edge to an already-deployed resource.
|
|
49
65
|
~schemaReady: Pulumi.Output.t<unit>,
|
|
66
|
+
// The deployment's include-list, when it declares one. Carried into the
|
|
67
|
+
// function's environment so the bake curates what the platform program
|
|
68
|
+
// declared and not what its caller asked for — the invocation supplies only
|
|
69
|
+
// where to write.
|
|
70
|
+
~bakedManifest: option<ReventlessInfra.Platform.bakedManifest>=?,
|
|
50
71
|
~opts: Pulumi.ComponentResource.options,
|
|
51
|
-
) => {
|
|
72
|
+
): t => {
|
|
52
73
|
let opts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
53
74
|
let name = "PlatformUIDefinitions"
|
|
54
75
|
|
|
@@ -106,6 +127,28 @@ let make = (
|
|
|
106
127
|
)
|
|
107
128
|
})
|
|
108
129
|
|
|
130
|
+
// The include-list as the handler reads it. Encoded here rather than passed
|
|
131
|
+
// as a record so the deploy input and the runtime input are the same shape —
|
|
132
|
+
// `views`/`commands` absent means "every public component", which a record
|
|
133
|
+
// with explicit nulls would not say.
|
|
134
|
+
let bakeSelectionsJson = switch bakedManifest {
|
|
135
|
+
| None => ""
|
|
136
|
+
| Some(bake) =>
|
|
137
|
+
bake.components
|
|
138
|
+
->Array.map(sel => {
|
|
139
|
+
let entry = Dict.fromArray([("plugin", JSON.Encode.string(sel.plugin))])
|
|
140
|
+
let strings = (key, value) =>
|
|
141
|
+
value->Option.forEach(names =>
|
|
142
|
+
entry->Dict.set(key, names->Array.map(JSON.Encode.string)->JSON.Encode.array)
|
|
143
|
+
)
|
|
144
|
+
strings("views", sel.views)
|
|
145
|
+
strings("commands", sel.commands)
|
|
146
|
+
entry->JSON.Encode.object
|
|
147
|
+
})
|
|
148
|
+
->JSON.Encode.array
|
|
149
|
+
->JSON.stringify
|
|
150
|
+
}
|
|
151
|
+
|
|
109
152
|
let adminEntryJson =
|
|
110
153
|
ReventlessCore.Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
|
|
111
154
|
~pluginId=ReventlessCore.Platform_Admin_Structure.pluginId,
|
|
@@ -167,6 +210,11 @@ let make = (
|
|
|
167
210
|
("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
|
|
168
211
|
("PLUGIN_RM_TABLE", pluginReadModelTableName->Pulumi.Output.asInput),
|
|
169
212
|
("OFFLOAD_BUCKET", offloadBucketName->Pulumi.Output.asInput),
|
|
213
|
+
// Absent declaration ⇒ empty string ⇒ a bake invocation fails saying
|
|
214
|
+
// this platform declares none, rather than writing an empty shop.
|
|
215
|
+
// An include-list is names only, so it stays far from the 4096-byte
|
|
216
|
+
// total the admin entry already pushed out of this dict.
|
|
217
|
+
("BAKE_SELECTIONS", bakeSelectionsJson->Pulumi.Input.make),
|
|
170
218
|
("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
|
|
171
219
|
("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
|
|
172
220
|
Util_LambdaLogging.logLevelEntry(),
|
|
@@ -256,4 +304,6 @@ let make = (
|
|
|
256
304
|
~opts,
|
|
257
305
|
)
|
|
258
306
|
)
|
|
307
|
+
|
|
308
|
+
{roleId: lambdaRole.id, functionName: lambda.name}
|
|
259
309
|
}
|
|
@@ -38,7 +38,7 @@ export function response(ctx) {
|
|
|
38
38
|
}
|
|
39
39
|
`;
|
|
40
40
|
|
|
41
|
-
function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, opts) {
|
|
41
|
+
function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, bakedManifest, opts) {
|
|
42
42
|
let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
43
43
|
let name = "PlatformUIDefinitions";
|
|
44
44
|
let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "Lambda", Pulumi.output(AWS$ReventlessAws.Lambda.principal), AWS_Tags$ReventlessAws.make(name + "Lambda", "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
|
|
@@ -70,6 +70,18 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, opt
|
|
|
70
70
|
role: lambdaRole.id
|
|
71
71
|
}, opts$1);
|
|
72
72
|
});
|
|
73
|
+
let bakeSelectionsJson = bakedManifest !== undefined ? JSON.stringify(bakedManifest.components.map(sel => {
|
|
74
|
+
let entry = Object.fromEntries([[
|
|
75
|
+
"plugin",
|
|
76
|
+
sel.plugin
|
|
77
|
+
]]);
|
|
78
|
+
let strings = (key, value) => Stdlib_Option.forEach(value, names => {
|
|
79
|
+
entry[key] = names.map(prim => prim);
|
|
80
|
+
});
|
|
81
|
+
strings("views", sel.views);
|
|
82
|
+
strings("commands", sel.commands);
|
|
83
|
+
return entry;
|
|
84
|
+
})) : "";
|
|
73
85
|
let adminEntryJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(Platform_Admin_Structure$ReventlessCore.pluginId, Platform_Admin_Structure$ReventlessCore.structure));
|
|
74
86
|
let packageDirs = Object.fromEntries([[
|
|
75
87
|
"@reventlessdev/reventless-aws",
|
|
@@ -104,6 +116,10 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, opt
|
|
|
104
116
|
"OFFLOAD_BUCKET",
|
|
105
117
|
offloadBucketName
|
|
106
118
|
],
|
|
119
|
+
[
|
|
120
|
+
"BAKE_SELECTIONS",
|
|
121
|
+
bakeSelectionsJson
|
|
122
|
+
],
|
|
107
123
|
[
|
|
108
124
|
"NODE_OPTIONS",
|
|
109
125
|
Util_Bundle$ReventlessAws.esmLoaderNodeOptions
|
|
@@ -143,6 +159,10 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, opt
|
|
|
143
159
|
}, opts$1);
|
|
144
160
|
AppSync_Resolver_Native$ReventlessAws.makeUnitJsResolver(name + "Resolver", api, dataSource.name, "Query", "Platform_ComponentDefinitions", resolverCode, opts$1);
|
|
145
161
|
schemaReady.apply(() => AppSync_Resolver_Native$ReventlessAws.makeUnitJsResolver(name + "StructuresResolver", api, dataSource.name, "Query", "Platform_PluginStructures", completeResolverCode, opts$1));
|
|
162
|
+
return {
|
|
163
|
+
roleId: lambdaRole.id,
|
|
164
|
+
functionName: lambda.name
|
|
165
|
+
};
|
|
146
166
|
}
|
|
147
167
|
|
|
148
168
|
export {
|
|
@@ -236,14 +236,162 @@ let isComplete = (event: JSON.t): bool =>
|
|
|
236
236
|
->Option.flatMap(JSON.Decode.bool)
|
|
237
237
|
->Option.getOr(false)
|
|
238
238
|
|
|
239
|
+
// ── Bake mode ────────────────────────────────────────────────────────────────
|
|
240
|
+
// The third thing this function does with the same scan: write the curated
|
|
241
|
+
// manifest as a static object instead of answering a query with it.
|
|
242
|
+
//
|
|
243
|
+
// It belongs here rather than in a tool of its own for the reason the two GraphQL
|
|
244
|
+
// fields already share a data source — there is one place that decides what a
|
|
245
|
+
// deployed plugin's structure is, and the scan, the offload resolution and the
|
|
246
|
+
// version collapse are that decision. A separate reader would be a second copy of
|
|
247
|
+
// all three, free to drift about which version of a plugin is deployed.
|
|
248
|
+
//
|
|
249
|
+
// Invoked directly, after every plugin stack is up: the manifest describes the
|
|
250
|
+
// whole deployment, and no single stack's deploy is the moment that is settled.
|
|
251
|
+
|
|
252
|
+
// The persisted structure is served raw on the query paths — nothing decodes it —
|
|
253
|
+
// but curation filters the RECORD and re-encodes through the shared encoder,
|
|
254
|
+
// which is what makes an include-list naming everything produce the bytes the
|
|
255
|
+
// query returns. So the bake decodes, and heals first: healing fills the list
|
|
256
|
+
// fields an older persisted structure simply has no key for, and those are
|
|
257
|
+
// exactly the fields the schema requires.
|
|
258
|
+
//
|
|
259
|
+
// A structure that still fails to decode fails the bake, naming the plugin. The
|
|
260
|
+
// alternative — skipping it — ships a shop silently missing a section.
|
|
261
|
+
let structureOf = (item: dict<JSON.t>, ~name as _: string): option<(
|
|
262
|
+
string,
|
|
263
|
+
Reventless.Plugin.pluginStructure,
|
|
264
|
+
)> =>
|
|
265
|
+
switch (item->str("name"), item->Dict.get("structure")->Option.flatMap(JSON.Decode.object)) {
|
|
266
|
+
| (Some(pluginId), Some(structure)) =>
|
|
267
|
+
let healed = structure->healStructure->JSON.Encode.object
|
|
268
|
+
switch healed->S.parseJsonOrThrow(Reventless.Plugin.pluginStructureSchema) {
|
|
269
|
+
| decoded => Some((pluginId, decoded))
|
|
270
|
+
| exception _ =>
|
|
271
|
+
JsError.throwWithMessage(
|
|
272
|
+
`baked manifest: the structure persisted for "${pluginId}" cannot be decoded — ` ++
|
|
273
|
+
`it was written by a framework version this platform can no longer read. ` ++
|
|
274
|
+
`Redeploy that plugin, or drop it from the include-list.`,
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
| _ => None
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let bakeSelection = (json: JSON.t): option<ReventlessCore.Platform_BakedManifest.selection> => {
|
|
281
|
+
let strings = (o, key) =>
|
|
282
|
+
o
|
|
283
|
+
->Dict.get(key)
|
|
284
|
+
->Option.flatMap(JSON.Decode.array)
|
|
285
|
+
->Option.map(a => a->Array.filterMap(JSON.Decode.string))
|
|
286
|
+
json
|
|
287
|
+
->JSON.Decode.object
|
|
288
|
+
->Option.flatMap(o =>
|
|
289
|
+
o
|
|
290
|
+
->Dict.get("plugin")
|
|
291
|
+
->Option.flatMap(JSON.Decode.string)
|
|
292
|
+
->Option.map(plugin => {
|
|
293
|
+
ReventlessCore.Platform_BakedManifest.plugin,
|
|
294
|
+
views: strings(o, "views"),
|
|
295
|
+
commands: strings(o, "commands"),
|
|
296
|
+
})
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The declaration travels as an env var because it is deploy input — stated once
|
|
301
|
+
// in the platform program, beside the bucket it is written to. The target travels
|
|
302
|
+
// in the invocation payload instead: the bucket only exists inside the host-UI
|
|
303
|
+
// half of the deploy, long after this function is built, and a caller that had to
|
|
304
|
+
// know the include-list as well would be free to bake something the deployment
|
|
305
|
+
// never declared.
|
|
306
|
+
let bakeSelections = (): array<ReventlessCore.Platform_BakedManifest.selection> =>
|
|
307
|
+
switch NodeProcess.env->Dict.get("BAKE_SELECTIONS") {
|
|
308
|
+
| None | Some("") => []
|
|
309
|
+
| Some(raw) =>
|
|
310
|
+
switch raw->JSON.parseOrThrow->JSON.Decode.array {
|
|
311
|
+
| Some(entries) => entries->Array.filterMap(bakeSelection)
|
|
312
|
+
| None => []
|
|
313
|
+
| exception _ =>
|
|
314
|
+
JsError.throwWithMessage("baked manifest: BAKE_SELECTIONS is not a JSON array")
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
type bakeTarget = {bucket: string, key: string}
|
|
319
|
+
|
|
320
|
+
let bakeTargetOf = (event: JSON.t): option<bakeTarget> =>
|
|
321
|
+
event
|
|
322
|
+
->JSON.Decode.object
|
|
323
|
+
->Option.flatMap(o =>
|
|
324
|
+
switch (
|
|
325
|
+
o->Dict.get("bake")->Option.flatMap(JSON.Decode.bool),
|
|
326
|
+
o->Dict.get("bucket")->Option.flatMap(JSON.Decode.string),
|
|
327
|
+
) {
|
|
328
|
+
| (Some(true), Some(bucket)) =>
|
|
329
|
+
Some({
|
|
330
|
+
bucket,
|
|
331
|
+
key: o
|
|
332
|
+
->Dict.get("key")
|
|
333
|
+
->Option.flatMap(JSON.Decode.string)
|
|
334
|
+
->Option.getOr(ReventlessCore.Platform_BakedManifest.defaultKey),
|
|
335
|
+
})
|
|
336
|
+
| _ => None
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
// Every failure mode here is the deployment's own mistake — a name matching no
|
|
341
|
+
// component, a structure too old to read, a bucket the function may not write —
|
|
342
|
+
// and every one of them produces the same symptom if swallowed: a shop that
|
|
343
|
+
// renders nothing, with no line anywhere saying why. So the bake throws, and the
|
|
344
|
+
// pipeline step that invoked it fails.
|
|
345
|
+
let runBake = async (
|
|
346
|
+
~target: bakeTarget,
|
|
347
|
+
~structures: array<(string, Reventless.Plugin.pluginStructure)>,
|
|
348
|
+
): array<JSON.t> => {
|
|
349
|
+
let selections = bakeSelections()
|
|
350
|
+
if selections->Array.length == 0 {
|
|
351
|
+
JsError.throwWithMessage(
|
|
352
|
+
"baked manifest: BAKE_SELECTIONS is empty — this platform declares no bake, " ++
|
|
353
|
+
"so there is nothing to write and a shell pointed at the file would find none.",
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
switch ReventlessCore.Platform_BakedManifest.curate(~structures, ~selections) {
|
|
357
|
+
| Error(e) =>
|
|
358
|
+
JsError.throwWithMessage(ReventlessCore.Platform_BakedManifest.describe(e))
|
|
359
|
+
| Ok(manifest) =>
|
|
360
|
+
let body = JSON.stringify(manifest, ~space=2)
|
|
361
|
+
let _ = await AwsSdk.S3.PutObjectCommand.make({
|
|
362
|
+
bucket: target.bucket,
|
|
363
|
+
key: target.key,
|
|
364
|
+
body: AwsSdk.S3.PutObjectCommand.bodyFromString(body),
|
|
365
|
+
contentType: "application/json",
|
|
366
|
+
})->AwsSdk.S3.PutObjectCommand.send
|
|
367
|
+
[
|
|
368
|
+
Dict.fromArray([
|
|
369
|
+
("baked", JSON.Encode.bool(true)),
|
|
370
|
+
("bucket", JSON.Encode.string(target.bucket)),
|
|
371
|
+
("key", JSON.Encode.string(target.key)),
|
|
372
|
+
("plugins", JSON.Encode.int(selections->Array.length)),
|
|
373
|
+
("bytes", JSON.Encode.int(body->String.length)),
|
|
374
|
+
])->JSON.Encode.object,
|
|
375
|
+
]
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
239
379
|
let handler = async (event: JSON.t): array<JSON.t> => {
|
|
240
380
|
let complete = isComplete(event)
|
|
381
|
+
let bakeTarget = bakeTargetOf(event)
|
|
241
382
|
let toEntry = (item, ~name) => toEntryWith(~filter=!complete, item, ~name)
|
|
242
383
|
let admin = adminEntry->Option.mapOr([], e => [e])
|
|
243
384
|
switch NodeProcess.env->Dict.get("PLUGIN_RM_TABLE") {
|
|
244
385
|
| None | Some("") =>
|
|
245
386
|
Console.error("Platform_ComponentDefinitions: PLUGIN_RM_TABLE env var not set")
|
|
246
|
-
|
|
387
|
+
switch bakeTarget {
|
|
388
|
+
| Some(_) =>
|
|
389
|
+
JsError.throwWithMessage(
|
|
390
|
+
"baked manifest: PLUGIN_RM_TABLE env var not set — the bake would write an " ++
|
|
391
|
+
"empty shop rather than fail.",
|
|
392
|
+
)
|
|
393
|
+
| None => admin
|
|
394
|
+
}
|
|
247
395
|
| Some(table) =>
|
|
248
396
|
let rawItems = await Platform_AdminScan_Ops.scanAll(
|
|
249
397
|
~tableName=table,
|
|
@@ -257,8 +405,25 @@ let handler = async (event: JSON.t): array<JSON.t> => {
|
|
|
257
405
|
AwsSdk.S3.GetObjectCommand.getString(~bucket, ~key)
|
|
258
406
|
)
|
|
259
407
|
let items = await Promise.all(rawItems->Array.map(item => resolveStructure(fetch, item)))
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
408
|
+
switch bakeTarget {
|
|
409
|
+
| Some(target) =>
|
|
410
|
+
// The built-in admin entry is deliberately absent: it never enters the
|
|
411
|
+
// Plugin read model, and the in-memory bake curates the composed plugins
|
|
412
|
+
// only. A deployment naming it gets `UnknownPlugin`, on both platforms.
|
|
413
|
+
let structures = Platform_AdminScan_Ops.latestByName(
|
|
414
|
+
items,
|
|
415
|
+
~nameVersionOf=item => item->str("name"),
|
|
416
|
+
~toEntry=structureOf,
|
|
417
|
+
)
|
|
418
|
+
await runBake(~target, ~structures)
|
|
419
|
+
| None =>
|
|
420
|
+
let userEntries =
|
|
421
|
+
Platform_AdminScan_Ops.latestByName(
|
|
422
|
+
items,
|
|
423
|
+
~nameVersionOf=item => item->str("name"),
|
|
424
|
+
~toEntry,
|
|
425
|
+
)
|
|
426
|
+
Array.concat(admin, userEntries)
|
|
427
|
+
}
|
|
263
428
|
}
|
|
264
429
|
}
|