-
GraphQL

in Depth

(serverside)
크로키닷컴㈜ 윤상민
2019.7.4
➢ Just a query language
GraphQL is …
type User {
id: ID!
name: String!
}
type Query {
user(id: ID): User
}
query {
user(id: "10") {
id
name
}
}
{
"data": {
"user": {
"id": "10",
"name": "Croquis"
}
}
}
+
➢ So, following is only for GraphQL.js.
GraphQL is …
import { GraphQLObjectType, GraphQLSchema,
GraphQLString, printSchema } from 'graphql';
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
hello: {
type: GraphQLString,
},
},
}),
});
console.log(printSchema(schema));
Define Schema: raw object
schema {
query: RootQueryType
}
type RootQueryType {
hello: String
}
import { GraphQLList, GraphQLNonNull, GraphQLObjectType,
GraphQLSchema, GraphQLString } from 'graphql';
const user = new GraphQLObjectType({
name: 'User', fields: {
name: {
type: GraphQLNonNull(GraphQLString),
} } });
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'Query',
fields: {
users: {
type: GraphQLNonNull(GraphQLList(GraphQLNonNull(user))),
} } }) });
Define Schema: custom type
type Query {
users: [User!]!
}
type User {
name: String!
}
import { buildSchema } from 'graphql';
const schema = buildSchema(`
type User {
name: String!
}
type Query {
users: [User!]!
}
`);
Define Schema: using GraphQL schema language
import 'reflect-metadata';
import { buildSchemaSync, Field, ObjectType, Query, Resolver } from 'type-graphql';
@ObjectType()
class User {
@Field()
name: string;
}
@Resolver()
class UserResolver {
@Query((returns) => [User])
users() {
return [];
}
}
const schema = buildSchemaSync({
resolvers: [UserResolver],
});
Define Schema: TypeGraphQL
import { graphql, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
hello: {
type: GraphQLString,
resolve: () => 'World',
},
},
}),
});
console.log(await graphql(schema, '{ hello }’));
// { data: { hello: 'World' } }
Execute Query
// from graphql-express
import { Source, parse, execute } from 'graphql';
function graphqlHTTP(options: Options): Middleware {
return function graphqlMiddleware(request: $Request, response: $Response) {
return getGraphQLParams(request)
.then(() => {
const source = new Source(query, 'GraphQL request');
documentAST = parse(source);
return execute({ schema, documentAST, rootValue, context, variables, operationName });
})
.then(result => {
if (response.statusCode === 200 && result && !result.data) {
response.statusCode = 500;
}
const payload = JSON.stringify(result, null, pretty ? 2 : 0);
sendResponse(response, 'application/json', payload);
}); }; }
Transport Layer
➢ All other things are how to resolve values
Resolve
import { graphql, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
hello: {
type: GraphQLString,
resolve: () => 'World',
},
},
}),
});
console.log(await graphql(schema, '{ hello }'));
Basic Resolver
import { buildSchema, graphql } from 'graphql';
const schema = buildSchema(`
type Query {
hello: String
}
`);
console.log(await graphql(schema, '{ hello }', {
hello: () => 'World',
}));
Root Value
var defaultFieldResolver = function defaultFieldResolver(source, args, contextValue,
info) {
// ensure source is a value for which property access is acceptable.
if (_typeof(source) === 'object' || typeof source === 'function') {
var property = source[info.fieldName];
if (typeof property === 'function') {
return source[info.fieldName](args, contextValue, info);
}
return property;
}
};
Root Value: internal
import { buildSchema, graphql } from 'graphql';
const schema = buildSchema(`
type Query {
hello: String
}
`);
schema.getQueryType()!.getFields().hello.resolve = () => {
return 'World';
};
console.log(await graphql(schema, '{ hello }'));
Attach Resolver After Creation
import { buildSchema, graphql } from 'graphql';
import { addResolveFunctionsToSchema } from 'graphql-tools';
const schema = buildSchema(`
type Query {
hello: String
}
`);
const resolvers = {
Query: {
hello: () => {
return 'World';
} } };
addResolveFunctionsToSchema({ schema, resolvers });
console.log(await graphql(schema, '{ hello }'));
Attach Resolver After Creation: graphql-tools
// OR
const resolvers = {
Query: {
hello: {
type: GraphQLNonNull(GraphQLString),
resolve() {
return 'World';
},
} } };
➢ Function signature: resolver(obj, args, context, info)
✓ For root value: (args, context, info)
➢ Call defaultFieldResolver if not defined
➢ Always called even if object has a field value
Resolver
const schema = buildSchema(`
type User { name: String! }
type Query { users: [User!]! }`);
const resolvers = {
Query: {
users(obj) {
return [{ name: 'Name1' }, { name: 'Name2' }];
} },
User: {
name(obj) {
return obj.name.toLowerCase();
} } };
console.log(await graphql(schema, '{ users { name } }', { foo: 1 }));
// { data: { users: [ { name: 'name1' }, { name: 'name2' } ] } }
Resolver when object has a field value
➢ Contains the result returned from the resolver on the parent field
✓ Root Value for Query field
➢ @Root() in TypeGraphQL
Resolver Arguments: 1) obj (source, parent)
const schema = buildSchema(`
type User { name: String! }
type Query { users: [User!]! }`);
const resolvers = {
Query: {
users(obj) { // obj === {foo: 1}
return [{ id: 1 }, { id: 2 }];
} },
User: {
name(obj) { /// obj === {id: 1} or {id: 2}
return `Name${obj.id}`;
} } };
console.log(await graphql(schema, '{ users { name } }', { foo: 1 }));
// { data: { users: [ { name: 'Name1' }, { name: 'Name2' } ] } }
Resolver Arguments: 1) obj (source, parent)
➢ An object with the arguments passed into the field in the query
➢ @Arg() or @Args() in TypeGraphQL
Resolver Arguments: 2) args
const schema = buildSchema(`
type User { name(length: Int): String! }
type Query { users(query: String, length: Int): [User!]! }`);
const resolvers = {
Query: {
users(obj, args) { // args === { query: 'n', length: 2 }
return [{ name: 'Daniel' }, { name: 'Johnny' }];
} },
User: {
name(obj, args) { // args === { length: 3 }
return obj.name.substr(0, args.length);
} } };
console.log(await graphql(schema, '{ users(query: "N", length: 2) { name(length:
3) } }'));
// { data: { users: [ { name: 'Dan' }, { name: 'Joh' } ] } }
Resolver Arguments: 2) args
➢ An object shared by all resolvers in a particular query
➢ Per-request
➢ For examples:
✓ authentication information
✓ dataloader instances
➢ @Ctx() in TypeGraphQL
Resolver Arguments: 3) context
const schema = buildSchema(`
type User { name: String! }
type Query { users: [User!]! }`);
const resolvers = {
Query: {
users(obj, args, context) { // context === { foo: 1 }
context.bar = 1;
return [{ name: 'Name1' }, { name: 'Name2' }];
} },
User: {
name(obj, args, context) { // context === { foo: 1, bar: 1 } or { foo: 1, bar: 2 }
context.bar++;
return obj.name;
} } };
console.log(await graphql(schema, '{ users { name } }', null, { foo: 1 }));
// { data: { users: [ { name: 'Name1' }, { name: 'Name2' } ] } }
Resolver Arguments: 3) context
➢ Contains information about the execution state of the query
➢ @Info in TypeGraphQL
➢ Some utilities: graphql-list-fields, graphql-fields-list, @croquiscom/crary-graphql
Resolver Arguments: 4) info
export interface GraphQLResolveInfo {
readonly fieldName: string;
readonly fieldNodes: ReadonlyArray<FieldNode>;
readonly returnType: GraphQLOutputType;
readonly parentType: GraphQLObjectType;
readonly path: ResponsePath;
readonly schema: GraphQLSchema;
readonly fragments: { [key: string]: FragmentDefinitionNode };
readonly rootValue: any;
readonly operation: OperationDefinitionNode;
readonly variableValues: { [variableName: string]: any };
}
const resolvers = {
Query: {
users(obj, args, context, info) {
// info.fieldName === 'users'
// print(info.fieldNodes[0]) === 'users { name }'
// info.parentType === Query
// info.returnType === [User!]!
return [{ name: 'Name1' }, { name: 'Name2' }];
} },
User: {
name(obj, args, context, info) {
// info.fieldName === 'name'
// print(info.fieldNodes[0]) === 'name'
// info.parentType === User
// info.returnType === String!
return obj.name;
} } };
Resolver Arguments: 4) info
import { addArgumentToInfo, getFieldList, getFieldList1st, getFieldString } from '@croquiscom/crary-graphql';
const schema = buildSchema(`
type Name { first: String, last: String }
type User { name: Name, age: Int }
type Query { users: [User!]! }`);
const resolvers = {
Query: {
users(obj, args, context, info) {
// getFieldList(info) === [ 'name.first', 'name.last', 'age' ]
// getFieldList1st(info)) === [ 'name', 'age' ]
// getFieldString(info) === 'name { first last } age'
// print(info.fieldNodes[0]) === 'users { name { first last } age }'
// info.variableValues === {}
const newInfo = addArgumentToInfo(info, 'length', 3, GraphQLInt);
// print(newInfo.fieldNodes[0]) === 'users(length: $_c_length) { name { first last } age }'
// newInfo.variableValues === { _c_length: 3 }
return [];
} } };
console.log(await graphql(schema, '{ users { name { first last } age } }'));
Resolver Arguments: 4) info - @croquiscom/crary-graphql
➢ buildSchema + addResolveFunctionsToSchema
Advanced: makeExecutableSchema
const typeDefs = `
type User { name: String! }
type Query { users: [User!]! }`;
const resolvers = {
Query: {
users() {
return [{ name: 'Name1' }, { name: 'Name2' }];
} },
User: {
name(obj) {
return obj.name;
} } };
const schema = makeExecutableSchema({ typeDefs, resolvers });
console.log(await graphql(schema, '{ users { name } }'));
➢ Generate GraphQL schema objects that delegate to a remote server
Advanced: makeRemoteExecutableSchema
import { HttpLink } from 'apollo-link-http';
import { importSchema } from 'graphql-import';
import { makeRemoteExecutableSchema } from 'graphql-tools';
import fetch from 'node-fetch';
const schema = makeRemoteExecutableSchema({
schema: importSchema(`${__dirname}/schema.graphql`),
link: new HttpLink({
uri: 'http://other.service/graphql',
fetch: fetch,
}),
});
export default function linkToFetcher(link: ApolloLink): Fetcher {
return (fetcherOperation: FetcherOperation) => {
return makePromise(execute(link, fetcherOperation as GraphQLRequest));
}; }
export function createResolver(fetcher: Fetcher): GraphQLFieldResolver<any, any> {
return async (root, args, context, info) => {
const fragments = Object.keys(info.fragments).map(
fragment => info.fragments[fragment],
);
const document = {
kind: Kind.DOCUMENT,
definitions: [info.operation, ...fragments],
};
const result = await fetcher({
query: document,
variables: info.variableValues,
context: { graphqlContext: context },
});
return checkResultAndHandleErrors(result, info);
}; }
Advanced: makeRemoteExecutableSchema internal
➢ Combine multiple GraphQL schemas together
Advanced: mergeSchemas
const user_schema = buildSchema(`
type User { id: ID!, name: String! }
type Query { users: [User!]! }`);
const post_schema = buildSchema(`
type Post { contents: String! }
type Query { posts(user_id: ID): [Post!]! }`);
const schema = mergeSchemas({
schemas: [
user_schema,
post_schema,
`extend type User { posts: [Post!]! }`,
],
});
type Post {
contents: String!
}
type Query {
users: [User!]!
posts(user_id: ID): [Post!]!
}
type User {
id: ID!
name: String!
posts: [Post!]!
}
User: {
posts: {
fragment: '... on User { id }',
resolve(obj, args, context, info) {
console.log(print(info.fieldNodes[0]));
return info.mergeInfo.delegateToSchema({
schema: post_schema,
operation: 'query',
fieldName: 'posts',
args: { user_id: obj.id },
context,
info,
});
} } }

// for query '{ users { name posts { contents } } }'
// 1. 'users { name ... on User { id } }' on user
// 2. 'posts { contents }' on merged
// 3. 'posts(user_id: $_v0_user_id) { contents }' on post
Advanced: mergeSchemas + delegateToSchema
➢ Merged schema must have all operations for delegateToSchema
Advanced: manual delegation
resolve(obj, args, context, info) {
info = transformInfoForSchema(info, post_schema, info.mergeInfo.fragments);
const fields = getFieldString(info);
const query = `query($user_id: ID) { posts(user_id: $user_id) { ${fields} } }`;
const variables = { user_id: obj.id };
return fetch('http://post/graphql', {
method: 'POST',
body: JSON.stringify({ query, variables }),
headers: {
'Content-Type': 'application/json',
},
});
},
directive @authorized on FIELD_DEFINITION
type Query {
posts: [Post!]! @authorized
}
import { SchemaDirectiveVisitor } from 'graphql-tools';
class AuthorizedDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field: GraphQLField<any, any>) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async (source, args, context, info) => {
checkAuthorized(context);
const result = await resolve(source, args, context, info);
return result;
};
}
}
SchemaDirectiveVisitor.visitSchemaDirectives(schema, { authorized: AuthorizedDirective });
Advanced: directive
➢ Use DataLoader
➢ Batch function must return an array of values that have same order to an array of
keys
➢ It is common to create a new DataLoader per request (context will be helpful)
Advanced: N+1 problem
import DataLoader from 'dataloader';
resolve(parent, args, context, info) {
if (!context.loader) {
context.loader = new DataLoader((keys) => {
return ['value for keys[0]', 'value for keys[1]', ...];
});
}
return context.loader.load(key);
},
➢ Subscription is a one-way communication from server to client.
✓ Socket.io is a two-way
➢ You need to implement ‘subscribe’ method that returns AsyncIterator
Advanced: subscription
import { PubSub } from 'graphql-subscriptions';
const pubSub = new PubSub();
async function doTask(task_id: number) {
for (let i = 0; i < 10; i++) {
await Bluebird.delay(1000);
pubSub.publish(`doTask-${task_id}`, i);
}
pubSub.publish(`doTask-${task_id}`, 'done');
}
const schema = new GraphQLSchema({
subscription: new GraphQLObjectType({
name: 'RootSubscriptionType',
fields: {
doTaskProgressed: {
type: GraphQLString,
args: { task_id: { type: new GraphQLNonNull(GraphQLInt) } },
subscribe: (source, args) => {
const task_id = args.task_id;
return pubSub.asyncIterator(`doTask-${task_id}`);
},
} } }) });
Advanced: subscription - run task in separate thread
- Task will run regardless subscription
- You need ‘done’ to notify task ended
const schema = new GraphQLSchema({
subscription: new GraphQLObjectType({
name: 'RootSubscriptionType',
fields: {
doTaskWithProgress: {
type: GraphQLString,
async *subscribe(source, args) {
for (let i = 0; i < 10; i++) {
await Bluebird.delay(1000);
yield i;
}
},
} } }) });
Advanced: subscription - run task in subscribe
- Task will stop when subscription ended
- Subscription will be ended when task ended
➢ graphql-resolvers
✓ combineResolvers, pipeResolvers, allResolvers
✓ resolveDependee, resolveDependees, isDependee
➢ graphql-scalars
✓ DateTime, EmailAddress, NegativeInt, PostalCode, URL, …
➢ Apollo Federation
✓ Replace schema stitching (remote + merge)
Advanced: other packages
➢ GraphQL Resolvers: Best Practices
➢
Further Reading
-
Thank you
for your attention

[2019-07] GraphQL in depth (serverside)

  • 1.
  • 2.
    ➢ Just aquery language GraphQL is … type User { id: ID! name: String! } type Query { user(id: ID): User } query { user(id: "10") { id name } } { "data": { "user": { "id": "10", "name": "Croquis" } } } +
  • 3.
    ➢ So, followingis only for GraphQL.js. GraphQL is …
  • 4.
    import { GraphQLObjectType,GraphQLSchema, GraphQLString, printSchema } from 'graphql'; const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', fields: { hello: { type: GraphQLString, }, }, }), }); console.log(printSchema(schema)); Define Schema: raw object schema { query: RootQueryType } type RootQueryType { hello: String }
  • 5.
    import { GraphQLList,GraphQLNonNull, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; const user = new GraphQLObjectType({ name: 'User', fields: { name: { type: GraphQLNonNull(GraphQLString), } } }); const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'Query', fields: { users: { type: GraphQLNonNull(GraphQLList(GraphQLNonNull(user))), } } }) }); Define Schema: custom type type Query { users: [User!]! } type User { name: String! }
  • 6.
    import { buildSchema} from 'graphql'; const schema = buildSchema(` type User { name: String! } type Query { users: [User!]! } `); Define Schema: using GraphQL schema language
  • 7.
    import 'reflect-metadata'; import {buildSchemaSync, Field, ObjectType, Query, Resolver } from 'type-graphql'; @ObjectType() class User { @Field() name: string; } @Resolver() class UserResolver { @Query((returns) => [User]) users() { return []; } } const schema = buildSchemaSync({ resolvers: [UserResolver], }); Define Schema: TypeGraphQL
  • 8.
    import { graphql,GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', fields: { hello: { type: GraphQLString, resolve: () => 'World', }, }, }), }); console.log(await graphql(schema, '{ hello }’)); // { data: { hello: 'World' } } Execute Query
  • 9.
    // from graphql-express import{ Source, parse, execute } from 'graphql'; function graphqlHTTP(options: Options): Middleware { return function graphqlMiddleware(request: $Request, response: $Response) { return getGraphQLParams(request) .then(() => { const source = new Source(query, 'GraphQL request'); documentAST = parse(source); return execute({ schema, documentAST, rootValue, context, variables, operationName }); }) .then(result => { if (response.statusCode === 200 && result && !result.data) { response.statusCode = 500; } const payload = JSON.stringify(result, null, pretty ? 2 : 0); sendResponse(response, 'application/json', payload); }); }; } Transport Layer
  • 10.
    ➢ All otherthings are how to resolve values Resolve
  • 11.
    import { graphql,GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql'; const schema = new GraphQLSchema({ query: new GraphQLObjectType({ name: 'RootQueryType', fields: { hello: { type: GraphQLString, resolve: () => 'World', }, }, }), }); console.log(await graphql(schema, '{ hello }')); Basic Resolver
  • 12.
    import { buildSchema,graphql } from 'graphql'; const schema = buildSchema(` type Query { hello: String } `); console.log(await graphql(schema, '{ hello }', { hello: () => 'World', })); Root Value
  • 13.
    var defaultFieldResolver =function defaultFieldResolver(source, args, contextValue, info) { // ensure source is a value for which property access is acceptable. if (_typeof(source) === 'object' || typeof source === 'function') { var property = source[info.fieldName]; if (typeof property === 'function') { return source[info.fieldName](args, contextValue, info); } return property; } }; Root Value: internal
  • 14.
    import { buildSchema,graphql } from 'graphql'; const schema = buildSchema(` type Query { hello: String } `); schema.getQueryType()!.getFields().hello.resolve = () => { return 'World'; }; console.log(await graphql(schema, '{ hello }')); Attach Resolver After Creation
  • 15.
    import { buildSchema,graphql } from 'graphql'; import { addResolveFunctionsToSchema } from 'graphql-tools'; const schema = buildSchema(` type Query { hello: String } `); const resolvers = { Query: { hello: () => { return 'World'; } } }; addResolveFunctionsToSchema({ schema, resolvers }); console.log(await graphql(schema, '{ hello }')); Attach Resolver After Creation: graphql-tools // OR const resolvers = { Query: { hello: { type: GraphQLNonNull(GraphQLString), resolve() { return 'World'; }, } } };
  • 16.
    ➢ Function signature:resolver(obj, args, context, info) ✓ For root value: (args, context, info) ➢ Call defaultFieldResolver if not defined ➢ Always called even if object has a field value Resolver
  • 17.
    const schema =buildSchema(` type User { name: String! } type Query { users: [User!]! }`); const resolvers = { Query: { users(obj) { return [{ name: 'Name1' }, { name: 'Name2' }]; } }, User: { name(obj) { return obj.name.toLowerCase(); } } }; console.log(await graphql(schema, '{ users { name } }', { foo: 1 })); // { data: { users: [ { name: 'name1' }, { name: 'name2' } ] } } Resolver when object has a field value
  • 18.
    ➢ Contains theresult returned from the resolver on the parent field ✓ Root Value for Query field ➢ @Root() in TypeGraphQL Resolver Arguments: 1) obj (source, parent)
  • 19.
    const schema =buildSchema(` type User { name: String! } type Query { users: [User!]! }`); const resolvers = { Query: { users(obj) { // obj === {foo: 1} return [{ id: 1 }, { id: 2 }]; } }, User: { name(obj) { /// obj === {id: 1} or {id: 2} return `Name${obj.id}`; } } }; console.log(await graphql(schema, '{ users { name } }', { foo: 1 })); // { data: { users: [ { name: 'Name1' }, { name: 'Name2' } ] } } Resolver Arguments: 1) obj (source, parent)
  • 20.
    ➢ An objectwith the arguments passed into the field in the query ➢ @Arg() or @Args() in TypeGraphQL Resolver Arguments: 2) args
  • 21.
    const schema =buildSchema(` type User { name(length: Int): String! } type Query { users(query: String, length: Int): [User!]! }`); const resolvers = { Query: { users(obj, args) { // args === { query: 'n', length: 2 } return [{ name: 'Daniel' }, { name: 'Johnny' }]; } }, User: { name(obj, args) { // args === { length: 3 } return obj.name.substr(0, args.length); } } }; console.log(await graphql(schema, '{ users(query: "N", length: 2) { name(length: 3) } }')); // { data: { users: [ { name: 'Dan' }, { name: 'Joh' } ] } } Resolver Arguments: 2) args
  • 22.
    ➢ An objectshared by all resolvers in a particular query ➢ Per-request ➢ For examples: ✓ authentication information ✓ dataloader instances ➢ @Ctx() in TypeGraphQL Resolver Arguments: 3) context
  • 23.
    const schema =buildSchema(` type User { name: String! } type Query { users: [User!]! }`); const resolvers = { Query: { users(obj, args, context) { // context === { foo: 1 } context.bar = 1; return [{ name: 'Name1' }, { name: 'Name2' }]; } }, User: { name(obj, args, context) { // context === { foo: 1, bar: 1 } or { foo: 1, bar: 2 } context.bar++; return obj.name; } } }; console.log(await graphql(schema, '{ users { name } }', null, { foo: 1 })); // { data: { users: [ { name: 'Name1' }, { name: 'Name2' } ] } } Resolver Arguments: 3) context
  • 24.
    ➢ Contains informationabout the execution state of the query ➢ @Info in TypeGraphQL ➢ Some utilities: graphql-list-fields, graphql-fields-list, @croquiscom/crary-graphql Resolver Arguments: 4) info export interface GraphQLResolveInfo { readonly fieldName: string; readonly fieldNodes: ReadonlyArray<FieldNode>; readonly returnType: GraphQLOutputType; readonly parentType: GraphQLObjectType; readonly path: ResponsePath; readonly schema: GraphQLSchema; readonly fragments: { [key: string]: FragmentDefinitionNode }; readonly rootValue: any; readonly operation: OperationDefinitionNode; readonly variableValues: { [variableName: string]: any }; }
  • 25.
    const resolvers ={ Query: { users(obj, args, context, info) { // info.fieldName === 'users' // print(info.fieldNodes[0]) === 'users { name }' // info.parentType === Query // info.returnType === [User!]! return [{ name: 'Name1' }, { name: 'Name2' }]; } }, User: { name(obj, args, context, info) { // info.fieldName === 'name' // print(info.fieldNodes[0]) === 'name' // info.parentType === User // info.returnType === String! return obj.name; } } }; Resolver Arguments: 4) info
  • 26.
    import { addArgumentToInfo,getFieldList, getFieldList1st, getFieldString } from '@croquiscom/crary-graphql'; const schema = buildSchema(` type Name { first: String, last: String } type User { name: Name, age: Int } type Query { users: [User!]! }`); const resolvers = { Query: { users(obj, args, context, info) { // getFieldList(info) === [ 'name.first', 'name.last', 'age' ] // getFieldList1st(info)) === [ 'name', 'age' ] // getFieldString(info) === 'name { first last } age' // print(info.fieldNodes[0]) === 'users { name { first last } age }' // info.variableValues === {} const newInfo = addArgumentToInfo(info, 'length', 3, GraphQLInt); // print(newInfo.fieldNodes[0]) === 'users(length: $_c_length) { name { first last } age }' // newInfo.variableValues === { _c_length: 3 } return []; } } }; console.log(await graphql(schema, '{ users { name { first last } age } }')); Resolver Arguments: 4) info - @croquiscom/crary-graphql
  • 27.
    ➢ buildSchema +addResolveFunctionsToSchema Advanced: makeExecutableSchema const typeDefs = ` type User { name: String! } type Query { users: [User!]! }`; const resolvers = { Query: { users() { return [{ name: 'Name1' }, { name: 'Name2' }]; } }, User: { name(obj) { return obj.name; } } }; const schema = makeExecutableSchema({ typeDefs, resolvers }); console.log(await graphql(schema, '{ users { name } }'));
  • 28.
    ➢ Generate GraphQLschema objects that delegate to a remote server Advanced: makeRemoteExecutableSchema import { HttpLink } from 'apollo-link-http'; import { importSchema } from 'graphql-import'; import { makeRemoteExecutableSchema } from 'graphql-tools'; import fetch from 'node-fetch'; const schema = makeRemoteExecutableSchema({ schema: importSchema(`${__dirname}/schema.graphql`), link: new HttpLink({ uri: 'http://other.service/graphql', fetch: fetch, }), });
  • 29.
    export default functionlinkToFetcher(link: ApolloLink): Fetcher { return (fetcherOperation: FetcherOperation) => { return makePromise(execute(link, fetcherOperation as GraphQLRequest)); }; } export function createResolver(fetcher: Fetcher): GraphQLFieldResolver<any, any> { return async (root, args, context, info) => { const fragments = Object.keys(info.fragments).map( fragment => info.fragments[fragment], ); const document = { kind: Kind.DOCUMENT, definitions: [info.operation, ...fragments], }; const result = await fetcher({ query: document, variables: info.variableValues, context: { graphqlContext: context }, }); return checkResultAndHandleErrors(result, info); }; } Advanced: makeRemoteExecutableSchema internal
  • 30.
    ➢ Combine multipleGraphQL schemas together Advanced: mergeSchemas const user_schema = buildSchema(` type User { id: ID!, name: String! } type Query { users: [User!]! }`); const post_schema = buildSchema(` type Post { contents: String! } type Query { posts(user_id: ID): [Post!]! }`); const schema = mergeSchemas({ schemas: [ user_schema, post_schema, `extend type User { posts: [Post!]! }`, ], }); type Post { contents: String! } type Query { users: [User!]! posts(user_id: ID): [Post!]! } type User { id: ID! name: String! posts: [Post!]! }
  • 31.
    User: { posts: { fragment:'... on User { id }', resolve(obj, args, context, info) { console.log(print(info.fieldNodes[0])); return info.mergeInfo.delegateToSchema({ schema: post_schema, operation: 'query', fieldName: 'posts', args: { user_id: obj.id }, context, info, }); } } }
 // for query '{ users { name posts { contents } } }' // 1. 'users { name ... on User { id } }' on user // 2. 'posts { contents }' on merged // 3. 'posts(user_id: $_v0_user_id) { contents }' on post Advanced: mergeSchemas + delegateToSchema
  • 32.
    ➢ Merged schemamust have all operations for delegateToSchema Advanced: manual delegation resolve(obj, args, context, info) { info = transformInfoForSchema(info, post_schema, info.mergeInfo.fragments); const fields = getFieldString(info); const query = `query($user_id: ID) { posts(user_id: $user_id) { ${fields} } }`; const variables = { user_id: obj.id }; return fetch('http://post/graphql', { method: 'POST', body: JSON.stringify({ query, variables }), headers: { 'Content-Type': 'application/json', }, }); },
  • 33.
    directive @authorized onFIELD_DEFINITION type Query { posts: [Post!]! @authorized } import { SchemaDirectiveVisitor } from 'graphql-tools'; class AuthorizedDirective extends SchemaDirectiveVisitor { visitFieldDefinition(field: GraphQLField<any, any>) { const { resolve = defaultFieldResolver } = field; field.resolve = async (source, args, context, info) => { checkAuthorized(context); const result = await resolve(source, args, context, info); return result; }; } } SchemaDirectiveVisitor.visitSchemaDirectives(schema, { authorized: AuthorizedDirective }); Advanced: directive
  • 34.
    ➢ Use DataLoader ➢Batch function must return an array of values that have same order to an array of keys ➢ It is common to create a new DataLoader per request (context will be helpful) Advanced: N+1 problem import DataLoader from 'dataloader'; resolve(parent, args, context, info) { if (!context.loader) { context.loader = new DataLoader((keys) => { return ['value for keys[0]', 'value for keys[1]', ...]; }); } return context.loader.load(key); },
  • 35.
    ➢ Subscription isa one-way communication from server to client. ✓ Socket.io is a two-way ➢ You need to implement ‘subscribe’ method that returns AsyncIterator Advanced: subscription
  • 36.
    import { PubSub} from 'graphql-subscriptions'; const pubSub = new PubSub(); async function doTask(task_id: number) { for (let i = 0; i < 10; i++) { await Bluebird.delay(1000); pubSub.publish(`doTask-${task_id}`, i); } pubSub.publish(`doTask-${task_id}`, 'done'); } const schema = new GraphQLSchema({ subscription: new GraphQLObjectType({ name: 'RootSubscriptionType', fields: { doTaskProgressed: { type: GraphQLString, args: { task_id: { type: new GraphQLNonNull(GraphQLInt) } }, subscribe: (source, args) => { const task_id = args.task_id; return pubSub.asyncIterator(`doTask-${task_id}`); }, } } }) }); Advanced: subscription - run task in separate thread - Task will run regardless subscription - You need ‘done’ to notify task ended
  • 37.
    const schema =new GraphQLSchema({ subscription: new GraphQLObjectType({ name: 'RootSubscriptionType', fields: { doTaskWithProgress: { type: GraphQLString, async *subscribe(source, args) { for (let i = 0; i < 10; i++) { await Bluebird.delay(1000); yield i; } }, } } }) }); Advanced: subscription - run task in subscribe - Task will stop when subscription ended - Subscription will be ended when task ended
  • 38.
    ➢ graphql-resolvers ✓ combineResolvers,pipeResolvers, allResolvers ✓ resolveDependee, resolveDependees, isDependee ➢ graphql-scalars ✓ DateTime, EmailAddress, NegativeInt, PostalCode, URL, … ➢ Apollo Federation ✓ Replace schema stitching (remote + merge) Advanced: other packages
  • 39.
    ➢ GraphQL Resolvers:Best Practices ➢ Further Reading
  • 40.