1import type * as secretmanager from "@distilled.cloud/gcp/secretmanager_v1";
2import * as Data from "effect/Data";
3import type * as Effect from "effect/Effect";
4import type * as Redacted from "effect/Redacted";
5import * as Binding from "../../Binding.ts";
6import type { RuntimeContext } from "../../RuntimeContext.ts";
7import type { SecretBindingTarget } from "../SecretManager/BindingHttp.ts";
8import type { Database } from "./Database.ts";
9import type { Instance } from "./Instance.ts";
10import type { User } from "./User.ts";
11
12export interface ConnectOptions {
13 /** Database to connect to. */
14 database: Database;
15 /** Built-in database user to log in as. */
16 user: User;
17 /**
18 * Secret Manager secret whose `latest` version holds the user's
19 * password.
20 */
21 passwordSecret: SecretBindingTarget;
22}
23
24export interface ConnectionInfo {
25 /** Instance connection name `project:region:instance`. */
26 connectionName: string;
27 /** Unix socket directory, `/cloudsql/{connectionName}`. */
28 socketPath: string;
29 /** Database name. */
30 database: string;
31 /** Database user name. */
32 username: string;
33 /** Password read from the secret. */
34 password: Redacted.Redacted<string>;
35 /**
36 * Connection URL over the Unix socket — `postgresql://…?host=/cloudsql/…`
37 * for PostgreSQL, `mysql://…?socketPath=/cloudsql/…` for MySQL. Feed it
38 * to `Drizzle.Postgres` / your driver. `Redacted` because it embeds the
39 * password.
40 */
41 url: Redacted.Redacted<string>;
42}
43
44/** The password secret has no readable `latest` version. */
45export class PasswordMissing extends Data.TaggedError(
46 "GCP.SQL.PasswordMissing",
47)<{ secret: string }> {}
48
49/**
50 * Runtime binding that connects a Cloud Run service to a Cloud SQL
51 * instance over the Cloud SQL Unix socket — no public IP allow-list, no
52 * VPC.
53 *
54 * Binding it at init grants `roles/cloudsql.client` on the project under
55 * an IAM Condition naming only this instance, mounts the instance as the
56 * service's `cloudsql` volume (the socket appears at
57 * `/cloudsql/{connectionName}`), and grants
58 * `roles/secretmanager.secretAccessor` on the password secret. The
59 * returned Effect reads the password and resolves a {@link ConnectionInfo}
60 * on each execution; no socket is opened. Provide
61 * {@link ConnectHttp}. Cloud Run services only.
62 *
63 * ### Connecting with Drizzle
64 * **Example:** Drizzle over the Cloud SQL socket
65 * ```typescript
66 * const connect = yield* GCP.SQL.Connect(instance, {
67 * database,
68 * user,
69 * passwordSecret,
70 * });
71 * const db = yield* Drizzle.Postgres(
72 * connect.pipe(Effect.map((info) => info.url)),
73 * );
74 * // …provided with Effect.provide(GCP.SQL.ConnectHttp)
75 * ```
76 *
77 * @binding
78 * @category SQL
79 */
80export interface Connect extends Binding.Service<
81 Connect,
82 "GCP.SQL.Connect",
83 (
84 instance: Instance,
85 options: ConnectOptions,
86 ) => Effect.Effect<
87 Effect.Effect<
88 ConnectionInfo,
89 secretmanager.AccessProjectsSecretsVersionsError | PasswordMissing,
90 RuntimeContext
91 >
92 >
93> {}
94
95export const Connect = Binding.Service<Connect>("GCP.SQL.Connect");