1import * as GeoPlaces from "@/AWS/GeoPlaces";
2import * as Lambda from "@/AWS/Lambda";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import { HttpServerRequest } from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import path from "pathe";
9
10const main = path.resolve(import.meta.dirname, "handler.ts");
11
12export class GeoPlacesTestFunction extends Lambda.Function<Lambda.Function>()(
13 "GeoPlacesTestFunction",
14) {}
15
16export default GeoPlacesTestFunction.make(
17 {
18 main,
19 functionUrl: true,
20 // geo-places calls fan out to upstream providers and can exceed Lambda's
21 // 3s default.
22 timeout: Duration.seconds(30),
23 },
24 Effect.gen(function* () {
25 const autocomplete = yield* GeoPlaces.Autocomplete();
26 const geocode = yield* GeoPlaces.Geocode();
27 const getPlace = yield* GeoPlaces.GetPlace();
28 const reverseGeocode = yield* GeoPlaces.ReverseGeocode();
29 const searchNearby = yield* GeoPlaces.SearchNearby();
30 const searchText = yield* GeoPlaces.SearchText();
31 const suggest = yield* GeoPlaces.Suggest();
32
33 return {
34 fetch: Effect.gen(function* () {
35 const request = yield* HttpServerRequest;
36 const pathname = new URL(request.originalUrl).pathname;
37
38 // Cheap readiness route — no AWS call.
39 if (request.method === "GET" && pathname === "/ping") {
40 return yield* HttpServerResponse.json({ ok: true });
41 }
42
43 if (request.method === "GET" && pathname === "/autocomplete") {
44 const result = yield* autocomplete({
45 QueryText: "1600 Pennsylvania",
46 MaxResults: 5,
47 });
48 const items = result.ResultItems ?? [];
49 return yield* HttpServerResponse.json({
50 count: items.length,
51 firstTitle: items[0]?.Title,
52 });
53 }
54
55 if (request.method === "GET" && pathname === "/geocode") {
56 const result = yield* geocode({
57 QueryText: "1600 Pennsylvania Ave NW, Washington, DC",
58 MaxResults: 1,
59 });
60 const items = result.ResultItems ?? [];
61 return yield* HttpServerResponse.json({
62 count: items.length,
63 position: items[0]?.Position,
64 });
65 }
66
67 if (request.method === "GET" && pathname === "/get-place") {
68 // GetPlace needs a PlaceId — resolve one via Geocode first.
69 const geocoded = yield* geocode({
70 QueryText: "Space Needle, Seattle, WA",
71 MaxResults: 1,
72 });
73 const placeId = geocoded.ResultItems?.[0]?.PlaceId;
74 if (placeId === undefined) {
75 return yield* HttpServerResponse.json(
76 { error: "geocode returned no PlaceId" },
77 { status: 500 },
78 );
79 }
80 const place = yield* getPlace({ PlaceId: placeId });
81 return yield* HttpServerResponse.json({
82 placeId: place.PlaceId,
83 label: place.Address?.Label,
84 pricingBucket: place.PricingBucket,
85 });
86 }
87
88 if (request.method === "GET" && pathname === "/reverse-geocode") {
89 const result = yield* reverseGeocode({
90 // [longitude, latitude] — near the Space Needle, Seattle.
91 QueryPosition: [-122.3493, 47.6205],
92 MaxResults: 1,
93 });
94 const items = result.ResultItems ?? [];
95 return yield* HttpServerResponse.json({
96 count: items.length,
97 label: items[0]?.Address?.Label,
98 });
99 }
100
101 if (request.method === "GET" && pathname === "/search-nearby") {
102 const result = yield* searchNearby({
103 // [longitude, latitude] — the Space Needle, Seattle.
104 QueryPosition: [-122.3493, 47.6205],
105 QueryRadius: 1000,
106 MaxResults: 5,
107 });
108 const items = result.ResultItems ?? [];
109 return yield* HttpServerResponse.json({
110 count: items.length,
111 firstTitle: items[0]?.Title,
112 pricingBucket: result.PricingBucket,
113 });
114 }
115
116 if (request.method === "GET" && pathname === "/search-text") {
117 const result = yield* searchText({
118 QueryText: "Space Needle, Seattle, WA",
119 MaxResults: 5,
120 // SearchText requires exactly one of BiasPosition,
121 // Filter.BoundingBox, or Filter.Circle. [longitude, latitude]
122 BiasPosition: [-122.3493, 47.6205],
123 });
124 const items = result.ResultItems ?? [];
125 return yield* HttpServerResponse.json({
126 count: items.length,
127 firstPosition: items[0]?.Position,
128 });
129 }
130
131 if (request.method === "GET" && pathname === "/suggest") {
132 const result = yield* suggest({
133 QueryText: "coffee",
134 // [longitude, latitude] — downtown Seattle.
135 BiasPosition: [-122.3493, 47.6205],
136 MaxResults: 5,
137 });
138 const items = result.ResultItems ?? [];
139 return yield* HttpServerResponse.json({
140 count: items.length,
141 firstTitle: items[0]?.Title,
142 firstType: items[0]?.SuggestResultItemType,
143 });
144 }
145
146 return yield* HttpServerResponse.json(
147 { error: "Not found", method: request.method, pathname },
148 { status: 404 },
149 );
150 }).pipe(Effect.orDie),
151 };
152 }).pipe(
153 Effect.provide(
154 Layer.mergeAll(
155 GeoPlaces.AutocompleteHttp,
156 GeoPlaces.GeocodeHttp,
157 GeoPlaces.GetPlaceHttp,
158 GeoPlaces.ReverseGeocodeHttp,
159 GeoPlaces.SearchNearbyHttp,
160 GeoPlaces.SearchTextHttp,
161 GeoPlaces.SuggestHttp,
162 ),
163 ),
164 ),
165);