@reventlessdev/reventless-aws 3.0.0-alpha.258 → 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.
- package/CHANGELOG.md +20 -0
- package/package.json +8 -8
- package/src/Platform.res +79 -4
- package/src/Platform.res.mjs +24 -8
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Backend.res +77 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Backend.res.mjs +87 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +42 -36
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +24 -6
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +20 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +7 -0
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +3 -16
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +4 -19
- package/src/components/OutboundTranslationSlice_Builder.res +2 -2
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +3 -2
- package/src/plugin/runtime/PluginRuntime_Builder.res +31 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +13 -0
- package/src/util/Util_DcbMetrics.res +48 -0
- package/src/util/Util_DcbMetrics.res.mjs +37 -0
- package/src/util/Util_ShellConfig.res +72 -0
- package/src/util/Util_ShellConfig.res.mjs +60 -0
- package/tests/Geocoder_AwsLocation_OpsTest.res +78 -0
- package/tests/Geocoder_AwsLocation_OpsTest.res.mjs +71 -0
- package/tests/Util_DcbMetricsTest.res +35 -0
- package/tests/Util_DcbMetricsTest.res.mjs +29 -0
- package/tests/Util_ShellConfigTest.res +78 -0
- package/tests/Util_ShellConfigTest.res.mjs +113 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Location$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/Location.res.mjs";
|
|
5
6
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
6
7
|
import * as ClientLocation from "@aws-sdk/client-location";
|
|
7
8
|
|
|
@@ -42,7 +43,18 @@ async function handler(event) {
|
|
|
42
43
|
try {
|
|
43
44
|
let indexName = Stdlib_Option.getOr(getEnv("PLACE_INDEX_NAME"), "");
|
|
44
45
|
let q = Stdlib_Option.getOr(readQueryParam(event), "");
|
|
45
|
-
if (indexName === ""
|
|
46
|
+
if (indexName === "") {
|
|
47
|
+
console.error("Geocoder: PLACE_INDEX_NAME is unset");
|
|
48
|
+
return {
|
|
49
|
+
statusCode: 502,
|
|
50
|
+
headers: Object.fromEntries([[
|
|
51
|
+
"content-type",
|
|
52
|
+
"application/json"
|
|
53
|
+
]]),
|
|
54
|
+
body: "[]"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (q === "") {
|
|
46
58
|
return {
|
|
47
59
|
statusCode: 200,
|
|
48
60
|
headers: Object.fromEntries([[
|
|
@@ -52,8 +64,7 @@ async function handler(event) {
|
|
|
52
64
|
body: "[]"
|
|
53
65
|
};
|
|
54
66
|
}
|
|
55
|
-
let
|
|
56
|
-
let resp = await client.send(new ClientLocation.SearchPlaceIndexForTextCommand({
|
|
67
|
+
let resp = await Location$AwsSdk.SearchPlaceIndexForTextCommand.send(new ClientLocation.SearchPlaceIndexForTextCommand({
|
|
57
68
|
IndexName: indexName,
|
|
58
69
|
Text: q,
|
|
59
70
|
MaxResults: 5
|
|
@@ -73,6 +84,7 @@ async function handler(event) {
|
|
|
73
84
|
}
|
|
74
85
|
let lng = pt[0];
|
|
75
86
|
let lat = pt[1];
|
|
87
|
+
let rel = r.Relevance;
|
|
76
88
|
return Object.fromEntries([
|
|
77
89
|
[
|
|
78
90
|
"label",
|
|
@@ -86,7 +98,10 @@ async function handler(event) {
|
|
|
86
98
|
"lng",
|
|
87
99
|
lng
|
|
88
100
|
]
|
|
89
|
-
]
|
|
101
|
+
].concat(rel !== undefined ? [[
|
|
102
|
+
"relevance",
|
|
103
|
+
rel
|
|
104
|
+
]] : []));
|
|
90
105
|
});
|
|
91
106
|
return {
|
|
92
107
|
statusCode: 200,
|
|
@@ -100,7 +115,7 @@ async function handler(event) {
|
|
|
100
115
|
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
101
116
|
console.error("Geocoder: search failed", exn);
|
|
102
117
|
return {
|
|
103
|
-
statusCode:
|
|
118
|
+
statusCode: 502,
|
|
104
119
|
headers: Object.fromEntries([[
|
|
105
120
|
"content-type",
|
|
106
121
|
"application/json"
|
|
@@ -110,10 +125,13 @@ async function handler(event) {
|
|
|
110
125
|
}
|
|
111
126
|
}
|
|
112
127
|
|
|
128
|
+
let Search;
|
|
129
|
+
|
|
113
130
|
export {
|
|
131
|
+
Search,
|
|
114
132
|
getEnv,
|
|
115
133
|
jsonHeaders,
|
|
116
134
|
readQueryParam,
|
|
117
135
|
handler,
|
|
118
136
|
}
|
|
119
|
-
/*
|
|
137
|
+
/* Location-AwsSdk Not a pure module */
|
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
+
})
|