Graphql/Relay

Graphql

  • query language for api
  • ask for what you need, get exactly that
  • get many resource in single request
  • describe what’s possible with a type system

graphql support

  • fields, arguments, aliases, fragments, variables, operation name(query, mutation), directives(include, skip)
{
hero {
name
}
}
{
"data": {
"hero": {
"name": "R2-D2"
}
}
}
  • arguments

Type language

  • scalar type
  • enumeration type
  • list type
  • non-null type

Relay

  • javascript application framework that can talk to a GraphQL server

Examples

  • graphql-js
graphql implementation for javascript
  • Create Schema
const Query = new GraphQLObjectType({
name: 'Query',
fields: () => ({
project: {
type: ProjectType,
args: { name: { type: GraphQLString } },
resolve: ...
}
})
});
  • What is resolve function?
Invoked by GraphQL execution engine when data are actually required
  • resolve with mongodb
resolve: (parent, args, { model }) => {
return new Promise((resolve, reject) => {
model.Project.findOne({name: args.name}, (err, project) => {
if (err) reject(err);
else resolve(project);
});
});
}
  • query test(graphiql)
{
project(name: "Syrup") {
_id
name
entity
createdAt
description
}
}
{
"data": {
"project": {
"_id": "58304b13f74c3c449a642378",
"name": "Syrup",
"entity": "E1",
"createdAt": "Thu Nov 17 2016 13:00:00 GMT+0900 (KST)",
"description": "Sentinel project for syrup service"
}
}
}
  • embedded document

It is the same as the query you usually use.

  • query test(graphiql)
{
project(name: "Syrup") {
name
entity
description
tags {
name
}
}
}
{
"data": {
"project": {
"name": "Syrup",
"entity": "E1",
"description": "Sentinel project for syrup service",
"tags": [
{
"name": "syrup"
},
{
"name": "commerce"
}
]
}
}
}
  • reference document

Add a reference column to associate with the project.

logDefinitions: {
type: new GraphQLList(LogDefinitionType),
resolve: (parent, args, { model }) => {
return model.LogDefinition.find({ pid: model.ObjectId(parent._id) }).exec()
}
}
{
project(name: "Syrup") {
name
entity
description
logDefinitions {
name
description
createdAt
}
}
}
{
"data": {
"project": {
"name": "Syrup",
"entity": "E1",
"description": "Sentinel project for syrup service",
"logDefinitions": [
{
"name": "Syrup server",
"description": "Sentinel schema for syrup server",
"createdAt": "Thu Nov 17 2016 13:00:00 GMT+0900 (KST)"
},
{
"name": "Syrup client",
"description": "Sentinel schema for syrup client",
"createdAt": "Thu Nov 17 2016 13:00:00 GMT+0900 (KST)"
}
]
}
}
}
  • Project List include LogDefinition List — N + 1 problem

After fetching project list(1), you need to fetch log definition for each project(N)

  • How to solve it?

DataLoader(https://github.com/facebook/dataloader)

utility to be used as part of your application’s data fetching layer to provide a simplified and consistent API over various remote data sources such as databases or web services via batching and caching.

  • Batch processing
logDefinitions: {
type: new GraphQLList(LogDefinitionType),
resolve: (parent, args, { model }) => {
console.log('resolve log definition in project');
return LogDefinitionByProjectIdLoader.load(parent._id);
// return model.LogDefinition.find({ pid: model.ObjectId(parent._id) }).exec()
}
}
const LogDefinitionByProjectIdLoader = new DataLoader(async (keys) => {
keys = flatten(keys);
// Database access occurs only once actually
console
.log('Check how many time LogDefinitionByProjectIdLoader function is called', keys);
const logDefinitions = await model.LogDefinition.find({pid: {$in: keys.map(k => model.ObjectId(k))}}).exec();
const groupedLogDef = groupBy(logDefinitions, 'pid');
return keys.map(k => groupedLogDef[k]);
});
  • Cache with DataLoader

relay specification

  1. A mechanism for refetching an object.
  2. A description of how to page through connections.
  3. Structure around mutations to make them predictable.
const { connectionType: LogDefinitionConnection, edgeType: LogDefinitionEdge } =
connectionDefinitions({ name: 'LogDefinition', nodeType: LogDefinitionType });
logDefinitions: {
type: LogDefinitionConnection,
resolve: (parent, args, { model }) => {
const fetchPromise = model.LogDefinition.find({ pid: model.ObjectId(parent._id) }).exec();
connectionFromPromisedArray(fetchPromise, args)
}
}
  • query test(graphiql)
{
project(name: "Syrup") {
name
entity
description
logDefinitions {
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
edges {
cursor
node{
_id
name
owner
description
createdAt
}
}
}
}
}
{
"data": {
"project": {
"name": "Syrup",
"entity": "E1",
"description": "Sentinel project for syrup service",
"logDefinitions": {
"pageInfo": {
"startCursor": "YXJyYXljb25uZWN0aW9uOjA=",
"endCursor": "YXJyYXljb25uZWN0aW9uOjE=",
"hasNextPage": false,
"hasPreviousPage": false
},
"edges": [
{
"cursor": "YXJyYXljb25uZWN0aW9uOjA=",
"node": {
"_id": "58305afef74c3c449a64237a",
"name": "Syrup server",
"owner": "1002585",
"description": "Sentinel schema for syrup server",
"createdAt": "Thu Nov 17 2016 13:00:00 GMT+0900 (KST)"
}
},
{
"cursor": "YXJyYXljb25uZWN0aW9uOjE=",
"node": {
"_id": "58305b81f74c3c449a64237b",
"name": "Syrup client",
"owner": "1002585",
"description": "Sentinel schema for syrup client",
"createdAt": "Thu Nov 17 2016 13:00:00 GMT+0900 (KST)"
}
}
]
}
}
}
}
  • How is it possible? — pagination with connection
function connectionFromArraySlice(arraySlice, args, meta) {
var after = args.after;
var before = args.before;
var first = args.first;
var last = args.last;
var sliceStart = meta.sliceStart;
var arrayLength = meta.arrayLength;

var sliceEnd = sliceStart + arraySlice.length;
var beforeOffset = getOffsetWithDefault(before, arrayLength);
var afterOffset = getOffsetWithDefault(after, -1);

var startOffset = Math.max(sliceStart - 1, afterOffset, -1) + 1;
var endOffset = Math.min(sliceEnd, beforeOffset, arrayLength);

...
  return {
edges: edges,
pageInfo: {
startCursor: firstEdge ? firstEdge.cursor : null,
endCursor: lastEdge ? lastEdge.cursor : null, hasPreviousPage: typeof last === 'number' ? startOffset > lowerBound : false,
hasNextPage: typeof first === 'number' ? endOffset < upperBound : false
}
};
}
  • Simple example React with Relay using Sentinel-admin(explain cursor)
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.