BohemiaInteractive/bi-service

View on GitHub
lib/response.js

Summary

Maintainability
B
5 hrs
Test Coverage
const _  = require('lodash');

const utils           = require('./utils.js');
const ValidationError = require('./error/validationError.js');

const proxyResponsePrototypeProps = {
    send        : true, //signal that method takes data argument as first argument
    json        : true,
    jsonp       : true,
    write       : true,
    status      : false,
    links       : false,
    sendStatus  : false,
    type        : false,
    contentType : false,
    header      : false,
    setHeader   : false,
    set         : false,
    get         : false,
    clearCookie : false,
    cookie      : false
};


module.exports = Response;
module.exports.Response = Response;

/**
 * Error type to be thrown when a response validation error is encountered.
 * Defaults to {@link ValidationError}. Can be updated by the user.
 * @name Response.ValidationError
 * @type {Error}
 */
Response.ValidationError = ValidationError;

/**
 * simple wrapper object holding a callback which sets response headers & data when executed
 *
 * @param {Function} cb - callback function which actually sets response on the (express) `res` object. It's context is always set to the `res` object
 *
 * @constructor
 * @return {Response}
 */
function Response(cb) {
    if (!(this instanceof Response)) {
        return new Response(cb);
    }

    this._fn = cb;
}

/**
 * wraps express `res` object with additional methods
 *
 * @param {Response} res - express res object
 * @param {Route} route
 * @param {Object} [options]
 * @param {Boolean} [options.filter=true]
 * @param {Boolean} [options.setPaginationHeaders=true]
 *
 * @private
 * @return {Response}
 */
Response.wrap = function wrap(res, route, options) {
    options = options || {};

    if (options.filter !== false) {
        res.filter = function(data) {
            return filter.call(this, data, route);
        };
    }

    if (options.setPaginationHeaders !== false) {
        res.setPaginationHeaders = function(options) {
            return setPaginationHeaders.call(this, options, route);
        };
    }

    return res;
};

/**
 * filters received data according to json-inspector schema which has been staticaly
 * set via the `route.respondsWith(schema)`. Returned wrapped response object has
 * a constrain set so it can respond only with the filtered data.
 *
 * @param {mixin} data
 * @param {Route} route
 *
 * @private
 * @throws {ValidationError}
 * @this {Response} - object with express res object as its prototype
 * @return {Object} - wrapper around response object with explicitly set res data
 */
function filter(data, route) {

    var res = this
    ,   statusCode = res.statusCode
    ,   validator = route.Router.App.getValidator()
    ,   schemaId
    ,   schema;

    if (!statusCode) {
        statusCode = 200;
    }

    if (!route.description.responses.hasOwnProperty(statusCode)
        || !route.description.responses[statusCode].length
        || !route.description.responses[statusCode][0].schema
    ) {
        throw new Error(`There is no response data format set for status code: ${statusCode}. Can't filter response data`);
    }

    if (!_.isPlainObject(data) && data && data.toJSON instanceof Function) {
        data = data.toJSON();
    }

    schema = route.description.responses[statusCode][0].schema;

    if (typeof schema === 'string') {
        schemaId = schema;
    } else {
        schemaId = route.uid + '-RES';//req response schema

        if (!validator.getSchema(schemaId)) {
            validator.addSchema(schema, schemaId);
        }
    }

    if (!validator.validate(schemaId, data)) {
        let err = validator.errors.shift();
        if (err && !err.dataPath) {
            err.dataPath = '<response>';
        }
        throw new Response.ValidationError(err);
    }

    return new Proxy(res, {
        get: function(target, name, receiver) {
            if (proxyResponsePrototypeProps.hasOwnProperty(name)) {
                if (proxyResponsePrototypeProps[name]) {
                    return function() {
                        target[name](data);
                        return receiver;
                    };
                }

                return Reflect.get(target, name, receiver);
            }
        }
    });
}

/**
 * @private
 * @param {Object}  options
 * @param {Integer} options.count
 * @param {Integer} options.offset
 * @param {Integer} options.limit
 * @param {Route}   route
 * @this {Object}   res
 * return {Object} - res object
 */
function setPaginationHeaders(options, route) {
    options = options || {};

    let headerValue = ''
    ,   urlMeta = utils.getPaginationMeta(options)
    ,   res = this;

    Object.keys(urlMeta).forEach(function(rel) {
        let url = route.getAbsoluteUrl(res.req.params, urlMeta[rel] || {});
        headerValue += `<${url}>; rel="${rel}", `;
    });

    headerValue = headerValue.substr(0, headerValue.length - 2);

    this.setHeader('Link', headerValue);
    this.setHeader('x-total-count', options.count);

    return this;
}