@reventlessdev/reventless-aws 3.0.0-alpha.337 → 3.0.0-alpha.339

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 CHANGED
@@ -3,6 +3,29 @@
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.339 (2026-09-07)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-aws
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.338 (2026-09-07)
15
+
16
+ ### Bug Fixes
17
+
18
+ * **aws:** give the dead-letter handler a managed log group ([8631bd4](https://github.com/ReventlessDev/reventless-core/commit/8631bd457d168a5996c42e48db3c66d353bdda34))
19
+ * **aws:** stop a dead letter from being redelivered every three minutes ([1ae3a06](https://github.com/ReventlessDev/reventless-core/commit/1ae3a0686003c05079214a4eeeb60a38fcb775af))
20
+ * **aws:** the alarm backend stops wrapping Outputs in options ([476d974](https://github.com/ReventlessDev/reventless-core/commit/476d974223748024ae637b6cd5a6ebee95c21354))
21
+ * **logging:** log a message's identity, not its serialisation ([cd7467a](https://github.com/ReventlessDev/reventless-core/commit/cd7467abdc59609279f16f358cdd8884cc49f063))
22
+ * **ui:** a declared slot module is named, not only written ([de000ba](https://github.com/ReventlessDev/reventless-core/commit/de000baf428249f64cfc719ed52df3760198b82a))
23
+ ### Features
24
+
25
+ * **aws:** alarm every execution unit a stack provisions ([46614af](https://github.com/ReventlessDev/reventless-core/commit/46614af92efb9596a09d3e7a15699712707b7ca6))
26
+ * **ui:** a deployment can name the module that draws its own surfaces ([b43828c](https://github.com/ReventlessDev/reventless-core/commit/b43828ca50cddd31f7aefae016669fcb7c059838))
27
+
28
+
6
29
  # 3.0.0-alpha.337 (2026-09-04)
7
30
 
8
31
  * feat(spec)!: one optional encoding on the wire, with no annotation ([320f91d](https://github.com/ReventlessDev/reventless-core/commit/320f91daa8bd90812a6e82069e7a1cb473041930))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.337",
3
+ "version": "3.0.0-alpha.339",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -15,18 +15,18 @@
15
15
  "@aws-sdk/s3-request-presigner": "3.970.0",
16
16
  "sury": "11.0.0-rc.2",
17
17
  "uuid": "^13.0.0",
18
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
19
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
18
20
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.15",
19
21
  "@reventlessdev/rescript-node": "2.0.0-alpha.9",
20
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
21
- "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
22
22
  "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.8",
23
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
24
23
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
25
- "@reventlessdev/reventless-core": "3.0.0-alpha.256",
26
- "@reventlessdev/reventless-infra": "3.0.0-alpha.157",
24
+ "@reventlessdev/reventless-core": "3.0.0-alpha.258",
25
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
26
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.122",
27
27
  "@reventlessdev/reventless-interop": "3.0.0-alpha.35",
28
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.120",
29
- "@reventlessdev/reventless-spec": "3.0.0-alpha.129"
28
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.159",
29
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.131"
30
30
  },
31
31
  "devDependencies": {
32
32
  "rescript": "12.3.0",
package/src/Platform.res CHANGED
@@ -1172,6 +1172,18 @@ module MakeWithConfig = (
1172
1172
  // written; the shell treats the resulting 404 as "no hints" and boots
1173
1173
  // unchanged. Single-mode shape: one origin-relative file per deployment.
1174
1174
  uiHintsFile?: string,
1175
+ // Optional ES module registering AutoUI slot renderers — the part of a
1176
+ // surface that has to be drawn rather than said (a tile with the name over
1177
+ // the picture, a card face in a different type scale). When set, the deploy
1178
+ // reads it and writes it verbatim as a `ui-slots.js` BucketObject beside
1179
+ // `config.json`. Unset ⇒ no file written; the shell treats the resulting 404
1180
+ // as "no slots" and every mode draws its own regions.
1181
+ //
1182
+ // A module in the bundle's own origin rather than a fragment served from the
1183
+ // admin API, because the registry has to be populated for *every* caller —
1184
+ // including one served from a baked manifest, who issues no admin query and
1185
+ // for whom federation overrides therefore never apply.
1186
+ uiSlotsFile?: string,
1175
1187
  // Optional baked component manifest. Declared here so a deployment can state
1176
1188
  // it once; the emission that writes the file is not wired on this platform
1177
1189
  // yet, and a declaration without it writes nothing.
@@ -2090,11 +2102,16 @@ module MakeWithConfig = (
2090
2102
  ~stableName=true,
2091
2103
  // The explicit BucketObjects below write the production config.json
2092
2104
  // (resolved API endpoints + Cognito IDs) and, when configured, a
2093
- // ui-hints.json. The host-shell bundle's public/config.json and
2094
- // public/ui-hints.json are dev-mode fallbacks — exclude them from the
2095
- // bundle upload so the explicit BucketObjects don't race the bundle on
2096
- // the same S3 keys.
2097
- ~excludeFiles=["config.json", "ui-hints.json"],
2105
+ // ui-hints.json and a ui-slots.js. The host-shell bundle's
2106
+ // public/config.json and public/ui-hints.json are dev-mode fallbacks —
2107
+ // exclude them from the bundle upload so the explicit BucketObjects
2108
+ // don't race the bundle on the same S3 keys.
2109
+ //
2110
+ // `ui-slots.js` is excluded on the same rule though the shell ships no
2111
+ // such file today, and that is the point: were one ever added, it would
2112
+ // be a fallback for *appearance*, and a deployment inheriting a
2113
+ // stranger's appearance is exactly the failure that is hard to notice.
2114
+ ~excludeFiles=["config.json", "ui-hints.json", ReventlessCore.Platform_UiSlots.fileName],
2098
2115
  ~customDomain?,
2099
2116
  // Served buckets front the host-shell distribution with `{prefix}/*`
2100
2117
  // read paths (private, OAC-read) — same origin as the SPA, so served
@@ -2297,6 +2314,7 @@ module MakeWithConfig = (
2297
2314
  ~computed=withEvents,
2298
2315
  ~viewModes=?cfg.viewModes,
2299
2316
  ~bakedManifest=?cfg.bakedManifest,
2317
+ ~uiSlotsFile=?cfg.uiSlotsFile,
2300
2318
  ~shellConfig=?cfg.shellConfig,
2301
2319
  )
2302
2320
  ->JSON.Encode.object
@@ -2336,6 +2354,42 @@ module MakeWithConfig = (
2336
2354
  )
2337
2355
  }
2338
2356
 
2357
+ // Optional AutoUI slot renderers. Read verbatim and written beside the
2358
+ // hints file above — same seam, one file over, and the same rule that an
2359
+ // undeclared deployment writes nothing.
2360
+ //
2361
+ // Unlike the hints there is no content check at deploy time, because there
2362
+ // is no cheap one (see `readFileVerbatim`): a module is known to be good
2363
+ // once a browser has evaluated it. What the deploy owes instead is a
2364
+ // correct content type, which is the difference between the shell
2365
+ // importing this file and the shell refusing it — a module served as
2366
+ // `application/octet-stream` is blocked before it is read.
2367
+ //
2368
+ // Same origin as the bundle, deliberately: it rides this distribution, so
2369
+ // no CSP relaxation is involved and none is configured (this deploy sets
2370
+ // no response-headers policy at all). A slots file that loads locally and
2371
+ // silently fails on AWS is the worst shape this can take, and cross-origin
2372
+ // is how it would take it.
2373
+ switch cfg.uiSlotsFile {
2374
+ | None => ()
2375
+ | Some(slotsPath) =>
2376
+ let slotsContent = Util_StaticBundle.readFileVerbatim(
2377
+ ~path=slotsPath,
2378
+ ~label="host-ui ui-slots",
2379
+ )
2380
+ let _ = PulumiAws.S3.BucketObject.make(
2381
+ ~name="host-ui-ui-slots-js",
2382
+ ~args={
2383
+ bucket: bucketName->Pulumi.Output.asInput,
2384
+ // The same string the `uiSlotsUrl` above points at — see
2385
+ // `Platform_UiSlots` for why it is stated once.
2386
+ key: Pulumi.Input.make(ReventlessCore.Platform_UiSlots.fileName),
2387
+ content: Pulumi.Input.make(slotsContent),
2388
+ contentType: Pulumi.Input.make("application/javascript; charset=utf-8"),
2389
+ },
2390
+ )
2391
+ }
2392
+
2339
2393
  // The bake's write, granted here because here is where both facts are
2340
2394
  // known: which bucket serves the shell, and that this deployment declared a
2341
2395
  // manifest at all. Scoped to the one key the deploy told the shell to fetch
@@ -51,6 +51,7 @@ import * as AppSync_EventsApi$ReventlessAws from "./adapter/Api/AppSync_EventsAp
51
51
  import * as AppSync_MergedApi$ReventlessAws from "./components/Api/AppSync_MergedApi.res.mjs";
52
52
  import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
53
53
  import * as NoEventMappings$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/NoEventMappings.res.mjs";
54
+ import * as Platform_UiSlots$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_UiSlots.res.mjs";
54
55
  import * as Upload_Presign_S3$ReventlessAws from "./adapter/Upload/Upload_Presign_S3.res.mjs";
55
56
  import * as Util_HostUiDomain$ReventlessAws from "./util/Util_HostUiDomain.res.mjs";
56
57
  import * as Util_StaticBundle$ReventlessAws from "./util/Util_StaticBundle.res.mjs";
@@ -1145,7 +1146,8 @@ function MakeWithConfig(Config) {
1145
1146
  ).concat(declaredServedBuckets);
1146
1147
  let match$4 = Plugin_Stack$ReventlessAws.makeUiBundleDistribution("host-ui", Stdlib_Option.getOr(hostUiBundle.bundleVersion, version), Stdlib_Option.getOr(hostUiBundle.assetsDir, Util_Bundle$ReventlessAws.resolvePackageRoot(true, "@reventlessdev/reventless-host-shell") + "/dist"), true, undefined, true, [
1147
1148
  "config.json",
1148
- "ui-hints.json"
1149
+ "ui-hints.json",
1150
+ Platform_UiSlots$ReventlessCore.fileName
1149
1151
  ], customDomain, servedBuckets);
1150
1152
  let bucketName = match$4.bucketName;
1151
1153
  let regionStr = Stdlib_Option.getOr(new Pulumi.Config("aws").get("region"), "unknown");
@@ -1247,7 +1249,7 @@ function MakeWithConfig(Config) {
1247
1249
  AppSync_EventsApi$ReventlessAws.clientNamespaceName
1248
1250
  ]
1249
1251
  ]) : computed;
1250
- return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.shellConfig));
1252
+ return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.uiSlotsFile, hostUiBundle.shellConfig));
1251
1253
  });
1252
1254
  new (Aws.s3.BucketObject)("host-ui-config-json", {
1253
1255
  bucket: bucketName,
@@ -1265,6 +1267,16 @@ function MakeWithConfig(Config) {
1265
1267
  contentType: "application/json"
1266
1268
  });
1267
1269
  }
1270
+ let slotsPath = hostUiBundle.uiSlotsFile;
1271
+ if (slotsPath !== undefined) {
1272
+ let slotsContent = Util_StaticBundle$ReventlessAws.readFileVerbatim(slotsPath, "host-ui ui-slots");
1273
+ new (Aws.s3.BucketObject)("host-ui-ui-slots-js", {
1274
+ bucket: bucketName,
1275
+ key: Platform_UiSlots$ReventlessCore.fileName,
1276
+ content: slotsContent,
1277
+ contentType: "application/javascript; charset=utf-8"
1278
+ });
1279
+ }
1268
1280
  let match$9 = hostUiBundle.bakedManifest;
1269
1281
  if (componentDefinitions !== undefined && match$9 !== undefined) {
1270
1282
  let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$9).map(param => param[0]);
@@ -2506,7 +2518,8 @@ function Make($star) {
2506
2518
  ).concat(declaredServedBuckets);
2507
2519
  let match$4 = Plugin_Stack$ReventlessAws.makeUiBundleDistribution("host-ui", Stdlib_Option.getOr(hostUiBundle.bundleVersion, version), Stdlib_Option.getOr(hostUiBundle.assetsDir, Util_Bundle$ReventlessAws.resolvePackageRoot(true, "@reventlessdev/reventless-host-shell") + "/dist"), true, undefined, true, [
2508
2520
  "config.json",
2509
- "ui-hints.json"
2521
+ "ui-hints.json",
2522
+ Platform_UiSlots$ReventlessCore.fileName
2510
2523
  ], customDomain, servedBuckets);
2511
2524
  let bucketName = match$4.bucketName;
2512
2525
  let regionStr = Stdlib_Option.getOr(new Pulumi.Config("aws").get("region"), "unknown");
@@ -2608,7 +2621,7 @@ function Make($star) {
2608
2621
  AppSync_EventsApi$ReventlessAws.clientNamespaceName
2609
2622
  ]
2610
2623
  ]) : computed;
2611
- return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.shellConfig));
2624
+ return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withEvents, hostUiBundle.viewModes, hostUiBundle.bakedManifest, hostUiBundle.uiSlotsFile, hostUiBundle.shellConfig));
2612
2625
  });
2613
2626
  new (Aws.s3.BucketObject)("host-ui-config-json", {
2614
2627
  bucket: bucketName,
@@ -2626,6 +2639,16 @@ function Make($star) {
2626
2639
  contentType: "application/json"
2627
2640
  });
2628
2641
  }
2642
+ let slotsPath = hostUiBundle.uiSlotsFile;
2643
+ if (slotsPath !== undefined) {
2644
+ let slotsContent = Util_StaticBundle$ReventlessAws.readFileVerbatim(slotsPath, "host-ui ui-slots");
2645
+ new (Aws.s3.BucketObject)("host-ui-ui-slots-js", {
2646
+ bucket: bucketName,
2647
+ key: Platform_UiSlots$ReventlessCore.fileName,
2648
+ content: slotsContent,
2649
+ contentType: "application/javascript; charset=utf-8"
2650
+ });
2651
+ }
2629
2652
  let match$9 = hostUiBundle.bakedManifest;
2630
2653
  if (componentDefinitions !== undefined && match$9 !== undefined) {
2631
2654
  let manifestKeys = Platform_BakedManifest$ReventlessCore.files(match$9).map(param => param[0]);
@@ -0,0 +1,197 @@
1
+ // A CloudWatch alarm on every execution unit the framework provisions.
2
+ //
3
+ // Monitoring is deliberately not a framework concern — core announces a unit
4
+ // through the `Monitoring` seam and says nothing about what to do with it (see
5
+ // docs/plans/done/monitoring-hook-seam.md). This is one backend for that seam,
6
+ // living here rather than in core because it is provider-native to the bone:
7
+ // CloudWatch metrics, an SNS topic, an AWS notion of what "failing" means.
8
+ //
9
+ // It exists because the stacks this repo deploys itself had nothing attached, and
10
+ // those are the stacks where framework defects surface first — a wedged aggregate
11
+ // went unnoticed for twelve hours and was found by reading a bill. See
12
+ // docs/plans/no-monitoring-backend-on-deployed-stacks.md.
13
+ //
14
+ // A deploy program opts in with one line, before it builds its platform:
15
+ //
16
+ // ReventlessAws.Monitoring_CloudWatch.use()
17
+ //
18
+ // and the deployment decides whether that line does anything:
19
+ //
20
+ // alarmEmail ops@example.com creates a topic + email subscription
21
+ // alarmTopicArn arn:aws:sns:... publishes to a topic that exists
22
+ // alarmSilenceWindowSeconds 3600 how long a scheduler may be quiet
23
+ //
24
+ // With neither address configured the backend provisions nothing at all, so a
25
+ // stack that has not opted in deploys byte-identical to one built without this
26
+ // module. That is what makes `use()` safe to commit unconditionally.
27
+ //
28
+ // Each key is read through `Util_LocalConfig` first — `REVENTLESS_ALARM_EMAIL` in
29
+ // CI, a gitignored `Pulumi.local.yaml` on a workstation — before the stack's own
30
+ // `platform:` config. Where alerts go is per-deployment and often personal, and a
31
+ // tracked `Pulumi.<stack>.yaml` in a public repository is the wrong place for
32
+ // somebody's inbox.
33
+ open PulumiAws
34
+
35
+ module M = ReventlessCore.Monitoring
36
+
37
+ // Local override first (env var, then the gitignored sidecar), then stack config.
38
+ let configured = (key: string): option<string> =>
39
+ switch Util_LocalConfig.get(key) {
40
+ | Some(_) as local => local
41
+ | None => Pulumi.Config.make(Some("platform"))->Pulumi.Config.get(key)
42
+ }
43
+
44
+ let configuredEmail = () => configured("alarmEmail")
45
+ let configuredTopicArn = () => configured("alarmTopicArn")
46
+
47
+ let silenceWindowSeconds = () =>
48
+ configured("alarmSilenceWindowSeconds")
49
+ ->Option.flatMap(Int.fromString(_))
50
+ ->Option.getOr(Util_AlarmSpec.defaultSilenceWindowSeconds)
51
+
52
+ let topicName = "ReventlessAlarms"
53
+
54
+ // The memo's three states, spelled as a variant rather than an option: an
55
+ // `option<Pulumi.Output.t<_>>` is banned repo-wide because an Output is a Proxy
56
+ // that answers ReScript's nested-option probe, so one generic combinator on it
57
+ // silently yields None. "Not asked yet" and "asked, nothing configured" are
58
+ // distinct here anyway, which an option cannot say.
59
+ type topic =
60
+ | Unresolved
61
+ | NoTopic
62
+ | Topic(Pulumi.Output.t<string>)
63
+
64
+ // Resolved once, on the first alarm — so a stack that provisions no units, or one
65
+ // that has not configured an address, creates no topic either.
66
+ let resolvedTopic: ref<topic> = ref(Unresolved)
67
+
68
+ // An ARN the stack already owns wins over creating one: an estate with somewhere
69
+ // for alerts to go (a chat relay, a pager, one topic shared by several stacks)
70
+ // should point at it rather than accumulate a topic per stack that nobody reads.
71
+ let ensureTopicArn = (): topic =>
72
+ switch resolvedTopic.contents {
73
+ | Topic(_) as resolved => resolved
74
+ | NoTopic => NoTopic
75
+ | Unresolved =>
76
+ let resolved = switch (configuredTopicArn(), configuredEmail()) {
77
+ | (Some(arn), _) => Topic(arn->Pulumi.Output.make)
78
+ | (None, Some(email)) =>
79
+ let topic = SNS.Topic.make(
80
+ ~name=topicName,
81
+ ~args={
82
+ SNS.Topic.tags: AWS.Tags.make(
83
+ ~name=topicName,
84
+ ~kind=ReventlessCore.ComponentType.Plugin,
85
+ ~role=Other("Alarm"),
86
+ ~scope=Plugin,
87
+ ),
88
+ },
89
+ )
90
+ let _subscription = SNS.TopicSubscription.make(
91
+ ~name=topicName ++ "Email",
92
+ ~args={
93
+ endpoint: email->Pulumi.Input.make,
94
+ topic: topic.arn->Pulumi.Output.asInput,
95
+ protocol: Email,
96
+ },
97
+ ~opts=None,
98
+ )
99
+ Topic(topic.arn)
100
+ | (None, None) => NoTopic
101
+ }
102
+ resolvedTopic := resolved
103
+ resolved
104
+ }
105
+
106
+ let alarmFor = (
107
+ ~spec: Util_AlarmSpec.t,
108
+ ~kind: M.unitKind,
109
+ ~name: string,
110
+ ~component: ReventlessInfra.Adapter.resource,
111
+ ~plugin: option<string>,
112
+ ~platform: option<string>,
113
+ ~logLocator: Pulumi.Output.t<string>,
114
+ ~topicArn: Pulumi.Output.t<string>,
115
+ ) => {
116
+ let resourceName = Util_AlarmSpec.resourceName(~kind, ~name, ~plugin, ~suffix=spec.suffix)
117
+ let actions = [topicArn->Pulumi.Output.asInput]->Pulumi.Input.make
118
+
119
+ // The description is the only field a state-change message carries, so it is
120
+ // also where the log group goes — an alert names the unit and the metric and
121
+ // never where to read what happened. `~logLocator` is an Output, so the whole
122
+ // description resolves late; `""` is a unit with no logs of its own.
123
+ let describe = logs =>
124
+ Util_AlarmSpec.description(~kind, ~name, ~plugin, ~platform, ~spec, ~logs)
125
+ let alarmDescription =
126
+ logLocator
127
+ ->Pulumi.Output.map(g => describe(g == "" ? None : Some(g)))
128
+ ->Pulumi.Output.asInput
129
+
130
+ Cloudwatch.MetricAlarm.make(
131
+ ~name=resourceName,
132
+ ~args={
133
+ comparisonOperator: spec.comparisonOperator->Pulumi.Input.make,
134
+ evaluationPeriods: spec.evaluationPeriods->Pulumi.Input.make,
135
+ metricName: spec.metricName->Pulumi.Input.make,
136
+ namespace: spec.namespace->Pulumi.Input.make,
137
+ period: spec.period->Pulumi.Input.make,
138
+ statistic: spec.statistic->Pulumi.Input.make,
139
+ threshold: spec.threshold->Pulumi.Input.make,
140
+ // Every unit this seam announces on AWS is a Lambda function, which is what
141
+ // makes one dimension enough. A future non-Lambda execution unit needs the
142
+ // namespace and dimension to come from the announcement rather than from here.
143
+ dimensions: component.name
144
+ ->Pulumi.Output.apply(fn => Dict.fromArray([("FunctionName", fn)]))
145
+ ->Pulumi.Output.asInput,
146
+ treatMissingData: spec.treatMissingData->Pulumi.Input.make,
147
+ alarmActions: actions,
148
+ // Without these the channel reports every incident and no recovery, and a
149
+ // reader cannot tell an ongoing outage from one that cleared minutes later.
150
+ okActions: actions,
151
+ alarmDescription,
152
+ tags: AWS.Tags.make(
153
+ ~name=resourceName,
154
+ ~kind=ReventlessCore.ComponentType.Plugin,
155
+ ~role=Other("Alarm"),
156
+ ~component=name,
157
+ ~plugin?,
158
+ ~platform?,
159
+ ),
160
+ },
161
+ )
162
+ }
163
+
164
+ module Backend: M.Backend = {
165
+ let onProvisioned = (~kind, ~name, ~component, ~plugin, ~platform, ~logLocator) => {
166
+ // The seam hands the log address as an option; collapse it here, at the one
167
+ // boundary that has to, so no `option<Pulumi.Output.t<_>>` travels further.
168
+ let logLocator = switch logLocator {
169
+ | Some(locator) => locator
170
+ | None => ""->Pulumi.Output.make
171
+ }
172
+ switch ensureTopicArn() {
173
+ | Unresolved | NoTopic => ()
174
+ | Topic(topicArn) =>
175
+ Util_AlarmSpec.forKind(~kind, ~silenceWindowSeconds=silenceWindowSeconds())->Array.forEach(
176
+ spec => {
177
+ let _alarm = alarmFor(
178
+ ~spec,
179
+ ~kind,
180
+ ~name,
181
+ ~component,
182
+ ~plugin,
183
+ ~platform,
184
+ ~logLocator,
185
+ ~topicArn,
186
+ )
187
+ },
188
+ )
189
+ }
190
+ }
191
+ }
192
+
193
+ /** Register this backend. Call it before building the platform — announcements
194
+ made earlier still arrive, because the seam holds them until someone
195
+ registers, but resources this creates must belong to the deploy program's own
196
+ run. Safe on a stack that configures no address: it provisions nothing. */
197
+ let use = () => M.use(module(Backend: M.Backend))
@@ -0,0 +1,144 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
4
+ import * as Aws from "@pulumi/aws";
5
+ import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Pulumi from "@pulumi/pulumi";
8
+ import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
9
+ import * as Monitoring$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Monitoring/Monitoring.res.mjs";
10
+ import * as Util_AlarmSpec$ReventlessAws from "../../util/Util_AlarmSpec.res.mjs";
11
+ import * as Util_LocalConfig$ReventlessAws from "../../util/Util_LocalConfig.res.mjs";
12
+
13
+ function configured(key) {
14
+ let local = Util_LocalConfig$ReventlessAws.get(key);
15
+ if (local !== undefined) {
16
+ return local;
17
+ } else {
18
+ return new Pulumi.Config("platform").get(key);
19
+ }
20
+ }
21
+
22
+ function configuredEmail() {
23
+ return configured("alarmEmail");
24
+ }
25
+
26
+ function configuredTopicArn() {
27
+ return configured("alarmTopicArn");
28
+ }
29
+
30
+ function silenceWindowSeconds() {
31
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(configured("alarmSilenceWindowSeconds"), __x => Stdlib_Int.fromString(__x, undefined)), Util_AlarmSpec$ReventlessAws.defaultSilenceWindowSeconds);
32
+ }
33
+
34
+ let topicName = "ReventlessAlarms";
35
+
36
+ let resolvedTopic = {
37
+ contents: "Unresolved"
38
+ };
39
+
40
+ function ensureTopicArn() {
41
+ let resolved = resolvedTopic.contents;
42
+ if (typeof resolved === "object") {
43
+ return resolved;
44
+ }
45
+ if (resolved !== "Unresolved") {
46
+ return "NoTopic";
47
+ }
48
+ let match = configured("alarmTopicArn");
49
+ let match$1 = configured("alarmEmail");
50
+ let resolved$1;
51
+ if (match !== undefined) {
52
+ resolved$1 = {
53
+ TAG: "Topic",
54
+ _0: Pulumi.output(match)
55
+ };
56
+ } else if (match$1 !== undefined) {
57
+ let topic = new (Aws.sns.Topic)(topicName, {
58
+ tags: AWS_Tags$ReventlessAws.make(topicName, "Plugin", {
59
+ TAG: "Other",
60
+ _0: "Alarm"
61
+ }, "Plugin", undefined, undefined, undefined, undefined)
62
+ });
63
+ new (Aws.sns.TopicSubscription)(topicName + "Email", {
64
+ endpoint: match$1,
65
+ topic: topic.arn,
66
+ protocol: "email"
67
+ }, undefined);
68
+ resolved$1 = {
69
+ TAG: "Topic",
70
+ _0: topic.arn
71
+ };
72
+ } else {
73
+ resolved$1 = "NoTopic";
74
+ }
75
+ resolvedTopic.contents = resolved$1;
76
+ return resolved$1;
77
+ }
78
+
79
+ function alarmFor(spec, kind, name, component, plugin, platform, logLocator, topicArn) {
80
+ let resourceName = Util_AlarmSpec$ReventlessAws.resourceName(kind, name, plugin, spec.suffix);
81
+ let actions = [topicArn];
82
+ let alarmDescription = Output$Pulumi.map(logLocator, g => {
83
+ let logs = g === "" ? undefined : g;
84
+ return Util_AlarmSpec$ReventlessAws.description(kind, name, plugin, platform, spec, logs);
85
+ });
86
+ return new (Aws.cloudwatch.MetricAlarm)(resourceName, {
87
+ comparisonOperator: spec.comparisonOperator,
88
+ evaluationPeriods: spec.evaluationPeriods,
89
+ metricName: spec.metricName,
90
+ namespace: spec.namespace,
91
+ period: spec.period,
92
+ statistic: spec.statistic,
93
+ threshold: spec.threshold,
94
+ dimensions: component.name.apply(fn => Object.fromEntries([[
95
+ "FunctionName",
96
+ fn
97
+ ]])),
98
+ treatMissingData: spec.treatMissingData,
99
+ alarmActions: actions,
100
+ okActions: actions,
101
+ alarmDescription: alarmDescription,
102
+ tags: AWS_Tags$ReventlessAws.make(resourceName, "Plugin", {
103
+ TAG: "Other",
104
+ _0: "Alarm"
105
+ }, undefined, name, undefined, plugin, platform)
106
+ });
107
+ }
108
+
109
+ function onProvisioned(kind, name, component, plugin, platform, logLocator) {
110
+ let logLocator$1 = logLocator !== undefined ? logLocator : Pulumi.output("");
111
+ let topicArn = ensureTopicArn();
112
+ if (typeof topicArn !== "object") {
113
+ return;
114
+ }
115
+ let topicArn$1 = topicArn._0;
116
+ Util_AlarmSpec$ReventlessAws.forKind(kind, silenceWindowSeconds()).forEach(spec => {
117
+ alarmFor(spec, kind, name, component, plugin, platform, logLocator$1, topicArn$1);
118
+ });
119
+ }
120
+
121
+ let Backend = {
122
+ onProvisioned: onProvisioned
123
+ };
124
+
125
+ function use() {
126
+ Monitoring$ReventlessCore.use(Backend);
127
+ }
128
+
129
+ let M;
130
+
131
+ export {
132
+ M,
133
+ configured,
134
+ configuredEmail,
135
+ configuredTopicArn,
136
+ silenceWindowSeconds,
137
+ topicName,
138
+ resolvedTopic,
139
+ ensureTopicArn,
140
+ alarmFor,
141
+ Backend,
142
+ use,
143
+ }
144
+ /* @pulumi/aws Not a pure module */
@@ -98,7 +98,11 @@ let queryByTableName = (
98
98
  limit,
99
99
  }
100
100
  }
101
- ReventlessCore.EffectLogger.logDebug(~comp=__MODULE__, "queryByTableName params: " ++ params->JSON.stringifyAny->Option.getOr(""))
101
+ ReventlessCore.EffectLogger.logDebug(
102
+ ~comp=__MODULE__,
103
+ "queryByTableName params: " ++
104
+ params->JSON.stringifyAny->Option.getOr("")->ReventlessCore.LogFormat.truncate,
105
+ )
102
106
  ->Effect.flatMap(_ =>
103
107
  Util_DynamoDb_Runtime.queryStream(params)
104
108
  ->Stream.runCollect
@@ -130,7 +134,11 @@ let scanByTableName = (~tableName, ~filterConfigs, ~limit) => {
130
134
  expressionAttributeValues: ?attributeValues,
131
135
  limit,
132
136
  }
133
- ReventlessCore.EffectLogger.logDebug(~comp=__MODULE__, "scanByTableName params: " ++ params->JSON.stringifyAny->Option.getOr(""))
137
+ ReventlessCore.EffectLogger.logDebug(
138
+ ~comp=__MODULE__,
139
+ "scanByTableName params: " ++
140
+ params->JSON.stringifyAny->Option.getOr("")->ReventlessCore.LogFormat.truncate,
141
+ )
134
142
  ->Effect.flatMap(_ =>
135
143
  Util_DynamoDb_Runtime.scanStream(params)
136
144
  ->Stream.runCollect
@@ -8,6 +8,7 @@ import * as Effect from "effect/Effect";
8
8
  import * as Pulumi from "@pulumi/pulumi";
9
9
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
10
10
  import * as Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/adapter/Adapter.res.mjs";
11
+ import * as LogFormat$ReventlessCore from "@reventlessdev/reventless-core/src/util/LogFormat.res.mjs";
11
12
  import * as EffectLogger$ReventlessCore from "@reventlessdev/reventless-core/src/util/EffectLogger.res.mjs";
12
13
  import * as Util_DynamoDb$ReventlessAws from "../../util/Util_DynamoDb.res.mjs";
13
14
  import * as DynamoDb_Error$ReventlessAws from "../../errors/DynamoDb_Error.res.mjs";
@@ -166,7 +167,7 @@ function queryByTableName(tableName, keyOpt, id, subIdConfig, filterConfigsOpt,
166
167
  Limit: params_Limit,
167
168
  ScanIndexForward: params_ScanIndexForward
168
169
  };
169
- return Effect.runPromise(Effect.flatMap(EffectLogger$ReventlessCore.logDebug("QueryEngine_DynamoDb-ReventlessAws", undefined, "queryByTableName params: " + Stdlib_Option.getOr(JSON.stringify(params), "")), () => Effect.catchAll(Effect.map(Stream.runCollect(Util_DynamoDb_Runtime$ReventlessAws.queryStream(params)), items => items.map(js => JSON.parse(JSON.stringify(js)))), err => {
170
+ return Effect.runPromise(Effect.flatMap(EffectLogger$ReventlessCore.logDebug("QueryEngine_DynamoDb-ReventlessAws", undefined, "queryByTableName params: " + LogFormat$ReventlessCore.truncate(Stdlib_Option.getOr(JSON.stringify(params), ""))), () => Effect.catchAll(Effect.map(Stream.runCollect(Util_DynamoDb_Runtime$ReventlessAws.queryStream(params)), items => items.map(js => JSON.parse(JSON.stringify(js)))), err => {
170
171
  let msg = DynamoDb_Error$ReventlessAws.message(err);
171
172
  return Effect.map(EffectLogger$ReventlessCore.logError("QueryEngine_DynamoDb-ReventlessAws", undefined, "queryByTableName: " + msg), () => []);
172
173
  })));
@@ -196,7 +197,7 @@ function scanByTableName(tableName, filterConfigs, limit) {
196
197
  FilterExpression: params_FilterExpression,
197
198
  Limit: params_Limit
198
199
  };
199
- return Effect.runPromise(Effect.flatMap(EffectLogger$ReventlessCore.logDebug("QueryEngine_DynamoDb-ReventlessAws", undefined, "scanByTableName params: " + Stdlib_Option.getOr(JSON.stringify(params), "")), () => Effect.catchAll(Effect.map(Stream.runCollect(Util_DynamoDb_Runtime$ReventlessAws.scanStream(params)), items => items.map(js => JSON.parse(JSON.stringify(js)))), err => {
200
+ return Effect.runPromise(Effect.flatMap(EffectLogger$ReventlessCore.logDebug("QueryEngine_DynamoDb-ReventlessAws", undefined, "scanByTableName params: " + LogFormat$ReventlessCore.truncate(Stdlib_Option.getOr(JSON.stringify(params), ""))), () => Effect.catchAll(Effect.map(Stream.runCollect(Util_DynamoDb_Runtime$ReventlessAws.scanStream(params)), items => items.map(js => JSON.parse(JSON.stringify(js)))), err => {
200
201
  let msg = DynamoDb_Error$ReventlessAws.message(err);
201
202
  return Effect.map(EffectLogger$ReventlessCore.logError("QueryEngine_DynamoDb-ReventlessAws", undefined, "scanByTableName: " + msg), () => []);
202
203
  })));