@reventlessdev/reventless-aws 3.0.0-alpha.301 → 3.0.0-alpha.303

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +7 -7
  3. package/src/Platform.res +18 -4
  4. package/src/Platform.res.mjs +24 -4
  5. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +39 -4
  6. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +34 -13
  7. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +108 -20
  8. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +99 -17
  9. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res +41 -3
  10. package/src/adapter/QueryDb/PgQueryResolver_Lambda.res.mjs +27 -6
  11. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res +34 -4
  12. package/src/adapter/QueryDb/QueryDbResolvers_AppSync.res.mjs +16 -5
  13. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res +10 -0
  14. package/src/adapter/Runtime/PgQueryResolverEntryPoint_Ops.res.mjs +4 -1
  15. package/src/adapter/Runtime/StateTopicPublish.mjs +38 -5
  16. package/src/adapter/StateTopic/StateTopic_AppSync.res +49 -1
  17. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +28 -3
  18. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res +73 -2
  19. package/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs +39 -4
  20. package/src/util/Util_DynamoDbStream.res +15 -4
  21. package/src/util/Util_DynamoDbStream.res.mjs +15 -5
  22. package/src/util/Util_ShellConfig.res +22 -2
  23. package/src/util/Util_ShellConfig.res.mjs +11 -0
  24. package/tests/PgQueryResolver_LambdaTest.res +16 -1
  25. package/tests/PgQueryResolver_LambdaTest.res.mjs +46 -32
  26. package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res +111 -0
  27. package/tests/Platform_ComponentDefinitions_Lambda_OpsTest.res.mjs +67 -0
  28. package/tests/QueryDbResolvers_AppSyncTest.res.mjs +3 -3
  29. package/tests/StateChangeDescriptorParityTest.res +82 -1
  30. package/tests/StateChangeDescriptorParityTest.res.mjs +97 -1
  31. package/tests/Util_DynamoDbStreamTest.res +43 -0
  32. package/tests/Util_DynamoDbStreamTest.res.mjs +41 -0
  33. package/tests/Util_ShellConfigTest.res +107 -0
  34. package/tests/Util_ShellConfigTest.res.mjs +100 -0
  35. package/tests/stateChangeDescriptorParity.mjs +72 -3
package/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
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.303 (2026-08-16)
7
+
8
+ ### Features
9
+
10
+ * **core:** exclude retired rows from reads a caller may not widen ([662f31a](https://github.com/ReventlessDev/reventless-core/commit/662f31abb717bda5154199d349da6dcf8e2d3e78))
11
+ * **core:** let [@retired](https://github.com/retired) name a lifecycle state, not only a boolean ([6bb346b](https://github.com/ReventlessDev/reventless-core/commit/6bb346b4f6a5f33826fc24537953482a76067177))
12
+ * **core:** mark the state that retires a row, and allow more than one ([cb1461f](https://github.com/ReventlessDev/reventless-core/commit/cb1461f024d3ca3b53fd9c8b010a054e3fcc4555))
13
+ * **core:** withhold a retired row's payload from the live channel ([82c62db](https://github.com/ReventlessDev/reventless-core/commit/82c62db00b87f7135378496e36601b544d4d2b62))
14
+
15
+
16
+ # 3.0.0-alpha.302 (2026-08-15)
17
+
18
+ ### Bug Fixes
19
+
20
+ * **aws:** let a stream resource answer for its own stream ([4617773](https://github.com/ReventlessDev/reventless-core/commit/461777349f10ebc83085d59907bac5b5537429ed))
21
+ ### Features
22
+
23
+ * **aws:** serve one baked manifest per journey ([16d6307](https://github.com/ReventlessDev/reventless-core/commit/16d63071f059cf1fc7025f9be9206dda51f71053))
24
+
25
+
6
26
  # 3.0.0-alpha.301 (2026-08-15)
7
27
 
8
28
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.301",
3
+ "version": "3.0.0-alpha.303",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -14,16 +14,16 @@
14
14
  "uuid": "^13.0.0",
15
15
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.11",
16
16
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
- "@reventlessdev/rescript-node": "2.0.0-alpha.7",
18
17
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
19
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.76",
18
+ "@reventlessdev/rescript-node": "2.0.0-alpha.7",
19
+ "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.77",
20
20
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
21
- "@reventlessdev/reventless-core": "3.0.0-alpha.235",
21
+ "@reventlessdev/reventless-core": "3.0.0-alpha.237",
22
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.143",
22
23
  "@reventlessdev/reventless-interop": "3.0.0-alpha.31",
23
24
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
24
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.99",
25
- "@reventlessdev/reventless-spec": "3.0.0-alpha.114",
26
- "@reventlessdev/reventless-infra": "3.0.0-alpha.142"
25
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.101",
26
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.115"
27
27
  },
28
28
  "devDependencies": {
29
29
  "rescript": "12.3.0",
package/src/Platform.res CHANGED
@@ -2221,9 +2221,14 @@ module MakeWithConfig = (
2221
2221
  // target without reading either out of the deploy program.
2222
2222
  switch (componentDefinitions, cfg.bakedManifest) {
2223
2223
  | (Some(fn), Some(bake)) =>
2224
- let manifestKey = bake.key->Option.getOr(
2225
- ReventlessCore.Platform_BakedManifest.defaultKey,
2226
- )
2224
+ // Every file the declaration produces: the default journey's, which is
2225
+ // what the shell's `manifestUrl` points at, and one per declared journey.
2226
+ // Resolved through the same function the bake and `config.json` use, so
2227
+ // a grant cannot be scoped to a key nothing writes.
2228
+ let manifestKeys =
2229
+ ReventlessCore.Platform_BakedManifest.files(~config=bake)->Array.map(((key, _)) => key)
2230
+ let manifestKey =
2231
+ manifestKeys->Array.get(0)->Option.getOr(ReventlessCore.Platform_BakedManifest.defaultKey)
2227
2232
  let _ =
2228
2233
  (fn.roleId, bucketName)
2229
2234
  ->Pulumi.Output.all2
@@ -2239,7 +2244,16 @@ module MakeWithConfig = (
2239
2244
  sid: "AllowWriteBakedManifest",
2240
2245
  effect: Allow,
2241
2246
  actions: Actions(["s3:PutObject"]),
2242
- resources: Resource(`arn:aws:s3:::${bucket}/${manifestKey}`),
2247
+ // A single key stays a bare `Resource` string: the two
2248
+ // forms mean the same thing to IAM, and a deployment that
2249
+ // declares no journey should not see its policy rewritten
2250
+ // for a field it does not use.
2251
+ resources: switch manifestKeys->Array.map(key =>
2252
+ `arn:aws:s3:::${bucket}/${key}`
2253
+ ) {
2254
+ | [only] => Resource(only)
2255
+ | arns => Resources(arns)
2256
+ },
2243
2257
  },
2244
2258
  ],
2245
2259
  )
@@ -1210,17 +1210,27 @@ function MakeWithConfig(Config) {
1210
1210
  }
1211
1211
  let match$5 = hostUiBundle.bakedManifest;
1212
1212
  if (componentDefinitions !== undefined && match$5 !== undefined) {
1213
- let manifestKey = Stdlib_Option.getOr(match$5.key, Platform_BakedManifest$ReventlessCore.defaultKey);
1213
+ let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$5).map(param => param[0]);
1214
+ let manifestKey = Stdlib_Option.getOr(manifestKeys[0], Platform_BakedManifest$ReventlessCore.defaultKey);
1214
1215
  Pulumi.all([
1215
1216
  componentDefinitions.roleId,
1216
1217
  bucketName
1217
1218
  ]).apply(param => {
1219
+ let bucket = param[1];
1220
+ let arns = manifestKeys.map(key => `arn:aws:s3:::` + bucket + `/` + key);
1221
+ let tmp;
1222
+ if (arns.length !== 1) {
1223
+ tmp = arns;
1224
+ } else {
1225
+ let only = arns[0];
1226
+ tmp = only;
1227
+ }
1218
1228
  new (Aws.iam.RolePolicy)("PlatformUIDefinitionsBake", {
1219
1229
  policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "PlatformUIDefinitionsBakePolicy", [{
1220
1230
  Sid: "AllowWriteBakedManifest",
1221
1231
  Effect: "Allow",
1222
1232
  Action: ["s3:PutObject"],
1223
- Resource: `arn:aws:s3:::` + param[1] + `/` + manifestKey
1233
+ Resource: tmp
1224
1234
  }])),
1225
1235
  role: param[0]
1226
1236
  });
@@ -2490,17 +2500,27 @@ function Make($star) {
2490
2500
  }
2491
2501
  let match$5 = hostUiBundle.bakedManifest;
2492
2502
  if (componentDefinitions !== undefined && match$5 !== undefined) {
2493
- let manifestKey = Stdlib_Option.getOr(match$5.key, Platform_BakedManifest$ReventlessCore.defaultKey);
2503
+ let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$5).map(param => param[0]);
2504
+ let manifestKey = Stdlib_Option.getOr(manifestKeys[0], Platform_BakedManifest$ReventlessCore.defaultKey);
2494
2505
  Pulumi.all([
2495
2506
  componentDefinitions.roleId,
2496
2507
  bucketName
2497
2508
  ]).apply(param => {
2509
+ let bucket = param[1];
2510
+ let arns = manifestKeys.map(key => `arn:aws:s3:::` + bucket + `/` + key);
2511
+ let tmp;
2512
+ if (arns.length !== 1) {
2513
+ tmp = arns;
2514
+ } else {
2515
+ let only = arns[0];
2516
+ tmp = only;
2517
+ }
2498
2518
  new (Aws.iam.RolePolicy)("PlatformUIDefinitionsBake", {
2499
2519
  policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "PlatformUIDefinitionsBakePolicy", [{
2500
2520
  Sid: "AllowWriteBakedManifest",
2501
2521
  Effect: "Allow",
2502
2522
  Action: ["s3:PutObject"],
2503
- Resource: `arn:aws:s3:::` + param[1] + `/` + manifestKey
2523
+ Resource: tmp
2504
2524
  }])),
2505
2525
  role: param[0]
2506
2526
  });
@@ -141,10 +141,8 @@ let make = (
141
141
  // as a record so the deploy input and the runtime input are the same shape —
142
142
  // `views`/`commands` absent means "every public component", which a record
143
143
  // with explicit nulls would not say.
144
- let bakeSelectionsJson = switch bakedManifest {
145
- | None => ""
146
- | Some(bake) =>
147
- bake.components
144
+ let encodeSelections = (components: array<ReventlessCore.Platform_BakedManifest.selection>) =>
145
+ components
148
146
  ->Array.map(sel => {
149
147
  let entry = Dict.fromArray([("plugin", JSON.Encode.string(sel.plugin))])
150
148
  let strings = (key, value) =>
@@ -157,6 +155,39 @@ let make = (
157
155
  entry->JSON.Encode.object
158
156
  })
159
157
  ->JSON.Encode.array
158
+
159
+ let bakeSelectionsJson = switch bakedManifest {
160
+ | None => ""
161
+ | Some(bake) =>
162
+ bake.components
163
+ ->Array.map(ReventlessCore.Platform_BakedManifest.toSelection)
164
+ ->encodeSelections
165
+ ->JSON.stringify
166
+ }
167
+
168
+ // The journeys, in a variable of their own rather than folded into the one
169
+ // above: a deployment that declares none then puts the same string in the same
170
+ // place it always did, and its function is not replaced for a field it does
171
+ // not use.
172
+ //
173
+ // Each carries the key the DECLARATION resolves to, taken from the function
174
+ // that also decides the IAM grant and the URL `config.json` publishes. A key
175
+ // the handler derived for itself would be a fourth derivation of one name, and
176
+ // the way it fails is a file written where nothing fetches it.
177
+ let bakeJourneysJson = switch bakedManifest->Option.mapOr([], config =>
178
+ ReventlessCore.Platform_BakedManifest.journeyFiles(~config)
179
+ ) {
180
+ | [] => ""
181
+ | journeys =>
182
+ journeys
183
+ ->Array.map(j =>
184
+ Dict.fromArray([
185
+ ("group", JSON.Encode.string(j.group)),
186
+ ("key", JSON.Encode.string(j.key)),
187
+ ("components", j.selections->encodeSelections),
188
+ ])->JSON.Encode.object
189
+ )
190
+ ->JSON.Encode.array
160
191
  ->JSON.stringify
161
192
  }
162
193
 
@@ -226,6 +257,10 @@ let make = (
226
257
  // An include-list is names only, so it stays far from the 4096-byte
227
258
  // total the admin entry already pushed out of this dict.
228
259
  ("BAKE_SELECTIONS", bakeSelectionsJson->Pulumi.Input.make),
260
+ // One curated surface per audience beside the default one. Empty for
261
+ // a deployment that declares none, which is every deployment that
262
+ // predates journeys.
263
+ ("BAKE_JOURNEYS", bakeJourneysJson->Pulumi.Input.make),
229
264
  ("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
230
265
  ("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
231
266
  Util_LambdaLogging.logLevelEntry(),
@@ -13,6 +13,7 @@ import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
13
13
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
14
14
  import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
15
15
  import * as AppSync_Resolver_Native$ReventlessAws from "./AppSync_Resolver_Native.res.mjs";
16
+ import * as Platform_BakedManifest$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_BakedManifest.res.mjs";
16
17
  import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
17
18
  import * as Platform_ComponentDefinitionsApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_ComponentDefinitionsApi.res.mjs";
18
19
 
@@ -77,19 +78,35 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, bak
77
78
  role: lambdaRole.id
78
79
  }, opts$1);
79
80
  });
80
- let bakeSelectionsJson = bakedManifest !== undefined ? JSON.stringify(bakedManifest.components.map(sel => {
81
- let entry = Object.fromEntries([[
82
- "plugin",
83
- sel.plugin
84
- ]]);
85
- let strings = (key, value) => Stdlib_Option.forEach(value, names => {
86
- entry[key] = names.map(prim => prim);
87
- });
88
- strings("views", sel.views);
89
- strings("commands", sel.commands);
90
- strings("derived", sel.derived);
91
- return entry;
92
- })) : "";
81
+ let encodeSelections = components => components.map(sel => {
82
+ let entry = Object.fromEntries([[
83
+ "plugin",
84
+ sel.plugin
85
+ ]]);
86
+ let strings = (key, value) => Stdlib_Option.forEach(value, names => {
87
+ entry[key] = names.map(prim => prim);
88
+ });
89
+ strings("views", sel.views);
90
+ strings("commands", sel.commands);
91
+ strings("derived", sel.derived);
92
+ return entry;
93
+ });
94
+ let bakeSelectionsJson = bakedManifest !== undefined ? JSON.stringify(encodeSelections(bakedManifest.components.map(Platform_BakedManifest$ReventlessCore.toSelection))) : "";
95
+ let journeys = Stdlib_Option.mapOr(bakedManifest, [], Platform_BakedManifest$ReventlessCore.journeyFiles);
96
+ let bakeJourneysJson = journeys.length !== 0 ? JSON.stringify(journeys.map(j => Object.fromEntries([
97
+ [
98
+ "group",
99
+ j.group
100
+ ],
101
+ [
102
+ "key",
103
+ j.key
104
+ ],
105
+ [
106
+ "components",
107
+ encodeSelections(j.selections)
108
+ ]
109
+ ]))) : "";
93
110
  let adminEntryJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(Platform_Admin_Structure$ReventlessCore.pluginId, undefined, Platform_Admin_Structure$ReventlessCore.structure));
94
111
  let packageDirs = Object.fromEntries([[
95
112
  "@reventlessdev/reventless-aws",
@@ -128,6 +145,10 @@ function make(api, pluginReadModelTableName, offloadBucketName, schemaReady, bak
128
145
  "BAKE_SELECTIONS",
129
146
  bakeSelectionsJson
130
147
  ],
148
+ [
149
+ "BAKE_JOURNEYS",
150
+ bakeJourneysJson
151
+ ],
131
152
  [
132
153
  "NODE_OPTIONS",
133
154
  Util_Bundle$ReventlessAws.esmLoaderNodeOptions
@@ -90,6 +90,24 @@ let mapMembers = (obj: dict<JSON.t>, key: string, f: dict<JSON.t> => unit): unit
90
90
  )
91
91
  }
92
92
 
93
+ // Read-path shim for the `statusField` → `lifecycleField` rename. A structure
94
+ // persisted before the rename carries only the old key, and this handler serves
95
+ // raw JSON, so the resolver would find no `lifecycleField` and answer null until
96
+ // the plugin re-registers. That is not a crash — the SDL declares a nullable
97
+ // `String`, not a `[T!]!` that would null-propagate to the root — but it is a
98
+ // silent degradation: command-menu filtering, board columns, group sections,
99
+ // progress trackers and state diagrams go quiet while lists keep rendering.
100
+ //
101
+ // Delete a release later, once no structure predating the rename can be reached.
102
+ // It closes a window; it does not support two names.
103
+ let fillLifecycleField = (component: dict<JSON.t>): unit =>
104
+ switch (component->Dict.get("lifecycleField"), component->Dict.get("statusField")) {
105
+ | (None | Some(JSON.Null), Some(legacy)) => component->Dict.set("lifecycleField", legacy)
106
+ | _ => ()
107
+ }
108
+
109
+ let readSideCollections = ["readModels", "stateViewSlices"]
110
+
93
111
  let healStructure = (structure: dict<JSON.t>): dict<JSON.t> => {
94
112
  let out = Dict.fromArray(structure->Dict.toArray)
95
113
  // The collections themselves are `[T!]!` too, so an absent one is the same
@@ -103,6 +121,9 @@ let healStructure = (structure: dict<JSON.t>): dict<JSON.t> => {
103
121
  )
104
122
  })
105
123
  )
124
+ readSideCollections->Array.forEach(collection =>
125
+ out->mapMembers(collection, fillLifecycleField)
126
+ )
106
127
  out
107
128
  }
108
129
 
@@ -288,6 +309,18 @@ let structureOf = (item: dict<JSON.t>, ~name as _: string): option<(
288
309
  | _ => None
289
310
  }
290
311
 
312
+ // One curated surface per audience, beside the default one. Only what the write
313
+ // needs travels: the file's key, resolved by the deploy so that the grant, the
314
+ // URL in `config.json` and the object written here are one string, and the
315
+ // include-list that decides its contents. The group the journey serves is
316
+ // carried too, purely so the invocation's report names the audience a file was
317
+ // written for.
318
+ type journey = {
319
+ group: string,
320
+ key: string,
321
+ selections: array<ReventlessCore.Platform_BakedManifest.selection>,
322
+ }
323
+
291
324
  let bakeSelection = (json: JSON.t): option<ReventlessCore.Platform_BakedManifest.selection> => {
292
325
  let strings = (o, key) =>
293
326
  o
@@ -309,6 +342,9 @@ let bakeSelection = (json: JSON.t): option<ReventlessCore.Platform_BakedManifest
309
342
  )
310
343
  }
311
344
 
345
+ let decodeSelections = (json: JSON.t): array<ReventlessCore.Platform_BakedManifest.selection> =>
346
+ json->JSON.Decode.array->Option.getOr([])->Array.filterMap(bakeSelection)
347
+
312
348
  // The declaration travels as an env var because it is deploy input — stated once
313
349
  // in the platform program, beside the bucket it is written to. The target travels
314
350
  // in the invocation payload instead: the bucket only exists inside the host-UI
@@ -319,14 +355,44 @@ let bakeSelections = (): array<ReventlessCore.Platform_BakedManifest.selection>
319
355
  switch NodeProcess.env->Dict.get("BAKE_SELECTIONS") {
320
356
  | None | Some("") => []
321
357
  | Some(raw) =>
322
- switch raw->JSON.parseOrThrow->JSON.Decode.array {
323
- | Some(entries) => entries->Array.filterMap(bakeSelection)
324
- | None => []
358
+ switch raw->JSON.parseOrThrow {
359
+ | json => json->decodeSelections
325
360
  | exception _ =>
326
361
  JsError.throwWithMessage("baked manifest: BAKE_SELECTIONS is not a JSON array")
327
362
  }
328
363
  }
329
364
 
365
+ let bakeJourney = (json: JSON.t): option<journey> =>
366
+ json
367
+ ->JSON.Decode.object
368
+ ->Option.flatMap(o =>
369
+ switch (
370
+ o->Dict.get("group")->Option.flatMap(JSON.Decode.string),
371
+ o->Dict.get("key")->Option.flatMap(JSON.Decode.string),
372
+ ) {
373
+ | (Some(group), Some(key)) =>
374
+ Some({
375
+ group,
376
+ key,
377
+ selections: o->Dict.get("components")->Option.mapOr([], decodeSelections),
378
+ })
379
+ | _ => None
380
+ }
381
+ )
382
+
383
+ // Absent for every deployment that declares one audience, which is what the
384
+ // declaration meant before journeys existed and what it still means.
385
+ let bakeJourneys = (): array<journey> =>
386
+ switch NodeProcess.env->Dict.get("BAKE_JOURNEYS") {
387
+ | None | Some("") => []
388
+ | Some(raw) =>
389
+ switch raw->JSON.parseOrThrow->JSON.Decode.array {
390
+ | Some(entries) => entries->Array.filterMap(bakeJourney)
391
+ | None => []
392
+ | exception _ => JsError.throwWithMessage("baked manifest: BAKE_JOURNEYS is not a JSON array")
393
+ }
394
+ }
395
+
330
396
  type bakeTarget = {bucket: string, key: string}
331
397
 
332
398
  let bakeTargetOf = (event: JSON.t): option<bakeTarget> =>
@@ -417,27 +483,49 @@ let runBake = async (
417
483
  "so there is nothing to write and a shell pointed at the file would find none.",
418
484
  )
419
485
  }
420
- switch ReventlessCore.Platform_BakedManifest.curate(~structures, ~selections) {
421
- | Error(e) =>
422
- JsError.throwWithMessage(ReventlessCore.Platform_BakedManifest.describe(e))
423
- | Ok(manifest) =>
424
- let body = JSON.stringify(manifest, ~space=2)
425
- let _ = await AwsSdk.S3.PutObjectCommand.make({
426
- bucket: target.bucket,
427
- key: target.key,
428
- body: AwsSdk.S3.PutObjectCommand.bodyFromString(body),
429
- contentType: "application/json",
430
- })->AwsSdk.S3.PutObjectCommand.send
431
- [
432
- Dict.fromArray([
486
+ // The default journey first and always — the caller supplies its key, because
487
+ // the caller is what knows where the shell it configured fetches from — then
488
+ // one file per declared journey, under the key the deploy resolved.
489
+ let files = Array.concat(
490
+ [(None, target.key, selections)],
491
+ bakeJourneys()->Array.map(j => (Some(j.group), j.key, j.selections)),
492
+ )
493
+
494
+ // Every file is curated before any is written, for the reason the in-memory
495
+ // platform curates before it writes: a declaration naming a component that
496
+ // does not exist fails the bake, and failing it halfway would leave the bucket
497
+ // serving one audience's file beside a stale copy of another's.
498
+ let curated = files->Array.map(((group, key, selections)) => (
499
+ group,
500
+ key,
501
+ selections,
502
+ switch ReventlessCore.Platform_BakedManifest.curate(~structures, ~selections) {
503
+ | Error(e) => JsError.throwWithMessage(ReventlessCore.Platform_BakedManifest.describe(e))
504
+ | Ok(manifest) => JSON.stringify(manifest, ~space=2)
505
+ },
506
+ ))
507
+
508
+ await Promise.all(
509
+ curated->Array.map(async ((group, key, selections, body)) => {
510
+ let _ = await AwsSdk.S3.PutObjectCommand.make({
511
+ bucket: target.bucket,
512
+ key,
513
+ body: AwsSdk.S3.PutObjectCommand.bodyFromString(body),
514
+ contentType: "application/json",
515
+ })->AwsSdk.S3.PutObjectCommand.send
516
+ let report = Dict.fromArray([
433
517
  ("baked", JSON.Encode.bool(true)),
434
518
  ("bucket", JSON.Encode.string(target.bucket)),
435
- ("key", JSON.Encode.string(target.key)),
519
+ ("key", JSON.Encode.string(key)),
436
520
  ("plugins", JSON.Encode.int(selections->Array.length)),
437
521
  ("bytes", JSON.Encode.int(body->String.length)),
438
- ])->JSON.Encode.object,
439
- ]
440
- }
522
+ ])
523
+ // Only a journey's entry carries the audience it was written for, so the
524
+ // default one reports exactly what it reported before journeys existed.
525
+ group->Option.forEach(g => report->Dict.set("group", JSON.Encode.string(g)))
526
+ report->JSON.Encode.object
527
+ }),
528
+ )
441
529
  }
442
530
 
443
531
  let handler = async (event: JSON.t): array<JSON.t> => {
@@ -148,6 +148,23 @@ function mapMembers(obj, key, f) {
148
148
  }
149
149
  }
150
150
 
151
+ function fillLifecycleField(component) {
152
+ let match = component["lifecycleField"];
153
+ let match$1 = component["statusField"];
154
+ if (match !== undefined && match !== null) {
155
+ return;
156
+ }
157
+ if (match$1 !== undefined) {
158
+ component["lifecycleField"] = match$1;
159
+ return;
160
+ }
161
+ }
162
+
163
+ let readSideCollections = [
164
+ "readModels",
165
+ "stateViewSlices"
166
+ ];
167
+
151
168
  function healStructure(structure) {
152
169
  let out = Object.fromEntries(Object.entries(structure));
153
170
  fillLists(out, healByCollection.map(param => param[0]));
@@ -161,6 +178,7 @@ function healStructure(structure) {
161
178
  });
162
179
  });
163
180
  });
181
+ readSideCollections.forEach(collection => mapMembers(out, collection, fillLifecycleField));
164
182
  return out;
165
183
  }
166
184
 
@@ -268,6 +286,10 @@ function bakeSelection(json) {
268
286
  })));
269
287
  }
270
288
 
289
+ function decodeSelections(json) {
290
+ return Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(json), []), bakeSelection);
291
+ }
292
+
271
293
  function bakeSelections() {
272
294
  let raw = process.env["BAKE_SELECTIONS"];
273
295
  if (raw === undefined) {
@@ -276,14 +298,45 @@ function bakeSelections() {
276
298
  if (raw === "") {
277
299
  return [];
278
300
  }
301
+ let json;
302
+ try {
303
+ json = JSON.parse(raw);
304
+ } catch (exn) {
305
+ return Stdlib_JsError.throwWithMessage("baked manifest: BAKE_SELECTIONS is not a JSON array");
306
+ }
307
+ return decodeSelections(json);
308
+ }
309
+
310
+ function bakeJourney(json) {
311
+ return Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), o => {
312
+ let match = Stdlib_Option.flatMap(o["group"], Stdlib_JSON.Decode.string);
313
+ let match$1 = Stdlib_Option.flatMap(o["key"], Stdlib_JSON.Decode.string);
314
+ if (match !== undefined && match$1 !== undefined) {
315
+ return {
316
+ group: match,
317
+ key: match$1,
318
+ selections: Stdlib_Option.mapOr(o["components"], [], decodeSelections)
319
+ };
320
+ }
321
+ });
322
+ }
323
+
324
+ function bakeJourneys() {
325
+ let raw = process.env["BAKE_JOURNEYS"];
326
+ if (raw === undefined) {
327
+ return [];
328
+ }
329
+ if (raw === "") {
330
+ return [];
331
+ }
279
332
  let entries;
280
333
  try {
281
334
  entries = Stdlib_JSON.Decode.array(JSON.parse(raw));
282
335
  } catch (exn) {
283
- return Stdlib_JsError.throwWithMessage("baked manifest: BAKE_SELECTIONS is not a JSON array");
336
+ return Stdlib_JsError.throwWithMessage("baked manifest: BAKE_JOURNEYS is not a JSON array");
284
337
  }
285
338
  if (entries !== undefined) {
286
- return Stdlib_Array.filterMap(entries, bakeSelection);
339
+ return Stdlib_Array.filterMap(entries, bakeJourney);
287
340
  } else {
288
341
  return [];
289
342
  }
@@ -336,18 +389,37 @@ async function runBake(target, structures) {
336
389
  if (selections.length === 0) {
337
390
  Stdlib_JsError.throwWithMessage("baked manifest: BAKE_SELECTIONS is empty — this platform declares no bake, so there is nothing to write and a shell pointed at the file would find none.");
338
391
  }
339
- let e = Platform_BakedManifest$ReventlessCore.curate(structures, selections);
340
- if (e.TAG !== "Ok") {
341
- return Stdlib_JsError.throwWithMessage(Platform_BakedManifest$ReventlessCore.describe(e._0));
342
- }
343
- let body = JSON.stringify(e._0, undefined, 2);
344
- await S3$AwsSdk.PutObjectCommand.send(new ClientS3.PutObjectCommand({
345
- Bucket: target.bucket,
346
- Key: target.key,
347
- Body: body,
348
- ContentType: "application/json"
349
- }));
350
- return [Object.fromEntries([
392
+ let files = [[
393
+ undefined,
394
+ target.key,
395
+ selections
396
+ ]].concat(bakeJourneys().map(j => [
397
+ j.group,
398
+ j.key,
399
+ j.selections
400
+ ]));
401
+ let curated = files.map(param => {
402
+ let selections = param[2];
403
+ let e = Platform_BakedManifest$ReventlessCore.curate(structures, selections);
404
+ let tmp;
405
+ tmp = e.TAG === "Ok" ? JSON.stringify(e._0, undefined, 2) : Stdlib_JsError.throwWithMessage(Platform_BakedManifest$ReventlessCore.describe(e._0));
406
+ return [
407
+ param[0],
408
+ param[1],
409
+ selections,
410
+ tmp
411
+ ];
412
+ });
413
+ return await Promise.all(curated.map(async param => {
414
+ let body = param[3];
415
+ let key = param[1];
416
+ await S3$AwsSdk.PutObjectCommand.send(new ClientS3.PutObjectCommand({
417
+ Bucket: target.bucket,
418
+ Key: key,
419
+ Body: body,
420
+ ContentType: "application/json"
421
+ }));
422
+ let report = Object.fromEntries([
351
423
  [
352
424
  "baked",
353
425
  true
@@ -358,17 +430,22 @@ async function runBake(target, structures) {
358
430
  ],
359
431
  [
360
432
  "key",
361
- target.key
433
+ key
362
434
  ],
363
435
  [
364
436
  "plugins",
365
- selections.length
437
+ param[2].length
366
438
  ],
367
439
  [
368
440
  "bytes",
369
441
  body.length
370
442
  ]
371
- ])];
443
+ ]);
444
+ Stdlib_Option.forEach(param[0], g => {
445
+ report["group"] = g;
446
+ });
447
+ return report;
448
+ }));
372
449
  }
373
450
 
374
451
  async function handler(event) {
@@ -424,6 +501,8 @@ export {
424
501
  healByCollection,
425
502
  fillLists,
426
503
  mapMembers,
504
+ fillLifecycleField,
505
+ readSideCollections,
427
506
  healStructure,
428
507
  isPublicQueryable,
429
508
  filterStructure,
@@ -433,7 +512,10 @@ export {
433
512
  isComplete,
434
513
  structureOf,
435
514
  bakeSelection,
515
+ decodeSelections,
436
516
  bakeSelections,
517
+ bakeJourney,
518
+ bakeJourneys,
437
519
  bakeTargetOf,
438
520
  bakeExpectations,
439
521
  structureRefKey,