483 lines
21 KiB
JavaScript
483 lines
21 KiB
JavaScript
|
||
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); |