Hun's Blog

Nexus GraphQL tutorials (3) - Adding mutations to Your API 본문

Backend/GraphQL

Nexus GraphQL tutorials (3) - Adding mutations to Your API

jhk-im 2020. 12. 1. 15:53

GraphQL을 개인 프로젝트에 적용하기위해 제대로 학습해보기 시리즈

해당 글은 Nexus 를 개인 프로젝트에 적용하고자 제대로 학습하기 위해 Nexus tutorial의 내용을 정리한 것입니다.

Nexus tutorial에도 상세하게 나와있음을 알려드립니다. 

 Adding mutations to Your API

 

3. Adding mutations to Your API

3. Adding mutations to Your API

nexusjs.org

Overview

  • GraphQL mutation 작성.
  • mutation에 대한 GraphQL 객체 노출
  • GraphQL Context 사용
  • GraphQL argument 사용 

Wire up the context

먼저 메모리 내 데이터베이스를 설정하고 GraphQL context를 사용하여 resolver에 노출한다. 

GraphQL context는 resolver 간에 공유되는 일반 JavaScript 객체이다.

GraphQL 서버는 각 요청에 대해 새로운 요청을 작성한다. 

// api/db.ts
export interface Post {
  id: number
  title: string
  body: string
  published: boolean
}
export interface Db {
  posts: Post[]
}
export const db: Db = {
  posts: [{ id: 1, title: 'Nexus', body: '...', published: false }],
}

이제 GraphQL context에 노출하기 위해 2가지를 수행해야한다.

1. db객체를 GraphQL 서버 context에 전달 

2. Nexus에 컨텍스트 타입이 무엇인지 알려준다. 

// api/server.ts
import { ApolloServer } from 'apollo-server'
import { schema } from './schema'
import { db } from './db'
export const server = new ApolloServer({
  schema,
  context: () => ({
    db
  })
})

 

api/context.ts 파일을 만들어 GraphQL context 타입을 알 수 있도록 Nexus를 구성해보자. 

// api/context.ts
import { Db } from './db'
export interface Context {
  db: Db
}

 

마지막으로 api/schema.ts에서 @nexus/schema의 makeSchema 기능에 다음을  추가한다.

// api/schema.ts
import { join } from 'path'
export const schema = makeSchema({
  types: [],
  outputs: {
    typegen: join(__dirname, '..', 'nexus-typegen.ts'),
    schema: join(__dirname, '..', 'schema.graphql')
  },
  // 추가
  typegenAutoConfig: {
    sources: [
      {
        source: require.resolve("./context"), // 1
        alias: "ContextModule",                         // 2
      },
    ],
    contextType: "ContextModule.Context",               // 3
  },
})

1. Nexus는 context.ts 파일을 추출하여 available 타입을 추출할 것이다. 

2. 이러한 타입은 ContextModule alias를 사용할 수 있게 된다. 

3. 그 다음 정의된 모든 타입을 참조하기 위하여 alias를 사용할 수 있다. 

 

Use the context

이제 이전 Query.draft resolver를 다시 구현해 보자. 

// api/graphql/Post.ts
export const PostQuery = queryType({
  definition(t) {
    t.list.field('drafts', {
      type: Post,
      resolve(_root, _args, ctx) {                              // 1
        return ctx.db.posts.filter(p => p.published === false)  // 2
      },
    })
  },
})

1. Context는 세번째 parameter로 일반적으로 ctx로 식별된다. 

2. 필터링된 데이터는 draft로 반환. Nexus가 타입이 정렬되도록 한다. 

 

Your first mutation

Mutation은 root type이며 진입점이다. 

Mutation filed와 객체를 결합하거나 모든 mutation fileds를 중앙으로 모을 수 있다. 

// api/graphql/Post.ts
// ...
export const PostMutation = extendType({
  type: 'Mutation',
  definition(t) {
    t.nonNull.field('createDraft', {
      type: Post,
      resolve(_root, args, ctx) {
        ctx.db.posts.push(/*...*/)
        return // ...
      },
    })
  },
})

 

resolver를 완성하려면 클라이언트의 입력 데이터가 필요하다. 여기서 GraphQL의 arguments를 가져온다. 

GraphQL의 모든 필드는 해당 필드를 수용할 수 있다.

GraphQL의 각 필드를 함수처럼 생각하고 일부 입력을받고 무엇인가를 수행하고 출력을 반환할 수 있다. 

Mutation 필드 내에서 무엇인가를 수행하는 것은 부작용을 수반한다. 

GraphQL arguments를 사용해 수정해보자. 

import { extendType, stringArg } from '@nexus/schema'
export const PostMutation = extendType({
  type: 'Mutation',
  definition(t) {
    t.nonNull.field('createDraft', {
      type: Post,
      args: {                                        // 1
        title: nonNull(stringArg()),                 // 2
        body: nonNull(stringArg()),                  // 2
      },
      resolve(_root, args, ctx) {
        const draft = {
          id: ctx.db.posts.length + 1,
          title: args.title,                         // 3
          body: args.body,                           // 3
          published: false,
        }
        ctx.db.posts.push(draft)
        return draft
      },
    })
  },
})

1. args 속성을 추가하여 정의한다. 값은 타입의 사양이다. 

2. arg 타입을 정의할 때 Nexus의 helper를 사용하자. GraphQL scala에는 intArg, booleanArg와 같은 helper 있다.

만약 InputObject와 같은 타입을 참조하려면 type: "..." 을 사용한다.  

null 및 nullable helper를 사용하여 nullability를 조정할 수 있다. 

list를 활용하여 arg를 list 타입으로 만들수도 있다. 

3. Resolver에서는 위에서 지정한 arg에  접근하여 custom logic 으로 전달.

args위에 마우스를 올려놓으면 제대로 입력되었는지 알수있다. 

 

Model the domain: Part 2

마무리하기 전에 publish mutation을 추가하여 draft를 실제 발행 된 게시물로 바꿔보자. 

// api/graphql/Post.ts
import { extendType, intArg, stringArg } from '@nexus/schema'
export const PostMutation = extendType({
  type: 'Mutation',
  definition(t) {
    // ...
    t.field('publish', {
      type: Post,
      args: {
        draftId: nonNull(intArg()),
      },
      resolve(_root, args, ctx) {
        let draftToPublish = ctx.db.posts.find(p => p.id === args.draftId)
        if (!draftToPublish) {
          throw new Error('Could not find draft with id ' + args.draftId)
        }
        draftToPublish.published = true
        return draftToPublish
      },
    })
  },
})

그런 후 API 클라이언트에서 해당 게시물을 읽을 수 있도록 할 것이다. 

// api/graphql/Post.ts
import { extendType } from '@nexus/schema'
export const PostQuery = extendType({
  type: 'Query',
  definition(t) {
    // ...
    t.list.field('posts', {
      type: Post,
      resolve(_root, _args, ctx) {
        return ctx.db.posts.filter(p => p.published === true)
      },
    })
  },
})

Try it out

mutation {
  publish(draftId: 1) {
    id
    title
    body
    published
  }
}
{
  "data": {
    "publish": {
      "id": 1,
      "title": "Nexus",
      "body": "...",
      "published": true
    }
  }
}
query {
  posts {
    id
    title
    body
    published
  }
}
{
  "data": {
    "posts": [
      {
        "id": 1,
        "title": "Nexus",
        "body": "...",
        "published": true
      }
    ]
  }
}