@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.
- package/CHANGELOG.md +17 -0
- package/package.json +7 -7
- package/src/Platform.res +66 -45
- package/src/Platform.res.mjs +31 -35
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res +227 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver.res.mjs +127 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver_Ops.res +55 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver_Ops.res.mjs +48 -0
- package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +21 -3
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res +219 -27
- package/src/adapter/Runtime/AutomationSliceEntryPoint_Ops.res.mjs +121 -7
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +118 -0
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +61 -0
- package/src/components/OutboundTranslationSlice_Builder.res +15 -0
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +11 -1
- package/src/plugin/runtime/PluginRuntime_Builder.res +26 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +26 -0
- package/tests/AutomationSliceEntryPoint_OpsTest.res +44 -3
- package/tests/AutomationSliceEntryPoint_OpsTest.res.mjs +65 -1
- package/tests/Geocoder_AwsLocation_Resolver_OpsTest.res +53 -0
- package/tests/Geocoder_AwsLocation_Resolver_OpsTest.res.mjs +55 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res +0 -166
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +0 -105
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +0 -144
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +0 -137
- package/tests/Geocoder_AwsLocation_OpsTest.res +0 -78
- package/tests/Geocoder_AwsLocation_OpsTest.res.mjs +0 -71
|
@@ -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
|
-
}
|