Files
api_builder/backend/src/services/AqlExecutor.ts
eshmeshek e9032001bd Add test/prod environments for databases
- Migration 010: test_* columns on databases table, environment column on request_logs
- DatabasePoolManager: dual-pool strategy (prod + test), getPool(id, env) with fallback
- All executors (SQL, Script, AQL): environment param threaded through execution paths
- dynamicApiController: X-Environment header detection, environment in logging
- databaseManagementController: CRUD for test credentials, testConnection with ?env=test
- Frontend: test env form in DatabaseModal, env toggle in EndpointEditor test panel

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 08:28:28 +03:00

283 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, string>;
parameters?: Record<string, any>;
}
export class AqlExecutor {
/**
* Получает конфигурацию AQL базы данных
*/
private async getDatabaseConfig(databaseId: string): Promise<DatabaseConfig | null> {
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, any>): 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<string, string>, requestParams: Record<string, any>): string {
const processedParams: Record<string, string> = {};
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<QueryResult> {
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<string, string> = {
'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();