GraphQL-Codegen: Warum es sich lohnt

Michel

graphql-codegen-warum-es-sich-lohnt

GraphQL ist eine tolle Sache! Eine getypte API, Vermeidung von Overfetching und Generierung von Frontend-Services mit nur einem Befehl. Kein Wunder also, dass auch wir bei ATMINA auf GraphQL bauen; umgesetzt mit HotChocolate im .NET Core Backend, GraphQL Flutter in mobilen Projekten und Apollo Angular in unseren Webanwendungen. Mit dem GraphQL Code Generator und verfügbaren Plugins haben wir in der Vergangenheit bereits Apollo Queries automatisch generiert. Zunächst alles in einer Datei, später dann mit Hilfe des near-operation-file Preset direkt bei den zugehörigen GraphQL Operations.

Das Problem

Das Zusammenspiel zwischen den einzelnen Komponenten funktioniert wunderbar, insbesondere der Code Generator nimmt viel Arbeit ab! Allerdings gab es doch Kritik an dem erzeugten Output:

  • Scalars werden durch einen Scalars-Wrapper definiert statt direkt TypeScript Types wie string oder number zu nutzen
    Der Output nutzt viele Utility Types wie Pick und Maybe, was die Lesbarkeit in Fehlermeldungen massiv verschlechtert
  • Alle Typen (Scalars, Enums, Input- und Object-Types) werden in eine types.ts Datei generiert, aber oftmals nicht genutzt. Wenn man aber nur einen Enum importieren möchte, kann es schnell passieren, dass auch andere Typen anstelle des eigentlichen Query-Ergebnisses aus der Datei mit importiert werden und so Attribute als verfügbar angezeigt werden, die es nicht sind
  • Es gibt keine Möglichkeit für Aliases für Selections (außer der fragwürdigen Nutzung von GraphQL Fragments), wodurch Code-Wiederverwendbarkeit drastisch reduziert wird

Zum Vergleich: der in der folgenden Fehlermeldung angezeigte Type ist aufgrund der Verwendung von Pick sowie des Intersection-Operators (&) so komplex, dass Teile davon im Tooltip des Editors ausgelassen und durch Ellipsen ersetzt werden. Darunter zeigen wir eine vereinfachte Form des Types, der auch vollständig im Tooltip angezeigt werden könnte.

import {ShelvesQuery} from './example.generated';
const query: ShelvesQuery = {
    shelves: [
        {
            items: [
                {
                    // Fehlerquelle: hier fehlt `id`!
                    title: 'The Hobbit',
                    author: {
                        id: '',
                        name: 'J. R. R. Tolkien',
                    },
                },
            ],
        },
    ],
};

Fehlermeldung in der Standardkonfiguration des Plugins Fehlermeldung in der Standardkonfiguration des Plugins

Gewünschte Fehlermeldung nach Vereinfachung des Types Gewünschte Fehlermeldung nach Vereinfachung des Types

Umsetzung

Im Rahmen eines unserer OKR-Zyklen sind wir dieses Problem angegangen und haben auf der Grundlage der existierenden GraphQL-Codegen Plugins typescript und typescript-operations unsere eigenen Plugins geschrieben, die (wenn zusammen genutzt) die oben beschriebenen Dinge verbessert. Die folgenden Beispiele basieren auf diesem Schema.

@atmina/only-enum-types

@atmina/only-enum-types ist eine reduzierte Variante des typescript Plugins für GraphQL-Codegen, das ausschließlich Enums in der angemerkten types Datei ablegt. Mit Hilfe der Option onlyOperationTypes: true im typescript Plugin lässt sich der Umfang der generierten Typen zwar bereits auf Enums, Scalars und Input-Typen reduzieren; eine Option zum Generieren ausschließlich von Enums ist allerdings nicht vorhanden. Durch das Custom-Plugin wird das Risiko von falschen (oder zu vielen) Imports und die Größe der Datei deutlich verkleinert.

Vorher

Codebeispiel - enum-types vorher

Nachher

Codebeispiel - enum-types nachher

@atmina/local-typescript-operations

@atmina/local-typescript-operations ist ein Ersatz für das typescript-operations Plugin für GraphQL-Codegen, das einen Großteil der Kritik umsetzt, die bei uns am “Original” geübt wurde und hat in der Entwicklung den meisten Aufwand gekostet. Auch typescript-operations hat Konfigurationsoptionen, so konnte beispielsweise die Anforderung, Pick durch vollständige Typen zu ersetzen, durch preResolveTypes: true umgesetzt werden.
typescript-operations nutzt für die Erzeugung der Operation-Typen sowohl Input- als auch (in bestimmten Konfigurationen) die Object-Types, die sich eigentlich durch das typescript-Plugin in der großen types.ts Datei befinden. Entsprechend mussten nun die benötigten Input-Types ermittelt und (rekursiv) generiert werden. Verweise auf die zuvor importierten Typen mussten (mit der Ausnahme von Enums) korrigiert werden und die Nutzung des vom Plugin definierten Utility Types Maybe<T> = T | null wurde durch direkte Verwendung von T | null ersetzt.

Vorher

Codebeispiel - typescript-operations vorher

Nachher

Codebeispiel - typescript-operations nachher

Und dann war da noch die Sache mit den Aliases für Selections. In einem zuvor erstellen Konzept wurde die Idee einer GraphQL Directive ins Spiel gebracht, mit der in Queries und Mutations Selectionsets markiert werden können, um für diese einen Typen zu erzeugen und so nicht auf Fragments angewiesen zu sein. In der Implementierung wurde der im Konzept festgelegte Name @export verwendet und nach guten 40 Stunden war auch dieses Plugin bereit zur Nutzung!

Die folgenden Queries produzieren (abgesehen vom Namen) identische Typen, der rechte Weg erlaubt allerdings auch die Nutzung von Query Variablen für Fields, was mit Fragments nicht möglich ist:

query ExampleQuery {
    shelves {
        floor
        items {
            ...Book
        }
    }
}

fragment Book on Book {
    id
    author {
        id
        name
    }
}
query ExampleQuery {
    shelves {
        floor
        items @export(exportName: "Book") {
            id
            ... on Book {
                author {
                    id
                    name
                }
            }
        }
    }
}

Die Zukunft des Plugins

Mit der Implementierung der beiden Plugins wurden die Probleme, die bei der Nutzung der GraphQL Code-Generator Plugins typescript und typescript-operation aufgetreten sind, für uns behoben. Zunächst waren die beiden Plugins nur durch die interne GitLab NPM-Registry bei uns verfügbar. Da ATMINA in der Entwicklung viel auf Open Source Software setzt, möchten wir nun der Community etwas zurückgeben und wir haben uns dazu entschieden, die beiden Plugins auf GitHub und NPM (@atmina/only-enum-types@atmina/local-typescript-operations) zu veröffentlichen. Wir hoffen, dass anderen mit ähnlichen Gedanken zu generiertem TypeScript GraphQL Code damit geholfen wird, zumal die Plugins auch unabhängig von der tatsächlichen Service-Implementierung sind; das heißt, dass die ATMINA Plugins mit GraphQL Codegen für Apollo Angular, urql und weiteren funktionieren werden.

Fazit

Mit der Entwicklung der Plugins wurde bei ATMINA nicht nur auf interne Kritik an genutzten Tools reagiert, wir haben auch den Grundstein für eine rege Teilnahme in der GraphQL Community gelegt. Mit den Plugins erwarten wir nicht nur effizientere Arbeit bei uns, wir gehen auch einen weiteren Schritt auf dem Weg zu mehr Präsenz in der Entwickler-Community und lernen dabei eine Menge dazu. Die Zeit, die im Rahmen von OKR in das Projekt gesteckt wurde, hat sich für uns auf jeden Fall gelohnt und wir hoffen, dass es auch Dir weiterhilft!

Themen:

  • Softwareentwicklung
  • Programmiersprachen
  • Von Entwickler zu Entwickler