Hello, GraphQL 

Part 2

Contents

  1. GraphQL 구현  (Code ? SDL ?)
  2. SDL First 프로젝트 구조
  3. Resolver 이해
  4. 간단한 API 작성

GraphQL 개발 과정

초기 Facebook 에서 release 한 graphql-jsCode-First

Apollo Group  - SDL-First 개발

Code-First를 지원하는 여러 Library, Framework 가 나오고 있음

SDL(Schema Definition Language)

type User {
    id: Int!
    name: String!
    username: String!
    email: String!
    phone: String!
    posts: [Post!]!
}

type Post {
    id: Int!
    title: String!
    body: String!
    user: User!
}

type Query {
    users: [User]!
    user(id: Int!): User!
    posts: [Post]!
}

Facebook graphql-js

import {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString,
} from 'graphql';

var schema = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'RootQueryType',
    fields: {
      hello: {
        type: GraphQLString,
        resolve() {
          return 'world';
        },
      },
    },
  }),
});

Schema 구조를 별도로 표현하지 않음

코드상에 Type을 정의하고 구현도 같이 녹아들어감

SDL-First

import { users, posts } from './db';

const resolvers = {
    Query: {
        users: () => users;
        user: ( _, { id }) => users.find(u => u.id === id);
        
        posts: () => posts;
    },
    Post: {
        user: ({ userId }) => users.find(u => u.id === userId)
    },
    User: {
        posts: ({ id }) => posts.filter(p => p.userId === id)
    }
};

export default resolvers;

Schema 구조를 이해하기 쉬움

설계 프로세스가 보다 직관적으로 이뤄질 수 있음

Code-First Library (TypeGraphQL)

@Service()
@Resolver(() => Issue)
export class IssueResolver {
  constructor(private readonly issueService: IssueService) {}

  @Authorized()
  @Query(() => [Issue])
  public async issues(@Args() args: IssueArgs) {
    return this.issueService.getIssues(args)
  }

  @Authorized()
  @Query(() => Issue)
  public async issue(@Arg('id') id: number) {
    return this.issueService.getIssue(id)
  }

  @Authorized()
  @FieldResolver(() => [Event])
  public async events(
    @Root() issue: Issue,
    @Ctx() { loaders: { eventsLoader } }: ApolloContext,
  ) {
    return eventsLoader.load(issue.id)
  }
}

GraphQL Quick Start

const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
  # Comments in GraphQL strings (such as this one) start with the hash (#) symbol.

  # This "Book" type defines the queryable fields for every book in our data source.
  type Book {
    title: String
    author: String
  }
   ...
`;

const resolvers = {
  Query: {
    books: () => books,
  },
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

typeDefs, resolvers 를 정의하고 Apollo Server 에 전달

Folder structure

  • Domain 별로 typeDefs, Resolver 필요
  • BusinessLogic Layer
  • DB Models
  • Utility 
  • Etc... 

GraphQL Folder Struture

.
├── package.json
└── src
    ├── data
    │   └── index.js
    ├── models
    │   ├── Book.js
    │   └── index.js
    ├── resolvers
    │   ├── booksResolvers.js
    │   └── index.js
    └── typeDefs
        ├── index.js
        ├── query.js
        └── types
            ├── bookType.js
            └── index.js

Role 단위

Domain 단위

.
├── package.json
└── src
    ├── books
    │   ├── Book.js
    │   ├── data.js
    │   ├── index.js
    │   ├── resolvers.js
    │   └── typeDef.js
    └── index.js

graphql-tools Schema 구성

├── src
│   ├── graphql
│   │   ├── directive
│   │   ├── entity
│   │   │   └── event
│   │   │       ├── event.graphql
│   │   │       └── event.resolver.ts
│   │   ├── index.ts

Domain 별로 나뉘어있는 typeDefs, resolvers 로딩 후 merge

entity 아래 domain 별로 graphql, resolvers 구성

graphql-tools Schema 구성

graphql/index.ts

import path from 'path'
import { loadFilesSync } from '@graphql-tools/load-files'
import { mergeTypeDefs, mergeResolvers } from '@graphql-tools/merge'

const typesArray = loadFilesSync(path.join(__dirname, './**/*.graphql'))
const resolversArray = loadFilesSync(path.join(__dirname, './**/*.resolver.ts'))

const typeDefs = mergeTypeDefs(typesArray)
const resolvers = mergeResolvers(resolversArray)

export { typeDefs, resolvers }

glob pattern 이용 File loading 후 merge 하는 작업 처리

import { typeDefs, resolvers } from '~/src/graphql'

graphql-tools Schema 구성

graphql/index.ts

import path from 'path'
import { loadFilesSync } from '@graphql-tools/load-files'
import { mergeTypeDefs, mergeResolvers } from '@graphql-tools/merge'

const typesArray = loadFilesSync(path.join(__dirname, './**/*.graphql'))
const resolversArray = loadFilesSync(path.join(__dirname, './**/*.resolver.ts'))

const typeDefs = mergeTypeDefs(typesArray)
const resolvers = mergeResolvers(resolversArray)

export { typeDefs, resolvers }

glob pattern 이용 File loading 후 merge 하는 작업 처리

import { typeDefs, resolvers } from '~/src/graphql'

Simple Resolver

const resolver = {
  Query: {
    events: (_, args, info, context) => [
      {
        id: 'cewf2',
        name: 'test',
        mark: false,
      },
      {
        id: 'cewf3',
        name: 'test',
        mark: false,
      },
    ],
  },
}

export default resolver

TypeScript 사용시 SDL에서 정의했던 Type을

Typescript 인터페이스에 맞춰서 다시 정의

GraphQL Code Generator

Merge된 GraphQL defintion기반으로 Code Type을 생성

export type EventResolvers<
  ContextType = any,
  ParentType extends ResolversParentTypes['Event'] = ResolversParentTypes['Event']
> = {
  id?: Resolver<ResolversTypes['String'], ParentType, ContextType>
  name?: Resolver<ResolversTypes['String'], ParentType, ContextType>
  mark?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>
  createdAt?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
  updatedAt?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
  __isTypeOf?: isTypeOfResolverFn<ParentType>
}

GraphQL Code Generator

Resolver에 대한 Type Definition import 해서 사용

Resolver 이해

GQL Type field 에 대해 데이터를 채우는 함수

Datasource 는  DB혹은 third-part api 등 자유롭게 사용

type Query {
  numberSix: Int! # Should always return the number 6 when queried
  numberSeven: Int! # Should always return 7
}
const resolvers = {
  Query: {
    numberSix() {
       // fetch DB or API call
      return 6;
    },
    numberSeven() {
      return 7;
    }
  }
};

Arguments Handle

id를 받아서 User를 return Query


type User {
  id: ID!
  name: String
}

type Query {
  user(id: ID!): User
}


// mock data
const users = [
  {
    id: '1',
    name: 'Elizabeth Bennet'
  },
  {
    id: '2',
    name: 'Fitzwilliam Darcy'
  }
];

Resolver arguments

Parents, args, context, info 4가지 인자를 받을 수 있음

const resolvers = {
  Query: {
    user(parent, args, context, info) {
      return users.find(user => user.id === args.id);
    }
  }
}

Parents, args, context, info 4가지 인자를 받을 수 있음

Parents, args, context, info 4가지 인자를 받을 수 있음

Resolver chain

# A library has a branch and books
type Library {
  branch: String!
  books: [Book!]
}

# A book has a title and author
type Book {
  title: String!
  author: Author!
}

# An author has a name
type Author {
  name: String!
}

type Query {
  libraries: [Library]
}

Resolver 는 GraphQL Field의 데이터를 채우는 function

GQL Server는 들어온 Query를 분석하고 field별로 resolver 를 호출

Resolver chain

query GetBooksByLibrary {
  libraries {
    books {
      author {
        name
      }
    }
  }
}

위와 같은 순서로 Resolver chain 을 타게됨

Hello, GraphQL (part2)

By y0c

Hello, GraphQL (part2)

GrpahQL 구현 부분

  • 194