Головна / Статті / Діагностика невдач 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. Операції upsert отримують 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 повертає нуль рядків та флаг продовження у форматі false, при цьому загальна кількість залишається незмінною.

Загальна кількість обчислюється за зовсім іншою логікою. Розрахунок окремих елементів враховує налаштований ліміт. Джерело попередньо обчисленої кількості використовується лише тоді, коли запит є невідфільтрованим, без захисту та не є окремим. Будь-який динамічний фільтр, умова окремості чи механізм захисту змушують вдаватися до реального підрахунку кількості у момент виконання запиту.

Цей захід зберігає коректність, але змінює як вартість операції, так і джерело отримання числа. Семантику загальної кількості слід розглядати як окрему проблему, відокремлену від семантики розділення рядків.

Запити типу POST існують для обробки розміру та кодування даних, а не для розширення можливостей мови запитів:

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

Надсилайте тіло запиту у вигляді нативного JSON. Очікується, що версії GET та POST одного маршруту будуть дотримуватися ідентичних правил обробки. Якщо хук переписує тіло запиту, ця еквівалентність може порушитися, оскільки маршрут GET читає дані з вже оброблених параметрів запиту, а не з тіла у форматі JSON.

Server-sent events оновлюються під час надходження даних, а не залежно від того, які саме дані надходять. Цей механізм має сенс лише тоді, коли клієнт дійсно реалізує обробку подій прогресу, подій кінцевого успіху, подій кінцевої невдачі та запасного маршруту.

{"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, визначаючи, хто є власником кожного значення, які дані можуть бути включені у відповідь та які операції запису може виконувати створений кінцевий пункт.