/** * Database schema check * Ensures required tables and columns exist at startup. */ const path = require('path'); require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); const db = require('./db'); const fs = require('fs'); const SCHEMA = { users: { createSql: ` CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, salt VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, columns: [ { name: 'username', sql: "ALTER TABLE users ADD COLUMN username VARCHAR(255) NOT NULL UNIQUE AFTER id" }, { name: 'password', sql: "ALTER TABLE users ADD COLUMN password VARCHAR(255) NOT NULL AFTER username" }, { name: 'salt', sql: "ALTER TABLE users ADD COLUMN salt VARCHAR(255) NOT NULL AFTER password" } ] }, prometheus_sources: { createSql: ` CREATE TABLE IF NOT EXISTS prometheus_sources ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, url VARCHAR(500) NOT NULL, description TEXT, is_server_source TINYINT(1) DEFAULT 1, is_overview_source TINYINT(1) DEFAULT 1, is_detail_source TINYINT(1) DEFAULT 1, type VARCHAR(50) DEFAULT 'prometheus', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, columns: [ { name: 'name', sql: "ALTER TABLE prometheus_sources ADD COLUMN name VARCHAR(255) NOT NULL AFTER id" }, { name: 'url', sql: "ALTER TABLE prometheus_sources ADD COLUMN url VARCHAR(500) NOT NULL AFTER name" }, { name: 'description', sql: "ALTER TABLE prometheus_sources ADD COLUMN description TEXT AFTER url" }, { name: 'is_server_source', sql: "ALTER TABLE prometheus_sources ADD COLUMN is_server_source TINYINT(1) DEFAULT 1 AFTER description" }, { name: 'is_overview_source', sql: "ALTER TABLE prometheus_sources ADD COLUMN is_overview_source TINYINT(1) DEFAULT 1 AFTER is_server_source" }, { name: 'is_detail_source', sql: "ALTER TABLE prometheus_sources ADD COLUMN is_detail_source TINYINT(1) DEFAULT 1 AFTER is_overview_source" }, { name: 'type', sql: "ALTER TABLE prometheus_sources ADD COLUMN type VARCHAR(50) DEFAULT 'prometheus' AFTER is_detail_source" } ] }, site_settings: { createSql: ` CREATE TABLE IF NOT EXISTS site_settings ( id INT PRIMARY KEY DEFAULT 1, page_name VARCHAR(255) DEFAULT '数据可视化展示大屏', show_page_name TINYINT(1) DEFAULT 1, title VARCHAR(255) DEFAULT '数据可视化展示大屏', logo_url TEXT, logo_url_dark TEXT, favicon_url TEXT, default_theme VARCHAR(20) DEFAULT 'dark', show_95_bandwidth TINYINT(1) DEFAULT 0, p95_type VARCHAR(20) DEFAULT 'tx', require_login_for_server_details TINYINT(1) DEFAULT 1, blackbox_source_id INT, latency_source VARCHAR(100), latency_dest VARCHAR(100), latency_target VARCHAR(255), icp_filing VARCHAR(255), ps_filing VARCHAR(255), show_server_ip TINYINT(1) DEFAULT 0, ip_metric_name VARCHAR(100) DEFAULT NULL, ip_label_name VARCHAR(100) DEFAULT 'address', custom_metrics JSON DEFAULT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, seedSql: ` INSERT IGNORE INTO site_settings ( id, page_name, show_page_name, title, default_theme, show_95_bandwidth, p95_type, require_login_for_server_details ) VALUES ( 1, '数据可视化展示大屏', 1, '数据可视化展示大屏', 'dark', 0, 'tx', 1 ) `, columns: [ { name: 'page_name', sql: "ALTER TABLE site_settings ADD COLUMN page_name VARCHAR(255) DEFAULT '数据可视化展示大屏' AFTER id" }, { name: 'show_page_name', sql: "ALTER TABLE site_settings ADD COLUMN show_page_name TINYINT(1) DEFAULT 1 AFTER page_name" }, { name: 'title', sql: "ALTER TABLE site_settings ADD COLUMN title VARCHAR(255) DEFAULT '数据可视化展示大屏' AFTER show_page_name" }, { name: 'logo_url', sql: "ALTER TABLE site_settings ADD COLUMN logo_url TEXT AFTER title" }, { name: 'logo_url_dark', sql: "ALTER TABLE site_settings ADD COLUMN logo_url_dark TEXT AFTER logo_url" }, { name: 'favicon_url', sql: "ALTER TABLE site_settings ADD COLUMN favicon_url TEXT AFTER logo_url_dark" }, { name: 'default_theme', sql: "ALTER TABLE site_settings ADD COLUMN default_theme VARCHAR(20) DEFAULT 'dark' AFTER favicon_url" }, { name: 'show_95_bandwidth', sql: "ALTER TABLE site_settings ADD COLUMN show_95_bandwidth TINYINT(1) DEFAULT 0 AFTER default_theme" }, { name: 'p95_type', sql: "ALTER TABLE site_settings ADD COLUMN p95_type VARCHAR(20) DEFAULT 'tx' AFTER show_95_bandwidth" }, { name: 'require_login_for_server_details', sql: "ALTER TABLE site_settings ADD COLUMN require_login_for_server_details TINYINT(1) DEFAULT 1 AFTER p95_type" }, { name: 'blackbox_source_id', sql: "ALTER TABLE site_settings ADD COLUMN blackbox_source_id INT AFTER require_login_for_server_details" }, { name: 'latency_source', sql: "ALTER TABLE site_settings ADD COLUMN latency_source VARCHAR(100) AFTER blackbox_source_id" }, { name: 'latency_dest', sql: "ALTER TABLE site_settings ADD COLUMN latency_dest VARCHAR(100) AFTER latency_source" }, { name: 'latency_target', sql: "ALTER TABLE site_settings ADD COLUMN latency_target VARCHAR(255) AFTER latency_dest" }, { name: 'icp_filing', sql: "ALTER TABLE site_settings ADD COLUMN icp_filing VARCHAR(255) AFTER latency_target" }, { name: 'ps_filing', sql: "ALTER TABLE site_settings ADD COLUMN ps_filing VARCHAR(255) AFTER icp_filing" }, { name: 'show_server_ip', sql: "ALTER TABLE site_settings ADD COLUMN show_server_ip TINYINT(1) DEFAULT 0 AFTER ps_filing" }, { name: 'ip_metric_name', sql: "ALTER TABLE site_settings ADD COLUMN ip_metric_name VARCHAR(100) DEFAULT NULL AFTER show_server_ip" }, { name: 'ip_label_name', sql: "ALTER TABLE site_settings ADD COLUMN ip_label_name VARCHAR(100) DEFAULT 'address' AFTER ip_metric_name" }, { name: 'custom_metrics', sql: "ALTER TABLE site_settings ADD COLUMN custom_metrics JSON DEFAULT NULL AFTER ip_label_name" } ] }, traffic_stats: { createSql: ` CREATE TABLE IF NOT EXISTS traffic_stats ( id INT AUTO_INCREMENT PRIMARY KEY, rx_bytes BIGINT UNSIGNED DEFAULT 0, tx_bytes BIGINT UNSIGNED DEFAULT 0, rx_bandwidth DOUBLE DEFAULT 0, tx_bandwidth DOUBLE DEFAULT 0, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE INDEX (timestamp) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, columns: [ { name: 'rx_bytes', sql: "ALTER TABLE traffic_stats ADD COLUMN rx_bytes BIGINT UNSIGNED DEFAULT 0 AFTER id" }, { name: 'tx_bytes', sql: "ALTER TABLE traffic_stats ADD COLUMN tx_bytes BIGINT UNSIGNED DEFAULT 0 AFTER rx_bytes" }, { name: 'rx_bandwidth', sql: "ALTER TABLE traffic_stats ADD COLUMN rx_bandwidth DOUBLE DEFAULT 0 AFTER tx_bytes" }, { name: 'tx_bandwidth', sql: "ALTER TABLE traffic_stats ADD COLUMN tx_bandwidth DOUBLE DEFAULT 0 AFTER rx_bandwidth" } ] }, server_locations: { createSql: ` CREATE TABLE IF NOT EXISTS server_locations ( id INT AUTO_INCREMENT PRIMARY KEY, ip VARCHAR(255) NOT NULL UNIQUE, country CHAR(2), country_name VARCHAR(100), region VARCHAR(100), city VARCHAR(100), latitude DOUBLE, longitude DOUBLE, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, columns: [ { name: 'ip', sql: "ALTER TABLE server_locations ADD COLUMN ip VARCHAR(255) NOT NULL UNIQUE AFTER id" }, { name: 'country', sql: "ALTER TABLE server_locations ADD COLUMN country CHAR(2) AFTER ip" }, { name: 'country_name', sql: "ALTER TABLE server_locations ADD COLUMN country_name VARCHAR(100) AFTER country" }, { name: 'region', sql: "ALTER TABLE server_locations ADD COLUMN region VARCHAR(100) AFTER country_name" }, { name: 'city', sql: "ALTER TABLE server_locations ADD COLUMN city VARCHAR(100) AFTER region" }, { name: 'latitude', sql: "ALTER TABLE server_locations ADD COLUMN latitude DOUBLE AFTER city" }, { name: 'longitude', sql: "ALTER TABLE server_locations ADD COLUMN longitude DOUBLE AFTER latitude" } ] }, latency_routes: { createSql: ` CREATE TABLE IF NOT EXISTS latency_routes ( id INT AUTO_INCREMENT PRIMARY KEY, source_id INT NOT NULL, latency_source VARCHAR(100) NOT NULL, latency_dest VARCHAR(100) NOT NULL, latency_target VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `, columns: [ { name: 'source_id', sql: "ALTER TABLE latency_routes ADD COLUMN source_id INT NOT NULL AFTER id" }, { name: 'latency_source', sql: "ALTER TABLE latency_routes ADD COLUMN latency_source VARCHAR(100) NOT NULL AFTER source_id" }, { name: 'latency_dest', sql: "ALTER TABLE latency_routes ADD COLUMN latency_dest VARCHAR(100) NOT NULL AFTER latency_source" }, { name: 'latency_target', sql: "ALTER TABLE latency_routes ADD COLUMN latency_target VARCHAR(255) NOT NULL AFTER latency_dest" } ] } }; async function ensureTable(tableName, tableSchema) { try { // 1. Ensure table exists await db.query(tableSchema.createSql); // 2. Check columns const [columns] = await db.query(`SHOW COLUMNS FROM \`${tableName}\``); const existingColumns = new Set(columns.map((column) => column.Field)); console.log(`[Database Integrity] Table '${tableName}' verified (${columns.length} columns)`); for (const column of tableSchema.columns || []) { if (!existingColumns.has(column.name)) { console.log(`[Database Integrity] Missing column '${column.name}' in '${tableName}'. Adding it...`); await db.query(column.sql); console.log(`[Database Integrity] Column '${column.name}' added to '${tableName}'.`); } } // 3. Seed data if (tableSchema.seedSql) { const [rows] = await db.query(`SELECT count(*) as count FROM \`${tableName}\``); if (rows[0].count === 0) { console.log(`[Database Integrity] Table '${tableName}' is empty. Seeding initial data...`); await db.query(tableSchema.seedSql); } } } catch (err) { console.error(`[Database Integrity] Error ensuring table '${tableName}':`, err.message); throw err; } } async function checkAndFixDatabase() { console.log('[Database Integrity] Starting comprehensive database audit...'); // Try to check if we can even connect try { const health = await db.checkHealth(); if (health.status !== 'up') { console.warn(`[Database Integrity] initial health check failed: ${health.error}`); // If we can't connect, maybe the DB itself doesn't exist? // For now, we rely on the pool to handle connection retries/errors. } } catch (e) { // Ignore health check errors, let ensureTable handle the primary queries } try { let tablesChecked = 0; for (const [tableName, tableSchema] of Object.entries(SCHEMA)) { await ensureTable(tableName, tableSchema); tablesChecked++; } console.log(`[Database Integrity] Audit complete. ${tablesChecked} tables verified and healthy.`); return true; } catch (err) { console.error('[Database Integrity] ❌ Audit failed:', err.message); throw err; } } module.exports = checkAndFixDatabase;