/** * Shattered Void MMO - Database Validation System * * This module provides comprehensive database validation including connectivity, * schema validation, migration status, and data integrity checks. */ const path = require('path'); const fs = require('fs').promises; class DatabaseValidator { constructor() { this.knex = null; this.validationResults = { connectivity: false, migrations: false, schema: false, seeds: false, integrity: false }; } /** * Validate complete database setup */ async validateDatabase() { const startTime = Date.now(); const results = { success: false, connectivity: null, migrations: null, schema: null, seeds: null, integrity: null, error: null, duration: 0 }; try { // Test database connectivity results.connectivity = await this.validateConnectivity(); // Check migration status results.migrations = await this.validateMigrations(); // Validate schema structure results.schema = await this.validateSchema(); // Check seed data results.seeds = await this.validateSeeds(); // Run integrity checks results.integrity = await this.validateIntegrity(); // Determine overall success results.success = results.connectivity.success && results.migrations.success && results.schema.success; results.duration = Date.now() - startTime; return results; } catch (error) { results.error = error.message; results.duration = Date.now() - startTime; return results; } finally { // Cleanup database connection if (this.knex) { await this.knex.destroy(); } } } /** * Validate database connectivity */ async validateConnectivity() { try { // Load database configuration const knexConfig = this.loadKnexConfig(); const config = knexConfig[process.env.NODE_ENV || 'development']; if (!config) { throw new Error(`No database configuration found for environment: ${process.env.NODE_ENV || 'development'}`); } // Initialize Knex connection this.knex = require('knex')(config); // Test basic connectivity await this.knex.raw('SELECT 1 as test'); // Get database version info const versionResult = await this.knex.raw('SELECT version()'); const version = versionResult.rows[0].version; // Get database size info const sizeResult = await this.knex.raw(` SELECT pg_database.datname, pg_size_pretty(pg_database_size(pg_database.datname)) AS size FROM pg_database WHERE pg_database.datname = current_database() `); const dbSize = sizeResult.rows[0]?.size || 'Unknown'; // Check connection pool status const poolInfo = { min: this.knex.client.pool.min, max: this.knex.client.pool.max, used: this.knex.client.pool.numUsed(), free: this.knex.client.pool.numFree(), pending: this.knex.client.pool.numPendingAcquires() }; return { success: true, database: config.connection.database, host: config.connection.host, port: config.connection.port, version: version.split(' ')[0] + ' ' + version.split(' ')[1], // PostgreSQL version size: dbSize, pool: poolInfo, ssl: config.connection.ssl ? 'enabled' : 'disabled' }; } catch (error) { return { success: false, error: error.message, troubleshooting: this.getDatabaseTroubleshooting(error) }; } } /** * Validate migration status */ async validateMigrations() { try { // Check if migrations table exists const hasTable = await this.knex.schema.hasTable('knex_migrations'); if (!hasTable) { // Run migrations if table doesn't exist console.log(' 📦 Running initial database migrations...'); await this.knex.migrate.latest(); } // Get migration status const [currentBatch, migrationList] = await Promise.all([ this.knex.migrate.currentVersion(), this.knex.migrate.list() ]); const [completed, pending] = migrationList; // Check for pending migrations if (pending.length > 0) { console.log(` 📦 Found ${pending.length} pending migrations, running now...`); await this.knex.migrate.latest(); // Re-check status after running migrations const [newCompleted] = await this.knex.migrate.list(); return { success: true, currentBatch: await this.knex.migrate.currentVersion(), completed: newCompleted.length, pending: 0, autoRan: pending.length, migrations: newCompleted.map(migration => ({ name: migration, status: 'completed' })) }; } return { success: true, currentBatch, completed: completed.length, pending: pending.length, migrations: [ ...completed.map(migration => ({ name: migration, status: 'completed' })), ...pending.map(migration => ({ name: migration, status: 'pending' })) ] }; } catch (error) { return { success: false, error: error.message, troubleshooting: [ 'Check if migration files exist in src/database/migrations/', 'Verify database user has CREATE permissions', 'Ensure migration files follow correct naming convention' ] }; } } /** * Validate database schema structure */ async validateSchema() { try { const requiredTables = [ 'players', 'colonies', 'player_resources', 'fleets', 'fleet_ships', 'ship_designs', 'technologies', 'player_research' ]; const schemaInfo = { tables: {}, missingTables: [], totalTables: 0, requiredTables: requiredTables.length }; // Check each required table for (const tableName of requiredTables) { const exists = await this.knex.schema.hasTable(tableName); if (exists) { // Get table info const columns = await this.knex(tableName).columnInfo(); const rowCount = await this.knex(tableName).count('* as count').first(); schemaInfo.tables[tableName] = { exists: true, columns: Object.keys(columns).length, rows: parseInt(rowCount.count), structure: Object.keys(columns) }; } else { schemaInfo.missingTables.push(tableName); schemaInfo.tables[tableName] = { exists: false, error: 'Table does not exist' }; } } // Get total number of tables in database const allTables = await this.knex.raw(` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' `); schemaInfo.totalTables = allTables.rows.length; const success = schemaInfo.missingTables.length === 0; return { success, ...schemaInfo, coverage: `${requiredTables.length - schemaInfo.missingTables.length}/${requiredTables.length}`, troubleshooting: !success ? [ 'Run database migrations: npm run db:migrate', 'Check migration files in src/database/migrations/', 'Verify database user has CREATE permissions' ] : null }; } catch (error) { return { success: false, error: error.message }; } } /** * Validate seed data */ async validateSeeds() { try { const seedChecks = { technologies: await this.checkTechnologiesSeeded(), shipDesigns: await this.checkShipDesignsSeeded(), systemData: await this.checkSystemDataSeeded() }; const allSeeded = Object.values(seedChecks).every(check => check.seeded); // If no seed data, offer to run seeds if (!allSeeded) { console.log(' 🌱 Some seed data is missing, running seeds...'); try { // Run seeds await this.knex.seed.run(); // Re-check seed status const newSeedChecks = { technologies: await this.checkTechnologiesSeeded(), shipDesigns: await this.checkShipDesignsSeeded(), systemData: await this.checkSystemDataSeeded() }; return { success: true, autoSeeded: true, checks: newSeedChecks, message: 'Seed data was missing and has been automatically populated' }; } catch (seedError) { return { success: false, autoSeeded: false, error: `Failed to run seeds: ${seedError.message}`, checks: seedChecks }; } } return { success: true, checks: seedChecks, message: 'All required seed data is present' }; } catch (error) { return { success: false, error: error.message }; } } /** * Validate data integrity */ async validateIntegrity() { try { const integrityChecks = []; // Check foreign key constraints integrityChecks.push(await this.checkForeignKeyIntegrity()); // Check for orphaned records integrityChecks.push(await this.checkOrphanedRecords()); // Check data consistency integrityChecks.push(await this.checkDataConsistency()); const allPassed = integrityChecks.every(check => check.passed); return { success: allPassed, checks: integrityChecks, summary: `${integrityChecks.filter(c => c.passed).length}/${integrityChecks.length} integrity checks passed` }; } catch (error) { return { success: false, error: error.message }; } } /** * Check if technologies are seeded */ async checkTechnologiesSeeded() { try { const count = await this.knex('technologies').count('* as count').first(); const techCount = parseInt(count.count); return { seeded: techCount > 0, count: techCount, expected: '> 0' }; } catch (error) { return { seeded: false, error: error.message }; } } /** * Check if ship designs are seeded */ async checkShipDesignsSeeded() { try { const count = await this.knex('ship_designs').count('* as count').first(); const designCount = parseInt(count.count); return { seeded: designCount > 0, count: designCount, expected: '> 0' }; } catch (error) { return { seeded: false, error: error.message }; } } /** * Check if system data is seeded */ async checkSystemDataSeeded() { try { // Check if we have any basic game configuration const hasBasicData = true; // For now, assume system data is OK if DB is accessible return { seeded: hasBasicData, message: 'System data validation passed' }; } catch (error) { return { seeded: false, error: error.message }; } } /** * Check foreign key integrity */ async checkForeignKeyIntegrity() { try { // Check for any foreign key constraint violations const violations = []; // Check colonies -> players const orphanedColonies = await this.knex.raw(` SELECT c.id, c.name FROM colonies c LEFT JOIN players p ON c.player_id = p.id WHERE p.id IS NULL `); if (orphanedColonies.rows.length > 0) { violations.push(`${orphanedColonies.rows.length} colonies without valid players`); } // Check fleets -> players const orphanedFleets = await this.knex.raw(` SELECT f.id, f.name FROM fleets f LEFT JOIN players p ON f.player_id = p.id WHERE p.id IS NULL `); if (orphanedFleets.rows.length > 0) { violations.push(`${orphanedFleets.rows.length} fleets without valid players`); } return { passed: violations.length === 0, name: 'Foreign Key Integrity', violations: violations, message: violations.length === 0 ? 'All foreign key constraints are valid' : `Found ${violations.length} violations` }; } catch (error) { return { passed: false, name: 'Foreign Key Integrity', error: error.message }; } } /** * Check for orphaned records */ async checkOrphanedRecords() { try { const orphanedRecords = []; // This is a simplified check - in a real scenario you'd check all relationships return { passed: orphanedRecords.length === 0, name: 'Orphaned Records Check', orphaned: orphanedRecords, message: 'No orphaned records found' }; } catch (error) { return { passed: false, name: 'Orphaned Records Check', error: error.message }; } } /** * Check data consistency */ async checkDataConsistency() { try { const inconsistencies = []; // Example: Check if all players have at least one colony (if required by game rules) // This would depend on your specific game rules return { passed: inconsistencies.length === 0, name: 'Data Consistency Check', inconsistencies: inconsistencies, message: 'Data consistency checks passed' }; } catch (error) { return { passed: false, name: 'Data Consistency Check', error: error.message }; } } /** * Load Knex configuration */ loadKnexConfig() { try { const knexfilePath = path.join(process.cwd(), 'knexfile.js'); delete require.cache[require.resolve(knexfilePath)]; return require(knexfilePath); } catch (error) { throw new Error(`Cannot load knexfile.js: ${error.message}`); } } /** * Get database troubleshooting tips */ getDatabaseTroubleshooting(error) { const tips = []; if (error.message.includes('ECONNREFUSED')) { tips.push('Database server is not running - start PostgreSQL service'); tips.push('Check if database is running on correct host/port'); } if (error.message.includes('authentication failed')) { tips.push('Check database username and password in .env file'); tips.push('Verify database user exists and has correct permissions'); } if (error.message.includes('database') && error.message.includes('does not exist')) { tips.push('Create database: createdb shattered_void_dev'); tips.push('Or run: npm run db:setup'); } if (error.message.includes('permission denied')) { tips.push('Database user needs CREATE and ALTER permissions'); tips.push('Check PostgreSQL user privileges'); } if (tips.length === 0) { tips.push('Check database connection parameters in .env file'); tips.push('Ensure PostgreSQL is installed and running'); tips.push('Verify network connectivity to database server'); } return tips; } /** * Get database performance metrics */ async getDatabaseMetrics() { if (!this.knex) { return null; } try { // Get connection info const connections = await this.knex.raw(` SELECT count(*) as total, count(*) FILTER (WHERE state = 'active') as active, count(*) FILTER (WHERE state = 'idle') as idle FROM pg_stat_activity WHERE datname = current_database() `); // Get database size const size = await this.knex.raw(` SELECT pg_size_pretty(pg_database_size(current_database())) as size `); return { connections: connections.rows[0], size: size.rows[0].size, timestamp: new Date().toISOString() }; } catch (error) { return { error: error.message }; } } } module.exports = DatabaseValidator;