CentralBackend/database/createDb.js
2025-09-07 12:44:01 +02:00

599 lines
28 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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') DEFAULT 'pending',
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');
// --- 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');
// --- 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');
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 };