AdonisJS Hooks
AdonisJS 5 uses hooks as its event system to notify us when a particular action is run against our models. Each of the hooks has both a before and after variant.
So, if you wanted to know when an action was attempted, you would want to utilize the before variant. If you wanted to know when an action was successfully performed, you’d want to use the after variant.
All of the available hooks can be found in the AdonisJS documentation, under ORM inside the Hooks page.
Before we get too far into this, let me also plug Melchyore's package Adonis Lucid Observer, which expands the observability of what we'll be covering here today.
Hook Event Handlers
There are a couple of ways we can add an event handler to a hook. Let’s quickly take a look at our options.
Decorators
The preferred approach to add an event handler to a hook is by using the decorators made available for each of the hooks. The one you may be most familiar with is the beforeSave
hook added to the default auth User model.
@beforeSave() public static async hashPassword (user: User) { if (user.$dirty.password) { user.password = await Hash.make(user.password) } }
Copied!
- app
- Models
- User.ts
By using before save this hook will run before the model is both created and updated.
Boot Method
We can also define an event handler for each of these events within our model’s boot method. The boot method, as the name implies, is in charge of ensuring everything is prepped and ready to go for our model to be used.
public static boot () { if (this.booted) { return } super.boot() // the same hook as before defined within our boot method this.before('save', (user) => { if (user.$dirty.password) { user.password = await Hash.make(user.password) } }) }
Copied!
- app
- Models
- User.ts
Defining Global Hooks
Every model extends a BaseModel
and just like every other model, the BaseModel
also has a boot method. We can utilize the BaseModel
boot method, just like our resource models to bind event handlers.
However, our BaseModel boot hooks will receive every model type we have, so type safety isn’t super strict here unless you manually check your types.
Adding An Event Handler Via The BaseModel
First, we’ll want to work within a provider’s boot method, I’ll be using the AppProvider
here today.
Let’s begin by grabbing our BaseModel
.
public async boot() { const { BaseModel } = this.app.container.resolveBinding('Adonis/Lucid/Orm') }
Copied!
- providers
- AppProvider.ts
Next, let’s grab the boot method of the BaseModel
since we’ll need to define a function in its place.
public async boot() { const { BaseModel } = this.app.container.resolveBinding('Adonis/Lucid/Orm') const boot = BaseModel.boot }
Copied!
- providers
- AppProvider.ts
We’re now clear to define our own boot method, we’ll need to call the default boot method so all the internal functionality of the BaseModel
still executes.
public async boot() { const { BaseModel } = this.app.container.resolveBinding('Adonis/Lucid/Orm') const boot = BaseModel.boot BaseModel.boot = function() { if (this.booted) return boot.call(this) } }
Copied!
- providers
- AppProvider.ts
We’re now clear to add any of the model’s hooks to our BaseModel
boot method.
public async boot() { const { BaseModel } = this.app.container.resolveBinding('Adonis/Lucid/Orm') const boot = BaseModel.boot BaseModel.boot = function() { if (this.booted) return boot.call(this) this.before('create', (item) => { console.log(`[BEFORE] User is creating ${item.constructor.name}`) }) this.after('create', (item) => { console.log(`[AFTER] User has created ${item.constructor.name}`) }) } }
Copied!
- providers
- AppProvider.ts
Now, boot up your server, create any record using a Lucid model and you should see this show up in your console:
[BEFORE] User is creating Taxonomy [AFTER] User has created Taxonomy 19
Copied!
This is great, but we can take it a step further and include who performed the action as well.
Getting Your Authenticated User Via HttpContext
First, in order to get access to our HttpContext outside the realm of our controllers, we’ll need to enable the useAsyncLocalStorage
flag within our app’s HTTP config.
export const http: ServerConfig = { useAsyncLocalStorage: true, // ... }
Copied!
- config
- app.ts
Once we have that in place, we can get our HttpContext
anywhere inside the flow of an HTTP request. Remember, no HTTP request means no HttpContext
.
import { HttpContext } from '@adonisjs/core/build/standalone' // ... public async boot() { const { BaseModel } = this.app.container.resolveBinding('Adonis/Lucid/Orm') const boot = BaseModel.boot BaseModel.boot = function() { if (this.booted) return boot.call(this) this.before('create', (item) => { const ctx = HttpContext.getOrFail() console.log(`[BEFORE] User ${ctx.auth.user?.id} is creating ${item.constructor.name}`) }) this.after('create', (item) => { const ctx = HttpContext.getOrFail() console.log(`[AFTER] User ${ctx.auth.user?.id} has created ${item.constructor.name}`) }) } }
Copied!
- providers
- AppProvider.ts
Now our console logs will look like the below, with the expectation that we do have an authenticated user.
[BEFORE] User 1 is creating Taxonomy [AFTER] User 1 has created Taxonomy 19
Copied!
Note that you can also use the request’s logger via the HttpContext as well.
public async boot() { const { BaseModel } = this.app.container.resolveBinding('Adonis/Lucid/Orm') const boot = BaseModel.boot BaseModel.boot = function() { if (this.booted) return boot.call(this) this.before('create', (item) => { const ctx = HttpContext.getOrFail() ctx.logger.debug(`[BEFORE] User ${ctx.auth.user?.id} is creating ${item.constructor.name}`) }) this.after('create', (item) => { const ctx = HttpContext.getOrFail() ctx.logger.debug(`[AFTER] User ${ctx.auth.user?.id} has created ${item.constructor.name}`) }) } }
Copied!
- providers
- AppProvider.ts
Expanded Example
import { HttpContext } from '@adonisjs/core/build/standalone' import { ApplicationContract } from '@ioc:Adonis/Core/Application' export default class AppProvider { constructor (protected app: ApplicationContract) {} public register () { // Register your own bindings } public async boot () { // IoC container is ready const { BaseModel } = this.app.container.resolveBinding('Adonis/Lucid/Orm') const boot = BaseModel.boot BaseModel.boot = function() { if (this.booted) return boot.call(this) this.before('create', (item) => { const ctx = HttpContext.getOrFail() ctx.logger.debug(`[BEFORE] User ${ctx.auth.user?.id} is creating ${item.constructor.name}`) console.log(`[BEFORE] User ${ctx.auth.user?.id} is creating ${item.constructor.name}`) }) this.after('create', (item) => { const ctx = HttpContext.getOrFail() console.log(`[AFTER] User ${ctx.auth.user?.id} has created ${item.constructor.name} ${item.id}`) }) this.before('update', (item) => { const ctx = HttpContext.getOrFail() console.log(`[BEFORE] User ${ctx.auth.user?.id} is updating ${item.constructor.name} ${item.id}`) }) this.after('update', (item) => { const ctx = HttpContext.getOrFail() console.log(`[AFTER] User ${ctx.auth.user?.id} has updated ${item.constructor.name} ${item.id}`) }) this.before('delete', (item) => { const ctx = HttpContext.getOrFail() console.log(`[BEFORE] User ${ctx.auth.user?.id} is deleting ${item.constructor.name} ${item.id}`) }) this.after('delete', (item) => { const ctx = HttpContext.getOrFail() console.log(`[AFTER] User ${ctx.auth.user?.id} has deleted ${item.constructor.name} ${item.id}`) }) } } public async ready () { // App is ready } public async shutdown () { // Cleanup, since app is going down } }
Copied!
- providers
- AppProvider.ts
Join The Discussion! (6 Comments)
Please sign in or sign up for free to join in on the dicussion.
leandro-hermes
Thank you very much for the class! It will help me a lot
Please sign in or sign up for free to reply
tomgobich
That's great to hear, Leandro! Thanks again for the lesson request!
Please sign in or sign up for free to reply
walison-gomes
Congratulations on the excellent example. In my case I'm using @ioc:Adonis/Addons/LucidSoftDeletes to preserve deleted records, but when I try to run some query in the models that use softdelete (export default class KanbanPhase extends compose(BaseModel, SoftDeletes)) I get an error in my queries, where the table name is changed to select * from model_with_soft_deletes. Any ideas?
Please sign in or sign up for free to reply
tomgobich
Thank you, Walison! Unfortunately, I haven't used the LucidSoftDeletes package, so I'm not familiar with it's inner workings at all.
My best guess would be to go back through the package's documentation and ensure everything is configured correctly.
Please sign in or sign up for free to reply
alands
Very good example, is it still work in adonis 6?
Please sign in or sign up for free to reply
tomgobich
Thank you, Alands! The Model Hooks portion will still work a-okay as those didn't change much, if at all. However, Global Hooks will likely need some tweaking to get working due to changes with the container.
Please sign in or sign up for free to reply