claudia_blog/server/db.js

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
}