Skip to content

Commit

Permalink
feat: add support for model hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Oct 7, 2019
1 parent cad26ca commit fee7c50
Show file tree
Hide file tree
Showing 13 changed files with 895 additions and 99 deletions.
36 changes: 24 additions & 12 deletions adonis-typings/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ declare module '@ioc:Adonis/Lucid/Model' {
'manyToMany' |
'hasManyThrough'

/**
* List of events for which a model will trigger hooks
*/
export type EventsList = 'save' | 'create' | 'update' | 'delete'
export type HooksHandler<T> = ((model: T) => Promise<void> | void) | string

/**
* Lookup map required for related method
*/
Expand Down Expand Up @@ -565,6 +571,24 @@ declare module '@ioc:Adonis/Lucid/Model' {

$boot (): void

/**
* Register a before hook
*/
$before<T extends ModelConstructorContract> (
this: T,
event: EventsList,
handler: HooksHandler<InstanceType<T>>,
)

/**
* Register an after hook
*/
$after<T extends ModelConstructorContract> (
this: T,
event: EventsList,
handler: HooksHandler<InstanceType<T>>,
)

/**
* Creating model from adapter results
*/
Expand Down Expand Up @@ -718,18 +742,6 @@ declare module '@ioc:Adonis/Lucid/Model' {
): ModelQueryBuilderContract<ModelConstructorContract> & ExcutableQueryBuilderContract<ModelContract[]>
}

/**
* Shape of the hooks contract used by transaction client and models
*/
export interface HooksContract<Events extends string, Handler extends any> {
add (lifecycle: 'before' | 'after', event: Events, handler: Handler): this
before (event: Events, handler: Handler): this
after (event: Events, handler: Handler): this
execute (lifecycle: 'before' | 'after', event: Events, payload: any): Promise<void>
clear (event: Events): void
clearAll (): void
}

/**
* Shape of the preloader to preload relationships
*/
Expand Down
4 changes: 4 additions & 0 deletions example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class User extends BaseModel {
public profile: Profile
}

User.$before('save', async (user) => {
user.username
})

const user = new User()
user.related<'hasOne', 'profile'>('profile').save(new Profile())

Expand Down
1 change: 1 addition & 0 deletions providers/DatabaseProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export default class DatabaseServiceProvider {
* a different adapter.
*/
BaseModel.$adapter = new Adapter(this.$container.use('Adonis/Lucid/Database'))
BaseModel.$container = this.$container

return {
BaseModel,
Expand Down
78 changes: 0 additions & 78 deletions src/Hooks/index.ts

This file was deleted.

65 changes: 58 additions & 7 deletions src/Orm/BaseModel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@

import pluralize from 'pluralize'
import snakeCase from 'snake-case'
import { IocContract } from '@adonisjs/fold'
import { Exception } from '@poppinss/utils'

import { QueryClientContract, TransactionClientContract } from '@ioc:Adonis/Lucid/Database'
import {
CacheNode,
EventsList,
ColumnNode,
ModelObject,
HooksHandler,
ModelOptions,
ComputedNode,
ModelContract,
Expand All @@ -30,6 +33,7 @@ import {
ModelConstructorContract,
} from '@ioc:Adonis/Lucid/Model'

import { Hooks } from '../Hooks'
import { Preloader } from '../Preloader'
import { HasOne } from '../Relations/HasOne'
import { proxyHandler } from './proxyHandler'
Expand All @@ -49,10 +53,21 @@ function StaticImplements<T> () {
@StaticImplements<ModelConstructorContract>()
export class BaseModel implements ModelContract {
/**
* The adapter to be used for persisting and fetching data
* The adapter to be used for persisting and fetching data.
*
* NOTE: Adapter is a singleton and share among all the models, unless
* a user wants to swap the adapter for a given model
*/
public static $adapter: AdapterContract

/**
* The container required to resolve hooks
*
* NOTE: Container is a singleton and share among all the models, unless
* a user wants to swap the container for a given model
*/
public static $container: IocContract

/**
* Primary key is required to build relationships across models
*/
Expand Down Expand Up @@ -115,6 +130,27 @@ export class BaseModel implements ModelContract {
cast: Map<string, string>,
}

/**
* Storing model hooks
*/
private static _hooks: Hooks

/**
* Register before hooks
*/
public static $before (event: EventsList, handler: HooksHandler<any>) {
this._hooks.add('before', event, handler)
return this
}

/**
* Register after hooks
*/
public static $after (event: EventsList, handler: HooksHandler<any>) {
this._hooks.add('after', event, handler)
return this
}

/**
* Returns the model query instance for the given model
*/
Expand Down Expand Up @@ -181,15 +217,13 @@ export class BaseModel implements ModelContract {
this.$booted = true
this.$primaryKey = this.$primaryKey || 'id'

Object.defineProperty(this, '$refs', { value: {} })
Object.defineProperty(this, '$columns', { value: new Map() })
Object.defineProperty(this, '$computed', { value: new Map() })
Object.defineProperty(this, '$relations', { value: new Map() })
Object.defineProperty(this, '$refs', { value: {} })
Object.defineProperty(this, '_mappings', {
value: {
cast: new Map(),
},
})

Object.defineProperty(this, '_hooks', { value: new Hooks(this.$container) })
Object.defineProperty(this, '_mappings', { value: { cast: new Map() }})

this.$increments = this.$increments === undefined ? true : this.$increments
this.$table = this.$table === undefined ? pluralize(snakeCase(this.name)) : this.$table
Expand Down Expand Up @@ -866,9 +900,15 @@ export class BaseModel implements ModelContract {
* Persit the model when it's not persisted already
*/
if (!this.$persisted) {
await Model._hooks.execute('before', 'create', this)
await Model._hooks.execute('before', 'save', this)

await Model.$adapter.insert(this, this.$prepareForAdapter(this.$attributes))
this.$hydrateOriginals()
this.$persisted = true

await Model._hooks.execute('after', 'create', this)
await Model._hooks.execute('after', 'save', this)
return
}

Expand All @@ -881,12 +921,18 @@ export class BaseModel implements ModelContract {
return
}

await Model._hooks.execute('before', 'update', this)
await Model._hooks.execute('before', 'save', this)

/**
* Perform update
*/
await Model.$adapter.update(this, this.$prepareForAdapter(dirty))
this.$hydrateOriginals()
this.$persisted = true

await Model._hooks.execute('after', 'update', this)
await Model._hooks.execute('after', 'save', this)
}

/**
Expand All @@ -895,8 +941,13 @@ export class BaseModel implements ModelContract {
public async delete () {
this._ensureIsntDeleted()
const Model = this.constructor as typeof BaseModel

await Model._hooks.execute('before', 'delete', this)

await Model.$adapter.delete(this)
this.$isDeleted = true

await Model._hooks.execute('after', 'delete', this)
}

/**
Expand Down
90 changes: 90 additions & 0 deletions src/Orm/Hooks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* @adonisjs/lucid
*
* (c) Harminder Virk <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

/// <reference path="../../../adonis-typings/index.ts" />

import { EventsList, HooksHandler } from '@ioc:Adonis/Lucid/Model'
import { IocContract, IocResolverContract, IocResolverLookupNode } from '@adonisjs/fold'

/**
* A generic class to implement before and after lifecycle hooks
*/
export class Hooks {
private _hooks: {
[event: string]: {
before: Set<Exclude<HooksHandler<any>, string> | IocResolverLookupNode>,
after: Set<Exclude<HooksHandler<any>, string> | IocResolverLookupNode>,
},
} = {}

/**
* Resolver to resolve IoC container bindings
*/
private _resolver: IocResolverContract

constructor (container: IocContract) {
this._resolver = container.getResolver(undefined, 'modelHooks', 'App/Models/Hooks')
}

/**
* Add hook for a given event and lifecycle
*/
public add (lifecycle: 'before' | 'after', event: EventsList, handler: HooksHandler<any>) {
this._hooks[event] = this._hooks[event] || { before: new Set(), after: new Set() }

let resolvedHook

/**
* If hook is a string, then resolve it from the container
*/
if (typeof (handler) === 'string') {
resolvedHook = this._resolver.resolve(handler)
} else {
resolvedHook = handler
}

this._hooks[event][lifecycle].add(resolvedHook)
return this
}

/**
* Execute hooks for a given event and lifecycle
*/
public async execute (lifecycle: 'before' | 'after', event: EventsList, payload: any): Promise<void> {
if (!this._hooks[event]) {
return
}

for (let hook of this._hooks[event][lifecycle]) {
if (typeof (hook) === 'function') {
await hook(payload)
} else {
await this._resolver.call(hook, undefined, [payload])
}
}
}

/**
* Remove hooks for a given event
*/
public clear (event: EventsList): void {
if (!this._hooks[event]) {
return
}

delete this._hooks[event]
}

/**
* Remove all hooks
*/
public clearAll (): void {
this._hooks = {}
}
}
Loading

0 comments on commit fee7c50

Please sign in to comment.