docfy patch-spec

Правит уже собранный документ OpenAPI исключительно статическим анализом (ts-morph), без единого require() docs-файлов.

Зачем это нужно

Это ручной способ из CI обойти то единственное, чего конвейер DocfyModule в рантайме сделать не может: сработать под режимом сборки webpack: true из NestJS CLI (автоматическая и рекомендуемая альтернатива описана в разделе Плагин CLI, она выполняет тот же анализ на каждой сборке, без отдельного шага). patch-spec обходит стену webpack тем, что сопоставляет по пути и HTTP-методу, вычисляя их так же, как это уже делают check, coverage и lint, вместо того чтобы требовать живую ссылку на класс контроллера.

Использование

bash
npx nestjs-docfy patch-spec --spec <path-or-url> [options]
bash
# Patch a running app's served document
npx nestjs-docfy patch-spec --spec http://localhost:3000/api-json --out openapi.json

# Patch a file already written to disk
npx nestjs-docfy patch-spec --spec dist/openapi.json --out dist/openapi.json

Параметры

OptionDefaultDescription
--spec <path|url>(required)A local openapi.json, or a URL (e.g. a running app's /api-json)
--out <path>stdoutWhere to write the patched document
--root <path>.Project root directory
--tsconfig <path>auto-detectedPath to tsconfig.json
--pattern <glob>**/*.controller.tsGlob pattern to find controllers
--format <format>tsФормат docs-файлов, которые нужно искать: ts или js
--quietfalseSuppress all output except errors

Что попадает в документ

Сопоставление с базовым документом идёт по path и HTTP-методу:

  • ApiTags → объединяются с tags (теги, которые уже были в базовом документе, не теряются)
  • ApiOperation({ summary, description, deprecated }) → перезаписывает эти поля
  • ApiResponse({ status, description, schema, example, examples }) → сливается по коду ответа (остальные коды не трогаются). Когда schema или type не заданы, берётся разрешённый тип возврата самого метода, то есть тот же вывод из DTO, class-validator и интерфейсов, который уже делает generate, включая oneOf из $ref, если тип возврата — это объединение двух и более именованных DTO или сущностей (например, Promise<UserDto | AdminDto>)
  • ApiBody({ schema, examples }) → задаёт requestBody, с таким же откатом к разрешённому типу параметра @Body() и с той же обработкой объединения через oneOf (example здесь намеренно не читается: у настоящего типа ApiBodyOptions из @nestjs/swagger есть только examples, в отличие от ApiResponse, где доступны оба)
  • ApiBearerAuth() → дописывается в security
  • ApiParam / ApiQuery / ApiHeader → сливаются в parameters по имени и месту: по-настоящему новый параметр дописывается, а у уже существующего поля накладываются поверх, а не выбрасываются (например, @nestjs/swagger по одной лишь рефлексии сам заводит голую запись с required: true для любого аргумента с @Query() или @Param()). enum читается из литерала массива (enum: ['a', 'b']) либо из ссылки на enum в TS, в том числе импортированный из другого файла (enum: Role), а type в схеме выводится как number, когда все разрешённые значения числовые, и как string в остальных случаях

What this does not do

Область намеренно ограничена

Команда намеренно покрывает не всё и не повторяет семантику декораторов @nestjs/swagger целиком. anyOf не трогается, потому что ни одна конструкция TS не ложится на «любой из» так же естественно, как тип-объединение ложится на oneOf. links и callbacks тоже, ведь у самого @nestjs/swagger нет опции декоратора ни для того, ни для другого. Любой аргумент декоратора, который не является литералом (переменная, вызов функции, спред), остаётся как есть, вместо того чтобы гадать. Лучше оставить поле таким, каким оно было в базовом документе, чем вписать туда неверное значение. Тип возврата или содержимое @Body() превращаются в oneOf только когда каждая ветка разрешается в именованный DTO или сущность. Маршруты, которые docs-файл описывает, но которых нет в --spec, попадают в предупреждения, а не отбрасываются молча и не роняют команду.