@reventlessdev/reventless-aws 3.0.0-alpha.227 → 3.0.0-alpha.229
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 +12 -9
- package/src/Platform.res +98 -9
- package/src/Platform.res.mjs +73 -8
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res +233 -0
- package/src/adapter/Geocoder/Geocoder_AwsLocation.res.mjs +168 -0
- package/src/adapter/Mcp/MCP_Lambda.res +8 -7
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res +7 -1
- package/src/adapter/Runtime/RuntimeEnvironment_Lambda.res.mjs +4 -1
- package/src/adapter/Runtime/StateViewSliceEntryPoint.mjs +7 -1
- package/src/adapter/Task/TaskBucket_S3.res +2 -2
- package/src/adapter/Task/TaskBucket_S3.res.mjs +4 -2
- package/src/adapter/Upload/Upload_Presign_S3.res +258 -0
- package/src/adapter/Upload/Upload_Presign_S3.res.mjs +170 -0
- package/src/plugin/stack/Plugin_Stack.res +71 -8
- package/src/plugin/stack/Plugin_Stack.res.mjs +57 -8
- package/src/util/Util_DynamoDbStream_Runtime.res +6 -0
- package/src/util/Util_DynamoDbStream_Runtime.res.mjs +5 -0
- package/src/util/Util_StaticBundle.res +21 -0
- package/src/util/Util_StaticBundle.res.mjs +14 -0
- package/tests/Util_StaticBundleTest.res +40 -0
- package/tests/Util_StaticBundleTest.res.mjs +44 -0
- package/tests/integration/SvsTestSlice_Projection.res +1 -1
- package/tests/integration/SvsTestSlice_Projection.res.mjs +2 -1
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.229 (2026-07-26)
|
|
7
|
+
|
|
8
|
+
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# 3.0.0-alpha.228 (2026-07-26)
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **auto-ui:** write ui-hints.json from the host-ui AWS deploy ([cbb334c](https://github.com/ReventlessDev/reventless-core/commit/cbb334c5bc574d7be56746100bae93457a8831ce)), closes [#1](https://github.com/ReventlessDev/reventless-core/issues/1)
|
|
19
|
+
* **aws:** geocoder + upload presign Lambda Function URL services ([be43179](https://github.com/ReventlessDev/reventless-core/commit/be4317972e781dbe3c378f21a28975d1ac8b9add))
|
|
20
|
+
* **aws:** presign service returns the object URL as storageRef ([ec551e6](https://github.com/ReventlessDev/reventless-core/commit/ec551e69cfa82c0fa80122ebe122f827ddf280ec))
|
|
21
|
+
* **dcb:** emit decision-model cache hit/miss + delta metrics ([967fb7f](https://github.com/ReventlessDev/reventless-core/commit/967fb7fb42f00d22b9dd6f6261e2f5308a6e9516))
|
|
22
|
+
* **platform:** serve a private bucket to the UI under a same-origin prefix ([793c8b2](https://github.com/ReventlessDev/reventless-core/commit/793c8b25e13969b42ce331c698bad473be4c0c88))
|
|
23
|
+
|
|
24
|
+
|
|
6
25
|
# 3.0.0-alpha.227 (2026-07-24)
|
|
7
26
|
|
|
8
27
|
**Note:** Version bump only for package @reventlessdev/reventless-aws
|
package/package.json
CHANGED
|
@@ -1,24 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-aws",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.229",
|
|
4
4
|
"description": "AWS adapters for Reventless",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@aws-sdk/client-appsync": "3.970.0",
|
|
8
8
|
"@aws-sdk/client-cloudfront": "3.970.0",
|
|
9
|
+
"@aws-sdk/client-location": "3.970.0",
|
|
10
|
+
"@aws-sdk/client-s3": "3.970.0",
|
|
11
|
+
"@aws-sdk/s3-request-presigner": "3.970.0",
|
|
9
12
|
"sury": "11.0.0-alpha.4",
|
|
10
13
|
"uuid": "^13.0.0",
|
|
11
|
-
"@reventlessdev/rescript-effect": "0.1.0-alpha.30",
|
|
12
14
|
"@reventlessdev/rescript-aws-sdk": "2.2.0-alpha.24",
|
|
13
|
-
"@reventlessdev/rescript-
|
|
14
|
-
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.
|
|
15
|
+
"@reventlessdev/rescript-effect": "0.1.0-alpha.31",
|
|
16
|
+
"@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.60",
|
|
15
17
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
|
|
16
|
-
"@reventlessdev/
|
|
17
|
-
"@reventlessdev/reventless-infra": "3.0.0-alpha.104",
|
|
18
|
-
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
18
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.9",
|
|
19
19
|
"@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
|
|
20
|
-
"@reventlessdev/reventless-
|
|
21
|
-
"@reventlessdev/reventless-
|
|
20
|
+
"@reventlessdev/reventless-core": "3.0.0-alpha.182",
|
|
21
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.29",
|
|
22
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.105",
|
|
23
|
+
"@reventlessdev/reventless-postgres": "3.0.0-alpha.46",
|
|
24
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.81"
|
|
22
25
|
},
|
|
23
26
|
"devDependencies": {
|
|
24
27
|
"rescript": "12.3.0",
|
package/src/Platform.res
CHANGED
|
@@ -1001,6 +1001,30 @@ module MakeWithConfig = (
|
|
|
1001
1001
|
type hostUiBundleConfig = {
|
|
1002
1002
|
assetsDir: string,
|
|
1003
1003
|
bundleVersion: string,
|
|
1004
|
+
// Optional path to a static AutoUI `ui-hints.json` (assistant-authored,
|
|
1005
|
+
// plain data — no app rebuild). When set, the deploy reads it and writes it
|
|
1006
|
+
// verbatim as a `ui-hints.json` BucketObject beside `config.json`, so the
|
|
1007
|
+
// shell's static-hints path works in a deployed app. Unset ⇒ no file
|
|
1008
|
+
// written; the shell treats the resulting 404 as "no hints" and boots
|
|
1009
|
+
// unchanged. Single-mode shape: one origin-relative file per deployment.
|
|
1010
|
+
uiHintsFile?: string,
|
|
1011
|
+
// Optional AWS Location place-index name. When set, the deploy provisions a
|
|
1012
|
+
// public geocoder Lambda Function URL (Geocoder_AwsLocation) and threads its
|
|
1013
|
+
// URL into config.json as `geocoderEndpoint`. Unset ⇒ no service, field
|
|
1014
|
+
// omitted (byte-identical config.json).
|
|
1015
|
+
geocoderPlaceIndex?: Pulumi.Input.t<string>,
|
|
1016
|
+
// Opt into the direct-to-S3 upload presign service (Upload_Presign_S3). Both
|
|
1017
|
+
// this flag and `uploadBucketName` must be set; the deploy then provisions a
|
|
1018
|
+
// public presign Lambda Function URL and threads its URL into config.json as
|
|
1019
|
+
// `uploadEndpoint`. Unset ⇒ no service, field omitted.
|
|
1020
|
+
enableUploads?: bool,
|
|
1021
|
+
// S3 bucket the upload presign service issues PUT URLs against. Required when
|
|
1022
|
+
// `enableUploads` is true.
|
|
1023
|
+
uploadBucketName?: Pulumi.Input.t<string>,
|
|
1024
|
+
// Buckets served read-only to the UI under a path prefix on the host-shell
|
|
1025
|
+
// CloudFront distribution (same-origin, private, OAC-read). Each entry adds a
|
|
1026
|
+
// `{prefix}/*` ordered cache behavior + origin + scoped BucketPolicy.
|
|
1027
|
+
servedBuckets?: array<ReventlessInfra.Platform.servedBucket>,
|
|
1004
1028
|
}
|
|
1005
1029
|
|
|
1006
1030
|
// Merged-mode outputs of deployPlatform — the merged API(s), plus the
|
|
@@ -1465,13 +1489,19 @@ module MakeWithConfig = (
|
|
|
1465
1489
|
~assetsDir=cfg.assetsDir,
|
|
1466
1490
|
~spaFallback=true,
|
|
1467
1491
|
~stableName=true,
|
|
1468
|
-
// The explicit
|
|
1469
|
-
//
|
|
1470
|
-
// bundle's public/config.json
|
|
1471
|
-
//
|
|
1472
|
-
// the
|
|
1473
|
-
|
|
1492
|
+
// The explicit BucketObjects below write the production config.json
|
|
1493
|
+
// (resolved API endpoints + Cognito IDs) and, when configured, a
|
|
1494
|
+
// ui-hints.json. The host-shell bundle's public/config.json and
|
|
1495
|
+
// public/ui-hints.json are dev-mode fallbacks — exclude them from the
|
|
1496
|
+
// bundle upload so the explicit BucketObjects don't race the bundle on
|
|
1497
|
+
// the same S3 keys.
|
|
1498
|
+
~excludeFiles=["config.json", "ui-hints.json"],
|
|
1474
1499
|
~customDomain?,
|
|
1500
|
+
// Served buckets front the host-shell distribution with `{prefix}/*`
|
|
1501
|
+
// read paths (private, OAC-read) — same origin as the SPA, so served
|
|
1502
|
+
// objects are addressable by same-origin relative URL with no config.json
|
|
1503
|
+
// change. Unset ⇒ [] ⇒ distribution byte-identical to today.
|
|
1504
|
+
~servedBuckets=cfg.servedBuckets->Option.getOr([]),
|
|
1475
1505
|
)
|
|
1476
1506
|
|
|
1477
1507
|
let regionStr =
|
|
@@ -1494,6 +1524,27 @@ module MakeWithConfig = (
|
|
|
1494
1524
|
| None => Pulumi.Output.make(None)
|
|
1495
1525
|
}
|
|
1496
1526
|
|
|
1527
|
+
// Optional public geocoder Function URL. Provisioned only when a place
|
|
1528
|
+
// index is configured; its resolved URL becomes config.json's
|
|
1529
|
+
// `geocoderEndpoint`. Unset ⇒ Output<None> ⇒ field omitted below.
|
|
1530
|
+
let geocoderEndpointOutput: Pulumi.Output.t<option<string>> = switch cfg.geocoderPlaceIndex {
|
|
1531
|
+
| Some(placeIndexName) =>
|
|
1532
|
+
Geocoder_AwsLocation.make(~placeIndexName).url->Pulumi.Output.apply(u => Some(u))
|
|
1533
|
+
| None => Pulumi.Output.make(None)
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Optional public upload presign Function URL. Needs both the opt-in flag
|
|
1537
|
+
// and a target bucket; its resolved URL becomes config.json's
|
|
1538
|
+
// `uploadEndpoint`. Otherwise Output<None> ⇒ field omitted below.
|
|
1539
|
+
let uploadEndpointOutput: Pulumi.Output.t<option<string>> = switch (
|
|
1540
|
+
cfg.enableUploads,
|
|
1541
|
+
cfg.uploadBucketName,
|
|
1542
|
+
) {
|
|
1543
|
+
| (Some(true), Some(bucketName)) =>
|
|
1544
|
+
Upload_Presign_S3.make(~bucketName).url->Pulumi.Output.apply(u => Some(u))
|
|
1545
|
+
| _ => Pulumi.Output.make(None)
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1497
1548
|
let configJsonContent =
|
|
1498
1549
|
(
|
|
1499
1550
|
(
|
|
@@ -1503,9 +1554,16 @@ module MakeWithConfig = (
|
|
|
1503
1554
|
cognitoPool.clientId,
|
|
1504
1555
|
)->Pulumi.Output.all4,
|
|
1505
1556
|
domainEventsEndpointOutput,
|
|
1557
|
+
geocoderEndpointOutput,
|
|
1558
|
+
uploadEndpointOutput,
|
|
1506
1559
|
)
|
|
1507
|
-
->Pulumi.Output.
|
|
1508
|
-
->Pulumi.Output.apply(((
|
|
1560
|
+
->Pulumi.Output.all4
|
|
1561
|
+
->Pulumi.Output.apply(((
|
|
1562
|
+
(domainEp, platformEp, poolId, clientId),
|
|
1563
|
+
eventsEpOpt,
|
|
1564
|
+
geocoderEpOpt,
|
|
1565
|
+
uploadEpOpt,
|
|
1566
|
+
)) => {
|
|
1509
1567
|
let fields = [
|
|
1510
1568
|
("apiEndpoint", JSON.Encode.string(domainEp)),
|
|
1511
1569
|
("platformApiEndpoint", JSON.Encode.string(platformEp)),
|
|
@@ -1526,7 +1584,15 @@ module MakeWithConfig = (
|
|
|
1526
1584
|
)
|
|
1527
1585
|
| None => fields
|
|
1528
1586
|
}
|
|
1529
|
-
|
|
1587
|
+
let withGeocoder = switch geocoderEpOpt {
|
|
1588
|
+
| Some(ep) => Array.concat(withEvents, [("geocoderEndpoint", JSON.Encode.string(ep))])
|
|
1589
|
+
| None => withEvents
|
|
1590
|
+
}
|
|
1591
|
+
let withUpload = switch uploadEpOpt {
|
|
1592
|
+
| Some(ep) => Array.concat(withGeocoder, [("uploadEndpoint", JSON.Encode.string(ep))])
|
|
1593
|
+
| None => withGeocoder
|
|
1594
|
+
}
|
|
1595
|
+
withUpload->Dict.fromArray->JSON.Encode.object->JSON.stringify
|
|
1530
1596
|
})
|
|
1531
1597
|
|
|
1532
1598
|
let _ = PulumiAws.S3.BucketObject.make(
|
|
@@ -1539,6 +1605,29 @@ module MakeWithConfig = (
|
|
|
1539
1605
|
},
|
|
1540
1606
|
)
|
|
1541
1607
|
|
|
1608
|
+
// Optional static AutoUI hints. Read + JSON-validated at deploy time
|
|
1609
|
+
// (a malformed file fails the deploy rather than shipping a file the
|
|
1610
|
+
// shell warn-and-ignores), then written verbatim beside config.json.
|
|
1611
|
+
// Unset ⇒ nothing written ⇒ GET /ui-hints.json 404 ⇒ shell boots
|
|
1612
|
+
// unchanged, byte-identical to a hints-less deploy.
|
|
1613
|
+
switch cfg.uiHintsFile {
|
|
1614
|
+
| None => ()
|
|
1615
|
+
| Some(hintsPath) =>
|
|
1616
|
+
let hintsContent = Util_StaticBundle.readJsonFileVerbatim(
|
|
1617
|
+
~path=hintsPath,
|
|
1618
|
+
~label="host-ui ui-hints",
|
|
1619
|
+
)
|
|
1620
|
+
let _ = PulumiAws.S3.BucketObject.make(
|
|
1621
|
+
~name="host-ui-ui-hints-json",
|
|
1622
|
+
~args={
|
|
1623
|
+
bucket: bucketName->Pulumi.Output.asInput,
|
|
1624
|
+
key: Pulumi.Input.make("ui-hints.json"),
|
|
1625
|
+
content: Pulumi.Input.make(hintsContent),
|
|
1626
|
+
contentType: Pulumi.Input.make("application/json"),
|
|
1627
|
+
},
|
|
1628
|
+
)
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1542
1631
|
Pulumi.Pulumi.export("hostShellUrl", distributionUrl)
|
|
1543
1632
|
}
|
|
1544
1633
|
|
package/src/Platform.res.mjs
CHANGED
|
@@ -40,7 +40,9 @@ import * as AppSync_EventsApi$ReventlessAws from "./adapter/Api/AppSync_EventsAp
|
|
|
40
40
|
import * as AppSync_MergedApi$ReventlessAws from "./components/Api/AppSync_MergedApi.res.mjs";
|
|
41
41
|
import * as GraphQL_Stitcher$ReventlessCore from "@reventlessdev/reventless-core/src/components/Api/GraphQL_Stitcher.res.mjs";
|
|
42
42
|
import * as NoEventMappings$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/NoEventMappings.res.mjs";
|
|
43
|
+
import * as Upload_Presign_S3$ReventlessAws from "./adapter/Upload/Upload_Presign_S3.res.mjs";
|
|
43
44
|
import * as Util_HostUiDomain$ReventlessAws from "./util/Util_HostUiDomain.res.mjs";
|
|
45
|
+
import * as Util_StaticBundle$ReventlessAws from "./util/Util_StaticBundle.res.mjs";
|
|
44
46
|
import * as DcbEventLogStorage$ReventlessAws from "./adapter/DcbEventLog/DcbEventLogStorage.res.mjs";
|
|
45
47
|
import * as ExtensionMapping$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/ExtensionMapping.res.mjs";
|
|
46
48
|
import * as Platform_AdminApi$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_AdminApi.res.mjs";
|
|
@@ -50,6 +52,7 @@ import * as AppSync_SdlDecorate$ReventlessAws from "./components/Api/AppSync_Sdl
|
|
|
50
52
|
import * as UiFragmentRegistry$ReventlessCore from "@reventlessdev/reventless-core/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs";
|
|
51
53
|
import * as Util_ResourceNaming$ReventlessAws from "./util/Util_ResourceNaming.res.mjs";
|
|
52
54
|
import * as ClonerRunner_Fargate$ReventlessAws from "./plugin/cloner/ClonerRunner_Fargate.res.mjs";
|
|
55
|
+
import * as Geocoder_AwsLocation$ReventlessAws from "./adapter/Geocoder/Geocoder_AwsLocation.res.mjs";
|
|
53
56
|
import * as QueryEngine_DynamoDb$ReventlessAws from "./adapter/QueryEngine/QueryEngine_DynamoDb.res.mjs";
|
|
54
57
|
import * as PluginRuntime_Builder$ReventlessAws from "./plugin/runtime/PluginRuntime_Builder.res.mjs";
|
|
55
58
|
import * as PluginsReadModelSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs";
|
|
@@ -880,10 +883,19 @@ function MakeWithConfig(Config) {
|
|
|
880
883
|
} else {
|
|
881
884
|
customDomain = undefined;
|
|
882
885
|
}
|
|
883
|
-
let match$3 = Plugin_Stack$ReventlessAws.makeUiBundleDistribution("host-ui", hostUiBundle.bundleVersion, hostUiBundle.assetsDir, true, undefined, true, [
|
|
886
|
+
let match$3 = Plugin_Stack$ReventlessAws.makeUiBundleDistribution("host-ui", hostUiBundle.bundleVersion, hostUiBundle.assetsDir, true, undefined, true, [
|
|
887
|
+
"config.json",
|
|
888
|
+
"ui-hints.json"
|
|
889
|
+
], customDomain, Stdlib_Option.getOr(hostUiBundle.servedBuckets, []));
|
|
890
|
+
let bucketName = match$3.bucketName;
|
|
884
891
|
let regionStr = Stdlib_Option.getOr(new Pulumi.Config("aws").get("region"), "unknown");
|
|
885
892
|
let cognitoPool = Platform_Stack$ReventlessAws.resolveCognitoUserPool();
|
|
886
893
|
let domainEventsEndpointOutput = domainEventsApiOpt !== undefined ? AppSync_EventsApi$ReventlessAws.httpEndpoint(domainEventsApiOpt).apply(ep => ep + "/event") : Pulumi.output(undefined);
|
|
894
|
+
let placeIndexName = hostUiBundle.geocoderPlaceIndex;
|
|
895
|
+
let geocoderEndpointOutput = placeIndexName !== undefined ? Geocoder_AwsLocation$ReventlessAws.make(placeIndexName, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
|
|
896
|
+
let match$4 = hostUiBundle.enableUploads;
|
|
897
|
+
let match$5 = hostUiBundle.uploadBucketName;
|
|
898
|
+
let uploadEndpointOutput = match$4 !== undefined && match$4 && match$5 !== undefined ? Upload_Presign_S3$ReventlessAws.make(match$5, undefined, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
|
|
887
899
|
let configJsonContent = Pulumi.all([
|
|
888
900
|
Pulumi.all([
|
|
889
901
|
resolvedDomainApiEndpoint,
|
|
@@ -891,8 +903,12 @@ function MakeWithConfig(Config) {
|
|
|
891
903
|
cognitoPool.poolId,
|
|
892
904
|
cognitoPool.clientId
|
|
893
905
|
]),
|
|
894
|
-
domainEventsEndpointOutput
|
|
906
|
+
domainEventsEndpointOutput,
|
|
907
|
+
geocoderEndpointOutput,
|
|
908
|
+
uploadEndpointOutput
|
|
895
909
|
]).apply(param => {
|
|
910
|
+
let uploadEpOpt = param[3];
|
|
911
|
+
let geocoderEpOpt = param[2];
|
|
896
912
|
let eventsEpOpt = param[1];
|
|
897
913
|
let match = param[0];
|
|
898
914
|
let fields = [
|
|
@@ -935,14 +951,32 @@ function MakeWithConfig(Config) {
|
|
|
935
951
|
eventsEpOpt
|
|
936
952
|
]
|
|
937
953
|
]) : fields;
|
|
938
|
-
|
|
954
|
+
let withGeocoder = geocoderEpOpt !== undefined ? withEvents.concat([[
|
|
955
|
+
"geocoderEndpoint",
|
|
956
|
+
geocoderEpOpt
|
|
957
|
+
]]) : withEvents;
|
|
958
|
+
let withUpload = uploadEpOpt !== undefined ? withGeocoder.concat([[
|
|
959
|
+
"uploadEndpoint",
|
|
960
|
+
uploadEpOpt
|
|
961
|
+
]]) : withGeocoder;
|
|
962
|
+
return JSON.stringify(Object.fromEntries(withUpload));
|
|
939
963
|
});
|
|
940
964
|
new (Aws.s3.BucketObject)("host-ui-config-json", {
|
|
941
|
-
bucket:
|
|
965
|
+
bucket: bucketName,
|
|
942
966
|
key: "config.json",
|
|
943
967
|
content: configJsonContent,
|
|
944
968
|
contentType: "application/json"
|
|
945
969
|
});
|
|
970
|
+
let hintsPath = hostUiBundle.uiHintsFile;
|
|
971
|
+
if (hintsPath !== undefined) {
|
|
972
|
+
let hintsContent = Util_StaticBundle$ReventlessAws.readJsonFileVerbatim(hintsPath, "host-ui ui-hints");
|
|
973
|
+
new (Aws.s3.BucketObject)("host-ui-ui-hints-json", {
|
|
974
|
+
bucket: bucketName,
|
|
975
|
+
key: "ui-hints.json",
|
|
976
|
+
content: hintsContent,
|
|
977
|
+
contentType: "application/json"
|
|
978
|
+
});
|
|
979
|
+
}
|
|
946
980
|
Pulumi$Pulumi.$$export("hostShellUrl", match$3.distributionUrl);
|
|
947
981
|
}
|
|
948
982
|
return Pulumi$Pulumi.getOutputs();
|
|
@@ -1832,10 +1866,19 @@ function Make($star) {
|
|
|
1832
1866
|
} else {
|
|
1833
1867
|
customDomain = undefined;
|
|
1834
1868
|
}
|
|
1835
|
-
let match$3 = Plugin_Stack$ReventlessAws.makeUiBundleDistribution("host-ui", hostUiBundle.bundleVersion, hostUiBundle.assetsDir, true, undefined, true, [
|
|
1869
|
+
let match$3 = Plugin_Stack$ReventlessAws.makeUiBundleDistribution("host-ui", hostUiBundle.bundleVersion, hostUiBundle.assetsDir, true, undefined, true, [
|
|
1870
|
+
"config.json",
|
|
1871
|
+
"ui-hints.json"
|
|
1872
|
+
], customDomain, Stdlib_Option.getOr(hostUiBundle.servedBuckets, []));
|
|
1873
|
+
let bucketName = match$3.bucketName;
|
|
1836
1874
|
let regionStr = Stdlib_Option.getOr(new Pulumi.Config("aws").get("region"), "unknown");
|
|
1837
1875
|
let cognitoPool = Platform_Stack$ReventlessAws.resolveCognitoUserPool();
|
|
1838
1876
|
let domainEventsEndpointOutput = domainEventsApiOpt !== undefined ? AppSync_EventsApi$ReventlessAws.httpEndpoint(domainEventsApiOpt).apply(ep => ep + "/event") : Pulumi.output(undefined);
|
|
1877
|
+
let placeIndexName = hostUiBundle.geocoderPlaceIndex;
|
|
1878
|
+
let geocoderEndpointOutput = placeIndexName !== undefined ? Geocoder_AwsLocation$ReventlessAws.make(placeIndexName, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
|
|
1879
|
+
let match$4 = hostUiBundle.enableUploads;
|
|
1880
|
+
let match$5 = hostUiBundle.uploadBucketName;
|
|
1881
|
+
let uploadEndpointOutput = match$4 !== undefined && match$4 && match$5 !== undefined ? Upload_Presign_S3$ReventlessAws.make(match$5, undefined, undefined, undefined).url.apply(u => u) : Pulumi.output(undefined);
|
|
1839
1882
|
let configJsonContent = Pulumi.all([
|
|
1840
1883
|
Pulumi.all([
|
|
1841
1884
|
resolvedDomainApiEndpoint,
|
|
@@ -1843,8 +1886,12 @@ function Make($star) {
|
|
|
1843
1886
|
cognitoPool.poolId,
|
|
1844
1887
|
cognitoPool.clientId
|
|
1845
1888
|
]),
|
|
1846
|
-
domainEventsEndpointOutput
|
|
1889
|
+
domainEventsEndpointOutput,
|
|
1890
|
+
geocoderEndpointOutput,
|
|
1891
|
+
uploadEndpointOutput
|
|
1847
1892
|
]).apply(param => {
|
|
1893
|
+
let uploadEpOpt = param[3];
|
|
1894
|
+
let geocoderEpOpt = param[2];
|
|
1848
1895
|
let eventsEpOpt = param[1];
|
|
1849
1896
|
let match = param[0];
|
|
1850
1897
|
let fields = [
|
|
@@ -1887,14 +1934,32 @@ function Make($star) {
|
|
|
1887
1934
|
eventsEpOpt
|
|
1888
1935
|
]
|
|
1889
1936
|
]) : fields;
|
|
1890
|
-
|
|
1937
|
+
let withGeocoder = geocoderEpOpt !== undefined ? withEvents.concat([[
|
|
1938
|
+
"geocoderEndpoint",
|
|
1939
|
+
geocoderEpOpt
|
|
1940
|
+
]]) : withEvents;
|
|
1941
|
+
let withUpload = uploadEpOpt !== undefined ? withGeocoder.concat([[
|
|
1942
|
+
"uploadEndpoint",
|
|
1943
|
+
uploadEpOpt
|
|
1944
|
+
]]) : withGeocoder;
|
|
1945
|
+
return JSON.stringify(Object.fromEntries(withUpload));
|
|
1891
1946
|
});
|
|
1892
1947
|
new (Aws.s3.BucketObject)("host-ui-config-json", {
|
|
1893
|
-
bucket:
|
|
1948
|
+
bucket: bucketName,
|
|
1894
1949
|
key: "config.json",
|
|
1895
1950
|
content: configJsonContent,
|
|
1896
1951
|
contentType: "application/json"
|
|
1897
1952
|
});
|
|
1953
|
+
let hintsPath = hostUiBundle.uiHintsFile;
|
|
1954
|
+
if (hintsPath !== undefined) {
|
|
1955
|
+
let hintsContent = Util_StaticBundle$ReventlessAws.readJsonFileVerbatim(hintsPath, "host-ui ui-hints");
|
|
1956
|
+
new (Aws.s3.BucketObject)("host-ui-ui-hints-json", {
|
|
1957
|
+
bucket: bucketName,
|
|
1958
|
+
key: "ui-hints.json",
|
|
1959
|
+
content: hintsContent,
|
|
1960
|
+
contentType: "application/json"
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1898
1963
|
Pulumi$Pulumi.$$export("hostShellUrl", match$3.distributionUrl);
|
|
1899
1964
|
}
|
|
1900
1965
|
return Pulumi$Pulumi.getOutputs();
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// AWS Location Service geocoder behind a public Lambda Function URL.
|
|
2
|
+
//
|
|
3
|
+
// Deploy-time: `make` provisions a CallbackFunction (its handler closure is
|
|
4
|
+
// serialized by Pulumi), an IAM execution role scoped to
|
|
5
|
+
// `geo:SearchPlaceIndexForText` on the target place index, and a Function URL
|
|
6
|
+
// (no auth) so a browser can geocode directly.
|
|
7
|
+
//
|
|
8
|
+
// Runtime: `handleGeocode` reads a `q` query-string param from the Function URL
|
|
9
|
+
// event, calls SearchPlaceIndexForText against the place index named in
|
|
10
|
+
// `PLACE_INDEX_NAME`, and returns `[{label, lat, lng}]` as JSON. Any failure
|
|
11
|
+
// degrades to an empty array so the caller never sees a hard error.
|
|
12
|
+
|
|
13
|
+
open PulumiAws
|
|
14
|
+
|
|
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
|
+
type serviceOutputs = {
|
|
137
|
+
url: Pulumi.Output.t<string>,
|
|
138
|
+
resources: array<Pulumi.Output.t<string>>,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let make = (
|
|
142
|
+
~placeIndexName: Pulumi.Input.t<string>,
|
|
143
|
+
~corsOrigins: array<string>=["*"],
|
|
144
|
+
~opts=?,
|
|
145
|
+
): serviceOutputs => {
|
|
146
|
+
let serviceName = "GeocoderService"
|
|
147
|
+
let opts =
|
|
148
|
+
opts->Option.map(ReventlessCore.Util.Pulumi.ComponentResourceOptions.toCustomResourceOptions)
|
|
149
|
+
|
|
150
|
+
let lambdaRole = IAM.Role.makeWithDefaultPolicy(
|
|
151
|
+
~name=serviceName,
|
|
152
|
+
~servicePrincipal=AWS.Lambda.principal->Pulumi.Output.make,
|
|
153
|
+
~tags=AWS.Tags.make(
|
|
154
|
+
~name=serviceName,
|
|
155
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
156
|
+
~role=Identity,
|
|
157
|
+
~scope=Platform,
|
|
158
|
+
),
|
|
159
|
+
~opts?,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
// Least-privilege: SearchPlaceIndexForText on the one place index.
|
|
163
|
+
let _policy =
|
|
164
|
+
placeIndexName
|
|
165
|
+
->Pulumi.Output.fromInput
|
|
166
|
+
->Pulumi.Output.apply(idx => {
|
|
167
|
+
let arn = `arn:aws:geo:*:*:place-index/${idx}`
|
|
168
|
+
let _ = IAM.RolePolicy.make(
|
|
169
|
+
~name=`${serviceName}Policy`,
|
|
170
|
+
~args={
|
|
171
|
+
policy: PolicyDocument.make(
|
|
172
|
+
~id=`${serviceName}Policy`,
|
|
173
|
+
~statements=[
|
|
174
|
+
{
|
|
175
|
+
sid: "AllowGeocode",
|
|
176
|
+
effect: Allow,
|
|
177
|
+
actions: Action("geo:SearchPlaceIndexForText"),
|
|
178
|
+
resources: Resource(arn),
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
)
|
|
182
|
+
->PolicyDocument.toJsonString
|
|
183
|
+
->Pulumi.Input.make,
|
|
184
|
+
role: lambdaRole.id->Pulumi.Output.asInput,
|
|
185
|
+
},
|
|
186
|
+
~opts?,
|
|
187
|
+
)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
let environment: Lambda.CallbackFunction.Args.functionEnvironment = {
|
|
191
|
+
// CallbackFunction under-types env values as `dict<string>`; Pulumi resolves
|
|
192
|
+
// Output-valued variables at deploy time, so bridge the Input here.
|
|
193
|
+
variables: Dict.fromArray([("PLACE_INDEX_NAME", placeIndexName)])->Obj.magic,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let lambda = Lambda.CallbackFunction.make(
|
|
197
|
+
~name=serviceName,
|
|
198
|
+
~args=Lambda.CallbackFunction.Args.make(
|
|
199
|
+
~callback=handleGeocode,
|
|
200
|
+
~role=lambdaRole,
|
|
201
|
+
~environment,
|
|
202
|
+
~timeout=30->Pulumi.Input.make,
|
|
203
|
+
~tags=AWS.Tags.make(
|
|
204
|
+
~name=serviceName,
|
|
205
|
+
~kind=ReventlessCore.ComponentType.Platform,
|
|
206
|
+
~role=Runtime,
|
|
207
|
+
~scope=Platform,
|
|
208
|
+
),
|
|
209
|
+
),
|
|
210
|
+
~opts?,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
let functionUrl = FunctionUrl.make(
|
|
214
|
+
~name=`${serviceName}Url`,
|
|
215
|
+
~args={
|
|
216
|
+
authorizationType: FunctionUrl.None,
|
|
217
|
+
functionName: lambda.name->Pulumi.Output.asInput,
|
|
218
|
+
cors: (
|
|
219
|
+
{
|
|
220
|
+
allowMethods: ["GET"]->Array.map(Pulumi.Input.make)->Pulumi.Input.make,
|
|
221
|
+
allowOrigins: corsOrigins->Array.map(Pulumi.Input.make)->Pulumi.Input.make,
|
|
222
|
+
allowHeaders: ["*"]->Array.map(Pulumi.Input.make)->Pulumi.Input.make,
|
|
223
|
+
}: FunctionUrl.cors
|
|
224
|
+
)->Pulumi.Input.make,
|
|
225
|
+
},
|
|
226
|
+
~opts?,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
{
|
|
230
|
+
url: functionUrl.functionUrl,
|
|
231
|
+
resources: [lambda.arn, functionUrl.functionArn],
|
|
232
|
+
}
|
|
233
|
+
}
|