<commit_before>'use strict';
//jshint camelcase: false
//jshint maxstatements: false

var assert = require('assert');
var Bluebird = require('bluebird');
var sinon = require('sinon');

describe('apiController', function() {
  var api;
  var knexClient;
  var options;

  beforeEach(function() {
    var apiController;

    knexClient = {
      del: sinon.stub(),
      first: sinon.stub(),
      insert: sinon.stub(),
      limit: sinon.stub(),
      max: sinon.stub(),
      map: sinon.stub(),
      offset: sinon.stub(),
      orderBy: sinon.stub(),
      return: sinon.stub(),
      select: sinon.stub(),
      table: sinon.stub(),
      where: sinon.stub()
    };

    options = {
      tableName: 'test-table',
      index: 'id'
    };

    apiController = require('../../lib/apiController');

    api = apiController(options, knexClient);
  });

  describe('create', function() {
    var reply;
    var request;

    beforeEach(function() {
      reply = sinon.stub();
      request = {
        payload: {
          name: 'test-name',
          domainId: 100
        }
      };
    });

    afterEach(function() {
      assert(reply.calledOnce);
    });

    it('should create a row in the given mysql table', function(done) {
      knexClient.table.returnsThis();
      knexClient.insert.returns(Bluebird.resolve([{ id: 100 }]));

      api
        .create(request, reply)
        .then(function() {
          assert(knexClient.table.calledOnce);
          assert(knexClient.insert.calledOnce);

          assert(reply.args[0][0], { id: 100 });

          done();
        });
    });

    it('should catch error', function(done) {
      knexClient.table.returnsThis();
      knexClient.insert.returns(Bluebird.reject(new Error('test-error')));

      api
        .create(request, reply)
        .then(function() {
          assert.equal(reply.args[0][0].message, 'test-error');
          done();
        });
    });
  });

  describe('destroy', function() {
    var reply;
    var request;
    var responseCode;

    beforeEach(function() {
      responseCode = {code: sinon.stub()};
      reply = sinon.stub().returns(responseCode);
      request = {
        params: {
          id: 100
        }
      };
    });

    afterEach(function() {
      assert(reply.calledOnce);
    });

    it('should delete a row from the given mysql table', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient.del.returns(Bluebird.resolve(1));

      api
        .destroy(request, reply)
        .then(function() {
          assert(knexClient.table.calledOnce);
          assert(knexClient.where.calledOnce);
          assert(knexClient.del.calledOnce);

          assert(responseCode.code.args[0][0], 204);

          done();
        });
    });

    it('should return error if delete operation was not successful',
      function(done) {
        knexClient.table.returnsThis();
        knexClient.where.returnsThis();
        knexClient.del.returns(Bluebird.resolve(undefined));

        api
          .destroy(request, reply)
          .then(function() {
            assert(knexClient.table.calledOnce);
            assert(knexClient.where.calledOnce);
            assert(knexClient.del.calledOnce);

            assert(responseCode.code.args[0][0], 500);

            done();
          });
      });

    it('should catch error', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient.del.returns(Bluebird.reject(new Error('test-error')));

      api
        .destroy(request, reply)
        .then(function() {
          assert.equal(reply.args[0][0].message, 'test-error');
          done();
        });
    });
  });

  describe('list', function() {
    var reply;
    var request;
    var response;

    beforeEach(function() {
      reply = sinon.stub();
      request = {
        query: {
          name: 'test-name',
          id: 100,
          cursor: 0,
          limit: 10
        }
      };
      response = {
        limit: 10,
        cursor: 100,
        records: [ { name: 'test-name', id: 100 } ]
      };
    });

    afterEach(function() {
      assert(reply.calledOnce);
    });

    it('should return a filtered/paginated list of rows from the given table',
      function(done) {
        var context;
        var prepareResponse;

        knexClient.table.returnsThis();
        knexClient.where.returnsThis();
        knexClient.limit.returnsThis();
        knexClient.offset.returnsThis();
        knexClient
          .map
          .returns(Bluebird.resolve([{
            name: 'test-name',
            id: 100
          }]));

        api
          .list(request, reply)
          .then(function() {
            var filterQuery;

            assert(knexClient.table.calledOnce);
            assert(knexClient.where.calledOnce);
            assert(knexClient.limit.calledOnce);
            assert(knexClient.offset.calledOnce);

            context = { where: sinon.spy() };
            filterQuery = knexClient.where.args[0][0];
            filterQuery.bind(context)();
            assert(context.where.calledTwice);
            assert(context.where.args[0], [ 'name', 'test-name' ]);
            assert(context.where.args[1], [ 'id', 100 ]);

            prepareResponse = knexClient.map.args[0][0];
            assert(
              prepareResponse({ id: 100 }),
              { id: 100 }
            );

            assert(reply.args[0][0], response);

            done();
          });
      });

    it('should return an empty array when at the end of the paginated list' +
      ' of rows from the given mysql table',
      function(done) {
        knexClient.table.returnsThis();
        knexClient.where.returnsThis();
        knexClient.limit.returnsThis();
        knexClient.offset.returnsThis();
        knexClient.map.returns(Bluebird.resolve({}));

        api
          .list(request, reply)
          .then(function() {
            assert(knexClient.table.calledOnce);
            assert(knexClient.where.calledOnce);
            assert(knexClient.limit.calledOnce);
            assert(knexClient.offset.calledOnce);

            assert(reply.args[0][0], []);

            done();
          });
      });

    it('should catch error', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient.limit.returnsThis();
      knexClient.offset.returnsThis();
      knexClient.map.returns(Bluebird.reject(new Error('test-error')));

      api
        .list(request, reply)
        .then(function() {
          assert.equal(reply.args[0][0].message, 'test-error');
          done();
        });
    });
  });

  describe('show', function() {
    var reply;
    var request;

    beforeEach(function() {
      reply = sinon.stub();
      request = {
        params: {
          id: 100
        }
      };
    });

    afterEach(function() {
      assert(reply.calledOnce);
    });

    it('should return a row from the mysql table given an id', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient
        .first
        .returns(Bluebird.resolve({ name: 'test-name' }));

      api
        .show(request, reply)
        .then(function() {
          assert(knexClient.table.calledOnce);
          assert(knexClient.where.calledOnce);
          assert(knexClient.first.calledOnce);

          assert(reply.args[0][0], { name: 'test-name' });

          done();
        });
    });

    it('should return a 404 if given id is not found', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient
        .first
        .returns(Bluebird.resolve({}));

      api
        .show(request, reply)
        .then(function() {
          assert(knexClient.table.calledOnce);
          assert(knexClient.where.calledOnce);
          assert(knexClient.first.calledOnce);

          assert(reply.args[0][0].output.statusCode, 404);

          done();
        });
    });

    it('should catch error', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient.first.returns(Bluebird.reject(new Error('test-error')));

      api
        .show(request, reply)
        .then(function() {
          assert.equal(reply.args[0][0].message, 'test-error');
          done();
        });
    });
  });
});
<commit_msg>SEN-1612 fixed tests<commit_after>'use strict';
//jshint camelcase: false
//jshint maxstatements: false

var assert = require('assert');
var Bluebird = require('bluebird');
var sinon = require('sinon');

describe('createRouteHandler', function() {
  var routeHandler;
  var knexClient;
  var options;

  beforeEach(function() {
    var createRouteHandler;

    knexClient = {
      del: sinon.stub(),
      first: sinon.stub(),
      insert: sinon.stub(),
      limit: sinon.stub(),
      max: sinon.stub(),
      map: sinon.stub(),
      offset: sinon.stub(),
      orderBy: sinon.stub(),
      return: sinon.stub(),
      select: sinon.stub(),
      table: sinon.stub(),
      where: sinon.stub()
    };

    options = {
      tableName: 'test-table',
      index: 'id'
    };

    createRouteHandler = require('../../lib/createRouteHandler');

    routeHandler = createRouteHandler(options, knexClient);
  });

  describe('create', function() {
    var reply;
    var request;

    beforeEach(function() {
      reply = sinon.stub();
      request = {
        payload: {
          name: 'test-name',
          domainId: 100
        }
      };
    });

    afterEach(function() {
      assert(reply.calledOnce);
    });

    it('should create a row in the given mysql table', function(done) {
      knexClient.table.returnsThis();
      knexClient.insert.returns(Bluebird.resolve([{ id: 100 }]));

      routeHandler
        .create(request, reply)
        .then(function() {
          assert(knexClient.table.calledOnce);
          assert(knexClient.insert.calledOnce);

          assert(reply.args[0][0], { id: 100 });

          done();
        });
    });

    it('should catch error', function(done) {
      knexClient.table.returnsThis();
      knexClient.insert.returns(Bluebird.reject(new Error('test-error')));

      routeHandler
        .create(request, reply)
        .then(function() {
          assert.equal(reply.args[0][0].message, 'test-error');
          done();
        });
    });
  });

  describe('destroy', function() {
    var reply;
    var request;
    var responseCode;

    beforeEach(function() {
      responseCode = {code: sinon.stub()};
      reply = sinon.stub().returns(responseCode);
      request = {
        params: {
          id: 100
        }
      };
    });

    afterEach(function() {
      assert(reply.calledOnce);
    });

    it('should delete a row from the given mysql table', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient.del.returns(Bluebird.resolve(1));

      routeHandler
        .destroy(request, reply)
        .then(function() {
          assert(knexClient.table.calledOnce);
          assert(knexClient.where.calledOnce);
          assert(knexClient.del.calledOnce);

          assert(responseCode.code.args[0][0], 204);

          done();
        });
    });

    it('should return error if delete operation was not successful',
      function(done) {
        knexClient.table.returnsThis();
        knexClient.where.returnsThis();
        knexClient.del.returns(Bluebird.resolve(undefined));

        routeHandler
          .destroy(request, reply)
          .then(function() {
            assert(knexClient.table.calledOnce);
            assert(knexClient.where.calledOnce);
            assert(knexClient.del.calledOnce);

            assert(responseCode.code.args[0][0], 500);

            done();
          });
      });

    it('should catch error', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient.del.returns(Bluebird.reject(new Error('test-error')));

      routeHandler
        .destroy(request, reply)
        .then(function() {
          assert.equal(reply.args[0][0].message, 'test-error');
          done();
        });
    });
  });

  describe('list', function() {
    var reply;
    var request;
    var response;

    beforeEach(function() {
      reply = sinon.stub();
      request = {
        query: {
          name: 'test-name',
          id: 100,
          cursor: 0,
          limit: 10
        }
      };
      response = {
        limit: 10,
        cursor: 100,
        records: [ { name: 'test-name', id: 100 } ]
      };
    });

    afterEach(function() {
      assert(reply.calledOnce);
    });

    it('should return a filtered/paginated list of rows from the given table',
      function(done) {
        var context;
        var prepareResponse;

        knexClient.table.returnsThis();
        knexClient.where.returnsThis();
        knexClient.limit.returnsThis();
        knexClient.offset.returnsThis();
        knexClient
          .map
          .returns(Bluebird.resolve([{
            name: 'test-name',
            id: 100
          }]));

        routeHandler
          .list(request, reply)
          .then(function() {
            var filterQuery;

            assert(knexClient.table.calledOnce);
            assert(knexClient.where.calledOnce);
            assert(knexClient.limit.calledOnce);
            assert(knexClient.offset.calledOnce);

            context = { where: sinon.spy() };
            filterQuery = knexClient.where.args[0][0];
            filterQuery.bind(context)();
            assert(context.where.calledTwice);
            assert(context.where.args[0], [ 'name', 'test-name' ]);
            assert(context.where.args[1], [ 'id', 100 ]);

            prepareResponse = knexClient.map.args[0][0];
            assert(
              prepareResponse({ id: 100 }),
              { id: 100 }
            );

            assert(reply.args[0][0], response);

            done();
          });
      });

    it('should return an empty array when at the end of the paginated list' +
      ' of rows from the given mysql table',
      function(done) {
        knexClient.table.returnsThis();
        knexClient.where.returnsThis();
        knexClient.limit.returnsThis();
        knexClient.offset.returnsThis();
        knexClient.map.returns(Bluebird.resolve({}));

        routeHandler
          .list(request, reply)
          .then(function() {
            assert(knexClient.table.calledOnce);
            assert(knexClient.where.calledOnce);
            assert(knexClient.limit.calledOnce);
            assert(knexClient.offset.calledOnce);

            assert(reply.args[0][0], []);

            done();
          });
      });

    it('should catch error', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient.limit.returnsThis();
      knexClient.offset.returnsThis();
      knexClient.map.returns(Bluebird.reject(new Error('test-error')));

      routeHandler
        .list(request, reply)
        .then(function() {
          assert.equal(reply.args[0][0].message, 'test-error');
          done();
        });
    });
  });

  describe('show', function() {
    var reply;
    var request;

    beforeEach(function() {
      reply = sinon.stub();
      request = {
        params: {
          id: 100
        }
      };
    });

    afterEach(function() {
      assert(reply.calledOnce);
    });

    it('should return a row from the mysql table given an id', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient
        .first
        .returns(Bluebird.resolve({ name: 'test-name' }));

      routeHandler
        .show(request, reply)
        .then(function() {
          assert(knexClient.table.calledOnce);
          assert(knexClient.where.calledOnce);
          assert(knexClient.first.calledOnce);

          assert(reply.args[0][0], { name: 'test-name' });

          done();
        });
    });

    it('should return a 404 if given id is not found', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient
        .first
        .returns(Bluebird.resolve({}));

      routeHandler
        .show(request, reply)
        .then(function() {
          assert(knexClient.table.calledOnce);
          assert(knexClient.where.calledOnce);
          assert(knexClient.first.calledOnce);

          assert(reply.args[0][0].output.statusCode, 404);

          done();
        });
    });

    it('should catch error', function(done) {
      knexClient.table.returnsThis();
      knexClient.where.returnsThis();
      knexClient.first.returns(Bluebird.reject(new Error('test-error')));

      routeHandler
        .show(request, reply)
        .then(function() {
          assert.equal(reply.args[0][0].message, 'test-error');
          done();
        });
    });
  });
});
<|endoftext|>


<commit_before>/*
* mvcController
* author: ronglin
* create date: 2014.6.24
*/

'use strict';

var events = require('events'),
	utils = require('./utilities'),
    mvcHelper = require('./mvcHelper'),
    mvcAction = require('./mvcAction'),
    mvcInjector = require('./mvcInjector'),
    mvcTempData = require('./mvcTempData'),
    mvcViewData = require('./mvcViewData'),
    mvcHelperUrl = require('./mvcHelperUrl'),
    mvcResultApi = require('./mvcActionResultApi');

var mvcController = function(set) {
    utils.extend(this, set);
};

mvcController.define = function(name, impl) {
    var len = arguments.length;
    if (len === 0) {
        return new mvcController();
    } else if (len === 1) {
        return new mvcController({
            _impl: arguments[0]
        });
    } else if (len === 2) {
        return new mvcController({
            _filt: arguments[0],
            _impl: arguments[1]
        });
    } else {
        return new mvcController({
            _name: arguments[0],
            _filt: arguments[1],
            _impl: arguments[2]
        });
    }
};

mvcController.prototype = {

    _name: null, _path: null, _filt: null, _impl: null,

    actions: null,  events: null, url: null,

    viewData: null, tempData: null, httpContext: null,

    resultApi: null, resultApiSync: null,

    constructor: mvcController, className: 'mvcController',

    name: function(p) { return (p === undefined) ? (this._name) : (this._name = p, this); },
    path: function(p) { return (p === undefined) ? (this._path) : (this._path = p, this); },
    filt: function(p) { return (p === undefined) ? (this._filt) : (this._filt = p, this); },
    impl: function(p) { return (p === undefined) ? (this._impl) : (this._impl = p, this); },

    clone: function() {
        return new mvcController({
            _name: this._name,
            _path: this._path,
            _impl: this._impl
        });
    },

    destroy: function() {
        if (!this.actions) { return; } // already destroyed
        // break object leaks
        utils.each(this.actions, function() { this.controller = null; });
        this.events.removeAllListeners();
        this.url.httpContext = null;
        this.resultApi.httpContext = null;
        this.resultApiSync.httpContext = null;
        // clear reference types
        this._impl = null;
        this.actions = null;
        this.events = null;
        this.url = null;
        this.viewData = null;
        this.tempData = null;
        this.httpContext = null;
        this.resultApi = null;
        this.resultApiSync = null;
    },

    initialize: function(req, res, route, routeSet, routeData) {
        var self = this;
        this.actions = [];
        this.events = new events.EventEmitter();
        this.httpContext = {
            request: req,
            response: res,
            route: route,
            routeSet: routeSet,
            routeData: routeData
        };
        //
        this.url = new mvcHelperUrl({ httpContext: this.httpContext });
        this.viewData = new mvcViewData();
        //
        this.tempData = new mvcTempData({ provider: mvcTempData.sessionProvider });
        this.events.on('actionExecuting', function() { self.tempData.load(self.httpContext); });
        this.events.on('resultExecuting', function() { self.tempData.save(self.httpContext); });
        //
        this.resultApi = new mvcResultApi({ httpContext: this.httpContext, sync: false });
        this.resultApiSync = new mvcResultApi({ httpContext: this.httpContext, sync: true });
        utils.each(this.resultApiSync, function(name, func) {
            if (utils.isFunction(func) && !self[name]) {
                self[name] = function() {
                    var args = utils.arg2arr(arguments);
                    return self.resultApiSync[name].apply(self.resultApiSync, args);
                };
            }
        });
        //
        if (utils.isFunction(this.impl())) {
            var injectedParams = this.injectImpl(this.httpContext);
            this.impl().apply(this, injectedParams);
        }
        //
        return this;
    },

    injectImpl: function(httpContext) {
        var params = [];
        var paramNames = mvcInjector.annotate(this.impl());
        if (!paramNames || paramNames.length === 0) { return params; }
        //
        var self = this, actionWrap;
        utils.each(paramNames, function(i, name) {
            var loweName = name.toLowerCase();
            if (loweName.charAt(0) === '$') {
                loweName = loweName.substr(1);
            }
            switch(loweName) {
                case 'ctx':      params.push(httpContext); break;
                case 'req':      params.push(httpContext.request); break;
                case 'res':      params.push(httpContext.response); break;
                case 'context':  params.push(httpContext); break;
                case 'request':  params.push(httpContext.request); break;
                case 'response': params.push(httpContext.response); break;
                case 'session':  params.push(httpContext.request.session); break;
                case 'query':    params.push(httpContext.request.query); break;
                case 'form':     params.push(httpContext.request.body); break;
                case 'body':     params.push(httpContext.request.body); break;
                //
                case 'events':   params.push(self.events); break;
                case 'tempdata': params.push(self.tempData); break;
                case 'viewdata': params.push(self.viewData); break;
                case 'end':      params.push(self.resultApi); break;
                case 'url':      params.push(self.url); break;
                //
                case 'action': params.push(actionWrap || (actionWrap = function() {
                    var args = utils.arg2arr(arguments);
                    self.action.apply(self, args);
                    return actionWrap;
                })); break;
                //
                default: params.push(null); break;
            }
        });
        //
        return params;
    },

    on: function() {
        var args = utils.arg2arr(arguments);
        this.events.on.apply(this.events, args);
        return this;
    },

    action: function() {
        var len = arguments.length, act;
        if (len === 0) {
            act = new mvcAction();
        } else if (len === 1) {
            act = new mvcAction({
                _name: arguments[0]
            });
        } else if (len === 2) {
            act = new mvcController({
                _name: arguments[0],
                _impl: arguments[1]
            });
        } else {
            act = new mvcController({
                _name: arguments[0],
                _filt: arguments[1],
                _impl: arguments[2]
            });
        }
        act.controller = this;
        this.actions.push(act);
        return act; //  for chain
    },

    findAction: function(name, method) {
        var action, self = this;
        utils.each([method, null], function(i, md) {
            if (action) { return false; }
            utils.each(self.actions, function() {
                if (mvcHelper.lowerEqual(this.name(), name) && this.hasMethod(md)) {
                    action = this;
                    return false;
                }
            });
        });
        return action;
    }
};

module.exports = mvcController;
<commit_msg>fix bug<commit_after>/*
* mvcController
* author: ronglin
* create date: 2014.6.24
*/

'use strict';

var events = require('events'),
	utils = require('./utilities'),
    mvcHelper = require('./mvcHelper'),
    mvcAction = require('./mvcAction'),
    mvcInjector = require('./mvcInjector'),
    mvcTempData = require('./mvcTempData'),
    mvcViewData = require('./mvcViewData'),
    mvcHelperUrl = require('./mvcHelperUrl'),
    mvcResultApi = require('./mvcActionResultApi');

var mvcController = function(set) {
    utils.extend(this, set);
};

mvcController.define = function(name, impl) {
    var len = arguments.length;
    if (len === 0) {
        return new mvcController();
    } else if (len === 1) {
        return new mvcController({
            _impl: arguments[0]
        });
    } else if (len === 2) {
        return new mvcController({
            _filt: arguments[0],
            _impl: arguments[1]
        });
    } else {
        return new mvcController({
            _name: arguments[0],
            _filt: arguments[1],
            _impl: arguments[2]
        });
    }
};

mvcController.prototype = {

    _name: null, _path: null, _filt: null, _impl: null,

    actions: null,  events: null, url: null,

    viewData: null, tempData: null, httpContext: null,

    resultApi: null, resultApiSync: null,

    constructor: mvcController, className: 'mvcController',

    name: function(p) { return (p === undefined) ? (this._name) : (this._name = p, this); },
    path: function(p) { return (p === undefined) ? (this._path) : (this._path = p, this); },
    filt: function(p) { return (p === undefined) ? (this._filt) : (this._filt = p, this); },
    impl: function(p) { return (p === undefined) ? (this._impl) : (this._impl = p, this); },

    clone: function() {
        return new mvcController({
            _name: this._name,
            _path: this._path,
            _impl: this._impl
        });
    },

    destroy: function() {
        if (!this.actions) { return; } // already destroyed
        // break object leaks
        utils.each(this.actions, function() { this.controller = null; });
        this.events.removeAllListeners();
        this.url.httpContext = null;
        this.resultApi.httpContext = null;
        this.resultApiSync.httpContext = null;
        // clear reference types
        this._impl = null;
        this.actions = null;
        this.events = null;
        this.url = null;
        this.viewData = null;
        this.tempData = null;
        this.httpContext = null;
        this.resultApi = null;
        this.resultApiSync = null;
    },

    initialize: function(req, res, route, routeSet, routeData) {
        var self = this;
        this.actions = [];
        this.events = new events.EventEmitter();
        this.httpContext = {
            request: req,
            response: res,
            route: route,
            routeSet: routeSet,
            routeData: routeData
        };
        //
        this.url = new mvcHelperUrl({ httpContext: this.httpContext });
        this.viewData = new mvcViewData();
        //
        this.tempData = new mvcTempData({ provider: mvcTempData.sessionProvider });
        this.events.on('actionExecuting', function() { self.tempData.load(self.httpContext); });
        this.events.on('resultExecuting', function() { self.tempData.save(self.httpContext); });
        //
        this.resultApi = new mvcResultApi({ httpContext: this.httpContext, sync: false });
        this.resultApiSync = new mvcResultApi({ httpContext: this.httpContext, sync: true });
        utils.each(this.resultApiSync, function(name, func) {
            if (utils.isFunction(func) && !self[name]) {
                self[name] = function() {
                    var args = utils.arg2arr(arguments);
                    return self.resultApiSync[name].apply(self.resultApiSync, args);
                };
            }
        });
        //
        if (utils.isFunction(this.impl())) {
            var injectedParams = this.injectImpl(this.httpContext);
            this.impl().apply(this, injectedParams);
        }
        //
        return this;
    },

    injectImpl: function(httpContext) {
        var params = [];
        var paramNames = mvcInjector.annotate(this.impl());
        if (!paramNames || paramNames.length === 0) { return params; }
        //
        var self = this, actionWrap;
        utils.each(paramNames, function(i, name) {
            var loweName = name.toLowerCase();
            if (loweName.charAt(0) === '$') {
                loweName = loweName.substr(1);
            }
            switch(loweName) {
                case 'ctx':      params.push(httpContext); break;
                case 'req':      params.push(httpContext.request); break;
                case 'res':      params.push(httpContext.response); break;
                case 'context':  params.push(httpContext); break;
                case 'request':  params.push(httpContext.request); break;
                case 'response': params.push(httpContext.response); break;
                case 'session':  params.push(httpContext.request.session); break;
                case 'query':    params.push(httpContext.request.query); break;
                case 'form':     params.push(httpContext.request.body); break;
                case 'body':     params.push(httpContext.request.body); break;
                //
                case 'events':   params.push(self.events); break;
                case 'tempdata': params.push(self.tempData); break;
                case 'viewdata': params.push(self.viewData); break;
                case 'end':      params.push(self.resultApi); break;
                case 'url':      params.push(self.url); break;
                //
                case 'action': params.push(actionWrap || (actionWrap = function() {
                    var args = utils.arg2arr(arguments);
                    self.action.apply(self, args);
                    return actionWrap;
                })); break;
                //
                default: params.push(null); break;
            }
        });
        //
        return params;
    },

    on: function() {
        var args = utils.arg2arr(arguments);
        this.events.on.apply(this.events, args);
        return this;
    },

    action: function() {
        var len = arguments.length, act;
        if (len === 0) {
            act = new mvcAction();
        } else if (len === 1) {
            act = new mvcAction({
                _name: arguments[0]
            });
        } else if (len === 2) {
            act = new mvcAction({
                _name: arguments[0],
                _impl: arguments[1]
            });
        } else {
            act = new mvcAction({
                _name: arguments[0],
                _filt: arguments[1],
                _impl: arguments[2]
            });
        }
        act.controller = this;
        this.actions.push(act);
        return act; //  for chain
    },

    findAction: function(name, method) {
        var action, self = this;
        utils.each([method, null], function(i, md) {
            if (action) { return false; }
            utils.each(self.actions, function() {
                if (mvcHelper.lowerEqual(this.name(), name) && this.hasMethod(md)) {
                    action = this;
                    return false;
                }
            });
        });
        return action;
    }
};

module.exports = mvcController;
<|endoftext|>


<commit_before>"use strict";

const url = require("url");
const readline = require("readline");
const fs = require("fs-extra-promise");
const moment = require("moment");
const { SshClient } = require("ssh");
const { serviceMgr } = require("service");
const { asyncWithTmo } = require("misc");
const log = require("log");
const { AsyncEventEmitter } = require("emitter");
const GerritEventEmitter = require("./gerrit_event_emitter");

const DEFAULT_GERRIT_PORT = 29418;
const DEFAULT_GERRIT_TIMEOUT = 15 * 1000;

/*
Note that localhost below needs to be changed to the host where
gerrit is running!

To get started with gerrit docker container:
1. Start container
2. Navigate to http://localhost:8080
3. Upload the public key configured for your gerrit backend to the admin user
  a. Click Become in Top-right corner
  b. Click Administrator
  c. Go to Settings (Located under user-menu in top-right corner)
  d. Click SSH Publick keys
  e. Click Add Key...
  f. Paste content of public key and click Add
4. Create repo via CodeRepo from UI or REST api

To get started with client side:
1. To clone repo, create user that will clone in gerrit
  cat /home/$USER/.ssh/id_rsa.pub | ssh -p 29418 admin@localhost gerrit create-account --full-name "'Marcus Christensson'" --email "'marcus.christensson@combitech.se'" --ssh-key - $USER
2. Clone using
  git clone ssh://$USER@localhost:29418/REPO_NAME
3. Copy gerrit hooks to local repo
  scp -p -P 29418 $USER@localhost:hooks/commit-msg REPO_ROOT/.git/hooks/
4. Set name and email in cloned repo
  git config --local user.name "Marcus Christensson"
  git config --local user.email "marcus.christensson@combitech.se"
4. When pusing changes, push with
  git push origin HEAD:refs/for/master
5. Add rights for registered users to code-review +2 and submit from gerrit UI
  a. Go to Project -> List and select Project (Repository)
  b. Select Access tab and click Edit
  c. Click "Add Reference"
  d. Reference field shall be "refs/heads/*"
  e. Click "Add Permission..." and select "Label Code Review"
  f. Enter Group Name "Registered Users"
  g. Click "Add Permission..." and select "Submit"
  h. Enter Group Name "Registered Users"
  i. Click "Save Changes"
6. Code-review +2 and submit via gerrit UI or using:
  ssh -p 29418 $USER@localhost gerrit review `git rev-parse HEAD` --code-review '+2' --submit
*/

class GerritBackend extends AsyncEventEmitter {
    constructor(id, backend, Repository, Revision) {
        super();
        this.id = id;
        this.backend = backend;
        this.locks = {};
        this.Repository = Repository;
        this.Revision = Revision;
        this.gerritEmitter = new GerritEventEmitter();

        this.__ssh = new SshClient();
    }

    async start() {
        const info = url.parse(this.backend.uri);
        const privateKey = await fs.readFileAsync(this.backend.privateKeyPath);
        const params = {
            host: info.hostname,
            port: info.port || DEFAULT_GERRIT_PORT,
            username: info.auth || process.env.USER,
            privateKey: privateKey
        };

        try {
            // TODO: Handle errors correctly from ssh client!
            await this.__ssh.connect(params, (err) => {
                throw new Error(`SSH client error: ${err}`);
            });
            await this._startMonitorEventStream();
        } catch (err) {
            log.error(`Failed to connect to gerrit using uri ${this.backend.uri}`, err);
        }
    }

    async validateRepository(/* event, data */) {
        // TODO: Validate gerrit specific options
    }

    async _readStream(stream) {
        const lines = [];
        const rl = readline.createInterface({
            input: stream,
            terminal: false
        });

        await new Promise((resolve) => {
            rl.on("line", (line) => {
                if (line.length === 0) {
                    return;
                }

                lines.push(line);
            });

            rl.on("close", resolve);
        });

        return lines;
    }

    async _execGerritCommand(cmd) {
        log.verbose("gerrit command:", cmd);

        return this.__ssh.execute(`gerrit ${cmd}`);
    }

    async _execGerritCommandReadOutput(cmd) {
        return new Promise(async (resolve) => {
            const { stdout, stderr } = await this._execGerritCommand(cmd);
            const exitCodePromise = new Promise((resolve) => stdout.on("close", resolve));
            const outLines = await this._readStream(stdout, "stdout");
            const errLines = await this._readStream(stderr, "stderr");

            if (errLines.length > 0) {
                log.error(`gerrit command: ${cmd} produced error output, stderr:`, errLines.join("\n"));
            }
            if (outLines.length > 0) {
                log.verbose(`gerrit command: ${cmd} produced output, stdout:`, outLines.join("\n"));
            }
            const exitCode = await exitCodePromise;
            resolve({ exitCode, outLines, errLines });
        });
    }

    async _onPatchsetCreated(event) {
        const changeId = event.changeKey.id;
        const repositoryId = event.project;
        const repository = await this.Repository.findOne({ _id: repositoryId });
        if (repository) {
            const revision = await this.Revision.findOne({ _id: changeId });
            const refIndex = revision ? revision.patches.length + 1 : 1;
            // TODO: Shall we use email and name from uploader?
            // TODO: Handle draft changes
            const ref = {
                index: refIndex,
                email: event.uploader.email,
                name: event.uploader.name,
                submitted: moment.unix(event.patchSet.createdOn).utc().format(),
                comment: event.change.commitMessage,
                change: {
                    oldrev: event.patchSet.parents[0],
                    newrev: event.patchSet.revision,
                    refname: event.patchSet.ref // Use event.refName all the time instead?
                }
            };

            await this.Revision.allocate(repository._id, changeId, ref);
            log.info(`Gerrit event allocated revision ${changeId}`);
        }
    }

    async _onChangeMerged(event) {
        const changeId = event.changeKey.id;
        // Make sure that we know about repo
        const repositoryId = event.project;
        const repository = await this.Repository.findOne({ _id: repositoryId });
        if (repository) {
            // And that we know about revision...
            const revision = await this.Revision.findOne({ _id: changeId });
            if (revision) {
                const ref = {
                    index: revision.patches.length + 1,
                    email: event.patchSet.uploader.email,
                    name: event.patchSet.uploader.name,
                    submitted: moment.unix(event.patchSet.createdOn).utc().format(),
                    comment: event.change.commitMessage,
                    change: {
                        oldrev: event.patchSet.parents[0],
                        newrev: event.newRev,
                        refname: event.refName
                    }
                };
                await revision.setMerged(ref);
                await this.emit("revision.merged", revision);
                log.info(`Gerrit event merged revision ${changeId}`);
            }
        }
    }

    async _startMonitorEventStream() {
        const { stdout, stderr } = await this._execGerritCommand("stream-events");

        this.gerritEmitter.addListener("stderr-output", (line) => {
            log.error("Gerrit event stream stderr: ", line);
        });
        this.gerritEmitter.addListener("patchset-created", this._onPatchsetCreated.bind(this));
        this.gerritEmitter.addListener("change-merged", this._onChangeMerged.bind(this));

        await this.gerritEmitter.start(stdout, stderr);
    }

    async create(repository) {
        log.info(`gerrit create repo ${repository._id}`);
        const { exitCode, errLines } = await this._execGerritCommandReadOutput(
            `create-project ${repository._id} ` +
            "--submit-type REBASE_IF_NECESSARY --empty-commit"
        );

        if (exitCode !== 0) {
            if (errLines[0].match(/Project already exists/)) {
                // OK, project already exists!
                log.info(`gerrit repo ${repository._id} already exists, attached to existing`);
            } else {
                throw new Error(`Gerrit command failed with exit code ${exitCode}. stderr=${errLines.join("\n")}`);
            }
        }
    }

    async merge(repository, revision) {
        log.info(`gerrit merge ${revision._id} in repo ${repository._id}`);
        // ssh -p 29418 $USER@localhost gerrit review `git rev-parse HEAD` --code-review '+2' --submit
        const ref = revision.patches[revision.patches.length - 1];
        const { exitCode, errLines } = await this._execGerritCommandReadOutput(
            `review ${ref.change.newrev} --submit`
        );

        if (exitCode !== 0) {
            const needsCodeReviewLines = errLines.filter((line) => line.match(/needs Code-Review/));
            if (needsCodeReviewLines.length > 0) {
                throw new Error(`Cannot merge, needs Code-Review: ${needsCodeReviewLines[0]}`);
            } else {
                throw new Error(`Gerrit command failed with exit code ${exitCode}. stderr=${errLines.join("\n")}`);
            }
        }

        let revisionMergedListener;
        const revisionMergedPromise = new Promise((resolve) => {
            revisionMergedListener = (resolve, mergedRev) => {
                if (mergedRev._id === revision._id) {
                    resolve(mergedRev);
                }
            };
            revisionMergedListener = revisionMergedListener.bind(this, resolve);
            this.on("revision.merged", revisionMergedListener);
        });
        await asyncWithTmo(
            revisionMergedPromise,
            DEFAULT_GERRIT_TIMEOUT,
            new Error(`Timeout while waiting for merge of revision ${revision._id} to complete`)
        );
        if (revisionMergedListener) {
            this.removeListener("revision.merged", revisionMergedListener);
        }

        return false;
    }

    async getUri(backend, repository) {
        const gerritUrl = url.parse(backend.uri);
        const connectUri = gerritUrl.format().replace(gerritUrl.auth, "$USER");

        return `${connectUri}/${repository._id}`;
    }

    async update(/* repository */) {
        // TODO: Implement update
    }

    async remove(/* repository */) {
        // TODO: Implement remove
    }

    async _getUserPublicKeys(username) {
        const restClient = await serviceMgr.use("userrepo");

        return await restClient.get(`/user/${username}/keys`);
    }

    async dispose() {
        this.removeAllListeners();
        this.gerritEmitter.removeAllListeners();
        await this.gerritEmitter.dispose();
        await this.__ssh.disconnect();
    }
}

module.exports = GerritBackend;
<commit_msg>CodeRepo: Increase gerrit merge timeout<commit_after>"use strict";

const url = require("url");
const readline = require("readline");
const fs = require("fs-extra-promise");
const moment = require("moment");
const { SshClient } = require("ssh");
const { serviceMgr } = require("service");
const { asyncWithTmo } = require("misc");
const log = require("log");
const { AsyncEventEmitter } = require("emitter");
const GerritEventEmitter = require("./gerrit_event_emitter");

const DEFAULT_GERRIT_PORT = 29418;
const DEFAULT_GERRIT_TIMEOUT = 60 * 1000;

/*
Note that localhost below needs to be changed to the host where
gerrit is running!

To get started with gerrit docker container:
1. Start container
2. Navigate to http://localhost:8080
3. Upload the public key configured for your gerrit backend to the admin user
  a. Click Become in Top-right corner
  b. Click Administrator
  c. Go to Settings (Located under user-menu in top-right corner)
  d. Click SSH Publick keys
  e. Click Add Key...
  f. Paste content of public key and click Add
4. Create repo via CodeRepo from UI or REST api

To get started with client side:
1. To clone repo, create user that will clone in gerrit
  cat /home/$USER/.ssh/id_rsa.pub | ssh -p 29418 admin@localhost gerrit create-account --full-name "'Marcus Christensson'" --email "'marcus.christensson@combitech.se'" --ssh-key - $USER
2. Clone using
  git clone ssh://$USER@localhost:29418/REPO_NAME
3. Copy gerrit hooks to local repo
  scp -p -P 29418 $USER@localhost:hooks/commit-msg REPO_ROOT/.git/hooks/
4. Set name and email in cloned repo
  git config --local user.name "Marcus Christensson"
  git config --local user.email "marcus.christensson@combitech.se"
4. When pusing changes, push with
  git push origin HEAD:refs/for/master
5. Add rights for registered users to code-review +2 and submit from gerrit UI
  a. Go to Project -> List and select Project (Repository)
  b. Select Access tab and click Edit
  c. Click "Add Reference"
  d. Reference field shall be "refs/heads/*"
  e. Click "Add Permission..." and select "Label Code Review"
  f. Enter Group Name "Registered Users"
  g. Click "Add Permission..." and select "Submit"
  h. Enter Group Name "Registered Users"
  i. Click "Save Changes"
6. Code-review +2 and submit via gerrit UI or using:
  ssh -p 29418 $USER@localhost gerrit review `git rev-parse HEAD` --code-review '+2' --submit
*/

class GerritBackend extends AsyncEventEmitter {
    constructor(id, backend, Repository, Revision) {
        super();
        this.id = id;
        this.backend = backend;
        this.locks = {};
        this.Repository = Repository;
        this.Revision = Revision;
        this.gerritEmitter = new GerritEventEmitter();

        this.__ssh = new SshClient();
    }

    async start() {
        const info = url.parse(this.backend.uri);
        const privateKey = await fs.readFileAsync(this.backend.privateKeyPath);
        const params = {
            host: info.hostname,
            port: info.port || DEFAULT_GERRIT_PORT,
            username: info.auth || process.env.USER,
            privateKey: privateKey
        };

        try {
            // TODO: Handle errors correctly from ssh client!
            await this.__ssh.connect(params, (err) => {
                throw new Error(`SSH client error: ${err}`);
            });
            await this._startMonitorEventStream();
        } catch (err) {
            log.error(`Failed to connect to gerrit using uri ${this.backend.uri}`, err);
        }
    }

    async validateRepository(/* event, data */) {
        // TODO: Validate gerrit specific options
    }

    async _readStream(stream) {
        const lines = [];
        const rl = readline.createInterface({
            input: stream,
            terminal: false
        });

        await new Promise((resolve) => {
            rl.on("line", (line) => {
                if (line.length === 0) {
                    return;
                }

                lines.push(line);
            });

            rl.on("close", resolve);
        });

        return lines;
    }

    async _execGerritCommand(cmd) {
        log.verbose("gerrit command:", cmd);

        return this.__ssh.execute(`gerrit ${cmd}`);
    }

    async _execGerritCommandReadOutput(cmd) {
        return new Promise(async (resolve) => {
            const { stdout, stderr } = await this._execGerritCommand(cmd);
            const exitCodePromise = new Promise((resolve) => stdout.on("close", resolve));
            const outLines = await this._readStream(stdout, "stdout");
            const errLines = await this._readStream(stderr, "stderr");

            if (errLines.length > 0) {
                log.error(`gerrit command: ${cmd} produced error output, stderr:`, errLines.join("\n"));
            }
            if (outLines.length > 0) {
                log.verbose(`gerrit command: ${cmd} produced output, stdout:`, outLines.join("\n"));
            }
            const exitCode = await exitCodePromise;
            resolve({ exitCode, outLines, errLines });
        });
    }

    async _onPatchsetCreated(event) {
        const changeId = event.changeKey.id;
        const repositoryId = event.project;
        const repository = await this.Repository.findOne({ _id: repositoryId });
        if (repository) {
            const revision = await this.Revision.findOne({ _id: changeId });
            const refIndex = revision ? revision.patches.length + 1 : 1;
            // TODO: Shall we use email and name from uploader?
            // TODO: Handle draft changes
            const ref = {
                index: refIndex,
                email: event.uploader.email,
                name: event.uploader.name,
                submitted: moment.unix(event.patchSet.createdOn).utc().format(),
                comment: event.change.commitMessage,
                change: {
                    oldrev: event.patchSet.parents[0],
                    newrev: event.patchSet.revision,
                    refname: event.patchSet.ref // Use event.refName all the time instead?
                }
            };

            await this.Revision.allocate(repository._id, changeId, ref);
            log.info(`Gerrit event allocated revision ${changeId}`);
        }
    }

    async _onChangeMerged(event) {
        const changeId = event.changeKey.id;
        // Make sure that we know about repo
        const repositoryId = event.project;
        const repository = await this.Repository.findOne({ _id: repositoryId });
        if (repository) {
            // And that we know about revision...
            const revision = await this.Revision.findOne({ _id: changeId });
            if (revision) {
                const ref = {
                    index: revision.patches.length + 1,
                    email: event.patchSet.uploader.email,
                    name: event.patchSet.uploader.name,
                    submitted: moment.unix(event.patchSet.createdOn).utc().format(),
                    comment: event.change.commitMessage,
                    change: {
                        oldrev: event.patchSet.parents[0],
                        newrev: event.newRev,
                        refname: event.refName
                    }
                };
                await revision.setMerged(ref);
                await this.emit("revision.merged", revision);
                log.info(`Gerrit event merged revision ${changeId}`);
            }
        }
    }

    async _startMonitorEventStream() {
        const { stdout, stderr } = await this._execGerritCommand("stream-events");

        this.gerritEmitter.addListener("stderr-output", (line) => {
            log.error("Gerrit event stream stderr: ", line);
        });
        this.gerritEmitter.addListener("patchset-created", this._onPatchsetCreated.bind(this));
        this.gerritEmitter.addListener("change-merged", this._onChangeMerged.bind(this));

        await this.gerritEmitter.start(stdout, stderr);
    }

    async create(repository) {
        log.info(`gerrit create repo ${repository._id}`);
        const { exitCode, errLines } = await this._execGerritCommandReadOutput(
            `create-project ${repository._id} ` +
            "--submit-type REBASE_IF_NECESSARY --empty-commit"
        );

        if (exitCode !== 0) {
            if (errLines[0].match(/Project already exists/)) {
                // OK, project already exists!
                log.info(`gerrit repo ${repository._id} already exists, attached to existing`);
            } else {
                throw new Error(`Gerrit command failed with exit code ${exitCode}. stderr=${errLines.join("\n")}`);
            }
        }
    }

    async merge(repository, revision) {
        log.info(`gerrit merge ${revision._id} in repo ${repository._id}`);
        // ssh -p 29418 $USER@localhost gerrit review `git rev-parse HEAD` --code-review '+2' --submit
        const ref = revision.patches[revision.patches.length - 1];
        const { exitCode, errLines } = await this._execGerritCommandReadOutput(
            `review ${ref.change.newrev} --submit`
        );

        if (exitCode !== 0) {
            const needsCodeReviewLines = errLines.filter((line) => line.match(/needs Code-Review/));
            if (needsCodeReviewLines.length > 0) {
                throw new Error(`Cannot merge, needs Code-Review: ${needsCodeReviewLines[0]}`);
            } else {
                throw new Error(`Gerrit command failed with exit code ${exitCode}. stderr=${errLines.join("\n")}`);
            }
        }

        let revisionMergedListener;
        const revisionMergedPromise = new Promise((resolve) => {
            revisionMergedListener = (resolve, mergedRev) => {
                if (mergedRev._id === revision._id) {
                    resolve(mergedRev);
                }
            };
            revisionMergedListener = revisionMergedListener.bind(this, resolve);
            this.on("revision.merged", revisionMergedListener);
        });
        await asyncWithTmo(
            revisionMergedPromise,
            DEFAULT_GERRIT_TIMEOUT,
            new Error(`Timeout while waiting for merge of revision ${revision._id} to complete`)
        );
        if (revisionMergedListener) {
            this.removeListener("revision.merged", revisionMergedListener);
        }

        return false;
    }

    async getUri(backend, repository) {
        const gerritUrl = url.parse(backend.uri);
        const connectUri = gerritUrl.format().replace(gerritUrl.auth, "$USER");

        return `${connectUri}/${repository._id}`;
    }

    async update(/* repository */) {
        // TODO: Implement update
    }

    async remove(/* repository */) {
        // TODO: Implement remove
    }

    async _getUserPublicKeys(username) {
        const restClient = await serviceMgr.use("userrepo");

        return await restClient.get(`/user/${username}/keys`);
    }

    async dispose() {
        this.removeAllListeners();
        this.gerritEmitter.removeAllListeners();
        await this.gerritEmitter.dispose();
        await this.__ssh.disconnect();
    }
}

module.exports = GerritBackend;
<|endoftext|>


<commit_before>//# sourceURL=rServeService.js

window.smartRApp.factory('rServeService', ['smartRUtils', '$q', '$http', function(smartRUtils, $q, $http) {

    var service = {};

    var NOOP_ABORT = function() {};
    var TIMEOUT = 10000 /* 10 s */;
    var CHECK_DELAY = 1000;
    var SESSION_TOUCH_DELAY = 9 * 60 * 1000; /* 9 min; session timeout is 10 */

    /* we only support one session at a time */

    var state = {
        currentRequestAbort: NOOP_ABORT,
        sessionId: null,
        touchTimeout: null // for current session id
    };

    var workflow = '';
    /* returns a promise with the session id and
     * saves the session id for future calls */
    service.startSession = function(name) {
        workflow = name;
        return $http({
            url: pageInfo.basePath + '/RSession/create',
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            config: {
                timeout: TIMEOUT
            },
            data: {
                workflow: workflow
            }
        })
            .then(function(response) {
                state.sessionId = response.data.sessionId;
                rServeService_scheduleTouch();
            }, transformAjaxFailure);

    };

    service.touch = function(sessionId) {
        if (sessionId != state.sessionId) {
            return;
        }

        return $http({
            url: pageInfo.basePath + '/RSession/touch',
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            config: {
                timeout: TIMEOUT
            },
            data: {
                sessionId: sessionId
            }
        })
            .finally(function() {
                rServeService_scheduleTouch(); // schedule another
            })
    };

    function rServeService_scheduleTouch() {
        var sessionId = state.sessionId;
        window.clearTimeout(state.touchTimeout);
        state.touchTimeout = window.setTimeout(function() {
            service.touch(sessionId);
        }, SESSION_TOUCH_DELAY);
    }

    service.destroySession = function(sessionId) {
        sessionId = sessionId || state.sessionId;

        if (!sessionId) {
            return;
        }

        return $http({
            url: pageInfo.basePath + '/RSession/delete',
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            config: {
                timeout: TIMEOUT
            },
            data: {
                sessionId: sessionId
            }
        })
            .catch(transformAjaxFailure)
            .finally(function() {
                if (state.sessionId == sessionId) {
                    service.abandonCurrentSession();
                }
            });
    };

    service.abandonCurrentSession = function() {
        window.clearTimeout(state.touchTimeout);
        state.sessionId = null;
    };

    service.destroyAndStartSession = function(workflowName) {

        // delete session before creating a new one
        $q.when(service.destroySession())
            .then(function() {
                // start a new session
                service.startSession(workflowName);
            });

    }

    /*
     * taskData = {
     *     arguments: { ... },
     *     taskType: 'fetchData' or name of R script minus .R,
     *     phase: 'fetch' | 'preprocess' | 'run',
     * }
     *
     * If it succeeds, the result will be the data returned by the server.
     * If it fails, it will return an object with at least these fields:
     * {
     *   status: 0 | <http error code>,
     *   statusText: <a description of the error>,
     * }
     */
    service.startScriptExecution = function(taskDataOrig) {

        var taskData = $.extend({}, taskDataOrig); // clone the thing
        state.currentRequestAbort();

        var runRequest = $http({
            url: pageInfo.basePath + '/ScriptExecution/run',
            method: 'POST',
            timeout: TIMEOUT,
            responseType: 'json',
            data: JSON.stringify({
                sessionId: state.sessionId,
                arguments: taskData.arguments,
                taskType: taskData.taskType,
                workflow: workflow
            })
        });

        _setCancellationForAjaxCall(runRequest);

        /* schedule checks */
        var promise = runRequest.then(
            function(response) {
                taskData.executionId = response.data.executionId;
                return _checkStatus(taskData.executionId, CHECK_DELAY);
            },
            transformAjaxFailure
        );

        promise.cancel = function timeoutRequest_cancel() {
            // calling this method should by itself resolve the promise
            state.currentRequestAbort();
        };

        // no touching necessary when a task is running
        window.clearTimeout(state.touchTimeout);
        promise.finally(rServeService_scheduleTouch.bind(this));

        return promise;
    };

    function _setCancellationForAjaxCall(ajax) {
        /* request in-flight; aborting is cancelling this request */
        state.currentRequestAbort = function() { ajax.abort(); };
        /* once the request finishes, there's nothing to abort.
         * this needs to be the 1st callback, so that later callbacks can
         * override this */
        ajax.finally(function() {
            state.currentRequestAbort = NOOP_ABORT;
        });
    }

    /* set a function to be executed after a period of time and
     * simultaneously define the request cancellation function
     * as the cancellation of this timeout.
     * Return the result as a promise. */
    function _setStatusRequestTimeout(funcToCall, delay /*, ... */) {
        var defer = $.Deferred();
        var promise = defer.promise();
        var restOfArguments = Array.prototype.slice.call(arguments)
            .splice(2); // arguments after delay

        function _setStatusRequestTimeout_wrappedFunc() {
            // we cannot abort by calling clearTimeout() anymore
            // funcToCall will probably set its own abort method
            state.currentRequestAbort = NOOP_ABORT;

            // funcToCall should itself return a promise or a final result
            // but let's cover the case where it throws
            var result;
            try {
                result = funcToCall.apply(undefined, restOfArguments); // promise
                result.done(function(x) { defer.resolve(x); });
                result.fail(function(x) { defer.reject(x); });
            } catch (e) {
                defer.fail(e);
            }
        }

        var timeout = window.setTimeout(
            _setStatusRequestTimeout_wrappedFunc, delay);

        state.currentRequestAbort = function() {
            clearTimeout(timeout);
            if (defer.state() == 'pending') {
                defer.reject({
                    status: 0,
                    statusText: 'abort'
                });
            }
            state.currentRequestAbort = NOOP_ABORT;
        };

        return promise;
    }

    /* aux function of _startScriptExecution. Needs to follow its contract
     * with respect to the fail and success result of the promise */
    function _checkStatus(executionId, delay) {
        var ajax = $.ajax({
            type: 'GET',
            url : pageInfo.basePath + '/ScriptExecution/status',
            data: {
                sessionId  : state.sessionId,
                executionId: executionId
            }
        });

        state.currentRequestAbort = function() { ajax.abort(); };
        ajax.always(function() { state.currentRequestAbort = NOOP_ABORT; });

        return ajax.then(function (d) {
            if (d.state === 'FINISHED') {
                d.executionId = executionId;
                return d;
            } else if (d.state === 'FAILED') {
                return $.Deferred().reject({
                    status: 0,
                    statusText: d.result.exception
                }).promise();
            } else {
                // else still pending
                return _setStatusRequestTimeout(
                    _checkStatus, delay, executionId, delay);
            }
        }, transformAjaxFailure);
    }

    function transformAjaxFailure(jqXHR, textStatus, errorThrown) {
        var ret = {
            status: jqXHR.status,
            statusText: jqXHR.statusText
        };
        if (jqXHR.responseJSON !== undefined) {
            ret.response = jqXHR.responseJSON;
        }

        return ret;
    }

    service.downloadJsonFile = function(executionId, filename) {
        // Simple GET request example:
        return $http({
            method: 'GET',
            url: this.urlForFile(executionId, filename)
        });
    };

    service.urlForFile = function(executionId, filename) {
        return pageInfo.basePath +
            '/ScriptExecution/downloadFile?sessionId=' +
            state.sessionId +
            '&executionId=' +
            executionId +
            '&filename=' +
            filename;
    };

    service.loadDataIntoSession = function(conceptKeys, dataConstraints) {
        return $q( function(resolve, reject) {
            smartRUtils.getSubsetIds().then(
                function(subsets) {
                    var _arg = {
                        conceptKeys: conceptKeys,
                        resultInstanceIds: subsets,
                        projection:'log_intensity'
                    };

                    if (typeof dataConstraints !== 'undefined') {
                        _arg.dataConstraints = dataConstraints;
                    }

                    service.startScriptExecution({
                        taskType: 'fetchData',
                        arguments: _arg
                    }).then(
                        function(ret) { resolve('Task complete! State: ' + ret.state); },
                        function(ret) { reject(ret.response); }
                    );
                },
                function() {
                    reject('Could not create subsets. Did you select a cohort?');
                }
            );
        });
    };

    service.executeSummaryStats = function(phase) {
        return $q( function(resolve, reject) {
            service.startScriptExecution({
                taskType: 'summary',
                arguments: {
                    phase: phase,
                    projection: 'log_intensity' // always required, even for low-dim data
                }
            }).then(
                function(ret) {
                    var _result = {};
                    if (ret.result.artifacts.files.length > 0) {
                        service.composeSummaryResults(ret.result.artifacts.files, ret.executionId, phase)
                            .then(function (result) {
                                _result = result;
                                resolve({result : _result, msg:'Task complete! State: ' + ret.state});
                            });
                    } else {
                        resolve({result : _result, msg:'Task complete! State: ' + ret.state});
                    }
                },
                function(ret) { reject(ret.response); }
            );
        });
    };

    service.composeSummaryResults = function(files, executionId, phase) {
        return $q(function (resolve, reject) {
            var retObj = {summary : []},
                fileExt = {fetch : ['.png', 'json'], preprocess :['all.png', 'all.json']};

                // find matched items in an array by key
                _find = function composeSummaryResults_find (key, array) {
                    // The variable results needs var in this case (without 'var' a global variable is created)
                    var results = [];
                    for (var i = 0; i < array.length; i++) {
                        if (array[i].indexOf(key) > -1) {
                            results.push(array[i]);
                        }
                    }
                    return results;
                },

                // process each item
                _processItem  = function composeSummaryResults_processItem(img, json) {
                    return $q(function (resolve, reject) {
                        service.downloadJsonFile(executionId, json).then(
                            function (d) {
                                resolve({img: service.urlForFile(executionId, img), json:d})
                            },
                            function (err) {reject(err);}
                        );
                    });
                };

            // first identify image and json files
            var _images = _find(fileExt[phase][0], files),_jsons = _find(fileExt[phase][1], files);

            // load each json file contents
            for (var i = 0; i < _images.length; i++){
                retObj.summary.push(_processItem(_images[i], _jsons[i]));
            }

            $.when.apply($, retObj.summary).then(function () {
                resolve(retObj); // when all contents has been loaded
            });
        });
    };

    service.preprocess = function(args) {
        return $q(function (resolve, reject) {
            service.startScriptExecution({
                taskType: 'preprocess',
                arguments: args
            }).then(
                function(ret) { resolve('Task complete! State: ' + ret.state); },
                function(ret) { reject(ret.response); }
            );
        });
    };

    return service;
}]);
<commit_msg>Properly handle $http error response<commit_after>//# sourceURL=rServeService.js

window.smartRApp.factory('rServeService', ['smartRUtils', '$q', '$http', function(smartRUtils, $q, $http) {

    var service = {};

    var NOOP_ABORT = function() {};
    var TIMEOUT = 10000 /* 10 s */;
    var CHECK_DELAY = 1000;
    var SESSION_TOUCH_DELAY = 9 * 60 * 1000; /* 9 min; session timeout is 10 */

    /* we only support one session at a time */

    var state = {
        currentRequestAbort: NOOP_ABORT,
        sessionId: null,
        touchTimeout: null // for current session id
    };

    var workflow = '';
    /* returns a promise with the session id and
     * saves the session id for future calls */
    service.startSession = function(name) {
        workflow = name;
        return $http({
            url: pageInfo.basePath + '/RSession/create',
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            config: {
                timeout: TIMEOUT
            },
            data: {
                workflow: workflow
            }
        })
            .then(function(response) {
                state.sessionId = response.data.sessionId;
                rServeService_scheduleTouch();
            }, transformAjaxFailure);

    };

    service.touch = function(sessionId) {
        if (sessionId != state.sessionId) {
            return;
        }

        return $http({
            url: pageInfo.basePath + '/RSession/touch',
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            config: {
                timeout: TIMEOUT
            },
            data: {
                sessionId: sessionId
            }
        })
            .finally(function() {
                rServeService_scheduleTouch(); // schedule another
            })
    };

    function rServeService_scheduleTouch() {
        var sessionId = state.sessionId;
        window.clearTimeout(state.touchTimeout);
        state.touchTimeout = window.setTimeout(function() {
            service.touch(sessionId);
        }, SESSION_TOUCH_DELAY);
    }

    service.destroySession = function(sessionId) {
        sessionId = sessionId || state.sessionId;

        if (!sessionId) {
            return;
        }

        return $http({
            url: pageInfo.basePath + '/RSession/delete',
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            config: {
                timeout: TIMEOUT
            },
            data: {
                sessionId: sessionId
            }
        })
            .catch(transformAjaxFailure)
            .finally(function() {
                if (state.sessionId == sessionId) {
                    service.abandonCurrentSession();
                }
            });
    };

    service.abandonCurrentSession = function() {
        window.clearTimeout(state.touchTimeout);
        state.sessionId = null;
    };

    service.destroyAndStartSession = function(workflowName) {

        // delete session before creating a new one
        $q.when(service.destroySession())
            .then(function() {
                // start a new session
                service.startSession(workflowName);
            });

    }

    /*
     * taskData = {
     *     arguments: { ... },
     *     taskType: 'fetchData' or name of R script minus .R,
     *     phase: 'fetch' | 'preprocess' | 'run',
     * }
     *
     * If it succeeds, the result will be the data returned by the server.
     * If it fails, it will return an object with at least these fields:
     * {
     *   status: 0 | <http error code>,
     *   statusText: <a description of the error>,
     * }
     */
    service.startScriptExecution = function(taskDataOrig) {

        var taskData = $.extend({}, taskDataOrig); // clone the thing
        state.currentRequestAbort();

        var runRequest = $http({
            url: pageInfo.basePath + '/ScriptExecution/run',
            method: 'POST',
            timeout: TIMEOUT,
            responseType: 'json',
            data: JSON.stringify({
                sessionId: state.sessionId,
                arguments: taskData.arguments,
                taskType: taskData.taskType,
                workflow: workflow
            })
        });

        _setCancellationForAjaxCall(runRequest);

        /* schedule checks */
        var promise = runRequest.then(
            function(response) {
                taskData.executionId = response.data.executionId;
                return _checkStatus(taskData.executionId, CHECK_DELAY);
            },
            transformAjaxFailure
        );

        promise.cancel = function timeoutRequest_cancel() {
            // calling this method should by itself resolve the promise
            state.currentRequestAbort();
        };

        // no touching necessary when a task is running
        window.clearTimeout(state.touchTimeout);
        promise.finally(rServeService_scheduleTouch.bind(this));

        return promise;
    };

    function _setCancellationForAjaxCall(ajax) {
        /* request in-flight; aborting is cancelling this request */
        state.currentRequestAbort = function() { ajax.abort(); };
        /* once the request finishes, there's nothing to abort.
         * this needs to be the 1st callback, so that later callbacks can
         * override this */
        ajax.finally(function() {
            state.currentRequestAbort = NOOP_ABORT;
        });
    }

    /* set a function to be executed after a period of time and
     * simultaneously define the request cancellation function
     * as the cancellation of this timeout.
     * Return the result as a promise. */
    function _setStatusRequestTimeout(funcToCall, delay /*, ... */) {
        var defer = $.Deferred();
        var promise = defer.promise();
        var restOfArguments = Array.prototype.slice.call(arguments)
            .splice(2); // arguments after delay

        function _setStatusRequestTimeout_wrappedFunc() {
            // we cannot abort by calling clearTimeout() anymore
            // funcToCall will probably set its own abort method
            state.currentRequestAbort = NOOP_ABORT;

            // funcToCall should itself return a promise or a final result
            // but let's cover the case where it throws
            var result;
            try {
                result = funcToCall.apply(undefined, restOfArguments); // promise
                result.done(function(x) { defer.resolve(x); });
                result.fail(function(x) { defer.reject(x); });
            } catch (e) {
                defer.fail(e);
            }
        }

        var timeout = window.setTimeout(
            _setStatusRequestTimeout_wrappedFunc, delay);

        state.currentRequestAbort = function() {
            clearTimeout(timeout);
            if (defer.state() == 'pending') {
                defer.reject({
                    status: 0,
                    statusText: 'abort'
                });
            }
            state.currentRequestAbort = NOOP_ABORT;
        };

        return promise;
    }

    /* aux function of _startScriptExecution. Needs to follow its contract
     * with respect to the fail and success result of the promise */
    function _checkStatus(executionId, delay) {
        var ajax = $.ajax({
            type: 'GET',
            url : pageInfo.basePath + '/ScriptExecution/status',
            data: {
                sessionId  : state.sessionId,
                executionId: executionId
            }
        });

        state.currentRequestAbort = function() { ajax.abort(); };
        ajax.always(function() { state.currentRequestAbort = NOOP_ABORT; });

        return ajax.then(function (d) {
            if (d.state === 'FINISHED') {
                d.executionId = executionId;
                return d;
            } else if (d.state === 'FAILED') {
                return $.Deferred().reject({
                    status: 0,
                    statusText: d.result.exception
                }).promise();
            } else {
                // else still pending
                return _setStatusRequestTimeout(
                    _checkStatus, delay, executionId, delay);
            }
        }, transformAjaxFailure);
    }

    function transformAjaxFailure(response) {
        var ret = {
            status: response.status,
            statusText: response.statusText
        };
        if (response.data !== undefined) {
            ret.response = response.data;
        }

        return ret;
    }

    service.downloadJsonFile = function(executionId, filename) {
        // Simple GET request example:
        return $http({
            method: 'GET',
            url: this.urlForFile(executionId, filename)
        });
    };

    service.urlForFile = function(executionId, filename) {
        return pageInfo.basePath +
            '/ScriptExecution/downloadFile?sessionId=' +
            state.sessionId +
            '&executionId=' +
            executionId +
            '&filename=' +
            filename;
    };

    service.loadDataIntoSession = function(conceptKeys, dataConstraints) {
        return $q( function(resolve, reject) {
            smartRUtils.getSubsetIds().then(
                function(subsets) {
                    var _arg = {
                        conceptKeys: conceptKeys,
                        resultInstanceIds: subsets,
                        projection:'log_intensity'
                    };

                    if (typeof dataConstraints !== 'undefined') {
                        _arg.dataConstraints = dataConstraints;
                    }

                    service.startScriptExecution({
                        taskType: 'fetchData',
                        arguments: _arg
                    }).then(
                        function(ret) { resolve('Task complete! State: ' + ret.state); },
                        function(ret) { reject(ret.response); }
                    );
                },
                function() {
                    reject('Could not create subsets. Did you select a cohort?');
                }
            );
        });
    };

    service.executeSummaryStats = function(phase) {
        return $q( function(resolve, reject) {
            service.startScriptExecution({
                taskType: 'summary',
                arguments: {
                    phase: phase,
                    projection: 'log_intensity' // always required, even for low-dim data
                }
            }).then(
                function(ret) {
                    var _result = {};
                    if (ret.result.artifacts.files.length > 0) {
                        service.composeSummaryResults(ret.result.artifacts.files, ret.executionId, phase)
                            .then(function (result) {
                                _result = result;
                                resolve({result : _result, msg:'Task complete! State: ' + ret.state});
                            });
                    } else {
                        resolve({result : _result, msg:'Task complete! State: ' + ret.state});
                    }
                },
                function(ret) { reject(ret.response); }
            );
        });
    };

    service.composeSummaryResults = function(files, executionId, phase) {
        return $q(function (resolve, reject) {
            var retObj = {summary : []},
                fileExt = {fetch : ['.png', 'json'], preprocess :['all.png', 'all.json']};

                // find matched items in an array by key
                _find = function composeSummaryResults_find (key, array) {
                    // The variable results needs var in this case (without 'var' a global variable is created)
                    var results = [];
                    for (var i = 0; i < array.length; i++) {
                        if (array[i].indexOf(key) > -1) {
                            results.push(array[i]);
                        }
                    }
                    return results;
                },

                // process each item
                _processItem  = function composeSummaryResults_processItem(img, json) {
                    return $q(function (resolve, reject) {
                        service.downloadJsonFile(executionId, json).then(
                            function (d) {
                                resolve({img: service.urlForFile(executionId, img), json:d})
                            },
                            function (err) {reject(err);}
                        );
                    });
                };

            // first identify image and json files
            var _images = _find(fileExt[phase][0], files),_jsons = _find(fileExt[phase][1], files);

            // load each json file contents
            for (var i = 0; i < _images.length; i++){
                retObj.summary.push(_processItem(_images[i], _jsons[i]));
            }

            $.when.apply($, retObj.summary).then(function () {
                resolve(retObj); // when all contents has been loaded
            });
        });
    };

    service.preprocess = function(args) {
        return $q(function (resolve, reject) {
            service.startScriptExecution({
                taskType: 'preprocess',
                arguments: args
            }).then(
                function(ret) { resolve('Task complete! State: ' + ret.state); },
                function(ret) { reject(ret.response); }
            );
        });
    };

    return service;
}]);
<|endoftext|>