Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

[2019-07] GraphQL in depth (serverside)

901 views

Published on

GraphQL in depth

Published in: Technology
  • Be the first to comment

  • Be the first to like this

[2019-07] GraphQL in depth (serverside)

  1. 1. - GraphQL
 in Depth
 (serverside) 크로키닷컴㈜ 윤상민 2019.7.4
  2. 2. ➢ 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" } } } +
  3. 3. ➢ So, following is only for GraphQL.js. GraphQL is …
  4. 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. 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. 6. import { buildSchema } from 'graphql'; const schema = buildSchema(` type User { name: String! } type Query { users: [User!]! } `); Define Schema: using GraphQL schema language
  7. 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. 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. 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. 10. ➢ All other things are how to resolve values Resolve
  11. 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. 12. import { buildSchema, graphql } from 'graphql'; const schema = buildSchema(` type Query { hello: String } `); console.log(await graphql(schema, '{ hello }', { hello: () => 'World', })); Root Value
  13. 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. 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. 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. 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. 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. 18. ➢ 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)
  19. 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. 20. ➢ An object with the arguments passed into the field in the query ➢ @Arg() or @Args() in TypeGraphQL Resolver Arguments: 2) args
  21. 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. 22. ➢ 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
  23. 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. 24. ➢ 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 }; }
  25. 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. 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. 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. 28. ➢ 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, }), });
  29. 29. 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
  30. 30. ➢ 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!]! }
  31. 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. 32. ➢ 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', }, }); },
  33. 33. 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
  34. 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. 35. ➢ 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
  36. 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. 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. 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. 39. ➢ GraphQL Resolvers: Best Practices ➢ Further Reading
  40. 40. - Thank you for your attention

×