sequelize v5とtypescript

# はじめに
Node.jsを使う場合、ORMにsequelizeを使うことが多いと思います。
そのsequelizeをtypescriptでちゃんと型定義してあげると、

sequelizeのモデルに生えているメソッドを推論してくれたり、
<img width="588" alt="スクリーンショット 2019-07-07 13.28.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/5bb6ccdd-1b14-40cd-aae4-ba24d2f568ce.png">
findOneなどの引数に渡す検索のoptionの推論をしてくれたり、
<img width="634" alt="スクリーンショット 2019-07-07 13.31.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/6ca9eb1b-a193-fbf9-e148-868265cfd188.png">
取ってきたデータがどんなカラムを持っているのかがわかったりします。
<img width="580" alt="スクリーンショット 2019-07-07 13.32.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131625/7946701a-cb00-80ed-9e6b-446ec7b6e60f.png">

sequelizeとtypescriptを組み合わせるには、[sequelize-typescript](https://github.com/RobinBuschmann/sequelize-typescript)を使えばできたりしますが、sequelize v5からは、[公式がtypescriptをサポート](http://docs.sequelizejs.com/manual/typescript)してくれています。

その公式のやり方を使って、sequelizeとtypescriptを組み合わせてみます。

# 前提
sequelizeを使う場合、 *俺が作った最高のmodelGenerator* みたいなコードができがちです。
今回、ここに載せた例も一例に過ぎないので作り方は色々あります。
`HasManyGetAssociationsMixin` とか適当なsequelizeのメソッド名でgithubを検索すれば、世の中の人がどんな感じでsequelizeとtypescriptを組み合わせているのか、例がたくさん出てきます。

# コード

## モデル定義
usersとuser_detailsというテーブルがあると仮定して、usersとuser_detailsが一対一で繋がっているようなモデルを定義していきます。

```
src
├── models
│   ├── hoge
│   │   ├── index.ts
│   │   ├── schema.ts
│   │   ├── user_details.ts
│   │   └── users.ts
```

### usersのモデル定義
user_detailsというテーブルと一対一で繋がる。
interfaceの末尾に!をつけるとそのカラムはnullになることはないという意味。[!についての詳細はこちら](https://qiita.com/zigenin/items/364264a6cf635b962542)
このモデルのclassで定義したプロパティが、推論を行なってくれるカラムになってきます。

```ts:src/models/hoge/users.ts

import { Sequelize, Model, DataTypes } from 'sequelize';
import { UserDetails } from './user_details';

const TABLE_NAME = 'users';

class Users extends Model {
  public id!: number;
  public email!: string;
  public created_at!: Date;
  public updated_at!: Date;
  public user_detail!: UserDetails;

  public static attach(sequelize: Sequelize): void {
    this.init(
      {
        id: {
          type: DataTypes.INTEGER,
          autoIncrement: true,
          allowNull: false,
          primaryKey: true,
        },
        email: {
          type: DataTypes.STRING,
          allowNull: false,
          defaultValue: '',
        },
        created_at: {
          type: DataTypes.DATE,
          allowNull: false,
        },
        updated_at: {
          type: DataTypes.DATE,
          allowNull: false,
        },
      },
      {
        tableName: TABLE_NAME,
        underscored: true,
        sequelize: sequelize,
      }
    );
  }

  public static associate(): void {
    Users.hasOne(UserDetails, {
      foreignKey: 'user_id',
    });
  }
}

const factory = (sequelize: Sequelize) => {
  Users.attach(sequelize);

  return Users;
};

export { Users, factory };

```

### user_detailsのモデル定義
interfaceの末尾に?がついたカラムはnullの場合もあるカラム。

```ts:src/models/hoge/user_details.ts

import { Sequelize, Model, DataTypes } from 'sequelize';
import { Users } from './users';

const TABLE_NAME = 'user_details';

class UserDetails extends Model {
  public id!: number;
  public user_id!: number;
  public nick_name?: string;
  public first_name?: string;
  public last_name?: string;
  public created_at!: Date;
  public updated_at!: Date;

  public static attach(sequelize: Sequelize): void {
    this.init(
      {
        id: {
          type: DataTypes.INTEGER,
          autoIncrement: true,
          allowNull: false,
          primaryKey: true,
        },
        user_id: {
          type: DataTypes.INTEGER,
          allowNull: false,
        },
        nick_name: {
          type: DataTypes.STRING,
          allowNull: true,
        },
        first_name: {
          type: DataTypes.STRING,
          allowNull: true,
        },
        last_name: {
          type: DataTypes.STRING,
          allowNull: true,
        },
        created_at: {
          type: DataTypes.DATE,
          allowNull: false,
        },
        updated_at: {
          type: DataTypes.DATE,
          allowNull: false,
        },        
      },
      {
        tableName: TABLE_NAME,
        underscored: true,
        sequelize: sequelize,
      }
    );
  }

  public static associate(): void {
    UserDetails.hasOne(Users, {
      foreignKey: 'user_id',
      as: 'user_detail',
    });
  }
}

const factory = (sequelize: Sequelize) => {
  UserDetails.attach(sequelize);

  return UserDetails;
};

export { UserDetails, factory };
```


## 各モデルをまとめてimportできるファイル
このファイルは型定義のために使ったり、モデルの作成のために使います。

```ts:src/models/hoge/schema.ts
import * as users from './users';
import * as userDetails from './user_details';

export default {
  users,
  user_details: userDetails
};

```

## dbアクセス時のエントリーポイントとなるファイル
dbにアクセスしたい際は、このファイルをimportします。
model_generatorは `setModel()` を受け取るようにしておき、使う側が利用するモデル群をハンドリングできるようにしました。（そうしておけば例えば、dbが違う別サービスでも共通したmodel_generatorが使えるようになる）

```ts:src/models/hoge/index.ts
import { Sequelize } from 'sequelize';
import { hogeModelGenerate } from '../../utils/model_generator';
import dbConfig from '../../config/db';
import schema from '../../models/hoge/schema';

const setModel = (sequelize: Sequelize): HogeDB => {
  const db: any = {};

  Object.keys(schema).forEach(tableName => {
    db[tableName] = schema[tableName].factory(sequelize);
  });

  // associationを貼るのは各Modelのinit()が全て終わってから
  // (全モデルのinit()が終わる前にassociationを貼るとそんなモデル知らないみたいなエラーで死ぬ）
  Object.keys(schema).forEach(tableName => {
    if ('associate' in db[tableName]) {
      db[tableName].associate(db);
    }
  });

  return db;
};

const modelGenerator = hogeModelGenerate(dbConfig.service);
const db = modelGenerator(setModel);

export default db;
```


## モデルの型定義
モデルの型を定義します。
（@typesディレクトリはtsconfigのtypeRootsに指定しておき、グローバルに読めるようにしておく）

```
src
├── @types
│   ├── hoge
│   │   └── index.d.ts
```

```ts:src/@types/hoge/index.d.ts

/* eslint @typescript-eslint/camelcase: 0 */
import schema from '../../../models/hoge/schema';

declare interface HogeDB {
  users: typeof schema.users.Users;
  user_details: typeof schema.users.UserDetails;
}

```

## モデルをgenerateするファイル
Sequelizeのインスタンスを作って各モデルのinitをします。
このmodel_generatorを使う側は、 `setModel()` の実装を渡してもらう必要があります。

```
src
└── utils
    └── model_generator.ts
```

```ts:src/utils/model_generator.ts
import { Sequelize, Op } from 'sequelize';

export default class ModelGenerater {
  public sequelize: Sequelize;

  public constructor(dbConfig: any) {
    this.sequelize = new Sequelize(dbConfig.database, dbConfig.username, dbConfig.password, {
      host: dbConfig.host,
      dialect: 'mysql',
      port: 3306,
      logging: true,
      omitNull: true,
    });
  }
}

export const hogeModelGenerate = (dbConfig: any) => {
  const modelGenerator = new ModelGenerater(dbConfig);

  return (setModel: any) => {
    const db: HogeDB = setModel(modelGenerator.sequelize);

    return {
      ...db,
      Sequelize,
      sequelize: modelGenerator.sequelize,
      Op,
    };
  };
};

```

constructorに渡しているdbConfigはこんな感じの、DB周りの設定を環境変数から読み込むようなやつです。

```ts

export default {
  host: process.env.HOGE_SERVICE_DB_HOST || '127.0.0.1',
  username: process.env.HOGE_SERVICE_DB_USER || 'root',
  password: process.env.HOGE_SERVICE_DB_PASSWORD || '',
  database: process.env.HOGE_SERVICE_DB_DATABASE || 'hoge',
};
```

# 使い方
`src/models/hoge/index.ts` をimportして
db.users.findOne() みたいな感じで使えば、冒頭に紹介したような感じで推論してくれるようになってます。