Startseite / Artikel

Überprüfung der prisma-guard Shapes: Eigentumsverhältnisse, Projektion und Schreibkontrakte

Lernen Sie, Prisma-Guard-Formen als API-Verträge zu bewerten, indem Sie herausfinden, wer für jeden Wert verantwortlich ist, welche Daten in einer Antwort enthalten sein dürfen und welche generierten Endpunkte ausgeführt werden können.

Verlag: REACTAPP.TOP12 Min. Lesezeit2744 Wörter

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

Prismas Argumentiersprache ist ausdrucksstark – und genau das braucht man in vertrauenswürdigem Servercode. Sobald jedoch dieselbe Argumentstruktur über HTTP akzeptiert wird, verwandelt sich jede Eigenschaft dieser Sprache in eine Produktentscheidung: Was ein Aufrufer schreiben darf, welche Filter er verwenden kann, was der Server festlegen muss, welche Beziehungen zurückgegeben werden dürfen und wie viele Unterdatensätze eine Antwort enthalten darf. Mit den prisma-guard-Shapes können Sie diese Entscheidungen direkt neben der Prisma-Operation festhalten. Dieser Leitfaden zeigt, wie Sie eine Shape als kompakten Vertrag für Steuerung, Filtern, Projektion und Mutation nutzen können, indem Sie drei Fragen stellen, die Sie bei jedem Pull Request anwenden können, der einen generierten Endpunkt hinzufügt.

Warum eine Shape mehr ist als nur Validierung

Eine Form überprüft zwar die Eingaben, doch ihr größter Vorteil besteht darin, dass sie die Entscheidungen bezüglich der Grenzen an einem Ort bündelt. Eine generierte Operation kann sich auf diese Form stützen, anstatt dass jeder Handler die Eingabeschemata sowie Listen der zulässigen Felder manuell neu erstellen muss. Dadurch entfällt wiederholender Code und auch das Risiko, dass zwei Handler unbemerkt in ihren Zulassungen auseinanderdriften.

Der Preis für diese Bequemlichkeit ist Präzision. Eine Struktur stellt nur dann einen zuverlässigen Vertrag dar, wenn sie mit dem übereinstimmt, was die Bibliothek tatsächlich durchsetzt – und das beinhaltet auch Ehrlichkeit darüber, wo der Geltungsbereich des obersten Mieters aufhört. Das unten beschriebene Verhalten spiegelt die Implementierung wider, wie sie von ihren Wartern zum Zeitpunkt der Erstellung getestet wurde, und nicht eine etablierte Gemeinschaftskonvention; überprüfen Sie daher die Details in der Dokumentation für Ihre Version. Wenn eine geschützte Anfrage bereits fehlschlägt und Sie wissen möchten, warum, beginnen Sie mit unserem phasenbasierten diagnostischen Modell für Prisma-Guard-Fehler.

Frage eins: Wer besitzt jeden Wert?

Jeder „Blattknoten“ einer Struktur übermittelt einen Wert entweder an den Client oder an den Server. Das Vokabular ist klein; das Beispiel zeigt alle vier Formen nebeneinander.

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

Lesen Sie die Beschreibungen sorgfältig:

  • Einfaches true ist nicht der boolesche Wert. Es handelt sich dabei um einen Sentinel, der „der Client entscheidet“ bedeutet.
  • Eine Literale wie 'published' gehört zum Server.
  • force(value) gehört ebenfalls zum Server. Sie ist erforderlich, wenn der Wert, den Sie festlegen möchten, buchstäblich true ist – schließlich würde ein einfaches true als Client-Sentinel interpretiert werden.
  • Eine Verfeinerungsfunktion überlässt die Entscheidung dem Client, ersetzt jedoch die vom Feld generierte Zod-Kette durch die von Ihnen inline definierte Regel.

Die Platzierung beeinflusst, was „force“ bedeutet

Die Wörter sind einfach – ihre Position ist es, die die Leute überrascht. In einer where-Struktur wird ein erzwungener Skalar in ein Operator-Objekt eingefügt. Der untenstehende Ereignisfilter fixiert den Veröffentlichungszustand, lässt dem Client aber weiterhin die Suche nach Titeln zu:

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

Wenn ein gesamter Skalarprädikat am Anfang von where erzwungen wird, behandelt die Bibliothek die Eingaben des Clients für dieses Feld nachsichtig. Egal, ob der Aufrufer den fixierten Wert wiederholt, einen anderen sendet oder einen weiteren Operator hinzufügt – das Prädikat des Servers hat Vorrang, und der Anfrage wird nicht allein wegen der Erwähnung des Feldes abgelehnt. Das ist nicht dasselbe wie ein Standardwert. Ein Standardwert tritt zurück, wenn der Client etwas liefert; eine Erzwingung tritt niemals zurück.

In allen anderen Fällen sind die Regeln streng. Man denke an einen erzwungenen Modifikator, der neben einem vom Client gesteuerten Operator steht, an ein fixiertes Feld innerhalb eines Relationenfilters, an einen fixierten Filter in einer verschachtelten Include-Struktur oder an einen fixierten Wert in Daten zur Mutation. In all diesen Fällen verschwindet der Schlüssel aus dem Schema, gegen das der Client validiert wird, wodurch die Übertragung trotz exakter Übereinstimmung der Werte die Validierung scheitern lässt.

Dies ist für den Client-Code von Bedeutung. Ein Client muss möglicherweise ein vom Server verwaltetes Schreibfeld vollständig entfernen. Selbst wenn es auf den Wert des Servers „gefixt“ wird, scheitert die Validierung weiterhin, da die Überprüfung darauf abzielt, wem der Schlüssel gehört – und nicht darauf, ob die Werte gleich sind.

Standardwerte, Fehlerbehandlung und Optimierungen verschieben ebenfalls das Eigentumsrecht

Auch die Standardwerte und Fehlerbehandlungen von Zod verdienen dieselbe sorgfältige Prüfung. Auf einem vom Client kontrollierten Feld behält ein Standardwert sein normales Verhalten bei, wenn der Wert weggelassen wird, und eine Fehlerbehandlung kann einen fehlerhaften Eingabewert durch einen Ersatz ersetzen, anstatt die Anfrage abzulehnen. Die Erstellungsoperationen bringen eine zusätzliche Komplexität mit sich: Eine von dem Generator erzeugte Standardwert- oder Fehlerbehandlungsanweisung kann sogar ein erforderliches Feld ausfüllen, das in der Strukturbeschreibung nicht erwähnt wird, wobei der erzeugte Wert anschließend so eingefügt wird, als hätte der Server ihn festgelegt. Somit bestimmt die Stelle, an der ein Standardwert gesetzt wird, wer den Wert kontrolliert – verlassen Sie sich daher auf die Ersetzung nur dann, wenn es vom API-Vertrag gefordert wird.

Verbesserungen verbergen eine damit verbundene Falle. Sie erweitern die generierte Kette an ihrem Knoten nicht, sondern ersetzen sie. Ihre Funktion muss daher jede Einschränkung wiederholen, von der das Endpunkt abhängt – nicht nur das zusätzliche Minimum oder Maximum, das Sie hinzufügen wollten. Dies ist besonders wichtig, wenn der Prisma-Typ lockerer ist als die Produktregel, beispielsweise eine Zeichenkolonne, die niemals leer sein darf.

Erzwungene Aktualisierungen und erzwungene Disjunktionen

Gefestigte Aktualisierungsdaten werden bei jeder passenden Aufrufung geschrieben. Angenommen, eine Aktualisierungsstruktur erzwingt status: 'active': Jede Aktualisierung durch diese Struktur, selbst eine, die nur einen Titel ändert, setzt den Status ebenfalls zurück. Wenn eine Operation ein Feld unverändert lassen muss, sollte dieses Feld aus der Struktur ausgeklammert werden.

Die boolesche Struktur fügt eine weitere Regel hinzu. Eine unter OR platzierte erzwungene Bedingung wird zur obligatorischen Bedingung auf der Wurfebene erhoben. Infolgedessen hat eine Struktur keine Möglichkeit, eine echte vom Server gesteuerte „entweder/oder“-Regel auszudrücken, wie beispielsweise Datensätze, die zu diesem Konto gehören oder mit dieser Gruppe geteilt wurden. Solche Regeln erfordern eine auf dem Server geschriebene Abfrage, eine einzige Spalte zur Bestimmung des Anwendungsbereichs, die die Richtlinie festhält, oder eine Durchsetzung direkt in der Datenbank selbst.

Daher ist die erste Überprüfungsfrage präziser als „Ist das validiert?“ Fragen Sie, wer jede Wertentscheidung trifft, wo jede Erzwernung platziert wird und welche Argumentstruktur tatsächlich bei Prisma ankommt.

Zweite Frage: Was kann eine Antwort preisgeben?

Filtern und Projektion beantworten unterschiedliche Fragen. Ein Mieterfilter entscheidet, welche Datensätze in Frage kommen. Eine Projektion bestimmt, welche Felder und Beziehungen zurückgesendet werden. Beides ist notwendig – die Überprüfung eines Aspekts sagt nichts über den anderen aus.

Bei geschützten Lesevorgängen ist eine auf der Struktur deklarierte select- oder include-Anweisung gleichzeitig eine Erlaubnisliste und die Ersatzlösung. Clients können einen engeren Baum anfordern, aber nichts darüber hinaus; senden sie keine Projektion, verwendet der geschützte Delegat die deklarierte.

Die untenstehende Ticket-Struktur ermöglicht es einem Client, ein Ticket über die ID abzurufen, gibt eine feste Menge an Wurzelfeldern zurück und stellt eine verschachtelte Liste von Kundennotizen mit eigenem Filter, Feldern sowie Seitengröße bereit.

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

Der Baum erledigt vier getrennte Aufgaben:

  • Wurzelfelder, die nicht aufgelistet sind, bleiben unzugänglich.
  • Nur die aufgelisteten Notizenfelder können zurückgegeben werden.
  • Zu den Notizen besteht stets nur Zugriff vom Konto des Aufrufers aus.
  • Eine einzige Antwort kann maximal 20 Notizen enthalten, bei Nichtangabe durch den Client 10.
  • Nestete Beziehungen fallen außerhalb des automatischen Scope-Bereichs

    Der Notizen-Filter wird absichtlich separat implementiert. Die automatische Scope-Einstellung gilt nur für die unterstützten Operationen am Wurzelpunkt der Abfrage; sie erstreckt sich nicht auf durch select oder include abgerufene Beziehungen, um dort zusätzliche Bedingungen hinzuzufügen. Vergessen Sie die nestierten where-Bedingungen – ein korrekt eingegrenztes Ticket kann dennoch Notizen zurückgeben, die zu einem anderen Konto gehören.

    Zu-vielen-Beziehungen unterstützen ein verschachteltes where, ihre eigene Projektion sowie eine Zeilengrenze. Empfindliche eins-zu-einen-Beziehungen können kein verschachteltes Prisma where verwenden, weshalb eine andere Strategie nötig ist: Die Beziehung weglassen, nur unschädliche skalare Felder daraus freigeben, sie über die eigene geschützte Abfrage auf Wurzelebene laden oder sie mit einer Datenbankrichtlinie schützen.

    Auswahl des Projektionsstils

    Wählen Sie die Projektionsform entsprechend dem Verhalten beim Wachstum des Schemas aus:

    • Mit select bleibt eine später zum Modell hinzugefügte Spalte versteckt, bis jemand sie überprüft und auflistet – das macht diese Option zur sichersten Wahl für öffentliche Endpunkte.
    • Mit include erhalten Sie zwar Beziehungen, doch das Wurzelmodell gibt weiterhin alle seinen skalaren Spalten aus, sodass empfindliche Wurzeldaten sichtbar bleiben.
  • Mit omit behalten Sie eine Ablehnungsliste bei, und ob sie verfügbar ist, hängt von Ihrer Generator- sowie Prisma-Version ab.
  • Dies sind Alternativen, keine Ebenen. Mischen Sie niemals select, include und omit auf derselben Ebene des Baums.

    Zählwerte, verschachtelte Standardwerte und Mutationsergebnisse

    Die Anzahl einer Beziehung ist ein separates Projektionsknoten, nicht eine Zusammenfassung, die mit der geladenen Beziehung verknüpft ist. Wenn eine zurückgegebene Zu-vielen-Beziehung nach Konto gefiltert wird, muss ihr _count immer denselben Filter enthalten, wenn Clients davon ausgehen, dass beide Zahlen dieselben Zeilen beschreiben. Prüfen Sie die generierte Beziehungsbedingung sowie die Zählbedingung unabhängig voneinander.

    Die Standardwerte für verschachtelte Projektionen leiten sich von der deklarierten Struktur ab. Wenn ein Client nach einer mit booleschen Abkürzungen deklarierten Beziehung fragt, kann diese weiterhin ihren festgelegten Filter sowie die Seitenbindung beibehalten. Die dem Client gewährten Berechtigungen füllen jedoch fehlende Optionen nicht aus: Die Erlaubnis, nach einem Feld zu sortieren, liefert keine Richtung, die der Client weggelassen hat. In Tests sollten drei Fälle abgedeckt werden – eine weggelassene Wurzelprojektion, eine explizite verschachtelte Projektion sowie die Verwendung von Beziehungsabkürzungen – anstatt davon auszugehen, dass ein Fall die anderen beweist.

    Mutationen sind absichtlich asymmetrisch. Eine Mutation kann eine vom Client angeforderte Projektion überprüfen, aber sofern enforceProjection deaktiviert ist, erhält ein Client, der nichts anfordert, keine geformte Projektion und es gilt Prismas übliches Verhalten. Aktivieren Sie die Durchsetzung immer dann, wenn die Mutationsergebnisse auf deklarierte Felder beschränkt werden müssen.

    Zuletzt sollten Sie sicherstellen, dass jeder Test die Phase ausführt, für die er vorgesehen ist. Die Aufrufung von guard.query(...).parse() ist eine gute Methode, um Lesefilter und Grenzen zu überprüfen, doch die Standardwerte der Leseprojektion werden erst angezeigt, nachdem der geschützte Delegat ausgeführt wurde, und auch die Mutationsprojektion erfordert den entsprechenden geschützten Mutationspfad.

    Daher lautet die zweite Frage nicht „Gibt es ein Select?“ – Fragen Sie stattdessen, welche Wurzelfelder, verknüpften Felder, verknüpften Zeilen sowie Zahlen jede Aufrufvariante erhalten könnte.

    Frage drei: Welche Schreibvorgänge können ausgeführt werden?

    Erstellte Schreibendpunkte nehmen Argumente im eigenen Format von Prisma entgegen. Die Struktur der Daten bestimmt, welche Werte vom Client bereitgestellt werden, welche vom Server geliefert werden und ob nestede Operationen möglich sind.

    Die Erstellungsstruktur muss alle erforderlichen Felder berücksichtigen

    Eine Erstellungsstruktur wird abgelehnt, wenn ein erforderliches Feld keine Quelle hat. Zulässige Quellen sind unter anderem:

    • der Client, wenn das Feld als vom Client gesteuert markiert ist
    • der Server, über einen Literal-Wert oder force
    • eine im Prisma-Schema definierte Standardwert
    • ein unterstützter Zod-Standardwert oder -Catch
    • Injektion des Tenant-Fremdschlüssels im Scope
    • eine erlaubte Beziehungsänderung, die diesen Schlüssel ausfüllt

    Das Abonnementbeispiel ermöglicht es dem Client, einen externen Schlüssel und Plan auszuwählen, beschränkt die Anzahl der Plätze mit einer Ganzzahlerregel, legt den Startstatus und die Version auf dem Server fest und gibt eine eingeschränkte Projektion zurück.

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

    Beachten Sie, was fehlt: der Tenant-Fremdschlüssel. Dieser sollte niemals vom Client bei der Erstellung festgelegt werden. Der Scope-Root stammt aus dem vertrauenswürdigen Anfragedatenkontext, und die Erweiterung fügt für die von ihr unterstützten Schreibvorgänge auf Root-Ebene den entsprechenden Fremdschlüssel selbst hinzu.

    Masseoperationen und batchbasierte Erstellungen

    Massenmutationen müssen zwei Überprüfungen bestehen. Erstens benötigen updateMany, seine zurückgegebenen Varianten sowie deleteMany eine Struktur, um ein where-Kriterium anzugeben. Zweitens muss die eingehende Anfrage eine echte Bedingung liefern. Ein fehlendes Datenobjekt, ein leeres Operator-Objekt oder eine logische Bedingung, die alles erfüllt, sind nicht zulässig – dies schützt vor versehentlichen Änderungen in der gesamten Tabelle.

    Der geschützte Batch-Modus verlangt immer ein Array und umhüllt kein einzelnes Objekt stillschweigend. Ob die Rückgabe von Batch-Operationen sowie Optionen wie dem Überspringen doppelter Einträge weiterhin funktioniert, hängt vom jeweiligen Datenbankanbieter ab.

    Upsert umfasst drei Kontrakte

    Die Struktur für ein Upsert setzt sich tatsächlich aus drei Teilen zusammen: einem eindeutigen Selektor, einem Payload für die Erstellung sowie einem Payload für Aktualisierungen.

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

    Jedes erforderliche Feld muss im Create-Payload enthalten sein, während im Update-Payload alles weggelassen werden kann. Ein Force-Parameter unter create setzt den Wert nur bei der Einfügung einer Zeile, während derselbe Force-Parameter unter update diesen Wert jedes Mal überschreibt, wenn eine vorhandene Zeile übereinstimmt. Im Beispiel werden Status und Version beim Create festgelegt und absichtlich im Update weggelassen, sodass bestehende Abonnements ihren ursprünglichen Zustand beibehalten.

    Eindeutige Mutationen verwenden Prismas einfache Form eines eindeutigen Selektors. Operator-Objekte dienen dem normalen Filtern und haben keinen Platz in einer eindeutigen where-Struktur auf Wurzelebene.

    Relationsschreib- und Rückgabeverträge

    Relation schreibt eine Forderung nach einer separaten Entscheidung bezüglich des Eigentums. Eine Struktur kann connect zulassen, die vom Client verwendbaren Selektortasten einschränken und jede verschachtelte Operation ablehnen, die nicht aufgelistet ist. Allerdings erstreckt sich dadurch der Umfang des Mieters nicht auf den Selektor. Wenn es darauf ankommt, welches Record verbunden wird, muss Ihr Anwendungscode oder die Datenbank überprüfen, ob dem Aufrufer die Verwendung gestattet ist.

    Auch die Rückgabeverträge variieren je nach Methode. Die rein zählenden Batch-Methoden createMany, updateMany und deleteMany antworten mit einem Zählwert und lehnen Projektionen ab. Ihre entsprechenden Rückgabemethoden können Projektionen entgegennehmen, sofern sowohl Prisma als auch Ihr Anbieter dies zulassen. Wenn eine Generator-Einstellung einen Pfad auf eine Rückgabemethode umleitet, sollten Sie die neue Antwort als API-Änderung prüfen und sie nicht einfach als Implementierungsdetail ignorieren.

    Die dritte Frage lautet nicht „Ist diese Methode aktiviert?“ Fragen Sie stattdessen, was die Methode zur Verfügung stellt: Datelfelder, Selektormuster, Massenbedingungen, Relationenoperationen und Antwortfelder.

    Strukturierung der Durchsetzung

    Formen funktionieren am besten, wenn ihre Grenzen zusammen mit den anderen Mechanismen dokumentiert werden. Jede Schicht hat ihre eigene Aufgabe:

    • Die authentifizierte Anfrage liefert einen vertrauenswürdigen Kontext.
    • Schwellenwertzuordnungen fügen Benutzerbedingungen zu den unterstützten Operationen auf Wurzelebene hinzu.
    • Rufvarianten wählen aus, welche deklarierte Form für ein bestimmtes Publikum gilt.
    • Hooks führen begrenzte Anwendungsprüfungen durch, die Ablehnungen zulassen.
    • Handgeschriebene Handler kümmern sich um Abfragen, die Formen nicht beschreiben können.
    • Datenbankrichtlinien schützen Invarianten, die bei jedem Zugriffspfad erhalten bleiben müssen.

    Eines davon sollte nicht vorgeben, die Arbeit eines anderen zu übernehmen. Wenn Sie entscheiden müssen, welche Routen automatisch generiert und welche manuell geschrieben werden sollen, behandelt unser Artikel zu der Abwägung zwischen Prisma CRUD-Generierung und gezieltem Routenmanagement genau diese Fragestellung.

    Überprüfungs- und Testcheckliste

    Wenn ein Pull Request einen generierten Endpunkt hinzufügt, prüfen Sie ihn anhand von drei Fragen:

    1. Wer entscheidet über jeden Wert, den der Endpunkt akzeptiert?
    2. Welche Felder und Beziehungen können in einer Antwort enthalten sein?
    3. Welche Mutationen und Selektormodule kann ein Aufrufer ausführen?

    Stellen Sie sicher, dass jeder Test auf die spezifische Behauptung abzielt, die er beweisen soll. Analysieren Sie die eingegebenen Argumente, um das Verhalten des Filters zu überprüfen. Führen Sie geschützte Delegaten aus, um Projections und Schreibvorgänge zu überprüfen. Prüfen Sie den Erweiterungspfad, um die Scope-Injektion zu bestätigen. Wenden Sie mehrfach genutzte Datenbank-Fixtures an, wenn es darum geht, welche Zeilen zurückgegeben werden, und HTTP-Tests, wenn es um den Transport oder die generierten Routen geht.

    Geben Sie negativen Tests denselben Stellenwert. Für jedes Szenario versuchen Sie, ein nicht aufgelistetes Root-Feld zu lesen, ein sensibles verschachteltes Feld zu lesen, eine Beziehungsbeschränkung zu überschreiten sowie eine Schreiboperation durchzuführen, die aufgrund der Struktur nicht zulässig ist. Positive Tests belegen, dass die beabsichtigte Anfrage erfolgreich ist; negative Tests zeigen, dass Grenzen weiterhin gewahrt bleiben, nachdem sich das Modell oder der Generator geändert haben.

    Zusammenfassung

    Eine Struktur erhält den Namen „Contract“ erst dann, wenn klar ist, was sie zulässt und was sie nicht durchsetzen kann. Eine getrennte Überprüfung von Eigentumsrechten, Projektionen und Schreibvorgängen macht typische Mängel früh sichtbar: nachsichtige Einstellungen auf Wurzelebene, unbeschränkte verschachtelte Beziehungen, Zählungen, die Beziehungsfilter ignorieren, sowie Verbindungen, die den Scope des Benutzers umgehen. Sobald diese Mängel Hook-Funktionen, Handler oder Datenbankrichtlinien zugeordnet werden, kann die Codegenerierung wiederholenden Grenzcode beseitigen, ohne die Prisma-Abfragesprache in eine öffentliche API zu verwandeln, die niemand veröffentlichen wollte. Für die vollständige Syntax, genaue Fehlermeldungen, versionsspezifisches Verhalten sowie reproduzierbare Beispiele siehe den Feldführer von prisma-generator-express.