Use classes instead of decorators to observe your Lucid models.
If you hate decorators or if you like small model files, then this package is for you.
You can listen to your model events from Observer classes that have method names which reflect Lucid hooks (events). These methods receive the same arguments as hooks.
If you don't know what are hooks, you can read about them here and here.
Node.js >= 16.14.0
@adonisjs/lucid >= 18.0.0
npm install @melchyore/adonis-lucid-observer
# or using Yarn
yarn add @melchyore/adonis-lucid-observer
node ace configure @melchyore/adonis-lucid-observer
node ace make:observer GlobalObserver -g
# or
node ace make:observer GlobalObserver --global
The GlobalObserver
will be placed in App/Observers
. This directory is created automatically if it doesn't exist.
Following is the content of the created file. Note that methods are not static.
// App/Observers/GlobalObserver.ts
import type { ModelQueryBuilderContract, LucidModel, LucidRow } from '@ioc:Adonis/Lucid/Orm'
import type { ObserverContract } from '@ioc:Adonis/Addons/LucidObserver'
export default class GlobalObserver implements ObserverContract {
public async beforeFind(query: ModelQueryBuilderContract<LucidModel>): Promise<void> {}
public async afterFind(model: LucidRow): Promise<void> {}
public async beforeFetch(query: ModelQueryBuilderContract<LucidModel>): Promise<void> {}
public async beforeSave(model: LucidRow): Promise<void> {}
public async afterSave(model: LucidRow): Promise<void> {}
}
node ace make:observer UserObserver --model=User
# or
node ace make:observer UserObserver -m "User"
The UserObserver
will be placed in App/Observers
. This directory is created automatically if it doesn't exist.
Following is the content of the created file. Note that methods are not static.
// App/Observers/UserObserver.ts
import type { ModelQueryBuilderContract } from '@ioc:Adonis/Lucid/Orm'
import type { ObserverContract } from '@ioc:Adonis/Addons/LucidObserver'
import User from 'App/Models/User'
export default class UserObserver implements ObserverContract {
public async beforeFind(query: ModelQueryBuilderContract<typeof User>): Promise<void> {}
public async afterFind(user: User): Promise<void> {}
public async beforeFetch(query: ModelQueryBuilderContract<typeof User>): Promise<void> {}
public async afterFetch(user: Array<User>): Promise<void> {}
public async beforeSave(user: User): Promise<void> {}
public async afterSave(user: User): Promise<void> {}
}
Note
The command will check if
User
model exist. If not, the observer will not be created. For example, if your model path isApp/Models/Foo/Bar.ts
then it should be--model=Foo/Bar
or-m "Foo/Bar"
. So, be sure to pass a valid model path.
If we want to hash the user password, then we will edit the beforeSave
method.
// App/Observers/UserObserver.ts
import type { ModelQueryBuilderContract } from '@ioc:Adonis/Lucid/Orm'
import type { ObserverContract } from '@ioc:Adonis/Addons/LucidObserver'
import Hash from '@ioc:Adonis/Core/Hash'
import User from 'App/Models/User'
export default class UserObserver implements ObserverContract {
// ... other methods
public async beforeSave(user: User): Promise<void> {
if (user.$dirty.password) {
user.password = await Hash.make(user.password)
}
}
}
Note
All Lucid hooks are supported:
beforeSave
,afterSave
,beforeCreate
,afterCreate
,beforeUpdate
,afterUpdate
,beforeDelete
,afterDelete
,beforeFind
,afterFind
,beforeFetch
,afterFetch
,beforePaginate
,afterPaginate
.
Before registering the observer, you need to apply the Observable
mixin to your model.
// App/Models/User.ts
import { BaseModel } from '@ioc:Adonis/Lucid/Orm'
import { compose } from '@ioc:Adonis/Core/Helpers'
import { Observable } from '@ioc:Adonis/Addons/LucidObserver'
class User extends compose(BaseModel, Observable) {}
Now, User
model has the following static methods:
Add an observer to a model.
Add multiple observers to a model.
Get a list of model observers.
Get a list of model observers (including global observers if any).
Start to observe the model.
Execute model actions without firing any observer event. For example, the below code won't fire the beforeFind
and afterFind
events:
await User.withoutObservers(async () => {
await User.find(1)
})
You can also return whatever you want from the method, it will be automatically typed.
const user = await User.withoutObservers(async () => {
return await User.create({
name: 'John Doe',
email: 'john@doe.com'
})
})
// ^^^ const user: User
Thanks to the mixin, each model instance can call the saveQuietly
method. This method will execute save
method under the hood without firing any observer event.
The following example will not fire beforeSave
and afterSave
events.
const user = new User()
user.email = 'john@doe.com'
user.name = 'John Doe'
await user.saveQuietly()
Note
saveQuietly
always return an instance of the model.
Global observers will observe all models that extend the Observable
mixin.
Create a preloaded file.
node ace make:provider Observer
Open the newly created file providers/ObserverProvider.ts
and add the following code to the boot
method.
import type { ApplicationContract } from '@ioc:Adonis/Core/Application'
export default class ObserverProvider {
constructor(protected app: ApplicationContract) {}
public register() {}
public async boot() {
const { default: GlobalObserver } = await import('App/Observers/GlobalObserver')
const { BaseModel } = this.app.container.use('Adonis/Lucid/Orm')
BaseModel.$addGlobalObserver(GlobalObserver)
}
public async ready() {}
public async shutdown() {}
Note
Inside
.adonisrc.json
, this provider file must come after@adonisjs/lucid
and@melchyore/adonis-lucid-observer
in the providers array, not before.
Note
You can use
$addGlobalObservers
method, which accepts anArray
of observers as the only argument.
Note
If you are creating a third-party package, this way is preferred to register a global observer.
Create a preloaded file.
node ace make:prldfile observers
Open the newly created file start/observers.ts
and paste the following code.
import { BaseModel } from '@ioc:Adonis/Lucid/Orm'
import GlobalObserver from 'App/Observers/Globals/GlobalObserver'
BaseModel.$addGlobalObserver(GlobalObserver)
Note
You can use
$addGlobalObservers
method, which accepts anArray
of observers as the only argument.
Model observers are applied to specific models.
// App/Observers/UserObserver.ts
import type { ObserverContract } from '@ioc:Adonis/Addons/LucidObserver'
class UserObserver implements ObserverContract {
// Some methods
}
You can register this observer in many ways.
// App/Models/User.ts
import { BaseModel } from '@ioc:Adonis/Lucid/Orm'
import { compose } from '@ioc:Adonis/Core/Helpers'
import { Observable } from '@ioc:Adonis/Addons/LucidObserver'
class User extends compose(BaseModel, Observable) {
protected static $observers = [new UserObserver()]
@column({ isPrimary: true })
public id: number
@column()
public email: string
@column()
public name: string
}
Note
You must register an instance of the observer class. All other ways don't require class instance.
// App/Models/User.ts
import { BaseModel } from '@ioc:Adonis/Lucid/Orm'
import { compose } from '@ioc:Adonis/Core/Helpers'
import { Observable } from '@ioc:Adonis/Addons/LucidObserver'
class User extends compose(BaseModel, Observable) {
@column({ isPrimary: true })
public id: number
@column()
public email: string
@column()
public name: string
public static boot() {
if (this.booted) {
return
}
super.boot()
this.$addObserver(UserObserver)
}
}
Note
You can use
$addObservers
method, which accepts anArray
of observers as the only argument.
Create a preloaded file.
node ace make:prldfile observers
Open the newly created file start/observers.ts
and paste the following code.
// start/observers.ts
import User from 'App/Models/User'
import UserObserver from 'App/Observers/UserObserver'
User.$addObserver(UserObserver)
Note
You can use
$addObservers
method, which accepts anArray
of observers as the only argument.
If you wish to register and observe a model in one go, make use of the observe
method, which accept an optional array of observers as the only argument.
// start/observers.ts
import User from 'App/Models/User'
import UserObserver from 'App/Observers/UserObserver'
User.observe([UserObserver])
To start observing a model, you need to call the observe
method for each model. It will execute both global and model observers.
// start/observers.ts
import User from 'App/Models/User'
import UserObserver from 'App/Observers/UserObserver'
User.$addObserver(UserObserver)
User.observe()
// Or
User.observe([UserObserver]) // <- Preferred way.
Note
If you have registered an observer via the
observe
method, you don't need to call it again. If you call theobserve
method twice, an exception will be raised.
If you don't want to import each model and observer, you can register the observers inside models, as described in observers attribute in the model and Model boot method sections.
Next, you need to create a new file called index.ts
inside App/Models
. You will export all the models you want to observe from that file.
// App/Models/index.ts
import User from './User'
export {
User
}
Then, paste the following code inside start/observers.ts
.
import * as Models from 'App/Models'
for (const Model of Object.values(Models)) {
Model.observe()
}
If you wish to execute observer methods after the database transaction is committed, you can define an afterCommit
property on the observer class. In the following example, afterFetch
will be executed once the transaction is committed.
import type { ObserverContract } from '@ioc:Adonis/Addons/LucidObserver'
class UserObserver implements ObserverContract {
public afterCommit = true
public async afterFetch(users: Array<User>): Promise<void> {
// Some code
}
}
Note
If
afterCommit
is set totrue
and no transaction is defined, the observer methods will still be executed.
Note
afterPaginate
won't work with transactions.
If a model extends from another model that has observers, it will inherit its observers. In other words, if the parent model extends from Observable
mixin, the child model doesn't need to extend from that mixin.
Each observer action will fire an event. Each event is named using the pattern observer:actionName
. For example, beforeFind
will fire the observer:beforeFind
event. Each event handler will receive an object with the following data.
Event.on('observer:afterSave', (data) => {
data.type // <- afterSave
data.data // <- model instance, not all the events have the same `data`, it depends on the type of the event.
data.observer // <- name of the observer.
data.isTransaction // <- a boolean indicating if the query has been run in a transaction. Always `false` for afterPaginate.
})
You can still use hooks decorators along with the observers in your models. Hooks decorators will always be executed before observers. However, it's preferable not to use both.
yarn run test
👤 Oussama Benhamed
- Twitter: @Melchyore
- Github: @Melchyore
Contributions, issues and feature requests are welcome!
Feel free to check issues page.
Give a ⭐️ if this project helped you!
Copyright © 2022 Oussama Benhamed.
This project is MIT licensed.