@reventlessdev/reventless-aws 3.0.0-alpha.264 → 3.0.0-alpha.266

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 (27) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +7 -7
  3. package/src/Platform.res +66 -45
  4. package/src/Platform.res.mjs +31 -35
  5. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +227 -0
  6. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +127 -0
  7. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver_Ops.res +55 -0
  8. package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver_Ops.res.mjs +48 -0
  9. package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +21 -3
  10. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +219 -27
  11. package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +121 -7
  12. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +118 -0
  13. package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +61 -0
  14. package/src/components/OutboundTranslationSlice_Builder.res +15 -0
  15. package/src/components/OutboundTranslationSlice_Builder.res.mjs +11 -1
  16. package/src/plugin/runtime/PluginRuntime_Builder.res +26 -0
  17. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +26 -0
  18. package/tests/AutomationSliceEntryPoint_OpsTest.res +44 -3
  19. package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +65 -1
  20. package/tests/Geocoder_AwsLocation_Resolver_OpsTest.res +53 -0
  21. package/tests/Geocoder_AwsLocation_Resolver_OpsTest.res.mjs +55 -0
  22. package/src/adapter/Geocoder/Geocoder_AwsLocation.res +0 -166
  23. package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +0 -105
  24. package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +0 -144
  25. package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +0 -137
  26. package/tests/Geocoder_AwsLocation_OpsTest.res +0 -78
  27. package/tests/Geocoder_AwsLocation_OpsTest.res.mjs +0 -71
@@ -153,6 +153,8 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandl
153
153
  sliceName: "S"
154
154
  }, callback, noopPublish, async () => {
155
155
  steps.push("sync");
156
+ }, async () => {
157
+ steps.push("restore");
156
158
  });
157
159
  let envelope = Object.fromEntries([
158
160
  [
@@ -177,6 +179,7 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandl
177
179
  ]);
178
180
  globalThis.expect(Stdlib_Option.map(receivedCtx.contents, c => c.sliceName)).toEqual("S");
179
181
  globalThis.expect(steps).toEqual([
182
+ "restore",
180
183
  "phase1",
181
184
  "sync",
182
185
  "phase2",
@@ -185,6 +188,64 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeAutomationJsonEventsHandl
185
188
  });
186
189
  });
187
190
 
191
+ globalThis.describe("AutomationSliceEntryPoint_Ops.runSweeps", () => {
192
+ let dummyRegistered_handler = (_event, _context) => Effect.succeed();
193
+ let dummyRegistered = {
194
+ handler: dummyRegistered_handler
195
+ };
196
+ globalThis.test("sweeps every slice", async () => {
197
+ let swept = [];
198
+ await AutomationSliceEntryPoint_Ops$ReventlessAws.runSweeps([
199
+ {
200
+ registered: dummyRegistered,
201
+ sweep: async () => {
202
+ swept.push("a");
203
+ },
204
+ comp: "A"
205
+ },
206
+ {
207
+ registered: dummyRegistered,
208
+ sweep: async () => {
209
+ swept.push("b");
210
+ },
211
+ comp: "B"
212
+ }
213
+ ]);
214
+ globalThis.expect(swept).toEqual([
215
+ "a",
216
+ "b"
217
+ ]);
218
+ });
219
+ globalThis.test("a throwing slice does not stop the others", async () => {
220
+ let swept = [];
221
+ await AutomationSliceEntryPoint_Ops$ReventlessAws.runSweeps([
222
+ {
223
+ registered: dummyRegistered,
224
+ sweep: async () => {
225
+ swept.push("a");
226
+ },
227
+ comp: "A"
228
+ },
229
+ {
230
+ registered: dummyRegistered,
231
+ sweep: async () => Stdlib_JsError.throwWithMessage("gateway down"),
232
+ comp: "Boom"
233
+ },
234
+ {
235
+ registered: dummyRegistered,
236
+ sweep: async () => {
237
+ swept.push("c");
238
+ },
239
+ comp: "C"
240
+ }
241
+ ]);
242
+ globalThis.expect(swept).toEqual([
243
+ "a",
244
+ "c"
245
+ ]);
246
+ });
247
+ });
248
+
188
249
  let outboundEventSchema = S.union([
189
250
  S.schema(s => ({
190
251
  TAG: "OrderPlaced",
@@ -204,7 +265,7 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler
204
265
  received.contents = events;
205
266
  steps.push("phase1");
206
267
  };
207
- let callback_phase2 = async _publish => {
268
+ let callback_phase2 = async (_publish, param) => {
208
269
  steps.push("phase2");
209
270
  };
210
271
  let callback = {
@@ -214,6 +275,8 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler
214
275
  };
215
276
  let handler = AutomationSliceEntryPoint_Ops$ReventlessAws.makeOutboundJsonEventsHandler(outboundEventSchema, callback, noopPublish, async () => {
216
277
  steps.push("sync");
278
+ }, async () => {
279
+ steps.push("restore");
217
280
  });
218
281
  let placed = Object.fromEntries([
219
282
  [
@@ -273,6 +336,7 @@ globalThis.describe("AutomationSliceEntryPoint_Ops.makeOutboundJsonEventsHandler
273
336
  ]
274
337
  ]);
275
338
  globalThis.expect(steps).toEqual([
339
+ "restore",
276
340
  "phase1",
277
341
  "sync",
278
342
  "phase2",
@@ -0,0 +1,53 @@
1
+ // Guards the geocode resolver handler's contract — the client door of the
2
+ // geocoding capability (D9 half 2), which replaced the Function URL. The Function
3
+ // URL served two callers through one `200`/`502` body-vs-status split; this handler
4
+ // serves only the browser, through GraphQL, so the contract is simpler:
5
+ //
6
+ // a value returned — an answer (a possibly-empty candidate list)
7
+ // a thrown error — no answer (the resolver's response mapper turns it into a
8
+ // GraphQL error, so the client degrades rather than reading
9
+ // an empty list as "no such address")
10
+ //
11
+ // The two arms below are the ones reachable without a live AWS Location call: an
12
+ // unset index throws (misconfiguration is not a verdict on the address), and an
13
+ // empty query returns `[]` (nothing was asked). The success path and the thrown
14
+ // service-failure path both need the SDK and stay unasserted here — see the plan's
15
+ // 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_Resolver_Ops.handler contract", () => {
23
+ test("an unset PLACE_INDEX_NAME throws — a misconfiguration, not a verdict", async () => {
24
+ clearIndex()
25
+ let threw = switch await Geocoder_AwsLocation_Resolver_Ops.handler({
26
+ arguments: {text: "10 Downing Street"},
27
+ }) {
28
+ | _ => false
29
+ | exception _ => true
30
+ }
31
+ // Returning `[]` here would tell the browser this address does not exist, when
32
+ // in fact the deployment has no geocoder — the client must see an error and be
33
+ // able to distinguish the two, which is the whole point of throwing.
34
+ expect(threw)->toBe(true)
35
+ clearIndex()
36
+ })
37
+
38
+ test("an empty query is an answer — nothing was asked, so `[]`", async () => {
39
+ setIndex("some-index")
40
+ let results = await Geocoder_AwsLocation_Resolver_Ops.handler({arguments: {text: ""}})
41
+ // The empty text short-circuits in the shared backend before any SDK call, so
42
+ // this arm is reachable in a unit test and returns a true, final empty answer.
43
+ expect(results->Array.length)->toBe(0)
44
+ clearIndex()
45
+ })
46
+
47
+ test("a missing text argument is treated as an empty query", async () => {
48
+ setIndex("some-index")
49
+ let results = await Geocoder_AwsLocation_Resolver_Ops.handler({arguments: {}})
50
+ expect(results->Array.length)->toBe(0)
51
+ clearIndex()
52
+ })
53
+ })
@@ -0,0 +1,55 @@
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_Resolver_Ops$ReventlessAws from "../src/adapter/Geocoder/Geocoder_AwsLocation_Resolver_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_Resolver_Ops.handler contract", () => {
15
+ globalThis.test("an unset PLACE_INDEX_NAME throws — a misconfiguration, not a verdict", async () => {
16
+ Stdlib_Dict.$$delete(process.env, "PLACE_INDEX_NAME");
17
+ let threw;
18
+ try {
19
+ await Geocoder_AwsLocation_Resolver_Ops$ReventlessAws.handler({
20
+ arguments: {
21
+ text: "10 Downing Street"
22
+ }
23
+ });
24
+ threw = false;
25
+ } catch (exn) {
26
+ threw = true;
27
+ }
28
+ globalThis.expect(threw).toBe(true);
29
+ return Stdlib_Dict.$$delete(process.env, "PLACE_INDEX_NAME");
30
+ });
31
+ globalThis.test("an empty query is an answer — nothing was asked, so `[]`", async () => {
32
+ setIndex("some-index");
33
+ let results = await Geocoder_AwsLocation_Resolver_Ops$ReventlessAws.handler({
34
+ arguments: {
35
+ text: ""
36
+ }
37
+ });
38
+ globalThis.expect(results.length).toBe(0);
39
+ return Stdlib_Dict.$$delete(process.env, "PLACE_INDEX_NAME");
40
+ });
41
+ globalThis.test("a missing text argument is treated as an empty query", async () => {
42
+ setIndex("some-index");
43
+ let results = await Geocoder_AwsLocation_Resolver_Ops$ReventlessAws.handler({
44
+ arguments: {}
45
+ });
46
+ globalThis.expect(results.length).toBe(0);
47
+ return Stdlib_Dict.$$delete(process.env, "PLACE_INDEX_NAME");
48
+ });
49
+ });
50
+
51
+ export {
52
+ setIndex,
53
+ clearIndex,
54
+ }
55
+ /* Not a pure module */
@@ -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
- }