Introduction

GraphQL kézako ?

GraphQL est un langage de requête initié par Facebook en 2012 et développé en 2015. Facebook Manifest. GraphQL permet de se brancher à n'importe quel type de base de données ou d'API. Le but de GraphQL est de décrire les données et les fonctions disponibles entre les applications client-serveur.

GraphQL ne stocke donc pas de données. Il va seulement décrire la donnée et savoir comment aller la récupérer sur vos différentes applications backend.

Je vous invite à lire l'article de notre blog expliquant comment fonctionne GraphQL.

Qu'allons-nous faire ?

Dans ce tutoriel nous allons mettre en place un serveur GraphQL via le framework Apollo.

Le but est de comprendre :

  • la mise en place d'un serveur GraphQL ;
  • la création des requêtes pour lire la donnée ;
  • la création des requêtes d'ecriture des données.

Pré-requis

Nous allons utiliser une base de données PostgreSQL pour le stockage des données.

Le serveur Apollo sera en NodeJS en version 9. Nous utiliserons Yarn comme gestionnaire de dépendance.

Le code javascript sera en ES6 avec l'utilisation de Babel pour la compilation.

Si vous ne souhaitez pas installer node sur votre machine, vous pouvez utiliser Docker. Le code fourni pour le tutoriel disponible ici, contient un fichier docker-compose.yml vous permettant d'installer le projet.

Installation du serveur GraphQL

Installation de docker

Si vous souhaitez utiliser Docker, je vous invite à cloner le projet github.

Une fois cloné vous pouvez lancer :

docker-compose up -d

et vous devez ajouter la ligne suivante dans votre /etc/hosts :

127.0.0.1 apollo.local

Si vous travaillez dans le container docker, les commandes suivantes doivent être lancées dans la machine docker :

docker-compose exec node sh

Initialisation de l'environnement Node

On commence par configurer le gestionnaire de package yarn :

yarn init

Suivre les instructions en laissant comme entry point le fichier index.js.

Il faut ensuite installer l'ensemble des packages suivant pour l'utiliser babel :

yarn add --dev babel-cli babel-preset-env babel-preset-es2015 babel-preset-stage-0

Puis vous devez installer les packages apollo-server-express, graphql et express :

yarn add apollo-server-express graphql express

Une fois terminé, vous devez ajouter le script pour start le projet. Dans le fichier package.json il faut ajouter :

"scripts": { "start": "babel-node index.js" }

Mise en place du serveur

Il nous reste à mettre en place le serveur express qui permettra de lancer le GraphQL.

Ajouter le code suivant dans le fichier index.js :

import express from 'express'; import bodyParser from 'body-parser'; import { graphqlExpress } from 'apollo-server-express'; const PORT = 3000; const app = express(); app.get('/', function (req, res) { res.send('Hello World!') }) app.listen(PORT);

Si tout est ok, vous devriez, en faisant un yarn start avoir le résultat suivant sur l'url 127.0.0.1:3000

Hello Word

Retrouvez le code directement ici

Installation du serveur GraphQL

Création de la base de données

Si vous utilisez le container docker, la base de données PostgreSQL est comprise dans le projet.

Si vous n'utilisez pas le docker, vous devez installer un PostgreSQL sur votre machine via la documentation suivante

Création du schéma

Pour gérer la communication avec PostgreSQL, nous allons utiliser la librairie Knex.

Pour cela il faut l'installer via Yarn :

yarn add knex pg

Nous allons commencer par gérer la connexion à la base de données en ajoutant un fichier pg.js à la racine du projet.

import Knex from 'knex'; const config = { host: 'pg', user: 'pg', password: 'pg', database: 'graphql' }; const pg = Knex({ client: 'pg', connection: config }); export default pg;

Puis nous allons créer le schéma de base de données. Dans la suite du tutoriel nous allons imaginer que l'application doit gérer les astronautes d'Eleven Labs, les liens avec leurs planètes et leurs grades.

Commençons par créer le dossier schemas qui contiendra l'ensemble des schémas de la base de données.

Astroanutes

Ajoutez le fichier astronaute.js contenant la table astronaute :

const up = function up(pg) { return pg.schema.createTable('astronaute', function (table) { table.increments(); table.string('pseudo'); table.string('photo'); table.integer('grade_id'); table.foreign('grade_id').references('grade.id'); table.timestamps(true, true); }); }; const down = function down(pg) { return pg.schema.hasTable('astronaute').then(function (exists) { if (exists) { return pg.schema.table('astronaute', function (table) { table.dropForeign('grade_id'); }).then(() => { return pg.schema.dropTable('astronaute'); }); } }); }; export default { up, down };

Nous utiliserons les fonctions up et down pour la création de la base.

Planets

Ajoutez le fichier planet.js contenant la table planet :

const up = function up(pg) { return pg.schema.createTable('planet', function (table) { table.increments(); table.string('name'); table.string('logo'); table.timestamps(true, true); }); }; const down = function down(pg) { return pg.schema.dropTableIfExists('planet'); }; export default { up, down };

Planets-Astronautes

Ajoutez le fichier planet-astronaute.js contenant la table de liaison entre un astronaute et sa planète :

const up = function up(pg) { return pg.schema.createTable('planet-astronaute', function (table) { table.increments(); table.integer('planet_id'); table.integer('astronaute_id'); table.foreign('planet_id').references('planet.id'); table.foreign('astronaute_id').references('astronaute.id'); table.timestamps(true, true); }); }; const down = function down(pg) { return pg.schema.hasTable('planet-astronaute').then(function (exists) { if (exists) { return pg.schema.table('planet-astronaute', function (table) { table.dropForeign('planet_id'); table.dropForeign('astronaute_id'); }).then(() => { return pg.schema.dropTable('planet-astronaute'); }); } }); }; export default { up, down };

Grades

Ajouter le fichier grade.js contenant la table des grades :

const up = function up(pg) { return pg.schema.createTable('grade', function (table) { table.increments(); table.string('name'); table.timestamps(true, true); }); }; const down = function down(pg) { return pg.schema.dropTableIfExists('grade'); }; export default { up, down };

Création de la base

Ajouter le fichier index.js permettant de générer la base de données :

import pg from './../pg'; import astronaute from './astronaute'; import planet from './planet'; import grade from './grade'; import planetAstronaute from './planet-astronaute'; planetAstronaute.down(pg).then(() => { console.log('planet-astronaute DROP'); return astronaute.down(pg); }).then(() => { console.log('astronaute DROP'); return grade.down(pg); }).then(() => { console.log('grade City DROP'); return planet.down(pg); }).then(() => { console.log('planet DROP'); return planet.up(pg); }).then(() => { console.log('planet CREATE'); return grade.up(pg); }).then(() => { console.log('grade CREATE'); return astronaute.up(pg); }).then(() => { console.log('astronaute CREATE'); return planetAstronaute.up(pg); }).then(() => { console.log('planetAstronaute City CREATE'); }).then(() => { console.log('Success !'); process.exit(); }).catch((e) => { console.log(e); });

Vous pouvez ajouter votre script suivant dans le package.json :

"scripts": { "start": "babel-node index.js", "pg": "babel-node schemas/index.js" }

Si vous lancez yarn pg, vos tables sont créées.

Retrouvez le code directement ici

Création des types GraphQL

Types objet

Nous allons commencer par créer les types GraphQL pour les trois principaux objets :

  • Astronaute
  • Planète
  • Grade

Commencez par créer le dossier typedefs qui contiendra les types GraphQL.

Grade

On commence par grade, qui est l'objet le plus simple : il ne contient que le nom du grade.

Ajoutez le fichier grade.js avec le code suivant :

const Grade = ` type Grade { id: Int! name: String! } `; export default Grade;

Planète

Ajoutez le fichier planet.js avec le code suivant :

const Planet = ` type Planet { id: Int! name: String! logo: String! astronautes: [Astronaute] } `; export default Planet;

Comme vous le remarquez, le type GraphQL ne suit pas directement le type postgreSQL. Ici on permet la récupération directement dans l'object planet de l'ensemble des astronautes.

Astronaute

Ajoutez le fichier astronaute.js avec le code suivant :

const Astronaute = ` type Astronaute { id: Int! pseudo: String! photo: String grade: Grade! planet: Planet! } `; export default Astronaute;

Dans le cas de l'astronaute, l'objet contient directement le grade et la planet.

Retrouvez le code directement ici

Resolver des queries

Création du type Query

Avant de mettre en place les resolvers pour les query en lecture, vous devez créer le type query.

Il faut ensuite créer un type avec l'ensemble des fonctions que vous souhaitez avoir. Nous allons :

  • récupérer l'ensemble des astronautes
  • récupérer un astronaute
  • récupérer une planète

Ajoutez le fichier schemas.js à la racine de votre projet. Importez l'ensemble des types que nous avons défini à l'étape précédente :

import Astronaute from './typedefs/astronaute'; import Planet from './typedefs/planet'; import Grade from './typedefs/grade'; const RootQuery = ` type RootQuery { astronaute(id: Int!): Astronaute, astronautes: [Astronaute] planet(id: Int!): Planet } `; const SchemaDefinition = ` schema { query: RootQuery } `;

Configuration de GraphQL

Toujours dans le même fichier schemas.js vous devez dire à votre serveur GraphQL où est votre schéma. Pour cela vous devez ajouter le module graphql-tools :

yarn add graphql-tools

Ce module expose la fonction makeExecutableSchema qui prend en paramètre vos types.

Vous devriez avoir le code suivant :

import { makeExecutableSchema } from 'graphql-tools'; import { resolvers } from './resolver'; import Astronaute from './typedefs/astronaute'; import Planet from './typedefs/planet'; import Grade from './typedefs/grade'; const RootQuery = ` type RootQuery { astronaute(id: Int!): Astronaute, astronautes: [Astronaute] planet(id: Int!): Planet } `; const SchemaDefinition = ` schema { query: RootQuery } `; export default makeExecutableSchema({ typeDefs: [SchemaDefinition, RootQuery, Astronaute, Planet, Grade], resolvers: resolvers, });

Ajoutez le resolver

Ajoutez un fichier resolver.js contenant seulement :

export const resolvers = {}

Activez le serveur

Il nous reste à indiquer à GraphQL comment récupérer notre schéma. Dans le fichier index.js vous devez importer ledit schéma :

import express from 'express'; import bodyParser from 'body-parser'; import { graphqlExpress, graphiqlExpress } from 'apollo-server-express'; import schema from './schemas'; const PORT = 3000; const app = express(); app.use('/graphiql', graphiqlExpress({ endpointURL: '/graphql' })); app.use('/graphql', bodyParser.json(), graphqlExpress({ schema })); app.listen(PORT);

Nous ajoutons au même moment l'IDE GraphiQL qui est contenu dans la librairie Apollo. L'IDE permet d'afficher directement la documentation, ainsi que d'effectuer les query.

Si tout est ok, vous devriez avoir accès à l'url suivante http://127.0.0.1:3000/graphiql et voir la doucmentation (à droite).

Documentation

Création des resolvers

Si vous essayez la query :

{ astronautes { id } }

Vous devriez voir la réponse suivante :

{ "data": { "astronautes": null } }

Et oui, pour l'instant vous n'avez aucun resolver !

Le resolver est le code qui permet de récuperer la donnée dans la base.

Dans un dossier resolvers vous devez ajouter le fichier astronautes.js avec le code suivant :

import pg from './../pg'; import Astronaute from '../typedefs/astronaute'; const resolvers = { RootQuery: { async astronautes() { return await pg.select().table('astronaute'); } }, }; export default resolvers;

Dans le fichier resolver.js il vous faut ajouter le resolver que nous venons de définir :

import AstronauteResolver from './resolvers/astronaute'; export const resolvers = AstronauteResolver;

Si tout est ok la réponse à votre requête est :

{ "data": { "astronautes": [] } }

Et si vous ajoutez des astronautes dans votre base de donnnées et changer la requête en :

{ astronautes { id, pseudo } }

la réponse est donc :

{ "data": { "astronautes": [ { "id": 1, "pseudo": "CaptainJojo" }, { "id": 2, "pseudo": "Pouzor" }, { "id": 3, "pseudo": "Franky" } ] } }

Maintenant nous allons modifer le resolver pour récupérer via l'id :

import pg from './../pg'; import Astronaute from '../typedefs/astronaute'; const resolvers = { RootQuery: { async astronautes() { return await pg.select().table('astronaute'); }, async astronaute(obj, args, context, info) { return (await pg .select() .table('astronaute') .where('id', args.id) .limit(1)).pop(); }, }, }; export default resolvers;

Puis nous allons résoudre la récupération du grade et de la planet :

import pg from './../pg'; import Astronaute from '../typedefs/astronaute'; const resolvers = { RootQuery: { async astronautes() { return await pg.select().table('astronaute'); }, async astronaute(obj, args, context, info) { return (await pg .select() .table('astronaute') .where('id', args.id) .limit(1)).pop(); }, }, Astronaute: { async grade(astronaute) { return (await pg .select() .table('grade') .where('id', astronaute.grade_id) .limit(1)).pop(); }, async planet(astronaute) { return ( await pg .select() .table('planet') .innerJoin('planet-astronaute', 'planet-astronaute.planet_id', '=', 'planet.id') .where('planet-astronaute.astronaute_id', astronaute.id) .limit(1)).pop(); } }, }; export default resolvers;

Comme vous pouvez le voir, c'est assez simple, il suffit de spécifier pour chaque attribut comment le récupérer.

Enfin créons le resolver pour la planet.

Ajouter le fichier planet.js au dossier resolvers :

import pg from './../pg'; import Planet from '../typedefs/planet'; const resolvers = { RootQuery: { async planet(obj, args, context, info) { return (await pg .select() .table('planet') .where('id', args.id) .limit(1)).pop(); }, }, Planet: { async astronautes(planet) { return (await pg .select() .table('astronaute') .innerJoin('planet-astronaute', 'planet-astronaute.astronaute_id', '=', 'astronaute.id') .where('planet-astronaute.planet_id', planet.id) ); } }, }; export default resolvers;

Puis dans le fichier resolver.js vous devez ajouter le resolver :

import { merge } from 'lodash'; import AstronauteResolver from './resolvers/astronaute'; import PlanetResolver from './resolvers/planet'; export const resolvers = merge(AstronauteResolver, PlanetResolver);

Si tout est ok, la requête suivante doit fonctionner :

{ astronautes { id, pseudo }, astronaute(id: 1) { id, pseudo grade { id, name }, planet { id, name } }, planet(id: 2) { id, astronautes { id, pseudo, grade { name } } } }

La réponse doit ressembler à cela :

{ "data": { "astronautes": [ { "id": 1, "pseudo": "CaptainJojo" }, { "id": 2, "pseudo": "Pouzor" }, { "id": 3, "pseudo": "Franky" } ], "astronaute": { "id": 1, "pseudo": "CaptainJojo", "grade": { "id": 1, "name": "admiral" }, "planet": { "id": 1, "name": "duck" } }, "planet": { "id": 2, "astronautes": [ { "id": 2, "pseudo": "Pouzor", "grade": { "name": "rookie" } }, { "id": 3, "pseudo": "Franky", "grade": { "name": "rookie" } } ] } } }

Retrouvez le code directement ici

Resolver des mutations

Création d'un type mutation

Comme pour la query, nous devons définir les mutations possibles. Il s'agit là aussi d'une fonction prenant en entrée un type input et renvoyant un objet.

Type Input

Pour ce tutoriel, nous allons seulement créer un nouvel astronaute. Nous avons donc besoin d'un seul type input pour l'astronaute.

Dans le dossier typedefs vous devez ajouter un fichier astronauteInput.js qui contient :

const AstronauteInput = ` input AstronauteInput { pseudo: String! photo: String } `; export default AstronauteInput;

Ajout de la mutation

Dans le fichier schemas.js vous devez ajouter la mutation :

import { makeExecutableSchema } from 'graphql-tools'; import { resolvers } from './resolver'; import Astronaute from './typedefs/astronaute'; import AstronauteInput from './typedefs/astronauteInput'; import Planet from './typedefs/planet'; import Grade from './typedefs/grade'; const RootQuery = ` type RootQuery { astronaute(id: Int!): Astronaute, astronautes: [Astronaute] planet(id: Int!): Planet } `; const RootMutation = ` type RootMutation { saveAstronaute(input: AstronauteInput!): Astronaute } `; const SchemaDefinition = ` schema { query: RootQuery, mutation: RootMutation } `; export default makeExecutableSchema({ typeDefs: [SchemaDefinition, RootQuery, RootMutation, AstronauteInput, Astronaute, Planet, Grade], resolvers: resolvers, });

Resolver de mutation

Dans le resolver du fichier astronaute.js vous devez ajouter la fonction permettant la mutation :

import pg from './../pg'; import Astronaute from '../typedefs/astronaute'; const resolvers = { RootMutation: { async saveAstronaute(value, { input }, context, infos) { const astronaute = await pg('astronaute').returning(['id', 'pseudo']).insert(input); return astronaute.pop(); } }, RootQuery: { async astronautes() { return await pg.select().table('astronaute'); }, async astronaute(obj, args, context, info) { return (await pg .select() .table('astronaute') .where('id', args.id) .limit(1)).pop(); }, }, Astronaute: { async grade(astronaute) { return (await pg .select() .table('grade') .where('id', astronaute.grade_id) .limit(1)).pop(); }, async planet(astronaute) { return ( await pg .select() .table('planet') .innerJoin('planet-astronaute', 'planet-astronaute.planet_id', '=', 'planet.id') .where('planet-astronaute.astronaute_id', astronaute.id) .limit(1)).pop(); } }, }; export default resolvers;

La fonction saveAstronaute prend l'input en entrée, sauvegarde dans la base et renvoie l'objet sauvegardé :

Testons

Dans GraphiQL vous pouvez mettre la query suivante :

mutation saveAstronaute($astronaute: AstronauteInput!) { saveAstronaute(input: $astronaute) { id pseudo } }

Puis dans query variables en bas à gauche :

{ "astronaute": { "pseudo": "test" } }

Si tout est ok pour devriez avoir cela comme réponse :

{ "data": { "saveAstronaute": { "id": 1, "pseudo": "test" } } }

Retrouvez le code directement ici

Conclusion

Je vous invite à lire la documentation de GraphQL et de Apollo pour voir l'ensemble des fonctionnalités disponible dans GraphQL.