| 1 | import * as Presign from "@distilled.cloud/aws/Presign"; |
| 2 | import * as PgClient from "@effect/sql-pg/PgClient"; |
| 3 | import * as Alchemy from "alchemy"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Redacted from "effect/Redacted"; |
| 6 | |
| 7 | export const BootstrapDatabase = Alchemy.Action( |
| 8 | "BootstrapDatabase", |
| 9 | Effect.fn(function* (input: { |
| 10 | endpoint: string; |
| 11 | roleArn: string; |
| 12 | version: string; |
| 13 | }) { |
| 14 | const token = yield* Presign.presignUrl({ |
| 15 | method: "GET", |
| 16 | url: `https://${input.endpoint}/?Action=DbConnectAdmin`, |
| 17 | service: "dsql", |
| 18 | expiresIn: 900, |
| 19 | }); |
| 20 | yield* Effect.gen(function* () { |
| 21 | const sql = yield* PgClient.PgClient; |
| 22 | // DSQL requires separate autocommit statements for catalog changes. |
| 23 | yield* sql`CREATE SCHEMA IF NOT EXISTS app`; |
| 24 | const roles = |
| 25 | yield* sql`SELECT rolname FROM pg_roles WHERE rolname = 'app_user'`; |
| 26 | if (roles.length === 0) { |
| 27 | yield* sql`CREATE ROLE app_user WITH LOGIN`; |
| 28 | } |
| 29 | yield* sql`CREATE TABLE IF NOT EXISTS app.todos ( |
| 30 | id uuid PRIMARY KEY, |
| 31 | text text NOT NULL, |
| 32 | done boolean NOT NULL DEFAULT false, |
| 33 | created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP |
| 34 | )`; |
| 35 | yield* sql`GRANT USAGE ON SCHEMA app TO app_user`; |
| 36 | yield* sql`GRANT SELECT, INSERT, UPDATE, DELETE ON app.todos TO app_user`; |
| 37 | const mappings = yield* sql`SELECT arn FROM sys.iam_pg_role_mappings |
| 38 | WHERE pg_role_name = 'app_user' AND arn = ${input.roleArn}`; |
| 39 | if (mappings.length === 0) { |
| 40 | yield* sql.unsafe( |
| 41 | `AWS IAM GRANT app_user TO '${input.roleArn.replaceAll("'", "''")}'`, |
| 42 | ); |
| 43 | } |
| 44 | }).pipe( |
| 45 | Effect.provide( |
| 46 | PgClient.layer({ |
| 47 | host: input.endpoint, |
| 48 | port: 5432, |
| 49 | database: "postgres", |
| 50 | username: "admin", |
| 51 | password: Redacted.make(token.slice("https://".length)), |
| 52 | ssl: { rejectUnauthorized: true, servername: input.endpoint }, |
| 53 | }), |
| 54 | ), |
| 55 | ); |
| 56 | return input.version; |
| 57 | }), |
| 58 | ); |