1import * as Cloudflare from "@/Cloudflare/index.ts";
2import * as Drizzle from "@/Drizzle/MySQL.ts";
3import { eq } from "drizzle-orm";
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 { Hyperdrive } from "./Stack.ts";
9import { relations, Widgets } from "./schema.ts";
10
11/**
12 * Worker fixture that binds a Cloudflare Hyperdrive (pointed at a
13 * PlanetScale MySQL password) and exercises Drizzle's Effect-native
14 * MySQL client. Mirrors the Postgres Hyperdrive fixture, minus
15 * `.returning()` / `onConflictDoUpdate` — MySQL has neither, so writes
16 * re-select and upserts use `onDuplicateKeyUpdate`.
17 */
18export default class MySQLHyperdriveWorker extends Cloudflare.Worker<MySQLHyperdriveWorker>()(
19 "PlanetscaleMySQLHyperdriveWorker",
20 {
21 main: import.meta.url,
22 },
23 Effect.gen(function* () {
24 const conn = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
25 const db = yield* Drizzle.MySQL(conn.connectionString, { relations });
26
27 return {
28 fetch: Effect.gen(function* () {
29 const request = yield* HttpServerRequest;
30 const url = new URL(request.url, "http://x");
31
32 if (request.method === "GET" && url.pathname === "/hyperdrive") {
33 return yield* HttpServerResponse.json({
34 host: yield* conn.host,
35 port: yield* conn.port,
36 user: yield* conn.user,
37 database: yield* conn.database,
38 });
39 }
40
41 if (request.method === "GET" && url.pathname === "/widgets") {
42 const widgets = yield* db.select().from(Widgets);
43 return yield* HttpServerResponse.json({ widgets });
44 }
45
46 if (request.method === "POST" && url.pathname === "/widgets") {
47 const body = (yield* request.json) as { id: number; name: string };
48 yield* db
49 .insert(Widgets)
50 .values({ id: body.id, name: body.name })
51 .onDuplicateKeyUpdate({ set: { name: body.name } });
52 const [inserted] = yield* db
53 .select()
54 .from(Widgets)
55 .where(eq(Widgets.id, body.id));
56 return yield* HttpServerResponse.json({ widget: inserted });
57 }
58
59 const idMatch = url.pathname.match(/^\/widgets\/(\d+)$/);
60 if (request.method === "DELETE" && idMatch) {
61 const id = Number(idMatch[1]);
62 const [existing] = yield* db
63 .select()
64 .from(Widgets)
65 .where(eq(Widgets.id, id));
66 yield* db.delete(Widgets).where(eq(Widgets.id, id));
67 return yield* HttpServerResponse.json({ widget: existing ?? null });
68 }
69
70 return HttpServerResponse.text("Not Found", { status: 404 });
71 }).pipe(
72 Effect.catch((cause: any) =>
73 HttpServerResponse.json(
74 { ok: false, error: String(cause) },
75 { status: 500 },
76 ),
77 ),
78 ),
79 };
80 }).pipe(Effect.provide(Layer.mergeAll(Cloudflare.Hyperdrive.ConnectBinding))),
81) {}