- Implemented `validatePlayer` and `normalizeTabInfo` functions in `database/validate.js` for player data validation and normalization. - Added error handling for required fields and enforced data types and constraints. - Introduced standardization for characteristics and skills. feat: Create script for extracting rules from PDF files - Developed `extract-rules.js` to extract text from PDF rulebooks and categorize rules. - Integrated `pdftotext` for PDF processing and created a searchable rules database. - Implemented rule categorization and indexing for efficient searching. feat: Implement RulesTab component for rule searching - Created `RulesTab.jsx` for searching and displaying rules with filtering options. - Added recent searches and quick reference buttons for user convenience. - Integrated API calls for fetching rules and displaying results dynamically. feat: Add test script for Rules API - Created `test-rules.js` to test the functionality of the Rules API endpoints. - Included tests for search and stats endpoints to ensure proper response handling.
228 lines
7.5 KiB
JavaScript
228 lines
7.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// Simple DB cleaner/validator for SQLite players table
|
|
// Usage: node clean-database.js [--apply] [--delete-tests]
|
|
|
|
const { playerHelpers } = require('./sqlite-db');
|
|
|
|
function isObject(v) {
|
|
return v && typeof v === 'object' && !Array.isArray(v);
|
|
}
|
|
|
|
function normalizeName(name) {
|
|
if (!name) return '';
|
|
// trim and collapse spaces
|
|
return String(name).trim().replace(/\s+/g, ' ');
|
|
}
|
|
|
|
function looksLikeTestName(name) {
|
|
if (!name) return true;
|
|
return /test|dummy|sample|example/i.test(name);
|
|
}
|
|
|
|
function cleanPlayer(player) {
|
|
const issues = [];
|
|
const updates = {};
|
|
|
|
// name
|
|
const cleanName = normalizeName(player.name);
|
|
if (!cleanName) issues.push('empty name');
|
|
if (cleanName !== player.name) updates.name = cleanName;
|
|
if (cleanName.length > 100) issues.push('name too long');
|
|
|
|
// pw/pwHash
|
|
if (player.pw && player.pw.length > 0) issues.push('plain password present');
|
|
if (player.pwHash && typeof player.pwHash !== 'string') updates.pwHash = String(player.pwHash);
|
|
|
|
// rollerInfo / shopInfo / tabInfo should be objects
|
|
if (!isObject(player.rollerInfo)) {
|
|
issues.push('rollerInfo not object');
|
|
updates.rollerInfo = {};
|
|
}
|
|
if (!isObject(player.shopInfo)) {
|
|
issues.push('shopInfo not object');
|
|
updates.shopInfo = {};
|
|
}
|
|
if (!isObject(player.tabInfo)) {
|
|
issues.push('tabInfo not object');
|
|
updates.tabInfo = {};
|
|
}
|
|
|
|
// Flatten nested tabInfo.tabInfo
|
|
if (isObject(player.tabInfo) && isObject(player.tabInfo.tabInfo)) {
|
|
issues.push('nested tabInfo.tabInfo found; will be flattened');
|
|
updates.tabInfo = Object.assign({}, player.tabInfo.tabInfo, player.tabInfo);
|
|
delete updates.tabInfo.tabInfo;
|
|
}
|
|
|
|
// Remove mongo-specific fields inside tabInfo or rollerInfo
|
|
['mongoId', 'mongo_id', '_id', 'mongoIdString'].forEach(k => {
|
|
if (isObject(player.tabInfo) && player.tabInfo[k]) {
|
|
issues.push(`tabInfo contains ${k}; will be removed`);
|
|
const t = Object.assign({}, player.tabInfo);
|
|
delete t[k];
|
|
updates.tabInfo = Object.assign({}, updates.tabInfo || player.tabInfo || {}, t);
|
|
}
|
|
if (isObject(player.rollerInfo) && player.rollerInfo[k]) {
|
|
issues.push(`rollerInfo contains ${k}; will be removed`);
|
|
const r = Object.assign({}, player.rollerInfo);
|
|
delete r[k];
|
|
updates.rollerInfo = Object.assign({}, updates.rollerInfo || player.rollerInfo || {}, r);
|
|
}
|
|
});
|
|
|
|
// Normalize common known fields types in tabInfo
|
|
const expectedTabFields = ['renown','rp','xp','xpSpent','wounds','movement','notes','playerName','charName','rank'];
|
|
if (isObject(player.tabInfo)) {
|
|
expectedTabFields.forEach(f => {
|
|
if (Object.prototype.hasOwnProperty.call(player.tabInfo, f)) {
|
|
const val = player.tabInfo[f];
|
|
if (f === 'rp' || f === 'xp' || f === 'xpSpent' || f === 'wounds' || f === 'movement') {
|
|
// ensure numeric
|
|
const n = Number(val);
|
|
if (!Number.isFinite(n)) {
|
|
issues.push(`${f} not numeric; setting to 0`);
|
|
updates.tabInfo = Object.assign({}, updates.tabInfo || player.tabInfo || {}, { [f]: 0 });
|
|
} else if (n !== val) {
|
|
updates.tabInfo = Object.assign({}, updates.tabInfo || player.tabInfo || {}, { [f]: n });
|
|
}
|
|
}
|
|
if (f === 'renown' && val && typeof val === 'string' && val.trim() === '') {
|
|
issues.push('renown blank string; normalizing to "None"');
|
|
updates.tabInfo = Object.assign({}, updates.tabInfo || player.tabInfo || {}, { renown: 'None' });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Detect junk keys: very long strings or binary-like values
|
|
['rollerInfo','shopInfo','tabInfo'].forEach(k => {
|
|
const v = player[k];
|
|
if (isObject(v)) {
|
|
Object.keys(v).forEach(key => {
|
|
const val = v[key];
|
|
if (typeof val === 'string' && val.length > 2000) {
|
|
issues.push(`${k}.${key} unusually long (>${2000})`);
|
|
// truncate
|
|
updates[k] = Object.assign({}, updates[k] || v, { [key]: val.substring(0, 2000) });
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Identify obvious test users
|
|
if (looksLikeTestName(player.name)) {
|
|
issues.push('test/dummy/example user');
|
|
}
|
|
|
|
return { issues, updates };
|
|
}
|
|
|
|
async function run() {
|
|
const args = process.argv.slice(2);
|
|
const apply = args.includes('--apply');
|
|
const deleteTests = args.includes('--delete-tests');
|
|
|
|
const players = playerHelpers.getAll();
|
|
const report = {
|
|
total: players.length,
|
|
toDelete: [],
|
|
toUpdate: []
|
|
};
|
|
|
|
const seenNames = new Map();
|
|
|
|
for (const pl of players) {
|
|
const { issues, updates } = cleanPlayer(pl);
|
|
|
|
// Duplicate name detection
|
|
const nameKey = normalizeName(pl.name).toLowerCase();
|
|
if (seenNames.has(nameKey)) {
|
|
issues.push('duplicate name');
|
|
report.toDelete.push({ name: pl.name, reason: 'duplicate' });
|
|
continue;
|
|
}
|
|
seenNames.set(nameKey, true);
|
|
|
|
if (issues.length > 0) {
|
|
report.toUpdate.push({ name: pl.name, issues, updates });
|
|
}
|
|
|
|
if (looksLikeTestName(pl.name) && deleteTests) {
|
|
report.toDelete.push({ name: pl.name, reason: 'test user' });
|
|
}
|
|
}
|
|
|
|
// Print report
|
|
console.log('=== Clean DB Report (dry-run unless --apply) ===');
|
|
console.log('Total players:', report.total);
|
|
console.log('Candidates for update:', report.toUpdate.length);
|
|
console.log('Candidates for delete:', report.toDelete.length);
|
|
|
|
if (report.toUpdate.length > 0) {
|
|
console.log('\n--- Updates ---');
|
|
report.toUpdate.forEach(u => {
|
|
console.log('Player:', u.name);
|
|
console.log(' Issues:', u.issues.join('; '));
|
|
console.log(' Planned updates:', JSON.stringify(u.updates));
|
|
});
|
|
}
|
|
|
|
if (report.toDelete.length > 0) {
|
|
console.log('\n--- Deletes ---');
|
|
report.toDelete.forEach(d => console.log('Player:', d.name, 'Reason:', d.reason));
|
|
}
|
|
|
|
if (apply) {
|
|
console.log('\nApplying fixes...');
|
|
let updated=0, deleted=0;
|
|
for (const u of report.toUpdate) {
|
|
try {
|
|
if (Object.keys(u.updates).length > 0) {
|
|
// If name is changing, perform delete/create to maintain unique constraint
|
|
if (u.updates.name) {
|
|
const orig = u.name;
|
|
const newName = u.updates.name;
|
|
const pl = playerHelpers.getByName(orig);
|
|
if (pl) {
|
|
// delete then recreate
|
|
playerHelpers.delete(orig);
|
|
playerHelpers.create(Object.assign({}, pl, { name: newName, rollerInfo: u.updates.rollerInfo || pl.rollerInfo, shopInfo: u.updates.shopInfo || pl.shopInfo, tabInfo: u.updates.tabInfo || pl.tabInfo, pw: pl.pw, pwHash: pl.pwHash }));
|
|
updated++;
|
|
}
|
|
} else {
|
|
// normal update
|
|
const pl = playerHelpers.getByName(u.name);
|
|
if (pl) {
|
|
const merged = {
|
|
rollerInfo: u.updates.rollerInfo || pl.rollerInfo,
|
|
shopInfo: u.updates.shopInfo || pl.shopInfo,
|
|
tabInfo: u.updates.tabInfo || pl.tabInfo,
|
|
pw: pl.pw,
|
|
pwHash: pl.pwHash
|
|
};
|
|
playerHelpers.update(u.name, merged);
|
|
updated++;
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to apply update for', u.name, err);
|
|
}
|
|
}
|
|
|
|
for (const d of report.toDelete) {
|
|
try {
|
|
if (playerHelpers.delete(d.name)) deleted++;
|
|
} catch (err) {
|
|
console.error('Failed to delete', d.name, err);
|
|
}
|
|
}
|
|
|
|
console.log(`Applied: ${updated} updates, ${deleted} deletes`);
|
|
}
|
|
|
|
console.log('\nDone.');
|
|
}
|
|
|
|
run().catch(err => { console.error('Clean script error', err); process.exit(1); });
|