├── .gitignore ├── Makefile ├── package.json ├── hooks.alt.js ├── hooks.js ├── README.md └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | **.swp 2 | node_modules 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @NODE_ENV=test ./node_modules/expresso/bin/expresso \ 3 | $(TESTFLAGS) \ 4 | ./test.js 5 | 6 | test-cov: 7 | @TESTFLAGS=--cov $(MAKE) test 8 | 9 | .PHONY: test test-cov 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hooks-fixed", 3 | "description": "Adds pre and post hook functionality to your JavaScript methods.", 4 | "version": "2.0.2", 5 | "keywords": [ 6 | "node", 7 | "hooks", 8 | "middleware", 9 | "pre", 10 | "post" 11 | ], 12 | "homepage": "https://github.com/vkarpov15/hooks-fixed/", 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/vkarpov15/hooks-fixed.git" 16 | }, 17 | "author": "Brian Noguchi (https://github.com/bnoguchi/)", 18 | "main": "./hooks.js", 19 | "directories": { 20 | "lib": "." 21 | }, 22 | "scripts": { 23 | "test": "make test" 24 | }, 25 | "dependencies": {}, 26 | "devDependencies": { 27 | "expresso": ">=0.7.6", 28 | "should": ">=0.2.1", 29 | "underscore": ">=1.1.4" 30 | }, 31 | "engines": { 32 | "node": ">=0.4.0" 33 | }, 34 | "licenses": [ 35 | "MIT" 36 | ], 37 | "optionalDependencies": {} 38 | } 39 | -------------------------------------------------------------------------------- /hooks.alt.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hooks are useful if we want to add a method that automatically has `pre` and `post` hooks. 3 | * For example, it would be convenient to have `pre` and `post` hooks for `save`. 4 | * _.extend(Model, mixins.hooks); 5 | * Model.hook('save', function () { 6 | * console.log('saving'); 7 | * }); 8 | * Model.pre('save', function (next, done) { 9 | * console.log('about to save'); 10 | * next(); 11 | * }); 12 | * Model.post('save', function (next, done) { 13 | * console.log('saved'); 14 | * next(); 15 | * }); 16 | * 17 | * var m = new Model(); 18 | * m.save(); 19 | * // about to save 20 | * // saving 21 | * // saved 22 | */ 23 | 24 | // TODO Add in pre and post skipping options 25 | module.exports = { 26 | /** 27 | * Declares a new hook to which you can add pres and posts 28 | * @param {String} name of the function 29 | * @param {Function} the method 30 | * @param {Function} the error handler callback 31 | */ 32 | hook: function (name, fn, err) { 33 | if (arguments.length === 1 && typeof name === 'object') { 34 | for (var k in name) { // `name` is a hash of hookName->hookFn 35 | this.hook(k, name[k]); 36 | } 37 | return; 38 | } 39 | 40 | if (!err) err = fn; 41 | 42 | var proto = this.prototype || this 43 | , pres = proto._pres = proto._pres || {} 44 | , posts = proto._posts = proto._posts || {}; 45 | pres[name] = pres[name] || []; 46 | posts[name] = posts[name] || []; 47 | 48 | function noop () {} 49 | 50 | proto[name] = function () { 51 | var self = this 52 | , pres = this._pres[name] 53 | , posts = this._posts[name] 54 | , numAsyncPres = 0 55 | , hookArgs = [].slice.call(arguments) 56 | , preChain = pres.map( function (pre, i) { 57 | var wrapper = function () { 58 | if (arguments[0] instanceof Error) 59 | return err(arguments[0]); 60 | if (numAsyncPres) { 61 | // arguments[1] === asyncComplete 62 | if (arguments.length) 63 | hookArgs = [].slice.call(arguments, 2); 64 | pre.apply(self, 65 | [ preChain[i+1] || allPresInvoked, 66 | asyncComplete 67 | ].concat(hookArgs) 68 | ); 69 | } else { 70 | if (arguments.length) 71 | hookArgs = [].slice.call(arguments); 72 | pre.apply(self, 73 | [ preChain[i+1] || allPresDone ].concat(hookArgs)); 74 | } 75 | }; // end wrapper = function () {... 76 | if (wrapper.isAsync = pre.isAsync) 77 | numAsyncPres++; 78 | return wrapper; 79 | }); // end posts.map(...) 80 | function allPresInvoked () { 81 | if (arguments[0] instanceof Error) 82 | err(arguments[0]); 83 | } 84 | 85 | function allPresDone () { 86 | if (arguments[0] instanceof Error) 87 | return err(arguments[0]); 88 | if (arguments.length) 89 | hookArgs = [].slice.call(arguments); 90 | fn.apply(self, hookArgs); 91 | var postChain = posts.map( function (post, i) { 92 | var wrapper = function () { 93 | if (arguments[0] instanceof Error) 94 | return err(arguments[0]); 95 | if (arguments.length) 96 | hookArgs = [].slice.call(arguments); 97 | post.apply(self, 98 | [ postChain[i+1] || noop].concat(hookArgs)); 99 | }; // end wrapper = function () {... 100 | return wrapper; 101 | }); // end posts.map(...) 102 | if (postChain.length) postChain[0](); 103 | } 104 | 105 | if (numAsyncPres) { 106 | complete = numAsyncPres; 107 | function asyncComplete () { 108 | if (arguments[0] instanceof Error) 109 | return err(arguments[0]); 110 | --complete || allPresDone.call(this); 111 | } 112 | } 113 | (preChain[0] || allPresDone)(); 114 | }; 115 | 116 | return this; 117 | }, 118 | 119 | pre: function (name, fn, isAsync) { 120 | var proto = this.prototype 121 | , pres = proto._pres = proto._pres || {}; 122 | if (fn.isAsync = isAsync) { 123 | this.prototype[name].numAsyncPres++; 124 | } 125 | (pres[name] = pres[name] || []).push(fn); 126 | return this; 127 | }, 128 | post: function (name, fn, isAsync) { 129 | var proto = this.prototype 130 | , posts = proto._posts = proto._posts || {}; 131 | (posts[name] = posts[name] || []).push(fn); 132 | return this; 133 | } 134 | }; 135 | -------------------------------------------------------------------------------- /hooks.js: -------------------------------------------------------------------------------- 1 | // TODO Add in pre and post skipping options 2 | module.exports = { 3 | /** 4 | * Declares a new hook to which you can add pres and posts 5 | * @param {String} name of the function 6 | * @param {Function} the method 7 | * @param {Function} the error handler callback 8 | */ 9 | $hook: function (name, fn, errorCb) { 10 | if (arguments.length === 1 && typeof name === 'object') { 11 | for (var k in name) { // `name` is a hash of hookName->hookFn 12 | this.$hook(k, name[k]); 13 | } 14 | return; 15 | } 16 | 17 | var proto = this.prototype || this 18 | , pres = proto._pres = proto._pres || {} 19 | , posts = proto._posts = proto._posts || {}; 20 | pres[name] = pres[name] || []; 21 | posts[name] = posts[name] || []; 22 | 23 | proto[name] = function () { 24 | var self = this 25 | , hookArgs // arguments eventually passed to the hook - are mutable 26 | , lastArg = arguments[arguments.length-1] 27 | , pres = this._pres[name] 28 | , posts = this._posts[name] 29 | , _total = pres.length 30 | , _current = -1 31 | , _asyncsLeft = proto[name].numAsyncPres 32 | , _asyncsDone = function(err) { 33 | if (err) { 34 | return handleError(err); 35 | } 36 | --_asyncsLeft || _done.apply(self, hookArgs); 37 | } 38 | , handleError = function(err) { 39 | if ('function' == typeof lastArg) 40 | return lastArg(err); 41 | if (errorCb) return errorCb.call(self, err); 42 | throw err; 43 | } 44 | , _next = function () { 45 | if (arguments[0] instanceof Error) { 46 | return handleError(arguments[0]); 47 | } 48 | var _args = Array.prototype.slice.call(arguments) 49 | , currPre 50 | , preArgs; 51 | if (_args.length && !(arguments[0] == null && typeof lastArg === 'function')) 52 | hookArgs = _args; 53 | if (++_current < _total) { 54 | currPre = pres[_current] 55 | if (currPre.isAsync && currPre.length < 2) 56 | throw new Error("Your pre must have next and done arguments -- e.g., function (next, done, ...)"); 57 | if (currPre.length < 1) 58 | throw new Error("Your pre must have a next argument -- e.g., function (next, ...)"); 59 | preArgs = (currPre.isAsync 60 | ? [once(_next), once(_asyncsDone)] 61 | : [once(_next)]).concat(hookArgs); 62 | try { 63 | return currPre.apply(self, preArgs); 64 | } catch (error) { 65 | _next(error); 66 | } 67 | } else if (!_asyncsLeft) { 68 | return _done.apply(self, hookArgs); 69 | } 70 | } 71 | , _done = function () { 72 | var args_ = Array.prototype.slice.call(arguments) 73 | , ret, total_, current_, next_, done_, postArgs; 74 | 75 | if (_current === _total) { 76 | 77 | next_ = function () { 78 | if (arguments[0] instanceof Error) { 79 | return handleError(arguments[0]); 80 | } 81 | var args_ = Array.prototype.slice.call(arguments, 1) 82 | , currPost 83 | , postArgs; 84 | if (args_.length) hookArgs = args_; 85 | if (++current_ < total_) { 86 | currPost = posts[current_] 87 | if (currPost.length < 1) 88 | throw new Error("Your post must have a next argument -- e.g., function (next, ...)"); 89 | postArgs = [once(next_)].concat(hookArgs); 90 | return currPost.apply(self, postArgs); 91 | } else if (typeof lastArg === 'function'){ 92 | // All post handlers are done, call original callback function 93 | return lastArg.apply(self, arguments); 94 | } 95 | }; 96 | 97 | // We are assuming that if the last argument provided to the wrapped function is a function, it was expecting 98 | // a callback. We trap that callback and wait to call it until all post handlers have finished. 99 | if(typeof lastArg === 'function'){ 100 | args_[args_.length - 1] = once(next_); 101 | } 102 | 103 | total_ = posts.length; 104 | current_ = -1; 105 | ret = fn.apply(self, args_); // Execute wrapped function, post handlers come afterward 106 | 107 | if (total_ && typeof lastArg !== 'function') return next_(); // no callback provided, execute next_() manually 108 | return ret; 109 | } 110 | }; 111 | 112 | return _next.apply(this, arguments); 113 | }; 114 | 115 | proto[name].numAsyncPres = 0; 116 | 117 | return this; 118 | }, 119 | 120 | pre: function (name, isAsync, fn, errorCb) { 121 | if ('boolean' !== typeof arguments[1]) { 122 | errorCb = fn; 123 | fn = isAsync; 124 | isAsync = false; 125 | } 126 | var proto = this.prototype || this 127 | , pres = proto._pres = proto._pres || {}; 128 | 129 | this._lazySetupHooks(proto, name, errorCb); 130 | 131 | if (fn.isAsync = isAsync) { 132 | proto[name].numAsyncPres++; 133 | } 134 | 135 | (pres[name] = pres[name] || []).push(fn); 136 | return this; 137 | }, 138 | post: function (name, isAsync, fn) { 139 | if (arguments.length === 2) { 140 | fn = isAsync; 141 | isAsync = false; 142 | } 143 | var proto = this.prototype || this 144 | , posts = proto._posts = proto._posts || {}; 145 | 146 | this._lazySetupHooks(proto, name); 147 | (posts[name] = posts[name] || []).push(fn); 148 | return this; 149 | }, 150 | removePre: function (name, fnToRemove) { 151 | var proto = this.prototype || this 152 | , pres = proto._pres || (proto._pres || {}); 153 | if (!pres[name]) return this; 154 | if (arguments.length === 1) { 155 | // Remove all pre callbacks for hook `name` 156 | pres[name].length = 0; 157 | } else { 158 | pres[name] = pres[name].filter( function (currFn) { 159 | return currFn !== fnToRemove; 160 | }); 161 | } 162 | return this; 163 | }, 164 | removePost: function (name, fnToRemove) { 165 | var proto = this.prototype || this 166 | , posts = proto._posts || (proto._posts || {}); 167 | if (!posts[name]) return this; 168 | if (arguments.length === 1) { 169 | // Remove all post callbacks for hook `name` 170 | posts[name].length = 0; 171 | } else { 172 | posts[name] = posts[name].filter( function (currFn) { 173 | return currFn !== fnToRemove; 174 | }); 175 | } 176 | return this; 177 | }, 178 | 179 | _lazySetupHooks: function (proto, methodName, errorCb) { 180 | if ('undefined' === typeof proto[methodName].numAsyncPres) { 181 | this.$hook(methodName, proto[methodName], errorCb); 182 | } 183 | } 184 | }; 185 | 186 | function once (fn, scope) { 187 | return function fnWrapper () { 188 | if (fnWrapper.hookCalled) return; 189 | fnWrapper.hookCalled = true; 190 | fn.apply(scope, arguments); 191 | }; 192 | } 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hooks 2 | ============ 3 | 4 | Add pre and post middleware hooks to your JavaScript methods. 5 | 6 | ## Installation 7 | npm install hooks 8 | 9 | ## Motivation 10 | Suppose you have a JavaScript object with a `save` method. 11 | 12 | It would be nice to be able to declare code that runs before `save` and after `save`. 13 | For example, you might want to run validation code before every `save`, 14 | and you might want to dispatch a job to a background job queue after `save`. 15 | 16 | One might have an urge to hard code this all into `save`, but that turns out to 17 | couple all these pieces of functionality (validation, save, and job creation) more 18 | tightly than is necessary. For example, what if someone does not want to do background 19 | job creation after the logical save? 20 | 21 | It is nicer to tack on functionality using what we call `pre` and `post` hooks. These 22 | are functions that you define and that you direct to execute before or after particular 23 | methods. 24 | 25 | ## Example 26 | We can use `hooks` to add validation and background jobs in the following way: 27 | 28 | ```javascript 29 | var hooks = require('hooks') 30 | , Document = require('./path/to/some/document/constructor'); 31 | 32 | // Add hooks' methods: `hook`, `pre`, and `post` 33 | for (var k in hooks) { 34 | Document[k] = hooks[k]; 35 | } 36 | 37 | // Define a new method that is able to invoke pre and post middleware 38 | Document.hook('save', Document.prototype.save); 39 | 40 | // Define a middleware function to be invoked before 'save' 41 | Document.pre('save', function validate (next) { 42 | // The `this` context inside of `pre` and `post` functions 43 | // is the Document instance 44 | if (this.isValid()) next(); // next() passes control to the next middleware 45 | // or to the target method itself 46 | else next(new Error("Invalid")); // next(error) invokes an error callback 47 | }); 48 | 49 | // Define a middleware function to be invoked after 'save' 50 | Document.post('save', function createJob (next) { 51 | this.sendToBackgroundQueue(); 52 | next(); 53 | }); 54 | ``` 55 | 56 | If you already have defined `Document.prototype` methods for which you want pres and posts, 57 | then you do not need to explicitly invoke `Document.hook(...)`. Invoking `Document.pre(methodName, fn)` 58 | or `Document.post(methodName, fn)` will automatically and lazily change `Document.prototype[methodName]` 59 | so that it plays well with `hooks`. An equivalent way to implement the previous example is: 60 | 61 | ```javascript 62 | var hooks = require('hooks') 63 | , Document = require('./path/to/some/document/constructor'); 64 | 65 | // Add hooks' methods: `hook`, `pre`, and `post` 66 | for (var k in hooks) { 67 | Document[k] = hooks[k]; 68 | } 69 | 70 | Document.prototype.save = function () { 71 | // ... 72 | }; 73 | 74 | // Define a middleware function to be invoked before 'save' 75 | Document.pre('save', function validate (next) { 76 | // The `this` context inside of `pre` and `post` functions 77 | // is the Document instance 78 | if (this.isValid()) next(); // next() passes control to the next middleware 79 | // or to the target method itself 80 | else next(new Error("Invalid")); // next(error) invokes an error callback 81 | }); 82 | 83 | // Define a middleware function to be invoked after 'save' 84 | Document.post('save', function createJob (next) { 85 | this.sendToBackgroundQueue(); 86 | next(); 87 | }); 88 | ``` 89 | 90 | ## Pres and Posts as Middleware 91 | We structure pres and posts as middleware to give you maximum flexibility: 92 | 93 | 1. You can define **multiple** pres (or posts) for a single method. 94 | 2. These pres (or posts) are then executed as a chain of methods. 95 | 3. Any functions in this middleware chain can choose to halt the chain's execution by `next`ing an Error from that middleware function. If this occurs, then none of the other middleware in the chain will execute, and the main method (e.g., `save`) will not execute. This is nice, for example, when we don't want a document to save if it is invalid. 96 | 97 | ## Defining multiple pres (or posts) 98 | `pre` and `post` are chainable, so you can define multiple via: 99 | ```javascript 100 | Document.pre('save', function (next) { 101 | console.log("hello"); 102 | next(); 103 | }).pre('save', function (next) { 104 | console.log("world"); 105 | next(); 106 | }); 107 | 108 | Document.post('save', function (next) { 109 | console.log("hello"); 110 | next(); 111 | }).post('save', function (next) { 112 | console.log("world"); 113 | next(); 114 | }); 115 | ``` 116 | 117 | As soon as one pre finishes executing, the next one will be invoked, and so on. 118 | 119 | ## Error Handling 120 | You can define a default error handler by passing a 2nd function as the 3rd argument to `hook`: 121 | ```javascript 122 | Document.hook('set', function (path, val) { 123 | this[path] = val; 124 | }, function (err) { 125 | // Handler the error here 126 | console.error(err); 127 | }); 128 | ``` 129 | 130 | Then, we can pass errors to this handler from a pre or post middleware function: 131 | ```javascript 132 | Document.pre('set', function (next, path, val) { 133 | next(new Error()); 134 | }); 135 | ``` 136 | 137 | If you do not set up a default handler, then `hooks` makes the default handler that just throws the `Error`. 138 | 139 | The default error handler can be over-rided on a per method invocation basis. 140 | 141 | If the main method that you are surrounding with pre and post middleware expects its last argument to be a function 142 | with callback signature `function (error, ...)`, then that callback becomes the error handler, over-riding the default 143 | error handler you may have set up. 144 | 145 | ```javascript 146 | Document.hook('save', function (callback) { 147 | // Save logic goes here 148 | ... 149 | }); 150 | 151 | var doc = new Document(); 152 | doc.save( function (err, saved) { 153 | // We can pass err via `next` in any of our pre or post middleware functions 154 | if (err) console.error(err); 155 | 156 | // Rest of callback logic follows ... 157 | }); 158 | ``` 159 | 160 | ## Mutating Arguments via Middleware 161 | `pre` and `post` middleware can also accept the intended arguments for the method 162 | they augment. This is useful if you want to mutate the arguments before passing 163 | them along to the next middleware and eventually pass a mutated arguments list to 164 | the main method itself. 165 | 166 | As a simple example, let's define a method `set` that just sets a key, value pair. 167 | If we want to namespace the key, we can do so by adding a `pre` middleware hook 168 | that runs before `set`, alters the arguments by namespacing the `key` argument, and passes them onto `set`: 169 | 170 | ```javascript 171 | Document.hook('set', function (key, val) { 172 | this[key] = val; 173 | }); 174 | Document.pre('set', function (next, key, val) { 175 | next('namespace-' + key, val); 176 | }); 177 | var doc = new Document(); 178 | doc.set('hello', 'world'); 179 | console.log(doc.hello); // undefined 180 | console.log(doc['namespace-hello']); // 'world' 181 | ``` 182 | 183 | As you can see above, we pass arguments via `next`. 184 | 185 | If you are not mutating the arguments, then you can pass zero arguments 186 | to `next`, and the next middleware function will still have access 187 | to the arguments. 188 | 189 | ```javascript 190 | Document.hook('set', function (key, val) { 191 | this[key] = val; 192 | }); 193 | Document.pre('set', function (next, key, val) { 194 | // I have access to key and val here 195 | next(); // We don't need to pass anything to next 196 | }); 197 | Document.pre('set', function (next, key, val) { 198 | // And I still have access to the original key and val here 199 | next(); 200 | }); 201 | ``` 202 | 203 | Finally, you can add arguments that downstream middleware can also see: 204 | 205 | ```javascript 206 | // Note that in the definition of `set`, there is no 3rd argument, options 207 | Document.hook('set', function (key, val) { 208 | // But... 209 | var options = arguments[2]; // ...I have access to an options argument 210 | // because of pre function pre2 (defined below) 211 | console.log(options); // '{debug: true}' 212 | this[key] = val; 213 | }); 214 | Document.pre('set', function pre1 (next, key, val) { 215 | // I only have access to key and val arguments 216 | console.log(arguments.length); // 3 217 | next(key, val, {debug: true}); 218 | }); 219 | Document.pre('set', function pre2 (next, key, val, options) { 220 | console.log(arguments.length); // 4 221 | console.log(options); // '{ debug: true}' 222 | next(); 223 | }); 224 | Document.pre('set', function pre3 (next, key, val, options) { 225 | // I still have access to key, val, AND the options argument introduced via the preceding middleware 226 | console.log(arguments.length); // 4 227 | console.log(options); // '{ debug: true}' 228 | next(); 229 | }); 230 | 231 | var doc = new Document() 232 | doc.set('hey', 'there'); 233 | ``` 234 | 235 | ## Post middleware 236 | 237 | Post middleware intercepts the callback originally sent to the asynchronous function you have hooked to. 238 | 239 | This means that the following chain of execution will occur in a typical `save` operation: 240 | 241 | (1) doc.save -> (2) pre --(next)--> (3) save calls back -> (4) post --(next)--> (5) targetFn 242 | 243 | Illustrated below: 244 | 245 | ``` 246 | Document.pre('save', function (next) { 247 | this.key = "value"; 248 | next(); 249 | }); 250 | // Post handler occurs before `set` calls back. This is useful if we need to grab something 251 | // async before `set` finishes. 252 | Document.post('set', function (next) { 253 | var me = this; 254 | getSomethingAsync(function(value){ // let's assume it returns "Hello Async" 255 | me.key2 = value; 256 | next(); 257 | }); 258 | }); 259 | 260 | var doc = new Document(); 261 | doc.save(function(err){ 262 | console.log(this.key); // "value" - this value was saved 263 | console.log(this.key2); // "Hello Async" - this value was *not* saved 264 | } 265 | 266 | ``` 267 | 268 | Post middleware must call `next()` or execution will stop. 269 | 270 | ## Parallel `pre` middleware 271 | 272 | All middleware up to this point has been "serial" middleware -- i.e., middleware whose logic 273 | is executed as a serial chain. 274 | 275 | Some scenarios call for parallel middleware -- i.e., middleware that can wait for several 276 | asynchronous services at once to respond. 277 | 278 | For instance, you may only want to save a Document only after you have checked 279 | that the Document is valid according to two different remote services. 280 | 281 | We accomplish asynchronous middleware by adding a second kind of flow control callback 282 | (the only flow control callback so far has been `next`), called `done`. 283 | 284 | - `next` passes control to the next middleware in the chain 285 | - `done` keeps track of how many parallel middleware have invoked `done` and passes 286 | control to the target method when ALL parallel middleware have invoked `done`. If 287 | you pass an `Error` to `done`, then the error is handled, and the main method that is 288 | wrapped by pres and posts will not get invoked. 289 | 290 | We declare pre middleware that is parallel by passing a 3rd boolean argument to our `pre` 291 | definition method. 292 | 293 | We illustrate via the parallel validation example mentioned above: 294 | 295 | ```javascript 296 | Document.hook('save', function targetFn (callback) { 297 | // Save logic goes here 298 | // ... 299 | // This only gets run once the two `done`s are both invoked via preOne and preTwo. 300 | }); 301 | 302 | // true marks this as parallel middleware 303 | Document.pre('save', true, function preOne (next, doneOne, callback) { 304 | remoteServiceOne.validate(this.serialize(), function (err, isValid) { 305 | // The code in here will probably be run after the `next` below this block 306 | // and could possibly be run after the console.log("Hola") in `preTwo 307 | if (err) return doneOne(err); 308 | if (isValid) doneOne(); 309 | }); 310 | next(); // Pass control to the next middleware 311 | }); 312 | 313 | // We will suppose that we need 2 different remote services to validate our document 314 | Document.pre('save', true, function preTwo (next, doneTwo, callback) { 315 | remoteServiceTwo.validate(this.serialize(), function (err, isValid) { 316 | if (err) return doneTwo(err); 317 | if (isValid) doneTwo(); 318 | }); 319 | next(); 320 | }); 321 | 322 | // While preOne and preTwo are parallel, preThree is a serial pre middleware 323 | Document.pre('save', function preThree (next, callback) { 324 | next(); 325 | }); 326 | 327 | var doc = new Document(); 328 | doc.save( function (err, doc) { 329 | // Do stuff with the saved doc here... 330 | }); 331 | ``` 332 | 333 | In the above example, flow control may happen in the following way: 334 | 335 | (1) doc.save -> (2) preOne --(next)--> (3) preTwo --(next)--> (4) preThree --(next)--> (wait for dones to invoke) -> (5) doneTwo -> (6) doneOne -> (7) targetFn 336 | 337 | So what's happening is that: 338 | 339 | 1. You call `doc.save(...)` 340 | 2. First, your preOne middleware gets executed. It makes a remote call to the validation service and `next()`s to the preTwo middleware. 341 | 3. Now, your preTwo middleware gets executed. It makes a remote call to another validation service and `next()`s to the preThree middleware. 342 | 4. Your preThree middleware gets executed. It immediately `next()`s. But nothing else gets executing until both `doneOne` and `doneTwo` are invoked inside the callbacks handling the response from the two valiation services. 343 | 5. We will suppose that validation remoteServiceTwo returns a response to us first. In this case, we call `doneTwo` inside the callback to remoteServiceTwo. 344 | 6. Some fractions of a second later, remoteServiceOne returns a response to us. In this case, we call `doneOne` inside the callback to remoteServiceOne. 345 | 7. `hooks` implementation keeps track of how many parallel middleware has been defined per target function. It detects that both asynchronous pre middlewares (`preOne` and `preTwo`) have finally called their `done` functions (`doneOne` and `doneTwo`), so the implementation finally invokes our `targetFn` (i.e., our core `save` business logic). 346 | 347 | ## Removing Pres 348 | 349 | You can remove a particular pre associated with a hook: 350 | 351 | Document.pre('set', someFn); 352 | Document.removePre('set', someFn); 353 | 354 | And you can also remove all pres associated with a hook: 355 | Document.removePre('set'); // Removes all declared `pre`s on the hook 'set' 356 | 357 | ## Tests 358 | To run the tests: 359 | make test 360 | 361 | ### Contributors 362 | - [Brian Noguchi](https://github.com/bnoguchi) 363 | 364 | ### License 365 | MIT License 366 | 367 | --- 368 | ### Author 369 | Brian Noguchi 370 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var hooks = require('./hooks') 2 | , should = require('should') 3 | , assert = require('assert') 4 | , _ = require('underscore'); 5 | 6 | // TODO Add in test for making sure all pres get called if pre is defined directly on an instance. 7 | // TODO Test for calling `done` twice or `next` twice in the same function counts only once 8 | module.exports = { 9 | 'should be able to assign multiple hooks at once': function () { 10 | var A = function () {}; 11 | _.extend(A, hooks); 12 | A.$hook({ 13 | hook1: function (a) {}, 14 | hook2: function (b) {} 15 | }); 16 | var a = new A(); 17 | assert.equal(typeof a.hook1, 'function'); 18 | assert.equal(typeof a.hook2, 'function'); 19 | }, 20 | 'should run without pres and posts when not present': function () { 21 | var A = function () {}; 22 | _.extend(A, hooks); 23 | A.$hook('save', function () { 24 | this.value = 1; 25 | }); 26 | var a = new A(); 27 | a.save(); 28 | a.value.should.equal(1); 29 | }, 30 | 'should run with pres when present': function () { 31 | var A = function () {}; 32 | _.extend(A, hooks); 33 | A.$hook('save', function () { 34 | this.value = 1; 35 | }); 36 | A.pre('save', function (next) { 37 | this.preValue = 2; 38 | next(); 39 | }); 40 | var a = new A(); 41 | a.save(); 42 | a.value.should.equal(1); 43 | a.preValue.should.equal(2); 44 | }, 45 | 'should run with posts when present': function () { 46 | var A = function () {}; 47 | _.extend(A, hooks); 48 | A.$hook('save', function () { 49 | this.value = 1; 50 | }); 51 | A.post('save', function (next) { 52 | this.value = 2; 53 | next(); 54 | }); 55 | var a = new A(); 56 | a.save(); 57 | a.value.should.equal(2); 58 | }, 59 | 'should run pres and posts when present': function () { 60 | var A = function () {}; 61 | _.extend(A, hooks); 62 | A.$hook('save', function () { 63 | this.value = 1; 64 | }); 65 | A.pre('save', function (next) { 66 | this.preValue = 2; 67 | next(); 68 | }); 69 | A.post('save', function (next) { 70 | this.value = 3; 71 | next(); 72 | }); 73 | var a = new A(); 74 | a.save(); 75 | a.value.should.equal(3); 76 | a.preValue.should.equal(2); 77 | }, 78 | 'should run posts after pres': function () { 79 | var A = function () {}; 80 | _.extend(A, hooks); 81 | A.$hook('save', function () { 82 | this.value = 1; 83 | }); 84 | A.pre('save', function (next) { 85 | this.override = 100; 86 | next(); 87 | }); 88 | A.post('save', function (next) { 89 | this.override = 200; 90 | next(); 91 | }); 92 | var a = new A(); 93 | a.save(); 94 | a.value.should.equal(1); 95 | a.override.should.equal(200); 96 | }, 97 | 'should not run a hook if a pre fails': function () { 98 | var A = function () {}; 99 | _.extend(A, hooks); 100 | var counter = 0; 101 | A.$hook('save', function () { 102 | this.value = 1; 103 | }, function (err) { 104 | counter++; 105 | }); 106 | A.pre('save', true, function (next, done) { 107 | next(new Error()); 108 | }); 109 | var a = new A(); 110 | a.save(); 111 | counter.should.equal(1); 112 | assert.equal(typeof a.value, 'undefined'); 113 | }, 114 | 'should be able to run multiple pres': function () { 115 | var A = function () {}; 116 | _.extend(A, hooks); 117 | A.$hook('save', function () { 118 | this.value = 1; 119 | }); 120 | A.pre('save', function (next) { 121 | this.v1 = 1; 122 | next(); 123 | }).pre('save', function (next) { 124 | this.v2 = 2; 125 | next(); 126 | }); 127 | var a = new A(); 128 | a.save(); 129 | a.v1.should.equal(1); 130 | a.v2.should.equal(2); 131 | }, 132 | 'should run multiple pres until a pre fails and not call the hook': function () { 133 | var A = function () {}; 134 | _.extend(A, hooks); 135 | A.$hook('save', function () { 136 | this.value = 1; 137 | }, function (err) {}); 138 | A.pre('save', function (next) { 139 | this.v1 = 1; 140 | next(); 141 | }).pre('save', function (next) { 142 | next(new Error()); 143 | }).pre('save', function (next) { 144 | this.v3 = 3; 145 | next(); 146 | }); 147 | var a = new A(); 148 | a.save(); 149 | a.v1.should.equal(1); 150 | assert.equal(typeof a.v3, 'undefined'); 151 | assert.equal(typeof a.value, 'undefined'); 152 | }, 153 | 'should be able to run multiple posts': function () { 154 | var A = function () {}; 155 | _.extend(A, hooks); 156 | A.$hook('save', function () { 157 | this.value = 1; 158 | }); 159 | A.post('save', function (next) { 160 | this.value = 2; 161 | next(); 162 | }).post('save', function (next) { 163 | this.value = 3.14; 164 | next(); 165 | }).post('save', function (next) { 166 | this.v3 = 3; 167 | next(); 168 | }); 169 | var a = new A(); 170 | a.save(); 171 | assert.equal(a.value, 3.14); 172 | assert.equal(a.v3, 3); 173 | }, 174 | 'should run only posts up until an error': function () { 175 | var A = function () {}; 176 | _.extend(A, hooks); 177 | A.$hook('save', function () { 178 | this.value = 1; 179 | }, function (err) {}); 180 | A.post('save', function (next) { 181 | this.value = 2; 182 | next(); 183 | }).post('save', function (next) { 184 | this.value = 3; 185 | next(new Error()); 186 | }).post('save', function (next) { 187 | this.value = 4; 188 | next(); 189 | }); 190 | var a = new A(); 191 | a.save(); 192 | a.value.should.equal(3); 193 | }, 194 | "should fall back first to the hook method's last argument as the error handler if it is a function of arity 1 or 2": function () { 195 | var A = function () {}; 196 | _.extend(A, hooks); 197 | var counter = 0; 198 | A.$hook('save', function (callback) { 199 | this.value = 1; 200 | }); 201 | A.pre('save', true, function (next, done) { 202 | next(new Error()); 203 | }); 204 | var a = new A(); 205 | a.save( function (err) { 206 | if (err instanceof Error) counter++; 207 | }); 208 | counter.should.equal(1); 209 | should.deepEqual(undefined, a.value); 210 | }, 211 | 'should fall back second to the default error handler if specified': function () { 212 | var A = function () {}; 213 | _.extend(A, hooks); 214 | var counter = 0; 215 | A.$hook('save', function (callback) { 216 | this.value = 1; 217 | }, function (err) { 218 | if (err instanceof Error) counter++; 219 | }); 220 | A.pre('save', true, function (next, done) { 221 | next(new Error()); 222 | }); 223 | var a = new A(); 224 | a.save(); 225 | counter.should.equal(1); 226 | should.deepEqual(undefined, a.value); 227 | }, 228 | 'fallback default error handler should scope to the object': function () { 229 | var A = function () { 230 | this.counter = 0; 231 | }; 232 | _.extend(A, hooks); 233 | var counter = 0; 234 | A.$hook('save', function (callback) { 235 | this.value = 1; 236 | }, function (err) { 237 | if (err instanceof Error) this.counter++; 238 | }); 239 | A.pre('save', true, function (next, done) { 240 | next(new Error()); 241 | }); 242 | var a = new A(); 243 | a.save(); 244 | a.counter.should.equal(1); 245 | should.deepEqual(undefined, a.value); 246 | }, 247 | 'should fall back last to throwing the error': function () { 248 | var A = function () {}; 249 | _.extend(A, hooks); 250 | var counter = 0; 251 | A.$hook('save', function (err) { 252 | if (err instanceof Error) return counter++; 253 | this.value = 1; 254 | }); 255 | A.pre('save', true, function (next, done) { 256 | next(new Error()); 257 | }); 258 | var a = new A(); 259 | var didCatch = false; 260 | try { 261 | a.save(); 262 | } catch (e) { 263 | didCatch = true; 264 | e.should.be.an.instanceof(Error); 265 | counter.should.equal(0); 266 | assert.equal(typeof a.value, 'undefined'); 267 | } 268 | didCatch.should.be.true; 269 | }, 270 | "should proceed without mutating arguments if `next(null|undefined)` is called in a serial pre, and the last argument of the target method is a callback with node-like signature function (err, obj) {...} or function (err) {...}": function () { 271 | var A = function () {}; 272 | _.extend(A, hooks); 273 | var counter = 0; 274 | A.prototype.save = function (callback) { 275 | this.value = 1; 276 | callback(); 277 | }; 278 | A.pre('save', function (next) { 279 | next(null); 280 | }); 281 | A.pre('save', function (next) { 282 | next(undefined); 283 | }); 284 | var a = new A(); 285 | a.save( function (err) { 286 | if (err instanceof Error) counter++; 287 | else counter--; 288 | }); 289 | counter.should.equal(-1); 290 | a.value.should.eql(1); 291 | }, 292 | "should proceed with mutating arguments if `next(null|undefined)` is callback in a serial pre, and the last argument of the target method is not a function": function () { 293 | var A = function () {}; 294 | _.extend(A, hooks); 295 | A.prototype.set = function (v) { 296 | this.value = v; 297 | }; 298 | A.pre('set', function (next) { 299 | next(undefined); 300 | }); 301 | A.pre('set', function (next) { 302 | next(null); 303 | }); 304 | var a = new A(); 305 | a.set(1); 306 | should.strictEqual(null, a.value); 307 | }, 308 | 'should not run any posts if a pre fails': function () { 309 | var A = function () {}; 310 | _.extend(A, hooks); 311 | A.$hook('save', function () { 312 | this.value = 2; 313 | }, function (err) {}); 314 | A.pre('save', function (next) { 315 | this.value = 1; 316 | next(new Error()); 317 | }).post('save', function (next) { 318 | this.value = 3; 319 | next(); 320 | }); 321 | var a = new A(); 322 | a.save(); 323 | a.value.should.equal(1); 324 | }, 325 | 326 | "can pass the hook's arguments verbatim to pres": function () { 327 | var A = function () {}; 328 | _.extend(A, hooks); 329 | A.$hook('set', function (path, val) { 330 | this[path] = val; 331 | }); 332 | A.pre('set', function (next, path, val) { 333 | path.should.equal('hello'); 334 | val.should.equal('world'); 335 | next(); 336 | }); 337 | var a = new A(); 338 | a.set('hello', 'world'); 339 | a.hello.should.equal('world'); 340 | }, 341 | // "can pass the hook's arguments as an array to pres": function () { 342 | // // Great for dynamic arity - e.g., slice(...) 343 | // var A = function () {}; 344 | // _.extend(A, hooks); 345 | // A.hook('set', function (path, val) { 346 | // this[path] = val; 347 | // }); 348 | // A.pre('set', function (next, hello, world) { 349 | // hello.should.equal('hello'); 350 | // world.should.equal('world'); 351 | // next(); 352 | // }); 353 | // var a = new A(); 354 | // a.set('hello', 'world'); 355 | // assert.equal(a.hello, 'world'); 356 | // }, 357 | "can pass the hook's arguments verbatim to posts": function () { 358 | var A = function () {}; 359 | _.extend(A, hooks); 360 | A.$hook('set', function (path, val) { 361 | this[path] = val; 362 | }); 363 | A.post('set', function (next, path, val) { 364 | path.should.equal('hello'); 365 | val.should.equal('world'); 366 | next(); 367 | }); 368 | var a = new A(); 369 | a.set('hello', 'world'); 370 | assert.equal(a.hello, 'world'); 371 | }, 372 | // "can pass the hook's arguments as an array to posts": function () { 373 | // var A = function () {}; 374 | // _.extend(A, hooks); 375 | // A.hook('set', function (path, val) { 376 | // this[path] = val; 377 | // }); 378 | // A.post('set', function (next, halt, args) { 379 | // assert.equal(args[0], 'hello'); 380 | // assert.equal(args[1], 'world'); 381 | // next(); 382 | // }); 383 | // var a = new A(); 384 | // a.set('hello', 'world'); 385 | // assert.equal(a.hello, 'world'); 386 | // }, 387 | "pres should be able to modify and pass on a modified version of the hook's arguments": function () { 388 | var A = function () {}; 389 | _.extend(A, hooks); 390 | A.$hook('set', function (path, val) { 391 | this[path] = val; 392 | assert.equal(arguments[2], 'optional'); 393 | }); 394 | A.pre('set', function (next, path, val) { 395 | next('foo', 'bar'); 396 | }); 397 | A.pre('set', function (next, path, val) { 398 | assert.equal(path, 'foo'); 399 | assert.equal(val, 'bar'); 400 | next('rock', 'says', 'optional'); 401 | }); 402 | A.pre('set', function (next, path, val, opt) { 403 | assert.equal(path, 'rock'); 404 | assert.equal(val, 'says'); 405 | assert.equal(opt, 'optional'); 406 | next(); 407 | }); 408 | var a = new A(); 409 | a.set('hello', 'world'); 410 | assert.equal(typeof a.hello, 'undefined'); 411 | a.rock.should.equal('says'); 412 | }, 413 | 'posts should see the modified version of arguments if the pres modified them': function () { 414 | var A = function () {}; 415 | _.extend(A, hooks); 416 | A.$hook('set', function (path, val) { 417 | this[path] = val; 418 | }); 419 | A.pre('set', function (next, path, val) { 420 | next('foo', 'bar'); 421 | }); 422 | A.post('set', function (next, path, val) { 423 | path.should.equal('foo'); 424 | val.should.equal('bar'); 425 | }); 426 | var a = new A(); 427 | a.set('hello', 'world'); 428 | assert.equal(typeof a.hello, 'undefined'); 429 | a.foo.should.equal('bar'); 430 | }, 431 | 'should pad missing arguments (relative to expected arguments of the hook) with null': function () { 432 | // Otherwise, with hookFn = function (a, b, next, ), 433 | // if we use hookFn(a), then because the pre functions are of the form 434 | // preFn = function (a, b, next, ), then it actually gets executed with 435 | // preFn(a, next, ), so when we call next() from within preFn, we are actually 436 | // calling () 437 | 438 | var A = function () {}; 439 | _.extend(A, hooks); 440 | A.$hook('set', function (path, val, opts) { 441 | this[path] = val; 442 | }); 443 | A.pre('set', function (next, path, val, opts) { 444 | next('foo', 'bar'); 445 | assert.equal(typeof opts, 'undefined'); 446 | }); 447 | var a = new A(); 448 | a.set('hello', 'world'); 449 | }, 450 | 451 | 'should not invoke the target method until all asynchronous middleware have invoked dones': function () { 452 | var counter = 0; 453 | var A = function () {}; 454 | _.extend(A, hooks); 455 | A.$hook('set', function (path, val) { 456 | counter++; 457 | this[path] = val; 458 | counter.should.equal(7); 459 | }); 460 | A.pre('set', function (next, path, val) { 461 | counter++; 462 | next(); 463 | }); 464 | A.pre('set', true, function (next, done, path, val) { 465 | counter++; 466 | setTimeout(function () { 467 | counter++; 468 | done(); 469 | }, 1000); 470 | next(); 471 | }); 472 | A.pre('set', function (next, path, val) { 473 | counter++; 474 | next(); 475 | }); 476 | A.pre('set', true, function (next, done, path, val) { 477 | counter++; 478 | setTimeout(function () { 479 | counter++; 480 | done(); 481 | }, 500); 482 | next(); 483 | }); 484 | var a = new A(); 485 | a.set('hello', 'world'); 486 | }, 487 | 488 | 'invoking a method twice should run its async middleware twice': function () { 489 | var counter = 0; 490 | var A = function () {}; 491 | _.extend(A, hooks); 492 | A.$hook('set', function (path, val) { 493 | this[path] = val; 494 | if (path === 'hello') counter.should.equal(1); 495 | if (path === 'foo') counter.should.equal(2); 496 | }); 497 | A.pre('set', true, function (next, done, path, val) { 498 | setTimeout(function () { 499 | counter++; 500 | done(); 501 | }, 1000); 502 | next(); 503 | }); 504 | var a = new A(); 505 | a.set('hello', 'world'); 506 | a.set('foo', 'bar'); 507 | }, 508 | 509 | 'calling the same done multiple times should have the effect of only calling it once': function () { 510 | var A = function () { 511 | this.acked = false; 512 | }; 513 | _.extend(A, hooks); 514 | A.$hook('ack', function () { 515 | console.log("UH OH, YOU SHOULD NOT BE SEEING THIS"); 516 | this.acked = true; 517 | }); 518 | A.pre('ack', true, function (next, done) { 519 | next(); 520 | done(); 521 | done(); 522 | }); 523 | A.pre('ack', true, function (next, done) { 524 | next(); 525 | // Notice that done() is not invoked here 526 | }); 527 | var a = new A(); 528 | a.ack(); 529 | setTimeout( function () { 530 | a.acked.should.be.false; 531 | }, 1000); 532 | }, 533 | 534 | 'calling the same next multiple times should have the effect of only calling it once': function (beforeExit) { 535 | var A = function () { 536 | this.acked = false; 537 | }; 538 | _.extend(A, hooks); 539 | A.$hook('ack', function () { 540 | console.log("UH OH, YOU SHOULD NOT BE SEEING THIS"); 541 | this.acked = true; 542 | }); 543 | A.pre('ack', function (next) { 544 | // force a throw to re-exec next() 545 | try { 546 | next(new Error('bam')); 547 | } catch (err) { 548 | next(); 549 | } 550 | }); 551 | A.pre('ack', function (next) { 552 | next(); 553 | }); 554 | var a = new A(); 555 | a.ack(); 556 | beforeExit( function () { 557 | a.acked.should.be.false; 558 | }); 559 | }, 560 | 561 | 'asynchronous middleware should be able to pass an error via `done`, stopping the middleware chain': function () { 562 | var counter = 0; 563 | var A = function () {}; 564 | _.extend(A, hooks); 565 | A.$hook('set', function (path, val, fn) { 566 | counter++; 567 | this[path] = val; 568 | fn(null); 569 | }); 570 | A.pre('set', true, function (next, done, path, val, fn) { 571 | setTimeout(function () { 572 | counter++; 573 | done(new Error); 574 | }, 1000); 575 | next(); 576 | }); 577 | var a = new A(); 578 | a.set('hello', 'world', function (err) { 579 | err.should.be.an.instanceof(Error); 580 | should.strictEqual(undefined, a['hello']); 581 | counter.should.eql(1); 582 | }); 583 | }, 584 | 585 | 'should be able to remove a particular pre': function () { 586 | var A = function () {} 587 | , preTwo; 588 | _.extend(A, hooks); 589 | A.$hook('save', function () { 590 | this.value = 1; 591 | }); 592 | A.pre('save', function (next) { 593 | this.preValueOne = 2; 594 | next(); 595 | }); 596 | A.pre('save', preTwo = function (next) { 597 | this.preValueTwo = 4; 598 | next(); 599 | }); 600 | A.removePre('save', preTwo); 601 | var a = new A(); 602 | a.save(); 603 | a.value.should.equal(1); 604 | a.preValueOne.should.equal(2); 605 | should.strictEqual(undefined, a.preValueTwo); 606 | }, 607 | 608 | 'should be able to remove all pres associated with a hook': function () { 609 | var A = function () {}; 610 | _.extend(A, hooks); 611 | A.$hook('save', function () { 612 | this.value = 1; 613 | }); 614 | A.pre('save', function (next) { 615 | this.preValueOne = 2; 616 | next(); 617 | }); 618 | A.pre('save', function (next) { 619 | this.preValueTwo = 4; 620 | next(); 621 | }); 622 | A.removePre('save'); 623 | var a = new A(); 624 | a.save(); 625 | a.value.should.equal(1); 626 | should.strictEqual(undefined, a.preValueOne); 627 | should.strictEqual(undefined, a.preValueTwo); 628 | }, 629 | 630 | 'should be able to remove a particular post': function () { 631 | var A = function () {} 632 | , postTwo; 633 | _.extend(A, hooks); 634 | A.$hook('save', function () { 635 | this.value = 1; 636 | }); 637 | A.post('save', function (next) { 638 | this.postValueOne = 2; 639 | next(); 640 | }); 641 | A.post('save', postTwo = function (next) { 642 | this.postValueTwo = 4; 643 | next(); 644 | }); 645 | A.removePost('save', postTwo); 646 | var a = new A(); 647 | a.save(); 648 | a.value.should.equal(1); 649 | a.postValueOne.should.equal(2); 650 | should.strictEqual(undefined, a.postValueTwo); 651 | }, 652 | 653 | 'should be able to remove all posts associated with a hook': function () { 654 | var A = function () {}; 655 | _.extend(A, hooks); 656 | A.$hook('save', function () { 657 | this.value = 1; 658 | }); 659 | A.post('save', function (next) { 660 | this.postValueOne = 2; 661 | next(); 662 | }); 663 | A.post('save', function (next) { 664 | this.postValueTwo = 4; 665 | next(); 666 | }); 667 | A.removePost('save'); 668 | var a = new A(); 669 | a.save(); 670 | a.value.should.equal(1); 671 | should.strictEqual(undefined, a.postValueOne); 672 | should.strictEqual(undefined, a.postValueTwo); 673 | }, 674 | 675 | '#pre should lazily make a method hookable': function () { 676 | var A = function () {}; 677 | _.extend(A, hooks); 678 | A.prototype.save = function () { 679 | this.value = 1; 680 | }; 681 | A.pre('save', function (next) { 682 | this.preValue = 2; 683 | next(); 684 | }); 685 | var a = new A(); 686 | a.save(); 687 | a.value.should.equal(1); 688 | a.preValue.should.equal(2); 689 | }, 690 | 691 | '#pre lazily making a method hookable should be able to provide a default errorHandler as the last argument': function () { 692 | var A = function () {}; 693 | var preValue = ""; 694 | _.extend(A, hooks); 695 | A.prototype.save = function () { 696 | this.value = 1; 697 | }; 698 | A.pre('save', function (next) { 699 | next(new Error); 700 | }, function (err) { 701 | preValue = 'ERROR'; 702 | }); 703 | var a = new A(); 704 | a.save(); 705 | should.strictEqual(undefined, a.value); 706 | preValue.should.equal('ERROR'); 707 | }, 708 | 709 | '#post should lazily make a method hookable': function () { 710 | var A = function () {}; 711 | _.extend(A, hooks); 712 | A.prototype.save = function () { 713 | this.value = 1; 714 | }; 715 | A.post('save', function (next) { 716 | this.value = 2; 717 | next(); 718 | }); 719 | var a = new A(); 720 | a.save(); 721 | a.value.should.equal(2); 722 | }, 723 | 724 | "a lazy hooks setup should handle errors via a method's last argument, if it's a callback": function () { 725 | var A = function () {}; 726 | _.extend(A, hooks); 727 | A.prototype.save = function (fn) {}; 728 | A.pre('save', function (next) { 729 | next(new Error("hi there")); 730 | }); 731 | var a = new A(); 732 | a.save( function (err) { 733 | err.should.be.an.instanceof(Error); 734 | }); 735 | }, 736 | 737 | 'should intercept method callbacks for post handlers': function () { 738 | var A = function () {}; 739 | _.extend(A, hooks); 740 | A.$hook('save', function (val, callback) { 741 | this.value = val; 742 | callback(); 743 | }); 744 | A.post('save', function (next) { 745 | assert.equal(a.value, 2); 746 | this.value += 2; 747 | setTimeout(next, 10); 748 | }).post('save', function (next) { 749 | assert.equal(a.value, 4); 750 | this.value += 3; 751 | setTimeout(next, 10); 752 | }).post('save', function (next) { 753 | assert.equal(a.value, 7); 754 | this.value2 = 3; 755 | setTimeout(next, 10); 756 | }); 757 | var a = new A(); 758 | a.save(2, function(){ 759 | assert.equal(a.value, 7); 760 | assert.equal(a.value2, 3); 761 | }); 762 | }, 763 | 764 | 'should handle parallel followed by serial': function () { 765 | var A = function () {}; 766 | _.extend(A, hooks); 767 | A.$hook('save', function (val, callback) { 768 | this.value = val; 769 | callback(); 770 | }); 771 | A.pre('save', true, function(next, done) { 772 | process.nextTick(function() { 773 | done(); 774 | }); 775 | next(); 776 | }).pre('save', function(done) { 777 | process.nextTick(function() { 778 | done(); 779 | }); 780 | }); 781 | var a = new A(); 782 | a.save(2, function(){ 783 | assert.ok(true); 784 | }); 785 | } 786 | }; 787 | --------------------------------------------------------------------------------