GraphQL+NestJS+TypeORM+MySQLにHelloworldしてみた

## はじめに
GraphQL+NestJS+TypeORM+MySQLという構成でのアプリケーション開発の初期環境構築を試してみました。
MySQL以外は素人なので調べながらなんとか動く構成にしたという感じです。
誰かの参考になれば幸せです。

成果物はこちら

https://github.com/shinobushiva/nestjs-typeorm-ts-example/tree/step1

## 事前準備

`Node.js`, `yarn`, `mysql`をインストールしてください（説明は割愛します）

実行時の環境は次のとおりです。
```
$ npm -v
8.8.0

$ yarn -v
1.22.4

$ mysql -V
mysql  Ver 8.0.28 for macos12.2 on x86_64 (Homebrew)
```

### Nest CLI のインストール
https://github.com/nestjs/nest-cli

次のコマンドを実行して `Nest CLI` をインストールします
```
$ npm install -g @nestjs/cli
```

## プロジェクトの作成と初期動作確認

CLIで新しいNestのプロジェクトを作り、そのフォルダに移動します。
パッケージマネージャーは `yarn` を選択します。

```
$ nest new nestjs-typeorm-ts-example

? Which package manager would you ❤️  to use? 
  npm 
❯ yarn 
  pnpm 

$ cd nestjs-typeorm-ts-example
```

まずはお約束の動作確認を行います。下記コマンドでnestサーバーを起動します。
```
$ yarn start:dev
```
ブラウザで `localhost:3000`にアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5557/0c422dcc-d866-763b-9afc-2cd15c45418b.png)

上記の表示がされれば動作確認完了です。

## GraphQLの導入

GraphQLに必要なパッケージを `yarn` で追加します。
```
$ yarn add @nestjs/graphql @nestjs/apollo graphql apollo-server-express
$ yarn add class-validator class-transformer #Validator使わなければ無くても良いけど
```

### リソースの作成

`tasks` リソースをジェネレーターで作ります。
トランスポートレイヤーは `GraphQL (code first)` で。
`CRUD` エントリポイントも作っちゃいます。
```
$ nest g resource tasks

? What transport layer do you use? 
  REST API 
❯ GraphQL (code first) 
  GraphQL (schema first) 
  Microservice (non-HTTP) 
  WebSockets 

? Would you like to generate CRUD entry points? (Y/n) Y

CREATE src/tasks/tasks.module.ts (224 bytes)
CREATE src/tasks/tasks.resolver.spec.ts (525 bytes)
CREATE src/tasks/tasks.resolver.ts (1109 bytes)
CREATE src/tasks/tasks.service.spec.ts (453 bytes)
CREATE src/tasks/tasks.service.ts (625 bytes)
CREATE src/tasks/dto/create-task.input.ts (196 bytes)
CREATE src/tasks/dto/update-task.input.ts (243 bytes)
CREATE src/tasks/entities/task.entity.ts (187 bytes)
UPDATE src/app.module.ts (1163 bytes)
```

### GraphQLのための設定

`nest-cli.json` を次のように修正します。
```json:nest-cli.json
{
  "$schema": "https://json.schemastore.org/nest-cli",
  "collection": "@nestjs/schematics",
  "sourceRoot": "src",
  "compilerOptions": {
    "plugins": [
      {
        "name": "@nestjs/graphql",
        "options": {
          "introspectComments": true
        }
      }
    ]
  }
}

```

また、`src/app.module.ts` を次のように修正。これで`src/schema.gql`が自動的に生成されるようになります。

```ts:src/app.module.ts
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TasksModule } from './tasks/tasks.module';

@Module({
  imports: [
    TasksModule,
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      debug: true,
      playground: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

## GraphQLの動作確認

ここまで設定して `localhost:3000/graphql` にアクセスすると、 `GraphQL Playground` が立ち上がります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5557/f7f1b565-4871-6c89-218b-ab3e4dc4a1bb.png)

`src/tasks/tasks.service.ts` の `findAll` を次のように変更します。

```ts:src/tasks/tasks.service.ts
...
  findAll() {
    return [];
  }
...
```

次のようなクエリを打つと結果を取得することが出来ます（空配列が返ってきます）

```graphql:query
{
  tasks{
    exampleField
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5557/92b657b0-de6d-658a-83ba-c62892e2f286.png)

## TypeORMの導入

必要なパッケージを追加します。

```
$ yarn add @nestjs/typeorm typeorm mysql2 reflect-metadata
```

### typeorm-extensionの導入

データベースの create や `drop` も行いたいので `typeorm-extension` も導入します。
https://github.com/tada5hi/typeorm-extension

```
$ yarn add -D typeorm-extension
```

### 設定ファイルの作成

設定ファイルを２つ作ります。

```ts:src/config/ormconfig.ts
import { DataSourceOptions } from 'typeorm';

const ormconfig: DataSourceOptions = {
  name: 'default',
  type: 'mysql',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT, 10),
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE,
  synchronize: false,
  logging: false,
  connectTimeout: 30 * 1000,
  entities: [process.cwd() + '/dist/**/entities/**/*.entity.js'],
  migrations: [process.cwd() + '/dist/database/migrations/**/*.js'],
  charset: 'utf8mb4_general_ci',
};
export default ormconfig;
```

```ts:src/config/ormdatasource.ts
import { DataSource } from 'typeorm';
import ormconfig from './ormconfig';

export const AppDataSource = new DataSource(ormconfig);
```

`.env` を作ります

```:.env
# DB
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=password
DB_DATABASE=nestjs-typeorm-ts-example
```

必要に応じてシェルで以下のコマンドを実行して環境変数を設定してください。ここで環境変数を設定してからnestサーバーをサイド起動するのが良いでしょう。
```
$ export $(cat .env | grep -v ^# | xargs)
```

### コマンドの追加

`package.json` にコマンドを追加します。

```json:package.json
...
  "scripts": {
    ...
    "db:create": "ts-node ./node_modules/typeorm-extension/dist/cli/index.js -f ./src/config/ormconfig.ts db:create",
    "db:drop": "ts-node ./node_modules/typeorm-extension/dist/cli/index.js -f ./src/config/ormconfig.ts db:drop",
  },
...
```

### データベース作成

以下のコマンドでデータベースを作成します。エラーが発生した際は `.env` から環境変数を読み込んでいるか、設定が間違っていないかなどを確認してください。

```
$ yarn db:create

yarn run v1.22.4
$ ts-node ./node_modules/typeorm-extension/dist/cli/index.js -f ./src/config/ormconfig.ts db:create
query: SELECT VERSION() AS `version`
query: START TRANSACTION
query: SELECT * FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA` = 'nestjs-typeorm-ts-example' AND `TABLE_NAME` = 'typeorm_metadata'
query: COMMIT
✨  Done in 5.61s.
```

また、データベースを削除する場合は次のように実行します。

```
$ yarn db:drop
```

### エンティティをORMにつなぎこむ

`task`エンティティをORMにつなぎこみます。
細かいコードの説明は割愛（もとい、説明できるほどわかってない）します。

```ts:src/tasks/entities/task.entity.ts
import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql';
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  CreateDateColumn,
} from 'typeorm';

export enum TaskStatus {
  NEW,
  IN_PROGRESS,
  COMPLETE,
}

registerEnumType(TaskStatus, {
  name: 'TaskStatus',
});

@Entity()
@ObjectType()
export class Task {
  @PrimaryGeneratedColumn()
  @Field(() => ID)
  id: string;

  @Column({ length: '255' })
  @Field()
  title: string;

  @Column('text')
  @Field({ nullable: true })
  description: string;

  @Column({
    type: 'enum',
    enum: TaskStatus,
    default: TaskStatus.NEW,
  })
  @Field(() => TaskStatus)
  status: TaskStatus;

  @CreateDateColumn()
  @Field()
  createdAt: Date;

  @CreateDateColumn()
  @Field()
  updatedAt: Date;
}
```

```ts:src/tasks/dto/create-task.input.ts
import { InputType, Field } from '@nestjs/graphql';
import { MaxLength } from 'class-validator';
import { TaskStatus } from '../entities/task.entity';

@InputType()
export class CreateTaskInput {
  @MaxLength(255)
  @Field()
  title: string;

  @Field()
  description: string;

  @Field(() => TaskStatus)
  status: TaskStatus;
}
```

```ts:src/tasks/dto/update-task.input.ts
import { CreateTaskInput } from './create-task.input';
import { InputType, Field, Int, PartialType } from '@nestjs/graphql';

@InputType()
export class UpdateTaskInput extends PartialType(CreateTaskInput) {
  @Field(() => Int)
  id: number;
}
```

マイグレーションを作ります。

```
$ npx ts-node ./node_modules/.bin/typeorm migration:generate src/database/migrations/create-task -d src/config/ormdatasource
```

マイグレーションを実行します。

```
$ npx ts-node ./node_modules/.bin/typeorm migration:run -d src/config/ormdatasource
```

```ts:src/tasks/tasks.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateTaskInput } from './dto/create-task.input';
import { UpdateTaskInput } from './dto/update-task.input';
import { Task } from './entities/task.entity';

@Injectable()
export class TasksService {
  constructor(
    @InjectRepository(Task)
    private taskRepostiory: Repository<Task>,
  ) {}

  async create(createTaskInput: CreateTaskInput) {
    const task = this.taskRepostiory.create(createTaskInput);
    await this.taskRepostiory.save(task);
    return task;
  }

  findAll() {
    return this.taskRepostiory.find();
  }

  async findOne(id: number) {
    return await this.taskRepostiory.findOne({
      where: {
        id,
      },
    });
  }

  async update(id: number, updateTaskInput: UpdateTaskInput) {
    const task = this.findOne(id);
    if (task) {
      await this.taskRepostiory.save(updateTaskInput);
    }
  }

  async remove(id: number) {
    const result = await this.taskRepostiory.delete(id);
    return result.affected > 0;
  }
}
```

```ts:src/tasks/tasks.module.ts
import { Module } from '@nestjs/common';
import { TasksService } from './tasks.service';
import { TasksResolver } from './tasks.resolver';
import { Task } from './entities/task.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([Task])],
  exports: [TypeOrmModule],
  providers: [TasksResolver, TasksService],
})
export class TasksModule {}
```

```ts:src/app.module.ts
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import ormconfig from './config/ormconfig';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TasksModule } from './tasks/tasks.module';

@Module({
  imports: [
    TypeOrmModule.forRoot(ormconfig),
    GraphQLModule.forRoot<ApolloDriverConfig>({
      driver: ApolloDriver,
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      debug: true,
      playground: true,
    }),
    TasksModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
```

ここまででGraphQLからデータを操作してMySQLに格納したり取り出したりできるようになりました。

## GraphQLからデータ操作の動作確認

新しく作成するには次のようなクエリを実行します。

```graphql:query
mutation {
  createTask(createTaskInput:{title:"Hello", description:"Hello World!", status: NEW}) {
    id
    title
    description
    status
    createdAt
    updatedAt
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5557/2a2d98ed-69ab-e74f-741f-15f0f3f88ab0.png)

一覧を取得するには次のようなクエリを実行します。

```graphql:query
{
  tasks{
    id
    title
    description
    status
    createdAt
    updatedAt
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5557/2b608ed8-e827-8e5a-0f1a-3ed4cd560a67.png)

データベースの中はこんな感じに格納されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/5557/23333b2e-7223-7930-7115-efea70377063.png)


## まとめ
実際のところマイグレーション周りの設定とコマンドが一番時間かかりました。参考にした記事から色々とコードを拝借しています。先人の皆様に感謝します。


## 参考
- https://zenn.dev/naonao70/articles/a91d8835f1832b
- https://zenn.dev/msksgm/articles/20211107-typeorm-ormconfig
- https://qiita.com/potato4d/items/64a1f518abdfe281ce01
- https://zenn.dev/hakushun/articles/7daac74ae9af25
- https://zenn.dev/azukiazusa/articles/e84be9735d357e
- https://www.kindacode.com/snippet/using-enum-type-in-typeorm/
- https://www.wakuwakubank.com/posts/729-typeorm-migration/