Автоматическая сборка examples для Swagger NestJs

от автора

Возможно кому-нибудь понадобится этот хак, ибо я не смог найти подходящее моей проблеме решение в официальной документации 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


ссылка на оригинал статьи https://habr.com/ru/articles/843492/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *