GraphQL Recipes: Filtering
Tags: graphql, webdev
Published: 2022-10-25 10:24:14

## Introduction


GraphQL itself does not define any rules on how to perform filtering and search, so an engineer have to came up with some approach.

## Individual queries
In the simplest cases, it might look like
```graphql
extend Query {
  getUsersByFirstName(name: String!) [User!]!
  getRecentUsers(since: Date!) [User!]!
  # ...
}
```
Obviously, this is not ideal and have following problems:
* Lack of scalability potential - adding more filtering options require creating a new query or modifying an existing one,
* Code style and rules could not be enforced - different engineers at different period of time might have different vision on how to name and implement query endpoints, and implementation might deviate between endpoints.

## Proposed approach
I would like you introduce a different approach which I personally find concise and logical. Consider following example of a User query:
```graphql
{
  user(
    where: {
      firstName: { exact: "John" },
      age: { gte: 21, lte: 28 }
    }
  ) {
    firstName
    lastName
    age
  }
}
```
Also, since this filtering is represented as a field attribute, it's possible to write filtering for nested properties:
```graphql
{
  user(where: { firstName: { exact: "John" } }) {
    firstName
    lastName
    age
    friends(where: { age: { gte: 18 } }) {
      firstName
      lastName
    }
  }
}
```
This approach gives a lot of control to a GraphQL API clients, and require minimal maintenance. Other advantages include:
* Low implementation cost - requires only write a schema and create an abstraction for resolvers once,
* Predictable - common GQL types and resolver abstraction requires to follow a convention
* Not require to change schema/implementation for complex queries,
* Portability - the query parts could be reused between queries or projects if needed

## Implementation details
If you want to have a filtering for a multivalue property, you should define a `where` argument for that property of following format:
```graphql
extend type Query {
  user(where: UserFilter = {}) [User!]!
}

input UserFilter {
  id: WhereArgInt
  firstName: WhereArgString
  lastName: WhereArgString
  age: WhereArgInt
  # Add more as needed
}
```
Let's add `WhereArg*` types to some common GraphQL schema file:
```graphql
input WhereArgInt {
    exact: Int
    gt: Int
    lt: Int
    lte: Int
    gte: Int
    in: [Int]
}

input WhereArgString {
    exact: String
    gt: String
    lt: String
    contains: String
    in: [String]
}

# Other data types will follow the same convention
```

## Resolvers
This convention highly relies on the resolvers and require you to create a specific abstraction for your backend API. Let's consider few popular examples:

### JS/Sequelize
Resolver should read `where` expression and convert it something like listed below before passing it to `.findAll()` method:
```js
{
  where: {
    firstName: {[Op.eq]: "John"},
    age: {[Op.gte]: 21, [Op.lte]: 28]}
  }
}
```

### JS/Prisma
Prisma should be really easy to implement, because it uses almost exactly same syntax. We might consider using Prisma convention for our GQL schema instead to expose it's filtering API through GQL directly:
```
const result = prisma[model].findMany({
    where: whereArgGQL,
})
```

### Python/Django
Your model resolver base classes should have this function implemented.
```python
where_arg_example = {
    "first_name": {"exact": "John"},
    "age": {"lte": 21, "gte": 28},
}

def read_filtering(where_arg):
    if where_arg is None:
        return Q()

    predicates = {}
    for where_item in where_arg.items():
        field, cond = where_item
        for operator, value in cond.items():
            predicates[f"{field}__{operator}"] = value

    return Q(**predicates)

q = read_filtering(where_arg_example)
some_query_set.filter(q)
```