@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.
- package/CHANGELOG.md +21 -0
- package/package.json +8 -8
- package/src/Platform.res +71 -6
- package/src/Platform.res.mjs +63 -12
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res +5 -3
- package/src/adapter/Runtime/AggregateRuntime_Builder_Single.res.mjs +6 -1
- package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +21 -1
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +93 -11
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +53 -4
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +53 -11
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +30 -8
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +5 -3
- package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res.mjs +6 -1
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res +5 -3
- package/src/adapter/Runtime/SideEffectHandlerRuntime_Builder_Single.res.mjs +6 -1
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res +3 -1
- package/src/adapter/Runtime/StateViewSliceRuntime_Builder_Single.res.mjs +4 -1
- package/src/capability/Capability_Messaging.res +141 -0
- package/src/capability/Capability_Messaging.res.mjs +96 -0
- package/src/capability/Capability_Messaging_Ses.res +27 -14
- package/src/capability/Capability_Messaging_Ses.res.mjs +5 -6
- package/tests/AutomationSliceEntryPoint_OpsTest.res +122 -0
- package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +187 -0
- package/tests/Capability_MessagingTest.res +100 -0
- 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,
|
|
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
|
|
597
|
-
urns->Array.map(JSON.Encode.string)->JSON.Encode.array->JSON.stringify
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
`{"
|
|
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 =>
|
|
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,
|
|
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
|
|
320
|
-
let
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return `{"
|
|
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
|
|
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
|
|
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 =>
|
|
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 =>
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
/**
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
~
|
|
36
|
+
~address: string,
|
|
37
|
+
~displayName: option<string>,
|
|
26
38
|
~opts: option<Pulumi.CustomResourceOptions.t>=?,
|
|
27
|
-
):
|
|
28
|
-
let identity = SES.EmailIdentity.make(~name, ~args={email:
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
7
|
+
function emailSender(name, address, displayName, opts) {
|
|
7
8
|
let identity = new (Aws.ses.EmailIdentity)(name, {
|
|
8
|
-
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
|
-
|
|
15
|
+
emailSender,
|
|
17
16
|
}
|
|
18
17
|
/* @pulumi/aws Not a pure module */
|