Refactoring search queries in Adonis.js
Tags: adonis, node, refactoring, javascript
Published: 2019-04-25 22:37:38

> Originally posted at [michaelzanggl.com](https://michaelzanggl.com/articles/refactoring-search-queries-in-adonisjs). Subscribe to [my newsletter](https://michaelzanggl.com/) to never miss out on new content.

In the previous post of this series we were looking at various ways to keep controllers in Adonis small, but the various ways were not helping us with the following:

```javascript
const Post = use('App/Models/Post')

class PostsController {
    async index({ response, request }) {    
        const query = Post.query()

        if (request.input('category_id')) {
            query.where('category_id', request.input('category_id'))
        }

        let keyword = request.input('keyword')

        if (keyword) {
            keyword = `%${decodeURIComponent(keyword)}%`
            query
                .where('title', 'like', keyword)
                .orWhere('description', 'like', keyword)
        }

        const tags = request.input('tags')
        if (tags) {
            query.whereIn('tags', tags)
        }

        const posts = await query.where('active', true).fetch()

        return response.json({ posts: posts.toJSON() })
    }
}
```

So let's dive into various ways we can clean this up.

## Scopes

Adonis has a feature called [query scopes](https://adonisjs.com/docs/4.1/lucid#_query_scopes) that allows us to extract query constraints. Let's try this with the keyword constraint.

```javascript
keyword = `%${decodeURIComponent(keyword)}%`
query
    .where('title', 'like', keyword)
    .orWhere('description', 'like', keyword)
```

To create a new scope we would go into our `Posts` model and add the following method to the class

```javascript
static scopeByEncodedKeyword(query, keyword) {
    keyword = `%${decodeURIComponent(keyword)}%`
    
    return query
        .where('title', 'like', keyword)
        .orWhere('description', 'like', keyword)
}
```

Now back in the controller, we can simply write

```javascript
if (keyword) {
    query.byEncodedKeyword(keyword)
}
```

It's important that the method name is prefixed with `scope`. When calling scopes, drop the `scope` keyword and call the method in camelCase (`ByEncodedKeyword` => `byEncodedKeyword`).

This is a great way to simplify queries and hide complexity! It also makes query constraints reusable.

---

Let's talk about these conditionals...

I actually created two traits to overcome all these conditionals. If you are new to traits please check out in the repositories on how to set them up.

## [Optional](https://github.com/MZanggl/adonis-lucid-optional-queries)

Repository: https://github.com/MZanggl/adonis-lucid-optional-queries

With Optional we will be able to turn the `index` method into

```javascript
async index({ response, request }) {    
    const posts = await Post.query()
        .optional(query => query
            .where('category_id', request.input('category_id'))
            .byEncodedKeyword(request.input('keyword'))
            .whereIn('tags', request.input('tags'))
        )
        .where('active', true)
        .fetch()

    return response.json({ posts: posts.toJSON() })
}
```



We were able to get rid of all the conditionals throughout the controller by wrapping optional queries in the higher order function `optional`. The higher order function traps the query object in an ES6 proxy that checks if the passed arguments are truthy. Only then will it add the constraint to the query.

---

## [When](https://github.com/MZanggl/adonis-lucid-when)

Repository: https://github.com/MZanggl/adonis-lucid-when

The second trait I wrote implements Laravel's `when` method as a trait. `Optional` has the drawback that you can only check for truthy values, sometimes you might also want to check if an input is a certain value before you apply the constraint. With `when` we can turn the search method into

```javascript
async index({ response, request }) {    
    const posts = await Post.query()
        .when(request.input('category_id'), (q, value) => q.where('category_id', value))
        .when(request.input('keyword'), (q, value) => q.byEncodedKeyword(value))
        .when(request.input('sort') === 1, q => q.orderBy('id', 'DESC'))
        .where('active', true)
        .fetch()

        return response.json({ posts: posts.toJSON() })
    }
```

`When` works similar to `Optional` in that it only applies the callback when the first argument is truthy. You can even add a third parameter to apply a default value in case the first argument is not truthy.

--- 

Of course we can also combine these two traits


```javascript
async index({ response, request }) {    
    const posts = await Post.query()
        .optional(query => query
            .where('category_id', request.input('category_id'))
            .byEncodedKeyword(request.input('keyword'))
            .whereIn('tags', request.input('tags'))
        )
        .when(request.input('sort') === 1, q => q.orderBy('id', 'DESC'))
        .where('active', true)
        .fetch()

    return response.json({ posts: posts.toJSON() })
}
```

---

An even more elegant way would be to use filters. Check out [this module](https://www.npmjs.com/package/adonis-lucid-filter).

We could turn our controller into

```javascript
const Post = use('App/Models/Post')

class PostsController {
    async index({ response, request }) {
        const posts = await Post.query()
            .filter(request.all())
            .fetch()

        return response.json({ posts: posts.toJSON() })
    }
}
```

This has the benefit that it removes all constraints from the controller, but also the drawback that it is not 100% clear what is happening without a close look to all the filters you created.

# Conclusion

There's always more than one way to skin a cat, we could have also extracted the queries and conditions into a separate class specifically for searching this table (kind of like a repository pattern but for searching).

I hope this post gave you some ideas on how to clean up your search queries.

---

If this article helped you, I have a lot more tips on simplifying writing software [here](https://michaelzanggl.gumroad.com/l/intent-driven-development).