Главная / Статьи / Отладка сбоев Prisma Guard: модель диагностики на основе этапов

Отладка сбоев Prisma Guard: модель диагностики на основе этапов

Узнайте, как диагностировать сбои API Prisma, возникающие в результате генерации кода, путем соотнесения ошибок с конкретным этапом — настройками, выбором вызывающего компонента, проверкой данных или формированием ответа — в котором они произошли.

2660 слов

Начните устранение неисправностей с определения того, на каком этапе именно происходит сбой.

Генерируемые API могут выходить из строя в нескольких разных местах.

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

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

Помимо этого существует более сложная категория: запрос технически успешен, однако аргументы Prisma, которые генерируются, или семантика ответа отклоняются от того, что предполагала логика приложения.

Каждая из этих категорий требует своего решения и своего вида тестирования. Поиск информации в полной строке ошибки гораздо менее эффективен, чем задание двух вопросов: когда впервые проявилось такое поведение и какой слой способен его зафиксировать?

Начните с карты этапов

Генерируемый запрос Prisma проходит через несколько определенных этапов на пути к выполнению:

router construction
  caller resolution
    operation before-hooks
      variant before-hooks
        guard shape construction
          request validation
            Prisma argument execution
              response transport

Точная последовательность операций, связанных с формами, может меняться в зависимости от того, являются ли формы статическими или зависят от контекста выполнения, но эта диагностическая схема по-прежнему остается полезной моделью для понимания процесса.

Сбои при запуске указывают на проблемы с описателями маршрутов. Сбои на этапе вызова указывают на логику выбора вариантов. Ошибки вроде Invalid query и Invalid data указывают на несоответствие между телом запроса и заявленной структурой. Сбои политик указывают на отсутствие надежного контекста. А когда запрос выполняется успешно, но результат неожиданный, необходимо совсем не обращать внимания на код статуса.

Имейте в виду, что текст ошибок связан с конкретными версиями. Приведенные здесь примеры, сосредоточенные на механизмах защиты, были созданы с использованием фиксированной комбинации: prisma-guard версии 1.33.0 вместе с Zod 4.4.3 и Prisma 6.19.3. Примеры, касающиеся операций чтения по протоколу HTTP, основаны на отдельной фиксированной комбинации: prisma-generator-express версии 1.64.4, работающей на Node 22.14.0 с базой данных PostgreSQL 16.6.

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

До отправки запроса: конфигурация не может служить договором

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

Операции не разрешается одновременно настраивать как shape, так и variants. Карта вариантов не может оставаться пустой. Каждый описатель варианта должен содержать информацию о форме. Зарезервированные ключи форм не могут использоваться в качестве имён вызывающих компонентов.

По своей природе это сбои, возникающие во время развертывания. Если система обнаружит их, но всё равно продолжит работу с частично настроенным маршрутизатором, она незаметно устранит те границы, которые приложение должно было соблюдать.

Операция, которая не определяет ни shape, ни variants, представляет собой совершенно иную ситуацию: она технически допустима и вызывает Prisma напрямую без какой-либо проверки. Решение о том, является ли это приемлемым, должно приниматься явно во время анализа маршрутов, а не становиться следствием случайности.

Создание структуры shape сопряжено со своими собственными условиями сбоя. Пустые комбинаторы, пустые проекции, противоречащие друг другу обязательные предикаты, неполные структуры create shapes, некорректные форматы upsert и методы больших объемов данных без параметра where отклоняются сразу, еще до того, как данные, предоставленные клиентом, смогут вступить с ними во взаимодействие.

Наиболее полезным является минимальный пример, при котором создание структуры shape разделено от слоя передачи данных:

const query = guard.query('Plant', 'findMany', {
  where: {
    name: { contains: true },
  },
  take: { max: 50, default: 20 },
})
const args = query.parse({
  where: {
    name: { contains: 'fern' },
  },
})

Этот изолированный путь хорошо подходит для тестирования фильтрации чтения, сортировки, параметров пагинации и большинства ошибок формирования структур. Однако он не позволяет выполнять операции через Prisma, применять проекцию данных на уровне делегата или показывать, как на самом деле ведут себя мутации.

Каким бы ни было решение проблемы, оно должно находиться в конфигурации серверной части. Никакие изменения в заголовке запроса не смогут исправить структурно некорректную форму данных.

До обработчика: не удалось выбрать вызывающий компонент

Когда используются именованные структуры и варианты, существует дополнительный этап маршрутизации, который выполняется до того, как запрос дойдет до генерированного обработчика.

Звонок считается пропавшим, когда в карте вариантов отсутствует запись default. Звонок считается неизвестным, когда ничто не соответствует ему: нет точного ключа, нет параметризованного шаблона и нет значения по умолчанию. Два перекрывающихся параметризованных шаблона не разрешаются в порядке их указания; система рассматривает такую ситуацию как неоднозначную и вместо этого выдает ошибку.

Данные для идентификации звонка передаются отдельным каналом, отличным от тела запроса Prisma. Попытки скрыть их внутри самих аргументов запроса приводят к отклонению.

Для контрактов, предназначенных для публичного использования, использование заголовка в качестве специального фильтра звонков может быть разумным дизайнерским решением. Однако для привилегированных вариантов выбор должен осуществляться на основе аутентифицированной логики внутри функции resolveVariant, а не на основе данных, поступающих от клиента. Присвоение заголовку пользовательского имени не делает его значение надежным.

Сбои маршрутизации происходят после выполнения предварительных хуков на уровне операции, но до запуска хуков, специфичных для конкретной версии. Именно такой порядок объясняет определённое поведение: логика аутентификации для всей операции всё равно выполняется, даже если ни одна из версий вызывающего элемента не соответствует условиям, тогда как хуки, специфичные для вызывающего элемента, в таком случае никогда не срабатывают.

Правильное решение — это не просто «добавить значение по умолчанию». Значение по умолчанию молча принимает отсутствующие, пустые и несоответствующие значения. Добавляйте его только в том случае, если такое поведение действительно приемлемо во всех трёх упомянутых сценариях.

Во время проверки: запрос превысил установленные границы

Ошибки чтения, выявленные в этой конфигурации, указывают на точный путь аргументов, который их вызвал.

Неизвестное поле внутри where означает, что это поле не является частью структуры фильтра. Неизвестное поле внутри select означает, что запрос пытается расширить объем проекции за пределы разрешенного. Отклоненное значение skip означает, что пропуск страниц в пагинации никогда не был включен для данной структуры. Ошибка в параметре take может означать либо то, что запрошенное значение превысило установленный максимум, либо то, что оно было передано в совершенно неверном скалярном типе.

Генерируемые помощники для запросов типа GET имеют важное значение здесь, поскольку аргументы в формате Prisma не всегда преобразуются одинаковым образом при ручном создании из строк запросов. Числовые значения фильтров и даты, как правило, преобразуются корректно в тех местах, где это поддерживается, но логические значения и параметры пагинации, передаваемые в виде строк, могут не преобразовываться должным образом. Более безопасный вариант — использовать генерируемый кодировщик для запросов типа GET или обратиться к нативному формату JSON через методы чтения, основанные на POST.

В отличие от этого, процесс написания кода для валидации следует структуре, специфичной для каждого метода Prisma. Методы создания принимают поле data. Методы обновления принимают как where, так и data. Методы установки/обновления принимают where, create и update. Вызов метода пакетного создания с проверками ожидает, что входные данные будут представлены в виде массива.

Операции с большими объемами данных могут провалиться на двух разных уровнях. Если в самой структуре отсутствует элемент where, это проблема, возникающая во время создания структуры. Если же в теле запроса во время выполнения технически присутствует элемент where, но он не соответствует никакому реальному условию с клиентской стороны, это уже проблема, возникающая в момент отправки запроса.

Ошибки политик также относятся к отдельной категории. Отсутствие корня диапазона или отсутствие контекста для структуры, зависящей от контекста во время выполнения, указывают на то, что какое-то элемент доверенного состояния просто отсутствует. Установка режима поведения при отсутствии диапазона в режим ошибки предотвращает тихое превращение отсутствующего контекста в нефильтрованный запрос верхнего уровня.

Привычка, которую стоит выработать, — это сохранение точного пути, где произошла ошибка. Фраза «получен код 400 от защитника» почти ничего полезного не говорит. Фраза «модуль обработки запроса попытался выполнить include.plants.take, превысив установленный лимит вложенности» указывает непосредственно на конкретный узел в контракте.

После того как защитник разрешает запрос: код 200 всё ещё скрывает реальный риск

Успешный HTTP-ответ сообщает лишь о том, что маршрут был выполнен до конца. Он ничего не говорит о том, было ли действительно учтено значение, которое вы отправили, выполнялась ли условие в том блоке кода, который вы считали верным, или использовалась ли в ответе проекция по умолчанию, которую вы ожидали.

Возьмем полностью принудительный предикат верхнего уровня: он переопределяет любые значения, отправленные клиентом, без каких-либо видимых признаков этого действия. Если объект фиксирует значение isPublished в true, клиент, отправляющий значение false, всё равно получает ответ об успехе, в то время как фактически выполняемый запрос сохраняет принудительное значение true.

Другие принудительные поля ведут себя наоборот — они отклоняют значения, предоставленные клиентом, вместо того чтобы тихо их переопределять. Поскольку принудительное задание значений может вести себя неконсистентно в зависимости от места его применения, ваши тесты должны проверять, кто на самом деле контролирует каждый аргумент, вместо того чтобы предполагать, что один экземпляр функции force() применим ко всем полям.

Применение принудительных условий становится ещё более сложным внутри оператора OR. Условие, наложенное принудительно в такой части запроса, выносится на верхний уровень и превращается в обязательное ограничение. Поэтому конструкция, которая кажется выражением «либо условие клиента, либо условие сервера», на самом деле может выполняться как комбинация условия клиента с принудительным предикатом с использованием логики AND. Если вам действительно нужна альтернатива, определяемая сервером, требуется отдельный запрос, созданный специально для этой цели, или же необходимо ввести такое ограничение на уровне политик базы данных.

Проекция ответа также сопровождается своими особенностями. Когда клиент не указывает проекцию при защищённом чтении, применяется стандартная проекция конструкции, но это замещение происходит в момент фактической обработки запроса, а не при выполнении операции guard.query().parse().

Мутации не подчиняются одним и тем же правилам. Если параметр enforceProjection не установлен, клиент, который не указывает проекцию для мутации, вообще не получает инъекции оператора select, в результате чего действует обычное поведение Prisma при отсутствии проекции.

Применение правил в вложенных областях — ещё один случай, когда легко ошибочно предположить более полный охват, чем на самом деле существует. Автоматический механизм обработки областей перехватывает только операции верхнего уровня, которые он явно поддерживает. Он не воздействует на связи, загружаемые через проекцию, и не фильтрует их рекурсивно. Кроме того, сам корень области никогда не фильтруется с помощью своего собственного маркера, а любой вручную введённый SQL полностью обходит слой проверки расширения.

Ни одно из этих явлений не проявляется, если проверять только код состояния.

Выберите правильный механизм чтения перед тем, как доверять формату ответа

Сгенерированный слой поставляется с тремя различными механизмами для передачи результатов чтения: ответами с пагинацией, транспортировкой через метод POST и событиями, отправляемыми сервером с использованием Express.

findManyPaginated возвращает фиксированную структуру данных:

type PaginatedResult<T> = {
  data: T[]
  total: number
  hasMore: boolean
}

Флаг hasMore является надежным именно для пагинации с использованием сдвига данных вперед в сочетании с положительным значением take. Если использовать пагинацию с курсором или отрицательное значение take, все равно можно получить булево значение, но оно уже не несет такой же гарантии. Значение take равное 0 возвращает нуль строк и ложный флаг продолжения, при этом общее количество записей остается прежним.

Общее количество рассчитывается совершенно по другой логике. Раздельный подсчёт учитывает заданное ограничение. Источник предварительно вычисленного количества используется только тогда, когда запрос нефильтрован, не защищён и не является раздельным. Любой динамический фильтр, условие различия или механизм защиты принуждают к использованию текущего подсчёта, рассчитываемого в момент запроса.

Такой запасной вариант сохраняет корректность, но меняет как стоимость операции, так и источник получения числа. Семантику общего количества следует рассматривать отдельно от семантики разделения строк.

Чтение по протоколу POST существует для обработки размера и кодирования данных, а не для расширения возможностей языка запросов:

POST /delivery/paginated
Content-Type: application/json
{"where":{"city":{"equals":"Bangkok"}},"take":20,"skip":0}

Отправляйте тело запроса в формате нативного JSON. Ожидается, что версии методов GET и POST для одного и того же маршрута будут соблюдать одинаковые требования к обработке запроса. Если хук изменяет тело запроса, эта эквивалентность может нарушиться, поскольку маршрут GET считывает данные из уже обработанных параметров запроса, а не из JSON-тела.

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

{"type":"progress","stage":"relations"}
{"type":"field","field":"summary","data":{"total":6}}
{"type":"result","data":{"summary":{"total":6},"deliveries":[]}}

Вручную организованные события SSE — это запросы на уровне приложения, которые вы пишете сами, и им также требуется явное обработание защитных механизмов, как и всему остальному. Функция автоматического включения действует только в отношении форм структур данных, описанных в документации и находящихся в пределах возможностей планировщика; всё, что выходит за эти рамки, обрабатывается согласно настроенному поведению при отказе. Кроме того, генерируемые послефиксы не являются надежным способом очистки потока SSE.

В совокупности «успешное» чтение может быть неверным по нескольким независимым причинам: ненадежный флаг продолжения, неправильное понимание источника счетчика, поведение хуков, специфичное для протокола передачи данных, или запрос, организованный вручную, но не защищенный соответствующим образом.

Направляйте каждый тест на тот слой, который вы действительно можете проверить

Ни один полный запрос от начала до конца не может одновременно проверить все слои.

Обращайтесь к парсеру при тестировании валидации данных или структуры принудительного слияния. Обращайтесь к защищенному делегату, когда речь идет о проекции во время выполнения или аргументах окончательной мутации. Обращайтесь к пути операций расширения, чтобы проверить, действительно ли произошла автоматическая инъекция области видимости.

Отдельный механизм захвата аргументов позволяет изучать аргументы окончательной мутации без обращения к базе данных, но только в том случае, если вы подключите расширение-защитник к делегату, который действительно возвращает полученные аргументы. Создание отдельного фиктивного объекта ничего не доказывает. Такой механизм показывает, какие аргументы были отправлены, но не то, какие строки на самом деле вернет база данных.

Для вопросов, связанных с результатами на уровне арендатора, принадлежностью объектов отношений, поведением транзакций, разными итоговыми значениями или особенностями конкретных поставщиков, необходимы фикстчеры, основанные на базе данных. Заполните хотя бы двух арендаторов строками, которые значительно отличаются друг от друга, чтобы любое утечки данных были очевидны.

Для вопросов, касающихся генерации маршрутов, сериализации, выполнения хуков, эквивалентности методов GET/POST, формата ответа при пагинации или последовательности событий SSE, используйте тесты на уровне HTTP.

Сохраняйте хотя бы один тест контракта с включенной защитой, даже если ваш набор тестов «клиент-сервер» работает в режиме, отключающем проверку защиты. Тест в браузере, проходящий в режиме с ограниченными проверками, ничего не говорит о том, что будет отклонено в производственной среде, поскольку слой обеспечения соблюдения правил был удален для этого теста.

Пишите каждый тест регрессии на самом низком уровне, способном подтвердить конкретное утверждение, которое он формулирует. Чем более узкие тесты, тем проще выявить момент сбоя на соответствующей стадии, вместо того чтобы заново исследовать всю цепочку обработки запроса с нуля.

Работайте над устранением сбоя в одном направлении

Короткая, повторяемая последовательность действий помогает избежать догадок относительно способов исправления:

  1. Определите, является ли это сбоем при запуске, сбоем в момент обработки запроса или успешным ответом, который вас удивил.
  2. Выявите стадию, ответственную за сбой: маршрутизатор, обработка вызова, формат данных, правила работы системы, выполнение команд Prisma или транспортный слой.
  3. Сократите процедуру воспроизведения сбоя до одной операции, одного формата данных и одного тела запроса.
  4. Проверьте аргументы на том уровне, который находится ближе всего к источнику возникновения проблемы.
  • Добавляйте тестирование базы данных или выполнения через HTTP только в том случае, если конкретное утверждение действительно от этого зависит.
  • Сравнивайте точные сообщения об ошибках только с версией зависимостей, которую вы задали в качестве фиксированной.
  • API, сгенерированные в таком порядке, становятся гораздо проще для анализа, если их этапы сохраняются раздельными. Ошибки конфигурации должны проявляться ещё до того, как начнется обработка запросов. Запросы, нарушающие правила, должны указывать точно, какую часть контракта они нарушили. А успешный ответ следует проверять с учётом аргументов, которые он фактически отправил, и семантики передачи данных, описанной для него, а не только с учётом кода состояния.

    Связанные материалы

  • Сокращение нагрузки на Prisma и PostgreSQL перед покупкой более мощной базы данных — Пятнадцать практических методов, от использования команды EXPLAIN ANALYZE и составных индексов до решений проблемы N+1, счетчиков, технологий пуллинга и операций вакуумирования, для снижения объема операций в базе данных Prisma.
  • Анализ структур prisma-guard: права собственности, проекция данных и контракты записи — Научитесь анализировать структуры prisma-guard в качестве API-контрактов, определяя, кто владеет каждым значением, какие данные могут входить в ответ и какие операции записи может выполнять генерированный конечный пункт.