Verständnis des GraphQL-Typsystems

Der Autor hat das Free and Open Source Fund ausgewählt, um im Rahmen des Write for Donations-Programms eine Spende zu erhalten.

Einführung

GraphQL ist eine moderne Lösung zur Erleichterung der Kommunikation zwischen einer Frontend-Anwendung und einer Datenquelle. Alle Details und Fähigkeiten einer GraphQL-Implementierung sind im GraphQL-Schema festgelegt. Um ein funktionierendes GraphQL-Schema zu erstellen, ist ein Verständnis des GraphQL-Typsystems erforderlich.

In diesem Artikel erfahren Sie mehr über GraphQL-Typen: die fünf integrierten skalaren Typen, Enums, die Typen Liste und Non-Null, Objekttypen sowie die abstrakten Interface– und Union-Typen, die mit ihnen zusammenarbeiten. Sie werden Beispiele für jeden Typ durchgehen und lernen, wie Sie sie verwenden können, um ein vollständiges GraphQL-Schema zu erstellen.

Voraussetzungen

Um das Beste aus diesem Tutorial herauszuholen, sollten Sie folgendes haben:

Skalare Typen

Alle Daten in einem GraphQL-Schema lösen sich letztendlich in verschiedene skalare Typen auf, die primitive Werte repräsentieren. GraphQL-Antworten können als Baum dargestellt werden, und die skalaren Typen sind die Blätter am Ende des Baumes. Es können viele Ebenen in einer verschachtelten Antwort vorhanden sein, aber die letzte Ebene löst sich immer in einen skalaren (oder Enum) Typ auf. GraphQL wird mit fünf integrierten skalaren Typen geliefert: Int, Float, String, Boolean und ID.

Int

Int ist ein vorzeichenbehafteter 32-Bit-Ganzzahlwert ohne Dezimalstellen. Es handelt sich um eine vorzeichenbehaftete (positive oder negative) Ganzzahl, die keine Dezimalstellen enthält. Der maximale Wert einer vorzeichenbehafteten 32-Bit-Ganzzahl ist 2.147.483.647. Dies ist einer der beiden integrierten Skalare, die für numerische Daten verwendet werden.

Float

A Float is a signed double-precision fractional value. It is a signed (positive or negative) number that contains a decimal point, such as 1.2. This is the other built-in scalar used for numerical data.

String

A String is a UTF-8 character sequence. The String type is used for any textual data. This can also include data like very large numbers. Most custom scalars will be types of string data.

Boolean

A Boolean is a true or false value.

ID

Ein ID ist ein eindeutiger Bezeichner. Dieser Wert wird immer als Zeichenkette serialisiert, auch wenn die ID numerisch ist. Ein ID-Typ kann häufig mit einer Universally Unique Identifier (UUID) dargestellt werden.

Benutzerdefinierte Skalare

Neben diesen integrierten Skalaren kann das Schlüsselwort scalar verwendet werden, um einen benutzerdefinierten Skalar zu definieren. Benutzerdefinierte Skalare ermöglichen die Erstellung von Typen mit zusätzlicher Servervalidierung, wie zum Beispiel Date, Time oder Url. Hier ist ein Beispiel für die Definition eines neuen Typs Date:

scalar Date

Der Server wird wissen, wie er mit diesem neuen Typ durch die Verwendung von GraphQLScalarType interagieren soll.

Enum-Typ

Der Enum-Typ, auch als Enumerator-Typ bekannt, beschreibt eine Reihe möglicher Werte.

Unter Verwendung des Fantasy Game API-Themas aus anderen Tutorials der How To Manage Data with GraphQL-Serie könnten Sie ein enum für die Job– und Species-Eigenschaften der Spielfiguren mit allen akzeptierten Werten erstellen. Ein Enum wird mit dem Schlüsselwort enum definiert, wie folgt:

"The job class of the character."
enum Job {
  FIGHTER
  WIZARD
}

"The species or ancestry of the character."
enum Species {
  HUMAN
  ELF
  DWARF
}

Auf diese Weise ist garantiert, dass der Job eines Charakters entweder FIGHTER oder WIZARD ist und niemals versehentlich "purple" oder eine andere zufällige Zeichenkette sein kann, was möglich wäre, wenn Sie einen String-Typ verwenden würden, anstatt ein benutzerdefiniertes Enum zu erstellen. Enums werden konventionell in Großbuchstaben geschrieben.

Enums können auch als akzeptierte Werte in Argumenten verwendet werden. Zum Beispiel könnten Sie ein Hand enum erstellen, um anzugeben, ob eine Waffe einhändig (wie ein Kurzschwert) oder zweihändig (wie eine schwere Axt) ist, und das verwenden, um festzulegen, ob eine oder zwei ausgerüstet werden können:

enum Hand {
  SINGLE
  DOUBLE
}

"A valiant weapon wielded by a fighter."
type Weapon {
  name: String!
  attack: Int
  range: Int
  hand: Hand
}

type Query {
  weapons(hand: Hand = SINGLE): [Weapon]
}

Das Hand enum wurde mit den Werten SINGLE und DOUBLE deklariert, und das Argument im weapons-Feld hat einen Standardwert von SINGLE, was bedeutet, dass es auf SINGLE zurückfällt, wenn kein Argument übergeben wird.

Non-Null Type

Sie werden vielleicht bemerken, dass null oder undefined, ein gemeinsamer Typ, den viele Sprachen als primitiv betrachten, in der Liste der integrierten Skalare fehlt. Null existiert in GraphQL und repräsentiert das Fehlen eines Wertes.

Alle Typen in GraphQL sind standardmäßig nullable, daher ist null eine gültige Antwort für jeden Typ. Um einen Wert erforderlich zu machen, muss er in einen GraphQL Non-Null-Typ mit einem abschließenden Ausrufezeichen umgewandelt werden. Non-Null ist als Typmodifikator definiert, der Typen modifiziert, auf die er sich bezieht. Als Beispiel ist String ein optionaler (oder nullable) String, und String! ist ein erforderlicher (oder Non-Null) String.

Listentyp

A List type in GraphQL is another type modifier. Any type that is wrapped in square brackets ([]) becomes a List type, which is a collection that defines the type of each item in a list.

Als Beispiel wird ein als [Int] definierter Typ eine Sammlung von Int-Typen sein, und [String] wird eine Sammlung von String-Typen sein. Non-Null und List können zusammen verwendet werden, um einen Typ sowohl erforderlich als auch als Liste definiert zu machen, wie zum Beispiel [String]!.

Objekttyp

Wenn GraphQL-Skalartypen die „Blätter“ am Ende der hierarchischen GraphQL-Antwort beschreiben, dann beschreiben Objekt-Typen die vermittelnden „Äste“, und fast alles in einem GraphQL-Schema ist ein Typ von Objekt.

Objekte bestehen aus einer Liste benannter Felder (Schlüssel) und dem Werttyp, den jedes Feld auflösen wird. Objekte werden mit dem Schlüsselwort type definiert. Mindestens ein oder mehrere Felder müssen definiert sein, und Felder dürfen nicht mit zwei Unterstrichen (__) beginnen, um Konflikte mit dem GraphQL-Introspektionsystem zu vermeiden.

In dem Beispiel der GraphQL-Fantasy-Game-API könnten Sie ein Kämpfer-Objekt erstellen, um einen Charaktertyp in einem Spiel darzustellen:

"A hero with direct combat ability and strength."
type Fighter {
  id: ID!
  name: String!
  level: Int
  active: Boolean!
}

In diesem Beispiel wurde der Objekttyp Kämpfer deklariert, und er hat vier benannte Felder:

  • id ergibt einen Non-Null-Typ ID.
  • Name ergibt einen Non-Null-Typ String.
  • Level ergibt einen Int-Typ.
  • Aktiv ergibt einen Non-Null-Typ Boolean.

Über der Deklaration können Sie auch einen Kommentar mit doppelten Anführungszeichen hinzufügen, wie in diesem Beispiel: "Ein Held mit direkter Kampffähigkeit und Stärke.". Dies erscheint als Beschreibung für den Typ.

In diesem Beispiel löst jedes Feld zu einem skalaren Typ auf, aber Objektfelder können auch zu anderen Objekttypen aufgelöst werden. Zum Beispiel könnten Sie einen Waffe-Typ erstellen, und das GraphQL-Schema kann so eingerichtet werden, dass das waffe-Feld auf dem Kämpfer zu einem Waffe-Objekt aufgelöst wird:

"A valiant weapon wielded by a fighter."
type Weapon {
  name: String!
  attack: Int
  range: Int
}

"A hero with direct combat ability and strength."
type Fighter {
  id: ID!
  name: String!
  level: Int
  active: Boolean!
  weapon: Weapon
}

Objekte können auch in die Felder anderer Objekte verschachtelt werden.

Wurzelbetriebstypen

Es gibt drei spezielle Objekte, die als Einstiegspunkte in ein GraphQL-Schema dienen: Abfrage, Mutation und Abonnement. Diese werden als Wurzelbetriebstypen bezeichnet und folgen allen gleichen Regeln wie jeder andere Objekttyp.

Das Schlüsselwort schema stellt den Einstiegspunkt in ein GraphQL-Schema dar. Ihre Wurzelabfrage-, Mutations- und Abonnementtypen werden im Wurzelobjekt schema sein:

schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

Der Abfrage-Typ ist in jedem GraphQL-Schema erforderlich und repräsentiert eine Leseanforderung, ähnlich wie ein REST-API GET. Folgendes ist ein Beispiel für ein Wurzel-Abfrage-Objekt, das eine Liste von Kämpfer-Typen zurückgibt:

type Query {
  fighters: [Fighter]
}

Mutationen repräsentieren eine Schreibanforderung, die analog zu einem POST, PUT oder DELETE in einer REST-API wäre. Im folgenden Beispiel hat die Mutation ein addFighter-Feld mit einem benannten Argument (Eingabe):

type Mutation {
  addFighter(input: FighterInput): Fighter
}

Zuletzt entspricht ein Abonnement einem Ereignisstrom, der in Verbindung mit einem Websocket in einer Webanwendung verwendet würde. In der GraphQL Fantasy API könnte es beispielsweise für zufällige Kampfbegegnungen verwendet werden, wie folgt:

type Subscription {
  randomBattle(enemy: Enemy): BattleResult
}

Beachten Sie, dass der Einstiegspunkt schema in einigen GraphQL-Implementierungen oft abstrahiert wird.

Feldargumente

Die Felder eines GraphQL-Objekts sind im Wesentlichen Funktionen, die einen Wert zurückgeben, und sie können wie jede Funktion Argumente akzeptieren. Feldargumente werden durch den Namen des Arguments gefolgt vom Typ definiert. Argumente können jeden nicht-Objekttyp haben. In diesem Beispiel kann das Fighter-Objekt nach dem id-Feld gefiltert werden (das zu einem Nicht-Null-Typ ID aufgelöst wird):

type Query {
  fighter(id: ID!): Fighter
}

Dieses spezielle Beispiel ist nützlich, um ein einzelnes Element aus dem Datenspeicher abzurufen, aber Argumente können auch für Filterung, Paginierung und andere spezifischere Abfragen verwendet werden.

Interface-Typ

Wie der Objekttyp besteht der abstrakte Interface-Typ aus einer Liste benannter Felder und ihrer zugehörigen Werttypen. Schnittstellen sehen aus wie und folgen allen gleichen Regeln wie Objekte, werden jedoch verwendet, um einen Teil der Implementierung eines Objekts zu definieren.

Bisher haben Sie in Ihrem Schema ein Fighter-Objekt, aber Sie möchten vielleicht auch ein Wizard, ein Healer und andere Objekte erstellen, die viele der gleichen Felder gemeinsam haben, aber einige Unterschiede aufweisen. In diesem Fall können Sie ein Interface verwenden, um die Felder zu definieren, die sie alle gemeinsam haben, und Objekte zu erstellen, die Implementierungen des Interfaces sind.

In folgendem Beispiel könnten Sie ein BaseCharacter-Interface mit dem interface-Schlüsselwort erstellen, das alle Felder definiert, die jeder Charaktertyp besitzen wird:

"A hero on a quest."
interface BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job
}

Jeder Charaktertyp wird die Felder id, name, level, species und job haben.

Stellen Sie sich nun vor, Sie haben einen Fighter-Typ und einen Wizard-Typ, die diese gemeinsamen Felder haben, aber Fighters verwenden ein Weapon und Wizards verwenden Spells. Sie können das implements-Schlüsselwort verwenden, um jeden als eine BaseCharacter-Implementierung abzugrenzen, was bedeutet, dass sie alle Felder aus dem erstellten Interface haben müssen:

"A hero with direct combat ability and strength."
type Fighter implements BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job!
  weapon: Weapon
}

"A hero with a variety of magical powers."
type Wizard implements BaseCharacter {
  id: ID!
  name: String!
  level: Int!
  species: Species
  job: Job!
  spells: [Spell]
}

Fighter und Wizard sind beide gültige Implementierungen des BaseCharacter-Interfaces, weil sie die erforderliche Teilmenge der Felder haben.

Union Type

Ein weiterer abstrakter Typ, der mit Objekten verwendet werden kann, ist der Union-Typ. Mit dem Schlüsselwort „union“ können Sie einen Typ mit einer Liste von Objekten definieren, die alle als Rückgabewerte gültig sind.

Unter Verwendung der in dem vorherigen Abschnitt erstellten Schnittstellen können Sie eine Character-Union erstellen, die einen Charakter als Zauberer ODER Kämpfer definiert:

union Character = Wizard | Fighter

Das Gleichheitszeichen (=) legt die Definition fest und das Pipe-Zeichen (|) dient als ODER-Anweisung. Beachten Sie, dass eine Union aus Objekten oder Schnittstellen bestehen muss. Skalare Typen sind in einer Union nicht gültig.

Wenn Sie nun nach einer Liste von Charakteren abfragen, kann die Character-Union verwendet werden und alle Zauberer- und Kämpfertypen zurückgeben.

Zusammenfassung

In diesem Tutorial haben Sie viele der Typen kennengelernt, die das GraphQL-Typsystem definieren. Die grundlegendsten Typen sind die Skalartypen, die die Werte darstellen, die als Blätter im Schema-Baum fungieren und aus Int, Float, String, Boolean, ID und jedem benutzerdefinierten Skalartyp bestehen, den eine GraphQL-Implementierung erstellt. Enums sind Listen gültiger konstanter Werte, die verwendet werden können, wenn Sie mehr Kontrolle über eine Antwort benötigen als nur die Deklaration als String, und sie sind ebenfalls Blätter im Schema-Baum. Listen- und Non-Null-Typen werden als Typmodifikatoren oder Wrapper-Typen bezeichnet und können andere Typen als Sammlungen oder erforderlich definieren. Objekte sind die Zweige des Schema-Baums, und fast alles in einem GraphQL-Schema ist ein Objekttyp, einschließlich der query-, mutation- und subscription-Einstiegspunkte. Interface- und Union-Typen sind abstrakte Typen, die bei der Definition von Objekten hilfreich sein können.

Für weiteres Lernen können Sie üben, ein GraphQL-Schema zu erstellen und zu ändern, indem Sie das Tutorial „Wie man einen GraphQL-API-Server in Node.js einrichtet“ lesen, um eine funktionierende GraphQL-Serverumgebung zu haben.

Source:
https://www.digitalocean.com/community/conceptual-articles/understanding-the-graphql-type-system