Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

factory.create() expected usage with an ORM or 3rd party API #53

Closed
avimoondra opened this issue Mar 12, 2021 · 3 comments · Fixed by #74
Closed

factory.create() expected usage with an ORM or 3rd party API #53

avimoondra opened this issue Mar 12, 2021 · 3 comments · Fixed by #74

Comments

@avimoondra
Copy link

I'm integrating fishery with Sequelize (https://sequelize.org/), and am not sure if I'm taking the correct approach... Is this the expected usage?

export type Organization = {
  id: number
  name: string
}

class OrganizationFactory extends Factory<Organization> {}
export const organizationFactory = OrganizationFactory.define(({ sequence, params, onCreate }) => {
  onCreate((org) => {
    return models.Organization.build({ ...org }).save()
  })

  return {
    id: sequence,
    name: params.name || `A New Org ${sequence}`,
  }
})
const { id } = await organizationFactory.create()
const organization = await models.Organization.findByPk(id)

Is there a way to return the created database object back from someFactory.create() calls? So this reads like...

const organization = await organizationFactory.create()
// organization is a Sequelize object.

A different approach was taken here - https://paramander.com/en/blog/extending-fishery-to-build-test-data-for-sequelize-models but seems to miss onCreate hooks since super.create is not called in the mixin?

Thanks!

@stevehanson
Copy link
Contributor

Thanks for reporting this, @avimoondra! I just started a Sequelize project to play around with this. Would it potentially help if we allowed specifying a different return type for create instead of it always returning the same thing as build? This is what I'm picturing but want to make sure it would solve your problem:

// These are all the attributes in the User model
interface UserAttributes {
  id: number;
  name: string;
}

// Some attributes are optional in `User.build` and `User.create` calls
interface UserCreationAttributes extends Optional<UserAttributes, "id"> {}

class User
  extends Model<UserAttributes, UserCreationAttributes>
  implements UserAttributes
{
  public id!: number; // Note that the `null assertion` `!` is required in strict mode.
  public name!: string;

  // timestamps!
  public readonly createdAt!: Date;
  public readonly updatedAt!: Date;
  ...
}

// template arg 1 (currently exists) = The type that is returned from build()
// template arg 2 (currently exists) = The type of any transient params that your factory takes (defaults to 'any')
// template arg 3 (new) = The type that is returned from create()
const userFactory = Factory.define<UserAttributes, TransientParams, User>(({ onCreate }) => {
  onCreate((user) => User.create(user));
  return {
    id: 1,
    name: "Stephen",
  };
});

const user = userFactory.create({ name: "Bill" });
user.createdAt // works

@avimoondra
Copy link
Author

@stevehanson that looks great!

Since the library is only a few core files (yet very powerful!) and we heavily use create (vs. build), we decided to port it over for our use with Sequelize v5/v6 with the hopes of coming back around to use Fishery again sometime in the future.

Below is what we extracted out... (very similar, using TS generics)

Would it potentially help if we allowed specifying a different return type for create instead of it always returning the same thing as build?

(yes!)

import type { DeepPartial } from 'fishery'

// Our factory class is heavily inspried by Fishery. The reason we're using our
// own is that Fishery makes it difficult to use different types for input
// parameters and output objects.
//
// The downside to using our own class is that the interface isn't as nice. As
// time goes on, we'll add more changes to make it easier to write better
// factories.
//
// https://github.com/thoughtbot/fishery/blob/master/lib/factory.ts
export abstract class Factory<Model, Attributes> {
  attrs: DeepPartial<Attributes>
  model: Model
  hooks: ((model: Model) => Promise<void>)[]

  abstract make(params: DeepPartial<Attributes>): Promise<Model>

  constructor() {
    this.hooks = []
    this.attrs = {}
  }

  params(newAttrs: DeepPartial<Attributes>) {
    this.attrs = { ...this.attrs, ...newAttrs }
    return this
  }

  onCreate(hook: (model: Model) => Promise<void>) {
    this.hooks.push(hook)
    return this
  }

  async createList(count: number, params: DeepPartial<Attributes>): Promise<Model[]> {
    const models: Model[] = []
    for (let i = 0; i < count; i++) {
      const model = await this.create(params)
      models.push(model)
    }
    return models
  }

  async create(params?: DeepPartial<Attributes>): Promise<Model> {
    const model = await this.make({ ...this.attrs, ...params })
    for (const hook of this.hooks) {
      await hook(model)
    }
    return model
  }
}

Example model

import { DataTypes, Model, Sequelize } from 'sequelize'

import { Group } from './group'
import { User } from './user'

export interface UserGroupAttributes {
  id: number
  groupId: number | null
  userId: number | null
  isAdmin: boolean
}

export class UserGroup extends Model implements UserGroupAttributes {
  public id!: number
  public groupId!: number | null
  public userId!: number | null
  public isAdmin!: boolean

  public readonly createdAt!: Date
  public readonly updatedAt!: Date

  static associate() {
    this.belongsTo(User, {
      foreignKey: 'userId',
      onDelete: 'CASCADE',
    })

    this.belongsTo(Group, {
      foreignKey: 'groupId',
      onDelete: 'CASCADE',
    })
  }
}

export function init(sequelize: Sequelize) {
  UserGroup.init(
    {
      id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
      userId: DataTypes.INTEGER,
      groupId: DataTypes.INTEGER,
      isAdmin: DataTypes.BOOLEAN,
    },
    {
      modelName: 'userGroup',
      tableName: 'user_groups',
      scopes: {
        isAdmin: {
          where: {
            isAdmin: true,
          },
        },
        isMember: {
          where: {
            isAdmin: false,
          },
        },
      },
      sequelize,
    },
  )
}

Example factory

import type { DeepPartial } from 'fishery'
import { Factory } from './factory'
import { UserGroup, UserGroupAttributes } from './../../server/orm/models/userGroup'

class UserGroupFactory extends Factory<UserGroup, UserGroupAttributes> {
  async make(params: DeepPartial<UserGroupAttributes>) {
    return UserGroup.create({
      userId: params.userId ?? -1,
      groupId: params.groupId ?? -1,
      isAdmin: params.isAdmin ?? false,
    })
  }

  admin() {
    return this.params({
      isAdmin: true,
    })
  }
}

export const userGroupFactory = () => {
  return new UserGroupFactory()
}

@nolaneo
Copy link

nolaneo commented May 26, 2021

@stevehanson that'd be fantastic. I've a similar use case where I need to call plain ol' Typescript class constructors after generating the POJO so your proposal looks perfect. 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants