Merge pull request #1 from strongloop/feature/initial-implementation
Initial implementation
This commit is contained in:
commit
b66ec60e98
32
README.md
32
README.md
|
@ -1,4 +1,34 @@
|
||||||
# loopback-context-cls
|
# loopback-context
|
||||||
|
|
||||||
Current context for LoopBack applications, based on
|
Current context for LoopBack applications, based on
|
||||||
node-continuation-local-storage.
|
node-continuation-local-storage.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1) Add `per-request-context` middleware to your
|
||||||
|
`server/middleware-config.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"initial": {
|
||||||
|
"loopback-context#per-request-context": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2) Then you can access the context from your code:
|
||||||
|
|
||||||
|
```js
|
||||||
|
var LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
MyModel.myMethod = function(cb) {
|
||||||
|
var ctx = LoopBackContext.getCurrentContext();
|
||||||
|
ctx.get('key');
|
||||||
|
ctx.set('key', { foo: 'bar' });
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
See also https://docs.strongloop.com/display/APIC/Using+current+context
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright IBM Corp. 2015. All Rights Reserved.
|
||||||
|
// Node module: loopback-context-cls
|
||||||
|
// This file is licensed under the MIT License.
|
||||||
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var LoopBackContext = module.exports;
|
||||||
|
|
||||||
|
LoopBackContext.getCurrentContext = function() {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
LoopBackContext.runInContext =
|
||||||
|
LoopBackContext.createContext = function() {
|
||||||
|
throw new Error('Current context is not supported in the browser.');
|
||||||
|
};
|
13
package.json
13
package.json
|
@ -11,19 +11,24 @@
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/strongloop/loopback-context"
|
"url": "https://github.com/strongloop/loopback-context"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "server/current-context.js",
|
||||||
"browser": "browser.js",
|
"browser": "browser/current-context.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha",
|
"test": "mocha",
|
||||||
"posttest": "npm run lint",
|
"posttest": "npm run lint",
|
||||||
"lint": "eslint ."
|
"lint": "eslint ."
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {},
|
"dependencies": {
|
||||||
|
"continuation-local-storage": "^3.1.7"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"chai": "^3.5.0",
|
||||||
|
"dirty-chai": "^1.2.2",
|
||||||
"eslint": "^2.13.1",
|
"eslint": "^2.13.1",
|
||||||
"eslint-config-loopback": "^4.0.0",
|
"eslint-config-loopback": "^4.0.0",
|
||||||
"loopback": "^3.0.0-alpha.1",
|
"loopback": "^3.0.0-alpha.1",
|
||||||
"mocha": "^2.5.3"
|
"mocha": "^2.5.3",
|
||||||
|
"supertest": "^1.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
// Copyright IBM Corp. 2015,2016. All Rights Reserved.
|
||||||
|
// Node module: loopback-context-cls
|
||||||
|
// This file is licensed under the MIT License.
|
||||||
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var domain = require('domain');
|
||||||
|
|
||||||
|
// Require CLS only when using the current context feature.
|
||||||
|
// As soon as this require is done, all the instrumentation/patching
|
||||||
|
// of async-listener is fired which is not ideal.
|
||||||
|
//
|
||||||
|
// Some users observed stack overflows due to promise instrumentation
|
||||||
|
// and other people have seen similar things:
|
||||||
|
// https://github.com/othiym23/async-listener/issues/57
|
||||||
|
// It all goes away when instrumentation is disabled.
|
||||||
|
var cls = function() {
|
||||||
|
return require('continuation-local-storage');
|
||||||
|
};
|
||||||
|
|
||||||
|
var LoopBackContext = module.exports;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current context object. The context is preserved
|
||||||
|
* across async calls, it behaves like a thread-local storage.
|
||||||
|
*
|
||||||
|
* @returns {Namespace} The context object or null.
|
||||||
|
*/
|
||||||
|
LoopBackContext.getCurrentContext = function() {
|
||||||
|
// A placeholder method, see LoopBackContext.createContext() for the real version
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the given function in such way that
|
||||||
|
* `LoopBackContext.getCurrentContext` returns the
|
||||||
|
* provided context object.
|
||||||
|
*
|
||||||
|
* **NOTE**
|
||||||
|
*
|
||||||
|
* The method is supported on the server only, it does not work
|
||||||
|
* in the browser at the moment.
|
||||||
|
*
|
||||||
|
* @param {Function} fn The function to run, it will receive arguments
|
||||||
|
* (currentContext, currentDomain).
|
||||||
|
* @param {Namespace} context An optional context object.
|
||||||
|
* When no value is provided, then the default global context is used.
|
||||||
|
*/
|
||||||
|
LoopBackContext.runInContext = function(fn, context) {
|
||||||
|
var currentDomain = domain.create();
|
||||||
|
currentDomain.oldBind = currentDomain.bind;
|
||||||
|
currentDomain.bind = function(callback, context) {
|
||||||
|
return currentDomain.oldBind(ns.bind(callback, context), context);
|
||||||
|
};
|
||||||
|
|
||||||
|
var ns = context || LoopBackContext.createContext('loopback');
|
||||||
|
|
||||||
|
currentDomain.run(function() {
|
||||||
|
ns.run(function executeInContext(context) {
|
||||||
|
fn(ns, currentDomain);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new LoopBackContext instance that can be used
|
||||||
|
* for `LoopBackContext.runInContext`.
|
||||||
|
*
|
||||||
|
* **NOTES**
|
||||||
|
*
|
||||||
|
* At the moment, `LoopBackContext.getCurrentContext` supports
|
||||||
|
* a single global context instance only. If you call `createContext()`
|
||||||
|
* multiple times, `getCurrentContext` will return the last context
|
||||||
|
* created.
|
||||||
|
*
|
||||||
|
* The method is supported on the server only, it does not work
|
||||||
|
* in the browser at the moment.
|
||||||
|
*
|
||||||
|
* @param {String} scopeName An optional scope name.
|
||||||
|
* @return {Namespace} The new context object.
|
||||||
|
*/
|
||||||
|
LoopBackContext.createContext = function(scopeName) {
|
||||||
|
// Make the namespace globally visible via the process.context property
|
||||||
|
process.context = process.context || {};
|
||||||
|
var ns = process.context[scopeName];
|
||||||
|
if (!ns) {
|
||||||
|
ns = cls().createNamespace(scopeName);
|
||||||
|
process.context[scopeName] = ns;
|
||||||
|
// Set up LoopBackContext.getCurrentContext()
|
||||||
|
LoopBackContext.getCurrentContext = function() {
|
||||||
|
return ns && ns.active ? ns : null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return ns;
|
||||||
|
};
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Copyright IBM Corp. 2014,2016. All Rights Reserved.
|
||||||
|
// Node module: loopback-context-cls
|
||||||
|
// This file is licensed under the MIT License.
|
||||||
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var LoopBackContext = require('../current-context');
|
||||||
|
|
||||||
|
module.exports = context;
|
||||||
|
|
||||||
|
var name = 'loopback';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context middleware.
|
||||||
|
* ```js
|
||||||
|
* var perRequestContext = require(
|
||||||
|
* 'loopback-context/server/middleware/per-request-context.js');
|
||||||
|
* var app = loopback();
|
||||||
|
* app.use(perRequestContext(options);
|
||||||
|
* app.use(loopback.rest());
|
||||||
|
* app.listen();
|
||||||
|
* ```
|
||||||
|
* @options {Object} [options] Options for context
|
||||||
|
* @property {String} name Context scope name.
|
||||||
|
* @property {Boolean} enableHttpContext Whether HTTP context is enabled. Default is false.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function context(options) {
|
||||||
|
options = options || {};
|
||||||
|
var scope = options.name || name;
|
||||||
|
var enableHttpContext = options.enableHttpContext || false;
|
||||||
|
var ns = LoopBackContext.createContext(scope);
|
||||||
|
|
||||||
|
// Return the middleware
|
||||||
|
return function contextHandler(req, res, next) {
|
||||||
|
if (req.loopbackContext) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
LoopBackContext.runInContext(function processRequestInContext(ns, domain) {
|
||||||
|
req.loopbackContext = ns;
|
||||||
|
|
||||||
|
// Bind req/res event emitters to the given namespace
|
||||||
|
ns.bindEmitter(req);
|
||||||
|
ns.bindEmitter(res);
|
||||||
|
|
||||||
|
// Add req/res event emitters to the current domain
|
||||||
|
domain.add(req);
|
||||||
|
domain.add(res);
|
||||||
|
|
||||||
|
// Run the code in the context of the namespace
|
||||||
|
if (enableHttpContext) {
|
||||||
|
// Set up the transport context
|
||||||
|
ns.set('http', {req: req, res: res});
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var chai = require('chai');
|
||||||
|
chai.use(require('dirty-chai'));
|
||||||
|
|
||||||
|
module.exports = chai.expect;
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Copyright IBM Corp. 2013,2016. All Rights Reserved.
|
||||||
|
// Node module: loopback-context-cls
|
||||||
|
// This file is licensed under the MIT License.
|
||||||
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var ClsContext = require('..');
|
||||||
|
var Domain = require('domain');
|
||||||
|
var EventEmitter = require('events').EventEmitter;
|
||||||
|
var expect = require('./helpers/expect');
|
||||||
|
var loopback = require('loopback');
|
||||||
|
var request = require('supertest');
|
||||||
|
|
||||||
|
describe('LoopBack Context', function() {
|
||||||
|
var runInOtherDomain, runnerInterval;
|
||||||
|
|
||||||
|
before(function setupRunInOtherDomain() {
|
||||||
|
var emitterInOtherDomain = new EventEmitter();
|
||||||
|
Domain.create().add(emitterInOtherDomain);
|
||||||
|
|
||||||
|
runInOtherDomain = function(fn) {
|
||||||
|
emitterInOtherDomain.once('run', fn);
|
||||||
|
};
|
||||||
|
|
||||||
|
runnerInterval = setInterval(function() {
|
||||||
|
emitterInOtherDomain.emit('run');
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function tearDownRunInOtherDomain() {
|
||||||
|
clearInterval(runnerInterval);
|
||||||
|
});
|
||||||
|
|
||||||
|
// See the following two items for more details:
|
||||||
|
// https://github.com/strongloop/loopback/issues/809
|
||||||
|
// https://github.com/strongloop/loopback/pull/337#issuecomment-61680577
|
||||||
|
it('preserves callback domain', function(done) {
|
||||||
|
var app = loopback({localRegistry: true, loadBuiltinModels: true});
|
||||||
|
app.set('remoting', {context: false});
|
||||||
|
app.set('legacyExplorer', false);
|
||||||
|
app.use(require('../server/middleware/per-request-context')());
|
||||||
|
app.use(loopback.rest());
|
||||||
|
app.dataSource('db', {connector: 'memory'});
|
||||||
|
|
||||||
|
var TestModel = loopback.createModel({name: 'TestModel'});
|
||||||
|
app.model(TestModel, {dataSource: 'db', public: true});
|
||||||
|
|
||||||
|
// function for remote method
|
||||||
|
TestModel.test = function(inst, cb) {
|
||||||
|
var tmpCtx = ClsContext.getCurrentContext();
|
||||||
|
if (tmpCtx) tmpCtx.set('data', 'a value stored in context');
|
||||||
|
if (process.domain) cb = process.domain.bind(cb); // IMPORTANT
|
||||||
|
runInOtherDomain(cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
// remote method
|
||||||
|
TestModel.remoteMethod('test', {
|
||||||
|
accepts: {arg: 'inst', type: 'TestModel'},
|
||||||
|
returns: {root: true},
|
||||||
|
http: {path: '/test', verb: 'get'},
|
||||||
|
});
|
||||||
|
|
||||||
|
// after remote hook
|
||||||
|
TestModel.afterRemote('**', function(ctxx, inst, next) {
|
||||||
|
var tmpCtx = ClsContext.getCurrentContext();
|
||||||
|
if (tmpCtx) {
|
||||||
|
ctxx.result.data = tmpCtx.get('data');
|
||||||
|
} else {
|
||||||
|
ctxx.result.data = 'context not available';
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
request(app)
|
||||||
|
.get('/TestModels/test')
|
||||||
|
.end(function(err, res) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
expect(res.body.data).to.equal('a value stored in context');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works outside REST middleware', function(done) {
|
||||||
|
ClsContext.runInContext(function() {
|
||||||
|
var ctx = ClsContext.getCurrentContext();
|
||||||
|
expect(ctx).is.an('object');
|
||||||
|
ctx.set('test-key', 'test-value');
|
||||||
|
process.nextTick(function() {
|
||||||
|
var ctx = ClsContext.getCurrentContext();
|
||||||
|
expect(ctx).is.an('object');
|
||||||
|
expect(ctx.get('test-key')).to.equal('test-value');
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue