Add test/prod environments and endpoint versioning
Phase 1: Test/Prod Database Configurations - Migration 010: test_* columns on databases table, environment column on request_logs - DatabasePoolManager: dual-pool strategy (prod + test), getPool(id, env) with fallback - SqlExecutor, ScriptExecutor, IsolatedScriptExecutor, AqlExecutor: environment param threaded through all execution paths - dynamicApiController: X-Environment header detection, environment in logging - databaseManagementController: CRUD for test credentials, testConnection with ?env=test - Frontend: test env form in DatabaseModal, env toggle in EndpointEditor test panel Phase 2: Endpoint Versioning - Migration 011: endpoint_versions table with full snapshots, backfill v1 for existing endpoints - VersionService: createVersion, saveDraft, publishVersion, rollbackToVersion, getHistory - endpointController: auto-versioning on update, 6 new version management handlers - dynamicApiController: draft serving when environment=test and draft exists - Frontend: Save Draft button, version history panel with publish/rollback actions Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { endpointsApi, foldersApi, databasesApi } from '@/services/api';
|
||||
import { EndpointParameter, QueryTestResult, LogEntry, QueryExecution } from '@/types';
|
||||
import { Plus, Trash2, Play, Edit2, ChevronDown, ChevronUp, ArrowLeft, CheckCircle, XCircle, Clock, Copy, X, Terminal } from 'lucide-react';
|
||||
import { endpointsApi, foldersApi, databasesApi, versionsApi } from '@/services/api';
|
||||
import { EndpointParameter, QueryTestResult, LogEntry, QueryExecution, EndpointVersion } from '@/types';
|
||||
import { Plus, Trash2, Play, Edit2, ChevronDown, ChevronUp, ArrowLeft, CheckCircle, XCircle, Clock, Copy, X, Terminal, History, RotateCcw, Upload } from 'lucide-react';
|
||||
import toast from 'react-hot-toast';
|
||||
import SqlEditor from '@/components/SqlEditor';
|
||||
import CodeEditor from '@/components/CodeEditor';
|
||||
@@ -93,6 +93,7 @@ export default function EndpointEditor() {
|
||||
const [testResult, setTestResult] = useState<QueryTestResult | null>(null);
|
||||
const [activeResultTab, setActiveResultTab] = useState<'data' | 'logs' | 'queries'>('data');
|
||||
const [curlApiKey, setCurlApiKey] = useState('');
|
||||
const [testEnvironment, setTestEnvironment] = useState<'test' | 'prod'>('test');
|
||||
|
||||
const selectedDatabase = databases?.find(db => db.id === formData.database_id);
|
||||
const isAqlDatabase = selectedDatabase?.type === 'aql';
|
||||
@@ -189,6 +190,7 @@ export default function EndpointEditor() {
|
||||
aql_query_params: typeof formData.aql_query_params === 'string' ? {} : formData.aql_query_params || {},
|
||||
parameters: paramValues,
|
||||
endpoint_parameters: formData.parameters,
|
||||
environment: testEnvironment,
|
||||
} as any);
|
||||
} else if (formData.execution_type === 'script') {
|
||||
const scriptQueries = formData.script_queries || [];
|
||||
@@ -201,6 +203,7 @@ export default function EndpointEditor() {
|
||||
script_queries: scriptQueries,
|
||||
parameters: paramValues,
|
||||
endpoint_parameters: formData.parameters,
|
||||
environment: testEnvironment,
|
||||
});
|
||||
} else {
|
||||
return endpointsApi.test({
|
||||
@@ -209,6 +212,7 @@ export default function EndpointEditor() {
|
||||
sql_query: formData.sql_query || '',
|
||||
parameters: paramValues,
|
||||
endpoint_parameters: formData.parameters,
|
||||
environment: testEnvironment,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -230,6 +234,43 @@ export default function EndpointEditor() {
|
||||
},
|
||||
});
|
||||
|
||||
// Version history
|
||||
const { data: versions, refetch: refetchVersions } = useQuery({
|
||||
queryKey: ['versions', id],
|
||||
queryFn: () => versionsApi.getHistory(id!).then(res => res.data),
|
||||
enabled: isEditing,
|
||||
});
|
||||
const [showVersionHistory, setShowVersionHistory] = useState(false);
|
||||
|
||||
const draftMutation = useMutation({
|
||||
mutationFn: () => versionsApi.saveDraft(id!, { ...formData, change_message: undefined }),
|
||||
onSuccess: () => {
|
||||
toast.success('Draft saved');
|
||||
refetchVersions();
|
||||
},
|
||||
onError: () => toast.error('Failed to save draft'),
|
||||
});
|
||||
|
||||
const publishVersionMutation = useMutation({
|
||||
mutationFn: (versionId: string) => versionsApi.publish(id!, versionId),
|
||||
onSuccess: () => {
|
||||
toast.success('Version published');
|
||||
queryClient.invalidateQueries({ queryKey: ['endpoint', id] });
|
||||
refetchVersions();
|
||||
},
|
||||
onError: () => toast.error('Failed to publish version'),
|
||||
});
|
||||
|
||||
const rollbackMutation = useMutation({
|
||||
mutationFn: (versionId: string) => versionsApi.rollback(id!, versionId),
|
||||
onSuccess: () => {
|
||||
toast.success('Rolled back');
|
||||
queryClient.invalidateQueries({ queryKey: ['endpoint', id] });
|
||||
refetchVersions();
|
||||
},
|
||||
onError: () => toast.error('Failed to rollback'),
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
let parsedSchema = null;
|
||||
@@ -841,13 +882,23 @@ export default function EndpointEditor() {
|
||||
<button type="button" onClick={() => navigate(-1)} className="btn btn-secondary">
|
||||
Отмена
|
||||
</button>
|
||||
{isEditing && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={draftMutation?.isPending}
|
||||
className="btn bg-orange-50 text-orange-700 border border-orange-200 hover:bg-orange-100"
|
||||
onClick={() => draftMutation?.mutate()}
|
||||
>
|
||||
{draftMutation?.isPending ? 'Сохранение...' : 'Save Draft'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saveMutation.isPending}
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setSaveAndStay(true)}
|
||||
>
|
||||
{saveMutation.isPending && saveAndStay ? 'Сохранение...' : 'Сохранить'}
|
||||
{saveMutation.isPending && saveAndStay ? 'Публикация...' : 'Publish'}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
@@ -855,7 +906,7 @@ export default function EndpointEditor() {
|
||||
className="btn btn-primary"
|
||||
onClick={() => setSaveAndStay(false)}
|
||||
>
|
||||
{saveMutation.isPending && !saveAndStay ? 'Сохранение...' : 'Сохранить и выйти'}
|
||||
{saveMutation.isPending && !saveAndStay ? 'Публикация...' : 'Publish & Exit'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -901,8 +952,38 @@ export default function EndpointEditor() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Test button */}
|
||||
<div className="card p-4">
|
||||
{/* Environment toggle + Test button */}
|
||||
<div className="card p-4 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-gray-500">Среда:</span>
|
||||
<div className="flex rounded-lg overflow-hidden border border-gray-200">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTestEnvironment('test')}
|
||||
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
testEnvironment === 'test'
|
||||
? 'bg-orange-500 text-white'
|
||||
: 'bg-white text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
Test
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTestEnvironment('prod')}
|
||||
className={`px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
testEnvironment === 'prod'
|
||||
? 'bg-green-600 text-white'
|
||||
: 'bg-white text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
Prod
|
||||
</button>
|
||||
</div>
|
||||
{selectedDatabase && !selectedDatabase.has_test_env && testEnvironment === 'test' && (
|
||||
<span className="text-xs text-amber-600">Test env не настроена — fallback на prod</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => testMutation.mutate()}
|
||||
@@ -915,10 +996,14 @@ export default function EndpointEditor() {
|
||||
: !formData.script_code
|
||||
)
|
||||
}
|
||||
className="btn btn-primary w-full flex items-center justify-center gap-2"
|
||||
className={`btn w-full flex items-center justify-center gap-2 ${
|
||||
testEnvironment === 'test'
|
||||
? 'bg-orange-500 hover:bg-orange-600 text-white'
|
||||
: 'btn-primary'
|
||||
}`}
|
||||
>
|
||||
<Play size={18} />
|
||||
{testMutation.isPending ? 'Тестирование...' : 'Тест запроса'}
|
||||
{testMutation.isPending ? 'Тестирование...' : `Тест (${testEnvironment})`}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -952,6 +1037,68 @@ export default function EndpointEditor() {
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Version History */}
|
||||
{isEditing && (
|
||||
<div className="card p-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowVersionHistory(!showVersionHistory)}
|
||||
className="flex items-center gap-2 text-sm font-medium text-gray-700 hover:text-gray-900 w-full"
|
||||
>
|
||||
<History size={16} />
|
||||
<span>Version History</span>
|
||||
<span className="text-xs text-gray-400 ml-1">({versions?.length || 0})</span>
|
||||
<span className="ml-auto">{showVersionHistory ? <ChevronUp size={14} /> : <ChevronDown size={14} />}</span>
|
||||
</button>
|
||||
|
||||
{showVersionHistory && versions && (
|
||||
<div className="mt-3 space-y-2 max-h-80 overflow-y-auto">
|
||||
{versions.map((v: EndpointVersion) => (
|
||||
<div key={v.id} className="flex items-center gap-2 text-xs border border-gray-100 rounded-lg p-2">
|
||||
<span className="font-mono font-bold text-gray-700">v{v.version_number}</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-[10px] font-medium ${
|
||||
v.status === 'published' ? 'bg-green-100 text-green-700' :
|
||||
v.status === 'draft' ? 'bg-orange-100 text-orange-700' :
|
||||
'bg-gray-100 text-gray-500'
|
||||
}`}>
|
||||
{v.status}
|
||||
</span>
|
||||
<span className="text-gray-400 truncate flex-1" title={v.change_message || ''}>
|
||||
{v.change_message || '-'}
|
||||
</span>
|
||||
<span className="text-gray-400 whitespace-nowrap">
|
||||
{new Date(v.created_at).toLocaleDateString('ru-RU')}
|
||||
</span>
|
||||
{v.status === 'draft' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => publishVersionMutation.mutate(v.id)}
|
||||
className="p-1 hover:bg-green-50 rounded text-green-600"
|
||||
title="Publish"
|
||||
>
|
||||
<Upload size={12} />
|
||||
</button>
|
||||
)}
|
||||
{v.status !== 'draft' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => rollbackMutation.mutate(v.id)}
|
||||
className="p-1 hover:bg-blue-50 rounded text-blue-600"
|
||||
title="Rollback to this version"
|
||||
>
|
||||
<RotateCcw size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{versions.length === 0 && (
|
||||
<p className="text-xs text-gray-400 text-center py-2">No versions yet</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Test results */}
|
||||
{testResult && (
|
||||
<div className="card overflow-hidden">
|
||||
|
||||
@@ -284,13 +284,13 @@ function DatabasesSubTab() {
|
||||
onError: () => toast.error('Не удалось удалить базу данных'),
|
||||
});
|
||||
|
||||
const testConnection = async (databaseId: string) => {
|
||||
const testConnection = async (databaseId: string, env?: 'prod' | 'test') => {
|
||||
try {
|
||||
const { data } = await dbManagementApi.test(databaseId);
|
||||
const { data } = await dbManagementApi.test(databaseId, env);
|
||||
if (data.success) {
|
||||
toast.success('Подключение успешно!');
|
||||
toast.success(env === 'test' ? 'Test Env: подключение успешно!' : 'Подключение успешно!');
|
||||
} else {
|
||||
toast.error('Ошибка подключения');
|
||||
toast.error(env === 'test' ? 'Test Env: ошибка подключения' : 'Ошибка подключения');
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Ошибка тестирования подключения');
|
||||
@@ -350,6 +350,9 @@ function DatabasesSubTab() {
|
||||
{db.is_active && (
|
||||
<span className="text-xs bg-green-100 text-green-700 px-2 py-1 rounded">Активна</span>
|
||||
)}
|
||||
{db.has_test_env && (
|
||||
<span className="text-xs bg-orange-100 text-orange-700 px-2 py-1 rounded">Test Env</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-sm text-gray-600 ml-8">
|
||||
{db.type === 'aql' ? (
|
||||
@@ -375,6 +378,14 @@ function DatabasesSubTab() {
|
||||
>
|
||||
Тест
|
||||
</button>
|
||||
{db.has_test_env && (
|
||||
<button
|
||||
onClick={() => testConnection(db.id, 'test')}
|
||||
className="btn text-sm bg-orange-50 text-orange-700 border border-orange-200 hover:bg-orange-100"
|
||||
>
|
||||
Тест (Test Env)
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => handleEdit(db)}
|
||||
className="btn btn-secondary text-sm flex items-center gap-1"
|
||||
@@ -431,6 +442,7 @@ function DatabaseModal({
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showTestEnv, setShowTestEnv] = useState(database?.has_test_env || false);
|
||||
const [formData, setFormData] = useState({
|
||||
name: database?.name || '',
|
||||
type: database?.type || 'postgresql',
|
||||
@@ -446,18 +458,30 @@ function DatabaseModal({
|
||||
aql_auth_type: database?.aql_auth_type || 'basic',
|
||||
aql_auth_value: database?.aql_auth_value || '',
|
||||
aql_headers: database?.aql_headers || {},
|
||||
// Test environment
|
||||
has_test_env: database?.has_test_env || false,
|
||||
test_host: database?.test_host || '',
|
||||
test_port: database?.test_port || 5432,
|
||||
test_database_name: database?.test_database_name || '',
|
||||
test_username: database?.test_username || '',
|
||||
test_password: '',
|
||||
test_ssl: database?.test_ssl || false,
|
||||
test_aql_base_url: database?.test_aql_base_url || '',
|
||||
test_aql_auth_value: '',
|
||||
test_aql_headers: database?.test_aql_headers || {},
|
||||
});
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (data: any) => {
|
||||
const payload = { ...data };
|
||||
// Если редактируем и пароль пустой, удаляем его из payload
|
||||
if (database && !payload.password) {
|
||||
delete payload.password;
|
||||
}
|
||||
// Для AQL: если редактируем и auth_value пустой, удаляем его
|
||||
if (database && data.type === 'aql' && !payload.aql_auth_value) {
|
||||
delete payload.aql_auth_value;
|
||||
if (database && !payload.password) delete payload.password;
|
||||
if (database && data.type === 'aql' && !payload.aql_auth_value) delete payload.aql_auth_value;
|
||||
if (database && !payload.test_password) delete payload.test_password;
|
||||
if (database && !payload.test_aql_auth_value) delete payload.test_aql_auth_value;
|
||||
if (!payload.has_test_env) {
|
||||
delete payload.test_host; delete payload.test_port; delete payload.test_database_name;
|
||||
delete payload.test_username; delete payload.test_password; delete payload.test_ssl;
|
||||
delete payload.test_aql_base_url; delete payload.test_aql_auth_value; delete payload.test_aql_headers;
|
||||
}
|
||||
return database ? dbManagementApi.update(database.id, payload) : dbManagementApi.create(payload);
|
||||
},
|
||||
@@ -693,6 +717,123 @@ function DatabaseModal({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Test Environment Section */}
|
||||
<div className="border-t border-gray-200 pt-4 mt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const next = !showTestEnv;
|
||||
setShowTestEnv(next);
|
||||
setFormData({ ...formData, has_test_env: next });
|
||||
}}
|
||||
className="flex items-center gap-2 text-sm font-medium text-gray-700 hover:text-gray-900"
|
||||
>
|
||||
<span className={`transform transition-transform ${showTestEnv ? 'rotate-90' : ''}`}>▶</span>
|
||||
Тестовая среда
|
||||
{formData.has_test_env && (
|
||||
<span className="text-xs bg-orange-100 text-orange-700 px-2 py-0.5 rounded">Включена</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{showTestEnv && (
|
||||
<div className="mt-4 space-y-4 pl-4 border-l-2 border-orange-200">
|
||||
<p className="text-xs text-gray-500">
|
||||
Альтернативные креды для тестирования endpoint-ов. При тестировании запросы пойдут на эту БД вместо продовой.
|
||||
</p>
|
||||
|
||||
{formData.type === 'aql' ? (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Test AQL Base URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.test_aql_base_url}
|
||||
onChange={(e) => setFormData({ ...formData, test_aql_base_url: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="http://test-api.example.com/api/rest/v1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Test Auth Value</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.test_aql_auth_value}
|
||||
onChange={(e) => setFormData({ ...formData, test_aql_auth_value: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder={database ? '(не менять)' : 'Значение'}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Test Host</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.test_host}
|
||||
onChange={(e) => setFormData({ ...formData, test_host: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="test-db-host"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Test Port</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.test_port}
|
||||
onChange={(e) => setFormData({ ...formData, test_port: parseInt(e.target.value) })}
|
||||
className="input w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Test DB Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.test_database_name}
|
||||
onChange={(e) => setFormData({ ...formData, test_database_name: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="test_database"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Test User</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.test_username}
|
||||
onChange={(e) => setFormData({ ...formData, test_username: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder="test_user"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Test Password</label>
|
||||
<input
|
||||
type="password"
|
||||
value={formData.test_password}
|
||||
onChange={(e) => setFormData({ ...formData, test_password: e.target.value })}
|
||||
className="input w-full"
|
||||
placeholder={database ? '(не менять)' : 'Пароль'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.test_ssl}
|
||||
onChange={(e) => setFormData({ ...formData, test_ssl: e.target.checked })}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm text-gray-700">Test SSL</span>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4 border-t border-gray-200">
|
||||
<button type="button" onClick={onClose} className="btn btn-secondary">
|
||||
Отмена
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { AuthResponse, User, Endpoint, Folder, ApiKey, Database, QueryTestResult, ImportPreviewResponse } from '@/types';
|
||||
import { AuthResponse, User, Endpoint, Folder, ApiKey, Database, QueryTestResult, ImportPreviewResponse, EndpointVersion } from '@/types';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
@@ -86,8 +86,8 @@ export const dbManagementApi = {
|
||||
delete: (id: string) =>
|
||||
api.delete(`/db-management/${id}`),
|
||||
|
||||
test: (id: string) =>
|
||||
api.get<{ success: boolean; message: string }>(`/db-management/${id}/test`),
|
||||
test: (id: string, env?: 'prod' | 'test') =>
|
||||
api.get<{ success: boolean; message: string }>(`/db-management/${id}/test${env === 'test' ? '?env=test' : ''}`),
|
||||
};
|
||||
|
||||
// Endpoints API
|
||||
@@ -120,6 +120,7 @@ export const endpointsApi = {
|
||||
aql_endpoint?: string;
|
||||
aql_body?: string;
|
||||
aql_query_params?: Record<string, string>;
|
||||
environment?: 'prod' | 'test';
|
||||
}) =>
|
||||
api.post<QueryTestResult>('/endpoints/test', data),
|
||||
|
||||
@@ -142,6 +143,27 @@ export const endpointsApi = {
|
||||
api.post<Endpoint>('/endpoints/import', data),
|
||||
};
|
||||
|
||||
// Versions API
|
||||
export const versionsApi = {
|
||||
getHistory: (endpointId: string) =>
|
||||
api.get<EndpointVersion[]>(`/endpoints/${endpointId}/versions`),
|
||||
|
||||
getVersion: (endpointId: string, versionId: string) =>
|
||||
api.get<EndpointVersion>(`/endpoints/${endpointId}/versions/${versionId}`),
|
||||
|
||||
publish: (endpointId: string, versionId: string) =>
|
||||
api.post(`/endpoints/${endpointId}/versions/${versionId}/publish`),
|
||||
|
||||
rollback: (endpointId: string, versionId: string) =>
|
||||
api.post(`/endpoints/${endpointId}/versions/${versionId}/rollback`),
|
||||
|
||||
saveDraft: (endpointId: string, data: any) =>
|
||||
api.post<EndpointVersion>(`/endpoints/${endpointId}/draft`, data),
|
||||
|
||||
getDraft: (endpointId: string) =>
|
||||
api.get<EndpointVersion>(`/endpoints/${endpointId}/draft`),
|
||||
};
|
||||
|
||||
// Folders API
|
||||
export const foldersApi = {
|
||||
getAll: () =>
|
||||
|
||||
@@ -22,6 +22,15 @@ export interface Database {
|
||||
aql_auth_type?: 'basic' | 'bearer' | 'custom';
|
||||
aql_auth_value?: string;
|
||||
aql_headers?: Record<string, string>;
|
||||
// Test environment
|
||||
has_test_env?: boolean;
|
||||
test_host?: string;
|
||||
test_port?: number;
|
||||
test_database_name?: string;
|
||||
test_username?: string;
|
||||
test_ssl?: boolean;
|
||||
test_aql_base_url?: string;
|
||||
test_aql_headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
@@ -125,6 +134,39 @@ export interface QueryTestResult {
|
||||
processedQuery?: string;
|
||||
}
|
||||
|
||||
export type VersionStatus = 'draft' | 'published' | 'archived';
|
||||
|
||||
export interface EndpointVersion {
|
||||
id: string;
|
||||
endpoint_id: string;
|
||||
version_number: number;
|
||||
name: string;
|
||||
description: string;
|
||||
method: string;
|
||||
path: string;
|
||||
database_id: string;
|
||||
sql_query: string;
|
||||
parameters: EndpointParameter[];
|
||||
execution_type: string;
|
||||
script_language?: string;
|
||||
script_code?: string;
|
||||
script_queries?: ScriptQuery[];
|
||||
aql_method?: string;
|
||||
aql_endpoint?: string;
|
||||
aql_body?: string;
|
||||
aql_query_params?: Record<string, string>;
|
||||
is_public: boolean;
|
||||
enable_logging: boolean;
|
||||
detailed_response: boolean;
|
||||
response_schema?: object | null;
|
||||
status: VersionStatus;
|
||||
change_message: string | null;
|
||||
user_id: string;
|
||||
user_name?: string;
|
||||
content_hash: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ImportPreviewDatabase {
|
||||
name: string;
|
||||
type: string;
|
||||
|
||||
Reference in New Issue
Block a user