Transcript
-
Right now, if we go ahead and click into one of our movies, we're still using a UUID generated via the faker call. Where we want a human legible version of the actual movie title to serve in its
-
place as our movie's slug. Now, these slugs are going to need to be unique, so we'll have to check the database to make sure that it doesn't already exist in there. And if it does, we'll have to perform some alteration to it to make it unique.
-
For this, we'll make use of model hooks. Model hooks allow us to define a hook that hooks into various actions performed against a model. For example, whenever we created our users inside of our fake seeder,
-
those users' passwords were hashed. That happened automatically because it's defined as a model hook. We'll take a look at that here in a second. But here's an example directly inside of the documentation.
-
So before a user is saved, it's going to hash the password. So it will check whether or not the password's dirty. If it is, then it will overwrite it with a hashed version of that password.
-
This is actually happening automatically thanks to the auth finder that's being extended and composed into our user model. Here's that hook right here from that auth finder class.
-
Now, their hook is taking into place dynamicness of various projects and reading from configuration files, so it looks a little bit more complex than what we'll actually be creating here today.
-
But overall, we have hooks available before save, after save, before create, after create, update, delete, find, fetch, and paginate.
-
Now, for our use case, we're only going to want our slug to change whenever we're creating the record inside of the database, not whenever we're updating, because that will result in 404s,
-
since we're using this as a unique identifier instead of our URL. So we'll define a hook using the decorator at before, and
-
we want to use before create, so that this hook only runs before we create an actual record, and that will import from Adonis Lucid ORM, and we'll call that as a decorator method.
-
These will be defined as static methods, and we'll also want this method to be async, and we'll call this method slugify. You can give it whatever name you'd like, and
-
this will be provided in an instance of the model itself. So this will be our movie of type movie. Now, a user could attempt to provide it in a slug of their own via a form
-
if we provide it, so we'll take account for that here. So we'll do if movie.slug, so if we already have a movie slug, we'll just go ahead and return out of this hook. Otherwise, we're gonna want to slugify it.
-
So first, we're gonna want to get a slug version of our movie title, const slug equals, and there's a string helper that provides a slug function provided by AdonisJS.
-
So we can import string from @adonisjs, core, helpers, and we'll import from string. Okay, cool, so we'll scroll back down.
-
So we'll do string. and there should be a slug method. And we provide in the value that we want to create a slug from as the first argument, so we'll do movie.title, because that's what we want to use to actually
-
generate out the slug. And then we can provide options as the second argument here. We could specify a replacement, so that if, for example,
-
the title contains a space, we can replace that with something like a hyphen. And then we can specify that we want this to be converted to lowercase.
-
We can get rid of special characters by specifying strict to true there as well. So now we have a URL safe slug representation of our movie's title.
-
Now we need to check whether or not this is unique inside of our database. So we'll do const rows equals await movie.query to get a query builder instance
-
here. We'll select just the slug, since that's all that we care about within this particular query. And then we can do where raw, convert the column to lower,
-
specifying in a placeholder for our column name there, equals the argument. The column name is the first parameter that we want to provide in, so we'll call that slug.
-
And then we want to provide the actual slug value as the check value. And then we can do or where raw, convert that to lower once more, is like the value that we provide in.
-
Provide in slug and slug there as well. And we'll wrap the slug here in backticks, provide in our separator of a hyphen, as specified right up here with our replacement, and
-
then do percent to say anything can come after that. So if we get results back from this particular query, we'll know that the slug that we're generating out here is not unique.
-
And so we'll want to increment whatever the max incrementer is that we're using on that slug. So do const incrementers equals rows, and
-
then we'll reduce through those rows our results in the individual row. And then we'll instantiate the default value here as an empty array.
-
So first we're going to want to split the slug that we got back from our query by the actual slug value that we are wanting to make unique and the separator which is our hyphen.
-
So do const tokens equals row slug dot, convert that to lowercase for safety's sake, and then we'll split that by our actual slug value and
-
our separator of the hyphen. If our tokens length is less than two, so if we don't have a second portion to that split, we'll just go ahead and
-
return back whatever we have for the current result without adding anything new in. Otherwise, we do const increment equals, convert this to a number, and
-
grab the first index from our tokens array like so. So if our increment is a number, so we'll do an inverse check on isNan and
-
provide our increment into there, we can go ahead and push into our result our increment. Now it's not happy about this particular push because our array doesn't necessarily have a type.
-
So let's go ahead and jump back up to our reduce and specify this as a number array. This will temporarily turn into a red squiggly because not all code paths
-
return a result, but we'll take care of that here next by returning result. So if our number is a number, we'll push that into our result,
-
meaning that our incrementers array here is an array of just the numbers that we're using to increment to make our slug instance unique.
-
So if we scroll back down to the end of our method, so we'll set const increment to a ternary check for incrementers.length.
-
So if our incrementers has a length, we'll want to grab the math.max value from our incrementers and use that as our increment and
-
then increment that once more by one, otherwise we'll just use one. So then we'll set movie.slug equal to, provide in our slug,
-
our separator, and our increment to make the value unique. Now, we did miss one thing. We don't want to use the increment if the slug as a whole is already unique inside
-
of our database. We can determine that by checking whether or not we got any rows back from our query here. If we did not, so if not rows length,
-
then we go ahead and set our movie.slug equal to whatever slug it is that we generated from our string.slug method. And then we can return here to exit out of our slugify hook.
-
Okay, so if we did everything correctly here, we should be able to jump into our movie factory and get rid of the slug declaration altogether.
-
Because this should now automatically be taken care of by our new slugify hook. Let's hide our text editor back away, stop our server, clear that out.
-
Node, ace, migration, refresh, hyphen, hyphen, seed to roll back, re-migrate, and then re-seed our database.
-
Hit run there, deep breath, and everything ran successfully, awesome. So let's go ahead and start our server back up, npm run dev, open our browser.
-
And of course, we're gonna get a 404 not found because this UUID no longer exists. Let's go back to our homepage, whoops, localhost 3333, there we go.
-
And let's try to view a new movie. So here we have Jumpin' Jack Flash. You have this little apostrophe here on Jumpin'. And our slug was generated as just Jumpin' Jack Flash.
-
Nice, human readable, and URL safe. If we go back to home, last train to Clarksville's got the same thing going on. Tossing ampersand turning, it's just tossing and turning.
-
So awesome, everything seems to be working A-okay. Let's put it to the test though. So let's bring our terminal back up, stop our server, clear this out. Node, ace, REPL.
-
And let's await load models, okay? Now let's await models movie create. Gonna see if I can remember everything that we have here. So we have a status ID, we'll just set that to one.
-
We have a writer ID, we'll set that to one. Director ID, idle, tossing, and turning. We'll use the exact same title that we're taking a look at right here.
-
And our slugify call should increment the slug that's generated with tossing and
-
turning hyphen one, because it's no longer going to be unique inside of our database. I believe everything else can be omitted from our create call here.
-
So let's go ahead and attempt to run this. All right, looks like it entered in okay. And look at that, we can see the actual slug that was generated out is indeed
-
tossing and turning hyphen one. So everything there worked A-okay.
Generating A Unique Movie Slug With Model Hooks
We'll learn how we can use Model Hooks to generate a unique URL-safe slug based on the movie's title.
Join the Discussion 2 comments
-
After implementing slugify hook, I got an error, when refreshing my database, from the seeders. I use sqlite for database. The refresh and start_seeder passed well, but not the fake_seeder, and I get : Knex: Timeout acquiring a connection. The pool is probably full. Are you missing a .transacting(trx) call? as error.
To fix it (after searching), I added pool: {min: 0, max: 30, idleTimeoutMillis: 600000,} in config/database.ts, just after client: 'better-sqlite3', probably not the best solution.3-
Responding to e4ma
Hi e4ma! I'm not all that familiar with SQLite, but if its default limit is a single connection or two, then that very well could've been the actual cause of your issue. This type of error can also occur due to a lingering SQL operation or a transaction that was left open.
0
-