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:
@@ -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= '$compositionId'"}'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">AQL Query Parameters (JSON)</label>
|
||||
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700">
|
||||
<div>Формат: <code className="bg-blue-100 px-1 rounded">{`{"key": "value", "CompositionLink": "$linkValue"}`}</code></div>
|
||||
</div>
|
||||
<textarea
|
||||
value={typeof formData.aql_query_params === 'string' ? formData.aql_query_params : JSON.stringify(formData.aql_query_params, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value);
|
||||
setFormData({ ...formData, aql_query_params: parsed });
|
||||
} catch {
|
||||
// Игнорируем невалидный JSON - не обновляем состояние
|
||||
}
|
||||
}}
|
||||
className="input w-full font-mono text-sm"
|
||||
rows={4}
|
||||
placeholder='{"CompositionLink": "ehr:compositions/$compositionId"}'
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : formData.execution_type === 'sql' ? (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">SQL Запрос</label>
|
||||
<div className="mb-2 p-2 bg-blue-50 border border-blue-200 rounded text-xs text-blue-700 space-y-1">
|
||||
@@ -512,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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -13,10 +13,15 @@ export interface AuthResponse {
|
||||
export interface Database {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'postgresql';
|
||||
type: 'postgresql' | 'mysql' | 'mssql' | 'aql';
|
||||
host: string;
|
||||
port: number;
|
||||
database: string;
|
||||
// AQL-specific fields
|
||||
aql_base_url?: string;
|
||||
aql_auth_type?: 'basic' | 'bearer' | 'custom';
|
||||
aql_auth_value?: string;
|
||||
aql_headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
@@ -59,10 +64,15 @@ export interface Endpoint {
|
||||
user_id: string;
|
||||
is_public: boolean;
|
||||
enable_logging: boolean;
|
||||
execution_type: 'sql' | 'script';
|
||||
execution_type: 'sql' | 'script' | 'aql';
|
||||
script_language?: 'javascript' | 'python';
|
||||
script_code?: string;
|
||||
script_queries?: ScriptQuery[];
|
||||
// AQL-specific fields
|
||||
aql_method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
aql_endpoint?: string;
|
||||
aql_body?: string;
|
||||
aql_query_params?: Record<string, string>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user