Docker
This commit is contained in:
164
server/db.js
Normal file
164
server/db.js
Normal file
@@ -0,0 +1,164 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user