Apollo serverを書くにあたって、ドキュメントがほとんどjavascriptでtypescriptで書くのがちょっと手こずったので覚書。
GraphQL Codegen
GraphQLの魅力はなんといってもそのスキーマの堅実さにありますよね。その堅実さをTypescriptの型定義にあやかろうというのがGraphQL Codegenです。
まずはnpmから必要なモジュールを取ってきてください
npm install @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-resolvers graphql-toolkit --save-dev
SDLで記述されたschema.graphql
が作成できたら、ルートフォルダあたりにcodegen.yml
を記述してください
overwrite: true
documents: null
schema: "**/*.graphql"
generates:
src/generated/graphql.ts:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-resolvers"
config:
useIndexSignature: true
avoidOptionals: true
src/generated/graphql.ts
にいろいろな型が生成されます。後述するApollo特有のものもありますが、普通のtypeやinput typeをそのまま型にしたものもあるので有効活用できます。
エントリポイントの作成
ほぼチュートリアルと一緒ですが、index.tsを作成します。
import { ApolloServer, gql } from 'apollo-server';
import fs from 'fs';
import path from 'path';
import resolvers from './resolvers'; //後で定義
import { dataSources, context } from './datasources'; //後で定義
const typeDefs = gql(fs.readFileSync(path.resolve(__dirname, '<スキーマファイルへのパス>'), 'utf8'));
const server = new ApolloServer({
typeDefs,
resolvers,
dataSources,
context
});
server.listen(4002).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
typeDefsの定義がエレガントじゃない気がしますが...もっといい方法募集してます。
Datasource, Contextの定義
とりあえず、contextを定義します。
export const context = async () => ({
/* Contextの定義 */
ctx1: "foo"
ctx2: "bar"
});
type PartialContext = ReturnType<typeof context> extends Promise<infer T> ? T : never;
Typescriptのinferについては、私の過去の記事を参照してください❤️
次にDataSourcesを定義します。
export const dataSources = () => ({
/* Datasourceの定義 */
ds1: new MyDataSource1<PartialContext>(),
ds2: new MyDataSource2<PartialContext>()
});
なんでDataSourceがジェネリクスを利用しているのかは後述いたします。実際に使われるContextの方は、PartialContextにdatasourceを加えたものになります。
export type TContext = PartialContext & {
dataSources: ReturnType<typeof dataSources>
};
自分のDatasourceを追加する
こんな感じでやります。
import { DataSource, DataSourceConfig } from 'apollo-datasource';
import { InMemoryLRUCache, KeyValueCache } from 'apollo-server-caching';
export interface Context {
ctx1: string
}
export default class MyDataSource1<T extends Context> extends DataSource<T> {
context?: Context
cache?: KeyValueCache
constructor() {
super();
}
initialize({ context, cache }: DataSourceConfig<Context>) {
this.context = context;
// this.context.ctx1にアクセスできる
this.cache = cache || new InMemoryLRUCache();
}
}
ジェネリクスを使ったのは、DataSourceに必要なContextがPartialContextと一貫性があることを保証するためです。
Resolverの定義
import { TContext } from './datasources';
import { Resolvers } from './generated/graphql';
export const resolvers: Resolvers<TContext> = {
// resolverのなかみ
};
やっとgraphql-codegenの強みが出てきました。ResolversとTContextによって、resolver関数の引数の型が推論されるようになります。resolverを分解してファイルをコンパクトにすることもできます。
import { TContext } from './datasources';
import { Resolvers, QueryResolvers, UserResovlers } from './generated/graphql';
const queryResolvers: QueryResolvers<TContext> = {...}
const userResolvers: UserResolvers<TContext> = {...}
export const resolvers: Resolvers<TContext> = {
Query: queryResolvers,
User: userResolvers
};
Scalarの定義
そもそもscalarの仕様がわかりにくすぎてヤバイ感じでした。
こんな感じです
import {
GraphQLScalarType,
Kind,
GraphQLScalarTypeConfig,
ValueNode
} from 'graphql';
import moment from 'moment';
const config: GraphQLScalarTypeConfig<moment.Moment, string> = {
name: 'DateTime',
description: 'DateTime custom scalar type, in the form of YYYY-MM-DDTHH:mm:ss',
parseValue(value: string) {
return moment(value);
},
serialize(value: moment.Moment) {
return value.format();
},
parseLiteral(ast: ValueNode) {
switch (ast.kind) {
case Kind.STRING:
return moment(ast.value);
default:
return null;
}
}
};
export const datetimeScalar = () => new GraphQLScalarType(config);
GraphQLScalarTypeConfig<moment.Moment, string>
は、SDLではstring型であり、resolverの第二引数ではMoment型であるScalar型であることを示しています。parseValue
はSDL→resolver、serialize
はresolver→SDLへの変換となります。
parseLiteral何に使うんですかね?ようわからんです。
Directiveの定義
まだ使ったことないのでわからんとです
Federationの利用
Apolloを使う理由の一つが、federationによるマイクロサービスの定義のしやすさかもしれません。
まず、codegen.ymlに次を追加します
generates:
src/generated/graphql.ts:
plugins:
...
config:
avoidOptionals: true
useIndexSignature: true
federation: true #追加
あとはチュートリアルの通りにschemaをすれば良いです。