Initial commit: UOMysticmoon backend (Express + MariaDB + JWT)
- Layered API (router -> controller -> model -> db), serverlinkr pattern - Public / auth / admin route groups; posts, wiki, settings, users, activity models - JWT httpOnly-cookie auth (Secure auto-detected: LAN HTTP + Pangolin HTTPS) - Site LIVE/MAINTENANCE mode with admin preview bypass - Dual file+console logging (info/warn/error/debug) + HTTP access logs - Docker Compose (app + MariaDB), schema.sql + seed, .env.example - Verified end-to-end against MariaDB (27/27 smoke checks) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
78
server/src/utils/db.js
Normal file
78
server/src/utils/db.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const mariadb = require('mariadb')
|
||||
require('dotenv').config()
|
||||
|
||||
const log = require('./logger')('db')
|
||||
|
||||
const pool = mariadb.createPool({
|
||||
host: process.env.DB_HOST || '127.0.0.1',
|
||||
port: Number(process.env.DB_PORT) || 3306,
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD || '',
|
||||
database: process.env.DB_NAME || 'uomysticmoon',
|
||||
connectionLimit: 5,
|
||||
// Return plain JS numbers, never BigInt — keeps JSON responses clean.
|
||||
insertIdAsNumber: true,
|
||||
bigIntAsNumber: true,
|
||||
decimalAsNumber: true,
|
||||
})
|
||||
|
||||
/**
|
||||
* Run a parameterized query and release the connection.
|
||||
* @param {string} sql
|
||||
* @param {Array} [params]
|
||||
*/
|
||||
async function query(sql, params) {
|
||||
const conn = await pool.getConnection()
|
||||
try {
|
||||
return await conn.query(sql, params)
|
||||
} finally {
|
||||
conn.release()
|
||||
}
|
||||
}
|
||||
|
||||
const SCHEMA_PATH = path.join(__dirname, '..', '..', 'db', 'schema.sql')
|
||||
|
||||
/**
|
||||
* Create tables if they do not exist. Idempotent. Retries while the DB is still
|
||||
* coming up (important under docker-compose even with a healthcheck).
|
||||
*/
|
||||
async function ensureSchema({ retries = 10, delayMs = 2000 } = {}) {
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
const conn = await pool.getConnection()
|
||||
try {
|
||||
const sql = fs.readFileSync(SCHEMA_PATH, 'utf8')
|
||||
// Strip full-line comments first, then split — so a leading comment block
|
||||
// doesn't get glued onto (and discard) the statement that follows it.
|
||||
const statements = sql
|
||||
.split('\n')
|
||||
.filter((line) => !line.trim().startsWith('--'))
|
||||
.join('\n')
|
||||
.split(';')
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0)
|
||||
for (const statement of statements) {
|
||||
await conn.query(statement)
|
||||
}
|
||||
log.info('schema ensured')
|
||||
return
|
||||
} finally {
|
||||
conn.release()
|
||||
}
|
||||
} catch (err) {
|
||||
if (attempt === retries) throw err
|
||||
log.warn(`database not ready, retrying (attempt ${attempt}/${retries})`, {
|
||||
code: err.code || err.message,
|
||||
})
|
||||
await new Promise((r) => setTimeout(r, delayMs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function close() {
|
||||
await pool.end()
|
||||
}
|
||||
|
||||
module.exports = { pool, query, ensureSchema, close }
|
||||
Reference in New Issue
Block a user