Skip to content

Commit

Permalink
feat: implement factory model hooks and add ids stub calls
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Jun 20, 2020
1 parent 4563a00 commit d494f13
Show file tree
Hide file tree
Showing 14 changed files with 554 additions and 38 deletions.
41 changes: 37 additions & 4 deletions adonis-typings/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,26 @@ declare module '@ioc:Adonis/Lucid/Factory' {
ctx: FactoryContextContract,
) => any | Promise<any>

/**
* ------------------------------------------------------
* Hooks
* ------------------------------------------------------
*/

/**
* List of events for which a factory will trigger hooks
*/
export type EventsList = 'makeStubbed' | 'create' | 'make'

/**
* Shape of hooks handler
*/
export type HooksHandler<Model extends FactoryModelContract<LucidModel>> = (
factory: FactoryBuilderContract<Model>,
model: InstanceType<Model['model']>,
ctx: FactoryContextContract,
) => void | Promise<void>

/**
* ------------------------------------------------------
* Runtime context
Expand Down Expand Up @@ -188,9 +208,10 @@ declare module '@ioc:Adonis/Lucid/Factory' {
useCtx (ctx: FactoryContextContract): this

/**
* Create model instance.
* Create model instance and stub out the persistance
* mechanism
*/
make (
makeStubbed (
callback?: (
model: InstanceType<FactoryModel['model']>,
ctx: FactoryContextContract,
Expand All @@ -208,9 +229,10 @@ declare module '@ioc:Adonis/Lucid/Factory' {
): Promise<InstanceType<FactoryModel['model']>>

/**
* Create more than one model instance
* Create one or more model instances and stub
* out the persistance mechanism.
*/
makeMany (
makeStubbedMany (
count: number,
callback?: (
model: InstanceType<FactoryModel['model']>,
Expand Down Expand Up @@ -279,6 +301,17 @@ declare module '@ioc:Adonis/Lucid/Factory' {
callback: Relation,
): this & { relations: { [P in K]: Relation } }

/**
* Define before hooks. Only `create` event is invoked
* during the before lifecycle
*/
before (event: 'create', handler: HooksHandler<this>): this

/**
* Define after hooks.
*/
after (event: EventsList, handler: HooksHandler<this>): this

/**
* Build model factory. This method returns the factory builder, which can be used to
* execute model queries
Expand Down
16 changes: 16 additions & 0 deletions adonis-typings/relations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,8 @@ declare module '@ioc:Adonis/Lucid/Relations' {
RelatedModel extends LucidModel,
> extends BaseRelationContract<ParentModel, RelatedModel> {
readonly type: 'hasOne'
readonly localKey: string
readonly foreignKey: string

/**
* Set related model as a relationship on the parent model.
Expand Down Expand Up @@ -342,6 +344,8 @@ declare module '@ioc:Adonis/Lucid/Relations' {
RelatedModel extends LucidModel
> extends BaseRelationContract<ParentModel, RelatedModel> {
readonly type: 'hasMany'
readonly localKey: string
readonly foreignKey: string

/**
* Set related models as a relationship on the parent model
Expand Down Expand Up @@ -394,6 +398,8 @@ declare module '@ioc:Adonis/Lucid/Relations' {
RelatedModel extends LucidModel
> extends BaseRelationContract<ParentModel, RelatedModel> {
readonly type: 'belongsTo'
readonly localKey: string
readonly foreignKey: string

/**
* Set related model as a relationship on the parent model
Expand Down Expand Up @@ -446,6 +452,12 @@ declare module '@ioc:Adonis/Lucid/Relations' {
> extends BaseRelationContract<ParentModel, RelatedModel> {
type: 'manyToMany'

readonly localKey: string
readonly relatedKey: string
readonly pivotForeignKey: string
readonly pivotRelatedForeignKey: string
readonly pivotTable: string

/**
* Set related models as a relationship on the parent model
*/
Expand Down Expand Up @@ -506,6 +518,10 @@ declare module '@ioc:Adonis/Lucid/Relations' {
RelatedModel extends LucidModel
> extends BaseRelationContract<ParentModel, RelatedModel> {
type: 'hasManyThrough'
readonly localKey: string
readonly foreignKey: string
readonly throughLocalKey: string
readonly throughForeignKey: string

/**
* Set related models as a relationship on the parent model
Expand Down
30 changes: 27 additions & 3 deletions src/Factory/FactoryBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
import { FactoryModel } from './FactoryModel'
import { FactoryContext } from './FactoryContext'

let Counter = 1

/**
* Factory builder exposes the API to create/persist factory model instances.
*/
Expand Down Expand Up @@ -197,16 +199,25 @@ export class FactoryBuilder implements FactoryBuilderContract<FactoryModelContra
* Returns a model instance without persisting it to the database.
* Relationships are still loaded and states are also applied.
*/
public async make (callback?: (
public async makeStubbed (callback?: (
model: LucidRow,
ctx: FactoryContextContract,
) => void) {
const { modelInstance, ctx } = await this.compile(true, callback)
await this.model.hooks.exec('after', 'make', this, modelInstance, ctx)

modelInstance[this.model.model.primaryKey] = modelInstance.$primaryKeyValue || Counter++

/**
* Make relationships. The relationships will be not persisted
*/
await this.makeRelations(modelInstance, ctx)

/**
* Fire the after hook
*/
await this.model.hooks.exec('after', 'makeStubbed', this, modelInstance, ctx)

return modelInstance
}

Expand All @@ -219,6 +230,12 @@ export class FactoryBuilder implements FactoryBuilderContract<FactoryModelContra
ctx: FactoryContextContract,
) => void) {
const { modelInstance, ctx } = await this.compile(false, callback)
await this.model.hooks.exec('after', 'make', this, modelInstance, ctx)

/**
* Fire the before hook
*/
await this.model.hooks.exec('before', 'create', this, modelInstance, ctx)

try {
/**
Expand All @@ -231,6 +248,13 @@ export class FactoryBuilder implements FactoryBuilderContract<FactoryModelContra
* Create relationships.
*/
await this.createRelations(modelInstance, ctx)

/**
* Fire after hook before the transaction is committed, so that
* hook can run db operations using the same transaction
*/
await this.model.hooks.exec('after', 'create', this, modelInstance, ctx)

if (!this.ctx && ctx.$trx) {
await ctx.$trx.commit()
}
Expand All @@ -247,7 +271,7 @@ export class FactoryBuilder implements FactoryBuilderContract<FactoryModelContra
/**
* Create many of factory model instances
*/
public async makeMany (
public async makeStubbedMany (
count: number,
callback?: (model: LucidRow, ctx: FactoryContextContract) => void,
) {
Expand All @@ -256,7 +280,7 @@ export class FactoryBuilder implements FactoryBuilderContract<FactoryModelContra
const counter = new Array(count).fill(0).map((_, i) => i)
for (let index of counter) {
this.currentIndex = index
modelInstances.push(await this.make(callback))
modelInstances.push(await this.makeStubbed(callback))
}

return modelInstances
Expand Down
30 changes: 30 additions & 0 deletions src/Factory/FactoryModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
* file that was distributed with this source code.
*/

import { Hooks } from '@poppinss/hooks'
import { LucidRow, LucidModel } from '@ioc:Adonis/Lucid/Model'
import { ExtractModelRelations, RelationshipsContract } from '@ioc:Adonis/Lucid/Relations'

import {
EventsList,
HooksHandler,
StateCallback,
MergeCallback,
NewUpCallback,
Expand Down Expand Up @@ -64,9 +67,36 @@ export class FactoryModel<Model extends LucidModel> implements FactoryModelContr
*/
public relations: { [relation: string]: FactoryRelationContract } = {}

/**
* A set of registered hooks
*/
public hooks = new Hooks()

constructor (public model: Model, public define: DefineCallback<LucidModel>) {
}

/**
* Register a before event hook
*/
public before (
event: EventsList,
handler: HooksHandler<FactoryModelContract<Model>>,
): this {
this.hooks.add('before', event, handler)
return this
}

/**
* Register an after event hook
*/
public after (
event: EventsList,
handler: HooksHandler<FactoryModelContract<Model>>,
): this {
this.hooks.add('after', event, handler)
return this
}

/**
* Returns state callback defined on the model factory. Raises an
* exception, when state is not registered
Expand Down
3 changes: 2 additions & 1 deletion src/Factory/Relations/BelongsTo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export class BelongsTo extends BaseRelation implements FactoryRelationContract {
*/
public async make (parent: LucidRow, callback?: RelationCallback) {
const factory = this.compile(callback)
const related = await factory.make()
const related = await factory.makeStubbed()
this.relation.hydrateForPersistance(parent, related)
parent.$setRelated(this.relation.relationName, related)
}

Expand Down
13 changes: 11 additions & 2 deletions src/Factory/Relations/HasMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ export class HasMany extends BaseRelation implements FactoryRelationContract {
*/
public async make (parent: LucidRow, callback?: RelationCallback, count?: number) {
const factory = this.compile(callback)
const instances = await factory.makeMany(count || 1)

const customAttributes = {}
this.relation.hydrateForPersistance(parent, customAttributes)
const instances = await factory.makeStubbedMany(count || 1, (related) => {
related.merge(customAttributes)
})

parent.$setRelated(this.relation.relationName, instances)
}

Expand All @@ -47,7 +53,10 @@ export class HasMany extends BaseRelation implements FactoryRelationContract {

const customAttributes = {}
this.relation.hydrateForPersistance(parent, customAttributes)
const instance = await factory.createMany(count || 1, (related) => related.merge(customAttributes))

const instance = await factory.createMany(count || 1, (related) => {
related.merge(customAttributes)
})

parent.$setRelated(this.relation.relationName, instance)
}
Expand Down
5 changes: 4 additions & 1 deletion src/Factory/Relations/HasOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export class HasOne extends BaseRelation implements FactoryRelationContract {
*/
public async make (parent: LucidRow, callback?: RelationCallback) {
const factory = this.compile(callback)
const instance = await factory.make()
const customAttributes = {}
this.relation.hydrateForPersistance(parent, customAttributes)

const instance = await factory.makeStubbed((related) => related.merge(customAttributes))
parent.$setRelated(this.relation.relationName, instance)
}

Expand Down
18 changes: 14 additions & 4 deletions src/Factory/Relations/ManyToMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,19 @@ export class ManyToMany extends BaseRelation implements FactoryRelationContract
*/
public async make (parent: LucidRow, callback?: RelationCallback, count?: number) {
const factory = this.compile(callback)
const instances = await factory.makeMany(count || 1)
const instances = await factory.makeStubbedMany(count || 1)

const [pivotKey, pivotValue] = this.relation.getPivotPair(parent)
instances.forEach((related) => {
const [pivotRelatedKey, pivotRelatedValue] = this.relation.getPivotRelatedPair(related)

/**
* Update model $extra properties
*/
related.$extras[pivotKey] = pivotValue
related.$extras[pivotRelatedKey] = pivotRelatedValue
})

parent.$setRelated(this.relation.relationName, instances)
}

Expand All @@ -44,9 +56,7 @@ export class ManyToMany extends BaseRelation implements FactoryRelationContract
*/
public async create (parent: LucidRow, callback?: RelationCallback, count?: number) {
const factory = this.compile(callback)

const customAttributes: ModelObject = {}
const instances = await factory.createMany(count || 1, (related) => related.merge(customAttributes))
const instances = await factory.createMany(count || 1)

/**
* Loop over instances to build pivot attributes
Expand Down
8 changes: 7 additions & 1 deletion src/Orm/Relations/HasOne/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,15 @@ export class HasOne implements HasOneRelationContract<LucidModel, LucidModel> {
: this.options.serializeAs

/**
* Available after boot is invoked
* Local key is reference to the primary key in the self table
* @note: Available after boot is invoked
*/
public localKey: string

/**
* Foreign key is reference to the foreign key in the related table
* @note: Available after boot is invoked
*/
public foreignKey: string

/**
Expand Down
Loading

0 comments on commit d494f13

Please sign in to comment.