1import * as Cloudflare from "@/Cloudflare/index.ts";
2import * as Neon from "@/Neon/index.ts";
3import * as SQL from "@/SQL/Postgres.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 project = yield* Neon.Project("SqlPostgresProject");
12 return yield* Cloudflare.Hyperdrive.Connection("SqlPostgresEdge", {
13 origin: project.origin,
14 // The tests assert read-after-write; Hyperdrive's default SELECT caching
15 // (~60s TTL) can serve a pre-insert empty result — and keep serving it
16 // across retries — so caching is disabled for correctness assertions.
17 caching: { disabled: true },
18 });
19});
20
21const TABLE = "alchemy_sql_users";
22
23const DDL = `CREATE TABLE IF NOT EXISTS ${TABLE} (
24 id INT PRIMARY KEY,
25 name TEXT NOT NULL,
26 email TEXT NOT NULL
27)`;
28
29class Rollback extends Data.TaggedError("Rollback")<{}> {}
30
31/**
32 * Init effect for the whole route surface: binds the Hyperdrive connection,
33 * builds the `SQL.Postgres` client and the `SQL.PostgresLayer`-backed users
34 * service, and returns a handler that serves the Postgres-only routes
35 * (`withTransaction` commit/rollback, multi-row `sql.updateValues`) before
36 * falling through to the shared dialect-agnostic table.
37 */
38const PostgresRoutes = Effect.gen(function* () {
39 const hd = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
40 const sql = yield* SQL.Postgres({ url: hd.connectionString });
41 const layerUsers = yield* makeLayerUsers(TABLE).pipe(
42 Effect.provide(SQL.PostgresLayer({ url: hd.connectionString })),
43 );
44 const shared = makeSqlRoutes({ sql, layerUsers, ddl: DDL, table: TABLE });
45
46 const handle = Effect.fn(function* (
47 request: HttpServerRequest.HttpServerRequest,
48 ) {
49 // POST /tx/commit — statements inside `withTransaction` share one
50 // transaction and commit together.
51 if (request.method === "POST" && request.url === "/tx/commit") {
52 const rows = (yield* request.json) as UserRow[];
53 yield* sql.withTransaction(
54 Effect.gen(function* () {
55 for (const row of rows) {
56 yield* sql`INSERT INTO ${sql(TABLE)} ${sql.insert(row)}`;
57 }
58 }),
59 );
60 const inserted = yield* sql`
61 SELECT id, name, email FROM ${sql(TABLE)}
62 WHERE ${sql.in(
63 "id",
64 rows.map((r) => r.id),
65 )}
66 ORDER BY id
67 `;
68 return yield* HttpServerResponse.json({ rows: inserted });
69 }
70
71 // POST /tx/rollback — a failure inside `withTransaction` rolls the
72 // insert back; the row must not be visible afterwards.
73 if (request.method === "POST" && request.url === "/tx/rollback") {
74 const row = (yield* request.json) as UserRow;
75 const error = yield* sql
76 .withTransaction(
77 Effect.gen(function* () {
78 yield* sql`INSERT INTO ${sql(TABLE)} ${sql.insert(row)}`;
79 return yield* Effect.fail(new Rollback());
80 }),
81 )
82 .pipe(Effect.flip);
83 const rows =
84 yield* sql`SELECT id FROM ${sql(TABLE)} WHERE id = ${row.id}`;
85 return yield* HttpServerResponse.json({
86 error: (error as { _tag: string })._tag,
87 rows,
88 });
89 }
90
91 // PUT /users/values — multi-row `sql.updateValues` (not supported on
92 // sqlite/D1, so it lives here rather than in the shared routes).
93 if (request.method === "PUT" && request.url === "/users/values") {
94 const rows = (yield* request.json) as UserRow[];
95 // `updateValues` binds every value as text — cast the join key back
96 // to int so `integer = text` doesn't reject the statement.
97 yield* sql`
98 UPDATE ${sql(TABLE)} AS t
99 SET name = data.name, email = data.email
100 FROM ${sql.updateValues(rows, "data")}
101 WHERE t.id = data.id::int
102 `;
103 const updated = yield* sql`
104 SELECT id, name, email FROM ${sql(TABLE)}
105 WHERE ${sql.in(
106 "id",
107 rows.map((r) => r.id),
108 )}
109 ORDER BY id
110 `;
111 return yield* HttpServerResponse.json({ rows: updated });
112 }
113
114 return yield* shared.handle(request);
115 });
116
117 return { handle };
118});
119
120/**
121 * Effect-native Worker driving `SQL.Postgres` (the raw `@effect/sql-pg`
122 * client) over a Hyperdrive connection to a Neon Postgres origin.
123 */
124export default class SqlPostgresWorker extends Cloudflare.Worker<SqlPostgresWorker>()(
125 "SqlPostgresWorker",
126 {
127 main: import.meta.url,
128 },
129 Effect.gen(function* () {
130 const routes = yield* PostgresRoutes;
131
132 return {
133 fetch: Effect.gen(function* () {
134 const request = yield* HttpServerRequest.HttpServerRequest;
135 const response = yield* routes.handle(request);
136 if (response !== undefined) {
137 return response;
138 }
139 return yield* HttpServerResponse.json(
140 { error: "not found" },
141 { status: 404 },
142 );
143 }).pipe(
144 Effect.catchCause((cause) =>
145 HttpServerResponse.json({ error: String(cause) }, { status: 500 }),
146 ),
147 ),
148 };
149 }).pipe(Effect.provide(Cloudflare.Hyperdrive.ConnectBinding)),
150) {}