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:
@@ -44,6 +44,7 @@ export async function generateDynamicSwagger(): Promise<SwaggerSpec> {
|
||||
e.path,
|
||||
e.parameters,
|
||||
e.is_public,
|
||||
e.response_schema,
|
||||
fp.full_path as folder_name
|
||||
FROM endpoints e
|
||||
LEFT JOIN folder_path fp ON e.folder_id = fp.id
|
||||
@@ -136,7 +137,9 @@ export async function generateDynamicSwagger(): Promise<SwaggerSpec> {
|
||||
description: 'Успешный ответ',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
schema: endpoint.response_schema
|
||||
? endpoint.response_schema
|
||||
: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean' },
|
||||
|
||||
@@ -88,6 +88,7 @@ export const createEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
aql_body,
|
||||
aql_query_params,
|
||||
detailed_response,
|
||||
response_schema,
|
||||
} = req.body;
|
||||
|
||||
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,
|
||||
folder_id, user_id, is_public, enable_logging,
|
||||
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 *`,
|
||||
[
|
||||
name,
|
||||
@@ -147,6 +148,7 @@ export const createEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
aql_body || null,
|
||||
JSON.stringify(aql_query_params || {}),
|
||||
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_query_params,
|
||||
detailed_response,
|
||||
response_schema,
|
||||
} = req.body;
|
||||
|
||||
const result = await mainPool.query(
|
||||
@@ -206,8 +209,9 @@ export const updateEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
aql_body = $17,
|
||||
aql_query_params = $18,
|
||||
detailed_response = $19,
|
||||
response_schema = $20,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $20
|
||||
WHERE id = $21
|
||||
RETURNING *`,
|
||||
[
|
||||
name,
|
||||
@@ -229,6 +233,7 @@ export const updateEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
aql_body || null,
|
||||
aql_query_params ? JSON.stringify(aql_query_params) : null,
|
||||
detailed_response || false,
|
||||
response_schema ? JSON.stringify(response_schema) : null,
|
||||
id,
|
||||
]
|
||||
);
|
||||
@@ -488,6 +493,7 @@ export const exportEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
is_public: endpoint.is_public || false,
|
||||
enable_logging: endpoint.enable_logging || false,
|
||||
detailed_response: endpoint.detailed_response || false,
|
||||
response_schema: endpoint.response_schema || null,
|
||||
folder_name: folderName,
|
||||
};
|
||||
|
||||
@@ -700,9 +706,9 @@ export const importEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
name, description, method, path, database_id, sql_query, parameters,
|
||||
folder_id, user_id, is_public, enable_logging,
|
||||
execution_type, script_language, script_code, script_queries,
|
||||
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 *`,
|
||||
[
|
||||
exportData.name,
|
||||
@@ -725,6 +731,7 @@ export const importEndpoint = async (req: AuthRequest, res: Response) => {
|
||||
exportData.aql_body || null,
|
||||
JSON.stringify(exportData.aql_query_params || {}),
|
||||
exportData.detailed_response || false,
|
||||
exportData.response_schema ? JSON.stringify(exportData.response_schema) : null,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
7
backend/src/migrations/009_add_response_schema.sql
Normal file
7
backend/src/migrations/009_add_response_schema.sql
Normal 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.';
|
||||
@@ -71,6 +71,8 @@ export interface Endpoint {
|
||||
aql_endpoint?: string;
|
||||
aql_body?: string;
|
||||
aql_query_params?: Record<string, string>;
|
||||
// Response schema for Swagger docs
|
||||
response_schema?: object | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
@@ -176,5 +178,7 @@ export interface ExportedEndpoint {
|
||||
is_public: boolean;
|
||||
enable_logging: boolean;
|
||||
detailed_response: boolean;
|
||||
response_schema: object | null;
|
||||
folder_name: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,9 +75,17 @@ export default function EndpointEditor() {
|
||||
aql_query_params: endpointData.aql_query_params || {},
|
||||
detailed_response: endpointData.detailed_response || false,
|
||||
});
|
||||
setResponseSchemaText(
|
||||
endpointData.response_schema
|
||||
? JSON.stringify(endpointData.response_schema, null, 2)
|
||||
: ''
|
||||
);
|
||||
}
|
||||
}, [endpointData]);
|
||||
|
||||
const [responseSchemaText, setResponseSchemaText] = useState('');
|
||||
const [responseSchemaExpanded, setResponseSchemaExpanded] = useState(false);
|
||||
const [responseSchemaError, setResponseSchemaError] = useState('');
|
||||
const [editingQueryIndex, setEditingQueryIndex] = useState<number | null>(null);
|
||||
const [showScriptCodeEditor, setShowScriptCodeEditor] = useState(false);
|
||||
const [parametersExpanded, setParametersExpanded] = useState(true);
|
||||
@@ -215,7 +223,17 @@ export default function EndpointEditor() {
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
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
|
||||
@@ -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 */}
|
||||
<div className="flex flex-wrap items-center gap-6">
|
||||
<label className="flex items-center gap-2">
|
||||
|
||||
@@ -81,6 +81,7 @@ export interface Endpoint {
|
||||
aql_query_params?: Record<string, string>;
|
||||
// Response format
|
||||
detailed_response?: boolean;
|
||||
response_schema?: object | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user