ваше сообщение коммита
This commit is contained in:
@@ -86,6 +86,7 @@ const countriesRoutes = require('./routes/countries'); // Добавляем и
|
||||
const russianClassifiersRoutes = require('./routes/russian-classifiers'); // Добавляем импорт российских классификаторов
|
||||
const ollamaRoutes = require('./routes/ollama'); // Добавляем импорт Ollama маршрутов
|
||||
const aiQueueRoutes = require('./routes/ai-queue'); // Добавляем импорт AI Queue маршрутов
|
||||
const tagsRoutes = require('./routes/tags'); // Добавляем импорт маршрутов тегов
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -210,6 +211,7 @@ app.use('/api/countries', countriesRoutes); // Добавляем маршрут
|
||||
app.use('/api/russian-classifiers', russianClassifiersRoutes); // Добавляем маршрут российских классификаторов
|
||||
app.use('/api/ollama', ollamaRoutes); // Добавляем маршрут Ollama
|
||||
app.use('/api/ai-queue', aiQueueRoutes); // Добавляем маршрут AI Queue
|
||||
app.use('/api/tags', tagsRoutes); // Добавляем маршрут тегов
|
||||
app.use('/api/messages', messagesRoutes);
|
||||
app.use('/api/identities', identitiesRoutes);
|
||||
app.use('/api/rag', ragRoutes); // Подключаем роут
|
||||
|
||||
@@ -19,6 +19,18 @@ const { requireAuth } = require('../middleware/auth');
|
||||
const vectorSearchClient = require('../services/vectorSearchClient');
|
||||
const { broadcastTableUpdate, broadcastTableRelationsUpdate } = require('../wsHub');
|
||||
|
||||
// Вспомогательная функция для получения ключа шифрования
|
||||
function getEncryptionKey() {
|
||||
const fs = require('fs');
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
|
||||
if (!fs.existsSync(keyPath)) {
|
||||
throw new Error('Encryption key file not found');
|
||||
}
|
||||
|
||||
return fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
|
||||
router.use((req, res, next) => {
|
||||
console.log('Tables router received:', req.method, req.originalUrl);
|
||||
next();
|
||||
@@ -28,17 +40,12 @@ router.use((req, res, next) => {
|
||||
router.get('/', async (req, res, next) => {
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
let encryptionKey;
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
const result = await db.getQuery()('SELECT id, created_at, updated_at, is_rag_source_id, decrypt_text(name_encrypted, $1) as name, decrypt_text(description_encrypted, $1) as description FROM user_tables ORDER BY id', [encryptionKey]);
|
||||
@@ -54,17 +61,12 @@ router.post('/', async (req, res, next) => {
|
||||
const { name, description, isRagSourceId } = req.body;
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
let encryptionKey;
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
const result = await db.getQuery()(
|
||||
@@ -81,17 +83,12 @@ router.post('/', async (req, res, next) => {
|
||||
router.get('/rag-sources', async (req, res, next) => {
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
let encryptionKey;
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
const result = await db.getQuery()(
|
||||
@@ -111,17 +108,12 @@ router.get('/:id', async (req, res, next) => {
|
||||
try {
|
||||
const tableId = req.params.id;
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
let encryptionKey;
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
// Выполняем все 4 запроса параллельно для ускорения
|
||||
@@ -164,13 +156,27 @@ function generatePlaceholder(name, existingPlaceholders = []) {
|
||||
return '';
|
||||
}).join('');
|
||||
translit = translit.replace(/_+/g, '_').replace(/^_+|_+$/g, '');
|
||||
|
||||
// Если translit пустой, используем fallback
|
||||
if (!translit) {
|
||||
translit = 'column';
|
||||
}
|
||||
|
||||
let base = translit;
|
||||
let candidate = base;
|
||||
let i = 1;
|
||||
|
||||
// Генерируем уникальный плейсхолдер
|
||||
while (existingPlaceholders.includes(candidate)) {
|
||||
candidate = `${base}_${i}`;
|
||||
i++;
|
||||
// Защита от бесконечного цикла
|
||||
if (i > 1000) {
|
||||
candidate = `${base}_${Date.now()}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
@@ -190,7 +196,13 @@ router.post('/:id/columns', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -201,8 +213,8 @@ router.post('/:id/columns', async (req, res, next) => {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Получаем уже существующие плейсхолдеры в таблице
|
||||
const existing = (await db.getQuery()('SELECT placeholder FROM user_columns WHERE table_id = $1', [tableId])).rows;
|
||||
// Получаем уже существующие плейсхолдеры во всей базе данных
|
||||
const existing = (await db.getQuery()('SELECT placeholder FROM user_columns WHERE placeholder IS NOT NULL', [])).rows;
|
||||
const existingPlaceholders = existing.map(c => c.placeholder).filter(Boolean);
|
||||
const placeholder = generatePlaceholder(name, existingPlaceholders);
|
||||
const result = await db.getQuery()(
|
||||
@@ -228,7 +240,13 @@ router.post('/:id/rows', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -262,7 +280,13 @@ router.get('/:id/rows', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -347,7 +371,13 @@ router.post('/cell', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -365,21 +395,11 @@ router.post('/cell', async (req, res, next) => {
|
||||
[row_id, column_id, value, encryptionKey]
|
||||
);
|
||||
|
||||
// Получаем table_id и проверяем, является ли это таблицей тегов
|
||||
// Получаем table_id
|
||||
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [row_id])).rows[0];
|
||||
if (table) {
|
||||
const tableId = table.table_id;
|
||||
|
||||
// Проверяем, является ли это таблицей "Теги клиентов"
|
||||
const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [tableId, encryptionKey])).rows[0];
|
||||
console.log('🔄 [Tables] Проверяем таблицу:', { tableId, tableName: tableName?.name });
|
||||
if (tableName && tableName.name === 'Теги клиентов') {
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
console.log('🔄 [Tables] Обновление ячейки в таблице тегов, отправляем уведомление');
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
}
|
||||
|
||||
// Получаем всю строку для upsert
|
||||
const rowData = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.id = $1', [row_id, encryptionKey])).rows[0];
|
||||
if (rowData) {
|
||||
@@ -408,35 +428,26 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
||||
// Получаем table_id
|
||||
const table = (await db.getQuery()('SELECT table_id FROM user_rows WHERE id = $1', [rowId])).rows[0];
|
||||
|
||||
// Проверяем, является ли это таблицей тегов перед удалением
|
||||
let isTagsTable = false;
|
||||
if (table) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
const tableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [table.table_id, encryptionKey])).rows[0];
|
||||
isTagsTable = tableName && tableName.name === 'Теги клиентов';
|
||||
if (!table) {
|
||||
return res.status(404).json({ error: 'Row not found' });
|
||||
}
|
||||
|
||||
const tableId = table.table_id;
|
||||
|
||||
// Удаляем строку
|
||||
await db.getQuery()('DELETE FROM user_rows WHERE id = $1', [rowId]);
|
||||
|
||||
if (table) {
|
||||
const tableId = table.table_id;
|
||||
// Получаем все строки для rebuild
|
||||
// Получаем ключ шифрования
|
||||
// Получаем все строки для rebuild
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -448,18 +459,10 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
||||
}
|
||||
|
||||
const rows = (await db.getQuery()('SELECT r.id as row_id, decrypt_text(c.value_encrypted, $2) as text, decrypt_text(c2.value_encrypted, $2) as answer FROM user_rows r LEFT JOIN user_cell_values c ON c.row_id = r.id AND c.column_id = 1 LEFT JOIN user_cell_values c2 ON c2.row_id = r.id AND c2.column_id = 2 WHERE r.table_id = $1', [tableId, encryptionKey])).rows;
|
||||
const rebuildRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } }));
|
||||
console.log('[DEBUG][rebuildRows]', rebuildRows);
|
||||
if (rebuildRows.length > 0) {
|
||||
await vectorSearchClient.rebuild(tableId, rebuildRows);
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление, если это была таблица тегов
|
||||
if (isTagsTable) {
|
||||
console.log('🔄 [Tables] Обновление строки в таблице тегов, отправляем уведомление');
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
const rebuildRows = rows.filter(r => r.row_id && r.text).map(r => ({ row_id: r.row_id, text: r.text, metadata: { answer: r.answer } }));
|
||||
console.log('[DEBUG][rebuildRows]', rebuildRows);
|
||||
if (rebuildRows.length > 0) {
|
||||
await vectorSearchClient.rebuild(tableId, rebuildRows);
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении таблицы
|
||||
@@ -468,6 +471,7 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[DELETE /row/:rowId] Error:', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
@@ -476,9 +480,29 @@ router.delete('/row/:rowId', async (req, res, next) => {
|
||||
router.delete('/column/:columnId', async (req, res, next) => {
|
||||
try {
|
||||
const columnId = req.params.columnId;
|
||||
|
||||
// Получаем информацию о столбце перед удалением
|
||||
const columnInfo = (await db.getQuery()('SELECT table_id FROM user_columns WHERE id = $1', [columnId])).rows[0];
|
||||
if (!columnInfo) {
|
||||
return res.status(404).json({ error: 'Column not found' });
|
||||
}
|
||||
|
||||
// Удаляем все связанные данные в правильном порядке
|
||||
// 1. Удаляем relations, связанные с этим столбцом
|
||||
await db.getQuery()('DELETE FROM user_table_relations WHERE column_id = $1', [columnId]);
|
||||
|
||||
// 2. Удаляем все значения ячеек для этого столбца
|
||||
await db.getQuery()('DELETE FROM user_cell_values WHERE column_id = $1', [columnId]);
|
||||
|
||||
// 3. Удаляем сам столбец
|
||||
await db.getQuery()('DELETE FROM user_columns WHERE id = $1', [columnId]);
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении таблицы
|
||||
broadcastTableUpdate(columnInfo.table_id);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('[DELETE /column/:columnId] Error:', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
@@ -492,7 +516,13 @@ router.patch('/column/:columnId', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -616,7 +646,13 @@ router.post('/:id/rebuild-index', requireAuth, async (req, res, next) => {
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
@@ -665,37 +701,38 @@ router.post('/:id/rebuild-index', requireAuth, async (req, res, next) => {
|
||||
|
||||
// DELETE: удалить таблицу и каскадно все связанные строки/столбцы/ячейки (доступно всем)
|
||||
router.delete('/:id', requireAuth, async (req, res, next) => {
|
||||
const dbModule = require('../db');
|
||||
try {
|
||||
// Логируем строку подключения и pool.options
|
||||
console.log('[DIAG][DELETE] pool.options:', dbModule.pool.options);
|
||||
console.log('[DIAG][DELETE] process.env.DATABASE_URL:', process.env.DATABASE_URL);
|
||||
console.log('[DIAG][DELETE] process.env.DB_HOST:', process.env.DB_HOST);
|
||||
console.log('[DIAG][DELETE] process.env.DB_NAME:', process.env.DB_NAME);
|
||||
console.log('=== [DIAG] Попытка удаления таблицы ===');
|
||||
console.log('Сессия пользователя:', req.session);
|
||||
if (!req.session.isAdmin) {
|
||||
console.log('[DIAG] Нет прав администратора');
|
||||
return res.status(403).json({ error: 'Удаление доступно только администраторам' });
|
||||
}
|
||||
|
||||
const tableId = Number(req.params.id);
|
||||
console.log('[DIAG] id из запроса:', req.params.id, 'Преобразованный id:', tableId, 'typeof:', typeof tableId);
|
||||
|
||||
|
||||
// Проверяем наличие таблицы перед удалением
|
||||
const checkBefore = await db.getQuery()('SELECT * FROM user_tables WHERE id = $1', [tableId]);
|
||||
console.log('[DIAG] Таблица перед удалением:', checkBefore.rows);
|
||||
|
||||
// Пытаемся удалить
|
||||
const tableExists = (await db.getQuery()('SELECT id FROM user_tables WHERE id = $1', [tableId])).rows[0];
|
||||
if (!tableExists) {
|
||||
return res.status(404).json({ error: 'Table not found' });
|
||||
}
|
||||
|
||||
// Удаляем все связанные данные в правильном порядке
|
||||
// 1. Удаляем все relations, где эта таблица является источником или целью
|
||||
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id IN (SELECT id FROM user_rows WHERE table_id = $1) OR to_table_id = $1', [tableId]);
|
||||
|
||||
// 2. Удаляем все значения ячеек для строк этой таблицы
|
||||
await db.getQuery()('DELETE FROM user_cell_values WHERE row_id IN (SELECT id FROM user_rows WHERE table_id = $1)', [tableId]);
|
||||
|
||||
// 3. Удаляем все строки таблицы
|
||||
await db.getQuery()('DELETE FROM user_rows WHERE table_id = $1', [tableId]);
|
||||
|
||||
// 4. Удаляем все столбцы таблицы
|
||||
await db.getQuery()('DELETE FROM user_columns WHERE table_id = $1', [tableId]);
|
||||
|
||||
// 5. Удаляем саму таблицу
|
||||
const result = await db.getQuery()('DELETE FROM user_tables WHERE id = $1 RETURNING *', [tableId]);
|
||||
console.log('[DIAG] Результат удаления (rowCount):', result.rowCount, 'rows:', result.rows);
|
||||
|
||||
// Проверяем наличие таблицы после удаления
|
||||
const checkAfter = await db.getQuery()('SELECT * FROM user_tables WHERE id = $1', [tableId]);
|
||||
console.log('[DIAG] Таблица после удаления:', checkAfter.rows);
|
||||
|
||||
|
||||
res.json({ success: true, deleted: result.rowCount });
|
||||
} catch (err) {
|
||||
console.error('[DIAG] Ошибка при удалении таблицы:', err);
|
||||
console.error('[DELETE /:id] Error:', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
@@ -718,14 +755,51 @@ router.get('/:tableId/row/:rowId/relations', async (req, res, next) => {
|
||||
router.post('/:tableId/row/:rowId/relations', async (req, res, next) => {
|
||||
try {
|
||||
const { tableId, rowId } = req.params;
|
||||
const { column_id, to_table_id, to_row_id } = req.body;
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
|
||||
VALUES ($1, $2, $3, $4) RETURNING *`,
|
||||
[rowId, column_id, to_table_id, to_row_id]
|
||||
);
|
||||
res.json(result.rows[0]);
|
||||
const { column_id, to_table_id, to_row_ids } = req.body;
|
||||
|
||||
// Если передается массив to_row_ids - это массовое обновление
|
||||
if (Array.isArray(to_row_ids)) {
|
||||
// Удаляем старые связи для этого столбца
|
||||
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
|
||||
|
||||
// Добавляем новые связи
|
||||
if (to_row_ids.length > 0) {
|
||||
const values = to_row_ids.map((to_row_id, index) =>
|
||||
`($1, $2, $3, $${index + 4})`
|
||||
).join(', ');
|
||||
|
||||
const params = [rowId, column_id, to_table_id, ...to_row_ids];
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
|
||||
VALUES ${values} RETURNING *`,
|
||||
params
|
||||
);
|
||||
|
||||
// Отправляем WebSocket уведомление
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate(null, rowId);
|
||||
|
||||
res.json(result.rows);
|
||||
} else {
|
||||
res.json([]);
|
||||
}
|
||||
} else {
|
||||
// Одиночная связь
|
||||
const { to_row_id } = req.body;
|
||||
const result = await db.getQuery()(
|
||||
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
|
||||
VALUES ($1, $2, $3, $4) RETURNING *`,
|
||||
[rowId, column_id, to_table_id, to_row_id]
|
||||
);
|
||||
|
||||
// Отправляем WebSocket уведомление
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate(null, rowId);
|
||||
|
||||
res.json(result.rows[0]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[POST /:tableId/row/:rowId/relations] Error:', err);
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
@@ -742,55 +816,8 @@ router.delete('/:tableId/row/:rowId/relations/:relationId', async (req, res, nex
|
||||
});
|
||||
|
||||
// --- Массовое обновление связей для multiselect-relation ---
|
||||
router.post('/:tableId/row/:rowId/multirelations', async (req, res, next) => {
|
||||
try {
|
||||
const { tableId, rowId } = req.params;
|
||||
const { column_id, to_table_id, to_row_ids } = req.body; // to_row_ids: массив id
|
||||
if (!Array.isArray(to_row_ids)) return res.status(400).json({ error: 'to_row_ids должен быть массивом' });
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Проверяем, является ли это обновлением тегов (проверяем связанную таблицу)
|
||||
const relatedTableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [to_table_id, encryptionKey])).rows[0];
|
||||
console.log('🔄 [Tables] Multirelations: проверяем связанную таблицу:', { to_table_id, tableName: relatedTableName?.name });
|
||||
|
||||
if (relatedTableName && relatedTableName.name === 'Теги клиентов') {
|
||||
console.log('🔄 [Tables] Multirelations: обновление тегов, отправляем уведомление');
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
}
|
||||
|
||||
// Удаляем старые связи для этой строки/столбца
|
||||
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
|
||||
// Добавляем новые связи
|
||||
for (const to_row_id of to_row_ids) {
|
||||
await db.getQuery()(
|
||||
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[rowId, column_id, to_table_id, to_row_id]
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении связей
|
||||
broadcastTableRelationsUpdate(tableId, rowId);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
// ПРИМЕЧАНИЕ: Для работы с тегами используйте /api/tags/user/:rowId/multirelations
|
||||
// Этот endpoint удален для избежания дублирования
|
||||
|
||||
// Получить плейсхолдеры для всех столбцов таблицы
|
||||
router.get('/:id/placeholders', async (req, res, next) => {
|
||||
@@ -799,7 +826,13 @@ router.get('/:id/placeholders', async (req, res, next) => {
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
let encryptionKey;
|
||||
try {
|
||||
encryptionKey = getEncryptionKey();
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
return res.status(500).json({ error: 'Database encryption error' });
|
||||
}
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
|
||||
134
backend/routes/tags.js
Normal file
134
backend/routes/tags.js
Normal file
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const db = require('../db');
|
||||
const { requireAuth } = require('../middleware/auth');
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
|
||||
console.log('[tags.js] ROUTER LOADED');
|
||||
|
||||
router.use((req, res, next) => {
|
||||
console.log('[tags.js] ROUTER REQUEST:', req.method, req.originalUrl);
|
||||
next();
|
||||
});
|
||||
|
||||
// PATCH /api/tags/user/:userId — установить теги пользователю
|
||||
router.patch('/user/:userId', async (req, res) => {
|
||||
const userId = Number(req.params.userId);
|
||||
const { tags } = req.body; // массив tagIds (id строк из таблицы тегов)
|
||||
if (!Array.isArray(tags)) {
|
||||
return res.status(400).json({ error: 'tags должен быть массивом' });
|
||||
}
|
||||
try {
|
||||
// Удаляем старые связи
|
||||
await db.getQuery()('DELETE FROM user_tag_links WHERE user_id = $1', [userId]);
|
||||
// Добавляем новые связи
|
||||
for (const tagId of tags) {
|
||||
await db.getQuery()(
|
||||
'INSERT INTO user_tag_links (user_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
|
||||
[userId, tagId]
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
broadcastTagsUpdate(null, userId);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/tags/user/:userId — получить все теги пользователя
|
||||
router.get('/user/:userId', async (req, res) => {
|
||||
const userId = Number(req.params.userId);
|
||||
try {
|
||||
const result = await db.getQuery()(
|
||||
'SELECT tag_id FROM user_tag_links WHERE user_id = $1',
|
||||
[userId]
|
||||
);
|
||||
res.json({ tags: result.rows.map(r => r.tag_id) });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/tags/user/:userId/tag/:tagId — удалить тег у пользователя
|
||||
router.delete('/user/:userId/tag/:tagId', async (req, res) => {
|
||||
const userId = Number(req.params.userId);
|
||||
const tagId = Number(req.params.tagId);
|
||||
try {
|
||||
await db.getQuery()(
|
||||
'DELETE FROM user_tag_links WHERE user_id = $1 AND tag_id = $2',
|
||||
[userId, tagId]
|
||||
);
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
broadcastTagsUpdate(null, userId);
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/tags/user/:rowId/multirelations — массовое обновление тегов через multirelations
|
||||
router.post('/user/:rowId/multirelations', async (req, res) => {
|
||||
const rowId = Number(req.params.rowId);
|
||||
const { column_id, to_table_id, to_row_ids } = req.body; // to_row_ids: массив id тегов
|
||||
if (!Array.isArray(to_row_ids)) return res.status(400).json({ error: 'to_row_ids должен быть массивом' });
|
||||
|
||||
// Получаем ключ шифрования
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
let encryptionKey = 'default-key';
|
||||
|
||||
try {
|
||||
const keyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
if (fs.existsSync(keyPath)) {
|
||||
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
|
||||
// Проверяем, является ли это обновлением тегов (проверяем связанную таблицу)
|
||||
const relatedTableName = (await db.getQuery()('SELECT decrypt_text(name_encrypted, $2) as name FROM user_tables WHERE id = $1', [to_table_id, encryptionKey])).rows[0];
|
||||
console.log('🔄 [Tags] Multirelations: проверяем связанную таблицу:', { to_table_id, tableName: relatedTableName?.name });
|
||||
|
||||
if (relatedTableName && relatedTableName.name === 'Теги клиентов') {
|
||||
console.log('🔄 [Tags] Multirelations: обновление тегов для строки:', rowId);
|
||||
|
||||
// Удаляем старые связи для этой строки/столбца
|
||||
await db.getQuery()('DELETE FROM user_table_relations WHERE from_row_id = $1 AND column_id = $2', [rowId, column_id]);
|
||||
|
||||
// Добавляем новые связи
|
||||
for (const to_row_id of to_row_ids) {
|
||||
await db.getQuery()(
|
||||
`INSERT INTO user_table_relations (from_row_id, column_id, to_table_id, to_row_id)
|
||||
VALUES ($1, $2, $3, $4)`,
|
||||
[rowId, column_id, to_table_id, to_row_id]
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
broadcastTagsUpdate(null, rowId);
|
||||
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.status(400).json({ error: 'Этот endpoint предназначен только для работы с тегами' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -561,67 +561,11 @@ router.post('/import', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// --- Работа с тегами пользователя через user_tag_links ---
|
||||
// PATCH /api/users/:id/tags — установить теги пользователю
|
||||
router.patch('/:id/tags', async (req, res) => {
|
||||
const userId = Number(req.params.id);
|
||||
const { tags } = req.body; // массив tagIds (id строк из таблицы тегов)
|
||||
if (!Array.isArray(tags)) {
|
||||
return res.status(400).json({ error: 'tags должен быть массивом' });
|
||||
}
|
||||
try {
|
||||
// Удаляем старые связи
|
||||
await db.getQuery()('DELETE FROM user_tag_links WHERE user_id = $1', [userId]);
|
||||
// Добавляем новые связи
|
||||
for (const tagId of tags) {
|
||||
await db.getQuery()(
|
||||
'INSERT INTO user_tag_links (user_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING',
|
||||
[userId, tagId]
|
||||
);
|
||||
}
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/users/:id/tags — получить все теги пользователя
|
||||
router.get('/:id/tags', async (req, res) => {
|
||||
const userId = Number(req.params.id);
|
||||
try {
|
||||
const result = await db.getQuery()(
|
||||
'SELECT tag_id FROM user_tag_links WHERE user_id = $1',
|
||||
[userId]
|
||||
);
|
||||
res.json({ tags: result.rows.map(r => r.tag_id) });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/users/:id/tags/:tagId — удалить тег у пользователя
|
||||
router.delete('/:id/tags/:tagId', async (req, res) => {
|
||||
const userId = Number(req.params.id);
|
||||
const tagId = Number(req.params.tagId);
|
||||
try {
|
||||
await db.getQuery()(
|
||||
'DELETE FROM user_tag_links WHERE user_id = $1 AND tag_id = $2',
|
||||
[userId, tagId]
|
||||
);
|
||||
|
||||
// Отправляем WebSocket уведомление об обновлении тегов
|
||||
const { broadcastTagsUpdate } = require('../wsHub');
|
||||
broadcastTagsUpdate();
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
// --- Работа с тегами перенесена в /api/tags ---
|
||||
// Используйте следующие endpoints:
|
||||
// PATCH /api/tags/user/:id — установить теги пользователю
|
||||
// GET /api/tags/user/:id — получить теги пользователя
|
||||
// DELETE /api/tags/user/:id/tag/:tagId — удалить тег у пользователя
|
||||
// POST /api/tags/user/:id/multirelations — массовое обновление тегов
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -20,6 +20,10 @@ const wsClients = new Map(); // userId -> Set of WebSocket connections
|
||||
const tagsChangeCache = new Map();
|
||||
const TAGS_CACHE_TTL = 5000; // 5 секунд
|
||||
|
||||
// Дебаунс для broadcastTagsUpdate
|
||||
let tagsUpdateTimeout = null;
|
||||
const TAGS_UPDATE_DEBOUNCE = 100; // 100ms
|
||||
|
||||
function initWSS(server) {
|
||||
wss = new WebSocket.Server({ server, path: '/ws' });
|
||||
|
||||
@@ -234,20 +238,32 @@ function broadcastTableRelationsUpdate(tableId, rowId, targetUserId = null) {
|
||||
}
|
||||
}
|
||||
|
||||
function broadcastTagsUpdate(targetUserId = null) {
|
||||
console.log('🔔 [WebSocket] Отправляем уведомление об обновлении тегов');
|
||||
const message = JSON.stringify({
|
||||
type: 'tags-updated',
|
||||
timestamp: Date.now()
|
||||
});
|
||||
function broadcastTagsUpdate(targetUserId = null, rowId = null) {
|
||||
// Дебаунс: отменяем предыдущий таймаут
|
||||
if (tagsUpdateTimeout) {
|
||||
clearTimeout(tagsUpdateTimeout);
|
||||
}
|
||||
|
||||
// Отправляем всем подключенным клиентам
|
||||
wss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
console.log('🔔 [WebSocket] Отправляем tags-updated клиенту');
|
||||
client.send(message);
|
||||
}
|
||||
});
|
||||
// Устанавливаем новый таймаут
|
||||
tagsUpdateTimeout = setTimeout(() => {
|
||||
console.log('🔔 [WebSocket] Отправляем уведомление об обновлении тегов', rowId ? `для строки ${rowId}` : '');
|
||||
const message = JSON.stringify({
|
||||
type: 'tags-updated',
|
||||
timestamp: Date.now(),
|
||||
rowId: rowId // Добавляем информацию о конкретной строке
|
||||
});
|
||||
|
||||
let sentCount = 0;
|
||||
// Отправляем всем подключенным клиентам
|
||||
wss.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(message);
|
||||
sentCount++;
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`🔔 [WebSocket] Отправлено tags-updated ${sentCount} клиентам`);
|
||||
}, TAGS_UPDATE_DEBOUNCE);
|
||||
}
|
||||
|
||||
function getConnectedUsers() {
|
||||
|
||||
Reference in New Issue
Block a user