const oils = require('../data/taxonomy/oils.json'); const concerns = require('../data/taxonomy/concerns.json'); const LOCALES = ['sr', 'en', 'de', 'fr']; const DEFAULT_LOCALE = 'sr'; class TaxonomyValidator { constructor() { this.errors = []; this.warnings = []; this.stats = { oils: 0, concerns: 0, relationships: 0, checked: 0 }; } validate() { console.log('\n=== TAXONOMY VALIDATION ===\n'); this.validateOils(); this.validateConcerns(); this.validateRelationships(); this.validateSlugs(); this.validateTranslations(); this.validateCategories(); this.printResults(); return { valid: this.errors.length === 0, errors: this.errors, warnings: this.warnings, stats: this.stats }; } validateOils() { console.log('Validating oils...'); const oilIds = Object.keys(oils.oils); this.stats.oils = oilIds.length; for (const oilId of oilIds) { const oil = oils.oils[oilId]; this.stats.checked++; if (oil.id !== oilId) { this.errors.push(`Oil ID mismatch: ${oilId} vs ${oil.id}`); } if (!oil.name || !oil.name[DEFAULT_LOCALE]) { this.errors.push(`Oil ${oilId} missing name for default locale`); } if (!oil.slug || !oil.slug[DEFAULT_LOCALE]) { this.errors.push(`Oil ${oilId} missing slug for default locale`); } if (!Array.isArray(oil.concerns) || oil.concerns.length === 0) { this.warnings.push(`Oil ${oilId} has no concerns`); } if (typeof oil.comedogenicRating !== 'number') { this.warnings.push(`Oil ${oilId} missing comedogenicRating`); } LOCALES.forEach(locale => { if (!oil.name[locale]) { this.errors.push(`Oil ${oilId} missing name translation for ${locale}`); } if (!oil.slug[locale]) { this.errors.push(`Oil ${oilId} missing slug for ${locale}`); } }); } } validateConcerns() { console.log('Validating concerns...'); const concernIds = Object.keys(concerns.concerns); this.stats.concerns = concernIds.length; for (const concernId of concernIds) { const concern = concerns.concerns[concernId]; this.stats.checked++; if (concern.id !== concernId) { this.errors.push(`Concern ID mismatch: ${concernId} vs ${concern.id}`); } if (!concern.name || !concern.name[DEFAULT_LOCALE]) { this.errors.push(`Concern ${concernId} missing name for default locale`); } if (!concern.slug || !concern.slug[DEFAULT_LOCALE]) { this.errors.push(`Concern ${concernId} missing slug for default locale`); } if (!concern.category) { this.warnings.push(`Concern ${concernId} missing category`); } if (!concerns.categories[concern.category]) { this.errors.push(`Concern ${concernId} has invalid category: ${concern.category}`); } LOCALES.forEach(locale => { if (!concern.name[locale]) { this.errors.push(`Concern ${concernId} missing name translation for ${locale}`); } if (!concern.slug[locale]) { this.errors.push(`Concern ${concernId} missing slug for ${locale}`); } }); } } validateRelationships() { console.log('Validating relationships...'); const oilIds = Object.keys(oils.oils); const concernIds = Object.keys(concerns.concerns); for (const oilId of oilIds) { const oil = oils.oils[oilId]; for (const concernId of oil.concerns) { this.stats.relationships++; if (!concerns.concerns[concernId]) { this.errors.push(`Oil ${oilId} references non-existent concern: ${concernId}`); continue; } const concern = concerns.concerns[concernId]; if (!concern.oils || !concern.oils.includes(oilId)) { this.warnings.push(`Bidirectional relationship missing: ${oilId} → ${concernId} exists, but not ${concernId} → ${oilId}`); } } } for (const concernId of concernIds) { const concern = concerns.concerns[concernId]; for (const oilId of concern.oils || []) { if (!oils.oils[oilId]) { this.errors.push(`Concern ${concernId} references non-existent oil: ${oilId}`); continue; } const oil = oils.oils[oilId]; if (!oil.concerns.includes(concernId)) { this.warnings.push(`Bidirectional relationship missing: ${concernId} → ${oilId} exists, but not ${oilId} → ${concernId}`); } } } } validateSlugs() { console.log('Validating slugs...'); const allSlugs = new Map(); LOCALES.forEach(locale => { const localeSlugs = new Set(); Object.values(oils.oils).forEach(oil => { const slug = oil.slug[locale]; if (localeSlugs.has(slug)) { this.errors.push(`Duplicate slug in ${locale}: ${slug}`); } localeSlugs.add(slug); const key = `${locale}:${slug}`; if (allSlugs.has(key)) { this.errors.push(`Slug collision: ${key}`); } allSlugs.set(key, `oil:${oil.id}`); }); Object.values(concerns.concerns).forEach(concern => { const slug = concern.slug[locale]; if (localeSlugs.has(slug)) { this.errors.push(`Duplicate slug in ${locale}: ${slug}`); } localeSlugs.add(slug); const key = `${locale}:${slug}`; if (allSlugs.has(key)) { this.errors.push(`Slug collision: ${key}`); } allSlugs.set(key, `concern:${concern.id}`); }); }); } validateTranslations() { console.log('Validating translations...'); Object.entries(oils.oils).forEach(([id, oil]) => { LOCALES.forEach(locale => { if (!oil.name[locale] || oil.name[locale].trim() === '') { this.errors.push(`Empty name translation: oil ${id} for ${locale}`); } }); }); Object.entries(concerns.concerns).forEach(([id, concern]) => { LOCALES.forEach(locale => { if (!concern.name[locale] || concern.name[locale].trim() === '') { this.errors.push(`Empty name translation: concern ${id} for ${locale}`); } }); }); } validateCategories() { console.log('Validating categories...'); Object.entries(concerns.categories).forEach(([catId, cat]) => { LOCALES.forEach(locale => { if (!cat.name[locale]) { this.errors.push(`Category ${catId} missing translation for ${locale}`); } }); }); } printResults() { console.log('\n=== RESULTS ===\n'); console.log(`✓ Checked: ${this.stats.checked} entities`); console.log(`✓ Oils: ${this.stats.oils}`); console.log(`✓ Concerns: ${this.stats.concerns}`); console.log(`✓ Relationships: ${this.stats.relationships}`); if (this.errors.length === 0 && this.warnings.length === 0) { console.log('\n✅ All validations passed!'); } else { if (this.errors.length > 0) { console.log(`\n❌ ERRORS (${this.errors.length}):`); this.errors.forEach(err => console.log(` - ${err}`)); } if (this.warnings.length > 0) { console.log(`\n⚠️ WARNINGS (${this.warnings.length}):`); this.warnings.forEach(warn => console.log(` - ${warn}`)); } } console.log('\n'); } } function validateContentFiles() { console.log('=== CONTENT FILE VALIDATION ===\n'); const fs = require('fs'); const path = require('path'); const contentDir = path.join(__dirname, '../data/content/oil-for-concern'); const missingFiles = []; const extraFiles = []; const expectedCombinations = []; Object.entries(oils.oils).forEach(([oilId, oil]) => { oil.concerns.forEach(concernId => { expectedCombinations.push(`${oilId}-${concernId}`); }); }); if (!fs.existsSync(contentDir)) { console.log('⚠️ Content directory does not exist yet'); console.log(` Expected: ${contentDir}`); console.log(` Missing ${expectedCombinations.length} content files\n`); return { valid: false, missing: expectedCombinations }; } const existingFiles = fs.readdirSync(contentDir) .filter(f => f.endsWith('.json')) .map(f => f.replace('.json', '')); expectedCombinations.forEach(combo => { if (!existingFiles.includes(combo)) { missingFiles.push(combo); } }); existingFiles.forEach(file => { if (!expectedCombinations.includes(file)) { extraFiles.push(file); } }); console.log(`Content files checked: ${existingFiles.length}`); console.log(`Expected combinations: ${expectedCombinations.length}`); if (missingFiles.length > 0) { console.log(`\n❌ Missing content files (${missingFiles.length}):`); missingFiles.forEach(f => console.log(` - ${f}.json`)); } if (extraFiles.length > 0) { console.log(`\n⚠️ Extra content files (${extraFiles.length}):`); extraFiles.forEach(f => console.log(` - ${f}.json`)); } if (missingFiles.length === 0 && extraFiles.length === 0) { console.log('\n✅ All content files present!\n'); } return { valid: missingFiles.length === 0, missing: missingFiles, extra: extraFiles }; } if (require.main === module) { const validator = new TaxonomyValidator(); const taxonomyResult = validator.validate(); const contentResult = validateContentFiles(); process.exit(taxonomyResult.valid && contentResult.valid ? 0 : 1); } module.exports = { TaxonomyValidator, validateContentFiles };