Apollo Server - Context and contextValue

Context and contextValue

operation을 진행하는 도중, contextValue라는 객체를 만들어서 서버에서 resolvers와 plugins 전반적으로 데이터를 공유할 수 있다.

contextValue를 통해서 해당하는 필요한 resolver에 유용하게 전달할 수 있다. 예를 들면 authentication 스코프 단위나, 데이터를 가져오는 소스라던지, 데이터베이스 커넥션, 그리고 커스텀 fetch 함수 등등을 할 수 있다. 너가 batch request를 하기 위해 dataloaders를 사용한다면, contextValue를 통해서 공유할 수 있다.

context function

apollo server 4에서 context function 정의하는 형태가 변경되었다.

context function은 비동기여야하고 객체를 리턴해야한다. 이 객체는 서버의 resolvers와 plugins에 contextValue라는 이름으로 접근할 수 있다.

선택적으로 expressMiddlewarestartStandaloneServer 같은 함수를 context 함수로 넘길 수도 있다.

서버는 모든 request에 한번씩은 context 함수를 호출해서 contextValue를 커스터마이징해서 http 헤더같은 요청 세부사항을 커스텀할 수 있게 해준다.

import { GraphQLError } from "graphql";

const resolvers = {
  Query: {
    // Example resolver
    adminExample: (parent, args, contextValue, info) => {
      if (contextValue.authScope !== ADMIN) {
        throw new GraphQLError("not admin!", {
          extensions: { code: "UNAUTHENTICATED" },
        });
      }
    },
  },
};

interface MyContext {
  // You can optionally create a TS interface to set up types
  // for your contextValue
  authScope?: String;
}

const server = new ApolloServer<MyContext>({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  // Your async context function should async and
  // return an object
  context: async ({ req, res }) => ({
    authScope: getScope(req.headers.authorization),
  }),
});

타입스크립트를 사용한다면 context 함수에 전달할 파라미터 타입을 ApolloServer에 전달해주어야한다.

context 초기화 함수가 비동기이기 때문에, 데이터베이스 연결설정이나 다른 작업이 완료될 때까지 기다리는게 가능하다.

context: async () => ({
  db: await client.connect(),
})

// Resolver
(parent, args, contextValue, info) => {
  return contextValue.db.query('SELECT * FROM table_name');
}

Throwing errors

기본적으로 context 함수는 에러를 던질 때, apollo server는 JSON응답으로 500 HTTP 코드를 반환한다. 만약 에러가 GraphQLError가 아니면, 에러 메세지 앞에 Context creation failed: 가 붙게된다.

http 기반 GraphQLError의 HTTP 상태코드를 바꿀 수도 있다.

context: async ({ req }) => {
  const user = await getUserFromReq(req);
  if (!user) {
    throw new GraphQLError('User is not authenticated', {
      extensions: {
        code: 'UNAUTHENTICATED',
        http: { status: 401 },
      }
    });
  }

  // If the below throws a non-GraphQLError, the server returns
  // `code: "INTERNAL_SERVER_ERROR"` with an HTTP status code 500, and
  // a message starting with "Context creation failed: ".
  const db = await getDatabaseConnection();

  return { user, db };
},

The contextValue object

context 함수는 contextValue라는 객체를 리턴한다. 그 객체를 통해서 plugins와 resolvers에 접근할 수 있다.

Resolvers

리졸버는 contextValue 인자를 수정해서는 안된다. 이 객체는 모든 resolvers에서 일관성이 있어야하고, 예상치못한 에러를 막을 수 있도록 보장되게 한다.

resolvers는 세번째 인자를 통해 contextValue 객체를 공유한다.모든 리졸버는 contextValue에 접근함으로써 특정한 operation을 실행시킬 수 있다.

Plugins

빌트인과 커스텀 플러그인은 request 라이프사이클 함수들을 통해서 contextValue에 접근할 수 있다.

interface MyContext {
  token: string;
}

const server = new ApolloServer<MyContext>({
  typeDefs,
  resolvers: {
    Query: {
      hello: (root, args, { token }) => {
        return token;
      },
    },
  },
  plugins: [
    {
      async requestDidStart({ contextValue }) {
        // token is properly inferred as a string
        console.log(contextValue.token);
      },
    },
  ],
});

const { url } = await startStandaloneServer(server, {
  context: async ({ req, res }) => ({
    token: await getTokenForRequest(req),
  }),
});

Reference