const sqlite3 = require('sqlite3').verbose(); const { Pool } = require('pg'); const path = require('path'); const fs = require('fs'); require('dotenv').config(); async function migrateToPostgres() { console.log('🚀 Начинаем миграцию данных из SQLite в PostgreSQL...'); // Проверяем существование SQLite базы const sqlitePath = path.join(__dirname, 'data', 'school_crm.db'); if (!fs.existsSync(sqlitePath)) { console.error('❌ Файл SQLite базы не найден:', sqlitePath); process.exit(1); } // Подключаемся к SQLite const sqliteDb = new sqlite3.Database(sqlitePath, (err) => { if (err) { console.error('❌ Ошибка подключения к SQLite:', err.message); process.exit(1); } }); console.log('✅ SQLite база найдена и подключена'); // Проверяем настройки PostgreSQL if (!process.env.DB_HOST || !process.env.DB_USER || !process.env.DB_PASSWORD) { console.error('❌ Настройки PostgreSQL не указаны в .env файле'); console.error(' Укажите DB_HOST, DB_USER, DB_PASSWORD'); process.exit(1); } // Подключаемся к PostgreSQL const pgPool = new Pool({ host: process.env.DB_HOST, port: process.env.DB_PORT || 5432, database: process.env.DB_NAME || 'minicrm', user: process.env.DB_USER, password: process.env.DB_PASSWORD, max: 5, idleTimeoutMillis: 30000, connectionTimeoutMillis: 10000, }); let client; try { console.log('🔌 Подключаемся к PostgreSQL...'); client = await pgPool.connect(); console.log('✅ Подключение к PostgreSQL установлено'); // Создаем таблицы в PostgreSQL если их нет console.log('🔧 Создаем/проверяем таблицы в PostgreSQL...'); await createPostgresTables(client); // Отключаем foreign key constraints для упрощения миграции await client.query('SET session_replication_role = replica;'); // Мигрируем таблицу users console.log('📦 Мигрируем таблицу users...'); const users = await new Promise((resolve, reject) => { sqliteDb.all('SELECT * FROM users ORDER BY id', [], (err, rows) => { if (err) reject(err); else resolve(rows); }); }); if (users.length > 0) { let migratedUsers = 0; for (const user of users) { try { await client.query(` INSERT INTO users (id, login, password, name, email, role, auth_type, groups, description, created_at, last_login, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT (id) DO UPDATE SET login = EXCLUDED.login, name = EXCLUDED.name, email = EXCLUDED.email, role = EXCLUDED.role, auth_type = EXCLUDED.auth_type, groups = EXCLUDED.groups, description = EXCLUDED.description, last_login = EXCLUDED.last_login, updated_at = EXCLUDED.updated_at `, [ user.id, user.login, user.password || null, user.name, user.email, user.role || 'teacher', user.auth_type || 'local', user.groups || '[]', user.description || '', user.created_at, user.last_login, user.updated_at || user.created_at ]); migratedUsers++; } catch (error) { console.error(` Ошибка при миграции пользователя ${user.id}:`, error.message); } } console.log(`✅ Мигрировано ${migratedUsers} из ${users.length} пользователей`); } else { console.log('ℹ️ В таблице users нет данных для миграции'); } // Мигрируем таблицу tasks console.log('📦 Мигрируем таблицу tasks...'); const tasks = await new Promise((resolve, reject) => { sqliteDb.all('SELECT * FROM tasks ORDER BY id', [], (err, rows) => { if (err) reject(err); else resolve(rows); }); }); if (tasks.length > 0) { let migratedTasks = 0; for (const task of tasks) { try { await client.query(` INSERT INTO tasks (id, title, description, status, created_by, created_at, updated_at, deleted_at, deleted_by, original_task_id, start_date, due_date, rework_comment, closed_at, closed_by) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title, description = EXCLUDED.description, status = EXCLUDED.status, created_by = EXCLUDED.created_by, updated_at = EXCLUDED.updated_at, deleted_at = EXCLUDED.deleted_at, deleted_by = EXCLUDED.deleted_by, original_task_id = EXCLUDED.original_task_id, start_date = EXCLUDED.start_date, due_date = EXCLUDED.due_date, rework_comment = EXCLUDED.rework_comment, closed_at = EXCLUDED.closed_at, closed_by = EXCLUDED.closed_by `, [ task.id, task.title, task.description || '', task.status || 'active', task.created_by, task.created_at, task.updated_at || task.created_at, task.deleted_at, task.deleted_by, task.original_task_id, task.start_date, task.due_date, task.rework_comment, task.closed_at, task.closed_by ]); migratedTasks++; } catch (error) { console.error(` Ошибка при миграции задачи ${task.id}:`, error.message); } } console.log(`✅ Мигрировано ${migratedTasks} из ${tasks.length} задач`); } else { console.log('ℹ️ В таблице tasks нет данных для миграции'); } // Мигрируем таблицу task_assignments console.log('📦 Мигрируем таблицу task_assignments...'); const assignments = await new Promise((resolve, reject) => { sqliteDb.all('SELECT * FROM task_assignments ORDER BY id', [], (err, rows) => { if (err) reject(err); else resolve(rows); }); }); if (assignments.length > 0) { let migratedAssignments = 0; for (const assignment of assignments) { try { await client.query(` INSERT INTO task_assignments (id, task_id, user_id, status, start_date, due_date, rework_comment, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) ON CONFLICT (id) DO UPDATE SET task_id = EXCLUDED.task_id, user_id = EXCLUDED.user_id, status = EXCLUDED.status, start_date = EXCLUDED.start_date, due_date = EXCLUDED.due_date, rework_comment = EXCLUDED.rework_comment, updated_at = EXCLUDED.updated_at `, [ assignment.id, assignment.task_id, assignment.user_id, assignment.status || 'assigned', assignment.start_date, assignment.due_date, assignment.rework_comment, assignment.created_at, assignment.updated_at || assignment.created_at ]); migratedAssignments++; } catch (error) { console.error(` Ошибка при миграции назначения ${assignment.id}:`, error.message); } } console.log(`✅ Мигрировано ${migratedAssignments} из ${assignments.length} назначений`); } else { console.log('ℹ️ В таблице task_assignments нет данных для миграции'); } // Мигрируем таблицу task_files console.log('📦 Мигрируем таблицу task_files...'); const files = await new Promise((resolve, reject) => { sqliteDb.all('SELECT * FROM task_files ORDER BY id', [], (err, rows) => { if (err) reject(err); else resolve(rows); }); }); if (files.length > 0) { let migratedFiles = 0; for (const file of files) { try { await client.query(` INSERT INTO task_files (id, task_id, user_id, filename, original_name, file_path, file_size, uploaded_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (id) DO UPDATE SET task_id = EXCLUDED.task_id, user_id = EXCLUDED.user_id, filename = EXCLUDED.filename, original_name = EXCLUDED.original_name, file_path = EXCLUDED.file_path, file_size = EXCLUDED.file_size `, [ file.id, file.task_id, file.user_id, file.filename, file.original_name, file.file_path, file.file_size, file.uploaded_at ]); migratedFiles++; } catch (error) { console.error(` Ошибка при миграции файла ${file.id}:`, error.message); } } console.log(`✅ Мигрировано ${migratedFiles} из ${files.length} файлов`); } else { console.log('ℹ️ В таблице task_files нет данных для миграции'); } // Мигрируем таблицу activity_logs console.log('📦 Мигрируем таблицу activity_logs...'); const logs = await new Promise((resolve, reject) => { sqliteDb.all('SELECT * FROM activity_logs ORDER BY id', [], (err, rows) => { if (err) reject(err); else resolve(rows); }); }); if (logs.length > 0) { let migratedLogs = 0; for (const log of logs) { try { await client.query(` INSERT INTO activity_logs (id, task_id, user_id, action, details, created_at) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET task_id = EXCLUDED.task_id, user_id = EXCLUDED.user_id, action = EXCLUDED.action, details = EXCLUDED.details `, [ log.id, log.task_id, log.user_id, log.action, log.details || '', log.created_at ]); migratedLogs++; } catch (error) { console.error(` Ошибка при миграции лога ${log.id}:`, error.message); } } console.log(`✅ Мигрировано ${migratedLogs} из ${logs.length} логов активности`); } else { console.log('ℹ️ В таблице activity_logs нет данных для миграции'); } // Мигрируем таблицу notification_logs console.log('📦 Мигрируем таблицу notification_logs...'); const notifications = await new Promise((resolve, reject) => { sqliteDb.all('SELECT * FROM notification_logs ORDER BY id', [], (err, rows) => { if (err) reject(err); else resolve(rows); }); }); if (notifications.length > 0) { let migratedNotifications = 0; for (const notification of notifications) { try { await client.query(` INSERT INTO notification_logs (id, notification_key, created_at) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET notification_key = EXCLUDED.notification_key `, [ notification.id, notification.notification_key, notification.created_at ]); migratedNotifications++; } catch (error) { console.error(` Ошибка при миграции уведомления ${notification.id}:`, error.message); } } console.log(`✅ Мигрировано ${migratedNotifications} из ${notifications.length} логов уведомлений`); } else { console.log('ℹ️ В таблице notification_logs нет данных для миграции'); } // Включаем foreign key constraints обратно await client.query('SET session_replication_role = DEFAULT;'); // Обновляем последовательности await updateSequences(client); client.release(); sqliteDb.close(); console.log('\n🎉 Миграция успешно завершена!'); console.log('📊 Сводка:'); console.log(` 👥 Пользователи: ${users.length}`); console.log(` 📋 Задачи: ${tasks.length}`); console.log(` 👤 Назначения: ${assignments.length}`); console.log(` 📁 Файлы: ${files.length}`); console.log(` 📝 Логи активности: ${logs.length}`); console.log(` 🔔 Логи уведомлений: ${notifications.length}`); console.log('\n⚠️ Для переключения на PostgreSQL выполните следующие действия:'); console.log(' 1. Откройте файл .env'); console.log(' 2. Измените POSTGRESQL=no на POSTGRESQL=yes'); console.log(' 3. Перезапустите сервер командой: npm start'); } catch (error) { console.error('❌ Ошибка миграции:', error.message); if (client) client.release(); sqliteDb.close(); process.exit(1); } finally { await pgPool.end(); } } async function createPostgresTables(client) { // Создаем таблицы PostgreSQL await client.query(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, login VARCHAR(100) UNIQUE NOT NULL, password TEXT, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, role VARCHAR(50) DEFAULT 'teacher', auth_type VARCHAR(50) DEFAULT 'local', groups TEXT DEFAULT '[]', description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_login TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(` CREATE TABLE IF NOT EXISTS tasks ( id SERIAL PRIMARY KEY, title VARCHAR(500) NOT NULL, description TEXT, status VARCHAR(50) DEFAULT 'active', created_by INTEGER NOT NULL REFERENCES users(id), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, deleted_at TIMESTAMP, deleted_by INTEGER REFERENCES users(id), original_task_id INTEGER REFERENCES tasks(id), start_date TIMESTAMP, due_date TIMESTAMP, rework_comment TEXT, closed_at TIMESTAMP, closed_by INTEGER REFERENCES users(id) ) `); await client.query(` CREATE TABLE IF NOT EXISTS task_assignments ( id SERIAL PRIMARY KEY, task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id), status VARCHAR(50) DEFAULT 'assigned', start_date TIMESTAMP, due_date TIMESTAMP, rework_comment TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(` CREATE TABLE IF NOT EXISTS task_files ( id SERIAL PRIMARY KEY, task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id), filename VARCHAR(255) NOT NULL, original_name VARCHAR(500) NOT NULL, file_path TEXT NOT NULL, file_size BIGINT NOT NULL, uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(` CREATE TABLE IF NOT EXISTS activity_logs ( id SERIAL PRIMARY KEY, task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, user_id INTEGER NOT NULL REFERENCES users(id), action VARCHAR(100) NOT NULL, details TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); await client.query(` CREATE TABLE IF NOT EXISTS notification_logs ( id SERIAL PRIMARY KEY, notification_key VARCHAR(500) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) `); console.log('✅ Таблицы PostgreSQL созданы/проверены'); } async function updateSequences(client) { // Обновляем последовательности для автоинкремента const tables = [ { name: 'users', id: 'id' }, { name: 'tasks', id: 'id' }, { name: 'task_assignments', id: 'id' }, { name: 'task_files', id: 'id' }, { name: 'activity_logs', id: 'id' }, { name: 'notification_logs', id: 'id' } ]; for (const table of tables) { try { const result = await client.query(` SELECT MAX(${table.id}) as max_id FROM ${table.name} `); const maxId = result.rows[0].max_id || 0; if (maxId > 0) { await client.query(` SELECT setval(pg_get_serial_sequence('${table.name}', '${table.id}'), ${maxId}, true) `); console.log(`🔢 Последовательность для ${table.name} обновлена до ${maxId}`); } } catch (error) { console.warn(`⚠️ Не удалось обновить последовательность для ${table.name}: ${error.message}`); } } } // Запускаем миграцию migrateToPostgres().catch(console.error);