Building a GraphQL Resolver For Filtered and Sorted Results
With the root-level `listings` field prepared in our GraphQL API, we'll construct the resolver function for this field to attempt to query for a list of listings from the "listings" collection in our database.
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 - 01:04] Now that we have the listings GraphQL field established, less updated so it retrieves a list of listings from the listings collection in our database when queried. We'll first update the GraphQL type definitions of this listings field before implementing the resolver function. The listings field will have at this moment three input arguments. It would have a filter argument that would be of an enum type we'll define shortly called listings filter. When this filter argument is applied, it will allow the field to return a list of listings that is sorted based on the filter applied. It would have a limit argument which is to be an integer to determine the number of listings to return for a certain query and it will have a page argument of type integer which is to determine the page or in other words the sub list of listings to be returned. So if the limit is five, are we going to return the first five or the next five, etc. And this is what the page argument is going to specify.
[01:05 - 02:28] We've seen how this offset based pagination works because we've introduced the capability to query for a list of listings and bookings within a user object. The listings field in the root query object is going to be very similar to the listings field in the user object except for some minor differences we'll see shortly. And when this field is to be resolved , it will return a listings object type that is non-null. This listings object type is something we've set up before and is to contain essentially the total number of items returned and the list of listings that is to be returned. Next, we'll set up the type definition for the listings filter enum. There are two kinds of filters will allow the client to specify for how it wants the collection of listings to be returned from the listings field. We'll have a filter that will allow the client to query listings from the lowest price to the highest price. We'll call this enum value price low to high. Similarly, the client will also be able to pass in a filter that allows us to filter listings from the highest price to the lowest. We'll call this enum value price high to low.
[02:29 - 03:41] I think we've mentioned this before, but we often define graph QL enum values as capital letters as sort of a best practice. With the graph QL type definitions of our listings field prepared, we'll now look to update the resolver function. And the first thing we'll do is we'll define the type definitions from the TypeScript perspective of the expected arguments and return value of the listings resolver. And we'll define these types in the types file within the listing resolver's folder. We'll first define the shape of arguments for this listings field, with which we'll call listings args. There are three current expected arguments like we've just mentioned, filter, limit, and page. Limit and page are to be numbers, while filter is to be a defined set of values or in other words an enum. So we'll declare a string based TypeScript enum to reflect this listings filter. And the two potential values would be price low to high and price high to low.
[03:42 - 04:14] And we'll create the shape of the data that is expected to be returned from the listings field. We'll call this interface listings data. And it is expected to have a total field of type number and a result field, which is to be a list of listings. We have the definition of a single listing defined in our lib types file. So we'll import it and use it.
[04:15 - 05:02] We'll now head over to the listing resolver's map and look to create the res olver functionality for the listings field. We'll first import the listings args and listings data interfaces from the corresponding type file. And we'll state the arguments and expected return for the listings resolver field. The root object is undefined. It'll expect a filter limit and page arguments. We'll need access to the database object available in context.
[05:03 - 07:06] And it's to return a promise when resolved would resolve to an object that conforms to the listings data shape. As we've mentioned before, the listings field in the root query object is going to be very similar to the listings field from the user graph QL object we've created before. The resolver function will simply return a paginated list of listings from the listings collection in our database. Since this resolver function is to be very similar to the one we've already created in the user graph QL object, we couldn't try and have the functionality here in a shared location where both resolver functions can use. But we won't worry about that and just instead look to replicate what we did in the user object, paste it over here and make the necessary changes. So with that said, we'll head over to the user resolver's map, copy the functionality of the listings resolver here and paste it for our root query object in the listing resolver's map. Some quick changes we'll make will declare the type of the data we're going to construct and return as listings data. And now something we did for the user graph QL object was when we searched for listings, we used the dollar in operator to find all the listings where the underscore ID field was in the array we specified here, user.listings . And this is how we basically said, give me all the listings for a certain user. However, for this particular listings field, we don't need to do that. We simply want to listings for any user. So with that said, we'll remove the option we've specified and simply find all the listings from the listings collection for our MongoDB cursor. And lastly, we'll update the error message if ever to occur as simply failed to query listings with the error of potentially caught and shown as well.
[07:07 - 08:09] At this moment, we're using the cursor limit and skip options to query for a certain limit of listings for a certain page, which is great. However, we haven't used the filter option that we've specified can be an argument for this field. Filter is an enum of one of two values, pricing low to high and pricing high to low. And what we want is when the client passes a filter, of pricing low to high, we want to query for listings that sorted from the lowest price to the highest price and vice versa for the other filter. So to facilitate these two options, we'll create if statements to determine what we'll do in each condition. One if statement for when the filter is set as pricing low to high and the other when for when the filter is set as pricing high to low. And we'll now need to import this listings filter interface from the corresponding types file.
[08:10 - 09:22] If you take a look at the types file, I usually conform to and I think most people do, to defining the enum descriptions in the singular term. So instead of listings filters, it'll be listings filter to determine which kind of filter can be passed. It will be sure to basically update it as well for our listing listings, our interface. Type script complains. And it tells us we probably meant price low to high instead of pricing low to high. And that's how we've defined it. So we'll be sure to update the mistake here. Cool. So now at this moment, we have two if statements and we want to handle how to sort our cursor or essentially how we want to sort the collection of information we've returned based on what we want to do for each of these filters. The cursor is just a pointer to the collection of documents that we've pulled or found with the find method. And we want to sort our cursor. Well, MongoDB gives us the option to run a sort method in our cursor.
[09:23 - 10:52] And what we'll want is to redefine the value of the cursor with a new set of listings or a new collection of listing documents based on the sort criteria we'll specify. The sort method allows us to define the order or the position of the documents in our found collection. And it accepts an options object where we can specify the properties we'll want the sort to occur in. In our instance, we want to sort based on the price of a listing document. And when it comes to sorting on a field from the highest to lowest value or the lowest to highest value, or in other words, in ascending or descending order, all we'll need to do is either pass a value of negative one or one by passing a value of negative one where interested in sorting our collection in descending order from the highest to the lowest value. The value of one denotes we're interested in sorting our collection in ascending order from the lowest to the highest value. Since price low to high refers to the ascending condition, we'll place one here. And for the below if statements will do the same, but provide a value of negative one. I'm noticing we have the same value for our filter here, price low to high, price low to high. The if statement below should say price high to low instead.
[10:53 - 13:37] And that's it for now. Let's test querying this field in our GraphQL playground before we move to working on the client. We'll query for the root listings field. We'll inquire for the total and result sub fields. That's part of the listings object. And for the result, all we'll ask for is the ID title and price of each listing we want to query. Now, at this moment , what we've said is each of the arguments we've defined for this field is required filter, limit and page. So if we try to make a query now, we'll get an error. So let's provide some values for these arguments. We'll say the filter will be pricing or price low to high. We'll want a limit of four listings to be returned and for the first page only. And with our query made successfully, we'll see the information that we expect to be returned. The total field here gives us the total amount of listings in our collection while the result arrays essentially the list of listings we've looked for. And in this case, we said we want four listings within a single page or the first page. So we get the first four results. Notice the price values. Since we've asked or specified a filter from low to high for the price, we get the price or the listings sorted in ascending order. So the first listing object has the lowest price, 1918, and slowly increments up to the fourth one we can see here. Now, if we were to change the page, they'll essentially continue from that fourth one. So we'll see 4051 and so on. Now, if I was to change the filter to price high to low, we'll get the listing documents or objects now sorted in descending order from the price field. So the first listing object we get has the highest price 24 842 and it goes down from there. Now, a good question might be why is the filter argument here required? It could be an optional field or optional argument. And that comes down to the use case in our use case, whenever we query for listings, whether it's in the home page or the listings page, we'll always query with the filter specified. So in the home page, would automatically specify we want the listings sorted from highest price to lowest price. And in the listings page, we'll automatically initially specify a filter from low to high, and we'll give the user the option to toggle that to hide a low. If we needed to query for listings where this price wasn't filtered, we can set this filter argument as optional, and we can only apply it when necessary. But in our case, we're going to be applying this filter everywhere. So we've made it a required argument.
[13:38 - 13:51] Fantastic. And we have our field available to us now. In the next lesson, we'll start working on the home page. And eventually, we'll look to query for the fourth for the four highest price listings available.