diff --git a/src/database/index.ts b/src/database/index.ts index 437f5a4..d3d8bc9 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -5,11 +5,13 @@ import { logger } from '../utils/logger'; // Prisma 7 requires a driver adapter for direct database connections. const resolveDbSchema = (): string => { - const raw = process.env.DATABASE_URL || ''; + const raw = (process.env.DATABASE_URL || '').trim().replace(/^['"]|['"]$/g, ''); try { const u = new URL(raw); // Prisma-style: ?schema=kord_live - return u.searchParams.get('schema') || 'public'; + const schema = u.searchParams.get('schema') || 'public'; + // Guard against invalid/unsafe identifiers in connection options. + return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schema) ? schema : 'public'; } catch { return 'public'; } @@ -18,6 +20,8 @@ const resolveDbSchema = (): string => { const dbSchema = resolveDbSchema(); const pool = new Pool({ connectionString: process.env.DATABASE_URL, + // Fail fast when DB is temporarily unreachable; connectDB will retry. + connectionTimeoutMillis: 5_000, // pg(Pool) doesn't understand Prisma's `?schema=...` param. // Force PostgreSQL search_path so both Prisma adapter and raw queries // operate on the intended schema (e.g. kord_live). @@ -30,16 +34,36 @@ export const prisma = new PrismaClient({ log: ['warn', 'error'], }); +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + export const connectDB = async () => { - try { - // Adapter-based client connects when first used, - // but we can test the pool connection here. - const client = await pool.connect(); - const { rows } = await client.query('SHOW search_path;'); - client.release(); - logger.info(`Connected to PostgreSQL successfully via Driver Adapter. (search_path=${rows?.[0]?.search_path ?? 'unknown'})`); - } catch (error) { - logger.error('Failed to connect to PostgreSQL:', error); - process.exit(1); + const maxAttempts = 6; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + // Adapter-based client connects when first used, + // but we can test the pool connection here. + const client = await pool.connect(); + const { rows } = await client.query('SHOW search_path;'); + client.release(); + + let hostPort = 'unknown'; + try { + const u = new URL((process.env.DATABASE_URL || '').trim().replace(/^['"]|['"]$/g, '')); + hostPort = `${u.hostname}:${u.port || '5432'}`; + } catch { + // ignore + } + + logger.info( + `Connected to PostgreSQL successfully via Driver Adapter. (targetSchema=${dbSchema}, search_path=${rows?.[0]?.search_path ?? 'unknown'}, host=${hostPort})`, + ); + return; + } catch (error) { + const isLast = attempt === maxAttempts; + logger.error(`Failed to connect to PostgreSQL (attempt ${attempt}/${maxAttempts}):`, error); + if (isLast) process.exit(1); + // Backoff: 0.5s, 1s, 2s, 4s, 8s... + await sleep(500 * 2 ** (attempt - 1)); + } } };