@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 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.288 (2026-08-12)
6
+ # 3.0.0-alpha.291 (2026-08-13)
7
7
 
8
- **Note:** Version bump only for package @reventlessdev/reventless-aws
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.288",
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.7",
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-core": "3.0.0-alpha.229",
23
- "@reventlessdev/reventless-infra": "3.0.0-alpha.137",
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-spec": "3.0.0-alpha.111"
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
- switch pluginReadModelTableName {
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
- Platform_ComponentDefinitions_Lambda.make(
1461
- ~api=platformApi,
1462
- ~pluginReadModelTableName=tableName,
1463
- ~offloadBucketName,
1464
- ~schemaReady=admin.adminSchemaPushed,
1465
- ~opts={},
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
 
@@ -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
- if (pluginReadModelTableName !== undefined) {
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
- if (pluginReadModelTableName !== undefined) {
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<JSON.t>,
115
- ): array<JSON.t> => {
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, JSON.t)> = Dict.make()
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
- admin
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
- let userEntries =
261
- Platform_AdminScan_Ops.latestByName(items, ~nameVersionOf=item => item->str("name"), ~toEntry)
262
- Array.concat(admin, userEntries)
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
  }