Skip to content

Commit 8c8a957

Browse files
committed
Merge pull request #337 from strongloop/feature/context-propagation
Add context propagation middleware
2 parents 0e35c18 + 4fdcbd1 commit 8c8a957

File tree

5 files changed

+261
-18
lines changed

5 files changed

+261
-18
lines changed

example/context/app.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
var loopback = require('../../');
2+
var app = loopback();
3+
4+
// Create a LoopBack context for all requests
5+
app.use(loopback.context());
6+
7+
// Store a request property in the context
8+
app.use(function saveHostToContext(req, res, next) {
9+
var ns = loopback.getCurrentContext();
10+
ns.set('host', req.host);
11+
next();
12+
});
13+
14+
app.use(loopback.rest());
15+
16+
var Color = loopback.createModel('color', { 'name': String });
17+
Color.beforeRemote('**', function (ctx, unused, next) {
18+
// Inside LoopBack code, you can read the property from the context
19+
var ns = loopback.getCurrentContext();
20+
console.log('Request to host', ns && ns.get('host'));
21+
next();
22+
});
23+
24+
app.dataSource('db', { connector: 'memory' });
25+
app.model(Color, { dataSource: 'db' });
26+
27+
app.listen(3000, function() {
28+
console.log('A list of colors is available at http://localhost:3000/colors');
29+
});

lib/middleware/context.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
var loopback = require('../loopback');
2+
var juggler = require('loopback-datasource-juggler');
3+
var remoting = require('strong-remoting');
4+
var cls = require('continuation-local-storage');
5+
6+
module.exports = context;
7+
8+
var name = 'loopback';
9+
10+
function createContext(scope) {
11+
// Make the namespace globally visible via the process.context property
12+
process.context = process.context || {};
13+
var ns = process.context[scope];
14+
if (!ns) {
15+
ns = cls.createNamespace(scope);
16+
process.context[scope] = ns;
17+
// Set up loopback.getCurrentContext()
18+
loopback.getCurrentContext = function() {
19+
return ns;
20+
};
21+
22+
chain(juggler);
23+
chain(remoting);
24+
}
25+
return ns;
26+
}
27+
28+
function context(options) {
29+
options = options || {};
30+
var scope = options.name || name;
31+
var enableHttpContext = options.enableHttpContext || false;
32+
var ns = createContext(scope);
33+
// Return the middleware
34+
return function contextHandler(req, res, next) {
35+
if (req.loopbackContext) {
36+
return next();
37+
}
38+
req.loopbackContext = ns;
39+
// Bind req/res event emitters to the given namespace
40+
ns.bindEmitter(req);
41+
ns.bindEmitter(res);
42+
// Create namespace for the request context
43+
ns.run(function processRequestInContext(context) {
44+
// Run the code in the context of the namespace
45+
if (enableHttpContext) {
46+
ns.set('http', {req: req, res: res}); // Set up the transport context
47+
}
48+
next();
49+
});
50+
};
51+
}
52+
53+
/**
54+
* Create a chained context
55+
* @param {Object} child The child context
56+
* @param {Object} parent The parent context
57+
* @private
58+
* @constructor
59+
*/
60+
function ChainedContext(child, parent) {
61+
this.child = child;
62+
this.parent = parent;
63+
}
64+
65+
/**
66+
* Get the value by name from the context. If it doesn't exist in the child
67+
* context, try the parent one
68+
* @param {String} name Name of the context property
69+
* @returns {*} Value of the context property
70+
*/
71+
ChainedContext.prototype.get = function(name) {
72+
var val = this.child && this.child.get(name);
73+
if (val === undefined) {
74+
return this.parent && this.parent.get(name);
75+
}
76+
};
77+
78+
ChainedContext.prototype.set = function(name, val) {
79+
if (this.child) {
80+
return this.child.set(name, val);
81+
} else {
82+
return this.parent && this.parent.set(name, val);
83+
}
84+
};
85+
86+
ChainedContext.prototype.reset = function(name, val) {
87+
if (this.child) {
88+
return this.child.reset(name, val);
89+
} else {
90+
return this.parent && this.parent.reset(name, val);
91+
}
92+
};
93+
94+
function chain(child) {
95+
if (typeof child.getCurrentContext === 'function') {
96+
var childContext = new ChainedContext(child.getCurrentContext(),
97+
loopback.getCurrentContext());
98+
child.getCurrentContext = function() {
99+
return childContext;
100+
};
101+
} else {
102+
child.getCurrentContext = loopback.getCurrentContext;
103+
}
104+
}

lib/middleware/rest.js

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
var loopback = require('../loopback');
6+
var async = require('async');
67

78
/*!
89
* Export the middleware.
@@ -22,36 +23,43 @@ module.exports = rest;
2223
*/
2324

2425
function rest() {
25-
var tokenParser = null;
26-
return function(req, res, next) {
26+
return function restApiHandler(req, res, next) {
2727
var app = req.app;
28-
var handler = app.handler('rest');
28+
var restHandler = app.handler('rest');
2929

3030
if (req.url === '/routes') {
31-
res.send(handler.adapter.allRoutes());
31+
return res.send(restHandler.adapter.allRoutes());
3232
} else if (req.url === '/models') {
3333
return res.send(app.remotes().toJSON());
34-
} else if (app.isAuthEnabled) {
35-
if (!tokenParser) {
34+
}
35+
36+
var preHandlers;
37+
38+
if (!preHandlers) {
39+
preHandlers = [];
40+
var remotingOptions = app.get('remoting') || {};
41+
42+
var contextOptions = remotingOptions.context;
43+
if (contextOptions !== false) {
44+
if (typeof contextOptions !== 'object')
45+
contextOptions = {};
46+
preHandlers.push(loopback.context(contextOptions));
47+
}
48+
49+
if (app.isAuthEnabled) {
3650
// NOTE(bajtos) It would be better to search app.models for a model
3751
// of type AccessToken instead of searching all loopback models.
3852
// Unfortunately that's not supported now.
3953
// Related discussions:
4054
// https://github.com/strongloop/loopback/pull/167
4155
// https://github.com/strongloop/loopback/commit/f07446a
4256
var AccessToken = loopback.getModelByType(loopback.AccessToken);
43-
tokenParser = loopback.token({ model: AccessToken });
57+
preHandlers.push(loopback.token({ model: AccessToken }));
4458
}
45-
46-
tokenParser(req, res, function(err) {
47-
if (err) {
48-
next(err);
49-
} else {
50-
handler(req, res, next);
51-
}
52-
});
53-
} else {
54-
handler(req, res, next);
5559
}
60+
61+
async.eachSeries(preHandlers.concat(restHandler), function(handler, done) {
62+
handler(req, res, done);
63+
}, next);
5664
};
5765
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
"strong-remoting": "^2.4.0",
4747
"uid2": "0.0.3",
4848
"underscore": "~1.7.0",
49-
"underscore.string": "~2.3.3"
49+
"underscore.string": "~2.3.3",
50+
"continuation-local-storage": "~3.1.1"
5051
},
5152
"peerDependencies": {
5253
"loopback-datasource-juggler": "^2.8.0"

test/rest.middleware.test.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,107 @@ describe('loopback.rest', function() {
130130
});
131131
});
132132

133+
describe('context propagation', function() {
134+
var User;
135+
136+
beforeEach(function() {
137+
User = givenUserModelWithAuth();
138+
User.getToken = function(cb) {
139+
var context = loopback.getCurrentContext();
140+
var req = context.get('http').req;
141+
expect(req).to.have.property('accessToken');
142+
143+
var juggler = require('loopback-datasource-juggler');
144+
expect(juggler.getCurrentContext().get('http').req)
145+
.to.have.property('accessToken');
146+
147+
var remoting = require('strong-remoting');
148+
expect(remoting.getCurrentContext().get('http').req)
149+
.to.have.property('accessToken');
150+
151+
cb(null, req && req.accessToken ? req.accessToken.id : null);
152+
};
153+
// Set up the ACL
154+
User.settings.acls.push({principalType: 'ROLE',
155+
principalId: '$authenticated', permission: 'ALLOW',
156+
property: 'getToken'});
157+
158+
loopback.remoteMethod(User.getToken, {
159+
accepts: [],
160+
returns: [
161+
{ type: 'object', name: 'id' }
162+
]
163+
});
164+
});
165+
166+
function invokeGetToken(done) {
167+
givenLoggedInUser(function(err, token) {
168+
if (err) return done(err);
169+
request(app).get('/users/getToken')
170+
.set('Authorization', token.id)
171+
.expect(200)
172+
.end(function(err, res) {
173+
if (err) return done(err);
174+
expect(res.body.id).to.equal(token.id);
175+
done();
176+
});
177+
});
178+
}
179+
180+
it('should enable context using loopback.context', function(done) {
181+
app.use(loopback.context({ enableHttpContext: true }));
182+
app.enableAuth();
183+
app.use(loopback.rest());
184+
185+
invokeGetToken(done);
186+
});
187+
188+
it('should enable context with loopback.rest', function(done) {
189+
app.enableAuth();
190+
app.set('remoting', { context: { enableHttpContext: true } });
191+
app.use(loopback.rest());
192+
193+
invokeGetToken(done);
194+
});
195+
196+
it('should support explicit context', function(done) {
197+
app.enableAuth();
198+
app.use(loopback.context());
199+
app.use(loopback.token(
200+
{ model: loopback.getModelByType(loopback.AccessToken) }));
201+
app.use(function(req, res, next) {
202+
loopback.getCurrentContext().set('accessToken', req.accessToken);
203+
next();
204+
});
205+
app.use(loopback.rest());
206+
207+
User.getToken = function(cb) {
208+
var context = loopback.getCurrentContext();
209+
var accessToken = context.get('accessToken');
210+
expect(context.get('accessToken')).to.have.property('id');
211+
212+
var juggler = require('loopback-datasource-juggler');
213+
context = juggler.getCurrentContext();
214+
expect(context.get('accessToken')).to.have.property('id');
215+
216+
var remoting = require('strong-remoting');
217+
context = remoting.getCurrentContext();
218+
expect(context.get('accessToken')).to.have.property('id');
219+
220+
cb(null, accessToken ? accessToken.id : null);
221+
};
222+
223+
loopback.remoteMethod(User.getToken, {
224+
accepts: [],
225+
returns: [
226+
{ type: 'object', name: 'id' }
227+
]
228+
});
229+
230+
invokeGetToken(done);
231+
});
232+
});
233+
133234
function givenUserModelWithAuth() {
134235
// NOTE(bajtos) It is important to create a custom AccessToken model here,
135236
// in order to overwrite the entry created by previous tests in

0 commit comments

Comments
 (0)