Возможно кому-нибудь понадобится этот хак, ибо я не смог найти подходящее моей проблеме решение в официальной документации Swagger.
Суть проблемы
Минимальное описание Swagger, а именно поле example, команде тестирование требовалось видеть все поля запроса и его типы.
Описание полей было делать слишком сложно, некоторые запросы были собраны из смапленных данных разных entity плюс это сильно нагружала кодовую базу, Entity стало тяжело читать из-за нагруженности декораторов описания полей, валидации, а теперь и описание типов свагера.
Для меня был странным сам тот факт что я не могу прокинуть в свагер уже описанную на ts энтити или даже элементарно класс или интерфейс с типами и полями.
Наше приложение проходило множество тестировании и решение было принято взять за основу те данные, которые мы получаем с фронта и принять их за истину того какие данные мы должны получить. И я написал такой метод сам, чтобы закрыть потребность бизнеса на текущие поля.
Никого не призываю делать ровно так же, но это может помочь тем кто точно уверен в том, что это сможет решить его проблему так же как нашу.
Реализация и сбор данных
if(process.env.DEV) {
app.use(GatherRequests);
}
Для начала подключим нашу кастомную мидлвару GatherRequests
. Ее задача собирать из Request данные, изменять их под тот формат, который нам нужен, после чего записывать эти данные в файл apiData.json
.
export const GatherRequests = (req,res,next) => {
// сгружаем имеющиеся данные
const apiData: any = loadApiData(apiDataPath);
// преобразуем в объект
const transformedObject = transformObject(JSON.parse(JSON.stringify(req.body)));
// это метод который заменяет id url с фронта на *
// например http://localhost:3000/page/18cfb1b0-7e01-423c-a44f-d84a30c39bd1/search
// на http://localhost:3000/page/*/search
const newUrl = replaceUUIDWithId(req.url);
// Приводим данные с request в нужный нам формат
const reqData = {
url: newUrl,
method: req.method.toLowerCase(),
body: transformedObject
}
// проверяем есть ли идентичная дата в файле apiData.json, если нет то обновляем
// В файле apiData.json хранится объект Map, ключем которого служит url
const isNeededToUpdate = () => {
const challengerData:any = reqData
const apiDataItem:any = apiData.get(reqData.url)
if(!(challengerData?.body)) {
return false
}
if(!apiData.has(reqData.url)) {
return true
}
const challengerDataKeys = Object.keys(challengerData.body)
const apiDataItemKeys:any = Object.keys(apiDataItem.body)
return apiDataItemKeys.length < challengerDataKeys.length;
}
// Пропускаем если обновление не требуется
if (!isNeededToUpdate()) {
return next()
}
// Обновляем если данные неактуальные
apiData.set(reqData.url, reqData);
saveApiData(apiData, apiDataPath)
next();
}
Работа с самим файлом json происходит посредствам двух функции: saveApiData и loadApiData
const saveApiData = (data, filePath) => {
fs.writeFileSync(filePath, JSON.stringify([...data]), 'utf-8');
}
export const loadApiData = (filePath: string) => {
if (fs.existsSync(filePath)) {
const fileData = fs.readFileSync(filePath, 'utf-8');
return new Map(JSON.parse(fileData));
}
return new Map();
}
Давайте еще подробнее взглянем на функцию transformObject
const transformObject = (input) => {
const transformed = {};
for (const key in input) {
if (input.hasOwnProperty(key)) {
const value = input[key];
const uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g;
if (typeof value === 'string') {
if(value.match(uuidRegex)) {
transformed[key] = 'id';
} else {
transformed[key] = 'string';
}
} else if (typeof value === 'number') {
transformed[key] = 1000;
} else if (typeof value === 'symbol') {
transformed[key] = 'symbol';
} else if (Array.isArray(value)) {
transformed[key] = [];
} else if (typeof value === 'object' && value !== null) {
transformed[key] = {};
} else {
transformed[key] = value;
}
}
}
return transformed;
}
В этом кейсе нет ничего необычного, я не хочу светить в Swagger example реальными данными, вполне хватит понимать тип поля, поэтому я вычисляю его и подменяю на строку, если я встречаю uuid, я меняю его на строку id, чтобы тестированию было понятно что поле явно относится к id.
Пример:
{
"name": "string",
"projectId": "id",
"customInformation": {}
}
Как видно из первого блока кода реализации метода GatherRequests, он работает только в Dev режиме.
Теперь мы храним все данные о реквестах всего api в файле apiData.json
Давайте теперь вернемся в то место где мы подключаем непосредственно Swagger
Работа со Swagger document
const docOptions = new DocumentBuilder()
.setTitle('Habr')
.setDescription('The Habr API description')
.setVersion('1.0')
.addTag('Habr')
.build();
const document = SwaggerModule.createDocument(app, docOptions);
Это базовое подключение Swagger в NestJs. Но прежде чем сделать настройку модуля и указать его url мы модерируем объект document напрямую, внеся туда кое-какие свои изменения
const apiDataPath = path.join(Paths.src, 'apiData.json');
const apiData = await loadApiData(apiDataPath);
// Проходим по документу Swagger и модифицируем его поле example
Object.entries(document.paths).forEach(([url]) => {
// В GatherRequests мы подменяли id на * в url, а здесь мы приводим все в формат Swagger
// /v2/api/product/* --> /v2/api/product/{id}
const apiDataUrl = replaceBracesWithAsterisk(url)
// Если находим соответствующи урлы то модифицируем объект document
if(apiData.has(apiDataUrl)) {
const apiDataObj:any = apiData.get(apiDataUrl)
document.paths[url][apiDataObj.method].responses[200] = {
content: {
'application/json': {
example: apiDataObj.body
}
}
}
}
});
// _____________Настраиваем Swagger__________________
SwaggerModule.setup(`/api/docs`, app, document);
Заключение
Это решение не является волшебной палочкой, а является только узконаправленной задачей. Если у вас есть возможность описать сущности стандартным путем в Swagger, воспользуйтесь им.
Надеюсь вам понравилась моя статья, если она была для вас интересной или хоть как-то вам помогла, поставьте ей лайк, мне приятно видеть, что я делюсь своим опытом не зря.
Мой linkedIn