modified: backend/src/config/dynamicSwagger.ts

modified:   backend/src/controllers/endpointController.ts
	new file:   backend/src/migrations/009_add_response_schema.sql
	modified:   backend/src/types/index.ts
	modified:   frontend/src/pages/EndpointEditor.tsx
	modified:   frontend/src/types/index.ts
This commit is contained in:
2026-03-13 15:22:32 +03:00
parent b6b7064a41
commit 727c6765f8
6 changed files with 94 additions and 15 deletions

View File

@@ -44,6 +44,7 @@ export async function generateDynamicSwagger(): Promise<SwaggerSpec> {
e.path, e.path,
e.parameters, e.parameters,
e.is_public, e.is_public,
e.response_schema,
fp.full_path as folder_name fp.full_path as folder_name
FROM endpoints e FROM endpoints e
LEFT JOIN folder_path fp ON e.folder_id = fp.id LEFT JOIN folder_path fp ON e.folder_id = fp.id
@@ -136,15 +137,17 @@ export async function generateDynamicSwagger(): Promise<SwaggerSpec> {
description: 'Успешный ответ', description: 'Успешный ответ',
content: { content: {
'application/json': { 'application/json': {
schema: { schema: endpoint.response_schema
type: 'object', ? endpoint.response_schema
properties: { : {
success: { type: 'boolean' }, type: 'object',
data: { type: 'array', items: { type: 'object' } }, properties: {
rowCount: { type: 'number' }, success: { type: 'boolean' },
executionTime: { type: 'number' }, data: { type: 'array', items: { type: 'object' } },
}, rowCount: { type: 'number' },
}, executionTime: { type: 'number' },
},
},
}, },
}, },
}, },

View File

@@ -88,6 +88,7 @@ export const createEndpoint = async (req: AuthRequest, res: Response) => {
aql_body, aql_body,
aql_query_params, aql_query_params,
detailed_response, detailed_response,
response_schema,
} = req.body; } = req.body;
if (!name || !method || !path) { if (!name || !method || !path) {
@@ -122,9 +123,9 @@ export const createEndpoint = async (req: AuthRequest, res: Response) => {
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, detailed_response aql_method, aql_endpoint, aql_body, aql_query_params, detailed_response, response_schema
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
RETURNING *`, RETURNING *`,
[ [
name, name,
@@ -147,6 +148,7 @@ export const createEndpoint = async (req: AuthRequest, res: Response) => {
aql_body || null, aql_body || null,
JSON.stringify(aql_query_params || {}), JSON.stringify(aql_query_params || {}),
detailed_response || false, detailed_response || false,
response_schema ? JSON.stringify(response_schema) : null,
] ]
); );
@@ -183,6 +185,7 @@ export const updateEndpoint = async (req: AuthRequest, res: Response) => {
aql_body, aql_body,
aql_query_params, aql_query_params,
detailed_response, detailed_response,
response_schema,
} = req.body; } = req.body;
const result = await mainPool.query( const result = await mainPool.query(
@@ -206,8 +209,9 @@ export const updateEndpoint = async (req: AuthRequest, res: Response) => {
aql_body = $17, aql_body = $17,
aql_query_params = $18, aql_query_params = $18,
detailed_response = $19, detailed_response = $19,
response_schema = $20,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $20 WHERE id = $21
RETURNING *`, RETURNING *`,
[ [
name, name,
@@ -229,6 +233,7 @@ export const updateEndpoint = async (req: AuthRequest, res: Response) => {
aql_body || null, aql_body || null,
aql_query_params ? JSON.stringify(aql_query_params) : null, aql_query_params ? JSON.stringify(aql_query_params) : null,
detailed_response || false, detailed_response || false,
response_schema ? JSON.stringify(response_schema) : null,
id, id,
] ]
); );
@@ -488,6 +493,7 @@ export const exportEndpoint = async (req: AuthRequest, res: Response) => {
is_public: endpoint.is_public || false, is_public: endpoint.is_public || false,
enable_logging: endpoint.enable_logging || false, enable_logging: endpoint.enable_logging || false,
detailed_response: endpoint.detailed_response || false, detailed_response: endpoint.detailed_response || false,
response_schema: endpoint.response_schema || null,
folder_name: folderName, folder_name: folderName,
}; };
@@ -700,9 +706,9 @@ export const importEndpoint = async (req: AuthRequest, res: Response) => {
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, detailed_response aql_method, aql_endpoint, aql_body, aql_query_params, detailed_response, response_schema
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
RETURNING *`, RETURNING *`,
[ [
exportData.name, exportData.name,
@@ -725,6 +731,7 @@ export const importEndpoint = async (req: AuthRequest, res: Response) => {
exportData.aql_body || null, exportData.aql_body || null,
JSON.stringify(exportData.aql_query_params || {}), JSON.stringify(exportData.aql_query_params || {}),
exportData.detailed_response || false, exportData.detailed_response || false,
exportData.response_schema ? JSON.stringify(exportData.response_schema) : null,
] ]
); );

View File

@@ -0,0 +1,7 @@
-- Add response_schema to endpoints for Swagger documentation
-- Stores an OpenAPI-compatible JSON schema describing the 200 response
ALTER TABLE endpoints
ADD COLUMN IF NOT EXISTS response_schema JSONB DEFAULT NULL;
COMMENT ON COLUMN endpoints.response_schema IS 'Optional OpenAPI JSON schema for the 200 response, displayed in Swagger documentation.';

View File

@@ -71,6 +71,8 @@ export interface Endpoint {
aql_endpoint?: string; aql_endpoint?: string;
aql_body?: string; aql_body?: string;
aql_query_params?: Record<string, string>; aql_query_params?: Record<string, string>;
// Response schema for Swagger docs
response_schema?: object | null;
created_at: Date; created_at: Date;
updated_at: Date; updated_at: Date;
} }
@@ -176,5 +178,7 @@ export interface ExportedEndpoint {
is_public: boolean; is_public: boolean;
enable_logging: boolean; enable_logging: boolean;
detailed_response: boolean; detailed_response: boolean;
response_schema: object | null;
folder_name: string | null; folder_name: string | null;
} }

View File

@@ -75,9 +75,17 @@ export default function EndpointEditor() {
aql_query_params: endpointData.aql_query_params || {}, aql_query_params: endpointData.aql_query_params || {},
detailed_response: endpointData.detailed_response || false, detailed_response: endpointData.detailed_response || false,
}); });
setResponseSchemaText(
endpointData.response_schema
? JSON.stringify(endpointData.response_schema, null, 2)
: ''
);
} }
}, [endpointData]); }, [endpointData]);
const [responseSchemaText, setResponseSchemaText] = useState('');
const [responseSchemaExpanded, setResponseSchemaExpanded] = useState(false);
const [responseSchemaError, setResponseSchemaError] = useState('');
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 [parametersExpanded, setParametersExpanded] = useState(true); const [parametersExpanded, setParametersExpanded] = useState(true);
@@ -215,7 +223,17 @@ export default function EndpointEditor() {
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
saveMutation.mutate(formData); let parsedSchema = null;
if (responseSchemaText.trim()) {
try {
parsedSchema = JSON.parse(responseSchemaText);
} catch {
setResponseSchemaError('Некорректный JSON');
setResponseSchemaExpanded(true);
return;
}
}
saveMutation.mutate({ ...formData, response_schema: parsedSchema });
}; };
// cURL generator // cURL generator
@@ -738,6 +756,45 @@ export default function EndpointEditor() {
</> </>
)} )}
{/* Response schema */}
<div className="border border-gray-200 rounded-lg">
<div
className="flex items-center justify-between p-3 bg-gray-50 cursor-pointer hover:bg-gray-100 rounded-t-lg"
onClick={() => setResponseSchemaExpanded(!responseSchemaExpanded)}
>
<div className="flex items-center gap-2">
{responseSchemaExpanded ? <ChevronUp size={18} /> : <ChevronDown size={18} />}
<label className="text-sm font-medium text-gray-700 cursor-pointer">
Схема ответа 200 (Swagger)
</label>
{responseSchemaText.trim() && (
<span className="px-2 py-0.5 bg-green-100 text-green-700 rounded-full text-xs">задана</span>
)}
{responseSchemaError && (
<span className="px-2 py-0.5 bg-red-100 text-red-700 rounded-full text-xs">{responseSchemaError}</span>
)}
</div>
</div>
{responseSchemaExpanded && (
<div className="p-4 bg-white rounded-b-lg space-y-2">
<p className="text-xs text-gray-500">
JSON Schema в формате OpenAPI для документирования ответа. Если не задана используется схема по умолчанию.
</p>
<textarea
value={responseSchemaText}
onChange={(e) => {
setResponseSchemaText(e.target.value);
setResponseSchemaError('');
}}
className="input w-full font-mono text-xs"
rows={10}
placeholder={'{\n "type": "array",\n "items": {\n "type": "object",\n "properties": {\n "id": { "type": "number" },\n "name": { "type": "string" }\n }\n }\n}'}
spellCheck={false}
/>
</div>
)}
</div>
{/* Checkboxes */} {/* Checkboxes */}
<div className="flex flex-wrap items-center gap-6"> <div className="flex flex-wrap items-center gap-6">
<label className="flex items-center gap-2"> <label className="flex items-center gap-2">

View File

@@ -81,6 +81,7 @@ export interface Endpoint {
aql_query_params?: Record<string, string>; aql_query_params?: Record<string, string>;
// Response format // Response format
detailed_response?: boolean; detailed_response?: boolean;
response_schema?: object | null;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
} }