Graphql ist eine API Anfrage Sprache, die für eine effiziente Kommunikation zwischen Client und Servern designt ist. Es ist dem Nutzer möglich genau zu spezifizieren, was in der Antwort enthalten sein soll. Bei Graphql wird ein Vertrag mit dem Server definiert, über den ein Client mit dem Server kommunizieren kann. Der Client muss dabei nicht wissen, wo sich die Daten befinden, statdessen werden die Abfragen an den Server geschickt, welcher wiederum alle Informationen von den entsprechenden Stellen abruft. GraphQL ist Platformunabhängig und lässt sich so zur Kommunikation mit praktisch jedem Datenspeicher nutzen.
Aufbau
Graphql hat dabei drei grundlegende Operationen: - Queries holen sich Daten - Mutations ändern, entfernen oder fügen Daten hinzu - Subscriptions ähnlich wie Queries, haben aber eine permanente Verbindung, mit welcher der Server Daten zum Client im spezifizierten Format schicken kann.
Die Daten werden als eine reihe von Typen definiert. Dabei können sie von einem Service implementiert werden. Die meisten dieser Typen sind Objekt Typen. Diese definieren die verfügbaren Objekte und die Felder und Argumente, welche sie haben. Dabei hat jedes Feld seinen eigenen Typ, welcher entweder ein anderes Objekt, ein skalar, enum, union, interface oder ein benutzerdefinierter Typ sein kann. Das Beispiel zeigt eine simple Schema Definition für einen Produkt Typen. Der ! Operator sagt, dass das Feld nicht null sein darf, wenn es aufgerufen wird.
#Example schema definition
type Product {
id: ID!
name: string!
description: String!
price: Int
}
Graphql Queries
Graphql Queries emfangen Daten von den Datenspeichern. Sie sind dabei ähnlich zu den GET Anfragen in REST API's.
Die Abfragen haben dabei für gewöhnlich die folgenden Komponenten:
- Einen query Operations Typ. Dieser ist optional, aber es empfiehlt sich diesen zu nutzen, um dem Server zu sagen, dass es sich um eine query handelt.
- Einen query Namen. Dieser ist optional da dieser lediglich für's debuggen hilfreich sein kann.
- Die Datenstruktur, welche die query zurückgeben soll.
- Optionall ein oder mehrere Argumente, um Informationen zu einem speziellen Objekt zurückzugeben.
Das Beispiel zeigt eine query, welche ein Produkt abfragt und zu diesem Produkt spezielle Informationen haben will:
query myGetProcuctQuery {
getProduct(id: 123) {
name
description
}
}
Graphql Mutationen
Mutationen ändern die Daten in irgendeiner Weise. Dabei sind sie mehr oder weniger äquivalent zu den REST API Operationen POST, PUT und DELETE.
Wie auch die querys haben mutations einen Operationstypen, namen und eine Datenstruktur für die zurückgegebenen Daten. Sie nehmen aber immer eine Eingabe irgendeines Typs. Dies kann ein inline Wert sein, aber in der Praxis wird meist ein Variable genutzt.
mutation {
createProduct(name: "Flamin' Cocktail Glasses", listed: "yes"){
id
name
listed
}
}
{
"data": {
"createdProduct": {
"id": 123,
"name": "Flamin' Cocktail Glasses",
"listed": "yes"
}
}
}
Felder
Die folgende Abfrage fragt die Felder id, name.firstname und name.lastname ab.
query myGetEmployeeQuery {
getEmployees {
id
name {
firstname
lastname
}
}
}
{
"data": {
"getEmployees": [
{
"id": 1,
"name" {
"firstname": "Carlos",
"lastname": "Montoya"
}
},
{
"id": 2,
"name" {
"firstname": "Peter",
"lastname": "Wiener"
}
}
]
}
}
Argumente
Argumente sind Werte, die zur Verfügung gestellt werden für spezifische Felder. Die Argumente, welche genutzt werden können sind in dem Schema definiert.
Variablen
Mit ihnen ist es möglich der Anfrage dynamische Daten mitzugeben. Variablen basierte queries sind änlich zu den inline queries. Allerding werden einige Aspekte aus einem JSON basierten Wörterbuch genommen. Sie ermöglichen es mehrere Anfragen zu schicken, wobei nur die Variablen geändert werden. Für eine query mit Variablen wird das folgende benötigt: - Deklaration der Variablen und des Typs - Hinzufügen der Variable an der richtigen Stelle in der query - Übergeben des Variablen Schlüssels, und des Wertes der Variablen
query getEmployeeWithVariable($id: ID!) {
getEmployees(id:$id) {
name {
firstname
lastname
}
}
}
Variables:
{
"id": 1
}
Aliase
Aliase
In Graphql ist es nicht möglich mehrere Objekte des selben Typs abzufragen. Hierfür müssen Aliase genutzt werden:
query getProductDetails {
product1: getProduct(id: "1") {
id
name
}
product2: getProduct(id: "2") {
id
name
}
}
Fragmente
Fragmente sind wiederverwendbare Teile von queries oder mutationen.
fragment productInfo on Product {
id
name
listed
}
productInfo erstellt. Dieses basiert dabei auf dem Product Typ.
query {
getProduct(id: 1) {
...productInfo
stock
}
}
query, die ein Produkt aufrufen, dabei verwenden sie die Argumente des Fragments und fügt noch das stock Argument hinzu.
Subscriptions
Subscriptions sind ein spezieller query Typ. Sie erstellen eine lang lebige Verbindung, um Push benachrichtigen zu schicken. Dies eignet sich besonders für kleine Updates. Diese werden dabei meistens mit WebSockets realisiert.
Bypassing rate-limiting mit Aliasen
Dank Aliases ist es möglich mehrere Abfragen des gleichen Objektes in einer Anfrage zu schicken. Dadurch ist es möglich das rate-limiting zu umgehen.
Finden eines Graphql Endpunktes
Mit der query query{__typename} kann geprüft werden, ob ein graphql Endpunkt erreichbar ist und funktioniert. Dies ist möglich, da jede graphql das reservierte Wort __typename hat, welches den Typ der Anfrage zurückgibt also in diesem Fall query. Graphql nutzt dabei zumeist einen der folgenden Suffixe:
- graphql
- /api
- /api/graphql
- /graphql/api
- /graphql/graphql
Falls dies nicht erfolgreich ist kann noch etwas wie /v1 angehängt werden.
Zum senden der Anfragen sollte dabei der Content-Type application/json benutzt werden, da dieser vor CSRF schützt, während dies beim Content-Type x-form-urlencoded nicht zutrifft.
Introspektion
Mit der Introspektion kann das Schema von Graphql abgefragt werden. Es kann mit der folgenden Anfrage überprüft werden, ob Introspektion erlaubt ist:
{ "query": ".
{__schema{queryType{name}}}"
}
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation #Often needs to be deleted to run query
onFragment #Often needs to be deleted to run query
onField #Often needs to be deleted to run query
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type {
...TypeRef
}
defaultValue
}
Bypassing rate-limiting mit Aliasen
Dank Aliases ist es möglich mehrere Abfragen des gleichen Objektes in einer Anfrage zu schicken. Dadurch ist es möglich das rate-limiting zu umgehen.
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
onOperation, onFragment und onFieldzu entfernen. Da manche diese nicht akzeptieren in einer Introspektion.
Umgehen der Introspektion Verteidigung
Um die Introspektion zu unterdrücken nutzen manche Entwickler Regex. Ist dieses Regex aber nicht richtig erstellt, kann es sein, dass durch kleine Anpassungen diese umgangen werden können. Dazu können zu dem Schlüsselwort __schema Zeichen wie Kommas, Leerzeichen oder newlines angehängt werden.
Auch kann es mit einem GET Request klappen oder einem POST mit dem Content-TYPE x-www-form-urlencoded.
Suggestions
Falls die Introspektion nicht klappt oder deaktiviert ist gibt es noch die Vorschläge. Beispielsweise benutzt Apollo graphql dies, wenn ein kleiner Fehler gemacht wurde liefert es Vorschläge:
There is no entry for 'productInfo'. Did you mean 'productInformation' instead?