Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 66 additions & 17 deletions lib/remote-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ var inherits = util.inherits;
var assert = require('assert');
var Dynamic = require('./dynamic');
var SharedClass = require('./shared-class');
var SharedMethod = require('./shared-method');
var ExportsHelper = require('./exports-helper');
var PhaseList = require('loopback-phase').PhaseList;

// require the rest adapter for browserification
// TODO(ritch) remove this somehow...?
Expand Down Expand Up @@ -54,6 +56,8 @@ function RemoteObjects(options) {
this.options = options || {};
this.exports = this.options.exports || {};
this._classes = {};

this._setupPhases();
}

/*!
Expand Down Expand Up @@ -382,6 +386,29 @@ RemoteObjects.prototype.afterError = function(methodMatch, fn) {
this.on('afterError.' + methodMatch, fn);
};

RemoteObjects.prototype.registerPhaseHandler = function(phaseName,
methodNameWildcard,
handler) {
var pattern = methodNameWildcard.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
// single star matches one segment only
.replace(/(^|\.)\*($|\.)/g, '$1[^.]*$2')
// double-start match one or more segments
.replace(/(^|\.)\*\*($|\.)/g, '$1.*$2');
var matcher = new RegExp('^' + pattern + '$');

debug('registerPhaseHandler(%j) -> pattern %j',
methodNameWildcard,
pattern);

this.phases.registerHandler(phaseName, function matchHandler(ctx, next) {
if (matcher.test(ctx.method.stringName)) {
handler(ctx, next);
} else {
next();
}
});
};

/*!
* Create a middleware style emit that supports wildcards.
*/
Expand Down Expand Up @@ -600,35 +627,57 @@ RemoteObjects.prototype._executeAuthorizationHook = function(ctx, cb) {
}
};

RemoteObjects.prototype._setupPhases = function() {
var self = this;
self.phases = new PhaseList();
var auth = self.phases.add('auth');
var invoke = self.phases.add('invoke');

auth.use(function phaseAuthorization(ctx, next) {
self._executeAuthorizationHook(ctx, next);
});

invoke.before(function phaseBeforeInvoke(ctx, next) {
self.execHooks('before', ctx.method, ctx.getScope(), ctx, next);
});

invoke.use(function phaseInvoke(ctx, next) {
ctx.invoke(ctx.getScope(), ctx.method, function(err, result) {
if (!err) ctx.result = result;
next(err);
});
});

invoke.after(function phaseAfterInvoke(ctx, next) {
self.execHooks('after', ctx.method, ctx.getScope(), ctx, next);
});
};

/**
* Invoke the given shared method using the supplied context.
* Execute registered before/after hooks.
*
* @param {Object} ctx
* @param {Object} method
* @param {function(Error=)} cb
*/
RemoteObjects.prototype.invokeMethodInContext = function(ctx, method, cb) {
var self = this;

var scope = ctx.getScope();

self._executeAuthorizationHook(ctx, function(err) {
if (err) return triggerErrorAndCallBack(err);

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

ctx.invoke(scope, method, function(err, result) {
if (err) return triggerErrorAndCallBack(err);
if (cb === undefined && typeof method === 'function') {
// the new API with two arguments
cb = method;
method = ctx.method;
} else {
// backwards compatibility: invokeMethodInContext(ctx, method, cb)
// TODO remove in v3.0
assert.equal(method, ctx.method);
deprecated('invokeMethodInContext(ctx, method, cb) is deprecated.' +
'Pass the method as ctx.method instead.');
}

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

function triggerErrorAndCallBack(err) {
ctx.error = err;
Expand Down
2 changes: 1 addition & 1 deletion lib/rest-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ RestAdapter.prototype._invokeMethod = function(ctx, method, next) {
}

steps.push(
this.remotes.invokeMethodInContext.bind(this.remotes, ctx, method)
this.remotes.invokeMethodInContext.bind(this.remotes, ctx)
);

if (method.rest.after) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"inflection": "^1.7.1",
"jayson": "^1.2.0",
"js2xmlparser": "^0.1.9",
"loopback-phase": "^1.3.0",
"mux-demux": "^3.7.9",
"qs": "^2.4.2",
"request": "^2.55.0",
Expand Down
131 changes: 131 additions & 0 deletions test/phase-handlers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
var expect = require('chai').expect;
var express = require('express');
var RemoteObjects = require('../');
var User = require('./e2e/fixtures/user');

describe('phase handlers', function() {
var server, remotes, clientRemotes;

beforeEach(function setupServer(done) {
var app = express();
remotes = RemoteObjects.create();
remotes.exports.User = User;
app.use(function(req, res, next) {
// always build a new handler to pick new methods added by tests
remotes.handler('rest')(req, res, next);
});
server = app.listen(0, '127.0.0.1', done);
});

beforeEach(function setupClient() {
clientRemotes = RemoteObjects.create();
clientRemotes.exports.User = User;
var url = 'http://127.0.0.1:' + server.address().port;
clientRemotes.connect(url, 'rest');
});

afterEach(function teardownServer(done) {
server.close(done);
});

it('has built-in phases "auth" and "invoke"', function() {
expect(remotes.phases.getPhaseNames()).to.eql(['auth', 'invoke']);
});

it('invokes phases in the correct order', function(done) {
var phasesRun = [];
var pushNameAndNext = function(name) {
return function(ctx, next) { phasesRun.push(name); next(); };
};

remotes.phases.find('auth').use(pushNameAndNext('phaseHandler-auth'));
var invokePhase = remotes.phases.find('invoke');
invokePhase.before(pushNameAndNext('phaseHandler-invoke:before'));
invokePhase.use(pushNameAndNext('phaseHandler-invoke:use'));
invokePhase.after(pushNameAndNext('phaseHandler-invoke:after'));

remotes.authorization = pushNameAndNext('hook-authorization');
remotes.before('**', pushNameAndNext('hook-remotes.before'));
remotes.after('**', pushNameAndNext('hook-remotes.after'));

User.pushName = function(cb) { phasesRun.push('invoke method'); cb(); };
User.pushName.shared = true;

invokeRemote('User.pushName', function(err) {
if (err) return done(err);
expect(phasesRun).to.eql([
'hook-authorization',
'phaseHandler-auth',
'hook-remotes.before',
'phaseHandler-invoke:before',
'invoke method',
'phaseHandler-invoke:use',
'hook-remotes.after',
'phaseHandler-invoke:after',
]);
done();
});
});

describe('registerPhaseHandler', function() {
var handlersRun;

beforeEach(function() {
User.static = function(cb) { cb(); };
User.static.shared = true;

User.prototype.proto = function(cb) { cb(); };
User.prototype.proto.shared = true;

handlersRun = [];
function register(wildcard) {
remotes.registerPhaseHandler('invoke', wildcard, function(ctx, next) {
handlersRun.push(wildcard);
next();
});
}

register('**');
register('User.**');

register('User.*');
register('User.static');
register('User.proto'); // does not exist

register('User.prototype.*');
register('User.prototype.proto');
register('User.prototype.static'); // does not exist
});

it('matches static methods using wildcards', function(done) {
invokeRemote('User.static', function(err) {
if (err) return done(err);
expect(handlersRun).to.eql([
'**',
'User.**',
'User.*',
'User.static',
]);
done();
});
});

it('matches prototype methods using wildcards', function(done) {
invokeRemote('User.prototype.proto', function(err) {
if (err) return done(err);
expect(handlersRun).to.eql([
'**',
'User.**',
'User.prototype.*',
'User.prototype.proto',
]);
done();
});
});
});

function invokeRemote(method, callback) {
var args = [];
clientRemotes.invoke(method, args, callback);
}
});