lib/index.js
'use strict'
/**
* A function generating a custom error to be passed into next when the version
* is not valid
* @typedef {function} ErrorGenerator
* @return {error}
* @public
*/
/**
* An object containing the configuration options of the validateVersion function
* @typedef {object} ValidatorOptions
* @property {string[]} versions
* @property {?boolean} sendReply
* @property {?boolean} isMandatory
* @property {?boolean} sendVersionHeader
* @property {?ErrorGenerator} generateError
* @public
*/
const semver = require('semver')
/**
* Generates a middleware to parse and validate versions out of the request
* @example
* let app = require('express')();
* let vv = require('version-validator');
*
* app.use(vv.validateVersion({
* versions: ['1.0.0'],
* isMandatory: true
* }));
* app.all('/', (req, res) => res.status(200).json({
* req.version,
* req.matchedVersion
* }));
*
* @param {ValidatorOptions|string[]} args An object of options, or an array of supported semver versions
* @return {Middleware} A middleware that validates the requested version and populates `req.version` and `req.matchedVersion`
* @throws {TypeError} Properties of args must have their specified types
* @public
*/
function validateVersion (args) {
args = validateArgs(args)
const options = {
supportedVersions: args.versions,
handleUnsupported: args.sendReply ? sendReply.bind(args.versions) : sendError.bind(args.generateError),
initialVersion: args.isMandatory ? null : '*',
sendVersionHeader: args.sendVersionHeader
}
return addVersion.bind(options)
}
/**
* Partial middleware that will be extended by validateVersion
* @param {Request} req An expressjs request object
* @param {Response} res An expressjs request object
* @param {function} next A callback that signals the middleware is done processing the request
* @return {undefined} Returns undefined
* @private
*/
function addVersion (req, res, next) {
let requestedVersion = this.initialVersion
if (req.query.version) requestedVersion = req.query.version
else if (req.headers['accept-version']) requestedVersion = req.headers['accept-version']
const matchedVersion = semver.maxSatisfying(this.supportedVersions, requestedVersion)
if (!matchedVersion) return this.handleUnsupported(req, res, next)
req.matchedVersion = matchedVersion
req.version = requestedVersion
if (this.sendVersionHeader) res.set('API-VERSION', matchedVersion)
next()
}
/**
* Validates the options passed to validateVersion and sets default values where required
* @param {ValidatorOptions} args The options passed to validateVersion
* @return {ValidatorOptions} The options with default values where necessary
* @throws {TypeError} Properties of args must have their specified types
* @private
*/
function validateArgs (args) {
if (Array.isArray(args)) args = { versions: args }
if (!args.versions) throw new TypeError('Arguments should contain a list of supported versions')
if (!Array.isArray(args.versions)) throw new TypeError('versions should be a list of strings')
args.versions.forEach((version) => {
if (!semver.valid(version)) throw new TypeError(`Version ${version} is not a valid semver string`)
})
if (args.sendReply === undefined) args.sendReply = true
if (typeof args.sendReply !== 'boolean') throw new TypeError('sendReply should be a boolean')
if (args.isMandatory === undefined) args.isMandatory = false
if (typeof args.isMandatory !== 'boolean') throw new TypeError('isMandatory should be a boolean')
if (args.sendVersionHeader === undefined) args.sendVersionHeader = true
if (typeof args.sendVersionHeader !== 'boolean') throw new TypeError('sendVersionHeader should be a boolean')
if (args.generateError === undefined) args.generateError = generateDefaultError.bind(args.versions)
if (args.generateError && typeof args.generateError !== 'function') throw new TypeError('generateError should be a function')
return args
}
/**
* Call the next middleware with the error generated by this()
* `this` is a function that will generate the error to be passed into the callback (bound at configuration time)
* @this ValidatorOptions.generateError
* @param {Request} req An expressjs Request object
* @param {Response} res An expressj Response object
* @param {function} next The callback to signal that we are done processing the Request
* @return {undefined} Returns void
* @private
*/
function sendError (req, res, next) {
next(this())
}
/**
* Send a reply to the client to indicate that requested version is not Supported
* @param {Request} req An expressjs Request object
* @param {Response} res An expressjs Response object
* @return {undefined} Returns void
* @private
*/
function sendReply (req, res) {
const output = {
statusCode: 400,
title: 'Invalid Version',
detail: `Supported Versions: [${this.join(', ')}]`
}
res.status(400).json(output)
}
/**
* A default implementation of generateError, to serve as a bound argument in sendError
* @return {error} An error object with error.status and error.detail set
* @private
*/
function generateDefaultError () {
const error = new Error('Invalid Version')
error.status = 400
error.detail = `Supported Versions: [${this.join(', ')}]`
return error
}
/**
* Generates a middleware route that routes the request into `next()` if the version
* matches and `next('route')` if the version does not matches
* @example
* let app = require('express')();
* let vv = require('version-validator');
*
* app.use(vv.validateVersion(['1.0.0']));
* app.all('/', vv.isVersion('1.0.0'), (req, res) => res.status(200).send(`${req.matchedVersion} is 1.0.0`));
* app.all('/', (req, res) => res.status(200).send(`${req.matchedVersion} is not 1.0.0`));
*
* @param {string} version A semver string
* @return {Middleware} A middleware to route requests for this version
* @throws {TypeError} `version` must be a valid semver string
* @public
*/
function isVersion (version) {
if (!semver.valid(version)) throw new TypeError(`Version ${version} is not a valid semver string`)
return routeVersion.bind(version)
}
/**
* Partial middleware function. Will route requests for the version bound into @this
* @this version
* @param {Request} req An expressjs Request object
* @param {Response} res An expressjs Response object
* @param {function} next A callback to signal that the middleware is done processing the requests
* @return {undefined} Returns void
* @private
*/
function routeVersion (req, res, next) {
if (semver.eq(req.matchedVersion, this.toString())) return next()
return next('route')
}
module.exports = {
isVersion,
validateVersion
}