| 1 | import * as Drizzle from "@/Drizzle/Postgres.ts"; |
| 2 | import * as GCP from "@/GCP"; |
| 3 | import { sql } from "drizzle-orm"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Redacted from "effect/Redacted"; |
| 6 | import { serveProbes } from "../../bindingHost.ts"; |
| 7 | |
| 8 | /** Test-only password; the test writes it into {@link Password}. */ |
| 9 | export const USER_PASSWORD = "Alchemy-bindings-test-1"; |
| 10 | |
| 11 | export const Db = Effect.gen(function* () { |
| 12 | // Shared-core, zonal, no backups: the cheapest Cloud SQL there is. |
| 13 | const instance = yield* GCP.SQL.Instance("BindingsDb", { |
| 14 | databaseVersion: "POSTGRES_17", |
| 15 | edition: "ENTERPRISE", |
| 16 | tier: "db-f1-micro", |
| 17 | availabilityType: "ZONAL", |
| 18 | backupEnabled: false, |
| 19 | deletionProtectionEnabled: false, |
| 20 | dataApiAccess: true, |
| 21 | }); |
| 22 | const database = yield* GCP.SQL.Database("App", { |
| 23 | instance: instance.instanceName, |
| 24 | }); |
| 25 | const user = yield* GCP.SQL.User("AppUser", { |
| 26 | instance: instance.instanceName, |
| 27 | password: USER_PASSWORD, |
| 28 | }); |
| 29 | // The Data API only accepts regional secrets in the instance's region. |
| 30 | const password = yield* GCP.SecretManager.LocationsSecret("AppPassword", { |
| 31 | location: instance.region, |
| 32 | }); |
| 33 | return { instance, database, user, password }; |
| 34 | }); |
| 35 | |
| 36 | /** |
| 37 | * Effect-native Cloud Run service exercising every Cloud SQL binding as |
| 38 | * its own runtime service account. Deployed from |
| 39 | * {@link ../Bindings.test.ts}. |
| 40 | */ |
| 41 | export default class SqlBindingsHost extends GCP.Function<SqlBindingsHost>()( |
| 42 | "SqlBindingsHost", |
| 43 | { main: import.meta.url, invokerIamDisabled: true }, |
| 44 | Effect.gen(function* () { |
| 45 | const { instance, database, user, password } = yield* Db; |
| 46 | const connect = yield* GCP.SQL.Connect(instance, { |
| 47 | database, |
| 48 | user, |
| 49 | passwordSecret: password, |
| 50 | }); |
| 51 | const executeSql = yield* GCP.SQL.ExecuteSql(instance); |
| 52 | const getInstance = yield* GCP.SQL.GetInstance(instance); |
| 53 | const getUser = yield* GCP.SQL.GetUser(user); |
| 54 | const db = yield* Drizzle.Postgres( |
| 55 | connect.pipe(Effect.map((info) => info.url)), |
| 56 | ); |
| 57 | const databaseName = yield* database.databaseName; |
| 58 | const userName = yield* user.userName; |
| 59 | const secretName = yield* password.name; |
| 60 | |
| 61 | return { |
| 62 | fetch: serveProbes({ |
| 63 | connect: Effect.gen(function* () { |
| 64 | const info = yield* connect; |
| 65 | // A real query over the `/cloudsql` socket: the Cloud SQL |
| 66 | // connector authorizes it with the host's cloudsql.client grant. |
| 67 | const rows = yield* db.execute<{ user: string; database: string }>( |
| 68 | sql`SELECT current_user AS "user", current_database() AS "database"`, |
| 69 | "objects", |
| 70 | ); |
| 71 | return { |
| 72 | connectionName: info.connectionName, |
| 73 | socketPath: info.socketPath, |
| 74 | username: info.username, |
| 75 | database: info.database, |
| 76 | passwordMatches: Redacted.value(info.password) === USER_PASSWORD, |
| 77 | rows, |
| 78 | }; |
| 79 | }), |
| 80 | executeSql: Effect.gen(function* () { |
| 81 | const response = yield* executeSql({ |
| 82 | body: { |
| 83 | database: yield* databaseName, |
| 84 | user: yield* userName, |
| 85 | passwordSecretVersion: `${yield* secretName}/versions/latest`, |
| 86 | sqlStatement: "SELECT current_user AS who, 41 + 1 AS answer", |
| 87 | }, |
| 88 | }); |
| 89 | return { |
| 90 | status: response.status, |
| 91 | columns: (response.results?.[0]?.columns ?? []).map( |
| 92 | (column) => column.name, |
| 93 | ), |
| 94 | values: (response.results?.[0]?.rows?.[0]?.values ?? []).map( |
| 95 | (cell) => cell.value, |
| 96 | ), |
| 97 | }; |
| 98 | }), |
| 99 | getInstance: getInstance().pipe( |
| 100 | Effect.map((live) => ({ |
| 101 | name: live.name, |
| 102 | state: live.state, |
| 103 | connectionName: live.connectionName, |
| 104 | })), |
| 105 | ), |
| 106 | getUser: getUser().pipe( |
| 107 | Effect.map((live) => ({ name: live.name, instance: live.instance })), |
| 108 | ), |
| 109 | }), |
| 110 | }; |
| 111 | }).pipe( |
| 112 | Effect.provide(GCP.SQL.ConnectHttp), |
| 113 | Effect.provide(GCP.SQL.ExecuteSqlHttp), |
| 114 | Effect.provide(GCP.SQL.GetInstanceHttp), |
| 115 | Effect.provide(GCP.SQL.GetUserHttp), |
| 116 | ), |
| 117 | ) {} |