Огляд примітивів prisma-guard: власність, проєкція та контракти запису
Навчіться розглядати форми prisma-guard як контракти API, запитуючи, хто володіє кожною значенням, які дані можуть бути включені до відповіді та які операції запису можуть виконуватися через створений кінцевий пункт.
Джерела: Define Every Prisma Endpoint’s Inputs and Outputs in One Place
Мова аргументів Prisma є виразною, і саме цього ви хочете у надійному серверному коді. Однак як тільки така сама структура аргументів починає використовуватися через HTTP, кожна можливість цієї мови перетворюється на рішення щодо продукту: що може написати користувач, які фільтри він може використати, що сервер повинен обмежити, які зв’язки можна повернути та скільки дочірніх рядків може містити одна відповідь. Функції prisma-guard дозволяють записувати ці відповіді безпосередньо поруч із операцією Prisma. Цей посібник показує, як розглядати таку структуру як компактний контракт для керування, фільтрації, проекції та змін, використовуючи три запитання, які можна поставити до будь-якого pull request з додаванням генерованого кінцевого пункту.
Чому структура є чимось більшим, ніж просто валідація
Форма дійсно перевіряє вхідні дані, але її головна перевага полягає у тому, що вона об’єднує рішення щодо меж у одному місці. Операція, створена на основі форми, може користуватися нею замість того, щоб кожен обробник вручну перебудовував схеми вхідних даних та списки дозволених полів. Це усуває повторюваний код та також ризик того, що два обробники поступово будуть відрізнятися у тому, що вони дозволяють.
Ціною такої зручності є точність. Форма є надійним контрактом лише тоді, коли вона відповідає тому, що насправді реалізує бібліотека, включаючи чесність щодо того, де припиняється дія скоупу верхнього рівня. Описана нижче поведінка відображає реалізацію у тому вигляді, в якій її перевіряють її адміністратори на момент написання, а не встановлену спільнотою конвенцію, тому перевірте деталі у документації до вашої версії. Якщо запит, який проходить перевірку, вже зазнає невдачі та ви хочете дізнатися причину, почніть з нашої моделі діагностики невдач prisma-guard на основі фаз.
Перше запитання: хто є власником кожної значення?
Кожен елемент форми передає значення або клієнту, або серверу. Словник мінімальний; у прикладі показані всі чотири форми поруч.
const values = {
clientChooses: true,
serverChooses: 'published',
serverChoosesTrue: force(true),
clientChoosesWithinRule: (base) => base.min(1).max(120),
}
Уважно читайте описи:
- Чисте
true— це не булеве значення. Це сигнал, що означає „рішення приймає клієнт“. - Літерал, такий як
'published', належить серверу. force(value)також належить серверу. Він потрібен тоді, коли значення, яке потрібно зафіксувати, є літераломtrue, адже чистеtrueбуде сприйняте як сигнал клієнта.- Функція уточнення залишає рішення за клієнтом, але замінює генерований Zod-ланцюг поля на правило, яке ви вказуєте безпосередньо.
Місце розміщення впливає на значення функції force
Слова прості; саме їхнє розташування дивує людей. У форматі where примусовий скаляр потрапляє всередину об’єкта оператора. Фільтр подій нижче фіксує статус публікації, водночас дозволяючи клієнту шукати заголовки:
const eventShape = {
where: {
isPublished: { equals: force(true) },
title: { contains: true },
},
}
Коли весь скалярний предикат на корені where є примусовим, бібліотека лояльно ставиться до введених клієнтом даних у цьому полі. Незалежно від того, чи повторює користувач фіксоване значення, надсилає інше чи додає якийсь інший оператор, предикат сервера має перевагу, і запит не відхиляється лише через згадку про це поле. Це не те саме, що значення за замовчуванням. Значення за замовчуванням відступає убік, коли клієнт надає якісь дані; примусове значення ніколи не відступає.
У всіх інших випадках правила є суворими. Уявіть собі модифікатор примусу, розташований поруч із оператором, яким керує клієнт, закріплене поле в фільтрі взаємозв’язку, закріплений фільтр у вкладеному елементі або закріплене значення в даних зміни. У всіх цих випадках ключ зникає з схеми, за якою перевіряється клієнт, тож надсилання його призводить до невдачі перевірки, навіть якщо значення збігається точно.
Це має значення для коду клієнта. Клієнту може знадобитися повністю видалити поле для запису, яке належить серверу. „Виправлення“ його значенням сервера все одно призведе до невдачі, оскільки перевірка стосується того, хто володіє ключем, а не того, чи є значення рівними.
За замовчуванням, обробка помилок та доробки також змінюють право власності
Значення за замовчуванням та механізми обробки помилок заслуговують на однакову увагу. У полі, яке контролює клієнт, значення за замовчуванням зберігає свою звичайну поведінку, коли значення не вказано, а механізм обробки помилок може замінити некоректний вхідний даний на альтернативний замість того, щоб відхилити запит. Операції створення додають ще одну складовість: директива за замовчуванням чи обробки помилок, створена генератором, навіть може заповнити обов’язкове поле, про яке в форматі нічого не зазначено, причому значення, яке вона створює, потім вводиться так, ніби його визначив сам сервер. Тож місце розміщення значення за замовчуванням визначає, хто є власником цього значення, тому покладайтеся на заміну лише тоді, коли це вимагається контрактом API.
Удосконалення приховують пов’язану пастку. Вони не розширюють створену ланцюгову структуру у своєму вузлі; вони її замінюють. Тому ваша функція повинна знову вказати кожну обмеження, від яких залежить кінцева точка, а не лише додаткові мінімальні чи максимальні значення, які ви хотіли додати. Це особливо важливо, коли тип Prisma є більш слабким, ніж правило продукту, наприклад, колонка з рядком, яка ніколи не повинна бути порожньою.
Примусові оновлення та примусові диз’юнкції
Дані оновлення, які закріплені, записуються під час кожного відповідного виклику. Припустимо, що форма оновлення вимагає значення status: 'active': будь-яке оновлення через неї, навіть те, яке змінює лише заголовок, також скидає статус. Якщо операція має залишити певне поле недоторканим, виключіть це поле з форми оновлення.
Булева структура додає ще одне правило. Умова, встановлена під OR, піднімається до рівня обов’язкової умови на вершині. Внаслідок цього форма не може виразити справжнє правило типу „або“, яке належить серверу, наприклад записи, що належать цьому обліковому запису або були поділені з цією групою. Такі правила вимагають запиту, написаного на сервері, окремої колонки для визначення діапазону, яка фіксує правила, або застосування контролю безпосередньо в самій базі даних.
Тому перше запитання для перевірки є більш конкретним, ніж „Чи це перевірено?“. Слід запитати, хто визначає кожне значення, де розміщується кожен примус та яка структура аргументів насправді потрапляє до Prisma.
Друге запитання: що може розкрити відповідь?
Фільтрація та проєкція відповідають на різні запитання. Фільтр орендаря визначає, які записи підходять. Проєкція визначає, які з їхніх полів та взаємозв’язків будуть надіслані назад. Вам потрібні обидва елементи, і перевірка одного нічого не говорить про інший.
Для захищених операцій читання select або include, оголошені для форми, водночас є списком дозволених елементів та запасним варіантом. Клієнти можуть запитувати лише вужчу структуру даних, але не більше; коли вони не надсилають проєкцію, захисний деплегат використовує оголошену раніше.
Форма квитка нижче дозволяє клієнту отримати квиток за ідентифікатором, повертає фіксований набір кореневих полів та відображає вкладений список приміток клієнта з власним фільтром, полями та розміром сторінки.
const ticketShape = {
where: {
id: { equals: true },
},
select: {
id: true,
subject: true,
description: true,
customerNotes: {
where: {
accountId: { equals: force(accountId) },
},
select: {
id: true,
body: true,
},
take: { max: 20, default: 10 },
},
},
}
Ця одна структура виконує чотири окремі завдання:
- Кореневі поля, які не вказані, залишаються недоступними.
- Можуть бути повернуті лише вказані поля приміток.
Вкладені взаємозв’язки не підпадають під автоматичне обмеження
Фільтр приміток реалізований навмисно так, щоб бути окремим елементом. Автоматичне обмеження за тенантом застосовується лише до операцій, підтримуваних на рівні кореня запиту; воно не поширюється на взаємозв’язки, отримані за допомогою select чи include, та не додає умов до них. Не варто забувати про вкладені умови where — навіть правильно обмежений запит може повертати примітки, які належать до іншого акаунту.
У відношеннях типу „багато-до-багатьох“ допускається використання вкладеного оператора where, власної проекції та обмежень для рядків. У відношеннях типу „чутливе один-до-одного“ не можна використовувати вкладений оператор where у Prisma, тому потрібна інша стратегія: виключити це відношення, відобразити лише нешкідливі скалярні поля з нього, завантажити його за допомогою власного запиту на рівні кореня з захистом або захистити його за допомогою політики бази даних.
Вибір стилю проекції
Виберіть форму проекції з урахуванням того, як вона буде функціонувати у міру зростання схеми:
- За допомогою
selectстовпець, доданий до моделі пізніше, залишається прихованим, поки хтось його не перегляне та не додасть до списку, що робить цей варіант найбезпечнішим для публічних кінцевих точок. - За допомогою
includeви отримуєте відношення, але коренева модель все одно повертає всі свої скалярні стовпці, тож чутливі дані кореня залишаються видимими.
omit ви підтримуєте список заборонених елементів, і наявність цього списку залежить від вашого генератора та версії Prisma.Це альтернативи, а не різні рівні структури. Ніколи не поєднуйте select, include та omit на одному рівні дерева.
Підрахунки, вкладені значення за замовчуванням та відповіді на мутації
Кількість елементів у взаємозв’язку є окремим вузлом проекції, а не підсумком, пов’язаним із завантаженим взаємозв’язком. Якщо взаємозв’язок типу „багато-до-багатьох“, що повертається, фільтрується за обліковим записом, його _count має містити той самий фільтр, щоб клієнти вважали, що обидва числа описують одні й ті самі рядки. Тестуйте умови генерованого взаємозв’язку та умови підрахунку окремо.
Стандартні параметри вкладеної проекції визначаються на основі оголошеної структури. Якщо клієнт просить про оголошене співвідношення з використанням скорочень типу Boolean, це співвідношення все одно може зберегти свій фільтр та обмеження сторінки. Однак дозволи, надані клієнту, не заповнюють відсутні опції: дозвіл на сортування за певним полем не визначає напрямок, який клієнт пропустив. У тестах необхідно перевірити три випадки: пропущену кореневу проекцію, явну вкладену проекцію та скорочення співвідношень, замість того щоб припускати, що один випадок доводить решту.
Мутації навмисно є асиметричними. Форма мутації може перевіряти проекцію, яку запитує клієнт, але якщо параметр enforceProjection не увімкнений, клієнт, який нічого не просить, не отримує проекції у визначеній формі, і діє звичайна поведінка Prisma. Увімкніть цей параметр кожного разу, коли відповіді на мутації мають обмежуватися лише оголошеними полями.
Наостанок переконайтеся, що кожен тест виконує ту фазу, яку він має перевіряти. Виклик guard.query(...).parse() — це хороший спосіб перевірити фільтри читання та межі, але значення за замовчуванням для проекції даних видно лише після виконання захищеного делегата, і те саме стосується проекції для змін — вона також потребує захищеного шляху зміни.
Тож друге запитання — це не „чи є select?“, а які кореневі поля, пов’язані поля, пов’язані рядки та кількості може отримати кожна версія виклику.
Третє запитання: які операції запису можна виконати?
Сформовані кінцеві точки запису приймають аргументи у власному форматі Prisma. Форма даних визначає, які значення надає клієнт, які — сервер, та чи можливі будь-які вкладені операції.
Форми для створення мають враховувати всі обов’язкові поля
Форма для створення відхиляється, якщо для деякого обов’язкового поля немає джерела. Допустимі джерела включають:
- клієнт, коли поле позначене як кероване клієнтом
- сервер, через літеральне значення або метод
force - стандартне значення, визначене у схемі Prisma
- підтримуване стандартне значення або механізм обробки помилок Zod
- вставка індексу стороннього ключа тенанта
- дозволена операція запису в зв’язку, яка заповнює цей ключ
Приклад підписки дозволяє клієнту обирати зовнішній ключ та план, обмежує кількість місць за допомогою правила з цілим числом, фіксує початковий статус та версію на сервері та повертає скорочену проекцію даних.
const createSubscriptionShape = {
data: {
externalKey: (base) => base.min(1).max(100),
planId: true,
status: 'trialing',
seatCount: (base) => base.int().min(1).max(500),
version: 0,
},
select: {
id: true,
externalKey: true,
status: true,
seatCount: true,
},
}
Зверніть увагу на те, чого відсутньо: сторонній ключ тенанта. Його ніколи не повинен встановлювати клієнт під час створення. Кореневий індекс походить з надійного контексту запиту, а розширення саме додає відповідний сторонній ключ для операцій запису на рівні кореня, які воно підтримує.
Масові операції та створення пакетом
Блокові мутації повинні пройти дві перевірки. По-перше, updateMany, його повернені варіанти та deleteMany потребують структури для визначення словника where. По-друге, вхідний запит має створювати справжню умову. Відсутній тіло запиту, порожній об’єкт оператора чи логічна гілка, яка відповідає всьому, не підходять, що запобігає випадковим змінам у всій таблиці.
Функція обробки пакетних операцій вимагає масиву та не може безпомічно обробляти окремий об’єкт. Те, чи будуть повертатися операції з пакетом та такі опції, як ігнорування дублікатів, все ще залежить від вашого постачальника баз даних.
Upsert — це три контракти
Структура upsert насправді складається з трьох частин: унікальний селектор, дані для створення та дані для оновлення.
const upsertSubscriptionShape = {
where: {
externalKey: true,
},
create: {
externalKey: true,
planId: true,
status: 'trialing',
seatCount: true,
version: 0,
},
update: {
planId: true,
seatCount: true,
},
}
У полі даних create необхідно вказати всі обов’язкові поля, тоді як у полі даних update можна пропустити що завгодно. Параметр force у операції create встановлює значення лише під час додавання нового рядка, тоді як такий самий параметр у операції update перезаписує його щоразу, коли знаходить відповідний існуючий рядок. У прикладі поля status та version вказуються під час створення, а у операції update їх навмисно не вказують, тож існуючі підписки залишаються у своєму поточному стані.
Унікальні зміни виконуються за допомогою простого формату унікального вибору Prisma. Об’єкти операторів призначені для звичайного фільтрування та не мають місця у структурі умови where на рівні кореня.
Контракти запису та повернення даних у відношеннях
Інтерфейс відношення передбачає необхідність окремого рішення щодо власності. Форма може дозволяти використання методу connect, обмежувати клавіші-селектори, які може використовувати клієнт, та відхиляти будь-які вкладені операції, які не перелічені. Однак жоден з цих елементів не розширює область дії тенанта на селектор. Коли має значення, до якої запису буде встановлено з’єднання, ваш код додатку або база даних повинні перевірити, чи дозволено користувачеві це робити.
Контракти повернення даних також відрізняються залежно від методу. Методи пакетних операцій, які повертають лише кількість записів — createMany, updateMany та deleteMany — відповідають із показником кількості та відхиляють проекції даних. Їхні аналоги, які повертають дані, можуть приймати проекції, якщо це дозволено як Prisma, так і вашим постачальником. Якщо налаштування генератора змінює маршрут на метод повернення даних, необхідно розглянути нову відповідь як зміну API, а не ігнорувати її як деталь реалізації.
Третє запитання — це не «Чи увімкнений цей метод?» Запитайте, що саме надає цей метод: поля даних, форми вибору, умови масової обробки, операції з взаємозв’язками та поля відповідей.
Шарування механізмів забезпечення
Форми працюють найкраще, коли їхні обмеження документовані разом із іншими механізмами. Кожен шар виконує свою функцію:
- Аутентифікований запит надає контекст, якому можна довіряти.
- Мапування обсягів додає умови користувачів до підтримуваних операцій на рівні кореня.
- Варіанти виклику вибирають, яка з оголошених форм підходить для певної аудиторії.
- Хуки виконують обмежені перевірки додатку, які можуть призвести до відхилення запиту.
- Власноруч написані обробники керують запитами, які форми не можуть описати.
- Політика бази даних захищає інваріанти, які мають зберігатися незалежно від шляху доступу.
Жоден з цих інструментів не повинен претендувати на виконання роботи іншого. Якщо ви вирішуєте, які маршрути генерувати, а які писати вручну, наша стаття про збалансування генерації Prisma CRUD та контролю маршрутів розглядає саме цей підхід.
Огляд та чек-лист для тестування
Коли запит pull request додає генерований кінцевий пункт, перегляньте його, відповівши на три запитання:
- Хто визначає кожне значення, яке приймає кінцевий пункт?
- Які поля та зв’язки може містити відповідь?
- Які мутації та форми вибору може виконати користувач?
Тоді налаштовуйте кожен тест так, щоб він перевіряв конкретне твердження, яке має підтвердити. Аналізуйте аргументи вхідних даних для перевірки поведінки фільтрів. Запускайте захищені депутати для перевірки проекцій та операцій запису. Оглядайте шлях розширення для перевірки введення області дії. Використовуйте фіксати багатокористувацької бази даних, коли йдеться про те, які рядки повертаються, а для тестів HTTP — коли йдеться про транспортування чи генеровані маршрути.
Надавайте негативним тестам таку саму вагу. Для кожної аудиторії намагайтеся прочитати кореневе поле, яке не вказано, прочитати конфіденційне вкладене поле, перевищити межу взаємозв’язку та виконати операцію запису, яка заборонена формою. Позитивні тести підтверджують, що запит виконується успішно; негативні тести підтверджують, що критичні моменти залишаються закритими після змін у моделі чи генераторі.
Підсумок
Форма отримує назву „contract“ лише тоді, коли чітко визначено, що вона дозволяє та що не може примусити до виконання. Окремий аналіз прав власності, проєкцій та операцій запису дозволяє рано виявити типові прогалини: слабкі правила на рівні кореня, необмежені вкладені взаємозв’язки, підрахунки, які ігнорують фільтри взаємозв’язків, та з’єднання, що обходять межі тенанта. Як тільки ці прогалини будуть призначені до відповідних хуків, обробників чи правил бази даних, генерація коду може усунути повторюваний код меж без перетворення мови запитів Prisma на публічний API, який ніхто не планував оприлюднювати. Для отримання повної синтаксису, точних повідомлень про помилки, поведінки, специфічної для версій, та відтворюваних прикладів дивіться посібник prisma-generator-express.