Skip to content

Latest commit

 

History

History
385 lines (283 loc) · 12.7 KB

8-filtering-pagination-and-sorting.md

File metadata and controls

385 lines (283 loc) · 12.7 KB
title pageTitle description question answers correctAnswer
Filtering, Pagination & Sorting
GraphQL Filtering, Pagination & Sorting Tutorial with JavaScript
Learn how to add filtering and pagination capabilities to a GraphQL API with Node.js, Express & Prisma.
Which arguments are typically to paginate through a list in the Prisma API using limit-offset pagination?
skip & last
skip & first
first & last
where & orderBy
1

This is the last section of the tutorial where you'll implemented the finishing touches on your API. The goal is to allow clients to constrain the list of Link elements returned by the feed query by providing filtering and pagination parameters.

Filtering

Thanks to Prisma, you'll be able to implement filtering capabilities to your API without major effort. Similar to the previous chapters, the heavy-lifting of query resolution will be performed by the powerful Prisma engine. All you need to do is delegate incoming queries to it.

The first step is to think about the filters you want to expose through your API. In your case, the feed query 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 string to the feed query in your application schema:

type Query {
  info: String!
  feed(filter: String): [Link!]!
}

Next, you need to update the implementation of the feed resolver to account for the new parameter clients can provide.

Open src/resolvers/Query.js and update the feed resolver to look as follows:

function feed(parent, args, context, info) {
  const where = args.filter
    ? {
        OR: [
          { url_contains: args.filter },
          { description_contains: args.filter },
        ],
      }
    : {}

  return context.db.query.links({ where }, info)
}

If not filter string is provided, then the where object will be just an empty object and no filtering conditions will be applied by the Prisma engine when it returns the response for the links query.

In case there is a filter carried by the incoming args, 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 conidtions.

Notice that the the Prisma binding object translates the above function call into a GraphQL query that will look somewhat similar to this. This query is sent by the Yoga server to the Prisma API and resolved there:

query ($filter: String) {
  links(where: {
    OR: [{ url_contains: $filter }, { description_contains: $filter }]
  }) {
    id
    url
    description
  }
}

Note: You can learn more about Prisma's filtering capabilities in the docs. Another way explore those is to use the Playground of the Prisma API and read the schema documentation for the where argument or experiment directly by using the autocompletion features of the Playground.

That's it already for the filtering functionality! Go ahead and test your filter API - here's a sample query you can use:

query {
  feed(filter: "Prisma") {
    id
    description
    url
  }
}

Pagination

Pagination is a tricky topic in API design. On a high-level, there are two major approaches how it can be tackled:

  • 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 retrieved (limit)).
  • 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 provid 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 are called differently in the Prisma API:

  • The limit is called first, meaning you're grabbing the first x elements after a provided start index. Note that you also have a last argument which correspondingly returns the last x elements.
  • The start index is called skip, since you're skipping that many elements in the list before collecting the items to be returned. If skip is not provided, it's 0 by default. The pagination then always starts from the beginning of the list (or the end in case you're using last).

So, go ahead and add the skip and last arguments to the feed query.

Open your application schema and adjust the feed query to accept skip and first arguments:

type Query {
  info: String!
  feed(filter: String, skip: Int, first: Int): [Link!]!
}

Now, on to the resolver implementation.

In src/resolvers/Query.js, adjust the implementation of the feed resolver:

function feed(parent, args, context, info) {
  const where = args.filter
    ? {
        OR: [
          { url_contains: args.filter },
          { description_contains: args.filter },
        ],
      }
    : {}

  return context.db.query.links(
    { where, skip: args.skip, first: args.first },
    info,
  )
}

Really all that's changing here is that the invokation of the links query now receives two additional arguments which might be carried by the incoming args object. Again, Prisma will do the hard work for us 🙏

You can test the pagination API with the following query which returns the second Link from the list:

query {
  feed(
    first: 1
    skip: 1
  ) {
    id
  }
}

Sorting

With Prisma, it is possible to return lists of elements that are sorted (ordered) according to specific criteria. For example, you can order the list of Links alphabetically by their url or description. For the Hackernews API, you'll leave it up to the client to decide how exactly it should be sorted and thus include all the ordering options from the Prisma API in the API of your Yoga server. You can do so by directly referencing the LinkOrderByInput enum from the Prisma database schema. Here is what it looks like:

enum LinkOrderByInput {
  id_ASC
  id_DESC
  description_ASC
  description_DESC
  url_ASC
  url_DESC
  updatedAt_ASC
  updatedAt_DESC
  createdAt_ASC
  createdAt_DESC
}

It represents the various ways how the list of Link elemenets can be sorted.

Open your application schema and import the LinkOrderByInput enum from the Prisma database schema:

# import Link, LinkSubscriptionPayload, Vote, VoteSubscriptionPayload, LinkOrderByInput from "./generated/prisma.graphql"

Then, adjust the feed query again to include the orderBy argument:

type Query {
  info: String!
  feed(filter: String, skip: Int, first: Int, orderBy: LinkOrderByInput): [Link!]!
}

The implementation of the resolver is similar to what you just did with the pagination API.

Update the implementation of the feed resolver in src/resolvers/Query.js and pass the orderBy argument along to Prisma:

function feed(parent, args, context, info) {
  const where = args.filter
    ? {
        OR: [
          { url_contains: args.filter },
          { description_contains: args.filter },
        ],
      }
    : {}

  return context.db.query.links(
    { where, skip: args.skip, first: args.first, orderBy: args.orderBy },
    info,
  )
}

Awesome! Here's a query that sorts the returned links by their creation dates:

query {
  feed(orderBy: createdAt_ASC) {
    id
    description
    url
  }
}

Returning the total amount of Link elements

The last thing you're going to implement for your Hackernews API is the information how many Link elements are currently stored in the database. To do so, you're going to refactor the feed query a bit and create a new type to be returned by your API: Feed.

Add the new Feed type to your application schema. Then also adjust the return type of the feed query accordingly:

type Query {
  info: String!
  feed(filter: String, skip: Int, first: Int, orderBy: LinkOrderByInput): Feed!
}

type Feed {
  links: [Link!]!
  count: Int!
}

Now, go ahead and adjust the feed resolver again:

async function feed(parent, args, context, info) {
  const where = args.filter
    ? {
        OR: [
          { url_contains: args.filter },
          { description_contains: args.filter },
        ],
      }
    : {}

  // 1
  const queriedLinkes = await context.db.query.links(
    { where, skip: args.skip, first: args.first, orderBy: args.orderBy },
    `{ id }`,
  )

  // 2
  const countSelectionSet = `
    {
      aggregate {
        count
      }
    }
  `
  const linksConnection = await context.db.query.linksConnection({}, countSelectionSet)

  // 3
  return {
    count: linksConnection.aggregate.count,
    linkIds: queriedLinkes.map(link => link.id),
  }
}
  1. You're first using the provided filtering, ordering and pagination arguments to retrieve a number of Link elements. This is similar to what you did before except that you're now hardcoding the selection set of the query again and only retrieve the id fields of the queried Links. In fact, if you tried to pass info here, the API would throw an error (read this article to understand why).
  2. Next, you're using the linksConnection query from the Prisma database schema to retrieve the total number of Link elements currently stored in the database. You're hardcoding the selection set to return the count of elements which can be retrieved via the aggregate field.
  3. The count can be returned directly. The links that are specified on the Feed type are not yet returned - this will only happen at the next resolver level that you still need to implement. Notice that because you're returning the linkIds from this resolver level, the next resolver in the resolver chain will have access to these.

The last step now is to implement the resolver for the Feed type.

Create a new file inside src/resolvers and call it Feed.js:

touch src/resolvers/Feed.js

Now, add the following code to it:

function links(parent, args, context, info) {
  return context.db.query.links({ where: { id_in: parent.linkIds } }, info)
}

module.exports = {
  links,
}

Here is where the links field from the Feed type actually gets resolved. As you can see, the incoming parent argument is carrying the linkIds which were returned on the previous resolver level.

The last step is to include that new resolver when instantiating the GraphQLServer.

In index.js, first import the Feed resolver at the top of the file:

const Feed = require('./resolvers/Feed')

Then, include it in the resolvers object:

const resolvers = {
  Query,
  Mutation,
  AuthPayload,
  Subscription,
  Feed
}

You can test now the revamped feed query as follows:

query {
  feed {
    count
    links {
      id
      description
      url
    }
  }
}