@reventlessdev/reventless-aws 3.0.0-alpha.330 → 3.0.0-alpha.332
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 +20 -0
- package/package.json +5 -5
- package/src/Platform.res +131 -4
- package/src/Platform.res.mjs +113 -22
- package/src/adapter/Messaging/Messaging_Ses_Backend.res +84 -0
- package/src/adapter/Messaging/Messaging_Ses_Backend.res.mjs +120 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +6 -1
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +5 -7
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +38 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +19 -0
- package/src/capability/Capability_Messaging_Ses.res +31 -0
- package/src/capability/Capability_Messaging_Ses.res.mjs +18 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res +14 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +26 -0
- package/tests/BakeConvergenceTest.res +2 -0
- package/tests/BakeConvergenceTest.res.mjs +6 -2
- package/tests/integration/AggTestAggregate.res +2 -0
- package/tests/integration/AggTestAggregate.res.mjs +5 -0
- package/tests/integration/EpCompositeSlice.res +2 -0
- package/tests/integration/EpCompositeSlice.res.mjs +5 -0
- package/tests/integration/EpTestSlice.res +2 -0
- package/tests/integration/EpTestSlice.res.mjs +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# 3.0.0-alpha.332 (2026-09-01)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.331 (2026-09-01)
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **capabilities:** a plugin's geocoding need was declared nowhere and failed silently ([67917dd](https://github.com/ReventlessDev/reventless-core/commit/67917dd504b43fa78b7c6a51644c9eae656b7f6b))
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* **capabilities:** a plugin can send a message without naming a provider ([b001a1e](https://github.com/ReventlessDev/reventless-core/commit/b001a1e9361a0f4d0affe228cb4c08c38a5a995e))
|
|
22
|
+
* **spec:** a command declares its lifecycle edge as a value ([40eee9f](https://github.com/ReventlessDev/reventless-core/commit/40eee9f7723dc05e418be680528f01967d074da4))
|
|
23
|
+
* **spec:** a graft leaves a trace the deployed plugin can read ([c08ff6c](https://github.com/ReventlessDev/reventless-core/commit/c08ff6c0f6177d58603e7ae1e5cec392d9bac16a))
|
|
24
|
+
|
|
25
|
+
|
|
6
26
|
# 3.0.0-alpha.330 (2026-08-27)
|
|
7
27
|
|
|
8
28
|
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.332",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"bin": {
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
"@reventlessdev/rescript-node": "2.0.0-alpha.8",
|
|
23
23
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
|
|
24
24
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
25
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.
|
|
26
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.
|
|
25
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.253",
|
|
26
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.154",
|
|
27
27
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.34",
|
|
28
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
29
|
-
"@reventlessdev/reventless-spec": "3.0.0-alpha.
|
|
28
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.117",
|
|
29
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.126"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -99,6 +99,10 @@ let getObjectStoreEndpoints = () => objectStoreEndpointsRef.contents
|
|
|
99
99
|
the same sentinel, as `PluginRuntime_Builder.inboundSliceReg.auditTableName`. */
|
|
100
100
|
let geocoderPlaceIndexRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make(""))
|
|
101
101
|
|
|
102
|
+
/** The verified sender address, for the same single-program case and with the
|
|
103
|
+
same `""` sentinel and the same reason. */
|
|
104
|
+
let messagingEmailSenderRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make(""))
|
|
105
|
+
|
|
102
106
|
module MakeWithConfig = (
|
|
103
107
|
Config: {
|
|
104
108
|
let splitApi: bool
|
|
@@ -1174,6 +1178,12 @@ module MakeWithConfig = (
|
|
|
1174
1178
|
// on the domain API (`Geocoder_AwsLocation_Resolver`, split mode) — and exports
|
|
1175
1179
|
// the index name for the unattended slice path. Unset ⇒ no resolver, no export.
|
|
1176
1180
|
geocoderPlaceIndex?: ReventlessInfra.Platform.geocoderIndex,
|
|
1181
|
+
// Optional messaging sender, as returned by `Capability_Messaging_Ses.make`.
|
|
1182
|
+
// When set, the deploy exports the verified `From:` address for the
|
|
1183
|
+
// unattended slice path and grants those Lambdas `ses:SendEmail`. No client
|
|
1184
|
+
// door: unlike geocoding, a browser has no business sending mail from the
|
|
1185
|
+
// deployment's identity. Unset ⇒ no export, no grant.
|
|
1186
|
+
messagingSender?: ReventlessInfra.Platform.messagingSender,
|
|
1177
1187
|
// Optional object store for direct-to-S3 uploads, as returned by
|
|
1178
1188
|
// `Capability_ObjectStore_S3.make`. When set, the deploy registers it with
|
|
1179
1189
|
// the platform's one Upload service (`Upload_Presign_S3`) under the `uploads`
|
|
@@ -1726,7 +1736,8 @@ module MakeWithConfig = (
|
|
|
1726
1736
|
->Array.filterMap(c =>
|
|
1727
1737
|
switch c {
|
|
1728
1738
|
| ObjectStore({plugin, store}) => Some((plugin, store))
|
|
1729
|
-
| Geocoding
|
|
1739
|
+
| Geocoding
|
|
1740
|
+
| Messaging => None
|
|
1730
1741
|
}
|
|
1731
1742
|
)
|
|
1732
1743
|
->Array.reduce([], (acc, (plugin, store)) =>
|
|
@@ -2141,6 +2152,47 @@ module MakeWithConfig = (
|
|
|
2141
2152
|
Pulumi.Pulumi.export("geocoderPlaceIndex", geocoderPlaceIndexFlat)
|
|
2142
2153
|
geocoderPlaceIndexRef := geocoderPlaceIndexFlat
|
|
2143
2154
|
|
|
2155
|
+
// The generated capability list says a plugin of this deployment declared it
|
|
2156
|
+
// needs geocoding. Provisioning it is a hand-written line in this root —
|
|
2157
|
+
// `Capability_Geocoding_AwsLocation.make`, passed as `~geocoderPlaceIndex` —
|
|
2158
|
+
// so the list and the handle can disagree, and the way they disagree is
|
|
2159
|
+
// silent: the slice retries, exhausts, and writes a permanent verdict.
|
|
2160
|
+
// Caught here rather than only in the plugin stack because this is the stack
|
|
2161
|
+
// that can fix it, and it deploys first.
|
|
2162
|
+
if capabilities->Array.includes(Geocoding) && cfg.geocoderPlaceIndex->Option.isNone {
|
|
2163
|
+
JsError.throwWithMessage(
|
|
2164
|
+
`A plugin of this deployment declares the Geocoding capability, but this platform ` ++
|
|
2165
|
+
`provisions no place index.\n` ++
|
|
2166
|
+
` Add \`let placeIndex = ReventlessAws.Capability_Geocoding_AwsLocation.make(~name=…)\` ` ++
|
|
2167
|
+
`and pass \`~geocoderPlaceIndex=placeIndex\` in \`~hostUiBundle\`.\n` ++
|
|
2168
|
+
` The declaration is generated from the plugins' capabilities.json — regenerate with ` ++
|
|
2169
|
+
`\`pnpm run generate:platform\` if it is stale.`,
|
|
2170
|
+
)
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// The verified sender, exported for the same reason and read the same way.
|
|
2174
|
+
let messagingEmailSenderFlat = switch cfg.messagingSender {
|
|
2175
|
+
| Some({emailSender: ?Some(sender)}) => sender->Pulumi.Output.fromInput
|
|
2176
|
+
| _ => Pulumi.Output.make("")
|
|
2177
|
+
}
|
|
2178
|
+
Pulumi.Pulumi.export("messagingEmailSender", messagingEmailSenderFlat)
|
|
2179
|
+
messagingEmailSenderRef := messagingEmailSenderFlat
|
|
2180
|
+
|
|
2181
|
+
// Same gate as geocoding's, and the failure it prevents is worse: an
|
|
2182
|
+
// unprovisioned mailer answers `Unavailable` on every send, so the messages
|
|
2183
|
+
// queue and retry and are eventually abandoned — a notification nobody
|
|
2184
|
+
// receives and no error names.
|
|
2185
|
+
if capabilities->Array.includes(Messaging) && cfg.messagingSender->Option.isNone {
|
|
2186
|
+
JsError.throwWithMessage(
|
|
2187
|
+
`A plugin of this deployment declares the Messaging capability, but this platform ` ++
|
|
2188
|
+
`provisions no sender.\n` ++
|
|
2189
|
+
` Add \`let sender = ReventlessAws.Capability_Messaging_Ses.make(~name=…, ~email=…)\` ` ++
|
|
2190
|
+
`and pass \`~messagingSender=sender\` in \`~hostUiBundle\`.\n` ++
|
|
2191
|
+
` The declaration is generated from the plugins' capabilities.json — regenerate with ` ++
|
|
2192
|
+
`\`pnpm run generate:platform\` if it is stale.`,
|
|
2193
|
+
)
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2144
2196
|
// Presign endpoints are no longer written to config.json: under route B the
|
|
2145
2197
|
// client calls the platform API's `Upload_Presign` mutation (reachable from
|
|
2146
2198
|
// `platformApiEndpoint`, already present) with the store it declares, so there
|
|
@@ -2449,6 +2501,21 @@ module MakeWithConfig = (
|
|
|
2449
2501
|
PluginRuntime_Builder.registerCapabilityEnv("PLACE_INDEX_NAME", geocoderPlaceIndex)
|
|
2450
2502
|
PluginRuntime_Builder.registerGeocoderPlaceIndex(geocoderPlaceIndex)
|
|
2451
2503
|
|
|
2504
|
+
// The verified `From:` address, read across the same two halves and carrying
|
|
2505
|
+
// the same `""`-means-unprovisioned convention. Registered twice for the same
|
|
2506
|
+
// reason the place index is: the Lambda reads the address, and its role has to
|
|
2507
|
+
// be allowed to send from the identity that address names.
|
|
2508
|
+
let messagingEmailSender: Pulumi.Output.t<string> = switch platformStackRef {
|
|
2509
|
+
| Some(stackRef) =>
|
|
2510
|
+
(
|
|
2511
|
+
stackRef->Pulumi.StackReference.getOutput("messagingEmailSender"):
|
|
2512
|
+
Pulumi.Output.t<option<string>>
|
|
2513
|
+
)->Pulumi.Output.apply(o => o->Option.getOr(""))
|
|
2514
|
+
| None => messagingEmailSenderRef.contents
|
|
2515
|
+
}
|
|
2516
|
+
PluginRuntime_Builder.registerCapabilityEnv("MESSAGING_EMAIL_SENDER", messagingEmailSender)
|
|
2517
|
+
PluginRuntime_Builder.registerMessagingSender(messagingEmailSender)
|
|
2518
|
+
|
|
2452
2519
|
module P = unpack(plugin)
|
|
2453
2520
|
let pluginComponent = P.make()
|
|
2454
2521
|
ReventlessCore.Plugin_Helpers.clearOffload()
|
|
@@ -2580,9 +2647,69 @@ module MakeWithConfig = (
|
|
|
2580
2647
|
// is: a dangling `apply` is not guaranteed to be evaluated, and a check
|
|
2581
2648
|
// that might not run is not a check.
|
|
2582
2649
|
let capabilityGate: Pulumi.Output.t<unit> =
|
|
2583
|
-
(
|
|
2584
|
-
|
|
2585
|
-
|
|
2650
|
+
(
|
|
2651
|
+
pluginOutputs.pluginStructure,
|
|
2652
|
+
stackRef->Pulumi.StackReference.getOutput("objectStores"),
|
|
2653
|
+
geocoderPlaceIndex,
|
|
2654
|
+
messagingEmailSender,
|
|
2655
|
+
)
|
|
2656
|
+
->Pulumi.Output.all4
|
|
2657
|
+
->Pulumi.Output.apply((((structure, objectStores, placeIndex, emailSender): (
|
|
2658
|
+
_,
|
|
2659
|
+
option<JSON.t>,
|
|
2660
|
+
string,
|
|
2661
|
+
string,
|
|
2662
|
+
))) => {
|
|
2663
|
+
// ── Declared capabilities ──────────────────────────────────────────
|
|
2664
|
+
//
|
|
2665
|
+
// A slice that names `Geocoding` in `capabilityNeeds` says its
|
|
2666
|
+
// `translate` will reach `capabilities.geocode`. A platform that
|
|
2667
|
+
// provisioned no place index hands it an empty endpoint, and the client
|
|
2668
|
+
// reports `Unavailable` — retried three times, then `onExhausted` fires
|
|
2669
|
+
// and every address the slice ever sees is recorded as permanently
|
|
2670
|
+
// unresolvable. A silent data outcome with no error anywhere, which is
|
|
2671
|
+
// why a *declared* need fails the deploy.
|
|
2672
|
+
//
|
|
2673
|
+
// Only a declared need. A plugin that says nothing still degrades
|
|
2674
|
+
// exactly as before: the empty endpoint is a modelled outcome for a
|
|
2675
|
+
// deployment that simply has no geocoder, and failing those would break
|
|
2676
|
+
// deployments that work.
|
|
2677
|
+
switch Reventless.CapabilityNeed.unmet(
|
|
2678
|
+
~declared=structure
|
|
2679
|
+
->Option.flatMap(s => s.requiredCapabilities)
|
|
2680
|
+
->Option.getOr([])
|
|
2681
|
+
->Array.map(d => (d.capability, d.component)),
|
|
2682
|
+
// The values the slice Lambdas are actually handed, so this reads
|
|
2683
|
+
// what decides the outcome rather than a second record of the same
|
|
2684
|
+
// intent. `""` is what an unprovisioned platform exports.
|
|
2685
|
+
~provisioned=Array.concat(
|
|
2686
|
+
placeIndex == "" ? [] : [Reventless.CapabilityNeed.Geocoding],
|
|
2687
|
+
emailSender == "" ? [] : [Reventless.CapabilityNeed.Messaging],
|
|
2688
|
+
),
|
|
2689
|
+
) {
|
|
2690
|
+
| [] => ()
|
|
2691
|
+
| missing =>
|
|
2692
|
+
JsError.throwWithMessage(
|
|
2693
|
+
Reventless.CapabilityNeed.unmetMessage(missing) ++
|
|
2694
|
+
missing
|
|
2695
|
+
->Array.map(u =>
|
|
2696
|
+
switch u.need {
|
|
2697
|
+
| Geocoding =>
|
|
2698
|
+
`\n Geocoding is \`Capability_Geocoding_AwsLocation.make\`, passed to the ` ++
|
|
2699
|
+
`platform as \`~geocoderPlaceIndex\`.`
|
|
2700
|
+
| Messaging =>
|
|
2701
|
+
`\n Messaging is \`Capability_Messaging_Ses.make\`, passed to the platform ` ++
|
|
2702
|
+
`as \`~messagingSender\`.`
|
|
2703
|
+
}
|
|
2704
|
+
)
|
|
2705
|
+
// One line per capability, not per declaring component: two slices
|
|
2706
|
+
// missing the same provider need telling once.
|
|
2707
|
+
->Belt.Set.String.fromArray
|
|
2708
|
+
->Belt.Set.String.toArray
|
|
2709
|
+
->Array.join(""),
|
|
2710
|
+
)
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2586
2713
|
let required =
|
|
2587
2714
|
structure->Option.flatMap(s => s.requiredStores)->Option.getOr([])
|
|
2588
2715
|
let provisioned =
|
package/src/Platform.res.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output
|
|
|
11
11
|
import * as Pulumi$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Pulumi.res.mjs";
|
|
12
12
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
13
13
|
import * as Pulumi from "@pulumi/pulumi";
|
|
14
|
+
import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
|
|
14
15
|
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
15
16
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
16
17
|
import * as Plugin$ReventlessAws from "./components/Plugin.res.mjs";
|
|
@@ -25,6 +26,7 @@ import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/co
|
|
|
25
26
|
import * as DcbBackend$ReventlessAws from "./adapter/DcbEventLog/DcbBackend.res.mjs";
|
|
26
27
|
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
27
28
|
import * as ReadModel$ReventlessCore from "@reventlessdev/reventless-core/src/components/ReadModel/ReadModel.res.mjs";
|
|
29
|
+
import * as CapabilityNeed$Reventless from "@reventlessdev/reventless-spec/src/semantic/CapabilityNeed.res.mjs";
|
|
28
30
|
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
29
31
|
import * as Util_Bundle$ReventlessAws from "./util/Util_Bundle.res.mjs";
|
|
30
32
|
import * as Plugin_Stack$ReventlessAws from "./plugin/stack/Plugin_Stack.res.mjs";
|
|
@@ -138,6 +140,10 @@ let geocoderPlaceIndexRef = {
|
|
|
138
140
|
contents: Pulumi.output("")
|
|
139
141
|
};
|
|
140
142
|
|
|
143
|
+
let messagingEmailSenderRef = {
|
|
144
|
+
contents: Pulumi.output("")
|
|
145
|
+
};
|
|
146
|
+
|
|
141
147
|
function MakeWithConfig(Config) {
|
|
142
148
|
Stdlib_Option.forEach(Config.commandHandlerConfig.aggregates, param => {
|
|
143
149
|
Stdlib_Option.forEach(param.sync, AggregateRuntime_Builder_Single$ReventlessAws.setConfig);
|
|
@@ -451,6 +457,8 @@ function MakeWithConfig(Config) {
|
|
|
451
457
|
eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema,
|
|
452
458
|
commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
|
|
453
459
|
commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization,
|
|
460
|
+
commandTransition: UiFragmentRegistry$ReventlessCore.commandTransition,
|
|
461
|
+
traits: UiFragmentRegistry$ReventlessCore.traits,
|
|
454
462
|
readConsistency: UiFragmentRegistry$ReventlessCore.readConsistency
|
|
455
463
|
};
|
|
456
464
|
let UiFragmentRegistrySlice = StateChangeSlice_Builder$ReventlessAws.Make(Spec)({
|
|
@@ -638,7 +646,9 @@ function MakeWithConfig(Config) {
|
|
|
638
646
|
errorSchema: PluginSpec$ReventlessCore.errorSchema,
|
|
639
647
|
commandSchema: PluginSpec$ReventlessCore.commandSchema,
|
|
640
648
|
moduleUrl: PluginSpec$ReventlessCore.moduleUrl,
|
|
641
|
-
commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization
|
|
649
|
+
commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization,
|
|
650
|
+
commandTransition: PluginSpec$ReventlessCore.commandTransition,
|
|
651
|
+
traits: PluginSpec$ReventlessCore.traits
|
|
642
652
|
})({
|
|
643
653
|
initialState: PluginBehavior$ReventlessCore.initialState,
|
|
644
654
|
evolve: PluginBehavior$ReventlessCore.evolve,
|
|
@@ -1145,6 +1155,22 @@ function MakeWithConfig(Config) {
|
|
|
1145
1155
|
let geocoderPlaceIndexFlat = index$1 !== undefined ? index$1.indexName : Pulumi.output("");
|
|
1146
1156
|
Pulumi$Pulumi.$$export("geocoderPlaceIndex", geocoderPlaceIndexFlat);
|
|
1147
1157
|
geocoderPlaceIndexRef.contents = geocoderPlaceIndexFlat;
|
|
1158
|
+
if (capabilities.includes("Geocoding") && Stdlib_Option.isNone(hostUiBundle.geocoderPlaceIndex)) {
|
|
1159
|
+
Stdlib_JsError.throwWithMessage(`A plugin of this deployment declares the Geocoding capability, but this platform provisions no place index.\n Add \`let placeIndex = ReventlessAws.Capability_Geocoding_AwsLocation.make(~name=…)\` and pass \`~geocoderPlaceIndex=placeIndex\` in \`~hostUiBundle\`.\n The declaration is generated from the plugins' capabilities.json — regenerate with \`pnpm run generate:platform\` if it is stale.`);
|
|
1160
|
+
}
|
|
1161
|
+
let match$5 = hostUiBundle.messagingSender;
|
|
1162
|
+
let messagingEmailSenderFlat;
|
|
1163
|
+
if (match$5 !== undefined) {
|
|
1164
|
+
let sender = match$5.emailSender;
|
|
1165
|
+
messagingEmailSenderFlat = sender !== undefined ? sender : Pulumi.output("");
|
|
1166
|
+
} else {
|
|
1167
|
+
messagingEmailSenderFlat = Pulumi.output("");
|
|
1168
|
+
}
|
|
1169
|
+
Pulumi$Pulumi.$$export("messagingEmailSender", messagingEmailSenderFlat);
|
|
1170
|
+
messagingEmailSenderRef.contents = messagingEmailSenderFlat;
|
|
1171
|
+
if (capabilities.includes("Messaging") && Stdlib_Option.isNone(hostUiBundle.messagingSender)) {
|
|
1172
|
+
Stdlib_JsError.throwWithMessage(`A plugin of this deployment declares the Messaging capability, but this platform provisions no sender.\n Add \`let sender = ReventlessAws.Capability_Messaging_Ses.make(~name=…, ~email=…)\` and pass \`~messagingSender=sender\` in \`~hostUiBundle\`.\n The declaration is generated from the plugins' capabilities.json — regenerate with \`pnpm run generate:platform\` if it is stale.`);
|
|
1173
|
+
}
|
|
1148
1174
|
let configJsonContent = Pulumi.all([
|
|
1149
1175
|
Pulumi.all([
|
|
1150
1176
|
resolvedDomainApiEndpoint,
|
|
@@ -1214,9 +1240,9 @@ function MakeWithConfig(Config) {
|
|
|
1214
1240
|
contentType: "application/json"
|
|
1215
1241
|
});
|
|
1216
1242
|
}
|
|
1217
|
-
let match$
|
|
1218
|
-
if (componentDefinitions !== undefined && match$
|
|
1219
|
-
let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$
|
|
1243
|
+
let match$6 = hostUiBundle.bakedManifest;
|
|
1244
|
+
if (componentDefinitions !== undefined && match$6 !== undefined) {
|
|
1245
|
+
let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$6).map(param => param[0]);
|
|
1220
1246
|
let manifestKey = Stdlib_Option.getOr(manifestKeys[0], Platform_BakedManifest$ReventlessCore.defaultKey);
|
|
1221
1247
|
Pulumi.all([
|
|
1222
1248
|
componentDefinitions.roleId,
|
|
@@ -1297,6 +1323,9 @@ function MakeWithConfig(Config) {
|
|
|
1297
1323
|
let geocoderPlaceIndex = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("geocoderPlaceIndex").apply(o => Stdlib_Option.getOr(o, "")) : geocoderPlaceIndexRef.contents;
|
|
1298
1324
|
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("PLACE_INDEX_NAME", geocoderPlaceIndex);
|
|
1299
1325
|
PluginRuntime_Builder$ReventlessAws.registerGeocoderPlaceIndex(geocoderPlaceIndex);
|
|
1326
|
+
let messagingEmailSender = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("messagingEmailSender").apply(o => Stdlib_Option.getOr(o, "")) : messagingEmailSenderRef.contents;
|
|
1327
|
+
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("MESSAGING_EMAIL_SENDER", messagingEmailSender);
|
|
1328
|
+
PluginRuntime_Builder$ReventlessAws.registerMessagingSender(messagingEmailSender);
|
|
1300
1329
|
let pluginComponent = plugin.make();
|
|
1301
1330
|
Plugin_Helpers$ReventlessCore.clearOffload();
|
|
1302
1331
|
currentDeployTarget.contents = "Domain";
|
|
@@ -1352,17 +1381,36 @@ function MakeWithConfig(Config) {
|
|
|
1352
1381
|
let mergeGate = AppSync_MergedApi$ReventlessAws.mergeStatusGateWith(checkedMergedApiArn, association);
|
|
1353
1382
|
let capabilityGate = Pulumi.all([
|
|
1354
1383
|
pluginOutputs.pluginStructure,
|
|
1355
|
-
stackRef.getOutput("objectStores")
|
|
1384
|
+
stackRef.getOutput("objectStores"),
|
|
1385
|
+
geocoderPlaceIndex,
|
|
1386
|
+
messagingEmailSender
|
|
1356
1387
|
]).apply(param => {
|
|
1357
|
-
let
|
|
1388
|
+
let structure = param[0];
|
|
1389
|
+
let missing = CapabilityNeed$Reventless.unmet(Stdlib_Option.getOr(Stdlib_Option.flatMap(structure, s => s.requiredCapabilities), []).map(d => [
|
|
1390
|
+
d.capability,
|
|
1391
|
+
d.component
|
|
1392
|
+
]), (
|
|
1393
|
+
param[2] === "" ? [] : ["Geocoding"]
|
|
1394
|
+
).concat(param[3] === "" ? [] : ["Messaging"]));
|
|
1395
|
+
if (missing.length !== 0) {
|
|
1396
|
+
Stdlib_JsError.throwWithMessage(CapabilityNeed$Reventless.unmetMessage(missing) + Belt_SetString.toArray(Belt_SetString.fromArray(missing.map(u => {
|
|
1397
|
+
let match = u.need;
|
|
1398
|
+
if (match === "Geocoding") {
|
|
1399
|
+
return `\n Geocoding is \`Capability_Geocoding_AwsLocation.make\`, passed to the platform as \`~geocoderPlaceIndex\`.`;
|
|
1400
|
+
} else {
|
|
1401
|
+
return `\n Messaging is \`Capability_Messaging_Ses.make\`, passed to the platform as \`~messagingSender\`.`;
|
|
1402
|
+
}
|
|
1403
|
+
}))).join(""));
|
|
1404
|
+
}
|
|
1405
|
+
let required = Stdlib_Option.getOr(Stdlib_Option.flatMap(structure, s => s.requiredStores), []);
|
|
1358
1406
|
let provisioned = Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(param[1], Stdlib_JSON.Decode.object), prim => Object.keys(prim)), []);
|
|
1359
|
-
let missing = Util_StoreLayout$ReventlessAws.coverageFor(required, provisioned);
|
|
1360
|
-
if (typeof missing !== "object") {
|
|
1407
|
+
let missing$1 = Util_StoreLayout$ReventlessAws.coverageFor(required, provisioned);
|
|
1408
|
+
if (typeof missing$1 !== "object") {
|
|
1361
1409
|
return;
|
|
1362
|
-
} else if (missing.TAG === "NotAdopted") {
|
|
1363
|
-
return log.warn("Platform:deployPlugin", undefined, `declares ` + missing._0.join(", ") + ` but the platform stack provisions no object stores — add them to the platform's ~capabilities and redeploy the platform first, or uploads will fall back to the legacy service and write to the wrong bucket`);
|
|
1410
|
+
} else if (missing$1.TAG === "NotAdopted") {
|
|
1411
|
+
return log.warn("Platform:deployPlugin", undefined, `declares ` + missing$1._0.join(", ") + ` but the platform stack provisions no object stores — add them to the platform's ~capabilities and redeploy the platform first, or uploads will fall back to the legacy service and write to the wrong bucket`);
|
|
1364
1412
|
} else {
|
|
1365
|
-
return Stdlib_JsError.throwWithMessage(`Plugin requires object store(s) the platform does not provision: ` + missing.missing.join(", ") + `.\n` + (` The platform stack provisions: ` + missing.provisioned.join(", ") + `.\n`) + ` A store's key is {plugin}.{store}, where {plugin} is the name the plugin registers — check the capability's spelling and case against it.\n Add the missing entr(ies) to the platform's ~capabilities and redeploy the platform stack first.`);
|
|
1413
|
+
return Stdlib_JsError.throwWithMessage(`Plugin requires object store(s) the platform does not provision: ` + missing$1.missing.join(", ") + `.\n` + (` The platform stack provisions: ` + missing$1.provisioned.join(", ") + `.\n`) + ` A store's key is {plugin}.{store}, where {plugin} is the name the plugin registers — check the capability's spelling and case against it.\n Add the missing entr(ies) to the platform's ~capabilities and redeploy the platform stack first.`);
|
|
1366
1414
|
}
|
|
1367
1415
|
});
|
|
1368
1416
|
Pulumi$Pulumi.$$export("sourceApiAssociationId", Pulumi.all([
|
|
@@ -1770,6 +1818,8 @@ function Make($star) {
|
|
|
1770
1818
|
eventSchema: UiFragmentRegistry$ReventlessCore.eventSchema,
|
|
1771
1819
|
commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
|
|
1772
1820
|
commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization,
|
|
1821
|
+
commandTransition: UiFragmentRegistry$ReventlessCore.commandTransition,
|
|
1822
|
+
traits: UiFragmentRegistry$ReventlessCore.traits,
|
|
1773
1823
|
readConsistency: UiFragmentRegistry$ReventlessCore.readConsistency
|
|
1774
1824
|
};
|
|
1775
1825
|
let UiFragmentRegistrySlice = StateChangeSlice_Builder$ReventlessAws.Make(Spec)({
|
|
@@ -1957,7 +2007,9 @@ function Make($star) {
|
|
|
1957
2007
|
errorSchema: PluginSpec$ReventlessCore.errorSchema,
|
|
1958
2008
|
commandSchema: PluginSpec$ReventlessCore.commandSchema,
|
|
1959
2009
|
moduleUrl: PluginSpec$ReventlessCore.moduleUrl,
|
|
1960
|
-
commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization
|
|
2010
|
+
commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization,
|
|
2011
|
+
commandTransition: PluginSpec$ReventlessCore.commandTransition,
|
|
2012
|
+
traits: PluginSpec$ReventlessCore.traits
|
|
1961
2013
|
})({
|
|
1962
2014
|
initialState: PluginBehavior$ReventlessCore.initialState,
|
|
1963
2015
|
evolve: PluginBehavior$ReventlessCore.evolve,
|
|
@@ -2441,6 +2493,22 @@ function Make($star) {
|
|
|
2441
2493
|
let geocoderPlaceIndexFlat = index$1 !== undefined ? index$1.indexName : Pulumi.output("");
|
|
2442
2494
|
Pulumi$Pulumi.$$export("geocoderPlaceIndex", geocoderPlaceIndexFlat);
|
|
2443
2495
|
geocoderPlaceIndexRef.contents = geocoderPlaceIndexFlat;
|
|
2496
|
+
if (capabilities.includes("Geocoding") && Stdlib_Option.isNone(hostUiBundle.geocoderPlaceIndex)) {
|
|
2497
|
+
Stdlib_JsError.throwWithMessage(`A plugin of this deployment declares the Geocoding capability, but this platform provisions no place index.\n Add \`let placeIndex = ReventlessAws.Capability_Geocoding_AwsLocation.make(~name=…)\` and pass \`~geocoderPlaceIndex=placeIndex\` in \`~hostUiBundle\`.\n The declaration is generated from the plugins' capabilities.json — regenerate with \`pnpm run generate:platform\` if it is stale.`);
|
|
2498
|
+
}
|
|
2499
|
+
let match$5 = hostUiBundle.messagingSender;
|
|
2500
|
+
let messagingEmailSenderFlat;
|
|
2501
|
+
if (match$5 !== undefined) {
|
|
2502
|
+
let sender = match$5.emailSender;
|
|
2503
|
+
messagingEmailSenderFlat = sender !== undefined ? sender : Pulumi.output("");
|
|
2504
|
+
} else {
|
|
2505
|
+
messagingEmailSenderFlat = Pulumi.output("");
|
|
2506
|
+
}
|
|
2507
|
+
Pulumi$Pulumi.$$export("messagingEmailSender", messagingEmailSenderFlat);
|
|
2508
|
+
messagingEmailSenderRef.contents = messagingEmailSenderFlat;
|
|
2509
|
+
if (capabilities.includes("Messaging") && Stdlib_Option.isNone(hostUiBundle.messagingSender)) {
|
|
2510
|
+
Stdlib_JsError.throwWithMessage(`A plugin of this deployment declares the Messaging capability, but this platform provisions no sender.\n Add \`let sender = ReventlessAws.Capability_Messaging_Ses.make(~name=…, ~email=…)\` and pass \`~messagingSender=sender\` in \`~hostUiBundle\`.\n The declaration is generated from the plugins' capabilities.json — regenerate with \`pnpm run generate:platform\` if it is stale.`);
|
|
2511
|
+
}
|
|
2444
2512
|
let configJsonContent = Pulumi.all([
|
|
2445
2513
|
Pulumi.all([
|
|
2446
2514
|
resolvedDomainApiEndpoint,
|
|
@@ -2510,9 +2578,9 @@ function Make($star) {
|
|
|
2510
2578
|
contentType: "application/json"
|
|
2511
2579
|
});
|
|
2512
2580
|
}
|
|
2513
|
-
let match$
|
|
2514
|
-
if (componentDefinitions !== undefined && match$
|
|
2515
|
-
let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$
|
|
2581
|
+
let match$6 = hostUiBundle.bakedManifest;
|
|
2582
|
+
if (componentDefinitions !== undefined && match$6 !== undefined) {
|
|
2583
|
+
let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$6).map(param => param[0]);
|
|
2516
2584
|
let manifestKey = Stdlib_Option.getOr(manifestKeys[0], Platform_BakedManifest$ReventlessCore.defaultKey);
|
|
2517
2585
|
Pulumi.all([
|
|
2518
2586
|
componentDefinitions.roleId,
|
|
@@ -2591,6 +2659,9 @@ function Make($star) {
|
|
|
2591
2659
|
let geocoderPlaceIndex = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("geocoderPlaceIndex").apply(o => Stdlib_Option.getOr(o, "")) : geocoderPlaceIndexRef.contents;
|
|
2592
2660
|
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("PLACE_INDEX_NAME", geocoderPlaceIndex);
|
|
2593
2661
|
PluginRuntime_Builder$ReventlessAws.registerGeocoderPlaceIndex(geocoderPlaceIndex);
|
|
2662
|
+
let messagingEmailSender = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("messagingEmailSender").apply(o => Stdlib_Option.getOr(o, "")) : messagingEmailSenderRef.contents;
|
|
2663
|
+
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("MESSAGING_EMAIL_SENDER", messagingEmailSender);
|
|
2664
|
+
PluginRuntime_Builder$ReventlessAws.registerMessagingSender(messagingEmailSender);
|
|
2594
2665
|
let pluginComponent = plugin.make();
|
|
2595
2666
|
Plugin_Helpers$ReventlessCore.clearOffload();
|
|
2596
2667
|
currentDeployTarget.contents = "Domain";
|
|
@@ -2646,17 +2717,36 @@ function Make($star) {
|
|
|
2646
2717
|
let mergeGate = AppSync_MergedApi$ReventlessAws.mergeStatusGateWith(checkedMergedApiArn, association);
|
|
2647
2718
|
let capabilityGate = Pulumi.all([
|
|
2648
2719
|
pluginOutputs.pluginStructure,
|
|
2649
|
-
stackRef.getOutput("objectStores")
|
|
2720
|
+
stackRef.getOutput("objectStores"),
|
|
2721
|
+
geocoderPlaceIndex,
|
|
2722
|
+
messagingEmailSender
|
|
2650
2723
|
]).apply(param => {
|
|
2651
|
-
let
|
|
2724
|
+
let structure = param[0];
|
|
2725
|
+
let missing = CapabilityNeed$Reventless.unmet(Stdlib_Option.getOr(Stdlib_Option.flatMap(structure, s => s.requiredCapabilities), []).map(d => [
|
|
2726
|
+
d.capability,
|
|
2727
|
+
d.component
|
|
2728
|
+
]), (
|
|
2729
|
+
param[2] === "" ? [] : ["Geocoding"]
|
|
2730
|
+
).concat(param[3] === "" ? [] : ["Messaging"]));
|
|
2731
|
+
if (missing.length !== 0) {
|
|
2732
|
+
Stdlib_JsError.throwWithMessage(CapabilityNeed$Reventless.unmetMessage(missing) + Belt_SetString.toArray(Belt_SetString.fromArray(missing.map(u => {
|
|
2733
|
+
let match = u.need;
|
|
2734
|
+
if (match === "Geocoding") {
|
|
2735
|
+
return `\n Geocoding is \`Capability_Geocoding_AwsLocation.make\`, passed to the platform as \`~geocoderPlaceIndex\`.`;
|
|
2736
|
+
} else {
|
|
2737
|
+
return `\n Messaging is \`Capability_Messaging_Ses.make\`, passed to the platform as \`~messagingSender\`.`;
|
|
2738
|
+
}
|
|
2739
|
+
}))).join(""));
|
|
2740
|
+
}
|
|
2741
|
+
let required = Stdlib_Option.getOr(Stdlib_Option.flatMap(structure, s => s.requiredStores), []);
|
|
2652
2742
|
let provisioned = Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(param[1], Stdlib_JSON.Decode.object), prim => Object.keys(prim)), []);
|
|
2653
|
-
let missing = Util_StoreLayout$ReventlessAws.coverageFor(required, provisioned);
|
|
2654
|
-
if (typeof missing !== "object") {
|
|
2743
|
+
let missing$1 = Util_StoreLayout$ReventlessAws.coverageFor(required, provisioned);
|
|
2744
|
+
if (typeof missing$1 !== "object") {
|
|
2655
2745
|
return;
|
|
2656
|
-
} else if (missing.TAG === "NotAdopted") {
|
|
2657
|
-
return log.warn("Platform:deployPlugin", undefined, `declares ` + missing._0.join(", ") + ` but the platform stack provisions no object stores — add them to the platform's ~capabilities and redeploy the platform first, or uploads will fall back to the legacy service and write to the wrong bucket`);
|
|
2746
|
+
} else if (missing$1.TAG === "NotAdopted") {
|
|
2747
|
+
return log.warn("Platform:deployPlugin", undefined, `declares ` + missing$1._0.join(", ") + ` but the platform stack provisions no object stores — add them to the platform's ~capabilities and redeploy the platform first, or uploads will fall back to the legacy service and write to the wrong bucket`);
|
|
2658
2748
|
} else {
|
|
2659
|
-
return Stdlib_JsError.throwWithMessage(`Plugin requires object store(s) the platform does not provision: ` + missing.missing.join(", ") + `.\n` + (` The platform stack provisions: ` + missing.provisioned.join(", ") + `.\n`) + ` A store's key is {plugin}.{store}, where {plugin} is the name the plugin registers — check the capability's spelling and case against it.\n Add the missing entr(ies) to the platform's ~capabilities and redeploy the platform stack first.`);
|
|
2749
|
+
return Stdlib_JsError.throwWithMessage(`Plugin requires object store(s) the platform does not provision: ` + missing$1.missing.join(", ") + `.\n` + (` The platform stack provisions: ` + missing$1.provisioned.join(", ") + `.\n`) + ` A store's key is {plugin}.{store}, where {plugin} is the name the plugin registers — check the capability's spelling and case against it.\n Add the missing entr(ies) to the platform's ~capabilities and redeploy the platform stack first.`);
|
|
2660
2750
|
}
|
|
2661
2751
|
});
|
|
2662
2752
|
Pulumi$Pulumi.$$export("sourceApiAssociationId", Pulumi.all([
|
|
@@ -2764,6 +2854,7 @@ export {
|
|
|
2764
2854
|
objectStoreEndpointsRef,
|
|
2765
2855
|
getObjectStoreEndpoints,
|
|
2766
2856
|
geocoderPlaceIndexRef,
|
|
2857
|
+
messagingEmailSenderRef,
|
|
2767
2858
|
MakeWithConfig,
|
|
2768
2859
|
Make,
|
|
2769
2860
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Amazon SES as a messaging transport for unattended callers.
|
|
2
|
+
//
|
|
3
|
+
// Policy — who a message can be addressed to, what a send can answer, and which
|
|
4
|
+
// failures are worth retrying — lives in `Reventless.Messaging`, shared with
|
|
5
|
+
// every other transport. This module only knows how to ask SES and how to turn
|
|
6
|
+
// its answer into that vocabulary.
|
|
7
|
+
//
|
|
8
|
+
// Runtime-pure: no Pulumi import, so it can be bundled into a Lambda entry point
|
|
9
|
+
// without dragging deploy-time modules into the cold-start graph.
|
|
10
|
+
|
|
11
|
+
module Send = AwsSdk.SES.SendEmailCommand
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
The channels this transport can attempt, given what the deployment provisioned.
|
|
15
|
+
|
|
16
|
+
Derived from the sender address rather than declared beside it: SES sends from a
|
|
17
|
+
verified identity, so no address means no email channel, and a list that claimed
|
|
18
|
+
one anyway would be a promise the first send breaks. SMS and push are not wired
|
|
19
|
+
here — `Messaging.UnsupportedChannel` is what a caller gets, which is settled
|
|
20
|
+
rather than retried, so a preference for a channel this deployment does not run
|
|
21
|
+
costs one refusal and not a retry budget.
|
|
22
|
+
*/
|
|
23
|
+
let channels = (~sender: string): array<Reventless.Messaging.channel> =>
|
|
24
|
+
sender == "" ? [] : [Email]
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
Send one message.
|
|
28
|
+
|
|
29
|
+
`Refused` rather than a blank subject line for a message that carries none: SES
|
|
30
|
+
requires one, retrying will not produce one, and a delivered email with an empty
|
|
31
|
+
header is the outcome nobody wanted. The other two arms follow the port's retry
|
|
32
|
+
rule — an unset sender is the deployment's gap (`Unavailable`, retried and swept)
|
|
33
|
+
and anything SES itself refuses is settled.
|
|
34
|
+
*/
|
|
35
|
+
let send = async (
|
|
36
|
+
~sender: string,
|
|
37
|
+
~recipient: Reventless.Messaging.recipient,
|
|
38
|
+
~message: Reventless.Messaging.message,
|
|
39
|
+
): result<Reventless.Messaging.receipt, Reventless.Messaging.failure> =>
|
|
40
|
+
switch recipient {
|
|
41
|
+
| ToSms(_) => Error(UnsupportedChannel(Sms))
|
|
42
|
+
| ToPush(_) => Error(UnsupportedChannel(Push))
|
|
43
|
+
| ToEmail(address) =>
|
|
44
|
+
if sender == "" {
|
|
45
|
+
Error(Unavailable("no messaging sender configured (MESSAGING_EMAIL_SENDER unset)"))
|
|
46
|
+
} else {
|
|
47
|
+
switch message.subject {
|
|
48
|
+
| None | Some("") => Error(Refused("an email needs a subject, and this message carries none"))
|
|
49
|
+
| Some(subject) =>
|
|
50
|
+
switch await Send.send(
|
|
51
|
+
Send.make({
|
|
52
|
+
fromEmailAddress: sender,
|
|
53
|
+
destination: {toAddresses: [address->Reventless.Email.toString]},
|
|
54
|
+
content: {
|
|
55
|
+
simple: {
|
|
56
|
+
subject: {data: subject},
|
|
57
|
+
body: {text: {data: message.body}},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
) {
|
|
62
|
+
// SES answers 200 with no MessageId only if the shape changed under us;
|
|
63
|
+
// an accepted message without an id is still accepted, and inventing one
|
|
64
|
+
// would put a value in a receipt that no support conversation resolves.
|
|
65
|
+
| resp => Ok({ref: resp.messageId->Option.getOr("")})
|
|
66
|
+
| exception exn =>
|
|
67
|
+
let msg = exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown")
|
|
68
|
+
// Every SES error is treated as transient. The permanent ones it does
|
|
69
|
+
// raise — an unverified identity, an address on the suppression list —
|
|
70
|
+
// are deployment or recipient state that a sweep should keep surfacing,
|
|
71
|
+
// and reading them apart means matching on message text, which is a
|
|
72
|
+
// provider's prose and not a contract.
|
|
73
|
+
Error(Unavailable(msg))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The port, closed over the deployment's sender. What a Lambda entry point puts
|
|
80
|
+
on `Capabilities.messaging`. */
|
|
81
|
+
let provider = (~sender: string): Reventless.Messaging.provider => {
|
|
82
|
+
channels: channels(~sender),
|
|
83
|
+
send: (~recipient, ~message) => send(~sender, ~recipient, ~message),
|
|
84
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as SES$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/SES.res.mjs";
|
|
4
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
7
|
+
import * as ClientSesv2 from "@aws-sdk/client-sesv2";
|
|
8
|
+
|
|
9
|
+
function channels(sender) {
|
|
10
|
+
if (sender === "") {
|
|
11
|
+
return [];
|
|
12
|
+
} else {
|
|
13
|
+
return ["Email"];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function send(sender, recipient, message) {
|
|
18
|
+
switch (recipient.TAG) {
|
|
19
|
+
case "ToEmail" :
|
|
20
|
+
if (sender === "") {
|
|
21
|
+
return {
|
|
22
|
+
TAG: "Error",
|
|
23
|
+
_0: {
|
|
24
|
+
TAG: "Unavailable",
|
|
25
|
+
_0: "no messaging sender configured (MESSAGING_EMAIL_SENDER unset)"
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let subject = message.subject;
|
|
30
|
+
if (subject === undefined) {
|
|
31
|
+
return {
|
|
32
|
+
TAG: "Error",
|
|
33
|
+
_0: {
|
|
34
|
+
TAG: "Refused",
|
|
35
|
+
_0: "an email needs a subject, and this message carries none"
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (subject === "") {
|
|
40
|
+
return {
|
|
41
|
+
TAG: "Error",
|
|
42
|
+
_0: {
|
|
43
|
+
TAG: "Refused",
|
|
44
|
+
_0: "an email needs a subject, and this message carries none"
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
let resp;
|
|
49
|
+
try {
|
|
50
|
+
resp = await SES$AwsSdk.SendEmailCommand.send(new ClientSesv2.SendEmailCommand({
|
|
51
|
+
Content: {
|
|
52
|
+
Simple: {
|
|
53
|
+
Body: {
|
|
54
|
+
Text: {
|
|
55
|
+
Data: message.body
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
Subject: {
|
|
59
|
+
Data: subject
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
Destination: {
|
|
64
|
+
ToAddresses: [recipient._0]
|
|
65
|
+
},
|
|
66
|
+
FromEmailAddress: sender
|
|
67
|
+
}));
|
|
68
|
+
} catch (raw_exn) {
|
|
69
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
70
|
+
let msg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
|
|
71
|
+
return {
|
|
72
|
+
TAG: "Error",
|
|
73
|
+
_0: {
|
|
74
|
+
TAG: "Unavailable",
|
|
75
|
+
_0: msg
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
TAG: "Ok",
|
|
81
|
+
_0: {
|
|
82
|
+
ref: Stdlib_Option.getOr(resp.MessageId, "")
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
break;
|
|
86
|
+
case "ToSms" :
|
|
87
|
+
return {
|
|
88
|
+
TAG: "Error",
|
|
89
|
+
_0: {
|
|
90
|
+
TAG: "UnsupportedChannel",
|
|
91
|
+
_0: "Sms"
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
case "ToPush" :
|
|
95
|
+
return {
|
|
96
|
+
TAG: "Error",
|
|
97
|
+
_0: {
|
|
98
|
+
TAG: "UnsupportedChannel",
|
|
99
|
+
_0: "Push"
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function provider(sender) {
|
|
106
|
+
return {
|
|
107
|
+
channels: channels(sender),
|
|
108
|
+
send: (recipient, message) => send(sender, recipient, message)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let Send;
|
|
113
|
+
|
|
114
|
+
export {
|
|
115
|
+
Send,
|
|
116
|
+
channels,
|
|
117
|
+
send,
|
|
118
|
+
provider,
|
|
119
|
+
}
|
|
120
|
+
/* SES-AwsSdk Not a pure module */
|
|
@@ -226,7 +226,9 @@ public unauthenticated endpoint for an unattended path.
|
|
|
226
226
|
Resolved per call rather than captured once, so a Lambda whose configuration is
|
|
227
227
|
updated does not need a cold start to see it. An unset `PLACE_INDEX_NAME` reaches
|
|
228
228
|
`Geocoder_AwsLocation_Backend` as `""`, which answers `Unavailable` — a retryable
|
|
229
|
-
outcome, not a verdict on the address.
|
|
229
|
+
outcome, not a verdict on the address. `MESSAGING_EMAIL_SENDER` is read the same
|
|
230
|
+
way, and empty yields a provider with no channels rather than one that claims a
|
|
231
|
+
channel it cannot send on.
|
|
230
232
|
*/
|
|
231
233
|
let capabilities = (): Reventless.Capabilities.t => {
|
|
232
234
|
geocode: (~text) =>
|
|
@@ -234,6 +236,9 @@ let capabilities = (): Reventless.Capabilities.t => {
|
|
|
234
236
|
~indexName=NodeProcess.env->Dict.get("PLACE_INDEX_NAME")->Option.getOr(""),
|
|
235
237
|
~text,
|
|
236
238
|
),
|
|
239
|
+
messaging: Messaging_Ses_Backend.provider(
|
|
240
|
+
~sender=NodeProcess.env->Dict.get("MESSAGING_EMAIL_SENDER")->Option.getOr(""),
|
|
241
|
+
),
|
|
237
242
|
}
|
|
238
243
|
|
|
239
244
|
// ── Phase-1/phase-2 pipelines ───────────────────────────────────────────────
|
|
@@ -14,6 +14,7 @@ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/U
|
|
|
14
14
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
15
15
|
import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
|
|
16
16
|
import * as DynamoDb_Error$ReventlessAws from "../../errors/DynamoDb_Error.res.mjs";
|
|
17
|
+
import * as Messaging_Ses_Backend$ReventlessAws from "../Messaging/Messaging_Ses_Backend.res.mjs";
|
|
17
18
|
import * as Util_DynamoDb_Runtime$ReventlessAws from "../../util/Util_DynamoDb_Runtime.res.mjs";
|
|
18
19
|
import * as AutomationSlice_Callback$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs";
|
|
19
20
|
import * as StreamRoutedEntryPoint_Ops$ReventlessAws from "./StreamRoutedEntryPoint_Ops.res.mjs";
|
|
@@ -151,7 +152,8 @@ function makeLoadTodoItems(queryDbTableName, todoItems, rowSchema, comp) {
|
|
|
151
152
|
|
|
152
153
|
function capabilities() {
|
|
153
154
|
return {
|
|
154
|
-
geocode: text => Geocoder_AwsLocation_Backend$ReventlessAws.search(Stdlib_Option.getOr(process.env["PLACE_INDEX_NAME"], ""), text, undefined)
|
|
155
|
+
geocode: text => Geocoder_AwsLocation_Backend$ReventlessAws.search(Stdlib_Option.getOr(process.env["PLACE_INDEX_NAME"], ""), text, undefined),
|
|
156
|
+
messaging: Messaging_Ses_Backend$ReventlessAws.provider(Stdlib_Option.getOr(process.env["MESSAGING_EMAIL_SENDER"], ""))
|
|
155
157
|
};
|
|
156
158
|
}
|
|
157
159
|
|
|
@@ -185,9 +187,7 @@ function makeOutboundJsonEventsHandler(consumedEventSchema, callback, publishJso
|
|
|
185
187
|
await loadTodoItems();
|
|
186
188
|
callback.phase1(events);
|
|
187
189
|
await syncTodoItems();
|
|
188
|
-
await callback.phase2(publishJsons,
|
|
189
|
-
geocode: text => Geocoder_AwsLocation_Backend$ReventlessAws.search(Stdlib_Option.getOr(process.env["PLACE_INDEX_NAME"], ""), text, undefined)
|
|
190
|
-
});
|
|
190
|
+
await callback.phase2(publishJsons, capabilities());
|
|
191
191
|
return await syncTodoItems();
|
|
192
192
|
}));
|
|
193
193
|
}
|
|
@@ -223,9 +223,7 @@ function makeOutboundRegisteredHandler(entry, sliceName, consumedEventSchema, ca
|
|
|
223
223
|
},
|
|
224
224
|
sweep: async () => {
|
|
225
225
|
await loadTodoItems();
|
|
226
|
-
await callback.phase2(publishJsons,
|
|
227
|
-
geocode: text => Geocoder_AwsLocation_Backend$ReventlessAws.search(Stdlib_Option.getOr(process.env["PLACE_INDEX_NAME"], ""), text, undefined)
|
|
228
|
-
});
|
|
226
|
+
await callback.phase2(publishJsons, capabilities());
|
|
229
227
|
return await syncTodoItems();
|
|
230
228
|
},
|
|
231
229
|
comp: comp
|
|
@@ -296,6 +296,43 @@ let makeGeocoderGrant = (~runtime: ReventlessCore.Runtime.environment<runtimePar
|
|
|
296
296
|
)
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
+
// Least-privilege `ses:SendEmail` on the platform's verified sender identity,
|
|
300
|
+
// for the slices that send through the injected messaging capability. Built the
|
|
301
|
+
// same way as the geocoder grant above, and for the same three reasons: the
|
|
302
|
+
// provisioned bit is a plain bool, the address is an Output, and the RolePolicy
|
|
303
|
+
// is created at top level rather than inside an `apply`.
|
|
304
|
+
let makeMessagingGrant = (~runtime: ReventlessCore.Runtime.environment<runtimeParts>, ~opts) =>
|
|
305
|
+
if PluginRuntime_Builder.messagingProvisioned() {
|
|
306
|
+
let customOpts =
|
|
307
|
+
opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
308
|
+
let policyJson =
|
|
309
|
+
PluginRuntime_Builder.messagingSender()->Pulumi.Output.apply(sender => {
|
|
310
|
+
open PulumiAws.PolicyDocument
|
|
311
|
+
PulumiAws.PolicyDocument.make(
|
|
312
|
+
~id="AllAutomationSlicesSendEmail",
|
|
313
|
+
~statements=[
|
|
314
|
+
{
|
|
315
|
+
sid: "AllowSendEmail",
|
|
316
|
+
effect: Allow,
|
|
317
|
+
// SESv2's SendEmail authorizes against the v1 action name, and the
|
|
318
|
+
// resource is the identity the message is sent FROM — not the
|
|
319
|
+
// recipient, which is why one grant covers every destination.
|
|
320
|
+
actions: Action("ses:SendEmail"),
|
|
321
|
+
resources: Resource(`arn:aws:ses:*:*:identity/${sender}`),
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
)->PulumiAws.PolicyDocument.toJsonString
|
|
325
|
+
})
|
|
326
|
+
let _ = PulumiAws.IAM.RolePolicy.make(
|
|
327
|
+
~name="AllAutomationSlicesSendEmail",
|
|
328
|
+
~args={
|
|
329
|
+
policy: policyJson->Pulumi.Output.asInput,
|
|
330
|
+
role: runtime.parts.lambdaRole.id->Pulumi.Output.asInput,
|
|
331
|
+
},
|
|
332
|
+
~opts=customOpts,
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
299
336
|
let finished = ref(false)
|
|
300
337
|
|
|
301
338
|
// Legacy finalizer, kept only to satisfy `EventCollectorRuntime_Builder.T`.
|
|
@@ -619,6 +656,7 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
|
|
|
619
656
|
|
|
620
657
|
makeSweepSchedule(~runtime, ~opts)
|
|
621
658
|
makeGeocoderGrant(~runtime, ~opts)
|
|
659
|
+
makeMessagingGrant(~runtime, ~opts)
|
|
622
660
|
| None =>
|
|
623
661
|
log.warn(
|
|
624
662
|
~comp="AutomationSliceRuntime_Builder_Single",
|
|
@@ -150,6 +150,23 @@ function makeGeocoderGrant(runtime, opts) {
|
|
|
150
150
|
}, customOpts);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
function makeMessagingGrant(runtime, opts) {
|
|
154
|
+
if (!PluginRuntime_Builder$ReventlessAws.messagingProvisioned()) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
let customOpts = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
158
|
+
let policyJson = PluginRuntime_Builder$ReventlessAws.messagingSender().apply(sender => PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, "AllAutomationSlicesSendEmail", [{
|
|
159
|
+
Sid: "AllowSendEmail",
|
|
160
|
+
Effect: "Allow",
|
|
161
|
+
Action: "ses:SendEmail",
|
|
162
|
+
Resource: `arn:aws:ses:*:*:identity/` + sender
|
|
163
|
+
}])));
|
|
164
|
+
new (Aws.iam.RolePolicy)("AllAutomationSlicesSendEmail", {
|
|
165
|
+
policy: policyJson,
|
|
166
|
+
role: runtime.parts.lambdaRole.id
|
|
167
|
+
}, customOpts);
|
|
168
|
+
}
|
|
169
|
+
|
|
153
170
|
let finished = {
|
|
154
171
|
contents: false
|
|
155
172
|
};
|
|
@@ -327,6 +344,7 @@ function finishWithDcbEventLog(dcbEventLog) {
|
|
|
327
344
|
}], runtime, opts);
|
|
328
345
|
makeSweepSchedule(runtime, opts);
|
|
329
346
|
makeGeocoderGrant(runtime, opts);
|
|
347
|
+
makeMessagingGrant(runtime, opts);
|
|
330
348
|
} else {
|
|
331
349
|
log.warn("AutomationSliceRuntime_Builder_Single", undefined, "finishWithDcbEventLog: DCB EventLog has no parent");
|
|
332
350
|
}
|
|
@@ -357,6 +375,7 @@ export {
|
|
|
357
375
|
sweepIntervalMinutes,
|
|
358
376
|
makeSweepSchedule,
|
|
359
377
|
makeGeocoderGrant,
|
|
378
|
+
makeMessagingGrant,
|
|
360
379
|
finished,
|
|
361
380
|
finish,
|
|
362
381
|
finishWithDcbEventLog,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// A verified SES sender identity, provisioned with the framework's house
|
|
2
|
+
// conventions applied — the backing for the messaging capability's email channel.
|
|
3
|
+
//
|
|
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.
|
|
8
|
+
//
|
|
9
|
+
// **Verification is not instant and not automatic.** Creating the identity asks
|
|
10
|
+
// AWS to send a confirmation mail to the address; until somebody follows it, SES
|
|
11
|
+
// refuses every send. That is a deployment step this code cannot perform, and a
|
|
12
|
+
// deploy that "succeeded" is not evidence mail flows — which is why the runtime
|
|
13
|
+
// treats an SES refusal as retryable, so the first sends queue rather than being
|
|
14
|
+
// written off while the address is still pending.
|
|
15
|
+
//
|
|
16
|
+
// A deployment in the SES sandbox may also only send *to* verified addresses.
|
|
17
|
+
// Leaving the sandbox is an account-level request, not a resource.
|
|
18
|
+
|
|
19
|
+
open PulumiAws
|
|
20
|
+
|
|
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 = (
|
|
24
|
+
~name: string,
|
|
25
|
+
~email: string,
|
|
26
|
+
~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}
|
|
31
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Aws from "@pulumi/aws";
|
|
4
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
5
|
+
|
|
6
|
+
function make(name, email, opts) {
|
|
7
|
+
let identity = new (Aws.ses.EmailIdentity)(name, {
|
|
8
|
+
email: email
|
|
9
|
+
}, opts !== undefined ? Primitive_option.valFromOption(opts) : undefined);
|
|
10
|
+
return {
|
|
11
|
+
emailSender: identity.email
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
make,
|
|
17
|
+
}
|
|
18
|
+
/* @pulumi/aws Not a pure module */
|
|
@@ -77,6 +77,20 @@ let registerGeocoderPlaceIndex = (indexName: Pulumi.Output.t<string>) => {
|
|
|
77
77
|
let geocoderPlaceIndex = () => geocoderPlaceIndexRef.contents
|
|
78
78
|
let geocoderProvisioned = () => geocoderProvisionedRef.contents
|
|
79
79
|
|
|
80
|
+
/** The messaging sender, carried for the same reason and in the same shape: a
|
|
81
|
+
slice Lambda sending mail needs `ses:SendEmail` on the identity, and the
|
|
82
|
+
identity is what the address names. */
|
|
83
|
+
let messagingSenderRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make(""))
|
|
84
|
+
let messagingProvisionedRef = ref(false)
|
|
85
|
+
|
|
86
|
+
let registerMessagingSender = (sender: Pulumi.Output.t<string>) => {
|
|
87
|
+
messagingSenderRef := sender
|
|
88
|
+
messagingProvisionedRef := true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let messagingSender = () => messagingSenderRef.contents
|
|
92
|
+
let messagingProvisioned = () => messagingProvisionedRef.contents
|
|
93
|
+
|
|
80
94
|
type sliceModulePaths = {
|
|
81
95
|
specPath: string,
|
|
82
96
|
behaviorPath: string,
|
|
@@ -68,6 +68,27 @@ function geocoderProvisioned() {
|
|
|
68
68
|
return geocoderProvisionedRef.contents;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
let messagingSenderRef = {
|
|
72
|
+
contents: Pulumi.output("")
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
let messagingProvisionedRef = {
|
|
76
|
+
contents: false
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function registerMessagingSender(sender) {
|
|
80
|
+
messagingSenderRef.contents = sender;
|
|
81
|
+
messagingProvisionedRef.contents = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function messagingSender() {
|
|
85
|
+
return messagingSenderRef.contents;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function messagingProvisioned() {
|
|
89
|
+
return messagingProvisionedRef.contents;
|
|
90
|
+
}
|
|
91
|
+
|
|
71
92
|
let dcbConfigRef = {
|
|
72
93
|
contents: {
|
|
73
94
|
pluginName: "",
|
|
@@ -544,6 +565,11 @@ export {
|
|
|
544
565
|
registerGeocoderPlaceIndex,
|
|
545
566
|
geocoderPlaceIndex,
|
|
546
567
|
geocoderProvisioned,
|
|
568
|
+
messagingSenderRef,
|
|
569
|
+
messagingProvisionedRef,
|
|
570
|
+
registerMessagingSender,
|
|
571
|
+
messagingSender,
|
|
572
|
+
messagingProvisioned,
|
|
547
573
|
dcbConfigRef,
|
|
548
574
|
registeredSliceModulePaths,
|
|
549
575
|
registerStateChangeSliceSpec,
|
|
@@ -50,6 +50,8 @@ let emptyStructure: Reventless.Plugin.pluginStructure = {
|
|
|
50
50
|
extensionPoints: None,
|
|
51
51
|
requiredStores: None,
|
|
52
52
|
requiredStoreDeclarations: None,
|
|
53
|
+
requiredCapabilities: None,
|
|
54
|
+
traitDeclarations: None,
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
let definition = (~structure): Reventless.Plugin.pluginDefinition => {
|
|
@@ -63,7 +63,9 @@ let emptyStructure = {
|
|
|
63
63
|
extensions: emptyStructure_extensions,
|
|
64
64
|
extensionPoints: undefined,
|
|
65
65
|
requiredStores: undefined,
|
|
66
|
-
requiredStoreDeclarations: undefined
|
|
66
|
+
requiredStoreDeclarations: undefined,
|
|
67
|
+
requiredCapabilities: undefined,
|
|
68
|
+
traitDeclarations: undefined
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
function definition(structure) {
|
|
@@ -134,7 +136,9 @@ globalThis.describe("the bake converges when a plugin's structure changes", () =
|
|
|
134
136
|
extensions: emptyStructure_extensions,
|
|
135
137
|
extensionPoints: undefined,
|
|
136
138
|
requiredStores: ["Ordering.receipts"],
|
|
137
|
-
requiredStoreDeclarations: undefined
|
|
139
|
+
requiredStoreDeclarations: undefined,
|
|
140
|
+
requiredCapabilities: undefined,
|
|
141
|
+
traitDeclarations: undefined
|
|
138
142
|
});
|
|
139
143
|
let keyB = match$1[0];
|
|
140
144
|
let defA = definition(match[1]);
|
|
@@ -19,3 +19,5 @@ type error = AlreadyExists
|
|
|
19
19
|
let name = "AggTestAggregate"
|
|
20
20
|
let moduleUrl = "agg-test://AggTestAggregate"
|
|
21
21
|
let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAnonymous
|
|
22
|
+
type lifecycleState = unit
|
|
23
|
+
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
@@ -18,6 +18,10 @@ function commandAuthorization(param) {
|
|
|
18
18
|
return "AllowAnonymous";
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function commandTransition(param) {
|
|
22
|
+
return "Unrestricted";
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
let name = "AggTestAggregate";
|
|
22
26
|
|
|
23
27
|
let moduleUrl = "agg-test://AggTestAggregate";
|
|
@@ -29,5 +33,6 @@ export {
|
|
|
29
33
|
name,
|
|
30
34
|
moduleUrl,
|
|
31
35
|
commandAuthorization,
|
|
36
|
+
commandTransition,
|
|
32
37
|
}
|
|
33
38
|
/* commandSchema Not a pure module */
|
|
@@ -56,6 +56,8 @@ type event =
|
|
|
56
56
|
let name = "EpCompositeSlice"
|
|
57
57
|
let moduleUrl = "ep-test://EpCompositeSlice"
|
|
58
58
|
let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAnonymous
|
|
59
|
+
type lifecycleState = unit
|
|
60
|
+
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
59
61
|
let readConsistency = Reventless.ReadConsistency.EscalateOnRetry
|
|
60
62
|
|
|
61
63
|
// `module Id = Reventless.Id.String` — patched in by `patchSpecId` at runtime.
|
|
@@ -51,6 +51,10 @@ function commandAuthorization(param) {
|
|
|
51
51
|
return "AllowAnonymous";
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function commandTransition(param) {
|
|
55
|
+
return "Unrestricted";
|
|
56
|
+
}
|
|
57
|
+
|
|
54
58
|
let name = "EpCompositeSlice";
|
|
55
59
|
|
|
56
60
|
let moduleUrl = "ep-test://EpCompositeSlice";
|
|
@@ -65,6 +69,7 @@ export {
|
|
|
65
69
|
name,
|
|
66
70
|
moduleUrl,
|
|
67
71
|
commandAuthorization,
|
|
72
|
+
commandTransition,
|
|
68
73
|
readConsistency,
|
|
69
74
|
}
|
|
70
75
|
/* consumedEventSchema Not a pure module */
|
|
@@ -22,6 +22,8 @@ type event = WidgetAdded({widgetId: @s.matches(Reventless.DcbTag.partition) stri
|
|
|
22
22
|
let name = "EpTestSlice"
|
|
23
23
|
let moduleUrl = "ep-test://EpTestSlice"
|
|
24
24
|
let commandAuthorization = (_: command): Reventless.Authorization.permission => AllowAnonymous
|
|
25
|
+
type lifecycleState = unit
|
|
26
|
+
let commandTransition = (_: command): Reventless.Transition.t<lifecycleState> => Unrestricted
|
|
25
27
|
let readConsistency = Reventless.ReadConsistency.EscalateOnRetry
|
|
26
28
|
|
|
27
29
|
// `module Id = Reventless.Id.String` — patched in by `patchSpecId` at runtime,
|
|
@@ -24,6 +24,10 @@ function commandAuthorization(param) {
|
|
|
24
24
|
return "AllowAnonymous";
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function commandTransition(param) {
|
|
28
|
+
return "Unrestricted";
|
|
29
|
+
}
|
|
30
|
+
|
|
27
31
|
let name = "EpTestSlice";
|
|
28
32
|
|
|
29
33
|
let moduleUrl = "ep-test://EpTestSlice";
|
|
@@ -38,6 +42,7 @@ export {
|
|
|
38
42
|
name,
|
|
39
43
|
moduleUrl,
|
|
40
44
|
commandAuthorization,
|
|
45
|
+
commandTransition,
|
|
41
46
|
readConsistency,
|
|
42
47
|
}
|
|
43
48
|
/* consumedEventSchema Not a pure module */
|