Model Query Builder Macros in AdonisJS 6

In this lesson, we'll learn how we can add custom methods to the Model Query Builder with Lucid in AdonisJS 6 using macros.

Published
Dec 10
Duration
7m 44s

Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.

Adocasts

Burlington, KY

Chapters

00:00 - Setting the Baseline
01:01 - Where Do We Define our Macros?
01:47 - Defining A GetCount Model Query Builder Macro
03:15 - Defining our Macro Types on the Model Query Builder
05:54 - Passing Arguments into Macros

What is a Model Query Builder Macro?

In AdonisJS, macros provide us a way to easily extend internal AdonisJS classes with our own methods. A lot in AdonisJS is macroable, including the request, response, and query builder.

When we add a macro, that function will then be readily available to use with instances of whatever class it is we've defined that macro. In the case of the Model Query Builder, adding a macro will make that function available within all model's query builders.

In order for macros to be properly added, we'll need to register them before our application is booted. That makes this an ideal job for a preload, which are preloaded before our application is booted and are typically located within the start directory of our application.

node ace make:preload macros/model_query_builder_macros
Copied!

Then, when asked if you'd like to register the preload file in your adonisrc.ts file, be sure to select yes so it is used.

This will create a folder called macros, which we can use to house any/all macros we make for our application. Inside this macros folder it'll then create our model_query_builder_macros.ts file, which will be empty.

Our Query

Imagine we have the following query.

const publishedPostCount = await Post.query()
  .apply((scope) => scope.published())
  .count('* as total')
  .first()
  .then((result: Post) => res.$extras.total)
Copied!

Here, we're counting the total number of published posts we have in our database by using the Post model and it's published query scope. We're aliasing that count as "total" via the * as total string inside our count method.

With aggregates, the results are returned in the $extras object of the first index, so we're grabbing just the first item, then reaching for $extras.total

Note that query scopes are a great option when you need to add reusable query methods for a specific model. If you need to add reusable query methods to all models, that's where a macro becomes ideal.

Our Macro

If you're performing a lot of counts, sums, or other aggregations the above query might be all throughout your application, making it a prime candidate for a macro called getCount, getSum, or the like. So, let's convert the count portion from our above query into a getCount Model Query Builder macro.

import { ModelQueryBuilder } from '@adonisjs/lucid/orm'

ModelQueryBuilder.macro('macroName', function (this: ModelQueryBuilder) {
  // perform query operations using `this`
})
Copied!
  • start
  • macros
  • model_query_builder_macros.ts

First, we'll import the ModelQueryBuilder class. This class, along with many others in AdonisJS, extend a Macroable class. It is this Macroable class that adds in this macro functionality for us to use.

The first argument is the name we'd like to give this macro. This name is the same name we'll use to call the macro and apply it to our query. The second argument is the function that will get called whenever our macro is called. This function is provided this, which is our query builder instance.

So, we'll want to give our macro a name of "getCount" and within the function, we'll want to apply the count to our query, select the first result, and return back the value within that first result's $extras.total.

import { ModelQueryBuilder } from '@adonisjs/lucid/orm'

ModelQueryBuilder.macro('getCount', function (this: ModelQueryBuilder) {
  return this.count('* as total')
    .first()
    .then((result: LucidRow) => result.$extras.total)
})
Copied!
  • start
  • macros
  • model_query_builder_macros.ts

The only difference from our original query is that we've replaced the Post type with LucidRow. Since this macro will be available for all our model query builders, we need the type to be shared or generic and all model row instances extend LucidRow. Making this the perfect type for us since we only care about the $extras object here for our returned value.

Informing TypeScript

Before we use our macro, we need to inform TypeScript about it. As of right now, this includes adding the macro to two difference interfaces

  1. ModelQueryBuilder - to make the macro name happy

  2. ModelQueryBuilderContract - to make the actual macro function available on the query builder methods.

import { ModelQueryBuilder } from '@adonisjs/lucid/orm'

declare module '@adonisjs/lucid/types/model' {
  interface ModelQueryBuilderContract<Model extends LucidModel, Result = InstanceType<Model>> {
    getCount(): Promise<BigInt>
  }
}

declare module '@adonisjs/lucid/orm' {
  interface ModelQueryBuilder {
    getCount(): Promise<BigInt>
  }
}

ModelQueryBuilder.macro('getCount', function (this: ModelQueryBuilder) {
  return this.count('* as total')
    .first()
    .then((result: LucidRow) => result.$extras.total)
})
Copied!
  • start
  • macros
  • model_query_builder_macros.ts

Using our Macro

Lastly, we can now use our macro on any of our model's query builders.

const publishedPostCount = await Post.query()
  .apply((scope) => scope.published())
  .getCount()
Copied!

Join The Discussion! (0 Comments)

Please sign in or sign up for free to join in on the dicussion.

robot comment bubble

Be the first to Comment!