Filtering and Pagination
This is an exciting section of the tutorial where you’ll implement some key features of many robust
APIs! The goal is to allow clients to constrain the list of Link
elements returned by the
Query.feed
field by providing filtering and pagination parameters.
Let’s jump in! 🚀
Filtering
By using PrismaClient
, you’ll be able to implement filtering capabilities to your API without too
much effort. Similar to the previous chapters, the heavy-lifting of query resolution will be
performed by Prisma. All you need to do is pass the correct parameters within the field resolver
functions.
The first step is to think about the filters you want to expose through your API. In your case, the
Query.feed
field in your API will accept a filter string. The query then should only return the
Link
elements where the url
or the description
contain that filter string.
Go ahead and add the filter
argument definition of the type String
to the Query.feed
field
definition within your application schema (under src/schema.ts
):
type Query {
info: String!
feed(filterNeedle: String): [Link!]!
}
Next, you need to update the implementation of the Query.feed
resolver function to account for the
new arguments clients can send when executing a GraphQL query operation.
Now, update the Query.feed
resolver function to look as follows:
const resolvers = {
// ... other resolver maps ...
Query: {
// ... other Query object type field resolver functions ...
async feed(parent: unknown, args: { filterNeedle?: string }, context: GraphQLContext) {
const where = args.filterNeedle
? {
OR: [
{ description: { contains: args.filterNeedle } },
{ url: { contains: args.filterNeedle } }
]
}
: {}
return context.prisma.link.findMany({ where })
}
}
}
If no filterNeedle
argument value is provided, then the where
object will be just an empty
object and no filtering conditions will be applied by Prisma Client when it resolves the data for
the Query.links
field.
In cases where there is a filterNeedle
argument provided when executing the Query.links
field
resolver, you’re constructing a where
object that expresses our two filter conditions from above.
This where
argument is used by Prisma to filter out those Link
elements that don’t adhere to the
specified conditions.
That’s it for the filtering functionality! Go ahead and test your filter API - here’s a sample query operation you can use:
query {
feed(filterNeedle: "QL") {
id
description
url
}
}
Pagination
Pagination is a tricky topic in API design. On a high level, there are two major approaches for tackling it:
- Limit-Offset: Request a specific chunk of the list by providing the indices of the items to
be retrieved. In fact, you’re mostly providing the start index
offset
as well as a count of items to be retrievedlimit
. - Cursor-Based: This pagination model is a bit more advanced. Every element in the list is associated with a unique ID (the cursor). Clients paginating through the list then provide the cursor of the starting element as well as a count of items to be retrieved.
Prisma supports both pagination approaches (read more in the docs). In this tutorial, you’re going to implement limit-offset pagination.
Note: You can read more about the ideas behind both pagination approaches here.
Limit and offset have different names in the Prisma API:
- The limit is called
take
, meaning you’re “taking”x
elements after a provided start index. - The start index is called
skip
, since you’re skipping that many elements in the list before collecting the items to be returned. Ifskip
is not provided, it’s0
by default. The pagination then always starts from the beginning of the list.
So, go ahead and add the skip
and take
arguments to the Query.field
field definition.
Open your schema definitions and adjust the Query.field
field to have a skip
and take
argument
of the Int
type defined:
type Query {
info: String!
feed(filterNeedle: String, skip: Int, take: Int): [Link!]!
}
Now, adjust the field resolver function implementation:
And now adjust the implementation of the Query.feed
resolver function:
const resolvers = {
// ... other resolvers maps ...
Query: {
// ... other Query object type resolver functions ...
async feed(
parent: unknown,
args: { filterNeedle?: string; skip?: number; take?: number },
context: GraphQLContext
) {
const where = args.filterNeedle
? {
OR: [
{ description: { contains: args.filterNeedle } },
{ url: { contains: args.filterNeedle } }
]
}
: {}
return context.prisma.link.findMany({
where,
skip: args.skip,
take: args.take
})
}
}
}
All that’s changing here is that the invocation of the links
query now receives two additional
arguments which might be carried by the incoming args
object. Again, Prisma will take care of the
rest.
You can test the pagination API with the following query operation which returns the second Link
from the list:
query {
feed(take: 1, skip: 1) {
id
description
url
}
}
Pagination Field Argument Sanitization
In the last chapter, you already sanitized the arguments within the Mutation.postCommentOnLink
field resolver function.
Up next, let’s also take into consideration that the Query.feed
arguments should also have some
sanitizing.
As more links are posted onto the feed, the size of the feed increases. At some point, there could
be thousands of feed items. When deploying the GraphQL API to production you would not want to allow
querying ALL the links at once. That means you have to introduce a default value for the take
argument.
Furthermore, you should limit the take
argument to be within a range that makes sense for the
feed. The original Hackernews shows 30 links per page.
Let’s use the value 30 as the default value and 50 as the upper limit and 1 as the lower limit. Fetching 0 records does not make sense.
Adjust the current schema resolver implementation according to the following.
// ... other code ...
import { GraphQLError } from 'graphql'
const applyTakeConstraints = (params: { min: number; max: number; value: number }) => {
if (params.value < params.min || params.value > params.max) {
throw new GraphQLError(
`'take' argument value '${params.value}' is outside the valid range of '${params.min}' to '${params.max}'.`
)
}
return params.value
}
const resolvers = {
// ... other resolvers maps ...
Query: {
// ... other Query object type resolver functions ...
async feed(
parent: unknown,
args: { filterNeedle?: string; skip?: number; take?: number },
context: GraphQLContext
) {
const where = args.filterNeedle
? {
OR: [
{ description: { contains: args.filterNeedle } },
{ url: { contains: args.filterNeedle } }
]
}
: {}
const take = applyTakeConstraints({
min: 1,
max: 50,
value: args.take ?? 30
})
return context.prisma.link.findMany({
where,
skip: args.skip,
take
})
}
}
}
Cool! Now we can try executing an operation with a take
argument value outside the range!
Execute the following query operation via GraphiQL:
query {
feed(take: -1) {
id
description
url
}
}
As expected, you receive an error:
{
"errors": [
{
"message": "'take' argument value '-1' is outside the valid range of '1' to '50'.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": ["feed"]
}
],
"data": null
}
Optional Exercise
As an optional exercise for interiorizing the knowledge, you can now also implement validation of
the skip
argument of Query.feed
field and prohibit using negative numbers.