GraphQL MasterClass

PRÉ-REQUIS:

Vous devrez disposer d’un jdk 8, d’un IDE et si possible du plugin lombok.

Vous trouverez l’intégralité du code sur la branche master. Chacune des étapes de ce tutoriel dispose d’une branche que vous pourrez checkouter en cas de souci.

Depuis tout petit j’ai toujours été passionné d’aquariophilie. J’ai implémenté une petite application me permettant de stocker quelques informations sur mes poissons.

Étape 1 : L’état des lieux

A) PRISE DE CONNAISSANCE DE LA STRUCTURE DE L’APPLICATION :

Il s’agit d’une application Spring boot exposant une api Rest.

A noter que les données sont servies par une base de données H2.

Vous y trouverez un certain nombre de fonctionnalités déjà implémentées, l’idée de ce tutoriel est de se focaliser sur GraphQL.

B) EXAMINONS L’API :

Vous trouverez dans le répertoire doc des exports de configuration Postman vous permettant interroger l’api.

Rest se base sur le protocole HTTP et repose sur un concept de découpage des données en ressource. Dans notre cas nous en avons deux.

Lancez l’application et appelez les deux services REST.

{
    "id": 1,
    "name": "Cichlidae",
    "waterType": "FRESH"
}

Étape 2 : Setup GraphQL

Je souhaite faire évoluer mon application et mettre en place GraphQL.

Il existe plusieurs librairies java :
– https://www.graphql-java.com/
– https://www.graphql-java-kickstart.com/spring-boot/
– https://download.eclipse.org/microprofile/microprofile-graphql-1.0/microprofile-graphql.html

Pour ce tutoriel je choisis GraphQL Kickstart pour sa bonne intégration à Spring Boot.

A) DÉPENDANCES MAVEN :

<!-- GraphQL -->
   <dependency>
     <groupId>com.graphql-java-kickstart</groupId>
     <artifactId>graphql-spring-boot-starter</artifactId>
     <version>${graphql.version}</version>
   </dependency>
   <!-- to embed GraphiQL tool -->
   <dependency>
     <groupId>com.graphql-java-kickstart</groupId>
     <artifactId>graphiql-spring-boot-starter</artifactId>
     <version>${graphql.version}</version>
     <scope>runtime</scope>
   </dependency>

La première contient l’implémentation, GraphiQL quant à lui est un client web dont nous parlerons plus tard.

B) CONFIGURATION DE L’APPLICATION SPRING BOOT :

graphql:
  servlet:
    mapping: /graphql
    enabled: true
    corsEnabled: true
    # if you want to @ExceptionHandler annotation for custom GraphQLErrors
    exception-handlers-enabled: true
    contextSetting: PER_REQUEST_WITH_INSTRUMENTATION
graphiql:
  mapping: /graphiql
  endpoint:
    graphql: /graphql
    subscriptions: /subscriptions
  subscriptions:
    timeout: 30
    reconnect: false
  static:
    basePath: /
  enabled: true

application.yaml

C) DÉCLARATION DU SCHÉMA GRAPHQL :

MASTERCLASS-GRAPHQL
│   .gitignore
│   pom.xml
│   README.md
└───src
    └───main
        ├───java
        │
        └───resources
            │   application.yaml
            │   data.sql
            │
            └───graphql
                    fish.graphqls

Le Schéma est l’élément central de l’application. GraphQL est dit déclaratif, c’est à l’intérieur de ce fichier que sont déclarés  l’ensemble des objets exposés.

Définissons ensemble l’objet Family :

enum WaterType {
    SEA,
    FRESH
}

type Family {
    id: ID!
    # Name of the family
    name: String
    # Type of water
    waterType: WaterType
    " Fish inner the family"
    fishs: [Fish!]
}

Il existe un certain nombre de champs de base appelés Scalar (ID, String, Int, …). Les types tableau sont indiqués par des crochets []. Il est aussi possible de définir des validations simples, telles que la notion de champs obligatoires avec le signe « ! ». Il est aussi possible de documenter votre API en utilisant « # » ou les guillemets.

D) EN VOUS INSPIRANT DE CE QUI A ÉTÉ FAIT PRÉCÉDEMMENT ET EN VOUS AIDANT DE CETTE DOCUMENTATION, DÉCLAREZ LE SCHÉMA DE L’OBJET FISH.

Étape 3 : Les Query

L’une des grandes forces de GraphQL, c’est qu’il impose un découpage de ses fonctionnalités d’interrogations et de mutations (pattern CQRS).

A) CRÉATION D’UNE QUERY SIMPLE :

L’objectif de cette tâche est d’exposer en GraphQL la fonctionnalité getMostExpensiveFish présente dans la classe MostExpensiveFish.

Tout ajout de fonctionnalité GrapQL doit faire l’objet de déclaration dans le Schéma.

type Query {
    # The most Expensive Fish
    mostExpensiveFish: Fish
}

Créez un nouveau package par exemple « graphql » et ajoutez la classe FishQueryResolver implémentant l’interface GraphQLQueryResolver.


import com.coxautodev.graphql.tools.GraphQLQueryResolver;

@Component
public class FishQueryResolver implements GraphQLQueryResolver {

    private FishDatabaseService fishDatabaseService;

    @Autowired
    public FishQueryResolver(FishDatabaseService fishDatabaseService) {
        this.fishDatabaseService = fishDatabaseService;
    }

    public Fish getMostExpensiveFish() {
        return fishDatabaseService.getMostExpensiveFish();
    }

B) GRAPHIQL

Ce client web est automatiquement lancé par SpringBoot. Lancez l’application et rendez-vous sur localhost:8080/graphiql


Exécutez la query :

query{
  mostExpensiveFish {
    id,
    name,
    family {
      waterType
    }
  }
}

En GraphQL le client est un acteur tout aussi important que la partie serveur. C’est lui qui détient la responsabilité de définir les champs dont vous souhaitez récupérer les valeurs ! Il est tout à fait possible de le faire en Rest, mais il s’agit d’une implémentation à réaliser coté serveur, ici il s’agit du comportement natif du client! Cela permet de réduire considérablement la taille du payload, imaginez les gains en terme réseau !

C) QUERY AVEC PARAMÈTRE :

Ce type de query se déclare de la même façon, il suffit tout simplement de faire attention au mapping des arguments schéma / QueryResolver.

Déclarez dans le schéma la méthode findFishByName située dans la classe FishDatabaseService en suivant les conseils de la documentation officielle.

query{
 fishByName(name:"Discus") {
   id,
  name
 }
}

D) QUERY AVEC PARAMÈTRES PLUS COMPLEXE

Pour les requêtes nécessitant d’avantage de paramètres, GraphQL met à disposition du développeur la notion d’Input. Il se déclare de la même façon qu’un objet classique à la différence près que cet objet ne peut être situé qu’en paramètre.

Prenons l’exemple de la pagination:

Commençons par créer l’objet java :


package com.bmeynier.masterclass.spring.graphql.graphql.input;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Positive;
import javax.validation.constraints.PositiveOrZero;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class PaginationInput {
    @Positive
    private int first;
    @PositiveOrZero
    private int offset;
}

Puis la déclaration dans le schéma :


input PaginationInput {
    # Number of element returned
    first: Int
    # Index where element will be returned
    offset: Int
}
...
type Query {
    # Fish with pagination
    fishWithPagination(pagination: PaginationInput): [Fish]
}

Et dans le QueryResolver :

public List<Fish> fishWithPagination(PaginationInput paginationInput) {
     Fish first = fishDatabaseService.findByOffset(paginationInput.getOffset());
     return fishDatabaseService.findFish(first.getId(), paginationInput.getFirst());
 }
query{
 fishWithPagination(pagination: {first: 2, offset: 1}) {
  id,
  name
 }
}

E) METTEZ EN PLACE LA MÊME STRUCTURE POUR L’INPUT CURSORINPUT.

Étape 4 : Query coté client

A) LES FRAGMENTS :

Nous avons vu plus haut que le client était responsable de la récupération des champs et qu’il fallait énumérer un à un les champs récupérables. Il est possible grâce aux Fragments de définir un groupement de champs que l’on souhaite récupérer.

Lancez l’application et connectez-vous à l’interface de GraphiQL.

fragment standard on Fish {
 id,
 name
}

query{
 fishWithCursor(cursor: { first: 2,after: 3}) {
  ...standard,
  price
 }
}

Déclarez un fragment pour l’entity family et utilisez le sur la query mostExpensiveFish.

B) CLIENT PARAMÉTRABLE :

Dans GraphiQL se cache un petit onglet appelé « Variables »


Dans la zone query:

query Fish($name: String){ 
    fishByName(name: $name) { id, name } 
}

Puis dans la zone query variables:

{"name": "Discus"}

Utilisez cette technique sur la méthode fishWithPagination.

ÉTAPE 5 : LES RESOLVERS

Dans GraphQL kick start la notion de Resolvers est extrêmement importante. Le framework tente de résoudre le mapping Schéma/Code via le FieldResolver qui tentera d’effectuer un mapping par le nom de champ, puis par les méthodes commençant par getNAME, puis isNAME et enfin getNAME.

Il est aussi possible de créer son propre Resolver !

A) CRÉER UN RESOLVER PERSONNALISÉ :

Mettez le schéma à jour avec le type Purchase:

type Purchase {
    id:ID!
    shopName: String
}

type Fish {
    id: ID!
    "Fish name"
    name: String
    "Optimal temperature accept by the fish"
    temperature: Int
    "Price of the fish"
    price: Float
    "Family of the fish"
    family: Family
    "Information on the purchase"
    purchase: Purchase
}

Voici la classe Purchase :

package com.bmeynier.masterclass.spring.graphql.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Purchase {
    private long id;
    private String shopName;
}

Notre source de données sera un fichier json à mettre dans le répertoire resources:

[
  { "id": 1, "shopName": "BricoFish" },
  { "id": 2,  "shopName": "AquaFish" },
  { "id": 3,  "shopName": "FisherShop" }
]

Le code permettant de charger les données du fichier est le suivant :

public Purchase getPurchase(Fish fish) throws IOException {

   File purchasesDatasource = new File(getClass().getClassLoader().getResource("purchases.json").getFile());

   Purchase[] purchaseList = new ObjectMapper().readValue(purchasesDatasource, Purchase[].class);

   int randomIndex = new Random().nextInt(purchaseList.length);

   return purchaseList[randomIndex];
}

Déclarez une nouvelle classe appelé FishResolver :

@Component
public class FishResolver implements GraphQLResolver<Fish> {}

Complétez le code de la classe pour charger aléatoirement un achat.

Cet exercice, en agrégeant des données venant de deux sources différentes (sql et fichier json), de mettre en lumière un cas d’utilisation de GraphQL, la Gateway.

Étape 6 : Les Scalars personnalisés

Je souhaite maintenant préciser la date à laquelle j’ai effectué mon achat.

Le type Date ne fait pas parti des types scalar de base de GraphQL. Heureusement pour nous, GraphQL nous laisse la main pour créer nos propres types.

Enrichissons d’abord le schéma :

scalar Date

type Purchase {
    id:ID!
    shopName: String
    date: Date
}

Mettons à jour le fichier purchase.json avec des dates :

[
  { "id": 1, "shopName": "BricoFish", "date": "2009-01-12"},
  { "id": 2, "shopName": "AquaFish", "date": "2020-03-02"  },
  { "id": 3, "shopName": "FisherShop", "date": "1985-12-24"}
]

Cette fois-ci utilisons les configurators Spring pour déclarer notre nouveau type :

@Configuration
public class FishConfiguration {

    private static final String DATE_FORMAT = "yyyy-MM-dd";

    private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(DATE_FORMAT);

    @Bean
    public GraphQLScalarType getCustomDate() {
	GraphQLScalarType.newScalar().name("Date").description("A date with format " + DATE_FORMAT).coercing(new Coercing<LocalDate, String>() {
		 public String serialize(Object input) throws CoercingSerializeException {}
		@Override
		public LocalDate parseValue(Object input) throws CoercingParseValueException {}
		@Override
		public LocalDate parseLiteral(Object input) throws CoercingParseLiteralException {}

		}
	}
}

L’élément le plus important de la création d’un Scalar est le Coercing, c’est lui qui est en charge de serializer/deserializer le champ.

Voici le code (un peu complexe) du type Date :

@Bean
  public GraphQLScalarType getCustomDate() {
      return GraphQLScalarType.newScalar().name("Date").description("A date with format " + DATE_FORMAT).coercing(new Coercing<LocalDate, String>() {

          private LocalDate parseLocalDate(String s, Function<String, RuntimeException> exceptionMaker) {
              try {
                  TemporalAccessor temporalAccessor = dateFormatter.parse(s);
                  return LocalDate.from(temporalAccessor);
              } catch (DateTimeParseException e) {
                  throw exceptionMaker.apply("Invalid RFC3339 full date value : '" + s + "'. because of : '" + e.getMessage() + "'");
              }
          }

          @Override
          public String serialize(Object input) throws CoercingSerializeException {
              TemporalAccessor temporalAccessor;
              if (input instanceof TemporalAccessor) {
                  temporalAccessor = (TemporalAccessor) input;
              } else if (input instanceof String) {
                  temporalAccessor = parseLocalDate(input.toString(), CoercingSerializeException::new);
              } else {
                  throw new CoercingSerializeException(
                          "Expected a 'String' or 'java.time.temporal.TemporalAccessor' but was '" + input + "'."
                  );
              }
              try {
                  return dateFormatter.format(temporalAccessor);
              } catch (DateTimeException e) {
                  throw new CoercingSerializeException(
                          "Unable to turn TemporalAccessor into full date because of : '" + e.getMessage() + "'."
                  );
              }
          }

          @Override
          public LocalDate parseValue(Object input) throws CoercingParseValueException {
              TemporalAccessor temporalAccessor;
              if (input instanceof TemporalAccessor) {
                  temporalAccessor = (TemporalAccessor) input;
              } else if (input instanceof String) {
                  temporalAccessor = parseLocalDate(input.toString(), CoercingParseValueException::new);
              } else {
                  throw new CoercingParseValueException(
                          "Expected a 'String' or 'java.time.temporal.TemporalAccessor' but was '" + input + "'."
                  );
              }
              try {
                  return LocalDate.from(temporalAccessor);
              } catch (DateTimeException e) {
                  throw new CoercingParseValueException(
                          "Unable to turn TemporalAccessor into full date because of : '" + e.getMessage() + "'."
                  );
              }
          }

          @Override
          public LocalDate parseLiteral(Object input) throws CoercingParseLiteralException {
              if (!(input instanceof StringValue)) {
                  throw new CoercingParseLiteralException(
                          "Expected AST type 'StringValue' but was '" + input + "'."
                  );
              }
              return parseLocalDate(((StringValue) input).getValue(), CoercingParseLiteralException::new);
          }

      }).build();
  }

Ajoutez un champ date de type string dans la classe Purchase. Nous aurions pu mettre un type LocalDate, mais il aurait fallu implémenter un serialiser/deserialiser json ce qui n’est pas l’objectif de la formation.

Étape 7 : Mutation

A) ET SI ON INSÉRAIT DES DONNÉES ?

Pour cela implémentez l’interface MutationResolver dans une classe appelée FishMutationResolver.

package com.bmeynier.masterclass.spring.graphql.graphql;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.bmeynier.masterclass.spring.graphql.model.Family;
import com.bmeynier.masterclass.spring.graphql.model.WaterType;
import com.bmeynier.masterclass.spring.graphql.repository.FamilyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class FishMutationResolver implements GraphQLMutationResolver {

    private final FamilyRepository familyRepository;

    @Autowired
    public FishMutationResolver(FamilyRepository familyRepository) {
        this.familyRepository = familyRepository;
    }

    @Transactional
    public Family createFamily(String name, WaterType waterType) {
        return this.familyRepository.save(Family.builder().name(name).waterType(waterType).build());
    }
}

Ajoutez dans le schéma une nouvelle section Mutation :

type Mutation {
  createFamily(name: String, waterType: WaterType): Family
}
package com.bmeynier.masterclass.spring.graphql.graphql;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.bmeynier.masterclass.spring.graphql.model.Family;
import com.bmeynier.masterclass.spring.graphql.model.WaterType;
import com.bmeynier.masterclass.spring.graphql.repository.FamilyRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class FishMutationResolver implements GraphQLMutationResolver {

    private final FamilyRepository familyRepository;

    @Autowired
    public FishMutationResolver(FamilyRepository familyRepository) {
        this.familyRepository = familyRepository;
    }

    @Transactional
    public Family createFamily(String name, WaterType waterType) {
        return this.familyRepository.save(Family.builder().name(name).waterType(waterType).build());
    }
}

Rien de bien différent de la query, voici l’appel coté client :

mutation{
 createFamily(name: "Galeaspida",waterType: SEA){ name }
}

B) AJOUTEZ UNE FONCTIONNALITÉ DE MUTATION PERMETTANT D’AJOUTER UN POISSON PRENANT UN NOM DE FAMILLE. A CELA JETEZ UNE ERREUR DE TYPE NOSUCHELEMENTEXCEPTION LORSQU’AUCUNE FAMILLE N’EST TROUVÉE.

createFish(name: String, temperature: Int, price : Float, familyName: String): Fish

Voici un exemple de retour d’erreur :

{
  "errors": [
    {
      "message": "Impossible to find Family azert"
    }
  ],
  "data": {
    "createFish": null
  }
}

A noter qu’en cas d’erreur, GraphQL renvoie tout de même les attributs qu’il a réussi à récupérer, tout en énumérant les erreurs qu’il a rencontré.

ÉTAPE 8 : SUBSCRIPTIONS

GraphQL propose un système d’abonnement. Je souhaite implémenter un système pouvant m’alerter lorsqu’une nouvelle famille de poisson est ajoutée.

Nous allons utiliser les EventPublisher Spring :

package com.bmeynier.fmasterclass.spring.graphql.event;

import com.bmeynier.masterclass.spring.graphql.model.Family;
import org.springframework.context.ApplicationEvent;

public class FamilyCreationEvent extends ApplicationEvent {
    private Family family;

    public FamilyCreationEvent(Object source, Family family) {
        super(source);
        this.family = family;
    }
    public Family getFamily() {
        return family;
    }
}
private ApplicationEventPublisher applicationEventPublisher;
  private final FamilyRepository familyRepository;
  private final FishRepository fishRepository;

  @Autowired
  public FishMutationResolver(ApplicationEventPublisher applicationEventPublisher,
                              FamilyRepository familyRepository,
                              FishRepository fishRepository) {
      this.applicationEventPublisher = applicationEventPublisher;
      this.familyRepository = familyRepository;
      this.fishRepository = fishRepository;
  }

  @Transactional
  public Family createFamily(String name, WaterType waterType) {
      Family family = Family.builder().name(name).waterType(waterType).build();
      FamilyCreationEvent customSpringEvent = new FamilyCreationEvent(this, family);
      applicationEventPublisher.publishEvent(customSpringEvent);
      return this.familyRepository.save(family);
  }

Nous allons ensuite créer un publicateur d’événements sous forme de flowable :

package com.bmeynier.masterclass.spring.graphql.service;

import com.bmeynier.masterclass.spring.graphql.event.FamilyCreationEvent;
import com.bmeynier.masterclass.spricng.graphql.model.Family;
import com.bmeynier.masterclass.spring.graphql.model.Fish;
import com.bmeynier.masterclass.spring.graphql.model.WaterType;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.Observable;
import io.reactivex.ObservableEmitter;
import io.reactivex.observables.ConnectableObservable;
import org.reactivestreams.Publisher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import java.util.Queue;
import java.util.concurrent.*;

@Component
public class FamilyPublisher {

    private Flowable<Family> publisher;

    private Queue<Family> events= new ArrayBlockingQueue<>(30);

    @Autowired
    public FamilyPublisher() {
        this.initSubscriber();
    }

    private void initSubscriber() {
        Observable<Family> familyObservable = Observable.create(observableEmitter -> {
                    ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
                    executorService.scheduleAtFixedRate(newFamilyTick(observableEmitter), 0, 2, TimeUnit.SECONDS);
                }
        );
        ConnectableObservable<Family> connectableObservable = familyObservable.share().publish();
        connectableObservable.connect();

        this.publisher = connectableObservable.toFlowable(BackpressureStrategy.BUFFER);
    }

    private Runnable newFamilyTick(ObservableEmitter<Family> observableEmitter) {
        return () -> {
            while(!events.isEmpty()) {
                observableEmitter.onNext(events.poll());
            }
        };
    }

    @EventListener
    public void handleSuccessful(FamilyCreationEvent event) {
        events.add(event.getFamily());
    }

    public Publisher<Family> getPublisher() {
        return publisher;
    }
}

Coté GraphQL commençons par mettre à jour le schéma :

type Subscription {
	lastFamily: Family!
}

Puis comme pour les query/mutation implémenter l’interface SubscriptionResolver :

package com.bmeynier.masterclass.spring.graphql.graphql;

import com.coxautodev.graphql.tools.GraphQLSubscriptionResolver;
import com.bmeynier.masterclass.spring.graphql.model.Family;
import com.bmeynier.masterclass.spring.graphql.service.FamilyPublisher;
import org.reactivestreams.Publisher;t
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class FishSubscriptionResolver implements GraphQLSubscriptionResolver {

    private final FamilyPublisher familyPublisher;

    @Autowired
    public FishSubscriptionResolver(FamilyPublisher familyPublisher) {
        this.familyPublisher = familyPublisher;
    }

    public Publisher<Family> lastFamily(){
        return familyPublisher.getPublisher();
    }
}

Pour vérifier son fonctionnement nous allons implémenter un client Javascript à placer dans le répertoire resources/public/index.html.

<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Subscriptions over Web Sockets</title>
    <style>
        body {
            font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
            font-weight: 300;
        }

        .networking {
            display: none;
        }

        .stockTicker {
            border: solid black 1px;
            margin: 2px;
            min-height: 400px
        }

        .stockWrapper {
            display: block;
            padding: 20px;
            font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
            font-weight: 300;
        }

        .stockSymbol {
            font-weight: 600;
        }

        .stockPrice {
            font-weight: 600;
            color: red;
        }

        .stockUp {
            font-weight: 600;
            color: green;
        }
    </style>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">

    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

    <script>

        function networkBlip() {

            var $networking = $('.networking');
            if (!$networking.is(":visible")) {
                $networking.show(100, function () {
                    var $that = $(this);
                    setTimeout(function () {
                        $that.hide();
                    }, 500)
                });
            }
        }

        function subscribeToStocks() {
            var exampleSocket = new WebSocket("ws://localhost:8080/subscriptions");
            networkBlip();

            exampleSocket.onopen = function () {
                networkBlip();
                console.log("web socket opened");

                var query = 'subscription LastFamilySubscription \{ \n' +
                    '    lastFamily {' +
                    '       id ' +
                    '       name ' +
                    '     }' +
                    '}';
                var graphqlMsg = {
                    query: query,
                    variables: {}
                };
                exampleSocket.send(JSON.stringify(graphqlMsg));
            };

            var STOCK_CODES_UPDATES = {};

            exampleSocket.onmessage = function (event) {
                networkBlip();

                var data = JSON.parse(event.data);
                console.log(data);
            };
        }

        window.addEventListener("load", subscribeToStocks);
    </script>
</head>
<body>

<div class="jumbotron text-center">
    <h2>graphql-java Subscriptions</h2>
    <p>An example of graphql-java subscriptions sending continuous updates over websockets</p>
</div>


<div class="container">
    <div class="row">
        <div class="col-sm-1">
            <img src="https://avatars1.githubusercontent.com/u/14289921?s=200&v=4"
                 class="img-thumbnail" alt="graphql-java" width="400" height="400">
        </div>
        <div class="col-sm-3">
            <h3>Explanation</h3>
            <p>This demonstrates the use of graphql subscriptions and web sockets to send a stream of imagined stock price
                updates to this page.</p>
            <p>The updates are continuously sent from a server side publish and subscribe system (RxJava in this case) and
                pushed
                down to the browser client while applying graphql shapes to the subscription data</p>
            <p>The graphql query used in this example is :</p>
            <pre>

            </pre>
        </div>
        <div class="col-sm-8">
            <h3>Stock Price Updates</h3>
            <div class="stockTicker">Pending subscription...</div>
            <div class="networking"></div>
        </div>
    </div>
</div>
</body>
</html>

La connexion à l’adresse localhost:8080/index.html initialisera le client. Lancez la console de développement pour y voir plus clair. Il s’agit d’une communication web-socket standard avec initialisation de la conversation avec une requête http 101. En parallèle lancez GraphiQL et ajoutez une famille.

Étape 9 : Les outils

Kick start facilite l’intégration d’outils de l’écosystème GraphQL.

ALTAIR ET PLAYGROUND

Il s’agit de sur-couche à GraphiQL, ils ajoutent un certain nombre de fonctionnalités telles que la gestion des onglets, une meilleure intégration des subscriptions ou encore une personnalisation de l’interface. Ces outils sont également disponibles en client lourd.

VOYAGER

Et pour finir un outil de consultation du Graph.