@reventlessdev/reventless-aws 3.0.0-alpha.265 → 3.0.0-alpha.267
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 +14 -0
- package/package.json +6 -6
- package/src/Platform.res +49 -53
- package/src/Platform.res.mjs +17 -36
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +36 -0
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +14 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +37 -6
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +19 -6
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +227 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +127 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver_Ops.res +55 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver_Ops.res.mjs +48 -0
- package/tests/Geocoder_AwsLocation_Resolver_OpsTest.res +53 -0
- package/tests/Geocoder_AwsLocation_Resolver_OpsTest.res.mjs +55 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res +0 -166
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +0 -105
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +0 -144
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +0 -137
- package/tests/Geocoder_AwsLocation_OpsTest.res +0 -78
- package/tests/Geocoder_AwsLocation_OpsTest.res.mjs +0 -71
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
// AWS Location Service geocoder behind a public Lambda Function URL.
|
|
2
|
-
//
|
|
3
|
-
// Deploy-time only: `make` provisions a compiled-EntryPoint Lambda (a plain
|
|
4
|
-
// `Lambda.Function` whose code archive re-exports `handler` from the compiled,
|
|
5
|
-
// type-checked runtime module `Geocoder_AwsLocation_Ops` and ships the shared
|
|
6
|
-
// ESM resolve-hook loader), an IAM execution role scoped to CloudWatch Logs +
|
|
7
|
-
// `geo:SearchPlaceIndexForText` on the target place index, and a Function URL
|
|
8
|
-
// (no auth) so a browser can geocode directly.
|
|
9
|
-
//
|
|
10
|
-
// Why an EntryPoint and not a Pulumi `CallbackFunction`: a serialized closure
|
|
11
|
-
// bakes the deploy machine's version-specific AWS SDK internals into the archive
|
|
12
|
-
// but then resolves `@smithy/*`/`@aws-sdk/*` transitives from independently-
|
|
13
|
-
// versioned layer/runtime sources that can disagree at cold start (the exact
|
|
14
|
-
// skew that crashed the upload presign service). Shipping the compiled `_Ops`
|
|
15
|
-
// module with bare `@aws-sdk/*` imports, resolved through the resolve-hook, loads
|
|
16
|
-
// one internally consistent SDK. The runtime logic lives in
|
|
17
|
-
// [Geocoder_AwsLocation_Ops.res].
|
|
18
|
-
|
|
19
|
-
open PulumiAws
|
|
20
|
-
|
|
21
|
-
type serviceOutputs = {
|
|
22
|
-
url: Pulumi.Output.t<string>,
|
|
23
|
-
resources: array<Pulumi.Output.t<string>>,
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
let make = (
|
|
27
|
-
~placeIndexName: Pulumi.Input.t<string>,
|
|
28
|
-
~corsOrigins: array<string>=["*"],
|
|
29
|
-
~opts=?,
|
|
30
|
-
): serviceOutputs => {
|
|
31
|
-
let serviceName = "GeocoderService"
|
|
32
|
-
let opts =
|
|
33
|
-
opts->Option.map(ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions)
|
|
34
|
-
|
|
35
|
-
let lambdaRole = IAM.Role.makeWithDefaultPolicy(
|
|
36
|
-
~name=serviceName,
|
|
37
|
-
~servicePrincipal=AWS.Lambda.principal->Pulumi.Output.make,
|
|
38
|
-
~tags=AWS.Tags.make(
|
|
39
|
-
~name=serviceName,
|
|
40
|
-
~kind=ReventlessCore.ComponentType.Platform,
|
|
41
|
-
~role=Identity,
|
|
42
|
-
~scope=Platform,
|
|
43
|
-
),
|
|
44
|
-
~opts?,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
// CloudWatch Logs (so failures are observable) plus least-privilege
|
|
48
|
-
// `geo:SearchPlaceIndexForText` on the one place index.
|
|
49
|
-
let _policy =
|
|
50
|
-
placeIndexName
|
|
51
|
-
->Pulumi.Output.fromInput
|
|
52
|
-
->Pulumi.Output.apply(idx => {
|
|
53
|
-
let arn = `arn:aws:geo:*:*:place-index/${idx}`
|
|
54
|
-
let _ = IAM.RolePolicy.make(
|
|
55
|
-
~name=`${serviceName}Policy`,
|
|
56
|
-
~args={
|
|
57
|
-
policy: PolicyDocument.make(
|
|
58
|
-
~id=`${serviceName}Policy`,
|
|
59
|
-
~statements=[
|
|
60
|
-
{
|
|
61
|
-
sid: "AllowLambdaLogging",
|
|
62
|
-
effect: Allow,
|
|
63
|
-
actions: Actions(["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]),
|
|
64
|
-
resources: Resource("arn:aws:logs:*:*:*"),
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
sid: "AllowGeocode",
|
|
68
|
-
effect: Allow,
|
|
69
|
-
actions: Action("geo:SearchPlaceIndexForText"),
|
|
70
|
-
resources: Resource(arn),
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
)
|
|
74
|
-
->PolicyDocument.toJsonString
|
|
75
|
-
->Pulumi.Input.make,
|
|
76
|
-
role: lambdaRole.id->Pulumi.Output.asInput,
|
|
77
|
-
},
|
|
78
|
-
~opts?,
|
|
79
|
-
)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
// Bundle reventless-aws (the compiled `_Ops` handler lives inside it) and
|
|
83
|
-
// re-export its `handler`; buildCodeArchive also ships the ESM resolve-hook.
|
|
84
|
-
let packageDirs = Dict.fromArray([
|
|
85
|
-
(
|
|
86
|
-
"@reventlessdev/reventless-aws",
|
|
87
|
-
Util_Bundle.resolvePackageRoot("@reventlessdev/reventless-aws"),
|
|
88
|
-
),
|
|
89
|
-
])
|
|
90
|
-
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
91
|
-
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs",
|
|
92
|
-
~packageDirs,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
let layers =
|
|
96
|
-
Lambda.reventlessLayerArn
|
|
97
|
-
->Option.map(arn => [arn->Pulumi.Input.make])
|
|
98
|
-
->Option.getOr([])
|
|
99
|
-
->Pulumi.Input.make
|
|
100
|
-
|
|
101
|
-
let lambda = Lambda.Function.make(
|
|
102
|
-
~name=serviceName,
|
|
103
|
-
~args={
|
|
104
|
-
handler: "index.handler"->Pulumi.Input.make,
|
|
105
|
-
runtime: "nodejs22.x"->Pulumi.Input.make,
|
|
106
|
-
code: code->Pulumi.Input.make,
|
|
107
|
-
sourceCodeHash: sourceCodeHash->Pulumi.Input.make,
|
|
108
|
-
role: lambdaRole.arn->Pulumi.Output.asInput,
|
|
109
|
-
memorySize: 256->Pulumi.Input.make,
|
|
110
|
-
timeout: 30->Pulumi.Input.make,
|
|
111
|
-
layers,
|
|
112
|
-
tags: AWS.Tags.make(
|
|
113
|
-
~name=serviceName,
|
|
114
|
-
~kind=ReventlessCore.ComponentType.Platform,
|
|
115
|
-
~role=Runtime,
|
|
116
|
-
~scope=Platform,
|
|
117
|
-
),
|
|
118
|
-
environment: (
|
|
119
|
-
{
|
|
120
|
-
Lambda.Function.variables: Dict.fromArray([
|
|
121
|
-
("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
|
|
122
|
-
("PLACE_INDEX_NAME", placeIndexName),
|
|
123
|
-
("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
|
|
124
|
-
("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
|
|
125
|
-
Util_LambdaLogging.logLevelEntry(),
|
|
126
|
-
]),
|
|
127
|
-
}: Lambda.Function.functionEnvironment
|
|
128
|
-
)->Pulumi.Input.make,
|
|
129
|
-
},
|
|
130
|
-
~opts?,
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
Util_LambdaLogging.makeManagedLogGroup(
|
|
134
|
-
~name=serviceName,
|
|
135
|
-
~lambdaName=lambda.name,
|
|
136
|
-
~tags=AWS.Tags.make(
|
|
137
|
-
~name=serviceName ++ "LogGroup",
|
|
138
|
-
~kind=ReventlessCore.ComponentType.Platform,
|
|
139
|
-
~role=Logs,
|
|
140
|
-
~scope=Platform,
|
|
141
|
-
),
|
|
142
|
-
~opts?,
|
|
143
|
-
(),
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
let functionUrl = FunctionUrl.make(
|
|
147
|
-
~name=`${serviceName}Url`,
|
|
148
|
-
~args={
|
|
149
|
-
authorizationType: FunctionUrl.None,
|
|
150
|
-
functionName: lambda.name->Pulumi.Output.asInput,
|
|
151
|
-
cors: (
|
|
152
|
-
{
|
|
153
|
-
allowMethods: ["GET"]->Array.map(Pulumi.Input.make)->Pulumi.Input.make,
|
|
154
|
-
allowOrigins: corsOrigins->Array.map(Pulumi.Input.make)->Pulumi.Input.make,
|
|
155
|
-
allowHeaders: ["*"]->Array.map(Pulumi.Input.make)->Pulumi.Input.make,
|
|
156
|
-
}: FunctionUrl.cors
|
|
157
|
-
)->Pulumi.Input.make,
|
|
158
|
-
},
|
|
159
|
-
~opts?,
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
{
|
|
163
|
-
url: functionUrl.functionUrl,
|
|
164
|
-
resources: [lambda.arn, functionUrl.functionArn],
|
|
165
|
-
}
|
|
166
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
-
|
|
3
|
-
import * as Aws from "@pulumi/aws";
|
|
4
|
-
import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
|
|
5
|
-
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
-
import * as Pulumi from "@pulumi/pulumi";
|
|
7
|
-
import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
|
|
8
|
-
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
9
|
-
import * as AWS$ReventlessAws from "../AWS.res.mjs";
|
|
10
|
-
import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
|
|
11
|
-
import * as PolicyDocument$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/PolicyDocument.res.mjs";
|
|
12
|
-
import * as Util_Bundle$ReventlessAws from "../../util/Util_Bundle.res.mjs";
|
|
13
|
-
import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
|
|
14
|
-
import * as Util_LambdaLogging$ReventlessAws from "../../util/Util_LambdaLogging.res.mjs";
|
|
15
|
-
|
|
16
|
-
function make(placeIndexName, corsOriginsOpt, opts) {
|
|
17
|
-
let corsOrigins = corsOriginsOpt !== undefined ? corsOriginsOpt : ["*"];
|
|
18
|
-
let serviceName = "GeocoderService";
|
|
19
|
-
let opts$1 = Stdlib_Option.map(opts, Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions);
|
|
20
|
-
let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(serviceName, Pulumi.output(AWS$ReventlessAws.Lambda.principal), AWS_Tags$ReventlessAws.make(serviceName, "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
|
|
21
|
-
placeIndexName.apply(idx => {
|
|
22
|
-
let arn = `arn:aws:geo:*:*:place-index/` + idx;
|
|
23
|
-
new (Aws.iam.RolePolicy)(serviceName + `Policy`, {
|
|
24
|
-
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, serviceName + `Policy`, [
|
|
25
|
-
{
|
|
26
|
-
Sid: "AllowLambdaLogging",
|
|
27
|
-
Effect: "Allow",
|
|
28
|
-
Action: [
|
|
29
|
-
"logs:CreateLogGroup",
|
|
30
|
-
"logs:CreateLogStream",
|
|
31
|
-
"logs:PutLogEvents"
|
|
32
|
-
],
|
|
33
|
-
Resource: "arn:aws:logs:*:*:*"
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
Sid: "AllowGeocode",
|
|
37
|
-
Effect: "Allow",
|
|
38
|
-
Action: "geo:SearchPlaceIndexForText",
|
|
39
|
-
Resource: arn
|
|
40
|
-
}
|
|
41
|
-
])),
|
|
42
|
-
role: lambdaRole.id
|
|
43
|
-
}, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
44
|
-
});
|
|
45
|
-
let packageDirs = Object.fromEntries([[
|
|
46
|
-
"@reventlessdev/reventless-aws",
|
|
47
|
-
Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
|
|
48
|
-
]]);
|
|
49
|
-
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs", packageDirs, undefined);
|
|
50
|
-
let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
|
|
51
|
-
let lambda = new (Aws.lambda.Function)(serviceName, {
|
|
52
|
-
handler: "index.handler",
|
|
53
|
-
runtime: "nodejs22.x",
|
|
54
|
-
code: match.code,
|
|
55
|
-
role: lambdaRole.arn,
|
|
56
|
-
memorySize: 256,
|
|
57
|
-
timeout: 30,
|
|
58
|
-
layers: layers,
|
|
59
|
-
tags: AWS_Tags$ReventlessAws.make(serviceName, "Platform", "Runtime", "Platform", undefined, undefined, undefined, undefined),
|
|
60
|
-
environment: {
|
|
61
|
-
variables: Object.fromEntries([
|
|
62
|
-
[
|
|
63
|
-
"Environment",
|
|
64
|
-
Pulumi.getStack()
|
|
65
|
-
],
|
|
66
|
-
[
|
|
67
|
-
"PLACE_INDEX_NAME",
|
|
68
|
-
placeIndexName
|
|
69
|
-
],
|
|
70
|
-
[
|
|
71
|
-
"NODE_OPTIONS",
|
|
72
|
-
Util_Bundle$ReventlessAws.esmLoaderNodeOptions
|
|
73
|
-
],
|
|
74
|
-
[
|
|
75
|
-
"ESM_FALLBACK_DIRS",
|
|
76
|
-
Util_Bundle$ReventlessAws.esmFallbackDirs
|
|
77
|
-
],
|
|
78
|
-
Util_LambdaLogging$ReventlessAws.logLevelEntry()
|
|
79
|
-
])
|
|
80
|
-
},
|
|
81
|
-
sourceCodeHash: match.sourceCodeHash
|
|
82
|
-
}, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
83
|
-
Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(serviceName, lambda.name, AWS_Tags$ReventlessAws.make(serviceName + "LogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
|
|
84
|
-
let functionUrl = new (Aws.lambda.FunctionUrl)(serviceName + `Url`, {
|
|
85
|
-
authorizationType: "NONE",
|
|
86
|
-
functionName: lambda.name,
|
|
87
|
-
cors: {
|
|
88
|
-
allowHeaders: ["*"].map(prim => prim),
|
|
89
|
-
allowMethods: ["GET"].map(prim => prim),
|
|
90
|
-
allowOrigins: corsOrigins.map(prim => prim)
|
|
91
|
-
}
|
|
92
|
-
}, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
|
|
93
|
-
return {
|
|
94
|
-
url: functionUrl.functionUrl,
|
|
95
|
-
resources: [
|
|
96
|
-
lambda.arn,
|
|
97
|
-
functionUrl.functionArn
|
|
98
|
-
]
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export {
|
|
103
|
-
make,
|
|
104
|
-
}
|
|
105
|
-
/* @pulumi/aws Not a pure module */
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
// Runtime handler for the AWS Location geocoder — compiled, type-checked, and
|
|
2
|
-
// Pulumi-free so it can be shipped as an EntryPoint module (`Geocoder_AwsLocation`
|
|
3
|
-
// bundles it and re-exports `handler` from the code archive). Keeping it out of
|
|
4
|
-
// the deploy-time module avoids both the serialized-closure SDK skew and a
|
|
5
|
-
// deploy-time Pulumi import leaking into the Lambda's cold-start graph.
|
|
6
|
-
//
|
|
7
|
-
// Reads a `q` query-string param from the Function URL event, calls
|
|
8
|
-
// SearchPlaceIndexForText against `PLACE_INDEX_NAME`, and returns
|
|
9
|
-
// `[{label, lat, lng}]` as JSON. Any failure degrades to an empty array so the
|
|
10
|
-
// caller never sees a hard error.
|
|
11
|
-
|
|
12
|
-
// AWS Location lives in the bindings package, shared with the backend geocoder
|
|
13
|
-
// adapter — one place owns the `[lng, lat]` order and the optional `Relevance`.
|
|
14
|
-
module Search = AwsSdk.Location.SearchPlaceIndexForTextCommand
|
|
15
|
-
|
|
16
|
-
@val external decodeURIComponent: string => string = "decodeURIComponent"
|
|
17
|
-
|
|
18
|
-
// ── Node bindings (replacing the former `%raw` env helper with a typed one) ──
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// Read an env var, mapping "" / unset to None.
|
|
22
|
-
let getEnv = (k: string): option<string> =>
|
|
23
|
-
switch NodeProcess.env->Dict.get(k) {
|
|
24
|
-
| Some("") | None => None
|
|
25
|
-
| Some(v) => Some(v)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ── Function URL event / response shapes (payload format 2.0) ────────────────
|
|
29
|
-
|
|
30
|
-
type functionUrlEvent = {
|
|
31
|
-
rawQueryString?: string,
|
|
32
|
-
queryStringParameters?: dict<string>,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
type response = {
|
|
36
|
-
statusCode: int,
|
|
37
|
-
headers?: dict<string>,
|
|
38
|
-
body: string,
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// CORS belongs to the Function URL's own `cors` configuration and to nothing
|
|
42
|
-
// else. AWS injects the allow-origin header itself whenever the request carries
|
|
43
|
-
// an `Origin`, so a handler that also sets one sends the header *twice* — and a
|
|
44
|
-
// browser rejects `Access-Control-Allow-Origin: *, *` outright, failing every
|
|
45
|
-
// cross-origin call while leaving `curl` (which sends no `Origin`, so AWS adds
|
|
46
|
-
// nothing) working perfectly.
|
|
47
|
-
//
|
|
48
|
-
// It is also the only way `~corsOrigins` can mean anything: a hardcoded `*`
|
|
49
|
-
// here would keep answering `*` for a deployment that narrowed the allow-list.
|
|
50
|
-
let jsonHeaders = () => Dict.fromArray([("content-type", "application/json")])
|
|
51
|
-
|
|
52
|
-
// Pull `q` from the parsed query-string params, falling back to the raw string.
|
|
53
|
-
let readQueryParam = (event: functionUrlEvent): option<string> =>
|
|
54
|
-
switch event.queryStringParameters->Option.flatMap(p => p->Dict.get("q")) {
|
|
55
|
-
| Some(q) => Some(q)
|
|
56
|
-
| None =>
|
|
57
|
-
event.rawQueryString->Option.flatMap(raw =>
|
|
58
|
-
raw
|
|
59
|
-
->String.split("&")
|
|
60
|
-
->Array.findMap(pair =>
|
|
61
|
-
switch pair->String.split("=") {
|
|
62
|
-
| [k, v] if k == "q" => Some(v->decodeURIComponent)
|
|
63
|
-
| _ => None
|
|
64
|
-
}
|
|
65
|
-
)
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// ── Runtime handler ─────────────────────────────────────────────────────────
|
|
70
|
-
|
|
71
|
-
let handler = async (event: functionUrlEvent): response => {
|
|
72
|
-
try {
|
|
73
|
-
let indexName = getEnv("PLACE_INDEX_NAME")->Option.getOr("")
|
|
74
|
-
let q = readQueryParam(event)->Option.getOr("")
|
|
75
|
-
if indexName == "" {
|
|
76
|
-
// A handler with no place index cannot answer anything, so this is the
|
|
77
|
-
// service being misconfigured — not a verdict on the address. It has to
|
|
78
|
-
// read as `502` for the same reason the catch below does: a `200 []` here
|
|
79
|
-
// tells an unattended caller "no such address", and it would then write
|
|
80
|
-
// that verdict, unretried, for every address it is handed while the
|
|
81
|
-
// deployment is broken. The browser is unaffected — it reads the body.
|
|
82
|
-
Console.error("Geocoder: PLACE_INDEX_NAME is unset")
|
|
83
|
-
{statusCode: 502, headers: jsonHeaders(), body: "[]"}
|
|
84
|
-
} else if q == "" {
|
|
85
|
-
// `200`, unlike the arm above: nothing was asked, so "no results" is a
|
|
86
|
-
// true and final answer rather than a failure to produce one. A search box
|
|
87
|
-
// sends this on every cleared input.
|
|
88
|
-
{statusCode: 200, headers: jsonHeaders(), body: "[]"}
|
|
89
|
-
} else {
|
|
90
|
-
let resp = await Search.send(Search.make({indexName, text: q, maxResults: 5}))
|
|
91
|
-
let results =
|
|
92
|
-
resp.results
|
|
93
|
-
->Option.getOr([])
|
|
94
|
-
->Array.filterMap(r =>
|
|
95
|
-
switch r.place {
|
|
96
|
-
| Some(place) =>
|
|
97
|
-
let label = place.label->Option.getOr("")
|
|
98
|
-
switch place.geometry->Option.flatMap(g => g.point) {
|
|
99
|
-
| Some(pt) if pt->Array.length >= 2 =>
|
|
100
|
-
let lng = pt->Array.getUnsafe(0)
|
|
101
|
-
let lat = pt->Array.getUnsafe(1)
|
|
102
|
-
Some(
|
|
103
|
-
Dict.fromArray(
|
|
104
|
-
Array.concat(
|
|
105
|
-
[
|
|
106
|
-
("label", JSON.Encode.string(label)),
|
|
107
|
-
("lat", JSON.Encode.float(lat)),
|
|
108
|
-
("lng", JSON.Encode.float(lng)),
|
|
109
|
-
],
|
|
110
|
-
// Additive: the browser client reads the three fields above
|
|
111
|
-
// and ignores this one. An unattended caller needs it to
|
|
112
|
-
// apply `Geocoding.confidentMatch`, which is what keeps a
|
|
113
|
-
// vague match from becoming a confident pin.
|
|
114
|
-
switch r.relevance {
|
|
115
|
-
| Some(rel) => [("relevance", JSON.Encode.float(rel))]
|
|
116
|
-
| None => []
|
|
117
|
-
},
|
|
118
|
-
),
|
|
119
|
-
)->JSON.Encode.object,
|
|
120
|
-
)
|
|
121
|
-
| _ => None
|
|
122
|
-
}
|
|
123
|
-
| None => None
|
|
124
|
-
}
|
|
125
|
-
)
|
|
126
|
-
{
|
|
127
|
-
statusCode: 200,
|
|
128
|
-
headers: jsonHeaders(),
|
|
129
|
-
body: results->JSON.Encode.array->JSON.stringify,
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
} catch {
|
|
133
|
-
| exn =>
|
|
134
|
-
Console.error2("Geocoder: search failed", exn)
|
|
135
|
-
// 502, not 200 — and still `[]`, which is the point. A browser search box
|
|
136
|
-
// reads the body and degrades to "no results" whether or not it checks the
|
|
137
|
-
// status, so nothing on that side changes. An unattended caller reads the
|
|
138
|
-
// status and can tell "the service is down" from "there is no such address",
|
|
139
|
-
// which is the difference between retrying and writing a verdict into an
|
|
140
|
-
// event log. One contract serves both because they disagree only about which
|
|
141
|
-
// half of the response they read.
|
|
142
|
-
{statusCode: 502, headers: jsonHeaders(), body: "[]"}
|
|
143
|
-
}
|
|
144
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
-
|
|
3
|
-
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
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";
|
|
6
|
-
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
7
|
-
import * as ClientLocation from "@aws-sdk/client-location";
|
|
8
|
-
|
|
9
|
-
function getEnv(k) {
|
|
10
|
-
let v = process.env[k];
|
|
11
|
-
if (v !== undefined && v !== "") {
|
|
12
|
-
return v;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function jsonHeaders() {
|
|
17
|
-
return Object.fromEntries([[
|
|
18
|
-
"content-type",
|
|
19
|
-
"application/json"
|
|
20
|
-
]]);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function readQueryParam(event) {
|
|
24
|
-
let q = Stdlib_Option.flatMap(event.queryStringParameters, p => p["q"]);
|
|
25
|
-
if (q !== undefined) {
|
|
26
|
-
return q;
|
|
27
|
-
} else {
|
|
28
|
-
return Stdlib_Option.flatMap(event.rawQueryString, raw => Stdlib_Array.findMap(raw.split("&"), pair => {
|
|
29
|
-
let match = pair.split("=");
|
|
30
|
-
if (match.length !== 2) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
let k = match[0];
|
|
34
|
-
let v = match[1];
|
|
35
|
-
if (k === "q") {
|
|
36
|
-
return decodeURIComponent(v);
|
|
37
|
-
}
|
|
38
|
-
}));
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async function handler(event) {
|
|
43
|
-
try {
|
|
44
|
-
let indexName = Stdlib_Option.getOr(getEnv("PLACE_INDEX_NAME"), "");
|
|
45
|
-
let q = Stdlib_Option.getOr(readQueryParam(event), "");
|
|
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 === "") {
|
|
58
|
-
return {
|
|
59
|
-
statusCode: 200,
|
|
60
|
-
headers: Object.fromEntries([[
|
|
61
|
-
"content-type",
|
|
62
|
-
"application/json"
|
|
63
|
-
]]),
|
|
64
|
-
body: "[]"
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
let resp = await Location$AwsSdk.SearchPlaceIndexForTextCommand.send(new ClientLocation.SearchPlaceIndexForTextCommand({
|
|
68
|
-
IndexName: indexName,
|
|
69
|
-
Text: q,
|
|
70
|
-
MaxResults: 5
|
|
71
|
-
}));
|
|
72
|
-
let results = Stdlib_Array.filterMap(Stdlib_Option.getOr(resp.Results, []), r => {
|
|
73
|
-
let place = r.Place;
|
|
74
|
-
if (place === undefined) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
let label = Stdlib_Option.getOr(place.Label, "");
|
|
78
|
-
let pt = Stdlib_Option.flatMap(place.Geometry, g => g.Point);
|
|
79
|
-
if (pt === undefined) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
if (pt.length < 2) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
let lng = pt[0];
|
|
86
|
-
let lat = pt[1];
|
|
87
|
-
let rel = r.Relevance;
|
|
88
|
-
return Object.fromEntries([
|
|
89
|
-
[
|
|
90
|
-
"label",
|
|
91
|
-
label
|
|
92
|
-
],
|
|
93
|
-
[
|
|
94
|
-
"lat",
|
|
95
|
-
lat
|
|
96
|
-
],
|
|
97
|
-
[
|
|
98
|
-
"lng",
|
|
99
|
-
lng
|
|
100
|
-
]
|
|
101
|
-
].concat(rel !== undefined ? [[
|
|
102
|
-
"relevance",
|
|
103
|
-
rel
|
|
104
|
-
]] : []));
|
|
105
|
-
});
|
|
106
|
-
return {
|
|
107
|
-
statusCode: 200,
|
|
108
|
-
headers: Object.fromEntries([[
|
|
109
|
-
"content-type",
|
|
110
|
-
"application/json"
|
|
111
|
-
]]),
|
|
112
|
-
body: JSON.stringify(results)
|
|
113
|
-
};
|
|
114
|
-
} catch (raw_exn) {
|
|
115
|
-
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
116
|
-
console.error("Geocoder: search failed", exn);
|
|
117
|
-
return {
|
|
118
|
-
statusCode: 502,
|
|
119
|
-
headers: Object.fromEntries([[
|
|
120
|
-
"content-type",
|
|
121
|
-
"application/json"
|
|
122
|
-
]]),
|
|
123
|
-
body: "[]"
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
let Search;
|
|
129
|
-
|
|
130
|
-
export {
|
|
131
|
-
Search,
|
|
132
|
-
getEnv,
|
|
133
|
-
jsonHeaders,
|
|
134
|
-
readQueryParam,
|
|
135
|
-
handler,
|
|
136
|
-
}
|
|
137
|
-
/* Location-AwsSdk Not a pure module */
|
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
})
|