Inicio / Artículos / Depuración de fallos en Prisma Guard: un modelo de diagnóstico basado en fases

Depuración de fallos en Prisma Guard: un modelo de diagnóstico basado en fases

Aprenda a diagnosticar los fallos de la API Prisma generados, asociando los errores a la fase exacta —configuración, selección del llamante, validación o respuesta— que los causa.

2660 palabras

Comience a solucionar el problema identificando qué fase es la responsable real del fallo.

Las APIs generadas pueden fallar en varios puntos distintos.

Nota de alcance: las fases de fallo descritas aquí provienen de la documentación del proyecto y de entornos de reproducción específicos, no de estadísticas generales de uso en una gran base de usuarios.

Un router puede rechazar su propia configuración antes incluso de atender una sola solicitud. Un mecanismo de validación puede rechazar una estructura mal formada en el momento en que se crea. El enrutamiento de llamadas puede fallar antes de que se ejecute siquiera un complemento específico para una variante. La validación de solicitudes puede rechazar un cuerpo individual. Una operación con alcance definido puede fallar simplemente porque el contexto en el que depende no está presente.

Más allá de estos, existe una categoría más complicada: la solicitud tiene éxito técnicamente, pero los argumentos de Prisma que se generan, o la semántica de la respuesta, difieren de lo que asumía la lógica de la aplicación.

Cada una de estas categorías requiere una solución diferente y un tipo distinto de prueba. Analizar toda la cadena de errores resulta mucho menos eficaz que hacerse dos preguntas: ¿cuándo apareció por primera vez este comportamiento? ¿Qué capa es capaz de observarlo?

Comience con un mapa de fases

Una solicitud de Prisma generada pasa por varios límites distintos en su camino hacia la ejecución:

router construction
  caller resolution
    operation before-hooks
      variant before-hooks
        guard shape construction
          request validation
            Prisma argument execution
              response transport

El orden preciso de las tareas relacionadas con las formas puede variar según si una forma es estática o depende del contexto en tiempo de ejecución, pero este desglose diagnóstico sigue siendo un modelo mental útil.

Los fallos al iniciar indican problemas en los descriptores de ruta. Los fallos en la etapa del llamante apuntan a la lógica de selección de variantes. Errores como Invalid query y Invalid data señalan una discrepancia entre el cuerpo de la solicitud y la estructura declarada. Los fallos en las políticas indican que falta un contexto de confianza. Y cuando una solicitud tiene éxito pero el resultado es inesperado, es necesario ignorar por completo el código de estado.

Tenga en cuenta que el texto de los errores está vinculado a versiones específicas. Los ejemplos centrados en las protecciones mencionados aquí se generaron con una combinación fija: prisma-guard en la versión 1.33.0, junto con Zod 4.4.3 y Prisma 6.19.3. Los ejemplos relacionados con lecturas basadas en HTTP, por su parte, dependen de un conjunto separado: prisma-generator-express 1.64.4 ejecutándose en Node 22.14.0 contra PostgreSQL 16.6.

Considere la redacción exacta de un error como evidencia específica para esa combinación de versiones. Trate la fase y la causa subyacente como el modelo de depuración que seguirá siendo útil más adelante.

Antes de una solicitud: la configuración no puede constituir un contrato

La construcción del enrutador es responsable de validar los descriptores de operación antes de que ocurra cualquier otra cosa.

No se permite que una operación configure tanto shape como variants al mismo tiempo. Un mapa de variantes no puede quedar vacío. Cada descriptor de variante debe incluir una forma. No se permite que las claves de formas reservadas sirvan también como nombres de llamantes.

Estos son fallos por naturaleza que ocurren durante el despliegue. Si el sistema los detectara y siguiera funcionando con un enrutador parcialmente configurado, borraría silenciosamente los límites que la aplicación debía respetar.

Una operación que no define ni shape ni variants representa una situación completamente diferente: es técnicamente válida y llama directamente a Prisma sin aplicar ninguna restricción. Si eso es aceptable debe ser una decisión explícita tomada durante la revisión de las rutas, y no un accidente.

La construcción de shapes conlleva su propio conjunto de condiciones de fallo. Los combinadores vacíos, las proyecciones vacías, los predicados forzados contradictorios, las shapes creadas de forma incompleta, las estructuras de upsert mal formadas y los métodos en lote que carecen de una shape where son rechazados desde el principio, antes de que los datos proporcionados por el cliente tengan la oportunidad de interactuar con ellos de forma insegura.

Una reproducción mínima es la más útil cuando mantiene la construcción de shapes separada de la capa de transporte:

const query = guard.query('Plant', 'findMany', {
  where: {
    name: { contains: true },
  },
  take: { max: 50, default: 20 },
})
const args = query.parse({
  where: {
    name: { contains: 'fern' },
  },
})

Este camino aislado funciona bien para probar la filtración de lectura, los argumentos de ordenamiento y paginación, así como la mayoría de los errores en la construcción de estructuras. Lo que no hace es ejecutarse realmente contra Prisma, aplicar una proyección de lectura a nivel de delegado ni representar cómo se comportan las mutaciones en la práctica.

Cualquiera que sea la solución, debe incluirse en la configuración del lado servidor. Ningún ajuste en el payload de la solicitud podrá reparar una estructura que desde un principio es defectuosa desde el punto de vista estructural.

Antes del manejador: falló la selección del llamante

Cuando se utilizan estructuras y variantes nombradas, existe una fase adicional de enrutamiento que se ejecuta antes de que la solicitud llegue al manejador generado.

Se considera que un llamante está desaparecido cuando el mapa de variantes no tiene una entrada default. Se considera que un llamante es desconocido cuando nada coincide con él: ni clave exacta, ni patrón parametrizado y ni valor por defecto. Dos patrones parametrizados que se superponen no se resuelven según el orden de declaración; el sistema trata la situación como ambigua y falla en su lugar.

Los datos de identificación del llamante se transmiten por un canal separado del cuerpo de la solicitud de Prisma. Intentar incluirlos dentro de los propios argumentos de la solicitud es rechazado.

Para contratos orientados al público, utilizar un encabezado como selector intencional del llamante puede ser una elección de diseño razonable. Pero para variantes con privilegios, la selección debe provenir de una lógica autenticada dentro de resolveVariant, y no de la entrada del cliente. Asignar un nombre personalizado a un encabezado no hace que su valor sea fiable.

Los fallos de enrutamiento ocurren después de que se ejecuten los before-hooks a nivel de operación, pero antes de que se ejecuten los hooks específicos de cada variante. Ese orden explica un comportamiento sutil: la lógica de autenticación a nivel de operación sigue ejecutándose incluso cuando no hay ninguna variante del llamante que coincida, mientras que los hooks específicos del llamante nunca se activan en ese caso.

La solución adecuada no es automáticamente “solo añadir un valor por defecto”. Un llamante por defecto acepta de forma silenciosa valores faltantes, en blanco o que no coincidan. Solo añada uno si ese comportamiento de fallback es realmente aceptable en los tres escenarios mencionados.

Durante la validación: la solicitud superó su límite declarado

Los errores de lectura verificados en esta configuración apuntan al camino exacto del argumento que los provocó.

Un campo no reconocido dentro de where significa que dicho campo no forma parte de la estructura del filtro. Un campo no reconocido dentro de select indica que la solicitud intenta ampliar la proyección más allá de lo permitido. Un valor skip rechazado significa que la omisión de páginas nunca se activó para esa estructura. Un error en take puede deberse a que el valor solicitado superó su máximo configurado, o a que llegó con un tipo escalar completamente incorrecto.

Los ayudantes GET generados son importantes aquí porque los argumentos en formato Prisma no se convierten siempre de la misma manera cuando se crean manualmente a partir de cadenas de consulta. Los valores numéricos de filtro y las fechas suelen convertirse correctamente en los lugares donde está soportado, pero los valores booleanos y los valores de paginación pasados como cadenas pueden no convertirse adecuadamente. La opción más segura es utilizar el codificador generado para las solicitudes GET, o recurrir al JSON nativo a través de la versión de lectura basada en POST.

Por el contrario, la escritura de validaciones sigue una estructura específica para cada método de Prisma. Las operaciones de creación reciben un campo data. Las operaciones de actualización reciben tanto where como data. Las operaciones de inserción o actualización reciben where, create y update. Una llamada de creación por lotes protegida espera que su entrada sea un array.

Las operaciones en bloque pueden fallar en dos niveles distintos. Si falta where en la propia estructura, se trata de un problema durante la construcción. Si el cuerpo de una solicitud en tiempo de ejecución técnicamente contiene un where pero no corresponde a ninguna condición real del lado del cliente, entonces es un problema en el momento de la solicitud.

Los errores de política forman nuevamente su propia categoría. La falta de una raíz de ámbito o la ausencia de contexto para una estructura que depende del contexto en tiempo de ejecución indican que alguna información confiable simplemente no está presente. Mantener el comportamiento ante la falta de ámbito en modo de error es lo que impide que un contexto ausente se convierta silenciosamente en una consulta de nivel superior sin filtrado.

La costumbre que vale la pena desarrollar aquí es conservar exactamente el camino por donde ocurrió un fallo. Decir “recibí un 400 del guardián” no proporciona casi ninguna información útil. En cambio, indicar “el proceso de lectura intentó ejecutar include.plants.take más allá del límite anidado configurado” apunta directamente a un nodo específico en el contrato.

Después de que el guardián da su aprobación: un estado 200 sigue ocultando riesgos reales

Una respuesta HTTP exitosa solo indica que la ruta se ejecutó hasta el final. No dice nada sobre si el valor enviado realmente fue respetado, si se ejecutó alguna condición dentro del ramo que se suponía que ocurriría, o si la respuesta utilizó la proyección esperada por defecto.

Tomemos un predicado de nivel superior completamente forzado: este anula todo lo que envíe el cliente sin dejar ninguna señal visible de ello. Si una forma fija isPublished en true, un cliente que envíe false seguirá recibiendo una respuesta de éxito, mientras que la consulta que se ejecuta realmente mantiene el valor forzado true en su interior.

Los otros campos forzados se comportan de manera opuesta y rechazan directamente los valores proporcionados por el cliente en lugar de anularlos silenciosamente. Dado que la fuerzación puede comportarse de forma inconsistente dependiendo del lugar donde se aplique, sus pruebas deben verificar quién es el propietario real de cada argumento en lugar de asumir que una instancia de force() se aplica a todos los campos.

Forzar la ejecución se vuelve aún más complicado dentro de una cláusula OR. Una condición forzada colocada allí es elevada al nivel superior y se convierte en una restricción obligatoria. Por lo tanto, una estructura que parece expresar “o la condición del cliente o la del servidor” en realidad puede ejecutarse como la condición del cliente combinada con el predicado forzado mediante lógica AND. Si realmente se necesita una alternativa controlada por el servidor, es necesario utilizar una consulta dedicada para ese propósito, o bien imponerla en la capa de políticas de la base de datos.

La proyección de respuestas introduce su propia divergencia sutil. Cuando un cliente omite una proyección en una lectura protegida, se aplica la proyección predeterminada de la estructura, pero esa sustitución ocurre en el momento en que se ejecuta efectivamente la delegación, no cuando se ejecuta guard.query().parse().

Las mutaciones no siguen la misma regla. Si enforceProjection no está configurado, un cliente que omite una proyección en una mutación no recibe ninguna cláusula select insertada, lo que significa que en su lugar se aplica el comportamiento habitual de Prisma sin proyección.

La aplicación de restricciones en el ámbito anidado es otro punto donde es fácil suponer que existe más cobertura de la que realmente hay. El ámbito automático solo intercepta las operaciones de nivel superior que soporta explícitamente. No accede a las relaciones que se incluyen mediante una proyección ni las filtra de forma recursiva. Además, la raíz del ámbito en sí nunca es filtrada por su propio marcador, y cualquier SQL bruto que se ejecute ignora completamente la capa de aplicación de restricciones de la extensión.

Ninguno de estos comportamientos se manifiesta si solo se verifica el código de estado.

Elija el mecanismo de lectura correcto antes de confiar en la estructura de la respuesta

La capa generada incluye tres mecanismos distintos para entregar los resultados de lectura: respuestas paginadas, transporte basado en POST y eventos enviados por el servidor basados en Express.

findManyPaginated devuelve una estructura externa fija:

type PaginatedResult<T> = {
  data: T[]
  total: number
  hasMore: boolean
}

La bandera hasMore es fiable específicamente para la paginación por offset forward combinada con un valor positivo de take. Si se utiliza paginación por cursor o un valor negativo de take, aún se puede obtener un valor booleano, pero ya no ofrece esa misma garantía. Un take de 0 devuelve cero filas y una bandera de continuación falsa, mientras que el recuento total permanece intacto.

El recuento total sigue un camino lógico completamente diferente. El conteo distinto respeta un límite configurado. Una fuente de recuento precalculada solo se utiliza cuando la solicitud está sin filtrar, sin protección y no es distinta. Cualquier filtro dinámico, cláusula de distinción o mecanismo de protección obliga a recurrir a un conteo en tiempo real calculado al momento de la solicitud.

Ese recurso alternativo mantiene la corrección, pero cambia tanto el costo de la operación como el origen del número. Considere la semántica del total como un tema separado de la semántica del recorte de filas.

Las lecturas basadas en POST existen para manejar el tamaño y la codificación de los datos, no para ampliar lo que puede expresar el lenguaje de consultas:

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

Envíe el cuerpo como JSON nativo. Se espera que las versiones GET y POST de la misma ruta apliquen un contrato de protección idéntico. Si un complemento reescribe el cuerpo de la solicitud, esa equivalencia puede romperse, ya que la ruta GET lee los parámetros de consulta ya analizados en lugar del cuerpo JSON.

Los eventos enviados por el servidor se activan cuando llega la data, no por qué tipo de data es. Este mecanismo solo tiene sentido si el cliente implementa realmente un manejo para eventos de progreso, eventos de éxito final, eventos de fallo final y una ruta de respaldo.

{"type":"progress","stage":"relations"}
{"type":"field","field":"summary","data":{"total":6}}
{"type":"result","data":{"summary":{"total":6},"deliveries":[]}}

Los eventos SSE preparados manualmente son consultas a nivel de aplicación que se escriben uno mismo, y necesitan un manejo explícito de protecciones al igual que cualquier otra cosa. La inclusión automática solo cubre las estructuras de relaciones que están documentadas y dentro de los límites del planificador; todo lo que está fuera de eso recurre al comportamiento de fallback configurado. Además, los hooks generados posteriormente no constituyen un mecanismo garantizado para limpiar una secuencia SSE.

En conjunto, una lectura “éxito” aún puede ser incorrecta por varias razones independientes: una bandera de continuación poco fiable, una interpretación errónea del origen del conteo, un comportamiento de hook específico del transporte, o una consulta preparada que nunca fue protegida.

Dirija cada prueba hacia la capa que realmente pueda verificar

Ninguna solicitud de extremo a extremo puede validar todas las capas al mismo tiempo.

Utilice el analizador al probar la validación de datos o la estructura de fusión forzada. Acuda al delegado protegido cuando se trate de la proyección en tiempo de ejecución o de los argumentos finales de mutación. Empiece por la ruta de operaciones de la extensión al verificar si realmente tuvo lugar la inyección automática de ámbito.

Un mecanismo independiente para capturar argumentos le permite inspeccionar los argumentos finales de mutación sin acceder a una base de datos, pero solo si conecta la extensión de protección a un delegado que devuelva realmente los argumentos recibidos. Crear un objeto falso desconectado no demuestra nada. Este tipo de mecanismo le indica qué argumentos se emitieron, pero no qué filas devolvería realmente una base de datos.

Para consultas sobre resultados a nivel de inquilino, propiedad de relaciones, comportamiento transaccional, totales distintos o peculiaridades específicas del proveedor, se necesitan pruebas respaldadas por una base de datos. Incluya al menos a dos inquilinos con filas que difieran claramente entre sí para que cualquier fuga de datos sea evidente si ocurre.

Para dudas relacionadas con la generación de rutas, serialización, ejecución de ganchos, equivalencia entre GET/POST, formato de las respuestas de paginación o secuenciación de eventos SSE, utilice pruebas a nivel HTTP.

Mantenga al menos una prueba de contrato con protecciones activadas, incluso si su suite de pruebas end-to-end en el navegador funciona en un modo que desactiva la validación de esas protecciones. Una prueba en el navegador que pase en un modo relajado no demuestra nada sobre lo que rechazará la producción, ya que la capa de control fue eliminada para dicha prueba.

Escribe cada prueba de regresión en el nivel más bajo capaz de demostrar la afirmación específica que hace. Las pruebas más específicas significan que, cuando algo falla posteriormente, los puntos de fallo se localizan en la fase correspondiente en lugar de obligarte a investigar todo el camino de la solicitud desde cero.

Trabaja el fallo en una sola dirección

Una secuencia corta y repetible evita que tengas que adivinar las soluciones:

  1. Determina si se trata de un fallo al iniciar, un fallo en el momento de la solicitud o de una respuesta exitosa que te sorprendió.
  2. Identifica qué fase es la responsable: el enrutador, la resolución de llamadas, la estructura, la política, la ejecución de Prisma o el transporte.
  3. Reduce la reproducción del fallo a una sola operación, una sola estructura y un solo cuerpo de solicitud.
  4. Inspecciona el argumento en la capa más cercana al lugar donde se origina el comportamiento.
  • Solo agregue la ejecución en la base de datos o por HTTP a la prueba si la afirmación específica realmente depende de ella.
  • Solo compare los mensajes de error exactos con la versión de dependencia que haya fijado.
  • Las APIs generadas se vuelven mucho más fáciles de comprender una vez que mantiene sus fases separadas entre sí. Los errores de configuración deben manifestarse antes de que se sirva cualquier tráfico. Las solicitudes que violen una regla deben indicar con exactitud qué parte del contrato han infringido. Y una respuesta exitosa debe verificarse en función de los argumentos que realmente emitió y de las semánticas de transporte documentadas para ella, nunca únicamente en función del código de estado.

    Lecturas relacionadas

  • Reducir la carga de Prisma y PostgreSQL antes de pagar por una base de datos más grande — Quince técnicas prácticas, desde EXPLAIN ANALYZE e índices compuestos hasta soluciones para el problema N+1, contadores, pooling y vacuum, para disminuir las operaciones de la base de datos Prisma.
  • Revisión de las formas prisma-guard: Propiedad, proyección y contratos de escritura — Aprenda a revisar las formas prisma-guard como contratos de API al preguntarse quién es el propietario de cada valor, qué datos pueden formar parte de una respuesta y qué escrituras puede ejecutar un endpoint generado.