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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/package.json +9 -9
  3. package/src/Platform.res +79 -4
  4. package/src/Platform.res.mjs +24 -8
  5. package/src/adapter/Geocoder/Geocoder_AwsLocation_Backend.res +77 -0
  6. package/src/adapter/Geocoder/Geocoder_AwsLocation_Backend.res.mjs +87 -0
  7. package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +42 -36
  8. package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +24 -6
  9. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +13 -1
  10. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +87 -8
  11. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +36 -5
  12. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +3 -16
  13. package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +4 -19
  14. package/src/components/AutomationSlice_Builder.res +13 -0
  15. package/src/components/AutomationSlice_Builder.res.mjs +7 -2
  16. package/src/components/OutboundTranslationSlice_Builder.res +18 -2
  17. package/src/components/OutboundTranslationSlice_Builder.res.mjs +9 -3
  18. package/src/plugin/runtime/PluginRuntime_Builder.res +31 -0
  19. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +13 -0
  20. package/src/util/Util_DcbMetrics.res +48 -0
  21. package/src/util/Util_DcbMetrics.res.mjs +37 -0
  22. package/src/util/Util_ShellConfig.res +72 -0
  23. package/src/util/Util_ShellConfig.res.mjs +60 -0
  24. package/tests/Geocoder_AwsLocation_OpsTest.res +78 -0
  25. package/tests/Geocoder_AwsLocation_OpsTest.res.mjs +71 -0
  26. package/tests/Util_DcbMetricsTest.res +35 -0
  27. package/tests/Util_DcbMetricsTest.res.mjs +29 -0
  28. package/tests/Util_ShellConfigTest.res +78 -0
  29. package/tests/Util_ShellConfigTest.res.mjs +113 -0
@@ -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 */
@@ -0,0 +1,78 @@
1
+ open JestGlobals
2
+
3
+ // The host shell reads config.json by key name, and every failure here is a
4
+ // silent one: a missing key deletes a feature with nothing in any log, and a
5
+ // key overwritten by a passthrough points the app at the wrong API. Both are
6
+ // only assertable because `fields` is pure.
7
+
8
+ let computed = [
9
+ ("apiEndpoint", JSON.Encode.string("https://domain.example/graphql")),
10
+ ("region", JSON.Encode.string("eu-west-1")),
11
+ ]
12
+
13
+ let get = (out, key) => out->Dict.get(key)->Option.getOr(JSON.Encode.null)
14
+
15
+ describe("Util_ShellConfig.fields — viewModes", () => {
16
+ testSync("unset ⇒ no viewModes key and the computed set is untouched", () => {
17
+ let out = Util_ShellConfig.fields(~computed)
18
+ expect(out->Dict.keysToArray)->toEqual(["apiEndpoint", "region"])
19
+ expect(out->Dict.get("viewModes")->Option.isNone)->toBe(true)
20
+ })
21
+
22
+ testSync("Map with defaults ⇒ the mode, and no mapStyle key", () => {
23
+ let out = Util_ShellConfig.fields(~computed, ~viewModes=[Map({})])
24
+ expect(out->get("viewModes"))->toEqual(JSON.Encode.array([JSON.Encode.string("map")]))
25
+ expect(out->Dict.get("mapStyle")->Option.isNone)->toBe(true)
26
+ })
27
+
28
+ testSync("per-mode options flatten beside the mode", () => {
29
+ let out = Util_ShellConfig.fields(
30
+ ~computed,
31
+ ~viewModes=[Map({style: "https://tiles.example/style.json"}), Graph({layout: "dagre"})],
32
+ )
33
+ expect(out->get("viewModes"))->toEqual(
34
+ JSON.Encode.array([JSON.Encode.string("map"), JSON.Encode.string("graph")]),
35
+ )
36
+ expect(out->get("mapStyle"))->toEqual(
37
+ JSON.Encode.string("https://tiles.example/style.json"),
38
+ )
39
+ expect(out->get("graphLayout"))->toEqual(JSON.Encode.string("dagre"))
40
+ })
41
+ })
42
+
43
+ describe("Util_ShellConfig.fields — shellConfig passthrough", () => {
44
+ testSync("shell-owned keys land verbatim, under the computed ones", () => {
45
+ let out = Util_ShellConfig.fields(
46
+ ~computed,
47
+ ~shellConfig=Dict.fromArray([
48
+ ("platformName", JSON.Encode.string("Online Shop")),
49
+ ("accessTiers", JSON.Encode.array([JSON.Encode.string("public")])),
50
+ ]),
51
+ )
52
+ expect(out->Dict.keysToArray)->toEqual([
53
+ "apiEndpoint",
54
+ "region",
55
+ "platformName",
56
+ "accessTiers",
57
+ ])
58
+ expect(out->get("platformName"))->toEqual(JSON.Encode.string("Online Shop"))
59
+ })
60
+
61
+ testSync("a key the deploy computes fails the deploy, naming it", () => {
62
+ let failure = try {
63
+ let _ = Util_ShellConfig.fields(
64
+ ~computed,
65
+ ~shellConfig=Dict.fromArray([
66
+ ("apiEndpoint", JSON.Encode.string("https://wrong.example/graphql")),
67
+ ]),
68
+ )
69
+ None
70
+ } catch {
71
+ | Failure(message) => Some(message)
72
+ }
73
+ switch failure {
74
+ | Some(message) => expect(message->String.includes("apiEndpoint"))->toBe(true)
75
+ | None => fail("a shellConfig key colliding with a computed one must fail the deploy")
76
+ }
77
+ })
78
+ })
@@ -0,0 +1,113 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as JestGlobals from "@reventlessdev/rescript-jest/src/JestGlobals.res.mjs";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
6
+ import * as Util_ShellConfig$ReventlessAws from "../src/util/Util_ShellConfig.res.mjs";
7
+
8
+ let computed = [
9
+ [
10
+ "apiEndpoint",
11
+ "https://domain.example/graphql"
12
+ ],
13
+ [
14
+ "region",
15
+ "eu-west-1"
16
+ ]
17
+ ];
18
+
19
+ function get(out, key) {
20
+ return Stdlib_Option.getOr(out[key], null);
21
+ }
22
+
23
+ globalThis.describe("Util_ShellConfig.fields — viewModes", () => {
24
+ globalThis.test("unset ⇒ no viewModes key and the computed set is untouched", () => {
25
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, undefined);
26
+ globalThis.expect(Object.keys(out)).toEqual([
27
+ "apiEndpoint",
28
+ "region"
29
+ ]);
30
+ globalThis.expect(Stdlib_Option.isNone(out["viewModes"])).toBe(true);
31
+ });
32
+ globalThis.test("Map with defaults ⇒ the mode, and no mapStyle key", () => {
33
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, [{
34
+ TAG: "Map",
35
+ _0: {}
36
+ }], undefined);
37
+ globalThis.expect(get(out, "viewModes")).toEqual(["map"]);
38
+ globalThis.expect(Stdlib_Option.isNone(out["mapStyle"])).toBe(true);
39
+ });
40
+ globalThis.test("per-mode options flatten beside the mode", () => {
41
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, [
42
+ {
43
+ TAG: "Map",
44
+ _0: {
45
+ style: "https://tiles.example/style.json"
46
+ }
47
+ },
48
+ {
49
+ TAG: "Graph",
50
+ _0: {
51
+ layout: "dagre"
52
+ }
53
+ }
54
+ ], undefined);
55
+ globalThis.expect(get(out, "viewModes")).toEqual([
56
+ "map",
57
+ "graph"
58
+ ]);
59
+ globalThis.expect(get(out, "mapStyle")).toEqual("https://tiles.example/style.json");
60
+ globalThis.expect(get(out, "graphLayout")).toEqual("dagre");
61
+ });
62
+ });
63
+
64
+ globalThis.describe("Util_ShellConfig.fields — shellConfig passthrough", () => {
65
+ globalThis.test("shell-owned keys land verbatim, under the computed ones", () => {
66
+ let out = Util_ShellConfig$ReventlessAws.fields(computed, undefined, Object.fromEntries([
67
+ [
68
+ "platformName",
69
+ "Online Shop"
70
+ ],
71
+ [
72
+ "accessTiers",
73
+ ["public"]
74
+ ]
75
+ ]));
76
+ globalThis.expect(Object.keys(out)).toEqual([
77
+ "apiEndpoint",
78
+ "region",
79
+ "platformName",
80
+ "accessTiers"
81
+ ]);
82
+ globalThis.expect(get(out, "platformName")).toEqual("Online Shop");
83
+ });
84
+ globalThis.test("a key the deploy computes fails the deploy, naming it", () => {
85
+ let failure;
86
+ try {
87
+ Util_ShellConfig$ReventlessAws.fields(computed, undefined, Object.fromEntries([[
88
+ "apiEndpoint",
89
+ "https://wrong.example/graphql"
90
+ ]]));
91
+ failure = undefined;
92
+ } catch (raw_message) {
93
+ let message = Primitive_exceptions.internalToException(raw_message);
94
+ if (message.RE_EXN_ID === "Failure") {
95
+ failure = message._1;
96
+ } else {
97
+ throw message;
98
+ }
99
+ }
100
+ if (failure !== undefined) {
101
+ globalThis.expect(failure.includes("apiEndpoint")).toBe(true);
102
+ return;
103
+ } else {
104
+ return JestGlobals.fail("a shellConfig key colliding with a computed one must fail the deploy");
105
+ }
106
+ });
107
+ });
108
+
109
+ export {
110
+ computed,
111
+ get,
112
+ }
113
+ /* Not a pure module */