Files
api_builder/backend/src/config/dynamicSwagger.ts
eshmeshek 727c6765f8 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
2026-03-13 15:22:32 +03:00

241 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
};
}