From 246f38c05d92d1d6fb691fa134719f9b51793ccd Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Tue, 17 Jun 2014 10:35:19 -0700 Subject: [PATCH] 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` --- example/client-server/models.js | 4 ++ example/client-server/server.js | 6 +++ lib/middleware/context.js | 95 +++++++++++++++++++++++++++++++++ package.json | 3 +- test/rest.middleware.test.js | 43 +++++++++++++++ 5 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 lib/middleware/context.js diff --git a/example/client-server/models.js b/example/client-server/models.js index 34d5c8ba..60285dd7 100644 --- a/example/client-server/models.js +++ b/example/client-server/models.js @@ -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); }); } diff --git a/example/client-server/server.js b/example/client-server/server.js index 7e466a56..6663f541 100644 --- a/example/client-server/server.js +++ b/example/client-server/server.js @@ -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); diff --git a/lib/middleware/context.js b/lib/middleware/context.js new file mode 100644 index 00000000..aef8260a --- /dev/null +++ b/lib/middleware/context.js @@ -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; + } +} diff --git a/package.json b/package.json index f0d385b0..71276842 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/test/rest.middleware.test.js b/test/rest.middleware.test.js index 56b6d523..ac0ced76 100644 --- a/test/rest.middleware.test.js +++ b/test/rest.middleware.test.js @@ -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