1import * as Cloudflare from "@/Cloudflare/index.ts";
2import * as Planetscale from "@/Planetscale/index.ts";
3import * as SQL from "@/SQL/MySQL.ts";
4import * as Data from "effect/Data";
5import * as Effect from "effect/Effect";
6import * as HttpServerRequest from "effect/http/HttpServerRequest";
7import * as HttpServerResponse from "effect/http/HttpServerResponse";
8import { makeLayerUsers, makeSqlRoutes, type UserRow } from "./routes.ts";
9
10export const Hyperdrive = Effect.gen(function* () {
11 const database = yield* Planetscale.MySQLDatabase("SqlMySQLDb", {
12 name: "alchemy-sql-mysql",
13 region: { slug: "us-east" },
14 clusterSize: "PS_10",
15 });
16 // `admin` because the fixture creates its table over the wire (`/init`
17 // runs raw DDL); the default branch is `main`.
18 const password = yield* Planetscale.MySQLPassword("SqlMySQLPassword", {
19 database,
20 role: "admin",
21 });
22 return yield* Cloudflare.Hyperdrive.Connection("SqlMySQLEdge", {
23 origin: password.origin,
24 // The tests assert read-after-write; Hyperdrive's default SELECT caching
25 // (~60s TTL) can serve a pre-insert empty result — and keep serving it
26 // across retries — so caching is disabled for correctness assertions.
27 caching: { disabled: true },
28 });
29});
30
31const TABLE = "alchemy_sql_users";
32
33const DDL = `CREATE TABLE IF NOT EXISTS ${TABLE} (
34 id INT PRIMARY KEY,
35 name VARCHAR(255) NOT NULL,
36 email VARCHAR(255) NOT NULL
37)`;
38
39class Rollback extends Data.TaggedError("Rollback")<{}> {}
40
41/**
42 * Init effect for the whole route surface: binds the Hyperdrive connection,
43 * builds the `SQL.MySQL` client and the `SQL.MySQLLayer`-backed users
44 * service, and returns a handler that serves the MySQL transaction routes
45 * (`withTransaction` commit/rollback) before falling through to the shared
46 * dialect-agnostic table.
47 */
48const MySQLRoutes = Effect.gen(function* () {
49 const hd = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
50 const sql = yield* SQL.MySQL({ url: hd.connectionString });
51 const layerUsers = yield* makeLayerUsers(TABLE).pipe(
52 Effect.provide(SQL.MySQLLayer({ url: hd.connectionString })),
53 );
54 const shared = makeSqlRoutes({ sql, layerUsers, ddl: DDL, table: TABLE });
55
56 const handle = Effect.fn(function* (
57 request: HttpServerRequest.HttpServerRequest,
58 ) {
59 // POST /tx/commit — statements inside `withTransaction` share one
60 // transaction and commit together.
61 if (request.method === "POST" && request.url === "/tx/commit") {
62 const rows = (yield* request.json) as UserRow[];
63 yield* sql.withTransaction(
64 Effect.gen(function* () {
65 for (const row of rows) {
66 yield* sql`INSERT INTO ${sql(TABLE)} ${sql.insert(row)}`;
67 }
68 }),
69 );
70 const inserted = yield* sql`
71 SELECT id, name, email FROM ${sql(TABLE)}
72 WHERE ${sql.in(
73 "id",
74 rows.map((r) => r.id),
75 )}
76 ORDER BY id
77 `;
78 return yield* HttpServerResponse.json({ rows: inserted });
79 }
80
81 // POST /tx/rollback — a failure inside `withTransaction` rolls the
82 // insert back; the row must not be visible afterwards.
83 if (request.method === "POST" && request.url === "/tx/rollback") {
84 const row = (yield* request.json) as UserRow;
85 const error = yield* sql
86 .withTransaction(
87 Effect.gen(function* () {
88 yield* sql`INSERT INTO ${sql(TABLE)} ${sql.insert(row)}`;
89 return yield* Effect.fail(new Rollback());
90 }),
91 )
92 .pipe(Effect.flip);
93 const rows =
94 yield* sql`SELECT id FROM ${sql(TABLE)} WHERE id = ${row.id}`;
95 return yield* HttpServerResponse.json({
96 error: (error as { _tag: string })._tag,
97 rows,
98 });
99 }
100
101 return yield* shared.handle(request);
102 });
103
104 return { handle };
105});
106
107/**
108 * Effect-native Worker driving `SQL.MySQL` (the raw `@effect/sql-mysql2`
109 * client) over a Hyperdrive connection to a PlanetScale MySQL origin.
110 */
111export default class SqlMySQLWorker extends Cloudflare.Worker<SqlMySQLWorker>()(
112 "SqlMySQLWorker",
113 {
114 main: import.meta.url,
115 },
116 Effect.gen(function* () {
117 const routes = yield* MySQLRoutes;
118
119 return {
120 fetch: Effect.gen(function* () {
121 const request = yield* HttpServerRequest.HttpServerRequest;
122 const response = yield* routes.handle(request);
123 if (response !== undefined) {
124 return response;
125 }
126 return yield* HttpServerResponse.json(
127 { error: "not found" },
128 { status: 404 },
129 );
130 }).pipe(
131 Effect.catchCause((cause) =>
132 HttpServerResponse.json({ error: String(cause) }, { status: 500 }),
133 ),
134 ),
135 };
136 }).pipe(Effect.provide(Cloudflare.Hyperdrive.ConnectBinding)),
137) {}