Files
minicrm/migrate.js
2025-12-21 20:42:12 +05:00

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