@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 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.227",
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-jest": "1.0.0-alpha.9",
14
- "@reventlessdev/rescript-pulumi-aws": "2.4.0-alpha.59",
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/reventless-core": "3.0.0-alpha.180",
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-postgres": "3.0.0-alpha.44",
21
- "@reventlessdev/reventless-spec": "3.0.0-alpha.80"
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 BucketObject below writes the production config.json
1469
- // with the resolved API endpoints + Cognito IDs. The host-shell
1470
- // bundle's public/config.json is the dev-mode fallback — exclude
1471
- // it from the bundle upload so both BucketObjects don't race on
1472
- // the same S3 key.
1473
- ~excludeFiles=["config.json"],
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.all2
1508
- ->Pulumi.Output.apply((((domainEp, platformEp, poolId, clientId), eventsEpOpt)) => {
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
- withEvents->Dict.fromArray->JSON.Encode.object->JSON.stringify
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
 
@@ -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, ["config.json"], customDomain);
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
- return JSON.stringify(Object.fromEntries(withEvents));
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: match$3.bucketName,
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, ["config.json"], customDomain);
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
- return JSON.stringify(Object.fromEntries(withEvents));
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: match$3.bucketName,
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
+ }