Skip to content

Melchyore/adonis-lucid-observer

Repository files navigation

Lucid Observer

npm License: MIT Typescript

Use classes instead of decorators to observe your Lucid models.

Introduction

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.

Pre-requisites

Node.js >= 16.14.0

@adonisjs/lucid >= 18.0.0

Installation

npm install @melchyore/adonis-lucid-observer

# or using Yarn

yarn add @melchyore/adonis-lucid-observer

Configure

node ace configure @melchyore/adonis-lucid-observer

Usage

Create an Observer

Global 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> {}
}

Model observer

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 is App/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:

$addObserver(observer)

Add an observer to a model.


$addObservers([observers])

Add multiple observers to a model.


$getObservers()

Get a list of model observers.


$getAllObservers()

Get a list of model observers (including global observers if any).


observe()

Start to observe the model.


withoutObservers(callback)

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: '[email protected]'
  })
})
// ^^^ 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 = '[email protected]'
user.name = 'John Doe'

await user.saveQuietly()

Note

saveQuietly always return an instance of the model.

Register Observers

Global Observers

Global observers will observe all models that extend the Observable mixin.

Inside a provider file

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 an Array of observers as the only argument.

Note

If you are creating a third-party package, this way is preferred to register a global observer.

Inside a preloaded file

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 an Array of observers as the only argument.

Model Observers

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.

$observers attribute in the 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) {
  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.

Model boot method

// 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 an Array of observers as the only argument.

Inside a preloaded file (the preferred way)

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 an Array 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])

Observe models

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 the observe 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()
}

Transactions

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 to true and no transaction is defined, the observer methods will still be executed.

Note

afterPaginate won't work with transactions.

Inherit observers through models

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.

Events

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.
})

What about hooks decorators?

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.

Run tests

yarn run test

Author

👤 Oussama Benhamed

🤝 Contributing

Contributions, issues and feature requests are welcome!
Feel free to check issues page.

Show your support

Give a ⭐️ if this project helped you!

Buy Me A Coffee

License

Copyright © 2022 Oussama Benhamed.
This project is MIT licensed.