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

@@ -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;
if (!name || !host || !port || !database_name || !username || !password) {
return res.status(400).json({ error: 'Не заполнены обязательные поля' });
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];
// Добавить пул подключений
await databasePoolManager.reloadPool(newDb.id);
// Добавить пул подключений (только для не-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: 'База данных не найдена' });
}
// Перезагрузить пул
await databasePoolManager.reloadPool(id);
// Перезагрузить пул (только для не-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 isConnected = await databasePoolManager.testConnection(id);
// Получаем тип БД
const dbResult = await mainPool.query('SELECT type FROM databases WHERE id = $1', [id]);
res.json({
success: isConnected,
message: isConnected ? 'Подключение успешно' : 'Ошибка подключения',
});
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: 'Ошибка тестирования подключения' });

View File

@@ -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;

View File

@@ -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' });
}

View 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);

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();

View File

@@ -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;
}

View File

@@ -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,19 +176,21 @@ export default function EndpointModal({
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Тип выполнения</label>
<select
value={formData.execution_type}
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>
</select>
</div>
{!isAqlDatabase && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Тип выполнения</label>
<select
value={formData.execution_type}
onChange={(e) => setFormData({ ...formData, execution_type: e.target.value as 'sql' | 'script' })}
className="input w-full"
>
<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= &apos;$compositionId&apos;"}'
/>
</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,9 +602,11 @@ export default function EndpointModal({
onClick={() => testMutation.mutate()}
disabled={
testMutation.isPending ||
(formData.execution_type === 'sql'
? (!formData.database_id || !formData.sql_query)
: !formData.script_code
(formData.execution_type === 'aql'
? (!formData.database_id || !formData.aql_endpoint)
: formData.execution_type === 'sql'
? (!formData.database_id || !formData.sql_query)
: !formData.script_code
)
}
className="btn btn-secondary flex items-center gap-2"

View File

@@ -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,102 +270,188 @@ 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
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
className="input w-full"
>
<option value="postgresql">PostgreSQL</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Порт</label>
<input
type="number"
required
value={formData.port}
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })}
className="input w-full"
/>
</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 })}
<label className="block text-sm font-medium text-gray-700 mb-1">Тип</label>
<select
value={formData.type}
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
className="input w-full"
placeholder="localhost"
/>
>
<option value="postgresql">PostgreSQL</option>
<option value="aql">AQL (HTTP API)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Имя базы данных</label>
<input
type="text"
required
value={formData.database_name}
onChange={(e) => setFormData({ ...formData, database_name: e.target.value })}
className="input w-full"
placeholder="my_database"
/>
</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 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.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
className="input w-full"
placeholder="postgres"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Пароль</label>
<input
type="password"
required={!database}
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
className="input w-full"
placeholder={database ? '••••••••' : 'Введите пароль'}
/>
{database && (
<p className="text-xs text-gray-500 mt-1">Оставьте пустым, чтобы не менять пароль</p>
)}
</div>
</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 className="flex items-center gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={formData.ssl}
onChange={(e) => setFormData({ ...formData, ssl: e.target.checked })}
className="rounded"
/>
<span className="text-sm text-gray-700">Использовать SSL</span>
</label>
<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>
<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>
<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
type="number"
required
value={formData.port}
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })}
className="input w-full"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Имя базы данных</label>
<input
type="text"
required
value={formData.database_name}
onChange={(e) => setFormData({ ...formData, database_name: e.target.value })}
className="input w-full"
placeholder="my_database"
/>
</div>
<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.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
className="input w-full"
placeholder="postgres"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Пароль</label>
<input
type="password"
required={!database}
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
className="input w-full"
placeholder={database ? '••••••••' : 'Введите пароль'}
/>
{database && (
<p className="text-xs text-gray-500 mt-1">Оставьте пустым, чтобы не менять пароль</p>
)}
</div>
</div>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={formData.ssl}
onChange={(e) => setFormData({ ...formData, ssl: e.target.checked })}
className="rounded"
/>
<span className="text-sm text-gray-700">Использовать SSL</span>
</label>
<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>
</>
)}
<div className="flex gap-3 pt-4 border-t border-gray-200">
<button type="button" onClick={onClose} className="btn btn-secondary">

View File

@@ -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;
}