Examen des formes Prisma-Guard : propriété, projection et contrats d’écriture
Apprenez à examiner les formes Prisma Guard en tant que contrats API en vous demandant qui possède chaque valeur, quels données peuvent figurer dans une réponse, et quelles écritures un point de terminaison généré peut exécuter.
Sources: Define Every Prisma Endpoint’s Inputs and Outputs in One Place
Le langage d’arguments de Prisma est expressif, et c’est précisément ce dont on a besoin dans du code serveur fiable. Cependant, une fois que la même structure d’arguments est acceptée via HTTP, chaque fonctionnalité de ce langage devient une décision liée au produit : ce que l’appelant peut écrire, quels filtres il peut choisir, ce que le serveur doit impérativement valider, quelles relations peuvent être renvoyées, et combien de lignes enfants une réponse peut contenir. Les modèles prisma-guard vous permettent d’enregistrer ces règles directement à côté de l’opération Prisma. Ce guide montre comment examiner un modèle en tant que contrat compact pour le contrôle, le filtrage, la projection et la mutation, à l’aide de trois questions que vous pouvez poser pour toute demande de mise à jour ajoutant une extrémité générée.
Pourquoi un modèle va au-delà de la simple validation
Le prix de cette commodité, c’est la précision. Une forme ne constitue un contrat fiable que si elle correspond à ce que la bibliothèque applique réellement, y compris en étant honnête quant aux points où la portée des locataires de niveau supérieur cesse d’être applicable. Le comportement décrit ci-dessous reflète l’implémentation telle que ses mainteneurs l’ont testée au moment de la rédaction, et non une convention communautaire établie ; veuillez donc vérifier les détails dans la documentation de votre version. Si une requête protégée échoue déjà et que vous souhaitez en connaître la raison, commencez par notre modèle de diagnostic basé sur les phases pour les échecs prisma-guard.
Première question : qui est responsable de chaque valeur ?
Chaque nœud d’une forme transmet une valeur soit au client, soit au serveur. Le vocabulaire est restreint ; l’exemple montre les quatre formes côte à côte.
const values = {
clientChooses: true,
serverChooses: 'published',
serverChoosesTrue: force(true),
clientChoosesWithinRule: (base) => base.min(1).max(120),
}
Lisez attentivement les explications :
- Un
truesans contexte n’est pas la valeur booléenne. C’est un signal indiquant que « c’est le client qui décide ». - Un littéral comme
'published'appartient au serveur. force(value)appartient également au serveur. Vous l’utilisez lorsque la valeur que vous souhaitez imposer est littéralementtrue, car untruesans contexte serait interprété comme le signal du client.- Une fonction de précision laisse la décision au client, mais remplace la chaîne Zod générée pour le champ par la règle que vous écrivez directement.
Le contexte d’utilisation influence le sens de force
Les mots sont simples ; c’est leur emplacement qui surprend les gens. Dans une forme where, un scalaire forcé est placé à l’intérieur d’un objet opérateur. Le filtre d’événement ci-dessous fixe l’état de publication tout en permettant au client de rechercher des titres :
const eventShape = {
where: {
isPublished: { equals: force(true) },
title: { contains: true },
},
}
Lorsqu’un prédicat scalaire complet situé à la racine de where est forcé, la bibliothèque traite les entrées du client concernant ce champ avec tolérance. Que le demandeur répète la valeur fixée, envoie une autre ou ajoute un autre opérateur, le prédicat du serveur a la priorité et la requête n’est pas rejetée simplement parce que ce champ est mentionné. Ce n’est pas la même chose qu’une valeur par défaut. Une valeur par défaut est mise de côté lorsque le client fournit quelque chose ; un forçage, lui, ne l’est jamais.
Dans tous les autres cas, les règles sont strictes. Pensez à un modificateur forcé situé à côté d’un opérateur contrôlé par le client, à un champ fixé au sein d’un filtre de relation, à un filtre fixé sur une inclusion imbriquée, ou encore à une valeur fixée dans les données de mutation. Dans tous ces cas, la clé disparaît du schéma contre lequel le client est validé, ce qui fait échouer la validation même si la valeur correspond exactement.
Cela a une importance pour le code client. Un client peut être contraint d’omettre complètement un champ d’écriture appartenant au serveur. Le fait de le « corriger » en utilisant la valeur du serveur échoue néanmoins, car la vérification porte sur le propriétaire de la clé, et non sur l’égalité des valeurs.
Les valeurs par défaut, les gestionnaires d’erreurs et les ajustements transfèrent également la propriété
Les valeurs par défaut et les mécanismes de gestion des erreurs de Zod méritent une analyse similaire. Dans un champ contrôlé par le client, une valeur par défaut conserve son comportement normal lorsque la valeur n’est pas fournie, tandis qu’un mécanisme de gestion des erreurs peut remplacer une entrée invalide par une valeur de remplacement plutôt que de faire échouer la requête. Les opérations de création ajoutent une complexité supplémentaire : une directive de valeur par défaut ou de gestion des erreurs générée par le générateur peut même remplir un champ obligatoire qui n’est jamais mentionné dans la définition, et la valeur ainsi produite est injectée comme si le serveur l’avait définie lui-même. Le choix de l’endroit où placer une valeur par défaut détermine donc à qui appartient cette valeur ; il convient donc de recourir uniquement à la substitution lorsque le contrat API l’exige.
Les affinements cachent un piège similaire. Ils ne prolongent pas la chaîne générée à leur nœud ; ils la remplacent. Votre fonction doit donc réénoncer chaque contrainte sur laquelle dépend l’extrémité, et non seulement le minimum ou maximum supplémentaire que vous souhaitiez ajouter. Cela est particulièrement important lorsque le type Prisma est moins strict que la règle de produit, par exemple une colonne de chaîne qui ne doit jamais être vide.
Mises à jour forcées et disjonctions forcées
Les données de mise à jour fixées sont écrites à chaque appel correspondant. Supposons qu’une structure de mise à jour impose status: 'active' : toute mise à jour utilisant cette structure, même celle qui ne modifie que le titre, réinitialise également le statut. Si une opération doit laisser un champ inchangé, excluez ce champ de la structure.
La structure booléenne ajoute une règle supplémentaire. Une condition forcée placée sous OR est rehaussée au niveau de la racine pour devenir une condition obligatoire. Par conséquent, une entité n’a pas la possibilité d’exprimer une véritable règle de type « soit… soit » gérée par le serveur, comme par exemple des enregistrements appartenant à ce compte ou ayant été partagés avec ce groupe. De telles règles nécessitent une requête écrite sur le serveur, une seule colonne de délimitation permettant de capturer la politique en vigueur, ou encore une application des règles directement dans la base de données.
La première question d’évaluation est donc plus précise que « est-ce validé ? ». Il faut se demander qui décide de chaque valeur, où sont placées toutes les contraintes, et quel arbre d’arguments parvient réellement à Prisma.
Deuxième question : qu’est-ce qu’une réponse peut révéler ?
Le filtrage et la projection répondent à des questions différentes. Un filtre de locataire détermine quels enregistrements sont éligibles. Une projection décide quels de leurs champs et relations seront renvoyés. Vous avez besoin des deux, et vérifier l’un ne vous dit rien sur l’autre.
Pour les lectures protégées, un select ou include déclaré sur la structure sert à la fois de liste d’autorisation et de solution de repli. Les clients peuvent demander un arbre plus restreint mais rien au-delà, et lorsqu’ils ne fournissent aucune projection, le délégué protégé utilise celle qui a été déclarée.
La structure de ticket ci-dessous permet à un client de récupérer un ticket par ID, de retourner un ensemble fixe de champs racines, et d’exposer une liste imbriquée des notes du client avec son propre filtre, ses champs et sa taille de page.
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 },
},
},
}
Cet arbre effectue quatre tâches distinctes :
- Les champs racines qui ne sont pas listés restent indisponibles.
- Seuls les champs de notes listés peuvent être renvoyés.
Les relations imbriquées échappent au champ d’application automatique
Le filtre des notes a été conçu délibérément de cette manière. Le champ d’application automatique s’applique uniquement aux opérations prises en charge au niveau racine de la requête ; il ne prend pas en compte les relations récupérées via select ou include pour y ajouter des conditions. Oubliez les where imbriqués : un ticket ayant un champ d’application correct peut tout de même renvoyer des notes appartenant à un autre compte.
Les relations many-to-many acceptent un where imbriqué, leur propre projection ainsi qu’une limite de ligne. Les relations one-to-sensitive ne peuvent pas utiliser un where Prisma imbriqué, il vous faut donc une autre stratégie : omettre la relation, n’exposer que des champs scalaires inoffensifs provenant d’elle, la charger via sa propre requête de niveau racine protégée, ou la sécuriser avec une politique de base de données.
Choix du style de projection
Choisissez la forme de projection en fonction du comportement qu’elle aura à mesure que le schéma évolue :
- Avec
select, une colonne ajoutée ultérieurement au modèle reste cachée jusqu’à ce que quelqu’un la consulte et la liste, ce qui en fait le choix le plus sûr pour les points d’accès publics. - Avec
include, vous obtenez des relations, mais le modèle racine renvoie toujours toutes ses colonnes scalaires, de sorte que les données sensibles du niveau racine restent visibles.
omit, vous conservez une liste de refus, et sa disponibilité dépend de votre générateur ainsi que de la version de Prisma.Ce sont des alternatives, pas des niveaux hiérarchiques. Ne mélangez jamais select, include et omit au même niveau de l’arborescence.
Comptages, valeurs par défaut imbriquées et réponses aux mutations
Le comptage d’une relation est un nœud de projection distinct, et non un résumé lié à la relation que vous avez chargée. Si une relation de type many-to-many retournée est filtrée par compte, son _count doit intégrer le même filtre chaque fois que les clients supposent que ces deux valeurs décrivent les mêmes lignes. Testez indépendamment la condition de la relation générée et la condition de comptage.
Les paramètres par défaut des projections imbriquées proviennent de la structure déclarée. Si un client demande une relation spécifiée sous forme abrégée booléenne, cette relation peut conserver son filtre fixe et ses limites de page. Cependant, les permissions accordées au client ne complètent pas les choix manquants : autoriser le tri par un champ ne définit pas la direction que le client n’a pas indiquée. Couvrez trois cas dans les tests : une projection racine omise, une projection imbriquée explicite ainsi qu’une forme abrégée de relation, plutôt que d’assumer que l’un prouve le reste.
Les mutations sont intentionnellement asymétriques. Une forme de mutation peut vérifier une projection demandée par le client, mais à moins que l’option enforceProjection ne soit activée, un client qui ne demande rien ne reçoit aucune projection formatée et le comportement habituel de Prisma s’applique. Activez cette contrainte chaque fois que les réponses aux mutations doivent être limitées aux champs déclarés.
Finalement, assurez-vous que chaque test exécute bien la phase qu’il prétend tester. Appeler guard.query(...).parse() est un bon moyen de vérifier les filtres de lecture et les limites, mais les projections de lecture n’apparaissent que lorsque le délégué protégé s’exécute, et il en va de même pour les projections de mutation qui nécessitent le chemin de mutation protégé.
Ainsi, la deuxième question n’est pas « y a-t-il un select ? » Demandez quels champs racines, champs associés, lignes associées et comptages chaque variante d’appel pourrait recevoir.
Troisième question : quelles écritures peuvent être exécutées ?
Les points de terminaison d’écriture générés prennent des arguments dans le propre format de Prisma. La structure des données détermine quels valeurs fournit le client, lesquelles fournit le serveur, et si des opérations imbriquées sont possibles.
Les structures de création doivent prendre en compte tous les champs requis
Une structure de création est rejetée si un champ requis n’a pas de source. Les sources acceptables incluent :
- le client, lorsque le champ est marqué comme contrôlé par le client
- le serveur, via une valeur littérale ou l’utilisation de
force - une valeur par défaut définie dans le schéma Prisma
- une valeur par défaut ou un mécanisme de gestion des erreurs pris en charge par Zod
- l’injection du clé étrangère tenant dans le contexte d’exécution
- une écriture de relation autorisée qui la remplit
L’exemple de souscription permet au client de choisir une clé externe et un forfait, limite le nombre d’utilisateurs par une règle entière, fixe l’état et la version de départ sur le serveur, et renvoie une projection restreinte.
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,
},
}
Remarquez ce qui est absent : la clé étrangère tenant. Elle ne doit jamais être définie par le client lors de la création. La racine du contexte d’exécution provient d’un contexte de requête fiable, et l’extension ajoute elle-même la clé étrangère correspondante pour les écritures au niveau racine qu’elle prend en charge.
Opérations en masse et créations par lots
Les mutations en masse doivent passer deux vérifications. Premièrement, updateMany, ses variantes de retour et deleteMany ont besoin d’une structure permettant de définir un vocabulaire where. Deuxièmement, la requête entrante doit produire une condition réelle. Un corps manquant, un objet opérateur vide ou une branche logique qui correspond à tout ne sont pas acceptés, ce qui empêche des modifications accidentelles sur l’ensemble de la table.
Les opérations par lots protégées exigent un tableau et n’encapsulent pas silencieusement un objet isolé. Le fait que les opérations par lots et des options comme l’évitement des doublons fonctionnent encore dépend de votre fournisseur de base de données.
L’upsert repose sur trois contrats
Une structure d’upsert correspond en réalité à trois structures : un sélecteur unique, une charge utile pour la création et une charge utile pour les mises à jour.
const upsertSubscriptionShape = {
where: {
externalKey: true,
},
create: {
externalKey: true,
planId: true,
status: 'trialing',
seatCount: true,
version: 0,
},
update: {
planId: true,
seatCount: true,
},
}
Tout champ obligatoire doit être inclus dans le payload de création, tandis que le payload de mise à jour peut omettre n’importe quel champ. Une contrainte sous create ne définit la valeur que lors de l’insertion d’une ligne, alors que la même contrainte sous update la remplace chaque fois qu’une ligne existante correspond. Dans l’exemple, le statut et la version sont spécifiés lors de la création mais volontairement omis lors de la mise à jour, afin que les abonnements existants conservent ce qu’ils ont.
Les mutations uniques prennent la forme simple de sélecteur unique de Prisma. Les objets d’opérateur servent au filtrage ordinaire et n’ont pas leur place dans une structure where unique au niveau racine.
Contrats d’écriture et de retour des relations
Relation écrit une requête demandant une décision distincte concernant la propriété. Une forme peut autoriser l’opération connect, limiter les touches de sélection que le client peut utiliser et rejeter toute opération imbriquée qui n’est pas listée. Cependant, rien de tout cela ne prolonge la portée du locataire dans le sélecteur. Lorsque l’identité du record à connecter est importante, votre code d’application ou la base de données doit vérifier que l’appelant a le droit de l’utiliser.
Les contrats de retour varient également selon la méthode. Les méthodes par lots qui ne renvoient que un comptage, createMany, updateMany et deleteMany, répondent avec un en-tête de comptage et rejettent les projections. Leurs équivalents qui renvoient des données peuvent accepter des projections si tant Prisma que votre fournisseur le permettent. Si une configuration de générateur change une route en méthode de retour, examinez la nouvelle réponse comme un changement d’API plutôt que de la considérer simplement comme un détail d’implémentation.
La troisième question n’est pas « Cette méthode est-elle activée ? » Demandez plutôt ce que la méthode met à disposition : champs de données, formulaires de sélection, conditions en masse, opérations de relation et champs de réponse.
Hiérarchisation des mécanismes de contrôle
Les formes fonctionnent le mieux lorsque leurs limites sont documentées en même temps que les autres mécanismes. Chaque niveau a sa propre fonction :
- La demande authentifiée fournit un contexte fiable.
- Les mappages de portée ajoutent des conditions spécifiques aux utilisateurs aux opérations de niveau racine prises en charge.
- Les variantes d’appel choisissent quelle forme déclarée s’applique à un public donné.
- Les hooks exécutent des vérifications de l’application qui peuvent aboutir à un rejet.
- Les gestionnaires manuels gèrent les requêtes que les formes ne peuvent pas décrire.
- La politique de base de données protège les invariants qui doivent rester intacts quel que soit le chemin d’accès.
Aucun de ces outils ne devrait prétendre remplacer le travail d’un autre. Si vous devez décider quels chemins générer et lesquels écrire manuellement, notre article sur l’équilibre entre la génération Prisma CRUD et un contrôle ciblé des chemins aborde cette question.
Liste de contrôle pour l’examen et les tests
Lorsqu’une demande de fusion ajoute un point d’entrée généré, examinez-le en vous posant trois questions :
- Qui décide de chaque valeur que le point d’entrée accepte ?
- Quels champs et relations une réponse peut-elle contenir ?
- Quelles mutations et formulaires de sélection un utilisateur peut-il exécuter ?
Ensuite, veillez à ce que chaque test vise l’affirmation spécifique qu’il doit prouver. Analysez les arguments de lecture pour confirmer le comportement du filtre. Exécutez des délégués protégés pour valider les projections et les écritures. Examinez le chemin d’extension afin de vérifier l’injection de contexte. Utilisez des fixtures de base de données multi-locataires lorsque l’affirmation concerne les lignes à retourner, et des tests HTTP lorsqu’il s’agit du transport ou des routes générées.
Accordez aux tests négatifs la même importance. Pour chaque cas d’usage, essayez de lire un champ racine qui n’est pas listé, de lire un champ imbriqué sensible, de dépasser une limite de relation, et d’exécuter une opération d’écriture interdite par la structure. Les tests positifs prouvent que la requête prévue réussit ; les tests négatifs démontrent que les failles restent bloquées après des modifications du modèle ou du générateur.
Conclusion
Une forme ne mérite le nom de « contract » que lorsqu’il est clair à la fois ce qu’elle autorise et ce qu’elle ne peut pas imposer. En examinant séparément la propriété, la projection et les écritures, on met en évidence tôt les lacunes typiques : des forces laxistes au niveau racine, des relations imbriquées sans cadre défini, des comptages qui ignorent les filtres de relation, ainsi que des connexions qui contournent le champ d’application du locataire. Une fois ces lacunes attribuées à des hooks, des gestionnaires ou des politiques de base de données, la génération de code peut éliminer le code de bordure répétitif sans transformer le langage de requête Prisma en une API publique que personne n’avait l’intention de publier. Pour la syntaxe complète, les messages d’erreur exacts, le comportement spécifique à chaque version et des laboratoires reproductibles, consultez le guide de terrain prisma-generator-express.