Skip to content

Commit

Permalink
feat: add factorized attrs
Browse files Browse the repository at this point in the history
BREAKING CHANGE  definition function has been substituted with attrs
  • Loading branch information
jorgebodega committed Feb 9, 2022
1 parent ad40cd0 commit 4a2ce08
Show file tree
Hide file tree
Showing 9 changed files with 69 additions and 92 deletions.
82 changes: 38 additions & 44 deletions src/factory.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,23 @@
import type { SaveOptions } from 'typeorm'
import { fetchConnection } from './connection'
import { isPromiseLike } from './utils/isPromiseLike'
import { LazyAttribute } from './lazyAttribute'
import type { Constructable, FactorizedAttrs } from './types'

export abstract class Factory<Entity> {
private mapFunction?: (entity: Entity) => void
protected abstract definition(): Promise<Entity>

/**
* This function is used to alter the generated values of entity, before it
* is persist into the database
*/
map(mapFunction: (entity: Entity) => void) {
this.mapFunction = mapFunction
return this
}
export abstract class Factory<T> {
protected abstract entity: Constructable<T>
protected abstract attrs: FactorizedAttrs<T>

/**
* Make a new entity without persisting it
*/
async make(overrideParams: Partial<Entity> = {}): Promise<Entity> {
return this.makeEntity(overrideParams, false)
async make(overrideParams: Partial<FactorizedAttrs<T>> = {}): Promise<T> {
return this.makeEntity(overrideParams)
}

/**
* Make many new entities without persisting it
*/
async makeMany(amount: number, overrideParams: Partial<Entity> = {}): Promise<Entity[]> {
async makeMany(amount: number, overrideParams: Partial<FactorizedAttrs<T>> = {}): Promise<T[]> {
const list = []
for (let index = 0; index < amount; index++) {
list[index] = await this.make(overrideParams)
Expand All @@ -36,53 +28,55 @@ export abstract class Factory<Entity> {
/**
* Create a new entity and persist it
*/
async create(overrideParams: Partial<Entity> = {}, saveOptions?: SaveOptions): Promise<Entity> {
const entity = await this.makeEntity(overrideParams, true)
async create(overrideParams: Partial<FactorizedAttrs<T>> = {}, saveOptions?: SaveOptions): Promise<T> {
const entity = await this.makeEntity(overrideParams)

const connection = await fetchConnection()
return connection.createEntityManager().save<Entity>(entity, saveOptions)
return connection.createEntityManager().save<T>(entity, saveOptions)
}

/**
* Create many new entities and persist them
*/
async createMany(amount: number, overrideParams: Partial<Entity> = {}, saveOptions?: SaveOptions): Promise<Entity[]> {
async createMany(
amount: number,
overrideParams: Partial<FactorizedAttrs<T>> = {},
saveOptions?: SaveOptions,
): Promise<T[]> {
const list = []
for (let index = 0; index < amount; index++) {
list[index] = await this.create(overrideParams, saveOptions)
}
return list
}

private async makeEntity(overrideParams: Partial<Entity>, isSeeding: boolean) {
const entity = await this.definition()
private async makeEntity(overrideParams: Partial<FactorizedAttrs<T>>) {
const entity = new this.entity()
const attrs = { ...this.attrs, ...overrideParams }

if (this.mapFunction) this.mapFunction(entity)
await Promise.all(
Object.entries(attrs).map(async ([key, value]) => {
Object.assign(entity, { [key]: await Factory.resolveValue(value) })
}),
)

for (const key in overrideParams) {
const actualValue = entity[key]
entity[key] = overrideParams[key] as typeof actualValue
}
await Promise.all(
Object.entries(attrs).map(async ([key, value]) => {
if (value instanceof LazyAttribute) {
const newAttrib = value.resolve(entity)
Object.assign(entity, { [key]: await Factory.resolveValue(newAttrib) })
}
}),
)

return this.resolveEntity(entity, isSeeding)
return entity
}

private async resolveEntity(entity: Entity, isSeeding: boolean): Promise<Entity> {
for (const attribute in entity) {
const attributeValue = entity[attribute]

if (isPromiseLike(attributeValue)) {
entity[attribute] = await attributeValue
}

if (attributeValue instanceof Factory) {
if (isSeeding) {
entity[attribute] = await attributeValue.create()
} else {
entity[attribute] = await attributeValue.make()
}
}
private static resolveValue(value: unknown) {
if (value instanceof Function) {
return value()
} else {
return value
}
return entity
}
}
9 changes: 9 additions & 0 deletions src/lazyAttribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { FactorizedAttrs, LazyAttributeCallback } from './types'

export class LazyAttribute<T> {
constructor(private callback: LazyAttributeCallback<T>) {}

resolve(entity: T): FactorizedAttrs<T> {
return this.callback(entity)
}
}
9 changes: 6 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import type { ConnectionOptions as TypeORMConnectionOptions } from 'typeorm'

export type ClassConstructor<T> = new () => T
import type { Factory } from './factory'

export type ConnectionOptions = TypeORMConnectionOptions & {
seeders: string[]
defaultSeeder: string
}

export type ConnectionConfiguration = {
root?: string
configName?: string
connection: string
}
export type Constructable<T> = new () => T
export type FactorizedAttrs<T> = {
[K in keyof Partial<T>]: T[K] | (() => T[K] | Promise<T[K]>) | Factory<T[K]>
}
export type LazyAttributeCallback<T> = (entity: T) => FactorizedAttrs<T>
2 changes: 0 additions & 2 deletions src/utils/isPromiseLike.ts

This file was deleted.

3 changes: 3 additions & 0 deletions test/entities/Pet.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export class Pet {
@Column()
name!: string

@Column()
lastName!: string

@ManyToOne(() => User)
@JoinColumn({ name: 'user_id' })
owner!: User
Expand Down
5 changes: 4 additions & 1 deletion test/entities/User.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'
@Entity()
export class User {
@PrimaryGeneratedColumn('increment')
id!: string
id!: number

@Column()
name!: string

@Column()
lastName!: string
}
13 changes: 5 additions & 8 deletions test/factories/Pet.factory.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import faker from '@faker-js/faker'
import { Factory } from '../../src/factory'
import { Pet } from '../entities/Pet.entity'
import { UserFactory } from './User.factory'

export class PetFactory extends Factory<Pet> {
protected async definition(): Promise<Pet> {
const pet = new Pet()

pet.name = faker.name.findName()
pet.owner = new UserFactory() as any

return pet
protected entity = Pet
protected attrs = {
name: faker.name.findName(),
lastName: async () => faker.name.findName(),
// owner: new UserFactory() as any,
}
}
10 changes: 4 additions & 6 deletions test/factories/User.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import { Factory } from '../../src/factory'
import { User } from '../entities/User.entity'

export class UserFactory extends Factory<User> {
protected async definition(): Promise<User> {
const user = new User()

user.name = faker.name.findName()

return user
protected entity = User
protected attrs = {
name: faker.name.findName(),
lastName: () => faker.name.lastName(),
}
}
28 changes: 0 additions & 28 deletions test/utils/isPromiseLike.test.ts

This file was deleted.

0 comments on commit 4a2ce08

Please sign in to comment.