npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@melchyore/adonis-lucid-observer

v1.0.2

Published

Observer package for AdonisJS V5 Lucid ORM

Downloads

174

Readme

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!

License

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