modified: backend/src/controllers/databaseManagementController.ts

modified:   backend/src/controllers/dynamicApiController.ts
	modified:   backend/src/controllers/endpointController.ts
	new file:   backend/src/migrations/005_add_aql_support.sql
	new file:   backend/src/services/AqlExecutor.ts
	modified:   backend/src/types/index.ts
	modified:   frontend/src/components/EndpointModal.tsx
	modified:   frontend/src/pages/Databases.tsx
	modified:   frontend/src/types/index.ts
This commit is contained in:
GEgorov
2025-10-07 19:33:50 +03:00
parent 7d8fddfe4f
commit 713e9ba7f7
9 changed files with 793 additions and 147 deletions

View File

@@ -0,0 +1,230 @@
import { QueryResult, DatabaseConfig } from '../types';
import { mainPool } from '../config/database';
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
): Promise<QueryResult> {
const startTime = Date.now();
try {
// Получаем конфигурацию БД
const dbConfig = await this.getDatabaseConfig(databaseId);
if (!dbConfig) {
throw new Error(`AQL database with id ${databaseId} not found or not configured`);
}
if (!dbConfig.aql_base_url) {
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 = `${dbConfig.aql_base_url}${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' && dbConfig.aql_auth_value) {
headers['Authorization'] = `Basic ${dbConfig.aql_auth_value}`;
} else if (dbConfig.aql_auth_type === 'bearer' && dbConfig.aql_auth_value) {
headers['Authorization'] = `Bearer ${dbConfig.aql_auth_value}`;
} else if (dbConfig.aql_auth_type === 'custom' && dbConfig.aql_auth_value) {
headers['Authorization'] = dbConfig.aql_auth_value;
}
// Добавляем кастомные заголовки из конфигурации БД
if (dbConfig.aql_headers) {
const customHeaders = typeof dbConfig.aql_headers === 'string'
? JSON.parse(dbConfig.aql_headers)
: dbConfig.aql_headers;
Object.assign(headers, customHeaders);
}
// Выполняем HTTP запрос
const response = await fetch(fullUrl, {
method: config.method,
headers,
body: processedBody,
});
const executionTime = Date.now() - startTime;
// Проверяем статус ответа
if (!response.ok) {
const errorText = await response.text();
throw new Error(`AQL API error (${response.status}): ${errorText}`);
}
// Парсим JSON ответ
const data = await response.json();
// Нормализуем ответ к формату QueryResult
let rows: any[];
let rowCount: number;
if (Array.isArray(data)) {
rows = data;
rowCount = data.length;
} else if (data && typeof data === 'object') {
// Если ответ - объект, оборачиваем его в массив
rows = [data];
rowCount = 1;
} else {
rows = [];
rowCount = 0;
}
return {
rows,
rowCount,
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): Promise<{ success: boolean; error?: string }> {
try {
const dbConfig = await this.getDatabaseConfig(databaseId);
if (!dbConfig) {
return { success: false, error: 'Database not found' };
}
if (!dbConfig.aql_base_url) {
return { success: false, error: 'AQL base URL not configured' };
}
// Пробуем выполнить простой запрос для проверки соединения
const response = await fetch(dbConfig.aql_base_url, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
});
if (response.ok || response.status === 404) {
// 404 тоже OK - это значит что сервер доступен
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();