1import * as Alchemy from "alchemy";
2import * as AWS from "alchemy/AWS";
3import * as Effect from "effect/Effect";
4import * as Schedule from "effect/Schedule";
5import { database } from "./database.ts";
6
7// One atomic, replay-safe initial migration. Application roles never own DDL.
8const initialMigration = `DO $$
9BEGIN
10 IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'app_iam') THEN
11 CREATE ROLE app_iam LOGIN;
12 END IF;
13 GRANT rds_iam TO app_iam;
14 REVOKE CREATE ON SCHEMA public FROM PUBLIC;
15 CREATE TABLE IF NOT EXISTS public.todos (
16 id uuid PRIMARY KEY,
17 title text NOT NULL,
18 done boolean NOT NULL DEFAULT false
19 );
20 GRANT CONNECT ON DATABASE app TO app_iam;
21 GRANT USAGE ON SCHEMA public TO app_iam;
22 GRANT SELECT, INSERT, UPDATE, DELETE ON public.todos TO app_iam;
23END $$`;
24
25export const bootstrap = Effect.gen(function* () {
26 const db = yield* database;
27 const Bootstrap = Alchemy.Action(
28 "BootstrapDatabase",
29 Effect.gen(function* () {
30 const execute = yield* AWS.RDSData.ExecuteStatement(db.cluster, {
31 secret: db.secret,
32 database: "app",
33 });
34 return Effect.fn(function* (input: { writer: string; sql: string }) {
35 yield* execute({ sql: input.sql }).pipe(
36 Effect.retry({
37 while: (error) =>
38 error._tag === "DatabaseUnavailableException" ||
39 error._tag === "DatabaseResumingException",
40 schedule: Schedule.spaced("5 seconds"),
41 times: 8,
42 }),
43 );
44 return "1";
45 });
46 }).pipe(Effect.provide(AWS.RDSData.ExecuteStatementHttp)),
47 );
48 return yield* Bootstrap({
49 writer: db.writer.dbInstanceArn,
50 sql: initialMigration,
51 });
52});