import { Response } from 'express'; import { ApiKeyRequest } from '../middleware/apiKey'; import { mainPool } from '../config/database'; import { sqlExecutor } from '../services/SqlExecutor'; import { scriptExecutor } from '../services/ScriptExecutor'; import { EndpointParameter, ScriptQuery, ScriptExecutionError, Environment } from '../types'; export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response) => { const startTime = Date.now(); let shouldLog = false; let endpointId: string | null = null; try { const environment: Environment = (req.headers['x-environment'] as string)?.toLowerCase() === 'test' ? 'test' : 'prod'; // Extract the path from the request (remove /api/v1 prefix) const requestPath = req.path; // This already has the path without /api/v1 const requestMethod = req.method.toUpperCase(); // Fetch endpoint configuration by path and method const endpointResult = await mainPool.query( 'SELECT * FROM endpoints WHERE path = $1 AND method = $2', [requestPath, requestMethod] ); if (endpointResult.rows.length === 0) { return res.status(404).json({ error: 'Endpoint not found', path: requestPath, method: requestMethod }); } let endpoint = endpointResult.rows[0]; endpointId = endpoint.id; // If test environment requested and a draft exists, serve draft content if (environment === 'test' && endpoint.draft_version_id) { const draftResult = await mainPool.query( `SELECT * FROM endpoint_versions WHERE id = $1 AND status = 'draft'`, [endpoint.draft_version_id] ); if (draftResult.rows.length > 0) { const draft = draftResult.rows[0]; endpoint = { ...endpoint, ...draft, id: endpoint.id, folder_id: endpoint.folder_id, user_id: endpoint.user_id }; } } // Check if logging is enabled (on endpoint OR on API key, but log only once) const endpointLogging = endpoint.enable_logging || false; const apiKeyLogging = req.apiKey?.enable_logging || false; shouldLog = endpointLogging || apiKeyLogging; // Check if endpoint is public or if API key has permission if (!endpoint.is_public) { if (!req.apiKey) { return res.status(401).json({ error: 'API key required for this endpoint' }); } let hasPermission = req.apiKey.permissions.includes(endpointId!) || req.apiKey.permissions.includes('*'); // If no direct permission, check folder permissions if (!hasPermission && endpoint.folder_id) { // Check if this folder or any parent folder has permission let currentFolderId: string | null = endpoint.folder_id; while (currentFolderId && !hasPermission) { if (req.apiKey.permissions.includes(`folder:${currentFolderId}`)) { hasPermission = true; break; } // Get parent folder const folderResult = await mainPool.query( 'SELECT parent_id FROM folders WHERE id = $1', [currentFolderId] ); currentFolderId = folderResult.rows.length > 0 ? folderResult.rows[0].parent_id : null; } } if (!hasPermission) { return res.status(403).json({ error: 'Access denied to this endpoint' }); } } // Parse parameters - PostgreSQL может вернуть как JSON строку, так и уже распарсенный объект let parameters: EndpointParameter[] = []; if (endpoint.parameters) { if (typeof endpoint.parameters === 'string') { try { parameters = JSON.parse(endpoint.parameters); } catch (e) { parameters = []; } } else if (Array.isArray(endpoint.parameters)) { parameters = endpoint.parameters; } } // Build request parameters object const requestParams: Record = {}; // Extract and validate parameters from request for (const param of parameters) { let value; if (param.in === 'query') { value = req.query[param.name]; } else if (param.in === 'body') { value = req.body[param.name]; } else if (param.in === 'path') { value = req.params[param.name]; } // Use default value if not provided if (value === undefined || value === null) { if (param.required) { return res.status(400).json({ error: `Missing required parameter: ${param.name}`, }); } value = param.default_value; } // Type conversion if (value !== undefined && value !== null) { switch (param.type) { case 'number': value = Number(value); if (isNaN(value)) { return res.status(400).json({ error: `Parameter ${param.name} must be a number`, }); } break; case 'boolean': value = value === 'true' || value === true; break; case 'date': value = new Date(value); if (isNaN(value.getTime())) { return res.status(400).json({ error: `Parameter ${param.name} must be a valid date`, }); } break; } } requestParams[param.name] = value; } let result; const executionType = endpoint.execution_type || 'sql'; if (executionType === 'aql') { // Execute AQL query const aqlMethod = endpoint.aql_method; const aqlEndpoint = endpoint.aql_endpoint; const aqlBody = endpoint.aql_body; let aqlQueryParams: Record = {}; if (endpoint.aql_query_params) { if (typeof endpoint.aql_query_params === 'string') { try { aqlQueryParams = JSON.parse(endpoint.aql_query_params); } catch (e) { aqlQueryParams = {}; } } else if (typeof endpoint.aql_query_params === 'object') { aqlQueryParams = endpoint.aql_query_params; } } if (!aqlMethod || !aqlEndpoint) { return res.status(500).json({ error: 'AQL configuration is incomplete' }); } const { aqlExecutor } = require('../services/AqlExecutor'); result = await aqlExecutor.executeAqlQuery(endpoint.database_id, { method: aqlMethod, endpoint: aqlEndpoint, body: aqlBody, queryParams: aqlQueryParams, parameters: requestParams, }, environment); } else if (executionType === 'script') { // Execute script const scriptLanguage = endpoint.script_language; const scriptCode = endpoint.script_code; let scriptQueries: ScriptQuery[] = []; if (endpoint.script_queries) { if (typeof endpoint.script_queries === 'string') { try { scriptQueries = JSON.parse(endpoint.script_queries); } catch (e) { scriptQueries = []; } } else if (Array.isArray(endpoint.script_queries)) { scriptQueries = endpoint.script_queries; } } if (!scriptLanguage || !scriptCode) { return res.status(500).json({ error: 'Script configuration is incomplete' }); } const scriptResult = await scriptExecutor.execute(scriptLanguage, scriptCode, { databaseId: endpoint.database_id, scriptQueries, requestParams, endpointParameters: parameters, environment, }); result = { rows: scriptResult.result, rowCount: 0, executionTime: 0, scriptLogs: scriptResult.logs, scriptQueries: scriptResult.queries, }; } else { // Execute SQL query const queryParams: any[] = []; parameters.forEach((param) => { queryParams.push(requestParams[param.name]); }); // Преобразуем именованные параметры ($paramName) в позиционные ($1, $2, $3...) let processedQuery = endpoint.sql_query; parameters.forEach((param, index) => { const paramName = param.name; const position = index + 1; // Заменяем все вхождения $paramName на $position const regex = new RegExp(`\\$${paramName}\\b`, 'g'); processedQuery = processedQuery.replace(regex, `$${position}`); }); result = await sqlExecutor.executeQuery( endpoint.database_id, processedQuery, queryParams, environment ); } // Build response based on detailed_response flag const detailedResponse = endpoint.detailed_response || false; const responseData = detailedResponse ? { success: true, data: result.rows, rowCount: result.rowCount, executionTime: result.executionTime, ...(result.scriptLogs && result.scriptLogs.length > 0 ? { logs: result.scriptLogs } : {}), ...(result.scriptQueries && result.scriptQueries.length > 0 ? { queries: result.scriptQueries } : {}), } : result.rows; // Log if needed if (shouldLog && endpointId) { const executionTime = Date.now() - startTime; await logRequest({ endpoint_id: endpointId, api_key_id: req.apiKey?.id || null, method: req.method, path: req.path, request_params: req.query || {}, request_body: req.body || {}, response_status: 200, response_data: responseData, execution_time: executionTime, error_message: null, ip_address: req.ip || req.socket.remoteAddress || 'unknown', user_agent: req.headers['user-agent'] || 'unknown', environment, }); } res.json(responseData); } catch (error: any) { console.error('Dynamic API execution error:', error); const isScriptError = error instanceof ScriptExecutionError; const errorResponse: any = { success: false, error: error.message, ...(isScriptError && error.logs.length > 0 ? { logs: error.logs } : {}), ...(isScriptError && error.queries.length > 0 ? { queries: error.queries } : {}), }; // Log error if needed if (shouldLog && endpointId) { const executionTime = Date.now() - startTime; await logRequest({ endpoint_id: endpointId, api_key_id: req.apiKey?.id || null, method: req.method, path: req.path, request_params: req.query || {}, request_body: req.body || {}, response_status: 500, response_data: errorResponse, execution_time: executionTime, error_message: error.message, ip_address: req.ip || req.socket.remoteAddress || 'unknown', user_agent: req.headers['user-agent'] || 'unknown', environment: (req.headers['x-environment'] as string)?.toLowerCase() === 'test' ? 'test' : 'prod', }); } res.status(500).json(errorResponse); } }; async function logRequest(data: { endpoint_id: string; api_key_id: string | null; method: string; path: string; request_params: any; request_body: any; response_status: number; response_data: any; execution_time: number; error_message: string | null; ip_address: string; user_agent: string; environment?: string; }) { try { await mainPool.query( `INSERT INTO request_logs ( endpoint_id, api_key_id, method, path, request_params, request_body, response_status, response_data, execution_time, error_message, ip_address, user_agent, environment ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`, [ data.endpoint_id, data.api_key_id, data.method, data.path, JSON.stringify(data.request_params), JSON.stringify(data.request_body), data.response_status, JSON.stringify(data.response_data), data.execution_time, data.error_message, data.ip_address, data.user_agent, data.environment || 'prod', ] ); } catch (error) { console.error('Failed to log request:', error); } }