Add context propagation middleware

- Implement the middleware `loopback.context`
 - Inject context into juggler and strong-remoting
 - Make http context optional and default to false
 - Optionally mount context middleware from `loopback.rest`
This commit is contained in:
Raymond Feng 2014-06-17 10:35:19 -07:00 committed by Miroslav Bajtoš
parent 0e35c1877c
commit 246f38c05d
5 changed files with 150 additions and 1 deletions

View File

@ -18,6 +18,10 @@ CartItem.sum = function(cartId, callback) {
return prev + cur;
}, 0);
var ns = loopback.getCurrentContext();
if (ns && ns.get('http')) {
console.log('Remote call via url: %s', ns.get('http').req.url);
}
callback(null, total);
});
}

View File

@ -5,6 +5,12 @@ var memory = loopback.createDataSource({
connector: loopback.Memory
});
server.use(loopback.context());
server.use(function(req, res, next) {
loopback.getCurrentContext().set('http', {req: req, res: res});
next();
});
server.use(loopback.rest());
server.model(CartItem);

95
lib/middleware/context.js Normal file
View File

@ -0,0 +1,95 @@
var loopback = require('../loopback');
var juggler = require('loopback-datasource-juggler');
var remoting = require('strong-remoting');
var cls = require('continuation-local-storage');
module.exports = context;
var name = 'loopback';
function context(options) {
options = options || {};
var scope = options.name || name;
var enableHttpContext = options.enableHttpContext || false;
var ns = cls.createNamespace(scope);
// Make the namespace globally visible via the process.context property
process.context = process.context || {};
process.context[scope] = ns;
// Set up loopback.getCurrentContext()
loopback.getCurrentContext = function() {
return ns;
};
chain(juggler);
chain(remoting);
// Return the middleware
return function(req, res, next) {
// Bind req/res event emitters to the given namespace
ns.bindEmitter(req);
ns.bindEmitter(res);
// Create namespace for the request context
ns.run(function(context) {
// Run the code in the context of the namespace
if(enableHttpContext) {
ns.set('http', {req: req, res: res}); // Set up the transport context
}
next();
});
};
}
/**
* Create a chained context
* @param {Object} child The child context
* @param {Object} parent The parent context
* @private
* @constructor
*/
function ChainedContext(child, parent) {
this.child = child;
this.parent = parent;
}
/**
* Get the value by name from the context. If it doesn't exist in the child
* context, try the parent one
* @param {String} name Name of the context property
* @returns {*} Value of the context property
*/
ChainedContext.prototype.get = function (name) {
var val = this.child && this.child.get(name);
if (val === undefined) {
return this.parent && this.parent.get(name);
}
};
ChainedContext.prototype.set = function (name, val) {
if (this.child) {
return this.child.set(name, val);
} else {
return this.parent && this.parent.set(name, val);
}
};
ChainedContext.prototype.reset = function (name, val) {
if (this.child) {
return this.child.reset(name, val);
} else {
return this.parent && this.parent.reset(name, val);
}
};
function chain(child) {
if (typeof child.getCurrentContext === 'function') {
var childContext = new ChainedContext(child.getCurrentContext(),
loopback.getCurrentContext());
child.getCurrentContext = function() {
return childContext;
};
} else {
child.getCurrentContext = loopback.getCurrentContext;
}
}

View File

@ -46,7 +46,8 @@
"strong-remoting": "^2.4.0",
"uid2": "0.0.3",
"underscore": "~1.7.0",
"underscore.string": "~2.3.3"
"underscore.string": "~2.3.3",
"continuation-local-storage": "~3.1.1"
},
"peerDependencies": {
"loopback-datasource-juggler": "^2.8.0"

View File

@ -130,6 +130,49 @@ describe('loopback.rest', function() {
});
});
it('should pass req to remote method via context', function(done) {
var User = givenUserModelWithAuth();
User.getToken = function(cb) {
var context = loopback.getCurrentContext();
var req = context.get('http').req;
expect(req).to.have.property('accessToken');
var juggler = require('loopback-datasource-juggler');
expect(juggler.getCurrentContext().get('http').req)
.to.have.property('accessToken');
var remoting = require('strong-remoting');
expect(remoting.getCurrentContext().get('http').req)
.to.have.property('accessToken');
cb(null, req && req.accessToken ? req.accessToken.id : null);
};
// Set up the ACL
User.settings.acls.push({principalType: 'ROLE',
principalId: '$authenticated', permission: 'ALLOW', property: 'getToken'});
loopback.remoteMethod(User.getToken, {
accepts: [],
returns: [{ type: 'object', name: 'id' }]
});
app.use(loopback.context({enableHttpContext: true}));
app.enableAuth();
app.use(loopback.rest());
givenLoggedInUser(function(err, token) {
if (err) return done(err);
request(app).get('/users/getToken')
.set('Authorization', token.id)
.expect(200)
.end(function(err, res) {
if (err) return done(err);
expect(res.body.id).to.equal(token.id);
done();
});
});
});
function givenUserModelWithAuth() {
// NOTE(bajtos) It is important to create a custom AccessToken model here,
// in order to overwrite the entry created by previous tests in