165 lines
4.0 KiB
JavaScript
165 lines
4.0 KiB
JavaScript
const { Pool, Client } = require('pg')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
|
|
const dotenvPath = path.resolve(__dirname, '..', '.env')
|
|
if (fs.existsSync(dotenvPath)) {
|
|
require('dotenv').config({ path: dotenvPath })
|
|
} else {
|
|
require('dotenv').config()
|
|
}
|
|
|
|
const hasConnectionString =
|
|
typeof process.env.DATABASE_URL === 'string' && process.env.DATABASE_URL.trim().length > 0
|
|
|
|
if (!hasConnectionString) {
|
|
console.warn('[db] DATABASE_URL is not set. Falling back to discrete Postgres environment variables.')
|
|
}
|
|
|
|
const sslConfig =
|
|
process.env.DATABASE_SSL === 'true' || process.env.PGSSLMODE === 'require'
|
|
? { rejectUnauthorized: false }
|
|
: undefined
|
|
|
|
const defaultConfig = {
|
|
host: process.env.PGHOST || 'localhost',
|
|
port: Number(process.env.PGPORT || 5432),
|
|
user: process.env.PGUSER || 'postgres',
|
|
password: process.env.PGPASSWORD || 'postgres',
|
|
database: process.env.PGDATABASE || 'claudia_blog'
|
|
}
|
|
|
|
const poolConfig = hasConnectionString
|
|
? { connectionString: process.env.DATABASE_URL.trim() }
|
|
: { ...defaultConfig }
|
|
|
|
if (sslConfig) {
|
|
poolConfig.ssl = sslConfig
|
|
}
|
|
|
|
let pool
|
|
let ensuringDatabasePromise
|
|
|
|
function quoteIdentifier(identifier) {
|
|
return `"${identifier.replace(/"/g, '""')}"`
|
|
}
|
|
|
|
function resolveTargetDatabase() {
|
|
if (hasConnectionString) {
|
|
try {
|
|
const url = new URL(process.env.DATABASE_URL.trim())
|
|
const dbName = url.pathname ? url.pathname.replace(/^\//, '') : null
|
|
return dbName || null
|
|
} catch (error) {
|
|
console.warn(`[db] Unable to parse DATABASE_URL: ${error.message}`)
|
|
return null
|
|
}
|
|
}
|
|
|
|
return poolConfig.database || null
|
|
}
|
|
|
|
async function ensureDatabaseExists(force = false) {
|
|
if (ensuringDatabasePromise && !force) {
|
|
return ensuringDatabasePromise
|
|
}
|
|
|
|
ensuringDatabasePromise = (async () => {
|
|
const targetDb = resolveTargetDatabase()
|
|
if (!targetDb) {
|
|
return
|
|
}
|
|
|
|
let adminConfig
|
|
|
|
if (hasConnectionString) {
|
|
try {
|
|
const adminUrl = new URL(process.env.DATABASE_URL.trim())
|
|
adminUrl.pathname = '/postgres'
|
|
adminConfig = { connectionString: adminUrl.toString() }
|
|
if (sslConfig) {
|
|
adminConfig.ssl = sslConfig
|
|
}
|
|
} catch (error) {
|
|
console.warn(`[db] Unable to prepare admin connection to create database: ${error.message}`)
|
|
return
|
|
}
|
|
} else {
|
|
const adminDatabase = process.env.PGDEFAULTDB || 'postgres'
|
|
adminConfig = {
|
|
...poolConfig,
|
|
database: adminDatabase
|
|
}
|
|
}
|
|
|
|
const client = new Client(adminConfig)
|
|
|
|
try {
|
|
await client.connect()
|
|
const exists = await client.query('SELECT 1 FROM pg_database WHERE datname = $1', [targetDb])
|
|
if (exists.rowCount === 0) {
|
|
await client.query(`CREATE DATABASE ${quoteIdentifier(targetDb)}`)
|
|
console.log(`[db] Created database ${targetDb}`)
|
|
}
|
|
} catch (error) {
|
|
if (error.code === '42P04') {
|
|
return
|
|
}
|
|
console.warn(`[db] Could not ensure database ${targetDb}: ${error.message}`)
|
|
} finally {
|
|
await client.end().catch(() => {})
|
|
}
|
|
})()
|
|
|
|
return ensuringDatabasePromise
|
|
}
|
|
|
|
async function initialisePool() {
|
|
const activePool = new Pool(poolConfig)
|
|
activePool.on('error', (err) => {
|
|
console.error('[db] Unexpected error on idle client', err)
|
|
})
|
|
// Force a connection so we surface errors immediately
|
|
await activePool.query('SELECT 1')
|
|
return activePool
|
|
}
|
|
|
|
async function getPool() {
|
|
if (pool) {
|
|
return pool
|
|
}
|
|
|
|
try {
|
|
await ensureDatabaseExists()
|
|
pool = await initialisePool()
|
|
return pool
|
|
} catch (error) {
|
|
if (error.code === '3D000') {
|
|
// Database missing, retry once after forcing ensure
|
|
await ensureDatabaseExists(true)
|
|
pool = await initialisePool()
|
|
return pool
|
|
}
|
|
throw error
|
|
}
|
|
}
|
|
|
|
async function query(text, params) {
|
|
const activePool = await getPool()
|
|
return activePool.query(text, params)
|
|
}
|
|
|
|
async function closePool() {
|
|
if (pool) {
|
|
await pool.end()
|
|
pool = null
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
query,
|
|
getPool,
|
|
closePool,
|
|
ensureDatabaseExists
|
|
}
|