One of my favorite underused mongoose features is plugins. Plugins are the answer to several mongoose feature requests. As a matter of fact, I just released my first mongoose plugin, mongoose-autopopulate, last week. Still, the term "plugin" is nebulous and intimidating to many users. In this article, I'll explain what mongoose plugins are all about and provide some examples for things you can do with mongoose plugins.
Overview
First, we're going to play a very exciting game called "what is a mongoose plugin and what can it do?" A mongoose plugin is a single JavaScript function that takes two parameters: the schema, and an optional options object.
function MyPlugin(schema, options) {}
The options
parameter lets you define tuneable parameters
for your plugin. But the more interesting parameter is the
schema.
The core purpose of plugins is to manipulate your schemas. Plugins don't touch models or documents, so they can't execute queries or write to the database directly. What can they do? Here's a few examples of useful things they can do:
- Add middleware (AKA pre, post hooks)
function ProfileQueryPlugin(schema) {
schema.pre('find', function() {
this.start = Date.now();
});
schema.post('find', function() {
console.log('Query time: ' + (Date.now() - this.start));
});
}
- Add paths to the schema
function CreatedAtPlugin(schema) {
schema.path('createdAt', Date).default(Date.now);
}
- Tweak schema options
function InvertDefaultToJSONOptionsPlugin(schema) {
schema.set('toObject', {
getters: true,
virtuals: true,
minimize: false,
depopulate: true
});
}
In this article, you'll take a look at 3 plugins that each do one of these operations. By diving into these plugins, you'll hopefully be more comfortable using plugins in your own code.
Adding Middleware: mongoose-autopopulate
The aforementioned mongoose-autopopulate
plugin is an
excellent example of using a plugin to define middleware.
The purpose of this plugin is to address a commonly requested
mongoose feature: the ability to say "this field should
always be populated every time I run a query on this model."
For instance, if you define a schema as below:
var bandSchema = new Schema({
name: String,
lead: { type: ObjectId, ref: 'people', autopopulate: true }
});
bandSchema.plugin(require('mongoose-autopopulate'));
var Band = mongoose.model('band2', bandSchema, 'bands');
Every time you call Band.find()
or Band.findOne()
,
mongoose-autopopulate
will populate the lead
field
for you. Now that mongoose 4.0 has pre and post hooks for
find()
and findOne()
, this plugin's implementation is
concise.
module.exports = function(schema) {
var pathsToPopulate = [];
// Find every path that has an `autopopulate` option
schema.eachPath(function (pathname, schemaType) {
if (schemaType.options && schemaType.options.autopopulate) {
var option = schemaType.options.autopopulate;
pathsToPopulate.push({ path: pathname, autopopulate: option });
}
});
/**
* On find() and findOne(), process autopopulate option
* for each path. If `autopopulate` is a function,
* mongoose-autopopulate will call it.
*/
var autopopulateHandler = function() {
var numPaths = pathsToPopulate.length;
var optionsToUse;
for (var i = 0; i < numPaths; ++i) {
processOption.call(this,
pathsToPopulate[i].autopopulate, pathsToPopulate[i].path);
}
};
schema.
pre('find', autopopulateHandler).
pre('findOne', autopopulateHandler);
};
You can see the full implementation on GitHub.
The key pattern here is
the eachPath()
function.
This function allows you to iterate over every path in the
schema and check for autopopulate
options. You will see
this function used in many plugins in order to find options
specified in the schema. Take note of this, you will see
this feature again when you look at the mongoose-hidden
plugin later.
Adding Extra Fields: mongoose-random
I recently ran into the
mongoose-random
plugin,
a plugin that enables you to efficiently query for a random
document in your collection. This case is a good example for
where a plugin is excellent: this feature (probably) isn't
important enough to be in the mongodb server or mongoose
core. But, I implemented the same algorithm for querying a
random document back when I was working on Ascot Project,
so N=1 sample says that people still need to implement this
feature.
An efficient way to pull a random document from a
MongoDB collection is to associate a randomly generated
point (x, y)
with each document such that 0 <= x, y < 1
.
To generate a random document, you pick another random
point (a, b)
and ask MongoDB for the document closest
to (a, b)
. You can do this efficiently by creating a
2dsphere
index on the (x, y)
points.
Here's how the plugin is implemented. You can find the full implementation on GitHub. The below code doesn't match the GitHub exactly, I changed comments to flow better with this article and removed some redundant code to make it more concise.
function random(schema, options) {
options = options || {};
// Can specify a random function other than Math.random
var randFn = options.fn || Math.random;
var randCoords = function () { return [randFn(), randFn()] }
// Create a path 'random' that's a GeoJSON point
var path = options.path || 'random';
var field = {};
field[path] = {
type: { type: String, default: 'Point' },
coordinates: { type: [Number], default: randCoords }
};
var index = {};
index[path] = '2dsphere';
// Add the 'random' field and a 2dsphere index on it
schema.add(field);
schema.index(index);
/**
* Attach a `findRandom()` helper to the schema for
* syntactic sugar
*/
schema.statics.findRandom = function (conditions, fields, options, callback) {
if (!conditions || typeof conditions === 'function') {
conditions = {};
}
conditions[path] = conditions[path] || {
$near: {
$geometry: { type: 'Point', coordinates: randCoords() }
}
};
return this.find(conditions, fields, options, callback);
};
}
The above code is included from this GitHub repo and subject to the MIT license
Note that you can also create helper methods and functions
in your plugins, like the findRandom()
function above.
Modifying Schema Options: mongoose-hidden
The last plugin use case you'll see in this article is
tweaking schema options. This use case is primary useful
for defining
transform functions
for the toObject()
function. A classic example is
hiding certain sensitive fields before sending the object to
the client, like passwords or email addresses.
The
mongoose-hidden
plugin gives you an easy way to always hide certain fields
from toObject()
output. For instance, suppose you had
the following schema.
var userSchema = new mongoose.Schema({
name: String,
password: { type: String, hide: true }
});
userSchema.plugin(require('mongoose-hidden'));
var User = mongoose.model('User', userSchema, 'users');
var user = new User({ name: 'Val', password: 'pwd' });
// Prints `{ "name": "Val" }`. No password!
console.log(JSON.stringify(user.toObject()));
The implementation of mongoose-hidden
is pretty complex
because it has numerous options and has the ability to hide
virtuals. However, the general idea for implementing
the hide
option for schemas is pretty straightforward.
function HidePlugin(schema) {
var toHide = [];
schema.eachPath(function(pathname, schemaType) {
if (schemaType.options && schemaType.options.hide) {
toHide.push(pathname);
}
});
schema.options.toObject = schema.options.toObject || {};
schema.options.toObject.transform = function(doc, ret) {
// Loop over all fields to hide
toHide.forEach(function(pathname) {
// Break the path up by dots to find the actual
// object to delete
var sp = pathname.split('.');
var obj = ret;
for (var i = 0; i < sp.length - 1; ++i) {
if (!obj) {
return;
}
obj = obj[sp[i]];
}
// Delete the actual field
delete obj[sp[sp.length - 1]];
});
return ret;
};
}
The transform
option is passed to toObject()
and
toJSON()
. It lets you define a function that manipulates
the returned object (the ret
parameter in the transform
function above) before it is returned. In the above
case, you use the same eachPath()
pattern you saw in the
mongoose-autopopulate plugin to get a list of fields to
hide. The transform
function loops through all the
fields marked "hide" and deletes them in the final object.
Conclusion
Mongoose plugins are a powerful feature that enable you to reuse sophisticated functionality across your schemas. Too often mongoose users ignore plugins. Using established plugins can save you a lot of work, and writing your own plugins gives you another way to DRY up your code.