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
offsetas 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”xelements 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. Ifskipis not provided, it’s0by 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.