PageEditor Da wir ein headless cms benutzen müssen wir unsere eigenen Lösungen entwickeln um Daten einpflegen zu können. Auf der einen Seite haben wir verschiedene Formulare um gewisse Datensätze anzulegen bzw zu bearbeiten, auf der anderen den PageEditor in dem die webseite schlussendlich zusammengepuzzlet werden kann. Das war meine Aufgabe. Nach verschiedenen Prototypen lief es darauf hinaus, dass man einen richtext editor mit plugin unterstützung als basis heranzieht (quill.js in userem fall). Normaler text und andere basis features wie bold, italic, links, überschriften etc wurden so schon einmal out of the box supported. Alle komplexeren user interface elemente wurden erst einmal seperat in der module-library als react componenten entwickelt die dann schlussendlich vom static site generator im frontent (gatsby) und meinem page editor konsumiert werden. Ich habe dann dafür gesort das man diese react komponenten in den quill rich text editor über ein menu einfügen kann. Damit konnte man sie wie ein bild einfach in den dokumentenfluss einfügen. Da die meisten module allerdings einen bestimmten inhalt oder einstellungsmöglichkeiten beinhalteten musste noch eine möglichkeit her wie man diese module in editor konfigurieren kann. Anfangs hatte ich ein system gebaut indem jedes module seine variablen festlegen konnte (inklusive typ) und anschließend automatisch eine settings page generiert wurde mit der man das module konfigurieren und updaten konnte. Obwohl dieser ansatz sehr gut funktioniert hat, habe ich ihn einige wochen später ersetzt durch jeweilig spezifische settings seiten pro module. Das lag einfach daran, dass unserer auftragsgeber sehr penibel mit seinem layouting wahr. Da war die automatisch generierten settings seiten einfach zu generisch da wenn man hier einen style angepasst hat es direkt über all hatte. Beim neuen ansatz kann jedes module seine eigene settings react komponente definieren und die schnittstelle kümmert sich nur noch darum, dass sich das module entsprechend der settings änderungen updated. Der Zustand des quill editors wir in einem eigenen delta format abgespeichert. Dieser beschreibt schritt für schritt mit welchen actionen man von einem leeren editor zum aktuellen inhalt gelangt. Mit hilfe diesem formats (array and json objekten) konnte man schon einmal seiten in der datenbank persistieren und laden. Problematisch ist dieses format allerdings für gatsby der schlussendlich die seiten daraus generieren soll. Damit der inhalt auch dafür nutzbar ist musste neben dem delta format noch ein anderes format abgespeichert werden. Dafür musste der inhalt des quills mit einem eigenen konverter umstrukturiert werden. Das format sah in etwa so aus: . Im frontend konnte dann aus dem component name und den entsprechenden props der komponent aus der module-library geladen und gerendert werden. Validation Mein drittes und letztes größere Themengebiet war die implementierung einer grundlegenden Infrastruktur für die Validierung von GraphQL requests im backend. Da wir ein headless cms benutzen und das ganze user interface für die Verwaltung selbst von Grund auf hochgezogen haben, benötigten wir natürlich auch im backend die dazu entsprechende Infrastruktur um die Daten abzuspeichern. Soweit waren auch schon die Grundlagen vorhanden um gezielt Daten in der Datenbank abzuspeichern, das Problem war nur, dass diese vorab nicht groß validiert wurde. Das einzige was von haus aus beim Apollo Server dabei ist, ist eine sehr primivite validierung gegen das von prisma generierte model. Hier werden allerdings nur die Anwesenheit von Feldern eines Objekts und deren Typ berücksichtigt. Die wichtigsten Punkte für uns waren allerdings nicht abgedeckt: Die Range von einzelen Variablen, komplexere constraints auf einzelne Felder als auch untereinander - d.h. dependencies á la wenn A gegeben ist kann B nicht null sein, andernfalls schon. Hinzu kam auch noch das Thema von entsprechenden Fehlermeldungen im frontend. Also mussten die Fehlermeldungen einem bestimmten Format entsprechen damit z.b. im frontend die richtigen Felder einer Form angesprochen werden können in denen der Fehler aufgetreten ist. Die von mir umgesetzte Infrastruktur umfasste folgendes: -- Alle neuen und bestehenden Fehlermeldungen in einem einheitlichen Format Um das zu erreichen musste man an einigen Stellen die von Apollo generierten Fehler abfangen und in das neue Format konvertieren. Hier gab und gibt es eine entscheidende Einschränkung: Damit alle Validierungsfehler im frontend gleichzeitig angezeigt werden können, muss im backend alle validierungen ausgeführt werden, alle fehler gesammelt und gebündelt zurückgeschickt werden. Das Problem ist, dass man die basis validierung von apollo nicht abschalten kann (fehlende/zuviele Felder oder falsche typen). Falls es hier zu einem validierungsfehler kommt bricht apollo ab vor man die Möglichkeit hat eigene validierungslogik laufen zu lassen. Schlussendlich bedeutet das, falls das Format der Request nicht valide ist bekommt man zuerst alle Fehler zurück die die Strukturennen probleme der query aufweisen. Erst wenn diese behoben sind kommen die eigentlichen validierungen die wir selber umgesetzt haben (dazu mehr im nächsten ABschnitt). Das Ganze ist an sich natürlich nicht so schön, sollte aber tatsächlich in der produktion nie vorkommen, da sobald die requests im frontend einmal richtig implementiert werden kann ein feld nicht mehr fehlen sondern nur noch aufgrund einer falschen eingabe fehlerhaft sein. Deshalb war die Lösung für uns soweit akzeptable. -- Eigene komplexere validierung mithilfe der class-validator library Um komplexere anforderungen an ein Feld zu stellen als einfach nur sein typ, war der erste schritt die validation library "class-validator" zu unterstützgen. Diese library enthält sehr viele decorators für die häufigsten use cases. Diese kann man bei den graphql models einfach an die properties mit dran packen. zb kann man mithilfe von @Min(0) bzw @Max(10) die range einer Zahl festlegen oder mit @Email sicherstellen das ein string dem email format entspricht. -- Komplexere abhänigkeiten und non standart validierungen Damit auch die komplexesten fälle abgedeckt werden können, habe ich auch noch "custom validators" implementiert. Einfach gesagt man kann eine Klasse erstellen die von einer generischen CustomValidator klasse erbt. Hier muss die validate methode überschrieben werden. In dieser Methode kann man nach lust und laune abhänigkeiten innerhalb des objektes überprüfen und mithilfe der assert methode der super klasse sicherstellen das diese eingehalten werden. Um den custom validator nun einem model anzuheften muss man einfach nur die model klasse mit einerm decorator und der custom validator instance annotieren "@CustomValidator(new ModelTypeValidator())" -- Der Validator All diese themen kommen im "root" Validator zusammen. Er ist dafür zuständig ein graphql model object zu validiern. Der Validator kommt in den graphql resolvern zum einsatz um die übergebenen parameter einer query zu validiern. Um es einfach zu halten muss man nichts weiteres machen als "await Validator.validate(param);" aufzurufen. Anschließend werden auf dem Objekt alle "class-validator" decorators ausgeführt die am Model annotiert wurden, wird der custom validator des models ausgeführt (falls vorhanden) und das wichtigste alle diese Schritte werden rekursive auf allen object und array properties des "root objekts" aufgerufen. Allerdings jeweils mit den dementsprechenden decorators und custom validators der model klassen. Nachdem all das durchgelaufen ist und mindestens ein validation fehler aufgetreten ist, wird der aktuelle resolver durch einen geworfenen fehler abgebrochen und alle gesammelten validation errors werdem dem client gebündelt zurückgegeben. Andernfalls läuft der resolver ganz normal weiter und führt seine logik aus.