273 lines
8.9 KiB
JavaScript
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
|
|
};
|