@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.
@@ -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
- })