Inicio / Artículos

Revisión de las formas Prisma-Guard: Propiedad, proyección y contratos de escritura

Aprenda a revisar las formas de prisma-guard como contratos de API preguntando quién es el propietario de cada valor, qué datos pueden formar parte de una respuesta y qué escrituras en un endpoint generado pueden ejecutarse.

Editor: REACTAPP.TOP12 min de lectura2744 palabras

Fuentes: Define Every Prisma Endpoint’s Inputs and Outputs in One Place

El lenguaje de argumentos de Prisma es expresivo, y dentro del código del servidor de confianza eso es exactamente lo que se desea. Sin embargo, una vez que la misma estructura de argumentos se acepta a través de HTTP, cada capacidad de ese lenguaje se convierte en una decisión relacionada con el producto: qué puede escribir quien llama, qué filtros puede elegir, qué debe fijar el servidor, qué relaciones se pueden devolver y cuántas filas hijos puede expandir una respuesta. Las formas prisma-guard le permiten registrar esas respuestas directamente junto a la operación de Prisma. Esta guía muestra cómo revisar una forma como un contrato compacto para el control, filtrado, proyección y mutación, utilizando tres preguntas que puede plantear ante cualquier solicitud de integración que añada un endpoint generado.

Por qué una forma es más que una validación

Una forma sí valida la entrada, pero su mayor ventaja es que concentra las decisiones relacionadas con los límites en un único lugar. Una operación generada puede basarse en dicha forma, en lugar de que cada manejador reconstruya manualmente los esquemas de entrada y las listas de campos permitidos. Esto elimina el código repetitivo y también reduce el riesgo de que dos manejadores diverjan silenciosamente en cuanto a lo que permiten.

El precio de esa comodidad es la precisión. Una forma constituye un contrato fiable solo si coincide con lo que realmente aplica la biblioteca, y eso incluye ser honesto respecto a dónde deja de aplicarse el ámbito del inquilino de nivel superior. El comportamiento descrito refleja la implementación tal como la prueban sus mantenedores en el momento de redactar este texto, y no una convención establecida por la comunidad; por lo tanto, confirme los detalles en la documentación de su versión. Si una solicitud protegida ya está fallando y desea saber por qué, comience con nuestro modelo de diagnóstico basado en fases para los fallos de prisma-guard.

Pregunta uno: ¿quién es el propietario de cada valor?

Cada nodo de una forma entrega un valor ya sea al cliente o al servidor. El vocabulario es reducido; el ejemplo muestra las cuatro formas una junto a otra.

const values = {
  clientChooses: true,
  serverChooses: 'published',
  serverChoosesTrue: force(true),
  clientChoosesWithinRule: (base) => base.min(1).max(120),
}

Lea atentamente las indicaciones:

  • Un true sin más no es el valor booleano. Es un indicador que significa “el cliente decide”.
  • Un literal como 'published' pertenece al servidor.
  • force(value) también pertenece al servidor. Lo necesita cuando el valor que desea fijar es literalmente true, ya que un true sin más se interpretaría como el indicador del cliente.
  • Una función de refinamiento deja la decisión en manos del cliente, pero reemplaza la cadena Zod generada para el campo por la regla que se escribe directamente.

La ubicación modifica el significado de “force”

Las palabras son simples; lo que sorprende a la gente es su ubicación. En una forma de where, un escalar forzado se coloca dentro de un objeto operador. El filtro de eventos a continuación fija el estado de publicación mientras permite al cliente buscar títulos:

const eventShape = {
  where: {
    isPublished: { equals: force(true) },
    title: { contains: true },
  },
}

Cuando todo un predicado escalar en la raíz de where se fuerza, la biblioteca trata con flexibilidad la entrada del cliente en ese campo. Ya sea que el usuario repita el valor fijado, envíe uno diferente o agregue otro operador, el predicado del servidor tiene prioridad y la solicitud no es rechazada solo por mencionar ese campo. Eso no es lo mismo que un valor predeterminado. Un valor predeterminado se descarta cuando el cliente proporciona algo; un valor forzado nunca se descarta.

En todos los demás casos, las reglas son estrictas. Imagine un modificador forzado junto a un operador controlado por el cliente, un campo fijado dentro de un filtro de relación, un filtro fijado en una inclusión anidada o un valor fijado en los datos de mutación. En todos estos casos, la clave desaparece del esquema contra el cual se valida al cliente, por lo que enviarla hace que la validación falle incluso si el valor coincide exactamente.

Esto es importante para el código del cliente. Es posible que el cliente tenga que eliminar por completo un campo de escritura propiedad del servidor. “Arreglarlo” con el valor del servidor sigue fallando, porque la verificación se refiere a quién posee la clave, no a si los valores son iguales.

Los valores por defecto, las excepciones y los ajustes también transfieren la propiedad

Los valores por defecto y las maneras de manejar errores merecen la misma atención. En un campo controlado por el cliente, un valor por defecto mantiene su comportamiento normal cuando se omite el valor, y una maniobra de manejo de errores puede reemplazar una entrada inválida por un valor alternativo en lugar de rechazar la solicitud. Las operaciones de creación añaden un matiz adicional: una directiva de valor por defecto o de manejo de errores generada por el generador incluso puede completar un campo obligatorio que la estructura no menciona, y el valor generado se inserta como si el servidor lo hubiera establecido. Por lo tanto, el lugar donde se coloca un valor por defecto determina quién es el responsable de dicho valor, así que solo confíe en la sustitución cuando lo exija el contrato de la API.

Las mejoras ocultan una trampa relacionada. No extienden la cadena generada en su nodo; la reemplazan. Por lo tanto, su función debe volver a especificar cada restricción de la que depende el endpoint, no solo el mínimo o máximo adicional que pretendía agregar. Esto es especialmente importante cuando el tipo de Prisma es más flexible que la regla del producto, por ejemplo, una columna de texto que nunca debe quedar vacía.

Actualizaciones forzadas y disyunciones forzadas

Los datos de actualización fijados se escriben en cada llamada que coincida. Supongamos que una estructura de actualización exige status: 'active': cualquier actualización a través de ella, incluso una que solo modifique un título, también restablece el estado. Si una operación debe dejar un campo sin cambios, mantenga ese campo fuera de la estructura.

La estructura booleana agrega una regla más. Una condición forzada colocada bajo OR es elevada a condición obligatoria en el nivel raíz. Como resultado, una forma no tiene manera de expresar una verdadera regla de tipo “o” gestionada por el servidor, como los registros que pertenecen a esta cuenta o aquellos que fueron compartidos con este grupo. Reglas de este tipo requieren una consulta escrita en el servidor, una columna de ámbito única que capture la política, o su aplicación directa en la propia base de datos.

Por lo tanto, la primera pregunta de revisión es más precisa que “¿está esto validado?”. Hay que preguntar quién decide cada valor, dónde se aplica cada fuerza, y qué árbol de argumentos llega realmente a Prisma.

Pregunta dos: ¿qué puede exponer una respuesta?

El filtrado y la proyección responden a preguntas diferentes. Un filtro de inquilino decide qué registros califican. Una proyección determina cuáles de sus campos y relaciones se devuelven. Se necesitan ambos, y verificar uno no proporciona información alguna sobre el otro.

En las lecturas protegidas, un select o include declarado en la estructura funciona al mismo tiempo como lista de permisos y como solución alternativa. Los clientes pueden solicitar un árbol más restringido, pero nada más allá de él; cuando no envían ninguna proyección, el delegado protegido utiliza la declarada.

La estructura de ticket a continuación permite a un cliente obtener un ticket por su ID, devuelve un conjunto fijo de campos raíz y expone una lista anidada de notas del cliente con su propio filtro, campos y tamaño de página.

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 },
    },
  },
}

Ese árbol realiza cuatro tareas separadas:

  • Los campos raíz que no están listados permanecen inaccesibles.
  • Solo se pueden devolver los campos de nota que están enumerados.
  • Las notas siempre están restringidas a la cuenta del llamante.
  • Una única respuesta puede contener como máximo 20 notas, y 10 si el cliente no especifica lo contrario.
  • Las relaciones anidadas quedan fuera del alcance automático

    El filtro de notas se ha diseñado intencionadamente de esta manera. El alcance automático del inquilino solo se aplica a las operaciones soportadas en la raíz de la consulta; no analiza las relaciones obtenidas mediante select o include ni agrega condiciones allí. Olvídese de los where anidados, ya que un ticket con el alcance definido correctamente aún puede devolver notas que pertenezcan a otra cuenta.

    Las relaciones many-to-many aceptan un where anidado, su propia proyección y un límite de filas. Las relaciones one-to-sensitive no pueden utilizar un where anidado de Prisma, por lo que se necesita otra estrategia: excluir la relación, mostrar solo los campos escalares inocuos de ella, cargarla mediante su propia consulta protegida a nivel raíz o protegerla con una política de base de datos.

    Elegir un estilo de proyección

    Elige la forma de proyección según su comportamiento a medida que el esquema crece:

    • Con select, una columna añadida posteriormente al modelo permanece oculta hasta que alguien la revisa y la incluye, lo que la convierte en la opción más segura para endpoints públicos.
    • Con include, obtienes las relaciones, pero el modelo raíz sigue devolviendo todas sus columnas escalares, por lo que los datos sensibles del modelo raíz permanecen visibles.
  • Con omit, se mantiene una lista de exclusiones, y su disponibilidad depende del generador y de la versión de Prisma.
  • Estas son alternativas, no capas. Nunca se deben combinar select, include y omit en el mismo nivel del árbol.

    Cuentas, valores por defecto anidados y respuestas de mutación

    El recuento de una relación es un nodo de proyección separado, y no un resumen vinculado a la relación que se cargó. Si una relación de tipo many-to-many devuelta se filtra por cuenta, su _count debe incluir el mismo filtro siempre que los clientes asuman que ambos números describen las mismas filas. Pruebe la condición de la relación generada y la condición de recuento de forma independiente.

    Los valores predeterminados de la proyección anidada se derivan de la estructura declarada. Si un cliente solicita una relación declarada con abreviatura booleana, dicha relación puede mantener su filtro fijo y su límite de página. Sin embargo, los permisos otorgados al cliente no completan las opciones faltantes: permitir el ordenamiento por un campo no proporciona la dirección que el cliente omitió. En las pruebas se deben cubrir tres casos: una proyección raíz omitida, una proyección anidada explícita y abreviaturas de relación, en lugar de asumir que demostrar uno implica los demás.

    Las mutaciones son intencionalmente asimétricas. Una forma de mutación puede verificar una proyección que solicite el cliente, pero a menos que esté activada la opción enforceProjection, un cliente que no solicite nada no recibirá ninguna proyección estructurada y se aplicará el comportamiento habitual de Prisma. Active esta opción siempre que las respuestas de mutación deban limitarse a los campos declarados.

    Finalmente, asegúrese de que cada prueba ejecute la fase que afirma probar. Llamar a guard.query(...).parse() es una buena forma de verificar los filtros y límites de lectura, pero las proyecciones de lectura por defecto solo se muestran una vez que se ejecuta el delegado protegido, y lo mismo ocurre con las proyecciones de mutación, que requieren el camino de mutación protegido.

    Por lo tanto, la segunda pregunta no es “¿hay un select?”, sino cuáles campos raíz, campos relacionados, filas relacionadas y conteos podría recibir cada variante de llamada.

    Pregunta tres: ¿qué escrituras se pueden ejecutar?

    Los puntos de extremo de escritura generados aceptan argumentos en el propio formato de Prisma. La estructura de los datos determina qué valores proporciona el cliente, cuáles los suministra el servidor y si es posible realizar operaciones anidadas.

    Las estructuras de creación deben incluir todos los campos requeridos

    Una estructura de creación es rechazada cuando algún campo requerido no tiene fuente. Las fuentes aceptables incluyen:

    • el cliente, cuando el campo está marcado como controlado por el cliente
    • el servidor, a través de un valor literal o mediante force
    • un valor por defecto definido en el esquema de Prisma
    • un valor por defecto o manejo de errores compatible con Zod
    • inyección de alcance de la clave foránea del inquilino
    • una escritura de relación permitida que la llene

    El ejemplo de suscripción permite al cliente elegir una clave y un plan externos, limita la cantidad de usuarios con una regla entera, fija el estado y la versión iniciales en el servidor, y devuelve una proyección restringida.

    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,
      },
    }
    

    Observe lo que falta: la clave foránea del inquilino. Nunca debería ser algo que el cliente establezca al crear. La raíz del alcance proviene del contexto de la solicitud confiable, y la extensión agrega ella misma la clave foránea correspondiente para las escrituras a nivel de raíz que soporta.

    Operaciones masivas y creaciones por lotes

    Las mutaciones masivas deben superar dos verificaciones. Primero, updateMany, sus variantes de retorno y deleteMany necesitan una estructura que permita declarar un vocabulario where. Segundo, la solicitud recibida debe generar una condición real; faltar un cuerpo, tener un objeto operador vacío o contar con un ramo lógico que coincida con todo no cumple los requisitos, lo cual evita cambios accidentales en toda la tabla.

    La operación por lotes protegida exige un array y no envuelve silenciosamente un objeto individual. Si las operaciones por lotes de retorno y opciones como el omitir duplicados siguen funcionando depende del proveedor de tu base de datos.

    Upsert: tres estructuras

    En realidad, la estructura de un upsert consta de tres partes: un selector único, un payload para la creación y un payload para las actualizaciones.

    const upsertSubscriptionShape = {
      where: {
        externalKey: true,
      },
      create: {
        externalKey: true,
        planId: true,
        status: 'trialing',
        seatCount: true,
        version: 0,
      },
      update: {
        planId: true,
        seatCount: true,
      },
    }
    

    Cada campo obligatorio debe incluirse en el payload de creación, mientras que el payload de actualización puede omitir cualquier elemento. La fuerza aplicada bajo create solo establece el valor cuando se inserta una fila, mientras que la misma fuerza bajo update lo sobrescribe cada vez que existe una fila coincidente. En el ejemplo, status y version se especifican en la operación de creación y se omiten intencionadamente en la actualización, de modo que las suscripciones existentes mantienen su estado actual.

    Las mutaciones únicas adoptan la forma simple de selector único de Prisma. Los objetos operador se utilizan para filtrado ordinario y no tienen lugar en una estructura where única a nivel raíz.

    Contratos de escritura y retorno de relaciones

    Relation escribe una solicitud para que se tome una decisión separada sobre la propiedad. Una forma puede permitir connect, limitar las teclas de selector que puede usar un cliente y rechazar cualquier operación anidada que no esté enumerada. Sin embargo, nada de eso extiende el ámbito del inquilino al selector. Cuando sea importante determinar qué registro se conectará, el código de su aplicación o la base de datos deben verificar que al llamante se le permita usarlo.

    Los contratos de retorno también varían según el método. Los métodos por lotes que solo devuelven un conteo, createMany, updateMany y deleteMany, responden con un payload de conteo y rechazan las proyecciones. Sus equivalentes que devuelven datos pueden aceptar proyecciones si tanto Prisma como su proveedor lo permiten. Si una configuración del generador cambia una ruta a un método que devuelve datos, revise la nueva respuesta como un cambio en la API en lugar de descartarla como un detalle de implementación.

    La tercera pregunta no es “¿está activado este método?”. Pregunte qué hace disponible el método: campos de datos, formularios de selección, condiciones masivas, operaciones de relación y campos de respuesta.

    Estratificación de la aplicación de reglas

    Las formas funcionan mejor cuando sus límites se documentan junto con los demás mecanismos. Cada capa tiene su propia función:

    • La solicitud autenticada proporciona un contexto en el que se puede confiar.
    • Los mapeos de alcance añaden condiciones específicas para cada usuario a las operaciones de nivel raíz soportadas.
    • Las variantes del llamante determinan qué forma declarada se aplica a un público determinado.
    • Los ganchos ejecutan verificaciones limitadas de la aplicación que permiten rechazar solicitudes.
    • Los controladores manuales se encargan de las consultas que las formas no pueden describir.
    • La política de la base de datos protege las invariantes que deben mantenerse en cualquier ruta de acceso.

    Ninguno de estos debería pretender hacer el trabajo de otro. Si está decidiendo qué rutas generar y cuáles escribir a mano, nuestro artículo sobre equilibrar la generación de Prisma CRUD con un control deliberado de rutas aborda esa decisión.

    Lista de revisión y pruebas

    Cuando una solicitud de pull añade un punto de extremo generado, revíselo con tres preguntas:

    1. ¿Quién decide cada valor que acepta el punto de extremo?
    2. ¿Qué campos y relaciones puede contener una respuesta?
    3. ¿Qué mutaciones y formularios de selector puede ejecutar quien lo llama?

    Luego, oriente cada prueba hacia la afirmación específica que debe demostrar. Analice los argumentos de lectura para confirmar el comportamiento del filtro. Ejecute delegados protegidos para verificar las proyecciones y escrituras. Examine la ruta de extensión para confirmar la inyección de ámbito. Utilice fixtures de base de datos multiinquilino cuando la afirmación se refiere a qué filas se devuelven, y pruebas HTTP cuando se trata del transporte o de las rutas generadas.

    Dé la misma importancia a las pruebas negativas. Para cada público objetivo, intente leer un campo raíz que no esté listado, leer un campo anidado sensible, exceder un límite de relación y ejecutar una operación de escritura que la estructura no permita. Las pruebas positivas demuestran que la solicitud prevista tiene éxito; las pruebas negativas confirman que los límites siguen vigentes tras cambios en el modelo o en el generador.

    Conclusión

    Una estructura recibe el nombre de “contracto” únicamente cuando está claro tanto qué permite como qué no puede hacer cumplir. Al analizar por separado la propiedad, la proyección y las escrituras, se detectan tempranamente las deficiencias típicas: fuerzas laxas en el nivel raíz, relaciones anidadas sin delimitación, conteos que ignoran los filtros de relación y conexiones que eluden el ámbito del inquilino. Una vez que esas deficiencias se asignan a ganchos, manejadores o políticas de base de datos, la generación de código puede eliminar códigos repetitivos relacionados con los límites sin convertir el lenguaje de consultas de Prisma en una API pública que nadie pretendía publicar. Para conocer la sintaxis completa, los mensajes de error exactos, el comportamiento específico por versión y laboratorios reproducibles, consulte la guía de campo de prisma-generator-express.

    Lecturas relacionadas