const mysql = require('mysql2/promise'); require('dotenv').config(); const fs = require('fs'); const path = require('path'); const NODE_ENV = process.env.NODE_ENV || 'development'; const getSSLConfig = () => { const useSSL = String(process.env.DB_SSL || '').toLowerCase() === 'true'; const caPath = process.env.DB_SSL_CA_PATH; if (!useSSL) return undefined; try { if (caPath) { const resolved = path.resolve(caPath); if (fs.existsSync(resolved)) { console.log('๐Ÿ” (createDb) Loading DB CA certificate from:', resolved); return { ca: fs.readFileSync(resolved), rejectUnauthorized: false }; } else { console.warn('โš ๏ธ (createDb) CA file not found at:', resolved, '- proceeding with rejectUnauthorized:false'); } } else { console.warn('โš ๏ธ (createDb) DB_SSL_CA_PATH not set - proceeding with rejectUnauthorized:false'); } } catch (e) { console.warn('โš ๏ธ (createDb) Failed to load CA file:', e.message, '- proceeding with rejectUnauthorized:false'); } return { rejectUnauthorized: false }; }; let dbConfig; if (NODE_ENV === 'development') { dbConfig = { host: process.env.DEV_DB_HOST || 'localhost', port: Number(process.env.DEV_DB_PORT) || 3306, user: process.env.DEV_DB_USER || 'root', password: process.env.DEV_DB_PASSWORD || '', // XAMPP default: no password database: process.env.DEV_DB_NAME || 'profitplanet_centralserver', ssl: undefined }; } else { dbConfig = { host: process.env.DB_HOST, port: Number(process.env.DB_PORT) || 3306, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, ssl: getSSLConfig() }; } const allowCreateDb = String(process.env.DB_ALLOW_CREATE_DB || 'false').toLowerCase() === 'true'; // --- Performance Helpers (added) --- async function ensureIndex(conn, table, indexName, indexDDL) { const [rows] = await conn.query(`SHOW INDEX FROM \`${table}\` WHERE Key_name = ?`, [indexName]); if (!rows.length) { await conn.query(`CREATE INDEX \`${indexName}\` ON \`${table}\` (${indexDDL})`); console.log(`๐Ÿ†• Created index ${indexName} ON ${table}`); } else { console.log(`โ„น๏ธ Index ${indexName} already exists on ${table}`); } } async function createDatabase() { console.log('๐Ÿš€ Starting MySQL database initialization...'); console.log('๐Ÿ“ Database host:', process.env.DB_HOST); console.log('๐Ÿ“ Database name:', process.env.DB_NAME); let connection; try { if (allowCreateDb) { // Connect without specifying a database to create it if it doesn't exist connection = await mysql.createConnection({ host: dbConfig.host, port: dbConfig.port, user: dbConfig.user, password: dbConfig.password, ssl: dbConfig.ssl }); await connection.query(`CREATE DATABASE IF NOT EXISTS \`${dbConfig.database}\`;`); console.log(`โœ… Database "${dbConfig.database}" created/verified`); await connection.end(); } else { console.log('โ„น๏ธ Skipping database creation (DB_ALLOW_CREATE_DB=false)'); } // Reconnect with the database specified connection = await mysql.createConnection(dbConfig); console.log('โœ… Connected to MySQL database'); // --- Core Tables --- // 1. users table: Central user authentication and common attributes await connection.query(` CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, user_type ENUM('personal', 'company') NOT NULL, role ENUM('user', 'admin', 'super_admin') DEFAULT 'user', iban VARCHAR(34), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, last_login_at TIMESTAMP NULL, INDEX idx_email (email), INDEX idx_user_type (user_type), INDEX idx_role (role) ); `); console.log('โœ… Users table created/verified'); // 2. personal_profiles table: Details specific to personal users await connection.query(` CREATE TABLE IF NOT EXISTS personal_profiles ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, phone VARCHAR(255) NULL, date_of_birth DATE, nationality VARCHAR(255), address TEXT, zip_code VARCHAR(20), city VARCHAR(100), country VARCHAR(100), phone_secondary VARCHAR(255), emergency_contact_name VARCHAR(255), emergency_contact_phone VARCHAR(255), account_holder_name VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE KEY unique_user_profile (user_id) ); `); console.log('โœ… Personal profiles table created/verified'); // 3. company_profiles table: Details specific to company users await connection.query(` CREATE TABLE IF NOT EXISTS company_profiles ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, company_name VARCHAR(255) NOT NULL, registration_number VARCHAR(255) UNIQUE, phone VARCHAR(255) NULL, address TEXT, zip_code VARCHAR(20), city VARCHAR(100), country VARCHAR(100), branch VARCHAR(255), number_of_employees INT, business_type VARCHAR(255), contact_person_name VARCHAR(255), contact_person_phone VARCHAR(255), account_holder_name VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE KEY unique_user_profile (user_id), UNIQUE KEY unique_registration_number (registration_number) ); `); console.log('โœ… Company profiles table created/verified'); // 4. user_status table: Comprehensive tracking of user verification and completion steps await connection.query(` CREATE TABLE IF NOT EXISTS user_status ( user_id INT PRIMARY KEY, status ENUM('inactive', 'pending', 'active', 'suspended', 'archived') DEFAULT 'pending', previous_status ENUM('inactive', 'pending', 'active', 'suspended', 'archived') NULL, email_verified BOOLEAN DEFAULT FALSE, email_verified_at TIMESTAMP NULL, profile_completed BOOLEAN DEFAULT FALSE, profile_completed_at TIMESTAMP NULL, documents_uploaded BOOLEAN DEFAULT FALSE, documents_uploaded_at TIMESTAMP NULL, contract_signed BOOLEAN DEFAULT FALSE, contract_signed_at TIMESTAMP NULL, is_admin_verified BOOLEAN DEFAULT FALSE, admin_verified_at TIMESTAMP NULL, registration_completed BOOLEAN DEFAULT FALSE, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); `); console.log('โœ… User status table created/verified'); // Modify existing ENUM columns to add 'archived' status (for existing databases) try { await connection.query(` ALTER TABLE user_status MODIFY COLUMN status ENUM('inactive', 'pending', 'active', 'suspended', 'archived') DEFAULT 'pending' `); console.log('โœ… Updated status column to include archived'); } catch (err) { console.warn('โš ๏ธ Could not modify status column:', err.message); } try { await connection.query(` ALTER TABLE user_status MODIFY COLUMN previous_status ENUM('inactive', 'pending', 'active', 'suspended', 'archived') NULL `); console.log('โœ… Updated previous_status column to include archived'); } catch (err) { console.warn('โš ๏ธ Could not modify previous_status column:', err.message); } // --- Authentication & Verification Tables --- // 5. refresh_tokens table: For refresh token authentication await connection.query(` CREATE TABLE IF NOT EXISTS refresh_tokens ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, token VARCHAR(255) UNIQUE NOT NULL, expires_at DATETIME NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, revoked_at TIMESTAMP NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, INDEX idx_user_id (user_id), INDEX idx_expires_at (expires_at) ); `); console.log('โœ… Refresh tokens table created/verified'); // 6. email_verifications table: For email verification codes await connection.query(` CREATE TABLE IF NOT EXISTS email_verifications ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, verification_code VARCHAR(6) NOT NULL, expires_at DATETIME NOT NULL, verified_at TIMESTAMP NULL, attempts INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, INDEX idx_user_code (user_id, verification_code), INDEX idx_expires_at (expires_at) ); `); console.log('โœ… Email verifications table created/verified'); // 7. password_resets table: For password reset tokens await connection.query(` CREATE TABLE IF NOT EXISTS password_resets ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, token VARCHAR(255) UNIQUE NOT NULL, expires_at DATETIME NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, used_at TIMESTAMP NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, INDEX idx_user_token (user_id, token), INDEX idx_expires_at (expires_at) ); `); console.log('โœ… Password resets table created/verified'); // --- Document & Logging Tables --- // 8. user_documents table: Stores object storage IDs await connection.query(` CREATE TABLE IF NOT EXISTS user_documents ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, document_type ENUM('personal_id', 'company_id', 'signature', 'contract', 'other') NOT NULL, object_storage_id VARCHAR(255) UNIQUE NOT NULL, original_filename VARCHAR(255), file_size INT, mime_type VARCHAR(100), upload_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, verified_by_admin BOOLEAN DEFAULT FALSE, admin_verified_at TIMESTAMP NULL, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, INDEX idx_user_document_type (user_id, document_type), INDEX idx_object_storage_id (object_storage_id) ); `); console.log('โœ… User documents table created/verified'); // 8c. document_templates table: Stores template metadata and object storage keys await connection.query(` CREATE TABLE IF NOT EXISTS document_templates ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, type VARCHAR(100) NOT NULL, storageKey VARCHAR(255) NOT NULL, description TEXT, lang VARCHAR(10) NOT NULL, user_type ENUM('personal','company','both') DEFAULT 'both', version INT DEFAULT 1, state ENUM('active','inactive') DEFAULT 'inactive', createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); `); console.log('โœ… Document templates table created/verified'); // 8b. user_id_documents table: Stores ID-specific metadata (front/back object storage IDs) await connection.query(` CREATE TABLE IF NOT EXISTS user_id_documents ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, document_type ENUM('personal_id', 'company_id') NOT NULL, front_object_storage_id VARCHAR(255) NOT NULL, back_object_storage_id VARCHAR(255) NULL, original_filename_front VARCHAR(255), original_filename_back VARCHAR(255), id_type VARCHAR(50), id_number VARCHAR(100), expiry_date DATE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE ); `); console.log('โœ… User ID documents table created/verified'); // 9. user_action_logs table: For detailed user activity logging await connection.query(` CREATE TABLE IF NOT EXISTS user_action_logs ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NULL, affected_user_id INT NULL, action VARCHAR(100) NOT NULL, performed_by_user_id INT NULL, details JSON, ip_address VARCHAR(45), user_agent TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE, -- NEW FK FOREIGN KEY (affected_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE, FOREIGN KEY (performed_by_user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE, INDEX idx_user_id (user_id), -- NEW index INDEX idx_affected_user (affected_user_id), INDEX idx_performed_by (performed_by_user_id), INDEX idx_action (action), INDEX idx_created_at (created_at) ); `); console.log('โœ… User action logs table created/verified'); // --- Add missing user_id column for existing databases + backfill --- try { // Add column if missing await connection.query(`ALTER TABLE user_action_logs ADD COLUMN user_id INT NULL`); console.log('๐Ÿ†• Added user_action_logs.user_id column'); // Add FK if just added try { await connection.query(` ALTER TABLE user_action_logs ADD CONSTRAINT fk_user_action_logs_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE `); console.log('๐Ÿ†• Added FK fk_user_action_logs_user'); } catch (e) { console.log('โ„น๏ธ FK fk_user_action_logs_user already exists or cannot add:', e.message); } // Add index if missing try { await connection.query(`CREATE INDEX idx_user_id ON user_action_logs (user_id)`); console.log('๐Ÿ†• Added index idx_user_id on user_action_logs.user_id'); } catch (e) { console.log('โ„น๏ธ idx_user_id already exists or cannot add:', e.message); } // Backfill: prefer performed_by_user_id, else affected_user_id try { const [res1] = await connection.query(` UPDATE user_action_logs SET user_id = performed_by_user_id WHERE user_id IS NULL AND performed_by_user_id IS NOT NULL `); const [res2] = await connection.query(` UPDATE user_action_logs SET user_id = affected_user_id WHERE user_id IS NULL AND affected_user_id IS NOT NULL `); console.log('๐Ÿงน Backfilled user_action_logs.user_id from performed_by_user_id/affected_user_id'); } catch (e) { console.warn('โš ๏ธ Could not backfill user_action_logs.user_id:', e.message); } } catch (e) { // Column may already exist; ignore if (!/Duplicate column name|exists/i.test(e.message)) { console.log('โ„น๏ธ user_action_logs.user_id add skipped or not required:', e.message); } else { console.log('โ„น๏ธ user_action_logs.user_id already exists'); } } // --- Email & Registration Flow Tables --- // 10. email_attempts table: For tracking email sending attempts await connection.query(` CREATE TABLE IF NOT EXISTS email_attempts ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, attempt_type ENUM('verification', 'password_reset', 'registration_completion', 'notification', 'other') NOT NULL, email_address VARCHAR(255) NOT NULL, success BOOLEAN DEFAULT FALSE, error_message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, INDEX idx_user_attempts (user_id, attempt_type), INDEX idx_created_at (created_at) ); `); console.log('โœ… Email attempts table created/verified'); // --- Referral Tables --- // 12. referral_tokens table: Manages referral codes await connection.query(` CREATE TABLE IF NOT EXISTS referral_tokens ( id INT AUTO_INCREMENT PRIMARY KEY, token VARCHAR(64) UNIQUE NOT NULL, created_by_user_id INT NOT NULL, expires_at DATETIME NOT NULL, max_uses INT DEFAULT -1, uses_remaining INT DEFAULT -1, status ENUM('active', 'inactive', 'expired', 'exhausted') DEFAULT 'active', deactivation_reason VARCHAR(50), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, INDEX idx_token (token), INDEX idx_status_expires (status, expires_at), INDEX idx_created_by (created_by_user_id) ); `); console.log('โœ… Referral tokens table created/verified'); // 13. referral_token_usage table: Tracks each use of a referral token await connection.query(` CREATE TABLE IF NOT EXISTS referral_token_usage ( id INT AUTO_INCREMENT PRIMARY KEY, referral_token_id INT NOT NULL, used_by_user_id INT NOT NULL, used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (referral_token_id) REFERENCES referral_tokens(id) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (used_by_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE KEY unique_token_user_usage (referral_token_id, used_by_user_id), INDEX idx_token_usage (referral_token_id), INDEX idx_user_usage (used_by_user_id) ); `); console.log('โœ… Referral token usage table created/verified'); // --- Authorization Tables --- // 14. permissions table: Defines granular permissions await connection.query(` CREATE TABLE IF NOT EXISTS permissions ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL, description VARCHAR(255), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Added created_by INT NULL, -- Added FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE ); `); console.log('โœ… Permissions table created/verified'); // 15. user_permissions join table: Assigns specific permissions to users await connection.query(` CREATE TABLE IF NOT EXISTS user_permissions ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, permission_id INT NOT NULL, granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, granted_by INT NULL, -- Added column FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE ON UPDATE CASCADE, FOREIGN KEY (granted_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE, -- FK constraint UNIQUE KEY unique_user_permission (user_id, permission_id) ); `); console.log('โœ… User permissions table created/verified'); // --- User Settings Table --- await connection.query(` CREATE TABLE IF NOT EXISTS user_settings ( user_id INT PRIMARY KEY, theme ENUM('light', 'dark') DEFAULT 'light', font_size ENUM('normal', 'large') DEFAULT 'normal', high_contrast_mode BOOLEAN DEFAULT FALSE, two_factor_auth_enabled BOOLEAN DEFAULT FALSE, account_visibility ENUM('public', 'private') DEFAULT 'public', show_email BOOLEAN DEFAULT TRUE, show_phone BOOLEAN DEFAULT TRUE, data_export_requested BOOLEAN DEFAULT FALSE, last_data_export_at TIMESTAMP NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE ); `); console.log('โœ… User settings table created/verified'); // --- Rate Limiting Table --- await connection.query(` CREATE TABLE IF NOT EXISTS rate_limit ( id INT AUTO_INCREMENT PRIMARY KEY, rate_key VARCHAR(255) NOT NULL, window_start DATETIME NOT NULL, count INT DEFAULT 0, window_seconds INT NOT NULL, max INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY unique_key_window (rate_key, window_start) ); `); console.log('โœ… Rate limit table created/verified'); // --- NEW: company_stamps table (for company/admin managed stamps) --- await connection.query(` CREATE TABLE IF NOT EXISTS company_stamps ( id INT AUTO_INCREMENT PRIMARY KEY, company_id INT NOT NULL, label VARCHAR(100) NULL, mime_type VARCHAR(50) NOT NULL, image_base64 LONGTEXT NOT NULL, is_active BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (company_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, UNIQUE KEY unique_company_label (company_id, label), INDEX idx_company_active (company_id, is_active) ); `); console.log('โœ… Company stamps table created/verified'); // --- Coffee / Subscriptions Table (simplified) --- await connection.query(` CREATE TABLE IF NOT EXISTS coffee_table ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, description TEXT NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, currency CHAR(3) NOT NULL DEFAULT 'EUR', is_featured BOOLEAN NOT NULL DEFAULT FALSE, billing_interval ENUM('day','week','month','year') NULL, interval_count INT UNSIGNED NULL, object_storage_id VARCHAR(255) NULL, original_filename VARCHAR(255) NULL, state BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); `); console.log('โœ… Coffee table (simplified) created/verified'); // --- Matrix: Global 5-ary tree config and relations --- await connection.query(` CREATE TABLE IF NOT EXISTS matrix_config ( id TINYINT PRIMARY KEY DEFAULT 1, master_top_user_id INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_matrix_config_master FOREIGN KEY (master_top_user_id) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT chk_matrix_singleton CHECK (id = 1) ); `); console.log('โœ… Matrix config table created/verified'); await connection.query(` CREATE TABLE IF NOT EXISTS user_tree_edges ( id BIGINT AUTO_INCREMENT PRIMARY KEY, parent_user_id INT NOT NULL, child_user_id INT NOT NULL, position TINYINT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_edges_parent FOREIGN KEY (parent_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_edges_child FOREIGN KEY (child_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT uq_child UNIQUE (child_user_id), CONSTRAINT uq_parent_position UNIQUE (parent_user_id, position), CONSTRAINT chk_position CHECK (position BETWEEN 1 AND 5) ); `); console.log('โœ… User tree edges table created/verified'); await connection.query(` CREATE TABLE IF NOT EXISTS user_tree_closure ( ancestor_user_id INT NOT NULL, descendant_user_id INT NOT NULL, depth INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT pk_closure PRIMARY KEY (ancestor_user_id, descendant_user_id), CONSTRAINT fk_closure_ancestor FOREIGN KEY (ancestor_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT fk_closure_descendant FOREIGN KEY (descendant_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT chk_depth_nonneg CHECK (depth >= 0) ); `); console.log('โœ… User tree closure table created/verified'); await connection.query(` CREATE TABLE IF NOT EXISTS user_matrix_metadata ( root_user_id INT PRIMARY KEY, ego_activated_at TIMESTAMP NULL, last_bfs_fill_at TIMESTAMP NULL, immediate_children_count INT DEFAULT 0, first_free_position TINYINT NULL, name VARCHAR(255) NULL, -- NEW: matrix display name is_active BOOLEAN DEFAULT TRUE, -- NEW: activation flag max_depth INT NULL, -- NEW: NULL=unlimited; otherwise enforce per root updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_matrix_meta_root FOREIGN KEY (root_user_id) REFERENCES users(id) ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT chk_first_free_position CHECK (first_free_position IS NULL OR (first_free_position BETWEEN 1 AND 5)) ); `); console.log('โœ… User matrix metadata table created/verified'); // --- Pools (NEW) --- await connection.query(` CREATE TABLE IF NOT EXISTS pools ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT NULL, state ENUM('active','inactive','archived') DEFAULT 'active', created_by INT NOT NULL, updated_by INT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_pool_name (name), INDEX idx_pool_name (name), INDEX idx_pool_state (state), INDEX idx_pool_created_by (created_by), INDEX idx_pool_updated_by (updated_by), CONSTRAINT fk_pools_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE, CONSTRAINT fk_pools_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE ); `); console.log('โœ… Pools table created/verified'); // Migrations for existing databases: add 'archived' and actor columns if missing try { await connection.query(` ALTER TABLE pools MODIFY COLUMN state ENUM('active','inactive','archived') DEFAULT 'active' `); console.log('๐Ÿ†™ Pools.state updated to include archived'); } catch (e) { console.log('โ„น๏ธ Pools.state alteration skipped:', e.message); } try { await connection.query(`ALTER TABLE pools ADD COLUMN created_by INT NOT NULL`); await connection.query(`CREATE INDEX idx_pool_created_by ON pools (created_by)`); await connection.query(` ALTER TABLE pools ADD CONSTRAINT fk_pools_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT ON UPDATE CASCADE `); console.log('๐Ÿ†• Pools.created_by added with FK and index'); } catch (e) { console.log('โ„น๏ธ Pools.created_by add skipped:', e.message); } try { await connection.query(`ALTER TABLE pools ADD COLUMN updated_by INT NULL`); await connection.query(`CREATE INDEX idx_pool_updated_by ON pools (updated_by)`); await connection.query(` ALTER TABLE pools ADD CONSTRAINT fk_pools_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE `); console.log('๐Ÿ†• Pools.updated_by added with FK and index'); } catch (e) { console.log('โ„น๏ธ Pools.updated_by add skipped:', e.message); } console.log('๐ŸŽ‰ Normalized database schema created/updated successfully!'); await connection.end(); return true; } catch (error) { console.error('๐Ÿ’ฅ Error during database initialization:', error.message); if (connection) { await connection.end(); } throw error; } } module.exports = { createDatabase };