1import * as AWS from "alchemy/AWS";
2import * as Drizzle from "alchemy/Drizzle/Postgres";
3import * as Duration from "effect/Duration";
4import { eq, sql } from "drizzle-orm";
5import * as Effect from "effect/Effect";
6import * as Redacted from "effect/Redacted";
7import * as Schema from "effect/Schema";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import { bootstrap } from "./bootstrap.ts";
11import { database } from "./database.ts";
12import { todos } from "./schema.ts";
13
14export default class Api extends AWS.Lambda.Function<Api>()(
15 "Api",
16 Effect.gen(function* () {
17 const schemaVersion = yield* bootstrap;
18 return {
19 main: import.meta.url,
20 runtime: "nodejs22.x" as const,
21 functionUrl: { authType: "AWS_IAM" as const },
22 timeout: Duration.seconds(30),
23 build: { install: ["pg"] },
24 env: {
25 SCHEMA_VERSION: schemaVersion,
26 NODE_EXTRA_CA_CERTS: "/var/runtime/ca-cert.pem",
27 },
28 };
29 }),
30 Effect.gen(function* () {
31 const { cluster, subnetIds, lambdaSecurityGroup } = yield* database;
32 const connect = yield* AWS.RDS.Connect(cluster, {
33 auth: "iam",
34 username: "app_iam",
35 database: "app",
36 subnetIds,
37 securityGroupIds: [lambdaSecurityGroup.groupId],
38 });
39 const db = yield* Drizzle.Postgres(
40 connect.pipe(
41 Effect.map((connection) => {
42 const url = new URL(Redacted.value(connection.url));
43 // Connect currently emits no-verify; require full certificate verification.
44 url.searchParams.set("sslmode", "verify-full");
45 return Redacted.make(url.toString());
46 }),
47 ),
48 );
49 return {
50 fetch: Effect.gen(function* () {
51 const request = yield* HttpServerRequest;
52 const url = new URL(request.originalUrl);
53 if (request.method === "GET" && url.pathname === "/health") {
54 const rows = yield* db.execute(sql`SELECT current_user AS username,
55 (SELECT ssl FROM pg_stat_ssl WHERE pid = pg_backend_pid()) AS tls,
56 has_table_privilege(current_user, 'todos', 'INSERT') AS can_insert,
57 has_schema_privilege(current_user, 'public', 'CREATE') AS can_create`);
58 return yield* HttpServerResponse.json(rows);
59 }
60 if (url.pathname === "/todos" && request.method === "GET") {
61 return yield* HttpServerResponse.json(yield* db.select().from(todos));
62 }
63 if (url.pathname === "/todos" && request.method === "POST") {
64 const input = yield* request.json.pipe(
65 Effect.flatMap(
66 Schema.decodeUnknownEffect(
67 Schema.Struct({
68 id: Schema.String.check(Schema.isUUID()),
69 title: Schema.NonEmptyString,
70 }),
71 ),
72 ),
73 );
74 return yield* HttpServerResponse.json(
75 yield* db.insert(todos).values(input).returning(),
76 { status: 201 },
77 );
78 }
79 if (url.pathname.startsWith("/todos/")) {
80 const id = yield* Schema.decodeUnknownEffect(
81 Schema.String.check(Schema.isUUID()),
82 )(url.pathname.slice(7));
83 if (request.method === "PATCH") {
84 const input = yield* request.json.pipe(
85 Effect.flatMap(
86 Schema.decodeUnknownEffect(
87 Schema.Struct({ done: Schema.Boolean }),
88 ),
89 ),
90 );
91 return yield* HttpServerResponse.json(
92 yield* db
93 .update(todos)
94 .set(input)
95 .where(eq(todos.id, id))
96 .returning(),
97 );
98 }
99 if (request.method === "DELETE") {
100 yield* db.delete(todos).where(eq(todos.id, id));
101 return HttpServerResponse.empty({ status: 204 });
102 }
103 }
104 return HttpServerResponse.text("Not found", { status: 404 });
105 }).pipe(
106 Effect.catchTag("SchemaError", () =>
107 Effect.succeed(
108 HttpServerResponse.text("Invalid request", { status: 400 }),
109 ),
110 ),
111 Effect.orDie,
112 ),
113 };
114 }).pipe(Effect.provide(AWS.RDS.ConnectHttp)),
115) {}