UNPKG

mongoose-hook

Version:

Abstract mongoose plugin, allowing usage of 'pre' and 'post' hooks on internal direct wrappers to database API for all (including static) mongoose operations.

255 lines (210 loc) 6.7 kB
'use strict'; // version ID for original injection var pluginSpec = 'mongooseHookPlugin_2015_03_20'; var pluginSpec_mongoose = pluginSpec + '_mongoose'; var pluginSpec_plugins = pluginSpec + '_plugins'; var hooks = {}; // async hook injector for mongoose database API wrappers function hookAsync(namespace, methodName, hookFunction, result) { var hooked = namespace[methodName]; // replace original method with a hook namespace[methodName] = function() { var _this = this; var outer = _this; var mongoose = outer[pluginSpec_mongoose]; var hookPlugin = mongoose && mongoose[pluginSpec]; var outerArgs = arguments; var T; // conditions to skip all the 'pre' and 'post' hooks if ( !hookPlugin || !(outer.name in hookPlugin.collectionOpts) || typeof outerArgs[0] === 'function' ) { return hooked.apply(outer, outerArgs); } // declare database operation object; add request arguments T = { mongoose: mongoose, collection: outer, req: outerArgs, method: methodName, collectionOpts: hookPlugin.collectionOpts[outer.name] }; // add named links to request arguments switch (T.method) { case 'insert': T.insert = T.req[0]; T.opts = T.req[1]; break; case 'find': case 'findOne': case 'remove': T.query = T.req[0]; T.opts = T.req[1]; break; case 'aggregate': T.pipe = T.req[0]; T.opts = T.req[1]; var match = T.pipe && T.pipe[0] && T.pipe[0].match; if (match) T.query = match; break; case 'mapReduce': T.opts = T.req[2]; T.query = T.opts.query; T.map = T.opts.map; T.reduce = T.opts.reduce; break; case 'update': T.query = T.req[0]; T.update = T.req[1]; T.opts = T.req[2]; if (T.opts && T.opts.upsert && T.update) T.insert = T.update.$setOnInsert; break; case 'findAndModify': T.query = T.req[0]; T.update = T.req[2]; T.opts = T.req[3]; if (T.opts && T.opts.upsert && T.update) T.insert = T.update.$setOnInsert; break; } // async call pre hook hookFunction.call(T, 'pre', function(after) { if (after) { var upstream = outerArgs[outerArgs.length - 1]; // prepare 'post' hook if (typeof after === 'function') { outerArgs[outerArgs.length - 1] = function() { var _this = this, inner = _this, innerArgs = arguments; // add response arguments T.resScope = inner; T.res = innerArgs; // add named links to response arguments T.error = innerArgs[0]; T.data = innerArgs[1]; T.stats = innerArgs[2]; // async call 'post' hook after.call(T, 'post', function() { upstream.apply(inner, innerArgs); }); }; } } // upstream to original method hooked.apply(outer, outerArgs); }); }; // return sync result (if specified) return result; } // hook for mongoose.model hooks.model = function(name, schema) { var hookPlugin = this[pluginSpec]; var model = hookPlugin.modelHooked.apply(this, arguments); //call original // if schema is marked with this plugin, also mark the collection if (schema && schema[pluginSpec_plugins]) { hookPlugin.collectionOpts[model.collection.name] = schema[pluginSpec_plugins]; model.collection[pluginSpec_mongoose] = this; } // return original result return model; }; // hooks for final wrappers of DBMS API hooks.collectionHook = function(stage, callback) { var _this = this; var T = _this; var plugins = T.collectionOpts.plugins; // iterate through the hook-based plugins var iterPlugins = function(pluginIdx) { if (pluginIdx >= plugins.length) return callback(hooks.collectionHook); var plugin = plugins[pluginIdx]; var next = function() {iterPlugins(pluginIdx + 1);}; var func = plugin[stage]; // call the plugin's stage hook, if defined if (func) { // T is database operation object func.call(plugin, T, next); } else next(); }; iterPlugins(0); }; // method to get model by collection name hooks.getModel = function() { var mongoose = this[pluginSpec_mongoose]; if (!mongoose) return null; var P = mongoose[pluginSpec]; var cached = P.modelByCollection[this.name]; if (cached) return cached; // find schema within models var i; var models = this.conn.models; for (i in models) { var model = models[i]; if (model.collection === this) { P.modelByCollection[this.name] = model; return model; } } return null; }; // makes given mongoose object affected by this plugin function patchMongoose(mongoose) { // patch only once if (mongoose[pluginSpec]) return false; var MCP = mongoose.Collection.prototype; // hook the wrappers of database API [ 'aggregate', 'findOne', 'find', 'mapReduce', 'update', 'remove', 'insert', 'findAndModify', 'findAndRemove' ] .forEach(function(method) { hookAsync(MCP, method, hooks.collectionHook); }); // mark mongoose as patched mongoose[pluginSpec] = { collectionOpts: {}, modelHooked: mongoose.model, modelByCollection: {} }; // hook the model generator mongoose.model = hooks.model; // emit getModel method on Collection if (!MCP.getModel) MCP.getModel = hooks.getModel; return true; } // emit mongoose plugin // opts = { // mongoose: Mongoose, // instance to patch, // pre: function(params, callback), // optional // post: function(params, callback), // optional // name: String, // optional name of sub-plugin // once: Boolean, // optional; if specified and has `name`, don't add a duplicate // replace: Boolean, // optional; if specified with `once`, will replace old one // top: Boolean // if specified, make this sub-plugin highest priority // } module.exports = function(schema, opts) { patchMongoose(opts.mongoose || require('mongoose')); // mark schema as involved into this plugin if (!schema[pluginSpec_plugins]) schema[pluginSpec_plugins] = {plugins: []}; // register the caller plugin on this schema and its deriving models/collections if (opts) { var p = schema[pluginSpec_plugins].plugins; if (opts.once && opts.name) for (var i in p) if (p[i].name === opts.name) { if (opts.replace) { delete p[i]; break; } else return; } if (opts.top) p.unshift(opts); else p.push(opts); } };