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:
@@ -7,7 +7,10 @@ import { databasePoolManager } from '../services/DatabasePoolManager';
|
||||
export const getDatabases = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const result = await mainPool.query(
|
||||
'SELECT id, name, type, host, port, database_name, username, ssl, is_active, created_at, updated_at FROM databases ORDER BY name'
|
||||
`SELECT id, name, type, host, port, database_name, username, ssl, is_active,
|
||||
aql_base_url, aql_auth_type, aql_auth_value, aql_headers,
|
||||
created_at, updated_at
|
||||
FROM databases ORDER BY name`
|
||||
);
|
||||
|
||||
res.json(result.rows);
|
||||
@@ -22,7 +25,10 @@ export const getDatabase = async (req: AuthRequest, res: Response) => {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await mainPool.query(
|
||||
'SELECT id, name, type, host, port, database_name, username, ssl, is_active, created_at, updated_at FROM databases WHERE id = $1',
|
||||
`SELECT id, name, type, host, port, database_name, username, ssl, is_active,
|
||||
aql_base_url, aql_auth_type, aql_auth_value, aql_headers,
|
||||
created_at, updated_at
|
||||
FROM databases WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
|
||||
@@ -39,26 +45,58 @@ export const getDatabase = async (req: AuthRequest, res: Response) => {
|
||||
|
||||
export const createDatabase = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { name, type, host, port, database_name, username, password, ssl } = req.body;
|
||||
const {
|
||||
name, type, host, port, database_name, username, password, ssl,
|
||||
aql_base_url, aql_auth_type, aql_auth_value, aql_headers
|
||||
} = req.body;
|
||||
|
||||
const dbType = type || 'postgresql';
|
||||
|
||||
// Валидация для обычных БД
|
||||
if (dbType !== 'aql') {
|
||||
if (!name || !host || !port || !database_name || !username || !password) {
|
||||
return res.status(400).json({ error: 'Не заполнены обязательные поля' });
|
||||
}
|
||||
} else {
|
||||
// Валидация для AQL
|
||||
if (!name || !aql_base_url) {
|
||||
return res.status(400).json({ error: 'Не заполнены обязательные поля для AQL базы' });
|
||||
}
|
||||
}
|
||||
|
||||
const result = await mainPool.query(
|
||||
`INSERT INTO databases (name, type, host, port, database_name, username, password, ssl, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true)
|
||||
`INSERT INTO databases (
|
||||
name, type, host, port, database_name, username, password, ssl, is_active,
|
||||
aql_base_url, aql_auth_type, aql_auth_value, aql_headers
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true, $9, $10, $11, $12)
|
||||
RETURNING *`,
|
||||
[name, type || 'postgresql', host, port, database_name, username, password, ssl || false]
|
||||
[
|
||||
name,
|
||||
dbType,
|
||||
host || '',
|
||||
port || 0,
|
||||
database_name || '',
|
||||
username || '',
|
||||
password || '',
|
||||
ssl || false,
|
||||
aql_base_url || null,
|
||||
aql_auth_type || null,
|
||||
aql_auth_value || null,
|
||||
aql_headers ? JSON.stringify(aql_headers) : null
|
||||
]
|
||||
);
|
||||
|
||||
const newDb = result.rows[0];
|
||||
|
||||
// Добавить пул подключений
|
||||
// Добавить пул подключений (только для не-AQL баз)
|
||||
if (dbType !== 'aql') {
|
||||
await databasePoolManager.reloadPool(newDb.id);
|
||||
}
|
||||
|
||||
// Не возвращаем пароль
|
||||
delete newDb.password;
|
||||
delete newDb.aql_auth_value; // Также не возвращаем auth value
|
||||
|
||||
res.status(201).json(newDb);
|
||||
} catch (error: any) {
|
||||
@@ -73,13 +111,16 @@ export const createDatabase = async (req: AuthRequest, res: Response) => {
|
||||
export const updateDatabase = async (req: AuthRequest, res: Response) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, type, host, port, database_name, username, password, ssl, is_active } = req.body;
|
||||
const {
|
||||
name, type, host, port, database_name, username, password, ssl, is_active,
|
||||
aql_base_url, aql_auth_type, aql_auth_value, aql_headers
|
||||
} = req.body;
|
||||
|
||||
// Если пароль не передан, не обновляем его
|
||||
// Если пароль/auth не передан, не обновляем его
|
||||
let query;
|
||||
let params;
|
||||
|
||||
if (password) {
|
||||
if (password || aql_auth_value) {
|
||||
query = `
|
||||
UPDATE databases
|
||||
SET name = COALESCE($1, name),
|
||||
@@ -88,14 +129,22 @@ export const updateDatabase = async (req: AuthRequest, res: Response) => {
|
||||
port = COALESCE($4, port),
|
||||
database_name = COALESCE($5, database_name),
|
||||
username = COALESCE($6, username),
|
||||
password = $7,
|
||||
password = COALESCE($7, password),
|
||||
ssl = COALESCE($8, ssl),
|
||||
is_active = COALESCE($9, is_active),
|
||||
aql_base_url = COALESCE($10, aql_base_url),
|
||||
aql_auth_type = COALESCE($11, aql_auth_type),
|
||||
aql_auth_value = COALESCE($12, aql_auth_value),
|
||||
aql_headers = COALESCE($13, aql_headers),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $10
|
||||
RETURNING id, name, type, host, port, database_name, username, ssl, is_active, created_at, updated_at
|
||||
WHERE id = $14
|
||||
RETURNING id, name, type, host, port, database_name, username, ssl, is_active,
|
||||
aql_base_url, aql_auth_type, aql_headers, created_at, updated_at
|
||||
`;
|
||||
params = [name, type, host, port, database_name, username, password, ssl, is_active, id];
|
||||
params = [
|
||||
name, type, host, port, database_name, username, password || aql_auth_value, ssl, is_active,
|
||||
aql_base_url, aql_auth_type, aql_auth_value, aql_headers ? JSON.stringify(aql_headers) : null, id
|
||||
];
|
||||
} else {
|
||||
query = `
|
||||
UPDATE databases
|
||||
@@ -107,11 +156,18 @@ export const updateDatabase = async (req: AuthRequest, res: Response) => {
|
||||
username = COALESCE($6, username),
|
||||
ssl = COALESCE($7, ssl),
|
||||
is_active = COALESCE($8, is_active),
|
||||
aql_base_url = COALESCE($9, aql_base_url),
|
||||
aql_auth_type = COALESCE($10, aql_auth_type),
|
||||
aql_headers = COALESCE($11, aql_headers),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $9
|
||||
RETURNING id, name, type, host, port, database_name, username, ssl, is_active, created_at, updated_at
|
||||
WHERE id = $12
|
||||
RETURNING id, name, type, host, port, database_name, username, ssl, is_active,
|
||||
aql_base_url, aql_auth_type, aql_headers, created_at, updated_at
|
||||
`;
|
||||
params = [name, type, host, port, database_name, username, ssl, is_active, id];
|
||||
params = [
|
||||
name, type, host, port, database_name, username, ssl, is_active,
|
||||
aql_base_url, aql_auth_type, aql_headers ? JSON.stringify(aql_headers) : null, id
|
||||
];
|
||||
}
|
||||
|
||||
const result = await mainPool.query(query, params);
|
||||
@@ -120,8 +176,10 @@ export const updateDatabase = async (req: AuthRequest, res: Response) => {
|
||||
return res.status(404).json({ error: 'База данных не найдена' });
|
||||
}
|
||||
|
||||
// Перезагрузить пул
|
||||
// Перезагрузить пул (только для не-AQL баз)
|
||||
if (result.rows[0].type !== 'aql') {
|
||||
await databasePoolManager.reloadPool(id);
|
||||
}
|
||||
|
||||
res.json(result.rows[0]);
|
||||
} catch (error: any) {
|
||||
@@ -172,12 +230,33 @@ export const testDatabaseConnection = async (req: AuthRequest, res: Response) =>
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
// Получаем тип БД
|
||||
const dbResult = await mainPool.query('SELECT type FROM databases WHERE id = $1', [id]);
|
||||
|
||||
if (dbResult.rows.length === 0) {
|
||||
return res.status(404).json({ success: false, error: 'База данных не найдена' });
|
||||
}
|
||||
|
||||
const dbType = dbResult.rows[0].type;
|
||||
|
||||
if (dbType === 'aql') {
|
||||
// Для AQL используем aqlExecutor
|
||||
const { aqlExecutor } = require('../services/AqlExecutor');
|
||||
const result = await aqlExecutor.testConnection(id);
|
||||
|
||||
res.json({
|
||||
success: result.success,
|
||||
message: result.success ? 'Подключение успешно' : result.error || 'Ошибка подключения',
|
||||
});
|
||||
} else {
|
||||
// Для обычных БД используем databasePoolManager
|
||||
const isConnected = await databasePoolManager.testConnection(id);
|
||||
|
||||
res.json({
|
||||
success: isConnected,
|
||||
message: isConnected ? 'Подключение успешно' : 'Ошибка подключения',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Test connection error:', error);
|
||||
res.status(500).json({ error: 'Ошибка тестирования подключения' });
|
||||
|
||||
@@ -141,7 +141,38 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
|
||||
let result;
|
||||
const executionType = endpoint.execution_type || 'sql';
|
||||
|
||||
if (executionType === 'script') {
|
||||
if (executionType === 'aql') {
|
||||
// Execute AQL query
|
||||
const aqlMethod = endpoint.aql_method;
|
||||
const aqlEndpoint = endpoint.aql_endpoint;
|
||||
const aqlBody = endpoint.aql_body;
|
||||
|
||||
let aqlQueryParams: Record<string, string> = {};
|
||||
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,
|
||||
});
|
||||
} else if (executionType === 'script') {
|
||||
// Execute script
|
||||
const scriptLanguage = endpoint.script_language;
|
||||
const scriptCode = endpoint.script_code;
|
||||
|
||||
@@ -81,6 +81,10 @@ export const createEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
script_language,
|
||||
script_code,
|
||||
script_queries,
|
||||
aql_method,
|
||||
aql_endpoint,
|
||||
aql_body,
|
||||
aql_query_params,
|
||||
} = req.body;
|
||||
|
||||
if (!name || !method || !path) {
|
||||
@@ -103,13 +107,21 @@ export const createEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Валидация для типа AQL
|
||||
if (execType === 'aql') {
|
||||
if (!database_id || !aql_method || !aql_endpoint) {
|
||||
return res.status(400).json({ error: 'Database ID, AQL method, and AQL endpoint are required for AQL execution type' });
|
||||
}
|
||||
}
|
||||
|
||||
const result = await mainPool.query(
|
||||
`INSERT INTO endpoints (
|
||||
name, description, method, path, database_id, sql_query, parameters,
|
||||
folder_id, user_id, is_public, enable_logging,
|
||||
execution_type, script_language, script_code, script_queries
|
||||
execution_type, script_language, script_code, script_queries,
|
||||
aql_method, aql_endpoint, aql_body, aql_query_params
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19)
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
@@ -127,6 +139,10 @@ export const createEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
script_language || null,
|
||||
script_code || null,
|
||||
JSON.stringify(script_queries || []),
|
||||
aql_method || null,
|
||||
aql_endpoint || null,
|
||||
aql_body || null,
|
||||
JSON.stringify(aql_query_params || {}),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -158,6 +174,10 @@ export const updateEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
script_language,
|
||||
script_code,
|
||||
script_queries,
|
||||
aql_method,
|
||||
aql_endpoint,
|
||||
aql_body,
|
||||
aql_query_params,
|
||||
} = req.body;
|
||||
|
||||
const result = await mainPool.query(
|
||||
@@ -176,8 +196,12 @@ export const updateEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
script_language = $12,
|
||||
script_code = $13,
|
||||
script_queries = $14,
|
||||
aql_method = $15,
|
||||
aql_endpoint = $16,
|
||||
aql_body = $17,
|
||||
aql_query_params = $18,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $15
|
||||
WHERE id = $19
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
@@ -194,6 +218,10 @@ export const updateEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
script_language || null,
|
||||
script_code || null,
|
||||
script_queries ? JSON.stringify(script_queries) : null,
|
||||
aql_method || null,
|
||||
aql_endpoint || null,
|
||||
aql_body || null,
|
||||
aql_query_params ? JSON.stringify(aql_query_params) : null,
|
||||
id,
|
||||
]
|
||||
);
|
||||
@@ -242,7 +270,11 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
execution_type,
|
||||
script_language,
|
||||
script_code,
|
||||
script_queries
|
||||
script_queries,
|
||||
aql_method,
|
||||
aql_endpoint,
|
||||
aql_body,
|
||||
aql_query_params,
|
||||
} = req.body;
|
||||
|
||||
const execType = execution_type || 'sql';
|
||||
@@ -305,6 +337,37 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
rowCount: scriptResult.rowCount || (Array.isArray(scriptResult.data) ? scriptResult.data.length : 0),
|
||||
executionTime: scriptResult.executionTime || 0,
|
||||
});
|
||||
} else if (execType === 'aql') {
|
||||
if (!database_id) {
|
||||
return res.status(400).json({ error: 'Missing database_id for AQL execution' });
|
||||
}
|
||||
if (!aql_method || !aql_endpoint) {
|
||||
return res.status(400).json({ error: 'Missing aql_method or aql_endpoint' });
|
||||
}
|
||||
|
||||
// Собираем параметры из тестовых значений
|
||||
const requestParams: Record<string, any> = {};
|
||||
if (endpoint_parameters && Array.isArray(endpoint_parameters) && parameters && Array.isArray(parameters)) {
|
||||
endpoint_parameters.forEach((param: any, index: number) => {
|
||||
requestParams[param.name] = parameters[index];
|
||||
});
|
||||
}
|
||||
|
||||
const { aqlExecutor } = require('../services/AqlExecutor');
|
||||
const result = await aqlExecutor.executeAqlQuery(database_id, {
|
||||
method: aql_method,
|
||||
endpoint: aql_endpoint,
|
||||
body: aql_body,
|
||||
queryParams: aql_query_params,
|
||||
parameters: requestParams,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result.rows,
|
||||
rowCount: result.rowCount,
|
||||
executionTime: result.executionTime,
|
||||
});
|
||||
} else {
|
||||
return res.status(400).json({ error: 'Invalid execution_type' });
|
||||
}
|
||||
|
||||
40
backend/src/migrations/005_add_aql_support.sql
Normal file
40
backend/src/migrations/005_add_aql_support.sql
Normal file
@@ -0,0 +1,40 @@
|
||||
-- Add AQL support to databases table
|
||||
ALTER TABLE databases
|
||||
ALTER COLUMN type TYPE VARCHAR(50);
|
||||
|
||||
-- Update the type check constraint to include 'aql'
|
||||
ALTER TABLE databases
|
||||
DROP CONSTRAINT IF EXISTS databases_type_check;
|
||||
|
||||
ALTER TABLE databases
|
||||
ADD CONSTRAINT databases_type_check
|
||||
CHECK (type IN ('postgresql', 'mysql', 'mssql', 'aql'));
|
||||
|
||||
-- Add AQL-specific columns to databases table
|
||||
ALTER TABLE databases
|
||||
ADD COLUMN IF NOT EXISTS aql_base_url TEXT,
|
||||
ADD COLUMN IF NOT EXISTS aql_auth_type VARCHAR(50) CHECK (aql_auth_type IN ('basic', 'bearer', 'custom')),
|
||||
ADD COLUMN IF NOT EXISTS aql_auth_value TEXT,
|
||||
ADD COLUMN IF NOT EXISTS aql_headers JSONB DEFAULT '{}'::jsonb;
|
||||
|
||||
-- Add AQL support to endpoints table
|
||||
ALTER TABLE endpoints
|
||||
ALTER COLUMN execution_type TYPE VARCHAR(50);
|
||||
|
||||
-- Update execution_type check constraint to include 'aql'
|
||||
ALTER TABLE endpoints
|
||||
DROP CONSTRAINT IF EXISTS endpoints_execution_type_check;
|
||||
|
||||
ALTER TABLE endpoints
|
||||
ADD CONSTRAINT endpoints_execution_type_check
|
||||
CHECK (execution_type IN ('sql', 'script', 'aql'));
|
||||
|
||||
-- Add AQL-specific columns to endpoints table
|
||||
ALTER TABLE endpoints
|
||||
ADD COLUMN IF NOT EXISTS aql_method VARCHAR(10) CHECK (aql_method IN ('GET', 'POST', 'PUT', 'DELETE')),
|
||||
ADD COLUMN IF NOT EXISTS aql_endpoint TEXT,
|
||||
ADD COLUMN IF NOT EXISTS aql_body TEXT,
|
||||
ADD COLUMN IF NOT EXISTS aql_query_params JSONB DEFAULT '{}'::jsonb;
|
||||
|
||||
-- Create index for AQL endpoints
|
||||
CREATE INDEX IF NOT EXISTS idx_endpoints_execution_type ON endpoints(execution_type);
|
||||
230
backend/src/services/AqlExecutor.ts
Normal file
230
backend/src/services/AqlExecutor.ts
Normal 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();
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface DatabaseConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'postgresql' | 'mysql' | 'mssql';
|
||||
type: 'postgresql' | 'mysql' | 'mssql' | 'aql';
|
||||
host: string;
|
||||
port: number;
|
||||
database_name: string;
|
||||
@@ -9,6 +9,11 @@ export interface DatabaseConfig {
|
||||
password: string;
|
||||
ssl: boolean;
|
||||
is_active?: boolean;
|
||||
// AQL-specific fields
|
||||
aql_base_url?: string;
|
||||
aql_auth_type?: 'basic' | 'bearer' | 'custom';
|
||||
aql_auth_value?: string;
|
||||
aql_headers?: Record<string, string>;
|
||||
created_at?: Date;
|
||||
updated_at?: Date;
|
||||
}
|
||||
@@ -57,10 +62,15 @@ export interface Endpoint {
|
||||
user_id: string;
|
||||
is_public: boolean;
|
||||
enable_logging: boolean;
|
||||
execution_type: 'sql' | 'script';
|
||||
execution_type: 'sql' | 'script' | 'aql';
|
||||
script_language?: 'javascript' | 'python';
|
||||
script_code?: string;
|
||||
script_queries?: ScriptQuery[];
|
||||
// AQL-specific fields
|
||||
aql_method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
aql_endpoint?: string;
|
||||
aql_body?: string;
|
||||
aql_query_params?: Record<string, string>;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
@@ -36,11 +36,20 @@ export default function EndpointModal({
|
||||
script_language: endpoint?.script_language || 'javascript',
|
||||
script_code: endpoint?.script_code || '',
|
||||
script_queries: endpoint?.script_queries || [],
|
||||
// AQL-specific fields
|
||||
aql_method: endpoint?.aql_method || 'GET',
|
||||
aql_endpoint: endpoint?.aql_endpoint || '',
|
||||
aql_body: endpoint?.aql_body || '',
|
||||
aql_query_params: endpoint?.aql_query_params || {},
|
||||
});
|
||||
|
||||
const [editingQueryIndex, setEditingQueryIndex] = useState<number | null>(null);
|
||||
const [showScriptCodeEditor, setShowScriptCodeEditor] = useState(false);
|
||||
|
||||
// Определяем тип выбранной базы данных
|
||||
const selectedDatabase = databases.find(db => db.id === formData.database_id);
|
||||
const isAqlDatabase = selectedDatabase?.type === 'aql';
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (data: any) =>
|
||||
endpoint ? endpointsApi.update(endpoint.id, data) : endpointsApi.create(data),
|
||||
@@ -72,7 +81,18 @@ export default function EndpointModal({
|
||||
}
|
||||
});
|
||||
|
||||
if (formData.execution_type === 'script') {
|
||||
if (formData.execution_type === 'aql') {
|
||||
return endpointsApi.test({
|
||||
database_id: formData.database_id || '',
|
||||
execution_type: 'aql',
|
||||
aql_method: formData.aql_method || 'GET',
|
||||
aql_endpoint: formData.aql_endpoint || '',
|
||||
aql_body: formData.aql_body || '',
|
||||
aql_query_params: typeof formData.aql_query_params === 'string' ? {} : formData.aql_query_params || {},
|
||||
parameters: paramValues,
|
||||
endpoint_parameters: formData.parameters,
|
||||
} as any);
|
||||
} else if (formData.execution_type === 'script') {
|
||||
// Для скриптов используем database_id из первого запроса или пустую строку
|
||||
const scriptQueries = formData.script_queries || [];
|
||||
const firstDbId = scriptQueries.length > 0 ? scriptQueries[0].database_id : '';
|
||||
@@ -156,6 +176,7 @@ export default function EndpointModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isAqlDatabase && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Тип выполнения</label>
|
||||
<select
|
||||
@@ -163,12 +184,13 @@ export default function EndpointModal({
|
||||
onChange={(e) => setFormData({ ...formData, execution_type: e.target.value as 'sql' | 'script' })}
|
||||
className="input w-full"
|
||||
>
|
||||
<option value="sql">SQL Запрос</option>
|
||||
<option value="script">Скрипт (JavaScript/Python)</option>
|
||||
<option value="sql">QL Запрос</option>
|
||||
<option value="script">Скрипт (JavaScript/Python) + QL запросы</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={`grid ${formData.execution_type === 'sql' ? 'grid-cols-3' : 'grid-cols-2'} gap-4`}>
|
||||
<div className={`grid ${(!isAqlDatabase && formData.execution_type === 'sql') || isAqlDatabase ? 'grid-cols-3' : 'grid-cols-2'} gap-4`}>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Путь</label>
|
||||
<input
|
||||
@@ -180,18 +202,18 @@ export default function EndpointModal({
|
||||
placeholder="/api/v1/users"
|
||||
/>
|
||||
</div>
|
||||
{formData.execution_type === 'sql' && (
|
||||
{(formData.execution_type === 'sql' || isAqlDatabase) && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">База данных</label>
|
||||
<select
|
||||
required={formData.execution_type === 'sql'}
|
||||
required
|
||||
value={formData.database_id}
|
||||
onChange={(e) => setFormData({ ...formData, database_id: e.target.value })}
|
||||
onChange={(e) => setFormData({ ...formData, database_id: e.target.value, execution_type: databases.find(db => db.id === e.target.value)?.type === 'aql' ? 'aql' : 'sql' })}
|
||||
className="input w-full"
|
||||
>
|
||||
<option value="">Выберите базу данных</option>
|
||||
{databases.map((db) => (
|
||||
<option key={db.id} value={db.id}>{db.name}</option>
|
||||
<option key={db.id} value={db.id}>{db.name} ({db.type})</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -210,7 +232,7 @@ export default function EndpointModal({
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Параметры запроса
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
(используйте $имяПараметра в SQL запросе)
|
||||
(используйте $имяПараметра в QL запросе)
|
||||
</span>
|
||||
</label>
|
||||
<button
|
||||
@@ -321,7 +343,75 @@ export default function EndpointModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{formData.execution_type === 'sql' ? (
|
||||
{formData.execution_type === 'aql' ? (
|
||||
<>
|
||||
{/* AQL Configuration */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">AQL HTTP Метод</label>
|
||||
<select
|
||||
value={formData.aql_method}
|
||||
onChange={(e) => setFormData({ ...formData, aql_method: e.target.value as 'GET' | 'POST' | 'PUT' | 'DELETE' })}
|
||||
className="input w-full"
|
||||
>
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
<option value="DELETE">DELETE</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">AQL Endpoint URL</label>
|
||||
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
||||
<div>Используйте <code className="bg-blue-100 px-1 rounded">$параметр</code> для подстановки</div>
|
||||
<div>Пример: <code className="bg-blue-100 px-1 rounded">/view/$viewId/GetFullCuidIsLink</code></div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.aql_endpoint}
|
||||
onChange={(e) => setFormData({ ...formData, aql_endpoint: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="/view/15151180-f7f9-4ecc-a48c-25c083511907/GetFullCuidIsLink"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">AQL Body (JSON)</label>
|
||||
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
||||
<div>Используйте <code className="bg-blue-100 px-1 rounded">$параметр</code> в JSON для подстановки</div>
|
||||
</div>
|
||||
<textarea
|
||||
value={formData.aql_body}
|
||||
onChange={(e) => setFormData({ ...formData, aql_body: e.target.value })}
|
||||
className="input w-full font-mono text-sm"
|
||||
rows={6}
|
||||
placeholder='{"aql": "select c from COMPOSITION c where c/uid/value= '$compositionId'"}'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">AQL Query Parameters (JSON)</label>
|
||||
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
||||
<div>Формат: <code className="bg-blue-100 px-1 rounded">{`{"key": "value", "CompositionLink": "$linkValue"}`}</code></div>
|
||||
</div>
|
||||
<textarea
|
||||
value={typeof formData.aql_query_params === 'string' ? formData.aql_query_params : JSON.stringify(formData.aql_query_params, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value);
|
||||
setFormData({ ...formData, aql_query_params: parsed });
|
||||
} catch {
|
||||
// Игнорируем невалидный JSON - не обновляем состояние
|
||||
}
|
||||
}}
|
||||
className="input w-full font-mono text-sm"
|
||||
rows={4}
|
||||
placeholder='{"CompositionLink": "ehr:compositions/$compositionId"}'
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : formData.execution_type === 'sql' ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">SQL Запрос</label>
|
||||
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700 space-y-1">
|
||||
@@ -512,7 +602,9 @@ export default function EndpointModal({
|
||||
onClick={() => testMutation.mutate()}
|
||||
disabled={
|
||||
testMutation.isPending ||
|
||||
(formData.execution_type === 'sql'
|
||||
(formData.execution_type === 'aql'
|
||||
? (!formData.database_id || !formData.aql_endpoint)
|
||||
: formData.execution_type === 'sql'
|
||||
? (!formData.database_id || !formData.sql_query)
|
||||
: !formData.script_code
|
||||
)
|
||||
|
||||
@@ -223,6 +223,11 @@ function DatabaseModal({
|
||||
password: database?.password || '',
|
||||
ssl: database?.ssl || false,
|
||||
is_active: database?.is_active !== undefined ? database.is_active : true,
|
||||
// AQL-specific fields
|
||||
aql_base_url: database?.aql_base_url || '',
|
||||
aql_auth_type: database?.aql_auth_type || 'basic',
|
||||
aql_auth_value: database?.aql_auth_value || '',
|
||||
aql_headers: database?.aql_headers || {},
|
||||
});
|
||||
|
||||
const saveMutation = useMutation({
|
||||
@@ -265,7 +270,6 @@ function DatabaseModal({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Тип</label>
|
||||
<select
|
||||
@@ -274,8 +278,105 @@ function DatabaseModal({
|
||||
className="input w-full"
|
||||
>
|
||||
<option value="postgresql">PostgreSQL</option>
|
||||
<option value="aql">AQL (HTTP API)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{formData.type === 'aql' ? (
|
||||
<>
|
||||
{/* AQL Fields */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">AQL Base URL *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.aql_base_url}
|
||||
onChange={(e) => setFormData({ ...formData, aql_base_url: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="http://api.ehrdb.ncms-i.ru/api/rest/v1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Тип аутентификации</label>
|
||||
<select
|
||||
value={formData.aql_auth_type}
|
||||
onChange={(e) => setFormData({ ...formData, aql_auth_type: e.target.value })}
|
||||
className="input w-full"
|
||||
>
|
||||
<option value="basic">Basic Auth</option>
|
||||
<option value="bearer">Bearer Token</option>
|
||||
<option value="custom">Custom Header</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{formData.aql_auth_type === 'basic' && 'Basic Auth Value (Base64)'}
|
||||
{formData.aql_auth_type === 'bearer' && 'Bearer Token'}
|
||||
{formData.aql_auth_type === 'custom' && 'Custom Authorization Value'}
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
required={!database}
|
||||
value={formData.aql_auth_value}
|
||||
onChange={(e) => setFormData({ ...formData, aql_auth_value: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder={database ? '••••••••' : 'Введите значение'}
|
||||
/>
|
||||
{database && (
|
||||
<p className="text-xs text-gray-500 mt-1">Оставьте пустым, чтобы не менять</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Дополнительные заголовки (JSON)
|
||||
<span className="text-xs text-gray-500 ml-2">необязательно</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={typeof formData.aql_headers === 'string' ? formData.aql_headers : JSON.stringify(formData.aql_headers, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value);
|
||||
setFormData({ ...formData, aql_headers: parsed });
|
||||
} catch {
|
||||
setFormData({ ...formData, aql_headers: e.target.value });
|
||||
}
|
||||
}}
|
||||
className="input w-full font-mono text-sm"
|
||||
rows={4}
|
||||
placeholder='{"x-dbrole": "KIS.EMIAS.XАПИД", "Hack-Time": "true"}'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_active}
|
||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Активна</span>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* PostgreSQL Fields */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Хост</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.host}
|
||||
onChange={(e) => setFormData({ ...formData, host: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="localhost"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Порт</label>
|
||||
<input
|
||||
@@ -288,18 +389,6 @@ function DatabaseModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Хост</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.host}
|
||||
onChange={(e) => setFormData({ ...formData, host: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="localhost"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Имя базы данных</label>
|
||||
<input
|
||||
@@ -361,6 +450,8 @@ function DatabaseModal({
|
||||
<span className="text-sm text-gray-700">Активна</span>
|
||||
</label>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-4 border-t border-gray-200">
|
||||
<button type="button" onClick={onClose} className="btn btn-secondary">
|
||||
|
||||
@@ -13,10 +13,15 @@ export interface AuthResponse {
|
||||
export interface Database {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'postgresql';
|
||||
type: 'postgresql' | 'mysql' | 'mssql' | 'aql';
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
// AQL-specific fields
|
||||
aql_base_url?: string;
|
||||
aql_auth_type?: 'basic' | 'bearer' | 'custom';
|
||||
aql_auth_value?: string;
|
||||
aql_headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
@@ -59,10 +64,15 @@ export interface Endpoint {
|
||||
user_id: string;
|
||||
is_public: boolean;
|
||||
enable_logging: boolean;
|
||||
execution_type: 'sql' | 'script';
|
||||
execution_type: 'sql' | 'script' | 'aql';
|
||||
script_language?: 'javascript' | 'python';
|
||||
script_code?: string;
|
||||
script_queries?: ScriptQuery[];
|
||||
// AQL-specific fields
|
||||
aql_method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
aql_endpoint?: string;
|
||||
aql_body?: string;
|
||||
aql_query_params?: Record<string, string>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user