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
241 lines
6.9 KiB
TypeScript
241 lines
6.9 KiB
TypeScript
import { mainPool } from './database';
|
||
|
||
interface SwaggerPath {
|
||
[method: string]: {
|
||
tags: string[];
|
||
summary: string;
|
||
description: string;
|
||
security: { apiKey: [] }[];
|
||
parameters: any[];
|
||
responses: any;
|
||
};
|
||
}
|
||
|
||
interface SwaggerSpec {
|
||
openapi: string;
|
||
info: any;
|
||
servers: any[];
|
||
components: any;
|
||
paths: { [path: string]: SwaggerPath };
|
||
tags: any[];
|
||
}
|
||
|
||
export async function generateDynamicSwagger(): Promise<SwaggerSpec> {
|
||
// Загружаем все эндпоинты из базы с полным путем папки
|
||
const endpointsResult = await mainPool.query(`
|
||
WITH RECURSIVE folder_path AS (
|
||
-- Базовый случай: папки без родителя
|
||
SELECT id, name, parent_id, name::text as full_path
|
||
FROM folders
|
||
WHERE parent_id IS NULL
|
||
|
||
UNION ALL
|
||
|
||
-- Рекурсивный случай: добавляем дочерние папки
|
||
SELECT f.id, f.name, f.parent_id, (fp.full_path || ' / ' || f.name)::text as full_path
|
||
FROM folders f
|
||
INNER JOIN folder_path fp ON f.parent_id = fp.id
|
||
)
|
||
SELECT
|
||
e.id,
|
||
e.name,
|
||
e.description,
|
||
e.method,
|
||
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
|
||
ORDER BY fp.full_path, e.name
|
||
`);
|
||
|
||
const endpoints = endpointsResult.rows;
|
||
|
||
// Группируем теги по папкам
|
||
const tags: any[] = [];
|
||
const folderSet = new Set<string>();
|
||
|
||
endpoints.forEach((endpoint: any) => {
|
||
const folderName = endpoint.folder_name || 'Без категории';
|
||
if (!folderSet.has(folderName)) {
|
||
folderSet.add(folderName);
|
||
tags.push({
|
||
name: folderName,
|
||
description: `Эндпоинты в папке "${folderName}"`,
|
||
});
|
||
}
|
||
});
|
||
|
||
// Генерируем пути
|
||
const paths: { [path: string]: SwaggerPath } = {};
|
||
|
||
endpoints.forEach((endpoint: any) => {
|
||
const method = endpoint.method.toLowerCase();
|
||
const swaggerPath = endpoint.path.replace(/\/api\/v1/, '');
|
||
const folderName = endpoint.folder_name || 'Без категории';
|
||
|
||
// Парсим параметры - PostgreSQL может вернуть как JSON строку, так и уже распарсенный объект
|
||
const parameters: any[] = [];
|
||
const bodyParams: any = {};
|
||
const bodyRequired: string[] = [];
|
||
|
||
let params: any[] = [];
|
||
if (endpoint.parameters) {
|
||
if (typeof endpoint.parameters === 'string') {
|
||
try {
|
||
params = JSON.parse(endpoint.parameters);
|
||
} catch (e) {
|
||
params = [];
|
||
}
|
||
} else if (Array.isArray(endpoint.parameters)) {
|
||
params = endpoint.parameters;
|
||
}
|
||
}
|
||
|
||
if (params.length > 0) {
|
||
params.forEach((param: any) => {
|
||
if (param.in === 'body') {
|
||
// Body параметры идут в requestBody
|
||
bodyParams[param.name] = {
|
||
type: param.type || 'string',
|
||
description: param.description || '',
|
||
default: param.default_value,
|
||
};
|
||
if (param.required) {
|
||
bodyRequired.push(param.name);
|
||
}
|
||
} else {
|
||
// Query и path параметры
|
||
parameters.push({
|
||
name: param.name,
|
||
in: param.in || 'query',
|
||
required: param.required || false,
|
||
description: param.description || '',
|
||
schema: {
|
||
type: param.type || 'string',
|
||
default: param.default_value,
|
||
},
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
if (!paths[swaggerPath]) {
|
||
paths[swaggerPath] = {} as SwaggerPath;
|
||
}
|
||
|
||
const endpointSpec: any = {
|
||
tags: [folderName],
|
||
summary: endpoint.name,
|
||
description: endpoint.description || '',
|
||
security: endpoint.is_public ? [] : [{ apiKey: [] }],
|
||
parameters,
|
||
responses: {
|
||
'200': {
|
||
description: 'Успешный ответ',
|
||
content: {
|
||
'application/json': {
|
||
schema: endpoint.response_schema
|
||
? endpoint.response_schema
|
||
: {
|
||
type: 'object',
|
||
properties: {
|
||
success: { type: 'boolean' },
|
||
data: { type: 'array', items: { type: 'object' } },
|
||
rowCount: { type: 'number' },
|
||
executionTime: { type: 'number' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'400': {
|
||
description: 'Ошибка в запросе',
|
||
content: {
|
||
'application/json': {
|
||
schema: {
|
||
type: 'object',
|
||
properties: {
|
||
error: { type: 'string' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'401': {
|
||
description: 'Не авторизован (неверный или отсутствующий API ключ)',
|
||
},
|
||
'403': {
|
||
description: 'Доступ запрещен (нет прав на этот эндпоинт)',
|
||
},
|
||
'500': {
|
||
description: 'Внутренняя ошибка сервера',
|
||
},
|
||
},
|
||
};
|
||
|
||
// Добавляем requestBody если есть body параметры
|
||
if (Object.keys(bodyParams).length > 0) {
|
||
endpointSpec.requestBody = {
|
||
required: bodyRequired.length > 0,
|
||
content: {
|
||
'application/json': {
|
||
schema: {
|
||
type: 'object',
|
||
properties: bodyParams,
|
||
required: bodyRequired,
|
||
},
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
paths[swaggerPath][method] = endpointSpec;
|
||
});
|
||
|
||
return {
|
||
openapi: '3.0.0',
|
||
info: {
|
||
title: 'KIS API Builder',
|
||
version: '1.0.0',
|
||
description: `
|
||
# Документация
|
||
|
||
## Авторизация
|
||
|
||
Для доступа к эндпоинтам используйте API ключ, полученный от администратора системы.
|
||
|
||
1. Нажмите кнопку **Authorize** справа вверху
|
||
2. Введите ваш API ключ в поле **x-api-key**
|
||
3. Нажмите **Authorize**
|
||
|
||
Теперь вы можете тестировать доступные вам эндпоинты прямо в этой документации.
|
||
|
||
## Примечание
|
||
|
||
Публичные эндпоинты доступны без API ключа.
|
||
`.trim(),
|
||
},
|
||
servers: [
|
||
{
|
||
url: '/api/v1',
|
||
description: 'API эндпоинты',
|
||
},
|
||
],
|
||
components: {
|
||
securitySchemes: {
|
||
apiKey: {
|
||
type: 'apiKey',
|
||
in: 'header',
|
||
name: 'x-api-key',
|
||
description: 'API ключ для доступа к эндпоинтам',
|
||
},
|
||
},
|
||
},
|
||
paths,
|
||
tags,
|
||
};
|
||
}
|