00:02
- So when it comes to our search, let's ease our way into things, starting with a basic search endpoint, and then we'll add additional features on top of it.
00:12
Now we are gonna be adding a search endpoint for both our courses and our lessons, but since we already have a query set up here for our courses via our index method, pretty much matching what we're going to need
00:22
to return back to our users at least, let's go ahead and start with our course search endpoint. Let's go ahead and just put our search just below our index method. So we'll do async search,
00:31
and we'll go ahead and grab our request and our organization. Out of our HTTP context, we can go ahead and grab everything that we have from our index method
00:40
and plop it now into our search method, and we'll alter it from here. And to start with, let's jump into our validation. So for our index method, we're just validating that we have a page
00:49
and per page for the pagination logic. What we want to do in addition to that for our search is allow our filters to be passed through. So do export const course,
00:58
and we'll call this search validator equals vine compile, vine object. And we can go ahead and jump up to our course paginating validator, copy that page and per page
01:08
and paste it into here as well. These are pretty basic rules, so there's no need to worry about duplicating this logic here. And plus, you might also want your per page
01:17
to vary between the index method and your search endpoint, specifically for the max and min values that you accept there. In addition to that, we're going to allow our users to specify a name filter.
01:27
And again, we're going to start relatively basic with these filters. So we're just going to assert that this is an optional string, allowing our users to either specify a name pattern
01:36
that will then perform a lookup on or not provide a name at all, in which case we'll omit a name filter. Then we'll have our status ID, that'll be of type vine number,
01:46
and that too will be optional. Then we'll do difficulty ID of type vine number, optional, and lastly, we have our access level ID. And you guessed it,
01:56
that too is an optional vine number. If you want to, you could validate that these IDs exist inside of your database. Since we're searching, not creating, that's not overly needed.
02:06
If the users pass in an ID that doesn't exist, they just won't get any results back. So I'm going to go ahead and omit it here for simplicity sake. And that'll also give us headway to make these validations that we're doing here
02:15
reusable in the future as we expand upon their functionality. Let's go ahead and save our validator there, jump back into our courses controller and replace this validator with our course search validator.
02:25
Now, in addition to our page and per page, we can spread in our filters and then off of our query, we can use these handy dandy if methods to check and see if we have filters
02:35
for each one of these four that we've defined validations for. So we can start with our name here, we can check and see whether or not we have that. If we do, then we want to perform a query,
02:45
doing a query where the name matches our filters name. And notice that our filters name is optional, since we've made that rule
02:54
within our validation optional as well. So TypeScript doesn't officially know that we have a value there, despite this being within an if statement. The way that this if statement works is
03:04
if the condition passed here into the first argument is truthy, then the second argument will be called. If it's falsy, then there is a third argument
03:13
that we can call here as well to apply a different query to our query builder. So in essence, it is an if else check. If our condition's true, then we're going to perform the first callback
03:23
that we define. If it's false, then the second callback that we define will be run. Since we're just applying a filter or not applying a filter, we don't need that else. And since this if works based off of callbacks,
03:33
TypeScript doesn't officially know that we actually do have a name here, but we can inform it that we have indeed check that and we're good to go by asserting that we have a value with an exclamation point.
03:42
So if we have a filter name, we're going to do a where statement, checking for that name. Right now though, we have this as a strict equals. The name that we're filtering by
03:51
needs to match the exact name of the course in order for it to return back. That is a little strict for a search endpoint. So what we can do is switch this to a like
04:00
in this check instead. So we can do where like, or you can add in a third middle argument to the where statement with like
04:09
as the where statements operator. We'll go ahead and use the official where like method. And then we need to add the parentheses around our filter's name.
04:18
So we'll wrap this in back ticks, add the value in to the string and then add parentheses after it, as well as before it, so that our like check essentially does a contains
04:28
or includes check for our filter name. Okay, so let's break down our where like here. So essentially we're saying that our course's name should match the filter name that we're providing in here.
04:38
By suffixing a percent sign at the end of our filter name, we're telling our where like that the course's name should start with our filter name
04:46
in order for the course to match and be returned back. By adding a percent to the beginning of our name, we're specifying that in order for the course's name
04:55
to match our filter name, the filter name should end with the course's name. When you provide both a percent at the beginning and end, it's doing a begins with, ends with,
05:05
or in otherwise contains our filter's name. So the filter name that we're providing in here can match anywhere inside of our course's name.
05:14
And if it does, then we're gonna get that course back within the results of our query. Now, I believe all but Postgres will do a case insensitive likeness check when you do this.
05:23
Postgres, however, has a separate I like method or operator that needs to be specified in order for this to be case insensitive. So if we do a where I like with Postgres,
05:33
now the filter name and the course's name lookup will be done case insensitive. If you leave the I out of the like with Postgres, then it will be case sensitive.
05:41
So let's leave that as case insensitive there, and that should do it for our name filter for now. So let's do another if for our filter's status ID,
05:50
do a query, query.where, and since this one's number-based, we're going to do a strict equality, and we wanna check where the course's status ID
05:59
matches our filter's status ID. And again, we need to assert that that does indeed actually have a value since we're using our if callback. We can do another if, filters.
06:08
And let's do our difficulty ID next, add in another query, query.where, the difficulty ID matches our filter's difficulty ID,
06:18
and again, assert that that has a value. And lastly, we have our filter's access level ID, add in another query, query.where,
06:26
the access level ID of the course matches our filter's access level ID, and again, assert that. And that does it for our filtering on our course query.
06:36
If the user has not provided any one of these filters that we have, then the callback function will not be applied to the underlying query, hence not applying our filter.
06:46
All right, so let's next jump into our route definitions, and I'm gonna go ahead and put this down at the bottom of our route group, and we'll do router.
06:54
And we're going to do this as a POST because some of the more advanced filtering that we're going to allow via this endpoint in the future is a little bit difficult to do via a query string,
07:03
and it's a lot easier to implement and do and send via a request body, which GET does not allow, but POST, PUT, PATCH, and DELETE do.
07:12
So POST is the most applicable here for us. We're gonna put this inside of a search namespace with our endpoints, and then we can call this our courses search.
07:21
So we'll use our courses controller and its search method for this as search courses. And now that we have the route defined, let's jump back into our courses controller real quick
07:31
because we also have our base URL that we're applying to the pagination, so we need to update that to match. So we switch this to search courses, and we should now be good to test this out.
07:41
So let's jump back in the hopscotch. Let's create a brand new folder for this inside of our API folder, and we'll call this folder search. And inside of the search folder, we'll add a new request,
07:51
and we'll call this search courses. All right, and we want this to point to /search/courses. Now, when we send the request out without a body,
08:01
which is how we have it right now, what we should get back is an unfiltered response of our courses, which should match the get courses pagination result that we have from our index method.
08:11
So that's just a meta with a data, and it returned back everything that we have for our courses thus far as a paginated list, five per page.
08:20
So when we send this out without a request body, we, oh, actually get a 404. And, oh, yeah, that's sending a get. We do need to switch that to a post,
08:29
and let's send this out now. There we go. Now we get back the same identical meta and data that we get back in our index method
08:38
for our get courses paginated endpoint. The only difference is our first page URL, last page URL, next page URL use our search endpoint as its underlying paginated URLs.
08:48
So let's next add in a JSON body to this, and we'll add our object in, and let's do a name search. I think we created several pagination tests,
08:57
so we can do pagination tests as our name search. And if the name of the course matches this pagination tests in any way, whether it starts with, ends with,
09:07
or contains it, we should get the response back. So we have 11 total matches, and if we scroll through these, we can see that we have pagination test one, which starts with our pagination tests.
09:17
Then if we scroll through that, we have two, three, four, so on and so forth. So that looks good. What if we get rid of just pagination and just send tests? When we send that off,
09:26
we get back the exact same results set because tests still matches our pagination test one string, two, three, so on and so forth. If we just send one, send this off,
09:36
we get back three total responses because that matches pagination test one. I think we also have 10, which yep, we do right there. And then did we create an 11th?
09:46
We sure did. So all of those match our one search, so we're getting those back appropriately. If we send pagination as a lowercase string, we get back the full set of 11
09:56
because we're doing a case insensitive search. Awesome. That then takes us to our difficulty status and access levels. So we just add those in as access level ID
10:05
and we can search one, send that off. I think we created all of these pagination tests with the same IDs. So we get back 11 again. If we switch this to two, we should get back zero.
10:14
If we get rid of our name filter, see if we get any back there. We do, we get back two. So my first course and then how to watch TV.
10:22
How to watch TV has a difficulty and status of one and four. So let's see what first one here has. It's two and two.
10:29
So if we send off a difficulty ID of one, we should get back just one result there, which is how to watch TV, which we do.
10:37
And if we send off a status ID of two, we should get back one, which is just the my first course. Awesome. So our basic filtering is now set up and ready to go.
10:47
What we're going to do in the next lesson is expand upon this with our name or string based filtering to take it from just doing a contains check to instead allowing us to also specify
10:57
the type of check we wanna do, whether it starts with, ends with, contains, equals,