1import * as Drizzle from "alchemy/Drizzle/Postgres";
2import * as Fly from "alchemy/Fly";
3import { desc, sql } from "drizzle-orm";
4import * as Effect from "effect/Effect";
5import { HttpServerRequest } from "effect/http/HttpServerRequest";
6import * as HttpServerResponse from "effect/http/HttpServerResponse";
7import { ENSURE_NOTES_SQL, Notes } from "./schema.ts";
8import { API_PORT, Db } from "./shared.ts";
9
10const cors = {
11 "access-control-allow-origin": "*",
12 "access-control-allow-methods": "GET,POST,OPTIONS",
13 "access-control-allow-headers": "content-type",
14};
15
16const json = (value: unknown, status = 200) =>
17 HttpServerResponse.json(value, { status, headers: cors });
18
19/**
20 * Effect HTTP API on a Fly Machine. Binds Managed Postgres via
21 * {@link Fly.ConnectPostgres} and serves `/notes` for the Vite SPA.
22 */
23export default class Api extends Fly.Service<Api>()(
24 "Api",
25 {
26 main: import.meta.url,
27 port: API_PORT,
28 guest: { cpuKind: "shared", cpus: 1, memoryMb: 256 },
29 build: { install: ["pg"] },
30 },
31 Effect.gen(function* () {
32 const conn = yield* Fly.ConnectPostgres(Db);
33 const db = yield* Drizzle.Postgres(conn.connectionString);
34
35 return {
36 fetch: Effect.gen(function* () {
37 const request = yield* HttpServerRequest;
38 const path = new URL(request.url, "http://service").pathname;
39 if (request.method === "OPTIONS") {
40 return yield* json({}, 204);
41 }
42 yield* db.execute(sql.raw(ENSURE_NOTES_SQL));
43 if (path === "/health") {
44 return yield* json({ ok: true });
45 }
46 if (path === "/notes" && request.method === "GET") {
47 const notes = yield* db
48 .select()
49 .from(Notes)
50 .orderBy(desc(Notes.createdAt));
51 return yield* json({ notes });
52 }
53 if (path === "/notes" && request.method === "POST") {
54 const raw = yield* request.text.pipe(Effect.orDie);
55 const payload = JSON.parse(raw) as { body?: unknown };
56 const body =
57 typeof payload.body === "string" ? payload.body.trim() : "";
58 if (body.length === 0) {
59 return yield* json({ error: "body required" }, 400);
60 }
61 const [note] = yield* db.insert(Notes).values({ body }).returning();
62 return yield* json({ note }, 201);
63 }
64 return yield* json({ error: "not found" }, 404);
65 }).pipe(
66 Effect.catch((cause: unknown) =>
67 json({ ok: false, error: String(cause) }, 500),
68 ),
69 Effect.orDie,
70 ),
71 };
72 }).pipe(Effect.provide(Fly.ConnectPostgresHttp)),
73) {}