backbone-api-99xp.js

Micro Service integrator plus advanced validation, formatting and control over process. Mix of express, validate-99xp, backbone-request-99xp

CODE DOCUMENTED BELOW


Baseline setup

import _ from 'underscore-99xp';
import v from 'validate-99xp';
import BackboneRequest from 'backbone-request-99xp';

var BackboneApi = {};

response object from express router

BackboneApi._res = null;

Extended Functionallity for Models and Collections

var extended = {

not meant to be used. will be used on setting api methods like POST or PUT

    idAttribute: '_id',
    auth: false,
    tokenField: 'accessToken',

list of methods available in the api and their configuration (paths, validations)

    methods: {
   auth: {
       public: true,
       path: '/a/b',
       validations: {},
       data: {
           user: '',
           pass: '',
       },

// method: {}, if I dont set it, it will use a generic one

   },
   _sample: Sample
   _sample: {
       path: '/a/b',
       validations: {}
   }
    },

default method applying JWT Auth might be necessary to overwrite it

    setAuthHeader(o) {
        if (!this.tokenField) {
            return BackboneApi.dataError('tokenField is not set');
        }
        o.headers.Authorization = 'Bearer ' + this.data[this.auth][this.tokenField];

        return o;
    },
    isAuthenticated() {
        return !this.auth || !!this.data[this.auth];
    },

constructor

    initialize(p, o = {}) {
        this.setRouterParameters(o.req, o.res);

set options applying default options and ignoring router parameters

        this.options = _.defaults(_.omit(o, 'req', 'res'), {
            autoexec: true
        });
        this.data = {};

keep method as an array to have a single way on processing

        typeof this.options.method === 'string' && (this.options.method = [this.options.method]);

autoexec if isn’t asked not to do it

        this.options.autoexec && this.execAll();
    },

recursively run through methods set

    execAll() {

store a copy of methods to process them

        !this._methodsExecution && (this._methodsExecution = _.clone(this.options.method));

stop execution when all methods were processed

        if (_.size(this._methodsExecution) === 0) {
            this._methodsExecution = null;
            _.bind(this.options.success, this)();
            return;
        }

grab first method out from list

        var method = this._methodsExecution.shift();

execute it

        this.exec(method, () => this.execAll(), this.options.error);
    },

execute one method

    exec(method, success, error) {
        var methodData = this.methodData(method);

validate input

        var vErr = this.validate(this.getMethodInput(methodData), {
            methodData: methodData
        });

dispach validation errors

        if (vErr !== null) {
            return this.validationErrors(vErr);
        }

set execution as callback to pass through authentication if needed

        var fn = _.bind(this.callApi, this),
            calledFn = _.partial(fn, method, methodData, success, error);

if authorization is not needed, or is authenticated already, execute method

        if (methodData.public || this.isAuthenticated()) {
            return calledFn();
        }

if method is not public and there’s no authentication set output an error

        else if (!this.auth) {
            return BackboneApi.dataError(`method "${method}" is not set as public but auth is not set`);
        }

execute authentication and requested method after if

        return this.exec(this.auth, calledFn);
    },

call api and trigger callbacks accordingly to what happens

    callApi(method, methodData, success, error) {
        this.setApiCall(methodData);

request data

        var o = {
            method: methodData.method, // http method
            data: this.getMethodInput(methodData), // request body
            headers: _.result2(methodData, 'headers', {}, [methodData], this)
        };

if method is private set headers

        if (!methodData.public) {
            o = this.setAuthHeader(o);
        }

before function that can be set on method config to change options or data

        var before = _.bind(typeof methodData.before === 'function' ? methodData.before : (c, _o, _methodData) => {
                c(_o, _methodData);
            }, this),

request function executed after before callabck

            request = _.bind(_.partial((_method, _methodData, _success, _error, _o) => {

listeners for success or error

                this.listenToOnce(this, 'sync', _.bind(() => {
                    this.stopListening(this);

store execution results with method name in data object

                    this.data[_method] = this.attributes;

empty attributes for next execution

                    this.attributes = {};

handle possible errors inside callbacks

                    try {
                        typeof _methodData.success === 'function' && _.bind(_methodData.success, this)(_o, this._req.body, _methodData);
                        typeof _success === 'function' && _.bind(_success, this)(_o, this._req.body, _methodData);
                    } catch (e) {
                        /*console.error('Internal Failure 1');*/
                        /*console.error(e);*/
                        this._res.status(500).send({
                            message: 'Internal Failure (1)'
                        });
                    }
                }, this));
                this.listenToOnce(this, 'error', _.bind((model, data) => {
                    this.stopListening(this);
                    /*!this._reqErr.response && console.error('Internal Failure 2');*/

handle possible errors inside callbacks

                    try {
                        /*console.error('Internal Failure 2a');*/
                        typeof _methodData.error === 'function' && _.bind(_methodData.error, this)(data, _o, this._req.body, _methodData);
                        typeof _error === 'function' ? _.bind(_error, this)(data, _o, this._req.body, _methodData) : !this._res._headerSent && this._res.status(data.response.statusCode ? data.response.statusCode : 500).send(data.error);
                    } catch (e) {
                        /*console.error('Internal Failure 3');*/
                        /*console.error(e);*/
                        this._res.status(500).send({
                            message: 'Internal Failure (2)'
                        });
                    }
                }, this));

execute http request

                this.save(null, _o);
            }, method, methodData, success, error), this);

handle possible errors inside before function

        try {
            before(request, o, methodData);
        } catch (e) {
            /*console.error('Internal Failure 4');*/
            /*console.log(e);*/
            this._res.status(500).send({
                message: 'Internal Failure (4)'
            });
        }
    },

getter for methods data. ensure data for method asked is set

    methodData(method) {
        if (!method || !_.result(this, 'methods')[method]) {
            return BackboneApi.dataError('Make sure you\'ve set your method and it\'s data');
        }

        var methodData = _.result(this, 'methods')[method] || {};
        if (!methodData.path) {
            BackboneApi.dataError('Make sure you\'ve set a path for method ' + method + '\'');
        }

        methodData = this.setHttpMethod(methodData);

        return methodData;
    },

get HTTP method from method configuration. if needed set a default HTTP method accordingly to method data input

    setHttpMethod(methodData) {
        var data = this.getMethodInput(methodData);
        methodData = _.defaults(methodData, {
            method: (typeof data === 'object' && _.size(data) > 0 ? 'POST' : 'GET')
        });
        methodData.method = methodData.method.toUpperCase();

        return methodData;
    },

get “data” from method configuration (can be an object or function) or req.body instead

    getMethodInput(methodData) {
        return _.result2(methodData, 'data', this._req.body || {}, [_.clone(this._req.body), methodData], this);
    },

replaces common behavior of backbone to ensure no id nonwanted will be added in Api URLs

    url() {
        return _.result(this, 'urlRoot') ||
            _.result(this.collection, 'url') ||
            BackboneApi.urlError();
    },

set asked method data for the request to be executed

    setApiCall(methodData) {
        this.setApiUrl(methodData.path, methodData);
    },

merge urlHost with the correct path for method asked. also render path as a template to make possible having attributes on it

    setApiUrl(path, methodData) {
        var host = _.result(this, 'urlHost') || BackboneApi.urlError(1),
            data = _.extend({}, _.clone(this.data), {
                _model: this,
                _input: this.getMethodInput(methodData),
                _params: this._req.params
            }),
            pathfix = new RegExp('((?<!\:)\/{1,})', 'g');

        path = _.template(path)(data);

        return this.urlRoot = [host, path].join('/').replace(pathfix, '/');
    },

Customization of sync to use BackboneRequest.sync

    sync(method, model, options) {
        method = methodMap[options.method];
        var args = [method, model, options];
        return BackboneRequest.sync.apply(this, args);
    },

inherit validations from method configuration

    validations(a, o) {
        var vl = _.result2(o.methodData, 'validations', {}, [a, o], this);
        return vl;
    },

Skip own validations. The ones that matter are method validations

    _validate() {
        return true;
    },

Dispatcher of validation errors

    validationErrors(err) {
        this._res.status(400).send({
            title: 'Invalid Data',
            errors: err
        });
    },

Store req and res objects from express router. Those are necessary to access input data and to send information to the client

    setRouterParameters(req, res) {
        if (!req || !res) {
            BackboneApi.dataError('Initialize requires an object with req and res (express route variables) within');
        }

        this._req = req;
        BackboneApi._res = this._res = res;
    },
};

add extension and validate to our classes

_.map(['Model', 'Collection'], (x) => {
    BackboneApi[x] = BackboneRequest[x].extend(_.extend(_.clone(v), extended));
});

Throw an error when a URL is needed, and none is supplied.

BackboneApi.urlError = function(code) {
    if (code === 2) {
        BackboneApi.dataError('A "path" property must be specified in methods list for the current method');
    }
    BackboneApi.dataError('A "urlHost" property or function must be specified');
};

Throw an error when some DATA is needed, and none is supplied.

BackboneApi.dataError = function(msg) {
    console.error(msg);
    if (BackboneApi._res) {
        return BackboneApi._res.status(500).send(msg);
    }
    throw new Error(msg);
};

Map from CRUD to HTTP for our default Backbone.sync implementation.

var methodMap = {
    'POST': 'create',
    'PUT': 'update',
    'PATCH': 'patch',
    'DELETE': 'delete',
    'GET': 'read'
};


export default BackboneApi;
h