@reventlessdev/reventless-aws 3.0.0-alpha.259 → 3.0.0-alpha.260

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.
@@ -198,6 +198,16 @@ let finish = () =>
198
198
 
199
199
  let envVars: dict<Pulumi.Input.t<string>> = Dict.make()
200
200
  envVars->Dict.set("HANDLER_CONFIG", handlerConfigOutput->Pulumi.Output.asInput)
201
+ // Deploy-derived capability endpoints — a geocoder URL today. Plugin-wide
202
+ // rather than per-handler, which is why they sit here and not inside
203
+ // HANDLER_CONFIG: this is one shared Lambda and every slice on it reaches
204
+ // the same capability. A capability the platform did not provision arrives
205
+ // as "", which its client reads as "not configured" and reports as a
206
+ // retryable `Unavailable` — a modelled outcome rather than a missing
207
+ // variable that fails as a crash.
208
+ PluginRuntime_Builder.capabilityEnv()->Array.forEach(((name, value)) =>
209
+ envVars->Dict.set(name, value->Pulumi.Output.asInput)
210
+ )
201
211
 
202
212
  let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
203
213
  ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/AutomationSliceEntryPoint.mjs",
@@ -310,6 +320,16 @@ let finishWithDcbEventLog = (dcbEventLog: ReventlessCore.DcbEventLog.component)
310
320
 
311
321
  let envVars: dict<Pulumi.Input.t<string>> = Dict.make()
312
322
  envVars->Dict.set("HANDLER_CONFIG", handlerConfigOutput->Pulumi.Output.asInput)
323
+ // Deploy-derived capability endpoints — a geocoder URL today. Plugin-wide
324
+ // rather than per-handler, which is why they sit here and not inside
325
+ // HANDLER_CONFIG: this is one shared Lambda and every slice on it reaches
326
+ // the same capability. A capability the platform did not provision arrives
327
+ // as "", which its client reads as "not configured" and reports as a
328
+ // retryable `Unavailable` — a modelled outcome rather than a missing
329
+ // variable that fails as a crash.
330
+ PluginRuntime_Builder.capabilityEnv()->Array.forEach(((name, value)) =>
331
+ envVars->Dict.set(name, value->Pulumi.Output.asInput)
332
+ )
313
333
 
314
334
  let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
315
335
  ~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Runtime/AutomationSliceEntryPoint.mjs",
@@ -8,6 +8,7 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
8
8
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
9
9
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
10
10
  import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
11
+ import * as PluginRuntime_Builder$ReventlessAws from "../../plugin/runtime/PluginRuntime_Builder.res.mjs";
11
12
  import * as RuntimeEnvironment_Lambda$ReventlessAws from "./RuntimeEnvironment_Lambda.res.mjs";
12
13
  import * as EventCollectorChannel_DynamoDbStream$ReventlessAws from "../EventCollector/EventCollectorChannel_DynamoDbStream.res.mjs";
13
14
 
@@ -129,6 +130,9 @@ function finish() {
129
130
  let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
130
131
  let envVars = {};
131
132
  envVars["HANDLER_CONFIG"] = handlerConfigOutput;
133
+ PluginRuntime_Builder$ReventlessAws.capabilityEnv().forEach(param => {
134
+ envVars[param[0]] = param[1];
135
+ });
132
136
  let match$1 = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AutomationSliceEntryPoint.mjs", packageDirs, undefined);
133
137
  let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset("AllAutomationSlices", "Reactor", "AutomationSlice", match$1.code, match$1.sourceCodeHash, envVars, match[0], match[1], undefined, undefined, undefined, undefined, undefined, opts);
134
138
  let channelSpecs = storedSpecs.map(param => param.channelSpec);
@@ -188,6 +192,9 @@ function finishWithDcbEventLog(dcbEventLog) {
188
192
  let handlerConfigOutput = Pulumi.all(handlerOutputs).apply(handlers => `{"handlers":[` + handlers.join(",") + `]}`);
189
193
  let envVars = {};
190
194
  envVars["HANDLER_CONFIG"] = handlerConfigOutput;
195
+ PluginRuntime_Builder$ReventlessAws.capabilityEnv().forEach(param => {
196
+ envVars[param[0]] = param[1];
197
+ });
191
198
  let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Runtime/AutomationSliceEntryPoint.mjs", packageDirs, undefined);
192
199
  let runtime = RuntimeEnvironment_Lambda$ReventlessAws.makeFromCodeAsset("AllAutomationSlices", "Reactor", "AutomationSlice", match.code, match.sourceCodeHash, envVars, 1024, 30, undefined, undefined, undefined, undefined, undefined, opts);
193
200
  EventCollectorChannel_DynamoDbStream$ReventlessAws.connect("AllAutomationSlices", [{
@@ -331,26 +331,13 @@ let makeFromCodeAsset: (
331
331
  // is no race against Lambda's lazy auto-created group. Namespace/dimension
332
332
  // are CloudWatch-specific and live here, not in core (provider-neutral).
333
333
  if dcbMetrics {
334
- [
335
- "AppendRetry",
336
- "AppendConflict",
337
- "DcbDecisionModelCacheHit",
338
- "DcbDecisionModelCacheMiss",
339
- "DcbDecisionModelDeltaEventCount",
340
- ]->Array.forEach(metricName => {
334
+ Util_DcbMetrics.metricNames->Array.forEach(metricName => {
341
335
  let _ = Cloudwatch.LogMetricFilter.make(
342
336
  ~name=`${name}${metricName}Filter`,
343
337
  ~args={
344
- pattern: `{ $.reventlessMetric = "${metricName}" }`->Pulumi.Input.make,
338
+ pattern: Util_DcbMetrics.patternFor(metricName)->Pulumi.Input.make,
345
339
  logGroupName: logGroup.name->Pulumi.Output.asInput,
346
- metricTransformation: ({
347
- name: metricName->Pulumi.Input.make,
348
- namespace: "Reventless/DCB"->Pulumi.Input.make,
349
- value: "$.value"->Pulumi.Input.make,
350
- defaultValue: "0"->Pulumi.Input.make,
351
- unit: "Count"->Pulumi.Input.make,
352
- dimensions: Dict.fromArray([("slice", "$.slice")])->Pulumi.Input.make,
353
- }: Cloudwatch.LogMetricFilter.metricTransformation)->Pulumi.Input.make,
340
+ metricTransformation: Util_DcbMetrics.transformationFor(metricName)->Pulumi.Input.make,
354
341
  },
355
342
  ~opts?,
356
343
  )
@@ -20,6 +20,7 @@ import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
20
20
  import * as Util_Lambda$ReventlessAws from "../../util/Util_Lambda.res.mjs";
21
21
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
22
22
  import * as Util_IAM_Role$ReventlessAws from "../../util/Util_IAM_Role.res.mjs";
23
+ import * as Util_DcbMetrics$ReventlessAws from "../../util/Util_DcbMetrics.res.mjs";
23
24
  import * as Util_LocalConfig$ReventlessAws from "../../util/Util_LocalConfig.res.mjs";
24
25
  import * as Util_HostUiDomain$ReventlessAws from "../../util/Util_HostUiDomain.res.mjs";
25
26
  import * as Util_LogRetention$ReventlessAws from "../../util/Util_LogRetention.res.mjs";
@@ -136,27 +137,11 @@ function makeFromCodeAsset(name, unitKind, componentKind, code, sourceCodeHash,
136
137
  tags: tagsFor(name + `LogGroup`, "Logs")
137
138
  }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
138
139
  if (dcbMetrics) {
139
- [
140
- "AppendRetry",
141
- "AppendConflict",
142
- "DcbDecisionModelCacheHit",
143
- "DcbDecisionModelCacheMiss",
144
- "DcbDecisionModelDeltaEventCount"
145
- ].forEach(metricName => {
140
+ Util_DcbMetrics$ReventlessAws.metricNames.forEach(metricName => {
146
141
  new (Aws.cloudwatch.LogMetricFilter)(name + metricName + `Filter`, {
147
- pattern: `{ $.reventlessMetric = "` + metricName + `" }`,
142
+ pattern: Util_DcbMetrics$ReventlessAws.patternFor(metricName),
148
143
  logGroupName: logGroup.name,
149
- metricTransformation: {
150
- name: metricName,
151
- namespace: "Reventless/DCB",
152
- value: "$.value",
153
- defaultValue: "0",
154
- unit: "Count",
155
- dimensions: Object.fromEntries([[
156
- "slice",
157
- "$.slice"
158
- ]])
159
- }
144
+ metricTransformation: Util_DcbMetrics$ReventlessAws.transformationFor(metricName)
160
145
  }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
161
146
  });
162
147
  return;
@@ -38,8 +38,8 @@ module Make = (Api: {
38
38
  type component = InnerMake.component
39
39
  let queryDbName = InnerMake.queryDbName
40
40
 
41
- let make = (~dcbEventLog, ~publishJsons, ~runtime=?, ~opts=?): component => {
42
- let ots = InnerMake.make(~dcbEventLog, ~publishJsons, ~runtime?, ~opts?)
41
+ let make = (~dcbEventLog, ~allEventTopics=Dict.make(), ~publishJsons, ~runtime=?, ~opts=?): component => {
42
+ let ots = InnerMake.make(~dcbEventLog, ~allEventTopics, ~publishJsons, ~runtime?, ~opts?)
43
43
 
44
44
  let queryDbOutputs = (ots->ReventlessCore.Component.outputs).queryDb
45
45
  let tableResource = queryDbOutputs.resources->Array.getUnsafe(0)
@@ -43,8 +43,9 @@ function Make(Api) {
43
43
  })(Api);
44
44
  let Make$1 = Spec => (Translation => {
45
45
  let InnerMake = Inner.Make(Spec)(Translation);
46
- let make = (dcbEventLog, publishJsons, runtime, opts) => {
47
- let ots = InnerMake.make(dcbEventLog, publishJsons, runtime, opts);
46
+ let make = (dcbEventLog, allEventTopicsOpt, publishJsons, runtime, opts) => {
47
+ let allEventTopics = allEventTopicsOpt !== undefined ? allEventTopicsOpt : ({});
48
+ let ots = InnerMake.make(dcbEventLog, allEventTopics, publishJsons, runtime, opts);
48
49
  let queryDbOutputs = Component$ReventlessCore.outputs(ots).queryDb;
49
50
  let tableResource = queryDbOutputs.resources[0];
50
51
  let queryDbTableName = tableResource.name;
@@ -20,6 +20,37 @@ let configRef: ref<adminConfig> = ref({
20
20
  clonerEnabled: false,
21
21
  })
22
22
 
23
+ /**
24
+ Deploy-derived environment for a plugin's slice Lambdas, keyed by variable name.
25
+
26
+ A capability the platform provisions — a geocoder, and the same shape whenever
27
+ the next one arrives — is reachable only through a value the platform computes:
28
+ a URL, an index name, a queue. The deployer never types it, which is what keeps
29
+ it out of `commandHandlerConfig` (memory, timeout, the knobs a human chooses)
30
+ and out of the hand-edited config files this framework exists to remove.
31
+
32
+ Kept apart from `configRef` because the two are filled at different moments and
33
+ by different halves: `registerConfig` runs inside `deployPlatform` and replaces
34
+ its record wholesale, while these values are only knowable in `deployPlugin`,
35
+ where a plugin stack reads them off the platform's stack reference.
36
+
37
+ Keyed by the variable name rather than by capability so the runtime builders
38
+ merge a dict they need no knowledge of. Naming a capability's variable is then
39
+ a single decision at the single call site that knows which capability it is —
40
+ not a table two modules have to agree on.
41
+ */
42
+ let capabilityEnvRef: dict<Pulumi.Output.t<string>> = Dict.make()
43
+
44
+ /** Registers one deploy-derived variable. Must be called before the plugin
45
+ builds — the slice runtimes read the dict when their finalizer runs. */
46
+ let registerCapabilityEnv = (name: string, value: Pulumi.Output.t<string>) =>
47
+ capabilityEnvRef->Dict.set(name, value)
48
+
49
+ /** The registered variables, for a runtime builder to merge into its Lambda's
50
+ environment. Empty is the ordinary answer for a platform that provisions no
51
+ capability, so this returns `[]` rather than throwing. */
52
+ let capabilityEnv = () => capabilityEnvRef->Dict.toArray
53
+
23
54
  type sliceModulePaths = {
24
55
  specPath: string,
25
56
  behaviorPath: string,
@@ -34,6 +34,16 @@ let configRef = {
34
34
  }
35
35
  };
36
36
 
37
+ let capabilityEnvRef = {};
38
+
39
+ function registerCapabilityEnv(name, value) {
40
+ capabilityEnvRef[name] = value;
41
+ }
42
+
43
+ function capabilityEnv() {
44
+ return Object.entries(capabilityEnvRef);
45
+ }
46
+
37
47
  let dcbConfigRef = {
38
48
  contents: {
39
49
  pluginName: "",
@@ -493,6 +503,9 @@ function Make(EventCollectorChannel) {
493
503
  export {
494
504
  log,
495
505
  configRef,
506
+ capabilityEnvRef,
507
+ registerCapabilityEnv,
508
+ capabilityEnv,
496
509
  dcbConfigRef,
497
510
  registeredSliceModulePaths,
498
511
  registerStateChangeSliceSpec,
@@ -0,0 +1,48 @@
1
+ // The CloudWatch half of DCB metrics: the provider-neutral metric lines that
2
+ // StateChangeSlice_Callback emits (`{reventlessMetric, slice, value}`) become
3
+ // CloudWatch metrics through a log metric filter per metric name. Namespace and
4
+ // dimension are CloudWatch-specific and belong here rather than in core.
5
+ //
6
+ // Pure on purpose. The filters are only built for a Lambda that has a MANAGED log
7
+ // group to attach to, so no stack exercised this code until managed groups existed
8
+ // — and an invalid transformation therefore shipped without any deploy catching it.
9
+ // Keeping the transformation a plain function makes the one rule CloudWatch enforces
10
+ // testable without standing up a Pulumi resource.
11
+
12
+ open PulumiAws
13
+
14
+ /** The metrics extracted from a DCB command handler's logs. */
15
+ let metricNames = [
16
+ "AppendRetry",
17
+ "AppendConflict",
18
+ "DcbDecisionModelCacheHit",
19
+ "DcbDecisionModelCacheMiss",
20
+ "DcbDecisionModelDeltaEventCount",
21
+ ]
22
+
23
+ let namespace = "Reventless/DCB"
24
+
25
+ /** Filter pattern selecting one metric's lines out of the log stream. */
26
+ let patternFor = (metricName: string): string => `{ $.reventlessMetric = "${metricName}" }`
27
+
28
+ /**
29
+ The metric transformation for one DCB metric, dimensioned by slice.
30
+
31
+ `dimensions` and `defaultValue` are **mutually exclusive** — `PutMetricFilter`
32
+ rejects a transformation carrying both with `InvalidParameterException: Invalid
33
+ metric transformation: dimensions and default value are mutually exclusive
34
+ properties`. The reason is that a default value is emitted when the pattern does not
35
+ match, and for a dimensioned metric there is no dimension value to attribute such a
36
+ point to.
37
+
38
+ The `slice` dimension is the half worth keeping: without it every slice's counts
39
+ collapse into one undifferentiated series. So this sets no `defaultValue`, and a
40
+ metric simply reports no data point for a period in which nothing matched.
41
+ */
42
+ let transformationFor = (metricName: string): Cloudwatch.LogMetricFilter.metricTransformation => {
43
+ name: metricName->Pulumi.Input.make,
44
+ namespace: namespace->Pulumi.Input.make,
45
+ value: "$.value"->Pulumi.Input.make,
46
+ unit: "Count"->Pulumi.Input.make,
47
+ dimensions: Dict.fromArray([("slice", "$.slice")])->Pulumi.Input.make,
48
+ }
@@ -0,0 +1,37 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ let metricNames = [
5
+ "AppendRetry",
6
+ "AppendConflict",
7
+ "DcbDecisionModelCacheHit",
8
+ "DcbDecisionModelCacheMiss",
9
+ "DcbDecisionModelDeltaEventCount"
10
+ ];
11
+
12
+ let namespace = "Reventless/DCB";
13
+
14
+ function patternFor(metricName) {
15
+ return `{ $.reventlessMetric = "` + metricName + `" }`;
16
+ }
17
+
18
+ function transformationFor(metricName) {
19
+ return {
20
+ name: metricName,
21
+ namespace: namespace,
22
+ value: "$.value",
23
+ unit: "Count",
24
+ dimensions: Object.fromEntries([[
25
+ "slice",
26
+ "$.slice"
27
+ ]])
28
+ };
29
+ }
30
+
31
+ export {
32
+ metricNames,
33
+ namespace,
34
+ patternFor,
35
+ transformationFor,
36
+ }
37
+ /* No side effect */
@@ -0,0 +1,72 @@
1
+ // Assembly of the `config.json` the deploy writes beside the host shell's
2
+ // `index.html`.
3
+ //
4
+ // The keys split in two: the ones the deploy *computes* (endpoints, region,
5
+ // pool ids — resolved Pulumi Outputs by the time they get here) and the ones a
6
+ // deployment *chooses*. This module is the pure join of the two, so the whole
7
+ // key set is reachable from a test instead of living inside deployPlatform's
8
+ // `Pulumi.Output.apply`, where nothing could assert a single key of it.
9
+
10
+ module Platform = ReventlessInfra.Platform
11
+
12
+ // A mode's options, flattened to the wire shape. They are payloads of their arm
13
+ // on the deploy side (so `mapStyle` with the map off cannot be expressed) and
14
+ // flat siblings of `viewModes` on the wire (because that is where the released
15
+ // shell reads them). This function is the whole of that translation.
16
+ let modeOptions = (mode: Platform.viewMode): array<(string, JSON.t)> =>
17
+ switch mode {
18
+ | Map(opts) =>
19
+ switch opts.style {
20
+ | Some(style) => [("mapStyle", JSON.Encode.string(style))]
21
+ | None => []
22
+ }
23
+ | Graph(opts) =>
24
+ switch opts.layout {
25
+ | Some(layout) => [("graphLayout", JSON.Encode.string(layout))]
26
+ | None => []
27
+ }
28
+ }
29
+
30
+ /**
31
+ The config.json field set.
32
+
33
+ `computed` arrives in wire order and keeps it. `viewModes` unset ⇒ no
34
+ `viewModes` key and no per-mode key, so a deployment that wants no optional mode
35
+ gets byte-identical output to before this input existed. `shellConfig` merges in
36
+ last; a key it shares with one already present fails the deploy naming it,
37
+ because silently resolving the collision either way produces an app pointed
38
+ somewhere unintended with nothing in the diff to say so.
39
+ */
40
+ let fields = (
41
+ ~computed: array<(string, JSON.t)>,
42
+ ~viewModes: option<array<Platform.viewMode>>=?,
43
+ ~shellConfig: option<dict<JSON.t>>=?,
44
+ ): dict<JSON.t> => {
45
+ let out = computed->Dict.fromArray
46
+
47
+ switch viewModes {
48
+ | Some(modes) =>
49
+ out->Dict.set(
50
+ "viewModes",
51
+ modes->Array.map(m => m->Platform.viewModeToString->JSON.Encode.string)->JSON.Encode.array,
52
+ )
53
+ modes->Array.forEach(m => m->modeOptions->Array.forEach(((k, v)) => out->Dict.set(k, v)))
54
+ | None => ()
55
+ }
56
+
57
+ switch shellConfig {
58
+ | Some(extra) =>
59
+ let collisions = extra->Dict.keysToArray->Array.filter(k => out->Dict.get(k)->Option.isSome)
60
+ if collisions->Array.length > 0 {
61
+ failwith(
62
+ "host UI config.json: shellConfig sets key(s) the deploy already computes — " ++
63
+ collisions->Array.join(", ") ++
64
+ ". Remove them from shellConfig; a passthrough cannot redirect a computed key.",
65
+ )
66
+ }
67
+ extra->Dict.forEachWithKey((v, k) => out->Dict.set(k, v))
68
+ | None => ()
69
+ }
70
+
71
+ out
72
+ }
@@ -0,0 +1,60 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Pervasives from "@rescript/runtime/lib/es6/Pervasives.js";
4
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Platform$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/Platform.res.mjs";
7
+
8
+ function modeOptions(mode) {
9
+ if (mode.TAG === "Map") {
10
+ let style = mode._0.style;
11
+ if (style !== undefined) {
12
+ return [[
13
+ "mapStyle",
14
+ style
15
+ ]];
16
+ } else {
17
+ return [];
18
+ }
19
+ }
20
+ let layout = mode._0.layout;
21
+ if (layout !== undefined) {
22
+ return [[
23
+ "graphLayout",
24
+ layout
25
+ ]];
26
+ } else {
27
+ return [];
28
+ }
29
+ }
30
+
31
+ function fields(computed, viewModes, shellConfig) {
32
+ let out = Object.fromEntries(computed);
33
+ if (viewModes !== undefined) {
34
+ out["viewModes"] = viewModes.map(Platform$ReventlessInfra.viewModeToString);
35
+ viewModes.forEach(m => {
36
+ modeOptions(m).forEach(param => {
37
+ out[param[0]] = param[1];
38
+ });
39
+ });
40
+ }
41
+ if (shellConfig !== undefined) {
42
+ let collisions = Object.keys(shellConfig).filter(k => Stdlib_Option.isSome(out[k]));
43
+ if (collisions.length !== 0) {
44
+ Pervasives.failwith("host UI config.json: shellConfig sets key(s) the deploy already computes — " + collisions.join(", ") + ". Remove them from shellConfig; a passthrough cannot redirect a computed key.");
45
+ }
46
+ Stdlib_Dict.forEachWithKey(shellConfig, (v, k) => {
47
+ out[k] = v;
48
+ });
49
+ }
50
+ return out;
51
+ }
52
+
53
+ let Platform;
54
+
55
+ export {
56
+ Platform,
57
+ modeOptions,
58
+ fields,
59
+ }
60
+ /* No side effect */
@@ -0,0 +1,78 @@
1
+ // Guards the geocoder Function URL's *status* contract, which is the whole
2
+ // reason one endpoint can serve two callers that want opposite things from it.
3
+ //
4
+ // A browser search box reads the body and must degrade quietly: never an error,
5
+ // just no results. An unattended translator reads the status, because it has to
6
+ // tell "there is no such address" (write a verdict, do not retry) from "the
7
+ // service cannot answer" (retry). The two only ever disagree about which half of
8
+ // the response they read, so:
9
+ //
10
+ // 200 + a possibly-empty array — an answer
11
+ // anything else — no answer
12
+ //
13
+ // The arms below are the ones reachable without a live AWS Location call. The
14
+ // success path and the thrown-error path both need the SDK and stay unasserted
15
+ // here — see the plan's Verification note.
16
+
17
+ open JestGlobals
18
+
19
+ let setIndex = v => NodeProcess.env->Dict.set("PLACE_INDEX_NAME", v)
20
+ let clearIndex = () => NodeProcess.env->Dict.delete("PLACE_INDEX_NAME")
21
+
22
+ describe("Geocoder_AwsLocation_Ops.handler status contract", () => {
23
+ test("an unset PLACE_INDEX_NAME is a service failure, not a verdict", async () => {
24
+ clearIndex()
25
+ let resp = await Geocoder_AwsLocation_Ops.handler({
26
+ queryStringParameters: Dict.fromArray([("q", "10 Downing Street")]),
27
+ })
28
+ // The arm that matters most: `200 []` here would tell a translator this
29
+ // address does not exist, and it would record that permanently — for every
30
+ // address handed to it while the deployment stayed misconfigured.
31
+ expect(resp.statusCode)->toBe(502)
32
+ expect(resp.body)->toBe("[]")
33
+ })
34
+
35
+ test("an empty query is an answer — nothing was asked", async () => {
36
+ setIndex("some-index")
37
+ let resp = await Geocoder_AwsLocation_Ops.handler({
38
+ queryStringParameters: Dict.fromArray([("q", "")]),
39
+ })
40
+ // 200, unlike the case above: "no results for nothing" is true and final,
41
+ // and a search box sends exactly this on every cleared input.
42
+ expect(resp.statusCode)->toBe(200)
43
+ expect(resp.body)->toBe("[]")
44
+ clearIndex()
45
+ })
46
+
47
+ test("a missing q param is treated as an empty one", async () => {
48
+ setIndex("some-index")
49
+ let resp = await Geocoder_AwsLocation_Ops.handler({})
50
+ expect(resp.statusCode)->toBe(200)
51
+ clearIndex()
52
+ })
53
+ })
54
+
55
+ describe("Geocoder_AwsLocation_Ops.readQueryParam", () => {
56
+ testSync("prefers the parsed params", () => {
57
+ expect(
58
+ Geocoder_AwsLocation_Ops.readQueryParam({
59
+ queryStringParameters: Dict.fromArray([("q", "Berlin")]),
60
+ }),
61
+ )->toEqual(Some("Berlin"))
62
+ })
63
+
64
+ testSync("falls back to the raw query string, percent-decoded", () => {
65
+ // Payload format 2.0 populates both fields, but a caller that builds its own
66
+ // URL is the reason the fallback exists — and the encoding is where a plain
67
+ // `split("=")` would hand back `10%20Downing%20Street`.
68
+ expect(
69
+ Geocoder_AwsLocation_Ops.readQueryParam({
70
+ rawQueryString: "lang=en&q=10%20Downing%20Street",
71
+ }),
72
+ )->toEqual(Some("10 Downing Street"))
73
+ })
74
+
75
+ testSync("no q anywhere is None", () => {
76
+ expect(Geocoder_AwsLocation_Ops.readQueryParam({rawQueryString: "lang=en"}))->toEqual(None)
77
+ })
78
+ })
@@ -0,0 +1,71 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
4
+ import * as Geocoder_AwsLocation_Ops$ReventlessAws from "../src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs";
5
+
6
+ function setIndex(v) {
7
+ process.env["PLACE_INDEX_NAME"] = v;
8
+ }
9
+
10
+ function clearIndex() {
11
+ Stdlib_Dict.$$delete(process.env, "PLACE_INDEX_NAME");
12
+ }
13
+
14
+ globalThis.describe("Geocoder_AwsLocation_Ops.handler status contract", () => {
15
+ globalThis.test("an unset PLACE_INDEX_NAME is a service failure, not a verdict", async () => {
16
+ Stdlib_Dict.$$delete(process.env, "PLACE_INDEX_NAME");
17
+ let resp = await Geocoder_AwsLocation_Ops$ReventlessAws.handler({
18
+ queryStringParameters: Object.fromEntries([[
19
+ "q",
20
+ "10 Downing Street"
21
+ ]])
22
+ });
23
+ globalThis.expect(resp.statusCode).toBe(502);
24
+ globalThis.expect(resp.body).toBe("[]");
25
+ });
26
+ globalThis.test("an empty query is an answer — nothing was asked", async () => {
27
+ setIndex("some-index");
28
+ let resp = await Geocoder_AwsLocation_Ops$ReventlessAws.handler({
29
+ queryStringParameters: Object.fromEntries([[
30
+ "q",
31
+ ""
32
+ ]])
33
+ });
34
+ globalThis.expect(resp.statusCode).toBe(200);
35
+ globalThis.expect(resp.body).toBe("[]");
36
+ return Stdlib_Dict.$$delete(process.env, "PLACE_INDEX_NAME");
37
+ });
38
+ globalThis.test("a missing q param is treated as an empty one", async () => {
39
+ setIndex("some-index");
40
+ let resp = await Geocoder_AwsLocation_Ops$ReventlessAws.handler({});
41
+ globalThis.expect(resp.statusCode).toBe(200);
42
+ return Stdlib_Dict.$$delete(process.env, "PLACE_INDEX_NAME");
43
+ });
44
+ });
45
+
46
+ globalThis.describe("Geocoder_AwsLocation_Ops.readQueryParam", () => {
47
+ globalThis.test("prefers the parsed params", () => {
48
+ globalThis.expect(Geocoder_AwsLocation_Ops$ReventlessAws.readQueryParam({
49
+ queryStringParameters: Object.fromEntries([[
50
+ "q",
51
+ "Berlin"
52
+ ]])
53
+ })).toEqual("Berlin");
54
+ });
55
+ globalThis.test("falls back to the raw query string, percent-decoded", () => {
56
+ globalThis.expect(Geocoder_AwsLocation_Ops$ReventlessAws.readQueryParam({
57
+ rawQueryString: "lang=en&q=10%20Downing%20Street"
58
+ })).toEqual("10 Downing Street");
59
+ });
60
+ globalThis.test("no q anywhere is None", () => {
61
+ globalThis.expect(Geocoder_AwsLocation_Ops$ReventlessAws.readQueryParam({
62
+ rawQueryString: "lang=en"
63
+ })).toEqual(undefined);
64
+ });
65
+ });
66
+
67
+ export {
68
+ setIndex,
69
+ clearIndex,
70
+ }
71
+ /* Not a pure module */
@@ -0,0 +1,35 @@
1
+ open JestGlobals
2
+
3
+ // The rule that matters is CloudWatch's, not ours: a metric transformation may carry
4
+ // `dimensions` OR `defaultValue`, never both. Violating it is not caught at compile
5
+ // time — both fields are optional in the binding — and not caught at deploy time
6
+ // either unless the stack happens to have a managed log group for the filter to
7
+ // attach to. So it is asserted here.
8
+
9
+ describe("Util_DcbMetrics.transformationFor", () => {
10
+ testSync("never sets defaultValue — it is mutually exclusive with dimensions", () =>
11
+ Util_DcbMetrics.metricNames->Array.forEach(m =>
12
+ expect(Util_DcbMetrics.transformationFor(m).defaultValue)->toBe(None)
13
+ )
14
+ )
15
+
16
+ testSync("dimensions the metric by slice", () =>
17
+ expect(
18
+ Util_DcbMetrics.transformationFor("AppendRetry").dimensions->Option.isSome,
19
+ )->toBe(true)
20
+ )
21
+
22
+ testSync("counts into the DCB namespace", () =>
23
+ expect(Util_DcbMetrics.transformationFor("AppendRetry").namespace)->toBe("Reventless/DCB")
24
+ )
25
+
26
+ testSync("names the metric after the emitted marker", () =>
27
+ expect(Util_DcbMetrics.transformationFor("AppendConflict").name)->toBe("AppendConflict")
28
+ )
29
+ })
30
+
31
+ describe("Util_DcbMetrics.patternFor", () => {
32
+ testSync("selects one metric's lines by its marker", () =>
33
+ expect(Util_DcbMetrics.patternFor("AppendRetry"))->toBe(`{ $.reventlessMetric = "AppendRetry" }`)
34
+ )
35
+ })
@@ -0,0 +1,29 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Util_DcbMetrics$ReventlessAws from "../src/util/Util_DcbMetrics.res.mjs";
5
+
6
+ globalThis.describe("Util_DcbMetrics.transformationFor", () => {
7
+ globalThis.test("never sets defaultValue — it is mutually exclusive with dimensions", () => {
8
+ Util_DcbMetrics$ReventlessAws.metricNames.forEach(m => {
9
+ globalThis.expect(Util_DcbMetrics$ReventlessAws.transformationFor(m).defaultValue).toBe(undefined);
10
+ });
11
+ });
12
+ globalThis.test("dimensions the metric by slice", () => {
13
+ globalThis.expect(Stdlib_Option.isSome(Util_DcbMetrics$ReventlessAws.transformationFor("AppendRetry").dimensions)).toBe(true);
14
+ });
15
+ globalThis.test("counts into the DCB namespace", () => {
16
+ globalThis.expect(Util_DcbMetrics$ReventlessAws.transformationFor("AppendRetry").namespace).toBe("Reventless/DCB");
17
+ });
18
+ globalThis.test("names the metric after the emitted marker", () => {
19
+ globalThis.expect(Util_DcbMetrics$ReventlessAws.transformationFor("AppendConflict").name).toBe("AppendConflict");
20
+ });
21
+ });
22
+
23
+ globalThis.describe("Util_DcbMetrics.patternFor", () => {
24
+ globalThis.test("selects one metric's lines by its marker", () => {
25
+ globalThis.expect(Util_DcbMetrics$ReventlessAws.patternFor("AppendRetry")).toBe(`{ $.reventlessMetric = "AppendRetry" }`);
26
+ });
27
+ });
28
+
29
+ /* Not a pure module */