Adds pre and post hook functionality to your JavaScript methods.
npm install hooks-fixedhooks
============
Add pre and post middleware hooks to your JavaScript methods.
save method.It would be nice to be able to declare code that runs before save and after save.
For example, you might want to run validation code before every save,
and you might want to dispatch a job to a background job queue after save.
One might have an urge to hard code this all into save, but that turns out to
couple all these pieces of functionality (validation, save, and job creation) more
tightly than is necessary. For example, what if someone does not want to do background
job creation after the logical save?
It is nicer to tack on functionality using what we call pre and post hooks. These
are functions that you define and that you direct to execute before or after particular
methods.
hooks to add validation and background jobs in the following way:``javascript
var hooks = require('hooks')
, Document = require('./path/to/some/document/constructor');
// Add hooks' methods: hook, pre, and post
for (var k in hooks) {
Document[k] = hooks[k];
}
// Define a new method that is able to invoke pre and post middleware
Document.hook('save', Document.prototype.save);
// Define a middleware function to be invoked before 'save'
Document.pre('save', function validate (next) {
// The this context inside of pre and post functions
// is the Document instance
if (this.isValid()) next(); // next() passes control to the next middleware
// or to the target method itself
else next(new Error("Invalid")); // next(error) invokes an error callback
});
// Define a middleware function to be invoked after 'save'
Document.post('save', function createJob (next) {
this.sendToBackgroundQueue();
next();
});
`
If you already have defined Document.prototype methods for which you want pres and posts,Document.hook(...)
then you do not need to explicitly invoke . Invoking Document.pre(methodName, fn)Document.post(methodName, fn)
or will automatically and lazily change Document.prototype[methodName]hooks
so that it plays well with . An equivalent way to implement the previous example is:
`javascript
var hooks = require('hooks')
, Document = require('./path/to/some/document/constructor');
// Add hooks' methods: hook, pre, and post
for (var k in hooks) {
Document[k] = hooks[k];
}
Document.prototype.save = function () {
// ...
};
// Define a middleware function to be invoked before 'save'
Document.pre('save', function validate (next) {
// The this context inside of pre and post functions
// is the Document instance
if (this.isValid()) next(); // next() passes control to the next middleware
// or to the target method itself
else next(new Error("Invalid")); // next(error) invokes an error callback
});
// Define a middleware function to be invoked after 'save'
Document.post('save', function createJob (next) {
this.sendToBackgroundQueue();
next();
});
`
1. You can define multiple pres (or posts) for a single method.
2. These pres (or posts) are then executed as a chain of methods.
3. Any functions in this middleware chain can choose to halt the chain's execution by nexting 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.
and post are chainable, so you can define multiple via:
`javascript
Document.pre('save', function (next) {
console.log("hello");
next();
}).pre('save', function (next) {
console.log("world");
next();
});Document.post('save', function (next) {
console.log("hello");
next();
}).post('save', function (next) {
console.log("world");
next();
});
`As soon as one pre finishes executing, the next one will be invoked, and so on.
Error Handling
You can define a default error handler by passing a 2nd function as the 3rd argument to hook:
`javascript
Document.hook('set', function (path, val) {
this[path] = val;
}, function (err) {
// Handler the error here
console.error(err);
});
`Then, we can pass errors to this handler from a pre or post middleware function:
`javascript
Document.pre('set', function (next, path, val) {
next(new Error());
});
`If you do not set up a default handler, then
hooks makes the default handler that just throws the Error.The default error handler can be over-rided on a per method invocation basis.
If the main method that you are surrounding with pre and post middleware expects its last argument to be a function
with callback signature
function (error, ...), then that callback becomes the error handler, over-riding the default
error handler you may have set up.`javascript
Document.hook('save', function (callback) {
// Save logic goes here
...
});var doc = new Document();
doc.save( function (err, saved) {
// We can pass err via
next in any of our pre or post middleware functions
if (err) console.error(err); // Rest of callback logic follows ...
});
`Mutating Arguments via Middleware
pre and post middleware can also accept the intended arguments for the method
they augment. This is useful if you want to mutate the arguments before passing
them along to the next middleware and eventually pass a mutated arguments list to
the main method itself.As a simple example, let's define a method
set that just sets a key, value pair.
If we want to namespace the key, we can do so by adding a pre middleware hook
that runs before set, alters the arguments by namespacing the key argument, and passes them onto set:`javascript
Document.hook('set', function (key, val) {
this[key] = val;
});
Document.pre('set', function (next, key, val) {
next('namespace-' + key, val);
});
var doc = new Document();
doc.set('hello', 'world');
console.log(doc.hello); // undefined
console.log(doc['namespace-hello']); // 'world'
`As you can see above, we pass arguments via
next.If you are not mutating the arguments, then you can pass zero arguments
to
next, and the next middleware function will still have access
to the arguments.`javascript
Document.hook('set', function (key, val) {
this[key] = val;
});
Document.pre('set', function (next, key, val) {
// I have access to key and val here
next(); // We don't need to pass anything to next
});
Document.pre('set', function (next, key, val) {
// And I still have access to the original key and val here
next();
});
`Finally, you can add arguments that downstream middleware can also see:
`javascript
// Note that in the definition of set, there is no 3rd argument, options
Document.hook('set', function (key, val) {
// But...
var options = arguments[2]; // ...I have access to an options argument
// because of pre function pre2 (defined below)
console.log(options); // '{debug: true}'
this[key] = val;
});
Document.pre('set', function pre1 (next, key, val) {
// I only have access to key and val arguments
console.log(arguments.length); // 3
next(key, val, {debug: true});
});
Document.pre('set', function pre2 (next, key, val, options) {
console.log(arguments.length); // 4
console.log(options); // '{ debug: true}'
next();
});
Document.pre('set', function pre3 (next, key, val, options) {
// I still have access to key, val, AND the options argument introduced via the preceding middleware
console.log(arguments.length); // 4
console.log(options); // '{ debug: true}'
next();
});var doc = new Document()
doc.set('hey', 'there');
`Post middleware
Post middleware intercepts the callback originally sent to the asynchronous function you have hooked to.
This means that the following chain of execution will occur in a typical
save operation:(1) doc.save -> (2) pre --(next)--> (3) save calls back -> (4) post --(next)--> (5) targetFn
Illustrated below:
`
Document.pre('save', function (next) {
this.key = "value";
next();
});
// Post handler occurs before set calls back. This is useful if we need to grab something
// async before set finishes.
Document.post('set', function (next) {
var me = this;
getSomethingAsync(function(value){ // let's assume it returns "Hello Async"
me.key2 = value;
next();
});
});var doc = new Document();
doc.save(function(err){
console.log(this.key); // "value" - this value was saved
console.log(this.key2); // "Hello Async" - this value was not saved
}
`Post middleware must call
next() or execution will stop.Parallel
pre middlewareAll middleware up to this point has been "serial" middleware -- i.e., middleware whose logic
is executed as a serial chain.
Some scenarios call for parallel middleware -- i.e., middleware that can wait for several
asynchronous services at once to respond.
For instance, you may only want to save a Document only after you have checked
that the Document is valid according to two different remote services.
We accomplish asynchronous middleware by adding a second kind of flow control callback
(the only flow control callback so far has been
next), called done.-
next passes control to the next middleware in the chain
- done keeps track of how many parallel middleware have invoked done and passes
control to the target method when ALL parallel middleware have invoked done. If
you pass an Error to done, then the error is handled, and the main method that is
wrapped by pres and posts will not get invoked.We declare pre middleware that is parallel by passing a 3rd boolean argument to our
pre
definition method.We illustrate via the parallel validation example mentioned above:
`javascript
Document.hook('save', function targetFn (callback) {
// Save logic goes here
// ...
// This only gets run once the two dones are both invoked via preOne and preTwo.
}); // true marks this as parallel middleware
Document.pre('save', true, function preOne (next, doneOne, callback) {
remoteServiceOne.validate(this.serialize(), function (err, isValid) {
// The code in here will probably be run after the
next below this block
// and could possibly be run after the console.log("Hola") in preTwo// We will suppose that we need 2 different remote services to validate our document
Document.pre('save', true, function preTwo (next, doneTwo, callback) {
remoteServiceTwo.validate(this.serialize(), function (err, isValid) {
if (err) return doneTwo(err);
if (isValid) doneTwo();
});
next();
});
// While preOne and preTwo are parallel, preThree is a serial pre middleware
Document.pre('save', function preThree (next, callback) {
next();
});
var doc = new Document();
doc.save( function (err, doc) {
// Do stuff with the saved doc here...
});
``
In the above example, flow control may happen in the following way:
(1) doc.save -> (2) preOne --(next)--> (3) preTwo --(next)--> (4) preThree --(next)--> (wait for dones to invoke) -> (5) doneTwo -> (6) doneOne -> (7) targetFn
So what's happening is that:
1. You call doc.save(...)next()
2. First, your preOne middleware gets executed. It makes a remote call to the validation service and s to the preTwo middleware.next()
3. Now, your preTwo middleware gets executed. It makes a remote call to another validation service and s to the preThree middleware.next()
4. Your preThree middleware gets executed. It immediately s. But nothing else gets executing until both doneOne and doneTwo are invoked inside the callbacks handling the response from the two valiation services.doneTwo
5. We will suppose that validation remoteServiceTwo returns a response to us first. In this case, we call inside the callback to remoteServiceTwo.doneOne
6. Some fractions of a second later, remoteServiceOne returns a response to us. In this case, we call inside the callback to remoteServiceOne.hooks
7. 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).
You can remove a particular pre associated with a hook:
Document.pre('set', someFn);
Document.removePre('set', someFn);
And you can also remove all pres associated with a hook:
Document.removePre('set'); // Removes all declared pre`s on the hook 'set'
---