From 9bd4afbe7e7e7c21b62de1cda02c096743194e49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Thu, 28 Jul 2016 15:40:19 +0200 Subject: [PATCH] Initial implementation --- README.md | 30 +++++++ browser/current-context.js | 17 ++++ package.json | 13 ++- server/current-context.js | 85 +++++++++++++++++++ server/middleware/per-request-context.js | 59 +++++++++++++ test/helpers/expect.js | 6 ++ test/main.test.js | 100 +++++++++++++++++++++++ 7 files changed, 306 insertions(+), 4 deletions(-) create mode 100644 browser/current-context.js create mode 100644 server/current-context.js create mode 100644 server/middleware/per-request-context.js create mode 100644 test/helpers/expect.js create mode 100644 test/main.test.js diff --git a/README.md b/README.md index 5162c27..e4ce733 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,33 @@ Current context for LoopBack applications, based on node-continuation-local-storage. + +## Usage + +1) Add `per-request-context` middleware to your +`server/middleware-config.json`: + +```json +{ + "initial": { + "loopback-context-cls#per-request-context": { + } + } +} +``` + +2) Then you can access the context from your code: + +```js +var ClsContext = require('loopback-context-cls'); + +// ... + +MyModel.myMethod = function(cb) { + var ctx = ClsContext.getCurrentContext(); + ctx.get('key'); + ctx.set('key', { foo: 'bar' }); +}); +``` + +See also https://docs.strongloop.com/display/APIC/Using+current+context diff --git a/browser/current-context.js b/browser/current-context.js new file mode 100644 index 0000000..5446219 --- /dev/null +++ b/browser/current-context.js @@ -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 ClsContext = module.exports; + +ClsContext.getCurrentContext = function() { + return null; +}; + +ClsContext.runInContext = +ClsContext.createContext = function() { + throw new Error('Current context is not supported in the browser.'); +}; diff --git a/package.json b/package.json index a7b80fb..c4f28ae 100644 --- a/package.json +++ b/package.json @@ -11,19 +11,24 @@ "type": "git", "url": "https://github.com/strongloop/loopback-context" }, - "main": "index.js", - "browser": "browser.js", + "main": "server/current-context.js", + "browser": "browser/current-context.js", "scripts": { "test": "mocha", "posttest": "npm run lint", "lint": "eslint ." }, "license": "MIT", - "dependencies": {}, + "dependencies": { + "continuation-local-storage": "^3.1.7" + }, "devDependencies": { + "chai": "^3.5.0", + "dirty-chai": "^1.2.2", "eslint": "^2.13.1", "eslint-config-loopback": "^4.0.0", "loopback": "^3.0.0-alpha.1", - "mocha": "^2.5.3" + "mocha": "^2.5.3", + "supertest": "^1.2.0" } } diff --git a/server/current-context.js b/server/current-context.js new file mode 100644 index 0000000..26088ae --- /dev/null +++ b/server/current-context.js @@ -0,0 +1,85 @@ +// 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 cls = require('continuation-local-storage'); +var domain = require('domain'); + +var ClsContext = 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. + */ +ClsContext.getCurrentContext = function() { + // A placeholder method, see CurrentContext.createContext() for the real version + return null; +}; + +/** + * Run the given function in such way that + * `CurrentContext.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. + */ +ClsContext.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 || ClsContext.createContext('loopback'); + + currentDomain.run(function() { + ns.run(function executeInContext(context) { + fn(ns, currentDomain); + }); + }); +}; + +/** + * Create a new LoopBackContext instance that can be used + * for `CurrentContext.runInContext`. + * + * **NOTES** + * + * At the moment, `CurrentContext.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. + */ +ClsContext.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 CurrentContext.getCurrentContext() + ClsContext.getCurrentContext = function() { + return ns && ns.active ? ns : null; + }; + } + return ns; +}; diff --git a/server/middleware/per-request-context.js b/server/middleware/per-request-context.js new file mode 100644 index 0000000..1b8a1b7 --- /dev/null +++ b/server/middleware/per-request-context.js @@ -0,0 +1,59 @@ +// 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 ClsContext = require('../current-context'); + +module.exports = context; + +var name = 'loopback'; + +/** + * Context middleware. + * ```js + * var app = loopback(); + * app.use(loopback.context(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. + * @header loopback.context([options]) + */ + +function context(options) { + options = options || {}; + var scope = options.name || name; + var enableHttpContext = options.enableHttpContext || false; + var ns = ClsContext.createContext(scope); + + // Return the middleware + return function contextHandler(req, res, next) { + if (req.loopbackContext) { + return next(); + } + + ClsContext.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(); + }); + }; +} diff --git a/test/helpers/expect.js b/test/helpers/expect.js new file mode 100644 index 0000000..830d8aa --- /dev/null +++ b/test/helpers/expect.js @@ -0,0 +1,6 @@ +'use strict'; + +var chai = require('chai'); +chai.use(require('dirty-chai')); + +module.exports = chai.expect; diff --git a/test/main.test.js b/test/main.test.js new file mode 100644 index 0000000..5e59862 --- /dev/null +++ b/test/main.test.js @@ -0,0 +1,100 @@ +// 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('CLS 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.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(); + }); + }); + }); +});