1import type { QueryDatabaseClient } from "@/Cloudflare/D1/QueryDatabase.ts";
2import * as Effect from "effect/Effect";
3import { HttpServerRequest } from "effect/http/HttpServerRequest";
4import * as HttpServerResponse from "effect/http/HttpServerResponse";
5
6/**
7 * Modality-agnostic D1 route handlers driving the Effect-native
8 * {@link QueryDatabaseClient} interface. Reused by the effect-worker shell.
9 * The async-worker shell drives the equivalent surface over the native
10 * `D1Database` binding (see `async-worker.ts`).
11 *
12 * Every worker stamps its rows with a `style` value ("effect" | "async")
13 * so the two workers can share one physical database. The table is shared;
14 * the rows are partitioned by `style`.
15 *
16 * POST /init — db.exec(CREATE TABLE)
17 * POST /seed — db.batch([prepare(...).bind(...), ...])
18 * POST /users — db.prepare(...).bind(...).run()
19 * GET /users — db.prepare(...).all()
20 * GET /users/:id — db.prepare(...).bind(id).first()
21 * GET /raw — db.raw -> direct runtime D1Database access
22 */
23export const d1Routes = (db: QueryDatabaseClient, style: string) =>
24 Effect.gen(function* () {
25 const request = yield* HttpServerRequest;
26 const url = new URL(request.url, "http://x");
27
28 if (request.method === "POST" && url.pathname === "/init") {
29 const result = yield* db.exec(
30 "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, style TEXT NOT NULL, name TEXT NOT NULL)",
31 );
32 return yield* HttpServerResponse.json({
33 count: result.count,
34 duration: result.duration,
35 });
36 }
37
38 if (request.method === "POST" && url.pathname === "/seed") {
39 const insert = db.prepare(
40 "INSERT OR REPLACE INTO users (id, style, name) VALUES (?, ?, ?)",
41 );
42 const results = yield* db.batch([
43 insert.bind(1, style, "alice"),
44 insert.bind(2, style, "bob"),
45 insert.bind(3, style, "carol"),
46 ]);
47 return yield* HttpServerResponse.json({
48 batches: results.length,
49 success: results.every((r) => r.success),
50 });
51 }
52
53 if (request.method === "POST" && url.pathname === "/users") {
54 const body = (yield* request.json) as { id: number; name: string };
55 const result = yield* db
56 .prepare(
57 "INSERT OR REPLACE INTO users (id, style, name) VALUES (?, ?, ?)",
58 )
59 .bind(body.id, style, body.name)
60 .run();
61 return yield* HttpServerResponse.json({
62 success: result.success,
63 meta: { changes: result.meta.changes },
64 });
65 }
66
67 if (request.method === "GET" && url.pathname === "/users") {
68 const result = yield* db
69 .prepare("SELECT id, name FROM users WHERE style = ? ORDER BY id")
70 .bind(style)
71 .all<{ id: number; name: string }>();
72 return yield* HttpServerResponse.json({
73 success: result.success,
74 results: result.results,
75 });
76 }
77
78 const userMatch = url.pathname.match(/^\/users\/(\d+)$/);
79 if (request.method === "GET" && userMatch) {
80 const id = Number(userMatch[1]);
81 const row = yield* db
82 .prepare("SELECT id, name FROM users WHERE id = ? AND style = ?")
83 .bind(id, style)
84 .first<{ id: number; name: string }>();
85 return yield* HttpServerResponse.json({ row });
86 }
87
88 if (request.method === "GET" && url.pathname === "/raw") {
89 // `db.raw` returns the underlying Cloudflare D1Database — the escape
90 // hatch libraries like Better Auth / Drizzle rely on.
91 const raw = yield* db.raw;
92 const result = yield* Effect.promise(() =>
93 raw
94 .prepare("SELECT COUNT(*) as count FROM users WHERE style = ?")
95 .bind(style)
96 .first<{ count: number }>(),
97 );
98 return yield* HttpServerResponse.json({ count: result?.count ?? 0 });
99 }
100
101 return HttpServerResponse.text("Not Found", { status: 404 });
102 });