#!/usr/bin/env node import { MongoClient, ObjectId } from 'mongodb'; import { execSync } from 'child_process'; import path from 'path'; import fs from 'fs'; import https from 'https'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Main database setup script * Orchestrates the complete data import and stats generation process */ async function setupDatabase() { console.log('🚀 Starting BuildPath database setup...'); console.log('====================================='); // Check if data directory exists and has files const dataDir = path.join(__dirname, '../data'); // Try to get latest patch version from existing data files or database let latestPatch = await getLatestPatchVersion(); // If no patch found, download snapshot if (!latestPatch) { if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } console.log('🚫 No match data found. Downloading latest snapshot...'); await downloadAndExtractSnapshot(); // Try again after download latestPatch = await getLatestPatchVersion(); } if (!latestPatch) { console.error('❌ Could not determine latest patch version'); console.log('💡 Make sure you have match data files in the data directory'); process.exit(1); } console.log(`🎯 Latest patch version: ${latestPatch}`); // Check if data directory exists and has files // Support both old format (patch_matches.json) and new platform-specific format (patch_PLATFORM_matches.json) // Also support both "XX.Y" and "XX.Y.Z" patch formats in filenames console.log('🔍 Checking for data files...'); const platforms = ['EUW1', 'EUN1', 'NA1', 'KR']; const dataFiles = []; // Check for platform-specific match files // Files may be named with either "16.8" or "16.8.1" format let foundPlatformFiles = []; for (const platform of platforms) { // Try both formats: "16.8_PLATFORM.json" and "16.8.1_PLATFORM.json" const files = fs.readdirSync(dataDir); const matchFile = files.find(f => { const match = f.match(/^(\d+\.\d+(?:\.\d+)?)_([A-Z0-9]+)\.json$/); const patchFromName = match ? match[1].split('.').slice(0, 2).join('.') : null; return match && patchFromName === latestPatch && match[2] === platform; }); if (matchFile) { foundPlatformFiles.push(platform); dataFiles.push({ path: matchFile, required: false, description: `Match data for ${platform}` }); } } // If no platform-specific files found, look for old format if (foundPlatformFiles.length === 0) { // Try to find any match file for this patch const files = fs.readdirSync(dataDir); const matchFile = files.find(f => { const match = f.match(/^(\d+\.\d+(?:\.\d+)?)(?:_matches)?\.json$/); const patchFromName = match ? match[1].split('.').slice(0, 2).join('.') : null; return match && patchFromName === latestPatch; }); if (matchFile) { dataFiles.push({ path: matchFile, required: true, description: 'Match data' }); } else { dataFiles.push({ path: `${latestPatch}_matches.json`, required: true, description: 'Match data' }); } } let filesExist = true; for (const file of dataFiles) { const fullPath = path.join(dataDir, file.path); if (file.required && !fs.existsSync(fullPath)) { filesExist = false; break; } } if (!filesExist) { console.log('📥 No data files found. Downloading latest snapshot...'); await downloadAndExtractSnapshot(); } else { console.log('✅ Data files found'); for (const file of dataFiles) { const fullPath = path.join(dataDir, file.path); const stats = fs.statSync(fullPath); const size = (stats.size / (1024 * 1024 * 1024)).toFixed(2); console.log(`✅ Found ${file.description}: ${size} GB`); } } // 3. Start MongoDB if not running console.log('🔄 Ensuring MongoDB is running...'); try { execSync('docker compose up -d mongodb', { stdio: 'inherit', cwd: path.join(__dirname, '..') }); } catch (error) { console.log('MongoDB service status:', error.message); } // 4. Wait for MongoDB to be ready await waitForMongoDB(); // 5. Check existing matches count and import if needed console.log('Checking existing matches count...'); // Check for platform-specific collections or fall back to old format const existingPlatforms = await getExistingPlatforms(latestPatch); if (existingPlatforms.length > 0) { console.log(`📊 Found platform-specific collections: ${existingPlatforms.join(', ')}`); let totalMatches = 0; for (const platform of existingPlatforms) { const count = await getMatchCount(latestPatch, platform); console.log(` ${platform}: ${count} matches`); totalMatches += count; } console.log(`📊 Total matches in database: ${totalMatches}`); if (totalMatches < 100) { console.log('📥 Importing matches (this may take a while)...'); await importMatchesData(latestPatch, foundPlatformFiles); } else { console.log('✅ Skipping matches import - sufficient data already present'); } } else { const matchCount = await getMatchCount(latestPatch); console.log(`📊 Current matches in database: ${matchCount}`); if (matchCount < 100) { console.log('📥 Importing matches (this may take a while)...'); await importMatchesData(latestPatch, foundPlatformFiles); } else { console.log('✅ Skipping matches import - sufficient data already present'); } } // 7. Run match collector to generate stats (this also handles CDragon caching) console.log('📊 Generating champion stats...'); await generateChampionStats(); // Create .env file in frontend with default MongoDB connection createFrontendEnvFile(); console.log('🎉 Database setup complete!'); console.log('====================================='); console.log('📊 Your development database is ready!'); console.log('🔗 Connect to MongoDB: mongodb://root:password@localhost:27017'); console.log('🌐 Access Mongo Express: http://localhost:8081'); } async function getLatestPatchVersion() { const dataDir = path.join(__dirname, '../data'); // First, try to get patch from match data files (format: PATCH_PLATFORM.json or PATCH_matches.json) if (fs.existsSync(dataDir)) { const files = fs.readdirSync(dataDir); const patches = new Set(); for (const file of files) { // Match patterns like "16.8.1_EUW1.json" or "15.1_EUW1.json" or "15.1_matches.json" or "15.1.json" // Patch version can be either "XX.Y" or "XX.Y.Z" format const match = file.match(/^(\d+\.\d+(?:\.\d+)?)(?:_[A-Z0-9]+)?(?:_matches)?\.json$/); if (match) { // Normalize to "XX.Y" format (strip the third part if present) const patch = match[1].split('.').slice(0, 2).join('.'); patches.add(patch); } } if (patches.size > 0) { // Sort patches and return the latest (highest version number) const sortedPatches = Array.from(patches).sort((a, b) => { const [aMajor, aMinor] = a.split('.').map(Number); const [bMajor, bMinor] = b.split('.').map(Number); if (aMajor !== bMajor) return bMajor - aMajor; return bMinor - aMinor; }); return sortedPatches[0]; } } // Fallback: try to get from database collections try { const client = new MongoClient(getMongoUri()); await client.connect(); const db = client.db('matches'); const collections = await db.listCollections().toArray(); const collectionNames = collections.map(c => c.name); const patches = new Set(); for (const name of collectionNames) { // Collection names are either "patch_platform" or just "patch" const patch = name.split('_')[0]; if (patch && /^\d+\.\d+$/.test(patch)) { patches.add(patch); } } await client.close(); if (patches.size > 0) { const sortedPatches = Array.from(patches).sort((a, b) => { const [aMajor, aMinor] = a.split('.').map(Number); const [bMajor, bMinor] = b.split('.').map(Number); if (aMajor !== bMajor) return bMajor - aMajor; return bMinor - aMinor; }); return sortedPatches[0]; } } catch (error) { // Database not available, continue with other methods } return null; } async function downloadAndExtractSnapshot() { const snapshotUrl = 'https://vhaudiquet.fr/public/buildpath-dev-snapshot.tar.xz'; const dataDir = path.join(__dirname, '../data'); const tempFile = path.join(dataDir, 'buildpath-dev-snapshot.tar.xz'); const extractDir = dataDir; console.log(`📥 Downloading snapshot to ${tempFile}...`); // Download the file await new Promise((resolve, reject) => { const file = fs.createWriteStream(tempFile); https.get(snapshotUrl, (response) => { if (response.statusCode !== 200) { reject(new Error(`Failed to download snapshot: ${response.statusCode}`)); return; } response.pipe(file); file.on('finish', () => { file.close(); resolve(); }); }).on('error', (error) => { fs.unlink(tempFile, () => {}); reject(error); }); }); console.log('✅ Download complete. Extracting...'); // Extract the tar.xz file using system tar command execSync(`tar -xJf ${tempFile} -C ${extractDir}`, { stdio: 'inherit' }); // Clean up the downloaded file fs.unlinkSync(tempFile); console.log('✅ Extraction complete'); } async function waitForMongoDB() { const client = new MongoClient(getMongoUri()); let retries = 30; while (retries > 0) { try { await client.connect(); await client.db('admin').command({ ping: 1 }); await client.close(); console.log('✅ MongoDB is ready'); return; } catch (error) { retries--; if (retries === 0) { console.error('❌ Failed to connect to MongoDB after multiple attempts'); throw error; } await new Promise(resolve => setTimeout(resolve, 2000)); console.log(`Waiting for MongoDB... (${retries} retries left)`); } } } async function importMatchesData(patchVersion, foundPlatformFiles = []) { const dataDir = path.join(__dirname, '../data'); const files = fs.readdirSync(dataDir); try { // If platform-specific files were found, import each one if (foundPlatformFiles.length > 0) { for (const platform of foundPlatformFiles) { // Find the actual file for this platform (could be "16.8_PLATFORM.json" or "16.8.1_PLATFORM.json") const matchFile = files.find(f => { const match = f.match(/^(\d+\.\d+(?:\.\d+)?)_([A-Z0-9]+)\.json$/); const patchFromName = match ? match[1].split('.').slice(0, 2).join('.') : null; return match && patchFromName === patchVersion && match[2] === platform; }); if (matchFile) { const matchesFile = path.join(dataDir, matchFile); const collectionName = `${patchVersion}_${platform}`; console.log(`📥 Importing matches for ${platform}...`); execSync( `node ${path.join(__dirname, 'process-matches.js')} ${matchesFile} ${collectionName} 1000`, { stdio: 'inherit', env: { ...process.env, MONGO_URI: getMongoUri() } } ); console.log(`✅ Matches import completed for ${platform}`); } else { console.log(`⚠️ No match file found for ${platform}`); } } } else { // Fall back to old format (single file without platform suffix) // Find any match file for this patch const matchFile = files.find(f => { const match = f.match(/^(\d+\.\d+(?:\.\d+)?)(?:_matches)?\.json$/); const patchFromName = match ? match[1].split('.').slice(0, 2).join('.') : null; return match && patchFromName === patchVersion; }); if (matchFile) { const matchesFile = path.join(dataDir, matchFile); const collectionName = patchVersion; execSync( `node ${path.join(__dirname, 'process-matches.js')} ${matchesFile} ${collectionName} 1000`, { stdio: 'inherit', env: { ...process.env, MONGO_URI: getMongoUri() } } ); console.log('✅ Matches import completed'); } else { console.log(`⚠️ No match file found for patch ${patchVersion}`); } } } catch (error) { console.error('❌ Failed to import matches:', error); throw error; } } async function generateChampionStats() { try { console.log('🔄 Running match collector...'); // Set environment variables for development mode const env = { ...process.env, NODE_ENV: 'development', USE_IMPORTED_DATA: 'true', MONGO_URI: getMongoUri(), MONGO_USER: 'root', MONGO_PASS: 'password', MONGO_HOST: 'localhost' }; // Run the match collector execSync(`npm run dev`, { stdio: 'inherit', env: env, cwd: path.join(__dirname, '../../match_collector') }); console.log('✅ Champion stats generated'); } catch (error) { console.error('❌ Failed to generate champion stats:', error); throw error; } } async function getMatchCount(patchVersion, platform = null) { const client = new MongoClient(getMongoUri()); await client.connect(); try { const db = client.db('matches'); const collectionName = platform ? `${patchVersion}_${platform}` : patchVersion; const collection = db.collection(collectionName); const count = await collection.countDocuments(); return count; } catch (error) { console.error('❌ Failed to get match count:', error); return 0; } finally { await client.close(); } } async function getExistingPlatforms(patchVersion) { const client = new MongoClient(getMongoUri()); await client.connect(); try { const db = client.db('matches'); const collections = await db.listCollections().toArray(); const collectionNames = collections.map(c => c.name); const platforms = ['EUW1', 'EUN1', 'NA1', 'KR']; const existingPlatforms = []; for (const platform of platforms) { if (collectionNames.includes(`${patchVersion}_${platform}`)) { existingPlatforms.push(platform); } } return existingPlatforms; } catch (error) { console.error('❌ Failed to get existing platforms:', error); return []; } finally { await client.close(); } } function getMongoUri() { return process.env.MONGO_URI || 'mongodb://root:password@localhost:27017/buildpath?authSource=admin'; } /** * Create .env file in frontend directory with default MongoDB connection */ function createFrontendEnvFile() { try { const frontendDir = path.join(__dirname, '../../frontend'); const envFile = path.join(frontendDir, '.env'); // Check if .env file already exists if (fs.existsSync(envFile)) { console.log('✅ .env file already exists in frontend directory'); return; } // Create .env content with default MongoDB connection const envContent = `MONGO_USER=root MONGO_PASS=password MONGO_HOST=localhost `; // Write the .env file fs.writeFileSync(envFile, envContent, 'utf8'); console.log('✅ Created .env file in frontend directory with default MongoDB connection'); } catch (error) { console.error('❌ Failed to create .env file:', error); } } /** * Convert MongoDB extended JSON format to standard MongoDB objects * Handles $oid, $date, and other extended JSON operators */ function convertMongoExtendedJson(doc) { if (!doc || typeof doc !== 'object') { return doc; } // Handle ObjectId if (doc._id && doc._id.$oid) { doc._id = new ObjectId(doc._id.$oid); } // Handle Date if (doc.date && doc.date.$date) { doc.date = new Date(doc.date.$date); } // Recursively process nested objects for (const key in doc) { if (doc[key] && typeof doc[key] === 'object') { if (Array.isArray(doc[key])) { doc[key] = doc[key].map(item => convertMongoExtendedJson(item)); } else if(key != "parent") { doc[key] = convertMongoExtendedJson(doc[key]); } } } return doc; } // Additional utility functions async function checkDatabaseStatus() { const client = new MongoClient(getMongoUri()); try { await client.connect(); const adminDb = client.db('admin'); const status = await adminDb.command({ serverStatus: 1 }); console.log('📊 Database Status:'); console.log(` - Version: ${status.version}`); console.log(` - Uptime: ${Math.floor(status.uptime / 60)} minutes`); console.log(` - Connections: ${status.connections.current}`); console.log(` - Memory Usage: ${(status.mem.resident / 1024 / 1024).toFixed(1)} MB`); // Check collections const dbNames = await adminDb.admin().listDatabases(); console.log('📦 Databases:'); dbNames.databases.forEach(db => { if (db.name !== 'admin' && db.name !== 'local' && db.name !== 'config') { console.log(` - ${db.name}: ${(db.sizeOnDisk / 1024 / 1024).toFixed(1)} MB`); } }); } catch (error) { console.error('❌ Failed to get database status:', error); } finally { await client.close(); } } // Command line interface if (import.meta.url === `file://${process.argv[1]}`) { const args = process.argv.slice(2); const command = args[0]; switch (command) { case 'status': checkDatabaseStatus().catch(console.error); break; case 'import-matches': if (args[1]) { importMatchesData(args[1]).catch(console.error); } else { console.error('❌ Please provide a patch version'); } break; case 'generate-stats': generateChampionStats().catch(console.error); break; case 'match-count': if (args[1]) { getMatchCount(args[1]).then(count => console.log(`Match count: ${count}`)).catch(console.error); } else { console.error('❌ Please provide a patch version'); } break; case 'latest-patch': getLatestPatchVersion().then(patch => console.log(`Latest patch: ${patch}`)).catch(console.error); break; default: setupDatabase().catch(error => { console.error('❌ Setup failed:', error); process.exit(1); }); } } export { setupDatabase, importMatchesData, generateChampionStats, checkDatabaseStatus, getMatchCount, getLatestPatchVersion };