Apollo serverをTypescriptで安全に開発する

Apollo serverを書くにあたって、ドキュメントがほとんどjavascriptでtypescriptで書くのがちょっと手こずったので覚書。


## GraphQL Codegen

GraphQLの魅力はなんといってもそのスキーマの堅実さにありますよね。その堅実さをTypescriptの型定義にあやかろうというのがGraphQL Codegenです。

まずはnpmから必要なモジュールを取ってきてください

```bash
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`を記述してください

```yaml: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を作成します。

```typescript: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を定義します。

```typescript:datasources.ts
export const context = async () => ({
  /* Contextの定義 */
  ctx1: "foo"
  ctx2: "bar"
});

type PartialContext = ReturnType<typeof context> extends Promise<infer T> ? T : never;
```

Typescriptのinferについては、私の[過去の記事](https://qiita.com/yutaro-t/items/4b0582e1fbe311e5717e)を参照してください❤️

次にDataSourcesを定義します。

```typescript:datasources.ts
export const dataSources = () => ({
  /* Datasourceの定義 */
  ds1: new MyDataSource1<PartialContext>(),
  ds2: new MyDataSource2<PartialContext>()
});
```

なんでDataSourceがジェネリクスを利用しているのかは後述いたします。実際に使われるContextの方は、PartialContextにdatasourceを加えたものになります。

```typescript:datasources.ts

export type TContext = PartialContext & {
  dataSources: ReturnType<typeof dataSources>
};
```

### 自分のDatasourceを追加する

こんな感じでやります。

```typescript:MyDatasource1.ts
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の定義

```Typescript:resolver.ts
import { TContext } from './datasources';
import { Resolvers } from './generated/graphql';

export const resolvers: Resolvers<TContext> = {
   // resolverのなかみ
};
```

やっとgraphql-codegenの強みが出てきました。ResolversとTContextによって、resolver関数の引数の型が推論されるようになります。resolverを分解してファイルをコンパクトにすることもできます。


```Typescript:resolver.ts
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の仕様がわかりにくすぎてヤバイ感じでした。

こんな感じです

```Typescript:datetime.ts
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](https://www.apollographql.com/docs/apollo-server/federation/introduction/)によるマイクロサービスの定義のしやすさかもしれません。

まず、codegen.ymlに次を追加します


```yaml:codegen.yml
generates:
  src/generated/graphql.ts:
    plugins:
      ...
    config:
      avoidOptionals: true
      useIndexSignature: true
      federation: true #追加
```

あとはチュートリアルの通りにschemaをすれば良いです。