import { storage } from "../storage"; import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { tenantIsolation } from "../middleware/tenant.middleware"; import { db } from "../db"; import { dataTables } from "@shared/schema"; import { eq, and } from "drizzle-orm"; export function registerDataTableSyncRoutes(app: import("express").Express): void { // ===================================================== // DATA TABLE SYNC API (для импорта/синхронизации) // ===================================================== // Sync table (полная синхронизация как в Pyrus) app.post(['/api/tables/:tableId/sync', '/api/directories/:tableId/sync'], authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const tableId = parseInt(req.params.tableId); const table = await storage.getDataTable(tableId, req.organizationId!); if (!table) { return res.status(404).json({ error: 'Таблица не найдена' }); } const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!); if (!role || role === 'reader') { return res.status(403).json({ error: 'Нет прав для синхронизации' }); } const { apply, rows } = req.body; if (!rows || !Array.isArray(rows)) { return res.status(400).json({ error: 'Строки обязательны' }); } const currentRows = await storage.getDataTableRows(tableId, req.organizationId!); const validCurrentRows = currentRows.filter(r => Array.isArray(r.values)); const currentRowsById = new Map(validCurrentRows.map(r => [r.id, r])); const normalizeValues = (values: (string | null)[] | null, columnCount: number): string[] => { const arr = values || []; const result: string[] = []; for (let i = 0; i < columnCount; i++) { const val = arr[i]; result.push(val === null || val === undefined ? '' : String(val).trim()); } return result; }; const columnCount = table.columns?.length || 6; const makeValuesKey = (values: (string | null)[] | null) => JSON.stringify(normalizeValues(values, columnCount)); // Detect extended format: rows contain id or parentId const isExtendedFormat = rows.some((r: any) => r && (r.id != null || r.parentId != null)); if (isExtendedFormat) { // Parse incoming rows const incomingRows: { rawId?: number | null; values: string[]; parentId?: number | null; position: number; }[] = rows.map((r: any, index: number) => ({ rawId: r.id != null ? Number(r.id) : undefined, values: normalizeValues(r.values, columnCount), parentId: r.parentId != null ? Number(r.parentId) : null, position: index, })); // Resolve existing row id for each incoming row const incomingWithResolvedId = incomingRows.map((row) => { const existing = row.rawId != null ? currentRowsById.get(row.rawId) : undefined; return { ...row, existingId: existing ? existing.id : undefined, }; }); // Determine deleted rows: existing ids not present in incoming rows const incomingExistingIds = new Set( incomingWithResolvedId .filter(r => r.existingId != null) .map(r => r.existingId!) ); const deleted = validCurrentRows .filter(r => !incomingExistingIds.has(r.id)) .map(r => ({ id: r.id, values: normalizeValues(r.values, columnCount) })); const deletedIds = new Set(deleted.map(d => d.id)); // Validate parent references const validParentIds = new Set(validCurrentRows.map(r => r.id).filter(id => !deletedIds.has(id))); for (const row of incomingWithResolvedId) { if (row.parentId != null) { if (!validParentIds.has(row.parentId)) { return res.status(400).json({ error: `Некорректный parentId ${row.parentId}: строка не найдена или будет удалена` }); } if (row.existingId != null && row.parentId === row.existingId) { return res.status(400).json({ error: `Строка не может быть родителем самой себя (id ${row.existingId})` }); } } } // Detect cycles using union-find / ancestor check const parentMap = new Map(); for (const row of incomingWithResolvedId) { const key = row.existingId != null ? row.existingId : `new:${row.position}`; parentMap.set(key, row.parentId ?? null); } const getAncestors = (start: number | string): Set => { const ancestors = new Set(); let current: number | string | null | undefined = start; while (current != null) { if (ancestors.has(current)) break; // cycle detected ancestors.add(current); current = parentMap.get(current); } return ancestors; }; for (const row of incomingWithResolvedId) { if (row.parentId == null) continue; const key = row.existingId != null ? row.existingId : `new:${row.position}`; const ancestors = getAncestors(row.parentId); if (ancestors.has(key)) { return res.status(400).json({ error: `Обнаружен цикл в иерархии для строки ${row.values[0] || row.position}` }); } } // Build preview lists const added: { values: string[]; position: number; parentId: number | null }[] = []; const updated: { id: number; values: string[]; oldPosition: number; newPosition: number; parentId: number | null; oldParentId: number | null }[] = []; for (const row of incomingWithResolvedId) { if (row.existingId == null) { added.push({ values: row.values, position: row.position, parentId: row.parentId ?? null }); } else { const existing = currentRowsById.get(row.existingId)!; const parentChanged = (row.parentId ?? null) !== (existing.parentId ?? null); const positionChanged = row.position !== existing.position; const valuesChanged = makeValuesKey(row.values) !== makeValuesKey(existing.values); if (parentChanged || positionChanged || valuesChanged) { updated.push({ id: existing.id, values: row.values, oldPosition: existing.position, newPosition: row.position, parentId: row.parentId ?? null, oldParentId: existing.parentId ?? null, }); } } } if (apply) { const positionUpdates = updated .filter(u => u.oldPosition === u.newPosition && u.oldParentId === u.parentId && makeValuesKey(u.values) === makeValuesKey(currentRowsById.get(u.id)!.values)) .map(u => ({ id: u.id, position: u.newPosition, parentId: u.parentId })); const reordered = updated .filter(u => u.oldPosition !== u.newPosition || u.oldParentId !== u.parentId || makeValuesKey(u.values) !== makeValuesKey(currentRowsById.get(u.id)!.values)) .map(u => ({ id: u.id, newPosition: u.newPosition, parentId: u.parentId })); await storage.applyDataTableSync( tableId, deleted, added, reordered, positionUpdates ); } return res.json({ apply, added: added.length, updated: updated.length, deleted: deleted.length, addedRows: added.map(r => ({ values: r.values, parentId: r.parentId })), updatedRows: updated.map(r => ({ id: r.id, values: r.values, oldPosition: r.oldPosition, newPosition: r.newPosition, parentId: r.parentId, oldParentId: r.oldParentId })), deletedRows: deleted.map(r => ({ id: r.id, values: r.values })), }); } // Legacy format: sync by full values key const currentRowsByKey = new Map(validCurrentRows.map(r => [makeValuesKey(r.values), r])); const currentKeysSet = new Set(validCurrentRows.map(r => makeValuesKey(r.values))); const newKeysSet = new Set(rows.map((r: { values: string[] }) => makeValuesKey(r.values))); const added: { values: string[], position: number; parentId?: number | null }[] = []; const deleted: { id: number; values: string[] }[] = []; const reordered: { id: number; values: string[]; oldPosition: number; newPosition: number; parentId?: number | null }[] = []; rows.forEach((newRow: { values: string[] }, index: number) => { const key = makeValuesKey(newRow.values); if (!currentKeysSet.has(key)) { added.push({ values: newRow.values, position: index }); } else { const existingRow = currentRowsByKey.get(key); if (existingRow && existingRow.position !== index) { reordered.push({ id: existingRow.id, values: normalizeValues(existingRow.values, columnCount), oldPosition: existingRow.position, newPosition: index, }); } } }); for (const currentRow of validCurrentRows) { const key = makeValuesKey(currentRow.values); if (!newKeysSet.has(key)) { deleted.push({ id: currentRow.id, values: normalizeValues(currentRow.values, columnCount) }); } } if (apply) { const positionUpdates: { id: number; position: number }[] = []; for (let i = 0; i < rows.length; i++) { const key = makeValuesKey(rows[i].values); const existingRow = currentRowsByKey.get(key); if (existingRow && !deleted.some(d => d.id === existingRow.id)) { positionUpdates.push({ id: existingRow.id, position: i }); } } await storage.applyDataTableSync( tableId, deleted, added, reordered.map(r => ({ id: r.id, newPosition: r.newPosition })), positionUpdates ); } res.json({ apply, added: added.length, deleted: deleted.length, reordered: reordered.length, addedRows: added.map(r => ({ values: r.values })), deletedRows: deleted.map(r => ({ id: r.id, values: r.values })), reorderedRows: reordered.map(r => ({ values: r.values, oldPosition: r.oldPosition, newPosition: r.newPosition })) }); } catch (error) { console.error('Sync table error:', error); res.status(500).json({ error: 'Ошибка синхронизации таблицы' }); } } ); // Diff table (частичное обновление как в Pyrus) app.post(['/api/tables/:tableId/diff', '/api/directories/:tableId/diff'], authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const tableId = parseInt(req.params.tableId); const table = await storage.getDataTable(tableId, req.organizationId!); if (!table) { return res.status(404).json({ error: 'Таблица не найдена' }); } const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!); if (!role || role === 'reader') { return res.status(403).json({ error: 'Нет прав для изменения' }); } const { upsert, delete: deleteKeys } = req.body; const currentRows = await storage.getDataTableRows(tableId, req.organizationId!); const upsertArr = upsert && Array.isArray(upsert) ? upsert : []; const deleteKeysArr = deleteKeys && Array.isArray(deleteKeys) ? deleteKeys : []; const result = await storage.applyDataTableDiff(tableId, upsertArr, deleteKeysArr, currentRows); res.json({ apply: true, added: result.added.map(r => ({ id: r.id, values: r.values })), updated: result.updated.map(r => ({ id: r.id, values: r.values })), deleted: result.deleted.map(r => ({ id: r.id, values: r.values })) }); } catch (error) { console.error('Diff table error:', error); res.status(500).json({ error: 'Ошибка изменения таблицы' }); } } ); // ===================================================== // DATA TABLE PERMISSIONS API // ===================================================== // Get table permissions app.get(['/api/tables/:tableId/permissions', '/api/directories/:tableId/permissions'], authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const tableId = parseInt(req.params.tableId); const table = await storage.getDataTable(tableId, req.organizationId!); if (!table) { return res.status(404).json({ error: 'Таблица не найдена' }); } const role = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!); if (role !== 'admin') { return res.status(403).json({ error: 'Нет прав для просмотра прав доступа' }); } const permissions = await storage.getDataTablePermissions(tableId, req.organizationId!); res.json({ permissions, createdBy: table.createdBy }); } catch (error) { console.error('Get table permissions error:', error); res.status(500).json({ error: 'Ошибка получения прав доступа' }); } } ); // Set table permission app.post(['/api/tables/:tableId/permissions', '/api/directories/:tableId/permissions'], authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const tableId = parseInt(req.params.tableId); const table = await storage.getDataTable(tableId, req.organizationId!); if (!table) { return res.status(404).json({ error: 'Таблица не найдена' }); } const currentRole = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!); if (currentRole !== 'admin') { return res.status(403).json({ error: 'Нет прав для управления правами доступа' }); } const { userId, role } = req.body; if (!userId || !role) { return res.status(400).json({ error: 'ID пользователя и роль обязательны' }); } if (!['admin', 'editor', 'reader'].includes(role)) { return res.status(400).json({ error: 'Недопустимая роль' }); } if (userId === table.createdBy) { return res.status(400).json({ error: 'Нельзя изменить права создателя таблицы' }); } const permission = await storage.setDataTablePermission({ tableId, userId, role }); res.json({ permission }); } catch (error) { console.error('Set table permission error:', error); res.status(500).json({ error: 'Ошибка установки прав доступа' }); } } ); // Delete table permission app.delete(['/api/tables/:tableId/permissions/:userId', '/api/directories/:tableId/permissions/:userId'], authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const tableId = parseInt(req.params.tableId); const targetUserId = parseInt(req.params.userId); const table = await storage.getDataTable(tableId, req.organizationId!); if (!table) { return res.status(404).json({ error: 'Таблица не найдена' }); } const currentRole = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!); if (currentRole !== 'admin') { return res.status(403).json({ error: 'Нет прав для управления правами доступа' }); } await storage.deleteDataTablePermission(tableId, targetUserId); res.json({ success: true }); } catch (error) { console.error('Delete table permission error:', error); res.status(500).json({ error: 'Ошибка удаления прав доступа' }); } } ); // ===================================================== // DATA TABLE ACCESS RULES API (role-based) // ===================================================== app.get(['/api/tables/:tableId/access-rules', '/api/directories/:tableId/access-rules'], authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const tableId = parseInt(req.params.tableId); const table = await storage.getDataTable(tableId, req.organizationId!); if (!table) return res.status(404).json({ error: 'Таблица не найдена' }); const currentRole = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!); if (currentRole !== 'admin') { return res.status(403).json({ error: 'Нет прав для просмотра прав доступа' }); } const rules = await storage.getDataTableAccessRules(tableId, req.organizationId!); const linkedForms = await storage.getLinkedFormsForTable(tableId, req.organizationId!); res.json({ rules, linkedForms }); } catch (error) { console.error('Get table access rules error:', error); res.status(500).json({ error: 'Ошибка получения прав доступа' }); } } ); app.post(['/api/tables/:tableId/access-rules', '/api/directories/:tableId/access-rules'], authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const tableId = parseInt(req.params.tableId); const table = await storage.getDataTable(tableId, req.organizationId!); if (!table) return res.status(404).json({ error: 'Таблица не найдена' }); const currentRole = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!); if (currentRole !== 'admin') { return res.status(403).json({ error: 'Нет прав для управления правами доступа' }); } const { targetType, targetId, accessLevel } = req.body; if (!targetType || !targetId || !accessLevel) { return res.status(400).json({ error: 'targetType, targetId и accessLevel обязательны' }); } if (!['user', 'role'].includes(targetType) || !['reader', 'editor', 'admin'].includes(accessLevel)) { return res.status(400).json({ error: 'Недопустимые значения targetType или accessLevel' }); } const rule = await storage.createDataTableAccessRule({ tableId, organizationId: req.organizationId!, targetType, targetId, accessLevel, }); res.json({ rule }); } catch (error) { console.error('Create table access rule error:', error); res.status(500).json({ error: 'Ошибка создания правила доступа' }); } } ); app.delete(['/api/tables/:tableId/access-rules/:ruleId', '/api/directories/:tableId/access-rules/:ruleId'], authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const tableId = parseInt(req.params.tableId); const ruleId = parseInt(req.params.ruleId); const table = await storage.getDataTable(tableId, req.organizationId!); if (!table) return res.status(404).json({ error: 'Таблица не найдена' }); const currentRole = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!); if (currentRole !== 'admin') { return res.status(403).json({ error: 'Нет прав для управления правами доступа' }); } await storage.deleteDataTableAccessRule(ruleId, req.organizationId!); res.json({ success: true }); } catch (error) { console.error('Delete table access rule error:', error); res.status(500).json({ error: 'Ошибка удаления правила доступа' }); } } ); app.patch(['/api/tables/:tableId/visibility', '/api/directories/:tableId/visibility'], authenticateToken, tenantIsolation, async (req: AuthenticatedRequest, res) => { try { const tableId = parseInt(req.params.tableId); const table = await storage.getDataTable(tableId, req.organizationId!); if (!table) return res.status(404).json({ error: 'Таблица не найдена' }); const currentRole = await storage.getUserTableRole(tableId, req.user!.id, table.createdBy, req.organizationId!); if (currentRole !== 'admin') { return res.status(403).json({ error: 'Нет прав для изменения настроек' }); } const { visibility } = req.body; if (!['restricted', 'organization'].includes(visibility)) { return res.status(400).json({ error: 'Недопустимое значение visibility' }); } const [updated] = await db.update(dataTables) .set({ visibility }) .where(and(eq(dataTables.id, tableId), eq(dataTables.organizationId, req.organizationId!))) .returning(); res.json({ table: updated }); } catch (error) { console.error('Update table visibility error:', error); res.status(500).json({ error: 'Ошибка изменения видимости' }); } } ); }