Главная / Статьи / Балансировка генерации Prisma CRUD и целенаправленного управления маршрутами

Балансировка генерации Prisma CRUD и целенаправленного управления маршрутами

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

2767 слов

Концовки CRUD в основном лишь повторяют информацию, уже содержащуюся в схеме Prisma. Имя модели превращается в сегмент маршрута, а скэларные поля — в механизмы проверки входных данных. Вызовы Prisma становятся методами контроллеров. Связи между моделями добавляют ещё один уровень обработки входящих запросов и формирования ответа.

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

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

prisma-generator-express берет на себя всю эту рутинную работу и включает её в шаг prisma generate. Он может генерировать роутеры для Express, Fastify или Hono. Сопутствующий пакет prisma-guard одновременно создаёт метаданные для валидации и определения диапазонов, учитывающие особенности Prisma, а спецификации операций точно указывают, какие аргументы может отправлять каждый тип вызывающего кода.

В итоге у вас получается не приложение без кода. Это приложение с гораздо меньшим количеством вспомогательных компонентов на уровне интерфейса и гораздо более чётким контролем над оставшимися решениями.

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

Переместите повторяющуюся работу в один шаг генерации

Генерируемый API начинается с трех взаимосвязанных компонентов: клиента Prisma, метаданных защиты и самих 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,
      },
    },
  },
}

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

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

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

Функции after-hooks выполняются после обработчика, но они не являются фазой очистки, на которую можно безоговорочно полагаться. Ответ, завершающийся раньше времени, или ошибка, возникающая в процессе запроса, могут помешать выполнению последующих этапов — включая after-hooks. Если ресурс обязательно должен быть освобождён в любом случае, ему нужен собственный цикл жизни с явным блоком 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, что более практично для больших структур аргументов. Хук, влияющий только на тело запроса, создаёт поведение, которое косвенно зависит от способа передачи данных, и именно поэтому там не следует устанавливать жёсткие ограничения.

Использование генерации без отказа от проверки

Практический способ оценки такой конфигурации включает короткую последовательность действий:

  1. Сгенерировать маршрутизатор для одной модели с режимом только чтения.
  2. Обеспечить доступ только к тем операциям, которые действительно необходимы.
  3. Добавить одну прямую форму с явным проектированием и ограничением по размеру страницы.
  4. Проверить аргументы Prisma, которые фактически отправляет маршрутизатор.
  5. Добавить контекст доверенного диапазона, если модель настроена для аренды.
  6. Разделить операцию на отдельные контракты вызывающих сторон только тогда, когда аудитории действительно различаются.
  7. Добавлять хуки только для решений, которые формы, диапазоны и варианты не могут принимать самостоятельно.
  8. Вводить операции записи только после того, как для полноты создания, массовой фильтрации и принадлежности к отношениям будут разработаны явные тесты.

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

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

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

  • Raw SQL, Prisma или Drizzle: как на самом деле выбрать слой базы данных — Узнайте, в чём различия между использованием сырого SQL, Prisma и Drizzle с точки зрения уровня контроля, безопасности типов и удобства работы для разработчиков, а также освойте практический способ выбора подходящего решения для проекта.
  • Обзор MovieVault: API списка для просмотра с использованием Express 5, Prisma 7 и JWT — Подробное описание задания для полноценной разработки фронтенда и бэкенда с использованием Express, Prisma и JWT, включая комментарии по проверкам прав собственности, каскадным действиям и обработке ошибок.
  • Анализ применения prisma-guard Shapes: модели владения, проекция и контракты записи — Научитесь анализировать применение prisma-guard Shapes в качестве API-контрактов, определяя, кто владеет каждым значением, какие данные могут быть возвращены и какие операции записи могут выполняться через генерированные конечные точки.
  • Выявление отклонений API-контрактов во время компиляции с использованием поэтапного развертывания tRPC — Как tRPC превращает изменение названия поля на серверной стороне в ошибку компиляции, как использовать его постепенно рядом с REST-интерфейсами и в каких случаях он не подходит.