1import * as Effect from "effect/Effect";
2import * as Redacted from "effect/Redacted";
3import * as Schedule from "effect/Schedule";
4import {
5 type GetDatabaseResponse,
6 getDatabase,
7 createConnectionRotate,
8} from "@distilled.cloud/prisma/management";
9import { Retry } from "@distilled.cloud/prisma";
10import { extractConnectionSecrets } from "../Client.ts";
11import { parsePostgresOrigin, type PostgresOrigin } from "../PostgresOrigin.ts";
12import type { PrismaSecretConnection } from "../Types.ts";
13import type { ObservedDatabase } from "./Observed.ts";
14
15export const hasCanonicalConnectionSecrets = (
16 secrets: PrismaSecretConnection,
17) =>
18 secrets.directConnectionString !== undefined ||
19 secrets.pooledConnectionString !== undefined ||
20 secrets.accelerateConnectionString !== undefined;
21
22/**
23 * Derive the conventional `databaseUrl` and parsed `origin` / `pooledOrigin`
24 * attributes from a connection's secret strings. Shared between the live
25 * Connection provider and the dev provider so local `@prisma/dev`
26 * connections materialize the same shape.
27 */
28export const deriveConnectionAttrs = (secrets: {
29 directConnectionString?: Redacted.Redacted<string> | undefined;
30 pooledConnectionString?: Redacted.Redacted<string> | undefined;
31 accelerateConnectionString?: Redacted.Redacted<string> | undefined;
32}): {
33 databaseUrl: Redacted.Redacted<string> | undefined;
34 origin: PostgresOrigin | undefined;
35 pooledOrigin: PostgresOrigin | undefined;
36} => ({
37 databaseUrl:
38 secrets.pooledConnectionString ??
39 secrets.directConnectionString ??
40 secrets.accelerateConnectionString,
41 origin: secrets.directConnectionString
42 ? parsePostgresOrigin(Redacted.value(secrets.directConnectionString))
43 : undefined,
44 pooledOrigin: secrets.pooledConnectionString
45 ? parsePostgresOrigin(Redacted.value(secrets.pooledConnectionString))
46 : undefined,
47});
48
49export const mergeConnectionSecrets = (
50 preferred: PrismaSecretConnection,
51 fallback: PrismaSecretConnection,
52): PrismaSecretConnection => ({
53 directConnectionString:
54 preferred.directConnectionString ?? fallback.directConnectionString,
55 pooledConnectionString:
56 preferred.pooledConnectionString ?? fallback.pooledConnectionString,
57 accelerateConnectionString:
58 preferred.accelerateConnectionString ?? fallback.accelerateConnectionString,
59 host: preferred.host ?? fallback.host,
60 user: preferred.user ?? fallback.user,
61 password: preferred.password ?? fallback.password,
62});
63
64class DatabaseCredentialsNotReady extends Error {}
65
66const databaseCredentialsSchedule = Schedule.max([
67 Schedule.exponential("250 millis"),
68 Schedule.recurs(6),
69]);
70
71const waitForRotatableDatabase = (database: ObservedDatabase) =>
72 getDatabase({ databaseId: database.id }).pipe(
73 Effect.map((response) => response.data),
74 Effect.catchTag("NotFound", () =>
75 Effect.fail(
76 new DatabaseCredentialsNotReady(
77 `Prisma database '${database.name}' (${database.id}) is not visible yet while waiting to recover its credentials.`,
78 ),
79 ),
80 ),
81 Effect.flatMap((observed) =>
82 observed.status === "failure"
83 ? Effect.fail(
84 new Error(
85 `Prisma database '${observed.name}' (${observed.id}) entered terminal status 'failure' before credentials could be recovered.`,
86 ),
87 )
88 : observed.status === "ready" && observed.defaultConnectionId !== null
89 ? Effect.succeed(observed)
90 : Effect.fail(
91 new DatabaseCredentialsNotReady(
92 `Prisma database '${observed.name}' (${observed.id}) is '${observed.status}' with defaultConnectionId '${observed.defaultConnectionId ?? "null"}'; waiting for a ready default connection before recovering credentials.`,
93 ),
94 ),
95 ),
96 Effect.retry({
97 while: (error) => error instanceof DatabaseCredentialsNotReady,
98 schedule: databaseCredentialsSchedule,
99 }),
100 );
101
102/**
103 * Recover one-time database credentials after create success but before state
104 * persistence. Prisma's ordinary database reads omit those values, so rotate
105 * the observed default connection once when no canonical URL is available.
106 */
107export const recoverDatabaseConnectionSecrets = Effect.fn(function* <
108 D extends ObservedDatabase,
109>(initialDatabase: D, known: PrismaSecretConnection) {
110 if (initialDatabase.status === "failure") {
111 return yield* Effect.fail(
112 new Error(
113 `Prisma database '${initialDatabase.name}' (${initialDatabase.id}) is in terminal status 'failure'; credentials cannot be recovered.`,
114 ),
115 );
116 }
117 let database: D | GetDatabaseResponse["data"] = initialDatabase;
118 const observedConnection =
119 database.connections.find(
120 (connection) => connection.id === database.defaultConnectionId,
121 ) ?? database.connections[0];
122 const available = mergeConnectionSecrets(
123 extractConnectionSecrets(observedConnection),
124 known,
125 );
126 if (hasCanonicalConnectionSecrets(available)) {
127 return { database, secrets: available };
128 }
129
130 if (database.status !== "ready" || database.defaultConnectionId === null) {
131 database = yield* waitForRotatableDatabase(database);
132 }
133
134 const refreshedConnection =
135 database.connections.find(
136 (connection) => connection.id === database.defaultConnectionId,
137 ) ?? database.connections[0];
138 const refreshed = mergeConnectionSecrets(
139 extractConnectionSecrets(refreshedConnection),
140 available,
141 );
142 if (hasCanonicalConnectionSecrets(refreshed)) {
143 return { database, secrets: refreshed };
144 }
145
146 const connectionId = database.defaultConnectionId;
147 if (connectionId === null) {
148 return yield* Effect.fail(
149 new DatabaseCredentialsNotReady(
150 `Prisma database '${database.name}' (${database.id}) was reported ready without a defaultConnectionId after the credential recovery wait.`,
151 ),
152 );
153 }
154 const rotated = yield* createConnectionRotate({ id: connectionId }).pipe(
155 // Rotation mints new credentials; a replay would revoke the ones we
156 // just persisted, so opt out of the retry policy.
157 Retry.none,
158 Effect.map((response) => response.data),
159 );
160 if (rotated.id !== connectionId || rotated.database.id !== database.id) {
161 return yield* Effect.fail(
162 new Error(
163 `Prisma rotated connection '${rotated.id}' for database '${rotated.database.id}', but connection '${connectionId}' belonging to database '${database.id}' was requested. Refusing to persist mismatched credentials.`,
164 ),
165 );
166 }
167 const recovered = mergeConnectionSecrets(
168 extractConnectionSecrets(rotated),
169 refreshed,
170 );
171 if (!hasCanonicalConnectionSecrets(recovered)) {
172 return yield* Effect.fail(
173 new Error(
174 `Prisma rotated default connection '${connectionId}' for database '${database.id}' without returning a canonical connection URL. Retry after database provisioning completes.`,
175 ),
176 );
177 }
178 return { database, secrets: recovered };
179});