@reventlessdev/reventless-aws 3.0.0-alpha.346 → 3.0.0-alpha.347

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 (80) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/package.json +17 -13
  3. package/rescript.json +2 -1
  4. package/run-bake-manifest.mjs +3 -0
  5. package/run-deploy-app.mjs +3 -0
  6. package/scripts/BakeManifest.res +395 -0
  7. package/scripts/BakeManifest.res.mjs +487 -0
  8. package/scripts/DeployApp.res +708 -0
  9. package/scripts/DeployApp.res.mjs +1015 -0
  10. package/scripts/DeployManifest.res +84 -0
  11. package/scripts/DeployManifest.res.mjs +112 -0
  12. package/scripts/ProvisionAccounts.res +60 -46
  13. package/scripts/ProvisionAccounts.res.mjs +51 -37
  14. package/scripts/PulumiCli.res +43 -0
  15. package/scripts/PulumiCli.res.mjs +106 -0
  16. package/src/Platform.res +19 -2
  17. package/src/Platform.res.mjs +12 -4
  18. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +1 -5
  19. package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +1 -1
  20. package/src/adapter/Api/Platform_UIFragments_Lambda.res +1 -5
  21. package/src/adapter/Api/Platform_UIFragments_Lambda.res.mjs +1 -2
  22. package/src/adapter/Auth/Auth_ActiveRoleStore.res +1 -5
  23. package/src/adapter/Auth/Auth_ActiveRoleStore.res.mjs +1 -2
  24. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res +1 -5
  25. package/src/adapter/Auth/Auth_ActiveRoleTrigger.res.mjs +1 -2
  26. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res +52 -31
  27. package/src/adapter/DcbEventLog/DcbEventLogStorage_DynamoDb_Runtime.res.mjs +75 -44
  28. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res +1 -5
  29. package/src/adapter/EventLogSubscription/EventLogSubscription_AppSync.res.mjs +1 -2
  30. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +1 -5
  31. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +1 -2
  32. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res +5 -4
  33. package/src/adapter/Postgres/PgChangeFeedRelay_Runtime.res.mjs +1 -1
  34. package/src/adapter/Postgres/PgMigration_Builder.res +1 -1
  35. package/src/adapter/Postgres/PgMigration_Builder.res.mjs +1 -2
  36. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res +2 -0
  37. package/src/adapter/Runtime/CommandGeneratorEntryPoint_Ops.res.mjs +2 -2
  38. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +15 -6
  39. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +51 -30
  40. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +31 -14
  41. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +1 -5
  42. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +1 -1
  43. package/src/adapter/StateTopic/StateTopic_AppSync.res +1 -5
  44. package/src/adapter/StateTopic/StateTopic_AppSync.res.mjs +1 -1
  45. package/src/adapter/Task/TaskBucket_S3.res +1 -3
  46. package/src/adapter/Task/TaskBucket_S3.res.mjs +1 -1
  47. package/src/adapter/Upload/Upload_Claim_S3.res +1 -5
  48. package/src/adapter/Upload/Upload_Claim_S3.res.mjs +1 -1
  49. package/src/adapter/Upload/Upload_Presign_S3.res +1 -5
  50. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +1 -2
  51. package/src/capability/Capability_Geocoding_AwsLocation.res +5 -1
  52. package/src/capability/Capability_Geocoding_AwsLocation.res.mjs +7 -1
  53. package/src/components/Api/AppSync_SdlDecorate.res +4 -2
  54. package/src/components/Api/AppSync_SdlDecorate.res.mjs +1 -1
  55. package/src/plugin/cloner/ClonerRunner_Fargate.res +1 -5
  56. package/src/plugin/cloner/ClonerRunner_Fargate.res.mjs +1 -1
  57. package/src/plugin/stack/Plugin_Stack.res +4 -0
  58. package/src/plugin/stack/Plugin_Stack.res.mjs +2 -0
  59. package/src/util/Util_DeadLetterQueue.res +1 -5
  60. package/src/util/Util_DeadLetterQueue.res.mjs +1 -2
  61. package/src/util/Util_StoreLayout.res +15 -2
  62. package/src/util/Util_StoreLayout.res.mjs +11 -3
  63. package/tests/BakeManifestTest.res +204 -0
  64. package/tests/BakeManifestTest.res.mjs +259 -0
  65. package/tests/DcbColdStartPartitionTest.res +32 -0
  66. package/tests/DcbColdStartPartitionTest.res.mjs +33 -0
  67. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res +88 -0
  68. package/tests/DcbEventLogStorage_DynamoDb_RuntimeTest.res.mjs +112 -0
  69. package/tests/DcbUnresolvedPartitionSlice.res +28 -0
  70. package/tests/DcbUnresolvedPartitionSlice.res.mjs +38 -0
  71. package/tests/DeployAppTest.res +90 -0
  72. package/tests/DeployAppTest.res.mjs +133 -0
  73. package/tests/LambdaLayerLookupTest.res +104 -0
  74. package/tests/LambdaLayerLookupTest.res.mjs +117 -0
  75. package/tests/ProvisionAccountsTest.res +26 -0
  76. package/tests/ProvisionAccountsTest.res.mjs +31 -0
  77. package/tests/Util_StoreLayoutTest.res +6 -0
  78. package/tests/Util_StoreLayoutTest.res.mjs +7 -4
  79. package/tests/dcbColdStartPartition.mjs +66 -0
  80. package/tests/setup/layerArn.cjs +5 -0
@@ -26,7 +26,7 @@ import { commandOutcomeToJson, runInlineAndCollect } from "@reventlessdev/revent
26
26
  // re-exporting it in sury 11. This is the same binding the compiled ReScript uses.
27
27
  import { json as jsonSchema } from "sury";
28
28
  import { handleQueueEvent, publishJsons as sqsPublishJsons } from "@reventlessdev/reventless-aws/src/adapter/CommandTopic/CommandTopicChannel_SQS_Runtime.res.mjs";
29
- // Typed cold-start core — scope/partitionTag derivation, storage-ops wiring, and
29
+ // Typed cold-start core — scope/partition derivation, storage-ops wiring, and
30
30
  // per-slice handler building (functor + decode + handleCommands), all
31
31
  // compiler-checked against the framework signatures (see the module header and
32
32
  // docs/plans/done/minimize-lambda-entrypoint-mjs-shell.md).
@@ -89,7 +89,6 @@ export async function buildHandlersForConfig(config, opts = {}) {
89
89
  // as `dcb_event.log_name`); absence keeps the DynamoDB path byte-identical.
90
90
  const specs = loadedSlices.map(({ patchedSpec }) => patchedSpec);
91
91
  const scope = deriveScope(specs);
92
- const { crossPartitionTagKeys, tagKeysByEventType } = scope;
93
92
  const rawStorageOps = makeStorageOps(
94
93
  config.dcbEventLogTableName,
95
94
  config.pgConnection,
@@ -98,6 +97,7 @@ export async function buildHandlersForConfig(config, opts = {}) {
98
97
 
99
98
  const handlersByType = {};
100
99
  const commandSchemasByType = {};
100
+ const partitionTagsByType = {};
101
101
  const sharedDcbEventLogOps = dcbEventLogOperationsMake({
102
102
  name: config.pluginName,
103
103
  // Mirrors DcbEventLog_Builder.res's `name ++ "DcbEventLog"` — the
@@ -116,8 +116,7 @@ export async function buildHandlersForConfig(config, opts = {}) {
116
116
  const jsonHandler = buildSliceHandler(
117
117
  patchedSpec,
118
118
  behaviorModule,
119
- tagKeysByEventType,
120
- crossPartitionTagKeys,
119
+ scope,
121
120
  sharedDcbEventLogOps,
122
121
  );
123
122
  for (const typeName of commandTypeNames(patchedSpec)) {
@@ -134,6 +133,8 @@ export async function buildHandlersForConfig(config, opts = {}) {
134
133
  );
135
134
  } else {
136
135
  commandSchemasByType[typeName] = patchedSpec.commandSchema;
136
+ // The envelope id is the command's value of its slice's partition key.
137
+ partitionTagsByType[typeName] = scope.partitionTagBySlice[patchedSpec.name];
137
138
  }
138
139
  }
139
140
  });
@@ -225,17 +226,21 @@ export async function buildHandlersForConfig(config, opts = {}) {
225
226
  // reads a command's `@owner` fields from: given `S.json` it finds none, for every
226
227
  // command, and the write keeps whatever owner the CLIENT sent. Validation was
227
228
  // never the only job.
228
- const makeGeneratorFor = (commandSchema) => makeCommandGenerator(
229
+ const makeGeneratorFor = (commandSchema, partitionTag) => makeCommandGenerator(
229
230
  publishJsons,
230
231
  publishJsonsAndWait,
231
232
  config.pluginName,
232
233
  commandSchema,
233
234
  "StateChangeSlice",
234
235
  false,
236
+ partitionTag,
235
237
  );
236
238
  const generatorsByType = {};
237
239
  for (const typeName of Object.keys(commandSchemasByType)) {
238
- generatorsByType[typeName] = makeGeneratorFor(commandSchemasByType[typeName]);
240
+ generatorsByType[typeName] = makeGeneratorFor(
241
+ commandSchemasByType[typeName],
242
+ partitionTagsByType[typeName],
243
+ );
239
244
  }
240
245
  // A command no loaded slice claims keeps the permissive generator — what every
241
246
  // command got before this change — so anything reaching this handler by another
@@ -301,6 +306,10 @@ async function buildHandler() {
301
306
  }
302
307
 
303
308
  const initPromise = buildHandler();
309
+ // Startup runs at module load, before any invocation awaits it. A failure left
310
+ // unhandled until then can surface as a bare `Runtime.UnhandledPromiseRejection`
311
+ // and a restart; handled here, every invocation rethrows the same clear error.
312
+ initPromise.catch(() => {});
304
313
 
305
314
  export async function handler(event, context) {
306
315
  setRequestId(context?.awsRequestId);
@@ -37,47 +37,62 @@ type pgConnectionJson
37
37
  @get external pgLockStrategy: pgConnectionJson => Nullable.t<string> = "lockStrategy"
38
38
  external asConnectionConfig: pgConnectionJson => PgConnection.connectionConfig = "%identity"
39
39
 
40
- // The decision-read scope + storage partition tag for one consistency boundary,
40
+ // The decision-read scope + storage partition for one consistency boundary,
41
41
  // derived exactly the way the deploy-time `Dcb_Builder` does (both call the same
42
42
  // `Reventless.DcbTag` functions — the single source of truth), so the runtime
43
43
  // query can't diverge from the storage/GSI scope.
44
44
  type derivedScope = {
45
45
  crossPartitionTagKeys: array<string>,
46
46
  tagKeysByEventType: dict<array<string>>,
47
+ /** `None` only for a Lambda hosting no StateChangeSlices, which never appends. */
47
48
  partitionTag: option<Reventless.DcbTag.derivedPartitionTag>,
49
+ /** Slice name -> its partition: the handler's entity id and each command's envelope id. */
50
+ partitionTagBySlice: dict<Reventless.DcbTag.derivedPartitionTag>,
48
51
  }
49
52
 
50
- let deriveScope = (specs: array<specModule>): derivedScope => {
51
- let scope = Reventless.DcbTag.deriveEffectiveScope(
52
- specs->Array.map(s => {
53
+ let deriveScope = (specs: array<specModule>): derivedScope =>
54
+ switch specs {
55
+ | [] => {
56
+ crossPartitionTagKeys: [],
57
+ tagKeysByEventType: Dict.make(),
58
+ partitionTag: None,
59
+ partitionTagBySlice: Dict.make(),
60
+ }
61
+ | _ =>
62
+ let slices = specs->Array.map(s => {
53
63
  Reventless.DcbTag.name: specName(s),
54
64
  commandSchema: specCommandSchema(s),
55
65
  consumedEventSchema: specConsumedEventSchema(s),
56
66
  eventSchema: specEventSchema(s),
57
- }),
58
- )
59
-
60
- // `derivePartitionTag` throws only on a misconfigured spec, which the deploy
61
- // would already have rejected; degrade to untagged fences rather than crash
62
- // cold start (matches the shell's prior defensive behaviour).
63
- let partitionTag = try Some(
64
- Reventless.DcbTag.derivePartitionTag(
65
- specs->Array.map(s => (
66
- specName(s),
67
- specModuleUrl(s)->Nullable.toOption->Option.getOr(specName(s)),
68
- specEventSchema(s),
69
- )),
70
- ),
71
- ) catch {
72
- | _ => None
73
- }
74
-
75
- {
76
- crossPartitionTagKeys: scope.crossPartitionTagKeys,
77
- tagKeysByEventType: scope.tagKeysByEventType,
78
- partitionTag,
67
+ moduleUrl: ?(specModuleUrl(s)->Nullable.toOption),
68
+ })
69
+ let scope = Reventless.DcbTag.deriveEffectiveScope(slices)
70
+ // Deploy derives this from the same slice list, so on the same code it cannot
71
+ // fail here. It can when the framework in the Lambda layer differs from the
72
+ // one deploy used — and then carrying on would file events under the wrong
73
+ // keys. Refuse to start instead, saying why before the rejection can be lost.
74
+ // Rethrown as the original JS error, so each invocation reports its message.
75
+ let boundary = try Reventless.DcbTag.deriveBoundaryPartition(slices) catch {
76
+ | JsExn(err) =>
77
+ ReventlessCore.EffectLogger.logError(
78
+ ~comp="DcbCommandTopicRuntime",
79
+ `refusing to start: ${err->JsExn.message->Option.getOr("unknown error")}`,
80
+ )->Effect.runSync
81
+ JsExn.throw(err)
82
+ }
83
+ let partitionTagBySlice = Dict.make()
84
+ slices->Array.forEach(s =>
85
+ boundary
86
+ ->Reventless.DcbTag.slicePartitionTag(s.name)
87
+ ->Option.forEach(tag => partitionTagBySlice->Dict.set(s.name, tag))
88
+ )
89
+ {
90
+ crossPartitionTagKeys: scope.crossPartitionTagKeys,
91
+ tagKeysByEventType: scope.tagKeysByEventType,
92
+ partitionTag: Some(boundary.partitionTag),
93
+ partitionTagBySlice,
94
+ }
79
95
  }
80
- }
81
96
 
82
97
  // The command TAG names this slice handles — used by the shell to build its
83
98
  // `handlersByType` routing map.
@@ -166,6 +181,7 @@ type sliceCallback = {
166
181
  handleCommands: (
167
182
  ~tagKeysByEventType: dict<array<string>>=?,
168
183
  ~crossPartitionTagKeys: array<string>=?,
184
+ ~partitionTag: Reventless.DcbTag.derivedPartitionTag=?,
169
185
  ReventlessInfra.DcbEventLog.operations,
170
186
  Stream.t<
171
187
  ReventlessInfra.CommandTopic.topicItem<
@@ -188,8 +204,7 @@ external makeSliceCallback: specModule => behaviorModule => sliceCallback = "Mak
188
204
  let buildSliceHandler = (
189
205
  spec: specModule,
190
206
  behavior: behaviorModule,
191
- ~tagKeysByEventType: dict<array<string>>,
192
- ~crossPartitionTagKeys: array<string>,
207
+ scope: derivedScope,
193
208
  dcbEventLog: ReventlessInfra.DcbEventLog.operations,
194
209
  ) => {
195
210
  let callback = makeSliceCallback(spec)(behavior)
@@ -220,7 +235,13 @@ let buildSliceHandler = (
220
235
  | None => Stream.empty
221
236
  }
222
237
  )
223
- callback.handleCommands(~tagKeysByEventType, ~crossPartitionTagKeys, dcbEventLog, decodedStream)
238
+ callback.handleCommands(
239
+ ~tagKeysByEventType=scope.tagKeysByEventType,
240
+ ~crossPartitionTagKeys=scope.crossPartitionTagKeys,
241
+ ~partitionTag=?scope.partitionTagBySlice->Dict.get(specName(spec)),
242
+ dcbEventLog,
243
+ decodedStream,
244
+ )
224
245
  }
225
246
  }
226
247
 
@@ -20,26 +20,43 @@ import * as StateChangeSlice_CallbackResMjs from "@reventlessdev/reventless-core
20
20
  import * as InboundTranslationSlice_CallbackResMjs from "@reventlessdev/reventless-core/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs";
21
21
 
22
22
  function deriveScope(specs) {
23
- let scope = DcbTag$Reventless.deriveEffectiveScope(specs.map(s => ({
23
+ if (specs.length === 0) {
24
+ return {
25
+ crossPartitionTagKeys: [],
26
+ tagKeysByEventType: {},
27
+ partitionTag: undefined,
28
+ partitionTagBySlice: {}
29
+ };
30
+ }
31
+ let slices = specs.map(s => ({
24
32
  name: s.name,
25
33
  commandSchema: s.commandSchema,
26
34
  consumedEventSchema: s.consumedEventSchema,
27
- eventSchema: s.eventSchema
28
- })));
29
- let partitionTag;
35
+ eventSchema: s.eventSchema,
36
+ moduleUrl: Primitive_option.fromNullable(s.moduleUrl)
37
+ }));
38
+ let scope = DcbTag$Reventless.deriveEffectiveScope(slices);
39
+ let boundary;
30
40
  try {
31
- partitionTag = DcbTag$Reventless.derivePartitionTag(specs.map(s => [
32
- s.name,
33
- Stdlib_Option.getOr(Primitive_option.fromNullable(s.moduleUrl), s.name),
34
- s.eventSchema
35
- ]));
36
- } catch (exn) {
37
- partitionTag = undefined;
41
+ boundary = DcbTag$Reventless.deriveBoundaryPartition(slices);
42
+ } catch (raw_err) {
43
+ let err = Primitive_exceptions.internalToException(raw_err);
44
+ if (err.RE_EXN_ID === "JsExn") {
45
+ let err$1 = err._1;
46
+ Effect.runSync(EffectLogger$ReventlessCore.logError("DcbCommandTopicRuntime", undefined, `refusing to start: ` + Stdlib_Option.getOr(Stdlib_JsExn.message(err$1), "unknown error")));
47
+ throw err$1;
48
+ }
49
+ throw err;
38
50
  }
51
+ let partitionTagBySlice = {};
52
+ slices.forEach(s => Stdlib_Option.forEach(DcbTag$Reventless.slicePartitionTag(boundary, s.name), tag => {
53
+ partitionTagBySlice[s.name] = tag;
54
+ }));
39
55
  return {
40
56
  crossPartitionTagKeys: scope.crossPartitionTagKeys,
41
57
  tagKeysByEventType: scope.tagKeysByEventType,
42
- partitionTag: partitionTag
58
+ partitionTag: boundary.partitionTag,
59
+ partitionTagBySlice: partitionTagBySlice
43
60
  };
44
61
  }
45
62
 
@@ -75,7 +92,7 @@ function makeStorageOps(tableName, pgConnection, scope) {
75
92
  }
76
93
  }
77
94
 
78
- function buildSliceHandler(spec, behavior, tagKeysByEventType, crossPartitionTagKeys, dcbEventLog) {
95
+ function buildSliceHandler(spec, behavior, scope, dcbEventLog) {
79
96
  let callback = StateChangeSlice_CallbackResMjs.Make(spec)(behavior);
80
97
  let commandSchema = spec.commandSchema;
81
98
  return jsonStream => {
@@ -97,7 +114,7 @@ function buildSliceHandler(spec, behavior, tagKeysByEventType, crossPartitionTag
97
114
  return Stream.empty;
98
115
  }
99
116
  });
100
- return callback.handleCommands(tagKeysByEventType, crossPartitionTagKeys, dcbEventLog, decodedStream);
117
+ return callback.handleCommands(scope.tagKeysByEventType, scope.crossPartitionTagKeys, scope.partitionTagBySlice[spec.name], dcbEventLog, decodedStream);
101
118
  };
102
119
  }
103
120
 
@@ -224,11 +224,7 @@ let makeFromCodeAsset: (
224
224
  })
225
225
  })
226
226
 
227
- let layers =
228
- Lambda.reventlessLayerArn
229
- ->Option.map(arn => [arn->Pulumi.Input.make])
230
- ->Option.getOr([])
231
- ->Pulumi.Input.make
227
+ let layers = Lambda.reventlessLayers()
232
228
 
233
229
  let variables = Dict.fromArray([("Environment", stack->Pulumi.Input.make)])
234
230
  envVars->Dict.forEachWithKey((value, key) => {
@@ -91,7 +91,7 @@ function makeFromCodeAsset(name, unitKind, componentKind, code, sourceCodeHash,
91
91
  });
92
92
  });
93
93
  });
94
- let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
94
+ let layers = Lambda$PulumiAws.reventlessLayers();
95
95
  let variables = Object.fromEntries([[
96
96
  "Environment",
97
97
  stack
@@ -332,11 +332,7 @@ let finish = (~eventsApi: AppSync_EventsApi.t, ~opts: Pulumi.CustomResourceOptio
332
332
  ~bundleRuntimeExtensions=false,
333
333
  )
334
334
 
335
- let layers =
336
- Lambda.reventlessLayerArn
337
- ->Option.map(arn => [arn->Pulumi.Input.make])
338
- ->Option.getOr([])
339
- ->Pulumi.Input.make
335
+ let layers = Lambda.reventlessLayers()
340
336
 
341
337
  let appsyncEndpoint = AppSync_EventsApi.httpEndpoint(eventsApi)
342
338
 
@@ -120,7 +120,7 @@ function finish(eventsApi, opts) {
120
120
  Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
121
121
  ]]);
122
122
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/StateTopic/StateTopic_AppSync_Ops.res.mjs", packageDirs, undefined, false);
123
- let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
123
+ let layers = Lambda$PulumiAws.reventlessLayers();
124
124
  let appsyncEndpoint = AppSync_EventsApi$ReventlessAws.httpEndpoint(eventsApi);
125
125
  let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "StateTopicPublisher", undefined, AWS_Tags$ReventlessAws.make(name + "StateTopicPublisherLogGroup", QueryDb$ReventlessCore.componentType, "Logs", undefined, name, undefined, undefined, undefined), opts, undefined);
126
126
  let lambda = new (Aws.lambda.Function)(name + "StateTopicPublisher", {
@@ -136,9 +136,7 @@ let make: ReventlessCore.Task_Adapter.bucketMaker<bucketParts> = (~name, ~opts)
136
136
  // bucket fails the deploy with `BucketNotEmpty` — which would wedge precisely
137
137
  // the throwaway stacks that are recreated most often. Protected stacks keep
138
138
  // the safe default and drain by hand.
139
- let forceDestroy =
140
- Util_StoreLayout.protectionFor(~stack=Pulumi.Pulumi.getStackName()) ==
141
- Util_StoreLayout.Unprotected
139
+ let forceDestroy = Util_StoreLayout.protectionOfDeployedStack() == Util_StoreLayout.Unprotected
142
140
 
143
141
  let bucket = {
144
142
  PulumiAws.S3.Bucket.make(
@@ -76,7 +76,7 @@ function connect(name, bucket, bucketMode, commandTopics, runtime, opts) {
76
76
 
77
77
  function make(name, opts) {
78
78
  let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
79
- let forceDestroy = Util_StoreLayout$ReventlessAws.protectionFor(Pulumi.getStack(), undefined) === "Unprotected";
79
+ let forceDestroy = Util_StoreLayout$ReventlessAws.protectionOfDeployedStack() === "Unprotected";
80
80
  let bucket = new (Aws.s3.Bucket)(name, {
81
81
  corsRules: [{
82
82
  allowedHeaders: ["*"],
@@ -257,11 +257,7 @@ let finish = (
257
257
  ~bundleRuntimeExtensions=false,
258
258
  )
259
259
 
260
- let layers =
261
- Lambda.reventlessLayerArn
262
- ->Option.map(arn => [arn->Pulumi.Input.make])
263
- ->Option.getOr([])
264
- ->Pulumi.Input.make
260
+ let layers = Lambda.reventlessLayers()
265
261
 
266
262
  // Created before the function that writes to it — see
267
263
  // `Util_LambdaLogging.makeManagedLogGroup` for why the ordering matters.
@@ -124,7 +124,7 @@ function finish(plugin, stores, iteratorAgeAlarmMsOpt, opts) {
124
124
  Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
125
125
  ]]);
126
126
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Upload/Upload_Claim_S3_Ops.res.mjs", packageDirs, undefined, false);
127
- let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
127
+ let layers = Lambda$PulumiAws.reventlessLayers();
128
128
  let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name, undefined, AWS_Tags$ReventlessAws.make(name + "LogGroup", "Plugin", "Logs", undefined, name, undefined, undefined, undefined), opts, undefined);
129
129
  let lambda = new (Aws.lambda.Function)(name, {
130
130
  handler: "index.handler",
@@ -180,11 +180,7 @@ let make = (
180
180
  ~bundleRuntimeExtensions=false,
181
181
  )
182
182
 
183
- let layers =
184
- Lambda.reventlessLayerArn
185
- ->Option.map(arn => [arn->Pulumi.Input.make])
186
- ->Option.getOr([])
187
- ->Pulumi.Input.make
183
+ let layers = Lambda.reventlessLayers()
188
184
 
189
185
  // Created before the function that writes to it — see
190
186
  // `Util_LambdaLogging.makeManagedLogGroup` for why the ordering matters.
@@ -3,7 +3,6 @@
3
3
  import * as Aws from "@pulumi/aws";
4
4
  import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
5
5
  import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
6
- import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
6
  import * as Pulumi from "@pulumi/pulumi";
8
7
  import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
9
8
  import * as AWS$ReventlessAws from "../AWS.res.mjs";
@@ -93,7 +92,7 @@ function make(api, stores, releaseWindowSecondsOpt, nameOpt, opts) {
93
92
  Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
94
93
  ]]);
95
94
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs", packageDirs, undefined, false);
96
- let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
95
+ let layers = Lambda$PulumiAws.reventlessLayers();
97
96
  let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", undefined, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
98
97
  let lambda = new (Aws.lambda.Function)(name + "Lambda", {
99
98
  handler: "index.handler",
@@ -23,6 +23,8 @@ open PulumiAws
23
23
  let defaultDataSource = "Esri"
24
24
  let defaultIntendedUse = "SingleUse"
25
25
 
26
+ let physicalName = (~name: string, ~stack: string): string => `${name}-${stack}`
27
+
26
28
  /** Create a place index with framework attribution tags and config-driven
27
29
  provider/retention settings. */
28
30
  let make = (
@@ -35,7 +37,9 @@ let make = (
35
37
  let index = Location.PlaceIndex.make(
36
38
  ~name,
37
39
  ~args={
38
- indexName: Pulumi.Input.make(name),
40
+ // Stack-scoped: an index name is unique per account and region, so two
41
+ // stacks of one app in the same place would otherwise collide.
42
+ indexName: Pulumi.Input.make(physicalName(~name, ~stack=Pulumi.Pulumi.getStackName())),
39
43
  dataSource: Pulumi.Input.make(dataSource),
40
44
  dataSourceConfiguration: Pulumi.Input.make({
41
45
  Location.PlaceIndex.intendedUse: Pulumi.Input.make(intendedUse),
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as Aws from "@pulumi/aws";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Pulumi from "@pulumi/pulumi";
5
6
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
6
7
  import * as AWS_Tags$ReventlessAws from "../adapter/AWS_Tags.res.mjs";
7
8
  import * as Util_LocalConfig$ReventlessAws from "../util/Util_LocalConfig.res.mjs";
@@ -10,11 +11,15 @@ let defaultDataSource = "Esri";
10
11
 
11
12
  let defaultIntendedUse = "SingleUse";
12
13
 
14
+ function physicalName(name, stack) {
15
+ return name + `-` + stack;
16
+ }
17
+
13
18
  function make(name, opts) {
14
19
  let dataSource = Stdlib_Option.getOr(Util_LocalConfig$ReventlessAws.get("geocoderDataSource"), defaultDataSource);
15
20
  let intendedUse = Stdlib_Option.getOr(Util_LocalConfig$ReventlessAws.get("geocoderIntendedUse"), defaultIntendedUse);
16
21
  let index = new (Aws.location.PlaceIndex)(name, {
17
- indexName: name,
22
+ indexName: physicalName(name, Pulumi.getStack()),
18
23
  dataSource: dataSource,
19
24
  dataSourceConfiguration: {
20
25
  intendedUse: intendedUse
@@ -32,6 +37,7 @@ function make(name, opts) {
32
37
  export {
33
38
  defaultDataSource,
34
39
  defaultIntendedUse,
40
+ physicalName,
35
41
  make,
36
42
  }
37
43
  /* @pulumi/aws Not a pure module */
@@ -162,8 +162,10 @@ let refuseAnonymousFields = (fieldNames: array<string>): 'a =>
162
162
  `Authorization.isAllowed), so a spec that passes locally can still not be\n` ++
163
163
  `deployable here. That divergence is the reason this refuses instead of\n` ++
164
164
  `emitting a directive that reads as gated and is not.\n\n` ++
165
- `To serve anonymous callers on AWS the API needs API_KEY added as a third\n` ++
166
- `auth provider, plus something to rotate the key. Until then, declare the\n` ++ `field AllowAuthenticated (or AllowGroups) and mean it.`,
165
+ `Anonymous fields on AWS are deferred. When they land it will be as a separate\n` ++
166
+ `API carrying only the unauthenticated fields. Do not add API_KEY to this one:\n` ++
167
+ `every field here would then have to say whether the key reaches it, and one\n` ++
168
+ `that forgot would be open. Until then, declare the field AllowAuthenticated\n` ++ `(or AllowGroups) and mean it.`,
167
169
  )
168
170
 
169
171
  // Injects the Cognito group gate on ALL mutation, query, and subscription fields
@@ -81,7 +81,7 @@ function permissionToGate(permission) {
81
81
  }
82
82
 
83
83
  function refuseAnonymousFields(fieldNames) {
84
- return Stdlib_JsError.throwWithMessage(`Refusing to push an AppSync schema that cannot honour AllowAnonymous.\n\n` + (` ` + fieldNames.length.toString() + ` field(s) declare AllowAnonymous:\n`) + (` ` + fieldNames.join(", ") + `\n\n`) + `AppSync has no anonymous authorization mode. This API is provisioned with\nAMAZON_COGNITO_USER_POOLS primary and AWS_IAM additional, so the only\ndirective available is @aws_cognito_user_pools — which means ANY\nAUTHENTICATED caller, the opposite of what the spec declares. Mutations have\nno runtime authorization check to correct it: on this platform the directive\nis the whole enforcement.\n\nThe local platform DOES honour AllowAnonymous (its resolvers call\nAuthorization.isAllowed), so a spec that passes locally can still not be\ndeployable here. That divergence is the reason this refuses instead of\nemitting a directive that reads as gated and is not.\n\nTo serve anonymous callers on AWS the API needs API_KEY added as a third\nauth provider, plus something to rotate the key. Until then, declare the\nfield AllowAuthenticated (or AllowGroups) and mean it.`);
84
+ return Stdlib_JsError.throwWithMessage(`Refusing to push an AppSync schema that cannot honour AllowAnonymous.\n\n` + (` ` + fieldNames.length.toString() + ` field(s) declare AllowAnonymous:\n`) + (` ` + fieldNames.join(", ") + `\n\n`) + `AppSync has no anonymous authorization mode. This API is provisioned with\nAMAZON_COGNITO_USER_POOLS primary and AWS_IAM additional, so the only\ndirective available is @aws_cognito_user_pools — which means ANY\nAUTHENTICATED caller, the opposite of what the spec declares. Mutations have\nno runtime authorization check to correct it: on this platform the directive\nis the whole enforcement.\n\nThe local platform DOES honour AllowAnonymous (its resolvers call\nAuthorization.isAllowed), so a spec that passes locally can still not be\ndeployable here. That divergence is the reason this refuses instead of\nemitting a directive that reads as gated and is not.\n\nAnonymous fields on AWS are deferred. When they land it will be as a separate\nAPI carrying only the unauthenticated fields. Do not add API_KEY to this one:\nevery field here would then have to say whether the key reaches it, and one\nthat forgot would be open. Until then, declare the field AllowAuthenticated\n(or AllowGroups) and mean it.`);
85
85
  }
86
86
 
87
87
  function injectAwsAuthAll(fragment, group, iamFieldNamesOpt) {
@@ -141,11 +141,7 @@ export const handler = async (event) => {
141
141
  let code = Pulumi.Archive.assetArchive(clonerArchiveContents)
142
142
  let sourceCodeHash = Util_Bundle.hashString(entryPointCode ++ "\n---\n" ++ loaderHash)
143
143
 
144
- let layers =
145
- Lambda.reventlessLayerArn
146
- ->Option.map(arn => [arn->Pulumi.Input.make])
147
- ->Option.getOr([])
148
- ->Pulumi.Input.make
144
+ let layers = Lambda.reventlessLayers()
149
145
 
150
146
  let vpcStackName = Pulumi.Config.make(Some("vpc"))->Pulumi.Config.get("stack")->Option.getOrThrow
151
147
  let vpcConfig = Util_Vpc.getVpcConfig(~stackName=vpcStackName, ~outputName="vpc")
@@ -87,7 +87,7 @@ export const handler = async (event) => {
87
87
  let loaderHash = Util_Bundle$ReventlessAws.addEsmLoaderAssets(clonerArchiveContents);
88
88
  let code = new (Pulumi.asset.AssetArchive)(clonerArchiveContents);
89
89
  let sourceCodeHash = Util_Bundle$ReventlessAws.hashString(entryPointCode + "\n---\n" + loaderHash);
90
- let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
90
+ let layers = Lambda$PulumiAws.reventlessLayers();
91
91
  let vpcStackName = Stdlib_Option.getOrThrow(new Pulumi.Config("vpc").get("stack"), undefined);
92
92
  let vpcConfig = Util_Vpc$ReventlessAws.getVpcConfig(vpcStackName, "vpc");
93
93
  let secrets = Pulumi.all(secretUrns.map(urn => GetSecretVersion$PulumiAws.getSecretNames(urn).apply(names => names.map(name => ({
@@ -157,9 +157,13 @@ let makeUiBundleDistribution = (
157
157
  // shell, where blue/green between versions is wanted.
158
158
  let name = stableName ? pluginId : pluginId ++ "-" ++ bundleVersion
159
159
 
160
+ // Emptied on removal from a disposable stack: the manifest bake writes files
161
+ // here outside Pulumi, which would otherwise fail the destroy.
160
162
  let bucket = PulumiAws.S3.Bucket.make(
161
163
  ~name=name ++ "-bundle",
162
164
  ~args={
165
+ forceDestroy: (Util_StoreLayout.protectionOfDeployedStack() == Unprotected)
166
+ ->Pulumi.Input.make,
163
167
  tags: AWS.Tags.make(
164
168
  ~name=name ++ "-bundle",
165
169
  ~kind=ReventlessCore.ComponentType.Plugin,
@@ -11,6 +11,7 @@ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_excep
11
11
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
12
12
  import * as AWS_Tags$ReventlessAws from "../../adapter/AWS_Tags.res.mjs";
13
13
  import * as ClientCloudfront from "@aws-sdk/client-cloudfront";
14
+ import * as Util_StoreLayout$ReventlessAws from "../../util/Util_StoreLayout.res.mjs";
14
15
  import * as Util_StaticBundle$ReventlessAws from "../../util/Util_StaticBundle.res.mjs";
15
16
 
16
17
  let log = Logger$ReventlessCore.fromEnv();
@@ -76,6 +77,7 @@ function makeUiBundleDistribution(pluginId, bundleVersion, assetsDir, spaFallbac
76
77
  let servedBuckets = servedBucketsOpt !== undefined ? servedBucketsOpt : [];
77
78
  let name = stableName ? pluginId : pluginId + "-" + bundleVersion;
78
79
  let bucket = new (Aws.s3.Bucket)(name + "-bundle", {
80
+ forceDestroy: Util_StoreLayout$ReventlessAws.protectionOfDeployedStack() === "Unprotected",
79
81
  tags: AWS_Tags$ReventlessAws.make(name + "-bundle", "Plugin", "Hosting", "Plugin", undefined, undefined, pluginId, undefined)
80
82
  });
81
83
  new (Aws.s3.BucketPublicAccessBlock)(name + "-bundle-pab", {
@@ -123,11 +123,7 @@ archiveContents->Dict.set(
123
123
  let code = Pulumi.Archive.assetArchive(archiveContents)
124
124
  let sourceCodeHash = Util_Bundle.hashString(entryPointCode)
125
125
 
126
- let layers =
127
- Lambda.reventlessLayerArn
128
- ->Option.map(arn => [arn->Pulumi.Input.make])
129
- ->Option.getOr([])
130
- ->Pulumi.Input.make
126
+ let layers = Lambda.reventlessLayers()
131
127
 
132
128
  // This is the one Lambda in the framework built by hand rather than through
133
129
  // `RuntimeEnvironment_Lambda`, and it was the one Lambda whose logs Lambda
@@ -2,7 +2,6 @@
2
2
 
3
3
  import * as Aws from "@pulumi/aws";
4
4
  import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
5
- import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
5
  import * as Pulumi from "@pulumi/pulumi";
7
6
  import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
8
7
  import * as AWS$ReventlessAws from "../adapter/AWS.res.mjs";
@@ -76,7 +75,7 @@ let code = new (Pulumi.asset.AssetArchive)(archiveContents);
76
75
 
77
76
  let sourceCodeHash = Util_Bundle$ReventlessAws.hashString(entryPointCode);
78
77
 
79
- let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
78
+ let layers = Lambda$PulumiAws.reventlessLayers();
80
79
 
81
80
  let logGroup = Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name, undefined, AWS_Tags$ReventlessAws.make(name + `LogGroup`, "Plugin", "Logs", "Plugin", undefined, undefined, undefined, undefined), opts, undefined);
82
81
 
@@ -86,13 +86,26 @@ Destroy semantics follow disposability, **not** layout.
86
86
  `reventless:wipeable`, but that authorises the reset tool to empty stores
87
87
  *deliberately*, after a scope prompt and a typed confirmation. It does not say a
88
88
  field rename may destroy a bucket by accident. Only stacks that are routinely
89
- torn down are unprotected.
89
+ torn down are unprotected: a `pr-*` stack, or one that declares
90
+ `reventless:disposable: "true"` — a try-out meant to be removed whole.
90
91
  */
91
92
  let protectionFor = (
92
93
  ~stack: string,
94
+ ~disposable: bool=false,
93
95
  ~ephemeralPrefixes: array<string>=defaultEphemeralPrefixes,
94
96
  ): protection =>
95
- ephemeralPrefixes->Array.some(p => stack->String.startsWith(p)) ? Unprotected : Protected
97
+ disposable || ephemeralPrefixes->Array.some(p => stack->String.startsWith(p))
98
+ ? Unprotected
99
+ : Protected
100
+
101
+ /** `protectionFor` the stack being deployed, reading its `reventless:disposable`
102
+ setting. The one function here that touches Pulumi. */
103
+ let protectionOfDeployedStack = (): protection =>
104
+ protectionFor(
105
+ ~stack=Pulumi.Pulumi.getStackName(),
106
+ ~disposable=Pulumi.Config.make(Some("reventless"))->Pulumi.Config.get("disposable") ==
107
+ Some("true"),
108
+ )
96
109
 
97
110
  /**
98
111
  The bucket a store's objects live in.