@reventlessdev/reventless-aws 3.0.0-alpha.265 → 3.0.0-alpha.267
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/package.json +6 -6
- package/src/Platform.res +49 -53
- package/src/Platform.res.mjs +17 -36
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res +36 -0
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda.res.mjs +14 -1
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res +37 -6
- package/src/adapter/Api/Platform_ComponentDefinitions_Lambda_Ops.res.mjs +19 -6
- 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/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
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// AWS Location Service geocoder, mounted on the platform GraphQL API.
|
|
2
|
+
//
|
|
3
|
+
// Deploy-time only: `make` provisions one compiled-EntryPoint Lambda (a plain
|
|
4
|
+
// `Lambda.Function` whose code archive re-exports `handler` from the compiled,
|
|
5
|
+
// type-checked runtime module `Geocoder_AwsLocation_Resolver_Ops` and ships the
|
|
6
|
+
// shared ESM resolve-hook loader), an IAM execution role scoped to CloudWatch Logs
|
|
7
|
+
// + `geo:SearchPlaceIndexForText` on the target place index, an AppSync Lambda data
|
|
8
|
+
// source, and one resolver — `Query.geocode` — on the platform API.
|
|
9
|
+
//
|
|
10
|
+
// There is no Function URL and no anonymous surface: authentication is the platform
|
|
11
|
+
// API's Cognito authorizer. This is the *client* door of the geocoding capability
|
|
12
|
+
// (D9 half 2), replacing the public Function URL the browser used to call. The
|
|
13
|
+
// unattended slice path reaches the same place index through the SDK directly
|
|
14
|
+
// (`Geocoder_AwsLocation_Backend`), so one capability, two doors — the same shape
|
|
15
|
+
// the object store already has with `Upload_Presign` and `Offload.resolve`.
|
|
16
|
+
//
|
|
17
|
+
// Why an EntryPoint and not a Pulumi `CallbackFunction`: same SDK-skew reason as the
|
|
18
|
+
// upload service — a serialized closure bakes the deploy machine's version-specific
|
|
19
|
+
// AWS SDK internals into the archive but then resolves `@smithy/*`/`@aws-sdk/*`
|
|
20
|
+
// transitives from independently-versioned layer/runtime sources that disagree at
|
|
21
|
+
// cold start. Shipping the compiled `_Ops` module with bare `@aws-sdk/*` imports,
|
|
22
|
+
// resolved through the resolve-hook, loads one internally consistent SDK. The
|
|
23
|
+
// runtime logic lives in [Geocoder_AwsLocation_Resolver_Ops.res].
|
|
24
|
+
|
|
25
|
+
open PulumiAws
|
|
26
|
+
|
|
27
|
+
type serviceOutputs = {resources: array<Pulumi.Output.t<string>>}
|
|
28
|
+
|
|
29
|
+
// JS resolver code (APPSYNC_JS runtime): forward the caller's arguments to the
|
|
30
|
+
// Lambda. CORS and auth belong to the API, not to this code. No identity is
|
|
31
|
+
// forwarded — geocoding is not scoped per caller (any authenticated user may
|
|
32
|
+
// resolve any address), unlike the upload service which namespaces objects by the
|
|
33
|
+
// verified `sub`.
|
|
34
|
+
let invokeCode: Pulumi.Input.t<string> =
|
|
35
|
+
`import { util } from '@aws-appsync/utils';
|
|
36
|
+
export function request(ctx) {
|
|
37
|
+
return { operation: 'Invoke', payload: { arguments: ctx.args } };
|
|
38
|
+
}
|
|
39
|
+
export function response(ctx) {
|
|
40
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
41
|
+
return ctx.result;
|
|
42
|
+
}
|
|
43
|
+
`->Pulumi.Input.make
|
|
44
|
+
|
|
45
|
+
let make = (
|
|
46
|
+
~api: Pulumi.Output.t<AppSync.GraphQLApi.t>,
|
|
47
|
+
~placeIndexName: Pulumi.Input.t<string>,
|
|
48
|
+
~name: string="GeocodeService",
|
|
49
|
+
~opts: Pulumi.ComponentResource.options,
|
|
50
|
+
): serviceOutputs => {
|
|
51
|
+
let opts = opts->ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions
|
|
52
|
+
|
|
53
|
+
let lambdaRole = IAM.Role.makeWithDefaultPolicy(
|
|
54
|
+
~name=name ++ "Lambda",
|
|
55
|
+
~servicePrincipal=AWS.Lambda.principal->Pulumi.Output.make,
|
|
56
|
+
~tags=AWS.Tags.make(
|
|
57
|
+
~name=name ++ "Lambda",
|
|
58
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
59
|
+
~role=Identity,
|
|
60
|
+
~scope=Platform,
|
|
61
|
+
),
|
|
62
|
+
~opts,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
// CloudWatch Logs (so failures are observable) plus least-privilege
|
|
66
|
+
// `geo:SearchPlaceIndexForText` on the one place index — the same grant the
|
|
67
|
+
// Function URL flavour carried, now on a resolver-invoked Lambda.
|
|
68
|
+
let _policy =
|
|
69
|
+
placeIndexName
|
|
70
|
+
->Pulumi.Output.fromInput
|
|
71
|
+
->Pulumi.Output.apply(idx => {
|
|
72
|
+
let arn = `arn:aws:geo:*:*:place-index/${idx}`
|
|
73
|
+
let _ = IAM.RolePolicy.make(
|
|
74
|
+
~name=name ++ "LambdaPolicy",
|
|
75
|
+
~args={
|
|
76
|
+
policy: PolicyDocument.make(
|
|
77
|
+
~id=name ++ "LambdaPolicy",
|
|
78
|
+
~statements=[
|
|
79
|
+
{
|
|
80
|
+
sid: "AllowLambdaLogging",
|
|
81
|
+
effect: Allow,
|
|
82
|
+
actions: Actions(["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]),
|
|
83
|
+
resources: Resource("arn:aws:logs:*:*:*"),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
sid: "AllowGeocode",
|
|
87
|
+
effect: Allow,
|
|
88
|
+
actions: Action("geo:SearchPlaceIndexForText"),
|
|
89
|
+
resources: Resource(arn),
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
->PolicyDocument.toJsonString
|
|
94
|
+
->Pulumi.Input.make,
|
|
95
|
+
role: lambdaRole.id->Pulumi.Output.asInput,
|
|
96
|
+
},
|
|
97
|
+
~opts,
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Bundle reventless-aws (the compiled `_Ops` handler lives inside it) and
|
|
102
|
+
// re-export its `handler`; buildCodeArchive also ships the ESM resolve-hook.
|
|
103
|
+
let packageDirs = Dict.fromArray([
|
|
104
|
+
(
|
|
105
|
+
"@reventlessdev/reventless-aws",
|
|
106
|
+
Util_Bundle.resolvePackageRoot("@reventlessdev/reventless-aws"),
|
|
107
|
+
),
|
|
108
|
+
])
|
|
109
|
+
let {code, sourceCodeHash} = Util_Bundle.buildCodeArchive(
|
|
110
|
+
~entryPointModule="@reventlessdev/reventless-aws/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver_Ops.res.mjs",
|
|
111
|
+
~packageDirs,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
let layers =
|
|
115
|
+
Lambda.reventlessLayerArn
|
|
116
|
+
->Option.map(arn => [arn->Pulumi.Input.make])
|
|
117
|
+
->Option.getOr([])
|
|
118
|
+
->Pulumi.Input.make
|
|
119
|
+
|
|
120
|
+
let lambda = Lambda.Function.make(
|
|
121
|
+
~name=name ++ "Lambda",
|
|
122
|
+
~args={
|
|
123
|
+
handler: "index.handler"->Pulumi.Input.make,
|
|
124
|
+
runtime: "nodejs22.x"->Pulumi.Input.make,
|
|
125
|
+
code: code->Pulumi.Input.make,
|
|
126
|
+
sourceCodeHash: sourceCodeHash->Pulumi.Input.make,
|
|
127
|
+
role: lambdaRole.arn->Pulumi.Output.asInput,
|
|
128
|
+
memorySize: 256->Pulumi.Input.make,
|
|
129
|
+
timeout: 30->Pulumi.Input.make,
|
|
130
|
+
layers,
|
|
131
|
+
tags: AWS.Tags.make(
|
|
132
|
+
~name=name ++ "Lambda",
|
|
133
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
134
|
+
~role=Runtime,
|
|
135
|
+
~scope=Platform,
|
|
136
|
+
),
|
|
137
|
+
environment: (
|
|
138
|
+
{
|
|
139
|
+
Lambda.Function.variables: Dict.fromArray([
|
|
140
|
+
("Environment", Pulumi.Pulumi.getStackName()->Pulumi.Input.make),
|
|
141
|
+
("PLACE_INDEX_NAME", placeIndexName),
|
|
142
|
+
("NODE_OPTIONS", Util_Bundle.esmLoaderNodeOptions->Pulumi.Input.make),
|
|
143
|
+
("ESM_FALLBACK_DIRS", Util_Bundle.esmFallbackDirs->Pulumi.Input.make),
|
|
144
|
+
Util_LambdaLogging.logLevelEntry(),
|
|
145
|
+
]),
|
|
146
|
+
}: Lambda.Function.functionEnvironment
|
|
147
|
+
)->Pulumi.Input.make,
|
|
148
|
+
},
|
|
149
|
+
~opts,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
Util_LambdaLogging.makeManagedLogGroup(
|
|
153
|
+
~name=name ++ "Lambda",
|
|
154
|
+
~lambdaName=lambda.name,
|
|
155
|
+
~tags=AWS.Tags.make(
|
|
156
|
+
~name=name ++ "LambdaLogGroup",
|
|
157
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
158
|
+
~role=Logs,
|
|
159
|
+
~scope=Platform,
|
|
160
|
+
),
|
|
161
|
+
~opts,
|
|
162
|
+
(),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
let dataSourceRole = IAM.Role.makeWithDefaultPolicy(
|
|
166
|
+
~name=name ++ "DataSource",
|
|
167
|
+
~servicePrincipal=AWS.AppSync.principal->Pulumi.Output.make,
|
|
168
|
+
~tags=AWS.Tags.make(
|
|
169
|
+
~name=name ++ "DataSource",
|
|
170
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
171
|
+
~role=Identity,
|
|
172
|
+
~scope=Platform,
|
|
173
|
+
),
|
|
174
|
+
~opts,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
let _ =
|
|
178
|
+
(lambda.arn, dataSourceRole.id)
|
|
179
|
+
->Pulumi.Output.all2
|
|
180
|
+
->Pulumi.Output.apply(((lambdaArn, dataSourceRoleId)) => {
|
|
181
|
+
let _attach = IAM.RolePolicy.make(
|
|
182
|
+
~name=name ++ "DataSource",
|
|
183
|
+
~args={
|
|
184
|
+
policy: PolicyDocument.make(
|
|
185
|
+
~id=name ++ "DataSourcePolicy",
|
|
186
|
+
~statements=[
|
|
187
|
+
{
|
|
188
|
+
sid: "AllowDataSourceInvokeLambda",
|
|
189
|
+
effect: Allow,
|
|
190
|
+
actions: Action("lambda:InvokeFunction"),
|
|
191
|
+
resources: Resource(lambdaArn),
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
)
|
|
195
|
+
->PolicyDocument.toJsonString
|
|
196
|
+
->Pulumi.Input.make,
|
|
197
|
+
role: dataSourceRoleId->Pulumi.Input.make,
|
|
198
|
+
},
|
|
199
|
+
~opts,
|
|
200
|
+
)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
let dataSource = AppSync.DataSource.make(
|
|
204
|
+
~name=name ++ "DataSource",
|
|
205
|
+
~args={
|
|
206
|
+
type_: AWS_LAMBDA,
|
|
207
|
+
apiId: api->Pulumi.Output.flatMap(api => api.id)->Pulumi.Output.asInput,
|
|
208
|
+
lambdaConfig: {
|
|
209
|
+
AppSync.DataSource.functionArn: lambda.arn->Pulumi.Output.asInput,
|
|
210
|
+
}->Pulumi.Input.make,
|
|
211
|
+
serviceRoleArn: dataSourceRole.arn->Pulumi.Output.asInput,
|
|
212
|
+
},
|
|
213
|
+
~opts=Some(opts),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
let _geocodeResolver = AppSync_Resolver_Native.makeUnitJsResolver(
|
|
217
|
+
~name=name ++ "Geocode",
|
|
218
|
+
~api,
|
|
219
|
+
~dataSourceName=dataSource.name->Pulumi.Output.asInput,
|
|
220
|
+
~type_="Query"->Pulumi.Input.make,
|
|
221
|
+
~field="geocode"->Pulumi.Input.make,
|
|
222
|
+
~code=invokeCode,
|
|
223
|
+
~opts,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
{resources: [lambda.arn]}
|
|
227
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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 Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output.res.mjs";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
8
|
+
import * as Lambda$PulumiAws from "@reventlessdev/rescript-pulumi-aws/src/Lambda/Lambda.res.mjs";
|
|
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
|
+
import * as AppSync_Resolver_Native$ReventlessAws from "../Api/AppSync_Resolver_Native.res.mjs";
|
|
16
|
+
|
|
17
|
+
let invokeCode = `import { util } from '@aws-appsync/utils';
|
|
18
|
+
export function request(ctx) {
|
|
19
|
+
return { operation: 'Invoke', payload: { arguments: ctx.args } };
|
|
20
|
+
}
|
|
21
|
+
export function response(ctx) {
|
|
22
|
+
if (ctx.error) util.error(ctx.error.message, ctx.error.type);
|
|
23
|
+
return ctx.result;
|
|
24
|
+
}
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
function make(api, placeIndexName, nameOpt, opts) {
|
|
28
|
+
let name = nameOpt !== undefined ? nameOpt : "GeocodeService";
|
|
29
|
+
let opts$1 = Util_Pulumi$ReventlessCore.ComponentResourceOptions.toCustomResourceOptions(opts);
|
|
30
|
+
let lambdaRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "Lambda", Pulumi.output(AWS$ReventlessAws.Lambda.principal), AWS_Tags$ReventlessAws.make(name + "Lambda", "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
|
|
31
|
+
placeIndexName.apply(idx => {
|
|
32
|
+
let arn = `arn:aws:geo:*:*:place-index/` + idx;
|
|
33
|
+
new (Aws.iam.RolePolicy)(name + "LambdaPolicy", {
|
|
34
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "LambdaPolicy", [
|
|
35
|
+
{
|
|
36
|
+
Sid: "AllowLambdaLogging",
|
|
37
|
+
Effect: "Allow",
|
|
38
|
+
Action: [
|
|
39
|
+
"logs:CreateLogGroup",
|
|
40
|
+
"logs:CreateLogStream",
|
|
41
|
+
"logs:PutLogEvents"
|
|
42
|
+
],
|
|
43
|
+
Resource: "arn:aws:logs:*:*:*"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
Sid: "AllowGeocode",
|
|
47
|
+
Effect: "Allow",
|
|
48
|
+
Action: "geo:SearchPlaceIndexForText",
|
|
49
|
+
Resource: arn
|
|
50
|
+
}
|
|
51
|
+
])),
|
|
52
|
+
role: lambdaRole.id
|
|
53
|
+
}, opts$1);
|
|
54
|
+
});
|
|
55
|
+
let packageDirs = Object.fromEntries([[
|
|
56
|
+
"@reventlessdev/reventless-aws",
|
|
57
|
+
Util_Bundle$ReventlessAws.resolvePackageRoot(undefined, "@reventlessdev/reventless-aws")
|
|
58
|
+
]]);
|
|
59
|
+
let match = Util_Bundle$ReventlessAws.buildCodeArchive("@reventlessdev/reventless-aws/src/adapter/Geocoder/Geocoder_AwsLocation_Resolver_Ops.res.mjs", packageDirs, undefined);
|
|
60
|
+
let layers = Stdlib_Option.getOr(Stdlib_Option.map(Lambda$PulumiAws.reventlessLayerArn, arn => [arn]), []);
|
|
61
|
+
let lambda = new (Aws.lambda.Function)(name + "Lambda", {
|
|
62
|
+
handler: "index.handler",
|
|
63
|
+
runtime: "nodejs22.x",
|
|
64
|
+
code: match.code,
|
|
65
|
+
role: lambdaRole.arn,
|
|
66
|
+
memorySize: 256,
|
|
67
|
+
timeout: 30,
|
|
68
|
+
layers: layers,
|
|
69
|
+
tags: AWS_Tags$ReventlessAws.make(name + "Lambda", "Platform", "Runtime", "Platform", undefined, undefined, undefined, undefined),
|
|
70
|
+
environment: {
|
|
71
|
+
variables: Object.fromEntries([
|
|
72
|
+
[
|
|
73
|
+
"Environment",
|
|
74
|
+
Pulumi.getStack()
|
|
75
|
+
],
|
|
76
|
+
[
|
|
77
|
+
"PLACE_INDEX_NAME",
|
|
78
|
+
placeIndexName
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
"NODE_OPTIONS",
|
|
82
|
+
Util_Bundle$ReventlessAws.esmLoaderNodeOptions
|
|
83
|
+
],
|
|
84
|
+
[
|
|
85
|
+
"ESM_FALLBACK_DIRS",
|
|
86
|
+
Util_Bundle$ReventlessAws.esmFallbackDirs
|
|
87
|
+
],
|
|
88
|
+
Util_LambdaLogging$ReventlessAws.logLevelEntry()
|
|
89
|
+
])
|
|
90
|
+
},
|
|
91
|
+
sourceCodeHash: match.sourceCodeHash
|
|
92
|
+
}, opts$1);
|
|
93
|
+
Util_LambdaLogging$ReventlessAws.makeManagedLogGroup(name + "Lambda", lambda.name, AWS_Tags$ReventlessAws.make(name + "LambdaLogGroup", "Platform", "Logs", "Platform", undefined, undefined, undefined, undefined), opts$1, undefined);
|
|
94
|
+
let dataSourceRole = IAM$PulumiAws.Role.makeWithDefaultPolicy(name + "DataSource", Pulumi.output(AWS$ReventlessAws.AppSync.principal), AWS_Tags$ReventlessAws.make(name + "DataSource", "Platform", "Identity", "Platform", undefined, undefined, undefined, undefined), opts$1);
|
|
95
|
+
Pulumi.all([
|
|
96
|
+
lambda.arn,
|
|
97
|
+
dataSourceRole.id
|
|
98
|
+
]).apply(param => {
|
|
99
|
+
new (Aws.iam.RolePolicy)(name + "DataSource", {
|
|
100
|
+
policy: PolicyDocument$PulumiAws.toJsonString(PolicyDocument$PulumiAws.make(undefined, name + "DataSourcePolicy", [{
|
|
101
|
+
Sid: "AllowDataSourceInvokeLambda",
|
|
102
|
+
Effect: "Allow",
|
|
103
|
+
Action: "lambda:InvokeFunction",
|
|
104
|
+
Resource: param[0]
|
|
105
|
+
}])),
|
|
106
|
+
role: param[1]
|
|
107
|
+
}, opts$1);
|
|
108
|
+
});
|
|
109
|
+
let dataSource = new (Aws.appsync.DataSource)(name + "DataSource", {
|
|
110
|
+
type: "AWS_LAMBDA",
|
|
111
|
+
apiId: Output$Pulumi.flatMap(api, api => api.id),
|
|
112
|
+
lambdaConfig: {
|
|
113
|
+
functionArn: lambda.arn
|
|
114
|
+
},
|
|
115
|
+
serviceRoleArn: dataSourceRole.arn
|
|
116
|
+
}, opts$1);
|
|
117
|
+
AppSync_Resolver_Native$ReventlessAws.makeUnitJsResolver(name + "Geocode", api, dataSource.name, "Query", "geocode", invokeCode, opts$1);
|
|
118
|
+
return {
|
|
119
|
+
resources: [lambda.arn]
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export {
|
|
124
|
+
invokeCode,
|
|
125
|
+
make,
|
|
126
|
+
}
|
|
127
|
+
/* @pulumi/aws Not a pure module */
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Runtime handler for the geocoder's client door — compiled, type-checked, and
|
|
2
|
+
// Pulumi-free so it can be shipped as an EntryPoint module
|
|
3
|
+
// (`Geocoder_AwsLocation_Resolver` bundles it and attaches it as the platform API's
|
|
4
|
+
// `Query.geocode` Lambda data source). Keeping it out of the deploy-time module
|
|
5
|
+
// avoids both the serialized-closure SDK skew and a deploy-time Pulumi import
|
|
6
|
+
// leaking into the Lambda's cold-start graph.
|
|
7
|
+
//
|
|
8
|
+
// Invoked by an AppSync resolver, not a Function URL: the platform API's Cognito
|
|
9
|
+
// authorizer has already authenticated the caller, so there is no token to decode
|
|
10
|
+
// and no anonymous surface. The SDK call, the `[lng, lat]` order and the relevance
|
|
11
|
+
// handling all live in `Geocoder_AwsLocation_Backend`, shared with the unattended
|
|
12
|
+
// slice path so one module owns the AWS Location call.
|
|
13
|
+
//
|
|
14
|
+
// Returns the ranked candidates as `[{label, lat, lng, relevance?}]`. A no-match is
|
|
15
|
+
// an empty array — a browser search box degrades to "no results". A service failure
|
|
16
|
+
// throws, which the resolver's response mapper turns into a GraphQL error rather
|
|
17
|
+
// than an empty answer: the client half of D2's status contract, so a browser can
|
|
18
|
+
// tell "no such address" from "the geocoder is down" the way the Function URL's
|
|
19
|
+
// `200`/`502` split did.
|
|
20
|
+
|
|
21
|
+
let candidateJson = (c: Reventless.Geocoding.candidate): JSON.t =>
|
|
22
|
+
Dict.fromArray(
|
|
23
|
+
Array.concat(
|
|
24
|
+
[
|
|
25
|
+
("label", JSON.Encode.string(c.label)),
|
|
26
|
+
("lat", JSON.Encode.float(c.point.lat)),
|
|
27
|
+
("lng", JSON.Encode.float(c.point.lng)),
|
|
28
|
+
],
|
|
29
|
+
switch c.relevance {
|
|
30
|
+
| Some(rel) => [("relevance", JSON.Encode.float(rel))]
|
|
31
|
+
| None => []
|
|
32
|
+
},
|
|
33
|
+
),
|
|
34
|
+
)->JSON.Encode.object
|
|
35
|
+
|
|
36
|
+
// Only `text` is read; `ctx.identity` is forwarded by the resolver but the geocoder
|
|
37
|
+
// does not scope by caller, so the handler ignores it.
|
|
38
|
+
type geocodeArgs = {text?: string}
|
|
39
|
+
type appSyncEvent = {arguments?: geocodeArgs}
|
|
40
|
+
|
|
41
|
+
let handler = async (event: appSyncEvent): array<JSON.t> => {
|
|
42
|
+
let indexName = switch NodeProcess.env->Dict.get("PLACE_INDEX_NAME") {
|
|
43
|
+
| Some("") | None => ""
|
|
44
|
+
| Some(v) => v
|
|
45
|
+
}
|
|
46
|
+
let text = event.arguments->Option.flatMap(a => a.text)->Option.getOr("")
|
|
47
|
+
switch await Geocoder_AwsLocation_Backend.search(~indexName, ~text) {
|
|
48
|
+
| Ok(candidates) => candidates->Array.map(candidateJson)
|
|
49
|
+
// The provider answered and had nothing — a true, final empty answer.
|
|
50
|
+
| Error(NoMatch) => []
|
|
51
|
+
// Misconfigured (no index) or the service failed: throw so `ctx.error` is set and
|
|
52
|
+
// the caller sees an error, never a silent empty list it would read as "no match".
|
|
53
|
+
| Error(Unavailable(msg)) => JsError.throwWithMessage(`geocoder unavailable: ${msg}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
5
|
+
import * as Geocoder_AwsLocation_Backend$ReventlessAws from "./Geocoder_AwsLocation_Backend.res.mjs";
|
|
6
|
+
|
|
7
|
+
function candidateJson(c) {
|
|
8
|
+
let rel = c.relevance;
|
|
9
|
+
return Object.fromEntries([
|
|
10
|
+
[
|
|
11
|
+
"label",
|
|
12
|
+
c.label
|
|
13
|
+
],
|
|
14
|
+
[
|
|
15
|
+
"lat",
|
|
16
|
+
c.point.lat
|
|
17
|
+
],
|
|
18
|
+
[
|
|
19
|
+
"lng",
|
|
20
|
+
c.point.lng
|
|
21
|
+
]
|
|
22
|
+
].concat(rel !== undefined ? [[
|
|
23
|
+
"relevance",
|
|
24
|
+
rel
|
|
25
|
+
]] : []));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function handler(event) {
|
|
29
|
+
let v = process.env["PLACE_INDEX_NAME"];
|
|
30
|
+
let indexName = v !== undefined && v !== "" ? v : "";
|
|
31
|
+
let text = Stdlib_Option.getOr(Stdlib_Option.flatMap(event.arguments, a => a.text), "");
|
|
32
|
+
let candidates = await Geocoder_AwsLocation_Backend$ReventlessAws.search(indexName, text, undefined);
|
|
33
|
+
if (candidates.TAG === "Ok") {
|
|
34
|
+
return candidates._0.map(candidateJson);
|
|
35
|
+
}
|
|
36
|
+
let msg = candidates._0;
|
|
37
|
+
if (typeof msg !== "object") {
|
|
38
|
+
return [];
|
|
39
|
+
} else {
|
|
40
|
+
return Stdlib_JsError.throwWithMessage(`geocoder unavailable: ` + msg._0);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
candidateJson,
|
|
46
|
+
handler,
|
|
47
|
+
}
|
|
48
|
+
/* Geocoder_AwsLocation_Backend-ReventlessAws Not a pure module */
|
|
@@ -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 */
|