Generating A Unique Movie Slug With Model Hooks
In this lesson, we'll learn how we can use Model Hooks to generate a unique URL-safe slug based on the movie's title.
- Author
- Tom Gobich
- Published
- Mar 15
- Duration
- 7m 59s
Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.
Adocasts
Burlington, KY
Transcript
Generating A Unique Movie Slug With Model Hooks
-
[MUSIC]
-
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.
-
Introduction
-
Fundamentals
-
2.0Routes and How To Create Them5m 23s
-
2.1Rendering a View for a Route6m 29s
-
2.2Linking Between Routes7m 51s
-
2.3Loading A Movie Using Route Parameters9m 17s
-
2.4Validating Route Parameters6m 6s
-
2.5Vite and Our Assets6m 38s
-
2.6Setting Up Tailwind CSS9m 5s
-
2.7Reading and Supporting Markdown Content4m 32s
-
2.8Listing Movies from their Markdown Files8m 51s
-
2.9Extracting Reusable Code with Services7m 4s
-
2.10Cleaning Up Routes with Controllers4m 52s
-
2.11Defining A Structure for our Movie using Models9m 38s
-
2.12Singleton Services and the Idea of Caching6m 11s
-
2.13Environment Variables and their Validation4m 16s
-
2.14Improved Caching with Redis10m 44s
-
2.15Deleting Items and Flushing our Redis Cache6m 46s
-
2.16Quick Start Apps with Custom Starter Kits6m 28s
-
2.17Easy Imports with NodeJS Subpath Imports8m 40s
-
-
Building Views with EdgeJS
-
3.0EdgeJS Templating Basics8m 49s
-
3.1HTML Attribute and Class Utilities6m 9s
-
3.2Making A Reusable Movie Card Component10m 24s
-
3.3Component Tags, State, and Props4m 53s
-
3.4Use Slots To Make A Button Component6m 56s
-
3.5Extracting A Layout Component5m 13s
-
3.6State vs Share Data Flow2m 59s
-
3.7Share vs Global Data Flow6m 7s
-
3.8Form Basics and CSRF Protection6m 13s
-
3.9HTTP Method Spoofing HTML Forms3m 3s
-
3.10Easy SVG Icons with Edge Iconify7m 57s
-
-
Database and Lucid ORM Basics
-
4.0Configuring Lucid and our Database Connection4m 3s
-
4.1Understanding our Database Schema9m 35s
-
4.2Introducing and Defining Database Migrations18m 35s
-
4.3The Flow of Migrations8m 28s
-
4.4Introducing Lucid Models5m 43s
-
4.5Defining Our Models6m 49s
-
4.6The Basics of CRUD11m 56s
-
4.7Defining Required Data with Seeders11m 11s
-
4.8Stubbing Fake Data with Model Factories13m 48s
-
4.9Querying Our Movies with the Query Builder15m 30s
-
4.10Unmapped and Computed Model Properties3m 24s
-
4.11Altering Tables with Migrations7m 6s
-
4.12Adding A Profile Model, Migration, Factory, and Controller2m 57s
-
4.13SQL Parameters and Injection Protection9m 19s
-
4.14Reusable Query Statements with Model Query Scopes8m 11s
-
4.15Tapping into Model Factory States9m 15s
-
4.16Querying Recently Released and Coming Soon Movies4m 59s
-
4.17Generating A Unique Movie Slug With Model Hooks7m 59s
-
-
Lucid ORM Relationships
-
5.0Defining One to One Relationships Within Lucid Models5m 49s
-
5.1Model Factory Relationships2m 54s
-
5.2Querying Relationships and Eager Vs Lazy Loading5m 17s
-
5.3Cascading and Deleting Model Relationships5m 16s
-
5.4Defining One to Many Relationships with Lucid Models6m 56s
-
5.5Seeding Movies with One to Many Model Factory Relationships5m 24s
-
5.6Listing A Director's Movies with Relationship Existence Queries8m 41s
-
5.7Listing and Counting a Writer's Movies8m 41s
-
5.8Using Eager and Lazy Loading to Load A Movie's Writer and Director5m 18s
-
5.9Defining Many-To-Many Relationships and Pivot Columns9m 48s
-
5.10Many-To-Many Model Factory Relationships4m 50s
-
5.11A Deep Dive Into Relationship CRUD with Models18m 5s
-
5.12How To Create Factory Relationships from a Pool of Data13m 55s
-
5.13How To Query, Sort, and Filter by Pivot Table Data9m 47s
-
-
Working With Forms
-
6.0Accepting Form Data12m 15s
-
6.1Validating Form Data with VineJS9m 29s
-
6.2Displaying Validation Errors and Validating from our Request7m 16s
-
6.3Reusing Old Form Values After A Validation Error2m 3s
-
6.4Creating An EdgeJS Form Input Component5m 28s
-
6.5Creating A Login Form and Validator5m 1s
-
6.6How To Create A Custom VineJS Validation Rule9m 7s
-
-
Authentication & Middleware
-
7.0The Flow of Middleware7m 49s
-
7.1Authenticating A Newly Registered User4m 14s
-
7.2Checking For and Populating an Authenticated User2m 10s
-
Logging Out An Authenticated User2m 24s
-
Logging In An Existing User6m 54s
-
Remembering A User's Authenticated Session6m 55s
-
Protecting Routes with Auth, Guest, and Admin Middleware5m 36s
-
-
Filtering and Paginating Queries
-
Creating A Movie List Page3m 43s
-
Filtering A Query By Pattern Likeness7m 9s
-
Filtering Our List by Movie Status5m 47s
-
How To Apply A Dynamic Sort Filter To Your Query7m 12s
-
Joining SQL Tables To Order By A Related Column4m 49s
-
Validating Query String Filter Values7m 23s
-
How To Paginate Filtered Query Results9m 15s
-
Pagination First, Last, Next, and Previous Buttons4m 2s
-
Join The Discussion! (2 Comments)
Please sign in or sign up for free to join in on the dicussion.
e4ma
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.
Please sign in or sign up for free to reply
tomgobich
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.
Please sign in or sign up for free to reply