BohemiaInteractive/bi-service

View on GitHub
lib/express/route.js

Summary

Maintainability
A
0 mins
Test Coverage
'use strict';

const _  = require('lodash');
const qs = require('qs');

const RouteI          = require('../common/route.js');
const Response        = require('../response.js');
const RouteError      = require('../error/routeError.js');
const RequestType     = require('./requestType.js');
const ServiceError    = require('../error/serviceError.js');
const reqBodyParser   = require('../middleware/requestContentType.js');

module.exports = Route;

/**
 * @param {Object} options
 * @param {String} [options.name]
 * @param {String} options.type - see {@link RequestType} enum for available option values
 * @param {String} options.url
 * @param {String} options.summary - document the end point
 * @param {String} options.desc - document the end point
 * @param {String} options.sdkMethodName - sdk method name for API clients
 *
 * @throws {RouteError}
 * @extends RouteInterface
 * @constructor
 **/
function Route(options) {
    options = options || {};
    const self = this;

    if (_.values(RequestType).indexOf(options.type) === -1) {
        throw new RouteError('Invalid request method type, got: ' + options.type);
    }

    RouteI.call(this, options);

    this.options.url = this.Router.$normalizeUrl(options.url);


    this.step('init', function(req, res) {
        //bind custom methods to the express res object
        Response.wrap(res, self);
        req.routeUID = self.uid;
    });
    this.$setContentTypeParser(reqBodyParser);
    //any route can respond with 500 - Service Error
    this.respondsWith(ServiceError);
};

Route.prototype = Object.create(RouteI.prototype);
Route.prototype.constructor = Route;

/**
 * returns hostname relative route endpoint
 *
 * @param {Object} [pathParams]
 * @param {Object} [queryParams]
 *
 * @return {String}
 */
Route.prototype.getUrl = function getUrl(pathParams, queryParams) {

    if (this.options.url instanceof RegExp) {
        throw new RouteError(
            'Not supported as the route endpoint includes regexp expression.'
        );
    }

    //we need to normalize the url when Router's url is just '/'
    var url = this.Router.$normalizeUrl(this.Router.getUrl() + this.options.url);

    if (typeof pathParams === 'object' && pathParams !== null) {
        Object.keys(pathParams).forEach(function(name) {
            url = url.replace(`:${name}`, pathParams[name]);
        });
    }

    //remove express-like regex matching part of url segment
    // eg.: /path/:id(\d+) => /path/:id
    url = url.replace(/\(.+\)/, '');

    if (   typeof queryParams === 'object'
        && queryParams !== null
        && Object.keys(queryParams).length
    ) {
        url = url + '?' + qs.stringify(queryParams);
    }

    return url;
};

/**
 * returns absolute route endpoint.  
 * In order to work properly service config option `apps.<app-name>.baseUrl` should be set
 *
 * @param {Object} [pathParams]
 * @param {Object} [queryParams]
 *
 * @return {String}
 */
Route.prototype.getAbsoluteUrl = function getAbsoluteUrl(pathParams, queryParams) {
    return this.Router.App.getHost() + this.getUrl(pathParams, queryParams);
};

/**
 * returns route's name. If no name has been assigned,
 * the name is dynamically created from route's url path
 *
 * @return {String}
 */
Route.prototype.getName = function() {
    if (this.options.name) {
        return this.options.name;
    } else if (this.options.url instanceof RegExp) {
        throw new RouteError('The route must define the `name` option value' +
            ' when its endpoint includes a regexp expression.');
    }

    var name = ''
    ,   url = this.Router.getUrl() + this.options.url
    ,   basePath = this.Router.App.config.get('basePath');

    if (url.indexOf(basePath) === 0) {
        url = url.substr(basePath.length);
    }
    //assign default route uid which we make up from route's endpoint
    url.split('/').forEach(function(segment) {
        var pattern = '^(api|s2s|{version}|v[0-9]+(\.[0-9])*|:[a-zA-Z0-9-_]+)$';
        if (!segment.match(pattern)) {
            name += _.upperFirst(segment.toLowerCase());
        }
    });

    return name;
};

/**
 * @private
 * @param {String} format
 * @return {String}
 */
Route.prototype.$formatUid = function(format) {
    var type    = this.options.type.toLowerCase();
    var name    = this.getName();
    var version = this.Router.$getVersionString();

    if (format.match(/{version}/g) && !version) {
        throw new RouteError('Can not format route UID, expected url version but got: ' + version);
    }

    format = format.replace(/{method}/g, type);
    format = format.replace(/{Method}/g, _.upperFirst(type));
    format = format.replace(/{name}/g, _.lowerFirst(name));
    format = format.replace(/{Name}/g, _.upperFirst(name));
    format = format.replace(/{version}/g, version);

    return format;
};

/**
 * pushes specifically configured validation middleware to the route's call stack
 *
 * @example
 *
 * route.validate({
 *     properties: {
 *         username: {type: 'string'}
 *     }
 * }, 'query');
 *
 * //or
 *
 * route.validate('ajv-registered-validation-schema-uid', 'body');
 *
 * @param {string|Object} valDef - string => registered validator's name. Object => schema definition
 * @param {string}  dataProp - query|body|params|headers
 *
 * @return {Route} - self
 */
Route.prototype.validate = function() {
    const args = Array.prototype.slice.call(arguments, 0);
    this.respondsWith(this.Router.App.getValidator().ValidationErrorConstructor);
    return RouteI.prototype.validate.apply(this, args);
};

/**
 * @private
 * @return {undefined}
 */
Route.prototype.$setSupportedContentTypes = function() {
    //these request methods are supposed to not accpent entity body by default
    //user still can enable them explicitly with route.acceptsContentType(type)
    if (['get', 'options', 'delete'].includes(this.options.type.toLowerCase())) {
        return;
    }

    const args = Array.prototype.slice.call(arguments, 0);
    return RouteI.prototype.$setSupportedContentTypes.apply(this, args);
};