BlogEditor/backend/config/database.js

179 lines
6.3 KiB
JavaScript

import pkg from 'pg'
import dotenv from 'dotenv'
dotenv.config()
const { Pool } = pkg
// Support both connection string (Supabase) and individual parameters
let poolConfig
let pool = null
// Validate and prepare pool configuration
function createPoolConfig() {
if (process.env.DATABASE_URL) {
// Use connection string (Supabase format)
// Validate connection string format
try {
const url = new URL(process.env.DATABASE_URL)
// Check for placeholder passwords
if (!url.password || url.password === '[YOUR-PASSWORD]' || url.password.includes('YOUR-PASSWORD')) {
const error = new Error('DATABASE_URL contains placeholder password. Please replace [YOUR-PASSWORD] with your actual Supabase password.')
error.code = 'INVALID_PASSWORD'
throw error
}
if (url.password.length < 1) {
console.warn('⚠️ DATABASE_URL appears to be missing password. Check your .env file.')
}
} catch (e) {
if (e.code === 'INVALID_PASSWORD') {
throw e
}
console.error('❌ Invalid DATABASE_URL format. Expected: postgresql://user:password@host:port/database')
throw new Error('Invalid DATABASE_URL format')
}
poolConfig = {
connectionString: process.env.DATABASE_URL,
ssl: {
rejectUnauthorized: false // Supabase requires SSL
},
// Connection pool settings for Supabase
// Reduced max connections to prevent pool limit issues when running multiple apps
max: 5, // Maximum number of clients in the pool (reduced for hot reload compatibility)
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 10000, // Increased timeout for Supabase
allowExitOnIdle: false,
}
} else {
// Use individual parameters (local development)
poolConfig = {
host: process.env.DB_HOST || 'localhost',
port: process.env.DB_PORT || 5432,
database: process.env.DB_NAME || 'blog_editor',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD,
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false,
connectionTimeoutMillis: 10000,
}
}
return poolConfig
}
// Initialize pool
try {
poolConfig = createPoolConfig()
pool = new Pool(poolConfig)
} catch (error) {
if (error.code === 'INVALID_PASSWORD') {
console.error('\n❌ ' + error.message)
console.error('💡 Please update your .env file with the correct DATABASE_URL')
console.error('💡 Format: postgresql://postgres.xxx:YOUR_ACTUAL_PASSWORD@aws-1-ap-south-1.pooler.supabase.com:5432/postgres\n')
}
// Create a dummy pool to prevent crashes, but it won't work
pool = new Pool({ connectionString: 'postgresql://invalid' })
}
// Reset pool function for recovery from authentication errors
export async function resetPool() {
if (pool) {
try {
await pool.end() // Wait for pool to fully close
} catch (err) {
// Ignore errors during pool closure
}
pool = null
}
// Wait a moment for Supabase circuit breaker to potentially reset
await new Promise(resolve => setTimeout(resolve, 2000))
try {
poolConfig = createPoolConfig()
pool = new Pool(poolConfig)
setupPoolHandlers()
return true
} catch (error) {
console.error('❌ Failed to reset connection pool:', error.message)
return false
}
}
// Setup pool error handlers
function setupPoolHandlers() {
if (pool) {
pool.on('error', (err) => {
console.error('❌ Unexpected error on idle database client:', err.message)
// Don't exit on error - let the application handle it
})
}
}
setupPoolHandlers()
export { pool }
// Helper function to test connection and provide better error messages
export async function testConnection(retryCount = 0) {
try {
// If pool is null or invalid, try to recreate it
if (!pool || pool.ended) {
console.log(' 🔄 Recreating connection pool...')
await resetPool()
}
const client = await pool.connect()
const result = await client.query('SELECT NOW()')
client.release()
return { success: true, time: result.rows[0].now }
} catch (error) {
// Handle authentication errors
if (error.message.includes('password authentication failed') ||
error.message.includes('password') && error.message.includes('failed')) {
const err = new Error('Database authentication failed. Check your password in DATABASE_URL')
err.code = 'AUTH_FAILED'
throw err
}
// Handle circuit breaker / too many attempts
else if (error.message.includes('Circuit breaker') ||
error.message.includes('too many') ||
error.message.includes('connection attempts') ||
error.message.includes('rate limit') ||
error.code === '53300') { // PostgreSQL error code for too many connections
// If this is the first retry, try resetting the pool and waiting
if (retryCount === 0) {
console.log(' ⏳ Circuit breaker detected. Waiting and retrying...')
await new Promise(resolve => setTimeout(resolve, 3000)) // Wait 3 seconds
await resetPool()
// Retry once
return testConnection(1)
}
const err = new Error('Too many failed connection attempts. Supabase connection pooler has temporarily blocked connections. Please wait 30-60 seconds and restart the server, or verify your DATABASE_URL password is correct.')
err.code = 'CIRCUIT_BREAKER'
throw err
}
// Handle host resolution errors
else if (error.message.includes('ENOTFOUND') || error.message.includes('getaddrinfo')) {
const err = new Error('Cannot resolve database host. Check your DATABASE_URL hostname.')
err.code = 'HOST_ERROR'
throw err
}
// Handle timeout errors
else if (error.message.includes('timeout') || error.message.includes('ETIMEDOUT')) {
const err = new Error('Database connection timeout. Check if the database is accessible and your network connection.')
err.code = 'TIMEOUT'
throw err
}
// Handle invalid connection string
else if (error.message.includes('invalid connection') || error.message.includes('connection string')) {
const err = new Error('Invalid DATABASE_URL format. Expected: postgresql://user:password@host:port/database')
err.code = 'INVALID_FORMAT'
throw err
}
throw error
}
}