DEV Community

Cover image for GraphQL Recipes: Filtering
Ruslan
Ruslan

Posted on

GraphQL Recipes: Filtering

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

extend Query {
  getUsersByFirstName(name: String!) [User!]!
  getRecentUsers(since: Date!) [User!]!
  # ...
}
Enter fullscreen mode Exit fullscreen mode

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:

{
  user(
    where: {
      firstName: { exact: "John" },
      age: { gte: 21, lte: 28 }
    }
  ) {
    firstName
    lastName
    age
  }
}
Enter fullscreen mode Exit fullscreen mode

Also, since this filtering is represented as a field attribute, it's possible to write filtering for nested properties:

{
  user(where: { firstName: { exact: "John" } }) {
    firstName
    lastName
    age
    friends(where: { age: { gte: 18 } }) {
      firstName
      lastName
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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:

extend type Query {
  user(where: UserFilter = {}) [User!]!
}

input UserFilter {
  id: WhereArgInt
  firstName: WhereArgString
  lastName: WhereArgString
  age: WhereArgInt
  # Add more as needed
}
Enter fullscreen mode Exit fullscreen mode

Let's add WhereArg* types to some common GraphQL schema file:

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
Enter fullscreen mode Exit fullscreen mode

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:

{
  where: {
    firstName: {[Op.eq]: "John"},
    age: {[Op.gte]: 21, [Op.lte]: 28]}
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

Python/Django

Your model resolver base classes should have this function implemented.

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)
Enter fullscreen mode Exit fullscreen mode

Top comments (0)