TypeScriptでApollo-Serverを構築する

## 概要
TypeScriptでApollo-Serverを構築して、resolverを実装した際のメモ

## ディレクトリ構成

```
$ tree -I node_modules
.
├── nodemon.json
├── package-lock.json
├── package.json
├── src
│   ├── mocks
│   │   ├── message.ts
│   │   ├── room.ts
│   │   └── user.ts
│   ├── resolvers
│   │   ├── message.ts
│   │   ├── query.ts
│   │   ├── room.ts
│   │   └── user.ts
│   ├── resolvers.ts
│   ├── schema.ts
│   └── server.ts
└── tsconfig.json
```

## 準備

### node.jsのインストール
- Apollo-Serverの構築にはnode.jsが必要なので、インストールする
- インストール手順は下記を参考に、nodebrewをインストールし現在の最新（v16.6.2）を利用する
  - https://qiita.com/kyosuke5_20/items/c5f68fc9d89b84c0df09

### 必要なライブラリのインストール

- TypeScriptでApollo-Server利用するための必要最低限のライブラリのみをインストールしている（nodemonは入れる）

```sh
$ npm install -save-dev typescript nodemon ts-node ts-loader
$ npm install apollo-server graphql
```
- package.json の中身を確認し、scriptにコマンドを追記する

```ts:package.json
{
  "name": "grapql",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "ts-node": ".\\node_modules\\.bin\\ts-node",
    "start": "NODE_ENV=development nodemon --config ./nodemon.json"
  },
  "dependencies": {
    "apollo-server": "^3.1.2",
    "graphql": "^15.5.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.12",
    "ts-loader": "^9.2.5",
    "ts-node": "^10.2.0",
    "typescript": "^4.3.5"
  }
}

```


- nodemonを使用して、ファイル更新時に自動的にサーバ再起動を行なうように、node.jsonを作成

```ts:node.json
{
  "watch": ["src"],
  "ext": "*",
  "exec": "ts-node ./src/server.ts"
}
```

- tsconfig.jsonを作成。設定はサバイバルType Scriptから拝借しています
  - https://book.yyts.org/handson/tsconfig.json-settings 

```ts:tsconfig.json
{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "lib": [
      "es2020"
    ],
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "moduleResolution": "node",
    "baseUrl": "src",
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "dist",
    "node_modules"
  ],
  "compileOnSave": false
}
```

## Apollo-Serverの構築

- TypeScriptを利用してApollo-Serverを構築して行く準備ができたので、起動のための設定を行なう
- Apollo-Server起動用ファイルと、Graphqlのスキーマ定義を行うだけで起動ができる

### スキーマ定義
- 簡単なチャットサービスを想定したスキーマ定義を行った。今回使用するのはQueryのみ作成を行ってみた

```ts:schema.ts
import { gql } from 'apollo-server';

export const typeDefs = gql`
  type Query {
    getRooms(id: ID): [Room]
    getUsers(id: ID): [User]
    getMessages(id: ID): [Message]
  }

  type Room {
    id: ID!
    name: String
    messages: [Message]
  }

  type Message {
    id: ID!
    body: String
    user: User
    room: Room
  }

  type User {
    id: ID!
    name: String
    messages: [Message]
  }
`;
```

### サーバ起動用の設定ファイル

- 必要最低限の設定だけを記述した、サーバ起動用のファイルを作成

```ts:server.ts
import { ApolloServer } from 'apollo-server';
import { typeDefs } from './schema';

const server = new ApolloServer({
  typeDefs
});

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

export default server;
```

### 起動

- package.jsonに記述したscriptをして、サーバを起動する

```
$ npm run start
> grapql@1.0.0 start
> NODE_ENV=development nodemon --config ./nodemon.json

[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src/**/*
[nodemon] watching extensions: (all)
[nodemon] starting `ts-node ./src/server.ts`
🚀 Server ready at http://localhost:4000/
```

- ブラウザで `http://localhost:4000/` にアクセスすると、Apollo Studioが表示される。Apollo-Server 3 以前では、playgroudが起動していたが、変わったらしい

![ab43fbf8-5143-4bb5-8953-582fa9ea916e.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366054/7e69d173-7858-fcf8-62e3-f8e5b3adf558.png)



## Resolverを実装してデータを取得する

- ルートクエリに対するresolverを実装して、Apollo Studioを使用してデータを取得してみる
- ここではDBへの接続等は行わず、jsonで作成したmockデータを利用する

### Resolversの実装

- 全てのリゾルバーを一箇所にまとめるためのファイルを作成

```ts:resolvers.ts
import { query } from './resolvers/query';

export const resolvers = {
  Query: query
};
```

- rootクエリーに対するResolverを実装する
- contextを経由してmockデータの中身を検索している
- 動かしてみることを目的にしているのでanyをたくさん使ってます・・・。user、room、message等の型を作製するのが良さそう

```ts:query.ts
export const query = {
  getUsers: (parent: any, args: any, context: any) => {
    let result = context.users.find((v: any) => v.id === args.id);
    return [result];
  },
  getRooms: (parent: any, args: any, context: any) => {
    let result = context.rooms.find((v: any) => v.id === args.id);
    return [result];
  },
  getMessages: (parent: any, args: any, context: any) => {
    let result = context.messages.find((v: any) => v.id === args.id);
    return [result];
  }
};
```

### mockデータの作製
- 各スキーマもmockデータを作成。DBのテーブルのレコードを想定しています。

```ts:users.ts
export const users = [
  {
    id: '1',
    name: 'user1'
  },
  {
    id: '2',
    name: 'user2'
  },
  {
    id: '3',
    name: 'user3'
  },
  {
    id: '4',
    name: 'user4'
  }
];
```

```ts:rooms.ts
export const messages = [
  {
    id: '1',
    body: 'hello1',
    user: '1',
    room: '1'
  },
  {
    id: '2',
    body: 'hello2',
    user: '2',
    room: '1'
  },
  {
    id: '3',
    body: 'hello3',
    user: '3',
    room: '2'
  },
  {
    id: '4',
    body: 'hello4',
    user: '4',
    room: '2'
  }
];
```

```ts:messages.ts
export const rooms = [
  {
    id: '1',
    name: 'room1'
  },
  {
    id: '2',
    name: 'room2'
  },
  {
    id: '3',
    name: 'room3'
  },
  {
    id: '4',
    name: 'room4'
  }
];
```

### Apollo-Server構築

- server.tsに対して、作成したResolverと、mockデータを利用す流ように追記
- contextを利用して、mockデータをresolver全体で扱えるように設定している

```ts:server.ts
import { ApolloServer } from 'apollo-server';
import { typeDefs } from './schema';
import { resolvers } from './resolvers';
import { users } from './mocks/user';
import { messages } from './mocks/message';
import { rooms } from './mocks/room';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: () => {
    return {
      users: users,
      messages: messages,
      rooms: rooms
    };
  }
});

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

export default server;
```

### クエリ実行

- Apollo Studio上で、下記のクエリーを実行してデータの取得を行えることが確認できる

```graphql
query sample {
  getUsers(id: "1") {
    id
    name
  }
  getMessages(id: "1") {
    id
    body
  }
  getRooms(id: "1") {
    id
    name
  }
}
```

- クエリーの実行結果

```ts
{
  "data": {
    "getUsers": [
      {
        "id": "1",
        "name": "user1"
      }
    ],
    "getMessages": [
      {
        "id": "1",
        "body": "hello1"
      }
    ],
    "getRooms": [
      {
        "id": "1",
        "name": "room1"
      }
    ]
  }
}
```


## 子ノードのResolverを実装する

- GraphQLがグラフ構造を持つことで得られる一番のメリットだとおもう
- 親ノードのデータ取得結果を利用して、子ノードデータを取得できる
- Resolverは各ノード（各スキーマ）のことだけを考えて実装するのが理想的


### Resolverを実装

- rootクエリ以外のResolverを実装する

```ts:resolvers.ts
import { query } from './resolvers/query';import { query } from './resolvers/query';
import { user } from './resolvers/user';
import { message } from './resolvers/message';
import { room } from './resolvers/room';

export const resolvers = {
  Query: query,
  User: user,
  Message: message,
  Room: room
};
```

### Resolverの定義

- 引数であるparentを利用して、親ノードのデータ取得結果を利用して、子ノードのデータを取得できるできるように実装を行っていく

```ts:resolvers/user.ts
export const user = {
  messages: (parent: any, args: any, context: any) => {
    let result = context.messages.filter((message: any) => {
      return message.user === parent.id;
    });
    return result;
  }
};
```

```ts:resolvers/message.ts
export const message = {
  user: (parent: any, args: any, context: any) => {
    let result = context.users.find((user: any) => {
      return user.id === parent.user;
    });
    return result;
  },
  room: (parent: any, args: any, context: any) => {
    let result = context.rooms.find((room: any) => {
      return room.id === parent.room;
    });
    return result;
  }
};
```

```ts:resolvers/room.ts
export const room = {
  messages: (parent: any, args: any, context: any) => {
    let result = context.messages.filter((message: any) => {
      return message.room === parent.id;
    });
    return result;
  }
};
```

### クエリの実行

- rootクエリであるgetMessagesのデータ取得結果を利用して、子ノードであるuser、roomのデータを取得できる

```graphql
query sample {
  getMessages(id: "4") {
    id
    body
    user {
      id
      name
    }
    room {
      id
      name
    }
  }
}
```

- クエリの実行結果

```ts
{
  "data": {
    "getMessages": [
      {
        "id": "4",
        "body": "hello4",
        "user": {
          "id": "4",
          "name": "user4"
        },
        "room": {
          "id": "2",
          "name": "room2"
        }
      }
    ]
  }
}
```

## 感想
各ノード（スキーマ）に対するResolverを定義するというのが興味深かった。サーバの監視方法やセキュリティ等は REST API の時とだいぶ異なるようなので、また色々試してみる