dmccarthy-dev/swagger-seneca-router

View on GitHub
index.js

Summary

Maintainability
B
6 hrs
Test Coverage
'use strict';

const jsonic = require('jsonic');


/**
 *
 * Builds a Seneca pattern string based on swagger properties.
 *
 * @param operation
 * @returns {*}
 */
const resolveOperationPattern = function( operation ){

    if ( operation['x-seneca-pattern'] ) {
        return operation['x-seneca-pattern'];
    }

    let pattern = '';

    if ( operation['x-swagger-router-controller'] ) {
        pattern += 'controller:' + operation['x-swagger-router-controller'];
    }

    if ( operation['x-swagger-router-controller'] && operation.operationId ){
        pattern +=  ',';
    }

    if ( operation.operationId ){
        pattern += 'operation:' + operation.operationId;
    }

    return pattern;
};


/**
 * Builds a seneca pattern object based on the swagger
 * properties and query parameters.
 *
 * @param swagger
 * @returns {*}
 */
const buildPattern = function( swagger ){

    const patternStr = resolveOperationPattern( swagger.operation );

    const pattern = jsonic( patternStr );

    for ( const i in swagger.params ) {
        if ( swagger.params.hasOwnProperty( i )){
            pattern[i] = swagger.params[i].value;
        }
    }

    return pattern;
};


/**
 *
 *
 * @param val the config value for this error.
 * @param err
 * @param context
 * @returns {*}
 */
const handleSenecaError = ( val, err, context ) => {

    if ( !val || val === 'error' ){
        return context.next( err );
    }
    else if( val === 'next'  ){
        context.next();
    }
    else if( val === 'response'  ){
        sendResp( { code : 500, body : err }, context );
    }
    else if( 0 === val.indexOf( 'jsonic' ) ){
        const message = jsonic( val );
        sendResp(  message.jsonic, context );
    }
    else{
        return context.next( err );
    }
};


/**
 *
 * Converts the seneca error output to https response.
 *
 * @param err
 * @param context
 * @param context.req
 * @param context.res
 * @param context.next
 * @param context.options
 */
const handleErr = function( err, context){

    if ( err.seneca ){

        if ( !context.options.senecaErrorMode ) {
            return context.next( err );
        }

        if ( context.options.senecaErrorMode[err.code] ){
            return handleSenecaError( context.options.senecaErrorMode[err.code], err, context );
        }

        if ( context.options.senecaErrorMode.default ){
            return handleSenecaError( context.options.senecaErrorMode.default, err, context );
        }

        return context.next( err );
    }


    if ( err.body ){

        if ( !err.code ){
            err.code = 500;
        }

        sendResp( err, context );
    }
    else{
        sendResp( { code : 500, body: err }, context );
    }

};


/**
 * Converts the seneca output to https response.
 * It expects the seneca output object to contain
 * a code,body and headers property.
 *
 *  The result object can have the following structure:
 *  {
 *      headers : {
 *          'Content-Type' : 'application/json'
 *      }
 *      code: 200,
 *      body: {
 *          a : 2,
 *          b : true
 *      }
 *  }
 *
 *
 * @param result
 * @param context
 */
const sendResp = function( result, context ){

    const res = context.res;

    if ( result.headers ){
        for ( const name in result.headers ){
            if ( result.headers.hasOwnProperty( name ) ){
                res.setHeader( name, result.headers[name] );
            }
        }
    }

    const code = result.code ? result.code : 200;
    const body = result.body ? result.body : ( code === 201 || code === 204 ? undefined : result );

    if ( body && (!result.headers || !result.headers['Content-Type']) ){
        res.setHeader( 'Content-Type', 'application/json' );
    }

    res.writeHead( code );
    res.end( JSON.stringify( body ) );
};


/**
 * Validate that the options parameter contains valid options.
 *
 *
 *
 * @param options
 */
const validateOptions = function ( options ){

    if ( !options || !options.senecaClient || !options.senecaClient.act ){
        throw new Error( 'senecaClient is required.' );
    }

    if ( options.hasOwnProperty( 'matchXSenecaPatternsOnly' ) && typeof options.matchXSenecaPatternsOnly !== 'boolean' ){
        throw new Error( 'Invalid matchXSenecaPatternsOnly, the value must have a boolean value.' );
    }

    if ( options.patternNotFoundMode ) {

        if ( -1 === ['error', 'next' ].indexOf( options.patternNotFoundMode ) && !isValidJsonicConfig( options.patternNotFoundMode ) ) {
            throw new Error('Invalid value for patternNotFoundMode.');
        }
    }

    if ( options.defaultErrorCode &&
        ( typeof options.defaultErrorCode !== "number" ||
            options.defaultErrorCode < 0 ||
            options.defaultErrorCode > 600 ) ){
        throw new Error( 'Invalid defaultErrorCode, the value must be number between 0 and 600.' );
    }


    if ( options.senecaCallbackOverride && typeof options.senecaCallbackOverride !== "function"){
        throw new Error( 'Invalid senecaCallbackOverride, the value must be a function.' );
    }

    if ( options.senecaErrorMode ) {

        if ( typeof  options.senecaErrorMode !== 'object'){
            throw new Error( 'Invalid senecaErrorMode, the value must be an object.' );
        }

        for ( const i in options.senecaErrorMode ){
            const val = options.senecaErrorMode[i];

            if ( -1 === ['error', 'next', 'response' ].indexOf( val ) && !isValidJsonicConfig( val ) ) {
                throw new Error('Invalid value for senecaErrorMode entry: ' + i );
            }
        }

    }

};


/**
 *
 * Validates that jsonic config is valid.
 *
 * @param str
 * @returns {boolean}
 */
const isValidJsonicConfig = function ( str ) {

    try{
        const obj = jsonic( str );
        return !!obj.jsonic;
    }
    catch (e) {
        return false;
    }
};


/**
 * Check that the swagger operation has properties that we can use to create a
 * seneca pattern.
 *
 * @param operation
 * @param context
 * @param context.options
 * @returns {boolean}
 */
const hasPattern = ( operation, context ) => {

    if ( context.options.matchXSenecaPatternsOnly ) return !!operation['x-seneca-pattern'];

    return !!( operation['x-seneca-pattern'] || ( operation['x-swagger-router-controller'] && operation.operationId ) );

};


/**
 *
 *
 * @param context
 * @returns {*}
 */
const handlePatternNotFound = ( context ) => {

    const options = context.options;

    if ( !options.patternNotFoundMode || options.patternNotFoundMode === 'next' ){
        return context.next();
    }
    else if( options.patternNotFoundMode === 'error'  ){
        context.next( new Error('Swagger Pattern not found') );
    }
    else if( 0 === options.patternNotFoundMode.indexOf( 'jsonic' ) ){
        const message = jsonic(options.patternNotFoundMode);
        sendResp( message.jsonic, context );
    }
    else{
        return context.next();
    }
};


/**
 * @param options
 * @param options.senecaClient
 * @param options.patternNotFoundMode
 * @param options.matchXSenecaPatternsOnly
 * @param options.defaultErrorCode
 * @param options.senecaErrorMode
 * @param options.senecaCallbackOverride
 * @returns {Function}
 */
module.exports = function ( options ) {

    validateOptions( options );

    return function (req, res, next ) {

        const context = { req : req, res : res, next : next, options : options};

        if ( !req.swagger || !hasPattern( req.swagger.operation, context )){
            return handlePatternNotFound( context );
        }

        context.pattern = buildPattern( req.swagger );

        options.senecaClient.act( context.pattern, function ( err, result ) {

            if ( options.senecaCallbackOverride ) {
                return options.senecaCallbackOverride( err, result, context );
            }
            else if (err) {
                handleErr( err, context );
            }
            else {
                sendResp( result, context );
            }
        });
    };

};