TypeScript + express-graphql + TypeORM on Node.js ( for MySQL ) 環境を構築したった

⚠️⚠️⚠️ Node で GraphQL サーバ実装するなら、以下の構成の方がオススメです ⚠️⚠️⚠️

https://qiita.com/les-r-pan/items/43c4013e09caab54e16e

# はじめに

はじめまして。突然ですが、[GraphQL](https://graphql.org/)、めちゃくちゃ良い技術です。
Rails に載った GraphQL を業務で使ってますが、フロントエンド開発がフッ軽になります。

もっと GraphQL に詳しくなりたい。でも、現在、フロントエンドエンジニアとして勤務中の私には、実務で GraphQL を触ることができたとしても　せいぜい Type をちょろっと修正するくらい。

そこで、趣味で書いてる Vue.js 製 WEB アプリの API に GraphQL を採用することにしました。
導入から API として動かすところまでを勉強がてら実装したので、せっかくだし最小限の構成をご紹介します。備忘録も兼ねて。

・・・Rails に対してのモチベーションが高くない ☺️ ので、今回は express に載せてます。

# 実際にやってみた

TypeScript 使ってますが、サンプルコードの中では ~~面倒臭いので~~ 厳密に取り扱っていない箇所があります。そーりー。

## 下準備

### package.json を用意

```json:package.json
{
  "name": "graphql-on-express",
  "dependencies": {
    "@types/cors": "^2.8.10",
    "@types/express": "^4.17.11",
    "@types/express-graphql": "^0.9.0",
    "@types/mysql": "^2.15.18",
    "@types/node": "^14.14.35",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "express-graphql": "^0.12.0",
    "graphql": "^15.5.0",
    "mysql": "^2.17.1",
    "typeorm": "^0.2.31",
    "typescript": "^4.2.3"
  },
  "devDependencies": {
    "ts-node": "^9.1.1",
    "tsconfig-paths": "^3.9.0"
  },
}
```

DB まわりのアレコレは [TypeORM](https://typeorm.io/) というライブラリに任せます。

・・・package.json で足りない項目がある場合はテキトーに埋めてください 😇

### TypeScript、TypeORM のコンフィグを用意

```json:tsconfig.json
{
  "compilerOptions": {
    "sourceMap": false,
    "noImplicitAny": true,
    "module": "commonjs",
    "target": "es5",
    "lib": [
      "es2018",
      "dom"
    ],
    "moduleResolution": "node",
    "removeComments": true,
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "strictFunctionTypes": false,
    "baseUrl": "./",
    "paths": {
      "@/*": [
        "src/*"
      ],
    },
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
  },
  "include": [
    "./src/**/*.ts"
  ]
}
```

```json:ormconfig.json
{
  "type": "mysql",
  "host": "Your DB endpoint",
  "port": 3306,
  "username": "Your DB username",
  "password": "Your DB password",
  "database": "Your DB name",
  "synchronize": false,
  "logging": false,
  "entities": [
    "src/database/entity/**/*.ts"
  ],
  "migrations": [
    "src/database/migration/**/*.ts"
  ],
  "subscribers": [
    "src/database/subscriber/**/*.ts"
  ],
  "cli": {
    "entitiesDir": "src/database/entity",
    "migrationsDir": "src/database/migration",
    "subscribersDir": "src/database/subscriber"
  }
}
```

### node modules をインストール

```bash
$ npm i
```

## データまわりの作業

### Entity を用意

データベースとプログラムとの間でマッピングされたデータが、どのようなデータ構造をとるのかを定義します。
TypeORM では、このようなデータモデルを Entity という名称で表現するようです。
一般的な MVC フレームワークにおいて、Model と呼ばれているモノにイメージは近いですが、Model と違ってロジックを持たせることはあまり想定してなさ気です。だから、あくまでも **"Model"** じゃなくて　ただの **"Entity"** (実体) なのかと思いました (小並感

```typescript:src/database/entity/user.ts
import { Entity, BaseEntity, Column, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class User extends BaseEntity {
  @PrimaryGeneratedColumn('increment')
  id!: number;

  @Column({ nullable: false })
  name!: string;
}
```

サンプルなので、id と name というカラムだけをもったシンプルな構造を用意します。

### マイグレーションする

なんと、TypeORM はマイグレーションの機能まで提供してくれています。ありがとう。

```bash
$ npx ts-node node_modules/.bin/typeorm migration:generate -n user
```

上記を実行すると、`src/database/migration/xxxxxxxxxxxxx-user.ts` というファイルが生成されます。

続けて、以下を実行します。

```bash
$ npx ts-node node_modules/.bin/typeorm migration:run
```

ずらずらと SQL の実行ログが流れ・・・

```
(省略)

Migration userxxxxxxxxxxxxx has been executed successfully.
query: COMMIT
```

最後にこんなログが出力されれば成功です。

ターミナルから MySQL に直接ログインできる方は実際にテーブルを確認してみてください。

```
(省略)

mysql> desc user;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | int          | NO   | PRI | NULL    | auto_increment |
| name        | varchar(255) | NO   |     | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+
2 rows in set (0.02 sec)
```

こんな感じになってるはず。

・・・テーブル名的には `users` であってほしいけど、Entity を複数形にしなきゃいけないのかな 🤔

## ロジックまわりの作業

### Type を定義

```typescript:src/schema/fields/user/types.ts
import { GraphQLObjectType, GraphQLNonNull, GraphQLString, GraphQLInt, GraphQLInputObjectType } from 'graphql';

export const UserType = new GraphQLObjectType({
  name: 'User',
  fields: {
    id: {
      type: new GraphQLNonNull(GraphQLInt),
    },
    name: {
      type: new GraphQLNonNull(GraphQLString),
    }
  }
});

export const FetchUserInput = new GraphQLInputObjectType({
  name: 'FetchUserInput',
  fields: {
    id: {
      type: new GraphQLNonNull(GraphQLInt),
    },
  }
});

export const CreateUserInput = new GraphQLInputObjectType({
  name: 'CreateUserInput',
  fields: {
    name: {
      type: new GraphQLNonNull(GraphQLString),
    },
  }
});
```

### Schema を定義

実際に運用する際は、Entity に対して query と mutation があり、場合によっては、そこからさらにバリエーションが派生する、なんてこともあります。
ファイルを細かく分けていて、サンプルコードをみてるだけだと「冗長じゃね？」と思うかもしれませんが、上記の理由から処理が増えることを視野に入れてこうしてます。

```typescript:src/schema/index.ts
import { GraphQLSchema } from "graphql";
import { queryType as query, mutationType as mutation } from "./fields";

export const schema = new GraphQLSchema({
  query,
  mutation,
});
```

```typescript:src/schema/fields/index.ts
import { GraphQLObjectType } from 'graphql';
import { userField } from './user';

export const queryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    ...userField.query,
  }
})

export const mutationType = new GraphQLObjectType({
  name: 'Mutation',
  fields: {
    ...userField.mutation,
  }
})
```

```typescript:src/schema/fields/user/index.ts
import { userQuery as query } from './query';
import { userMutation as mutation } from './mutation';

export const userField = {
  query,
  mutation,
};
```

```typescript:src/schema/fields/user/query.ts
import { GraphQLNonNull } from 'graphql';
import * as resolvers from './resolvers';
import { FetchUserInput, UserType } from './types';

const fetchUsers = {
  type: UserType,
  args: {
    input: {
      type: new GraphQLNonNull(FetchUserInput)
    }
  },
  resolve: resolvers.fetchUsers,
}

export const userQuery = {
  fetchUsers,
}
```

```typescript:src/schema/fields/user/mutation.ts
import { GraphQLNonNull, GraphQLList } from 'graphql';
import * as resolvers from './resolvers';
import { UserType, CreateUserInput } from './types';

const createUser = {
  type: new GraphQLList(UserType),
  args: {
    input: {
      type: new GraphQLNonNull(CreateUserInput)
    }
  },
  resolve: resolvers.createUser
}

export const userMutation = {
  createUser,
}
```

### "DB への問い合わせ" や "結果を受け取って返却する" などのコアとなる処理を用意

```typescript:src/schema/fields/user/resolvers.ts
import { User } from "@/database/entity/user"
import { find, findOne, insert } from "../crud-assistant"

// e.g.
type CreateUserArgs = {
  input: {
    // any
  }
}

// e.g.
type FetchUserArgs = {
  input: {
    // any
  }
}

// e.g.
type FetchUsersArgs = {
  input: {
    // any
  }
}

export const createUser = async (args: CreateUserArgs): Promise<typeof User> => {
  return new Promise(async (resolve, reject) => {
    const insertInput = args.input
    const result = await insert<typeof User, CreateUserArgs["input"]>(User, insertInput)

    if (!result) {
      reject()
      return
    }

    const findOneInput = args.input
    const user = await findOne<typeof User, FetchUserArgs["input"]>(User, findOneInput)

    if (user) {
      resolve(user)
    } else {
      reject()
    }
  })
}

export const fetchUsers = async (args: FetchUsersArgs): Promise<Array<typeof User>> => {
  return new Promise(async (resolve, reject) => {
    const findInput = args.input
    const users = await find<typeof User, FetchUsersArgs["input"]>(User, findInput)

    if (users) {
      resolve(users)
    } else {
      reject()
    }
  })
}
```

CRUD の処理を共通化しておきます。
TypeORM の Repository のメソッドと同名で公開。

```typescript:src/schema/fields/crud-assistant.ts
import { BaseEntity, createConnection, getRepository, InsertResult } from "typeorm"

export const insert = async<E extends typeof BaseEntity, I>(Entity: E, input: I): Promise<InsertResult | null> => {
  const connection = await createConnection()
  const repository = getRepository<E>(Entity)

  try {
    return new Promise(async (resolve, reject) => {
      const result = await repository
        .insert({
          ...input
        })
        .catch(async (e) => {
          reject()
        })

      await connection.close()

      resolve(result || null)
    })
  } catch (e) {
    await connection.close()
    return Promise.resolve(e)
  }
}

export const findOne = async<E extends typeof BaseEntity, I>(Entity: E, input: I): Promise<E | null> => {
  const connection = await createConnection()
  const repository = getRepository<E>(Entity)

  try {
    return new Promise(async (resolve, reject) => {
      const result = await repository
        .findOne({
          ...input
        })
        .catch(async (e) => {
          reject()
        })

      await connection.close()

      resolve(result || null)
    })
  } catch (e) {
    await connection.close()
    return Promise.resolve(e)
  }
}

export const find = async <E extends typeof BaseEntity, I>(Entity: E, input: I): Promise<E[] | null> => {
  const connection = await createConnection()
  const repository = getRepository<E>(Entity)

  try {
    return new Promise(async (resolve, reject) => {
      const result = await repository
        .find({
          ...input
        })
        .catch(async (e) => {
          reject()
        })

      await connection.close()

      resolve(result || null)
    })
  } catch (e) {
    await connection.close()
    return Promise.resolve(e)
  }
}
```

これでようやくロジックまわりのファイルが揃いました。

## サーバまわりの作業

### エントリポイントを用意

```typescript:src/index.ts
import * as express from 'express'
import { graphqlHTTP } from 'express-graphql'
import * as cors from 'cors'
import { schema } from './schema'

const port = 4000
const app = express()
app.use(cors())
app.use(express.static('./'));
app.use('/', graphqlHTTP({
  schema,
  graphiql: true
}))

app.listen(port, () => {
  console.log(`Started server, http://localhost:${port}`)
});
```

エントリポイントを用意したら、

```
$ npx ts-node -r tsconfig-paths/register src/index.ts
```

を実行。

これで、http://localhost:4000 にアクセスすると Graph **i** QL という GUI が表示されるようになります。
API としてのリクエストには、POST を用います。

Vue アプリ側からは Vue Apollo 経由で API を call してます。その話はまたどこかでするかもしれないししないかもしれない。

# おわりに

## GraphQL について

冒頭でも触れましたが、フロントエンドの開発においてめちゃくちゃ便利です。
良い技術なわりに、あまり世に浸透していない気がする。もったいない 🥺

## TypeORM について
便利ではありますが、提供してる型が微妙に扱いにくいと感じるところがあったり、(当たり前だけど) GraphQL 側にも型の指定が必要なので TypeORM ↔️ GraphQL 間で同じデータを指してるのに構造の定義が二重管理になっちゃったりと、小さな課題があるので要改善。暇なときに TypeORM 側のコードから GraphQL の Type 定義のコードを自動生成するようなスクリプトを書きたい。(すでに誰かが作ってるかも
まあ、使いこなせれば良きなライブラリな気はします。あと、日本語の文献が豊富ではないです。

おしまい。