@reventlessdev/reventless-aws 3.0.0-alpha.229 → 3.0.0-alpha.231

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 (33) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +5 -5
  3. package/src/adapter/Geocoder/Geocoder_AwsLocation.res +60 -141
  4. package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +52 -118
  5. package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +137 -0
  6. package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +124 -0
  7. package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +51 -6
  8. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +60 -0
  9. package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +29 -1
  10. package/src/adapter/Runtime/HeartbeatEntryPoint.res +44 -0
  11. package/src/adapter/Runtime/HeartbeatEntryPoint.res.mjs +47 -0
  12. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res +234 -0
  13. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res.mjs +226 -0
  14. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +49 -4
  15. package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +22 -4
  16. package/src/adapter/Upload/Upload_Presign_S3.res +65 -164
  17. package/src/adapter/Upload/Upload_Presign_S3.res.mjs +56 -119
  18. package/src/adapter/Upload/Upload_Presign_S3_Ops.res +157 -0
  19. package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +120 -0
  20. package/src/components/InboundTranslationSlice_Builder.res +27 -1
  21. package/src/components/InboundTranslationSlice_Builder.res.mjs +15 -2
  22. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +1 -1
  23. package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
  24. package/src/plugin/runtime/PluginRuntime_Builder.res +59 -1
  25. package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +49 -2
  26. package/tests/DcbInboundTranslationRoutingTest.res +73 -0
  27. package/tests/DcbInboundTranslationRoutingTest.res.mjs +71 -0
  28. package/tests/EpInboundTestSlice.res +19 -0
  29. package/tests/EpInboundTestSlice.res.mjs +31 -0
  30. package/tests/EpInboundTestSliceTranslation.res +13 -0
  31. package/tests/EpInboundTestSliceTranslation.res.mjs +33 -0
  32. package/src/adapter/Runtime/HeartbeatEntryPoint.mjs +0 -38
  33. package/src/adapter/Runtime/PluginExtensionPointEntryPoint.mjs +0 -127
package/CHANGELOG.md CHANGED
@@ -3,6 +3,21 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.231 (2026-07-27)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** route InboundTranslationSlice mutations in the deployed DCB Lambda ([feb129c](https://github.com/ReventlessDev/reventless-core/commit/feb129caf291395e7d35bb953030a1e3efa06e1a))
11
+
12
+
13
+ # 3.0.0-alpha.230 (2026-07-27)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **aws:** convert AWS Location geocoder to a compiled EntryPoint Lambda ([6bf9b7c](https://github.com/ReventlessDev/reventless-core/commit/6bf9b7c6b230857d8dda730fa08c3d0090081c96))
18
+ * **aws:** convert upload presign to a compiled EntryPoint Lambda ([39134f2](https://github.com/ReventlessDev/reventless-core/commit/39134f2f8e8236f043aae6ef410fa719624e625e))
19
+
20
+
6
21
  # 3.0.0-alpha.229 (2026-07-26)
7
22
 
8
23
  **Note:** Version bump only for package @reventlessdev/reventless-aws
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.229",
3
+ "version": "3.0.0-alpha.231",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -13,15 +13,15 @@
13
13
  "uuid": "^13.0.0",
14
14
  "@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.24",
15
15
  "@reventlessdev/rescript-effect": "0.1.0-alpha.31",
16
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
16
17
  "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.60",
17
18
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
18
- "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
19
19
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
20
- "@reventlessdev/reventless-core": "3.0.0-alpha.182",
21
- "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
22
20
  "@reventlessdev/reventless-infra": "3.0.0-alpha.105",
21
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
22
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.81",
23
23
  "@reventlessdev/reventless-postgres": "3.0.0-alpha.46",
24
- "@reventlessdev/reventless-spec": "3.0.0-alpha.81"
24
+ "@reventlessdev/reventless-core": "3.0.0-alpha.182"
25
25
  },
26
26
  "devDependencies": {
27
27
  "rescript": "12.3.0",
@@ -1,138 +1,23 @@
1
1
  // AWS Location Service geocoder behind a public Lambda Function URL.
2
2
  //
3
- // Deploy-time: `make` provisions a CallbackFunction (its handler closure is
4
- // serialized by Pulumi), an IAM execution role scoped to
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 +
5
7
  // `geo:SearchPlaceIndexForText` on the target place index, and a Function URL
6
8
  // (no auth) so a browser can geocode directly.
7
9
  //
8
- // Runtime: `handleGeocode` reads a `q` query-string param from the Function URL
9
- // event, calls SearchPlaceIndexForText against the place index named in
10
- // `PLACE_INDEX_NAME`, and returns `[{label, lat, lng}]` as JSON. Any failure
11
- // degrades to an empty array so the caller never sees a hard error.
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].
12
18
 
13
19
  open PulumiAws
14
20
 
15
- // ── AWS Location SDK bindings (AWS SDK v3, client.send(new Command(...))) ────
16
-
17
- type locationClient
18
-
19
- type searchPlaceIndexForTextInput = {
20
- @as("IndexName") indexName: string,
21
- @as("Text") text: string,
22
- @as("MaxResults") maxResults?: int,
23
- }
24
- // GeoJSON order: [lng, lat].
25
- type point = array<float>
26
- type geometry = {@as("Point") point?: point}
27
- type place = {@as("Label") label?: string, @as("Geometry") geometry?: geometry}
28
- type searchResult = {@as("Place") place?: place}
29
- type searchResponse = {@as("Results") results?: array<searchResult>}
30
- type searchCommand
31
-
32
- @module("@aws-sdk/client-location") @new
33
- external makeLocationClient: unit => locationClient = "LocationClient"
34
-
35
- @module("@aws-sdk/client-location") @new
36
- external makeSearchPlaceIndexForTextCommand: searchPlaceIndexForTextInput => searchCommand =
37
- "SearchPlaceIndexForTextCommand"
38
-
39
- @send external send: (locationClient, searchCommand) => promise<searchResponse> = "send"
40
-
41
- @val external decodeURIComponent: string => string = "decodeURIComponent"
42
-
43
- // ── Function URL event / response shapes (payload format 2.0) ────────────────
44
-
45
- type functionUrlEvent = {
46
- rawQueryString?: string,
47
- queryStringParameters?: dict<string>,
48
- }
49
-
50
- type response = {
51
- statusCode: int,
52
- headers?: dict<string>,
53
- body: string,
54
- }
55
-
56
- // Read an env var at runtime, mapping "" / unset to None.
57
- let getEnv: string => option<string> = %raw(`
58
- function(k) { var v = process.env[k]; return (v === undefined || v === null || v === "") ? undefined : v; }
59
- `)
60
-
61
- // Permissive CORS so the browser demo can call the endpoint from any origin.
62
- let corsHeaders = () =>
63
- Dict.fromArray([
64
- ("content-type", "application/json"),
65
- ("access-control-allow-origin", "*"),
66
- ("access-control-allow-methods", "GET,OPTIONS"),
67
- ("access-control-allow-headers", "*"),
68
- ])
69
-
70
- // Pull `q` from the parsed query-string params, falling back to the raw string.
71
- let readQueryParam = (event: functionUrlEvent): option<string> =>
72
- switch event.queryStringParameters->Option.flatMap(p => p->Dict.get("q")) {
73
- | Some(q) => Some(q)
74
- | None =>
75
- event.rawQueryString->Option.flatMap(raw =>
76
- raw
77
- ->String.split("&")
78
- ->Array.findMap(pair =>
79
- switch pair->String.split("=") {
80
- | [k, v] if k == "q" => Some(v->decodeURIComponent)
81
- | _ => None
82
- }
83
- )
84
- )
85
- }
86
-
87
- // ── Runtime handler ─────────────────────────────────────────────────────────
88
-
89
- let handleGeocode = async (event: functionUrlEvent, _context: Lambda.context): response => {
90
- try {
91
- let indexName = getEnv("PLACE_INDEX_NAME")->Option.getOr("")
92
- let q = readQueryParam(event)->Option.getOr("")
93
- if indexName == "" || q == "" {
94
- {statusCode: 200, headers: corsHeaders(), body: "[]"}
95
- } else {
96
- let client = makeLocationClient()
97
- let resp = await client->send(
98
- makeSearchPlaceIndexForTextCommand({indexName, text: q, maxResults: 5}),
99
- )
100
- let results =
101
- resp.results
102
- ->Option.getOr([])
103
- ->Array.filterMap(r =>
104
- switch r.place {
105
- | Some(place) =>
106
- let label = place.label->Option.getOr("")
107
- switch place.geometry->Option.flatMap(g => g.point) {
108
- | Some(pt) if pt->Array.length >= 2 =>
109
- let lng = pt->Array.getUnsafe(0)
110
- let lat = pt->Array.getUnsafe(1)
111
- Some(
112
- Dict.fromArray([
113
- ("label", JSON.Encode.string(label)),
114
- ("lat", JSON.Encode.float(lat)),
115
- ("lng", JSON.Encode.float(lng)),
116
- ])->JSON.Encode.object,
117
- )
118
- | _ => None
119
- }
120
- | None => None
121
- }
122
- )
123
- {
124
- statusCode: 200,
125
- headers: corsHeaders(),
126
- body: results->JSON.Encode.array->JSON.stringify,
127
- }
128
- }
129
- } catch {
130
- | _ => {statusCode: 200, headers: corsHeaders(), body: "[]"}
131
- }
132
- }
133
-
134
- // ── Deploy-time factory ─────────────────────────────────────────────────────
135
-
136
21
  type serviceOutputs = {
137
22
  url: Pulumi.Output.t<string>,
138
23
  resources: array<Pulumi.Output.t<string>>,
@@ -159,7 +44,8 @@ let make = (
159
44
  ~opts?,
160
45
  )
161
46
 
162
- // Least-privilege: SearchPlaceIndexForText on the one place index.
47
+ // CloudWatch Logs (so failures are observable) plus least-privilege
48
+ // `geo:SearchPlaceIndexForText` on the one place index.
163
49
  let _policy =
164
50
  placeIndexName
165
51
  ->Pulumi.Output.fromInput
@@ -171,6 +57,12 @@ let make = (
171
57
  policy: PolicyDocument.make(
172
58
  ~id=`${serviceName}Policy`,
173
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
+ },
174
66
  {
175
67
  sid: "AllowGeocode",
176
68
  effect: Allow,
@@ -187,26 +79,53 @@ let make = (
187
79
  )
188
80
  })
189
81
 
190
- let environment: Lambda.CallbackFunction.Args.functionEnvironment = {
191
- // CallbackFunction under-types env values as `dict<string>`; Pulumi resolves
192
- // Output-valued variables at deploy time, so bridge the Input here.
193
- variables: Dict.fromArray([("PLACE_INDEX_NAME", placeIndexName)])->Obj.magic,
194
- }
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
+ )
195
94
 
196
- let lambda = Lambda.CallbackFunction.make(
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(
197
102
  ~name=serviceName,
198
- ~args=Lambda.CallbackFunction.Args.make(
199
- ~callback=handleGeocode,
200
- ~role=lambdaRole,
201
- ~environment,
202
- ~timeout=30->Pulumi.Input.make,
203
- ~tags=AWS.Tags.make(
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(
204
113
  ~name=serviceName,
205
114
  ~kind=ReventlessCore.ComponentType.Platform,
206
115
  ~role=Runtime,
207
116
  ~scope=Platform,
208
117
  ),
209
- ),
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
+ ]),
126
+ }: Lambda.Function.functionEnvironment
127
+ )->Pulumi.Input.make,
128
+ },
210
129
  ~opts?,
211
130
  )
212
131
 
@@ -1,7 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Aws from "@pulumi/aws";
4
- import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
4
  import * as IAM$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/IAM/IAM.res.mjs";
6
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
6
  import * as Pulumi from "@pulumi/pulumi";
@@ -9,113 +8,10 @@ import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda
9
8
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
10
9
  import * as AWS$ReventlessAws from "../AWS.res.mjs";
11
10
  import * as AWS_Tags$ReventlessAws from "../AWS_Tags.res.mjs";
12
- import * as ClientLocation from "@aws-sdk/client-location";
13
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";
14
13
  import * as Util_Pulumi$ReventlessCore from "@reventlessdev/reventless-core/src/util/Util_Pulumi.res.mjs";
15
14
 
16
- let getEnv = (function(k) { var v = process.env[k]; return (v === undefined || v === null || v === "") ? undefined : v; });
17
-
18
- function corsHeaders() {
19
- return Object.fromEntries([
20
- [
21
- "content-type",
22
- "application/json"
23
- ],
24
- [
25
- "access-control-allow-origin",
26
- "*"
27
- ],
28
- [
29
- "access-control-allow-methods",
30
- "GET,OPTIONS"
31
- ],
32
- [
33
- "access-control-allow-headers",
34
- "*"
35
- ]
36
- ]);
37
- }
38
-
39
- function readQueryParam(event) {
40
- let q = Stdlib_Option.flatMap(event.queryStringParameters, p => p["q"]);
41
- if (q !== undefined) {
42
- return q;
43
- } else {
44
- return Stdlib_Option.flatMap(event.rawQueryString, raw => Stdlib_Array.findMap(raw.split("&"), pair => {
45
- let match = pair.split("=");
46
- if (match.length !== 2) {
47
- return;
48
- }
49
- let k = match[0];
50
- let v = match[1];
51
- if (k === "q") {
52
- return decodeURIComponent(v);
53
- }
54
- }));
55
- }
56
- }
57
-
58
- async function handleGeocode(event, _context) {
59
- try {
60
- let indexName = Stdlib_Option.getOr(getEnv("PLACE_INDEX_NAME"), "");
61
- let q = Stdlib_Option.getOr(readQueryParam(event), "");
62
- if (indexName === "" || q === "") {
63
- return {
64
- statusCode: 200,
65
- headers: corsHeaders(),
66
- body: "[]"
67
- };
68
- }
69
- let client = new ClientLocation.LocationClient();
70
- let resp = await client.send(new ClientLocation.SearchPlaceIndexForTextCommand({
71
- IndexName: indexName,
72
- Text: q,
73
- MaxResults: 5
74
- }));
75
- let results = Stdlib_Array.filterMap(Stdlib_Option.getOr(resp.Results, []), r => {
76
- let place = r.Place;
77
- if (place === undefined) {
78
- return;
79
- }
80
- let label = Stdlib_Option.getOr(place.Label, "");
81
- let pt = Stdlib_Option.flatMap(place.Geometry, g => g.Point);
82
- if (pt === undefined) {
83
- return;
84
- }
85
- if (pt.length < 2) {
86
- return;
87
- }
88
- let lng = pt[0];
89
- let lat = pt[1];
90
- return Object.fromEntries([
91
- [
92
- "label",
93
- label
94
- ],
95
- [
96
- "lat",
97
- lat
98
- ],
99
- [
100
- "lng",
101
- lng
102
- ]
103
- ]);
104
- });
105
- return {
106
- statusCode: 200,
107
- headers: corsHeaders(),
108
- body: JSON.stringify(results)
109
- };
110
- } catch (exn) {
111
- return {
112
- statusCode: 200,
113
- headers: corsHeaders(),
114
- body: "[]"
115
- };
116
- }
117
- }
118
-
119
15
  function make(placeIndexName, corsOriginsOpt, opts) {
120
16
  let corsOrigins = corsOriginsOpt !== undefined ? corsOriginsOpt : ["*"];
121
17
  let serviceName = "GeocoderService";
@@ -124,22 +20,64 @@ function make(placeIndexName, corsOriginsOpt, opts) {
124
20
  placeIndexName.apply(idx => {
125
21
  let arn = `arn:aws:geo:*:*:place-index/` + idx;
126
22
  new (Aws.iam.RolePolicy)(serviceName + `Policy`, {
127
- policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, serviceName + `Policy`, [{
23
+ policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, serviceName + `Policy`, [
24
+ {
25
+ Sid: "AllowLambdaLogging",
26
+ Effect: "Allow",
27
+ Action: [
28
+ "logs:CreateLogGroup",
29
+ "logs:CreateLogStream",
30
+ "logs:PutLogEvents"
31
+ ],
32
+ Resource: "arn:aws:logs:*:*:*"
33
+ },
34
+ {
128
35
  Sid: "AllowGeocode",
129
36
  Effect: "Allow",
130
37
  Action: "geo:SearchPlaceIndexForText",
131
38
  Resource: arn
132
- }])),
39
+ }
40
+ ])),
133
41
  role: lambdaRole.id
134
42
  }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
135
43
  });
136
- let environment = {
137
- variables: Object.fromEntries([[
138
- "PLACE_INDEX_NAME",
139
- placeIndexName
140
- ]])
141
- };
142
- let lambda = new (Aws.lambda.CallbackFunction)(serviceName, Lambda$PulumiAws.CallbackFunction.Args.make(handleGeocode, lambdaRole, undefined, undefined, undefined, undefined, 30, undefined, undefined, undefined, AWS_Tags$ReventlessAws.make(serviceName, "Platform", "Runtime", "Platform", undefined, undefined, undefined, undefined), environment), opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
44
+ let packageDirs = Object.fromEntries([[
45
+ "@reventlessdev/reventless-aws",
46
+ Util_Bundle$ReventlessAws.resolvePackageRoot("@reventlessdev/reventless-aws")
47
+ ]]);
48
+ let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs", packageDirs, undefined);
49
+ let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
50
+ let lambda = new (Aws.lambda.Function)(serviceName, {
51
+ handler: "index.handler",
52
+ runtime: "nodejs22.x",
53
+ code: match.code,
54
+ role: lambdaRole.arn,
55
+ memorySize: 256,
56
+ timeout: 30,
57
+ layers: layers,
58
+ tags: AWS_Tags$ReventlessAws.make(serviceName, "Platform", "Runtime", "Platform", undefined, undefined, undefined, undefined),
59
+ environment: {
60
+ variables: Object.fromEntries([
61
+ [
62
+ "Environment",
63
+ Pulumi.getStack()
64
+ ],
65
+ [
66
+ "PLACE_INDEX_NAME",
67
+ placeIndexName
68
+ ],
69
+ [
70
+ "NODE_OPTIONS",
71
+ Util_Bundle$ReventlessAws.esmLoaderNodeOptions
72
+ ],
73
+ [
74
+ "ESM_FALLBACK_DIRS",
75
+ Util_Bundle$ReventlessAws.esmFallbackDirs
76
+ ]
77
+ ])
78
+ },
79
+ sourceCodeHash: match.sourceCodeHash
80
+ }, opts$1 !== undefined ? Primitive_option.valFromOption(opts$1) : undefined);
143
81
  let functionUrl = new (Aws.lambda.FunctionUrl)(serviceName + `Url`, {
144
82
  authorizationType: "NONE",
145
83
  functionName: lambda.name,
@@ -159,10 +97,6 @@ function make(placeIndexName, corsOriginsOpt, opts) {
159
97
  }
160
98
 
161
99
  export {
162
- getEnv,
163
- corsHeaders,
164
- readQueryParam,
165
- handleGeocode,
166
100
  make,
167
101
  }
168
102
  /* @pulumi/aws Not a pure module */
@@ -0,0 +1,137 @@
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 SDK bindings (AWS SDK v3, client.send(new Command(...))) ────
13
+
14
+ type locationClient
15
+
16
+ type searchPlaceIndexForTextInput = {
17
+ @as("IndexName") indexName: string,
18
+ @as("Text") text: string,
19
+ @as("MaxResults") maxResults?: int,
20
+ }
21
+ // GeoJSON order: [lng, lat].
22
+ type point = array<float>
23
+ type geometry = {@as("Point") point?: point}
24
+ type place = {@as("Label") label?: string, @as("Geometry") geometry?: geometry}
25
+ type searchResult = {@as("Place") place?: place}
26
+ type searchResponse = {@as("Results") results?: array<searchResult>}
27
+ type searchCommand
28
+
29
+ @module("@aws-sdk/client-location") @new
30
+ external makeLocationClient: unit => locationClient = "LocationClient"
31
+
32
+ @module("@aws-sdk/client-location") @new
33
+ external makeSearchPlaceIndexForTextCommand: searchPlaceIndexForTextInput => searchCommand =
34
+ "SearchPlaceIndexForTextCommand"
35
+
36
+ @send external send: (locationClient, searchCommand) => promise<searchResponse> = "send"
37
+
38
+ @val external decodeURIComponent: string => string = "decodeURIComponent"
39
+
40
+ // ── Node bindings (replacing the former `%raw` env helper with a typed one) ──
41
+
42
+ @val @scope("process") external processEnv: dict<string> = "env"
43
+
44
+ // Read an env var, mapping "" / unset to None.
45
+ let getEnv = (k: string): option<string> =>
46
+ switch processEnv->Dict.get(k) {
47
+ | Some("") | None => None
48
+ | Some(v) => Some(v)
49
+ }
50
+
51
+ // ── Function URL event / response shapes (payload format 2.0) ────────────────
52
+
53
+ type functionUrlEvent = {
54
+ rawQueryString?: string,
55
+ queryStringParameters?: dict<string>,
56
+ }
57
+
58
+ type response = {
59
+ statusCode: int,
60
+ headers?: dict<string>,
61
+ body: string,
62
+ }
63
+
64
+ // Permissive CORS so the browser demo can call the endpoint from any origin.
65
+ let corsHeaders = () =>
66
+ Dict.fromArray([
67
+ ("content-type", "application/json"),
68
+ ("access-control-allow-origin", "*"),
69
+ ("access-control-allow-methods", "GET,OPTIONS"),
70
+ ("access-control-allow-headers", "*"),
71
+ ])
72
+
73
+ // Pull `q` from the parsed query-string params, falling back to the raw string.
74
+ let readQueryParam = (event: functionUrlEvent): option<string> =>
75
+ switch event.queryStringParameters->Option.flatMap(p => p->Dict.get("q")) {
76
+ | Some(q) => Some(q)
77
+ | None =>
78
+ event.rawQueryString->Option.flatMap(raw =>
79
+ raw
80
+ ->String.split("&")
81
+ ->Array.findMap(pair =>
82
+ switch pair->String.split("=") {
83
+ | [k, v] if k == "q" => Some(v->decodeURIComponent)
84
+ | _ => None
85
+ }
86
+ )
87
+ )
88
+ }
89
+
90
+ // ── Runtime handler ─────────────────────────────────────────────────────────
91
+
92
+ let handler = async (event: functionUrlEvent): response => {
93
+ try {
94
+ let indexName = getEnv("PLACE_INDEX_NAME")->Option.getOr("")
95
+ let q = readQueryParam(event)->Option.getOr("")
96
+ if indexName == "" || q == "" {
97
+ {statusCode: 200, headers: corsHeaders(), body: "[]"}
98
+ } else {
99
+ let client = makeLocationClient()
100
+ let resp = await client->send(
101
+ makeSearchPlaceIndexForTextCommand({indexName, text: q, maxResults: 5}),
102
+ )
103
+ let results =
104
+ resp.results
105
+ ->Option.getOr([])
106
+ ->Array.filterMap(r =>
107
+ switch r.place {
108
+ | Some(place) =>
109
+ let label = place.label->Option.getOr("")
110
+ switch place.geometry->Option.flatMap(g => g.point) {
111
+ | Some(pt) if pt->Array.length >= 2 =>
112
+ let lng = pt->Array.getUnsafe(0)
113
+ let lat = pt->Array.getUnsafe(1)
114
+ Some(
115
+ Dict.fromArray([
116
+ ("label", JSON.Encode.string(label)),
117
+ ("lat", JSON.Encode.float(lat)),
118
+ ("lng", JSON.Encode.float(lng)),
119
+ ])->JSON.Encode.object,
120
+ )
121
+ | _ => None
122
+ }
123
+ | None => None
124
+ }
125
+ )
126
+ {
127
+ statusCode: 200,
128
+ headers: corsHeaders(),
129
+ body: results->JSON.Encode.array->JSON.stringify,
130
+ }
131
+ }
132
+ } catch {
133
+ | exn =>
134
+ Console.error2("Geocoder: search failed", exn)
135
+ {statusCode: 200, headers: corsHeaders(), body: "[]"}
136
+ }
137
+ }