1import * as AWS from "alchemy/AWS";
2import type { RuntimeContext } from "alchemy/RuntimeContext";
3import * as Context from "effect/Context";
4import * as Data from "effect/Data";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import { Client } from "pg";
8import { Network } from "./Network.ts";
9
10export class SqlError extends Data.TaggedError("SqlError")<{
11 readonly message: string;
12 readonly cause?: unknown;
13}> {}
14
15export class Database extends Context.Service<
16 Database,
17 {
18 query<Row extends Record<string, unknown> = Record<string, unknown>>(
19 statement: string,
20 values?: ReadonlyArray<unknown>,
21 ): Effect.Effect<ReadonlyArray<Row>, SqlError, RuntimeContext>;
22 }
23>()("Sql") {}
24
25export const DatabaseAurora = Layer.effect(
26 Database,
27 Effect.gen(function* () {
28 const { vpc, databaseSecurityGroup } = yield* Network;
29 const database = yield* AWS.RDS.Aurora("Database", {
30 subnetIds: vpc.privateSubnetIds,
31 securityGroupIds: [databaseSecurityGroup.groupId],
32 });
33
34 const connect = yield* AWS.RDS.Connect(database.cluster, {
35 secret: database.secret,
36 subnetIds: vpc.privateSubnetIds,
37 securityGroupIds: [databaseSecurityGroup.groupId],
38 });
39
40 return Database.of({
41 query: <Row extends Record<string, unknown>>(
42 statement: string,
43 values: ReadonlyArray<unknown> = [],
44 ): Effect.Effect<ReadonlyArray<Row>, SqlError, RuntimeContext> =>
45 connect.pipe(
46 Effect.catch((cause) =>
47 Effect.fail(
48 new SqlError({
49 message: "Failed to resolve Aurora connection settings",
50 cause,
51 }),
52 ),
53 ),
54 Effect.flatMap((connection: AWS.RDS.ConnectionInfo) =>
55 Effect.tryPromise({
56 try: async () => {
57 const client = new Client({
58 host: connection.host,
59 port: connection.port,
60 database: connection.database,
61 user: connection.username,
62 password: connection.password,
63 // Example-only SSL posture for private Aurora connections.
64 ssl: connection.ssl
65 ? { rejectUnauthorized: false }
66 : undefined,
67 });
68
69 await client.connect();
70 try {
71 const result = await client.query<Row>({
72 text: statement,
73 values: [...values],
74 });
75 return result.rows as ReadonlyArray<Row>;
76 } finally {
77 await client.end();
78 }
79 },
80 catch: (cause) =>
81 new SqlError({
82 message: `Failed to execute SQL statement: ${statement}`,
83 cause,
84 }),
85 }),
86 ),
87 ),
88 });
89 }),
90);