Pre-save hooks in mongoose.js

Little Bits of 1’s and 0's
5 min readDec 10, 2017

--

Mongoose.js is a popular ODM for Express and MongoDB. It offers a great deal of power and flexibility and ease of use in defining “schemas”, models. and model methods in your application. One of the neat, but possibly confusing functionalities is its Middleware, also known as “pre” and “post” hooks that tie particular functions to particular lifecycle and query events. This middleware is defined on the schema level and can modify the query or the document itself as it executed. Today, we’re specifically looking at the Pre-save hook.

It might be obvious, but a pre-save hook is middleware that is executed when a document is saved. Let’s take a closer look at it.

Again, this middleware is a function defined on the schema-level and is invoked with two arguments: the event trigger (as a string) and the callback function that is triggered for that particular event. The callback itself takes in an argument of a function, which we typically call next , and when invoked — advances the document/query to the next awaiting middleware (This is pretty much the exact same pattern you encounter when using Express middleware). Invoking this function is critical, mongoose will not auto-advance if this function is not called. Also, even though it may be obvious, keep in mind that next() will immediately proceed to the next step in the process and will not automatically wait for asynchronous functions to be resolved. We will look at one possible strategy to handle this, later on. BUT, speaking of this….

This, that, and the other things….

One thing to keep in mind here is that this inside of a pre-save hook is the document that is about to be saved. It has has a few helpful properties that we can use in our pre-save hook. For example, if we want to execute the middleware ONLY when this document is new, we can check the isNew property of the document that’s about to be saved.

Another thing we can do is if there are embedded subdocuments (documents saved within field within a MongoDB document) we can call the methods defined within those fields. In order to take a look at that, we can examine an app I’m currently building — a webapp to track my progress using the Wendlers 5/3/1 powerlifting program.

Here, we’ve defined our programSchema with a cycles field that contains an array of cycle subdocs.

What we’re going to do is generate by pushing them directly into the cycles field using the attributes we have defined in our cycle schema. NOTE: That calling push() to create subdocs is a SYNCHRONOUS process. This is in contrast to create() which is an ASYNCHRONOUS process.

This code is inside the body of a for loop that executes 12 times to represent the 12 cycles in wendler’s program. For this application, I want to also use these training maxes to generate workouts within a particular cycle. However, in the structure of this application, workouts are stored in their own collection and are this created when the generateWorkouts method is called. What I want is an array of reference IDs stored on a particular cycle, so I can populate those later. However, this can lead into a problem if I want to use the create method to make these workouts rather than push the documents into the these fields directly.

Observant readers me realize that this is an asynchronous function. So, if we do the following:

this will be advanced to the next middleware likely before having those async functions resolved.

So, a conundrum — I have a synchronous for loop firing a bunch of asynchronous functions and I only want to advance to the next middleware when all those functions are resolved. What’s one to do? There are multiple ways to solve this, but a relatively easy way is with the magic of closures.

Stepping back into javascript land for a bit, closures are a way to give functions access to variables that are declared outside of this function. In our workout example:

This may be a bit confusing but the important thing is the completedCycleCounter that is declared outside of the function. What I have here is a counter that gets incremented in the then statement, and whenever that counter reaches four, it invokes a callback passed into the generateWorkouts method. This is an example of a closure!

But what is that callback being executed? Let’s step back a bit…

Instead of invoking the next() function immediately after the conclusion of the for loop, we have another counter that gets incremented every time that callback within generateWorkouts gets invoked. This way, we ensure that next() is ONLY invoked when all our asynchronous functions have resolved.

And that, folks, is a short intro to mongoose pre-save hooks. I hope this was informative and inspires you to explore this handy ODM even further!

--

--