← back to Watches
utils/validator.js
156 lines
/**
* Data Validation Utilities
* Validates watch data against schema
*/
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export class WatchDataValidator {
constructor() {
this.schema = JSON.parse(
fs.readFileSync(path.join(__dirname, '../schema/watch-schema.json'), 'utf8')
);
this.errors = [];
}
validate(data) {
this.errors = [];
// Check required top-level properties
if (!data.watches || !Array.isArray(data.watches)) {
this.errors.push('Missing or invalid "watches" array');
return false;
}
if (!data.metadata || typeof data.metadata !== 'object') {
this.errors.push('Missing or invalid "metadata" object');
return false;
}
// Validate each watch
data.watches.forEach((watch, index) => {
this.validateWatch(watch, index);
});
// Validate metadata
this.validateMetadata(data.metadata, data.watches.length);
return this.errors.length === 0;
}
validateWatch(watch, index) {
const requiredFields = ['id', 'model', 'series', 'reference', 'yearIntroduced', 'description', 'priceHistory', 'specifications'];
requiredFields.forEach(field => {
if (!watch[field]) {
this.errors.push(`Watch #${index}: Missing required field "${field}"`);
}
});
// Validate ID format
if (watch.id && !/^[a-z0-9-]+$/.test(watch.id)) {
this.errors.push(`Watch #${index} (${watch.id}): Invalid ID format - must be lowercase alphanumeric with hyphens`);
}
// Validate year
if (watch.yearIntroduced && (watch.yearIntroduced < 1848 || watch.yearIntroduced > 2100)) {
this.errors.push(`Watch #${index} (${watch.id}): Invalid year ${watch.yearIntroduced}`);
}
// Validate price history
if (watch.priceHistory && Array.isArray(watch.priceHistory)) {
if (watch.priceHistory.length === 0) {
this.errors.push(`Watch #${index} (${watch.id}): Price history is empty`);
}
watch.priceHistory.forEach((price, priceIndex) => {
if (!price.year || !price.price || !price.condition || !price.source) {
this.errors.push(`Watch #${index} (${watch.id}): Price history #${priceIndex} missing required fields`);
}
if (price.price < 0) {
this.errors.push(`Watch #${index} (${watch.id}): Negative price in history #${priceIndex}`);
}
if (!['new', 'vintage', 'prototype'].includes(price.condition)) {
this.errors.push(`Watch #${index} (${watch.id}): Invalid condition "${price.condition}" in history #${priceIndex}`);
}
});
// Check chronological order
for (let i = 1; i < watch.priceHistory.length; i++) {
if (watch.priceHistory[i].year < watch.priceHistory[i - 1].year) {
this.errors.push(`Watch #${index} (${watch.id}): Price history not in chronological order`);
break;
}
}
}
// Validate specifications
if (watch.specifications) {
const requiredSpecs = ['caseSize', 'caseMaterial', 'movement'];
requiredSpecs.forEach(spec => {
if (!watch.specifications[spec]) {
this.errors.push(`Watch #${index} (${watch.id}): Missing specification "${spec}"`);
}
});
}
}
validateMetadata(metadata, watchCount) {
if (metadata.totalWatches !== watchCount) {
this.errors.push(`Metadata totalWatches (${metadata.totalWatches}) doesn't match actual count (${watchCount})`);
}
if (!metadata.collections || !Array.isArray(metadata.collections)) {
this.errors.push('Metadata missing collections array');
}
if (!metadata.lastUpdated) {
this.errors.push('Metadata missing lastUpdated field');
}
}
getErrors() {
return this.errors;
}
getReport() {
return {
valid: this.errors.length === 0,
errorCount: this.errors.length,
errors: this.errors
};
}
}
// CLI usage
if (import.meta.url === `file://${process.argv[1]}`) {
const dataPath = process.argv[2] || './data/watches.json';
const data = JSON.parse(fs.readFileSync(dataPath, 'utf8'));
const validator = new WatchDataValidator();
const isValid = validator.validate(data);
console.log('\n=== Watch Data Validation Report ===\n');
if (isValid) {
console.log('✓ Validation passed!');
console.log(`✓ ${data.watches.length} watches validated successfully`);
} else {
console.log('✗ Validation failed!\n');
console.log('Errors found:');
validator.getErrors().forEach((error, index) => {
console.log(` ${index + 1}. ${error}`);
});
}
console.log();
process.exit(isValid ? 0 : 1);
}