const fs = require('fs').promises; const path = require('path'); const dbConnection = require('./connection'); class DatabaseInitializer { constructor() { this.migrationsPath = path.join(__dirname, 'migrations'); } /** * Initialize database with all migrations */ async initialize() { try { console.log('🚀 Starting database initialization...'); // Connect to database await dbConnection.connect(); // Create migrations table if it doesn't exist await this.createMigrationsTable(); // Run all migrations await this.runMigrations(); // Seed initial data if needed await this.seedData(); console.log('✅ Database initialization completed successfully'); } catch (error) { console.error('❌ Database initialization failed:', error); throw error; } } /** * Create migrations tracking table */ async createMigrationsTable() { const createMigrationsTableSQL = ` CREATE TABLE IF NOT EXISTS migrations ( id SERIAL PRIMARY KEY, filename VARCHAR(255) NOT NULL UNIQUE, executed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); `; await dbConnection.query(createMigrationsTableSQL); console.log('📋 Migrations table ready'); } /** * Get list of migration files */ async getMigrationFiles() { try { const files = await fs.readdir(this.migrationsPath); return files .filter(file => file.endsWith('.sql')) .sort(); // Ensure migrations run in order } catch (error) { console.error('❌ Error reading migrations directory:', error); return []; } } /** * Get executed migrations from database */ async getExecutedMigrations() { try { const result = await dbConnection.query('SELECT filename FROM migrations ORDER BY executed_at'); return result.rows.map(row => row.filename); } catch (error) { console.error('❌ Error fetching executed migrations:', error); return []; } } /** * Run all pending migrations */ async runMigrations() { const migrationFiles = await this.getMigrationFiles(); const executedMigrations = await this.getExecutedMigrations(); const pendingMigrations = migrationFiles.filter( file => !executedMigrations.includes(file) ); if (pendingMigrations.length === 0) { console.log('📝 No pending migrations to run'); return; } console.log(`📝 Running ${pendingMigrations.length} pending migrations...`); for (const migrationFile of pendingMigrations) { await this.runMigration(migrationFile); } } /** * Run a single migration */ async runMigration(filename) { try { console.log(`🔄 Running migration: ${filename}`); const migrationPath = path.join(this.migrationsPath, filename); const migrationSQL = await fs.readFile(migrationPath, 'utf8'); // Execute migration in a transaction await dbConnection.transaction(async (client) => { // Execute the migration SQL await client.query(migrationSQL); // Record the migration as executed await client.query( 'INSERT INTO migrations (filename) VALUES ($1)', [filename] ); }); console.log(`✅ Migration completed: ${filename}`); } catch (error) { console.error(`❌ Migration failed: ${filename}`, error); throw error; } } /** * Seed initial data */ async seedData() { try { // Check if we need to seed data (only in development) if (process.env.NODE_ENV !== 'development') { console.log('🌱 Skipping seed data (not in development mode)'); return; } // Check if users already exist const userCount = await dbConnection.query('SELECT COUNT(*) FROM users'); if (parseInt(userCount.rows[0].count) > 0) { console.log('🌱 Skipping seed data (users already exist)'); return; } console.log('🌱 Seeding initial data...'); // Create a test user (only in development) const bcrypt = require('bcrypt'); const testPassword = await bcrypt.hash('TestPassword123!', 12); await dbConnection.query(` INSERT INTO users (email, password_hash, is_verified) VALUES ($1, $2, $3) `, ['test@example.com', testPassword, true]); // Get the test user ID for sample bookmarks const testUser = await dbConnection.query( 'SELECT id FROM users WHERE email = $1', ['test@example.com'] ); const testUserId = testUser.rows[0].id; // Create sample bookmarks const sampleBookmarks = [ { title: 'Google', url: 'https://www.google.com', folder: 'Search Engines', add_date: new Date() }, { title: 'GitHub', url: 'https://github.com', folder: 'Development', add_date: new Date() }, { title: 'Stack Overflow', url: 'https://stackoverflow.com', folder: 'Development', add_date: new Date() } ]; for (const bookmark of sampleBookmarks) { await dbConnection.query(` INSERT INTO bookmarks (user_id, title, url, folder, add_date) VALUES ($1, $2, $3, $4, $5) `, [testUserId, bookmark.title, bookmark.url, bookmark.folder, bookmark.add_date]); } console.log('✅ Seed data created successfully'); console.log('👤 Test user: test@example.com / TestPassword123!'); } catch (error) { console.error('❌ Seeding failed:', error); // Don't throw error for seeding failures in production if (process.env.NODE_ENV === 'development') { throw error; } } } /** * Reset database (drop all tables) - USE WITH CAUTION */ async reset() { if (process.env.NODE_ENV === 'production') { throw new Error('Database reset is not allowed in production'); } console.log('⚠️ Resetting database...'); await dbConnection.query('DROP TABLE IF EXISTS bookmarks CASCADE'); await dbConnection.query('DROP TABLE IF EXISTS users CASCADE'); await dbConnection.query('DROP TABLE IF EXISTS migrations CASCADE'); await dbConnection.query('DROP FUNCTION IF EXISTS update_updated_at_column() CASCADE'); console.log('🗑️ Database reset completed'); // Re-initialize await this.initialize(); } /** * Get database status */ async getStatus() { try { const health = await dbConnection.healthCheck(); const migrationFiles = await this.getMigrationFiles(); const executedMigrations = await this.getExecutedMigrations(); return { ...health, migrations: { total: migrationFiles.length, executed: executedMigrations.length, pending: migrationFiles.length - executedMigrations.length, files: migrationFiles, executed_files: executedMigrations } }; } catch (error) { return { healthy: false, error: error.message }; } } } // Create singleton instance const dbInitializer = new DatabaseInitializer(); module.exports = dbInitializer;