How to Build Paginating GraphQL Resolvers [with examples]
With the GraphQL type definitions established for the root-level user query, in this lesson we'll modify the resolver function we have to help query for a certain user.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
[00:00 - 00:13] With the type definitions for how we want to query a user already established, we'll now look to modify the resolver function we have to help query a certain user based on an ID. So we'll head to the index file in the user resolver's folder.
[00:14 - 00:31] We'll expect an ID argument to be passed into this resolver function. We'll define the arguments for this resolver as an interface called user_args, which is to contain an ID of type_string, and we'll define this in a types file within the user resolver's folder.
[00:32 - 01:20] In the index file, we'll import the user_args interface, and we'll also import the database and user_interfaces defined in the lib_types file. We'll destruct the ID argument and database object from context, and when this resolver function is to be complete, it will resolve to the user interface.
[01:21 - 01:44] The user resolver is going to be a fairly simple resolver. We'll have a try_catch block, and in the try statement, we'll use Mongo's find1 function to find a user document from the user's collection with which the ID matches the ID argument passed in.
[01:45 - 02:10] We'll throw an error if this user can't be found, and if it is found, we'll simply return this user. In the catch block, we'll capture the error fired and state an error along the lines of failed to query user.
[02:11 - 02:32] The user field is the entry point from our client. As a result, this user_resolver function will be the first function run when someone attempts to query the user field.
[02:33 - 02:51] The other resolver functions that the user object will then depend on, with which we'll set up shortly, will be run pretty much shortly after. Now we've mentioned that certain fields in our user object is to be protected, and only shown when someone is querying their own information.
[02:52 - 03:14] To determine whether a user is authorized to query certain fields, we're going to introduce a new field in our user TypeScript interface called Authorized, that is to be a Boolean. This authorized field is unique, since it's not going to be part of our user document in our Mongo database.
[03:15 - 03:34] Instead, it's only going to be used in our resolver functions to determine whether a user has the authorization to resolve certain fields. Since the user_resolver function will be the first function run when someone queries the user field, we'll determine right here whether the user is authorized.
[03:35 - 03:44] Now how are we going to do this? We're going to see the ID of the viewer making the request, and we're going to see even matches the ID of the user being queried.
[03:45 - 03:58] But how would we get information about the viewer? We've created an authorized function in the libutils file that finds and returns the viewer object based on the cookie and token of the request being made.
[03:59 - 04:17] This authorized function only accepts the database and request objects and returns the viewer. So in our user_resolver function, we have the request object as part of context , so we'll destruct it and set its type to the request interface that will import from express.
[04:18 - 04:39] We'll also import the authorized function from the libutils file. And we'll use it by passing in the database and request objects.
[04:40 - 04:55] We'll then check to see if this viewer object exists. It won't exist if the viewer isn't signed into our application.
[04:56 - 05:13] If it does exist, we'll check to see if the underscore ID field of this viewer object matches that of the user. If it does, this means the person making the query is authorized and will set the authorized field of the user object to true.
[05:14 - 05:30] In our upcoming user_field resolvers that are to be protected, we'll check to see if authorized is true before we resolve to the intended data. Now, we'll need to create resolver functions for certain fields in our user object.
[05:31 - 05:50] We've mentioned before how GraphQL server implementations handle trivial resol vers for us, or in other words, resolver functions as simply return a field from the object passed in with the same name as the resolver field itself. Many of our fields in the user object will be handled as trivial resolvers except for 5.
[05:51 - 06:17] We'll create these resolver functions in the user object. The 5 resolver functions we'll define here are the ID, has wallet, income, book ings, and listings resolver functions.
[06:18 - 06:36] The ID resolver function will be straightforward and simply return the underscore ID field from the user object. Has wallets will be a boolean indicating if the user has connected their Stripe account to be able to accept payments.
[06:37 - 06:50] So we'll use the JavaScript boolean function and place the user wallet ID field within to return a boolean based on the presence of wallet ID. Income is one of the resolver functions that is to be protected.
[06:51 - 07:01] If user.authorized is true, we'll return the sensitive user.income value. Otherwise, we'll return null.
[07:02 - 07:24] Here's a good illustration of also how this prevents cross-site request forgery attacks. An attacker won't be able to impersonate a user to get their sensitive income information since the authorized field is also dependent on a token that the user needs to send from the client browser to the server.
[07:25 - 07:35] The bookings and listings resolver functions are to be a little more complicated but are to be similar to one another. Bookings and listings are to be paginated fields.
[07:36 - 07:53] So we'll take a brief tangent to discuss how we attempt to help conduct pag ination before we begin writing our implementations of the bookings and listings resolvers. First of all, whenever one attempts to return a large list of information to a client, a good practice is to use pagination.
[07:54 - 08:07] Pagination is the process of dividing a large list of data into smaller discrete chunks within pages. Pagination is pretty much used in almost all large-scale applications we use today.
[08:08 - 08:28] There's the simple number pagination, sequential pagination where users able to click next or previous to move from page to page, and in more recent web design , scroll-based pagination, or sometimes known as infinite scroll. That has become popular since this helps show more paginated content as a user scrolls a page.
[08:29 - 08:48] Page at the end of that sentence is in quotations since infinite scroll oft entimes gives the illusion of a single page. At the end of the day, pagination is used to reduce latency because a client doesn't have to wait for a full data dump and only receives the data in small chunks at a time.
[08:49 - 09:00] Though there's different ways to conduct pagination, there's two popular ways one can go about doing it. Offset-based pagination, sometimes known as numbered pages, or cursor-based pag ination.
[09:01 - 09:18] Offset-based pagination is often the easiest to implement since the backend simply retrieves an offset or limit and page values and determines the limit of content to be shown for a certain page. However, there is a certain disadvantage with offset-based pagination.
[09:19 - 09:33] When items are inserted or removed while a user is going through pages, there's a large chance of seeing the same item twice or skipping an additional item. This is due to the concept of boundaries between data within pages and limits.
[09:34 - 09:58] As an example, this can sometimes happen if an item is added to the beginning of a list where a user is already paginating through the list and see the same item as they go to the next page since that item might then satisfy both boundaries. As a result, offset-based pagination may not be ideal for applications where users often find themselves scrolling through pages fairly quickly and items are often being added or deleted.
[09:59 - 10:08] A social media application is a good example of this. Cursor-based pagination uses a cursor to keep track of the data within a set of items.
[10:09 - 10:27] This cursor can just be a reference to the idea of the last object fetched or in more complicated scenarios have a reference to the sorting criteria that has been encoded. From the client perspective, a cursor is simply passed in and the server determines the set of data that is to be returned from this cursor.
[10:28 - 10:49] Since this cursor is more accurate, it helps avoid the pagination disadvantages we can see with offset-based pagination as a user goes from page to page and items are being added or removed. In even more complicated scenarios, there is also relay-style pagination that takes the cursor model but also returns the data in a more particular style.
[10:50 - 11:15] By returning data within edges and nodes and also returning page information data that has a reference often times to when the cursor has an end and whether a previous or next page exists. We'll suggest using cursor-based pagination or relay-style cursor-based pag ination if you intend on building a very large application that will have a large number of pages where a large number of users are using the app.
[11:16 - 11:27] With that said, offset-based pagination is by far the easiest to get started with and is really straightforward with Mongo. So we're going to implement offset-based pagination based on limits and pages.
[11:28 - 11:35] We'll go back to the code and begin with the bookings resolver function. Bookings will receive a limits and page arguments.
[11:36 - 11:54] So we'll define the type of these arguments in the user types file and we'll call it user bookings arcs and state that it expects a limit to the page. We'll call this interface user bookings data and say it is to have a total field of type number and the type of page.
[11:55 - 12:08] We'll call this interface and state that it expects a limit and page fields that are numbers. Limit describes how many data objects to show per page and page describes the page number the user wants to view.
[12:09 - 12:43] We'll call this interface user bookings data and say it is to have a total field of type number and a result field, which is to be an array of the booking interface for a booking collection in the database with which will import from the lib types file. We'll import the user bookings arcs and user bookings data interfaces in our user resolver's index file.
[12:44 - 13:12] And in our bookings resolver we'll declare the parameters of our resolver function. We'll state that the resolver is to be asynchronous since it is to make a database call and when successfully resolved it will resolve to the user book ings data interface or no.
[13:13 - 13:27] Our bookings resolver function will have a try catch statements. Recall that we've said the bookings field is to be a protected field and we won 't want an unauthorized user from seeing the bookings made for another user.
[13:28 - 13:44] So with that said we'll place an if statement and say if the user is not authorized the bookings resolver will return no. If the user is authorized we'll first construct a data object that initializes the data will update and then return.
[13:45 - 14:12] We'll say total is zero and the result is an empty array. Then what we're going to do is run the database find method to find all documents in the bookings collection where the underscore ID field is in the user bookings array.
[14:13 - 14:30] Recall in the user document exists a bookings field that contains the IDs of bookings that the user has booked. The in operator allows us to find all the actual booking documents where the IDs of these documents are in the user bookings array.
[14:31 - 14:43] We'll assign a disfined statement to a cursor. The cursor here isn't related to the cursor we've talked about with cursor based pagination and more along the lines of how Mongo labels this as a cursor.
[14:44 - 15:00] A Mongo cursor has skip and limit functions that allow us to easily offset the number of data objects within a certain page. The cursor skip function allows us to skip a certain number of documents.
[15:01 - 15:18] If page is graded in zero subtracted by one and skip the n number of elements by multiplying it with the limits. So in this case if page is ever zero we've defaulted to zero but naturally we 'll start with page being one.
[15:19 - 15:49] So as an example if page is one and limits was ten we'll subtract one with one which is zero, zero multiplied by ten is zero so we don't skip anything which makes sense since we're in the very first page. But if page was two and limit was ten we subtract one from two and then multiplied by ten with which the value will then be ten which makes sense because we will then skip the first ten values and start with the tenth document.
[15:50 - 16:07] And this can go on and on and on keeping the limit as ten the next page was started twenty then thirty then forty and so on. Now to control the number of elements to show we'll use the cursor limit function with which we'll say limit by the limit argument passed in.
[16:08 - 16:28] So if the limit is ten we'll only get ten documents in total. Finally we'll say the data dot total field is equal to cursor dot count which is how Mongo allows us to get the total count of the initial query while ignoring the limit modifier.
[16:29 - 17:00] And for data result we'll ensure it is to be an array by stating two array and finally we'll return data. In the catch we'll conform to our usual error handling by catching and throwing an error along the lines of failed to query user bookings.
[17:01 - 17:19] The listings resolver function will be practically identical to the bookings resolver function except for it's going to return listings from the listings collection and it won't be a protected field. With that said let's create the types we'll need for the listings resolver in the user types file.
[17:20 - 17:48] The ARGS with which we'll create an interface called user listing ARGS will have a limit and page fields of type number. And the data to be returned from the resolver we'll call the user listings data interface will have a total and result field where total is a number and result is an array of the listing document interface with which will import from the lib types file.
[17:49 - 18:31] We could probably have a single interface for the two arguments interfaces for both the bookings and listing fields and probably have the ability to extend a third interface for the data to be returned for the shared total field but we 'll be explicit and individually define all these different types. In the user resolver's index file will import the user listings ARGS and user listings data interfaces. We'll copy the bookings resolver over for the listings resolver and make the minor differences between them.
[18:32 - 18:52] We'll reference the correct types. We'll find documents from the listings collection and reference the IDs in user listings, not bookings, remove the authorized field check and update the error statements.
[18:53 - 19:27] And that's it. We'll now be able to query for the user from the client and we can test this out in GraphQL Playground. We can grab the ID of a user from my profile page and if I was to place it in the query I'll be able to query my user information, my name, my avatar and so on.
[19:28 - 19:51] Notice however that the income and bookings fields return null. This is because through GraphQL Playground we aren't specifying or passing a cookie as well as the appropriate token to actually query the user for a certain viewer. We'll be able to verify this when we start building on the client side of our application in the next lesson.
[19:52 - 20:10] We've built resolver functions primarily for the user GraphQL object that get resolved from the user query field. And these resolver functions involve the individual fields within the user object such as the ID, the has wallet field, income, bookings and listings.
[20:11 - 20:24] And we also managed to actually create the resolver function for the root query level user field. The bookings and listings field within the user object actually get resolved to custom GraphQL objects we've created.
[20:25 - 20:35] And these are the bookings GraphQL object and the listings GraphQL object. We haven't built any resolver functions for any fields within these objects.
[20:36 - 20:47] When we build the user page in the client, in the beginning we're going to focus primarily on the user specific information such as the user's name, avatar, contact and has wallet. And income fields.
[20:48 - 21:06] But in our next upcoming lesson after that, we're going to attempt to build the functionality to query for the bookings and listings of a certain user. And at that point we're going to notice something will go wrong because at that moment in time we're going to attempt to query for an ID of the listing, the title, description, etc.
[21:07 - 21:28] As well as that for the booking and we'll notice an error because we haven't built any resolver functions to resolve a listing documents under score ID field to the ID field as well as that for the booking as well as some other fields as well. So this is just a note to keep in mind that we're going to attempt to query fields from these listing and booking objects when we build the user page.
[21:29 - 21:35] And at that point we'll notice that we have to build some additional resolver functions and we'll come back and do it at that moment in time.