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) => {
|
export const getDatabases = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const result = await mainPool.query(
|
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);
|
res.json(result.rows);
|
||||||
@@ -22,7 +25,10 @@ export const getDatabase = async (req: AuthRequest, res: Response) => {
|
|||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
|
||||||
const result = await mainPool.query(
|
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]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -39,26 +45,58 @@ export const getDatabase = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
export const createDatabase = async (req: AuthRequest, res: Response) => {
|
export const createDatabase = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
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) {
|
const dbType = type || 'postgresql';
|
||||||
return res.status(400).json({ error: 'Не заполнены обязательные поля' });
|
|
||||||
|
// Валидация для обычных БД
|
||||||
|
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(
|
const result = await mainPool.query(
|
||||||
`INSERT INTO databases (name, type, host, port, database_name, username, password, ssl, is_active)
|
`INSERT INTO databases (
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, true)
|
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 *`,
|
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];
|
const newDb = result.rows[0];
|
||||||
|
|
||||||
// Добавить пул подключений
|
// Добавить пул подключений (только для не-AQL баз)
|
||||||
await databasePoolManager.reloadPool(newDb.id);
|
if (dbType !== 'aql') {
|
||||||
|
await databasePoolManager.reloadPool(newDb.id);
|
||||||
|
}
|
||||||
|
|
||||||
// Не возвращаем пароль
|
// Не возвращаем пароль
|
||||||
delete newDb.password;
|
delete newDb.password;
|
||||||
|
delete newDb.aql_auth_value; // Также не возвращаем auth value
|
||||||
|
|
||||||
res.status(201).json(newDb);
|
res.status(201).json(newDb);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -73,13 +111,16 @@ export const createDatabase = async (req: AuthRequest, res: Response) => {
|
|||||||
export const updateDatabase = async (req: AuthRequest, res: Response) => {
|
export const updateDatabase = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
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 query;
|
||||||
let params;
|
let params;
|
||||||
|
|
||||||
if (password) {
|
if (password || aql_auth_value) {
|
||||||
query = `
|
query = `
|
||||||
UPDATE databases
|
UPDATE databases
|
||||||
SET name = COALESCE($1, name),
|
SET name = COALESCE($1, name),
|
||||||
@@ -88,14 +129,22 @@ export const updateDatabase = async (req: AuthRequest, res: Response) => {
|
|||||||
port = COALESCE($4, port),
|
port = COALESCE($4, port),
|
||||||
database_name = COALESCE($5, database_name),
|
database_name = COALESCE($5, database_name),
|
||||||
username = COALESCE($6, username),
|
username = COALESCE($6, username),
|
||||||
password = $7,
|
password = COALESCE($7, password),
|
||||||
ssl = COALESCE($8, ssl),
|
ssl = COALESCE($8, ssl),
|
||||||
is_active = COALESCE($9, is_active),
|
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
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $10
|
WHERE id = $14
|
||||||
RETURNING id, name, type, host, port, database_name, username, ssl, is_active, created_at, updated_at
|
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 {
|
} else {
|
||||||
query = `
|
query = `
|
||||||
UPDATE databases
|
UPDATE databases
|
||||||
@@ -107,11 +156,18 @@ export const updateDatabase = async (req: AuthRequest, res: Response) => {
|
|||||||
username = COALESCE($6, username),
|
username = COALESCE($6, username),
|
||||||
ssl = COALESCE($7, ssl),
|
ssl = COALESCE($7, ssl),
|
||||||
is_active = COALESCE($8, is_active),
|
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
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $9
|
WHERE id = $12
|
||||||
RETURNING id, name, type, host, port, database_name, username, ssl, is_active, created_at, updated_at
|
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);
|
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: 'База данных не найдена' });
|
return res.status(404).json({ error: 'База данных не найдена' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Перезагрузить пул
|
// Перезагрузить пул (только для не-AQL баз)
|
||||||
await databasePoolManager.reloadPool(id);
|
if (result.rows[0].type !== 'aql') {
|
||||||
|
await databasePoolManager.reloadPool(id);
|
||||||
|
}
|
||||||
|
|
||||||
res.json(result.rows[0]);
|
res.json(result.rows[0]);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -172,12 +230,33 @@ export const testDatabaseConnection = async (req: AuthRequest, res: Response) =>
|
|||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
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({
|
if (dbResult.rows.length === 0) {
|
||||||
success: isConnected,
|
return res.status(404).json({ success: false, error: 'База данных не найдена' });
|
||||||
message: isConnected ? 'Подключение успешно' : 'Ошибка подключения',
|
}
|
||||||
});
|
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error('Test connection error:', error);
|
console.error('Test connection error:', error);
|
||||||
res.status(500).json({ error: 'Ошибка тестирования подключения' });
|
res.status(500).json({ error: 'Ошибка тестирования подключения' });
|
||||||
|
|||||||
@@ -141,7 +141,38 @@ export const executeDynamicEndpoint = async (req: ApiKeyRequest, res: Response)
|
|||||||
let result;
|
let result;
|
||||||
const executionType = endpoint.execution_type || 'sql';
|
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
|
// Execute script
|
||||||
const scriptLanguage = endpoint.script_language;
|
const scriptLanguage = endpoint.script_language;
|
||||||
const scriptCode = endpoint.script_code;
|
const scriptCode = endpoint.script_code;
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ export const createEndpoint = async (req: AuthRequest, res: Response) => {
|
|||||||
script_language,
|
script_language,
|
||||||
script_code,
|
script_code,
|
||||||
script_queries,
|
script_queries,
|
||||||
|
aql_method,
|
||||||
|
aql_endpoint,
|
||||||
|
aql_body,
|
||||||
|
aql_query_params,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
if (!name || !method || !path) {
|
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(
|
const result = await mainPool.query(
|
||||||
`INSERT INTO endpoints (
|
`INSERT INTO endpoints (
|
||||||
name, description, method, path, database_id, sql_query, parameters,
|
name, description, method, path, database_id, sql_query, parameters,
|
||||||
folder_id, user_id, is_public, enable_logging,
|
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 *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
name,
|
name,
|
||||||
@@ -127,6 +139,10 @@ export const createEndpoint = async (req: AuthRequest, res: Response) => {
|
|||||||
script_language || null,
|
script_language || null,
|
||||||
script_code || null,
|
script_code || null,
|
||||||
JSON.stringify(script_queries || []),
|
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_language,
|
||||||
script_code,
|
script_code,
|
||||||
script_queries,
|
script_queries,
|
||||||
|
aql_method,
|
||||||
|
aql_endpoint,
|
||||||
|
aql_body,
|
||||||
|
aql_query_params,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const result = await mainPool.query(
|
const result = await mainPool.query(
|
||||||
@@ -176,8 +196,12 @@ export const updateEndpoint = async (req: AuthRequest, res: Response) => {
|
|||||||
script_language = $12,
|
script_language = $12,
|
||||||
script_code = $13,
|
script_code = $13,
|
||||||
script_queries = $14,
|
script_queries = $14,
|
||||||
|
aql_method = $15,
|
||||||
|
aql_endpoint = $16,
|
||||||
|
aql_body = $17,
|
||||||
|
aql_query_params = $18,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $15
|
WHERE id = $19
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[
|
[
|
||||||
name,
|
name,
|
||||||
@@ -194,6 +218,10 @@ export const updateEndpoint = async (req: AuthRequest, res: Response) => {
|
|||||||
script_language || null,
|
script_language || null,
|
||||||
script_code || null,
|
script_code || null,
|
||||||
script_queries ? JSON.stringify(script_queries) : 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,
|
id,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -242,7 +270,11 @@ export const testEndpoint = async (req: AuthRequest, res: Response) => {
|
|||||||
execution_type,
|
execution_type,
|
||||||
script_language,
|
script_language,
|
||||||
script_code,
|
script_code,
|
||||||
script_queries
|
script_queries,
|
||||||
|
aql_method,
|
||||||
|
aql_endpoint,
|
||||||
|
aql_body,
|
||||||
|
aql_query_params,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const execType = execution_type || 'sql';
|
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),
|
rowCount: scriptResult.rowCount || (Array.isArray(scriptResult.data) ? scriptResult.data.length : 0),
|
||||||
executionTime: scriptResult.executionTime || 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 {
|
} else {
|
||||||
return res.status(400).json({ error: 'Invalid execution_type' });
|
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 {
|
export interface DatabaseConfig {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'postgresql' | 'mysql' | 'mssql';
|
type: 'postgresql' | 'mysql' | 'mssql' | 'aql';
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
database_name: string;
|
database_name: string;
|
||||||
@@ -9,6 +9,11 @@ export interface DatabaseConfig {
|
|||||||
password: string;
|
password: string;
|
||||||
ssl: boolean;
|
ssl: boolean;
|
||||||
is_active?: 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;
|
created_at?: Date;
|
||||||
updated_at?: Date;
|
updated_at?: Date;
|
||||||
}
|
}
|
||||||
@@ -57,10 +62,15 @@ export interface Endpoint {
|
|||||||
user_id: string;
|
user_id: string;
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
enable_logging: boolean;
|
enable_logging: boolean;
|
||||||
execution_type: 'sql' | 'script';
|
execution_type: 'sql' | 'script' | 'aql';
|
||||||
script_language?: 'javascript' | 'python';
|
script_language?: 'javascript' | 'python';
|
||||||
script_code?: string;
|
script_code?: string;
|
||||||
script_queries?: ScriptQuery[];
|
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;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,11 +36,20 @@ export default function EndpointModal({
|
|||||||
script_language: endpoint?.script_language || 'javascript',
|
script_language: endpoint?.script_language || 'javascript',
|
||||||
script_code: endpoint?.script_code || '',
|
script_code: endpoint?.script_code || '',
|
||||||
script_queries: endpoint?.script_queries || [],
|
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 [editingQueryIndex, setEditingQueryIndex] = useState<number | null>(null);
|
||||||
const [showScriptCodeEditor, setShowScriptCodeEditor] = useState(false);
|
const [showScriptCodeEditor, setShowScriptCodeEditor] = useState(false);
|
||||||
|
|
||||||
|
// Определяем тип выбранной базы данных
|
||||||
|
const selectedDatabase = databases.find(db => db.id === formData.database_id);
|
||||||
|
const isAqlDatabase = selectedDatabase?.type === 'aql';
|
||||||
|
|
||||||
const saveMutation = useMutation({
|
const saveMutation = useMutation({
|
||||||
mutationFn: (data: any) =>
|
mutationFn: (data: any) =>
|
||||||
endpoint ? endpointsApi.update(endpoint.id, data) : endpointsApi.create(data),
|
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 из первого запроса или пустую строку
|
// Для скриптов используем database_id из первого запроса или пустую строку
|
||||||
const scriptQueries = formData.script_queries || [];
|
const scriptQueries = formData.script_queries || [];
|
||||||
const firstDbId = scriptQueries.length > 0 ? scriptQueries[0].database_id : '';
|
const firstDbId = scriptQueries.length > 0 ? scriptQueries[0].database_id : '';
|
||||||
@@ -156,19 +176,21 @@ export default function EndpointModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{!isAqlDatabase && (
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Тип выполнения</label>
|
<div>
|
||||||
<select
|
<label className="block text-sm font-medium text-gray-700 mb-1">Тип выполнения</label>
|
||||||
value={formData.execution_type}
|
<select
|
||||||
onChange={(e) => setFormData({ ...formData, execution_type: e.target.value as 'sql' | 'script' })}
|
value={formData.execution_type}
|
||||||
className="input w-full"
|
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>
|
||||||
</select>
|
<option value="script">Скрипт (JavaScript/Python) + QL запросы</option>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Путь</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Путь</label>
|
||||||
<input
|
<input
|
||||||
@@ -180,18 +202,18 @@ export default function EndpointModal({
|
|||||||
placeholder="/api/v1/users"
|
placeholder="/api/v1/users"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{formData.execution_type === 'sql' && (
|
{(formData.execution_type === 'sql' || isAqlDatabase) && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">База данных</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">База данных</label>
|
||||||
<select
|
<select
|
||||||
required={formData.execution_type === 'sql'}
|
required
|
||||||
value={formData.database_id}
|
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"
|
className="input w-full"
|
||||||
>
|
>
|
||||||
<option value="">Выберите базу данных</option>
|
<option value="">Выберите базу данных</option>
|
||||||
{databases.map((db) => (
|
{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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,7 +232,7 @@ export default function EndpointModal({
|
|||||||
<label className="block text-sm font-medium text-gray-700">
|
<label className="block text-sm font-medium text-gray-700">
|
||||||
Параметры запроса
|
Параметры запроса
|
||||||
<span className="text-xs text-gray-500 ml-2">
|
<span className="text-xs text-gray-500 ml-2">
|
||||||
(используйте $имяПараметра в SQL запросе)
|
(используйте $имяПараметра в QL запросе)
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<button
|
<button
|
||||||
@@ -321,7 +343,75 @@ export default function EndpointModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">SQL Запрос</label>
|
<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">
|
<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()}
|
onClick={() => testMutation.mutate()}
|
||||||
disabled={
|
disabled={
|
||||||
testMutation.isPending ||
|
testMutation.isPending ||
|
||||||
(formData.execution_type === 'sql'
|
(formData.execution_type === 'aql'
|
||||||
? (!formData.database_id || !formData.sql_query)
|
? (!formData.database_id || !formData.aql_endpoint)
|
||||||
: !formData.script_code
|
: formData.execution_type === 'sql'
|
||||||
|
? (!formData.database_id || !formData.sql_query)
|
||||||
|
: !formData.script_code
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
className="btn btn-secondary flex items-center gap-2"
|
className="btn btn-secondary flex items-center gap-2"
|
||||||
|
|||||||
@@ -223,6 +223,11 @@ function DatabaseModal({
|
|||||||
password: database?.password || '',
|
password: database?.password || '',
|
||||||
ssl: database?.ssl || false,
|
ssl: database?.ssl || false,
|
||||||
is_active: database?.is_active !== undefined ? database.is_active : true,
|
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({
|
const saveMutation = useMutation({
|
||||||
@@ -265,102 +270,188 @@ function DatabaseModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Хост</label>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Тип</label>
|
||||||
<input
|
<select
|
||||||
type="text"
|
value={formData.type}
|
||||||
required
|
onChange={(e) => setFormData({ ...formData, type: e.target.value })}
|
||||||
value={formData.host}
|
|
||||||
onChange={(e) => setFormData({ ...formData, host: e.target.value })}
|
|
||||||
className="input w-full"
|
className="input w-full"
|
||||||
placeholder="localhost"
|
>
|
||||||
/>
|
<option value="postgresql">PostgreSQL</option>
|
||||||
|
<option value="aql">AQL (HTTP API)</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{formData.type === 'aql' ? (
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Имя базы данных</label>
|
<>
|
||||||
<input
|
{/* AQL Fields */}
|
||||||
type="text"
|
<div>
|
||||||
required
|
<label className="block text-sm font-medium text-gray-700 mb-1">AQL Base URL *</label>
|
||||||
value={formData.database_name}
|
<input
|
||||||
onChange={(e) => setFormData({ ...formData, database_name: e.target.value })}
|
type="text"
|
||||||
className="input w-full"
|
required
|
||||||
placeholder="my_database"
|
value={formData.aql_base_url}
|
||||||
/>
|
onChange={(e) => setFormData({ ...formData, aql_base_url: e.target.value })}
|
||||||
</div>
|
className="input w-full"
|
||||||
|
placeholder="http://api.ehrdb.ncms-i.ru/api/rest/v1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div>
|
||||||
<div>
|
<label className="block text-sm font-medium text-gray-700 mb-1">Тип аутентификации</label>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Пользователь</label>
|
<select
|
||||||
<input
|
value={formData.aql_auth_type}
|
||||||
type="text"
|
onChange={(e) => setFormData({ ...formData, aql_auth_type: e.target.value })}
|
||||||
required
|
className="input w-full"
|
||||||
value={formData.username}
|
>
|
||||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
<option value="basic">Basic Auth</option>
|
||||||
className="input w-full"
|
<option value="bearer">Bearer Token</option>
|
||||||
placeholder="postgres"
|
<option value="custom">Custom Header</option>
|
||||||
/>
|
</select>
|
||||||
</div>
|
</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">
|
<div>
|
||||||
<label className="flex items-center gap-2">
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
<input
|
{formData.aql_auth_type === 'basic' && 'Basic Auth Value (Base64)'}
|
||||||
type="checkbox"
|
{formData.aql_auth_type === 'bearer' && 'Bearer Token'}
|
||||||
checked={formData.ssl}
|
{formData.aql_auth_type === 'custom' && 'Custom Authorization Value'}
|
||||||
onChange={(e) => setFormData({ ...formData, ssl: e.target.checked })}
|
</label>
|
||||||
className="rounded"
|
<input
|
||||||
/>
|
type="password"
|
||||||
<span className="text-sm text-gray-700">Использовать SSL</span>
|
required={!database}
|
||||||
</label>
|
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">
|
<div>
|
||||||
<input
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
type="checkbox"
|
Дополнительные заголовки (JSON)
|
||||||
checked={formData.is_active}
|
<span className="text-xs text-gray-500 ml-2">необязательно</span>
|
||||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
</label>
|
||||||
className="rounded"
|
<textarea
|
||||||
/>
|
value={typeof formData.aql_headers === 'string' ? formData.aql_headers : JSON.stringify(formData.aql_headers, null, 2)}
|
||||||
<span className="text-sm text-gray-700">Активна</span>
|
onChange={(e) => {
|
||||||
</label>
|
try {
|
||||||
</div>
|
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">
|
<div className="flex gap-3 pt-4 border-t border-gray-200">
|
||||||
<button type="button" onClick={onClose} className="btn btn-secondary">
|
<button type="button" onClick={onClose} className="btn btn-secondary">
|
||||||
|
|||||||
@@ -13,10 +13,15 @@ export interface AuthResponse {
|
|||||||
export interface Database {
|
export interface Database {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'postgresql';
|
type: 'postgresql' | 'mysql' | 'mssql' | 'aql';
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
database: string;
|
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 {
|
export interface Folder {
|
||||||
@@ -59,10 +64,15 @@ export interface Endpoint {
|
|||||||
user_id: string;
|
user_id: string;
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
enable_logging: boolean;
|
enable_logging: boolean;
|
||||||
execution_type: 'sql' | 'script';
|
execution_type: 'sql' | 'script' | 'aql';
|
||||||
script_language?: 'javascript' | 'python';
|
script_language?: 'javascript' | 'python';
|
||||||
script_code?: string;
|
script_code?: string;
|
||||||
script_queries?: ScriptQuery[];
|
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;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user