Hooks

Hooks

Hooks are the actions that you can perform against a model instance during a pre-defined life cycle event. Using hooks, you can encapsulate specific actions within your models vs. writing them everywhere inside your codebase.

A great example of hooks is password hashing. You can define a hook that runs before the save call and converts the plain text password to a hash.

app/models/user.ts
import hash from '@adonisjs/core/services/hash'
import { column, beforeSave, BaseModel } from '@adonisjs/lucid/orm'
export default class User extends BaseModel {
@column({ isPrimary: true })
declare id: number
@column()
declare email: string
@column()
declare password: string
@beforeSave()
static async hashPassword(user: User) {
if (user.$dirty.password) {
user.password = await hash.make(user.password)
}
}
}
  • The beforeSave hook is invoked before the INSERT and the UPDATE queries.
  • Hooks can be async. So you can use the await keyword inside them.
  • Hooks are always defined as static functions and receive the model's instance as the first argument.

Understanding the $dirty property

The beforeSave hook is called every time a new user is created or updated using the model instance.

During the update, you may have updated other properties but NOT the user password. Hence there is no need to re-hash the existing hash, which is why using the $dirty object.

The $dirty object only contains the changed values. So, you can check if the password was changed and then hash the new value.

Available hooks

Following is the list of all the available hooks.

HookDescription
beforeSaveInvoked before the insert or the update query. Receives the model instance as the only argument.
afterSaveInvoked after the insert or the update query. Receives the model instance as the only argument.
beforeCreateInvoked only before the insert query. Receives the model instance as the only argument.
afterCreateInvoked only after the insert query. Receives the model instance as the only argument.
beforeUpdateInvoked only before the update query. Receives the model instance as the only argument.
afterUpdateInvoked only after the update query. Receives the model instance as the only argument.
beforeDeleteInvoked before the delete query. Receives the model instance as the only argument.
afterDeleteInvoked after the delete query. Receives the model instance as the only argument.
beforePaginateInvoked before the paginate query. Receives the query main builder instance alongside the count query builder instance.
afterPaginateInvoked after the paginate query. Receives an instance of the simple paginator class.
beforeFetchInvoked before the fetch query. Receives the query builder instance as the only argument.
afterFetchInvoked after the fetch query. Receives an array of model instances
beforeFindInvoked before the find query. Receives the query builder instance as the only argument.
afterFindInvoked after the find query. Receives the model instance as the only argument.

beforeSave

The beforeSave decorator registers a given function as a before hook invoked before the insert and the update query.

import { BaseModel, beforeSave } from '@adonisjs/lucid/orm'
class User extends BaseModel {
@beforeSave()
static async hashPassword(user: User) {
if (user.$dirty.password) {
user.password = await Hash.make(user.password)
}
}
}

beforeCreate

The beforeCreate decorator registers the function to be invoked just before the insert operation.

import { BaseModel, beforeCreate } from '@adonisjs/lucid/orm'
class User extends BaseModel {
@beforeCreate()
static assignAvatar(user: User) {
user.avatarUrl = getRandomAvatar()
}
}

beforeUpdate

The beforeUpdate decorator registers the function to be invoked just before the update operation.

import { BaseModel, beforeUpdate } from '@adonisjs/lucid/orm'
class User extends BaseModel {
@beforeUpdate()
static async assignAvatar(user: User) {
user.avatarUrl = getRandomAvatar()
}
}

beforeDelete

The beforeDelete decorator registers the function to be invoked just before the delete operation.

import { BaseModel, beforeDelete } from '@adonisjs/lucid/orm'
class Post extends BaseModel {
@beforeDelete()
static async removeFromCache(post: Post) {
await Cache.remove(`post-${post.id}`)
}
}

beforeFind

The beforeFind hook is invoked just before the query is executed to find a single row. This hook receives the query builder instance, and you can attach your constraints to it.

Find operations are one's that intentionally selects a single database row. For example:

  • Model.find()
  • Model.findBy()
  • Model.first()
import { BaseModel, beforeFind } from '@adonisjs/lucid/orm'
import type { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'
export default class User extends BaseModel {
@beforeFind()
static ignoreDeleted(query: ModelQueryBuilderContract<typeof User>) {
query.whereNull('is_deleted')
}
}

afterFind

The afterFind event receives the model instance.

import { BaseModel, afterFind } from '@adonisjs/lucid/orm'
export default class User extends BaseModel {
@afterFind()
static afterFindHook(user: User) {}
}

beforeFetch

Similar to beforeFind, the beforeFetch hook also receives the query builder instance. However, this hook is invoked whenever a query is executed without using the first method.

import { BaseModel, beforeFetch } from '@adonisjs/lucid/orm'
import type { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'
export default class User extends BaseModel {
@beforeFetch()
static ignoreDeleted(query: ModelQueryBuilderContract<typeof User>) {
query.whereNull('is_deleted')
}
}

afterFetch

The afterFetch hook receives an array of model instances.

import { BaseModel, afterFetch } from '@adonisjs/lucid/orm'
export default class User extends BaseModel {
@afterFetch()
static afterFetchHook(users: User[]) {}
}

beforePaginate

The beforePaginate query is executed when you make use of the paginate method. The paginate method fires both the beforeFetch and beforePaginate hooks.

The hook function receives an array of query builders. The first instance is for the count's query, and the second is for the main query.

import { BaseModel, beforePaginate } from '@adonisjs/lucid/orm'
import type { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'
export default class User extends BaseModel {
@beforePaginate()
static ignoreDeleted (
countQuery: ModelQueryBuilderContract<typeof User>,
query: ModelQueryBuilderContract<typeof User>
) {
query.whereNull('is_deleted')
countQuery.whereNull('is_deleted')
}
}

afterPaginate

The afterPaginate hook receives an instance of the SimplePaginator class. The paginate method fires both the afterFetch and the afterPaginate hooks.

import { BaseModel, beforePaginate } from '@adonisjs/lucid/orm'
import type { SimplePaginatorContract } from '@adonisjs/lucid/types/querybuilder'
export default class User extends BaseModel {
@afterPaginate()
static afterPaginateHook(users: SimplePaginatorContract<User>) {}
}