const { Pool } = require('pg'); 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')"); return query; } // Convert MySQL ? to Postgres $1, $2 function convertSqlForPg(sql) { let i = 1; return sql.replace(/\?/g, () => '$' + (i++)); } // ================================================================ // INICIALIZAR BANCO DE DADOS // ================================================================ async function initSQLite() { const { DatabaseSync } = require('node:sqlite'); const dbPath = 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, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); `); sqliteDb.exec(` CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); `); console.log('✅ Tabela settings SQLite criada/verificada'); 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, thumb_filename 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) {} try { sqliteDb.exec("ALTER TABLE images ADD COLUMN remark TEXT"); } catch (e) {} try { sqliteDb.exec("ALTER TABLE images ADD COLUMN thumb_filename TEXT"); } catch (e) {} console.log('✅ Tabelas SQLite users e images criadas/verificadas'); 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) VALUES (?, ?, ?) `); stmtInsert.run('admin', 'admin@dental.local', passwordHash); 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 || 5432, user: process.env.DB_USER || 'postgres', password: process.env.DB_PASSWORD || '', }; const dbName = process.env.DB_NAME || 'dental_images'; const tempPool = new Pool({ ...dbConfig, database: 'postgres', }); try { const res = await tempPool.query(`SELECT 1 FROM pg_database WHERE datname = $1`, [dbName]); if (res.rowCount === 0) { console.log(`⚠️ Banco ${dbName} não encontrado. Criando...`); await tempPool.query(`CREATE DATABASE "${dbName}"`); console.log(`✅ Banco ${dbName} criado!`); } else { console.log(`✅ Banco ${dbName} já existe`); } } catch (error) { console.error('❌ Erro ao verificar/criar banco no PostgreSQL:', error.message); } finally { await tempPool.end(); } pool = new Pool({ ...dbConfig, database: dbName, }); const client = await pool.connect(); console.log('✅ Conectado ao PostgreSQL'); client.release(); await createTables(); } catch (error) { console.error('❌ Erro ao conectar ao PostgreSQL:', error.message); throw error; } } // ================================================================ // CRIAR TABELAS POSTGRESQL // ================================================================ async function createTables() { const client = await pool.connect(); try { // Tabela de usuários await client.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, email VARCHAR(100) UNIQUE NOT NULL, password_hash TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); // Tabela de configurações await client.query(` CREATE TABLE IF NOT EXISTS settings ( key VARCHAR(100) PRIMARY KEY, value TEXT NOT NULL ) `); console.log('✅ Tabela settings PostgreSQL criada/verificada'); console.log('✅ Tabela users criada/verificada'); // Tabela de imagens await client.query(` CREATE TABLE IF NOT EXISTS images ( id SERIAL PRIMARY KEY, image_guid VARCHAR(255) NOT NULL, patient_name TEXT, client_name VARCHAR(100), original_filename TEXT, filename TEXT NOT NULL, enabled SMALLINT 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 SMALLINT DEFAULT 0, flip_vertical SMALLINT DEFAULT 0, doctor VARCHAR(255) NULL, remark TEXT NULL, thumb_filename VARCHAR(500) NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(`CREATE INDEX IF NOT EXISTS idx_images_enabled ON images (enabled)`); await client.query(`CREATE INDEX IF NOT EXISTS idx_images_original ON images (original_image_id)`); await client.query(`CREATE INDEX IF NOT EXISTS idx_patient_name ON images (patient_name)`); await client.query(`CREATE INDEX IF NOT EXISTS idx_image_guid ON images (image_guid)`); await client.query(`CREATE INDEX IF NOT EXISTS idx_client_name ON images (client_name)`); await client.query(`CREATE INDEX IF NOT EXISTS idx_created_at ON images (created_at)`); console.log('✅ Tabela images criada/verificada'); // Tabela GTOs await client.query(` CREATE TABLE IF NOT EXISTS gtos ( id SERIAL PRIMARY KEY, gto_number VARCHAR(100) NOT NULL, patient_name VARCHAR(255) NOT NULL, description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(`CREATE INDEX IF NOT EXISTS idx_gtos_patient ON gtos (patient_name)`); console.log('✅ Tabela gtos criada/verificada'); // Tabela de vínculo GTO-Images await client.query(` CREATE TABLE IF NOT EXISTS gto_images ( id SERIAL PRIMARY KEY, gto_id INT NOT NULL, image_id INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (gto_id) REFERENCES gtos(id) ON DELETE CASCADE, FOREIGN KEY (image_id) REFERENCES images(id) ON DELETE CASCADE ) `); await client.query(`CREATE UNIQUE INDEX IF NOT EXISTS idx_gto_image ON gto_images (gto_id, image_id)`); console.log('✅ Tabela gto_images criada/verificada'); try { await client.query(`ALTER TABLE images ADD COLUMN client_name VARCHAR(100)`); await client.query(`CREATE INDEX idx_client_name ON images (client_name)`); console.log('✅ Coluna client_name adicionada'); } catch (e) { /* já existe */ } try { await client.query(`ALTER TABLE images ADD COLUMN doctor VARCHAR(255)`); } catch (e) { /* já existe */ } try { await client.query(`ALTER TABLE images ADD COLUMN remark TEXT`); } catch (e) { /* já existe */ } try { await client.query(`ALTER TABLE images ADD COLUMN thumb_filename VARCHAR(500)`); console.log('✅ Coluna thumb_filename adicionada'); } catch (e) { /* já existe */ } // Inserir usuários padrões se a tabela estiver vazia try { const userCountRes = await client.query('SELECT COUNT(*) as count FROM users'); const userCount = parseInt(userCountRes.rows[0].count, 10); if (userCount === 0) { console.log('⚠️ Nenhum usuário encontrado no PostgreSQL. Criando usuários padrões...'); const bcrypt = require('bcrypt'); const hash1 = await bcrypt.hash('Rc362514', 10); const hash2 = await bcrypt.hash('admin1234', 10); await client.query( `INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3)`, ['rcesar', 'rcesar@rcesar.com', hash1] ); await client.query( `INSERT INTO users (username, email, password_hash) VALUES ($1, $2, $3)`, ['admin', 'admin@dental.local', hash2] ); console.log('✅ Usuários padrões (rcesar e admin) criados com sucesso no PostgreSQL!'); } } catch (e) { console.error('⚠️ Erro ao inicializar usuários padrões no PostgreSQL:', e.message); } } catch (error) { console.error('Erro ao criar tabelas:', error); } finally { client.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 pgQuery = convertSqlForPg(query); const res = await pool.query(pgQuery, params); resolve(res.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 pgQuery = convertSqlForPg(query); const res = await pool.query(pgQuery, params); resolve(res.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 { let pgQuery = convertSqlForPg(query); // Auto-append RETURNING id for INSERT if not present (except for settings table) if (pgQuery.trim().toUpperCase().startsWith('INSERT') && !pgQuery.toUpperCase().includes('RETURNING') && !pgQuery.toUpperCase().includes('SETTINGS')) { pgQuery += ' RETURNING id'; } const res = await pool.query(pgQuery, params); let lastID = null; if (res.rows && res.rows.length > 0 && res.rows[0].id) { lastID = res.rows[0].id; } resolve({ lastID: lastID, changes: res.rowCount }); } catch (error) { reject(error); } }); } // ================================================================ // EXPORT // ================================================================ module.exports = { initDatabase, get, all, run, get db() { return isSqlite ? sqliteDb : pool; } };