Building a GraphQL Resolver For Specific Fields
With the root-level `listing` field prepared in our GraphQL API, we'll construct the resolver function for this field to attempt to query for the appropriate listing 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 with a single-time purchase.
[00:00 - 00:17] With our listing query field prepared, we'll construct the resolver for this field to perform the query for the appropriate listing from the database. Similar to how our user field queried for a user with a certain ID, the listing field will query for a certain listing based on the ID provided.
[00:18 - 00:34] As a result, we'll update the field in the type definitions schema and state it expects an argument of ID of type GraphQL ID and should be defined. In addition, this field when resolved should return the appropriate listing object.
[00:35 - 00:56] The listing interface here has already been created before and it has fields for the individual listing such as its title, description, host, etc. We'll now modify our query listing resolver to state that it is to accept an ID input from our client and return a listing object when resolved.
[00:57 - 01:13] So first, we'll construct the interface of the expected arguments for this field in a types file kept within the listing folder. We'll create this listing ARG's interface and state an ID field of type string.
[01:14 - 01:26] In our listing resolvers file, we'll import a few things we'll need for our res olver function. We'll first import the database and listing interfaces that have been defined earlier from our lib types file.
[01:27 - 01:42] We'll import the recently created listing ARG's interface from the types file adjacent to this file. We'll now implement the functionality to query for a listing from the database when the listing resolver function is run.
[01:43 - 02:05] We'll define the resolver function as an async function. We'll need access to the ID argument passed in at the database object available as context, so we'll define those arguments.
[02:06 - 02:17] When the listing function is to be complete, it should return a promise that when resolved is an object of type listing. Our function here will be fairly straightforward.
[02:18 - 02:36] We'll use the find one method that Mongo provides to find a listing document where the underscore ID field is the object ID representation of the ID argument passed in. If this listing document doesn't exist, we'll throw a new error.
[02:37 - 03:00] If it does exist, we'll return the listing document. We'll have all this be kept in a try block and in our catch statement, catch the error that is to be returned if it does get thrown and have it thrown as well within a new error message.
[03:01 - 03:17] We'll need to import the object ID function from the MongoDB library. The listing object contains a series of fields where we'll need to define explicit resolver functions for a certain number of them.
[03:18 - 03:34] In the last lesson, we mentioned that the bookings field within a listing object should be authorized only to the user making a query. With that being said, when we define the resolver for the booking field here, we'll need to check if the listing field query is authorized.
[03:35 - 03:50] We'll follow a very similar format to what we did for the user module and simply get the viewer details with the authorized function we've established in the libutels file. And we'll have an if statement to check if the viewer ID matches that of listing hosts.
[03:51 - 04:12] We'll know the viewer is requesting their own listing and will set an authorized field in the listing object to true. The first thing we'll do is add the authorized field in the listing TypeScript interface in the lib types file and state that it is of type boolean.
[04:13 - 04:31] In our listing resolvers file, we'll import the authorized function from the libutels file. We'll also import the request interface from Express.
[04:32 - 04:47] We'll access the request object available as part of context in all our resol vers. In the listing resolver, we'll run the authorized function and pass in the database and request objects it expects.
[04:48 - 05:03] And we'll do this after the listing document has already been found. We'll then state that if the viewer ID matches that of the listing host field, we'll set the authorized value of the listing object to true.
[05:04 - 05:17] Note that the host field within the listing object isn't an object but is the ID of the host of the listing. Or in other words, the ID of the user that owns the listing.
[05:18 - 05:34] The bookings field resolver within the listing will now run after this parent listing resolver function is complete. At that moment, we'll check if the listing authorized field is true before we can return the bookings information for the listing.
[05:35 - 06:00] We've mentioned this a little while back but as a reminder, the underscore ID field for the user document in our Mongo database is of type string and out of type object ID. MongoDB natively creates an object ID type for the underscore ID fields but we 've resorted to have the user document underscore ID field be a string since we simply capture whatever Google OAuth returns to us.
[06:01 - 06:17] The listing host field is the same string representation of this ID. We'll now create explicit resolver functions for the fields in the listing object that we want to resolve differently than the value being kept in the database.
[06:18 - 06:34] We already have the ID field resolved to the string representation when queried from the client. The host field in the database is a string ID when the client queries for this field will want the client to receive object information of the host.
[06:35 - 07:26] Since we want to resolve the host field to a user object value, we'll import the user interface from the lib types file to begin. We'll define a resolver function for the host field in the listing object and we'll use MongoDB's find one method to find the host from the listing host ID property.
[07:27 - 07:39] Now recall that listing dot host is a string. The underscore ID fields in the user collection are strings which is why we're not using the object ID representation here.
[07:40 - 07:59] Since there's only one request that can fail for this field, we want to use a try catch block and we'll only throw the only error of not finding the host if that error is to occur. In one of our earlier modules, we highlighted how the dates of bookings that have been made to listings is captured in our application.
[08:00 - 08:24] We've stated that a listing document is to have a bookings index field that is a key value representation of the dates that can be booked. On the client side, we'll want this key value object returned since on the client will look to control which dates a user can book and that is to say if a booking has been made on a certain date, we'll want to prevent another user from booking on that same date.
[08:25 - 08:44] Ideally, we'll want this object directly returned as is but unfortunately this JSON object is an unstructured data set where we don't know what the values are going to be. As a result, what we've done earlier is said the field return from the GraphQL API is of type string.
[08:45 - 09:12] So we'll need to create a resolver for this bookings index field that simply stringifies the bookings index object to a string. On the client, we'll receive this string from the field and parse it to then get the object structure we're looking for.
[09:13 - 09:29] Finally, we'll create the resolver function for the bookings field that is to be a paginated list of bookings that have been made for a certain listing. The structure of how we create this resolver will be almost identical to the paginated booking field in the user object.
[09:30 - 09:52] It'll be offset based pagination where the field accepts a limit and page arguments and will return data that contains the total amount of results and a result array of the list within a certain page. The bookings will be authorized for a viewer viewing their own listing and will use Mongo's cursor capability to find the paginated list of document objects and the total amount.
[09:53 - 10:12] Do go back to the previous module if you're interested in learning in more detail the steps taken here but for now we'll simply copy and change the context of how we create the bookings resolver field in the listing object. First, we'll define the arguments and data types for the bookings field in the listings types file.
[10:13 - 10:59] We'll import the booking interface from the lib types file and define the listing booking args and listing booking data interfaces. Limit and page are said to be the two arguments of type number and the data returned from the bookings field is to be an object that has a total field of type number and a result of an array of type booking.
[11:00 - 11:34] Since the bookings field being queried is in the plural sense where querying multiple booking objects will rename these interfaces to say listing booking args and listing booking data. We'll import the newly created interfaces in our listing resolvers file and we'll look to now copy the bookings resolver function from the user object and simply change the context of a few certain things.
[11:35 - 11:55] We'll say the root object passed in is listing, the argument type is listing booking args, the returned promise should resolve to listing booking's data or null. We're checking for the authorized field from the listing object and the data constructed is of type listing booking's data.
[11:56 - 12:27] The dollar in operator will use the listing booking's array and in our catch error statements will say if the errors ever occur, it's failed to query for listing bookings. And that's it for now. All the functions we've defined here, bookings, bookings index, host ID are explicit resolver functions for how we want these fields to be returned.
[12:28 - 12:47] The other fields within the listing object are to be trivially resolved such as the title, description, image, etc. At this moment, if we were to query the fields for a certain listing, we should get the data resolved from our API. So let's give this a check and verify this.
[12:48 - 13:03] We'll first head over to GraphQL Playground. Now to query for a certain listing , we'll need to pass in the ID argument for that listing. So let's grab the string ID of a certain listing from our MongoDB Atlas dashboard with our seed data.
[13:04 - 13:13] And we'll pass that ID value and look to query for all the fields in a listing. This is to verify all fields are being resolved appropriately.
[13:14 - 13:38] We'll query for the ID, the title, the description, the image, the host and we 'll get the ID for the host, the type, the address, the city, the bookings field and we'll say we want four bookings for the first page and try to get the total value. The bookings index, the price and the number of guests.
[13:39 - 13:48] And when we run our query, we see an error. It says expected a value of type listing type and received apartment.
[13:49 - 13:58] And this is from the listing type field. If we were to comment this field out and run our query again, our query works as intended.
[13:59 - 14:08] Let's see why this error could be happening. In our GraphQL API schema, we've defined the listing type enum as apartment and house and capital letters.
[14:09 - 14:23] In our TypeScript definition, we define the values of our listing type enum as apartment and house as well, but in shorthand format. Enums can behave differently depending on the GraphQL server implementation being done.
[14:24 - 14:40] However, in our instance, the GraphQL enum structure is to be mapped to the TypeScript enum values here. In essence, we have an issue where the capital letter format isn't being matched to the lower case format in this interface, which is being used to seed our database.
[14:41 - 14:59] The error comes from the fact that GraphQL states that, hey, the returned data doesn't match the GraphQL schema contract. There's two ways of resolving this. One is to have the values in our enum definition in our TypeScript server be in capital letters and to most likely reseed our database.
[15:00 - 15:17] Or we can change our GraphQL enum definition here and state the apartment and house are in lower case format. There is a documentation from folks in the GraphQL community that state that en ums in GraphQL schemas should be defined in capital letters as best practice.
[15:18 - 15:31] So we'll go with the former approach and attempt to have our enum definition in capital letters. This will require us to update the values of the listing type interface in the TypeScript definition to be in capital letters.
[15:32 - 15:57] We'll then clear our database with the clear script and reseed it with the seed script to ensure all the type fields in our listing documents are to have the capital letter from the database. Now, when we head back to GraphQL Playground and rerun our query, our query for the root to level listing field is now complete.
[15:58 - 16:04] We can begin building the client UI for the listing page in the next lesson.