1import * as Lambda from "@/AWS/Lambda";
2import * as Pricing from "@/AWS/Pricing";
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 PricingTestFunction extends Lambda.Function<Lambda.Function>()(
13 "PricingTestFunction",
14) {}
15
16export default PricingTestFunction.make(
17 {
18 main,
19 functionUrl: true,
20 // Price List queries return large JSON documents; the default 3s Lambda
21 // timeout intermittently trips under cold start + big responses.
22 timeout: Duration.seconds(30),
23 },
24 Effect.gen(function* () {
25 const getProducts = yield* Pricing.GetProducts();
26 const describeServices = yield* Pricing.DescribeServices();
27 const getAttributeValues = yield* Pricing.GetAttributeValues();
28 const listPriceLists = yield* Pricing.ListPriceLists();
29 const getPriceListFileUrl = yield* Pricing.GetPriceListFileUrl();
30
31 return {
32 fetch: Effect.gen(function* () {
33 const request = yield* HttpServerRequest;
34 const url = new URL(request.originalUrl);
35 const pathname = url.pathname;
36
37 // Cheap readiness route — no Pricing call.
38 if (request.method === "GET" && pathname === "/ping") {
39 return yield* HttpServerResponse.json({ ok: true });
40 }
41
42 if (request.method === "GET" && pathname === "/products") {
43 const result = yield* getProducts({
44 ServiceCode: "AmazonEC2",
45 Filters: [
46 { Type: "TERM_MATCH", Field: "instanceType", Value: "t3.micro" },
47 {
48 Type: "TERM_MATCH",
49 Field: "location",
50 Value: "US East (N. Virginia)",
51 },
52 { Type: "TERM_MATCH", Field: "operatingSystem", Value: "Linux" },
53 ],
54 MaxResults: 5,
55 });
56 const priceList = result.PriceList ?? [];
57 const first =
58 priceList.length > 0
59 ? (JSON.parse(priceList[0]) as {
60 product?: {
61 attributes?: {
62 servicecode?: string;
63 instanceType?: string;
64 };
65 };
66 })
67 : undefined;
68 return yield* HttpServerResponse.json({
69 count: priceList.length,
70 formatVersion: result.FormatVersion,
71 firstServiceCode: first?.product?.attributes?.servicecode,
72 firstInstanceType: first?.product?.attributes?.instanceType,
73 });
74 }
75
76 if (request.method === "GET" && pathname === "/services") {
77 const result = yield* describeServices({
78 ServiceCode: "AmazonEC2",
79 });
80 return yield* HttpServerResponse.json({
81 services: (result.Services ?? []).map((service) => ({
82 serviceCode: service.ServiceCode,
83 attributeNameCount: (service.AttributeNames ?? []).length,
84 })),
85 });
86 }
87
88 if (request.method === "GET" && pathname === "/attribute-values") {
89 const result = yield* getAttributeValues({
90 ServiceCode: "AmazonEC2",
91 AttributeName: "volumeType",
92 MaxResults: 25,
93 });
94 return yield* HttpServerResponse.json({
95 values: (result.AttributeValues ?? [])
96 .map((attribute) => attribute.Value)
97 .filter((value) => value !== undefined),
98 });
99 }
100
101 if (request.method === "GET" && pathname === "/price-list-file-url") {
102 // ListPriceLists -> GetPriceListFileUrl round trip: resolve a bulk
103 // Price List reference for EC2 in us-east-1, then presign its JSON
104 // file URL.
105 const lists = yield* listPriceLists({
106 ServiceCode: "AmazonEC2",
107 CurrencyCode: "USD",
108 EffectiveDate: new Date(),
109 RegionCode: "us-east-1",
110 MaxResults: 5,
111 });
112 const priceLists = lists.PriceLists ?? [];
113 const first = priceLists[0];
114 if (first?.PriceListArn === undefined) {
115 return yield* HttpServerResponse.json(
116 { error: "No price list returned" },
117 { status: 500 },
118 );
119 }
120 const fileFormat = (first.FileFormats ?? []).includes("json")
121 ? "json"
122 : (first.FileFormats ?? [])[0];
123 const { Url } = yield* getPriceListFileUrl({
124 PriceListArn: first.PriceListArn,
125 FileFormat: fileFormat ?? "json",
126 });
127 return yield* HttpServerResponse.json({
128 count: priceLists.length,
129 priceListArn: first.PriceListArn,
130 regionCode: first.RegionCode,
131 currencyCode: first.CurrencyCode,
132 fileFormats: first.FileFormats ?? [],
133 url: Url,
134 });
135 }
136
137 return yield* HttpServerResponse.json(
138 { error: "Not found", method: request.method, pathname },
139 { status: 404 },
140 );
141 }).pipe(Effect.orDie),
142 };
143 }).pipe(
144 Effect.provide(
145 Layer.mergeAll(
146 Pricing.GetProductsHttp,
147 Pricing.DescribeServicesHttp,
148 Pricing.GetAttributeValuesHttp,
149 Pricing.ListPriceListsHttp,
150 Pricing.GetPriceListFileUrlHttp,
151 ),
152 ),
153 ),
154);