1import * as DSQL from "@/AWS/DSQL";
2import * as Lambda from "@/AWS/Lambda";
3import * as Drizzle from "@/Drizzle/Postgres.ts";
4import { eq, sql } from "drizzle-orm";
5import * as Duration from "effect/Duration";
6import * as Effect from "effect/Effect";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9import path from "pathe";
10import { Db } from "./db.ts";
11import { Widgets } from "./schema.ts";
12
13const main = path.resolve(import.meta.dirname, "drizzle-handler.ts");
14
15export class DsqlDrizzleFunction extends Lambda.Function<Lambda.Function>()(
16 "DsqlDrizzleFunction",
17) {}
18
19/**
20 * Lambda fixture proving the full-stack Drizzle-over-DSQL path: the
21 * `DSQL.Connect` binding mints an IAM auth token per execution, and
22 * `Drizzle.Postgres` builds its execution-scoped pool from the resulting
23 * connection URL. No VPC — DSQL's endpoint is public and IAM-gated.
24 */
25export default DsqlDrizzleFunction.make(
26 {
27 main,
28 functionUrl: true,
29 timeout: Duration.seconds(30),
30 // `pg` is CommonJS and Rolldown's bundled interop turns its Client export
31 // into a namespace object under Node. Install it intact in the Lambda
32 // artifact so @effect/sql-pg loads the package with Node's CJS semantics.
33 build: { install: ["pg"] },
34 },
35 Effect.gen(function* () {
36 const cluster = yield* Db;
37 const conn = yield* DSQL.Connect(cluster, { admin: true });
38 // `proxyChain` defers the connect to the first query, so the pool (and
39 // the IAM token backing it) is built inside the invocation, per
40 // execution — a fresh token can never outlive its pool.
41 const db = yield* Drizzle.Postgres(
42 conn.pipe(Effect.map((info) => info.url)),
43 );
44
45 return {
46 fetch: Effect.gen(function* () {
47 const request = yield* HttpServerRequest;
48 const url = new URL(request.originalUrl);
49 const pathname = url.pathname;
50
51 // Cheap readiness probe — proves connect + token auth end-to-end.
52 if (request.method === "GET" && pathname === "/health") {
53 // postgres-js `execute()` resolves to the row array itself.
54 const rows = yield* db.execute(sql`SELECT 1 AS one`);
55 return yield* HttpServerResponse.json({ rows });
56 }
57
58 // DSQL runs DDL outside DML transactions; each execute is its own
59 // autocommit statement. DELETE (not DROP) keeps reruns fast.
60 if (request.method === "POST" && pathname === "/setup") {
61 yield* db.execute(
62 sql`CREATE TABLE IF NOT EXISTS dsql_connect_widgets (id integer PRIMARY KEY, title text NOT NULL)`,
63 );
64 yield* db.execute(sql`DELETE FROM dsql_connect_widgets`);
65 return yield* HttpServerResponse.json({ success: true });
66 }
67
68 if (request.method === "POST" && pathname === "/insert") {
69 const body = (yield* request.json) as unknown as {
70 id: number;
71 title: string;
72 };
73 yield* db.insert(Widgets).values({ id: body.id, title: body.title });
74 return yield* HttpServerResponse.json({ success: true });
75 }
76
77 if (request.method === "GET" && pathname === "/select") {
78 const id = url.searchParams.get("id");
79 if (!id) {
80 return HttpServerResponse.text("Missing id", { status: 400 });
81 }
82 const rows = yield* db
83 .select()
84 .from(Widgets)
85 .where(eq(Widgets.id, Number(id)));
86 return yield* HttpServerResponse.json({ rows });
87 }
88
89 return yield* HttpServerResponse.json(
90 { error: "Not found", method: request.method, pathname },
91 { status: 404 },
92 );
93 }).pipe(Effect.orDie),
94 };
95 }).pipe(Effect.provide(DSQL.ConnectHttp)),
96);