/** * CannaiQ Database Connection * * All database access for the CannaiQ platform goes through this module. * * SINGLE DATABASE ARCHITECTURE: * - All services (auth, orchestrator, crawlers, admin) use this ONE database * - States are modeled via states table + state_id on dispensaries (not separate DBs) * * CONFIGURATION (in priority order): * 1. CANNAIQ_DB_URL - Full connection string (preferred) * 2. Individual vars: CANNAIQ_DB_HOST, CANNAIQ_DB_PORT, CANNAIQ_DB_NAME, CANNAIQ_DB_USER, CANNAIQ_DB_PASS * 3. DATABASE_URL - Legacy fallback for K8s compatibility * * IMPORTANT: * - Do NOT create separate pools elsewhere * - All services should import from this module */ import { Pool, PoolClient } from 'pg'; /** * Get the database connection string from environment variables. * Supports multiple configuration methods with fallback for legacy compatibility. */ function getConnectionString(): string { // Priority 1: Full CANNAIQ connection URL if (process.env.CANNAIQ_DB_URL) { return process.env.CANNAIQ_DB_URL; } // Priority 2: Build from individual CANNAIQ env vars const host = process.env.CANNAIQ_DB_HOST; const port = process.env.CANNAIQ_DB_PORT; const name = process.env.CANNAIQ_DB_NAME; const user = process.env.CANNAIQ_DB_USER; const pass = process.env.CANNAIQ_DB_PASS; if (host && port && name && user && pass) { return `postgresql://${user}:${pass}@${host}:${port}/${name}`; } // Priority 3: Fallback to DATABASE_URL for legacy/K8s compatibility if (process.env.DATABASE_URL) { return process.env.DATABASE_URL; } // Report what's missing const required = ['CANNAIQ_DB_HOST', 'CANNAIQ_DB_PORT', 'CANNAIQ_DB_NAME', 'CANNAIQ_DB_USER', 'CANNAIQ_DB_PASS']; const missing = required.filter((key) => !process.env[key]); throw new Error( `[CannaiQ DB] Missing database configuration.\n` + `Set CANNAIQ_DB_URL, DATABASE_URL, or all of: ${missing.join(', ')}` ); } let pool: Pool | null = null; /** * Get the CannaiQ database pool (singleton) * * This is the canonical pool for all CannaiQ services. * Do NOT create separate pools elsewhere. */ export function getPool(): Pool { if (!pool) { pool = new Pool({ connectionString: getConnectionString(), max: 10, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, }); pool.on('error', (err) => { console.error('[CannaiQ DB] Unexpected error on idle client:', err); }); console.log('[CannaiQ DB] Pool initialized'); } return pool; } /** * @deprecated Use getPool() instead */ export function getDutchieAZPool(): Pool { console.warn('[CannaiQ DB] getDutchieAZPool() is deprecated. Use getPool() instead.'); return getPool(); } /** * Execute a query on the CannaiQ database */ export async function query(text: string, params?: any[]): Promise<{ rows: T[]; rowCount: number }> { const p = getPool(); const result = await p.query(text, params); return { rows: result.rows as T[], rowCount: result.rowCount || 0 }; } /** * Get a client from the pool for transaction use */ export async function getClient(): Promise { const p = getPool(); return p.connect(); } /** * Close the pool connection */ export async function closePool(): Promise { if (pool) { await pool.end(); pool = null; console.log('[CannaiQ DB] Pool closed'); } } /** * Check if the database is accessible */ export async function healthCheck(): Promise { try { const result = await query('SELECT 1 as ok'); return result.rows.length > 0 && result.rows[0].ok === 1; } catch (error) { console.error('[CannaiQ DB] Health check failed:', error); return false; } }