@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.
- package/CHANGELOG.md +15 -0
- package/package.json +5 -5
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res +60 -141
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +52 -118
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +137 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +124 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint.mjs +51 -6
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res +60 -0
- package/src/adapter/Runtime/DcbCommandTopicEntryPoint_Ops.res.mjs +29 -1
- package/src/adapter/Runtime/HeartbeatEntryPoint.res +44 -0
- package/src/adapter/Runtime/HeartbeatEntryPoint.res.mjs +47 -0
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res +234 -0
- package/src/adapter/Runtime/PluginExtensionPointEntryPoint.res.mjs +226 -0
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res +49 -4
- package/src/adapter/Runtime/StateChangeSliceRuntime_Builder_Single.res.mjs +22 -4
- package/src/adapter/Upload/Upload_Presign_S3.res +65 -164
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +56 -119
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res +157 -0
- package/src/adapter/Upload/Upload_Presign_S3_Ops.res.mjs +120 -0
- package/src/components/InboundTranslationSlice_Builder.res +27 -1
- package/src/components/InboundTranslationSlice_Builder.res.mjs +15 -2
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res +1 -1
- package/src/plugin/runtime/PluginExtensionPointRuntime_Builder.res.mjs +1 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +59 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +49 -2
- package/tests/DcbInboundTranslationRoutingTest.res +73 -0
- package/tests/DcbInboundTranslationRoutingTest.res.mjs +71 -0
- package/tests/EpInboundTestSlice.res +19 -0
- package/tests/EpInboundTestSlice.res.mjs +31 -0
- package/tests/EpInboundTestSliceTranslation.res +13 -0
- package/tests/EpInboundTestSliceTranslation.res.mjs +33 -0
- package/src/adapter/Runtime/HeartbeatEntryPoint.mjs +0 -38
- 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.
|
|
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-
|
|
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
|
|
4
|
-
//
|
|
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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
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=
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
let lambda = new (Aws.lambda.
|
|
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
|
+
}
|