GraphQL Server

Schemas, Mutations, DB Store, and data-loader

Make a tmp project

graphql-server

GraphQL Schema

  • create file schema.js
  • require node module graphql
  • import GraphQLSchema
  • create mySchema 
    • new instance of GraphQLSchema
  • define query to be queryType
    • ​queryType doesn't exist yet

schema.js

const {
  GraphQLSchema // class to create instance of Schema
} = require('graphql');

const mySchema = new GraphQLSchema({
  query: queryType
});

module.exports = mySchema;

Create a QueryType

  • create a queryType 
    • instance of GraphQLObjectType
      • name: RootQuery
      • fields: hello
        • type: GraphQLString
        • resolve: "World"

schema.js

const {
  GraphQLSchema // class to create instance of Schema
  , GraphQLObjectType
  , GraphQLString
} = require('graphql');

const queryType = new GraphQLObjectType({
  name: 'RootQuery',

  fields: {
    hello: {
      type: GraphQLString,
      resolve: () => "World"
    }
  }
});

GraphQL Executor

  • create index.js
  • require graphql
  • ​require mySchema
    • ​remember to export mySchema
  • get query from cli args
  • execute mySchema against query

index.js

// function, the actual executor of the schema
const { graphql } = require('graphql');

// the schema to execute
const mySchema = require('./schema');

// get query from cli arguments
const query = process.argv[2];

// execute mySchema against a query
graphql(mySchema, query);

Test Query

  • graphql() returns a promise
    • resolve it!
    • print to console
  • run a query

index.js

// execute mySchema against a query
//   async operation, returns a promise
graphql(mySchema, query)
  .then(console.log); // print results
$ node . '{ hello }'

cli

Add a Counter field

  • add a new field
    • name: counter
    • type: GraphQLInt
    • resolve: local variable counter
  • test the query

schema.js

let counter = 42;
...

    counter: {
      type: GraphQLInt,
      resolve: () => counter
    }
$ node . '{ counter }'

cli

Add Express

  • barebones server
    • "/" => "hello world"
  • listen on a PORT

index.js

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => res.send('hello world'));

app.listen(PORT);

graphql endpoint

  • '/graphql' => execute schema
  • execute mySchema using req.query
  • test query from browser/curl

index.js

app.get('/graphql', (req,res) => {
  const { query } = req.query;

  // execute mySchema against a query
  //   async operation, returns a promise
  graphql(mySchema, query)
    .then(result => res.json(result));
});
http://localhost:3000/graphql?query={hello,counter}

browser

$ curl localhost:3000/graphql\?query=%7Bhello%2Ccounter%7D

cli

express-graphql

  • add node module express-graphql
  • import module as graphqlHTTP
    • is connect middleware
  • use graphqlHTTP to execute mySchema
  • test in browser and cli

index.js

app.use('/graphql', graphqlHTTP({
  schema: mySchema
}));
http://localhost:3000/graphql?query={hello,counter}

browser

$ curl localhost:3000/graphql\?query=%7Bhello%2Ccounter%7D

cli

graphiql

  • enable graphiql

index.js

graphiql: true

Add Mutation

in the wrong place

  counter++

GraphQL Mutation

  • create mutationType
    • instance of GraphQLObjectType
    • name: RootMutation

    • fields: incrementCounter

    • type: GraphQLInt

    • resolve: ++counter

  • add mutationType to mySchema

 

schema.js

const mutationType = new GraphQLObjectType({
  name: 'RootMutation',

  fields: {
    incrementCounter: {
      type: GraphQLInt,
      resolve: () => ++counter
    }
  }
});

const mySchema = new GraphQLSchema({
  query: queryType,
  mutation: mutationType
});

GraphQLList

  • create a new counters field
    • type: new GraphQLList(GraphQLInt)

    • resolve: counters

  • create a counters array variable

schema.js

let counters = [42, 43];
...

counters: {
  type: new GraphQLList(GraphQLInt),
  resolve: () => counters
}

GraphQLList of counters

  • create CounterObjType
    • instance of GraphQLObjectType
    • name: CounterObj

    • fields 

      • id

        • type: GraphQLID

      • value

        • type: GraphQLInt

  • ​​​Add counterObj field to queryType

  • ​​​Add countersObj field to queryType

schema.js

let counterObj = {
  id: 55,
  value: 42
};
let countersObj = [
  { id: 550, value: 43 },
  { id: 551, value: 44 }
];
...

const CounterObjType = new GraphQLObjectType({
  name: 'CounterObj',

  fields: {
    id: { type: GraphQLID },
    value: { type: GraphQLInt }
  }
});
...

    counterObj: {
      type: CounterObjType,
      resolve: () => counterObj
    },
    countersObj: {
      type: new GraphQLList(CounterObjType),
      resolve: () => countersObj
    }

PersonType

  • create a PersonType
    • test in memory at first
    • clean out all old types
    • instance of GraphQLObjectType
      • name: Person
      • fields

        • id

        • first_name

        • last_name

        • email

        • spouse_id

  • queryType has one field, person

    • resolve to new temp person variable

schema.js

const person = {
  id: 1,
  first_name: 'Jon',
  last_name: 'Borgonia',
  email: 'jon.borgonia@gmail.com',
  spouse_id: 2
};

const PersonType = new GraphQLObjectType({
  name: 'Person',

  fields: {
    id: { type: GraphQLID },
    first_name: { type: GraphQLString },
    last_name: { type: GraphQLString },
    email: { type: GraphQLString },
    spouse_id: { type: GraphQLInt }
  }
});

const queryType = new GraphQLObjectType({
  name: 'RootQuery',

  fields: {
    person: {
      type: PersonType,
      resolve: () => person
    }
  }
});

Resolve underscore

  • camelize each field in PersonType
  • use the execution environment arg
  • don't actually map to underscore

The bad way

  fields: {
    id: { type: GraphQLID },
    firstName: {
      type: GraphQLString,
      // 4th arg is the execution env --v
      resolve: ( obj, {}, {}, { fieldName } ) => obj[fieldName]
    },
    lastName: {
      type: GraphQLString,
      resolve: obj => obj.last_name
    },
    email: { type: GraphQLString },
    spouseId: { type: GraphQLInt }
  }

humps!

  • install humps
  • camelize the person data

schema.js

const person = humps.camelizeKeys({
...

  fields: {
    id: { type: GraphQLID },
    firstName: { type: GraphQLString },
    lastName: { type: GraphQLString },
    email: { type: GraphQLString },
    spouseId: { type: GraphQLInt }
  }

Connect a DB

  • require the pg module

  • create a new postgres connection pool

index.js

const pool = new pg.Pool({
  database: 'graphql_server_db',
  user: 'postgres'
});

Seed Data

  • create a database named people

  • add about 10 rows, each married to another row

Add pool to context

  • add the db connection pool to GraphQL context
  • remove temp person variable
  • resolve person by querying the db

index.js

  context: { pool }
  fields: {
    person: {
      type: PersonType,
      // 3rd is context ------v
      resolve: ( obj, args, { pool }, info ) => pool.query(`
        select * from people
        where id = 1
      `).then(result => humps.camelizeKeys(result.rows[0]))
    }
  }

schema.js

field args

  • add id args to person field
    • type: GraphQLNonNull(GraphQLInt)
  • use args in resolve
    • map args.id as first argument
    • make query dynamic: $1

schema.js

  fields: {
    person: {
      type: PersonType,
      args: {
        id: {
          type: new GraphQLNonNull(GraphQLInt)
        }
      },
      resolve: ( obj, args, { pool }, info ) => pool.query(`
        select * from people
        where id = $1
      `, [args.id])
        .then(result => humps.camelizeKeys(result.rows[0]))
    }
  }

derived field

  • add fullName field to PersonType
    • resolve to concatenated fields

schema.js

fullName: {
  type: GraphQLString, 
  resolve: obj => `${obj.firstName} ${obj.lastName}`
}

resolve field from db

  • create a spouse field on PersonType
    • type: PersonType
    • resolve by querying db
  • PROBLEM!
  • PersonType is not yet defined!

schema.js

spouse: { 
  type: PersonType, 
  resolve: ( obj, args, { pool }, info ) => pool.query(`
    select * from people
    where id = $1
  `, [obj.spouseId]).then(result => humps.camelizeKeys(result.rows[0])) }
})

fields as thunk

  • Convert PersonType fields to thunk
    • thunks are functions that will return a value in the future
    • encapsulates sync or async operations in a function
    • passes the results into a callback function after being executed

The Solution!

schema.js

fields: () => ({
  ...
})

database module

  • create a database.js source file
  • create a module that takes in a db connection pool
  • add a getUserById(userId) function
  • refactor schema.js

cleaning up

database.js

const humps = require('humps');

module.exports = (pool) => ({
  getUserById(userId) {
    return pool.query(`
      select * from people
      where id = $1
    `, [userId]).then(result => humps.camelizeKeys(result.rows[0]));
  }
});

schema.js

spouse: {
  type: PersonType,
  resolve: ( obj, args, { pool }, info ) => db(pool).getUserById(obj.spouseId) 
}
...

// fields, person
resolve: ( obj, args, { pool }, info ) => db(pool).getUserById(args.id)

n+1 Problem

  • create a people field on queryType
    • ​resolve to getUsers()
  • ​create a getUsers function on database module
  • query people spouse

schema.js

people: {
  type: new GraphQLList(PersonType),
  resolve: ( obj, args, { pool } ) => db(pool).getUsers()
}

database.js

getUsers() {
  return pool.query(`
    select * from people
  `).then(result => humps.camelizeKeys(result.rows));
}

Dataloader

  • install dataloader
  • create loaders
    • ​usersByIds
      • instance of Dataloader
      • db.getUsersByIds
  • add loaders to GraphQL context
  • convert getUsersByIds to something dataloader can use
    • an array of ids
  • refactor schema resolves to use loaders

"Batching"

The Solution!

*WIP* *WIP* *WIP* *WIP* *WIP* *WIP*

Resources

  • http://graphql.org/
  • http://graphql.org/docs/api-reference-type-system/
  • https://slides.com/theremix/interactive-intro-graphql

GraphQL Server

By Jon Borgonia

GraphQL Server

GraphQL server, schemas, mutations, db store, data-loader

  • 1,775