00:02
- Okay, so let's give our users a little bit more power with our string-based filtering by allowing them to specify whether they want that filter
00:12
to start with, end with, contain, equal, not contain, not start with, not equal, all that fun stuff. While at the same time, we also want them to be able
00:22
to still do just a basic string-based search. And for these more advanced filters, let's package them together in their own file so that we have both the validation rule
00:31
and the where statement that we're going to end up applying to our query builder together so that if we need to change them down the road, we have both places that we would need to change them right there.
00:41
So within our app directory, let's right-click, new file, and let's put these inside of a folder called filters, and we'll call this first one string-filter, and .ts.
00:51
Right now, all that we're doing for our string filter is just a vine string optional. So if a name is provided, vine is just asserting that it is a type of string.
01:01
If it's not provided, then we're gonna get back undefined. So we still want to keep this vine rule at the end of the day, but what we wanna do on top of that is now allow an object-based structure
01:11
to specify the search type that we want to perform as well. So let's do const string-filter rule equals vine,
01:19
and let's import that from vine, .union, and then provide in our union conditionals. For these conditionals, we can do vine union,
01:28
and then there's an if as well as an else that we can utilize to build a condition statement for this. So first, let's do our if, and this is gonna give us back the value
01:37
that's being validated inside of this callback. What we need to do is return a conditional check with this value. So if the type of this value is an object,
01:47
then the validation schema that we ultimately want to apply to it is the type vine object, and we'll build that out further here momentarily. So that's our if check.
01:56
If this comes back falsy, then what we wanna do is vine union else and do our basic vine string optional check.
02:06
So if we send up a string type, this union will get matched, and if we send up an object, this union will get matched. If we send up neither, if we send up something like an array,
02:15
then we'll end up getting a validation exception because it doesn't match our structure. For our vine object, what we wanna do is accept a text property inside of this
02:24
of type vine string. You might decide to make this optional. I'm gonna leave it as required because if you're setting up an object, then I assume that you're going to wanna match something or nothing.
02:34
So we'll leave that as required there, and then we also want to allow them to specify their search type. To simplify our lookups, let's go ahead and create an enum for this.
02:43
So let's right-click our enums folder, new file, and we can call this text_search_types.ts, and we'll define our enum as text_search_types,
02:53
and we'll have one with starts with, set that equal to a camel case, starts with. The value structure here doesn't really matter too much.
03:01
We're just essentially using it to do an equality check and keep everything nice and consistent with this. So we'll do ends with, then we'll have includes,
03:09
and you can kind of see how we're going to be able to use the I like statement to build this out conditionally based off of which search type is being provided.
03:19
So we'll do not starts with, equals not, starts with, not, ends with, not, ends with,
03:27
and then lastly, we can do a strict equals check and not equals check as well. Once we have those, we can export default
03:36
our text search types, jump back into our string filter, and now for our search type property within our object, we can do a vine and use our enum to verify
03:46
that the search type provided matches one of our enum value. Okay, I'm gonna go ahead and give this a save so that everything formats for me and we'll walk through it.
03:55
So we have a rule here defined with a union. We're doing union instead of union of types because it'll give us a little bit more control over how we're checking things. Union of types is an alternative option though,
04:04
and it accepts a little bit of a more simplistic option set. The array that we pass into our union is a conditional check where the first is our if statement,
04:14
and the if statement accepts in a comeback with a condition. If this condition returns back truthy, then the second argument to our if will get run and applied to this validation rule.
04:24
If our if condition is falsy, then we're gonna move on to the second item in our array or the first index, which is our union else,
04:33
that will apply in the basic string optional validation to this rule. So at the end of the day, we're just asserting that what's provided here is either going to be an object with a text property
04:43
of type string and a search type property matching one of our text search type enum values, or it's just gonna be a string or undefined. And we'll be able to see that a little bit better
04:53
whenever we actually make use of this rule to get back our validated data. All right, so let's package this together in an object. In this case, I'm gonna do a class called string filter
05:02
so that it's all one entity. So do export default class string filter, and we can just do a static get for our rule
05:11
that returns back our string filter rule. And then from there, we'll have another static method to build out the actual filter statement for this.
05:21
And the first argument that we're going to accept in here is our query builder. That's gonna be of type model query builder contract from AdonisJS LucidTypes model.
05:31
This accepts in the type of model that we want the query builder to be for. So what we wanna do is add a generic to our build
05:39
that we'll call T to serve for that. Now, that's not going to satisfy the type requirements of our model query builder.
05:47
So we also need to assert that the T generic is going to extend the Lucid model
05:54
from AdonisJS LucidTypes model to satisfy our generic. At this point, our squiggly is just because I've put this on a line break. But in addition to our query,
06:02
we also want to accept in the column name that the where statement should be applied to, that'll be a type string, as well as the filter rule
06:10
that we want to apply the where statement with. So we'll use infer from find JS and add in the type of our string filter rule
06:19
to get the type of the union rule that we've defined right up here. So now that we have the inferred type of this, we can hover over our filter to see indeed that this is going to either be of type string
06:29
or object with a text and search type property or undefined. So within this build statement, what we wanna do is first,
06:37
we can simply eliminate our undefined out of the equation by checking to see if we don't have a filter, then we're just gonna go ahead and return
06:44
and not apply any where statements to the query passed in. If the type of our filter is a string then, since this is the easiest where statement to do,
06:54
we can go ahead and return query.where and we wanna do our I like again to match what we currently have inside of our courses controller search method.
07:02
The column name specified either ends with the filter string being passed through or starts with or contains the filter string passed through.
07:12
And that's all that we need to do there. The last thing that we need to do is build out our object based filtering. So we have our text as a type of string and we have our search type specifying the type of search
07:22
that we should be performing. So let's first add in a helper method, so static to get the operator that we should be using for the applicable search type that was passed through.
07:32
So this will be of type text search types and we can do a switch statement here for our search type, since there's really only three possible operators that we'll be returning back.
07:41
So we have our case text search types dot ends with, then we have our case text search types dot starts with, and then we also have includes
07:51
that we want to include in this statement. So includes any one of those three, we want to return an I like. If you're using Postgres,
07:59
again, I think MySQL and the other drivers do case and sensitive with like by default, but I like is definitely a Postgres feature. So if it starts with, ends with or includes,
08:09
we want to do a likeness check. If I'm going to copy all three of these, we have the not variant of any one of these three. So I'm gonna use command option up arrow
08:19
to get multi cursors and prefix all three of those with not. Oops, looks like I named that one not start with. Let me go fix that real quick. Let's get our S on there. There we go. All right.
08:28
So if it not ends with, not starts with or not includes, then we want to just flip the I like to a not I like. If it's not any of those six, then it's going to be either equals or not equals.
08:38
So we can do text search types dot not equals. And if it's not equals, then we can return a not equals check. And if it's none of those seven, then we can go ahead and default
08:48
to returning back an equality check. So that gets us our operator. So I'm going to do const operator equals this dot get operator
08:57
and pass in our filter search type into there. Then we need to get the appropriate value string. So where should we add the percent signs
09:05
based off of the starts with, ends with or include? Should it be at the beginning of our string, end of our string or on both sides? Or in a case of our equality check,
09:14
should it just be the plain filter value? So for that, let's add in one more static helper method called get value that also accepts in our search type
09:23
of type text search types and also our text string value. And again, let's utilize a switch once more for that search type.
09:32
And we can do a case text search types dot ends with or text search type dot not ends with.
09:41
Then we want to return back with the percent wildcard before our text value so that we tell either the like or not I like
09:49
to do an ends with check for our string. Then we want to do our starts with. So text search types dot starts with
09:56
or text search types dot not starts with. And we want to do the opposite. We want to start with our text value
10:04
and end it with a wildcard to tell our I like or not I like that the course match should start with our text value in order for it to match. Then we have our includes.
10:13
So text search types dot includes or text search types dot not includes. And we want to return back with the wildcard percent sign
10:22
on both sides of our text value so that the search match can either start with, end with or an otherwise contain the text value passed in.
10:31
If it's none of those six, then we are again doing it in a quality check. And this time we just need to return back that text value as is. Since it will not be an I like,
10:41
but rather an equals or not equals check. All right, so that's our get value. Let's scroll back up and do const value equals this dot get value.
10:50
Pass our filter search type in as well as our filter text. Once we have both the operator and the value, we are ready to build out our where statement. So we'll do query dot where.
11:00
And remember in the last lesson, we discussed how where can accept three arguments, the column name and the value or the column name operator and the value.
11:09
So we're going to utilize the three argument version to specify the column name, our operator and the value to build out our final query where statement.
11:18
And that is our string filter. All that's left now is to actually apply this into our validation. So let's jump back into our validator and replace our vine string optional
11:28
with our string filter and import that dot rule. And that will apply this rule into our validator. And then we of course need to update our actual query
11:38
to utilize the filter. So what I would like to do is be able to apply all of these filters in a separate method to the query itself.
11:46
So let's go ahead and extract this query out into an action. So if we jump back into our text editor, we can do node ace make action.
11:54
And let's see what folder we need to put this inside of here. I think it should be called courses. So we'll do courses slash, and we'll call this search courses, hit enter to create that.
12:04
And we can go ahead and clear our terminal out and jump back into our text editor. And let's copy just the query here. So our organization related down through the paginate and give that a copy,
12:14
jump into our search courses and let's go and paste it into our handle method. And I have a bunch of red squiggly, that's fine. Let's just ignore them for now. Within our parameters, we need to accept in our organization
12:24
so that we can query off of it, as well as our filters that we can infer from the type of our course search validator.
12:33
Extract both of those out of our handle method. So organization and filters, and the number of our red squiggly should now go away. We can go ahead and updates our page and per page
12:42
to filters dot page and default that to one if it's not provided. And then again, filters dot per page and default that to whatever you'd like
12:51
to get back per page there, if that's not provided. Again, since we're just testing here, I'm gonna keep it low since we're just working with minimal test data so that we assure that we're gonna get back multiple pages there.
13:01
But the real application, you definitely probably want that to be more than five. All right, now let's extract our filtering out into a separate method. So I'm gonna call this static, make it private.
13:10
And I'm gonna call this apply filters. And this will accept in our query. And since we're coming through our relationship here, the type of this query is gonna be
13:17
a has many query builder contract type of course course. So let's go ahead and just select that, give that a copy. And let's create a helper type up here called query
13:27
and set that equal to what we've copied there, command dot, add all missing imports. All right, so now within our query, we can assert that that's going to be a type of query.
13:36
And then we can accept in our filters of type filters. Well, actually we can extract the helper out for that as well from our infer. So just do type filters equals
13:46
infer type of course search validator. And we can set our filters equal to filters there. Okay, for our apply filters,
13:52
what we want to do is cut out each of these four if statements that we have and paste them within here. And then we can use multi lines here
14:02
with command option and down or up, and then just prefix each one of those with query dot. For our string based filter here for the name, let's go ahead and just mix that out
14:12
and replace this now with our string filter. And let's build the query statement, pass the query builder into it. The has many query builder contract that our current query type is,
14:22
is a subordinate of the model query builder contract. So that will satisfy its type. And then we also want to specify the column name that we want this filter to apply to, which is our course name,
14:32
followed by the actual name filter from our filters dot name value. Lastly, we are getting our red squigglies here for the query because queries are already declared
14:40
in a scope via the parameter from this function. So since we're gonna be getting rid of all of these anyway, as we build our more advanced queries, I'm just gonna replace them all with Q, just like that.
14:50
Okay, then let's apply our filters. So we can add a single if statement back into here. If filters is a type of object or just truthy in any way,
15:00
then we go ahead and apply in our query using this dot apply filters, pass the query into there as the first argument and our filters in as the second argument.
15:10
Give it a save and that should do it for our action. Let's jump back into our courses controller now and replace everything that we have right there
15:18
with our search courses action call, called a handle method. And we want to pass in our organization and our filters. Now we also need the page and per page
15:28
to go into there now. So let's just package that all up into a variable called filters there so that we also get those into our action call.
15:37
If you want to, you can also put the base URL into the action as well, just except in like a route identifier, route per RAM argument. But I think it's okay to also leave that out here in the controller.
15:46
We are getting a red squiggly though. I think we might not be returning back from our search controller. We sure are not. So we want to return the result of our query there as well.
15:55
All right, that should make our base URL happy there. Perfect. All right, so we should have everything set up. Let's jump back into Hopscotch so that we can test it out.
16:03
So let's send our previous test from the last lesson out and we get back the exact same result. That looks good. The name filter is the main one that we've worked with here
16:13
in this lesson. So let's work with that. So we had our pagination tests from the original that I think we had 11 results for. So still looks good there.
16:22
If we just send off a basic string based search, if we switch this basic string to an object, add our text in and let's do pagination tests again
16:31
with a search type. And you know what? I need to be wrapping those in quotes for JSON. There we go. And we'll set our search type to starts with.
16:41
We should get back 11 results there again because all of these tests start with pagination tests. All right, that looks good. What about ends with? We should get back zero
16:49
because none of them start with that, which we do. And then we have our includes. We send that off, we get back the 11 again. We have also not starts with.
16:59
Should get back probably the remainder of the 14 that we created, which would be three. So that also looks good. Those definitely don't start with pagination tests. If we do not ends with,
17:09
we should get back the entire result set because none of them start with that or none of them end with that. But if we send off one, remember that we had three that end with one.
17:18
So we should get back 11 or 12 there. Let's switch that to ends with so that we can purify that. So ends with, get back two. All right, 10 does not end with one.
17:28
So that would result in us just getting back two. We got our pagination test one and then we would also have 11. Okay, that was my brain that messed up there, not our query. So yeah, everything's looking good here.
17:38
What if we send off a non-valid search type? So we'll do ends with not real, send that off. The selected search type is invalid.
17:46
So cool, we still get back our validation error there. What if we send null for our text? So we'll do ends with again there, send that off.
17:55
The text field must be defined. All right, so our required check works A-okay there. And what if we send back a number for the name? Send that off, must be of type string.
18:05
And what if we send nothing at all? So if we send with an empty request body, we get back everything. So everything still works there with our optional validation rule there too.
18:14
So awesome, everything seems to be working perfect so far with our advanced string-based filtering. Let's next add an advanced number filtering. We're gonna allow just a single number to be passed through
18:24
as well as an array of numbers similar to how we just did with our strings.