Skip to content

Commit

Permalink
Merge pull request #190 from strongloop/feature/after-remote-error-hook
Browse files Browse the repository at this point in the history
Implement "afterError" hook
  • Loading branch information
bajtos committed Apr 2, 2015
2 parents f73aab4 + 8ca20b4 commit 1de9813
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 3 deletions.
58 changes: 55 additions & 3 deletions lib/remote-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,50 @@ RemoteObjects.prototype.after = function(methodMatch, fn) {
this.on('after.' + methodMatch, fn);
};

/**
* Execute the given `hook` function after the method matched by the method
* string failed.
*
* **Examples:**
*
* ```js
* // Do something after the `speak` instance method failed.
* remotes.afterError('dog.prototype.speak', function(ctx, next) {
* console.log('Cannot speak!', ctx.error);
* next();
* });
*
* // Do something before all methods.
* remotes.afterError('**', function(ctx, next, method) {
* console.log('Failed', method.name, ctx.error);
* next();
* });
*
* // Modify all returned errors
* remotes.after('**', function(ctx, next) {
* if (!ctx.error.details) ctx.result.details = {};
* ctx.error.details.info = 'intercepted by a hook';
* next();
* });
*
* // Report a different error
* remotes.after('dog.prototype.speak', function(ctx, next) {
* console.error(ctx.error);
* next(new Error('See server console log for details.'));
* });
* ```
*
* @param {String} methodMatch The glob to match a method string
* @callback {Function} hook
* @param {Context} ctx The adapter specific context
* @param {Function} next Call with an optional error object
* @param {SharedMethod} method The SharedMethod object
*/

RemoteObjects.prototype.afterError = function(methodMatch, fn) {
this.on('afterError.' + methodMatch, fn);
};

/*!
* Create a middleware style emit that supports wildcards.
*/
Expand Down Expand Up @@ -546,17 +590,25 @@ RemoteObjects.prototype.invokeMethodInContext = function(ctx, method, cb) {
var scope = this.getScope(ctx, method);

self.execHooks('before', method, scope, ctx, function(err) {
if (err) return cb(err);
if (err) return triggerErrorAndCallBack(err);

ctx.invoke(scope, method, function(err, result) {
if (err) return cb(err);
if (err) return triggerErrorAndCallBack(err);

ctx.result = result;
self.execHooks('after', method, scope, ctx, function(err) {
if (err) return cb(err);
if (err) return triggerErrorAndCallBack(err);
cb();
});
});
});

function triggerErrorAndCallBack(err) {
ctx.error = err;
self.execHooks('afterError', method, scope, ctx, function(hookErr) {
cb(hookErr || err);
});
}
};

/**
Expand Down
75 changes: 75 additions & 0 deletions test/rest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ var factory = require('./helpers/shared-objects-factory.js');
var Promise = global.Promise || require('bluebird');

var ACCEPT_XML_OR_ANY = 'application/xml,*/*;q=0.8';
var TEST_ERROR = new Error('expected test error');


describe('strong-remoting-rest', function() {
var app;
Expand Down Expand Up @@ -2128,4 +2130,77 @@ describe('strong-remoting-rest', function() {
expect(methodNames.length).to.equal(1);
done();
});

describe('afterError hook', function() {
it('should be called when the method fails', function(done) {
var method = givenSharedStaticMethod(function(cb) {
cb(TEST_ERROR);
});

verifyErrorHookIsCalled(method, TEST_ERROR, done);
});

it('should be called when a "before" hook fails', function(done) {
var method = givenSharedStaticMethod();

objects.before(method.name, function(ctx, next) {
next(TEST_ERROR);
});

verifyErrorHookIsCalled(method, TEST_ERROR, done);
});

it('should be called when an "after" hook fails', function(done) {
var method = givenSharedStaticMethod();

objects.after(method.name, function(ctx, next) {
next(TEST_ERROR);
});

verifyErrorHookIsCalled(method, TEST_ERROR, done);
});

it('can replace the error object', function(done) {
var method = givenSharedStaticMethod(function(cb) {
cb(new Error(
'error from the method, should have been shadowed by the hook'));
});
objects.afterError(method.name, function(ctx, next) {
next(new Error('error from the hook'));
});

json(method.url)
.expect(500)
.end(function(err, res) {
if (err) return done(err);
expect(res.body.error.message).to.equal('error from the hook');
done();
});
});

function verifyErrorHookIsCalled(method, expectedError, done) {
var hookContext = 'hook not called';

objects.afterError(method.name, function(ctx, next) {
if (Array.isArray(hookContext)) {
hookContext.push(context);
} else if (typeof hookContext === 'object') {
hookContext = [hookContext, ctx];
} else {
hookContext = ctx;
}
ctx.error.hookData = true;
next();
});

json(method.url)
.expect(500)
.end(function(err, res) {
if (err) return done(err);
expect(res.body.error).to.have.property('hookData', true);
expect(hookContext).to.have.property('error', expectedError);
done();
});
}
});
});

0 comments on commit 1de9813

Please sign in to comment.