const mysql = require('mysql2/promise'); const fs = require('fs').promises; const path = require('path'); let pool = null; let sqliteDb = null; const isSqlite = process.env.DB_TYPE === 'sqlite'; // Helper to rewrite MySQL-specific syntax to SQLite function rewriteQuery(sql) { if (!isSqlite) return sql; let query = sql; // Replace NOW() with datetime('now') query = query.replace(/\bNOW\(\)/gi, "datetime('now')"); // Rewrite ON DUPLICATE KEY UPDATE for users if (query.includes('ON DUPLICATE KEY UPDATE')) { query = `INSERT INTO users (username, email, password_hash, created_at) VALUES (?, ?, ?, datetime('now')) ON CONFLICT(username) DO UPDATE SET email = excluded.email, password_hash = excluded.password_hash`; } return query; } // ================================================================ // INICIALIZAR BANCO DE DADOS // ================================================================ async function initSQLite() { const { DatabaseSync } = require('node:sqlite'); const dbPath = process.env.DB_PATH || path.join(__dirname, 'dental_images.db'); sqliteDb = new DatabaseSync(dbPath); console.log(`✅ Banco de dados SQLite criado/inicializado em: ${dbPath}`); // Criar tabelas para SQLite sqliteDb.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, clinic_name TEXT, pc_name TEXT, is_admin INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); sqliteDb.exec(` CREATE TABLE IF NOT EXISTS images ( id INTEGER PRIMARY KEY AUTOINCREMENT, image_guid TEXT NOT NULL, patient_name TEXT, client_name TEXT, original_filename TEXT, filename TEXT NOT NULL, enabled INTEGER DEFAULT 1, tooth_number TEXT, tooth_side TEXT, patient_name_resumed TEXT, original_image_id INTEGER, rotation INTEGER DEFAULT 0, flip_horizontal INTEGER DEFAULT 0, flip_vertical INTEGER DEFAULT 0, doctor TEXT, remark TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); sqliteDb.exec(` CREATE TABLE IF NOT EXISTS gtos ( id INTEGER PRIMARY KEY AUTOINCREMENT, gto_number TEXT NOT NULL, patient_name TEXT NOT NULL, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); sqliteDb.exec(` CREATE TABLE IF NOT EXISTS gto_images ( id INTEGER PRIMARY KEY AUTOINCREMENT, gto_id INTEGER NOT NULL, image_id INTEGER NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(gto_id) REFERENCES gtos(id), FOREIGN KEY(image_id) REFERENCES images(id) ); `); // Migrações para adicionar colunas se o banco já existia try { sqliteDb.exec("ALTER TABLE images ADD COLUMN doctor TEXT"); } catch (e) { // Coluna já existe } try { sqliteDb.exec("ALTER TABLE images ADD COLUMN remark TEXT"); } catch (e) { // Coluna já existe } console.log('✅ Tabelas SQLite users e images criadas/verificadas'); // Migrações para adicionar colunas em users se o banco já existia try { sqliteDb.exec("ALTER TABLE users ADD COLUMN clinic_name TEXT"); } catch (e) {} try { sqliteDb.exec("ALTER TABLE users ADD COLUMN pc_name TEXT"); } catch (e) {} try { sqliteDb.exec("ALTER TABLE users ADD COLUMN is_admin INTEGER DEFAULT 0"); } catch (e) {} // Criar administrador padrão se não houver usuários try { const stmtCount = sqliteDb.prepare('SELECT COUNT(*) as count FROM users'); const userCount = stmtCount.get().count; if (userCount === 0) { console.log('⚠️ Nenhum usuário encontrado no SQLite. Criando admin padrão (admin / admin1234)...'); const bcrypt = require('bcrypt'); const passwordHash = bcrypt.hashSync('admin1234', 10); const stmtInsert = sqliteDb.prepare(` INSERT INTO users (username, email, password_hash, clinic_name, pc_name, is_admin) VALUES (?, ?, ?, ?, ?, 1) `); stmtInsert.run('admin', 'admin@dental.local', passwordHash, 'Admin Clinic', 'Admin PC'); console.log('✅ Usuário admin padrão criado com sucesso!'); } } catch (error) { console.error('⚠️ Erro ao criar admin padrão:', error.message); } } async function initDatabase() { if (isSqlite) { await initSQLite(); return; } try { const dbConfig = { host: process.env.DB_HOST || 'localhost', port: process.env.DB_PORT || 3306, user: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || '', }; const dbName = process.env.DB_NAME || 'dental_images'; // Criar pool temporário SEM database para poder criar o banco const tempPool = mysql.createPool({ ...dbConfig, waitForConnections: true, connectionLimit: 10, queueLimit: 0 }); console.log('✅ Pool de conexões MySQL criado'); // Verificar se banco existe, se não criar try { const [databases] = await tempPool.query( `SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`, [dbName] ); if (databases.length === 0) { console.log(`⚠️ Banco ${dbName} não encontrado. Criando...`); await tempPool.query(`CREATE DATABASE ${dbName} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`); console.log(`✅ Banco ${dbName} criado!`); } else { console.log(`✅ Banco ${dbName} já existe`); } } catch (error) { console.error('❌ Erro ao verificar/criar banco:', error); throw error; } // Fechar pool temporário await tempPool.end(); // Criar pool definitivo COM database pool = mysql.createPool({ ...dbConfig, database: dbName, waitForConnections: true, connectionLimit: 10, queueLimit: 0, enableKeepAlive: true, keepAliveInitialDelay: 0 }); // Testar conexão const connection = await pool.getConnection(); console.log('✅ Conectado ao MySQL'); connection.release(); // Criar tabelas await createTables(); } catch (error) { console.error('❌ Erro ao conectar ao MySQL:', error.message); console.error('Verifique:'); console.error('1. MySQL está rodando? (sudo systemctl status mysql)'); console.error('2. Credenciais corretas no .env?'); console.error('3. Usuário tem permissão para criar databases?'); throw error; } } // ================================================================ // CRIAR TABELAS MYSQL // ================================================================ async function createTables() { const connection = await pool.getConnection(); try { // Tabela de usuários await connection.query(` CREATE TABLE IF NOT EXISTS users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password_hash TEXT NOT NULL, clinic_name VARCHAR(100) NULL, pc_name VARCHAR(50) NULL, is_admin TINYINT(1) DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); console.log('✅ Tabela users criada/verificada'); // Tabela de imagens await connection.query(` CREATE TABLE IF NOT EXISTS images ( id INT AUTO_INCREMENT PRIMARY KEY, image_guid VARCHAR(255) NOT NULL, patient_name TEXT, client_name VARCHAR(100), original_filename TEXT, filename TEXT NOT NULL, enabled TINYINT(1) DEFAULT 1, tooth_number VARCHAR(10), tooth_side VARCHAR(10), patient_name_resumed VARCHAR(50), original_image_id INT, rotation INT DEFAULT 0, flip_horizontal INT DEFAULT 0, flip_vertical INT DEFAULT 0, doctor VARCHAR(255) NULL, remark TEXT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_images_enabled (enabled), INDEX idx_images_original (original_image_id), INDEX idx_patient_name (patient_name(100)), INDEX idx_image_guid (image_guid), INDEX idx_client_name (client_name), INDEX idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); console.log('✅ Tabela images criada/verificada'); // Tabela GTOs await connection.query(` CREATE TABLE IF NOT EXISTS gtos ( id INT AUTO_INCREMENT PRIMARY KEY, gto_number VARCHAR(100) NOT NULL, patient_name VARCHAR(255) NOT NULL, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_gtos_patient (patient_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); console.log('✅ Tabela gtos criada/verificada'); // Tabela de vínculo GTO-Images await connection.query(` CREATE TABLE IF NOT EXISTS gto_images ( id INT AUTO_INCREMENT PRIMARY KEY, gto_id INT NOT NULL, image_id INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (gto_id) REFERENCES gtos(id) ON DELETE CASCADE, FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE, UNIQUE INDEX idx_gto_image (gto_id, image_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `); console.log('✅ Tabela gto_images criada/verificada'); // Verificar e adicionar colunas se não existirem (migração) try { const columns = await connection.query(` SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'images' AND COLUMN_NAME = 'client_name' `); if (columns[0].length === 0) { console.log('⚠️ Coluna client_name não encontrada. Adicionando...'); await connection.query(` ALTER TABLE images ADD COLUMN client_name VARCHAR(100) NULL AFTER patient_name `); await connection.query(` ALTER TABLE images ADD INDEX idx_client_name (client_name) `); console.log('✅ Coluna client_name adicionada'); } } catch (error) { console.warn('⚠️ Erro ao verificar/adicionar coluna client_name:', error.message); } // Migração para coluna doctor try { const columnsDoctor = await connection.query(` SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'images' AND COLUMN_NAME = 'doctor' `); if (columnsDoctor[0].length === 0) { console.log('⚠️ Coluna doctor não encontrada. Adicionando...'); await connection.query(` ALTER TABLE images ADD COLUMN doctor VARCHAR(255) NULL AFTER client_name `); } } catch (error) { console.warn('⚠️ Erro ao verificar/adicionar coluna doctor:', error.message); } // Migração para coluna remark try { const columnsRemark = await connection.query(` SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'images' AND COLUMN_NAME = 'remark' `); if (columnsRemark[0].length === 0) { console.log('⚠️ Coluna remark não encontrada. Adicionando...'); await connection.query(` ALTER TABLE images ADD COLUMN remark TEXT NULL AFTER doctor `); } } catch (error) { console.warn('⚠️ Erro ao verificar/adicionar coluna remark:', error.message); } // Migração para colunas da tabela users (clinic_name, pc_name, is_admin) try { const columnsUsers = await connection.query(` SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'clinic_name' `); if (columnsUsers[0].length === 0) { console.log('⚠️ Colunas de clínica em users não encontradas. Adicionando...'); await connection.query(` ALTER TABLE users ADD COLUMN clinic_name VARCHAR(100) NULL AFTER password_hash, ADD COLUMN pc_name VARCHAR(50) NULL AFTER clinic_name, ADD COLUMN is_admin TINYINT(1) DEFAULT 0 AFTER pc_name `); // O primeiro usuário (admin padrão) deve virar admin se já existe await connection.query(`UPDATE users SET is_admin = 1 ORDER BY id ASC LIMIT 1`); } } catch (error) { console.warn('⚠️ Erro ao verificar/adicionar colunas de clínica em users:', error.message); } } catch (error) { console.error('Erro ao criar tabelas:', error); } finally { connection.release(); } } // ================================================================ // FUNÇÕES DE CONSULTA // ================================================================ function get(query, params = []) { if (isSqlite) { return new Promise((resolve, reject) => { try { const stmt = sqliteDb.prepare(rewriteQuery(query)); const row = stmt.get(...params); resolve(row || null); } catch (error) { reject(error); } }); } return new Promise(async (resolve, reject) => { try { const [rows] = await pool.query(query, params); resolve(rows[0] || null); } catch (error) { reject(error); } }); } function all(query, params = []) { if (isSqlite) { return new Promise((resolve, reject) => { try { const stmt = sqliteDb.prepare(rewriteQuery(query)); const rows = stmt.all(...params); resolve(rows); } catch (error) { reject(error); } }); } return new Promise(async (resolve, reject) => { try { const [rows] = await pool.query(query, params); resolve(rows); } catch (error) { reject(error); } }); } function run(query, params = []) { if (isSqlite) { return new Promise((resolve, reject) => { try { const stmt = sqliteDb.prepare(rewriteQuery(query)); const result = stmt.run(...params); resolve({ lastID: result.lastInsertRowid, changes: result.changes }); } catch (error) { reject(error); } }); } return new Promise(async (resolve, reject) => { try { const [result] = await pool.query(query, params); resolve({ lastID: result.insertId, changes: result.affectedRows }); } catch (error) { reject(error); } }); } // ================================================================ // EXPORT // ================================================================ module.exports = { initDatabase, get, all, run, get db() { return isSqlite ? sqliteDb : pool; } };