【2019年10月版】Apollo + Sequelize で GraphQL やるなら TypeScript バッチリの TypeGraphQL がおすすめ!

# TypeScript で Apollo + Sequelize の GraphQL API を実装するなら・・・？

Sequelize のバージョン5以降は TypeScript のサポートが入っており、以前のようなモデルの初期化処理、というかマッピングの記述がだいぶ簡易になりました。

これまでの処理は User モデル定義するのに、まず以下の定義というか宣言処理を書いて・・・

```js:models/user.js
module.exports = (sequelize, DataTypes) => {
  const User = sequelize.define(
    'user',
    {
      id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true,
      },
      name: DataTypes.STRING,
    },
    {
      timestamps: false,
      tableName: 'user',
    }
  );

  User.associate = models => {
    User.hasMany(models.todo);
  };

  return User;
};
```

以下の`models/index.js`でまとめて食わす、というようなやり方が主流だったかと思います。

```js:models/index.js
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');

const sequelize = require('../conf/db');

const db = {
  sequelize,
  Sequelize,
};

fs.readdirSync(__dirname)
  .filter(file => path.extname(file) === '.js' && file !== 'index.js')
  .forEach(file => {
    const model = sequelize.import(path.join(__dirname, file));
    db[model.name] = model;
  });

Object.keys(db).forEach(modelName => {
  if ('associate' in db[modelName]) {
    db[modelName].associate(db);
  }
});

module.exports = db;
```

こんな感じで非常に冗長なよく変わらん処理を、コード補完も効かないjsファイルでプロジェクトごとに毎度、書いていたわけです。つまり **こんなのフレームワーク側で処理するべきでしょ？** と~~(声を大にして)~~言いたいわけです。

最近の Sequelize では `Model` を継承した Class を宣言し、`init` 
メソッドでマッピングを記述すれば CRUD 操作系のメソッドも備えたバッチリな永続化層がさくっと構築できます。

```ts:models/user.ts
import { Model, UUID, UUIDV1, STRING } from "sequelize";
import { sequelize } from "../conf/db";

export class User extends Model {
  public id!: string;
  public name!: string;
  public todos?: Todo[];
}

User.init(
  {
    id: {
      type: UUID,
      defaultValue: UUIDV1,
      allowNull: false,
      primaryKey: true
    },
    name: {
      type: STRING,
      allowNull: false
    }
  },
  {
    sequelize,
    modelName: 'user',
  }
);

// リレーションシップの宣言もここだけでよい。
User.hasMany(Todo);
Todo.belongsTo(User);
```

かの `models/index.js` に相当するファイルはもうありませんよ！

今ではまとめて import するのに便利なファイルとして以下のように記述があるだけです。

```ts:models/index.ts
export * from "./user"
```

これで `import { User } from "models";` とかできちゃうわけです。

さらに、どうせ Sequelize のために**モデルクラスを定義するなら、それをそのままSchema定義に流用したい**ですよね？

ということで、**アノテーションで GraphQL のスキーマ定義をやってくれる** TypeGraphQL がイイ！！のであります。

先ほどの Sequelize のモデルクラスに以下のようにアノテーションを追加すると・・・

```ts:models/user.ts
import { ObjectType, Field, ID } from 'type-graphql'
import { Model, UUID, UUIDV1, STRING } from "sequelize";
import { sequelize } from "../conf/db";

@ObjectType()
export class User extends Model {
  @Field(type => ID)
  public id!: string;
  @Field()
  public name!: string;
  @Field(type => [Todo])
  public todos?: Todo[];
}
...
```

GraphQLでもちゃんと `User` Type が使えるようになります！！

以前は Sequlizeのモデル定義とは別に、GraphQL用のSchemaを定義する必要がありました。

```js:schema/user.js
module.exports = `
  type User {
    id: ID!
    name: String
    todos: [Todo!]!
  }
`;
```

こんな感じでしょうか？しかも文字列で宣言なので当然、コード補完はなく、動かしてみて起動時にエラー、タイポがあればテスト時にエラー、エラーが出てみないと宣言に問題があるかわからないので、とりあえずデプロイして動かしてみて、トライ＆エラー。。。なんて非効率なことをやっていたわけです。。。

TypeGraphQLでは、上記の文字列でSchemaを宣言する箇所そのものがいらなくなります。
その変わり Resolver 側のメソッドで入力値の型をきっちり宣言する必要があります。ですがそのお陰で型チェックが厳しく入り、コード補完も効いてコンパイルが通ればちゃんと動く(GraphQLとして整合性のとれた) GraphQL の API が作れるわけです。

というわけで早速、Sequelize + TypeGraphQL + Apollo +TypeScript のプロジェクトを作ってみましょう～！

## 本家のマニュアルはこちらから

今回は一通りの説明は致しますが、TypeGraphQLについてもSequelize+TypeScriptについてもGraphQLについてもフォローしなければならない情報が多いです。

該当箇所の本家マニュアル、サンプルなどを先に以下に貼り付けておきます。

- [TypeGraphQL: 本家サイト] (https://typegraphql.ml/)
- [TypeGraphQL: インストール方法などのマニュアル](https://typegraphql.ml/docs/installation.html)
- [TypeGraphQL: 実装サンプル](https://github.com/MichalLytek/type-graphql/tree/v0.17.5/examples/simple-usage)
- [Sequelize: TypeScriptについて](https://sequelize.org/master/manual/typescript.html)
- [Apollo Sequelize Typescript の実装例](https://github.com/dooboolab/ts-apollo-sequelize)

上記を踏まえて、以下の手順で実装していきます。

## 1. プロジェクトの作成

`npm install` するにも結構な量のパッケージが必要なので、package.jsonの dependencies を以下に記載します。

```json:package.json
...
  "dependencies": {
    "@types/bluebird": "^3.5.28",
    "apollo-server": "^2.9.7",
    "dataloader-sequelize": "^2.0.1",
    "graphql": "^14.5.8",
    "merge-graphql-schemas": "^1.7.0",
    "mysql2": "^1.7.0",
    "reflect-metadata": "^0.1.13",
    "sequelize": "^5.21.1",
    "sequelize-graphql-schema": "^0.1.70",
    "type-graphql": "^0.17.5"
  },
  "devDependencies": {
    "@types/node": "^12.11.6",
    "tslint": "^5.12.0",
    "typescript": "^3.2.2"
  },
...
```

量が多いですが、頑張って `npm i` してください。(いや、package.jsonにコピペして`npm i`でいいか。)

今回は RDBMS に MariaDB使いますのでクライアントとして `mysql2` 入れてますがこれはお好みでOKです。
`typescript`は '3.2.2' ちょっと古め、というか最新のものではないですが、ちゃんと動きましたね。

また、tsconfig.json もいくつか制約を追加しないとダメなので(アノテーション使うので・・・)、動いた結果を以下に記載します。

```json:tsconfig.json
...
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "outDir": "lib",
    "sourceMap": true,
    "strict": true,
    "target": "es2017",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "resolveJsonModule": true,
  },
...
```

targetは `es2016`以降がマストなのと、`esModuleInterop`、`emitDecoratorMetadata`、`experimentalDecorators`のこの辺りがマストですね。他のプロジェクトではなかなか使用しないオプションかと思います。
TypeGraphQLの本家サイトにも設定手順は記載されていますが、自分の環境ではそれだけではダメでした。後は環境というかお好みで調整＆怒られたら適宜、調整して・・・といった感じです。

### 1-2. Sequelize の接続定義

`db/conf.ts` を以下のような内容で作成しておきます。MariaDB の接続先の設定です。

```ts:db/conf.ts
import { Sequelize } from 'sequelize';

export const sequelize = new Sequelize("example","example" "example",
  {
    dialect: 'mysql',
    host: "localhost",
    port: 3306,
  }
);
```

DBの接続は今回の主役ではないので思い切り手抜きしてまして、わかりにくくてすいません。。。`example` はそれぞれ、`データベース名`、`ユーザ名`、`パスワード`の3項目に対応しています。
で、ローカルホストの3306ポートに繋いでね、と。

## 2. Sequelizeモデル兼GraphQLスキーマ定義

今回の**真骨頂その１**ですが、冒頭にすでに掲げてしまいました。・・・まぁその、以下に再度掲載いたします。

```ts:models/user.ts
import { ObjectType, Field, ID } from 'type-graphql'
import { Model, UUID, UUIDV1, STRING } from "sequelize";
import { Todo } from "./todo";
import { sequelize } from "../db/conf";

@ObjectType()
export class User extends Model {
  @Field(type => ID)
  public id!: string;
  @Field()
  public name!: string;
  @Field(type => [Todo])
  public todos?: Todo[];
}

User.init(
  {
    id: {
      type: UUID,
      defaultValue: UUIDV1,
      allowNull: false,
      primaryKey: true
    },
    name: {
      type: STRING,
      allowNull: false
    }
  },
  {
    sequelize,
    modelName: 'user',
  }
);

User.hasMany(Todo);
Todo.belongsTo(User);
```

Classの宣言にアノテーションでRDB用のマッピングも書けたらなぁ。。。と思いますが、今回は良しとしましょう。

`Model`を`extends`した`User`を宣言し、`init`メソッドでDBのマッピングを宣言します。同時に`User`クラスのアノテーションで GraphQL の型とのマッピングを行っています。だいぶ記述量が減っています。
さらに見た目では若干、うんざりなコード量ですが、ほとんど**コード補完が効く**ので実際のタイプする量は格段に減っています。
また、設定値の方もだいたい列挙値や型宣言されているので、コード補完が効くし候補リストに説明も出るので、以前のように "ここの設定、どう書くんだっけ？" みたいなことも格段に減ります。

Sequelize はもともとの機能で DBのマイグレーションをやってくれるので、ここでの宣言からテーブル作成も自動で行えます。
これ、開発の効率はかなりよいのではないでしょうか。。。(GraphQL書くのが一番大変だというツッコミは・・・あると思いますｗ)

## 3. Resolver 兼 GraphQLのスキーマ定義パート2

Resolver で実際の GraphQL の API インタフェースと、Sequelize を使ったビジネスロジックの実装、バインドを行います。
ここはビジネスロジックの本丸なのでしっかりと実装が必要ですが、ここでも TypeGraphQL のアノテーションが大活躍です。

試しには `User` の CRUD 操作を実装してみましょう。

```ts:resolvers/user.ts
import { Resolver, Query, InputType, Field, Arg, Ctx, Mutation, Int } from 'type-graphql';
import { User } from "../models";
import { Context } from "apollo-server-core";

@InputType({ description: "New User Argument" })
class AddUserInput implements Partial<User> {
  @Field()
  name!: string;
}

@InputType({ description: "Update User Argument" })
class UpdateUserInput implements Partial<User> {
  @Field()
  id!: string;
  @Field()
  name!: string;
}

@Resolver()
export class TodoAppResolver {
  @Query(returns => User, { nullable: true })
  async user(@Arg("id") id: string): Promise<User | null> {
    return User.findByPk(id);
  }


  @Mutation(returns => User)
  async addUser(@Arg("data") newUser: AddUserInput, @Ctx() ctx: Context): Promise<User> {
    return User.create(newUser);
  }

  @Mutation(returns => [Int, [User]])
  async updateUser(@Arg("data") updateUser: UpdateUserInput, @Ctx() ctx: Context): Promise<[number, User[]]> {
    return User.update(updateUser, {
      where: {
        id: updateUser.id
      }
    });
  }
}
```

いきなり `@InputType` や `@Resolver()`、`@Query`、`@Mutation` などアノテーションの嵐で面食らうかもしれませんが、ココこそが 'TypeGraphQL' の本丸で、Resolverクラスで記述したメソッド、そのものを、そっくりそのままGraphQLのAPIとして公開してくれるように、ここで細かく宣言とアノテーションとを入れています。
また、`User`モデルのメソッドに`findByPk`、`create`、`update`などのCRUD操作のメソッドが`<User>`付きで使えるように=型パラメータ付きで戻り値もちゃんとUserとなっており、Promizeの中身がanyで戻ってこないようになっております。自前でキャストしなくても、TypeGraphQLのアノテーションが要求する`戻り値`に合わせられるので、自然な記述ができます。TypeGraphQL＋Sequelize、やるじゃない！

以前は、Resolverは js、引数や戻り値データ型は Schema で文字列で宣言、という悲惨な開発状況から一転、ここでは引数も戻り値も**TypeScriptで宣言した通り**に動くのです。素晴らしい。。。コード補完もバッチリ効くしね！

## 4. Apollo Server の起動準備

SequelizeとTypeGraphQLのお蔭でApolloのやることは本当にほとんどなくなってしまいました。

server.tsを以下のように記述しておきます。

```ts:server.ts
import "reflect-metadata";

import { ApolloServer } from 'apollo-server';
import { buildSchema } from "type-graphql";

import * as db from "./db/conf";

async function serve() {

  db.sequelize
    .sync({ force: false, alter: true })
    .catch(console.log);

  const schema = await buildSchema({
    resolvers: [__dirname + "/resolvers/**/*.js"],
  });

  const server = new ApolloServer({ schema, playground: true });

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

serve().catch(console.log);

```

`serve` 関数の中身ですが、まず `sequelize.sync` の箇所は以下の記事を参考に、Apolloサーバー起動時に[Create/Alter] Tableさせてます。
- [Sequelizeのsyncで ALTER TABLE](https://qiita.com/takaboh/items/44b6185eead7f86818a1)

続いて `buildSchema` ですね。公式のチュートリアルでは `/resolvers/**/*.ts` となっていますが、今回は "tsc" 通してから動かしますので実行時の `*.js`を読み込むように設定してます。

そして、ApolloServerのインスタンスをnewしてlistenするだけ。

とんでもなく簡単になってますね。。。

## 5. MariaDB の起動

やっと出番です、MariaDB。しかし、わざわざインストールして設定して、なんてやってられないのでいつもの Docker Compose で一撃起動してしまいますよ！

```yaml:docker-compose.yml
version : "3"
services:
  db:
    image: mariadb
    restart: always
    ports:
      - 3306:3306
    environment:
      - MYSQL_ROOT_PASSWORD=example
      - MYSQL_DATABASE=example
      - MYSQL_USER=example
      - MYSQL_PASSWORD=example

  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080
```

接続設定は超適当で申し訳ないですが、とりあえず"example"です！
ポート3306を転送させて localhost:3306 で MariaDBに接続できるようにしております。

ちなみに `mariadb` のラインナップは以下に各バージョン揃ってます。

- [Docker Hub: MariaBD](https://hub.docker.com/_/mariadb)

~~(しっかし・・・ちょっと前まで MySQLのインストールに1時間くらいかかってたような気がしたんだけどな・・・)~~

で、以下のコマンドで起動します！

```shell
$ docker-compose up -d
```

これで、`http://localhost:8080` で Adminer のサイトが立ち上がるのでデータのチェックもバッチリです！

## 6. Apollo Server の起動

ついにこの時がやってきました。**Apollo、行きまーす！！**のお時間です。

package.json に以下のスクリプトを追加しましょう。

```json:package.json
...
  "scripts": {
    "start": "tsc && node lib/server.js",
  }
...
```

そして、`npm start` で起動です！

```
$ npm start

🚀 Server ready at http://localhost:4000/
Executing (default): CREATE TABLE IF NOT EXISTS `users` (`id` CHAR(36) BINARY NOT NULL , `name` VARCHAR(255) NOT NULL, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
Executing (default): SHOW FULL COLUMNS FROM `users`;
Executing (default): SELECT CONSTRAINT_NAME as constraint_name,CONSTRAINT_NAME as constraintName,CONSTRAINT_SCHEMA as constraintSchema,CONSTRAINT_SCHEMA as constraintCatalog,TABLE_NAME as tableName,TABLE_SCHEMA as tableSchema,TABLE_SCHEMA as tableCatalog,COLUMN_NAME as columnName,REFERENCED_TABLE_SCHEMA as referencedTableSchema,REFERENCED_TABLE_SCHEMA as referencedTableCatalog,REFERENCED_TABLE_NAME as referencedTableName,REFERENCED_COLUMN_NAME as referencedColumnName FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_NAME = 'users' AND CONSTRAINT_NAME!='PRIMARY' AND CONSTRAINT_SCHEMA='example' AND REFERENCED_TABLE_NAME IS NOT NULL;
Executing (default): ALTER TABLE `users` CHANGE `name` `name` VARCHAR(255) NOT NULL;
Executing (default): ALTER TABLE `users` CHANGE `createdAt` `createdAt` DATETIME NOT NULL;
Executing (default): ALTER TABLE `users` CHANGE `updatedAt` `updatedAt` DATETIME NOT NULL;
Executing (default): SHOW INDEX FROM `users`
....
```

MariaDBとの接続がうまくいけばそのままテーブル作成まで実行されてしまうはずです。Sequelize、賢いよ！

では、GraphQL Playground の画面からデータを投入してみましょう。

## 7. GraphQL Playground 画面での確認

ブラウザから`http://localhost:4000`を開きます。

ユーザーを一件登録しますので、左上のクエリのペインに・・・

```graphql
mutation CreateUser($userInput: AddUserInput!){
  addUser(data: $userInput) {
    id
    name
  }
}
```
と記述し、渡す引数を左下の'QUERY VARIABLES'のペインに記載します。

```graphql
{
  "userInput":{"name": "user1"}
}
```

これで画面中央の再生ボタンクリックで`user1`という名前のユーザーが登録されるはずですが・・・？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/f353e165-45f3-314e-bec4-42ba89eb41fc.png)

画面中央の再生ボタンクリックで右側のペインにidがUUIDで自動発番された`user1`のレコードが返却されました！

Adminer の方でも確認しましょう。

ブラウザで `localhost:8080` を開きます。DBへのログイン画面ではサーバーの接続先は adminer コンテナの中から解決できるホスト名、つまり docker-compose.ymlで指定したサービス名の `db` もしくは `docker-compose ps`で表示されるコンテナ名を指定します。あとはユーザー名、パスワード、データベースを `example` と入れればログインできるはずです。

そこから、`users` テーブルをクリックし、"データ"をクリックしてみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/a36e58e0-c23f-b47a-b744-d00976d132d9.png)

(あたりまえだけど)ちゃんと入ってるーー！！！
ここまで、短かったような、長かったような・・・

## 8. リレーションを設定したテーブルはどうなっているのか？

上記の`user`モデルの定義・宣言の箇所でちらっと出てきたリレーションの設定ですが・・・

```ts:models/user.ts
...

User.hasMany(Todo);
Todo.belongsTo(User);
```
最後に `Todo` モデルとのリレーションを設定していますね・・・？

`Todo` モデルは以下のような定義をしています。

```ts:models/todo.ts
@ObjectType()
export class Todo extends Model {
  @Field(type => ID)
  public id!: string;
  @Field(type => User)
  public user!: User;
  @Field()
  public title!: string;
  @Field()
  public content?: string;
  @Field()
  public readonly createdAt!: Date;
  @Field()
  public readonly updatedAt!: Date;
}
Todo.init({
  id: {
    type: UUID,
    defaultValue: UUIDV1,
    allowNull: false,
    primaryKey: true,
  },
  title: {
    type: STRING,
    allowNull: false,
  },
  content: STRING,
},
  {
    sequelize,
    modelName: 'todo',
  }
)
```
と、Classのフィールド宣言はモリモリですが、`init`でのDBスキーマの定義はシンプルに`id`,`title`,`content`のみを定義しているだけです。

これが実際のテーブルには以下のように反映されます。

![テーブル  todos - db - Adminer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/335150/acdcfe4b-40e2-bda2-326a-c4cd7d77987c.png)

まず、フィールドの一覧で `createdAt` と `updatedAt` がありますね。。。
実は Sequelize は `init` でのオプションでデフォルトでこのカラムを追加するスイッチが有効になっています。
無効にするには、

```ts
...
  {
    sequelize,
    modelName: 'todo',
    timestamps: false
  }
)
```
と、`timestamps` を `false` に指定する必要があります。
ここではこのようなオプションがモリモリです。こちらもコード補完で候補一覧を出すとたくさん出てきますので、用途に合わせて選びながら設定が可能です。これもありがたいです。

続いて、`userId` フィールド、indexに`userId`、さらに外部キー制約に `userId` とこちらもモリモリでリレーションの設定が入ってます。
いや～、Sequelize、賢い。

本日は簡易なリレーションのみのチェックでしたが、実際のプロジェクトではリレーションバリバリのクエリがマストかと思います。

とりあえず動作確認はできたということで、本日はここまでとします！