1import * as Drizzle from "alchemy/Drizzle/Postgres";
2import * as Railway from "alchemy/Railway";
3import * as Config from "effect/Config";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Stream from "effect/Stream";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9import {
10 API_PORT,
11 Cache,
12 Data,
13 Db,
14 Marker,
15 OBJECT_KEY,
16 REDIS_KEY,
17 SECRET_NAME,
18 Site,
19} from "./shared.ts";
20
21const runtime = Layer.mergeAll(
22 Railway.ConnectPostgresHttp,
23 Railway.ReadWriteRedisHttp,
24 Railway.PutObjectHttp,
25 Railway.GetObjectHttp,
26 Railway.HeadObjectHttp,
27 Railway.ListObjectsV2Http,
28 Railway.DeleteObjectHttp,
29);
30
31/**
32 * HTTP Service. Binds Postgres, Redis, and the Bucket. Reads the
33 * shared {@link Marker} variable from env.
34 */
35export default class Api extends Railway.Service<Api>()(
36 "Api",
37 {
38 project: Site,
39 main: import.meta.url,
40 port: API_PORT,
41 build: { install: ["pg"] },
42 healthcheck: "/health",
43 env: {
44 [SECRET_NAME]: Railway.ref("shared", SECRET_NAME),
45 },
46 },
47 Effect.gen(function* () {
48 yield* Marker;
49 const conn = yield* Railway.ConnectPostgres(Db);
50 const db = yield* Drizzle.Postgres(conn.connectionString);
51 const cache = yield* Railway.ReadWriteRedis(Cache);
52 const putObject = yield* Railway.PutObject(Data);
53 const getObject = yield* Railway.GetObject(Data);
54 const headObject = yield* Railway.HeadObject(Data);
55 const listObjects = yield* Railway.ListObjectsV2(Data);
56 const deleteObject = yield* Railway.DeleteObject(Data);
57
58 return {
59 fetch: Effect.gen(function* () {
60 const request = yield* HttpServerRequest;
61 const path = new URL(request.url, "http://service").pathname;
62
63 if (path === "/secret") {
64 const value = yield* Config.String(SECRET_NAME).pipe(
65 Effect.orElseSucceed(() => ""),
66 );
67 return yield* HttpServerResponse.json({
68 ok: value.length > 0,
69 name: SECRET_NAME,
70 });
71 }
72
73 if (path === "/redis") {
74 yield* cache.set(REDIS_KEY, "hello");
75 const value = yield* cache.get(REDIS_KEY);
76 return yield* HttpServerResponse.json({
77 ok: value === "hello",
78 value,
79 });
80 }
81
82 if (path === "/bucket") {
83 yield* putObject({
84 Key: OBJECT_KEY,
85 Body: "hello",
86 ContentType: "text/plain",
87 });
88 const head = yield* headObject({ Key: OBJECT_KEY });
89 const listed = yield* listObjects({
90 Prefix: OBJECT_KEY,
91 MaxKeys: 10,
92 });
93 const obj = yield* getObject({ Key: OBJECT_KEY });
94 const text =
95 obj.Body === undefined
96 ? ""
97 : yield* Stream.mkString(Stream.decodeText(obj.Body));
98 yield* deleteObject({ Key: OBJECT_KEY });
99 return yield* HttpServerResponse.json({
100 ok: text === "hello",
101 etag: head.ETag,
102 listed: listed.KeyCount,
103 });
104 }
105
106 const rows = yield* db.execute("select 1 as ok");
107 if (path === "/health" || path === "/" || path === "/postgres") {
108 return yield* HttpServerResponse.json({ rows });
109 }
110 return yield* HttpServerResponse.json({ rows }, { status: 404 });
111 }).pipe(
112 Effect.catch((error) =>
113 HttpServerResponse.json(
114 { ok: false, error: String(error) },
115 { status: 500 },
116 ),
117 ),
118 ),
119 };
120 }).pipe(Effect.provide(runtime)),
121) {}