├── .eslintignore ├── test ├── mocha.opts ├── stub-modules │ ├── async-1.5.2 │ │ ├── package.json │ │ └── index.js │ └── when-3.7.7 │ │ ├── package.json │ │ └── index.js ├── helpers │ └── expect.js └── main.test.js ├── .eslintrc ├── .travis.yml ├── .gitignore ├── CODEOWNERS ├── .github ├── ISSUE_TEMPLATE │ ├── Feature_request.md │ ├── config.yml │ ├── Question.md │ └── Bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── stale.yml ├── browser └── current-context.js ├── package.json ├── example └── app.js ├── server ├── middleware │ └── per-request.js └── current-context.js ├── CHANGES.md ├── CONTRIBUTING.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | -r cls-hooked 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "loopback" 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "8" 5 | - "10" 6 | - "12" 7 | -------------------------------------------------------------------------------- /test/stub-modules/async-1.5.2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-1.5.2", 3 | "version": "1.5.2", 4 | "description":"async version 1.5.2", 5 | "dependencies": { 6 | "async":"1.5.2" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/stub-modules/when-3.7.7/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "when-3.7.7", 3 | "version": "3.7.7", 4 | "description":"when version 3.7.7", 5 | "dependencies": { 6 | "when":"3.7.7" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .project 3 | .DS_Store 4 | *.sublime* 5 | *.seed 6 | *.log 7 | *.csv 8 | *.dat 9 | *.out 10 | *.pid 11 | *.swp 12 | *.swo 13 | *.iml 14 | *.tgz 15 | node_modules 16 | checkstyle.xml 17 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners, 3 | # the last matching pattern has the most precendence. 4 | 5 | # Core team members from IBM 6 | * @bajtos @josieusa 7 | -------------------------------------------------------------------------------- /test/stub-modules/async-1.5.2/index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-context 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = require('async'); 8 | -------------------------------------------------------------------------------- /test/stub-modules/when-3.7.7/index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2017. All Rights Reserved. 2 | // Node module: loopback-context 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | module.exports = require('when'); 8 | -------------------------------------------------------------------------------- /test/helpers/expect.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016. All Rights Reserved. 2 | // Node module: loopback-context 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var chai = require('chai'); 9 | chai.use(require('dirty-chai')); 10 | 11 | module.exports = chai.expect; 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: feature 5 | 6 | --- 7 | 8 | ## Suggestion 9 | 10 | 11 | 12 | ## Use Cases 13 | 14 | 18 | 19 | ## Examples 20 | 21 | 22 | 23 | ## Acceptance criteria 24 | 25 | TBD - will be filled by the team. 26 | -------------------------------------------------------------------------------- /browser/current-context.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016. All Rights Reserved. 2 | // Node module: loopback-context 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var LoopBackContext = module.exports; 9 | 10 | LoopBackContext.getCurrentContext = function() { 11 | return null; 12 | }; 13 | 14 | LoopBackContext.runInContext = 15 | LoopBackContext.createContext = function() { 16 | throw new Error('Current context is not supported in the browser.'); 17 | }; 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Report a security vulnerability 4 | url: https://loopback.io/doc/en/contrib/Reporting-issues.html#security-issues 5 | about: Do not report security vulnerabilities using GitHub issues. Please send an email to `reachsl@us.ibm.com` instead. 6 | - name: Get help on StackOverflow 7 | url: https://stackoverflow.com/tags/loopbackjs 8 | about: Please ask and answer questions on StackOverflow. 9 | - name: Join our mailing list 10 | url: https://groups.google.com/forum/#!forum/loopbackjs 11 | about: You can also post your question to our mailing list. 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: The issue tracker is not for questions. Please use Stack Overflow or other resources for help. 4 | labels: question 5 | 6 | --- 7 | 8 | 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | ## Checklist 12 | 13 | 👉 [Read and sign the CLA (Contributor License Agreement)](https://cla.strongloop.com/agreements/strongloop/loopback-context) 👈 14 | 15 | - [ ] `npm test` passes on your machine 16 | - [ ] New tests added or existing tests modified to cover all changes 17 | - [ ] Code conforms with the [style guide](https://loopback.io/doc/en/contrib/style-guide-es6.html) 18 | - [ ] Commit messages are following our [guidelines](https://loopback.io/doc/en/contrib/git-commit-messages.html) 19 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 14 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - pinned 8 | - security 9 | - critical 10 | - p1 11 | - major 12 | - good first issue 13 | # Label to use when marking an issue as stale 14 | staleLabel: stale 15 | # Comment to post when marking an issue as stale. Set to `false` to disable 16 | markComment: > 17 | This issue has been automatically marked as stale because it has not had 18 | recent activity. It will be closed if no further activity occurs. Thank you 19 | for your contributions. 20 | # Comment to post when closing a stale issue. Set to `false` to disable 21 | closeComment: > 22 | This issue has been closed due to continued inactivity. Thank you for your understanding. 23 | If you believe this to be in error, please contact one of the code owners, 24 | listed in the `CODEOWNERS` file at the top-level of this repository. 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-context", 3 | "version": "3.5.2", 4 | "description": "Current context for LoopBack applications, based on cls-hooked", 5 | "engines": { 6 | "node": "^8.2.1 || ^10.14 || ^12.15" 7 | }, 8 | "keywords": [ 9 | "StrongLoop", 10 | "LoopBack", 11 | "Context" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/strongloop/loopback-context" 16 | }, 17 | "main": "server/current-context.js", 18 | "browser": "browser/current-context.js", 19 | "scripts": { 20 | "test": "mocha", 21 | "posttest": "npm run lint", 22 | "lint": "eslint ." 23 | }, 24 | "license": "MIT", 25 | "dependencies": { 26 | "cls-hooked": "^4.2.0" 27 | }, 28 | "devDependencies": { 29 | "async-1.5.2": "file:./test/stub-modules/async-1.5.2", 30 | "chai": "^4.1.2", 31 | "dirty-chai": "^2.0.1", 32 | "eslint": "^5.0.1", 33 | "eslint-config-loopback": "^10.0.0", 34 | "loopback": "^3.0.0", 35 | "mocha": "^5.2.0", 36 | "supertest": "^3.1.0", 37 | "when-3.7.7": "file:./test/stub-modules/when-3.7.7" 38 | }, 39 | "author": "IBM Corp." 40 | } 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: bug 5 | 6 | --- 7 | 8 | 18 | 19 | ## Steps to reproduce 20 | 21 | 22 | 23 | ## Current Behavior 24 | 25 | 26 | 27 | ## Expected Behavior 28 | 29 | 30 | 31 | ## Link to reproduction sandbox 32 | 33 | 37 | 38 | ## Additional information 39 | 40 | 45 | 46 | ## Related Issues 47 | 48 | 49 | 50 | _See [Reporting Issues](http://loopback.io/doc/en/contrib/Reporting-issues.html) for more tips on writing good issues_ 51 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016. All Rights Reserved. 2 | // Node module: loopback-context 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var contextPerRequest = require('../server/middleware/per-request.js'); 9 | // Use `var lbContext = require('loopback-context');` in your app 10 | var lbContext = require('../'); 11 | var loopback = require('loopback'); 12 | 13 | var app = loopback(); 14 | 15 | // Configure the context middleware 16 | app.middleware('initial', contextPerRequest()); 17 | 18 | // Store a request property in the context 19 | app.use(function saveHostToContext(req, res, next) { 20 | var currentContext = lbContext.getCurrentContext(); 21 | if (currentContext) 22 | currentContext.set('host', req.hostname); 23 | next(); 24 | }); 25 | 26 | app.use(loopback.rest()); 27 | 28 | var Color = loopback.createModel('color', {name: String}); 29 | Color.beforeRemote('**', function(ctx, unused, next) { 30 | // Inside LoopBack code, you can read the property from the context 31 | var currentContext = lbContext.getCurrentContext(); 32 | if (currentContext) 33 | console.log('Request to host %s', 34 | currentContext && currentContext.get('host')); 35 | next(); 36 | }); 37 | 38 | app.dataSource('db', {connector: 'memory'}); 39 | app.model(Color, {dataSource: 'db'}); 40 | 41 | app.listen(3000, function() { 42 | console.log('A list of colors is available at http://localhost:3000/colors'); 43 | }); 44 | -------------------------------------------------------------------------------- /server/middleware/per-request.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016. All Rights Reserved. 2 | // Node module: loopback-context 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var LoopBackContext = require('../current-context'); 9 | 10 | module.exports = perRequestContextFactory; 11 | 12 | var name = 'loopback'; 13 | 14 | /** 15 | * Context middleware. 16 | * ```js 17 | * var perRequestContext = require( 18 | * 'loopback-context/server/middleware/per-request-context.js'); 19 | * var app = loopback(); 20 | * app.use(perRequestContext(options); 21 | * app.use(loopback.rest()); 22 | * app.listen(); 23 | * ``` 24 | * @options {Object} [options] Options for context 25 | * @property {String} name Context scope name. 26 | * @property {Boolean} enableHttpContext Whether HTTP context is enabled. Default is false. 27 | */ 28 | 29 | function perRequestContextFactory(options) { 30 | options = options || {}; 31 | var scope = options.name || name; 32 | var enableHttpContext = options.enableHttpContext || false; 33 | var ns = LoopBackContext.createContext(scope); 34 | 35 | // Return the middleware 36 | return function perRequestContext(req, res, next) { 37 | if (req.loopbackContext) { 38 | return next(); 39 | } 40 | 41 | LoopBackContext.runInContext(function processRequestInContext(ns, domain) { 42 | req.loopbackContext = ns; 43 | 44 | // Bind req/res event emitters to the given namespace 45 | ns.bindEmitter(req); 46 | ns.bindEmitter(res); 47 | 48 | // Add req/res event emitters to the current domain 49 | domain.add(req); 50 | domain.add(res); 51 | 52 | // Run the code in the context of the namespace 53 | if (enableHttpContext) { 54 | // Set up the transport context 55 | ns.set('http', {req: req, res: res}); 56 | } 57 | next(); 58 | }); 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2021-01-19, Version 3.5.2 2 | ========================= 3 | 4 | * chore: update LTS status to End-of-Life (Rifa Achrinza) 5 | 6 | 7 | 2020-03-06, Version 3.5.1 8 | ========================= 9 | 10 | * Update LTS status in README (Miroslav Bajtoš) 11 | 12 | 13 | 2020-02-17, Version 3.5.0 14 | ========================= 15 | 16 | * Add support for Node.js 12 (Ahmet Cetin) 17 | 18 | * chore: enable stalebot (Diana Lau) 19 | 20 | * chore: improve issue and PR templates (Nora) 21 | 22 | * Drop support for Node.js 6.x (Miroslav Bajtoš) 23 | 24 | * chore: update copyrights years (Agnes Lin) 25 | 26 | 27 | 2019-01-02, Version 3.4.0 28 | ========================= 29 | 30 | * Travis: enable Node.js 10.x (Miroslav Bajtoš) 31 | 32 | * Node version ^10.14 added to engines list (Ahmet Cetin) 33 | 34 | * add lts annoucement (jannyHou) 35 | 36 | * Upgrade dependecies, fix new linter errors (Miroslav Bajtoš) 37 | 38 | * Drop support for Node.js 4.x, 5.x and 7.x (Miroslav Bajtoš) 39 | 40 | * Create Issue and PR Templates (#36) (Sakib Hasan) 41 | 42 | * Add CODEOWNERS file (Diana Lau) 43 | 44 | 45 | 2017-07-21, Version 3.3.0 46 | ========================= 47 | 48 | * add nodejs v8.2.1 support (Oleg Kubrakov) 49 | 50 | 51 | 2017-07-10, Version 3.2.0 52 | ========================= 53 | 54 | * Add Node.js 7 to package.json and .travis.yml (Edgars Zagorskis) 55 | 56 | 57 | 2017-03-17, Version 3.1.0 58 | ========================= 59 | 60 | * fix typo in readme (biniam) 61 | 62 | * Add bind option to getCurrentContext() (Emiliano Daddario) 63 | 64 | * Upgrade eslint & config to latest (Miroslav Bajtoš) 65 | 66 | 67 | 2017-01-06, Version 3.0.0 68 | ========================= 69 | 70 | * Rework README, add info from docs site (Miroslav Bajtoš) 71 | 72 | * Drop continuation-local-storage, use cls-hooked (josieusa) 73 | 74 | * Update paid support URL (Siddhi Pai) 75 | 76 | * Add app using loopback-context (Amir Jafarian) 77 | 78 | * Drop support for Node v0.10 and v0.12 (Siddhi Pai) 79 | 80 | * Start the development of the next major version (Siddhi Pai) 81 | 82 | * Update deps to loopback 3.0.0 RC (Miroslav Bajtoš) 83 | 84 | * make warning less ambigious (Carl Fürstenberg) 85 | 86 | 87 | 2016-08-10, Version 1.0.0 88 | ========================= 89 | 90 | * First release! 91 | -------------------------------------------------------------------------------- /server/current-context.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2017. All Rights Reserved. 2 | // Node module: loopback-context 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var cls = require('cls-hooked'); 9 | var domain = require('domain'); 10 | 11 | var LoopBackContext = module.exports; 12 | 13 | /** 14 | * Get the current context object. The context is preserved 15 | * across async calls, it behaves like a thread-local storage. 16 | * 17 | * @options {Object} [options] 18 | * @property {Boolean} bind Bind get/set/bind methods of the context to the 19 | * context that's current at the time getCurrentContext() is invoked. This 20 | * can be used to work around 3rd party code breaking CLS context propagation. 21 | * @returns {Namespace} The context object or null. 22 | */ 23 | LoopBackContext.getCurrentContext = function(options) { 24 | // A placeholder method, see LoopBackContext.createContext() for the real version 25 | return null; 26 | }; 27 | 28 | /** 29 | * Run the given function in such way that 30 | * `LoopBackContext.getCurrentContext` returns the 31 | * provided context object. 32 | * 33 | * **NOTE** 34 | * 35 | * The method is supported on the server only, it does not work 36 | * in the browser at the moment. 37 | * 38 | * @param {Function} fn The function to run, it will receive arguments 39 | * (currentContext, currentDomain). 40 | * @param {Namespace} context An optional context object. 41 | * When no value is provided, then the default global context is used. 42 | */ 43 | LoopBackContext.runInContext = function(fn, context) { 44 | var currentDomain = domain.create(); 45 | currentDomain.oldBind = currentDomain.bind; 46 | currentDomain.bind = function(callback, context) { 47 | return currentDomain.oldBind(ns.bind(callback, context), context); 48 | }; 49 | 50 | var ns = context || LoopBackContext.createContext('loopback'); 51 | 52 | currentDomain.run(function() { 53 | ns.run(function executeInContext(context) { 54 | fn(ns, currentDomain); 55 | }); 56 | }); 57 | }; 58 | 59 | /** 60 | * Create a new LoopBackContext instance that can be used 61 | * for `LoopBackContext.runInContext`. 62 | * 63 | * **NOTES** 64 | * 65 | * At the moment, `LoopBackContext.getCurrentContext` supports 66 | * a single global context instance only. If you call `createContext()` 67 | * multiple times, `getCurrentContext` will return the last context 68 | * created. 69 | * 70 | * The method is supported on the server only, it does not work 71 | * in the browser at the moment. 72 | * 73 | * @param {String} scopeName An optional scope name. 74 | * @return {Namespace} The new context object. 75 | */ 76 | LoopBackContext.createContext = function(scopeName) { 77 | // Make the namespace globally visible via the process.context property 78 | process.context = process.context || {}; 79 | var ns = process.context[scopeName]; 80 | if (!ns) { 81 | ns = cls.createNamespace(scopeName); 82 | process.context[scopeName] = ns; 83 | // Set up LoopBackContext.getCurrentContext() 84 | LoopBackContext.getCurrentContext = function(options) { 85 | if (!ns || !ns.active) { 86 | return null; 87 | } 88 | if (!options || !options.bind) { 89 | return ns; 90 | } 91 | /** 92 | * **NOTE** 93 | * This only re-binds get, set and bind methods, the most used. 94 | * If you use other methods of the context, e.g. runInContext(), etc., 95 | * you may run into unexpected issues that are fixed only for get & set. 96 | */ 97 | var boundContext = Object.create(ns); 98 | boundContext.get = boundContext.bind(ns.get); 99 | boundContext.set = boundContext.bind(ns.set); 100 | 101 | // Call to Function.prototype.bind(), not ns.bind() 102 | boundContext.bind = ns.bind.bind(ns); 103 | 104 | return boundContext; 105 | }; 106 | } 107 | return ns; 108 | }; 109 | 110 | /** 111 | * Create middleware that sets up a new context for each incoming HTTP request. 112 | * 113 | * See perRequestContextFactory for more details. 114 | */ 115 | LoopBackContext.perRequest = require('./middleware/per-request'); 116 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing ### 2 | 3 | Thank you for your interest in `loopback-context`, an open source project 4 | administered by StrongLoop. 5 | 6 | Contributing to `loopback-context` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-context-cls) 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Contributor License Agreement ### 23 | 24 | ``` 25 | Individual Contributor License Agreement 26 | 27 | By signing this Individual Contributor License Agreement 28 | ("Agreement"), and making a Contribution (as defined below) to 29 | StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and 30 | agree to the following terms and conditions for Your present and 31 | future Contributions submitted to StrongLoop. Except for the license 32 | granted in this Agreement to StrongLoop and recipients of software 33 | distributed by StrongLoop, You reserve all right, title, and interest 34 | in and to Your Contributions. 35 | 36 | 1. Definitions 37 | 38 | "You" or "Your" shall mean the copyright owner or the individual 39 | authorized by the copyright owner that is entering into this 40 | Agreement with StrongLoop. 41 | 42 | "Contribution" shall mean any original work of authorship, 43 | including any modifications or additions to an existing work, that 44 | is intentionally submitted by You to StrongLoop for inclusion in, 45 | or documentation of, any of the products owned or managed by 46 | StrongLoop ("Work"). For purposes of this definition, "submitted" 47 | means any form of electronic, verbal, or written communication 48 | sent to StrongLoop or its representatives, including but not 49 | limited to communication or electronic mailing lists, source code 50 | control systems, and issue tracking systems that are managed by, 51 | or on behalf of, StrongLoop for the purpose of discussing and 52 | improving the Work, but excluding communication that is 53 | conspicuously marked or otherwise designated in writing by You as 54 | "Not a Contribution." 55 | 56 | 2. You Grant a Copyright License to StrongLoop 57 | 58 | Subject to the terms and conditions of this Agreement, You hereby 59 | grant to StrongLoop and recipients of software distributed by 60 | StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, 61 | royalty-free, irrevocable copyright license to reproduce, prepare 62 | derivative works of, publicly display, publicly perform, 63 | sublicense, and distribute Your Contributions and such derivative 64 | works under any license and without any restrictions. 65 | 66 | 3. You Grant a Patent License to StrongLoop 67 | 68 | Subject to the terms and conditions of this Agreement, You hereby 69 | grant to StrongLoop and to recipients of software distributed by 70 | StrongLoop a perpetual, worldwide, non-exclusive, no-charge, 71 | royalty-free, irrevocable (except as stated in this Section) 72 | patent license to make, have made, use, offer to sell, sell, 73 | import, and otherwise transfer the Work under any license and 74 | without any restrictions. The patent license You grant to 75 | StrongLoop under this Section applies only to those patent claims 76 | licensable by You that are necessarily infringed by Your 77 | Contributions(s) alone or by combination of Your Contributions(s) 78 | with the Work to which such Contribution(s) was submitted. If any 79 | entity institutes a patent litigation against You or any other 80 | entity (including a cross-claim or counterclaim in a lawsuit) 81 | alleging that Your Contribution, or the Work to which You have 82 | contributed, constitutes direct or contributory patent 83 | infringement, any patent licenses granted to that entity under 84 | this Agreement for that Contribution or Work shall terminate as 85 | of the date such litigation is filed. 86 | 87 | 4. You Have the Right to Grant Licenses to StrongLoop 88 | 89 | You represent that You are legally entitled to grant the licenses 90 | in this Agreement. 91 | 92 | If Your employer(s) has rights to intellectual property that You 93 | create, You represent that You have received permission to make 94 | the Contributions on behalf of that employer, that Your employer 95 | has waived such rights for Your Contributions, or that Your 96 | employer has executed a separate Corporate Contributor License 97 | Agreement with StrongLoop. 98 | 99 | 5. The Contributions Are Your Original Work 100 | 101 | You represent that each of Your Contributions are Your original 102 | works of authorship (see Section 8 (Submissions on Behalf of 103 | Others) for submission on behalf of others). You represent that to 104 | Your knowledge, no other person claims, or has the right to claim, 105 | any right in any intellectual property right related to Your 106 | Contributions. 107 | 108 | You also represent that You are not legally obligated, whether by 109 | entering into an agreement or otherwise, in any way that conflicts 110 | with the terms of this Agreement. 111 | 112 | You represent that Your Contribution submissions include complete 113 | details of any third-party license or other restriction (including, 114 | but not limited to, related patents and trademarks) of which You 115 | are personally aware and which are associated with any part of 116 | Your Contributions. 117 | 118 | 6. You Don't Have an Obligation to Provide Support for Your Contributions 119 | 120 | You are not expected to provide support for Your Contributions, 121 | except to the extent You desire to provide support. You may provide 122 | support for free, for a fee, or not at all. 123 | 124 | 6. No Warranties or Conditions 125 | 126 | StrongLoop acknowledges that unless required by applicable law or 127 | agreed to in writing, You provide Your Contributions on an "AS IS" 128 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 129 | EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES 130 | OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR 131 | FITNESS FOR A PARTICULAR PURPOSE. 132 | 133 | 7. Submission on Behalf of Others 134 | 135 | If You wish to submit work that is not Your original creation, You 136 | may submit it to StrongLoop separately from any Contribution, 137 | identifying the complete details of its source and of any license 138 | or other restriction (including, but not limited to, related 139 | patents, trademarks, and license agreements) of which You are 140 | personally aware, and conspicuously marking the work as 141 | "Submitted on Behalf of a Third-Party: [named here]". 142 | 143 | 8. Agree to Notify of Change of Circumstances 144 | 145 | You agree to notify StrongLoop of any facts or circumstances of 146 | which You become aware that would make these representations 147 | inaccurate in any respect. Email us at callback@strongloop.com. 148 | ``` 149 | 150 | [Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html 151 | [Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml 152 | -------------------------------------------------------------------------------- /test/main.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2016,2018. All Rights Reserved. 2 | // Node module: loopback-context 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | 'use strict'; 7 | 8 | var asyncV152 = require('async-1.5.2'); 9 | var whenV377 = require('when-3.7.7'); 10 | var LoopBackContext = require('..'); 11 | var Domain = require('domain'); 12 | var EventEmitter = require('events').EventEmitter; 13 | var expect = require('./helpers/expect'); 14 | var loopback = require('loopback'); 15 | var request = require('supertest'); 16 | 17 | describe('LoopBack Context', function() { 18 | var runInOtherDomain, runnerInterval; 19 | 20 | before(function setupRunInOtherDomain() { 21 | var emitterInOtherDomain = new EventEmitter(); 22 | Domain.create().add(emitterInOtherDomain); 23 | 24 | runInOtherDomain = function(fn) { 25 | emitterInOtherDomain.once('run', fn); 26 | }; 27 | 28 | runnerInterval = setInterval(function() { 29 | emitterInOtherDomain.emit('run'); 30 | }, 10); 31 | }); 32 | 33 | after(function tearDownRunInOtherDomain() { 34 | clearInterval(runnerInterval); 35 | }); 36 | 37 | // See the following two items for more details: 38 | // https://github.com/strongloop/loopback/issues/809 39 | // https://github.com/strongloop/loopback/pull/337#issuecomment-61680577 40 | it('preserves callback domain', function(done) { 41 | var app = loopback({localRegistry: true, loadBuiltinModels: true}); 42 | app.set('remoting', {context: false}); 43 | app.set('legacyExplorer', false); 44 | app.use(LoopBackContext.perRequest()); 45 | app.use(loopback.rest()); 46 | app.dataSource('db', {connector: 'memory'}); 47 | 48 | var TestModel = loopback.createModel({name: 'TestModel'}); 49 | app.model(TestModel, {dataSource: 'db', public: true}); 50 | 51 | // function for remote method 52 | TestModel.test = function(inst, cb) { 53 | var tmpCtx = LoopBackContext.getCurrentContext(); 54 | if (tmpCtx) tmpCtx.set('data', 'a value stored in context'); 55 | if (process.domain) cb = process.domain.bind(cb); // IMPORTANT 56 | runInOtherDomain(cb); 57 | }; 58 | 59 | // remote method 60 | TestModel.remoteMethod('test', { 61 | accepts: {arg: 'inst', type: 'TestModel'}, 62 | returns: {root: true}, 63 | http: {path: '/test', verb: 'get'}, 64 | }); 65 | 66 | // after remote hook 67 | TestModel.afterRemote('**', function(ctxx, inst, next) { 68 | var tmpCtx = LoopBackContext.getCurrentContext(); 69 | if (tmpCtx) { 70 | ctxx.result.data = tmpCtx.get('data'); 71 | } else { 72 | ctxx.result.data = 'context not available'; 73 | } 74 | 75 | next(); 76 | }); 77 | 78 | request(app) 79 | .get('/TestModels/test') 80 | .end(function(err, res) { 81 | if (err) return done(err); 82 | 83 | expect(res.body.data).to.equal('a value stored in context'); 84 | 85 | done(); 86 | }); 87 | }); 88 | 89 | it('works outside REST middleware', function(done) { 90 | LoopBackContext.runInContext(function() { 91 | var ctx = LoopBackContext.getCurrentContext(); 92 | expect(ctx).is.an('object'); 93 | ctx.set('test-key', 'test-value'); 94 | process.nextTick(function() { 95 | var ctx = LoopBackContext.getCurrentContext(); 96 | expect(ctx).is.an('object'); 97 | expect(ctx.get('test-key')).to.equal('test-value'); 98 | 99 | done(); 100 | }); 101 | }); 102 | }); 103 | 104 | // Credits for the original idea for this test case to @marlonkjoseph 105 | // Original source of the POC gist of the idea: 106 | // https://gist.github.com/marlonkjoseph/f42f3c71f746896a0d4b7279a34ea753 107 | // Heavily edited by others 108 | it('keeps context when using waterfall() from async 1.5.2', 109 | function(done) { 110 | LoopBackContext.runInContext(function() { 111 | // Trigger async waterfall callbacks 112 | asyncV152.waterfall([ 113 | function pushToContext(next) { 114 | var ctx = LoopBackContext.getCurrentContext(); 115 | expect(ctx).is.an('object'); 116 | ctx.set('test-key', 'test-value'); 117 | next(); 118 | }, 119 | function pullFromContext(next) { 120 | var ctx = LoopBackContext.getCurrentContext(); 121 | expect(ctx).is.an('object'); 122 | var testValue = ctx && ctx.get('test-key', 'test-value'); 123 | next(null, testValue); 124 | }, 125 | function verify(testValue, next) { 126 | expect(testValue).to.equal('test-value'); 127 | next(); 128 | }, 129 | ], done); 130 | }); 131 | }); 132 | 133 | it('handles concurrent then() calls with when v3.7.7 promises & bind option', 134 | function() { 135 | return Promise.all([ 136 | runWithPushedValue('test-value-1', {bind: true}), 137 | runWithPushedValue('test-value-2', {bind: true}), 138 | ]) 139 | .then(function verify(values) { 140 | var failureCount = getFailureCount(values); 141 | expect(failureCount).to.equal(0); 142 | }); 143 | }); 144 | 145 | it('fails once without bind option and when v3.7.7 promises', 146 | function() { 147 | return Promise.all([ 148 | runWithPushedValue('test-value-3'), 149 | runWithPushedValue('test-value-4'), 150 | ]) 151 | .then(function verify(values) { 152 | var failureCount = getFailureCount(values); 153 | expect(failureCount).to.equal(1); 154 | }); 155 | }); 156 | 157 | var timeout = 100; 158 | 159 | function runWithPushedValue(pushedValue, options) { 160 | return new Promise(function concurrentExecutor(outerResolve, reject) { 161 | LoopBackContext.runInContext(function pushToContext() { 162 | var ctx = LoopBackContext.getCurrentContext(options); 163 | expect(ctx).is.an('object'); 164 | ctx.set('test-key', pushedValue); 165 | var whenPromise = whenV377().delay(timeout); 166 | whenPromise.then(function pullFromContextAndReturn() { 167 | var pulledValue = ctx && ctx.get('test-key'); 168 | outerResolve({ 169 | pulledValue: pulledValue, 170 | pushedValue: pushedValue, 171 | }); 172 | }).catch(reject); 173 | }); 174 | }); 175 | } 176 | 177 | function getFailureCount(values) { 178 | var failureCount = 0; 179 | values.forEach(function(v) { 180 | if (v.pulledValue !== v.pushedValue) { 181 | failureCount++; 182 | } 183 | }); 184 | return failureCount; 185 | } 186 | 187 | it('doesn\'t mix up req\'s in chains of ' + 188 | 'Express-middleware-like func\'s if next() cb is bound', 189 | function() { 190 | return Promise.all([ 191 | runWithRequestId('test-value-5', true), 192 | runWithRequestId('test-value-6', true), 193 | ]) 194 | .then(function verify(values) { 195 | var failureCount = getFailureCount(values); 196 | expect(failureCount).to.equal(0); 197 | }); 198 | }); 199 | 200 | it('fails & mixes up ctx among requests in mw chains if next() cb is unbound', 201 | function() { 202 | return Promise.all([ 203 | runWithRequestId('test-value-7'), 204 | runWithRequestId('test-value-8'), 205 | ]) 206 | .then(function verify(values) { 207 | var failureCount = getFailureCount(values); 208 | expect(failureCount).to.equal(1); 209 | }); 210 | }); 211 | 212 | function runWithRequestId(pushedValue, bindNextCb) { 213 | return new Promise(function chainExecutor(outerResolve, reject) { 214 | LoopBackContext.runInContext(function concurrentChain() { 215 | function middlewareBreakingCls(req, res, next) { 216 | var ctx = LoopBackContext.getCurrentContext({bind: true}); 217 | if (bindNextCb) { 218 | next = ctx.bind(next); 219 | } 220 | ctx.set('test-key', pushedValue); 221 | var whenPromise = whenV377().delay(timeout); 222 | whenPromise.then(next).catch(reject); 223 | }; 224 | 225 | function middlewareReadingContext(req, res, next) { 226 | var ctx = LoopBackContext.getCurrentContext({bind: true}); 227 | var pulledValue = ctx && ctx.get('test-key'); 228 | next(null, pulledValue); 229 | }; 230 | 231 | // Run the chain 232 | var req = null; 233 | var res = null; 234 | middlewareBreakingCls(req, res, function(err) { 235 | if (err) return reject(err); 236 | 237 | middlewareReadingContext(req, res, function(err, result) { 238 | if (err) return reject(err); 239 | 240 | outerResolve({ 241 | pulledValue: result, 242 | pushedValue: pushedValue, 243 | }); 244 | }); 245 | }); 246 | }); 247 | }); 248 | } 249 | }); 250 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loopback-context 2 | 3 | **⚠️ LoopBack 3 has reached end of life. We are no longer accepting pull requests or providing 4 | support for community users. The only exception is fixes for critical bugs and security 5 | vulnerabilities provided as part of support for IBM API Connect customers. (See 6 | [Module Long Term Support Policy](#module-long-term-support-policy) below.)** 7 | 8 | We urge all LoopBack 3 users to migrate their applications to LoopBack 4 as 9 | soon as possible. Refer to our 10 | [Migration Guide](https://loopback.io/doc/en/lb4/migration-overview.html) 11 | for more information on how to upgrade. 12 | 13 | ## Overview 14 | 15 | Current context for LoopBack applications, based on cls-hooked. 16 | 17 | ## WARNING 18 | 19 | **`cls-hooked` module uses undocumented `AsyncWrap` API that was introduced to Node.js relatively recently. While this new API seems to be more reliable than the old `async-listener` used by `continuation-local-storage`, there are still cases where the context (local storage) is not preserved correctly. Please consider this risk before using loopback-context.** 20 | 21 | ### Known issues 22 | 23 | - [when](https://www.npmjs.com/package/when), a popular Promise 24 | implementation, breaks context propagation. Please consider using the 25 | built-in `Promise` implementation provided by Node.js or 26 | [Bluebird](https://www.npmjs.com/package/bluebird) instead. 27 | - Express middleware chains which contain a "bad" middleware (i.e. one which 28 | breaks context propagation inside its function body, in a way mentioned in 29 | this doc) especially if called before other "good" ones needs refactoring, 30 | in order to prevent the context from getting mixed up among HTTP requests. 31 | See usage below for details. 32 | 33 | Discussion: https://github.com/strongloop/loopback-context/issues/17 34 | 35 | In general, any module that implements a custom task queue or a connection pool 36 | is prone to break context storage. This is an inherent problem of continuation 37 | local storage that needs to be fixed at lower level - first in Node.js core 38 | and then in modules implementing task queues and connection pools. 39 | 40 | ## Installation 41 | 42 | ``` 43 | $ npm install --save loopback-context cls-hooked 44 | ``` 45 | 46 | Make sure you are running on a Node.js version supported by this module 47 | (`^4.5`, `^5.10`, `^6.0`, `^7.0`, `^8.2.1` or `^10.14`). When installing, check the output of `npm install` 48 | and make sure there are no `engine` related warnings. 49 | 50 | ## Usage 51 | 52 | ### Setup cls-hooked 53 | 54 | To minimize the likelihood of loosing context in your application, you should 55 | ensure that `cls-hooked` is loaded as the first module of your application, so 56 | that it can wrap certain Node.js APIs before any other modules start using these 57 | APIs. 58 | 59 | Our recommended approach is to add `-r cls-hooked` to node's list of 60 | arguments when starting your LoopBack application. 61 | 62 | ``` 63 | $ node -r cls-hooked . 64 | ``` 65 | 66 | If you are using a process manager like `strong-pm` or `pm2`, then consult 67 | their documentation whether it's possible to configure the arguments used to 68 | spawn worker processes. Note that `slc run` does not support this feature yet, 69 | see [strong-supervisor#56](https://github.com/strongloop/strong-supervisor/issues/56). 70 | 71 | Alternatively, you can add the following line as the first line of your main 72 | application file: 73 | 74 | ```js 75 | require('cls-hooked'); 76 | ``` 77 | 78 | This approach should be compatible with all process managers, including 79 | `strong-pm`. However, we feel that relying on the order of `require` statements 80 | is error-prone. 81 | 82 | ### Configure context propagation 83 | 84 | To setup your LoopBack application to create a new context for each incoming 85 | HTTP request, configure `per-context` middleware in your 86 | `server/middleware.json` as follows: 87 | 88 | 89 | ```json 90 | { 91 | "initial": { 92 | "loopback-context#per-request": { 93 | } 94 | } 95 | } 96 | ``` 97 | 98 | **IMPORTANT: By default, the HTTP req/res objects are not set onto the current context. You 99 | need to set `enableHttpContext` to true to enable automatic population 100 | of req/res objects.** 101 | 102 | ```json 103 | { 104 | "initial": { 105 | "loopback-context#per-request": { 106 | "params": { 107 | "enableHttpContext": true 108 | } 109 | } 110 | } 111 | } 112 | ``` 113 | 114 | ### Use the current context 115 | 116 | Once you’ve enabled context propagation, you can access the current context 117 | object using `LoopBackContext.getCurrentContext()`. The context will be 118 | available in middleware (if it is loaded after the context middleware), 119 | remoting hooks, model hooks, and custom methods. 120 | 121 | ```js 122 | var LoopBackContext = require('loopback-context'); 123 | 124 | // ... 125 | 126 | MyModel.myMethod = function(cb) { 127 | var ctx = LoopBackContext.getCurrentContext(); 128 | ctx.get('key'); 129 | ctx.set('key', { foo: 'bar' }); 130 | }); 131 | ``` 132 | 133 | ### Bind for concurrency 134 | 135 | In order to workaround the aforementioned concurrency issue with `when` (and 136 | similar `Promise`-like and other libraries implementing custom queues and/or 137 | connection pools), it's recommended to activate context binding inside each 138 | HTTP request or concurrent `runInContext()` call, by using the `bind` option, as 139 | in this example: 140 | 141 | var ctx = LoopBackContext.getCurrentContext({ bind: true }); 142 | 143 | With the option enabled, this both creates the context, and binds the access 144 | methods of the context (i.e. `get` and `set`), at once. 145 | 146 | **Warning**: this only works if it's **the first expression evaluated** in every 147 | middleware/operation hook/`runInContext()` call etc. that uses 148 | `getCurrentContext`. (It must be the first expression; it may not be enough if 149 | it's at the first line). Explanation: you must bind the context while it's still 150 | correct, i.e. before it gets mixed up between concurrent operations affected by 151 | bugs. Therefore, to be sure, you must bind it before *any* operation. 152 | 153 | Also, with respect to the "bad", context-breaking middleware use case mentioned in "Known issues" 154 | before, the following 2 lines need to be present at the beginning of the middleware 155 | body. At least the "bad" one; but, as a preventive measure, they can be present 156 | in every other middleware of every chain as well, being backward-compatible: 157 | 158 | var badMiddleware = function(req, res, next) { 159 | // these 2 lines below are needed 160 | var ctx = LoopBackContext.getCurrentContext({bind: true}); 161 | next = ctx.bind(next); 162 | ... 163 | 164 | The `bind` option defaults to `false`. This is only in order to prevent breaking 165 | legacy apps; but if your app doesn't have such issue, then you can safely use 166 | `bind: true` everywhere in your app (e.g. with a 167 | [codemod](https://github.com/facebook/jscodeshift), or by monkey-patching 168 | `getCurrentContext()` globally, if you prefer an automated fashion). 169 | 170 | **Warning**: this only applies to application modules. In fact, if the module 171 | affected by the concurrency issue is of this kind, you can easily refactor/write 172 | your own code so to enable `bind`. Not if it's a 3rd-party module, nor a 173 | Loopback non-core module, unless you fork and fix it. 174 | 175 | ### Use current authenticated user in remote methods 176 | 177 | In advanced use cases, for example when you want to add custom middleware, you 178 | have to add the context middleware at the right position in the middleware 179 | chain (before the middleware that depends on 180 | `LoopBackContext.getCurrentContext`). 181 | 182 | **IMPORTANT: `LoopBackContext.perRequest()` detects the situation when it is 183 | invoked multiple times on the same request and returns immediately in 184 | subsequent runs.** 185 | 186 | Here is a snippet using a middleware function to place the currently 187 | authenticated user into the context so that remote methods may use it: 188 | 189 | **server/middleware/store-current-user.js** 190 | ```js 191 | module.exports = function(options) { 192 | return function storeCurrentUser(req, res, next) { 193 | if (!req.accessToken) { 194 | return next(); 195 | } 196 | 197 | app.models.UserModel.findById(req.accessToken.userId, function(err, user) { 198 | if (err) { 199 | return next(err); 200 | } 201 | if (!user) { 202 | return next(new Error('No user with this access token was found.')); 203 | } 204 | var loopbackContext = LoopBackContext.getCurrentContext(); 205 | if (loopbackContext) { 206 | loopbackContext.set('currentUser', user); 207 | } 208 | next(); 209 | }); 210 | }; 211 | }; 212 | ``` 213 | 214 | **server/middleware.json** 215 | ```json 216 | { 217 | "initial": { 218 | "loopback-context#per-request": {} 219 | }, 220 | "auth": { 221 | "loopback#token": {} 222 | }, 223 | "auth:after": { 224 | "./middleware/store-current-user": {} 225 | } 226 | } 227 | ``` 228 | 229 | **common/models/YourModel.json** 230 | ```js 231 | var LoopBackContext = require('loopback-context'); 232 | module.exports = function(YourModel) { 233 | ... 234 | //remote method 235 | YourModel.someRemoteMethod = function(arg1, arg2, cb) { 236 | var ctx = LoopBackContext.getCurrentContext(); 237 | var currentUser = ctx && ctx.get('currentUser'); 238 | console.log('currentUser.username: ', currentUser.username); // voila! 239 | ... 240 | cb(null); 241 | }; 242 | ... 243 | }; 244 | ``` 245 | 246 | ## Module Long Term Support Policy 247 | 248 | This module adopts the [Module Long Term Support (LTS)](http://github.com/CloudNativeJS/ModuleLTS) policy, with the following End Of Life (EOL) dates: 249 | 250 | | Version | Status | Published | EOL | 251 | | ------- | --------------- | --------- | -------- | 252 | | 3.x | End-of-Life | Jan 2017 | Dec 2020 | 253 | | 1.x | End-of-Life | Aug 2016 | Apr 2019 | 254 | 255 | Learn more about our LTS plan in the [docs](https://loopback.io/doc/en/contrib/Long-term-support.html). 256 | --------------------------------------------------------------------------------