How to Store New Bookings and Resolve Overlapping Bookings
In this lesson, we'll continue from the previous lesson and look to see how we can update the bookingsIndex of a listing document with the dates that have recently been booked by a tenant.
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:18] In the last lesson, we got primarily almost everything within the create booking mutation resolver function complete except for preparing the resolve bookings index utility function. That will be responsible in producing a new bookings index object for the listing that's being booked.
[00:19 - 00:29] We could have this function set up in a different file, but we'll keep it in this file. So above and outside of our resolver's map, we'll construct the functions and the parameters it is to accept.
[00:30 - 00:46] It would accept a bookings index object and it would accept a check in date and check out date parameters of type string. And when resolved, it should return a map or an object that matched the type of the bookings index object being passed in.
[00:47 - 00:57] We'll need a type to specify the shape of the bookings index parameter. If we recall, the bookings index object will look something like this.
[00:58 - 01:14] We'll share this in the lesson manuscript and at the end of the day, the book ings index is our way for how we hope to handle dates and the booking of dates for listings. It's just an object that contains a set of nested objects with key value pairs.
[01:15 - 01:32] In the root level, the index will refer to the year the booking is made. Then the child of that will be the month the booking is made and then finally further down, the child will be the day in which at this moment, a true bullion will be applied for the days that have already been booked.
[01:33 - 01:49] The JavaScript function for getting a month starts at zero to represent January , which is why in this example here, zero references the month of January. So for this bookings index object, let's quickly talk about the dates that have already been booked.
[01:50 - 02:02] The first day of January 2019 has been booked. So the year would have the index of 2019, the month of zero would reference that of January and a one would reference the day.
[02:03 - 02:07] And that says true. So we booked this date or it's already been booked by someone else.
[02:08 - 02:12] And it's the same for the second day of the month. Once again, this is practically the same for all the months.
[02:13 - 02:31] So May 31, 2019, May would be 04 within the context of the JavaScript month and 31st year is true and so on. So let's assume that we have some new check in and check out dates for a booking that's just been made to this listing.
[02:32 - 02:48] Assume check in is December 1. Check out date is December 3, for example, of 2019.
[02:49 - 03:05] So if we wanted to update this bookings index object, essentially we'll get a new bookings index object that will look just like this. However, we'll have a new key value pair here to reference the month of December 11.
[03:06 - 03:24] And then here we'll have some new values to specify the days that have been booked. So in this context, it would be 01 true and 02 true and 03 true.
[03:25 - 03:37] Remember if a user checks in one day and checks out a few days later, they're going to be booking all the days in between as well. So this is essentially what we want to do for our function.
[03:38 - 03:54] We want to produce a new index object that simply has the new key value pairs to reference the fact that the dates, the new dates have been booked. There's another thing we want to check for as well, the fact that the user somehow hasn't booked over days that have already been booked.
[03:55 - 04:10] So for example, assume a check in of January 1 is made and a check out of January 3 is specified. So we do the client side validation and check for this.
[04:11 - 04:21] We need to do a server side fallback check as well. So in this context, we need to go through our bookings index map, see that the first of January has already been booked and throw an error.
[04:22 - 04:34] At this moment in time, we need to tell the actual request that this can't be done. So how can we implement this solution?
[04:35 - 04:44] Well, it's actually going to be fairly straightforward. And what we simply can do is specify a cursor or a date cursor for the check in dates.
[04:45 - 05:07] And then we can simply loop through from the check in dates up to the check out dates and then verify if those dates don't exist, simply create them in the index and specify them as true. So for example, if the user was to check in December 1 and wanted to check out December 3, we'll specify a cursor to begin at 11th, the 11th month, 2019.
[05:08 - 05:15] We'll look through our bookings index and say, does 2019 11 exist? If it doesn't, we'll create it.
[05:16 - 05:23] And then we'll do another check to say later on, does the day of 01 exist in this context? If not, we'll create it, right?
[05:24 - 05:30] And then the moment we see that it already exists, it basically tells us that it's already been booked. So we throw the error.
[05:31 - 05:39] This would be a little bit more prevalent or understandable when we actually write the code. So let's actually go about doing that.
[05:40 - 05:59] Before we begin, however, it'll be helpful to have a type for the bookings index object that's being passed in as an argument. If we take a look at the lib types file within our server project, we'll see we 've already prepared interfaces for the bookings index year and bookings index month objects.
[06:00 - 06:18] The bookings index object itself would essentially be the parent of these key value pairs, where the values will be the bookings index year objects. So let's create and export a bookings index interface to represent this.
[06:19 - 06:44] And in our bookings resolver file, we'll import the bookings index interface we 've just created from the lib types file and state that the type of the bookings index argument in our function is to be bookings index as well as the return type of the function. To begin, we'll specify a date cursor, which will be the beginning of our process.
[06:45 - 07:01] And that will essentially be the check-in dates, but the date object of this check-in date string. We'll also specify a check-out date object, which will be the date object of the actual check-out date string.
[07:02 - 07:28] And we'll declare a new variable called new bookings index that at this moment will be the actual bookings index, but we'll look to update this bookings index in our function and finally return it in the end. Here's where we want to make sure we update the bookings index or the new book ings index before we return it.
[07:29 - 07:44] Since we'll want to ensure that we update multiple days within the bookings index object for as long as the number of days being booked, we'll want to do some sort of iteration, some sort of repetitive work. And this is where a while loop can be helpful.
[07:45 - 08:04] The while loop is sort of the control flow statement that will allow the code we write to be executed over and over given a valid Boolean condition. So we'll say while the date cursor is still less than or equal to check-outs, do what we intend to do.
[08:05 - 08:11] Here is where we can start to update this new bookings index object. And we can begin with the date cursor.
[08:12 - 08:27] Now it's important to sort of control the moment in which we actually break out of this loop. So what we're going to do is we're going to say while date cursor is less than or equal to check-out, we're going to increment the date cursor by one day at a time.
[08:28 - 08:37] And we're going to do this with sort of the format of what we've done before is we'll convert it to milliseconds with the get time function. We'll add the number of milliseconds in a day.
[08:38 - 08:51] And then we'll use the date constructor again to bring it back to the date object format. This would allow us to sort of iterate and update the bookings index object day by day while the date cursor is still less than or equal to check-out.
[08:52 - 09:00] So the moment we hit the check-out date will be the last moment. And when it gets incremented once more again, we break out of the loop.
[09:01 - 09:15] Okay, so now let's begin to update the bookings index object for the days that are valid that are less than or equal to check-outs. Now the good news is the bookings index object that we have is an object or a hash.
[09:16 - 09:26] When it comes to updating hashes or objects or reading them, it can be done practically instantly. All we need to do is simply provide the key that we're looking for.
[09:27 - 09:34] Our object is broken down to sort of three steps. So we need to sort of compile information for the date cursor for each of these steps.
[09:35 - 09:44] In other words, we need to sort of compile what the year is, what the month is, and what the day is. And we can use three functions to help us here.
[09:45 - 09:58] We'll use the UTC or get UTC full year function to get the year for this particular day cursor. We could use the get UTC month function to get the month of this day cursor.
[09:59 - 10:13] And we can use the get UTC date function to get the day of this particular date cursor. That UTC fully year method returns the date according to universal time.
[10:14 - 10:29] So for example, in this case, it'll be 2019, 2019, or 2020. The get UTC month function returns the month according to universal time as a zero based value.
[10:30 - 10:35] So zero indicates the first month and the increments from there. 1, 2, 3, 4.
[10:36 - 10:47] And the example we've shown before in our example index, we sort of referenced that the month will be zero of zero or zero one. I believe with the get UTC month, it will just simply be either zero, one, two, three.
[10:48 - 10:57] But the concept is the same. And the get UTC date method returns the day of the month in the specified date according to universal time.
[10:58 - 11:13] So this is simply from, you know, one to 31 or 30, etc. So now with these values available for the existing date cursor, we can access and sort of manipulate the new bookings index object.
[11:14 - 11:27] So for example, in the beginning, we can check to say, hey, does the index or new bookings index have a value for this year? If it doesn't, let's construct an object.
[11:28 - 11:38] We will repeat that to say something similar and say, hey, does the new book ings index object also have a value for the month of this year? If not, let's create an object.
[11:39 - 11:55] And then finally, we'll say, hey, does this particular new bookings index object have a day for this month and year? If not, we'll simply add the true Boolean, which at this moment in time is us saying that this day is booked.
[11:56 - 12:02] However, this is where we'll have an else statement. We'll say, if this value does exist, it means it's already been booked.
[12:03 - 12:21] So we'll throw an error and say selected dates can't overlap dates that have already been booked. Let's walk through some cases that reference how this actually would work.
[12:22 - 12:30] So we'll paste that example we've seen before. We'll reference the month based on what the get UTC month function returns.
[12:31 - 12:43] So let's say zero here, the day will just be one, two, four, 31, five, one, six , 20. And we'll put this on the side.
[12:44 - 12:49] So it can be, there we go. So it'd be easier to understand and recognize.
[12:50 - 13:06] So assume the user in this context wanted to check in on, we use the examples before of December 1st, 2019 and December and wanted to check out December 3rd, 2019. So the date cursor will begin at December 1st.
[13:07 - 13:13] It will construct the year, month and day for this particular date. So the get UTC fully year will be 2019.
[13:14 - 13:29] The month would be 11 and the day would be one, right? In this case, our bookings index object will first check to say, hey, do we have a value for the key that is new bookings index 2019?
[13:30 - 13:32] We do. So we just skip this if statement.
[13:33 - 13:39] Next, we look to see if the month. We say, do we have a value for the new bookings index 2019 11?
[13:40 - 13:47] We don't. So let's construct an object for it.
[13:48 - 13:54] And then finally, we check to say, do we have a day for this particular month? So do we have 11, 1?
[13:55 - 14:01] We don't. So simply, let's specify 1 here and provide the value of true, which basically means this has been booked.
[14:02 - 14:11] We get to the end of our while loop and then we increment the date cursor by one day. So then we start to look at number two and we repeat the process once again.
[14:12 - 14:16] And finally, this will become true. And this is true.
[14:17 - 14:28] And then we'll end at the last day, which is three checkouts, which will be true as well. And the moment the date cursor becomes the day after, we exit the while loop and we return the new bookings index, right?
[14:29 - 14:36] So that was a test case to talk about the happy path. Now, let's talk about a test case in which an error could occur.
[14:37 - 14:56] So assume in this context, assume this is the starting bookings index and the user wanted to book the dates between, let's say, Jun, first and Jun, third, right? So in the very beginning, we're going to provide to get the year, which is 2019 .
[14:57 - 15:01] The UTC month here would be five. It's always one down.
[15:02 - 15:07] And the first day is one. It's first going to check if the year exists, 2019, it does exist.
[15:08 - 15:11] It's going to check if the month exists, it does exist. We go to the next step.
[15:12 - 15:23] And here is going to say, does a value already exist for 2019, five and one, it does. So we're automatically going to throw an error and we're going to exit this function.
[15:24 - 15:35] And essentially, we're going to say the selected dates can't overlap dates that have already been booked. This is a pretty simple solution, but it does what we need to do for our use case.
[15:36 - 15:41] Now there's a lot of questions and discussions that can come from this, right? One being performance.
[15:42 - 15:46] Is this a somewhat performance solution? And from what we've gathered, it's pretty good.
[15:47 - 16:05] The reason being is when the size of the input increases, so let's say the number of days between checkouts, check in and check out increase, our solution will only increase linearly based on the fact of that, right? We simply have a loop that extends over the length of time between the beginning and the end.
[16:06 - 16:10] So it's linearly proportional. Oftentimes this is sort of recognized as linear time.
[16:11 - 16:25] It isn't quadratically increasing over time, which is fairly decent. However, there are bigger discussions that could come from the fact that what if a user picks a date in 2019 for check in and picks a check out date of 2025?
[16:26 - 16:34] Technically, the way we have our loop, we're going to increment from that date all the way through. And if there's nobody who's done bookings in between, we're just going to keep going.
[16:35 - 16:42] Now a very simple solution to this would be sort of limiting the length of time the user can check in and check out. That's one solution.
[16:43 - 16:54] Another solution will be limiting the furthest time in the future that a user can either check in or check out. So for example, we can say listings can only be booked within the year.
[16:55 - 17:03] Now we're not going to address those solutions. We do provide some sort of client side check if I'm not mistaken, but we want to do the server side validation for now.
[17:04 - 17:21] We may provide some documentation in the lesson manuscript that talks about some of these edge cases. But other than that, this is essentially our function for actually resolving a brand new booking's index based on the check in and check out dates that a user books for a listing.
[17:22 - 17:30] And that's pretty much it actually. Now I do want to reiterate that this isn't a perfect solution and we don't want it to be.
[17:31 - 17:45] My co-instructured myself deliberately didn't want to take and discuss dates and date handling and have that as a core piece of the course since we wanted to cover a lot of different other things. So with that being said, is there something you think can be improved here?
[17:46 - 17:48] There's something that we might have forgotten. Do let us know.
[17:49 - 18:09] At the end of the day, the main takeaway within this module is to see how we will able to set this up in the server, but more on the client side and how we'll actually create the mutations to actually book the dates in our calendars. If you take a look below, we do also want to pass in, we see an error here.
[18:10 - 18:27] Bookings index year is not assignable to bookings index. This probably means that we didn't specify the correct type for the bookings index field for the listing documents in our TypeScript code and that is because we were referencing the child object, bookings index year.
[18:28 - 18:40] So we here will actually reference the newly created bookings index objects. So we made a mistake before, but that was before we actually started working on this area and now we'll resolve what we've done and this will be resolved.
[18:41 - 19:00] Let me take a look below what we do at the very end of this statement is we update the bookings index of the listing documents in our database. There's one other thing we should probably handle and that is essentially the resolver functions within the booking GraphQL object.
[19:01 - 19:29] We specified resolver functions for the ID and listing functions before when we needed them in the clients when we were querying listing information and the bookings within a listing, right? Now we'll take a very quick tangent and look through our client code and we set this up a while before, but when we set up the listing query, there was a moment we said we wanted to get information for the bookings of a certain listing and for this booking, we wanted to get the tenant of the booking.
[19:30 - 19:45] We wanted to get the ID of the person who made the booking, the name and the avatar. The reason being was in our listing bookings child component within the listing page, we wanted to show information about this tenant, the ID and the avatar.
[19:46 - 20:14] And if I recall correctly, I believe I may have used mock data to sort of reference how this might have worked. However, this would actually have caused an error or have broken because at this moment in time in our server code, we actually don't have a resolver function for the tenant field and we can't rely on the trivial resolver that Apollo server might do for us because the types of what is in the database is different than what we expect from the API.
[20:15 - 20:37] In the database, we expect the tenants of a booking to be an ID and this is the ID of a user. And if we recall, we mentioned this practically almost at the beginning of this part of the course, we've said for the user documents, we store the IDs as strings, not as object ID, purely for the fact that we simply store the ID retrieved from Google OAuth .
[20:38 - 20:58] So in this context, in the booking documents, the tenant field in our database references, the string ID of the user. But if you look at our graph QL API, and the type definitions file, what we want the tenant field to actually be for a booking graph QL object is the actual user object, right?
[20:59 - 21:12] So since the types don't match, we can't rely on a trivial resolver. We need to do something very similar to what we have here in this listing field within booking, and we need to specify an explicit resolver function for the tenant field.
[21:13 - 21:17] We haven't done that before. We then need to because we didn't actually have the data available.
[21:18 - 21:28] But once we start to implement this booking functionality, we're going to now provide the capability for people to make bookings. And with that being said, we're going to update information in our collections.
[21:29 - 21:49] And when we query for a listing where bookings are made, we'll need to ensure that this tenant resolver returns a user information, not just the ID of the user. In this tenant resolver function, we'll look to access the booking data object that will be passed from the parent resolver functions.
[21:50 - 22:01] There'll be no arguments we expect here. And we'll look to access the database object available in context for all our resolver functions.
[22:02 - 22:15] And similar to what we have in the above, we'll do something very similar. But for the user's collection, we'll say, we'll want to find a single document in the user's collection of our database.
[22:16 - 22:28] We'll use the find one method from Mongo. And we'll say, find me the user document where the ID is the tenant value within this booking object.
[22:29 - 22:48] And that's pretty much it. At this moment in time, if we were to have this data available in database and we query for this in the client, the tenant field within the booking object won't return the ID or the string ID, it will return the user document in which the ID satisfies what's being kept in the database.
[22:49 - 23:00] In the next coming lessons, we'll now look to begin the client side work to facilitate what we need to do to actually conduct a booking on a listing. Good job so far.