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), -- Added city column country VARCHAR(100), phone_secondary VARCHAR(255), emergency_contact_name VARCHAR(255), emergency_contact_phone VARCHAR(255), account_holder_name VARCHAR(255), -- Added column 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 updated'); // 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, -- allow NULL phone VARCHAR(255) NULL, address TEXT, zip_code VARCHAR(20), city VARCHAR(100), country VARCHAR(100), -- Added country column after city 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), -- Added column 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 updated'); // Ensure registration_number allows NULL if table already existed try { await connection.query(`ALTER TABLE company_profiles MODIFY COLUMN registration_number VARCHAR(255) UNIQUE NULL`); console.log('๐Ÿ”ง Ensured registration_number allows NULL'); } catch (e) { console.log('โ„น๏ธ registration_number column already allows NULL or ALTER not required'); } // Ensure phone columns are nullable if tables already existed try { await connection.query(`ALTER TABLE personal_profiles MODIFY COLUMN phone VARCHAR(255) NULL`); await connection.query(`ALTER TABLE company_profiles MODIFY COLUMN phone VARCHAR(255) NULL`); console.log('๐Ÿ”ง Ensured phone columns are nullable'); } catch (e) { console.log('โ„น๏ธ Phone columns already nullable or ALTER not required'); } // 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', -- NEW COLUMN version INT DEFAULT 1, state ENUM('active','inactive') DEFAULT 'inactive', -- Added state column createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); `); console.log('โœ… Document templates table created/verified'); // Ensure version column exists if table already existed try { await connection.query(`ALTER TABLE document_templates ADD COLUMN version INT DEFAULT 1`); console.log('๐Ÿ”ง Ensured version column exists'); } catch (e) { console.log('โ„น๏ธ Version column already exists or ALTER not required'); } // Ensure state column exists if table already existed try { await connection.query(`ALTER TABLE document_templates ADD COLUMN state ENUM('active','inactive') DEFAULT 'inactive'`); console.log('๐Ÿ”ง Ensured state column exists'); } catch (e) { console.log('โ„น๏ธ State column already exists or ALTER not required'); } // Ensure user_type column exists try { await connection.query(`ALTER TABLE document_templates ADD COLUMN user_type ENUM('personal','company','both') DEFAULT 'both'`); console.log('๐Ÿ”ง Ensured user_type column exists'); } catch (e) { console.log('โ„น๏ธ user_type column already exists or ALTER not required'); } // 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), -- NEW COLUMN original_filename_back VARCHAR(255), -- NEW COLUMN 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, 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 (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_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'); // --- 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'); // Generated label columns (virtual) to display 'unlimited' instead of -1 in UI tools try { await connection.query(` ALTER TABLE referral_tokens ADD COLUMN max_uses_label VARCHAR(20) GENERATED ALWAYS AS (CASE WHEN max_uses = -1 THEN 'unlimited' ELSE CAST(max_uses AS CHAR) END) VIRTUAL `); console.log('๐Ÿ†• Added virtual column referral_tokens.max_uses_label'); } catch (e) { console.log('โ„น๏ธ max_uses_label already exists or cannot add:', e.message); } try { await connection.query(` ALTER TABLE referral_tokens ADD COLUMN uses_remaining_label VARCHAR(20) GENERATED ALWAYS AS (CASE WHEN uses_remaining = -1 THEN 'unlimited' ELSE CAST(uses_remaining AS CHAR) END) VIRTUAL `); console.log('๐Ÿ†• Added virtual column referral_tokens.uses_remaining_label'); } catch (e) { console.log('โ„น๏ธ uses_remaining_label already exists or cannot add:', e.message); } // Normalized view now sources the generated label columns (still handy for joins) try { await connection.query(` CREATE OR REPLACE VIEW referral_tokens_normalized AS SELECT rt.*, rt.max_uses_label AS max_uses_display, rt.uses_remaining_label AS uses_remaining_display FROM referral_tokens rt; `); console.log('๐Ÿ†• referral_tokens_normalized view created/updated'); } catch (e) { console.warn('โš ๏ธ Could not create referral_tokens_normalized view:', e.message); } // 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 --- 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', tax_rate DECIMAL(5,2) NULL, is_featured BOOLEAN NOT NULL DEFAULT FALSE, billing_interval ENUM('day','week','month','year') NULL, interval_count INT UNSIGNED NULL, sku VARCHAR(100) NULL, slug VARCHAR(200) NULL, object_storage_id VARCHAR(255) NULL, original_filename VARCHAR(255) NULL, state BOOLEAN NOT NULL DEFAULT TRUE, -- available=true, unavailable=false created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uq_slug (slug) ); `); console.log('โœ… Coffee table created/verified'); // Ensure quantity column is removed for existing databases try { await connection.query(`ALTER TABLE coffee_table DROP COLUMN IF EXISTS quantity`); console.log('๐Ÿ”ง Removed coffee_table.quantity'); } catch (e) { console.log('โ„น๏ธ coffee_table.quantity already removed or ALTER not required'); } // --- 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'); // Ensure new columns exist if table already existed try { await connection.query(`ALTER TABLE user_matrix_metadata ADD COLUMN name VARCHAR(255) NULL`); console.log('๐Ÿ”ง Ensured user_matrix_metadata.name exists'); } catch (e) { console.log('โ„น๏ธ user_matrix_metadata.name already exists or ALTER not required'); } try { await connection.query(`ALTER TABLE user_matrix_metadata ADD COLUMN is_active BOOLEAN DEFAULT TRUE`); console.log('๐Ÿ”ง Ensured user_matrix_metadata.is_active exists'); } catch (e) { console.log('โ„น๏ธ user_matrix_metadata.is_active already exists or ALTER not required'); } // NEW: ensure max_depth column exists try { await connection.query(`ALTER TABLE user_matrix_metadata ADD COLUMN max_depth INT NULL`); console.log('๐Ÿ”ง Ensured user_matrix_metadata.max_depth exists'); } catch (e) { console.log('โ„น๏ธ user_matrix_metadata.max_depth already exists or ALTER not required'); } // NEW: backfill max_depth policy try { // Master top node gets unlimited depth (NULL) await connection.query(` UPDATE user_matrix_metadata SET max_depth = NULL WHERE root_user_id IN (SELECT master_top_user_id FROM matrix_config) `); // All other matrices default to depth 5 where NULL await connection.query(` UPDATE user_matrix_metadata SET max_depth = 5 WHERE max_depth IS NULL AND root_user_id NOT IN (SELECT master_top_user_id FROM matrix_config) `); console.log('๐Ÿงน Backfilled user_matrix_metadata.max_depth (master=NULL, others=5)'); } catch (e) { console.warn('โš ๏ธ Could not backfill user_matrix_metadata.max_depth:', e.message); } // --- Added Index Optimization Section --- try { // Core / status await ensureIndex(connection, 'users', 'idx_users_created_at', 'created_at'); await ensureIndex(connection, 'user_status', 'idx_user_status_status', 'status'); await ensureIndex(connection, 'user_status', 'idx_user_status_registration_completed', 'registration_completed'); // Tokens & auth await ensureIndex(connection, 'refresh_tokens', 'idx_refresh_user_expires', 'user_id, expires_at'); await ensureIndex(connection, 'email_verifications', 'idx_email_verifications_user_expires', 'user_id, expires_at'); await ensureIndex(connection, 'password_resets', 'idx_password_resets_user_expires', 'user_id, expires_at'); // Documents await ensureIndex(connection, 'user_documents', 'idx_user_documents_upload_at', 'upload_at'); await ensureIndex(connection, 'user_documents', 'idx_user_documents_verified', 'verified_by_admin'); await ensureIndex(connection, 'user_id_documents', 'idx_user_id_docs_user_type', 'user_id, document_type'); // Activity logs (composite for common filtered ordering) await ensureIndex(connection, 'user_action_logs', 'idx_user_action_logs_action_created', 'action, created_at'); // Referrals await ensureIndex(connection, 'referral_token_usage', 'idx_referral_token_usage_used_at', 'used_at'); // Permissions await ensureIndex(connection, 'permissions', 'idx_permissions_is_active', 'is_active'); await ensureIndex(connection, 'user_permissions', 'idx_user_permissions_granted_by', 'granted_by'); // Settings await ensureIndex(connection, 'user_settings', 'idx_user_settings_theme', 'theme'); // Rate limit (for queries only on rate_key) await ensureIndex(connection, 'rate_limit', 'idx_rate_limit_rate_key', 'rate_key'); await ensureIndex(connection, 'document_templates', 'idx_document_templates_user_type', 'user_type'); await ensureIndex(connection, 'document_templates', 'idx_document_templates_state_user_type', 'state, user_type'); // NEW composite index await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_company', 'company_id'); await ensureIndex(connection, 'company_stamps', 'idx_company_stamps_active', 'is_active'); // Coffee products await ensureIndex(connection, 'coffee_table', 'idx_coffee_state', 'state'); await ensureIndex(connection, 'coffee_table', 'idx_coffee_updated_at', 'updated_at'); await ensureIndex(connection, 'coffee_table', 'idx_coffee_billing', 'billing_interval, interval_count'); await ensureIndex(connection, 'coffee_table', 'idx_coffee_sku', 'sku'); // Matrix indexes await ensureIndex(connection, 'user_tree_edges', 'idx_user_tree_edges_parent', 'parent_user_id'); // child_user_id already has a UNIQUE constraint; extra index not needed await ensureIndex(connection, 'user_tree_closure', 'idx_user_tree_closure_ancestor_depth', 'ancestor_user_id, depth'); await ensureIndex(connection, 'user_tree_closure', 'idx_user_tree_closure_descendant', 'descendant_user_id'); await ensureIndex(connection, 'user_matrix_metadata', 'idx_user_matrix_is_active', 'is_active'); // NEW await ensureIndex(connection, 'user_matrix_metadata', 'idx_user_matrix_max_depth', 'max_depth'); // NEW console.log('๐Ÿš€ Performance indexes created/verified'); } catch (e) { console.warn('โš ๏ธ Index optimization phase encountered an issue:', 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 };