Sequelize+TypeScript による Apollo-Server の実装に関するメモです。
今回実装したリポジトリはこちらです;
https://github.com/suzukalight/study-graphql-apollo-server/tree/master/src/12-ddd
セットアップ
下記の環境で実践しました;
- Node v10
- Apollo-Server v2.9
- Sequelize v5
- SQLite v3
パッケージのインストール
$ yarn add sequelize sqlite3
.sequelizerc: 設定ファイルの所在に関する設定
const path = require('path');
module.exports = {
config: path.resolve('src/infrastructure/sequelize', 'config.json'),
'models-path': path.resolve('src/infrastructure/sequelize', 'models'),
'seeders-path': path.resolve('src/infrastructure/sequelize', 'seeders'),
'migrations-path': path.resolve('src/infrastructure/sequelize', 'migrations'),
};
config.json: DB 接続に関する設定
{
"development": {
"dialect": "sqlite",
"storage": "./database.sqlite",
"logging": false
}
}
モデリング
TypeScript による型定義
src/models/user.ts
import { Model, Association, DataTypes, HasManyCreateAssociationMixin } from 'sequelize';
import Message from './message';
export type Role = 'member' | 'admin';
class User extends Model {
// DBスキーマ
public id!: number;
public lastName!: string;
public firstName!: string;
public email!: string;
public password!: string;
public role?: Role;
public readonly createdAt!: Date;
public readonly updatedAt!: Date;
// resolverで生成するプロパティ
public readonly username!: string;
// カスタム static CRUDメソッド
public static findByEmail: (email: string) => Promise<User | null>;
// カスタム instance メソッド
public validatePassword!: (password: string) => Promise<boolean>;
// アソシエーション
public static associations: {
messages: Association<User, Message>;
};
// アソシエーションで得られる子エンティティ
public readonly messages?: Message[];
// アソシエーションで得られる子エンティティ
public createMessage!: HasManyCreateAssociationMixin<Message>;
}
export default User;
sequelize モデル定義
import { sequelize } from '../infrastructure/sequelize';
User.init(
{
id: {
type: DataTypes.INTEGER,
autoIncrement: true,
primaryKey: true,
},
lastName: {
type: DataTypes.STRING(250),
allowNull: true,
},
firstName: {
type: DataTypes.STRING(250),
allowNull: true,
},
email: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: true,
isEmail: true,
},
},
password: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notEmpty: true,
len: [7, 42],
},
},
role: {
type: DataTypes.STRING,
},
},
{
// hooks
},
);
sequelize hooks: CRUD 操作へのフック
import bcrypt from 'bcrypt';
const generatePasswordHash = async (user: User): Promise<string> => {
const saltRounds = 10;
return await bcrypt.hash(user.password, saltRounds);
};
User.init(
{
// models
},
{
tableName: 'users',
sequelize: sequelize,
hooks: {
beforeCreate: async (user) => {
user.set('password', await generatePasswordHash(user));
},
},
},
);
アソシエーション: 他エンティティとの接続
User.hasMany(Message, {
sourceKey: 'id',
foreignKey: 'userId',
as: 'messages',
});
static メソッド: カスタム CRUD の追加
User.findByEmail = async (email: string) =>
User.findOne({
where: { email },
});
instance メソッド: インスタンスへの操作
User.prototype.validatePassword = async function (password: string) {
return await bcrypt.compare(password, this.password);
};
アプリケーションとの接続
Sequelize の初期化
実行環境によって設定を切り替えられるように、config.json に複数の設定値を記述しておき、それをもとに初期化させます;
src/infrastructure/sequelize/config.json
{
"development": {
"dialect": "sqlite",
"storage": "./database.sqlite",
"logging": false
},
"test": {
"dialect": "sqlite",
"storage": "./testdatabase.sqlite",
"logging": false
}
}
src/infrastructure/sequelize/index.ts
import path from 'path';
import { Sequelize } from 'sequelize';
const env = process.env.NODE_ENV || 'development';
const config = require(path.join(__dirname, './config.json'))[env];
export const sequelize = new Sequelize(config);
サーバとの連携
src/domain/models/index.ts
import { sequelize } from '../../infrastructure/sequelize';
import User from './user';
import Message from './message';
export { sequelize };
const models = {
User,
Message,
};
export type Models = typeof models;
export default models;
src/index.ts
import models, { sequelize } from './domain/models';
const server = new ApolloServer({
// ...
context: async ({ req, connection }) => {
if (connection) return { models };
if (req) {
const me = await getMe(req);
return {
models,
me,
jwt: { secret: process.env.JWT_SECRET, expiresIn: process.env.JWT_EXPIRES_IN },
};
}
},
});
sequelize.sync().then(async () => {
httpServer.listen({ port: process.env.DB_PORT }, () => {
console.log(`Apollo Server on http://localhost:${process.env.DB_PORT}/graphql`);
});
});
リゾルバでの CRUD 操作実行
src/application/resolvers/user.ts
import User from '../models/user';
const resolvers: IResolvers<User, ResolverContext> = {
Query: {
me: async (parent, args, { models, me }) => models.User.findByPk(me?.id),
users: async (parent, args, { models }) => models.User.findAll(),
user: async (parent, { id }, { models }) => models.User.findByPk(id),
},
Mutation: {
deleteUser: combineResolvers(isAdmin, async (parent, { id }, { models }) =>
models.User.destroy({ where: { id } }),
),
},
User: {
messages: async (user, args, { models }) =>
models.Message.findAll({
where: { userId: user.id },
}),
},
};
マイグレーション: データを保持しながらスキーマを変更
運用中の DB を維持したまま、新しいデータスキーマ変更を適用したい場合に行う手順をマイグレーション(migration)と呼びます。sequelize ではこのマイグレーションを、sequelize-cli パッケージで提供しています。
雛形の生成
migration ファイルの雛形を生成できます。config で指定したディレクトリに出力されます;
$ npx sequelize-cli migration:generate --name user
up と down
- up: 適用したい変更を記述します
- down: 適用した変更を元に戻す方法を記述します
src/infrastructure/sequelize/migrations/20191222094216-user.js
'use strict';
module.exports = {
up: (queryInterface, Sequelize) =>
queryInterface.createTable(
'users',
{
id: {
type: Sequelize.INTEGER,
autoIncrement: true,
primaryKey: true,
},
lastName: {
type: Sequelize.STRING(250),
allowNull: true,
},
firstName: {
type: Sequelize.STRING(250),
allowNull: true,
},
email: {
type: Sequelize.STRING,
allowNull: false,
validate: {
notEmpty: true,
isEmail: true,
},
},
password: {
type: Sequelize.STRING,
allowNull: false,
validate: {
notEmpty: true,
len: [7, 42],
},
},
role: {
type: Sequelize.STRING,
},
createdAt: {
type: Sequelize.DATE,
allowNull: true,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: true,
},
},
{
charset: 'utf8mb4',
},
),
down: queryInterface => queryInterface.dropTable('users'),
};
コマンド実行
$ npx sequelize-cli db:migrate
シーダー(Seeder)
DB の初期データを設定するファイルです。
雛形の生成
seed ファイルの雛形を生成できます。config で指定したディレクトリに出力されます;
$ yarn sequelize-cli seed:generate --name user
seeding
src/infrastructure/sequelize/seeders/20191227020432-user.js
'use strict';
const bcrypt = require('bcrypt');
const saltRounds = 10;
const generatePasswordHash = async password => bcrypt.hash(password, saltRounds);
module.exports = {
up: async queryInterface =>
queryInterface.bulkInsert(
'users',
[
{
firstName: 'masahiko',
lastName: 'kubara',
email: 'masahiko_kubara@email.com',
password: await generatePasswordHash('masahikokubara'),
role: 'member',
},
{
firstName: 'suzuka',
lastName: 'light',
email: 'suzukalight@email.com',
password: await generatePasswordHash('suzukalight'),
role: 'admin',
},
],
{},
),
down: queryInterface => queryInterface.bulkDelete('users', null, {}),
};
コマンド実行
$ npx sequelize-cli db:seed:all
完成品
実装したリポジトリはこちらです;
https://github.com/suzukalight/study-graphql-apollo-server/tree/master/src/12-ddd