Closed avimoondra closed 3 years ago
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
@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()
}
@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. 🙂
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?
Is there a way to return the created database object back from someFactory.create() calls? So this reads like...
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!