Files
dwroller/database/clean-database.js
Alex dde3abda55 feat: Add validation and normalization utilities for player records
- 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.
2025-08-16 12:15:32 +02:00

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); });