├── .eslintignore ├── .gitignore ├── .travis.yml ├── test ├── removeWhitespaces.test.js ├── setVersion.test.js ├── formatVersion.test.js ├── setVersionByHeader.test.js ├── setVersionByQueryParam.test.js └── setVersionByAcceptHeader.test.js ├── LICENSE ├── .editorconfig ├── package.json ├── index.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | node_modules 6 | 7 | # Coverage 8 | lib-cov 9 | coverage 10 | .nyc_output 11 | 12 | # IDE 13 | *.iml 14 | .idea 15 | 16 | # Docs 17 | docs/ 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "7" 5 | - "8" 6 | before_install: 7 | - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.27.5 8 | - export PATH="$HOME/.yarn/bin:$PATH" 9 | install: 10 | - yarn global add codecov 11 | - yarn install 12 | script: 13 | - yarn run test:coverage 14 | - codecov 15 | -------------------------------------------------------------------------------- /test/removeWhitespaces.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava') 4 | const versionRequest = require('../index') 5 | 6 | test('it removes whitespaces from a string', t => { 7 | t.is(versionRequest.removeWhitespaces('a '), 'a') 8 | }) 9 | 10 | test('it returns the same string if nothing to remove', t => { 11 | t.is(versionRequest.removeWhitespaces('a'), 'a') 12 | }) 13 | 14 | test('it returns empty string if given object', t => { 15 | t.is(versionRequest.removeWhitespaces({}), '') 16 | }) 17 | 18 | test('it returns empty string if given number', t => { 19 | t.is(versionRequest.removeWhitespaces(42), '') 20 | }) 21 | 22 | test('it returns empty string if given array', t => { 23 | t.is(versionRequest.removeWhitespaces(['a', 'b']), '') 24 | }) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Liran Tal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/setVersion.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava') 4 | const versionRequest = require('../index') 5 | const sinon = require('sinon') 6 | 7 | test.beforeEach(t => { 8 | t.context.req = {} 9 | }) 10 | 11 | test('we can manually set a specific version to be integer', t => { 12 | const versionNumber = 1 13 | const versionRequestSpy = sinon.spy(versionRequest, 'formatVersion') 14 | 15 | const middleware = versionRequest.setVersion(versionNumber) 16 | middleware(t.context.req, {}, () => { 17 | t.is(t.context.req.version, versionNumber + '.0.0') 18 | t.is(versionRequestSpy.called, true) 19 | }) 20 | 21 | versionRequestSpy.restore() 22 | }) 23 | 24 | test('we can manually set a specific version to be string', t => { 25 | const versionNumber = '1.0.0' 26 | 27 | const middleware = versionRequest.setVersion(versionNumber) 28 | middleware(t.context.req, {}, () => { 29 | t.is(versionNumber, t.context.req.version) 30 | }) 31 | }) 32 | 33 | test('we can manually set a specific version to be object', t => { 34 | const versionNumber = { myVersion: 'alpha' } 35 | 36 | const middleware = versionRequest.setVersion(versionNumber) 37 | middleware(t.context.req, {}, () => { 38 | t.is(JSON.stringify(versionNumber), t.context.req.version) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /test/formatVersion.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava') 4 | const versionRequest = require('../index') 5 | 6 | test('it pads a shorter version with two zeros', t => { 7 | t.is(versionRequest.formatVersion('2'), '2.0.0') 8 | }) 9 | 10 | test('it pads a shorter version with one zero', t => { 11 | t.is(versionRequest.formatVersion('2.2'), '2.2.0') 12 | }) 13 | 14 | test('it doesnt change the version, if its correctly formatted', t => { 15 | t.is(versionRequest.formatVersion('2.2.0'), '2.2.0') 16 | }) 17 | 18 | test('it converts and corrects the version, if the input is a number', t => { 19 | t.is(versionRequest.formatVersion(1), '1.0.0') 20 | t.is(versionRequest.formatVersion(1.2), '1.2.0') 21 | }) 22 | 23 | test('it shold truncate the version if its longer than it should be', t => { 24 | t.is(versionRequest.formatVersion('1.0.0.0.0.0.1'), '1.0.0') 25 | t.is(versionRequest.formatVersion('1.0.1.1.0.0.1'), '1.0.1') 26 | }) 27 | 28 | test('it returns undefined, if the input cant be converted into a correct version', t => { 29 | t.is(versionRequest.formatVersion(undefined), undefined) 30 | t.is(versionRequest.formatVersion(null), undefined) 31 | t.is(versionRequest.formatVersion(''), undefined) 32 | t.is(versionRequest.formatVersion(0), undefined) 33 | t.is(versionRequest.formatVersion(() => {}), undefined) 34 | }) 35 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | # Howto with your editor: http://editorconfig.org/#download 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Unix-style newlines with a newline ending every file 8 | [**] 9 | end_of_line = lf 10 | insert_final_newline = true 11 | charset = utf-8 12 | 13 | # Standard at: https://github.com/felixge/node-style-guide 14 | [**.{js,json}] 15 | trim_trailing_whitespace = true 16 | indent_style = space 17 | indent_size = 2 18 | quote_type = single 19 | 20 | # LT: the following are enforced by standardjs so I commented 21 | #curly_bracket_next_line = false 22 | #spaces_around_operators = true 23 | #space_after_control_statements = true 24 | #space_after_anonymous_functions = true 25 | #spaces_in_brackets = false 26 | 27 | # No Standard. Please document a standard if different from .js 28 | [**.{yml,css}] 29 | trim_trailing_whitespace = true 30 | indent_style = space 31 | 32 | [**.html] 33 | trim_trailing_whitespace = true 34 | indent_style = space 35 | indent_size = 2 36 | 37 | # No standard. Please document a standard if different from .js 38 | [**.md] 39 | indent_style = tab 40 | 41 | # Standard at: 42 | [Makefile] 43 | indent_style = tab 44 | 45 | # The indentation in package.json will always need to be 2 spaces 46 | # https://github.com/npm/npm/issues/4718 47 | [{package, bower}.json] 48 | indent_style = space 49 | indent_size = 2 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-version-request", 3 | "version": "1.7.0", 4 | "description": "versions an incoming request to Express based on header or URL", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "standard && eslint *.js", 8 | "test": "yarn lint && ava", 9 | "test:watch": "yarn lint && ava --watch", 10 | "test:coverage": "yarn run lint && nyc --reporter=lcov ava --tap", 11 | "coverage:view": "opn coverage/lcov-report/index.html", 12 | "commit": "git-cz", 13 | "docs": "yarn run docs:code && yarn run docs:api", 14 | "docs:api": "doxdox *.js --layout bootstrap --output docs/index.html", 15 | "docs:code": "docco *.js --output docs/code" 16 | }, 17 | "keywords": [ 18 | "express", 19 | "api", 20 | "version", 21 | "versioned", 22 | "versioning", 23 | "routing", 24 | "router", 25 | "api versioning", 26 | "api version", 27 | "header" 28 | ], 29 | "author": "Liran Tal", 30 | "license": "MIT", 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/lirantal/express-version-request.git" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/lirantal/express-version-request/issues" 37 | }, 38 | "homepage": "https://github.com/lirantal/express-version-request#readme", 39 | "devDependencies": { 40 | "ava": "^0.25.0", 41 | "commitizen": "^2.9.5", 42 | "cz-conventional-changelog": "^2.1.0", 43 | "docco": "^0.8.0", 44 | "doxdox": "^2.0.1", 45 | "eslint": "^4.11.0", 46 | "eslint-plugin-ava": "^5.0.0", 47 | "eslint-plugin-import": "^2.2.0", 48 | "eslint-plugin-node": "^6.0.0", 49 | "eslint-plugin-security": "^1.3.0", 50 | "nyc": "^13.2.0", 51 | "opn-cli": "^3.1.0", 52 | "standard": "^11.0.0", 53 | "sinon": "^6.0.0" 54 | }, 55 | "nyc": { 56 | "statements": 90, 57 | "branches": 90, 58 | "functions": 90, 59 | "lines": 90, 60 | "reporter": [ 61 | "lcov", 62 | "text-summary" 63 | ], 64 | "cache": true, 65 | "check-coverage": true 66 | }, 67 | "config": { 68 | "commitizen": { 69 | "path": "./node_modules/cz-conventional-changelog" 70 | } 71 | }, 72 | "eslintConfig": { 73 | "env": { 74 | "node": true, 75 | "es6": true 76 | }, 77 | "plugins": [ 78 | "node", 79 | "security", 80 | "ava" 81 | ], 82 | "extends": [ 83 | "plugin:ava/recommended", 84 | "plugin:node/recommended" 85 | ], 86 | "rules": { 87 | "node/no-unsupported-features": "off", 88 | "node/no-unpublished-require": "off", 89 | "security/detect-non-literal-fs-filename": "error", 90 | "security/detect-unsafe-regex": "error", 91 | "security/detect-buffer-noassert": "error", 92 | "security/detect-child-process": "error", 93 | "security/detect-disable-mustache-escape": "error", 94 | "security/detect-eval-with-expression": "error", 95 | "security/detect-no-csrf-before-method-override": "error", 96 | "security/detect-non-literal-regexp": "error", 97 | "security/detect-non-literal-require": "error", 98 | "security/detect-object-injection": "error", 99 | "security/detect-possible-timing-attacks": "error", 100 | "security/detect-pseudoRandomBytes": "error" 101 | }, 102 | "parserOptions": { 103 | "ecmaFeatures": { 104 | "impliedStrict": true 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class versionRequest { 4 | static setVersion (version) { 5 | return (req, res, next) => { 6 | req.version = this.formatVersion(version) 7 | next() 8 | } 9 | } 10 | 11 | static setVersionByHeader (headerName) { 12 | return (req, res, next) => { 13 | if (req && req.headers) { 14 | const version = (headerName && req.headers[headerName.toLowerCase()]) || req.headers['x-api-version'] 15 | req.version = this.formatVersion(version) 16 | } 17 | 18 | next() 19 | } 20 | } 21 | 22 | static setVersionByQueryParam (queryParam, options = {removeQueryParam: false}) { 23 | return (req, res, next) => { 24 | if (req && req.query) { 25 | const version = (queryParam && req.query[queryParam.toLowerCase()]) || req.query['api-version'] 26 | if (version !== undefined) { 27 | req.version = this.formatVersion(version) 28 | if (options && options.removeQueryParam === true) { 29 | if (queryParam && req.query[queryParam.toLowerCase()]) { 30 | delete req.query[queryParam.toLowerCase()] 31 | } else { 32 | delete req.query['api-version'] 33 | } 34 | } 35 | } 36 | } 37 | next() 38 | } 39 | } 40 | 41 | static setVersionByAcceptHeader (customFunction) { 42 | return (req, res, next) => { 43 | if (req && req.headers && req.headers.accept) { 44 | if (customFunction && typeof customFunction === 'function') { 45 | req.version = this.formatVersion(customFunction(req.headers.accept)) 46 | } else { 47 | const acceptHeader = String(req.headers.accept) 48 | const params = acceptHeader.split(';')[1] 49 | const paramMap = {} 50 | if (params) { 51 | for (let i of params.split(',')) { 52 | const keyValue = i.split('=') 53 | if (typeof keyValue === 'object' && keyValue[0] && keyValue[1]) { 54 | paramMap[this.removeWhitespaces(keyValue[0]).toLowerCase()] = this.removeWhitespaces(keyValue[1]) 55 | } 56 | } 57 | req.version = this.formatVersion(paramMap.version) 58 | } 59 | 60 | if (req.version === undefined) { 61 | req.version = this.formatVersion(this.setVersionByAcceptFormat(req.headers)) 62 | } 63 | } 64 | } 65 | 66 | next() 67 | } 68 | } 69 | 70 | static setVersionByAcceptFormat (headers) { 71 | const acceptHeader = String(headers.accept) 72 | const header = this.removeWhitespaces(acceptHeader) 73 | let start = header.indexOf('-v') 74 | if (start === -1) { 75 | start = header.indexOf('.v') 76 | } 77 | const end = header.indexOf('+') 78 | if (start !== -1 && end !== -1) { 79 | return header.slice(start + 2, end) 80 | } 81 | } 82 | 83 | static removeWhitespaces (str) { 84 | if (typeof str === 'string') { 85 | return str.replace(/\s/g, '') 86 | } 87 | 88 | return '' 89 | } 90 | 91 | static formatVersion (version) { 92 | if (!version || typeof version === 'function' || version === true) { 93 | return undefined 94 | } 95 | if (typeof version === 'object') { 96 | return JSON.stringify(version) 97 | } 98 | let ver = version.toString() 99 | let split = ver.split('.') 100 | if (split.length === 3) { 101 | return ver 102 | } 103 | if (split.length < 3) { 104 | for (let i = split.length; i < 3; i++) { 105 | ver += '.0' 106 | } 107 | return ver 108 | } 109 | if (split.length > 3) { 110 | return split.slice(0, 3).join('.') 111 | } 112 | } 113 | } 114 | 115 | module.exports = versionRequest 116 | -------------------------------------------------------------------------------- /test/setVersionByHeader.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava') 4 | const versionRequest = require('../index') 5 | const sinon = require('sinon') 6 | 7 | test.beforeEach(t => { 8 | t.context.req = { 9 | headers: { 10 | 'x-timestamp': Date.now() 11 | } 12 | } 13 | }) 14 | 15 | test('dont set a version if req object is not well composed: req is null', t => { 16 | t.context.req = null 17 | const middleware = versionRequest.setVersionByHeader() 18 | 19 | middleware(t.context.req, {}, () => { 20 | t.is(t.context.req, null) 21 | t.throws(function () { 22 | return t.context.req.version 23 | }) 24 | }) 25 | }) 26 | 27 | test('dont set a version if req object is not well composed: req is undefined', t => { 28 | t.context.req = undefined 29 | const middleware = versionRequest.setVersionByHeader() 30 | 31 | middleware(t.context.req, {}, () => { 32 | t.is(t.context.req, undefined) 33 | t.throws(function () { 34 | return t.context.req.version 35 | }) 36 | }) 37 | }) 38 | 39 | test('dont set a version if req object is not well composed: req.headers is undefined', t => { 40 | t.context.req.headers = undefined 41 | const middleware = versionRequest.setVersionByHeader() 42 | 43 | middleware(t.context.req, {}, () => { 44 | t.is(t.context.req.headers, undefined) 45 | t.is(t.context.req.version, undefined) 46 | }) 47 | }) 48 | 49 | test('dont set a version if no version header is set', t => { 50 | t.context.req.headers = {} 51 | const middleware = versionRequest.setVersionByHeader() 52 | 53 | middleware(t.context.req, {}, () => { 54 | t.is(t.context.req.version, undefined) 55 | }) 56 | }) 57 | 58 | test('we can set a version on the request object by request headers', t => { 59 | const versionNumber = '1.0.0' 60 | 61 | t.context.req.headers['x-api-version'] = versionNumber 62 | const middleware = versionRequest.setVersionByHeader() 63 | 64 | middleware(t.context.req, {}, () => { 65 | t.is(t.context.req.version, versionNumber) 66 | }) 67 | }) 68 | 69 | test('we can manually set a specific version to be string', t => { 70 | const versionNumber = '1.0.0' 71 | 72 | t.context.req.headers['x-api-version'] = versionNumber 73 | const middleware = versionRequest.setVersionByHeader() 74 | 75 | middleware(t.context.req, {}, () => { 76 | t.is(t.context.req.version, versionNumber) 77 | }) 78 | }) 79 | 80 | test('we can manually set a specific version to be object', t => { 81 | const versionNumber = { myVersion: 'alpha' } 82 | 83 | t.context.req.headers['x-api-version'] = versionNumber 84 | const middleware = versionRequest.setVersionByHeader() 85 | 86 | middleware(t.context.req, {}, () => { 87 | t.is(t.context.req.version, JSON.stringify(versionNumber)) 88 | }) 89 | }) 90 | 91 | test('we can set a version on the request object by specifying custom http header as integer', t => { 92 | const versionNumber = 1 93 | const versionHeaderName = 'my-api-version-header' 94 | const versionRequestSpy = sinon.spy(versionRequest, 'formatVersion') 95 | 96 | t.context.req.headers[versionHeaderName] = versionNumber 97 | const middleware = versionRequest.setVersionByHeader(versionHeaderName) 98 | 99 | middleware(t.context.req, {}, () => { 100 | t.is(t.context.req.version, versionNumber + '.0.0') 101 | t.is(versionRequestSpy.called, true) 102 | }) 103 | versionRequestSpy.restore() 104 | }) 105 | 106 | test('we can set a version on the request object by specifying custom http header as string', t => { 107 | const versionNumber = '1.0.0' 108 | const versionHeaderName = 'my-api-version-header' 109 | 110 | t.context.req.headers[versionHeaderName] = versionNumber 111 | const middleware = versionRequest.setVersionByHeader(versionHeaderName) 112 | 113 | middleware(t.context.req, {}, () => { 114 | t.is(t.context.req.version, versionNumber) 115 | }) 116 | }) 117 | 118 | test('we can set a version on the request object by specifying custom http header by object', t => { 119 | const versionNumber = { myVersion: 'alpha' } 120 | const versionHeaderName = 'my-api-version-header' 121 | 122 | t.context.req.headers[versionHeaderName] = versionNumber 123 | const middleware = versionRequest.setVersionByHeader(versionHeaderName) 124 | 125 | middleware(t.context.req, {}, () => { 126 | t.is(t.context.req.version, JSON.stringify(versionNumber)) 127 | }) 128 | }) 129 | -------------------------------------------------------------------------------- /test/setVersionByQueryParam.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava') 4 | const versionRequest = require('../index') 5 | const sinon = require('sinon') 6 | 7 | test.beforeEach(t => { 8 | t.context.req = { 9 | query: {} 10 | } 11 | }) 12 | 13 | test('dont set a version if req object is not well composed: req is null', t => { 14 | t.context.req = null 15 | const middleware = versionRequest.setVersionByQueryParam() 16 | 17 | middleware(t.context.req, {}, () => { 18 | t.is(t.context.req, null) 19 | t.throws(function () { 20 | return t.context.req.version 21 | }) 22 | }) 23 | }) 24 | 25 | test('dont set a version if req object is not well composed: req is undefined', t => { 26 | t.context.req = undefined 27 | const middleware = versionRequest.setVersionByQueryParam() 28 | 29 | middleware(t.context.req, {}, () => { 30 | t.is(t.context.req, undefined) 31 | t.throws(function () { 32 | return t.context.req.version 33 | }) 34 | }) 35 | }) 36 | 37 | test('dont set a version if req object is not well composed: req.query is undefined', t => { 38 | t.context.req.query = undefined 39 | const middleware = versionRequest.setVersionByQueryParam() 40 | 41 | middleware(t.context.req, {}, () => { 42 | t.is(t.context.req.query, undefined) 43 | t.is(t.context.req.version, undefined) 44 | }) 45 | }) 46 | 47 | test('dont set a version if no version query is set', t => { 48 | t.context.req.query = {} 49 | const middleware = versionRequest.setVersionByQueryParam() 50 | 51 | middleware(t.context.req, {}, () => { 52 | t.is(t.context.req.version, undefined) 53 | }) 54 | }) 55 | 56 | test('we can set a version on the request object by request query parameters', t => { 57 | const versionNumber = '1.0.0' 58 | 59 | t.context.req.query['api-version'] = versionNumber 60 | const middleware = versionRequest.setVersionByQueryParam() 61 | 62 | middleware(t.context.req, {}, () => { 63 | t.is(t.context.req.version, versionNumber) 64 | }) 65 | }) 66 | 67 | test('we can manually set a specific version to be string', t => { 68 | const versionNumber = '1.0.0' 69 | 70 | t.context.req.query['api-version'] = versionNumber 71 | const middleware = versionRequest.setVersionByQueryParam() 72 | 73 | middleware(t.context.req, {}, () => { 74 | t.is(t.context.req.version, versionNumber) 75 | }) 76 | }) 77 | 78 | test('we can manually set a specific version to be object', t => { 79 | const versionNumber = { myVersion: 'alpha' } 80 | 81 | t.context.req.query['api-version'] = versionNumber 82 | const middleware = versionRequest.setVersionByQueryParam() 83 | 84 | middleware(t.context.req, {}, () => { 85 | t.is(t.context.req.version, JSON.stringify(versionNumber)) 86 | }) 87 | }) 88 | 89 | test('we can set a version on the request object by specifying custom http query param as integer', t => { 90 | const versionNumber = 1 91 | const versionParamName = 'my-api-version-param' 92 | const versionRequestSpy = sinon.spy(versionRequest, 'formatVersion') 93 | 94 | t.context.req.query[versionParamName] = versionNumber 95 | const middleware = versionRequest.setVersionByQueryParam(versionParamName) 96 | 97 | middleware(t.context.req, {}, () => { 98 | t.is(t.context.req.version, versionNumber + '.0.0') 99 | t.is(versionRequestSpy.called, true) 100 | }) 101 | versionRequestSpy.restore() 102 | }) 103 | 104 | test('we can set a version on the request object by specifying custom http query param as string', t => { 105 | const versionNumber = '1.0.0' 106 | const versionParamName = 'my-api-version-param' 107 | 108 | t.context.req.query[versionParamName] = versionNumber 109 | const middleware = versionRequest.setVersionByQueryParam(versionParamName) 110 | 111 | middleware(t.context.req, {}, () => { 112 | t.is(t.context.req.version, versionNumber) 113 | }) 114 | }) 115 | 116 | test('we can set a version on the request object by specifying custom http query param by object', t => { 117 | const versionNumber = { myVersion: 'alpha' } 118 | const versionParamName = 'my-api-version-param' 119 | 120 | t.context.req.query[versionParamName] = versionNumber 121 | const middleware = versionRequest.setVersionByQueryParam(versionParamName) 122 | 123 | middleware(t.context.req, {}, () => { 124 | t.is(t.context.req.version, JSON.stringify(versionNumber)) 125 | }) 126 | }) 127 | 128 | test('custom query param should be deleted from req.query after handling it', t => { 129 | const versionNumber = '1.0.0' 130 | const versionParamName = 'my-api-version-param' 131 | const options = {removeQueryParam: true} 132 | 133 | t.context.req.query[versionParamName] = versionNumber 134 | const middleware = versionRequest.setVersionByQueryParam(versionParamName, options) 135 | 136 | middleware(t.context.req, {}, () => { 137 | t.is(t.context.req.version, versionNumber) 138 | t.falsy(t.context.req.query.hasOwnProperty(versionParamName)) 139 | }) 140 | }) 141 | 142 | test('default query param should be deleted from req.query after handling it', t => { 143 | const versionNumber = '1.0.0' 144 | const options = {removeQueryParam: true} 145 | 146 | t.context.req.query['api-version'] = versionNumber 147 | const middleware = versionRequest.setVersionByQueryParam(null, options) 148 | 149 | middleware(t.context.req, {}, () => { 150 | t.is(t.context.req.version, versionNumber) 151 | t.falsy(t.context.req.query.hasOwnProperty('api-version')) 152 | }) 153 | }) 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-version-request 2 | 3 | 4 | [![view on npm](http://img.shields.io/npm/v/express-version-request.svg)](https://www.npmjs.org/package/express-version-request) 5 | [![view on npm](http://img.shields.io/npm/l/express-version-request.svg)](https://www.npmjs.org/package/express-version-request) 6 | [![npm module downloads](http://img.shields.io/npm/dt/express-version-request.svg)](https://www.npmjs.org/package/express-version-request) 7 | [![Build Status](https://travis-ci.org/lirantal/express-version-request.svg?branch=master)](https://travis-ci.org/lirantal/express-version-request) 8 | [![codecov](https://codecov.io/gh/lirantal/express-version-request/branch/master/graph/badge.svg)](https://codecov.io/gh/lirantal/express-version-request) 9 | [![Security Responsible Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yellow.svg)](https://github.com/nodejs/security-wg/blob/master/processes/responsible_disclosure_template.md) 10 | 11 | [![express-version-request](https://snyk.io/advisor/npm-package/express-version-request/badge.svg)](https://snyk.io/advisor/npm-package/express-version-request) 12 | 13 | ## What is this? 14 | 15 | This npm package provides an ExpressJS middleware that sets the request object with a `version` property by parsing a request HTTP header. 16 | 17 | ## Usage 18 | 19 | ### Set request version statically 20 | 21 | If you wish to employ your own logic in some middleware/configuration and set the request version programmaticaly and not by parsing a specific HTTP header: 22 | 23 | ```js 24 | const versionRequest = require('express-version-request') 25 | 26 | app.use(versionRequest.setVersion('1.2.3')) 27 | ``` 28 | 29 | Then in later middlewares you will be able to access `req.version` property and it's value set to 1.2.3. 30 | 31 | ### Set request version by HTTP header 32 | 33 | By default, the library will parse the version out of the `X-Api-Version` HTTP header: 34 | 35 | ```js 36 | const versionRequest = require('express-version-request') 37 | 38 | app.use(versionRequest.setVersionByHeader()) 39 | ``` 40 | 41 | ### Set request version by custom HTTP header 42 | 43 | If you wish to advise the library which HTTP header to parse to extract the version: 44 | 45 | ```js 46 | const versionRequest = require('express-version-request') 47 | 48 | app.use(versionRequest.setVersionByHeader('My-HTTP-Header-Name')) 49 | ``` 50 | 51 | ### Set request version by HTTP query parameter 52 | 53 | By default, the library will parse the version out of the `api-version` query parameter: 54 | 55 | ```js 56 | const versionRequest = require('express-version-request') 57 | 58 | app.use(versionRequest.setVersionByQueryParam()) 59 | ``` 60 | 61 | ### Set request version by custom HTTP query parameter 62 | 63 | If you wish to advise the library which query parameter to parse to extract the version: 64 | 65 | ```js 66 | const versionRequest = require('express-version-request') 67 | 68 | app.use(versionRequest.setVersionByQueryParam('myQueryParam')) 69 | ``` 70 | #### setVersionByQueryParam options 71 | The second parameter of `setVersionByQueryParam` is an options object. 72 | 73 | ### Set request version by 'Accept' header 74 | 75 | By default, the library will parse the version from the Accept header, expecting the following format: 76 | **Accept: application/vnd.company+json;version=1.0.0** 77 | For more details about the Accept header format, please refer to the [RFC](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html). 78 | 79 | 80 | ```js 81 | const versionRequest = require('express-version-request') 82 | 83 | app.use(versionRequest.setVersionByAcceptHeader()) 84 | ``` 85 | #### Parsing using an alternative format 86 | As a fallback, the lib supports an alternative Accept header format: 87 | 88 | **Accept: application/vnd.comapny-v1.0.0+json** 89 | 90 | or 91 | 92 | **Accept: application/vnd.comapny.v1.0.0+json** 93 | 94 | The lib will try to parse the header using the default format, and if it doesn't succeed, tries this alternative format. 95 | The usage is the same as in the case of the regular format: 96 | 97 | ```js 98 | const versionRequest = require('express-version-request') 99 | 100 | app.use(versionRequest.setVersionByAcceptHeader()) 101 | ``` 102 | #### Parsing using a custom function 103 | If you wish to use your own parsing, it is possible to pass a function as the first parameter. 104 | The lib will then call it with the actual value of the Accept header as the first parameter, and the returned value will be set as version. 105 | The provided function should return a **string**. 106 | 107 | ```js 108 | const versionRequest = require('express-version-request') 109 | function customParsingFunction(header) { 110 | //function body, that parses the header parameter and returns a string 111 | } 112 | 113 | app.use(versionRequest.setVersionByAcceptHeader(customParsingFunction)) 114 | ``` 115 | #### Version formatting 116 | Before setting the version, it is always formatted, so the resulting version is a semver comaptible string, except the following cases: 117 | 118 | * if the version was set as an Object, it will be returned in stringified format (using JSON.stringify) 119 | * if the version is longer than the semver format, we truncate it by cutting off the tail, and leaving the first three segments (e.g.: 1.2.3.4.5 will become 1.2.3) 120 | * if we encunter something, that can't be parsed or formatted as a version, undefined is returned 121 | 122 | This formatting function is called automatically for each version setting method, but it can also be used independently: 123 | ```js 124 | const versionRequest = require('express-version-request') 125 | const formattedVersion = versionRequest.formatVersion(versionThatNeedsFormatting) 126 | ``` 127 | ##### Options 128 | 129 | `removeQueryParam` 130 | 131 | Delete version HTTP query parameter after setting the request object with a `version` property. 132 | By default it is set to false. 133 | 134 | ```js 135 | const versionRequest = require('express-version-request') 136 | const options = {removeQueryParam: true} 137 | 138 | app.use(versionRequest.setVersionByQueryParam('myQueryParam', options)) 139 | ``` 140 | 141 | If you define a middleware after versionRequest then you can verify that the version is indeed set: 142 | 143 | ```js 144 | app.use((req, res, next) => { 145 | console.log(req.version) 146 | next() 147 | }) 148 | ``` 149 | 150 | ## Installation 151 | 152 | ```bash 153 | yarn add express-version-request 154 | ``` 155 | 156 | ## TypeScript Support 157 | 158 | ```bash 159 | yarn add --dev @types/express-version-request 160 | ``` 161 | 162 | _Note: Don't forget to add types for Express!_ 163 | 164 | ## Tests 165 | 166 | ```bash 167 | yarn test 168 | ``` 169 | 170 | Project linting: 171 | 172 | ```bash 173 | yarn lint 174 | ``` 175 | 176 | ## Coverage 177 | 178 | ```bash 179 | yarn test:coverage 180 | ``` 181 | 182 | ## Commit 183 | 184 | The project uses the commitizen tool for standardizing changelog style commit 185 | messages so you should follow it as so: 186 | 187 | ```bash 188 | git add . # add files to staging 189 | yarn commit # use the wizard for the commit message 190 | ``` 191 | 192 | ## Author 193 | 194 | Liran Tal 195 | -------------------------------------------------------------------------------- /test/setVersionByAcceptHeader.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava') 4 | const versionRequest = require('../index') 5 | const sinon = require('sinon') 6 | 7 | test.beforeEach(t => { 8 | t.context.req = { 9 | headers: {} 10 | } 11 | }) 12 | 13 | test('we can set the version using the Accept header version field', t => { 14 | const versionNumber = '1.0.0' 15 | 16 | t.context.req.headers['accept'] = 'application/vnd.company+json;version=' + versionNumber 17 | const middleware = versionRequest.setVersionByAcceptHeader() 18 | 19 | middleware(t.context.req, {}, () => { 20 | t.deepEqual(t.context.req.version, versionNumber) 21 | }) 22 | }) 23 | 24 | test('we can set the version using the Accept header version field, even if we have multiple parameters', t => { 25 | const versionNumber = '1.0.0' 26 | 27 | t.context.req.headers['accept'] = 'application/vnd.company+json;param1=1,version=' + versionNumber + ', param3=3' 28 | const middleware = versionRequest.setVersionByAcceptHeader() 29 | 30 | middleware(t.context.req, {}, () => { 31 | t.deepEqual(t.context.req.version, versionNumber) 32 | }) 33 | }) 34 | 35 | test('we can set the version using the Accept header version field, even if it has funky whitespaces', t => { 36 | const versionNumber = '1.0.0' 37 | 38 | t.context.req.headers['accept'] = 'application/vnd.company+json; param1=1, version =' + versionNumber + ' , param3=3' 39 | const middleware = versionRequest.setVersionByAcceptHeader() 40 | 41 | middleware(t.context.req, {}, () => { 42 | t.deepEqual(t.context.req.version, versionNumber) 43 | }) 44 | }) 45 | 46 | test('we can set the version using the Accept header version field, even if it mixes lower- and uppercase characters', t => { 47 | const versionNumber = '1.0.0' 48 | 49 | t.context.req.headers['accept'] = 'application/vnd.company+json; param1=1, Version =' + versionNumber + ' , param3=3' 50 | const middleware = versionRequest.setVersionByAcceptHeader() 51 | 52 | middleware(t.context.req, {}, () => { 53 | t.deepEqual(t.context.req.version, versionNumber) 54 | }) 55 | }) 56 | 57 | test('dont set the version if the Accept header has no "version" parameter', t => { 58 | t.context.req.headers['accept'] = 'application/vnd.company+json;param1=1, param2=2' 59 | const middleware = versionRequest.setVersionByAcceptHeader() 60 | 61 | middleware(t.context.req, {}, () => { 62 | t.deepEqual(t.context.req.version, undefined) 63 | }) 64 | }) 65 | 66 | test('dont set the version if the Accept header has no parameters at all', t => { 67 | t.context.req.headers['accept'] = 'application/vnd.company+json;' 68 | const middleware = versionRequest.setVersionByAcceptHeader() 69 | 70 | middleware(t.context.req, {}, () => { 71 | t.deepEqual(t.context.req.version, undefined) 72 | }) 73 | }) 74 | 75 | test('dont set the version if the Accept header has no parameters at all (without ending ;)', t => { 76 | t.context.req.headers['accept'] = 'application/vnd.company+json' 77 | const middleware = versionRequest.setVersionByAcceptHeader() 78 | 79 | middleware(t.context.req, {}, () => { 80 | t.deepEqual(t.context.req.version, undefined) 81 | }) 82 | }) 83 | 84 | test('dont set the version if the Accept header if we cant parse it', t => { 85 | t.context.req.headers['accept'] = 'application/json;abd' 86 | const middleware = versionRequest.setVersionByAcceptHeader() 87 | 88 | middleware(t.context.req, {}, () => { 89 | t.deepEqual(t.context.req.version, undefined) 90 | }) 91 | }) 92 | 93 | test('dont set the version if the Accept header if we cant parse it', t => { 94 | t.context.req.headers['accept'] = 42 95 | const middleware = versionRequest.setVersionByAcceptHeader() 96 | 97 | middleware(t.context.req, {}, () => { 98 | t.deepEqual(t.context.req.version, undefined) 99 | }) 100 | }) 101 | 102 | // Alternative format 103 | test('we can set the version using the Accept header alternative format 1', t => { 104 | const versionNumber = '1.0.0' 105 | 106 | t.context.req.headers['accept'] = 'application/vnd.company-v' + versionNumber + '+json' 107 | const middleware = versionRequest.setVersionByAcceptHeader() 108 | 109 | middleware(t.context.req, {}, () => { 110 | t.deepEqual(t.context.req.version, versionNumber) 111 | }) 112 | }) 113 | 114 | test('we can set the version using the Accept header alternative format 2', t => { 115 | const versionNumber = '1.0.0' 116 | 117 | t.context.req.headers['accept'] = 'application/vnd.company.v' + versionNumber + '+json' 118 | const middleware = versionRequest.setVersionByAcceptHeader() 119 | 120 | middleware(t.context.req, {}, () => { 121 | t.deepEqual(t.context.req.version, versionNumber) 122 | }) 123 | }) 124 | 125 | test('we can set the version using the Accept header alternative format, even if it has whitespaces', t => { 126 | const versionNumber = '1.0.0' 127 | 128 | const headers = { accept: 'application/ vnd.company -v' + versionNumber + ' + json' } 129 | const resultingVersion = versionRequest.setVersionByAcceptFormat(headers) 130 | 131 | t.deepEqual(resultingVersion, versionNumber) 132 | }) 133 | 134 | test('dont set the version, if the alternative format is incorrect', t => { 135 | const headers = { accept: 'application/ vnd.company -v1.0.0///json' } 136 | const resultingVersion = versionRequest.setVersionByAcceptFormat(headers) 137 | 138 | t.deepEqual(resultingVersion, undefined) 139 | }) 140 | 141 | // Custom function 142 | 143 | test('we can set the version using a custom function to parse the Accept header', t => { 144 | const versionNumber = '1.0.0' 145 | 146 | t.context.req.headers['accept'] = versionNumber 147 | const middleware = versionRequest.setVersionByAcceptHeader(v => v) 148 | 149 | middleware(t.context.req, {}, () => { 150 | t.deepEqual(t.context.req.version, versionNumber) 151 | }) 152 | }) 153 | 154 | test('we can handle, if the custom function returns a number', t => { 155 | const versionNumber = '1.1' 156 | const versionRequestSpy = sinon.spy(versionRequest, 'formatVersion') 157 | 158 | t.context.req.headers['accept'] = versionNumber 159 | const middleware = versionRequest.setVersionByAcceptHeader(v => parseFloat(v)) 160 | 161 | middleware(t.context.req, {}, () => { 162 | t.deepEqual(t.context.req.version, versionNumber + '.0') 163 | t.is(versionRequestSpy.called, true) 164 | }) 165 | versionRequestSpy.restore() 166 | }) 167 | 168 | test('we can handle, if the custom function returns a boolean', t => { 169 | const versionNumber = true 170 | 171 | t.context.req.headers['accept'] = versionNumber 172 | const middleware = versionRequest.setVersionByAcceptHeader(v => versionNumber) 173 | 174 | middleware(t.context.req, {}, () => { 175 | t.deepEqual(t.context.req.version, undefined) 176 | }) 177 | }) 178 | 179 | test('we can handle, if the custom function returns an object', t => { 180 | const versionNumber = {alpha: true} 181 | t.context.req.headers['accept'] = 1 182 | const middleware = versionRequest.setVersionByAcceptHeader(v => { return versionNumber }) 183 | 184 | middleware(t.context.req, {}, () => { 185 | t.deepEqual(t.context.req.version, JSON.stringify(versionNumber)) 186 | }) 187 | }) 188 | --------------------------------------------------------------------------------