Files
dwroller/database/sqlite-db.js
T

273 lines
8.9 KiB
JavaScript

const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
// Simple DB logger to backend.log
const backendLogPath = path.join(__dirname, 'backend.log');
function logToFile(...args) {
try {
const msg = `[${new Date().toISOString()}] ` + args.map(a => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ') + '\n';
fs.appendFileSync(backendLogPath, msg, { encoding: 'utf8' });
} catch (err) {
console.error('Failed to write backend log', err);
}
}
// Create database directory if it doesn't exist
const dbDir = path.join(__dirname, 'sqlite');
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
const dbPath = path.join(dbDir, 'deathwatch.db');
const db = new Database(dbPath);
// Enable WAL mode for better concurrency
db.pragma('journal_mode = WAL');
// Create tables
const createTables = () => {
// Players table
db.exec(`
CREATE TABLE IF NOT EXISTS players (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
roller_info TEXT DEFAULT '{}',
shop_info TEXT DEFAULT '{}',
tab_info TEXT DEFAULT '{}',
pw TEXT DEFAULT '',
pw_hash TEXT DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Sessions table (if you need it)
db.exec(`
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT UNIQUE NOT NULL,
data TEXT DEFAULT '{}',
expires_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Create indexes for better performance
db.exec(`
CREATE INDEX IF NOT EXISTS idx_players_name ON players(name);
CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id);
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
CREATE TABLE IF NOT EXISTS rules_staging (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT,
content TEXT,
category TEXT,
page TEXT,
original_json TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_rules_staging_category ON rules_staging(category);
`);
console.log('SQLite tables created successfully');
};
// Initialize database
createTables();
// Prepared statements for common operations
const statements = {
// Player operations
getAllPlayers: db.prepare('SELECT * FROM players ORDER BY name'),
getPlayerByName: db.prepare('SELECT * FROM players WHERE name = ?'),
insertPlayer: db.prepare(`
INSERT INTO players (name, roller_info, shop_info, tab_info, pw, pw_hash)
VALUES (?, ?, ?, ?, ?, ?)
`),
updatePlayer: db.prepare(`
UPDATE players
SET roller_info = ?, shop_info = ?, tab_info = ?, pw = ?, pw_hash = ?, updated_at = CURRENT_TIMESTAMP
WHERE name = ?
`),
deletePlayer: db.prepare('DELETE FROM players WHERE name = ?'),
// Session operations
getSession: db.prepare('SELECT * FROM sessions WHERE session_id = ? AND expires_at > datetime(\'now\')'),
insertSession: db.prepare(`
INSERT INTO sessions (session_id, data, expires_at)
VALUES (?, ?, ?)
`),
updateSession: db.prepare(`
UPDATE sessions
SET data = ?, expires_at = ?, updated_at = CURRENT_TIMESTAMP
WHERE session_id = ?
`),
deleteSession: db.prepare('DELETE FROM sessions WHERE session_id = ?'),
cleanExpiredSessions: db.prepare('DELETE FROM sessions WHERE expires_at <= datetime(\'now\')')
};
// Helper functions
const playerHelpers = {
getAll: () => {
logToFile('DB: getAllPlayers - start');
const rows = statements.getAllPlayers.all();
const result = rows.map(row => ({
name: row.name,
rollerInfo: JSON.parse(row.roller_info || '{}'),
shopInfo: JSON.parse(row.shop_info || '{}'),
tabInfo: JSON.parse(row.tab_info || '{}'),
pw: row.pw,
pwHash: row.pw_hash,
_id: row.id,
createdAt: row.created_at,
updatedAt: row.updated_at
}));
logToFile('DB: getAllPlayers - resultCount', result.length);
return result;
},
getByName: (name) => {
logToFile('DB: getPlayerByName - start', name);
const row = statements.getPlayerByName.get(name);
if (!row) {
logToFile('DB: getPlayerByName - not found', name);
return null;
}
const result = {
name: row.name,
rollerInfo: JSON.parse(row.roller_info || '{}'),
shopInfo: JSON.parse(row.shop_info || '{}'),
tabInfo: JSON.parse(row.tab_info || '{}'),
pw: row.pw,
pwHash: row.pw_hash,
_id: row.id,
createdAt: row.created_at,
updatedAt: row.updated_at
};
logToFile('DB: getPlayerByName - found', name);
return result;
},
create: (playerData) => {
logToFile('DB: createPlayer - start', playerData.name);
const result = statements.insertPlayer.run(
playerData.name,
JSON.stringify(playerData.rollerInfo || {}),
JSON.stringify(playerData.shopInfo || {}),
JSON.stringify(playerData.tabInfo || {}),
playerData.pw || '',
playerData.pwHash || ''
);
const out = { ...playerData, _id: result.lastInsertRowid };
logToFile('DB: createPlayer - done', playerData.name, 'rowid', result.lastInsertRowid);
return out;
},
update: (name, playerData) => {
logToFile('DB: updatePlayer - start', name);
// Ensure we don't have nested tabInfo
const cleanTabInfo = playerData.tabInfo || {};
if (cleanTabInfo.tabInfo) {
logToFile('DB: updatePlayer - fixing nested tabInfo structure');
Object.assign(cleanTabInfo, cleanTabInfo.tabInfo);
delete cleanTabInfo.tabInfo;
}
const result = statements.updatePlayer.run(
JSON.stringify(playerData.rollerInfo || {}),
JSON.stringify(playerData.shopInfo || {}),
JSON.stringify(cleanTabInfo),
playerData.pw || '',
playerData.pwHash || '',
name
);
logToFile('DB: updatePlayer - changes', result.changes, name);
return result.changes > 0;
},
delete: (name) => {
logToFile('DB: deletePlayer - start', name);
const result = statements.deletePlayer.run(name);
logToFile('DB: deletePlayer - changes', result.changes, name);
return result.changes > 0;
}
};
const sessionHelpers = {
get: (sessionId) => {
logToFile('DB: getSession - start', sessionId);
const row = statements.getSession.get(sessionId);
if (!row) {
logToFile('DB: getSession - not found', sessionId);
return null;
}
const out = {
sessionId: row.session_id,
data: JSON.parse(row.data || '{}'),
expiresAt: row.expires_at
};
logToFile('DB: getSession - ok', sessionId);
return out;
},
create: (sessionId, data, expiresAt) => {
logToFile('DB: createSession - start', sessionId, 'expiresAt', expiresAt);
const res = statements.insertSession.run(sessionId, JSON.stringify(data), expiresAt);
logToFile('DB: createSession - done', sessionId);
},
update: (sessionId, data, expiresAt) => {
logToFile('DB: updateSession - start', sessionId);
const result = statements.updateSession.run(JSON.stringify(data), expiresAt, sessionId);
logToFile('DB: updateSession - changes', result.changes, sessionId);
return result.changes > 0;
},
delete: (sessionId) => {
logToFile('DB: deleteSession - start', sessionId);
const result = statements.deleteSession.run(sessionId);
logToFile('DB: deleteSession - changes', result.changes, sessionId);
return result.changes > 0;
},
cleanExpired: () => {
logToFile('DB: cleanExpiredSessions - start');
const result = statements.cleanExpiredSessions.run();
logToFile('DB: cleanExpiredSessions - removed', result.changes);
return result.changes;
}
};
// Staging helpers for sanitized rules
const stagingStatements = {
insertStaging: db.prepare('INSERT INTO rules_staging (title, content, category, page, original_json) VALUES (?,?,?,?,?)'),
listStaging: db.prepare('SELECT id, title, content, category, page, original_json, created_at FROM rules_staging ORDER BY id'),
deleteStagingAll: db.prepare('DELETE FROM rules_staging'),
getStaging: db.prepare('SELECT id, title, content, category, page, original_json, created_at FROM rules_staging WHERE id = ?')
};
const stagingHelpers = {
insert: (obj) => {
const res = stagingStatements.insertStaging.run(obj.title || '', obj.content || '', obj.category || '', obj.page || '', JSON.stringify(obj.original || {}));
return res.lastInsertRowid;
},
list: () => stagingStatements.listStaging.all().map(r => ({ ...r, original: JSON.parse(r.original_json || '{}') })),
clear: () => stagingStatements.deleteStagingAll.run(),
get: (id) => {
const row = stagingStatements.getStaging.get(id);
if (!row) return null;
return { ...row, original: JSON.parse(row.original_json || '{}') };
}
};
module.exports = {
db,
statements,
playerHelpers,
sessionHelpers,
stagingHelpers,
close: () => db.close(),
logToFile
};