GraphQL Server の実装
TweetTypeScript Node 環境を作る
以下リポジトリを参照していただきたい。
一応動く Jest もいける https://github.com/superyusuke/ts-webpack-import
GraphQL にした https://github.com/superyusuke/ts-abimp-apollo
import { music } from "src/music" のような絶対パスを用いた import を実現するためには webpack を用いる必要がある。絶対パスを用いた import が使用できない場合には import { music } from "../../src/music" といった相対パス特有の、階層をかなり移動するパス指定を行うことになる。これは基本的にプロダクションレベルのアプリケーション開発においては避けるべきだろう。このような絶対パスを用いた import をするためにはサーバー開発においても webpack を用いる必要がある。たとえ tsconfig.json に以下のような設定項目を書いても、実はこれは実行時に起きる読み込みエラーの解決はしていない。
"paths": {
      "src/*": ["src/*"]
    }最小限の GraphQL Server
以下のコードは最小限の GraphQL Server である。
express 等を用いなくても ApolloServer 自体がサーバー機能を提供している。BFF を実装するのであればこれだけを用いればよい。
const typeDefs = gql`
  type Query {
    getAuthor(id: Int!): Author
  }
  type Author {
    name: String
    id: ID
  }
`;
const resolvers = {
  Query: {
    getAuthor: () => ({ name: "nakanishi", id: "123" })
  }
};
const server = new ApolloServer({
  typeDefs,
  resolvers
});
server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});
export default server;以下のコードではサーバーを作成している。typeDefs は schema と同じ意味で、これによって GraphQL server が返す値に関する型定義をする。typeDefs = schema によって型は明確になるが、実際にどんな値を返すのかは、ここでは定義できない。実際に返す値の定義は resolver によって行われる。これも server を作成する際に渡している。
const server = new ApolloServer({
  typeDefs,
  resolvers
});typeDefs = schema と resolver
schema は、GraphQL サーバーが返す値の型定義の集合だ。
以下のコードが示すのは、Query のうち getAuthor というものは、引数として文字列型の id を受け取って、Author 型の値を返すということである。
import { ApolloServer, gql } from "apollo-server";
const typeDefs = gql`
  type Query {
    getAuthor(id: Int!): Author
  }
  type Author {
    name: String
    id: ID
  }
`;
const resolvers = {
  Query: {
    getAuthor: () => ({ name: "nakanishi", id: "123" })
  }
};つまり以下のようなクエリを投げれるということであり、その結果として { name: "strings", id: "1234" } といった形式の値が取れるということを定義している。
query {
  getAuthor(id:1) {
    name
    id
  }
}schema が型を定義しているわけだが、その実行結果は resolver に定義する。上記 resolver は、Query のうち getAuthor を実行した際の解決を定義する。resolver は関数として定義する。ここでは単純にオブジェクトを返している。このオブジェクトは schema で定義した Author と型が一致している。
resolver の関数、例えばここでは getAuthor に与えられている関数は最大4つの引数を用いることができる。このうち今重要なのは二番目に与えられる args である。ここには Query で渡した値が入ってくる。getAuthor クエリが受け取ることになっている id を resolver で用いることができる。
const typeDefs = gql`
  type Query {
    getAuthor(id: String!): Author
  }
  type Author {
    name: String
    id: ID
  }
`;
const resolvers: Resolvers = {
  Query: {
    getAuthor: (parent, args, context) => ({ name: "nakanishi", id: args.id })
  }
};context から必要なデータを注入する
ApollorServer を作る際に context から外部のデータを resolver に注入することができます。https://www.apollographql.com/docs/apollo-server/data/data/#context-argument
注入する際にはオブジェクトを返す関数を渡します。
const resolvers = {
  Query: {
    getAuthor: (parent, args, context) => {
      return context.authors.find(author => author.id === args.id);
    }
  }
};
const authors = [{ id: "1", name: "nakanishi" }, { id: "2", name: "satou" }];
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => ({
    authors
  })
});context は resolver の第三引数に渡され、アクセスすることができます。
Schema から型定義ファイルを作成し、Resolver で型の恩恵を受ける
必要なパッケージを入れる。
yarn add @graphql-codegen/cli --dev
yarn add @graphql-codegen/typescript @graphql-codegen/typescript-resolvers --dev設定ファイルを置く
schema: "./src/index.ts"
overwrite: true
generates:
  ./src/types/graphql.d.ts:
    config:
      useIndexSignature: true
      contextType: ./context#Context
    plugins:
      - typescript
      - typescript-resolvers以下 npm script を実行する
"codegen": "gql-gen --watch"すると schema から、resolover のための型定義を生成することができる。
どこへ書き出されるか。 以下で定義した場所へである。
generates:
  ./src/types/graphql.d.ts:さらに context にも型を与えることができる。 https://github.com/dotansimha/graphql-code-generator/blob/master/docs/plugins/typescript-resolvers.md
ドキュメントにも書いてあるが、コマンドによって生成される graphql.d.ts から相対的にファイル名を指定すること。ここでは context.d.ts を参照している。#Context によって Context 変数を参照している。
    config:
      useIndexSignature: true
      contextType: ./context#Contextつまり以下の Context という type を参照している。
type Author = {
  name: string;
  id: string;
};
export type Context = {
  authors: Author[];
};これで resolver ないで context を参照した際に、全て型がつく。
resolve の解決関係の型を補足する
import { ApolloServer, gql } from "apollo-server";
import { Resolvers } from "src/types/graphql";
const typeDefs = gql`
  type Query {
    getAuthor(id: String!): Author
  }
  type Author {
    name: String!
    id: ID!
    books: [Book!]!
  }
  type Book {
    title: String!
  }
`;
const resolvers: Resolvers = {
  Query: {
    getAuthor: (parent, args, context) => {
      const authors = context.authors;
      return authors.find(author => author.id === args.id);
    }
  },
  Author: {
    books: () => {
      return [{ title: "book title" }];
    }
  }
};
const authors = [{ id: "1", name: "nakanishi" }, { id: "2", name: "satou" }];
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => ({
    authors
  })
});
server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});
export default server;export type AuthorParent = {
  id: string;
  name: string;
};schema: "./src/index.ts"
overwrite: true
generates:
  ./src/types/graphql.d.ts:
    config:
      useIndexSignature: true
      contextType: ./context#Context
      mappers: # これが重要
        Author: ./parent#AuthorParent
    plugins:
      - typescript
      - typescript-resolvers以下ドキュメントを参照
ようは、resolver で author を返す際に、そこにはない例えば books といった値は Book 用の resolver で解決させる時に、それが author には存在しないことを示さないといけない。そのために mapper で指定する。その設定を上記ファイルでおこなっている。
ただ上記の例でいえば books に余計なプロパティがあれば怒られるが、必要なものがあるかは判断されていないので注意。
BFF ように API ように値を使う
apollo-datasource-rest