NestJS で TypeORM を使った API を作成する

# コードサンプル

ここで作成したコード全文は Github から確認できる(若干違うかも)
https://github.com/tktcorporation/nestjs-sample/tree/create-some-api-with-typeorm

# DB の準備

## Dockerfile

```Dockerfile
FROM node:12.16.1

RUN npm i -g @nestjs/cli ts-node

ENTRYPOINT ["/bin/bash", "-c"]
```

## docker-compose
```yml
version: "3"
services:
  app:
    build: .
    container_name: app
    working_dir: /app
    volumes:
      - {プロジェクトディレクトリ}:/app
    ports:
      - "3000:3000"
    environment:
      - DB_HOST=db
      - DB_PORT=5432
      - DB_USER=root
      - DB_PASSWORD=password
      - DB_DATABASE_NAME=database
      - NODE_ENV=development
    depends_on:
      - db
  db:
    image: postgres:11
    ports:
      - 5432:5432
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: password
      POSTGRES_DB: database
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
    hostname: postgres
    restart: always
    user: root
```

別段の記述がない限り、以降の操作はすべて Docker コンテナ内にて行う

# 必要なモジュールの追加

```bash
$ yarn add @nestjs/typeorm typeorm pg
```

(@nestjs/typeorm はいらないかも)

# 接続に必要な設定を書いていく

- nestjs 標準でついている連携機構(?)は使わない
  - 標準でついている連携機構(?) -> TypeOrmModule.forRoot() をインポートする方法
- コネクション管理用のクラスを別で作成して使用する
- cli は ormconf.ts の設定を適用して実行する(ormconfig だとアプリ側で自動適用されたりしてよくわからなくなった)

もっと良い方法があればご教示お願いしたく…
(ちょちょいと書いたらおわりでしょーとか思ってたら見事に嵌ってしばらく抜け出せなかった)

## dbconnection.ts

- コネクション管理用のクラスを別で作成して使用する

に当たる部分。

こんな感じ([TypeORM をシングルトンで使う](https://qiita.com/tktcorporation/items/e8a6d2310d548ac48671#dbconnectionts)) のファイルを作成する

## ormconf.js

```js
module.exports = {
  type: 'postgres',
  host: process.env.DB_HOST,
  port: Number.parseInt(process.env.DB_PORT),
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE_NAME,
  synchronize: false,
  logging: process.env.NODE_ENV === 'production' ? ['error'] : 'all',
  migrations: ['src/migration/**/*.ts'],
  cli: {
    migrationsDir: 'src/migration',
  },
};
```

# マイグレーション

この記事では、[こちら](https://qiita.com/techneconn/items/0655f4d88187b65a8520#%E3%83%A2%E3%83%87%E3%83%AB%E5%AE%9A%E7%BE%A9%E4%BD%9C%E6%88%90) で書かれているような Repository パターンではなく、SQL を直接書く方法で DB を扱っていきます。
複雑な SQL を書く場合、O/R Mapper だとうまく書けないことが多いので、最近はこの方法で書くことが多いです。
ただ、実際の Table の型と SELECT してきたときの型が一致しないので、ちゃんとテストを書いておかないと Table の構成を変更したときのエラーを拾いきれなくなるかもです。

## ファイル作成コマンド

```bash
$ ts-node $(npm bin)/typeorm migration:create -f ormconf -n {ファイル名}
```

このコマンドで`00000000000000-{ファイル名}.ts` のファイルが作成される。

## SQL を書いていく

```ts
import { MigrationInterface, QueryRunner } from 'typeorm';

export class {ファイル名}00000000000000 implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
        // この中に実行するSQLを書く
        await queryRunner.query(`
            CREATE TABLE "users" (
                "id" SERIAL NOT NULL,
                "name" VARCHAR(250) NOT NULL
            );
        `);
    }

    // この中には実行したSQLの動作を一つ戻すためのものを書く
    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`
            DROP TABLE "users";
        `);
    }
}
```

## マイグレーション実行

このコマンドで、作成したマイグレーションファイルを実行できる。

```bash
ts-node $(npm bin)/typeorm migration:run
```

こんな感じに表示されれば成功

<details><summary>migration 実行ログ</summary><div>

```bash
$ ts-node $(npm bin)/typeorm migration:run
query: SELECT * FROM "information_schema"."tables" WHERE "table_schema" = current_schema() AND "table_name" = 'migrations'
query: SELECT * FROM "migrations" "migrations" ORDER BY "id" DESC
0 migrations are already loaded in the database.
1 migrations were found in the source code.
1 migrations are new migrations that needs to be executed.
query: START TRANSACTION
query: 
            CREATE TABLE "users" (
                "id" SERIAL NOT NULL,
                "name" VARCHAR(250) NOT NULL
            );
        
query: INSERT INTO "migrations"("timestamp", "name") VALUES ($1, $2) -- PARAMETERS: [1590802150528,"CreateUsersTable1590802150528"]
Migration CreateUsersTable1590802150528 has been executed successfully.
query: COMMIT
Done in 9.74s.
```

</div></details>

# DBデータの操作

せっかくなので、API が呼ばれた時に INSERT, SELECT が走るようにする。

## テストの実行

```bash
$ yarn test
```

## 補足

これ以降の実装をした後にテストを実行すると、どこかで下記のようなエラーが発生するかもしれない。

```
 FAIL  src/infrastructure/dbconnection.spec.ts
  ● Test suite failed to run

    Cannot find module 'src/infrastructure/dbconnection' from 'dbconnection.spec.ts'

      1 | import { Connection } from 'typeorm';
    > 2 | import { DBConnection } from 'src/infrastructure/dbconnection';
        | ^
      3 | 
      4 | describe('DBConnection', () => {
      5 |   let connection: Connection;

      at Resolver.resolveModule (../node_modules/jest-resolve/build/index.js:299:11)
      at Object.<anonymous> (infrastructure/dbconnection.spec.ts:2:1)
```

その場合は、 次の操作を行う

- `package.json` の編集

```json
{
  "name": "hoge", 
  ...
  "jest": {
    "rootDir": ".",
    "moduleNameMapper": {
      "src(.*)$": "<rootDir>/src/$1"
    }
  }
}
```

- `test/jest-e2e.json` の編集

```json
{
  "moduleFileExtensions": ["js", "json", "ts"],
  ...
  "rootDir": ".",
  "moduleNameMapper": {
    "src(.*)$": "<rootDir>/../src/$1"
  },
}

```

これでテストが問題なく走るようになったはず。

## DAO の作成

DB からデータを取ってくる部分は DAO(DataAccessObject) で

### src/dao/users.dao.ts

```ts
import { EntityManager } from 'typeorm';
import { InternalServerErrorException } from '@nestjs/common';

export class UsersDao {
  constructor(private readonly manager: EntityManager) {}
  create = (name: string): Promise<unknown> =>
    this.manager.query(`INSERT INTO "users" ("name") VALUES ($1);`, [name]);
  findAll = async (): Promise<UsersDao.Entity[]> => {
    const users: unknown[] = await this.manager.query(`
            SELECT
                id,
                name
            FROM
                "users" AS users;
        `);
    return users.map(user => UsersDao.buildEntity(user));
  };
  findOneById = async (id: string): Promise<UsersDao.Entity | undefined> => {
    const user: unknown[] = await this.manager.query(
      `
            SELECT
                users. id,
                users. name
            FROM
                "users"
            WHERE
                users. "id" = $1;
        `,
      [id],
    );
    if (user.length < 1) return undefined;
    return UsersDao.buildEntity(user[0]);
  };

  private static buildEntity(object: unknown): UsersDao.Entity {
    if (!UsersDao.isEntity(object))
      throw new InternalServerErrorException(
        object,
        'fetched data type is invalid',
      );
    return {
      id: object.id,
      name: object.name,
    };
  }
}
export namespace UsersDao {
  export interface Entity {
    id: number;
    name: string;
  }
  export const isEntity = (value: unknown): value is Entity => {
    return (
      typeof value === 'object' &&
      typeof (value as Entity).id === 'number' &&
      typeof (value as Entity).name === 'string'
    );
  };
}
```

テスト

<details><summary>users.dao.spec.ts</summary><div>

```ts
import { DBConnection } from 'src/infrastructure/dbconnection';
import { UsersDao } from './users.dao';

describe('UsersDao', () => {
  let dao: UsersDao;

  beforeEach(async () => {
    dao = new UsersDao(await DBConnection.getManager());
  });

  it('should be defined', () => {
    expect(dao).toBeDefined();
  });

  it('create', async () => {
    const result = await dao.create('JohnDoe');
  });
  it('get', async () => {
    const users = await dao.findAll();
    expect(users).toBeDefined();
    if (users.length < 1) return;
    expect(users[0].id).toBeGreaterThan(0);
    expect(users[0].name.length).toBeGreaterThan(0);
  });
  it('getById', async () => {
    const id = '1';
    const device = await dao.findOneById(id);
    if (device) {
      expect(device.id).toBeGreaterThan(0);
      expect(device.name.length).toBeGreaterThan(0);
    }
  });
  afterEach(async () => {
    await DBConnection.close();
  });
});

```

</div></details>

## サービスの実装

動作のひとかたまりをサービスに置いて、コントローラから呼び出す

### src/users/users.service.ts

```ts
import { Injectable } from '@nestjs/common';
import { DBConnection } from 'src/infrastructure/dbconnection';
import { UsersDao } from 'src/dao/users.dao';

@Injectable()
export class UsersService {
  createFullName = (params: {
    firstName?: string;
    lastName?: string;
  }): string => `${params.firstName}${params.lastName}`;

  createUser = async (params: {
    firstName?: string;
    lastName?: string;
  }): Promise<unknown> =>
    (await DBConnection.getManager())
      .transaction(manager =>
        new UsersDao(manager).create(
          this.createFullName({
            firstName: params.firstName,
            lastName: params.lastName,
          }),
        ),
      )
      .finally(() => DBConnection.close());
  getAllUser = async (): Promise<UsersDao.Entity[]> =>
    (await DBConnection.getManager())
      .transaction(manager => new UsersDao(manager).findAll())
      .finally(() => DBConnection.close());
}
```

テスト

<details><summary>users.service.spec.ts</summary><div>

```ts
import { DBConnection } from 'src/infrastructure/dbconnection';
import { UsersDao } from './users.dao';

describe('UsersDao', () => {
  let dao: UsersDao;

  beforeEach(async () => {
    dao = new UsersDao(await DBConnection.getManager());
  });

  it('should be defined', () => {
    expect(dao).toBeDefined();
  });

  it('create', async () => {
    const result = await dao.create('JohnDoe');
    expect(result).toStrictEqual([]);
  });
  it('get', async () => {
    const users = await dao.findAll();
    expect(users).toBeDefined();
    if (users.length < 1) return;
    expect(users[0].id).toBeGreaterThan(0);
    expect(users[0].name.length).toBeGreaterThan(0);
  });
  it('getById', async () => {
    const id = '1';
    const device = await dao.findOneById(id);
    if (device) {
      expect(device.id).toBeGreaterThan(0);
      expect(device.name.length).toBeGreaterThan(0);
    }
  });
  afterEach(async () => {
    await DBConnection.close();
  });
});

```

</div></details>

## コントローラの作成

API の呼び出しはここが起点になる

### src/users/users.controller.ts

```ts
import { Controller, Get, Query, Post, Body } from '@nestjs/common';
import { UsersService } from './users.service';

@Controller('users')
export class UsersController {
  constructor(private usersService: UsersService) {}

  @Post()
  async createUser(
    @Body() createFullNameDto: { first_name?: string; last_name?: string },
  ) {
    await this.usersService.createUser({
      firstName: createFullNameDto.first_name,
      lastName: createFullNameDto.last_name,
    });
    return {
      method: 'post',
      message: 'created',
    };
  }

  @Get()
  async getAllUsers() {
    return {
      users: await this.usersService.getAllUser(),
    };
  }
}
```

テスト

<details><summary>users.controller.spec.ts</summary><div>

```ts
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

describe('Users Controller', () => {
  let controller: UsersController;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UsersService],
      controllers: [UsersController],
    }).compile();

    controller = module.get<UsersController>(UsersController);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });

  it('create user', async () => {
    const first = 'John';
    const last = 'Doe';
    const result = await controller.createUser({
      first_name: first,
      last_name: last,
    });
    expect(result.message).toBe(`created`);
  });

  it('get all users', async () => {
    const result = await controller.getAllUsers();
    expect(result.users.length).toBeGreaterThan(0);
    expect(result.users[0].id).toBeGreaterThan(0);
    expect(result.users[0].name.length).toBeGreaterThan(0);
  });
});
```

</div></details>

## 統合テストの追加

### test/app.e2e-spec.ts

```ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from 'src/app.module';
import { UsersDao } from 'src/dao/users.dao';

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });

  it('/users (POST)', () => {
    return request(app.getHttpServer())
      .post('/users')
      .send({ first_name: 'John', last_name: 'Doe' })
      .expect(201)
      .expect(JSON.stringify({ method: 'post', message: 'created' }));
  });

  it('/users (GET)', async () => {
    const response = await request(app.getHttpServer())
      .get('/users?first_name=John&last_name=Doe')
      .expect(200);
    const body = response.body as {
      users: UsersDao.Entity[];
    };
    expect(body.users).toBeDefined();
    if (body.users.length < 1) return;
    expect(body.users[0].id).toBeGreaterThan(0);
    expect(body.users[0].name.length).toBeGreaterThan(0);
  });
});

```

# API にアクセスしてみる

- NestJS サーバーの起動

```bash
$ yarn start
```

- Docker コンテナ外からアクセス

```bash
$ curl -X GET "http://{DockerIP}:3000/users" -H "accept: */*"
{"users":[{"id":1,"name":"JohnDoe"}]}
```

この手順 ([NestJS でプロジェクト作成から API ドキュメントの表示まで](https://qiita.com/tktcorporation/items/ba2306ae46d99cf37035) ) でAPIドキュメント表示用の設定をしておけば、 `http://{DockerIP}:3000/api` にアクセスすることで、自由にAPIを試すことができる。

<img width="1418" alt="Screen Shot 2020-05-31 at 12.16.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252342/b9d8ab16-4beb-8e7e-16bc-26c4002ad8e7.png">