rosieとfaker.jsを使ってObjection.jsのModelを楽に生成する

## やりたいこと

テストコード等で、[rosie](https://github.com/rosiejs/rosie)と[faker.js](https://github.com/marak/Faker.js/)を使用して、Objection.jsのModelの生成を楽にすることが目的です。

```javascript
  const room = await factories.room.save({
    members: [factories.user.build()],
    messages: [factories.message.build()]
  });
  const message = await factories.message.save({ room: factories.room.build() });
  const user = await factories.user.save();  
```

## 依存モジュール

- [knex](https://github.com/tgriesser/knex): v0.19.1
- [objection](https://github.com/Vincit/objection.js/): v1.6.9
- [faker](https://github.com/marak/Faker.js/): v4.1.0
- [rosie](https://github.com/rosiejs/rosie): v2.0.1
- [sqlite3](https://github.com/mapbox/node-sqlite3): v4.0.9

## テーブル定義

事前に、以下のようなテーブルが定義されている想定です。

```javascript
async function createTables(knex) {
  await knex.schema.createTable('users', t => {
    t.increments('id').primary().notNullable();
    t.string('name').notNullable();
    t.string('email').notNullable();
  });

  await knex.schema.createTable('rooms', t => {
    t.increments('id').primary().notNullable();
    t.string('name').notNullable();
  });

  await knex.schema.createTable('room_members', t => {
    t.integer('room_id').notNullable();   
    t.integer('user_id').notNullable();

    t.foreign('room_id').references('id').inTable('rooms');
    t.foreign('user_id').references('id').inTable('users');

    t.primary(['room_id', 'user_id']);
  });

  await knex.schema.createTable('messages', t => {
    t.increments('id').primary().notNullable();
    t.string('text').notNullable();
    t.integer('room_id').notNullable();

    t.foreign('room_id').references('id').inTable('rooms');
  });
}
```

## セットアップ

### 依存モジュールの読み込み・設定

```javascript
const { Model, snakeCaseMappers, transaction } = require('objection');
const Knex = require('knex');
const { Factory } = require('rosie');
const faker = require('faker');

const dbConfig = { ... };
const knex = Knex(dbConfig);
Model.knex(knex);
```

### Modelの定義

```javascript
class BaseModel extends Model {
  static get columnNameMappers() {
    return snakeCaseMappers();
  }
}

class User extends BaseModel {
  static get tableName() {
    return 'users';
  }

  static get jsonSchema() {
    return {
      type: 'object',
      required: ['name', 'email'],
      properties: {
        id: { type: 'integer' },
        name: { type: 'string' },
        email: { type: 'string' }
      }
    };
  }
}

class Room extends BaseModel {
  static get tableName() {
    return 'rooms';
  }

  static get jsonSchema() {
    return {
      type: 'object',
      required: ['name'],
      properties: {
        id: { type: 'integer' },
        name: { type: 'string' }
      }
    };
  }

  static get relationMappings() {
    return {
      messages: {
        relation: Model.HasManyRelation,
        modelClass: Message,
        join: {
          from: 'rooms.id',
          to: 'messages.room_id'
        }
      },
      members: {
        relation: Model.ManyToManyRelation,
        modelClass: User,
        join: {
          from: 'rooms.id',
          to: 'users.id',
          through: {
            from: 'room_members.room_id', 
            to: 'room_members.user_id'
          }
        }
      }
    };
  }
}

class Message extends BaseModel {
  static get tableName() {
    return 'messages';
  }

  static get relationMappings() {
    return {
      room: {
        relation: Model.BelongsToOneRelation,
        modelClass: Room,
        join: {
          from: 'messages.room_id',
          to: 'rooms.id'
        }
      }
    };
  }

  static get jsonSchema() {
    return {
      type: 'object',
      required: ['text'],
      properties: {
        id: { type: 'number' },
        text: { type: 'string' }
      }
    };
  }
}
```

### ヘルパー関数を用意する

```javascript
function defineModelFactory(Model, defaultAttributes) {
  function Wrapper(attributes) {
    return Model.fromJson(attributes);
  }
  const factory = Factory.define(Model.tableName, Wrapper);

  for (const key of Object.keys(defaultAttributes)) {
    factory.attr(key, defaultAttributes[key]);
  }

  return {
    // インスタンスの生成のみ
    build: (override = {}) => factory.build(override),
    // インスタンスの生成及び永続化を行う
    save: async (override = {}) => {
      const model = factory.build(override)
      await transaction(Model.knex(), trx => {
        return model.$query(trx).insertGraph(); // `insertGraph`はatomicではないので、明示的にトランザクションオブジェクトを渡しています。
      });
      return model;
    }
  };
}

const emptyArray = () => [];
const factories = {
  user: defineModelFactory(User, {
    name: faker.name.findName, 
    email: faker.internet.email
  }),
  message: defineModelFactory(Message, {
    text: faker.random.word,
    room: null
  }),
  room: defineModelFactory(Room, {
    name: faker.random.word,
    members: emptyArray,
    messages: emptyArray
  })
};
```

## 使い方

```javascript
  const room = await factories.room.save({
    members: [factories.user.build()],
    messages: [factories.message.build()]
  });
  const room = await Room.query().eager('[members, messages]').first();
  console.log(room);
  // Room {
  //   id: 1,
  //   name: 'Ergonomic',
  //   members:
  //    [ User { id: 1, name: 'Napoleon Grady III', email: 'Jane8@hotmail.com' } ],
  // messages: [ Message { id: 1, text: '24/7', roomId: 1 } ] }

  
  const message = await factories.message.save({room: factories.room.build()});
  console.log(await Message.query().findById(message.id));
  // Message { id: 2, text: 'programming', roomId: 2 }

  const user = await factories.user.save();  
  console.log(await User.query().findById(user.id));
  // User {
  //   id: 2,
  //   name: 'Justine Marquardt',
  //   email: 'Keagan_Luettgen69@yahoo.com' }
```

## 参考

- [faker.js](https://github.com/marak/Faker.js/)
- [rosie](https://github.com/rosiejs/rosie)
- [Graph inserts](https://vincit.github.io/objection.js/guide/query-examples.html#graph-inserts)