FrEDA

FrEDA

  • Docs
  • Tutorial
  • Demo

›Backend

Präambel

  • Architektur
  • MonoRepo Struktur

External

  • PostgreSQL / Postgraphile
  • GraphiQL

Backend

  • GraphQL Server
  • GraphQL-Modules
  • TypeGraphQL
  • GraphQL-Playground
  • Besonderheiten
  • REST-Endpoints

Frontend

  • FrEDA ReactJS-Basics
  • Common Frontend-Library
  • CLI-Tools

Deploy

  • Deployment

Roadmap

  • geplante Weiterentwicklung

GraphQL-Modules

GraphQL-Modules erlaubt es, NodeJS-GraphQL-Server in einer strukturierten, DDD-Style Architiktur aufzubauen. Dinge die es vereinfacht und ermöglicht:

  • wiederverwendbare Module
  • skalierbare Struktur
  • Single-File-Concern in DDD-Style
  • Dependency-Injection
  • Testbarkeit

Für eine ausführliche Dokumentation des Frameworks bitte auf folgende Seite gehen:

https://graphql-modules.com/

simples GraphQL-Module Beispiel

Dieses Beispiel zeigt ein super simples GraphQL-Module, welches ohne Dependency-Injection, TypeORM, TypeGraphQL oder sonstiges Tools/Framework auskommt:

import { GraphQLModule } from '@graphql-modules/core';
import gql from 'graphql-tag';

export const MyFirstModule = new GraphQLModule({
  typeDefs: gql`
    type Query {
      user(id: ID!): User
    }
    
    type User {
      id: ID!
      username: String!
    }
  `,
  resolvers: {
    Query: {
      user: (root, { id }) => {
        return {
          _id: id,
          username: 'jhon'
        };
      }
    },
    User: {
      id: user => user._id,
      username: user => user.username
    }
  }
});

Module in Apollo-Server integrieren

Für ein funktionierenden Server benötigen wir noch ein Framwork wie Apollo-Server, welches mit dem generierten Module arbeiten kann . Das Beispiel ändert sich wie folgt:

import { GraphQLModule } from '@graphql-modules/core';
import * as express from 'express';
import * as graphqlHTTP from 'express-graphql';
import gql from 'graphql-tag';

const MyFirstModule = new GraphQLModule({
  typeDefs: gql`
    type Query {
      user(id: ID!): User
    }
    
    type User {
      id: ID!
      username: String!
    }
  `,
  resolvers: {
    Query: {
      user: (root, { id }) => {
        return {
          _id: id,
          username: 'jhon'
        };
      }
    },
    User: {
      id: user => user._id,
      username: user => user.username
    }
  }
});

const app = express();

app.use('/graphql', graphqlHTTP({
  schema: MyFirstModule.schema,
  graphiql: true
}));

app.listen(4000);

Dies ist ein komplett funktionierende GraphQL-Server in NodeJS, mit Verwendung eines GraphQL-Modules. Code-Splitting wird hier nicht gezeigt, sollte in einer ordenlichen Architektur stattfinden. Das Module würde demzufolge in einem extra Ordner liegen, welcher per Import in die Root-Serverdatei importiert wird.

GraphQL-Module from external/remote GraphQL-Server like Postgraphile

Um einen bestehenden GraphQL-Server in den GraphQL-Gateway einzuspeisen lässt sich dieser via Introsepction auslesen um das Schema und Resolver zu integrieren. Dabei wird das Schema des Remote-GraphQL-Servers mit dem Schema des Gateways gemerged, auch Schema-Stitching genannt. Hierfür gibt es Tools wie graphql-tools oder graphql-modules, welches merging/stitching ermöglichen.

Prodat GraphQL-Module

Das Prodat Modul bezieht die Daten aus dem externen remote Postgraphile-Server welcher zuvor gestartet wurde. Das Modul hierzu befindet sich unter

backend/freda-middleware/src/modules/prodat/index.module.ts

Filtering, Whitelisting, Transforming, Wrapping

Da die Prodat-Datenbank und ihr Schemen sehr viele Tabellen und Funktionen hat, wollen wir diese nicht alle in den GraphQL-Gateway durchreichen. Um dies zu filtern nutzen wir die Transform-Funktion des Zusatzmodules graphql -modules. Genaure Infos dazu findest du unter GraphQL-Transforming und der allgemeinen Doku von graphql-tools: https://www.graphql-tools.com/

Über folgenden Code im Prodat-GraphQL-Module transformieren wir das Schema und filtern RootTypes, Types und Wrappen das ganze auch noch mit einem zustzlichen Type, damit das Prodat-Module unterhalb von dem Feld "prodat" und dem Typ ProdatQuery/ProdatMutation liegt:

 const transformedSchema = transformSchema(prodatSchema, [
   new FilterRootFields((_type, name) => {
     return whitelistedRootFields.includes(name); //return false if should filter/omit
   }),
   new FilterTypes(type => {
     return whitelistedTypes.includes(type.name);
   }),
   new WrapType('Query', 'ProdatQuery', 'prodat'),
   new WrapType('Mutation', 'ProdatMutation', 'prodat'),
 ]); 

Dieses transformed Schema können wir in ein neues GraphQL-Module geben über:

const prodatModule = new GraphQLModule({
    name: 'prodat',
    extraSchemas: () => [
      transformedSchema,
    ],
  });

Introspection

Die Introspection des bestehnden Postgraphile-GraphQL-Servers ist nötig um das Schema und die Resolver des remote /extern GraphQL-Servers weiterzureichen. Diese Introspection findet aktuell noch im Root des GraphQl-Gateways -Servers statt unter backend/freda-middleware/src/server.ts in folgender Zeile 20:

const prodatGraphQlSchema = await prodatSchema();

"prodatSchema" ist eine Funktion, welche das Introspection betreibt. Hier gibt es optimierungsbedarf, um die Modularität das GraphQL-Gateway weiterhin zu garantieren und einer DDD-Architektur zu verfolgen. Die Datei hierzu ist zu finden unter backend/freda-middleware/src/externalSchemas/fredaSchema.ts

REST-DataSource GraphQL-Module

Ein schönes Beispiel wie man eine REST-Schnittstelle in den Gateway integriert ist bei dem amazonStock-Module zu finden:

backend/freda-middleware/src/modules/amazonStock/amazonStock.module.ts:

import {GraphQLModule} from '@graphql-modules/core';
import {gql} from "graphile-utils";
import {AmazonStockAPI} from "./provider/amazonStock.provider";

export const amazonStockModule = new GraphQLModule({
  providers: [AmazonStockAPI],
  typeDefs: gql`
    type AmazonResult {
      ASIN: String,
      title: String,
      price: String,
      listPrice: String,
      imageUrl: String,
      detailPageURL: String,
      rating: String,
      totalReviews: String,
      subtitle: String,
      isPrimeEligible: String,
    }
    type Query {
      getAmazonStock(keywords: String!): [AmazonResult]
    }
  `,
  resolvers: {
    Query: {
      getAmazonStock: (_root, args, context, _info) => context.injector.get(AmazonStockAPI).search(args.keywords)
    }
  }
});

Das GraphQl-Module generiert die GraphQL-Typen und der Resolver verweist per Dependency-Injection auf die implentierte Logik zum ansteuern der REST-Schnittstelle

Im Provider ist die REST-Schnittstelle mit ihren Endpunkten implentiert (in diesem Fall nur ein einziger Endpunkt):

import { RESTDataSource } from 'apollo-datasource-rest';
import {Injectable, ProviderScope} from '@graphql-modules/di';

@Injectable({
  scope: ProviderScope.Session
})
export class AmazonStockAPI extends RESTDataSource {
  baseURL = 'https://amazon-price1.p.rapidapi.com/';

  willSendRequest(request: any) {
    request.headers.set('x-rapidapi-host', "amazon-price1.p.rapidapi.com");
    request.headers.set('x-rapidapi-key', "55bd2a18d9mshc8ad60f9aa663f5p16c29ajsnb7ab52205778");
  }

  async search(keywords: string) {
    return this.get(`search?marketplace=DE&keywords=${keywords}`);
  }
}

Das Modul kann dann über ein Import-Statement in ein anderes GraphQL-Module integriert werden:

const exampleModule = new GraphQLModule({
    name: 'exampleModule',
    imports: [amazonStockModule, otherModule1, otherModule2],
});

GraphQL-Modules kümmert sich um das mergen/sitchen der Module/Schemas.

Relation zwischen verschiedenen GraphQL-Modules

In einigen Fällen ist es gewünscht eine Relation zwischen verschieden DataSources herzustellen. Hierfür verwenden wir die Funktion des Frameworks graphql-modules. Ein passendes Beispiel findet man aktuell unter

backend/freda-middleware/src/modules/fredaPublic/index.module.ts

 const mergedSchema = mergeSchemas({
   schemas: [
     authModule,
     prodatModule,
     mediaModule,
     amazonStockModule,
     twitterModule,
   ],
   typeDefs: gql`
     extend type WawiLiefschBelpTerminIsnull {
       twitterPostsByAkBez: [Tweet]
     }
   `,
   resolvers: {
     WawiLiefschBelpTerminIsnull: {
       twitterPostsByAkBez: {
         fragment: `... on WawiLiefschBelpTerminIsnull { akbez }`,
         resolve: (r, _args, context, info) => {
           return info.mergeInfo.delegateToSchema({
             schema: twitterModule,
             operation: 'query',
             fieldName: 'twitterSearch',
             args: {
               q: r.akbez.split(' ')[0]
             },
             context,
             info,
           });
         },
       },
     },
   },
 }); 

Dieses Beispiel recht den ersten Teil einer Zeichenfloge der Artikelbezeichnung weiter an das Twitter-Modul. Wichtig dabei ist, dass der GraphQL-Typ, welcher die Relation bekommen soll, eine Felderweierung im GraphQL-Schema bekommt . Dies lässt sich über diesen Part realisieren:

const mergedSchema = mergeSchemas({
/* --- some more code --- */
typeDefs: gql`
 extend type WawiLiefschBelpTerminIsnull {
   twitterPostsByAkBez: [Tweet]
 }
`,
/* --- some more code --- */
}); 

Wichtig ist das Keyword extend in der Schemadefinition, welches den Typ erweitert. Für das neue Feld twitterPostsByAkBez wird dann ein Resolver implementiert, welcher über die Funktion "delegateToSchema" zum richtigen GraphQL-Module-Resolver mit entsprechenden Argumenten weiterleitet.

delegateToSchema + fragment

Um sicherzustellen das die Delegation das abzufragene Feld 'akBez' beinhaltet kann über das Fragment das Feld obligatorisch hinzugefügt werden, egal ob der Nutzer das Feld in der Query mit abfragt oder nicht.

Custom Businesslogic

Vor und Nach GraphQL-Abfragen muss es möglich sein zusätzliche Logik auszuführen. Ein weg wäre es die Logik direkt im Resolver oder über ein Provider per Dependency-Injection zu implentieren, wie wir vorab schon gezeigt haben. Ein weiterer Weg sind Resolver-Compositions.

Resolver Composition

Eine Funktion welche von GraphQL-Modules zur Verfügung gestellt wird, um Anhand von RootTypes, Types oder Type-Fields Funktionen per Event triggern zu lassen. Ein GraphQL-Module mit Resolver-Composition sehe z.B. so aus:

return new GraphQLModule({
    name: 'fredaPublicModule',
    imports: [authModule, prodatModule, commonModule, mediaModule, amazonStockModule, twitterModule],
    extraSchemas: () => [
      transformedSchema,
    ],
    resolversComposition: {
      'Query': [loggingMiddleware()],
      'Mutation': [loggingMiddleware()],
      'FredaPublicMutation.updateJwtToken': [isAuthMiddleware()]
    },
  });

Dieses Module legt fest, das alle Queries und alle Mutations grundsätzlich eine Logginsfunktion ausführen sollen. Au ßerdem ist eine Authorisation für das Ausführen des Type-Fields / der Resolvers-Funktion "updateJwtToken" notwendig.

Im Resolver-Composition-Array können auch mehrere Funktionen aufgerufen werden. Siehe auch:

https://graphql-modules.com/docs/introduction/resolvers-composition

Example Use-Cases

  • Authorisation
  • Authentication
  • Logging
  • Side-Effect
  • Events / Trigger

before Resolver Execution

Logik auszuführen, bevor der eigentliche Resolver ausgeführt wird sieht in etwa so aus:

export const isAuthMiddleware = () => {
  return (next: any) => {
    return (root: any, args: any, context: MyContext, info: any) => {
      isAuthService(context, info);
      return next(root, args, context, info);
    };
  };
};

Die Funktion "isAuthService" schmeißt einen Error wenn der Request keine Authentication beinhaltet, der Request schl ägt damit fehl und der eigentliche Resolver, welcher über "next" weitergeleitet wird, wird erst gar nicht ausgeführt.

after Resolver Execution

Logiken nach dem eigenltichen Resolver auszuführen ist etwas komplizerter, aber möglich. Angenommen man benötigt zuerst den Datenbank-Wert bevor ich Logik ausführen kann. Eine Resolver-Composition Funktion würde wie folgt aussehen:

export const compositionAfterResolverMiddleware = () => {
 return (next: any) => {
   return (root: any, args: any, context: MyContext, info: any) => {
     const nextObj = next(root, args, context, info);
     nextObj.then((e: any) => {
       if(e.user.preventDelete == 1) {
         throw new Error('User cant be deleted');
       }
     });
     return nextObj;
   };
 };
}; 

Dieses fiktive Beispiel fragt ab ob der Nutzer gelöscht werden darf, und benötigt dazu vorest das Nutzerobjekt und den Wert des Feldes "preventDelete".

Dependency-Injection

Beispiel für Dependency-Injection ist im Resolver des auth-GraphQL-Modules zu finden, welches mit TypeGraphQL geschrieben ist:

Dependency-Injection-Example

Für ausführliche Infos bitte hier schauen:

https://graphql-modules.com/docs/introduction/dependency-injection#custom-injectables

Providers

GraphQL-Module Provider-Injection für Auth-Logik

Es empfiehlt sich die Logik der jeweiligen Resolver in einem Provider auszulagern. So lassen sich recht zügik Testlogiken oder weitere DataProvider im gleichen Interface austauschen.

Der aktuell genutzte Provider greift über GraphQL-Binding auf den Freda-AdminUI-Postgraphile-Server auf die Nutzerdaten des Freda-Framworks zu. Denkbar wäre, einen TypeORM Provider oder ein externen Auth-Provider zu schreiben , welcher die Authentifizierngslogiken übernimmt.

Der aktuelle Provider ist hier zu finden backend/freda-middleware/src/modules/auth/provider/auth.provider.ts:

@Injectable({
  scope: ProviderScope.Session,
})
export class AuthProvider implements AuthProviderInterface {
  @Inject() private module: ModuleSessionInfo;
  @Inject(USERS_PROVIDER) private usersProvider: UsersProviderInterface;

  constructor(@Inject('fredaGraphQlSchemaBinding') private _binding: Binding) {
  }

  private get req(): Request {
    return this.module.session.req || this.module.session.request;
  }

  private get res(): Response {
    return this.module.session.res;
  }

  async signUp(data: RegisterInput, _info: GraphQLResolveInfo): Promise<User> {
    let userInput: any = {
      firstName: data.firstName,
      lastName: data.lastName,
      email: data.email,
      password: hashSync(data.password, genSaltSync(8)),
    };
    if (data.roles) {
      userInput = {
        ...userInput,
        userRolesRolesUsingId: {
          create: data.roles.map(r => ({roleId: r}))
        }
      };
    }
    const fragment = `fragment MutationPayload on CreateUserPayload { 
      user {
        nodeId
        id
        firstName
        lastName
        email
        userRolesRolesByUserId {
          nodes {
            roleByRoleId {
              nodeId
              id
              name
            }
          }
        }
      } 
    }`;
    const createUserPayload = await this._binding.mutation.createUser({input: {user: userInput}}, fragment);
    return createUserPayload.user;
  }

  /* --- more methods for provider --- */
}  

Damit ein GraphQL-Module auf die Provider zugreifen können müssen diese bei Modul-Definition angegeben werden:

import { USERS_PROVIDER } from './symbol/user.symbol';
import { AUTH } from './symbol/auth.symbol';

const resolvers = [
  UserResolver,
];

export const getAuthModule = (fredaGraphQLProxyModule: any) => {

  return new GraphQLModule({
    name: 'authModule',
    imports: [fredaGraphQLProxyModule, commonModule],
    providers: () => [
      { provide: AUTH, useClass: AuthProvider },
      { provide: USERS_PROVIDER, useClass: UsersDbProvider },
      ...resolvers,
    ],
    extraSchemas: () => [
      buildSchemaSync({
        resolvers,
        container: ({ context }) => context.injector,
      }),
    ],
    resolversComposition: {
      'Mutation.updateUserAccount': [updateUserMutationMiddleware()],
    },
  });
};

USERS_PROVIDER und AUTH sind eindeutig generierte Strings, welche für die Dependency-Injection und dem Provider genutzt werden um die richtige Klasse zu finden.

Für ausführliche Infos bitte hier schauen:

https://graphql-modules.com/docs/introduction/dependency-injection#providers

← GraphQL ServerTypeGraphQL →
  • simples GraphQL-Module Beispiel
    • Module in Apollo-Server integrieren
  • GraphQL-Module from external/remote GraphQL-Server like Postgraphile
    • Prodat GraphQL-Module
  • REST-DataSource GraphQL-Module
  • Relation zwischen verschiedenen GraphQL-Modules
    • delegateToSchema + fragment
  • Custom Businesslogic
    • Resolver Composition
  • Dependency-Injection
  • Providers
    • GraphQL-Module Provider-Injection für Auth-Logik
FrEDA
Docs
Getting StartedFrontendBackendFrEDA Users
Tutorial
RequirementsSetup DevelopmentQuick run
Mehr
Prodat-SQLHochschule MittweidaDEVTIM IT Softwareentwicklung
Facebook Open Source
Copyright © 2021 Prodat-SQL. Built with ❤ and Docusaurus.