Client-Supplied Custom Sorting Using GraphQL
It’s not uncommon the need to sort/order data returned from an API, based on arguments sent by the client. With REST we could just use query-strings, but how to do it in GraphQL?
Most databases allow to sort on multiple fields at the same time, and the sorting order can differ from one to another, one way to translate this into GraphQL, and that I’ve been using, is to use enums and object input arguments.
Example
Let’s say we have a resolver getUsers: [User!]!
which returns users from MongoDB, and we are sorting the results by name
and createdAt
:
const users = await ctx.db.users.find().sort({
name: 1, // ascending
createdAt: -1, // descending
})
But we want to allow the client to specify the fields that are going to be sorted, and their respective order.
First, we could have a Direction
enum, whose values are ASC
and DESC
:
enum DirectionEnum {
ASC
DESC
}
Using graphql-js
this could be defined like the following:
import { GraphQLEnumType } from 'graphql'
export const DirectionEnumType = new GraphQLEnumType({
name: 'DirectionEnum',
values: {
ASC: {
value: 1,
},
DESC: {
value: -1,
},
},
})
Now let’s create a enum to represent the fields that are allowed to be sorted on our User
:
enum UserSortFieldEnum {
NAME
CREATED_AT
}
In graphql-js
:
import { GraphQLEnumType } from 'graphql'
export const UserSortFieldEnumType = new GraphQLEnumType({
name: 'UserSortFieldEnum',
values: {
NAME: {
value: 'name',
},
CREATED_AT: {
value: 'createdAt',
},
},
})
Now, to connect those two together, we are going to make a input type called UserSort
:
input UserSort {
field: UserSortFieldEnum!
direction: DirectionEnum!
}
graphql-js
:
import { GraphQLInputObjectType, GraphQLNonNull } from 'graphql'
import { DirectionEnumType } from './DirectionEnum'
import { UserSortFieldEnumType } from './UserSortFieldEnum'
const UserSortInputType = new GraphQLInputObjectType({
name: 'UserSort',
fields: () => ({
field: {
type: GraphQLNonNull(UserSortFieldEnumType),
},
direction: {
type: GraphQLNonNull(DirectionEnumType),
},
}),
})
Then our getUsers
resolver will become:
getUsers(sort: [UserSort!]): [User!]!
And now the client can query it and specify any sort order they need:
query GetUsers {
getUsers(sort: [
{
field: NAME,
direction: DESC,
},
{
field: CREATED_AT,
direction: ASC,
}
]) {
# ...
}
}
And if you are using MongoDB, and are going to start using this, @entria/graphql-mongo-helpers
has a simple helper to transform the sort
argument supplied by the client to something that can be used on MongoDB sort()
method.
It’s called buildSortArg
, using it on our getUser
resolver would look like this:
// ...
import { buildSortFromArg } from '@entria/graphql-mongo-helpers'
// ... inside getUser resolver:
const users = await ctx.db.users.find().sort(buildSortFromArg(args.sort))
The library also exports a SDL definition and GraphQLObject
for the Direction
enum, DirectionEnum
and DirectionEnumType
respectively.
Remarks
This same idea can be used with any other database library, knex
for example has the orderBy
method which accepts an argument in the format:
{
column: string,
order: 'asc' | 'desc'
}
and for that, a function called buildOrderByFromArg
can be as simple as:
export default function buildOrderByFromArg(orderByArg) {
return orderByArg.reduce(
(acc, item) => [
...acc,
{
column: item.field,
order: item.direction === -1 ? 'desc' : 'asc',
},
],
[],
)
}