1import * as CloudFront from "@/AWS/CloudFront";
2import * as Lambda from "@/AWS/Lambda";
3import * as Cause from "effect/Cause";
4import * as Duration from "effect/Duration";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import * as Redacted from "effect/Redacted";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import path from "pathe";
11
12const main = path.resolve(import.meta.dirname, "kvs-handler.ts");
13
14export class CloudFrontKvsTestFunction extends Lambda.Function<Lambda.Function>()(
15 "CloudFrontKvsTestFunction",
16) {}
17
18const unwrap = (value: string | Redacted.Redacted<string>): string =>
19 typeof value === "string" ? value : Redacted.value(value);
20
21export default CloudFrontKvsTestFunction.make(
22 {
23 main,
24 functionUrl: true,
25 timeout: Duration.seconds(30),
26 },
27 Effect.gen(function* () {
28 const store = yield* CloudFront.KeyValueStore("BindingsKvStore", {
29 // Deterministic marker so the test can find this store out-of-band
30 // via listKeyValueStores.
31 comment: "alchemy-cf-kvs-bindings-fixture",
32 });
33
34 const describeStore = yield* CloudFront.DescribeKeyValueStore(store);
35 const getKey = yield* CloudFront.GetKey(store);
36 const listKeys = yield* CloudFront.ListKeys(store);
37 const putKey = yield* CloudFront.PutKey(store);
38 const deleteKey = yield* CloudFront.DeleteKey(store);
39 const updateKeys = yield* CloudFront.UpdateKeys(store);
40
41 return {
42 fetch: Effect.gen(function* () {
43 const request = yield* HttpServerRequest;
44 const url = new URL(request.originalUrl);
45 const pathname = url.pathname;
46
47 if (request.method === "GET" && pathname === "/health") {
48 return yield* HttpServerResponse.json({ ok: true });
49 }
50
51 if (request.method === "GET" && pathname === "/describe") {
52 const meta = yield* describeStore({});
53 return yield* HttpServerResponse.json({
54 etag: meta.ETag,
55 itemCount: meta.ItemCount,
56 status: meta.Status,
57 kvsArn: meta.KvsARN,
58 });
59 }
60
61 if (request.method === "GET" && pathname === "/key") {
62 const key = url.searchParams.get("key")!;
63 const res = yield* getKey({ Key: key });
64 return yield* HttpServerResponse.json({
65 key: res.Key,
66 value: unwrap(res.Value),
67 });
68 }
69
70 if (request.method === "GET" && pathname === "/keys") {
71 const res = yield* listKeys({});
72 return yield* HttpServerResponse.json({
73 keys: (res.Items ?? []).map((item) => ({
74 key: item.Key,
75 value: unwrap(item.Value),
76 })),
77 });
78 }
79
80 if (request.method === "POST" && pathname === "/put") {
81 const body = (yield* request.json) as unknown as {
82 key: string;
83 value: string;
84 };
85 const meta = yield* describeStore({});
86 const res = yield* putKey({
87 Key: body.key,
88 Value: body.value,
89 IfMatch: meta.ETag,
90 });
91 return yield* HttpServerResponse.json({
92 etag: res.ETag,
93 itemCount: res.ItemCount,
94 });
95 }
96
97 if (request.method === "POST" && pathname === "/update") {
98 const body = (yield* request.json) as unknown as {
99 puts?: { key: string; value: string }[];
100 deletes?: { key: string }[];
101 };
102 const meta = yield* describeStore({});
103 const res = yield* updateKeys({
104 IfMatch: meta.ETag,
105 Puts: body.puts?.map((p) => ({ Key: p.key, Value: p.value })),
106 Deletes: body.deletes?.map((d) => ({ Key: d.key })),
107 });
108 return yield* HttpServerResponse.json({
109 etag: res.ETag,
110 itemCount: res.ItemCount,
111 });
112 }
113
114 if (request.method === "POST" && pathname === "/delete") {
115 const body = (yield* request.json) as unknown as { key: string };
116 const meta = yield* describeStore({});
117 const res = yield* deleteKey({
118 Key: body.key,
119 IfMatch: meta.ETag,
120 });
121 return yield* HttpServerResponse.json({
122 etag: res.ETag,
123 itemCount: res.ItemCount,
124 });
125 }
126
127 return yield* HttpServerResponse.json(
128 { error: "Not found" },
129 { status: 404 },
130 );
131 }).pipe(
132 Effect.catchCause((cause) =>
133 HttpServerResponse.json(
134 { error: Cause.pretty(cause) },
135 { status: 500 },
136 ),
137 ),
138 ),
139 };
140 }).pipe(
141 Effect.provide(
142 Layer.mergeAll(
143 CloudFront.DescribeKeyValueStoreHttp,
144 CloudFront.GetKeyHttp,
145 CloudFront.ListKeysHttp,
146 CloudFront.PutKeyHttp,
147 CloudFront.DeleteKeyHttp,
148 CloudFront.UpdateKeysHttp,
149 ),
150 ),
151 ),
152);