Перейти к содержанию

DTO и проверка тела PATCH

Контроллер описывает место получения body, но JSON приходит из внешней среды. Нельзя доверять ему только потому, что TypeScript-параметр назван PatchTicketDto. Добавим runtime-договор: DTO будет классом с validation decorators, а global ValidationPipe проверит построенное значение до service. Результат — определённый набор title/status/version с нормализацией заголовка и отказом неизвестным полям, который работает независимо от наличия Angular-формы.

Сервер проверяет свою границу даже тогда, когда клиент уже выполнил required и minLength. Браузерная проверка помогает человеку исправить ввод, но внешний клиент способен отправить любой JSON. Например, title может оказаться массивом, status — «В работе», version — строкой «1», а рядом добавлено поле actorId. Внутренний TS-тип сам по себе не проверяет ни один из этих случаев. Декораторы DTO и pipe выполняют отдельную runtime-операцию над реальным входом.

Класс входных данных

Ниже полный PatchTicketDto из составного src/dto.ts. Импорты class-validator, Transform и ApiProperty находятся в файле архива, statuses и Status — в model.ts. Функция trim возвращает обрезанную строку только для строкового value, оставляя остальные типы валидаторам. Она не пытается превратить любой объект в текст. Это важно: [object Object] не становится допустимым title только потому, что универсальный String сделал из ошибочного JSON несколько символов.

const trim = ({value}: {value: unknown}) => typeof value === 'string' ? value.trim() : value;
export class PatchTicketDto {
  @ApiProperty({minLength:3,maxLength:120})
  @IsString() @MinLength(3) @MaxLength(120) @Transform(trim) title!: string;
  @ApiProperty({enum:statuses}) @IsIn(statuses) status!: Status;
  @ApiProperty({minimum:1,maximum:2147483646})
  @IsInt() @Min(1) @Max(2147483646) version!: number;
}

IsString проверяет тип title, MinLength/MaxLength — его размер после transform. IsIn ограничивает status тремя машинными значениями. IsInt и Min/Max определяют version как числовую основу, совместимую с integer PostgreSQL и следующим увеличением. Верхняя граница 2147483646 оставляет место для плюс одного. Мы не преобразуем строковый version в число через Type: контракт PATCH требует JSON number, а не произвольный текст, который можно попытаться угадать.

Ожидаемый допустимый body содержит ровно {title:"Заменить проектор",status:"active",version:1}. Сервер может удалить пробелы по краям title, но сохраняет внутренние пробелы. Непустое сообщение после transform всё ещё должно иметь минимум три символа. Этот порядок удерживает договор с формой Angular, которая также trim перед submit. При сложных Unicode-строках точный смысл длины требует дополнительного согласования; лаборатория использует обычные вымышленные русские названия и не обещает графемную проверку.

Опции глобального pipe

Main устанавливает transform true, whitelist true и forbidNonWhitelisted true. Transform создаёт DTO-экземпляр и применяет объявленные преобразования. Whitelist определяет поля с validation metadata как разрешённые. ForbidNonWhitelisted отклоняет лишние поля, вместо молчаливого удаления. Такое поведение помогает обнаружить несовпадение клиента с API: body с id, actorId или updatedAt не будет принят как успешное изменение, хотя эти поля могли случайно выглядеть похожими на TicketResponse.

Request DTO и response модель имеют разные назначения. Клиенту возвращается id и updatedAt, но он не назначает их при PATCH. Version отправляется как ожидаемая основа, а следующий version определяет сервер. Если использовать один класс для входа и выхода без анализа, можно разрешить внешнему пользователю устанавливать служебные поля. В dto.ts они разделены: PatchTicketDto валидирует намерение изменения, TicketResponse описывает выдаваемую запись для OpenAPI.

ForbidUnknownValues помогает отвергать значения, к которым не применяется ожидаемая модель validation. ValidationError target/value выключены, чтобы ошибка не включала целый входной объект и его исходные значения. Это не универсальный фильтр секретов для любого logger: код tracing также не пишет body или Authorization. Несколько небольших ограничений складываются в понятную локальную политику. Добавление подробного debug logging требует отдельного решения о том, какие данные разрешено сохранять и показывать.

Ошибка раньше repository

При неверном status ValidationPipe ожидаемо выбрасывает BadRequestException до вызова service. Общий ApiErrors превращает массив сообщений в issues и пользовательское message «Некорректные поля запроса». StatusCode400 и code BAD_REQUEST относятся к форме запроса, а не к текущей версии заявки. Если version корректное число, но уже устарело, DTO его принимает, и repository позже сообщает 409. Так validation отделяется от проверки изменяемого состояния базы.

Не включайте skipMissingProperties для этого DTO только потому, что HTTP-метод называется PATCH. Наш договор требует все три поля: это изменение ограниченной проекции заявки с обязательной version. Пропущенный title не означает «оставить старый», а является нарушением входа. Частичная модель потребовала бы optional decorators и явной логики составления следующего состояния после чтения базы. Название метода HTTP не заменяет проектирование структуры body и её семантики.

Рассмотрите null, пустой объект и массив. Null не создаёт полноценного намерения, пустой объект не удовлетворяет обязательным правилам, массив не представляет нужные поля. Повторная отправка полного Ticket из GET также содержит неизвестные id/updatedAt и должна быть исправлена клиентом до TicketPatch. Такой отказ полезен для обучения: чтение и изменение разных представлений имеют общий предметный объект, но не обязаны иметь одинаковую wire-форму. Иначе доверие к служебным полям легко расширится случайно.

Для самостоятельного сценария читатель может отправить четыре тела: правильное, со строковой version, с лишним actorId и с title из пробелов. Ожидается один проход к service и три 400. Эти запросы в сессии не выполнялись. В source-файле есть точные decorators и pipe options, поэтому предполагаемые результаты можно проверить после подготовки базы и npm-окружения, не добавляя клиентскую форму и не меняя controller по каждому отдельному случаю.

Следующая глава применит DTO к query списка. Там входы являются URL-строками, а необязательные q/status имеют другой смысл, чем обязательная version JSON-body. Удержим это различие: хорошая validation не сводится к одному набору преобразований для любого источника. Она описывает конкретное действие, разрешённые формы и состояние, которое service вправе получить после транспортной границы.

Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.

Перейдите к оглавлению серии или общему маршруту интерактивной веб-разработки. Механизм этой главы сверяется с официальным первичным источником; конкретные версии и непроверенные части перечислены в договоре курса.