@reventlessdev/reventless-aws 3.0.0-alpha.259 → 3.0.0-alpha.261
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 +19 -0
- package/package.json +9 -9
- package/src/Platform.res +79 -4
- package/src/Platform.res.mjs +24 -8
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Backend.res +77 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Backend.res.mjs +87 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res +42 -36
- package/src/adapter/Geocoder/Geocoder_AwsLocation_Ops.res.mjs +24 -6
- package/src/adapter/Runtime/AutomationSliceEntryPoint.mjs +13 -1
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res +87 -8
- package/src/adapter/Runtime/AutomationSliceRuntime_Builder_Single.res.mjs +36 -5
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +3 -16
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +4 -19
- package/src/components/AutomationSlice_Builder.res +13 -0
- package/src/components/AutomationSlice_Builder.res.mjs +7 -2
- package/src/components/OutboundTranslationSlice_Builder.res +18 -2
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +9 -3
- package/src/plugin/runtime/PluginRuntime_Builder.res +31 -0
- package/src/plugin/runtime/PluginRuntime_Builder.res.mjs +13 -0
- package/src/util/Util_DcbMetrics.res +48 -0
- package/src/util/Util_DcbMetrics.res.mjs +37 -0
- package/src/util/Util_ShellConfig.res +72 -0
- package/src/util/Util_ShellConfig.res.mjs +60 -0
- package/tests/Geocoder_AwsLocation_OpsTest.res +78 -0
- package/tests/Geocoder_AwsLocation_OpsTest.res.mjs +71 -0
- package/tests/Util_DcbMetricsTest.res +35 -0
- package/tests/Util_DcbMetricsTest.res.mjs +29 -0
- package/tests/Util_ShellConfigTest.res +78 -0
- package/tests/Util_ShellConfigTest.res.mjs +113 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
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.261 (2026-08-03)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **aws:** let an outbound slice read an aggregate at runtime ([24585f8](https://github.com/ReventlessDev/reventless-core/commit/24585f869c15a07910434632d76d8c8d8058042b))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.260 (2026-08-03)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **aws:** a dimensioned DCB metric cannot carry a default value ([0c105e7](https://github.com/ReventlessDev/reventless-core/commit/0c105e7b7156ed64b923cc75731e3bc49a30bcbd))
|
|
18
|
+
### Features
|
|
19
|
+
|
|
20
|
+
* **aws:** let a deployment hand its plugins a geocoder ([143bf41](https://github.com/ReventlessDev/reventless-core/commit/143bf4137765362ce07dd42db0f4a57057da9f13))
|
|
21
|
+
* **outbound:** let an outbound slice read an aggregate, and geocode addresses with it ([867e63e](https://github.com/ReventlessDev/reventless-core/commit/867e63e774ebc8b78b2b19c78645c8a12a8d06f6))
|
|
22
|
+
* **platform:** let a deployment choose what the host shell does ([ae0aec3](https://github.com/ReventlessDev/reventless-core/commit/ae0aec33dcf641f02187e8d6a3f3594c506820b6))
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
# 3.0.0-alpha.259 (2026-08-02)
|
|
7
26
|
|
|
8
27
|
**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.261",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
"@aws-sdk/s3-request-presigner": "3.970.0",
|
|
12
12
|
"sury": "11.0.0-alpha.4",
|
|
13
13
|
"uuid": "^13.0.0",
|
|
14
|
-
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.
|
|
15
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
14
|
+
"@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.3",
|
|
16
15
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
16
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
17
17
|
"@reventlessdev/rescript-node": "2.0.0-alpha.1",
|
|
18
|
-
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.
|
|
19
|
-
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
18
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.65",
|
|
20
19
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
21
|
-
"@reventlessdev/
|
|
22
|
-
"@reventlessdev/reventless-core": "3.0.0-alpha.205",
|
|
20
|
+
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
23
21
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
24
|
-
"@reventlessdev/reventless-postgres": "3.0.0-alpha.
|
|
25
|
-
"@reventlessdev/reventless-
|
|
22
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.71",
|
|
23
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.207",
|
|
24
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.124",
|
|
25
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.99"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -82,6 +82,20 @@ let objectStoreEndpointsRef: ref<array<objectStoreEndpoint>> = ref([])
|
|
|
82
82
|
`getApiConfig` does. */
|
|
83
83
|
let getObjectStoreEndpoints = () => objectStoreEndpointsRef.contents
|
|
84
84
|
|
|
85
|
+
/** The geocoder endpoint this platform provisioned, for the monolithic case.
|
|
86
|
+
|
|
87
|
+
In plugin mode the value crosses to the plugin stack as the `geocoderEndpoint`
|
|
88
|
+
export; when platform and plugin are one program there is no stack to read, so
|
|
89
|
+
`deployPlatform` records it here and `deployPlugin` picks it up — the same two
|
|
90
|
+
halves `objectStoreEndpointsRef` exists for.
|
|
91
|
+
|
|
92
|
+
A plain Output with `""` as the "no geocoder" sentinel, never
|
|
93
|
+
`option<Pulumi.Output.t<_>>`: any generic `Option.*` over an Output runs
|
|
94
|
+
`valFromOption`, whose nested-option probe hits the Output proxy — where every
|
|
95
|
+
property access returns a truthy Output — and corrupts it. Same reasoning, and
|
|
96
|
+
the same sentinel, as `PluginRuntime_Builder.inboundSliceReg.auditTableName`. */
|
|
97
|
+
let geocoderEndpointRef: ref<Pulumi.Output.t<string>> = ref(Pulumi.Output.make(""))
|
|
98
|
+
|
|
85
99
|
module MakeWithConfig = (
|
|
86
100
|
Config: {
|
|
87
101
|
let splitApi: bool
|
|
@@ -1149,6 +1163,25 @@ module MakeWithConfig = (
|
|
|
1149
1163
|
// writes the prefix once and consumes it on both sides, so a mismatch is
|
|
1150
1164
|
// unrepresentable rather than merely discouraged.
|
|
1151
1165
|
uploadBucket?: ReventlessInfra.Platform.objectStore,
|
|
1166
|
+
// Optional view modes the deployed shell loads at boot. Written to
|
|
1167
|
+
// config.json as `viewModes`, with each mode's options flattened beside it
|
|
1168
|
+
// (`Map({style})` ⇒ `"viewModes": ["map"], "mapStyle": …`). Unset ⇒ both
|
|
1169
|
+
// keys omitted and the shell loads no optional mode, so a non-map
|
|
1170
|
+
// deployment still downloads no maplibre chunk and gets a byte-identical
|
|
1171
|
+
// config.json.
|
|
1172
|
+
//
|
|
1173
|
+
// This is the one input that decides what the shell *does* rather than what
|
|
1174
|
+
// it points at, and it is load-bearing twice over: naming `Map` is what
|
|
1175
|
+
// makes both the map view and the map-backed geo-point command input exist,
|
|
1176
|
+
// and it is what makes a provisioned `geocoderPlaceIndex` reachable at all
|
|
1177
|
+
// (the geocoder client is built inside the map chunk).
|
|
1178
|
+
viewModes?: array<ReventlessInfra.Platform.viewMode>,
|
|
1179
|
+
// Shell-owned config.json keys the framework has no opinion about
|
|
1180
|
+
// (`accessTiers`, `platformName`, `assetOrigins`, `uiHintsUrl`, …), merged
|
|
1181
|
+
// in verbatim under the computed keys. Untyped on purpose — re-declaring
|
|
1182
|
+
// the shell's schema here would cost a lockstep core release per UI knob.
|
|
1183
|
+
// A key that collides with a computed one fails the deploy naming it.
|
|
1184
|
+
shellConfig?: dict<JSON.t>,
|
|
1152
1185
|
}
|
|
1153
1186
|
|
|
1154
1187
|
// Merged-mode outputs of deployPlatform — the merged API(s), plus the
|
|
@@ -2033,6 +2066,18 @@ module MakeWithConfig = (
|
|
|
2033
2066
|
| None => Pulumi.Output.make(None)
|
|
2034
2067
|
}
|
|
2035
2068
|
|
|
2069
|
+
// Exported as well as written into config.json, because the browser is no
|
|
2070
|
+
// longer the only caller: a plugin stack's slice Lambdas geocode too, and a
|
|
2071
|
+
// plugin stack has no other way to see a value computed in this one.
|
|
2072
|
+
//
|
|
2073
|
+
// `""` when unset rather than a missing output, so the read side has one
|
|
2074
|
+
// shape to handle instead of two — and "" is already what a client reads as
|
|
2075
|
+
// "no geocoder configured".
|
|
2076
|
+
let geocoderEndpointFlat =
|
|
2077
|
+
geocoderEndpointOutput->Pulumi.Output.apply(o => o->Option.getOr(""))
|
|
2078
|
+
Pulumi.Pulumi.export("geocoderEndpoint", geocoderEndpointFlat)
|
|
2079
|
+
geocoderEndpointRef := geocoderEndpointFlat
|
|
2080
|
+
|
|
2036
2081
|
// Presign endpoints are no longer written to config.json: under route B the
|
|
2037
2082
|
// client calls the platform API's `Upload_Presign` mutation (reachable from
|
|
2038
2083
|
// `platformApiEndpoint`, already present) with the store it declares, so there
|
|
@@ -2055,7 +2100,7 @@ module MakeWithConfig = (
|
|
|
2055
2100
|
eventsEpOpt,
|
|
2056
2101
|
geocoderEpOpt,
|
|
2057
2102
|
)) => {
|
|
2058
|
-
let
|
|
2103
|
+
let computed = [
|
|
2059
2104
|
("apiEndpoint", JSON.Encode.string(domainEp)),
|
|
2060
2105
|
("platformApiEndpoint", JSON.Encode.string(platformEp)),
|
|
2061
2106
|
("region", JSON.Encode.string(regionStr)),
|
|
@@ -2067,7 +2112,7 @@ module MakeWithConfig = (
|
|
|
2067
2112
|
let withEvents = switch eventsEpOpt {
|
|
2068
2113
|
| Some(ep) =>
|
|
2069
2114
|
Array.concat(
|
|
2070
|
-
|
|
2115
|
+
computed,
|
|
2071
2116
|
[
|
|
2072
2117
|
("domainApiEventsEndpoint", JSON.Encode.string(ep)),
|
|
2073
2118
|
("platformApiEventsEndpoint", JSON.Encode.string(ep)),
|
|
@@ -2080,13 +2125,19 @@ module MakeWithConfig = (
|
|
|
2080
2125
|
),
|
|
2081
2126
|
],
|
|
2082
2127
|
)
|
|
2083
|
-
| None =>
|
|
2128
|
+
| None => computed
|
|
2084
2129
|
}
|
|
2085
2130
|
let withGeocoder = switch geocoderEpOpt {
|
|
2086
2131
|
| Some(ep) => Array.concat(withEvents, [("geocoderEndpoint", JSON.Encode.string(ep))])
|
|
2087
2132
|
| None => withEvents
|
|
2088
2133
|
}
|
|
2089
|
-
|
|
2134
|
+
Util_ShellConfig.fields(
|
|
2135
|
+
~computed=withGeocoder,
|
|
2136
|
+
~viewModes=?cfg.viewModes,
|
|
2137
|
+
~shellConfig=?cfg.shellConfig,
|
|
2138
|
+
)
|
|
2139
|
+
->JSON.Encode.object
|
|
2140
|
+
->JSON.stringify
|
|
2090
2141
|
})
|
|
2091
2142
|
|
|
2092
2143
|
let _ = PulumiAws.S3.BucketObject.make(
|
|
@@ -2181,6 +2232,30 @@ module MakeWithConfig = (
|
|
|
2181
2232
|
{Reventless.Offload.store, key, hash, bytes: bytes->String.length}
|
|
2182
2233
|
})
|
|
2183
2234
|
|
|
2235
|
+
// Capability endpoints the platform provisioned, handed to this plugin's
|
|
2236
|
+
// slice Lambdas as environment. Read across the stack reference in plugin
|
|
2237
|
+
// mode and out of the platform's own record when platform and plugin are one
|
|
2238
|
+
// program — the same two halves the offload bucket above is read through.
|
|
2239
|
+
//
|
|
2240
|
+
// Registered before `P.make()`: the slice runtime finalizers fire from inside
|
|
2241
|
+
// the plugin build, and a variable registered after it would be built into
|
|
2242
|
+
// nothing.
|
|
2243
|
+
//
|
|
2244
|
+
// A platform that provisioned no geocoder yields "", and the plugin's client
|
|
2245
|
+
// reports `Unavailable` — retried, then surfaced by the heartbeat sweep. That
|
|
2246
|
+
// is a modelled outcome, which is the point of the empty string: a plugin
|
|
2247
|
+
// deployed against a platform without the capability degrades rather than
|
|
2248
|
+
// failing to deploy.
|
|
2249
|
+
let geocoderEndpoint: Pulumi.Output.t<string> = switch platformStackRef {
|
|
2250
|
+
| Some(stackRef) =>
|
|
2251
|
+
(
|
|
2252
|
+
stackRef->Pulumi.StackReference.getOutput("geocoderEndpoint"):
|
|
2253
|
+
Pulumi.Output.t<option<string>>
|
|
2254
|
+
)->Pulumi.Output.apply(o => o->Option.getOr(""))
|
|
2255
|
+
| None => geocoderEndpointRef.contents
|
|
2256
|
+
}
|
|
2257
|
+
PluginRuntime_Builder.registerCapabilityEnv("GEOCODER_ENDPOINT", geocoderEndpoint)
|
|
2258
|
+
|
|
2184
2259
|
module P = unpack(plugin)
|
|
2185
2260
|
let pluginComponent = P.make()
|
|
2186
2261
|
ReventlessCore.Plugin_Helpers.clearOffload()
|
package/src/Platform.res.mjs
CHANGED
|
@@ -40,6 +40,7 @@ import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/s
|
|
|
40
40
|
import * as Upload_Claim_S3$ReventlessAws from "./adapter/Upload/Upload_Claim_S3.res.mjs";
|
|
41
41
|
import * as PgProjectionFeed$ReventlessAws from "./adapter/Postgres/PgProjectionFeed.res.mjs";
|
|
42
42
|
import * as Util_LocalConfig$ReventlessAws from "./util/Util_LocalConfig.res.mjs";
|
|
43
|
+
import * as Util_ShellConfig$ReventlessAws from "./util/Util_ShellConfig.res.mjs";
|
|
43
44
|
import * as Util_StoreLayout$ReventlessAws from "./util/Util_StoreLayout.res.mjs";
|
|
44
45
|
import * as IndexJs from "@pulumi/pulumi/runtime/index.js";
|
|
45
46
|
import * as AppSync_EventsApi$ReventlessAws from "./adapter/Api/AppSync_EventsApi.res.mjs";
|
|
@@ -129,6 +130,10 @@ function getObjectStoreEndpoints() {
|
|
|
129
130
|
return objectStoreEndpointsRef.contents;
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
let geocoderEndpointRef = {
|
|
134
|
+
contents: Pulumi.output("")
|
|
135
|
+
};
|
|
136
|
+
|
|
132
137
|
function MakeWithConfig(Config) {
|
|
133
138
|
Stdlib_Option.forEach(Config.commandHandlerConfig.aggregates, param => {
|
|
134
139
|
Stdlib_Option.forEach(param.sync, AggregateRuntime_Builder_Single$ReventlessAws.setConfig);
|
|
@@ -1123,6 +1128,9 @@ function MakeWithConfig(Config) {
|
|
|
1123
1128
|
let domainEventsEndpointOutput = domainEventsApiOpt !== undefined ? AppSync_EventsApi$ReventlessAws.httpEndpoint(domainEventsApiOpt).apply(ep => ep + "/event") : Pulumi.output(undefined);
|
|
1124
1129
|
let index = hostUiBundle.geocoderPlaceIndex;
|
|
1125
1130
|
let geocoderEndpointOutput = index !== undefined ? Geocoder_AwsLocation$ReventlessAws.make(index.indexName, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
|
|
1131
|
+
let geocoderEndpointFlat = geocoderEndpointOutput.apply(o => Stdlib_Option.getOr(o, ""));
|
|
1132
|
+
Pulumi$Pulumi.$$export("geocoderEndpoint", geocoderEndpointFlat);
|
|
1133
|
+
geocoderEndpointRef.contents = geocoderEndpointFlat;
|
|
1126
1134
|
let configJsonContent = Pulumi.all([
|
|
1127
1135
|
Pulumi.all([
|
|
1128
1136
|
resolvedDomainApiEndpoint,
|
|
@@ -1136,7 +1144,7 @@ function MakeWithConfig(Config) {
|
|
|
1136
1144
|
let geocoderEpOpt = param[2];
|
|
1137
1145
|
let eventsEpOpt = param[1];
|
|
1138
1146
|
let match = param[0];
|
|
1139
|
-
let
|
|
1147
|
+
let computed = [
|
|
1140
1148
|
[
|
|
1141
1149
|
"apiEndpoint",
|
|
1142
1150
|
match[0]
|
|
@@ -1166,7 +1174,7 @@ function MakeWithConfig(Config) {
|
|
|
1166
1174
|
true
|
|
1167
1175
|
]
|
|
1168
1176
|
];
|
|
1169
|
-
let withEvents = eventsEpOpt !== undefined ?
|
|
1177
|
+
let withEvents = eventsEpOpt !== undefined ? computed.concat([
|
|
1170
1178
|
[
|
|
1171
1179
|
"domainApiEventsEndpoint",
|
|
1172
1180
|
eventsEpOpt
|
|
@@ -1179,12 +1187,12 @@ function MakeWithConfig(Config) {
|
|
|
1179
1187
|
"clientEventsNamespace",
|
|
1180
1188
|
AppSync_EventsApi$ReventlessAws.clientNamespaceName
|
|
1181
1189
|
]
|
|
1182
|
-
]) :
|
|
1190
|
+
]) : computed;
|
|
1183
1191
|
let withGeocoder = geocoderEpOpt !== undefined ? withEvents.concat([[
|
|
1184
1192
|
"geocoderEndpoint",
|
|
1185
1193
|
geocoderEpOpt
|
|
1186
1194
|
]]) : withEvents;
|
|
1187
|
-
return JSON.stringify(
|
|
1195
|
+
return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withGeocoder, hostUiBundle.viewModes, hostUiBundle.shellConfig));
|
|
1188
1196
|
});
|
|
1189
1197
|
new (Aws.s3.BucketObject)("host-ui-config-json", {
|
|
1190
1198
|
bucket: bucketName,
|
|
@@ -1239,6 +1247,8 @@ function MakeWithConfig(Config) {
|
|
|
1239
1247
|
bytes: bytes.length
|
|
1240
1248
|
};
|
|
1241
1249
|
});
|
|
1250
|
+
let geocoderEndpoint = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("geocoderEndpoint").apply(o => Stdlib_Option.getOr(o, "")) : geocoderEndpointRef.contents;
|
|
1251
|
+
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("GEOCODER_ENDPOINT", geocoderEndpoint);
|
|
1242
1252
|
let pluginComponent = plugin.make();
|
|
1243
1253
|
Plugin_Helpers$ReventlessCore.clearOffload();
|
|
1244
1254
|
currentDeployTarget.contents = "Domain";
|
|
@@ -2349,6 +2359,9 @@ function Make($star) {
|
|
|
2349
2359
|
let domainEventsEndpointOutput = domainEventsApiOpt !== undefined ? AppSync_EventsApi$ReventlessAws.httpEndpoint(domainEventsApiOpt).apply(ep => ep + "/event") : Pulumi.output(undefined);
|
|
2350
2360
|
let index = hostUiBundle.geocoderPlaceIndex;
|
|
2351
2361
|
let geocoderEndpointOutput = index !== undefined ? Geocoder_AwsLocation$ReventlessAws.make(index.indexName, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
|
|
2362
|
+
let geocoderEndpointFlat = geocoderEndpointOutput.apply(o => Stdlib_Option.getOr(o, ""));
|
|
2363
|
+
Pulumi$Pulumi.$$export("geocoderEndpoint", geocoderEndpointFlat);
|
|
2364
|
+
geocoderEndpointRef.contents = geocoderEndpointFlat;
|
|
2352
2365
|
let configJsonContent = Pulumi.all([
|
|
2353
2366
|
Pulumi.all([
|
|
2354
2367
|
resolvedDomainApiEndpoint,
|
|
@@ -2362,7 +2375,7 @@ function Make($star) {
|
|
|
2362
2375
|
let geocoderEpOpt = param[2];
|
|
2363
2376
|
let eventsEpOpt = param[1];
|
|
2364
2377
|
let match = param[0];
|
|
2365
|
-
let
|
|
2378
|
+
let computed = [
|
|
2366
2379
|
[
|
|
2367
2380
|
"apiEndpoint",
|
|
2368
2381
|
match[0]
|
|
@@ -2392,7 +2405,7 @@ function Make($star) {
|
|
|
2392
2405
|
true
|
|
2393
2406
|
]
|
|
2394
2407
|
];
|
|
2395
|
-
let withEvents = eventsEpOpt !== undefined ?
|
|
2408
|
+
let withEvents = eventsEpOpt !== undefined ? computed.concat([
|
|
2396
2409
|
[
|
|
2397
2410
|
"domainApiEventsEndpoint",
|
|
2398
2411
|
eventsEpOpt
|
|
@@ -2405,12 +2418,12 @@ function Make($star) {
|
|
|
2405
2418
|
"clientEventsNamespace",
|
|
2406
2419
|
AppSync_EventsApi$ReventlessAws.clientNamespaceName
|
|
2407
2420
|
]
|
|
2408
|
-
]) :
|
|
2421
|
+
]) : computed;
|
|
2409
2422
|
let withGeocoder = geocoderEpOpt !== undefined ? withEvents.concat([[
|
|
2410
2423
|
"geocoderEndpoint",
|
|
2411
2424
|
geocoderEpOpt
|
|
2412
2425
|
]]) : withEvents;
|
|
2413
|
-
return JSON.stringify(
|
|
2426
|
+
return JSON.stringify(Util_ShellConfig$ReventlessAws.fields(withGeocoder, hostUiBundle.viewModes, hostUiBundle.shellConfig));
|
|
2414
2427
|
});
|
|
2415
2428
|
new (Aws.s3.BucketObject)("host-ui-config-json", {
|
|
2416
2429
|
bucket: bucketName,
|
|
@@ -2465,6 +2478,8 @@ function Make($star) {
|
|
|
2465
2478
|
bytes: bytes.length
|
|
2466
2479
|
};
|
|
2467
2480
|
});
|
|
2481
|
+
let geocoderEndpoint = platformStackRef !== undefined ? Primitive_option.valFromOption(platformStackRef).getOutput("geocoderEndpoint").apply(o => Stdlib_Option.getOr(o, "")) : geocoderEndpointRef.contents;
|
|
2482
|
+
PluginRuntime_Builder$ReventlessAws.registerCapabilityEnv("GEOCODER_ENDPOINT", geocoderEndpoint);
|
|
2468
2483
|
let pluginComponent = plugin.make();
|
|
2469
2484
|
Plugin_Helpers$ReventlessCore.clearOffload();
|
|
2470
2485
|
currentDeployTarget.contents = "Domain";
|
|
@@ -2616,6 +2631,7 @@ export {
|
|
|
2616
2631
|
getSplitApiOutputs,
|
|
2617
2632
|
objectStoreEndpointsRef,
|
|
2618
2633
|
getObjectStoreEndpoints,
|
|
2634
|
+
geocoderEndpointRef,
|
|
2619
2635
|
MakeWithConfig,
|
|
2620
2636
|
Make,
|
|
2621
2637
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Amazon Location as a geocoding transport for unattended callers.
|
|
2
|
+
//
|
|
3
|
+
// Policy — what a candidate is, the two ways a lookup fails, and when a ranked
|
|
4
|
+
// list is confident enough to store — lives in `Reventless.Geocoding`, shared
|
|
5
|
+
// with every other transport. This module only knows how to ask AWS and how to
|
|
6
|
+
// turn its answer into that vocabulary.
|
|
7
|
+
//
|
|
8
|
+
// The public Function URL handler next door answers `200 []` for a genuine
|
|
9
|
+
// no-match and `502 []` for a service failure, because a browser search box must
|
|
10
|
+
// degrade quietly while a translator must not. This module makes the same
|
|
11
|
+
// distinction at the source, for a caller that can reach the SDK directly.
|
|
12
|
+
//
|
|
13
|
+
// Runtime-pure: no Pulumi import, so it can be bundled into a Lambda entry point
|
|
14
|
+
// without dragging deploy-time modules into the cold-start graph.
|
|
15
|
+
|
|
16
|
+
module Search = AwsSdk.Location.SearchPlaceIndexForTextCommand
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
Geocode one address.
|
|
20
|
+
|
|
21
|
+
`Ok([])` is deliberately unreachable — an empty result set is `Error(NoMatch)`,
|
|
22
|
+
because "the call worked and the answer is nothing" is a different fact from
|
|
23
|
+
"the call worked", and the caller has to act differently on it.
|
|
24
|
+
|
|
25
|
+
Candidates come back in the provider's ranking order, unfiltered. Choosing among
|
|
26
|
+
them is `Reventless.Geocoding.confidentMatch`'s job.
|
|
27
|
+
*/
|
|
28
|
+
let search = async (~indexName: string, ~text: string, ~maxResults: int=5): result<
|
|
29
|
+
array<Reventless.Geocoding.candidate>,
|
|
30
|
+
Reventless.Geocoding.failure,
|
|
31
|
+
> => {
|
|
32
|
+
let trimmed = text->String.trim
|
|
33
|
+
if indexName == "" {
|
|
34
|
+
Error(Unavailable("no place index configured (PLACE_INDEX_NAME unset)"))
|
|
35
|
+
} else if trimmed == "" {
|
|
36
|
+
// Not a service failure and not a service verdict — an empty query was never
|
|
37
|
+
// going to match, and asking would spend a request to be told so.
|
|
38
|
+
Error(NoMatch)
|
|
39
|
+
} else {
|
|
40
|
+
switch await Search.send(Search.make({indexName, text: trimmed, maxResults})) {
|
|
41
|
+
| resp =>
|
|
42
|
+
let candidates =
|
|
43
|
+
resp.results
|
|
44
|
+
->Option.getOr([])
|
|
45
|
+
->Array.filterMap(r =>
|
|
46
|
+
switch r.place {
|
|
47
|
+
| None => None
|
|
48
|
+
| Some(place) =>
|
|
49
|
+
// `[lng, lat]` — RFC 7946 order, converted here and nowhere else.
|
|
50
|
+
switch place.geometry->Option.flatMap(g => g.point) {
|
|
51
|
+
| Some(pt) if pt->Array.length >= 2 =>
|
|
52
|
+
switch Reventless.GeoPoint.make(
|
|
53
|
+
~lat=pt->Array.getUnsafe(1),
|
|
54
|
+
~lng=pt->Array.getUnsafe(0),
|
|
55
|
+
) {
|
|
56
|
+
| Ok(point) =>
|
|
57
|
+
Some({
|
|
58
|
+
Reventless.Geocoding.label: place.label->Option.getOr(""),
|
|
59
|
+
point,
|
|
60
|
+
relevance: r.relevance,
|
|
61
|
+
})
|
|
62
|
+
// A coordinate that fails the range check is dropped rather than
|
|
63
|
+
// propagated: it cannot be stored, and one bad row should not lose
|
|
64
|
+
// the good ones beside it.
|
|
65
|
+
| Error(_) => None
|
|
66
|
+
}
|
|
67
|
+
| _ => None
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
candidates->Array.length == 0 ? Error(NoMatch) : Ok(candidates)
|
|
72
|
+
| exception exn =>
|
|
73
|
+
let msg = exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown")
|
|
74
|
+
Error(Unavailable(msg))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Location$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/Location.res.mjs";
|
|
7
|
+
import * as GeoPoint$Reventless from "@reventlessdev/reventless-spec/src/semantic/GeoPoint.res.mjs";
|
|
8
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
9
|
+
import * as ClientLocation from "@aws-sdk/client-location";
|
|
10
|
+
|
|
11
|
+
async function search(indexName, text, maxResultsOpt) {
|
|
12
|
+
let maxResults = maxResultsOpt !== undefined ? maxResultsOpt : 5;
|
|
13
|
+
let trimmed = text.trim();
|
|
14
|
+
if (indexName === "") {
|
|
15
|
+
return {
|
|
16
|
+
TAG: "Error",
|
|
17
|
+
_0: {
|
|
18
|
+
TAG: "Unavailable",
|
|
19
|
+
_0: "no place index configured (PLACE_INDEX_NAME unset)"
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (trimmed === "") {
|
|
24
|
+
return {
|
|
25
|
+
TAG: "Error",
|
|
26
|
+
_0: "NoMatch"
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let resp;
|
|
30
|
+
try {
|
|
31
|
+
resp = await Location$AwsSdk.SearchPlaceIndexForTextCommand.send(new ClientLocation.SearchPlaceIndexForTextCommand({
|
|
32
|
+
IndexName: indexName,
|
|
33
|
+
Text: trimmed,
|
|
34
|
+
MaxResults: maxResults
|
|
35
|
+
}));
|
|
36
|
+
} catch (raw_exn) {
|
|
37
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
38
|
+
let msg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "unknown");
|
|
39
|
+
return {
|
|
40
|
+
TAG: "Error",
|
|
41
|
+
_0: {
|
|
42
|
+
TAG: "Unavailable",
|
|
43
|
+
_0: msg
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
let candidates = Stdlib_Array.filterMap(Stdlib_Option.getOr(resp.Results, []), r => {
|
|
48
|
+
let place = r.Place;
|
|
49
|
+
if (place === undefined) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let pt = Stdlib_Option.flatMap(place.Geometry, g => g.Point);
|
|
53
|
+
if (pt === undefined) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (pt.length < 2) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
let point = GeoPoint$Reventless.make(pt[1], pt[0]);
|
|
60
|
+
if (point.TAG === "Ok") {
|
|
61
|
+
return {
|
|
62
|
+
label: Stdlib_Option.getOr(place.Label, ""),
|
|
63
|
+
point: point._0,
|
|
64
|
+
relevance: r.Relevance
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
if (candidates.length === 0) {
|
|
69
|
+
return {
|
|
70
|
+
TAG: "Error",
|
|
71
|
+
_0: "NoMatch"
|
|
72
|
+
};
|
|
73
|
+
} else {
|
|
74
|
+
return {
|
|
75
|
+
TAG: "Ok",
|
|
76
|
+
_0: candidates
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let Search;
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
Search,
|
|
85
|
+
search,
|
|
86
|
+
}
|
|
87
|
+
/* Stdlib_JsExn Not a pure module */
|
|
@@ -9,31 +9,9 @@
|
|
|
9
9
|
// `[{label, lat, lng}]` as JSON. Any failure degrades to an empty array so the
|
|
10
10
|
// caller never sees a hard error.
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
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"
|
|
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
|
|
37
15
|
|
|
38
16
|
@val external decodeURIComponent: string => string = "decodeURIComponent"
|
|
39
17
|
|
|
@@ -94,13 +72,22 @@ let handler = async (event: functionUrlEvent): response => {
|
|
|
94
72
|
try {
|
|
95
73
|
let indexName = getEnv("PLACE_INDEX_NAME")->Option.getOr("")
|
|
96
74
|
let q = readQueryParam(event)->Option.getOr("")
|
|
97
|
-
if indexName == ""
|
|
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.
|
|
98
88
|
{statusCode: 200, headers: jsonHeaders(), body: "[]"}
|
|
99
89
|
} else {
|
|
100
|
-
let
|
|
101
|
-
let resp = await client->send(
|
|
102
|
-
makeSearchPlaceIndexForTextCommand({indexName, text: q, maxResults: 5}),
|
|
103
|
-
)
|
|
90
|
+
let resp = await Search.send(Search.make({indexName, text: q, maxResults: 5}))
|
|
104
91
|
let results =
|
|
105
92
|
resp.results
|
|
106
93
|
->Option.getOr([])
|
|
@@ -113,11 +100,23 @@ let handler = async (event: functionUrlEvent): response => {
|
|
|
113
100
|
let lng = pt->Array.getUnsafe(0)
|
|
114
101
|
let lat = pt->Array.getUnsafe(1)
|
|
115
102
|
Some(
|
|
116
|
-
Dict.fromArray(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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,
|
|
121
120
|
)
|
|
122
121
|
| _ => None
|
|
123
122
|
}
|
|
@@ -133,6 +132,13 @@ let handler = async (event: functionUrlEvent): response => {
|
|
|
133
132
|
} catch {
|
|
134
133
|
| exn =>
|
|
135
134
|
Console.error2("Geocoder: search failed", exn)
|
|
136
|
-
|
|
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: "[]"}
|
|
137
143
|
}
|
|
138
144
|
}
|