diff --git a/3.0-RELEASE-NOTES.md b/3.0-RELEASE-NOTES.md index ea854b18..29915b12 100644 --- a/3.0-RELEASE-NOTES.md +++ b/3.0-RELEASE-NOTES.md @@ -153,3 +153,44 @@ We have removed `loopback#errorhandler` middleware, users should use `strong-err ``` See also strong-error-handler's [options](https://github.com/strongloop/strong-error-handler#options) and the [related code change](https://github.com/strongloop/loopback/pull/2411). + +## Remove current context API and middleware + +We have removed the following current-context-related APIs: + + - `loopback.getCurrentContext` + - `loopback.createContext` + - `loopback.runInContext` + +Additionaly, `loopback#context` middleware and `remoting.context` server +config were removed too. + +To setup "current context" feature in your LoopBack 3.x application, you +should use [loopback-context](https://www.npmjs.com/package/loopback-context) +module: + + 1. Add `loopback-context` to your dependencies + + 2. Configure the new context middleware in your `server/middleware-config.json` file + ```json + { + "initial": { + "loopback-context#per-request": {} + } + } + ``` + + 3. Replace all usages of `loopback.getCurrentContext` with the following: + ```js + // at the top of your file + var LoopBackContext = require('loopback-context'); + + // in your code + var ctx = LoopBackContext.getCurrentContext(); + if (ctx) { + // use the context + } + ``` + +See also [loopback#2564](https://github.com/strongloop/loopback/pull/2564) +and the official [documentation](https://docs.strongloop.com/display/APIC/Using+current+context) diff --git a/Gruntfile.js b/Gruntfile.js index 06872ee3..7ef9f0d8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -40,9 +40,6 @@ module.exports = function(grunt) { common: { src: ['common/**/*.js'], }, - browser: { - src: ['browser/**/*.js'], - }, server: { src: ['server/**/*.js'], }, diff --git a/browser/current-context.js b/browser/current-context.js deleted file mode 100644 index e3d52993..00000000 --- a/browser/current-context.js +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright IBM Corp. 2015. All Rights Reserved. -// Node module: loopback -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -var g = require('strong-globalize')(); - -module.exports = function(loopback) { - loopback.getCurrentContext = function() { - return null; - }; - - loopback.runInContext = - loopback.createContext = function() { - throw new Error(g.f('Current context is not supported in the browser.')); - }; -}; diff --git a/lib/current-context.js b/lib/current-context.js new file mode 100644 index 00000000..b7df2a8b --- /dev/null +++ b/lib/current-context.js @@ -0,0 +1,33 @@ +// Copyright IBM Corp. 2015,2016. All Rights Reserved. +// Node module: loopback +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +var g = require('strong-globalize')(); +var juggler = require('loopback-datasource-juggler'); +var remoting = require('strong-remoting'); + +module.exports = function(loopback) { + juggler.getCurrentContext = + remoting.getCurrentContext = + loopback.getCurrentContext = function() { + throw new Error(g.f( + '%s was removed in version 3.0. See %s for more details.', + 'loopback.getCurrentContext()', + 'https://docs.strongloop.com/display/APIC/Using%20current%20context')); + }; + + loopback.runInContext = function(fn) { + throw new Error(g.f( + '%s was removed in version 3.0. See %s for more details.', + 'loopback.runInContext()', + 'https://docs.strongloop.com/display/APIC/Using%20current%20context')); + }; + + loopback.createContext = function(scopeName) { + throw new Error(g.f( + '%s was removed in version 3.0. See %s for more details.', + 'loopback.createContext()', + 'https://docs.strongloop.com/display/APIC/Using%20current%20context')); + }; +}; diff --git a/lib/loopback.js b/lib/loopback.js index 80806089..a854ca60 100644 --- a/lib/loopback.js +++ b/lib/loopback.js @@ -200,7 +200,7 @@ loopback.template = function(file) { }); }; -require('../server/current-context')(loopback); +require('../lib/current-context')(loopback); /** * Create a named vanilla JavaScript class constructor with an attached diff --git a/package.json b/package.json index 3a8ae350..2e1e29eb 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "bluebird": "^3.1.1", "body-parser": "^1.12.0", "canonical-json": "0.0.4", - "continuation-local-storage": "^3.1.3", "debug": "^2.1.2", "depd": "^1.0.0", "ejs": "^2.3.1", @@ -82,6 +81,7 @@ "karma-phantomjs-launcher": "^1.0.0", "karma-script-launcher": "^1.0.0", "loopback-boot": "^2.7.0", + "loopback-context": "^1.0.0", "mocha": "^3.0.0", "phantomjs-prebuilt": "^2.1.7", "sinon": "^1.13.0", @@ -97,7 +97,6 @@ "browser": { "express": "./lib/browser-express.js", "./lib/server-app.js": "./lib/browser-express.js", - "./server/current-context.js": "./browser/current-context.js", "connect": false, "nodemailer": false, "supertest": false, diff --git a/server/current-context.js b/server/current-context.js deleted file mode 100644 index 5bf87030..00000000 --- a/server/current-context.js +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright IBM Corp. 2015,2016. All Rights Reserved. -// Node module: loopback -// This file is licensed under the MIT License. -// License text available at https://opensource.org/licenses/MIT - -var juggler = require('loopback-datasource-juggler'); -var remoting = require('strong-remoting'); -var cls = require('continuation-local-storage'); -var domain = require('domain'); - -module.exports = function(loopback) { - /** - * Get the current context object. The context is preserved - * across async calls, it behaves like a thread-local storage. - * - * @returns {ChainedContext} The context object or null. - */ - loopback.getCurrentContext = function() { - // A placeholder method, see loopback.createContext() for the real version - return null; - }; - - /** - * Run the given function in such way that - * `loopback.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 {ChainedContext} context An optional context object. - * When no value is provided, then the default global context is used. - */ - loopback.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 || loopback.createContext('loopback'); - - currentDomain.run(function() { - ns.run(function executeInContext(context) { - fn(ns, currentDomain); - }); - }); - }; - - /** - * Create a new LoopBackContext instance that can be used - * for `loopback.runInContext`. - * - * **NOTES** - * - * At the moment, `loopback.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 {ChainedContext} The new context object. - */ - loopback.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 loopback.getCurrentContext() - loopback.getCurrentContext = function() { - return ns && ns.active ? ns : null; - }; - - chain(juggler); - chain(remoting); - } - return ns; - }; - - /** - * 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/server/middleware/context.js b/server/middleware/context.js index 78cba8ed..2953252b 100644 --- a/server/middleware/context.js +++ b/server/middleware/context.js @@ -3,55 +3,9 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -var loopback = require('../../lib/loopback'); - -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 = loopback.createContext(scope); - - // Return the middleware - return function contextHandler(req, res, next) { - if (req.loopbackContext) { - return next(); - } - - loopback.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(); - }); - }; -} +module.exports = function() { + throw new Error(g.f( + '%s middleware was removed in version 3.0. See %s for more details.', + 'loopback#context', + 'https://docs.strongloop.com/display/APIC/Using%20current%20context')); +}; diff --git a/server/middleware/rest.js b/server/middleware/rest.js index 98970bde..1ff3d008 100644 --- a/server/middleware/rest.js +++ b/server/middleware/rest.js @@ -9,7 +9,7 @@ var loopback = require('../../lib/loopback'); var async = require('async'); -var deprecate = require('depd')('loopback'); +var g = require('strong-globalize')(); /*! * Export the middleware. @@ -40,11 +40,11 @@ function rest() { var remotingOptions = app.get('remoting') || {}; var contextOptions = remotingOptions.context; - if (contextOptions !== false) { - if (typeof contextOptions !== 'object') { - contextOptions = {}; - } - handlers.push(loopback.context(contextOptions)); + if (contextOptions !== undefined && contextOptions !== false) { + throw new Error(g.f( + '%s was removed in version 3.0. See %s for more details.', + 'remoting.context option', + 'https://docs.strongloop.com/display/APIC/Using%20current%20context')); } if (app.isAuthEnabled) { diff --git a/server/middleware/token.js b/server/middleware/token.js index b5038df2..21e3b5ca 100644 --- a/server/middleware/token.js +++ b/server/middleware/token.js @@ -124,7 +124,7 @@ function token(options) { TokenModel.findForRequest(req, options, function(err, token) { req.accessToken = token || null; rewriteUserLiteral(req, currentUserLiteral); - var ctx = loopback.getCurrentContext(); + var ctx = req.loopbackContext; if (ctx) ctx.set('accessToken', token); next(err); }); diff --git a/test/access-token.test.js b/test/access-token.test.js index b7f7fc27..1a02839e 100644 --- a/test/access-token.test.js +++ b/test/access-token.test.js @@ -4,6 +4,8 @@ // License text available at https://opensource.org/licenses/MIT var cookieParser = require('cookie-parser'); +var LoopBackContext = require('loopback-context'); +var contextMiddleware = require('loopback-context').perRequest; var loopback = require('../'); var extend = require('util')._extend; var Token = loopback.AccessToken.extend('MyToken'); @@ -477,7 +479,8 @@ describe('app.enableAuth()', function() { it('stores token in the context', function(done) { var TestModel = loopback.createModel('TestModel', { base: 'Model' }); TestModel.getToken = function(cb) { - cb(null, loopback.getCurrentContext().get('accessToken') || null); + var ctx = LoopBackContext.getCurrentContext(); + cb(null, ctx && ctx.get('accessToken') || null); }; TestModel.remoteMethod('getToken', { returns: { arg: 'token', type: 'object' }, @@ -488,7 +491,7 @@ describe('app.enableAuth()', function() { app.model(TestModel, { dataSource: null }); app.enableAuth(); - app.use(loopback.context()); + app.use(contextMiddleware()); app.use(loopback.token({ model: Token })); app.use(loopback.rest()); diff --git a/test/fixtures/shared-methods/both-configs-set/server/config.json b/test/fixtures/shared-methods/both-configs-set/server/config.json index bdef5b46..e0b95355 100644 --- a/test/fixtures/shared-methods/both-configs-set/server/config.json +++ b/test/fixtures/shared-methods/both-configs-set/server/config.json @@ -3,9 +3,6 @@ "host": "0.0.0.0", "port": 3000, "remoting": { - "context": { - "enableHttpContext": false - }, "rest": { "normalizeHttpPath": false, "xml": false diff --git a/test/fixtures/shared-methods/config-default-false/server/config.json b/test/fixtures/shared-methods/config-default-false/server/config.json index 972b298c..ce133361 100644 --- a/test/fixtures/shared-methods/config-default-false/server/config.json +++ b/test/fixtures/shared-methods/config-default-false/server/config.json @@ -3,9 +3,6 @@ "host": "0.0.0.0", "port": 3000, "remoting": { - "context": { - "enableHttpContext": false - }, "rest": { "normalizeHttpPath": false, "xml": false diff --git a/test/fixtures/shared-methods/config-default-true/server/config.json b/test/fixtures/shared-methods/config-default-true/server/config.json index d274fe47..f824b415 100644 --- a/test/fixtures/shared-methods/config-default-true/server/config.json +++ b/test/fixtures/shared-methods/config-default-true/server/config.json @@ -3,9 +3,6 @@ "host": "0.0.0.0", "port": 3000, "remoting": { - "context": { - "enableHttpContext": false - }, "rest": { "normalizeHttpPath": false, "xml": false diff --git a/test/fixtures/shared-methods/config-defined-false/server/config.json b/test/fixtures/shared-methods/config-defined-false/server/config.json index bbdd21de..a26d85fb 100644 --- a/test/fixtures/shared-methods/config-defined-false/server/config.json +++ b/test/fixtures/shared-methods/config-defined-false/server/config.json @@ -3,9 +3,6 @@ "host": "0.0.0.0", "port": 3000, "remoting": { - "context": { - "enableHttpContext": false - }, "rest": { "normalizeHttpPath": false, "xml": false diff --git a/test/fixtures/shared-methods/config-defined-true/server/config.json b/test/fixtures/shared-methods/config-defined-true/server/config.json index 8e0e0067..f98a414e 100644 --- a/test/fixtures/shared-methods/config-defined-true/server/config.json +++ b/test/fixtures/shared-methods/config-defined-true/server/config.json @@ -3,9 +3,6 @@ "host": "0.0.0.0", "port": 3000, "remoting": { - "context": { - "enableHttpContext": false - }, "rest": { "normalizeHttpPath": false, "xml": false diff --git a/test/fixtures/shared-methods/model-config-default-false/server/config.json b/test/fixtures/shared-methods/model-config-default-false/server/config.json index 7200f804..2a867347 100644 --- a/test/fixtures/shared-methods/model-config-default-false/server/config.json +++ b/test/fixtures/shared-methods/model-config-default-false/server/config.json @@ -3,9 +3,6 @@ "host": "0.0.0.0", "port": 3000, "remoting": { - "context": { - "enableHttpContext": false - }, "rest": { "normalizeHttpPath": false, "xml": false diff --git a/test/fixtures/shared-methods/model-config-default-true/server/config.json b/test/fixtures/shared-methods/model-config-default-true/server/config.json index 7200f804..2a867347 100644 --- a/test/fixtures/shared-methods/model-config-default-true/server/config.json +++ b/test/fixtures/shared-methods/model-config-default-true/server/config.json @@ -3,9 +3,6 @@ "host": "0.0.0.0", "port": 3000, "remoting": { - "context": { - "enableHttpContext": false - }, "rest": { "normalizeHttpPath": false, "xml": false diff --git a/test/fixtures/shared-methods/model-config-defined-false/server/config.json b/test/fixtures/shared-methods/model-config-defined-false/server/config.json index 7200f804..2a867347 100644 --- a/test/fixtures/shared-methods/model-config-defined-false/server/config.json +++ b/test/fixtures/shared-methods/model-config-defined-false/server/config.json @@ -3,9 +3,6 @@ "host": "0.0.0.0", "port": 3000, "remoting": { - "context": { - "enableHttpContext": false - }, "rest": { "normalizeHttpPath": false, "xml": false diff --git a/test/fixtures/shared-methods/model-config-defined-true/server/config.json b/test/fixtures/shared-methods/model-config-defined-true/server/config.json index 7200f804..2a867347 100644 --- a/test/fixtures/shared-methods/model-config-defined-true/server/config.json +++ b/test/fixtures/shared-methods/model-config-defined-true/server/config.json @@ -3,9 +3,6 @@ "host": "0.0.0.0", "port": 3000, "remoting": { - "context": { - "enableHttpContext": false - }, "rest": { "normalizeHttpPath": false, "xml": false diff --git a/test/loopback.test.js b/test/loopback.test.js index 01a7c41d..50629d9d 100644 --- a/test/loopback.test.js +++ b/test/loopback.test.js @@ -518,91 +518,6 @@ describe('loopback', function() { }); }); - describe.onServer('loopback.getCurrentContext', 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(); - 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 = loopback.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: uniqueModelName }, - returns: { root: true }, - http: { path: '/test', verb: 'get' }, - }); - - // after remote hook - TestModel.afterRemote('**', function(ctxx, inst, next) { - var tmpCtx = loopback.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) { - loopback.runInContext(function() { - var ctx = loopback.getCurrentContext(); - expect(ctx).is.an('object'); - ctx.set('test-key', 'test-value'); - process.nextTick(function() { - var ctx = loopback.getCurrentContext(); - expect(ctx).is.an('object'); - expect(ctx.get('test-key')).to.equal('test-value'); - - done(); - }); - }); - }); - }); - describe('new remote method configuration', function() { function getAllMethodNamesWithoutClassName(TestModel) { return TestModel.sharedClass.methods().map(function(m) { diff --git a/test/rest.middleware.test.js b/test/rest.middleware.test.js index 71b597fc..549ce05f 100644 --- a/test/rest.middleware.test.js +++ b/test/rest.middleware.test.js @@ -197,111 +197,6 @@ describe('loopback.rest', function() { }, done); }); - describe('context propagation', function() { - var User; - - beforeEach(function() { - 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' }, - ], - }); - }); - - function invokeGetToken(done) { - 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(); - }); - }); - } - - it('should enable context using loopback.context', function(done) { - app.use(loopback.context({ enableHttpContext: true })); - app.enableAuth({ dataSource: 'db' }); - app.use(loopback.rest()); - - invokeGetToken(done); - }); - - it('should enable context with loopback.rest', function(done) { - app.enableAuth({ dataSource: 'db' }); - app.set('remoting', { context: { enableHttpContext: true }}); - app.use(loopback.rest()); - - invokeGetToken(done); - }); - - it('should support explicit context', function(done) { - app.enableAuth({ dataSource: 'db' }); - app.use(loopback.context()); - app.use(loopback.token( - { model: app.registry.getModelByType('AccessToken') })); - app.use(function(req, res, next) { - loopback.getCurrentContext().set('accessToken', req.accessToken); - - next(); - }); - app.use(loopback.rest()); - - User.getToken = function(cb) { - var context = loopback.getCurrentContext(); - var accessToken = context.get('accessToken'); - expect(context.get('accessToken')).to.have.property('id'); - - var juggler = require('loopback-datasource-juggler'); - context = juggler.getCurrentContext(); - expect(context.get('accessToken')).to.have.property('id'); - - var remoting = require('strong-remoting'); - context = remoting.getCurrentContext(); - expect(context.get('accessToken')).to.have.property('id'); - - cb(null, accessToken ? accessToken.id : null); - }; - - loopback.remoteMethod(User.getToken, { - accepts: [], - returns: [ - { type: 'object', name: 'id' }, - ], - }); - - invokeGetToken(done); - }); - }); - function givenUserModelWithAuth() { var AccessToken = app.registry.getModel('AccessToken'); app.model(AccessToken, { dataSource: 'db' });