Балансування генерації Prisma CRUD та цілеспрямованого керування маршрутами
Дізнайтеся, як генерація маршрутів Prisma CRUD на основі схем може усунути повторюваний шаблонний код, зберігаючи при цьому рішення щодо довіри, обмежень та доступу в коді застосунку.
Кінцеві точки CRUD переважно повторюють інформацію, яка вже міститься у вашій схемі Prisma. Ім’я моделі стає частиною маршруту. Скалярні поля перетворюються на механізми верифікації вхідних даних. Виклики Prisma стають методами контролерів. Взаємозв’язки означають ще один рівень обробки вхідних запитів та формування результатів, які надсилаються назад.
Цей огляд теми ґрунтується на інструменті відкритого коду, створеному його адміністратором, оцінюваному за його поточною документацією та експериментами, які можна самостійно відтворити, а не на твердженнях щодо його широкого використання.
Це повторення є дорогим саме тому, що здається безневинним. Кожен вручну написаний обробник, який ви копіюєте, — це ще одне місце, де параметри сторінкування, дозволені поля, обмеження для користувачів та обробка помилок можуть потайки відрізнятися від інших.
prisma-generator-express забирає цю механічну роботу у вас з рук та об’єднує її у крок prisma generate. Він може створювати роутери для Express, Fastify чи Hono. Супутній пакет prisma-guard генерує валідацію, сумісну з Prisma, та метадані областей дії, а специфікації операцій чітко визначають, які аргументи дозволено надсилати кожному типу користувача.
У результаті у вас не буде застосунку без коду. Це застосунок із значно меншою кількістю елементів верхнього шару та набагато чіткішим контролем над усіма рештою рішеннями.
Саме це розрізнення є важливим. Генерація має брати на себе все, що сама схема може повністю описати. Аутентифікація, операції, які є доступними, ідентичність користувачів та будь-які правила, які потребують інформації, що виходить за межі схеми, все одно мають знаходитися у коді застосунку.
Перемістіть повторювану роботу в один крок генерації
Генерований API починається з трьох взаємопов’язаних елементів: Prisma Client, метаданих захисту та самих HTTP-маршрутизаторів.
generator client {
provider = "prisma-client-js"
}
generator guard {
provider = "prisma-guard"
output = "../generated/guard"
enforceProjection = "true"
}generator express {
provider = "prisma-generator-express"
target = "express"
}
Виконання команди npx prisma generate один раз знову генерує всі три елементи щоразу, коли змінюється схема чи конфігурація генератора.
Схема залишається єдиним джерелом істини для вашої моделі даних. Генеровані файли маршрутизаторів є лише результатом процесу будування, нічим більшим. Конфігурація маршрутів визначає, які операції фактично будуть використані, а структури захисту визначають, які аргументи Prisma може використовувати певний користувач.
Тримати ці аспекти окремо значно корисніше, ніж розглядати генерований код як заміну архітектурі. Навмисно використовуються два окремі вхідні дані для всього процесу: генерація, заснована на схемі, керує повторюваними механізмами, тоді як політика на рівні додатку вирішує питання довіри та визначення маршрутів.
Якщо якусь правило неможливо точно виразити за допомогою генератора чи спеціальної структури захисту, не намагайтеся впровадити його через конфігурацію. Спеціалізований обробник чи політика, яка діє на рівні бази даних, створюють чіткіші межі, ніж декларативні налаштування, які таємно приховують свою справжню функцію.
Генерація також змінює те, що насправді перевіряється під час рецензування коду. Ручно написаний CRUD спонукає рецензентів перевіряти логіку обробки даних та делегування завдань рядок за рядком. Генерований CRUD переносить цю увагу на значно меншу кількість елементів: схему Prisma, параметри генератора, описи маршрутів, їхню структуру та будь-які компоненти, які встановлюють надійний контекст.
Це не робить генерований результат неважливим — це просто означає, що пряма редагування є неправильним підходом. Якщо маршрут потребує виправлень, змініть конфігурацію, яка його створює, та згенеруйте його знову. Ручний патч, доданий до файлу маршрутизатора, може зникнути після наступної зміни схеми, не залишаючи жодних слідів того, який саме формат був спочатку передбачений.
Підвищення версій вимагає такої ж дисципліни. Фіксуйте Prisma, пакет захисту та генератор маршрутизатора на конкретні версії, генеруйте код з чистого репозиторію та запускайте тести контрактів щодо отриманого результату. Генерований код все одно залишається частиною вашого списку залежностей, навіть якщо ваш репозиторій не сприймає кожен створений рядок як код, написаний вручну.
Справжня перевага у швидкості полягає у можливості повторення дій. Одна зміна схеми може одночасно оновити метадані валідації, типи клієнта та механізми маршрутизатора. Це дозволяє зосередитися під час перевірки на відносно вузькому шарі правил — тій частині, яку справді неможливо вивести лише з моделі.
Нехай одна модель обслуговує кілька цілеспрямованих контрактів
Одна модель Prisma може одночасно підтримувати кілька різних інтерфейсів, орієнтованих на клієнтів.
Наприклад, запис про номер готелю може з’являтися на сторінці публічних пошуків, у потоці даних партнера та в внутрішній консолі персоналу. Цих трьох користувачів не слід змушувати ділитися одним об’ємним збором усіх полів та операцій, які їм можуть знадобитися.
Іменовані форми дозволяють одній створеній операції водночас обробляти кілька різних контрактів:
const roomRoutes = {
findMany: {
shape: {
storefront: {
where: {
isPublished: { equals: force(true) },
name: { contains: true },
},
select: { id: true, name: true, nightlyRate: true },
take: { max: 40, default: 20 },
},
backoffice: {
where: {
name: { contains: true },
floor: { equals: true },
},
select: {
id: true,
name: true,
nightlyRate: true,
floor: true,
internalNote: true,
},
take: { max: 200, default: 50 },
},
},
},
}
Кожен іменований ключ визначає повний, самодостатній контракт API. Публічний користувач не може розширити свою проекцію, щоб включити internalNote, оскільки це поле просто не існує у публічній формі. Персонал може отримати набагато багатшу проекцію, не змушуючи кожного іншого клієнта використовувати власний ручно створений маршрутизатор.
Використовуйте shape, коли між користувачами потрібно лише відрізнятися на рівні Prisma. Використовуйте variants, коли певному користувачеві також потрібні власні спеціалізовані хуки.
Спосіб ідентифікації абонента є частиною меж безпеки. Заголовок запиту вважається даними, наданими клієнтом — це підходить для навмисних публічних відмінностей, наприклад між стислим та детальним переглядом, але не може використовуватися для вибору привілейованого контракту співробітника.
Для будь-яких привілейованих дій слід ідентифікувати абонента за допомогою resolveVariant, використовуючи автентифікований стан з боку сервера. Спочатку перевіряються точні ключі абонента, а потім параметризовані. Ключ default обробляє випадки відсутнього, порожнього чи іншим чином непідтвердженого абонента, тому визначайте його лише тоді, коли впевнені, що всі ці випадки будуть перенаправлені на цей альтернативний варіант.
Іноді повне виключення певного контракту є кращим рішенням, ніж додавання ще однієї перевірки авторизації. Якщо партнери ніколи не повинні мати можливості видаляти кімнати, просто не надавайте генерованій операції видалення жодного ключа партнера.
Корисно розглядати маршрутизацію дзвінків у вигляді сітки: операції — вздовж однієї осі, аудиторія — вздовж іншої. Кожна клітинка має або містити елемент із належним форматуванням, або бути навмисно залишеною порожньою.
Зберігайте ці контракти окремо, навіть якщо вони сильно перетинаються за полями. Спільне використання об’єкта є досить безпечним у межах одного рівня довіри, але повторне використання одного спільного об’єкта серед публічної та привілейованої аудиторії створює ризик тихо розширення обох кінцевих точок як тільки хтось додасть нове поле. Трохи дублювання на межі рівнів довіри часто є доцільним, адже це значно полегшує зрозуміння того, хто насправді отримує які дані.
Параметризовані ключі викликаючих елементів дають ще одну причину використовувати вбудований механізм розв’язання замість того, щоб самостійно реалізовувати вибір викликаючого елемента всередині хука. Маршрутизатор зберігає первинне значення викликаючого елемента окремо від оголошеного ключа, з яким воно порівнюється, та безумовно відхиляє неоднозначні шаблони параметрів. Для того, щоб строга порівняння рядків могла стверджувати про надання таких самих гарантій, їй довелося б відтворити точне збіг, пріоритет параметрів, обробку за замовчуванням та поведінку при помилках.
Використовуйте хуки для прийняття рішень щодо життєвого циклу, а не для створення прихованих запитів
Генеровані маршрути не усувають потреби у рішеннях на рівні додатку. Вони просто надають цим рішенням передбачуване місце для реалізації.
Для запиту, який відповідає певній версії, виконання проходить через передні хуки на рівні операції, потім передні хуки на рівні версії, далі сам обробник, створений на основі цього запиту, потім задні хуки на рівні версії та, нарешті, задні хуки на рівні операції.
Хуки на рівні операції — це ідеальне місце для правил, які застосовуються до кожного користувача цієї операції, незалежно від того, до якої версії він належить. Хуки на рівні версії стосуються логіки, специфічної для певної схеми користувача.
const transferRoutes = {
update: {
before: [authenticateOperator],
variants: {
warehouse: {
before: [authorizeTransferLocation],
shape: warehouseTransferShape,
},
supervisor: {
before: [requireSupervisorApproval],
shape: supervisorTransferShape,
},
},
},
}
Передній хук може вільно перевірити саме той ідентифікатор, який має використати створений обробник, і може відхилити запит безпосередньо, якщо цей ідентифікатор не пройшов перевірку. Він ніколи не повинен дозволяти використання одного ідентифікатора, тихо підмінюючи його іншим у фактичному запиті. Така тиха заміна суперечить самій меті наявності обробника, який можна перевірити.
Обмеження, які ніколи не змінюються, належать до форм. Фільтрація на рівні орендаря, яка застосовується на початку запиту, має бути включена до створених картувань діапазонів у поєднанні з надійним контекстом. Відмінності між типами викликів належать до варіантів. Кожен з цих елементів має визначене місце, і саме їхнє змішування призводить до того, що логіка опиняється там, де ніхто її не шукає.
Існують випадки, коли сервер дійсно потребує створити запит, який неможливо виразити за допомогою жодної форми. Саме тоді стає важливим наявність спеціально створеного обробника — особливо коли потрібна справжня диз’юнкція, якою володіє сервер. Варто пам’ятати, що примусові умови, вбудовані всередину булевих комбінаторів, стають обов’язковими обмеженнями для запиту, а не гнучким механізмом для формулювання довільних правил авторизації. Використання їх як універсального механізму логіки — це поширений спосіб отримати правила, які насправді не забезпечують того, що ви очікуєте.
Післязапускові функції виконуються після обробника, але це не фаза очищення, на яку можна безумовно покладатися. Відповідь, яка завершується раніше, або помилка, що виникає під час запиту, можуть завадити виконанню наступних етапів — включаючи післязапускові функції. Якщо ресурс обов’язково має бути звільнений незалежно від усього, що відбувається, йому потрібен власний цикл життя з чітким блоком finally, розташованим поза ланцюгом гаків, а не всередині нього.
Конкретні деталі також залежать від вашої цільової фреймворк-системи. Express, Fastify та Hono реалізують сигнатури гаків та механізми переривання виконання по-різному. Загальний принцип — де має бути прийнято певне рішення — залишається однаковим у всіх трьох, але сам код програми має відповідати умовам конкретної фреймворк-системи, до якої він призначений.
Зберігайте надійний контекст поза аргументами Prisma
Ідентифікатор орендаря та стан автентифікованого абонента ніколи не повинні надсилатися на сервер як поля, якими керує клієнт у тілі запиту.
Натомість слід застосувати маркер @scope-root до моделі орендаря, запустити процес генерації для створення відповідної карти діапазону та приєднати резолвер контексту до Prisma Client через його механізм розширень:
const prisma = new PrismaClient().$extends(
guard.extension(() => ({
Nursery: requestStore.getStore()?.nurseryId,
}))
)
Саме значення походить від автентифікованого, локального до запиту стану — а не від чогось, що надсилає клієнт. Потім розширення вставляє його у підтримувані операції верхнього рівня над моделями, які відображаються як дочерні елементи цього кореня діапазону.
Це справжня, чітко визначена функція, а не універсальна гарантія того, що кожні відношення будуть автоматично захищені. Запобігання вихіду за межі не поширюється на вкладені операції читання чи запису. Сама модель делегування кореня також не фільтрується за допомогою власного маркера області. Крім того, будь-яка модель, якій бракує генерованого мапування, все одно потребує власного прямого захисту — контекст області за замовчуванням її не покриватиме.
Швидкість генерації все одно є корисною саме тому, що ці межі є видимими, а не прихованими. Ви можете безпосередньо переглядати карту областей. Вкладені проекції можуть мати власні незалежні фільтри та обмеження. А будь-які незвичайні правила власності, які не відповідають стандартній схемі, можна реалізувати у коді додатку або обробляти на рівні бази даних.
Стан користувацького додатку — все, що виходить за межі сфери дії окремого користувача — має знаходитися в контексті запиту, а не безпосередньо в аргументах Prisma. Перенесення ідентифікатора викликаючого об’єкта чи метаданих авторизації до тіла запиту Prisma ускладнює розуміння формату даних та може призвести до помилок під час перевірки, яка очікує на правильну структуру аргументів.
Розглядайте доступ до маршрутів як частину дизайну продукту
Генератор здатний створювати обробники для великої кількості операцій Prisma. Однак ця здатність нічого не говорить про те, які з цих обробників насправді мають бути активовані та доступні.
Читання даних, мутації однієї запису, масові мутації, записи в зв’язки та операції, які повертають дані, усі вони потребують окремого розгляду, а не єдиного загального рішення. Підтримка надавачем деяких операцій, що повертають дані масово, варіюється, тому це — не лише питання політики, а й сумісності. Будь-який маршрут, у якому відсутні поля shape та variants, буде безпосередньо викликати Prisma без жодних заходів контролю.
Добре продумана налаштування — це не результат увімкнення всього та подальшого додавання перевірок відмови. Вона починається з невеликої, чітко визначеної бази та розширюється лише тоді, коли реальний робочий процес продукту демонструє потребу в іншій операції.
Читання проекції вимагає такого ж рівня уваги, як і доступ для запису. При захищеному читанні select або include, оголошені на рівні форми, виступають одночасно як список дозволених елементів, так і значення за замовчуванням, коли запит клієнта не містить власної проекції. Проекція для мутацій дотримується інших правил за замовчуванням, і якщо пропуск проекції ніколи не повинен дозволяти розширювати відповідь, необхідно явно увімкнути enforceProjection.
Для масових маршрутів щоразу потрібне окреме рішення. Метод масової обробки слід вважати дійсним у створеній моделі лише тоді, коли його форма визначає належний словник фільтрації, а вхідний запит все ще надає значущу умову під час виконання. Увімкнення deleteMany просто тому, що вже дозволено видалення однієї запису, ігнорує цей другий, окремий ризик. Повернення варіантів масових операцій створює власну залежність від постачальника та підтримки Prisma, тому конфігурація маршруту повинна відображати те, що насправді може виконувати розгорнута база даних, а не те, що бажало б реалізувати продуктове розробницьке планування.
Генерований OpenAPI-документ може описувати шляхи маршрутів та структуру запиту, отриману на основі певних форматів даних. Він не може проникати в довільні функції-гачки, тому не може описувати правила, приховані всередині них. Якщо гачок блокує передачу даних, які знаходяться поза межами складу, призначеного оператором, цю умову необхідно задокументувати поруч із конфігурацією маршруту та перевірити за допомогою тестування поведінки додатку — генеровану документацію ніколи не слід сприймати як доказ логіки, яку вона не може перевірити.
Версії GET та POST для кінцевої точки читання повинні мати спільний формат запиту. GET базується на кодованих параметрах запиту; POST приймає вихідний формат JSON, що є більш практичним для складних структур аргументів. Гачок, який впливає лише на тіло запиту, створює поведінку, яка тихо залежить від методу передачі даних, саме тому там не слід встановлювати стабільні обмеження.
Використовуйте генерацію, не відмовляючись від перевірки
Практичний спосіб оцінки такої конфігурації передбачає коротку послідовність дій:
- Створіть маршрутизатор для однієї моделі з можливістю лише читання.
- Відкрийте доступ лише до тих операцій, які дійсно необхідні.
- Додайте одну пряму форму з чітким проєктуванням та обмеженням розміру сторінки.
- Перевірте аргументи Prisma, які насправді відправляє маршрутизатор.
- Якщо модель прив’язана до окремого користувача, додайте контекст довіреного обсягу.
- Розділіть операцію на окремі контракти викликача лише тоді, коли аудиторії справді різняться.
- Додавайте хуки лише для рішень, які форми, обсяг та варіанти не можуть приймати самостійно.
- Введіть можливість запису лише після того, як будуть створені чіткі тести для повноти даних, масового фільтрування та власності на зв’язки.
Зберігайте тести контрактів real-guard навіть у сценаріях, де тестування браузера від кінця до кінця виконується за конфігурацією, яка повністю ігнорує перевірку захисних механізмів. Тести браузера добре покривають роутинг та поведінку інтерфейсу, але вони не можуть довести, що продакшн-версія відхиляє заборонене поле, коли шар захисту насправді відсутній.
Генератори корисні тим, що звільняють команду для фокусування на рішеннях, які справді мають значення. Prisma описує дані; генератори виконують повторювану механічну роботу; шаблони визначають, які виклики дозволені. Код застосунку відповідає за забезпечення довіри, політики, специфічної для продукту, та винятків, які неможливо чесно описати іншим способом.
Пов’язана література
- Створення GraphQL API з типовою безпекою за допомогою Prisma та Nexus у Node.js — Детальний посібник з семи кроків для створення GraphQL API у Node.js, який поєднує модель даних Prisma з типами та резолверами, створеними за допомогою Nexus.
- Zod проти express-validator: два підходи до верифікації даних у Express — Порівнює верифікацію запитів на основі схеми з використанням Zod та середовища express-validator, що ґрунтується на ланцюгах, розглядаючи налаштування, форматування помилок та поширені проблеми.