PostGraphile-Schema erweitern & verfeinern: Plug-ins

Wir haben an unserem Schema in den vorherigen beiden Posts zu Smart Tags und Computed Columns schon einiges anpassen können. Dabei haben wir noch immer keinen Stein auf den Fuss bekommen. PostGraphile spielt einfach mit und macht einfach was wir wollen und schenkt uns immer noch ein super GraphQL-Schema, einfach generiert von unserer Postgres-Datenbank. Ist doch schonmal richtig gut!

Heute werden wir aber mal richtig kreativ und bohren unser GraphQL-API noch einmal richtig auf. PostGraphile bietet ein ausgiebiges Plug-in-System an – und das schauen wir uns jetzt mal genauer an!

Wie schon bei den vorhergegangenen Postst gilt: Der Code für das Beispielprojekt ist auf GitHub zu finden. (Mit ein paar einzelnen Commits, um die unternommenen Schritte von hier ein bisschen nachvollziehen zu können.)

PostGraphile-Schema erweitern & verfeinern


Plug-ins

Während wir also mit Smart Tags sehr genau steuern können, welche Felder unter welchen Bedingungen verfügbar sein sollen, können wir nun zusätzlich mit Computed Columns auch beliebige Daten mit Hilfe von SQL-Funktionen zu unserem Schema hinzufügen.

Bereits mit diesen beiden Funktionen bietet PosGraphile eine immense Flexibilität und ist dennoch weiterhin ein sehr bequemes und mächtiges Werkzeug, um GraphQL-APIs auf Basis von Postgres zu schaffen. Um nun auch die speziellsten, einzigartigsten Sonderwünsche, die wir an unser GraphQL-API stellen, abdecken zu können, bietet PostGraphile ein umfangreiches Plug-in-System an.

Open-Source-Plug-ins

PostGraphile ist ein Open-Source-Tool; und einige Spezialfälle kommen wohl häufiger vor. Die Community hat eine Liste mit von ihr gepflegten Plug-ins erstellt. Solltet ihr also einmal ein nützliches Plug-in verfassen das ihr der Community zur Verfügung stellen wollt, könnt ihr dieses auf der verlinkten Seite auch direkt mit auflisten lassen!

Eines der beliebtesten Plug-ins auf der Liste – postgraphile-plugin-connection-filter – wäre doch direkt nützlich, um unseren numberOfAuthoredPosts-Filter nicht nur mit genauen Treffern auszustatten, sondern auch um größer-als- / kleiner-als-Bedingungen zu unterstützen.

Um das Plug-in zu verwenden reicht es, wenn wir --append-plugins postgraphile-plugin-connection-filter beim Starten unseres PostGraphile-Servers per Kommandozeile mit einhängen.

Dazu erweitern wir zunächst das Standard-Dockerfile graphile/postgraphile. Erstellen wir also zunächst eine Datei Postgraphile.Dockerfile und fügen die Abhängigkeit hinzu:

# Postgraphile.Dockerfile
FROM graphile/postgraphile:latest
RUN yarn add postgraphile-plugin-connection-filter

Anschließend wollen wir dieses Dockerfile in unserem docker-compose-Stack verwenden und das Plug-in aktivieren:

#docker-compose.yml
 postgraphile:
    build: # Wir verwenden jetzt nicht nur das Standardimage, sondern unser eigenes Dockerfile 
      context: .
      dockerfile: Postgraphile.Dockerfile
    restart: unless-stopped
    network_mode: service:db
    depends_on:
      - db
    environment:
      PGPASSWORD: postgres
      PGUSER: postgres
      PGDATABASE: postgres
      PGHOST: localhost
    volumes:
      - "./postgraphile.tags.json5:/postgraphile/postgraphile.tags.json5"
    command:
      [
        "--enhance-graphiql",
        "--allow-explain",
        "--port",
        "5000",
        "--schema",
        "public",
        "--watch",
        "--append-plugins", # Aktivieren des Plug-Ins
        "postgraphile-plugin-connection-filter",
      ]

Beim nächsten Start, mit docker-compose up -d wird PostGraphile direkt mit dem neuen Plug-in gestartet. Dann werfen wir doch einen Blick auf unser GraphiQL:

Das Plug-In bietet uns direkt die filter-Funktion an, mit der wir nun auch komplexere Filter anwenden können. Wie hier beispielsweise numberOfAuthoredPosts: { greaterThanOrEqual: 1 }, um uns eben alle Writer auszugeben, die mindestens einen Post verfasst haben.

Wenn wir also gewisse Features vermissen, lohnt sich definitiv ein Blick auf die bereits genannte Liste; vielleicht hat ja schon jemand genau das gemacht, was wir brauchen.

Unser erstes eigenes Plug-in

In der realen Welt kommt es durchaus vor, dass man für das eigene API Daten von externen Quellen (APIs) anreichern muss. Um ein solches Szenario nachzuempfinden, fügen wir bei all unseren Blogposts ein Titelbild ein, welches im Frontend bei Bedarf verwendet werden kann.

Wie wäre es beispielsweise mit zufälligen Bildern von Katzen? Die Katzenbilder holen wir uns von docs.thecatapi.com. Um nicht in SQL eine Computed Column bauen zu müssen die uns vom API ein zufälliges Katzenbild holt, bauen wir uns ein eigenes Plug-in.

Umstellen auf den Library-Modus

Bisher haben wir PostGraphile im CLI-Modus verwendet. (Unser docker-compose führt einfach postgraphile als Start-Kommando aus). Wir können PostGraphile allerdings auch einfach aus einer Node-Anwendung heraus benutzen (selbst Node-Frameworks wie ExpressJS oder NestJS können verwendet werden).

Für unser Plug-in benötigen wir auch noch graphile-utils und axios,  um docs.thecatapi.com zu bedienen.

Also erstellen wir uns erst einmal einen Ordner graphql und starten darin ein neues Projekt mit yarn init. Anschließend installieren wir PostGraphile, unser bisher verwendetes Plug-in, sowie TypeScript und ts-node in unserem neuen Projekt:

yarn add postgraphile postgraphile-plugin-connection-filter typescript ts-node

Am besten fügen wir noch ein Script an unser package.json ein, um unsere Anwendung starten zu können. Meine package.json sieht dann erst einmal so aus:

{
  "name": "src",
  "version": "1.0.0",
  "main": "src/server.ts",
  "scripts": {
    "server": "ts-node src/server.ts"
  },
  "dependencies": {
    "axios": "^0.27.2",
    "graphile-utils": "^4.12.2",
    "postgraphile": "^4.12.9",
    "postgraphile-plugin-connection-filter": "^2.3.0",
    "ts-node": "^10.7.0",
    "typescript": "^4.6.4"
  },
  "devDependencies": {
    "@types/axios": "^0.14.0",
    "@types/node": "^17.0.33"
  }
}

Dann erstellen wir uns doch in unserem neuen Projekt einen Ordner src  und die Datei server.ts mit folgendem Inhalt, um zunächst unsere bisherigen Einstellungen zu replizieren:

import * as http from 'http';
import { postgraphile } from 'postgraphile';
import PostGraphileConnectionFilterPlugin  from 'postgraphile-plugin-connection-filter';
 
 
 
http.createServer(
    postgraphile(
        {
            host: process.env.PGHOST,
            user: process.env.PGUSER,
            password: process.env.PGPASSWORD,
            database: process.env.PGDATABASE,
        },
        "public", {
        watchPg: true,
        graphiql: true,
        enhanceGraphiql: true,
        appendPlugins:
            [PostGraphileConnectionFilterPlugin]
    })
).listen(5000);

Anschliessend sollten wir unser PostGraphile-Dockerfile noch aktualisieren, damit nun direkt unser Node server verwendet wird:

FROM node:alpine
 
COPY ./graphql/src /app/src
COPY ./graphql/package.json /app/package.json
WORKDIR /app
 
USER $USER
RUN yarn install
 
EXPOSE 5000
CMD [ "yarn", "server" ]

Dann benötigen wir noch ein paar Anpassungen an unserem docker-compose.yml:

postgraphile:
  build:
    context: .
    dockerfile: Postgraphile.Dockerfile
  restart: unless-stopped
  network_mode: service:db
  depends_on:
    - db
  environment:
    PGPASSWORD: postgres
    PGUSER: postgres
    PGDATABASE: postgres
    PGHOST: db # anstelle von localhost, sprechen wir nun direkt mit unserem Nachbar-Container 'db'
    CAT_API_KEY: # unseren API-Key

Nach einem erneuten docker-compose buildund docker-compose up sollte unser Schema unter http://localhost:5000/graphiql genauso aussehen, wie wir es von zuvor – aus dem CLI-Modus – kannten.

Katzenbilder!

Mit Hilfe der makeExtendSchemaPlugin-Funktion wird unser BlogPostTitleImagePlugin.ts sehr einfach zu bauen:

import { gql, makeExtendSchemaPlugin } from "postgraphile";
import axios from 'axios';
 
const BlogPostTitleImagePlugin = makeExtendSchemaPlugin(build => {
  return {
    typeDefs: gql'
    extend type Blogpost {
      titleImageUrl: String!
    }', // Wir erweitern den GraphQL-Typen 'Blogpost' um das Feld 'titleImageUrl'
    resolvers: {
      Blogpost: { // Wir nutzen eine Funktion, um unser neues Feld zu fuellen.
        titleImageUrl: async () => {
          const cat = await axios.get('https://api.thecatapi.com/v1/images/search?limit=1', {headers: {[ 'x-api-key']: process.env['CAT_API_KEY']!, 'Accept': 'application/json' } } )
          return cat.data[0]['url']
        }
      }
 
    }
  }
})
export default BlogPostTitleImagePlugin;

In unserer server.ts müssen wir natürlich noch eben das neue Plug-in verdrahten …

appendPlugins:
       [PostGraphileConnectionFilterPlugin, BlogPostTitleImagePlugin]

… und schon haben wir immer ein neues Katzenbild!

Übrigens …

In den Libray-Modus hätten wir nicht unbedingt wechseln müssen. Unser Plug-in hätte auch funktioniert, wenn wir unser BlogPostTitleImagePlugin.ts einfach kompiliert und mit –append-plugins BlogPostTitleImagePlugin.jsverwendet hätten. Aber da wir nun schon eine volle Node-Anwendung haben, erhalten wir deutlich mehr Flexibilität für unser API. Vielleicht wollen wir noch irgendeine Supersonder-Spezial-Notfall-Ganz-Wichtig-für-Legacy-Anwendung-Rest-Route hinzufügen oder ganz andere Experimente betreiben.

Zusammenfassung

Nachdem wir nun mit SmartTags unser Schema kontrollieren und mit Computed Colums Felder einfach mit SQL-Funktionen füttern können, haben wir zusätzlich noch ein externes API angebunden, um Katzenbilder auf unseren Blogposts anzuzeigen. Nebenbei haben wir mit dem Library-Modus nun absolute Kontrolle über unseren API-Server.

Mit diesen Werkzeugen an der Hand steht uns eigentlich nichts mehr Weg, unser GraphQL-API vollständig nach unseren eigenen Bedürfnissen zu gestalten.

PostGraphile ist nicht einfach nur ein kleines „Mal-Eben-Schnell-Boilerplate-CRUD-API“-Tool, obwohl wir es absolut dafür verwenden können. Wir haben hier eine äußerst mächtige und flexible Plattform, die uns bei unserer API-Gestaltung unterstützt ohne uns im Weg zu stehen.
Mit SQL-Funktionen zu arbeiten mag möglicherweise befremdlich wirken (zumindest war das für mich so), aber mit dem Plug-in-System sind wir nicht dazu gezwungen. Außerdem hat PL/pgSQL seinen eigenen Charme und eine gewisse Eleganz und Effizienz, was wir auch zu Schätzen gelernt haben.

Nachdem wir nun schon ein Passwort-Feld bei unseren Nutzern haben, wollen wir in einem späteren Blogpost Authentifikation mit JWT-Tokens, Registrierung und obendrein noch Rechte und Rollen handhaben, indem wir PostfreSQL’s Row-Level-Security-Feature (RLS) anwenden. Bleibt also dran, denn das Thema PostGraphile ist noch lange nicht durch!

Ich hoffe, ich konnte meine Begeisterung für PostGraphile ein wenig weitergeben. Und vielleicht kann dieser Post dazu führen, dass auch ihr einmal das eine oder andere Experiment damit wagt – egal, ob ihr eine bestehende Datenbank anbindet oder auf der grünen Wiese den Stack evaluiert.

Wir freuen uns auf jeden Fall, unsere Beispielanwendung weiterzuentwickeln – und vielleicht packen wir noch ein Frontend dazu, weil wir dank PostGraphile so viel Zeit gespart haben, dass wir uns ein bisschen Frontend-Hackerei auch noch erlauben können.

PostGraphile in-depth


Für neue Blogupdates anmelden:


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.