- 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>
283 lines
9.3 KiB
TypeScript
283 lines
9.3 KiB
TypeScript
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();
|