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!]!
# ...
}
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
}
}
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
}
}
}
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
}
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
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]}
}
}
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.
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)
Top comments (0)