/** * Database schema check * Ensures required tables and columns exist at startup. */ require('dotenv').config(); const db = require('./db'); const path = require('path'); 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: [] }, 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, 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: 'is_server_source', sql: "ALTER TABLE prometheus_sources ADD COLUMN is_server_source TINYINT(1) DEFAULT 1 AFTER description" }, { name: 'type', sql: "ALTER TABLE prometheus_sources ADD COLUMN type VARCHAR(50) DEFAULT 'prometheus' AFTER is_server_source" } ] }, site_settings: { createSql: ` CREATE TABLE IF NOT EXISTS site_settings ( id INT PRIMARY KEY DEFAULT 1, page_name VARCHAR(255) DEFAULT 'Data Visualization Display Wall', show_page_name TINYINT(1) DEFAULT 1, title VARCHAR(255) DEFAULT 'Data Visualization Display Wall', 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), network_data_sources TEXT, 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, 'Data Visualization Display Wall', 1, 'Data Visualization Display Wall', 'dark', 0, 'tx', 1 ) `, columns: [ { name: 'show_page_name', sql: "ALTER TABLE site_settings ADD COLUMN show_page_name TINYINT(1) DEFAULT 1 AFTER page_name" }, { 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: '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: 'network_data_sources', sql: "ALTER TABLE site_settings ADD COLUMN network_data_sources TEXT AFTER ps_filing" }, { name: 'show_server_ip', sql: "ALTER TABLE site_settings ADD COLUMN show_server_ip TINYINT(1) DEFAULT 0 AFTER network_data_sources" }, { 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: [] }, 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: [] }, 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: [] } }; async function addColumnIfMissing(tableName, existingColumns, column) { if (existingColumns.has(column.name)) { return; } try { 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}'.`); } catch (err) { console.error(`[Database Integrity] Failed to add '${tableName}.${column.name}':`, err.message); if (column.sql.includes(' AFTER ')) { try { const fallbackSql = column.sql.split(' AFTER ')[0]; await db.query(fallbackSql); console.log(`[Database Integrity] Column '${column.name}' added to '${tableName}' via fallback.`); } catch (fallbackErr) { console.error(`[Database Integrity] Fallback failed for '${tableName}.${column.name}':`, fallbackErr.message); } } } } async function ensureTable(tableName, tableSchema) { await db.query(tableSchema.createSql); const [columns] = await db.query(`SHOW COLUMNS FROM \`${tableName}\``); const existingColumns = new Set(columns.map((column) => column.Field)); for (const column of tableSchema.columns || []) { await addColumnIfMissing(tableName, existingColumns, column); } if (tableSchema.seedSql) { await db.query(tableSchema.seedSql); } } async function checkAndFixDatabase() { // Check for DB host to see if we have configuration (could be in .env or environment) if (!process.env.MYSQL_HOST && !fs.existsSync(path.join(__dirname, '..', '.env'))) { console.log('[Database Integrity] No configuration found, skipping check.'); return false; } try { for (const [tableName, tableSchema] of Object.entries(SCHEMA)) { await ensureTable(tableName, tableSchema); } return true; } catch (err) { console.error('[Database Integrity] Startup schema check failed:', err.message); // Rethrow to allow caller to decide if this is fatal throw err; } } module.exports = checkAndFixDatabase;