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"
-
instance of GraphQLObjectType
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
-
usersByIds
- 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,865