%
This commit is contained in:
355
database.js
355
database.js
@@ -1,8 +1,15 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const { Pool } = require('pg');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
require('dotenv').config();
|
||||
|
||||
// Определяем, какую базу использовать
|
||||
const USE_POSTGRES = process.env.POSTGRESQL === 'yes';
|
||||
let db = null; // Основной объект базы данных
|
||||
let postgresPool = null; // Пул соединений PostgreSQL
|
||||
let isInitialized = false; // Флаг инициализации
|
||||
|
||||
const dataDir = path.join(__dirname, 'data');
|
||||
const createDirIfNotExists = (dirPath) => {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
@@ -21,17 +28,72 @@ createDirIfNotExists(uploadsDir);
|
||||
createDirIfNotExists(tasksDir);
|
||||
createDirIfNotExists(logsDir);
|
||||
|
||||
const db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) {
|
||||
console.error('Ошибка подключения к БД:', err.message);
|
||||
} else {
|
||||
console.log('Подключение к SQLite установлено');
|
||||
console.log('База данных расположена:', dbPath);
|
||||
initializeDatabase();
|
||||
}
|
||||
});
|
||||
// Инициализация базы данных
|
||||
async function initializeDatabase() {
|
||||
console.log(`🔧 Используется ${USE_POSTGRES ? 'PostgreSQL' : 'SQLite'}`);
|
||||
|
||||
if (USE_POSTGRES) {
|
||||
// Используем PostgreSQL
|
||||
try {
|
||||
postgresPool = 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: 10,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 5000,
|
||||
});
|
||||
|
||||
function initializeDatabase() {
|
||||
// Тестируем подключение
|
||||
const client = await postgresPool.connect();
|
||||
await client.query('SELECT 1');
|
||||
client.release();
|
||||
|
||||
console.log('✅ Подключение к PostgreSQL установлено');
|
||||
|
||||
// Создаем адаптер для PostgreSQL
|
||||
db = createPostgresAdapter(postgresPool);
|
||||
|
||||
// Проверяем и создаем таблицы
|
||||
await createPostgresTables();
|
||||
|
||||
isInitialized = true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка подключения к PostgreSQL:', error.message);
|
||||
console.log('🔄 Пытаемся использовать SQLite как запасной вариант...');
|
||||
await initializeSQLite();
|
||||
}
|
||||
} else {
|
||||
// Используем SQLite
|
||||
await initializeSQLite();
|
||||
}
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
function initializeSQLite() {
|
||||
return new Promise((resolve, reject) => {
|
||||
db = new sqlite3.Database(dbPath, (err) => {
|
||||
if (err) {
|
||||
console.error('❌ Ошибка подключения к SQLite:', err.message);
|
||||
reject(err);
|
||||
return;
|
||||
} else {
|
||||
console.log('✅ Подключение к SQLite установлено');
|
||||
console.log('📁 База данных расположена:', dbPath);
|
||||
createSQLiteTables();
|
||||
isInitialized = true;
|
||||
resolve(db);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createSQLiteTables() {
|
||||
// SQLite таблицы
|
||||
db.run(`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
login TEXT UNIQUE NOT NULL,
|
||||
@@ -113,11 +175,260 @@ function initializeDatabase() {
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`);
|
||||
|
||||
console.log('База данных инициализирована в папке data');
|
||||
|
||||
console.log('✅ База данных SQLite инициализирована');
|
||||
setTimeout(addMissingColumns, 1000);
|
||||
}
|
||||
|
||||
function createPostgresAdapter(pool) {
|
||||
// Адаптер для PostgreSQL с совместимым API
|
||||
return {
|
||||
all: (sql, params = [], callback) => {
|
||||
if (!callback && typeof params === 'function') {
|
||||
callback = params;
|
||||
params = [];
|
||||
}
|
||||
|
||||
// Адаптируем SQL для PostgreSQL
|
||||
const adaptedSql = adaptSQLForPostgres(sql);
|
||||
|
||||
pool.query(adaptedSql, params)
|
||||
.then(result => callback(null, result.rows))
|
||||
.catch(err => {
|
||||
console.error('PostgreSQL Error (all):', err.message, 'SQL:', adaptedSql);
|
||||
callback(err);
|
||||
});
|
||||
},
|
||||
|
||||
get: (sql, params = [], callback) => {
|
||||
if (!callback && typeof params === 'function') {
|
||||
callback = params;
|
||||
params = [];
|
||||
}
|
||||
|
||||
// Адаптируем SQL для PostgreSQL
|
||||
const adaptedSql = adaptSQLForPostgres(sql);
|
||||
|
||||
pool.query(adaptedSql, params)
|
||||
.then(result => callback(null, result.rows[0] || null))
|
||||
.catch(err => {
|
||||
console.error('PostgreSQL Error (get):', err.message, 'SQL:', adaptedSql);
|
||||
callback(err);
|
||||
});
|
||||
},
|
||||
|
||||
run: (sql, params = [], callback) => {
|
||||
if (!callback && typeof params === 'function') {
|
||||
callback = params;
|
||||
params = [];
|
||||
}
|
||||
|
||||
// Адаптируем SQL для PostgreSQL
|
||||
const adaptedSql = adaptSQLForPostgres(sql);
|
||||
|
||||
pool.query(adaptedSql, params)
|
||||
.then(result => {
|
||||
if (callback) {
|
||||
const lastIdQuery = sql.toLowerCase().includes('insert into') ?
|
||||
"SELECT lastval() as last_id" : "SELECT 0 as last_id";
|
||||
|
||||
if (sql.toLowerCase().includes('insert into')) {
|
||||
pool.query("SELECT lastval() as last_id", [])
|
||||
.then(lastIdResult => {
|
||||
callback(null, {
|
||||
lastID: lastIdResult.rows[0]?.last_id || null,
|
||||
changes: result.rowCount || 0
|
||||
});
|
||||
})
|
||||
.catch(err => callback(err));
|
||||
} else {
|
||||
callback(null, {
|
||||
lastID: null,
|
||||
changes: result.rowCount || 0
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('PostgreSQL Error (run):', err.message, 'SQL:', adaptedSql);
|
||||
if (callback) callback(err);
|
||||
});
|
||||
},
|
||||
|
||||
// Для транзакций - эмуляция
|
||||
serialize: (callback) => {
|
||||
// В PostgreSQL транзакции обрабатываются по-другому
|
||||
// Здесь просто выполняем колбэк
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
console.error('Error in serialize:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// Закрытие соединения
|
||||
close: (callback) => {
|
||||
pool.end()
|
||||
.then(() => {
|
||||
if (callback) callback(null);
|
||||
})
|
||||
.catch(err => {
|
||||
if (callback) callback(err);
|
||||
});
|
||||
},
|
||||
|
||||
// Дополнительные методы
|
||||
exec: (sql, callback) => {
|
||||
pool.query(sql)
|
||||
.then(() => {
|
||||
if (callback) callback(null);
|
||||
})
|
||||
.catch(err => {
|
||||
if (callback) callback(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function adaptSQLForPostgres(sql) {
|
||||
// Адаптируем SQL запросы для PostgreSQL
|
||||
let adaptedSql = sql;
|
||||
|
||||
// Заменяем SQLite-специфичные синтаксисы
|
||||
adaptedSql = adaptedSql.replace(/AUTOINCREMENT/gi, 'SERIAL');
|
||||
adaptedSql = adaptedSql.replace(/DATETIME/gi, 'TIMESTAMP');
|
||||
adaptedSql = adaptedSql.replace(/INTEGER PRIMARY KEY/gi, 'SERIAL PRIMARY KEY');
|
||||
adaptedSql = adaptedSql.replace(/datetime\('now'\)/gi, 'CURRENT_TIMESTAMP');
|
||||
adaptedSql = adaptedSql.replace(/CURRENT_TIMESTAMP/gi, 'CURRENT_TIMESTAMP');
|
||||
|
||||
// Исправляем INSERT с возвратом ID
|
||||
if (adaptedSql.includes('INSERT INTO') && adaptedSql.includes('RETURNING id')) {
|
||||
adaptedSql = adaptedSql.replace('RETURNING id', 'RETURNING id');
|
||||
}
|
||||
|
||||
return adaptedSql;
|
||||
}
|
||||
|
||||
async function createPostgresTables() {
|
||||
if (!USE_POSTGRES) return;
|
||||
|
||||
try {
|
||||
const client = await postgresPool.connect();
|
||||
|
||||
console.log('🔧 Проверяем/создаем таблицы в PostgreSQL...');
|
||||
|
||||
// Создаем таблицы 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
|
||||
)
|
||||
`);
|
||||
|
||||
// Создаем индексы
|
||||
const indexes = [
|
||||
'CREATE INDEX IF NOT EXISTS idx_tasks_created_by ON tasks(created_by)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_tasks_closed_at ON tasks(closed_at)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_task_assignments_task_id ON task_assignments(task_id)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_task_assignments_user_id ON task_assignments(user_id)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_task_assignments_status ON task_assignments(status)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_task_files_task_id ON task_files(task_id)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_activity_logs_task_id ON activity_logs(task_id)',
|
||||
'CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs(created_at)'
|
||||
];
|
||||
|
||||
for (const indexQuery of indexes) {
|
||||
try {
|
||||
await client.query(indexQuery);
|
||||
} catch (err) {
|
||||
console.warn(`⚠️ Не удалось создать индекс: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
client.release();
|
||||
console.log('✅ Таблицы PostgreSQL проверены/созданы');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Ошибка создания таблиц PostgreSQL:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function addMissingColumns() {
|
||||
const columnsToAdd = [
|
||||
{ table: 'tasks', column: 'rework_comment', type: 'TEXT' },
|
||||
@@ -272,11 +583,25 @@ function checkOverdueTasks() {
|
||||
setInterval(checkOverdueTasks, 60000);
|
||||
|
||||
module.exports = {
|
||||
db,
|
||||
initializeDatabase, // Экспортируем функцию инициализации
|
||||
getDb: () => {
|
||||
if (!isInitialized) {
|
||||
throw new Error('База данных не инициализирована');
|
||||
}
|
||||
return db;
|
||||
},
|
||||
isInitialized: () => isInitialized,
|
||||
logActivity,
|
||||
createTaskFolder,
|
||||
createUserTaskFolder,
|
||||
saveTaskMetadata,
|
||||
updateTaskMetadata,
|
||||
checkTaskAccess
|
||||
};
|
||||
checkTaskAccess,
|
||||
USE_POSTGRES,
|
||||
getDatabaseType: () => USE_POSTGRES ? 'PostgreSQL' : 'SQLite'
|
||||
};
|
||||
|
||||
// Запускаем инициализацию при экспорте (но она завершится позже)
|
||||
initializeDatabase().catch(err => {
|
||||
console.error('❌ Ошибка инициализации базы данных:', err.message);
|
||||
});
|
||||
Reference in New Issue
Block a user