RackHD/on-http

View on GitHub
lib/services/swagger-api-service.js

Summary

Maintainability
F
4 days
Test Coverage
// Copyright 2016, EMC, Inc.

'use strict';

var di = require('di');
var util = require('util');
var path = require('path');
var parseUrl = require('url').parse;

module.exports = swaggerFactory;

di.annotate(swaggerFactory, new di.Provide('Http.Services.Swagger'));
di.annotate(swaggerFactory,
    new di.Inject(
            'Promise',
            'Errors',
            '_',
            di.Injector,
            'Views',
            'Assert',
            'Http.Api.Services.Schema',
            'Services.Configuration',
            'Services.Environment',
            'Services.Lookup',
            'Constants',
            'ejs',
            'Services.Waterline',
            'Protocol.Task',
            'Logger'
        )
    );

function swaggerFactory(
    Promise,
    Errors,
    _,
    injector,
    views,
    assert,
    schemaApiService,
    config,
    env,
    lookupService,
    Constants,
    ejs,
    waterline,
    taskProtocol,
    Logger
) {
    var logger = Logger.initialize(swaggerFactory);
    function _processError(err) {
        if (!util.isError(err) && err instanceof Object) {
            var status = err.status;
            var message = (err instanceof Error) ? err : err.message;
            err = new Error(message);
            if (status) { err.status = status; }
        }
        return err;
    }

    function _parseQuery(req) {
        req.swagger.query = _(req.swagger.params)
        .pick(function(param) {
            if (param.parameterObject) {
                return param.parameterObject.in === 'query' &&
                       param.value !== undefined;
            }
            return false;
        })
        .mapValues(function(param) {
            req.query = _(req.query).omit(param.parameterObject.definition.name).value();
            return param.value;
        }).value();
    }

    function _sortQuery(query){
        var sortParams = {};
        var match = query.raw.match(/([-+]{0,1})(.+)/);
        sortParams.sortBy = match[2].trim();
        sortParams.dsc= match[1] ==='-'? true:false;
        return sortParams;
    }

    function _sortByAny(field, reverse, primer) {
        var key = primer ?
           function(x) {return primer(x[field]);} :
           function(x) {return x[field];};
        reverse = !reverse ? 1 : -1;
        return function (a, b) {
            return a = key(a), b = key(b), reverse * ((a > b) - (b > a));
        };
    }

    function swaggerController(options, callback) {
        if (typeof options === 'function') {
            callback = options;
            options = {};
        }
        return function(req, res, next) {
            var toSort = null;
            req.swagger.options = options;
            return Promise.try(function() {
                _parseQuery(req);
                if (req.swagger.params.sort && req.swagger.params.sort.raw){
                    toSort = _sortQuery(req.swagger.params.sort);
                }

                return callback(req, res);
            }).then(function(result) {
                if (!res.headersSent && result) {
                    if (_.isArray(result)) {
                        res.body = result.map(function(element) {
                            if(toSort){
                                toSort.type = typeof element[toSort.sortBy];
                            }
                            return element.toJSON ? element.toJSON() : element;
                        });
                        if (toSort){
                            (toSort.type === 'string') ? res.body.sort(_sortByAny(toSort.sortBy, toSort.dsc, function(a){return a.toUpperCase()})): res.body.sort(_sortByAny(toSort.sortBy, toSort.dsc, parseInt)); // jshint ignore:line

                        }
                    } else {
                        res.body = result.toJSON ? result.toJSON() : result;
                    }
                }
                if (!res.headersSent) {
                    next();
                }
            }).catch(function(err) {
                next(_processError(err));
            });
        };
    }

    function _render(viewName, req, res) {
        var options;

        return Promise.try(function() {
            assert.optionalString(viewName);
            assert.object(req);
            assert.object(res);
        })
        .then(function() {
            options = {
                basepath: req.swagger.operation.api.basePath,
                Constants: Constants,
                _: _,
                filename:  Constants.Views.Directory
            };
        })
        .then(function() {
            if (_.isEmpty(res.body) || !viewName) {
                return res.body;
            } else if (_.isArray(res.body)) {
                // Use ejs render directly to avoid repeatedly loading the same view
                return Promise.try(function() {
                    return views.get(viewName);
                })
                .then(function(view) {
                    return Promise.map(res.body, function(element) {
                        return ejs.render(view.contents, _.merge(element, options));
                    });
                })
                .then(function(collection) {
                    return views.render('collection.2.0.json', { collection: collection });
                });
            } else {
                return views.render(viewName, _.merge(res.body, options));
            }
        })
        .then(function(data) {
            // determine content-type
            var body = data;
            if (typeof(data) === 'object') {
                res.set('Content-Type', 'application/json');
            } else if (typeof(data) === 'string') {
                body = Promise.try(function() {
                    var parsed = JSON.parse(data);
                    res.set('Content-Type', 'application/json');
                    return JSON.stringify(parsed);
                })
                .catch(function(err) {
                    if (!viewName) {
                        if (typeof res.get('Content-Type') === 'undefined') {
                            res.set('Content-Type', 'text/plain');
                        }
                        return data;
                    } else {
                        throw new Errors.ViewRenderError(err.message);
                    }
                });
            }
            return body;
        })
        .catch(function(err) {
            throw new Errors.ViewRenderError(err.message);
        });
    }

    function swaggerRenderer(req, res, viewName, next) {
        return Promise.try(function() {
            assert.ok(!res.headersSent, 'headers have already been sent');
            return [ viewName, req, res ];
        })
        .spread(_render)
        .tap(function() {
            // Set appropriate HTTP status.
            if (_.isEmpty(res.body) && req.swagger.options.send204OnEmpty) {
                res.status(204);
            } else {
                res.status(res.locals.errorStatus || req.swagger.options.success || 200);
            }
        })
        .then(res.send.bind(res))
        .catch(function(err) {
            if (res.locals.errorStatus) {
                next();
            } else {
                next(_processError(err));
            }

        });
    }

    function swaggerValidator() {
        var namespace = '/api/2.0/schemas/';
        var schemaPath = path.resolve(__dirname, '../../static/schemas/2.0');
        var namespace1Added = schemaApiService.addNamespace(schemaPath, namespace);

        namespace = '/api/2.0/obms/definitions/';
        schemaPath = path.resolve(__dirname, '../../static/schemas/obms');
        var namespace2Added = schemaApiService.addNamespace(schemaPath, namespace);

        namespace = '/api/2.0/ibms/definitions/';
        schemaPath = path.resolve(__dirname, '../../static/schemas/ibms');
        var namespace3Added = schemaApiService.addNamespace(schemaPath, namespace);

        var namespacesAdded = Promise.all([namespace1Added, namespace2Added, namespace3Added]);

        return function(schemaName, data, next) {
            namespacesAdded.then(function () {
                if (schemaName) {
                    return schemaApiService.validate(data, schemaName)
                        .then(function (validationResults) {
                            if (validationResults.error) {
                                throw new Error(validationResults.error);
                            }
                            next();
                        }).catch(function (err) {
                            next(_processError(err));
                        });
                } else {
                    next();
                }
            });
        };
    }

    function makeRenderableOptions(req, res, context, ignoreLookup) {
        var scope = res.locals.scope;
        var apiServer = util.format('http://%s:%d',
            config.get('apiServerAddress'),
            config.get('apiServerPort')
        );
        var baseUri = util.format('%s%s', apiServer, req.swagger.operation.api.basePath);
        var fileServerUri;
        if (config.get('fileServerAddress') !== undefined) {
            fileServerUri = 'http://' + config.get('fileServerAddress');
            if (config.get('fileServerPort') !== undefined) {
                fileServerUri = fileServerUri + ':' + config.get('fileServerPort');
            }
            if (config.get('fileServerPath') !== undefined) {
                fileServerUri = fileServerUri + config.get('fileServerPath');
                fileServerUri = _.trimRight(fileServerUri, '/');
            }
        } else {
            fileServerUri = apiServer;
        }
        context = context || {};

        return Promise.try(function() {
            return ignoreLookup ? '' : lookupService.ipAddressToMacAddress(res.locals.ipAddress);
        })
        .catch(function(error) {
            logger.error('makeRenderableOptions encountered ' + error.message);
            return '';
        })
        .then(function(macAddress) {
            return Promise.props({
                server: config.get('apiServerAddress', '10.1.1.1'),
                port: config.get('apiServerPort', 80),
                ipaddress: res.locals.ipAddress,
                netmask: config.get('dhcpSubnetMask', '255.255.255.0'),
                gateway: config.get('dhcpGateway', '10.1.1.1'),
                macaddress: res.locals.macAddress || macAddress,
                sku: env.get('config', {}, [ scope[0] ]),
                env: env.get('config', {}, scope),
                // Build structure that mimics the task renderContext
                api: {
                    server: apiServer,
                    base: baseUri,
                    files: baseUri + '/files',
                    nodes: baseUri + '/nodes'
                },
                file: {
                    server: fileServerUri
                },
                context: context,
                nodeId: context.target,
                taskId: _getActiveTaskId(context.target)
            });
        });
    }

    function _getActiveTaskId(nodeId) {
        if (nodeId) {
            return taskProtocol.activeTaskExists(nodeId)
                .then(function (activeTask) {
                    return activeTask.taskId;
                });
        } else {
            return Promise.resolve();
        }
    }

    function _addLinksHeader(req, res, count) {
        var skip = req.swagger.query.$skip;
        var top = req.swagger.query.$top;
        var uriBase = req.url.split('?')[0];

        assert.optionalNumber(skip);
        assert.optionalNumber(top);
        assert.string(uriBase);

        if ((skip === undefined && top === undefined) ||
            (top !== undefined && top >= count)) {
                return;
        }

        // Default values for skip and top
        top = top === undefined ? count - skip : top;
        skip = skip || 0;

        var links = {};

        if ( skip && skip < top) {
            links.first = util.format('%s?$skip=0&$top=%d', uriBase, skip);
        } else {
            links.first = util.format('%s?$skip=0&$top=%d', uriBase, top);
        }

        var lastSkip;
        if (skip + top === count) {
            lastSkip = skip;
        } else {
            lastSkip = (Math.ceil(count / top) - 1) * top;
        }
        links.last = util.format(
            '%s?$skip=%d&$top=%d',
            uriBase,
            lastSkip,
            top
        );

        if (skip) {
            if (skip < top) {
                links.prev = util.format('%s?$skip=0&$top=%d', uriBase, skip);
            } else {
                links.prev = util.format(
                    '%s?$skip=%d&$top=%d',
                    uriBase,
                    skip - top,
                    top
                );
            }
        }

        if (skip < lastSkip) {
            links.next = util.format(
                '%s?$skip=%d&$top=%d',
                uriBase,
                skip + top,
                top
            );
        }
        return res.links(links);
    }

    function addLinksHeader(req, res, collection, query) {
        return waterline[collection].count(query)
        .then(function(count) {
            return _addLinksHeader(req, res, count);
        });
    }

    function getTagName(req) {
        //TODO: sway 1.0.0 implements a fix for this, but it is lacking in the ^0.6.0 versions
        //      so we duplicate the 1.0.0 implementation here.
        // Ref: https://github.com/apigee-127/sway/commit/1b7acd9743aae2f2f94057d3f8911e2af72614f5
        var pathObject = req.swagger.operation.pathObject;
        var pathMatch = pathObject.regexp.exec(parseUrl(req.url).pathname);
        var value = decodeURIComponent(pathMatch[_.findIndex(pathObject.regexp.keys, function(key) {
            return key.name === 'tagName';
        }) + 1]);
        return value;
    }

    return {
        controller: swaggerController,
        validator: swaggerValidator,
        renderer: swaggerRenderer,
        makeRenderableOptions: makeRenderableOptions,
        addLinksHeader: addLinksHeader,
        getTagName: getTagName
    };
}