Files
dwroller/database/validate.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

145 lines
6.0 KiB
JavaScript

// Simple validation and normalization utilities for player records
function isObject(v) { return v && typeof v === 'object' && !Array.isArray(v); }
function normalizeName(name) {
if (!name) return '';
return String(name).trim().replace(/\s+/g, ' ');
}
function normalizeTabInfo(tabInfo) {
const out = Object.assign({}, isObject(tabInfo) ? tabInfo : {});
// Flatten nested tabInfo.tabInfo
if (isObject(out.tabInfo)) {
Object.assign(out, out.tabInfo);
delete out.tabInfo;
}
// numeric fields
['rp','xp','xpSpent','wounds','movement'].forEach(k=>{
if (out[k] !== undefined) {
const n = Number(out[k]);
out[k] = Number.isFinite(n) ? n : 0;
} else {
out[k] = 0;
}
});
// Ensure standard arrays/objects exist
if (!Array.isArray(out.skills)) out.skills = [];
if (!Array.isArray(out.inventory)) out.inventory = [];
if (!isObject(out.characteristics)) out.characteristics = {};
// Ensure standard Deathwatch Space Marine characteristic keys exist (lowercase keys
// matching the frontend PlayerTab expectations). Do not overwrite existing values.
const standardChars = ['ws','bs','s','t','ag','int','per','wp','fel'];
// Accept existing upper/lower keys but store lowercase
const charsIn = Object.assign({}, out.characteristics);
out.characteristics = {};
for (const k of standardChars) {
// prefer existing lowercase, then uppercase variants
if (charsIn[k] !== undefined) out.characteristics[k] = Number(charsIn[k]) || 0;
else if (charsIn[k.toUpperCase()] !== undefined) out.characteristics[k] = Number(charsIn[k.toUpperCase()]) || 0;
else out.characteristics[k] = 0;
}
// Standard Deathwatch core skills to add if missing (preserve existing extras)
const standardSkills = [
'Acrobatics (Ag)', 'Awareness (Per)', 'Barter (Fel)', 'Blather (Fel)',
'Carouse (Fel)', 'Charm (Fel)', 'Chem-Use (Int)', 'Ciphers (Int)',
'Chapter Runes', 'Climb (S)', 'Command (Fel)', 'Common Lore (Int)',
'Adeptus Astartes', 'Deathwatch', 'Imperium', 'War', 'Concealment (Ag)',
'Contortionist (Ag)', 'Deceive (Fel)', 'Demolition (Int)', 'Disguise (Fel)',
'Dodge (Ag)', 'Drive (Ag)', 'Evaluate', 'Forbidden Lore (Int)', 'Xenos',
'Gamble (Int)', 'Inquiry (Fel)', 'Interrogation (WP)', 'Intimidate (S)',
'Invocation (WP)', 'Lip Reading (Per)', 'Literacy (Int)', 'Logic (Int)',
'Medicae (Int)', 'Navigation (Int)', 'Surface', 'Performer (Fel)',
'Pilot (Ag)', 'Psyniscience (Per)', 'Scholastic Lore (Int)', 'Codex Astartes',
'Scrutiny (Per)', 'Search (Per)', 'Secret Tongue (Int)', 'Security (Ag)',
'Shadowing (Ag)', 'Silent Move (Ag)', 'Sleight of Hand (Ag)', 'Speak Language (Int)',
'Survival (Int)', 'Swim (S)', 'Tactics (Int)', 'Tech-Use (Int)', 'Tracking (Int)',
'Trade (Int)', 'Wrangling (Int)'
];
// Add any missing standard skills (case-sensitive check)
for (const s of standardSkills) {
if (!out.skills.includes(s)) out.skills.push(s);
}
// renown normalization - allowed values: None, Respected, Distinguished, Renowned
const allowedRenown = ['None','Respected','Distinguished','Renowned'];
if (out.renown === '' || out.renown === null || out.renown === undefined) out.renown = 'None';
if (typeof out.renown === 'string') {
// normalize casing
const cand = out.renown.trim();
const found = allowedRenown.find(a => a.toLowerCase() === cand.toLowerCase());
out.renown = found || 'None';
} else {
out.renown = 'None';
}
// Clamp rp/xp ranges to reasonable bounds
out.rp = Math.max(0, Math.min(1000000, Number(out.rp || 0)));
out.xp = Math.max(0, Math.min(1000000, Number(out.xp || 0)));
out.xpSpent = Math.max(0, Math.min(out.xp, Number(out.xpSpent || 0)));
return out;
}
function validatePlayer(player) {
const errors = [];
const normalized = Object.assign({}, player);
// name
normalized.name = normalizeName(player.name);
if (!normalized.name) errors.push('name: required');
if (normalized.name.length > 100) errors.push('name: too long');
// roller/shop/tab must be objects
normalized.rollerInfo = isObject(player.rollerInfo) ? player.rollerInfo : {};
normalized.shopInfo = isObject(player.shopInfo) ? player.shopInfo : {};
// If caller provided flat tab-related fields at top-level (common from frontend),
// move them into tabInfo before normalization so defaults and validation apply.
const flatFields = [
'playerName','charName','gear','chapter','demeanour','speciality','rank','powerArmour',
'description','pastEvent','personalDemeanour','characteristics','skills','weapons','armour',
'talents','psychic','wounds','insanity','movement','fate','corruption','renown','xp','xpSpent',
'notes','rp','inventory'
];
const incomingTab = isObject(player.tabInfo) ? Object.assign({}, player.tabInfo) : {};
for (const k of flatFields) {
if (Object.prototype.hasOwnProperty.call(player, k) && incomingTab[k] === undefined) {
incomingTab[k] = player[k];
}
}
normalized.tabInfo = normalizeTabInfo(incomingTab);
// Validate characteristics structure
if (isObject(normalized.tabInfo.characteristics)) {
const chars = normalized.tabInfo.characteristics;
Object.keys(chars).forEach(k => {
const n = Number(chars[k]);
if (!Number.isFinite(n) || n < 0 || n > 100) {
errors.push(`characteristics.${k}: must be number 0-100`);
chars[k] = Math.max(0, Math.min(100, Number.isFinite(n) ? n : 0));
}
});
} else {
normalized.tabInfo.characteristics = {};
}
// Truncate very long free-text fields
if (typeof normalized.tabInfo.description === 'string' && normalized.tabInfo.description.length > 2000) {
normalized.tabInfo.description = normalized.tabInfo.description.substring(0,2000);
errors.push('description: truncated to 2000 chars');
}
// passwords: prefer pwHash; if plain pw present flag it (do not accept plaintext pw)
if (player.pw && player.pw.length > 0) errors.push('pw: plain password present; provide pwHash instead');
return { valid: errors.length === 0, errors, normalized };
}
module.exports = { validatePlayer, normalizeTabInfo };