240 lines
7.8 KiB
JavaScript
240 lines
7.8 KiB
JavaScript
|
|
const { query } = require('./config/database');
|
|||
|
|
const fs = require('fs');
|
|||
|
|
const path = require('path');
|
|||
|
|
|
|||
|
|
async function validateAlignment() {
|
|||
|
|
console.log('🔍 Validating Database-Backend Alignment...\n');
|
|||
|
|
|
|||
|
|
const issues = [];
|
|||
|
|
const warnings = [];
|
|||
|
|
const successes = [];
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 1. Check required tables exist
|
|||
|
|
console.log('1️⃣ Checking required tables...');
|
|||
|
|
const requiredTables = [
|
|||
|
|
'products', 'product_images', 'blogposts', 'pages',
|
|||
|
|
'portfolioprojects', 'adminusers', 'customers', 'orders',
|
|||
|
|
'order_items', 'product_reviews'
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const table of requiredTables) {
|
|||
|
|
const exists = await query(`
|
|||
|
|
SELECT EXISTS (
|
|||
|
|
SELECT 1 FROM information_schema.tables
|
|||
|
|
WHERE table_name = $1
|
|||
|
|
) as exists
|
|||
|
|
`, [table]);
|
|||
|
|
|
|||
|
|
if (exists.rows[0].exists) {
|
|||
|
|
successes.push(`✓ Table ${table} exists`);
|
|||
|
|
} else {
|
|||
|
|
issues.push(`✗ Missing table: ${table}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 2. Check foreign key relationships
|
|||
|
|
console.log('\n2️⃣ Checking foreign key relationships...');
|
|||
|
|
const relationships = [
|
|||
|
|
{ table: 'product_images', column: 'product_id', ref: 'products' },
|
|||
|
|
{ table: 'order_items', column: 'order_id', ref: 'orders' },
|
|||
|
|
{ table: 'order_items', column: 'product_id', ref: 'products' },
|
|||
|
|
{ table: 'product_reviews', column: 'product_id', ref: 'products' },
|
|||
|
|
{ table: 'product_reviews', column: 'customer_id', ref: 'customers' }
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const rel of relationships) {
|
|||
|
|
const exists = await query(`
|
|||
|
|
SELECT EXISTS (
|
|||
|
|
SELECT 1 FROM information_schema.table_constraints tc
|
|||
|
|
JOIN information_schema.key_column_usage kcu
|
|||
|
|
ON tc.constraint_name = kcu.constraint_name
|
|||
|
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|||
|
|
AND tc.table_name = $1
|
|||
|
|
AND kcu.column_name = $2
|
|||
|
|
) as exists
|
|||
|
|
`, [rel.table, rel.column]);
|
|||
|
|
|
|||
|
|
if (exists.rows[0].exists) {
|
|||
|
|
successes.push(`✓ FK: ${rel.table}.${rel.column} -> ${rel.ref}`);
|
|||
|
|
} else {
|
|||
|
|
warnings.push(`⚠ Missing FK: ${rel.table}.${rel.column} -> ${rel.ref}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. Check critical indexes
|
|||
|
|
console.log('\n3️⃣ Checking critical indexes...');
|
|||
|
|
const criticalIndexes = [
|
|||
|
|
{ table: 'products', column: 'slug' },
|
|||
|
|
{ table: 'products', column: 'isactive' },
|
|||
|
|
{ table: 'product_images', column: 'product_id' },
|
|||
|
|
{ table: 'blogposts', column: 'slug' },
|
|||
|
|
{ table: 'pages', column: 'slug' },
|
|||
|
|
{ table: 'orders', column: 'ordernumber' }
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const idx of criticalIndexes) {
|
|||
|
|
const exists = await query(`
|
|||
|
|
SELECT EXISTS (
|
|||
|
|
SELECT 1 FROM pg_indexes
|
|||
|
|
WHERE tablename = $1
|
|||
|
|
AND indexdef LIKE '%' || $2 || '%'
|
|||
|
|
) as exists
|
|||
|
|
`, [idx.table, idx.column]);
|
|||
|
|
|
|||
|
|
if (exists.rows[0].exists) {
|
|||
|
|
successes.push(`✓ Index on ${idx.table}.${idx.column}`);
|
|||
|
|
} else {
|
|||
|
|
warnings.push(`⚠ Missing index on ${idx.table}.${idx.column}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 4. Check constraints
|
|||
|
|
console.log('\n4️⃣ Checking data constraints...');
|
|||
|
|
const constraints = [
|
|||
|
|
{ table: 'products', name: 'chk_products_price_positive' },
|
|||
|
|
{ table: 'products', name: 'chk_products_stock_nonnegative' },
|
|||
|
|
{ table: 'product_images', name: 'chk_product_images_order_nonnegative' }
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
for (const con of constraints) {
|
|||
|
|
const exists = await query(`
|
|||
|
|
SELECT EXISTS (
|
|||
|
|
SELECT 1 FROM information_schema.table_constraints
|
|||
|
|
WHERE constraint_name = $1 AND table_name = $2
|
|||
|
|
) as exists
|
|||
|
|
`, [con.name, con.table]);
|
|||
|
|
|
|||
|
|
if (exists.rows[0].exists) {
|
|||
|
|
successes.push(`✓ Constraint ${con.name}`);
|
|||
|
|
} else {
|
|||
|
|
issues.push(`✗ Missing constraint: ${con.name}`);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 5. Check CASCADE delete setup
|
|||
|
|
console.log('\n5️⃣ Checking CASCADE delete rules...');
|
|||
|
|
const cascades = await query(`
|
|||
|
|
SELECT
|
|||
|
|
tc.table_name,
|
|||
|
|
kcu.column_name,
|
|||
|
|
ccu.table_name AS foreign_table_name,
|
|||
|
|
rc.delete_rule
|
|||
|
|
FROM information_schema.table_constraints AS tc
|
|||
|
|
JOIN information_schema.key_column_usage AS kcu
|
|||
|
|
ON tc.constraint_name = kcu.constraint_name
|
|||
|
|
JOIN information_schema.constraint_column_usage AS ccu
|
|||
|
|
ON ccu.constraint_name = tc.constraint_name
|
|||
|
|
JOIN information_schema.referential_constraints AS rc
|
|||
|
|
ON rc.constraint_name = tc.constraint_name
|
|||
|
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|||
|
|
AND tc.table_name IN ('product_images', 'order_items', 'product_reviews')
|
|||
|
|
`);
|
|||
|
|
|
|||
|
|
cascades.rows.forEach(row => {
|
|||
|
|
if (row.delete_rule === 'CASCADE') {
|
|||
|
|
successes.push(`✓ CASCADE delete: ${row.table_name}.${row.column_name}`);
|
|||
|
|
} else {
|
|||
|
|
warnings.push(`⚠ Non-CASCADE delete: ${row.table_name}.${row.column_name} (${row.delete_rule})`);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 6. Test query performance
|
|||
|
|
console.log('\n6️⃣ Testing query performance...');
|
|||
|
|
const start = Date.now();
|
|||
|
|
await query(`
|
|||
|
|
SELECT p.*,
|
|||
|
|
COALESCE(
|
|||
|
|
json_agg(
|
|||
|
|
json_build_object('id', pi.id, 'image_url', pi.image_url)
|
|||
|
|
ORDER BY pi.display_order
|
|||
|
|
) FILTER (WHERE pi.id IS NOT NULL),
|
|||
|
|
'[]'::json
|
|||
|
|
) as images
|
|||
|
|
FROM products p
|
|||
|
|
LEFT JOIN product_images pi ON pi.product_id = p.id
|
|||
|
|
WHERE p.isactive = true
|
|||
|
|
GROUP BY p.id
|
|||
|
|
LIMIT 10
|
|||
|
|
`);
|
|||
|
|
const duration = Date.now() - start;
|
|||
|
|
|
|||
|
|
if (duration < 100) {
|
|||
|
|
successes.push(`✓ Query performance: ${duration}ms (excellent)`);
|
|||
|
|
} else if (duration < 300) {
|
|||
|
|
successes.push(`✓ Query performance: ${duration}ms (good)`);
|
|||
|
|
} else {
|
|||
|
|
warnings.push(`⚠ Query performance: ${duration}ms (needs optimization)`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 7. Check data integrity
|
|||
|
|
console.log('\n7️⃣ Checking data integrity...');
|
|||
|
|
|
|||
|
|
// Orphaned product images
|
|||
|
|
const orphanedImages = await query(`
|
|||
|
|
SELECT COUNT(*) as count
|
|||
|
|
FROM product_images pi
|
|||
|
|
LEFT JOIN products p ON p.id = pi.product_id
|
|||
|
|
WHERE p.id IS NULL
|
|||
|
|
`);
|
|||
|
|
|
|||
|
|
if (orphanedImages.rows[0].count === '0') {
|
|||
|
|
successes.push('✓ No orphaned product images');
|
|||
|
|
} else {
|
|||
|
|
warnings.push(`⚠ ${orphanedImages.rows[0].count} orphaned product images`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Products without images
|
|||
|
|
const noImages = await query(`
|
|||
|
|
SELECT COUNT(*) as count
|
|||
|
|
FROM products p
|
|||
|
|
LEFT JOIN product_images pi ON pi.product_id = p.id
|
|||
|
|
WHERE p.isactive = true AND pi.id IS NULL
|
|||
|
|
`);
|
|||
|
|
|
|||
|
|
if (noImages.rows[0].count === '0') {
|
|||
|
|
successes.push('✓ All active products have images');
|
|||
|
|
} else {
|
|||
|
|
warnings.push(`⚠ ${noImages.rows[0].count} active products without images`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Summary
|
|||
|
|
console.log('\n' + '='.repeat(60));
|
|||
|
|
console.log('📊 VALIDATION SUMMARY');
|
|||
|
|
console.log('='.repeat(60));
|
|||
|
|
|
|||
|
|
console.log(`\n✅ Successes: ${successes.length}`);
|
|||
|
|
if (successes.length > 0 && successes.length <= 10) {
|
|||
|
|
successes.forEach(s => console.log(` ${s}`));
|
|||
|
|
} else if (successes.length > 10) {
|
|||
|
|
console.log(` (${successes.length} items validated successfully)`);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (warnings.length > 0) {
|
|||
|
|
console.log(`\n⚠️ Warnings: ${warnings.length}`);
|
|||
|
|
warnings.forEach(w => console.log(` ${w}`));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (issues.length > 0) {
|
|||
|
|
console.log(`\n❌ Issues: ${issues.length}`);
|
|||
|
|
issues.forEach(i => console.log(` ${i}`));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
console.log('\n' + '='.repeat(60));
|
|||
|
|
|
|||
|
|
if (issues.length === 0) {
|
|||
|
|
console.log('\n🎉 Database is properly aligned with backend!\n');
|
|||
|
|
process.exit(0);
|
|||
|
|
} else {
|
|||
|
|
console.log('\n⚠️ Database has issues that need attention.\n');
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('\n❌ Validation error:', error.message);
|
|||
|
|
process.exit(1);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
validateAlignment();
|