@reventlessdev/reventless-aws 3.0.0-alpha.332 → 3.0.0-alpha.334

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 (25) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res +71 -6
  4. package/src/Platform.res.mjs +63 -12
  5. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +5 -3
  6. package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +6 -1
  7. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +21 -1
  8. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +93 -11
  9. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +53 -4
  10. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +53 -11
  11. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +30 -8
  12. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +5 -3
  13. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +6 -1
  14. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +5 -3
  15. package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +6 -1
  16. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +3 -1
  17. package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +4 -1
  18. package/src/capability/Capability_Messaging.res +141 -0
  19. package/src/capability/Capability_Messaging.res.mjs +96 -0
  20. package/src/capability/Capability_Messaging_Ses.res +27 -14
  21. package/src/capability/Capability_Messaging_Ses.res.mjs +5 -6
  22. package/tests/AutomationSliceEntryPoint_OpsTest.res +122 -0
  23. package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +187 -0
  24. package/tests/Capability_MessagingTest.res +100 -0
  25. package/tests/Capability_MessagingTest.res.mjs +85 -0
@@ -542,6 +542,12 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
542
542
  }
543
543
 
544
544
  let handlerOutputs: array<Pulumi.Output.t<string>> = []
545
+ // The static half of each slice's config — module paths and context, all
546
+ // known synchronously. Rides in the code archive rather than the
547
+ // environment: two module specifiers per slice is the term that grows
548
+ // with the plugin, and it is what pushed this Lambda past AWS's 4KB env
549
+ // limit. Keyed by slice name, which is what the compact env entry names.
550
+ let sliceModuleEntries: array<string> = []
545
551
  let packageDirs: dict<string> = Dict.make()
546
552
  let allQueryDbResources: array<ReventlessInfra.Adapter.resource> = []
547
553
  // Whether any slice publishes to the DCB fallback rather than a target
@@ -549,7 +555,7 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
549
555
  // too, once, below the loop.
550
556
  let anyDcbFallback = ref(false)
551
557
 
552
- bundledInfos->Dict.forEachWithKey((info, _name) => {
558
+ bundledInfos->Dict.forEachWithKey((info, name) => {
553
559
  info.queryDbResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
554
560
  // The queue this slice publishes its command to. An Aggregate registers
555
561
  // its CommandTopic while plugin construction is still synchronous —
@@ -582,6 +588,12 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
582
588
  | None => ""
583
589
  }
584
590
 
591
+ let nameJson = name->JSON.stringifyAny->Option.getOr(`""`)
592
+ let _ =
593
+ sliceModuleEntries->Array.push(
594
+ `${nameJson}:{"specModule":${specModule},"bodyModule":${bodyModule},"callbackType":${callbackType}${contextFragment}}`,
595
+ )
596
+
585
597
  // Per slice, not plugin-wide: an outbound slice targeting an Aggregate
586
598
  // publishes to that aggregate's CommandTopic. Everything else keeps the
587
599
  // DCB topic, which is what the field has always meant — and with it the
@@ -590,17 +602,23 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
590
602
  | Some({queueUrl, isFifo}) => (queueUrl, isFifo)
591
603
  | None => (dcbQueueUrl, dcbQueueIsFifo.contents)
592
604
  }
605
+ // What each slice may override is decided structurally, not from the
606
+ // resolved values: a slice listening only on the plugin's DCB log, and
607
+ // publishing to the DCB fallback, repeats the shared urn and queue URL
608
+ // exactly — so it emits neither and inherits both.
609
+ let hasOwnUrns =
610
+ !(info.consumesDcbLog) || info.sourceTopics->Dict.keysToArray->Array.length > 0
593
611
  let handlerJson =
594
612
  Pulumi.Output.all3((info.queryDbTableName, commandQueueUrl, sourceUrnsFor(info)))
595
613
  ->Pulumi.Output.apply(((tableName, queueUrl, urns)) => {
596
- let urnsJson =
597
- urns->Array.map(JSON.Encode.string)->JSON.Encode.array->JSON.stringify
598
- // `sourceUrn` stays beside `sourceUrns` so a handler config read by
599
- // an entry point that predates multi-source still routes its first
600
- // stream rather than none.
601
- let firstUrn = urns->Array.get(0)->Option.getOr("")
602
- let fifoJson = commandQueueIsFifo ? "true" : "false"
603
- `{"specModule":${specModule},"bodyModule":${bodyModule},"callbackType":${callbackType},"queryDbTableName":"${tableName}","dcbQueueUrl":"${queueUrl}","commandQueueIsFifo":${fifoJson},"sourceUrn":"${firstUrn}","sourceUrns":${urnsJson}${contextFragment}}`
614
+ let urnsFragment = hasOwnUrns
615
+ ? `,"u":${urns->Array.map(JSON.Encode.string)->JSON.Encode.array->JSON.stringify}`
616
+ : ""
617
+ let queueFragment = switch target {
618
+ | Some(_) => `,"k":"${queueUrl}","f":${commandQueueIsFifo ? "true" : "false"}`
619
+ | None => ""
620
+ }
621
+ `{"n":${nameJson},"q":"${tableName}"${urnsFragment}${queueFragment}}`
604
622
  })
605
623
  let _ = handlerOutputs->Array.push(handlerJson)
606
624
  })
@@ -613,9 +631,24 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
613
631
  dcbQueueResources->Array.forEach(r => allQueryDbResources->Array.push(r)->ignore)
614
632
  }
615
633
 
634
+ // The defaults every non-overriding slice inherits, carried once.
635
+ let sharedOutput =
636
+ Pulumi.Output.all2((dcbQueueUrl, dcbSourceUrn))->Pulumi.Output.apply(((
637
+ queueUrl,
638
+ urn,
639
+ )) => {
640
+ let fifoJson = dcbQueueIsFifo.contents ? "true" : "false"
641
+ let urnJson = urn->JSON.stringifyAny->Option.getOr(`""`)
642
+ `"queueUrl":"${queueUrl}","commandQueueIsFifo":${fifoJson},"urns":[${urnJson}]`
643
+ })
644
+
616
645
  let handlerConfigOutput =
617
- Pulumi.Output.all(handlerOutputs)
618
- ->Pulumi.Output.apply(handlers => `{"handlers":[${handlers->Array.join(",")}]}`)
646
+ Pulumi.Output.all2((sharedOutput, Pulumi.Output.all(handlerOutputs)))
647
+ ->Pulumi.Output.apply(((shared, handlers)) => {
648
+ let json = `{${shared},"handlers":[${handlers->Array.join(",")}]}`
649
+ Util_LambdaEnvBudget.check(~lambdaName="AllAutomationSlices", ~handlerConfigJson=json)
650
+ json
651
+ })
619
652
 
620
653
  let envVars: dict<Pulumi.Input.t<string>> = Dict.make()
621
654
  envVars->Dict.set("HANDLER_CONFIG", handlerConfigOutput->Pulumi.Output.asInput)
@@ -630,9 +663,18 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
630
663
  envVars->Dict.set(name, value->Pulumi.Output.asInput)
631
664
  )
632
665
 
666
+ // Carries no Pulumi Output, so the archive is still built synchronously —
667
+ // the table names and urns that do are what stays in HANDLER_CONFIG.
668
+ let extraStringAssets = Dict.make()
669
+ extraStringAssets->Dict.set(
670
+ "automationSliceModules.json",
671
+ `{${sliceModuleEntries->Array.join(",")}}`,
672
+ )
673
+
633
674
  let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
634
675
  ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/AutomationSliceEntryPoint.mjs",
635
676
  ~packageDirs,
677
+ ~extraStringAssets,
636
678
  )
637
679
 
638
680
  let runtime = RuntimeEnvironment_Lambda.makeFromCodeAsset(
@@ -18,6 +18,7 @@ import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/
18
18
  import * as Cloudwatch_EventRule$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Cloudwatch/Cloudwatch_EventRule.res.mjs";
19
19
  import * as Cloudwatch_EventTarget$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Cloudwatch/Cloudwatch_EventTarget.res.mjs";
20
20
  import * as CommandTopicRegistry$ReventlessAws from "../CommandTopic/CommandTopicRegistry.res.mjs";
21
+ import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBudget.res.mjs";
21
22
  import * as PluginRuntime_Builder$ReventlessAws from "../../plugin/runtime/PluginRuntime_Builder.res.mjs";
22
23
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
23
24
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
@@ -279,12 +280,13 @@ function finishWithDcbEventLog(dcbEventLog) {
279
280
  let url = dcbQueueUrlRef.contents;
280
281
  let dcbQueueUrl = url !== undefined ? url : Pulumi.output("NOT_AVAILABLE");
281
282
  let handlerOutputs = [];
283
+ let sliceModuleEntries = [];
282
284
  let packageDirs = {};
283
285
  let allQueryDbResources = [];
284
286
  let anyDcbFallback = {
285
287
  contents: false
286
288
  };
287
- Stdlib_Dict.forEachWithKey(bundledInfos, (info, _name) => {
289
+ Stdlib_Dict.forEachWithKey(bundledInfos, (info, name) => {
288
290
  info.queryDbResources.forEach(r => {
289
291
  allQueryDbResources.push(r);
290
292
  });
@@ -303,6 +305,8 @@ function finishWithDcbEventLog(dcbEventLog) {
303
305
  let callbackType = Stdlib_Option.getOr(JSON.stringify(info.callbackType), `""`);
304
306
  let context = info.context;
305
307
  let contextFragment = context !== undefined ? Stdlib_Option.getOr(Stdlib_Option.map(JSON.stringify(context), json => `,"context":` + json), "") : "";
308
+ let nameJson = Stdlib_Option.getOr(JSON.stringify(name), `""`);
309
+ sliceModuleEntries.push(nameJson + `:{"specModule":` + specModule + `,"bodyModule":` + bodyModule + `,"callbackType":` + callbackType + contextFragment + `}`);
306
310
  let match = target !== undefined ? [
307
311
  target.queueUrl,
308
312
  target.isFifo
@@ -311,16 +315,17 @@ function finishWithDcbEventLog(dcbEventLog) {
311
315
  dcbQueueIsFifo.contents
312
316
  ];
313
317
  let commandQueueIsFifo = match[1];
318
+ let hasOwnUrns = !info.consumesDcbLog || Object.keys(info.sourceTopics).length !== 0;
314
319
  let handlerJson = Pulumi.all([
315
320
  info.queryDbTableName,
316
321
  match[0],
317
322
  sourceUrnsFor(info)
318
323
  ]).apply(param => {
319
- let urns = param[2];
320
- let urnsJson = JSON.stringify(urns.map(prim => prim));
321
- let firstUrn = Stdlib_Option.getOr(urns[0], "");
322
- let fifoJson = commandQueueIsFifo ? "true" : "false";
323
- return `{"specModule":` + specModule + `,"bodyModule":` + bodyModule + `,"callbackType":` + callbackType + `,"queryDbTableName":"` + param[0] + `","dcbQueueUrl":"` + param[1] + `","commandQueueIsFifo":` + fifoJson + `,"sourceUrn":"` + firstUrn + `","sourceUrns":` + urnsJson + contextFragment + `}`;
324
+ let urnsFragment = hasOwnUrns ? `,"u":` + JSON.stringify(param[2].map(prim => prim)) : "";
325
+ let queueFragment = target !== undefined ? `,"k":"` + param[1] + `","f":` + (
326
+ commandQueueIsFifo ? "true" : "false"
327
+ ) : "";
328
+ return `{"n":` + nameJson + `,"q":"` + param[0] + `"` + urnsFragment + queueFragment + `}`;
324
329
  });
325
330
  handlerOutputs.push(handlerJson);
326
331
  });
@@ -329,13 +334,30 @@ function finishWithDcbEventLog(dcbEventLog) {
329
334
  allQueryDbResources.push(r);
330
335
  });
331
336
  }
332
- let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
337
+ let sharedOutput = Pulumi.all([
338
+ dcbQueueUrl,
339
+ dcbSourceUrn
340
+ ]).apply(param => {
341
+ let fifoJson = dcbQueueIsFifo.contents ? "true" : "false";
342
+ let urnJson = Stdlib_Option.getOr(JSON.stringify(param[1]), `""`);
343
+ return `"queueUrl":"` + param[0] + `","commandQueueIsFifo":` + fifoJson + `,"urns":[` + urnJson + `]`;
344
+ });
345
+ let handlerConfigOutput = Pulumi.all([
346
+ sharedOutput,
347
+ Pulumi.all(handlerOutputs)
348
+ ]).apply(param => {
349
+ let json = `{` + param[0] + `,"handlers":[` + param[1].join(",") + `]}`;
350
+ Util_LambdaEnvBudget$ReventlessAws.check("AllAutomationSlices", json, undefined);
351
+ return json;
352
+ });
333
353
  let envVars = {};
334
354
  envVars["HANDLER_CONFIG"] = handlerConfigOutput;
335
355
  PluginRuntime_Builder$ReventlessAws.capabilityEnv().forEach(param => {
336
356
  envVars[param[0]] = param[1];
337
357
  });
338
- let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AutomationSliceEntryPoint.mjs", packageDirs, undefined, undefined);
358
+ let extraStringAssets = {};
359
+ extraStringAssets["automationSliceModules.json"] = `{` + sliceModuleEntries.join(",") + `}`;
360
+ let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AutomationSliceEntryPoint.mjs", packageDirs, extraStringAssets, undefined);
339
361
  let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset("AllAutomationSlices", "Reactor", "AutomationSlice", match.code, match.sourceCodeHash, envVars, 1024, 30, undefined, undefined, undefined, undefined, undefined, opts);
340
362
  EventCollectorChannel_DynamoDbStream$ReventlessAws.connect("AllAutomationSlices", [{
341
363
  channel: channel,
@@ -251,9 +251,11 @@ let finish = () =>
251
251
 
252
252
  let handlerConfigOutput =
253
253
  Pulumi.Output.all(handlerOutputs)
254
- ->Pulumi.Output.apply(handlers =>
255
- `{"handlers":[${handlers->Array.join(",")}]}`
256
- )
254
+ ->Pulumi.Output.apply(handlers => {
255
+ let json = `{"handlers":[${handlers->Array.join(",")}]}`
256
+ Util_LambdaEnvBudget.check(~lambdaName="AllReadModels", ~handlerConfigJson=json)
257
+ json
258
+ })
257
259
 
258
260
  let envVars: dict<Pulumi.Input.t<string>> = Dict.make()
259
261
  envVars->Dict.set("HANDLER_CONFIG", handlerConfigOutput->Pulumi.Output.asInput)
@@ -18,6 +18,7 @@ import * as EventLogBackend$ReventlessAws from "../EventLog/EventLogBackend.res.
18
18
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
19
19
  import * as PgProjectionFeed$ReventlessAws from "../Postgres/PgProjectionFeed.res.mjs";
20
20
  import * as Util_LogAttribution$ReventlessAws from "../../util/Util_LogAttribution.res.mjs";
21
+ import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBudget.res.mjs";
21
22
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
22
23
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
23
24
 
@@ -142,7 +143,11 @@ function finish() {
142
143
  });
143
144
  handlerOutputs.push(handlerJson);
144
145
  });
145
- let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
146
+ let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => {
147
+ let json = `{"handlers":[` + handlers.join(",") + `]}`;
148
+ Util_LambdaEnvBudget$ReventlessAws.check("AllReadModels", json, undefined);
149
+ return json;
150
+ });
146
151
  let envVars = {};
147
152
  envVars["HANDLER_CONFIG"] = handlerConfigOutput;
148
153
  let anyPgStream = Object.keys(readModelInfos).some(rmName => {
@@ -153,9 +153,11 @@ let finish = () =>
153
153
 
154
154
  let handlerConfigOutput =
155
155
  Pulumi.Output.all(handlerOutputs)
156
- ->Pulumi.Output.apply(handlers =>
157
- `{"handlers":[${handlers->Array.join(",")}]}`
158
- )
156
+ ->Pulumi.Output.apply(handlers => {
157
+ let json = `{"handlers":[${handlers->Array.join(",")}]}`
158
+ Util_LambdaEnvBudget.check(~lambdaName="AllSideEffectHandlers", ~handlerConfigJson=json)
159
+ json
160
+ })
159
161
 
160
162
  let envVars: dict<Pulumi.Input.t<string>> = Dict.make()
161
163
  envVars->Dict.set("HANDLER_CONFIG", handlerConfigOutput->Pulumi.Output.asInput)
@@ -11,6 +11,7 @@ import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/co
11
11
  import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
12
12
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
13
13
  import * as Util_LogAttribution$ReventlessAws from "../../util/Util_LogAttribution.res.mjs";
14
+ import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBudget.res.mjs";
14
15
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
15
16
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
16
17
 
@@ -109,7 +110,11 @@ function finish() {
109
110
  });
110
111
  handlerOutputs.push(handlerJson);
111
112
  });
112
- let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
113
+ let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => {
114
+ let json = `{"handlers":[` + handlers.join(",") + `]}`;
115
+ Util_LambdaEnvBudget$ReventlessAws.check("AllSideEffectHandlers", json, undefined);
116
+ return json;
117
+ });
113
118
  let envVars = {};
114
119
  envVars["HANDLER_CONFIG"] = handlerConfigOutput;
115
120
  Stdlib_Dict.forEachWithKey(extraEnvVarsAll, (v, k) => {
@@ -222,7 +222,9 @@ let buildLambda = (
222
222
  ->Array.join(",")
223
223
  let baseJson = base->JSON.stringifyAny->Option.getOr(`""`)
224
224
  let urnJson = sharedUrn->JSON.stringifyAny->Option.getOr(`""`)
225
- `{"v":2,"base":${baseJson},"sourceUrn":${urnJson},"handlers":[${entries}]${pgFragment}}`
225
+ let json = `{"v":2,"base":${baseJson},"sourceUrn":${urnJson},"handlers":[${entries}]${pgFragment}}`
226
+ Util_LambdaEnvBudget.check(~lambdaName="AllStateViewSlices", ~handlerConfigJson=json)
227
+ json
226
228
  })
227
229
 
228
230
  let envVars: dict<Pulumi.Input.t<string>> = Dict.make()
@@ -18,6 +18,7 @@ import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/
18
18
  import * as QueryDbBackend$ReventlessAws from "../QueryDb/QueryDbBackend.res.mjs";
19
19
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
20
20
  import * as PgProjectionFeed$ReventlessAws from "../Postgres/PgProjectionFeed.res.mjs";
21
+ import * as Util_LambdaEnvBudget$ReventlessAws from "../../util/Util_LambdaEnvBudget.res.mjs";
21
22
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
22
23
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
23
24
 
@@ -144,7 +145,9 @@ function buildLambda(parent, handlerOutputs, packageDirs, channelSpecs, feedQueu
144
145
  }).join(",");
145
146
  let baseJson = Stdlib_Option.getOr(JSON.stringify(base), `""`);
146
147
  let urnJson = Stdlib_Option.getOr(JSON.stringify(sharedUrn), `""`);
147
- return `{"v":2,"base":` + baseJson + `,"sourceUrn":` + urnJson + `,"handlers":[` + entries + `]` + param[1] + `}`;
148
+ let json = `{"v":2,"base":` + baseJson + `,"sourceUrn":` + urnJson + `,"handlers":[` + entries + `]` + param[1] + `}`;
149
+ Util_LambdaEnvBudget$ReventlessAws.check("AllStateViewSlices", json, undefined);
150
+ return json;
148
151
  });
149
152
  let envVars = {};
150
153
  envVars["HANDLER_CONFIG"] = handlerConfigOutput;
@@ -0,0 +1,141 @@
1
+ // The messaging capability a deployment provisions: which transport, sending as
2
+ // whom. The entry point a platform root calls; the transports sit below it.
3
+ //
4
+ // **All of it is configuration, none of it is code.** Which transport is a
5
+ // per-environment decision — a staging stack should not mail customers, and
6
+ // nothing about the shop it deploys says so — and the address is a per-account
7
+ // fact, since SES sends from one verified identity. A repository cannot know
8
+ // either, and a placeholder default for the address is worse than none: it
9
+ // provisions an identity nobody can verify, after which every send is refused at
10
+ // the sender while reading like a delivery problem.
11
+ //
12
+ // Config keys, on the usual ladder — env var `REVENTLESS_<SCREAMING_SNAKE>`, then
13
+ // `Pulumi.local.yaml`, then `platform:<key>` in `Pulumi.<stack>.yaml`:
14
+ //
15
+ // - `messagingEmailProvider` — "ses" (default) or "log". Per channel, like
16
+ // the senders: an SMS transport will bring its own.
17
+ // - `messagingEmailSender` — the address messages are sent from. Under
18
+ // `ses` it is verified and required; under
19
+ // `log` it is only a header and defaults.
20
+ // - `messagingEmailSenderName` — optional display name for the `From:` header.
21
+ // - `messagingSmsSender` — origination number or sender id. Carried only;
22
+ // no transport reads it yet.
23
+ //
24
+ // **Unset is a real answer, and the deploy still checks it.** A platform whose
25
+ // plugins declare the Messaging capability and which provisions no sender is
26
+ // refused at deploy time by `Platform.deployPlatform`, naming the key — so "no
27
+ // mail configured" cannot reach production as silence.
28
+
29
+ let emailProviderKey = "messagingEmailProvider"
30
+ let emailSenderKey = "messagingEmailSender"
31
+ let emailSenderNameKey = "messagingEmailSenderName"
32
+ let smsSenderKey = "messagingSmsSender"
33
+
34
+ /** One key off the ladder, trimmed, with blank read as absent.
35
+
36
+ Blank matters because the ways to write "nothing" must not differ: a stray
37
+ `REVENTLESS_MESSAGING_EMAIL_SENDER=` in CI, a blank line in
38
+ `Pulumi.local.yaml`, and a key left with a space after the colon all mean the
39
+ stack named no sender. Honouring any of them would ask SES for an identity
40
+ with a blank address — a deploy failure whose cause is whitespace, and one
41
+ that reaches AWS before anything here explains it.
42
+
43
+ Trimmed rather than only compared, so a value that survives is also the value
44
+ used: an address with a trailing space is a `From:` header with one. */
45
+ let configured = (key: string): option<string> =>
46
+ switch Util_LocalConfig.get(key) {
47
+ | Some(_) as v => v
48
+ | None => Pulumi.Config.make(Some("platform"))->Pulumi.Config.get(key)
49
+ }->Option.flatMap(value => {
50
+ let trimmed = value->String.trim
51
+ trimmed === "" ? None : Some(trimmed)
52
+ })
53
+
54
+ /** The transports a deployment can choose between for the EMAIL channel. The name travels to the
55
+ Lambdas, so it is also the wire value — one spelling, not a deploy-time enum
56
+ and a runtime string that can disagree. */
57
+ type emailProvider =
58
+ | Ses
59
+ | Log
60
+
61
+ let emailProviderName = (provider: emailProvider): string =>
62
+ switch provider {
63
+ | Ses => "ses"
64
+ | Log => "log"
65
+ }
66
+
67
+ /**
68
+ Which transport this stack asked for. SES is the default, so a deployment that
69
+ says nothing gets the real one — the safe direction is that mail works, and a
70
+ stack opting out of sending says so explicitly.
71
+
72
+ An unrecognised value is refused rather than defaulted. Falling back to SES on a
73
+ typo would provision a real identity for a stack that asked to only log, which
74
+ is the one mistake this key exists to prevent.
75
+ */
76
+ let parseEmailProvider = (raw: string): emailProvider =>
77
+ switch raw->String.trim->String.toLowerCase {
78
+ | "ses" => Ses
79
+ | "log" => Log
80
+ | other =>
81
+ JsError.throwWithMessage(
82
+ `platform:${emailProviderKey} is "${other}", which is not an email provider.\n` ++
83
+ ` Known values: "ses" (default — a verified SES identity) and "log" ` ++
84
+ `(writes each message to the log, sends nothing).`,
85
+ )
86
+ }
87
+
88
+ let emailProvider = (): emailProvider =>
89
+ configured(emailProviderKey)->Option.mapOr(Ses, parseEmailProvider)
90
+
91
+ /** The `From:` a log-transport deployment presents as when the stack named no
92
+ address.
93
+
94
+ Hard-coded where the SES address is required, and the asymmetry follows from
95
+ what the value is for: a logged message reaches nobody, so there is nothing to
96
+ verify and nothing that differs per environment. `.test` is reserved for
97
+ exactly this (RFC 6761) — it can never resolve, so an address escaping into a
98
+ fixture cannot reach a real inbox. */
99
+ let logDefaultAddress = "notifications@online-shop.test"
100
+
101
+ /**
102
+ Provision the capability and hand back the platform's messaging handle.
103
+
104
+ `~name` is used only by the SES arm, which is the only arm with a resource: the
105
+ log transport is a runtime decision carried in an environment variable, so choosing it
106
+ creates nothing and costs nothing to leave configured on a stack that is torn
107
+ down and rebuilt.
108
+ */
109
+ let make = (
110
+ ~name: string,
111
+ ~opts: option<Pulumi.CustomResourceOptions.t>=?,
112
+ ): ReventlessInfra.Platform.messagingSender => {
113
+ // Read whether or not there is an email sender: the SMS half is independent,
114
+ // and a stack may state one without the other.
115
+ let smsSender = configured(smsSenderKey)->Option.map(Pulumi.Input.make)
116
+ let chosen = emailProvider()
117
+ let displayName = configured(emailSenderNameKey)
118
+ let providerInput = Pulumi.Input.make(chosen->emailProviderName)
119
+
120
+ switch (chosen, configured(emailSenderKey)) {
121
+ // Asked for SES and named no address. Nothing is provisioned, and the deploy
122
+ // gate refuses this the moment a plugin declares it needs messaging — here
123
+ // rather than there because this module has no view of what the plugins want.
124
+ | (Ses, None) => {emailProvider: providerInput, smsSender: ?smsSender}
125
+ | (Ses, Some(address)) => {
126
+ emailSender: Capability_Messaging_Ses.emailSender(~name, ~address, ~displayName, ~opts?),
127
+ emailProvider: providerInput,
128
+ smsSender: ?smsSender,
129
+ }
130
+ | (Log, address) => {
131
+ emailSender: Pulumi.Input.make(
132
+ Reventless.Messaging.fromHeader(
133
+ ~displayName,
134
+ ~address=address->Option.getOr(logDefaultAddress),
135
+ ),
136
+ ),
137
+ emailProvider: providerInput,
138
+ smsSender: ?smsSender,
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,96 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Pulumi from "@pulumi/pulumi";
5
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
+ import * as Messaging$Reventless from "@reventlessdev/reventless-spec/src/semantic/Messaging.res.mjs";
7
+ import * as Util_LocalConfig$ReventlessAws from "../util/Util_LocalConfig.res.mjs";
8
+ import * as Capability_Messaging_Ses$ReventlessAws from "./Capability_Messaging_Ses.res.mjs";
9
+
10
+ let emailProviderKey = "messagingEmailProvider";
11
+
12
+ let emailSenderKey = "messagingEmailSender";
13
+
14
+ let emailSenderNameKey = "messagingEmailSenderName";
15
+
16
+ let smsSenderKey = "messagingSmsSender";
17
+
18
+ function configured(key) {
19
+ let v = Util_LocalConfig$ReventlessAws.get(key);
20
+ return Stdlib_Option.flatMap(v !== undefined ? v : new Pulumi.Config("platform").get(key), value => {
21
+ let trimmed = value.trim();
22
+ if (trimmed === "") {
23
+ return;
24
+ } else {
25
+ return trimmed;
26
+ }
27
+ });
28
+ }
29
+
30
+ function emailProviderName(provider) {
31
+ if (provider === "Ses") {
32
+ return "ses";
33
+ } else {
34
+ return "log";
35
+ }
36
+ }
37
+
38
+ function parseEmailProvider(raw) {
39
+ let other = raw.trim().toLowerCase();
40
+ switch (other) {
41
+ case "log" :
42
+ return "Log";
43
+ case "ses" :
44
+ return "Ses";
45
+ default:
46
+ return Stdlib_JsError.throwWithMessage(`platform:` + emailProviderKey + ` is "` + other + `", which is not an email provider.\n Known values: "ses" (default — a verified SES identity) and "log" (writes each message to the log, sends nothing).`);
47
+ }
48
+ }
49
+
50
+ function emailProvider() {
51
+ return Stdlib_Option.mapOr(configured(emailProviderKey), "Ses", parseEmailProvider);
52
+ }
53
+
54
+ let logDefaultAddress = "notifications@online-shop.test";
55
+
56
+ function make(name, opts) {
57
+ let smsSender = Stdlib_Option.map(configured(smsSenderKey), prim => prim);
58
+ let chosen = emailProvider();
59
+ let displayName = configured(emailSenderNameKey);
60
+ let providerInput = emailProviderName(chosen);
61
+ let match = configured(emailSenderKey);
62
+ if (chosen === "Ses") {
63
+ if (match !== undefined) {
64
+ return {
65
+ emailSender: Capability_Messaging_Ses$ReventlessAws.emailSender(name, match, displayName, opts),
66
+ smsSender: smsSender,
67
+ emailProvider: providerInput
68
+ };
69
+ } else {
70
+ return {
71
+ smsSender: smsSender,
72
+ emailProvider: providerInput
73
+ };
74
+ }
75
+ } else {
76
+ return {
77
+ emailSender: Messaging$Reventless.fromHeader(displayName, Stdlib_Option.getOr(match, logDefaultAddress)),
78
+ smsSender: smsSender,
79
+ emailProvider: providerInput
80
+ };
81
+ }
82
+ }
83
+
84
+ export {
85
+ emailProviderKey,
86
+ emailSenderKey,
87
+ emailSenderNameKey,
88
+ smsSenderKey,
89
+ configured,
90
+ emailProviderName,
91
+ parseEmailProvider,
92
+ emailProvider,
93
+ logDefaultAddress,
94
+ make,
95
+ }
96
+ /* @pulumi/pulumi Not a pure module */
@@ -1,10 +1,11 @@
1
- // A verified SES sender identity, provisioned with the framework's house
2
- // conventions applied — the backing for the messaging capability's email channel.
1
+ // A verified SES sender identity the email channel's real transport.
3
2
  //
4
- // The identity is the whole of the provisioning: SES sends from a verified
5
- // address, and everything else a send needs travels on the message. So this
6
- // helper exists to make the sender a deploy-time decision with one owner rather
7
- // than an environment variable each stack spells for itself.
3
+ // Provisioning only. Which transport a deployment uses, and what address it
4
+ // sends from, are read by `Capability_Messaging`, which is the entry point a
5
+ // platform root calls; this module receives a resolved address and creates the
6
+ // identity for it. Split that way because a deployment can choose *not* to have
7
+ // an SES identity, and a module that read its own config could not be left
8
+ // uncalled without also leaving the config unread.
8
9
  //
9
10
  // **Verification is not instant and not automatic.** Creating the identity asks
10
11
  // AWS to send a confirmation mail to the address; until somebody follows it, SES
@@ -18,14 +19,26 @@
18
19
 
19
20
  open PulumiAws
20
21
 
21
- /** Declare the sender address and hand back the platform's messaging handle.
22
- `~email` is the `From:` every message this deployment sends carries. */
23
- let make = (
22
+ /**
23
+ Create the identity and hand back the `From:` it sends as.
24
+
25
+ `~name` is the Pulumi resource name — a URN rather than a setting, so it stays in
26
+ code: moving it to config would replace the resource whenever a stack spelled it
27
+ differently.
28
+
29
+ The identity is created for the bare address, because SES verifies an address and
30
+ not a header; the display name is applied afterwards, to the value the send path
31
+ reads. Applied over the identity's own output rather than over the configured
32
+ string, so the sender a Lambda is handed still depends on the resource existing.
33
+ */
34
+ let emailSender = (
24
35
  ~name: string,
25
- ~email: string,
36
+ ~address: string,
37
+ ~displayName: option<string>,
26
38
  ~opts: option<Pulumi.CustomResourceOptions.t>=?,
27
- ): ReventlessInfra.Platform.messagingSender => {
28
- let identity = SES.EmailIdentity.make(~name, ~args={email: email}, ~opts?)
29
-
30
- {emailSender: identity.email->Pulumi.Output.asInput}
39
+ ): Pulumi.Input.t<string> => {
40
+ let identity = SES.EmailIdentity.make(~name, ~args={email: address}, ~opts?)
41
+ identity.email
42
+ ->Pulumi.Output.apply(verified => Reventless.Messaging.fromHeader(~displayName, ~address=verified))
43
+ ->Pulumi.Output.asInput
31
44
  }
@@ -2,17 +2,16 @@
2
2
 
3
3
  import * as Aws from "@pulumi/aws";
4
4
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
5
+ import * as Messaging$Reventless from "@reventlessdev/reventless-spec/src/semantic/Messaging.res.mjs";
5
6
 
6
- function make(name, email, opts) {
7
+ function emailSender(name, address, displayName, opts) {
7
8
  let identity = new (Aws.ses.EmailIdentity)(name, {
8
- email: email
9
+ email: address
9
10
  }, opts !== undefined ? Primitive_option.valFromOption(opts) : undefined);
10
- return {
11
- emailSender: identity.email
12
- };
11
+ return identity.email.apply(verified => Messaging$Reventless.fromHeader(displayName, verified));
13
12
  }
14
13
 
15
14
  export {
16
- make,
15
+ emailSender,
17
16
  }
18
17
  /* @pulumi/aws Not a pure module */