import { Response } from 'express'; import { AuthRequest } from '../middleware/auth'; import { mainPool } from '../config/database'; import { ExportedEndpoint, ExportedScriptQuery, ScriptExecutionError, Environment } from '../types'; import { encryptEndpointData, decryptEndpointData } from '../services/endpointCrypto'; import { versionService } from '../services/VersionService'; export const getEndpoints = async (req: AuthRequest, res: Response) => { try { const { search, folder_id } = req.query; let query = ` SELECT e.*, f.name as folder_name FROM endpoints e LEFT JOIN folders f ON e.folder_id = f.id WHERE 1=1 `; const params: any[] = []; if (folder_id) { query += ` AND e.folder_id = $${params.length + 1}`; params.push(folder_id); } if (search) { const searchIndex = params.length + 1; query += ` AND ( e.name ILIKE $${searchIndex} OR e.description ILIKE $${searchIndex} OR e.sql_query ILIKE $${searchIndex} OR e.path ILIKE $${searchIndex} )`; params.push(`%${search}%`); } query += ` ORDER BY e.created_at DESC`; const result = await mainPool.query(query, params); res.json(result.rows); } catch (error) { console.error('Get endpoints error:', error); res.status(500).json({ error: 'Internal server error' }); } }; export const getEndpoint = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const result = await mainPool.query( `SELECT e.*, f.name as folder_name FROM endpoints e LEFT JOIN folders f ON e.folder_id = f.id WHERE e.id = $1`, [id] ); if (result.rows.length === 0) { return res.status(404).json({ error: 'Endpoint not found' }); } res.json(result.rows[0]); } catch (error) { console.error('Get endpoint error:', error); res.status(500).json({ error: 'Internal server error' }); } }; export const createEndpoint = async (req: AuthRequest, res: Response) => { try { const { name, description, method, path, database_id, sql_query, parameters, folder_id, is_public, enable_logging, execution_type, script_language, script_code, script_queries, aql_method, aql_endpoint, aql_body, aql_query_params, detailed_response, response_schema, } = req.body; if (!name || !method || !path) { return res.status(400).json({ error: 'Missing required fields' }); } const execType = execution_type || 'sql'; // Валидация для типа SQL if (execType === 'sql') { if (!database_id || !sql_query) { return res.status(400).json({ error: 'Database ID and SQL query are required for SQL execution type' }); } } // Валидация для типа Script if (execType === 'script') { if (!script_language || !script_code || !script_queries) { return res.status(400).json({ error: 'Script language, code, and queries are required for script execution type' }); } } // Валидация для типа AQL if (execType === 'aql') { if (!database_id || !aql_method || !aql_endpoint) { return res.status(400).json({ error: 'Database ID, AQL method, and AQL endpoint are required for AQL execution type' }); } } const result = await mainPool.query( `INSERT INTO endpoints ( name, description, method, path, database_id, sql_query, parameters, folder_id, user_id, is_public, enable_logging, execution_type, script_language, script_code, script_queries, aql_method, aql_endpoint, aql_body, aql_query_params, detailed_response, response_schema ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING *`, [ name, description || '', method, path, database_id || null, sql_query || '', JSON.stringify(parameters || []), folder_id || null, req.user!.id, is_public || false, enable_logging || false, execType, script_language || null, script_code || null, JSON.stringify(script_queries || []), aql_method || null, aql_endpoint || null, aql_body || null, JSON.stringify(aql_query_params || {}), detailed_response || false, response_schema ? JSON.stringify(response_schema) : null, ] ); res.status(201).json(result.rows[0]); } catch (error: any) { console.error('Create endpoint error:', error); if (error.code === '23505') { return res.status(400).json({ error: 'Endpoint path already exists' }); } res.status(500).json({ error: 'Internal server error' }); } }; export const updateEndpoint = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const { name, description, method, path, database_id, sql_query, parameters, folder_id, is_public, enable_logging, execution_type, script_language, script_code, script_queries, aql_method, aql_endpoint, aql_body, aql_query_params, detailed_response, response_schema, } = req.body; const result = await mainPool.query( `UPDATE endpoints SET name = $1, description = $2, method = $3, path = $4, database_id = $5, sql_query = $6, parameters = $7, folder_id = $8, is_public = $9, enable_logging = $10, execution_type = $11, script_language = $12, script_code = $13, script_queries = $14, aql_method = $15, aql_endpoint = $16, aql_body = $17, aql_query_params = $18, detailed_response = $19, response_schema = $20, updated_at = CURRENT_TIMESTAMP WHERE id = $21 RETURNING *`, [ name, description, method, path, database_id || null, sql_query, parameters ? JSON.stringify(parameters) : null, folder_id || null, is_public, enable_logging, execution_type, script_language || null, script_code || null, script_queries ? JSON.stringify(script_queries) : null, aql_method || null, aql_endpoint || null, aql_body || null, aql_query_params ? JSON.stringify(aql_query_params) : null, detailed_response || false, response_schema ? JSON.stringify(response_schema) : null, id, ] ); if (result.rows.length === 0) { return res.status(404).json({ error: 'Endpoint not found' }); } // Auto-create published version try { await versionService.createVersionFromEndpoint( id, req.user!.id, req.body.change_message || 'Updated', 'published' ); } catch (vErr) { console.error('Auto-version creation failed:', vErr); } res.json(result.rows[0]); } catch (error: any) { console.error('Update endpoint error:', error); if (error.code === '23505') { return res.status(400).json({ error: 'Endpoint path already exists' }); } res.status(500).json({ error: 'Internal server error' }); } }; export const deleteEndpoint = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const result = await mainPool.query( 'DELETE FROM endpoints WHERE id = $1 RETURNING id', [id] ); if (result.rows.length === 0) { return res.status(404).json({ error: 'Endpoint not found' }); } res.json({ message: 'Endpoint deleted successfully' }); } catch (error) { console.error('Delete endpoint error:', error); res.status(500).json({ error: 'Internal server error' }); } }; export const testEndpoint = async (req: AuthRequest, res: Response) => { try { const { database_id, sql_query, parameters, endpoint_parameters, execution_type, script_language, script_code, script_queries, aql_method, aql_endpoint, aql_body, aql_query_params, environment: reqEnv, } = req.body; const environment: Environment = reqEnv === 'prod' ? 'prod' : 'test'; const execType = execution_type || 'sql'; if (execType === 'sql') { if (!database_id) { return res.status(400).json({ error: 'Missing database_id for SQL execution' }); } if (!sql_query) { return res.status(400).json({ error: 'Missing sql_query' }); } // Преобразуем именованные параметры ($paramName) в позиционные ($1, $2, $3...) let processedQuery = sql_query; if (endpoint_parameters && Array.isArray(endpoint_parameters)) { endpoint_parameters.forEach((param: any, index: number) => { const paramName = param.name; const position = index + 1; // Заменяем все вхождения $paramName на $position const regex = new RegExp(`\\$${paramName}\\b`, 'g'); processedQuery = processedQuery.replace(regex, `$${position}`); }); } // eslint-disable-next-line @typescript-eslint/no-var-requires const { sqlExecutor } = require('../services/SqlExecutor'); const result = await sqlExecutor.executeQuery(database_id, processedQuery, parameters || [], environment); res.json({ success: true, data: result.rows, rowCount: result.rowCount, executionTime: result.executionTime, logs: [ { type: 'info', message: `Query executed in ${result.executionTime}ms, returned ${result.rowCount} rows`, timestamp: Date.now() }, ], queries: [], processedQuery, }); } else if (execType === 'script') { if (!script_language || !script_code) { return res.status(400).json({ error: 'Missing script_language or script_code' }); } // Собираем параметры из тестовых значений const requestParams: Record = {}; if (endpoint_parameters && Array.isArray(endpoint_parameters) && parameters && Array.isArray(parameters)) { endpoint_parameters.forEach((param: any, index: number) => { requestParams[param.name] = parameters[index]; }); } // eslint-disable-next-line @typescript-eslint/no-var-requires const { scriptExecutor } = require('../services/ScriptExecutor'); const scriptResult = await scriptExecutor.execute(script_language, script_code, { databaseId: database_id, scriptQueries: script_queries || [], requestParams, endpointParameters: endpoint_parameters || [], environment, }); res.json({ success: true, data: scriptResult.result, logs: scriptResult.logs, queries: scriptResult.queries, }); } else if (execType === 'aql') { if (!database_id) { return res.status(400).json({ error: 'Missing database_id for AQL execution' }); } if (!aql_method || !aql_endpoint) { return res.status(400).json({ error: 'Missing aql_method or aql_endpoint' }); } // Собираем параметры из тестовых значений const requestParams: Record = {}; if (endpoint_parameters && Array.isArray(endpoint_parameters) && parameters && Array.isArray(parameters)) { endpoint_parameters.forEach((param: any, index: number) => { requestParams[param.name] = parameters[index]; }); } // eslint-disable-next-line @typescript-eslint/no-var-requires const { aqlExecutor } = require('../services/AqlExecutor'); const result = await aqlExecutor.executeAqlQuery(database_id, { method: aql_method, endpoint: aql_endpoint, body: aql_body, queryParams: aql_query_params, parameters: requestParams, }, environment); res.json({ success: true, data: result.rows, rowCount: result.rowCount, executionTime: result.executionTime, logs: [ { type: 'info', message: `AQL ${aql_method} ${aql_endpoint} executed in ${result.executionTime}ms`, timestamp: Date.now() }, ], queries: [], }); } else { return res.status(400).json({ error: 'Invalid execution_type' }); } } catch (error: any) { const isScriptError = error instanceof ScriptExecutionError; res.status(400).json({ success: false, error: error.message, detail: error.detail || undefined, hint: error.hint || undefined, logs: isScriptError ? error.logs : [], queries: isScriptError ? error.queries : [], }); } }; export const exportEndpoint = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const endpointResult = await mainPool.query( 'SELECT * FROM endpoints WHERE id = $1', [id] ); if (endpointResult.rows.length === 0) { return res.status(404).json({ error: 'Endpoint not found' }); } const endpoint = endpointResult.rows[0]; // Resolve database_id -> name & type let databaseName: string | null = null; let databaseType: string | null = null; if (endpoint.database_id) { const dbResult = await mainPool.query( 'SELECT name, type FROM databases WHERE id = $1', [endpoint.database_id] ); if (dbResult.rows.length > 0) { databaseName = dbResult.rows[0].name; databaseType = dbResult.rows[0].type; } } // Resolve folder_id -> name let folderName: string | null = null; if (endpoint.folder_id) { const folderResult = await mainPool.query( 'SELECT name FROM folders WHERE id = $1', [endpoint.folder_id] ); if (folderResult.rows.length > 0) { folderName = folderResult.rows[0].name; } } // Resolve database_ids in script_queries const scriptQueries = endpoint.script_queries || []; const exportedScriptQueries: ExportedScriptQuery[] = []; for (const sq of scriptQueries) { let sqDbName: string | undefined; let sqDbType: string | undefined; if (sq.database_id) { const sqDbResult = await mainPool.query( 'SELECT name, type FROM databases WHERE id = $1', [sq.database_id] ); if (sqDbResult.rows.length > 0) { sqDbName = sqDbResult.rows[0].name; sqDbType = sqDbResult.rows[0].type; } } exportedScriptQueries.push({ name: sq.name, sql: sq.sql, database_name: sqDbName, database_type: sqDbType, aql_method: sq.aql_method, aql_endpoint: sq.aql_endpoint, aql_body: sq.aql_body, aql_query_params: sq.aql_query_params, }); } const exportData: ExportedEndpoint = { _format: 'kabe_v1', name: endpoint.name, description: endpoint.description || '', method: endpoint.method, path: endpoint.path, execution_type: endpoint.execution_type || 'sql', database_name: databaseName, database_type: databaseType, sql_query: endpoint.sql_query || '', parameters: endpoint.parameters || [], script_language: endpoint.script_language || null, script_code: endpoint.script_code || null, script_queries: exportedScriptQueries, aql_method: endpoint.aql_method || null, aql_endpoint: endpoint.aql_endpoint || null, aql_body: endpoint.aql_body || null, aql_query_params: endpoint.aql_query_params || null, is_public: endpoint.is_public || false, enable_logging: endpoint.enable_logging || false, detailed_response: endpoint.detailed_response || false, response_schema: endpoint.response_schema || null, folder_name: folderName, }; const encrypted = encryptEndpointData(exportData); const safeFileName = endpoint.name.replace(/[^a-zA-Z0-9_-]/g, '_'); const encodedFileName = encodeURIComponent(endpoint.name.replace(/[/\\:*?"<>|]/g, '_')) + '.kabe'; res.setHeader('Content-Type', 'application/octet-stream'); res.setHeader('Content-Disposition', `attachment; filename="${safeFileName}.kabe"; filename*=UTF-8''${encodedFileName}`); res.send(encrypted); } catch (error) { console.error('Export endpoint error:', error); res.status(500).json({ error: 'Internal server error' }); } }; export const importPreview = async (req: AuthRequest, res: Response) => { try { const buffer = req.body as Buffer; if (!buffer || buffer.length === 0) { return res.status(400).json({ error: 'No file uploaded' }); } let exportData: ExportedEndpoint; try { exportData = decryptEndpointData(buffer) as ExportedEndpoint; } catch (err) { return res.status(400).json({ error: 'Invalid or corrupted .kabe file' }); } if (exportData._format !== 'kabe_v1') { return res.status(400).json({ error: 'Unsupported file format version' }); } // Collect all referenced database names const referencedDatabases: { name: string; type: string }[] = []; if (exportData.database_name) { referencedDatabases.push({ name: exportData.database_name, type: exportData.database_type || 'unknown', }); } for (const sq of exportData.script_queries || []) { if (sq.database_name && !referencedDatabases.find(d => d.name === sq.database_name)) { referencedDatabases.push({ name: sq.database_name, type: sq.database_type || 'unknown', }); } } // Check which databases exist locally const localDatabases = await mainPool.query( 'SELECT id, name, type FROM databases WHERE is_active = true' ); const databaseMapping = referencedDatabases.map(ref => { const found = localDatabases.rows.find( (db: any) => db.name === ref.name && db.type === ref.type ); return { name: ref.name, type: ref.type, found: !!found, local_id: found?.id || null, }; }); // Check folder let folder: { name: string; found: boolean; local_id: string | null } | null = null; if (exportData.folder_name) { const folderResult = await mainPool.query( 'SELECT id FROM folders WHERE name = $1', [exportData.folder_name] ); folder = { name: exportData.folder_name, found: folderResult.rows.length > 0, local_id: folderResult.rows.length > 0 ? folderResult.rows[0].id : null, }; } // Check if path already exists const pathCheck = await mainPool.query( 'SELECT id FROM endpoints WHERE path = $1', [exportData.path] ); res.json({ endpoint: { name: exportData.name, description: exportData.description, method: exportData.method, path: exportData.path, execution_type: exportData.execution_type, is_public: exportData.is_public, enable_logging: exportData.enable_logging, detailed_response: exportData.detailed_response, folder_name: exportData.folder_name, }, databases: databaseMapping, all_databases_found: databaseMapping.every(d => d.found), local_databases: localDatabases.rows.map((db: any) => ({ id: db.id, name: db.name, type: db.type, })), folder, path_exists: pathCheck.rows.length > 0, }); } catch (error) { console.error('Import preview error:', error); res.status(500).json({ error: 'Internal server error' }); } }; export const importEndpoint = async (req: AuthRequest, res: Response) => { try { const { file_data, database_mapping, folder_id, override_path, } = req.body; if (!file_data) { return res.status(400).json({ error: 'No file data provided' }); } const buffer = Buffer.from(file_data, 'base64'); let exportData: ExportedEndpoint; try { exportData = decryptEndpointData(buffer) as ExportedEndpoint; } catch (err) { return res.status(400).json({ error: 'Invalid or corrupted .kabe file' }); } // Resolve main database_id let databaseId: string | null = null; if (exportData.database_name) { const mappedId = database_mapping?.[exportData.database_name]; if (mappedId) { databaseId = mappedId; } else { const dbResult = await mainPool.query( 'SELECT id FROM databases WHERE name = $1 AND is_active = true', [exportData.database_name] ); if (dbResult.rows.length > 0) { databaseId = dbResult.rows[0].id; } else { return res.status(400).json({ error: `Database "${exportData.database_name}" not found and no mapping provided` }); } } } // Resolve script_queries database_ids const resolvedScriptQueries = []; for (const sq of exportData.script_queries || []) { let sqDatabaseId: string | undefined; if (sq.database_name) { const mappedId = database_mapping?.[sq.database_name]; if (mappedId) { sqDatabaseId = mappedId; } else { const sqDbResult = await mainPool.query( 'SELECT id FROM databases WHERE name = $1 AND is_active = true', [sq.database_name] ); if (sqDbResult.rows.length > 0) { sqDatabaseId = sqDbResult.rows[0].id; } else { return res.status(400).json({ error: `Database "${sq.database_name}" (script query "${sq.name}") not found and no mapping provided` }); } } } resolvedScriptQueries.push({ name: sq.name, sql: sq.sql, database_id: sqDatabaseId, aql_method: sq.aql_method, aql_endpoint: sq.aql_endpoint, aql_body: sq.aql_body, aql_query_params: sq.aql_query_params, }); } // Resolve folder let resolvedFolderId: string | null = folder_id || null; if (!resolvedFolderId && exportData.folder_name) { const folderResult = await mainPool.query( 'SELECT id FROM folders WHERE name = $1', [exportData.folder_name] ); if (folderResult.rows.length > 0) { resolvedFolderId = folderResult.rows[0].id; } } const finalPath = override_path || exportData.path; const result = await mainPool.query( `INSERT INTO endpoints ( name, description, method, path, database_id, sql_query, parameters, folder_id, user_id, is_public, enable_logging, execution_type, script_language, script_code, script_queries, aql_method, aql_endpoint, aql_body, aql_query_params, detailed_response, response_schema ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING *`, [ exportData.name, exportData.description || '', exportData.method, finalPath, databaseId, exportData.sql_query || '', JSON.stringify(exportData.parameters || []), resolvedFolderId, req.user!.id, exportData.is_public || false, exportData.enable_logging || false, exportData.execution_type || 'sql', exportData.script_language || null, exportData.script_code || null, JSON.stringify(resolvedScriptQueries), exportData.aql_method || null, exportData.aql_endpoint || null, exportData.aql_body || null, JSON.stringify(exportData.aql_query_params || {}), exportData.detailed_response || false, exportData.response_schema ? JSON.stringify(exportData.response_schema) : null, ] ); res.status(201).json(result.rows[0]); } catch (error: any) { console.error('Import endpoint error:', error); if (error.code === '23505') { return res.status(400).json({ error: 'Endpoint path already exists' }); } res.status(500).json({ error: 'Internal server error' }); } }; // Version management handlers export const getVersionHistory = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const versions = await versionService.getVersionHistory(id); res.json(versions); } catch (error) { console.error('Get version history error:', error); res.status(500).json({ error: 'Internal server error' }); } }; export const getVersion = async (req: AuthRequest, res: Response) => { try { const { versionId } = req.params; const version = await versionService.getVersion(versionId); if (!version) return res.status(404).json({ error: 'Version not found' }); res.json(version); } catch (error) { console.error('Get version error:', error); res.status(500).json({ error: 'Internal server error' }); } }; export const publishVersion = async (req: AuthRequest, res: Response) => { try { const { versionId } = req.params; await versionService.publishVersion(versionId, req.user!.id); res.json({ message: 'Version published' }); } catch (error: any) { console.error('Publish version error:', error); res.status(500).json({ error: error.message || 'Internal server error' }); } }; export const rollbackVersion = async (req: AuthRequest, res: Response) => { try { const { id, versionId } = req.params; const newVersion = await versionService.rollbackToVersion(id, versionId, req.user!.id); res.json(newVersion); } catch (error: any) { console.error('Rollback version error:', error); res.status(500).json({ error: error.message || 'Internal server error' }); } }; export const saveDraftVersion = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const draft = await versionService.saveDraft(id, req.body, req.user!.id, req.body.change_message); res.json(draft); } catch (error: any) { console.error('Save draft error:', error); res.status(500).json({ error: error.message || 'Internal server error' }); } }; export const getDraftVersion = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const draft = await versionService.getDraft(id); if (!draft) return res.status(404).json({ error: 'No draft found' }); res.json(draft); } catch (error) { console.error('Get draft error:', error); res.status(500).json({ error: 'Internal server error' }); } };