Rails like scope methods in Objection.js (NodeJS ORM)
Let say we have a model called Label
const { Model } = require('objection')
class Label extends Model {
static get tableName() {
return "labels"
}
static get jsonSchema () {
return {
type: 'object',
required: [],
properties: {
id: { type: 'integer' },
name: { type: 'string' }
}
}
}
}
Now we want to get the last Label in the model.
const label = await Label.query().orderby('id', 'desc').limit(1).first()
Although this gets us the last label, it has a few shortcomings:
- It is verbose
- It requires too much repeated typing and thus prone to errors
- Its harder to test
- It doesn’t read well
- And things only get worse when its used in conjunction with other methods
Here are 3 ways to approach this:
- Modifiers
- A Regular Class Method
- Custom QueryBuilder Object
Lets dive into each of these one-by-one.
Approach 1: Modifiers
Modifiers is my preferred way to solve this. We specify a function on the modifiers object which:
- receives the
query
as a param - it then modifies the query by adding its filters etc.
Label.modifiers.last = query => {
query.orderby('id', 'desc').limit(1).first()
}
Now lets get the last record by using this modifier
const label = await Label.query().modify('last')
This reads so much better, encapsulates all logic under one function and we can test that one function easily.
The logs show that it ran:
select "labels".* from "labels" order by "id" DESC limit 1
With params
Lets build another modifier which gets all labels which start with the passed in letters
Label.modifiers.startsWith = (query, letters) => {
query.where('name', 'like', `${letters}%`)
}
Now lets run it
labels = await Label.query().modify('startsWith', 'XYYZ')
And logs show:
select "labels".* from "labels" where "name" like "AC%"
Combining multiple modifier functions
This is where i think modifier functions start to shine, just like scopes do in Rails.
So lets say we need the last label which starts with ‘A’. We can achieve this by using our startsWith
& last
modifier functions together.
const label = await Label.query().modify('startsWith','A').modify('last')
And our logs have:
select "labels".* from "labels" where "name" like "A%" order by "id" DESC limit 1
Approach 2: Class method on Label
A regular static method on Label class. We can have this method return the last record:
Label.last = () => {
return await Label.orderby('id', 'desc').limit(1).first()
}
This gets the job done, but not as good as a modifier function. Yes it reads good and encapsulates the work but it doesn’t return the query object and thus can’t be chained
Approach 3: Custom QueryBuilder
We can build our custom query object and have label class use our query object. On our custom query object we can define a custom methods which modify the query()
object directly.
This will allow us to modify the query by calling an internal method of the query object, without writing the words modify
and explicitly making it clear that we are modifying the query.
Lets see an example:
class MyQueryBuilder extends QueryBuilder {
last () {
logger.info('inside last')
this.orderBy('id', 'desc').limit(1).first()
return this
}
}
class Label exteds Model {
static get QueryBuilder () {
return MyQueryBuilder
}
}
Now to use it:
cons label = await Label.query().last()
I think this approach is an abuse of power. It works, but we have a cleaner way of modifying the query and we should do that instead of defining a custom query object which has special internal methods.
I think this custom query class might have good use cases though for other things like logging, making some other service calls etc.
Conclusion
modifiers
are great. the ability to chain them make them an asset.
What’s Next
Use modifiers with complex queries which use:
- join
- graphFetch (eager loading)
- use
ref
where we have ambiguous table names