import { QueryResult, DatabaseConfig, Environment } from '../types'; import { mainPool } from '../config/database'; import { databasePoolManager } from './DatabasePoolManager'; interface AqlRequestConfig { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; endpoint: string; body?: string; queryParams?: Record; parameters?: Record; } export class AqlExecutor { /** * Получает конфигурацию AQL базы данных */ private async getDatabaseConfig(databaseId: string): Promise { const result = await mainPool.query( 'SELECT * FROM databases WHERE id = $1 AND type = $2', [databaseId, 'aql'] ); if (result.rows.length === 0) { return null; } return result.rows[0]; } /** * Заменяет параметры вида $paramName в строке на значения из объекта parameters */ private replaceParameters(template: string, parameters: Record): string { let result = template; // Находим все параметры вида $paramName const paramMatches = template.match(/\$\w+/g) || []; const uniqueParams = [...new Set(paramMatches.map(p => p.substring(1)))]; uniqueParams.forEach((paramName) => { const regex = new RegExp(`\\$${paramName}\\b`, 'g'); const value = parameters[paramName]; if (value !== undefined && value !== null) { // Для строк в JSON нужны кавычки, для чисел - нет const replacement = typeof value === 'string' ? value : JSON.stringify(value); result = result.replace(regex, replacement); } else { result = result.replace(regex, ''); } }); return result; } /** * Строит query string из объекта параметров */ private buildQueryString(params: Record, requestParams: Record): string { const processedParams: Record = {}; for (const [key, value] of Object.entries(params)) { processedParams[key] = this.replaceParameters(value, requestParams); } const queryString = new URLSearchParams(processedParams).toString(); return queryString ? `?${queryString}` : ''; } /** * Выполняет AQL запрос */ async executeAqlQuery( databaseId: string, config: AqlRequestConfig, environment: Environment = 'prod' ): Promise { const startTime = Date.now(); try { const dbConfig = await databasePoolManager.getDatabaseConfig(databaseId); if (!dbConfig) { throw new Error(`AQL database with id ${databaseId} not found or not configured`); } // Use test credentials when environment is test and test env is configured let baseUrl = dbConfig.aql_base_url; let authValue = dbConfig.aql_auth_value; let headers_config = dbConfig.aql_headers; if (environment === 'test' && dbConfig.has_test_env) { if (dbConfig.test_aql_base_url) baseUrl = dbConfig.test_aql_base_url; if (dbConfig.test_aql_auth_value) authValue = dbConfig.test_aql_auth_value; if (dbConfig.test_aql_headers) headers_config = dbConfig.test_aql_headers; } if (!baseUrl) { throw new Error(`AQL base URL not configured for database ${databaseId}`); } const parameters = config.parameters || {}; // Обрабатываем endpoint с параметрами const processedEndpoint = this.replaceParameters(config.endpoint, parameters); // Обрабатываем query параметры const queryString = config.queryParams ? this.buildQueryString(config.queryParams, parameters) : ''; // Формируем полный URL const fullUrl = `${baseUrl}${processedEndpoint}${queryString}`; // Обрабатываем body с параметрами let processedBody: string | undefined; if (config.body) { processedBody = this.replaceParameters(config.body, parameters); } // Формируем заголовки const headers: Record = { 'Content-Type': 'application/json', 'Accept': 'application/json', }; // Добавляем аутентификацию if (dbConfig.aql_auth_type === 'basic' && authValue) { headers['Authorization'] = `Basic ${authValue}`; } else if (dbConfig.aql_auth_type === 'bearer' && authValue) { headers['Authorization'] = `Bearer ${authValue}`; } else if (dbConfig.aql_auth_type === 'custom' && authValue) { headers['Authorization'] = authValue; } // Добавляем кастомные заголовки из конфигурации БД if (headers_config) { const customHeaders = typeof headers_config === 'string' ? JSON.parse(headers_config) : headers_config; Object.assign(headers, customHeaders); } // Логируем запрос console.log('\n=== AQL Request ==='); console.log('URL:', fullUrl); console.log('Method:', config.method); console.log('Headers:', JSON.stringify(headers, null, 2)); console.log('Body:', processedBody || '(no body)'); console.log('===================\n'); // Формируем опции для fetch // ВАЖНО: body не должен передаваться для GET запросов // и если он undefined, иначе fetch может изменить метод на GET const fetchOptions: RequestInit = { method: config.method, headers, }; // Добавляем body только если он есть и метод поддерживает body if (processedBody && config.method !== 'GET') { fetchOptions.body = processedBody; } // Выполняем HTTP запрос const response = await fetch(fullUrl, fetchOptions); const executionTime = Date.now() - startTime; // Проверяем статус ответа if (!response.ok) { const errorText = await response.text(); console.log('\n=== AQL Error Response ==='); console.log('Status:', response.status); console.log('Response:', errorText); console.log('==========================\n'); throw new Error(`AQL API error (${response.status}): ${errorText}`); } // Обрабатываем пустой ответ (204 No Content) if (response.status === 204) { console.log('\n=== AQL Response ==='); console.log('Status: 204 (No Content)'); console.log('====================\n'); return { rows: [], rowCount: 0, executionTime, }; } // Парсим JSON ответ const responseText = await response.text(); console.log('\n=== AQL Response ==='); console.log('Status:', response.status); console.log('Raw Response:', responseText); console.log('====================\n'); // Если ответ пустой, возвращаем пустой результат if (!responseText || responseText.trim() === '') { return { rows: [], rowCount: 0, executionTime, }; } let data; try { data = JSON.parse(responseText); } catch (e) { console.error('Failed to parse JSON response:', e); throw new Error(`Invalid JSON response: ${responseText.substring(0, 200)}`); } // Возвращаем данные как есть return { rows: data, rowCount: Array.isArray(data) ? data.length : (data ? 1 : 0), executionTime, }; } catch (error: any) { console.error('AQL Execution Error:', error); throw new Error(`AQL Error: ${error.message}`); } } /** * Тестирует AQL запрос */ async testAqlQuery( databaseId: string, config: AqlRequestConfig ): Promise<{ success: boolean; error?: string }> { try { await this.executeAqlQuery(databaseId, config); return { success: true }; } catch (error: any) { return { success: false, error: error.message }; } } /** * Тестирует подключение к AQL базе */ async testConnection(databaseId: string, environment: Environment = 'prod'): Promise<{ success: boolean; error?: string }> { try { const dbConfig = await databasePoolManager.getDatabaseConfig(databaseId); if (!dbConfig) { return { success: false, error: 'Database not found' }; } let testUrl = dbConfig.aql_base_url; if (environment === 'test' && dbConfig.has_test_env && dbConfig.test_aql_base_url) { testUrl = dbConfig.test_aql_base_url; } if (!testUrl) { return { success: false, error: 'AQL base URL not configured' }; } const response = await fetch(testUrl, { method: 'GET', headers: { 'Accept': 'application/json' }, }); if (response.ok || response.status === 404) { return { success: true }; } else { return { success: false, error: `HTTP ${response.status}` }; } } catch (error: any) { return { success: false, error: error.message }; } } } export const aqlExecutor = new AqlExecutor();