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