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