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

37e4c15e673faaec99cb93688ee1e1ab.png

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

© Habrahabr.ru