Query DTO и буквальный поиск заявок
Список принимает фильтр из query string. Кажется, что это менее опасный вход, чем body изменения, но он также требует определённого договора. Q — необязательная строка максимум 80 символов, status — необязательное машинное значение. Рассмотрим ListTicketsDto и его связь с SQL. Результат — список с однозначной семантикой буквального поиска, явным ограничением объёма и отказом неожиданным полям вместо произвольной интерпретации адреса.
Query и JSON-body имеют разные естественные формы. В URL значения приходят строками, и q действительно должна оставаться строкой. Для status допустим один enum, а repeated status не создаёт многозначный фильтр. Контракт лаборатории не включает page, order и actorId. Если внешний клиент добавит их к API-запросу, strict whitelist ожидаемо вернёт 400. Это помогает заметить несовпадающую версию клиента, вместо молча игнорируемого параметра, на который пользователь рассчитывал.
Необязательные поля
Ниже полный ListTicketsDto из файла dto.ts. IsOptional разрешает отсутствие q или status, остальные decorators проверяют присутствующее значение. Transform trim нормализует строку поиска, не преобразуя массив или объект в допустимое слово. Status не получает default «all»: отсутствие означает любое состояние, а неизвестный литерал является ошибкой. Angular отправляет отсутствие фильтра именно отсутствующим параметром, поэтому стороны не вводят дополнительный четвёртый предметный статус.
export class ListTicketsDto {
@ApiPropertyOptional({maxLength:80})
@IsOptional() @IsString() @MaxLength(80) @Transform(trim) q?: string;
@ApiPropertyOptional({enum:statuses})
@IsOptional() @IsIn(statuses) status?: Status;
}
Ожидаемый GET без query возвращает первые 200 заявок в порядке id и число всех совпадений. GET с q «проектор» возвращает rq-101, с status active — rq-102, а оба условия совместно применяются как AND. Значение q из пробелов становится пустым и не ограничивает поиск. Повторяющийся q, разобранный HTTP-платформой как массив, не проходит IsString. Таким образом внешний клиент не может незаметно изменить однозначную модель одного текста на набор разных условий.
Слово «первые» имеет смысл только при определённом order. Repository сортирует по id, а не по случайному физическому размещению строк. Для трёх исходных id этот порядок очевиден, но текстовое сравнение rq-1000 и rq-99 не является числовой сортировкой суффикса. В контракте выбран именно порядок id. Если продукт потребует свежие заявки по времени, необходимо явно изменить order и согласовать стабильное разрешение одинаковых timestamps. Текущий курс не скрывает такое решение за неопределённым «обычно как в базе».
Буквальный фрагмент вместо шаблона
SQL использует strpos(lower(title),lower($2)) больше нуля. Поэтому % и _ в q остаются обычными символами, а не wildcard-операторами LIKE. Это совпадает с includes в клиентском fixture на обычных исходных строках. Параметры защищают место данных в запросе, но сами по себе не определяют семантику поиска. Если выбрать LIKE без escaping, безопасный от injection запрос всё равно может иметь неожиданное поведение при поиске процента. Здесь это две разные задачи.
Lower работает по текстовым правилам PostgreSQL и выбранной locale/collation. Fixture использует toLocaleLowerCase браузера. Для произвольных Unicode-случаев гарантии совпадения нет, и она прямо записана в контракте. Такое ограничение нельзя исправить одной фразой «поиск без регистра». Нужно определить языки, нормализацию и сравнительный алгоритм, а затем проверить обе стороны на согласованном наборе. Вымышленные projector/Wi-Fi/переговорная позволяют изучить механизм, но не доказывают универсальную международную эквивалентность.
Размер q ограничен на транспортной границе, потому что вход должен быть предсказуемым. Это не измеренный предел производительности SELECT. При росте данных понадобится анализ плана и выбранного индекса или поиск по другому источнику. Никакие EXPLAIN или нагрузки в этой сессии не выполнялись. Мы не обещаем, что lower/strpos найдёт миллионы записей быстро. Учебный запрос выбран ради ясной буквальной семантики и подготовленного небольшого набора, а не как универсальная production-стратегия.
Envelope списка
Response содержит items и total. Items ограничен 200, total получается оконным count по всей подходящей выборке до LIMIT. Поэтому число показанных строк может быть меньше числа найденных. Angular отдельно выводит обе величины. При нуле подходящих строк оконного значения нет, и repository возвращает total0. Эта модель честно описывает частичный список, но не предоставляет следующую страницу. Cursor или offset не реализованы, что важно назвать при передаче проекта.
Не считайте длину items автоматически равной total. На трёх seed-заявках такое равенство выполняется, но оно обусловлено размером данных. Клиентский readList допускает total больше items.length и отвергает total меньше. Таким способом контракт остаётся согласованным с ограничением выдачи. Если дописать локальный search поверх этих первых 200 строк, он не будет искать по всей базе. Применённые q/status должны оставаться серверными параметрами, а computed клиента считает лишь текущее представление.
Поведение Angular URL-нормализации отличается от строгого API-входа: неизвестный status в адресе страницы превращается в «Все», тогда как прямой API status=archived даёт 400. Это намеренно разные границы. Страница строит следующий canonical запрос из своей модели, а сервер не обязан угадывать желание внешнего клиента. При строгом UX можно добавить сообщение об ошибке самого адреса страницы, но изменение этой политики не требует ослаблять Nest DTO или расширять список допустимых статусов.
Для будущей проверки сравните q «проектор», q «%» и status new. Первый ожидаемо находит rq-101, второй — ничего в seed, третий — rq-101. Добавьте неизвестный page и ожидайте 400. Затем повторите q дважды и исследуйте 400 для неоднозначного значения. Эти HTTP-сценарии не выполнялись; их смысл выведен из написанного DTO и SQL. Следующая глава установит actor и проверит доступ к чтению и записи, сохранив весь фильтр без скрытой зависимости от браузерных кнопок.
Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.
Перейдите к оглавлению серии или общему маршруту интерактивной веб-разработки. Механизм этой главы сверяется с официальным первичным источником; конкретные версии и непроверенные части перечислены в договоре курса.