1import * as Drizzle from "@/Drizzle/Postgres.ts";
2import * as GCP from "@/GCP";
3import { sql } from "drizzle-orm";
4import * as Effect from "effect/Effect";
5import * as Redacted from "effect/Redacted";
6import { serveProbes } from "../../bindingHost.ts";
7
8/** Test-only password; the test writes it into {@link Password}. */
9export const USER_PASSWORD = "Alchemy-bindings-test-1";
10
11export 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 */
41export 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) {}