Перейти к основному содержимому

ADR 0033 — Справочник API выводится из кода, а табличный тест требует метаданных

Контекст​

api предоставляет 118 маршрутов в 23 controllers, и до сих пор единственной документацией контракта был docs/security-surface.md. Он отвечает, кто может вызывать каждый маршрут, — и ничего о том, что маршрут делает, что принимает и что возвращает. Тот, кто интегрировался, читал controller.

7.1 открыла исключение: два controller auth родились с @ApiTags/@ApiOperation/@ApiProperty, а заголовок auth/dto/auth.dto.ts зафиксировал намерение — «os decorators entram AGORA, junto com as rotas, e não numa varredura retroativa». Эта поставка и есть ретроактивная проверка, которая, как обещал тот комментарий, больше не понадобится, и механизм, делающий её последней.

@nestjs/swagger был зависимостью ещё с 7.1, но SwaggerModule нигде не был подключён: документа не существовало.

Три находки исследования перепроектировали поставку ещё до того, как была написана хоть одна строка:

  1. @nestjs/swagger СИНТЕЗИРУЕТ ответ, когда нет ни одного decorator. api-response.explorer.js возвращает { '<status>': { description: '' } } для каждого handler без @ApiResponse. До этой фазы 111 из 118 маршрутов находились ровно в этом состоянии. Очевидное утверждение — «у каждого маршрута есть ответ 2xx» — родилось бы зелёным, ничего не проверяя.
  2. @HttpCode игнорируется, как только существует любой @ApiResponse. Документированный статус начинает браться только из decorator. Дефект уже существовал в репозитории: у POST /auth/register и POST /auth/request-password-reset был @HttpCode(202) с @ApiOkResponse, и документ утверждал 200.
  3. Вне auth ни один handler не объявляет тип возврата. Шаблон — return this.useCase.execute(...), который разрешается в интерфейсы и type aliases из src/domain/**, — из них @nestjs/swagger не выводит схему и для них выдаёт {} без предупреждения.

Решение​

Справочник генерируется из OpenAPI и никогда не пишется вручную, а табличный тест Фазы 5 начинает требовать метаданных, которые его питают.

DTO ответов отражают сущность по ТИПУ​

Существует ~55 различных форм ответа (не 118: ProposedAction обслуживает 6 маршрутов, Session — 5). Каждая стала классом в interfaces/http/<domínio>/dto/*.response.dto.ts. Ничто в src/domain/** не было затронуто — ставить @ApiProperty на сущность нарушает чистоту домена.

Написать DTO — лёгкая часть. Риск — день, когда сущность получит поле, а DTO нет: справочник начинает лгать молча. Против этого — две типовые защёлки, и обе необходимы:

export class SessionResponseDto implements Wire<Session> { … }
export const _chavesSession: MesmasChaves<SessionResponseDto, Session> = true;

Прямой implements Session не годится: сущность говорит createdAt: Date, а тело JSON говорит string. Wire<T> — это сущность такой, какой она уходит в провод, и тогда implements честен. Но implements однонаправлен — он слеп к лишнему полю, и DTO, описывающий уже удалённое поле, компилировался бы вечно. MesmasChaves закрывает эту сторону.

Четыре режима отказа были проверены запуском до того, как поверх этого был написан хоть один DTO:

ошибкаловится
сущность получила полеimplements — TS2420
DTO типизировал Date, где в проводе stringimplements — TS2416
у DTO есть поле, которого у сущности больше нетMesmasChaves — TS2322, и только им
корректный DTOкомпилируется чисто

Защёлки выполняет tsc, а не vitest, — тот транспилирует через SWC и не проверяет никаких типов. Отсюда новый pnpm --filter api typecheck в CI: без него доказательство выполнялось бы только в job образов, на двадцать минут позже.

Табличный тест требует ДОКУМЕНТ, а не decorators​

route-surface.spec.ts получил семь утверждений, все о документе, собранном SwaggerModule.createDocument, — том же, что идёт на сайт. Отражать DECORATORS.API_RESPONSE handler за handler значило бы тестировать промежуточный шаг: type:, указывающий на интерфейс, прошёл бы проверку decorator и дал бы {} в документе.

Утверждение об ответе требует разрешённого содержимого или непустого описания, а не просто наличия ключа 2xx — к этому обязывает находка 1 выше. Утверждение о статусе пересчитывает реальное значение из HTTP_CODE_METADATA, что закрывает находку 2. А последнее привязывает документ к настоящим guards: маршрут @Public() не может объявлять security, аутентифицированный маршрут обязан объявлять. Без него справочник мог бы утверждать, что маршрут открыт, когда guard его закрывает, — ошибаться именно там, где ошибка дорога.

Исключение маршрута из справочника требует записи в EXCLUIDAS_DA_REFERENCIA, а маршрут без JSON-тела требует записи в SEM_CORPO_JSON с собственной обязанностью (SSE объявляет text/event-stream, redirect объявляет заголовок Location, 204 объявляет 204). Без этого @ApiExcludeEndpoint() был бы лёгким выходом, чтобы уйти от всего, а «это stream» стало бы лицензией ничего не документировать.

--check использует манифест, а не перегенерацию​

pnpm docs:check обещает не писать. Запуск gen-api-docs для сравнения нарушил бы обещание. Защёлка — манифест с sha256 каждого сгенерированного файла плюс openapi.json, который их произвёл, записываемый тем же escrever(), что и все остальные генерируемые, — а значит, с тем же поведением в check.

Он ловит четыре значимых дрейфа: MDX, отредактированный вручную, старый MDX для новой spec, отсутствующий сгенерированный файл и осиротевший файл. Три были проверены запуском.

Порядок openapi.json фиксирован (paths, глаголы, schemas, tags, статусы), потому что порядок, который отдаёт Nest, следует из порядка регистрации модулей: без нормализации перемещение строки import в AppModule дало бы тысячи строк diff, и следующим предсказуемым шагом кто-нибудь отключил бы check.

Swagger UI вне production​

/docs и /docs-json существуют только при NODE_ENV !== 'production'. Справочник production — это сайт документации, сгенерированный из того же документа; отдавать всю поверхность в реальном окружении ничего не добавляет и бесплатно даёт карту тому, кто станет зондировать.

Последствия​

В справочнике 118 страниц, по одной на маршрут, сгруппированных по тегам. Обзор — аутентификация, соглашение об ошибках, rate limit — берётся из info.description, поэтому он генерируется из единого источника, а не пишется в .md, который бы разошёлся.

Новый маршрут без метаданных не проходит. Это механизм против дрейфа, которого у docmap нет: docmap срабатывает при изменении файла, но не видит нового маршрута, который родился без документации.

Что проверка исправила по пути​

Это была не только документация:

  • PUT /projects/:id/agent-autonomy и DELETE /projects/:id/members/:userId возвращали 200 с пустым телом. api-client.ts в web считает «без тела» только 204 и попадал в res.json(), бросая SyntaxError. Оба стали @HttpCode(204).
  • UpdateWorkspaceDto и UpdateProjectDto использовали PartialType из @nestjs/mapped-types, который копирует только валидацию: оба вышли бы без единого свойства в документе.
  • Классовый @ApiBearerAuth в GitController протекал в callback OAuth, который @Public(), — справочник утверждал, что браузеру нужен токен, чтобы вернуться от provider. Ни один decorator @nestjs/swagger не очищает унаследованное требование, поэтому объявление стало по маршруту.
  • Два @HttpCode(202) в auth, задокументированные как 200.

Принятые издержки​

  • 2,7 МБ версионируемого сгенерированного (117 MDX плюс 352 JSON, которые они require()). Без этого у docmap было бы мёртвое правило, а git ls-files ничего бы не видел.
  • Без сниппетов по языкам. postman-code-generators запускает вложенный npm install в postinstall — сеть посреди нашей установки, а это то, от чего отказывается дисциплина Фазы 5. Он вошёл в allowBuilds как false.
  • outputDir удаляется целиком командой clean-api-docs, поэтому spec живёт уровнем выше. Генератор, удаляющий собственный вход, работает при первом запуске и падает при втором.
  • Id сгенерированного sidebar оказываются на уровень глубже, потому что плагин предполагает, что outputDir находится внутри сайта. Исправление живёт в написанном вручную sidebars.ts, а не в переписывании сгенерированного файла — его изменение заставило бы docs:check обвинять в дрейфе на каждом раунде.

Что остаётся открытым​

engine_api_client.ex по-прежнему без автоматической проверки соответствия маршрутам api. Справочник даёт обеим сторонам один источник для сверки, и TODO(humano) в internal-api.md остаётся в силе: по-настоящему это закрыло бы генерирование клиента Elixir из openapi.json или контрактный тест между сторонами.

Также вне охвата: сгенерированный TS-клиент для web (api-client.ts остаётся ручным), версионирование api и удаление GET / из scaffold — который получил @ApiExcludeEndpoint() с обоснованием, вместо того чтобы исчезнуть, потому что его удаление — продуктовое решение.

Ссылается на ADR 0027, создавший табличный тест, и ADR 0031, где появились первые decorators.