Einer der großen Vorteile von GraphQL gegenüber REST ist, dass es nur einen Endpunkt gibt. Aber das ist auch kein Muss, nur eine Empfehlung (und in meinen Augen Best Practice). Doch wenn jetzt mehrere Micro-Services bestehen, ist dieser Vorteil wieder verschwunden. Um das zu beheben, gibt es in den Apollo GraphQL-Tools das Schema-Stitching. Damit ist es möglich, mehrere Schemata zu einem zusammen zu fassen. Mit den RemoteSchemata können auch Schemata, die auf einem oder mehreren Microservices liegen, angesprochen werden und an einem zentralen Punkt zusammengefasst werden.

Schema-Stitching

In unserer Chat App haben wir den Fall, dass wir einen Microservice haben, der sich mit dem eigentlichen Chat befasst und einen Service, der ein Kontaktbuch für jeden Nutzer bereitstellt. Die einzelnen Schema Definitionen sind vollständig voneinander getrennt, die beiden Services kennen sich also nicht. Aber ich könnte auf die Idee kommen, dass ich alle Chats von meinen eigenen Kontakten sehen möchte (und bevor Fragen aufkommen: ja, wir haben noch einen eigenen Blogeintrag zum Thema Autorisierung  . Aber mein Kontakte-Microservice kennt die Gespräche nicht, wie kommen wir also an die Daten?

Das Schema-Stitching bietet hier Ansätze. Dazu müssen wir in der Chat-App eine Schnittstelle anbieten, die es uns erlaubt, einen User mit seiner ID zu finden. Wenn wir jetzt die ID von einem Kontakt kennen, können wir die Anfrage einfach an den GraphQL Server vom Chat weiterleiten. Aber die Frage ist: Kennen wir denn die ID überhaupt? Der User wird in den meisten Fällen diese ID abfragen, doch er muss sie nicht abfragen. In dem Fall bekommen auch wir die ID nicht unbedingt (siehe hierzu den Abschnitt Remote Schema und den Blogeintrag zur Autorisierung). Wir können aber Fragmente definieren, die mit abgefragt werden. So können wir sicherstellen, dass wir die User ID haben und dann so problemlos den entsprechenden User abfragen.

Aber an lebendem Code kann man solche Dinge besser erklären, deswegen hier ein bisschen davon.

Hier ein Auszug aus dem Schema der Chat-Komponente

type Query {
me: User
userById(id: Int!): User
}

type User @cacheControl(maxAge: 120) {
id: Int
conversations(limit: Int!, offset: Int): [Conversation]
}

Hier ein Auszug aus dem Schema für die Kontaktliste.

type Query {
contacts(limit: Int!, offset: Int): [Contact]
contactsByUserId(id: Int!, limit: Int!, offset: Int): [Contact]

}

type Contact {
         id: Int
         userId: Int
         birthdate: String
         contacts: [Contact]
}

Beide Schemata lassen es zu, dass nach einem User mit seiner ID gesucht wird. Damit wird die Kommunikation zwischen den beiden Server ermöglicht.

Hier noch ein Auszug aus dem Schema des API-Gateways, das die beiden Schemata zusammenfasst:

extend type User {
contacts(limit: Int!, offset: Int): [Contact]
}

extend type Contact {
user: User
}

Es erweitert jeweils die Typen User und Contact aus den beiden Server erweitert. Damit werden die beiden Schemata vereint, nach außen hin sind dann sowohl die Funktionen der beiden einzelnen Server, als auch die erweiterten Funktionen sichtbar. Die entsprechenden Resolver werden auch im Gateway definiert. Hier als Beispiel der Resolver für die Kontakte eines Users:

User: {
contacts: {

                  fragment: `fragment UserFragment on User { id }`,
                  resolve(parent: any, args: any, context: any, info: any) {
                           const id = parent.id;
                           return mergeInfo.delegate(
                                    ‚query‘,
                                    ‚contactsByUserId‘,
                                    {
                                             id,
                                             limit: args.limit,
                                             offset: args.offset

                                    },
                                    context,
                                    info,
                            );
                  },
         },
},

Der Resolver gibt ein Fragment mit an, welches dazu führt, dass beim Abfragen eines Users immer die ID mit an den Server gegeben wird. Wenn die ID von einem Client nicht mit abgefragt wurde, wird sie nicht zurückgegeben. Die eigentliche Resolve Funktion extrahiert zunächst die UserID aus dem Objekt und gibt dann die Anfrage an das Schema der Kontakt-Komponente weiter. Dass das Schema dabei ein Remote Schema ist, spielt keine Rolle.

RemoteSchemata

Für das SchemaStitching ist es egal, wo ein Schema definiert ist, es muss lediglich ein ausführbares Schema sein. Doch vor allem in Bereichen, die von Gateways abgedeckt werden, sind die Schemata nicht immer auf dem gleichen Server. Um dieses Problem zu beheben, gibt es in den GraphQL Tool die RemoteSchemata. Diese benötigen zwei Dinge: Ein Schema, welches angibt, welche Queries und Typen verfügbar sind und eine Verbindung zum Server, der das Schema bereitstellt.

Um das Schema zu bekommen gibt es im groben zwei Möglichkeiten. Zum einen können Introspect Queries genutzt werden. Diese fordern von einem Server das Schema an und ermöglichen so nur unter Kenntnis der Adresse des Remote Schemata den Zugriff auf das Schema. Die Einzige Anforderung ist hier, dass die Server erreichbar sind, wenn das Gateway gestartet wird. Unter Umständen muss auch eine Autorisierung für die Introspect Query erfolgen. Die andere Möglichkeit ist, das Schema direkt anzugeben. Diese Option bietet sich an, wenn man selbst die Microservices betreibt, auf die Zugegriffen werden soll. Hier muss aber der Quellcode für die Schemata zur Verfügung stehen. Dafür müssen hier die Server nicht zwangsweise laufen, wenn das Gateway gestartet wird. Für die Ausführung einer Query müssen sie dann logischerweise aber doch laufen.

Die Verbindung zum Server kann über verschiedene Bibliotheken hergestellt werden. Es gibt hier auch immer die Möglichkeit, die Anfrage zu manipulieren, z.B. durch das Hinzufügen von Headern für die Autorisierung. Die einzelnen Informationen können entweder statische sein, oder über den Kontext bei jeder Query einzeln in die Anfrage angefügt werden. Die Dokumentation der GraphQL Tools bietet hier noch weitere Einblicke.

Probleme

Beim Zusammenfassen der APIs in einem Gateway könnte man auf die Idee kommen, die Autorisierung in dem Gateway durchzuführen. In dem Fall muss man sich aber im Klaren sein, dass bei Verwendung von RemoteSchemata grundsätzlich nur die Daten zur Verfügung stehen, die der User angefragt hat „“ Außer man weißt den Server ausdrücklich drauf hin, mehr Daten anzufragen. Das geht zum Beispiel auch über das Schema Stitching. Näheres dazu steht in unserem Blogeintrag zum Thema Autorisierung.

Zusammenfassung

Auch mehrere Server lassen sich mit GraphQL Tools zu einem einzigen Endpunkt zusammenfassen und die Funktionalität mit SchemaStitching erweitern. Remote Server können problemlos eingebunden werden, auch Autorisierungsinformationen wie JWTs können bei jeder Anfrage an diese mitgereicht werden. Nur die Autorisierung wird dadurch etwas aufwändiger, aber auch die Probleme sind lösbar.

 

Alle Beiträge von Manuel Styrsky

Schreibe einen Kommentar