| 1 | /** |
| 2 | * Hyperdrive in local dev: the binding is a passthrough to the Connection's |
| 3 | * `dev` origin (there is no SQL simulator), so it needs a REAL reachable |
| 4 | * Postgres. The stack only includes this worker when `HYPERDRIVE_DEV_URL` |
| 5 | * is set, e.g.: |
| 6 | * |
| 7 | * ```sh |
| 8 | * docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres postgres:16 |
| 9 | * HYPERDRIVE_DEV_URL=postgres://postgres:postgres@localhost:5432/postgres bun test |
| 10 | * ``` |
| 11 | */ |
| 12 | import * as Cloudflare from "alchemy/Cloudflare"; |
| 13 | import * as SQL from "alchemy/SQL/Postgres"; |
| 14 | import * as Effect from "effect/Effect"; |
| 15 | import * as Redacted from "effect/Redacted"; |
| 16 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 17 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 18 | |
| 19 | const parseOrigin = (raw: string | undefined) => { |
| 20 | // The fallback keeps module evaluation safe inside workerd (where the env |
| 21 | // var doesn't exist); the worker only deploys when the var is set. |
| 22 | const url = new URL(raw ?? "postgres://user:pass@localhost:5432/postgres"); |
| 23 | return { |
| 24 | scheme: "postgres" as const, |
| 25 | host: url.hostname, |
| 26 | port: url.port ? Number(url.port) : 5432, |
| 27 | database: url.pathname.slice(1) || "postgres", |
| 28 | user: decodeURIComponent(url.username), |
| 29 | password: Redacted.make(decodeURIComponent(url.password)), |
| 30 | }; |
| 31 | }; |
| 32 | |
| 33 | /** |
| 34 | * The `dev` origin is what the local passthrough binding connects to; |
| 35 | * `origin` is what a live deploy would use. This example points both at the |
| 36 | * same database. `sslmode: "disable"` suits a local Docker Postgres — adapt |
| 37 | * if your dev database requires TLS. |
| 38 | */ |
| 39 | export const DevPostgres = Effect.gen(function* () { |
| 40 | const origin = parseOrigin(process.env.HYPERDRIVE_DEV_URL); |
| 41 | return yield* Cloudflare.Hyperdrive.Connection("DevPostgres", { |
| 42 | origin, |
| 43 | caching: { disabled: true }, |
| 44 | dev: { ...origin, sslmode: "disable" }, |
| 45 | }); |
| 46 | }); |
| 47 | |
| 48 | export default class HyperdriveWorker extends Cloudflare.Worker<HyperdriveWorker>()( |
| 49 | "HyperdriveWorker", |
| 50 | { |
| 51 | main: import.meta.url, |
| 52 | }, |
| 53 | Effect.gen(function* () { |
| 54 | const hyperdrive = yield* Cloudflare.Hyperdrive.Connect(DevPostgres); |
| 55 | const sql = yield* SQL.Postgres({ url: hyperdrive.connectionString }); |
| 56 | |
| 57 | return { |
| 58 | fetch: Effect.gen(function* () { |
| 59 | const request = yield* HttpServerRequest; |
| 60 | const url = new URL(request.url, "http://internal"); |
| 61 | if (url.pathname === "/query") { |
| 62 | const rows = (yield* sql` |
| 63 | SELECT 1 + 1 AS sum, current_database() AS db |
| 64 | `) as ReadonlyArray<{ sum: number; db: string }>; |
| 65 | return yield* HttpServerResponse.json({ row: rows[0] }); |
| 66 | } |
| 67 | return HttpServerResponse.text("Not Found", { status: 404 }); |
| 68 | }).pipe( |
| 69 | Effect.catchCause((cause) => |
| 70 | HttpServerResponse.json({ error: String(cause) }, { status: 500 }), |
| 71 | ), |
| 72 | ), |
| 73 | }; |
| 74 | }).pipe(Effect.provide(Cloudflare.Hyperdrive.ConnectBinding)), |
| 75 | ) {} |