<commit_before>var async = require('async'),
	StaticScraper = require('./StaticScraper'),
	DynamicScraper = require('./DynamicScraper'),
	ScraperError = require('./ScraperError');

/**
 * Transforms a string into a regular expression.
 * This function is from the project Routes.js, under the MIT licence,
 *   {@link https://github.com/aaronblohowiak/routes.js} it's present
 *   in the file {@link https://github.com/aaronblohowiak/routes.js/blob/bdad0a1ae10d11981bb286550bb3b8a1a71909bd/dist/routes.js#L49}.
 *
 * @param  {!string} path String path.
 * @param  {!Array.<string>} keys Empty array to be filled with the
 *   keys ids.
 * @return {!RegExp} Regular expression.
 */
function pathToRegExp(path, keys) {
	path = path
		.concat('/?')
		.replace(/\/\(/g, '(?:/')
		.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?|\*/g, function(_, slash, format, key, capture, optional) {
			if (_ === '*') {
				return _;
			}

			keys.push(key);
			slash = slash || '';
			return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || '([^/]+?)') + ')' + (optional || '');
		})
		.replace(/([\/.])/g, '\\$1')
		.replace(/\*/g, '(.*)');
	return new RegExp('^' + path + '$', 'i');
}

/**
 * Routes an url thought a valid, predefined, path.
 *
 * @param {!Object=} options Setup options.
 * @param {!boolean=} options.firstMatch If true the router will stop
 *   at the first path matched. The default is false, and tries to
 *   match every path.
 * @constructor
 */
var Router = function(options) {
	options = options || {};
	/**
	 * Stops routing at first successful match.
	 *
	 * @type {!boolean}
	 * @private
	 */
	this.firstMatchStop = options.firstMatch || false;
	/**
	 * Chain of promises.
	 *
	 * @type {!Array.<!Object>}
	 * @private
	 */
	this.promises = [];
	/**
	 * Otherwise promise.
	 *
	 * @type {!function(!string=)}
	 * @private
	 */
	this.otherwiseFn = function() {};
};
Router.prototype = {
	constructor: Router,
	/**
	 * Promise to url match. It's promise will fire only if the path
	 *   matches with and url being routed.
	 *
	 * @param  {!(string|RegExp|function(string):?)} path The
	 *   path or regular expression to match an url.
	 *   Alternatively a function that receives the url to be matched
	 *   can be passed. If the result is false, or any
	 *   !!result===false), the path is considered valid and the
	 *   scraping should be done. If ,in case of a valid path, an Object is returned, it will be associated with the params of this
	 *   route/path.
	 *   For more information on the path matching refer to {@link https://github.com/aaronblohowiak/routes.js/blob/76bc517037a0321507c4d84a0cdaca6db31ebaa4/README.md#path-formats}
	 * @return {!Router} This router.
	 * @public
	 */
	on: function(path) {
		var callback;
		if (typeof path === 'function') {
			callback = path;
		}

		this.promises.push({
			callback: callback ? function(url) {
				return callback(url);
			} : Router.pathMatcher(path),
			scraper: null,
			rqMethod: null
		});
		return this.get();
	},
	/**
	 * Sets the request method to be a simple HTTP GET.
	 * {@see AbstractScraper.get}
	 *
	 * @return {!Router} This router.
	 * @public
	 */
	get: function() {
		var length = this.promises.length,
			last = this.promises[length - 1];
		if (length && last) {
			last.rqMethod = function(scraper, url) {
				scraper.get(url);
			};
			return this;
		} else {
			throw new ScraperError('');
		}
	},
	/**
	 * Sets the request method to be according to the options.
	 * {@see AbstractScraper.request}
	 *
	 * @param  {!Object} options Request options.
	 * @return {!Router} This router.
	 * @public
	 */
	request: function(options) {
		var length = this.promises.length,
			last = this.promises[length - 1];
		if (length && last) {
			last.rqMethod = function(scraper, url) {
				options.uri = url;
				scraper.request(options);
			};
			return this;
		} else {
			throw new ScraperError('');
		}
	},
	/**
	 * A promise to be triggered when none of the paths where matched.
	 * This is a one time promise, which means that the last promise
	 *   is gonna be the one to be executed.
	 *
	 * @param  {!function(!string=)} callback Function with the url as
	 *   a parameter.
	 * @return {!Router} This router.
	 * @public
	 */
	otherwise: function(callback) {
		this.otherwiseFn = callback;
		return this;
	},
	/**
	 * Creates a static scraper, and associates it with the current
	 *   router promise chain. Note that this method returns a
	 *   {@see ScraperPromise} of a {@see StaticScraper}.
	 *
	 * @return {!ScraperPromise} A promise for the scraper.
	 * @public
	 */
	createStatic: function() {
		var length = this.promises.length,
			last = this.promises[length - 1];
		if (length && last && !last.scraper) {
			var ss = StaticScraper.create();
			last.scraper = ss;
			return ss;
		} else {
			throw new ScraperError('');
		}
	},
	/**
	 * Associates the current route with the a scraper (promise)
	 *   instance. Keep in mind that the done promise will not be
	 *   available.
	 *
	 * @param  {!AbstractScraper} scraper A scraper instance to use.
	 * @return {!Router} This router.
	 * @public
	 */
	use: function(scraper) {
		var length = this.promises.length,
			last = this.promises[length - 1];
		if (length && last && !last.scraper) {
			last.scraper = scraper;
			return this;
		} else {
			throw new ScraperError('');
		}
	},
	/**
	 * Creates a dynamic scraper, and associates it with the current
	 *   router promise chain. Note that this method returns a
	 *   {@see ScraperPromise} of a {@see DynamicScraper}.
	 *
	 * @return {!ScraperPromise} A promise for the scraper.
	 * @public
	 */
	createDynamic: function() {
		var length = this.promises.length,
			last = this.promises[length - 1];
		if (length && last && !last.scraper) {
			var ss = DynamicScraper.create();
			last.scraper = ss;
			return ss;
		} else {
			throw new ScraperError('');
		}
	},
	/**
	 * Routes a url through every path that matches it.
	 *
	 * @param  {!string} url The url to route.
	 * @param  {!function(boolean)} callback Function to call when the
	 *   routing is complete. If any of the paths was found the
	 *   parameter is true, false otherwise.
	 * @return {!Router} This router.
	 * @public
	 */
	route: function(url, callback) {
		var that = this,
			atLeastOne = false,
			stopFlag = {},
			lastReturn = undefined;
		callback = callback || function() {};
		async.eachSeries(this.promises, function(promiseObj, done) {

			var matcher = promiseObj.callback,
				scraper,
				reqMethod = promiseObj.rqMethod;
			var result = matcher(url);
			if (!!result) {
				scraper = promiseObj.scraper.clone();
				atLeastOne = true;
				scraper._setChainParameter(result);
				scraper.done(function(utils) {
					lastReturn = utils.lastReturn;
					done(that.firstMatchStop ? stopFlag : undefined);
				});
				reqMethod(scraper, url);
			} else {
				done();
			}

		}, function() {
			if (!atLeastOne) {
				that.otherwiseFn(url);
			}
			callback(atLeastOne, lastReturn);
		});
		return this;
	}
};
/**
 * Creates a function to match a path against a string.
 *
 * @param  {!(string|RegExp)} pathOrRE Pattern to match, if it's a
 *   string it will be transformed into a regular expression.
 * @return {!function(string):(Object|booelan)} A matching function,
 *   that given a string will check if it matches the path. If the
 *   path has parameters it will return an object with the parameters
 *   as keys and the values as the values of the parameters. An empty
 *   object if there were no valid parameters or false if the path
 *   doesn't match with the string.
 * @public
 * @static
 */
Router.pathMatcher = function(pathOrRE) {
	var pattern,
		keys = ['url'];
	if (pathOrRE instanceof RegExp) {
		pattern = pathOrRE;
	} else if (typeof pathOrRE === 'string') {
		pattern = pathToRegExp(pathOrRE, keys);
	} else {
		throw new ScraperError('A path must be a string or a regular expression.');
	}

	return function patternMatchingFunction(url) {
		var match = pattern.exec(url);
		if (!match) {
			return false;
		} else {
			return keys.reduce(function(obj, value, index) {
				obj[value] = match[index];
				return obj;
			}, {});
		}
	};
};

module.exports = Router;<commit_msg>Fixed minor jshint error.<commit_after>var async = require('async'),
	StaticScraper = require('./StaticScraper'),
	DynamicScraper = require('./DynamicScraper'),
	ScraperError = require('./ScraperError');

/**
 * Transforms a string into a regular expression.
 * This function is from the project Routes.js, under the MIT licence,
 *   {@link https://github.com/aaronblohowiak/routes.js} it's present
 *   in the file {@link https://github.com/aaronblohowiak/routes.js/blob/bdad0a1ae10d11981bb286550bb3b8a1a71909bd/dist/routes.js#L49}.
 *
 * @param  {!string} path String path.
 * @param  {!Array.<string>} keys Empty array to be filled with the
 *   keys ids.
 * @return {!RegExp} Regular expression.
 */
function pathToRegExp(path, keys) {
	path = path
		.concat('/?')
		.replace(/\/\(/g, '(?:/')
		.replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?|\*/g, function(_, slash, format, key, capture, optional) {
			if (_ === '*') {
				return _;
			}

			keys.push(key);
			slash = slash || '';
			return '' + (optional ? '' : slash) + '(?:' + (optional ? slash : '') + (format || '') + (capture || '([^/]+?)') + ')' + (optional || '');
		})
		.replace(/([\/.])/g, '\\$1')
		.replace(/\*/g, '(.*)');
	return new RegExp('^' + path + '$', 'i');
}

/**
 * Routes an url thought a valid, predefined, path.
 *
 * @param {!Object=} options Setup options.
 * @param {!boolean=} options.firstMatch If true the router will stop
 *   at the first path matched. The default is false, and tries to
 *   match every path.
 * @constructor
 */
var Router = function(options) {
	options = options || {};
	/**
	 * Stops routing at first successful match.
	 *
	 * @type {!boolean}
	 * @private
	 */
	this.firstMatchStop = options.firstMatch || false;
	/**
	 * Chain of promises.
	 *
	 * @type {!Array.<!Object>}
	 * @private
	 */
	this.promises = [];
	/**
	 * Otherwise promise.
	 *
	 * @type {!function(!string=)}
	 * @private
	 */
	this.otherwiseFn = function() {};
};
Router.prototype = {
	constructor: Router,
	/**
	 * Promise to url match. It's promise will fire only if the path
	 *   matches with and url being routed.
	 *
	 * @param  {!(string|RegExp|function(string):?)} path The
	 *   path or regular expression to match an url.
	 *   Alternatively a function that receives the url to be matched
	 *   can be passed. If the result is false, or any
	 *   !!result===false), the path is considered valid and the
	 *   scraping should be done. If ,in case of a valid path, an Object is returned, it will be associated with the params of this
	 *   route/path.
	 *   For more information on the path matching refer to {@link https://github.com/aaronblohowiak/routes.js/blob/76bc517037a0321507c4d84a0cdaca6db31ebaa4/README.md#path-formats}
	 * @return {!Router} This router.
	 * @public
	 */
	on: function(path) {
		var callback;
		if (typeof path === 'function') {
			callback = path;
		}

		this.promises.push({
			callback: callback ? function(url) {
				return callback(url);
			} : Router.pathMatcher(path),
			scraper: null,
			rqMethod: null
		});
		return this.get();
	},
	/**
	 * Sets the request method to be a simple HTTP GET.
	 * {@see AbstractScraper.get}
	 *
	 * @return {!Router} This router.
	 * @public
	 */
	get: function() {
		var length = this.promises.length,
			last = this.promises[length - 1];
		if (length && last) {
			last.rqMethod = function(scraper, url) {
				scraper.get(url);
			};
			return this;
		} else {
			throw new ScraperError('');
		}
	},
	/**
	 * Sets the request method to be according to the options.
	 * {@see AbstractScraper.request}
	 *
	 * @param  {!Object} options Request options.
	 * @return {!Router} This router.
	 * @public
	 */
	request: function(options) {
		var length = this.promises.length,
			last = this.promises[length - 1];
		if (length && last) {
			last.rqMethod = function(scraper, url) {
				options.uri = url;
				scraper.request(options);
			};
			return this;
		} else {
			throw new ScraperError('');
		}
	},
	/**
	 * A promise to be triggered when none of the paths where matched.
	 * This is a one time promise, which means that the last promise
	 *   is gonna be the one to be executed.
	 *
	 * @param  {!function(!string=)} callback Function with the url as
	 *   a parameter.
	 * @return {!Router} This router.
	 * @public
	 */
	otherwise: function(callback) {
		this.otherwiseFn = callback;
		return this;
	},
	/**
	 * Creates a static scraper, and associates it with the current
	 *   router promise chain. Note that this method returns a
	 *   {@see ScraperPromise} of a {@see StaticScraper}.
	 *
	 * @return {!ScraperPromise} A promise for the scraper.
	 * @public
	 */
	createStatic: function() {
		var length = this.promises.length,
			last = this.promises[length - 1];
		if (length && last && !last.scraper) {
			var ss = StaticScraper.create();
			last.scraper = ss;
			return ss;
		} else {
			throw new ScraperError('');
		}
	},
	/**
	 * Associates the current route with the a scraper (promise)
	 *   instance. Keep in mind that the done promise will not be
	 *   available.
	 *
	 * @param  {!AbstractScraper} scraper A scraper instance to use.
	 * @return {!Router} This router.
	 * @public
	 */
	use: function(scraper) {
		var length = this.promises.length,
			last = this.promises[length - 1];
		if (length && last && !last.scraper) {
			last.scraper = scraper;
			return this;
		} else {
			throw new ScraperError('');
		}
	},
	/**
	 * Creates a dynamic scraper, and associates it with the current
	 *   router promise chain. Note that this method returns a
	 *   {@see ScraperPromise} of a {@see DynamicScraper}.
	 *
	 * @return {!ScraperPromise} A promise for the scraper.
	 * @public
	 */
	createDynamic: function() {
		var length = this.promises.length,
			last = this.promises[length - 1];
		if (length && last && !last.scraper) {
			var ss = DynamicScraper.create();
			last.scraper = ss;
			return ss;
		} else {
			throw new ScraperError('');
		}
	},
	/**
	 * Routes a url through every path that matches it.
	 *
	 * @param  {!string} url The url to route.
	 * @param  {!function(boolean)} callback Function to call when the
	 *   routing is complete. If any of the paths was found the
	 *   parameter is true, false otherwise.
	 * @return {!Router} This router.
	 * @public
	 */
	route: function(url, callback) {
		var that = this,
			atLeastOne = false,
			stopFlag = {},
			lastReturn;
		callback = callback || function() {};
		async.eachSeries(this.promises, function(promiseObj, done) {

			var matcher = promiseObj.callback,
				scraper,
				reqMethod = promiseObj.rqMethod;
			var result = matcher(url);
			if (!!result) {
				scraper = promiseObj.scraper.clone();
				atLeastOne = true;
				scraper._setChainParameter(result);
				scraper.done(function(utils) {
					lastReturn = utils.lastReturn;
					done(that.firstMatchStop ? stopFlag : undefined);
				});
				reqMethod(scraper, url);
			} else {
				done();
			}

		}, function() {
			if (!atLeastOne) {
				that.otherwiseFn(url);
			}
			callback(atLeastOne, lastReturn);
		});
		return this;
	}
};
/**
 * Creates a function to match a path against a string.
 *
 * @param  {!(string|RegExp)} pathOrRE Pattern to match, if it's a
 *   string it will be transformed into a regular expression.
 * @return {!function(string):(Object|booelan)} A matching function,
 *   that given a string will check if it matches the path. If the
 *   path has parameters it will return an object with the parameters
 *   as keys and the values as the values of the parameters. An empty
 *   object if there were no valid parameters or false if the path
 *   doesn't match with the string.
 * @public
 * @static
 */
Router.pathMatcher = function(pathOrRE) {
	var pattern,
		keys = ['url'];
	if (pathOrRE instanceof RegExp) {
		pattern = pathOrRE;
	} else if (typeof pathOrRE === 'string') {
		pattern = pathToRegExp(pathOrRE, keys);
	} else {
		throw new ScraperError('A path must be a string or a regular expression.');
	}

	return function patternMatchingFunction(url) {
		var match = pattern.exec(url);
		if (!match) {
			return false;
		} else {
			return keys.reduce(function(obj, value, index) {
				obj[value] = match[index];
				return obj;
			}, {});
		}
	};
};

module.exports = Router;<|endoftext|>


<commit_before>angular.module('pvta.controllers').controller('RouteController', function($scope, $state, $stateParams, Route, RouteVehicles, FavoriteRoutes, Messages, KML, $location, LatLong, Map){

  var size = 0;

  var getVehicles = function(){
    $scope.vehicles = RouteVehicles.query({id: $stateParams.routeId});
  };

  var route = Route.get({routeId: $stateParams.routeId}, function() {
    route.$save();
    getHeart();
    $scope.stops = route.Stops;
    $scope.vehicles = route.Vehicles;

    // Need route to be defined before we can filter messages
    var messages = Messages.query(function(){
      var filteredMessages = [];
      for(var message of messages){
        if(message.Routes.indexOf($scope.route.RouteId) === -1) { continue; }
        filteredMessages.push(message);
      }
      $scope.messages = filteredMessages;
    });
  });
  $scope.route = route;

  $scope.stops = [];
  var j = $scope.size;

  $scope.toggleGroup = function(group) {
    if ($scope.isGroupShown(group)) {
      $scope.shownGroup = null;
    } else {
      $scope.shownGroup = group;
    }
  };
  $scope.isGroupShown = function(group) {
    return $scope.shownGroup === group;
  };
  $scope.toggleHeart = function(liked){
    FavoriteRoutes.contains(route, function(bool){
      if(bool) {
        FavoriteRoutes.remove(route);
      }
      else {
        FavoriteRoutes.push(route);
      }
    });
  };
  $scope.liked = false;
  var getHeart = function(){
    FavoriteRoutes.contains(route, function(bool){
      $scope.liked = bool;
    });
  };

  $scope.setKML = function(){
    KML.push(route.RouteTraceFilename);
    $state.go('app.route-map', {routeId: $stateParams.routeId});
  };

  $scope.refresh = function(){
    getVehicles();
    $scope.$broadcast('scroll.refreshComplete');
  };

  var size = 0;

  var bounds;

  function initMap () {
    bounds = new google.maps.LatLngBounds();
    var mapOptions = {
      center: bounds.getCenter(),
      zoom: 15,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    };
    $scope.map = new google.maps.Map(document.getElementById('map'), mapOptions);
    Map.init($scope.map, bounds);
  }
  initMap();

  function redrawMap () {
    addKML(route.ShortName);
    mapVehicles($scope.vehicles);
  }
  var getVehicles = function(){
    $scope.vehicles = RouteVehicles.query({id: $stateParams.routeId}, function(){
      mapVehicles($scope.vehicles);
      addKML(route.ShortName);
    });
  };

  function addKML (shortName) {
    var toAdd = 'http://bustracker.pvta.com/infopoint/Resources/Traces/route' + shortName + '.kml';
    var georssLayer = new google.maps.KmlLayer({
      url: toAdd
    });
    georssLayer.setMap($scope.map);
  }

  function mapVehicles(vehicles){
    _.each(vehicles, function(vehicle){
      var loc = new google.maps.LatLng(vehicle.Latitude, vehicle.Longitude);
      Map.addMapListener(Map.placeDesiredMarker(loc, 'http://www.google.com/mapfiles/kml/paddle/go.png'), 'Here is your bus');
    });
  }

  $scope.$on('$ionicView.enter', function(){
    getHeart();
    getVehicles();
    initMap();
    mapVehicles();
    addKML(route.ShortName);
  });
});
<commit_msg>Removed unneeded dependencies<commit_after>angular.module('pvta.controllers').controller('RouteController', function($scope, $state, $stateParams, Route, RouteVehicles, FavoriteRoutes, Messages, KML, Map){

  var size = 0;

  var getVehicles = function(){
    $scope.vehicles = RouteVehicles.query({id: $stateParams.routeId});
  };

  var route = Route.get({routeId: $stateParams.routeId}, function() {
    route.$save();
    getHeart();
    $scope.stops = route.Stops;
    $scope.vehicles = route.Vehicles;

    // Need route to be defined before we can filter messages
    var messages = Messages.query(function(){
      var filteredMessages = [];
      for(var message of messages){
        if(message.Routes.indexOf($scope.route.RouteId) === -1) { continue; }
        filteredMessages.push(message);
      }
      $scope.messages = filteredMessages;
    });
  });
  $scope.route = route;

  $scope.stops = [];
  var j = $scope.size;

  $scope.toggleGroup = function(group) {
    if ($scope.isGroupShown(group)) {
      $scope.shownGroup = null;
    } else {
      $scope.shownGroup = group;
    }
  };
  $scope.isGroupShown = function(group) {
    return $scope.shownGroup === group;
  };
  $scope.toggleHeart = function(liked){
    FavoriteRoutes.contains(route, function(bool){
      if(bool) {
        FavoriteRoutes.remove(route);
      }
      else {
        FavoriteRoutes.push(route);
      }
    });
  };
  $scope.liked = false;
  var getHeart = function(){
    FavoriteRoutes.contains(route, function(bool){
      $scope.liked = bool;
    });
  };

  $scope.setKML = function(){
    KML.push(route.RouteTraceFilename);
    $state.go('app.route-map', {routeId: $stateParams.routeId});
  };

  $scope.refresh = function(){
    getVehicles();
    $scope.$broadcast('scroll.refreshComplete');
  };

  var size = 0;

  var bounds;

  function initMap () {
    bounds = new google.maps.LatLngBounds();
    var mapOptions = {
      center: bounds.getCenter(),
      zoom: 15,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    };
    $scope.map = new google.maps.Map(document.getElementById('map'), mapOptions);
    Map.init($scope.map, bounds);
  }
  initMap();

  function redrawMap () {
    addKML(route.ShortName);
    mapVehicles($scope.vehicles);
  }
  var getVehicles = function(){
    $scope.vehicles = RouteVehicles.query({id: $stateParams.routeId}, function(){
      mapVehicles($scope.vehicles);
      addKML(route.ShortName);
    });
  };

  function addKML (shortName) {
    var toAdd = 'http://bustracker.pvta.com/infopoint/Resources/Traces/route' + shortName + '.kml';
    var georssLayer = new google.maps.KmlLayer({
      url: toAdd
    });
    georssLayer.setMap($scope.map);
  }

  function mapVehicles(vehicles){
    _.each(vehicles, function(vehicle){
      var loc = new google.maps.LatLng(vehicle.Latitude, vehicle.Longitude);
      Map.addMapListener(Map.placeDesiredMarker(loc, 'http://www.google.com/mapfiles/kml/paddle/go.png'), 'Here is your bus');
    });
  }

  $scope.$on('$ionicView.enter', function(){
    getHeart();
    getVehicles();
    initMap();
    mapVehicles();
    addKML(route.ShortName);
  });
});
<|endoftext|>


<commit_before>import React from 'react';
import {expect} from 'chai';
import {mobxPlugin, RouterStore} from 'mobx-router5';
import {createTestRouter} from './utils/test-utils';
import {shallow} from 'enzyme';
import {createRouter} from 'router5';
import BaseLink from '../src/modules/BaseLink';

describe('BaseLink component', () => {
  let router;
  let routerStore;

  beforeEach(() => {
    routerStore = new RouterStore();
    router = createTestRouter();
    router.usePlugin(mobxPlugin(routerStore));
  });

  context('Exceptions', function() {
    it('should not throw an error if `routeName`, `router` and `onClick` props are missing', () => {
      const renderTreeFn = () => shallow(
        <BaseLink />
      );
      expect(renderTreeFn).to.not.throw();
    });

    it('should throw an error if prop `routeName` passed but not props: `router` or `routerStore`', () => {
      router = createRouter();
      router.addNode('home', '/home');
      router.setOption('defaultRoute', 'home');
      router.start();
      const renderTreeFn = () => shallow(
        <BaseLink routeName={'home'}/>
      );
      expect(renderTreeFn).to.throw('[react-mobx-router5][BaseLink] missing router instance');
    });

    it('should not throw an error if props `routeName` and `router` are passed', () => {
      routerStore = new RouterStore();
      router.addNode('home', '/home');
      router.setOption('defaultRoute', 'home');
      router.start();
      const renderTreeFn = () => shallow(
        <BaseLink routeName={'home'} router={router}/>
      );
      expect(renderTreeFn).to.not.throw();
    });

    it('should not throw an error if props `routeName` and `routerStore` defined', () => {
      router.addNode('home', '/home');
      router.setOption('defaultRoute', 'home');
      router.start();
      const renderTreeFn = () => shallow(
        <BaseLink routeName={'home'} routerStore={routerStore}/>
      );
      expect(renderTreeFn).to.not.throw();
    });

    it('should throw an error if props passed: `router/routerStore` && `routeName` but `browserPlugin` is not used', () => {
      router = createRouter();
      const renderTreeFn = () => shallow(
        <BaseLink routeName={'home'} router={router} />
      );
      expect(renderTreeFn).to.throw('[react-mobx-router5][BaseLink] missing browser plugin, href might build incorrectly');
    });

  });

  context('Hyperlink', function() {
    it('should render an hyperlink element', () => {
      router.addNode('home', '/home');
      router.setOption('defaultRoute', 'home');
      router.start();
      const output = shallow(
        <BaseLink router={router} routeName={'home'}/>
      );
      expect(output.find('a')).to.have.attr('href', '/home');
    });

    it('should not have an active className', () => {
      router.addNode('home', '/home');
      router.setOption('defaultRoute', 'home');
      router.start();
      const output = shallow(
        <BaseLink router={router} routeName={'home'} className="just-a-class-name" />
      );
      expect(output.find('a')).to.have.attr('class', 'just-a-class-name');
    });
  });

});
<commit_msg>test(BaseLink): Add case for BaseLink (do not throw when only onClick is passed)<commit_after>import React from 'react';
import {expect} from 'chai';
import {mobxPlugin, RouterStore} from 'mobx-router5';
import {createTestRouter} from './utils/test-utils';
import {shallow} from 'enzyme';
import {createRouter} from 'router5';
import BaseLink from '../src/modules/BaseLink';

describe('BaseLink component', () => {
  let router;
  let routerStore;

  beforeEach(() => {
    routerStore = new RouterStore();
    router = createTestRouter();
    router.usePlugin(mobxPlugin(routerStore));
  });

  context('Exceptions', function() {
    it('should not throw an error if `routeName`, `router` and `onClick` props are missing', () => {
      const renderTreeFn = () => shallow(
        <BaseLink />
      );
      expect(renderTreeFn).to.not.throw();
    });

    it('should not throw an error if only `onClick` prop is passed', () => {
      function cb(e){
        e.preventDefault();
        return null;
      }
      const renderTreeFn = () => shallow(
        <BaseLink onClick={cb}/>
      );
      expect(renderTreeFn).to.not.throw();
    });

    it('should throw an error if prop `routeName` passed but not props: `router` or `routerStore`', () => {
      router = createRouter();
      router.addNode('home', '/home');
      router.setOption('defaultRoute', 'home');
      router.start();
      const renderTreeFn = () => shallow(
        <BaseLink routeName={'home'}/>
      );
      expect(renderTreeFn).to.throw('[react-mobx-router5][BaseLink] missing router instance');
    });

    it('should not throw an error if props `routeName` and `router` are passed', () => {
      routerStore = new RouterStore();
      router.addNode('home', '/home');
      router.setOption('defaultRoute', 'home');
      router.start();
      const renderTreeFn = () => shallow(
        <BaseLink routeName={'home'} router={router}/>
      );
      expect(renderTreeFn).to.not.throw();
    });

    it('should not throw an error if props `routeName` and `routerStore` defined', () => {
      router.addNode('home', '/home');
      router.setOption('defaultRoute', 'home');
      router.start();
      const renderTreeFn = () => shallow(
        <BaseLink routeName={'home'} routerStore={routerStore}/>
      );
      expect(renderTreeFn).to.not.throw();
    });

    it('should throw an error if props passed: `router/routerStore` && `routeName` but `browserPlugin` is not used', () => {
      router = createRouter();
      const renderTreeFn = () => shallow(
        <BaseLink routeName={'home'} router={router} />
      );
      expect(renderTreeFn).to.throw('[react-mobx-router5][BaseLink] missing browser plugin, href might build incorrectly');
    });

  });

  context('Hyperlink', function() {
    it('should render an hyperlink element', () => {
      router.addNode('home', '/home');
      router.setOption('defaultRoute', 'home');
      router.start();
      const output = shallow(
        <BaseLink router={router} routeName={'home'}/>
      );
      expect(output.find('a')).to.have.attr('href', '/home');
    });

    it('should not have an active className', () => {
      router.addNode('home', '/home');
      router.setOption('defaultRoute', 'home');
      router.start();
      const output = shallow(
        <BaseLink router={router} routeName={'home'} className="just-a-class-name" />
      );
      expect(output.find('a')).to.have.attr('class', 'just-a-class-name');
    });
  });

});
<|endoftext|>


<commit_before>import React from 'react'
import {
    Route, withRouter, Redirect
} from 'react-router-dom'
import { connect } from 'react-redux'

//STATIC PAGES
import Home from './pages/Landing';
import About from './pages/About';
import Code from './pages/Code';

//
import Register from './pages/Register';
import Login from './pages/Login';
import PasswordReset from './pages/PasswordReset';
import Dashboard from './pages/Dashboard';

const PrivateRoute = ({ component: Component, isAuthenticated, isAllowed, ...rest }) => (
    <Route {...rest} render={props => (
        isAuthenticated
        ? (isAllowed
            ? <Component {...props}/>
            :

Permission Denied
You don't have the rights to access this page.
)
        : (
            <Redirect to={{
                pathname: '/login',
                state: { from: props.location }
            }}/>
        )
    )}/>
);

const ExternalRedirect = ({ redirectTo: redirectTo, ...rest }) => (
    <Route {...rest} render={props => (
        window.location = redirectTo
    )}/>
);

const ExternalRoute = withRouter(ExternalRedirect);
const UserRoute = withRouter(connect((state) => ({isAuthenticated: state.user.authenticated, isAllowed: true}))(PrivateRoute));
const ExecRoute = withRouter(connect((state) => ({isAuthenticated: state.user.authenticated, isAllowed: state.user.tokenData && state.user.tokenData.roles.contains("exec")}))(PrivateRoute));

const Routes = () => (

      {/*Public Routes*/}
      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/code" component={Code}/>
      <Route path="/register" component={Register}/>
      <Route path="/login" component={Login}/>
      <Route path="/reset/:reset_token?" component={PasswordReset}/>

      {/*User Routes*/}
      <UserRoute path="/dashboard" component={Dashboard}/>
      {/*Exec Routes*/}
      <ExecRoute path="/exec/dashboard" component={Dashboard}/>

      {/*Offsite Redirects*/}
      <ExternalRoute path="/why-go-to-a-hackathon" redirectTo="https://medium.com/@BoilerMake/why-you-should-go-to-a-hackathon-2d4ede475c9"/>

);

export default Routes;
<commit_msg>redirect pt2<commit_after>import React from 'react'
import {
    Route, withRouter, Redirect
} from 'react-router-dom'
import { connect } from 'react-redux'

//STATIC PAGES
import Home from './pages/Landing';
import About from './pages/About';
import Code from './pages/Code';

//
import Register from './pages/Register';
import Login from './pages/Login';
import PasswordReset from './pages/PasswordReset';
import Dashboard from './pages/Dashboard';

const PrivateRoute = ({ component: Component, isAuthenticated, isAllowed, ...rest }) => (
    <Route {...rest} render={props => (
        isAuthenticated
        ? (isAllowed
            ? <Component {...props}/>
            :

Permission Denied
You don't have the rights to access this page.
)
        : (
            <Redirect to={{
                pathname: '/login',
                state: { from: props.location }
            }}/>
        )
    )}/>
);

const ExternalRedirect = ({ redirectTo, ...rest }) => (
    <Route {...rest} render={() => {
        window.location = redirectTo;
        return null;
    }}/>
);

const ExternalRoute = withRouter(ExternalRedirect);
const UserRoute = withRouter(connect((state) => ({isAuthenticated: state.user.authenticated, isAllowed: true}))(PrivateRoute));
const ExecRoute = withRouter(connect((state) => ({isAuthenticated: state.user.authenticated, isAllowed: state.user.tokenData && state.user.tokenData.roles.contains("exec")}))(PrivateRoute));

const Routes = () => (

      {/*Public Routes*/}
      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/code" component={Code}/>
      <Route path="/register" component={Register}/>
      <Route path="/login" component={Login}/>
      <Route path="/reset/:reset_token?" component={PasswordReset}/>

      {/*User Routes*/}
      <UserRoute path="/dashboard" component={Dashboard}/>
      {/*Exec Routes*/}
      <ExecRoute path="/exec/dashboard" component={Dashboard}/>

      {/*Offsite Redirects*/}
      <ExternalRoute path="/why-go-to-a-hackathon" redirectTo="https://medium.com/@BoilerMake/why-you-should-go-to-a-hackathon-2d4ede475c9"/>

);

export default Routes;
<|endoftext|>


<commit_before>// Copyright 2016, EMC Inc.

'use strict';

var injector = require('../../../index.js').injector;
var controller = injector.get('Http.Services.Swagger').controller;
var addLinks = injector.get('Http.Services.Swagger').addLinksHeader;
var nodes = injector.get('Http.Services.Api.Nodes');
var waterline = injector.get('Services.Waterline');
var _ = injector.get('_'); // jshint ignore:line
var constants = injector.get('Constants');
var Errors = injector.get('Errors');

var nodesGetAll = controller(function(req, res) {
    var options = {
        skip: req.swagger.query.$skip,
        limit: req.swagger.query.$top
    };
    return nodes.getAllNodes(req.query, options)
    .tap(function(nodes) {
        return addLinks(req, res, 'nodes', req.query);
    });
});

var nodesPost = controller({success: 201}, function(req) {
    return nodes.postNode(req.body);
});

var nodesGetById = controller(function(req) {
    return nodes.getNodeById(req.swagger.params.identifier.value);
});

var nodesPatchById = controller(function(req) {
    return nodes.patchNodeById(req.swagger.params.identifier.value, req.body);
});

var nodesDelById = controller({success: 204}, function(req) {
    return nodes.delNodeById(req.swagger.params.identifier.value);
});

var nodesGetSshById = controller(function(req) {
    return nodes.getNodeSshById(req.swagger.params.identifier.value);
});

var nodesPostSshById = controller({success: 201}, function(req) {
    return nodes.postNodeSshById(req.swagger.params.identifier.value, req.body);
});

var nodesGetCatalogById = controller(function(req) {
    return nodes.getNodeCatalogById(req.swagger.params.identifier.value, req.query);
});

var nodesGetCatalogSourceById = controller(function(req) {
    return nodes.getNodeCatalogSourceById(req.swagger.params.identifier.value,
                                          req.swagger.params.source.value);
});

var nodesGetPollersById = controller(function(req) {
    return nodes.getPollersByNodeId(req.swagger.params.identifier.value);
});

var nodesGetWorkflowById = controller(function(req) {
    return nodes.getNodeWorkflowById(req.swagger.params.identifier.raw);
});

var nodesPostWorkflowById = controller({success: 201}, function(req) {
    var config = _.defaults(
        req.swagger.params.name.value ? { name: req.swagger.params.name.value } : {},
        req.swagger.params.body.value || {}
    );
    return nodes.setNodeWorkflow(config, req.swagger.params.identifier.value);
});

var nodesWorkflowActionById = controller({success: 202}, function(req, res) {
    var command = req.body.command;

    var actionFunctions = {
        cancel: function() {
            return nodes.delActiveWorkflowById(req.swagger.params.identifier.value)
                .then(function(graph) {
                    res.setHeader('Location', '/api/2.0/workflows/' + graph.id);
                    return graph;
                });
        }
    };

    if(!_(actionFunctions).has(command)) {
        throw new Errors.BadRequestError(
            command + ' is not a valid workflow action'
        );
    }
    return actionFunctions[command]();

});

var nodesGetTagsById = controller(function(req) {
    return nodes.getTagsById(req.swagger.params.identifier.value);
});

var nodesDelTagById = controller({success: 204}, function(req) {
    return nodes.removeTagsById(req.swagger.params.identifier.value,
                                req.swagger.params.tagName.value);
});

var nodesPatchTagById = controller(function(req) {
    return nodes.addTagsById(req.swagger.params.identifier.value,
                             req.body.tags);
});

var nodesMasterDelTagById = controller({success: 204}, function(req) {
    return nodes.masterDelTagById(req.swagger.params.tagName.value);
});

var nodesDelRelations = controller(function(req) {
   return nodes.editNodeRelations(req.swagger.params.identifier.value,
           req.body,
           nodes._removeRelation
        );
});

var nodesAddRelations = controller(function(req) {
   return nodes.editNodeRelations(req.swagger.params.identifier.value,
           req.body,
           nodes._addRelation
        );
});

var nodesGetRelations = controller(function(req) {
   return nodes.getNodeRelations(req.swagger.params.identifier.value);
});

var nodesGetObmsByNodeId = controller(function(req) {
    return nodes.getObmsByNodeId(req.swagger.params.identifier.value);
});

var nodesPutObmsByNodeId = controller({success: 201}, function(req) {
    return nodes.putObmsByNodeId(req.swagger.params.identifier.value, req.body);
});

module.exports = {
    nodesGetAll: nodesGetAll,
    nodesPost: nodesPost,
    nodesGetById: nodesGetById,
    nodesPatchById: nodesPatchById,
    nodesDelById: nodesDelById,
    nodesGetSshById: nodesGetSshById,
    nodesPostSshById: nodesPostSshById,
    nodesGetCatalogById: nodesGetCatalogById,
    nodesGetCatalogSourceById: nodesGetCatalogSourceById,
    nodesGetPollersById: nodesGetPollersById,
    nodesGetWorkflowById: nodesGetWorkflowById,
    nodesPostWorkflowById: nodesPostWorkflowById,
    nodesWorkflowActionById: nodesWorkflowActionById,
    nodesGetTagsById: nodesGetTagsById,
    nodesDelTagById: nodesDelTagById,
    nodesPatchTagById: nodesPatchTagById,
    nodesMasterDelTagById: nodesMasterDelTagById,
    nodesDelRelations: nodesDelRelations,
    nodesAddRelations: nodesAddRelations,
    nodesGetRelations: nodesGetRelations,
    nodesGetObmsByNodeId: nodesGetObmsByNodeId,
    nodesPutObmsByNodeId: nodesPutObmsByNodeId
};
<commit_msg>Added query code back. Changed node arg.<commit_after>// Copyright 2016, EMC Inc.

'use strict';

var injector = require('../../../index.js').injector;
var controller = injector.get('Http.Services.Swagger').controller;
var addLinks = injector.get('Http.Services.Swagger').addLinksHeader;
var nodes = injector.get('Http.Services.Api.Nodes');
var waterline = injector.get('Services.Waterline');
var _ = injector.get('_'); // jshint ignore:line
var constants = injector.get('Constants');
var Errors = injector.get('Errors');

var nodesGetAll = controller(function(req, res) {
    var options = {
        skip: req.swagger.query.$skip,
        limit: req.swagger.query.$top
    };
    return nodes.getAllNodes(req.query, options)
    .tap(function(nodes) {
        return addLinks(req, res, 'nodes', req.query);
    });
});

var nodesPost = controller({success: 201}, function(req) {
    return nodes.postNode(req.body);
});

var nodesGetById = controller(function(req) {
    return nodes.getNodeById(req.swagger.params.identifier.value);
});

var nodesPatchById = controller(function(req) {
    return nodes.patchNodeById(req.swagger.params.identifier.value, req.body);
});

var nodesDelById = controller({success: 204}, function(req) {
    return nodes.delNodeById(req.swagger.params.identifier.value);
});

var nodesGetSshById = controller(function(req) {
    return nodes.getNodeSshById(req.swagger.params.identifier.value);
});

var nodesPostSshById = controller({success: 201}, function(req) {
    return nodes.postNodeSshById(req.swagger.params.identifier.value, req.body);
});

var nodesGetCatalogById = controller(function(req) {
    return nodes.getNodeCatalogById(req.swagger.params.identifier.value, req.query);
});

var nodesGetCatalogSourceById = controller(function(req) {
    return nodes.getNodeCatalogSourceById(req.swagger.params.identifier.value,
                                          req.swagger.params.source.value);
});

var nodesGetPollersById = controller(function(req) {
    return nodes.getPollersByNodeId(req.swagger.params.identifier.value);
});

var nodesGetWorkflowById = controller(function(req) {
    var newQuery;
    var swaggerQuery = req.swagger.query || [];
    if (_(swaggerQuery).has('active')) {
        if (swaggerQuery.active) {
            newQuery = ({
                _status: constants.Task.ActiveStates
            });
        } else {
            newQuery = ({
                _status: {'!': constants.Task.ActiveStates}
            });
        }
        newQuery = _.merge({}, newQuery, req.query);
    } else {
        newQuery = req.query;
    }

    return nodes.getNodeWorkflowById(req.swagger.params.identifier.value.newQuery);
});

var nodesPostWorkflowById = controller({success: 201}, function(req) {
    var config = _.defaults(
        req.swagger.params.name.value ? { name: req.swagger.params.name.value } : {},
        req.swagger.params.body.value || {}
    );
    return nodes.setNodeWorkflow(config, req.swagger.params.identifier.value);
});

var nodesWorkflowActionById = controller({success: 202}, function(req, res) {
    var command = req.body.command;

    var actionFunctions = {
        cancel: function() {
            return nodes.delActiveWorkflowById(req.swagger.params.identifier.value)
                .then(function(graph) {
                    res.setHeader('Location', '/api/2.0/workflows/' + graph.id);
                    return graph;
                });
        }
    };

    if(!_(actionFunctions).has(command)) {
        throw new Errors.BadRequestError(
            command + ' is not a valid workflow action'
        );
    }
    return actionFunctions[command]();

});

var nodesGetTagsById = controller(function(req) {
    return nodes.getTagsById(req.swagger.params.identifier.value);
});

var nodesDelTagById = controller({success: 204}, function(req) {
    return nodes.removeTagsById(req.swagger.params.identifier.value,
                                req.swagger.params.tagName.value);
});

var nodesPatchTagById = controller(function(req) {
    return nodes.addTagsById(req.swagger.params.identifier.value,
                             req.body.tags);
});

var nodesMasterDelTagById = controller({success: 204}, function(req) {
    return nodes.masterDelTagById(req.swagger.params.tagName.value);
});

var nodesDelRelations = controller(function(req) {
   return nodes.editNodeRelations(req.swagger.params.identifier.value,
           req.body,
           nodes._removeRelation
        );
});

var nodesAddRelations = controller(function(req) {
   return nodes.editNodeRelations(req.swagger.params.identifier.value,
           req.body,
           nodes._addRelation
        );
});

var nodesGetRelations = controller(function(req) {
   return nodes.getNodeRelations(req.swagger.params.identifier.value);
});

var nodesGetObmsByNodeId = controller(function(req) {
    return nodes.getObmsByNodeId(req.swagger.params.identifier.value);
});

var nodesPutObmsByNodeId = controller({success: 201}, function(req) {
    return nodes.putObmsByNodeId(req.swagger.params.identifier.value, req.body);
});

module.exports = {
    nodesGetAll: nodesGetAll,
    nodesPost: nodesPost,
    nodesGetById: nodesGetById,
    nodesPatchById: nodesPatchById,
    nodesDelById: nodesDelById,
    nodesGetSshById: nodesGetSshById,
    nodesPostSshById: nodesPostSshById,
    nodesGetCatalogById: nodesGetCatalogById,
    nodesGetCatalogSourceById: nodesGetCatalogSourceById,
    nodesGetPollersById: nodesGetPollersById,
    nodesGetWorkflowById: nodesGetWorkflowById,
    nodesPostWorkflowById: nodesPostWorkflowById,
    nodesWorkflowActionById: nodesWorkflowActionById,
    nodesGetTagsById: nodesGetTagsById,
    nodesDelTagById: nodesDelTagById,
    nodesPatchTagById: nodesPatchTagById,
    nodesMasterDelTagById: nodesMasterDelTagById,
    nodesDelRelations: nodesDelRelations,
    nodesAddRelations: nodesAddRelations,
    nodesGetRelations: nodesGetRelations,
    nodesGetObmsByNodeId: nodesGetObmsByNodeId,
    nodesPutObmsByNodeId: nodesPutObmsByNodeId
};
<|endoftext|>


<commit_before>import _ from 'lodash';
import Promise from 'bluebird';
import Joi from 'joi';
import type from 'type-of';
import CustomError from 'customerror';
import UUIDType from './datatypes/UUID';
import StringType from './datatypes/String';
import EnumType from './datatypes/Enum';
import NumberType from './datatypes/Number';
import FloatType from './datatypes/Float';
import IntegerType from './datatypes/Integer';
import DateType from './datatypes/Date';

const datatypes = {
  date: DateType,
  enum: EnumType,
  float: FloatType,
  integer: IntegerType,
  number: NumberType,
  string: StringType,
  uuid: UUIDType,
};

class Schema {

  /**
   * Creates a new Schema based on the supplied definition object.
   * @param {Object} definition a schema definition object.
   * @constructor
   * @throws {TypeError} if definition object is invalid or unspecified.
   */
  constructor(definition: Object) {
    // make sure definition is plain object
    if (!_.isPlainObject(definition)) {
      throw new TypeError(`Invalid definition argument; expected plain object, received ${type(definition)}`);
    }

    this._keys = {};
    this._primaryKey = {};
    this._indexKeys = {};
    this._uniqueKeys = {};
    this._joi = null;

    // eat your own dog food
    this.extend(definition);
  }

  /**
   * Extends schema with the supplied definition object.
   * @param {Object} definition a schema definition object.
   * @throws {TypeError} if definition object is invalid or unspecified.
   * @returns {Schema} this schema to allow method chaining.
   */
  extend(definition: Object): void {
    // make sure definition is plain object
    if (!_.isPlainObject(definition)) {
      throw new TypeError(`Invalid definition argument; expected plain object, received ${type(definition)}`);
    }

    // update _keys object
    _.forOwn(definition, (props, key) => {
      // make sure datatype is valid
      if (!datatypes.hasOwnProperty(props.type)) {
        throw new TypeError(`Unknown datatype ${props.type}`);
      }

      // create new datatype
      const dt = new datatypes[props.type];

      // set datatype properties
      _.forOwn(_.omit(props, 'type'), (v, k) => {
        dt[k] = v;
      });

      // update definition object
      _.set(this._keys, key, dt);
    });

    // invalidate joi cache
    this._joi = null;

    // allow method chaining
    return this;
  }

  /**
   * Creates the specified index in schema.
   * @param {Object} keys an object of key-value pairs, where value describes the related type of the index, i.e. 1 for ASC, -1 for DESC.
   * @param {Object} [options] index options.
   * @param {string} [options.name] the name of the index.
   * @param {string} [options.type="index"] the type of the index, i.e. "primary", "unique" or "index".
   * @throws {TypeError} if arguments are invalid.
   * @returns {Schema} this schema to allow method chaining.
   */
  index(keys: Object, options: ?{name: ?string, type: ?string}): void {
    // make sure keys is plain object
    if (!_.isPlainObject(keys)) {
      throw new TypeError(`Invalid keys argument; expected plain object, received ${type(keys)}`);
    }

    // make sure keys is not empty
    if (_.isEmpty(keys)) {
      throw new TypeError(`Invalid keys argument; object must not be empty`);
    }

    // iterate keys contents
    _.forOwn(keys, (order, key) => {
      // make sure keys exist in schema definition
      if (!this.has(key)) {
        throw new TypeError(`Key "${key}" not found in schema definition`);
      }

      // make sure order is number
      if (!_.isNumber(order)) {
        throw new TypeError(`Invalid order for key "${key}"; expected number, received ${type(order)}`);
      }

      // make sure order is specifically one of 1 or -1
      if (order !== 1 && order !== -1) {
        throw new TypeError(`Invalid order for key "${key}"; expected 1 (i.e. ASC) or -1 (i.e. DESC), received ${order}`);
      }
    });

    // handle options
    options = _.defaults(options, {type: 'index'});

    switch (options.type) {
    case 'index':
      if (_.isNil(options.name)) {
        options.name = 'idx' + (_.size(this._indexKeys) + 1);
      }

      // make sure index is not already defined in unique indices
      if (_.has(this._uniqueKeys, options.name)) {
        throw new Error(`Index ${options.name} is already set as unique index in schema`);
      }

      this._indexKeys[options.name] = keys;
      break;

    case 'unique':
      if (_.isNil(options.name)) {
        options.name = 'uidx' + (_.size(this._uniqueKeys) + 1);
      }

      // make sure index is not already defined in plain indices
      if (_.has(this._indexKeys, options.name)) {
        throw new Error(`Unique index ${options.name} is already set as plain index in schema`);
      }

      this._uniqueKeys[options.name] = keys;
      break;

    case 'primary':
      this._primaryKey = keys;
      break;

    default:
      throw new TypeError(`Invalid type option; expected "index", "unique" or "primary", received "${options.type}"`);
    }

    // allow method chaining
    return this;
  }

  /**
   * Indicates whether the specified key exists in schema.
   * @param {string} key the name of the key.
   * @returns {boolean}
   */
  has(key: string): boolean {
    return _.has(this._keys, key);
  }

  /**
   * Returns an array of keys specified in this schema.
   * @return {Array<string>}
   */
  keys(): Array<string> {
    return _.keys(this._keys);
  }

  /**
   * Returns an array of keys that compose the primary key.
   * @return {Array<string>}
   */
  primaryKey(): Array<string> {
    return _.keys(this._primaryKey);
  }

  /**
   * Returns an array of keys that compose the designate unique key.
   * @param {string} name: the name of the unique key index.
   * @return {Array<string>}
   */
  uniqueKey(name: string): Array<string> {
    const obj = this._uniqueKeys[name];

    if (obj === undefined) {
      throw new Error(`Unknow unique key "${name}"; does not exist in schema`);
    }

    return _.keys(obj);
  }

  /**
   * Returns an array of keys that compose the designate index key.
   * @param {string} name: the name of the index key index.
   * @return {Array<string>}
   */
  indexKey(name: string): Array<string> {
    const obj = this._indexKeys[name];

    if (obj === undefined) {
      throw new Error(`Unknow index key "${name}"; does not exist in schema`);
    }

    return _.keys(obj);
  }

  /**
   * Indicates whether the specified key is automatically incremented.
   * @param {string} key the name of the key.
   * @returns {boolean}
   * @throws {Error} If key does not exist in schema.
   */
  isAutoInc(key: string): boolean {
    const dt = _.get(this._keys, key);

    // make sure key exists
    if (_.isUndefined(dt)) {
      throw new Error(`Key "${key}" not found in schema definition`);
    }

    return dt.props.autoinc === true;
  }

  /**
   * Indicates whether the specified key(s) compose the primary key of this schema.
   * Primary keys may be compound, i.e. composed of multiple keys. Hence the acceptance of multiple params in this function.
   * @param {...string} keys the name of the keys.
   * @returns {boolean}
   */
  isPrimaryKey(...keys): boolean {
    return _.chain(this._primaryKey).keys().xor(keys).value().length === 0;
  }

  /*
   * Indicates whether the specified keys(s) represent a unique key in this schema.
   * Unique keys may be compound, i.e. composed of multiple keys, hence the acceptance of multiple params.
   * @param {...string} keys the name of the keys.
   * @returns {boolean}
   */
  isUniqueKey(...keys): boolean {
    return _.some(this._uniqueKeys, (obj) => {
      return _.xor(_.keys(obj), keys).length === 0;
    });
  }

  /*
   * Indicates whether the specified key(s) represent an index key.
   * Index keys may be compound, i.e. composed of multiple keys, hence the acceptance of multiple params.
   * @param {...string} keys the name of the keys.
   * @returns {boolean}
   */
  isIndexKey(...keys): boolean {
    return _.some(this._indexKeys, function (obj) {
      return _.xor(_.keys(obj), keys).length === 0;
    });
  }

  /**
   * Indicates whether the table has an atomic (i.e. consisted of a single key) auto-incremented primary key.
   * This method will always return false until database is ready.
   * @returns {boolean}
   */
  hasAutoIncPrimaryKey() {
    const keys = _.keys(this._primaryKey);
    return keys.length === 1 && this.isAutoInc(keys[0]);
  }

  /**
   * Validates the designated record against this schema.
   * @param {Object} record the record to validate.
   * @param {Function<Error, Object>} callback an optional callback function.
   * @return {Promise<Object>}
   */
  validate(record: Object, callback: ?Function): Promise {
    // make sure joi exists
    if (!this._joi) {
      this._joi = this.toJoi(); // cache to instance property
    }

    return new Promise((resolve, reject) => {
      Joi.validate(record, this._joi, {convert: false}, (err, value) => {
        if (err) return reject(new CustomError(err, 'ValidationError'));
        resolve(value);
      });
    }).nodeify(callback);
  }

  toMetaData(): Object {
    return this._keys;
  }

  static fromMetaData(obj: Object): Schema {
    return new Schema(obj);
  }

  toJoi() {
    return Joi.object()
      .strict(true)
      .keys(_.mapValues(this._keys, (datatype) => datatype.toJoi()));
  }

  toJSON(): Object {
    return _.mapValues(this._keys, (datatype) => datatype.toJSON());
  }

}

export default Schema;
<commit_msg>Tell typeflow that #extend() and #index() return Schmema<commit_after>import _ from 'lodash';
import Promise from 'bluebird';
import Joi from 'joi';
import type from 'type-of';
import CustomError from 'customerror';
import UUIDType from './datatypes/UUID';
import StringType from './datatypes/String';
import EnumType from './datatypes/Enum';
import NumberType from './datatypes/Number';
import FloatType from './datatypes/Float';
import IntegerType from './datatypes/Integer';
import DateType from './datatypes/Date';

const datatypes = {
  date: DateType,
  enum: EnumType,
  float: FloatType,
  integer: IntegerType,
  number: NumberType,
  string: StringType,
  uuid: UUIDType,
};

class Schema {

  /**
   * Creates a new Schema based on the supplied definition object.
   * @param {Object} definition a schema definition object.
   * @constructor
   * @throws {TypeError} if definition object is invalid or unspecified.
   */
  constructor(definition: Object) {
    // make sure definition is plain object
    if (!_.isPlainObject(definition)) {
      throw new TypeError(`Invalid definition argument; expected plain object, received ${type(definition)}`);
    }

    this._keys = {};
    this._primaryKey = {};
    this._indexKeys = {};
    this._uniqueKeys = {};
    this._joi = null;

    // eat your own dog food
    this.extend(definition);
  }

  /**
   * Extends schema with the supplied definition object.
   * @param {Object} definition a schema definition object.
   * @throws {TypeError} if definition object is invalid or unspecified.
   * @returns {Schema} this schema to allow method chaining.
   */
  extend(definition: Object): Schema {
    // make sure definition is plain object
    if (!_.isPlainObject(definition)) {
      throw new TypeError(`Invalid definition argument; expected plain object, received ${type(definition)}`);
    }

    // update _keys object
    _.forOwn(definition, (props, key) => {
      // make sure datatype is valid
      if (!datatypes.hasOwnProperty(props.type)) {
        throw new TypeError(`Unknown datatype ${props.type}`);
      }

      // create new datatype
      const dt = new datatypes[props.type];

      // set datatype properties
      _.forOwn(_.omit(props, 'type'), (v, k) => {
        dt[k] = v;
      });

      // update definition object
      _.set(this._keys, key, dt);
    });

    // invalidate joi cache
    this._joi = null;

    // allow method chaining
    return this;
  }

  /**
   * Creates the specified index in schema.
   * @param {Object} keys an object of key-value pairs, where value describes the related type of the index, i.e. 1 for ASC, -1 for DESC.
   * @param {Object} [options] index options.
   * @param {string} [options.name] the name of the index.
   * @param {string} [options.type="index"] the type of the index, i.e. "primary", "unique" or "index".
   * @throws {TypeError} if arguments are invalid.
   * @returns {Schema} this schema to allow method chaining.
   */
  index(keys: Object, options: ?{name: ?string, type: ?string}): Schema {
    // make sure keys is plain object
    if (!_.isPlainObject(keys)) {
      throw new TypeError(`Invalid keys argument; expected plain object, received ${type(keys)}`);
    }

    // make sure keys is not empty
    if (_.isEmpty(keys)) {
      throw new TypeError(`Invalid keys argument; object must not be empty`);
    }

    // iterate keys contents
    _.forOwn(keys, (order, key) => {
      // make sure keys exist in schema definition
      if (!this.has(key)) {
        throw new TypeError(`Key "${key}" not found in schema definition`);
      }

      // make sure order is number
      if (!_.isNumber(order)) {
        throw new TypeError(`Invalid order for key "${key}"; expected number, received ${type(order)}`);
      }

      // make sure order is specifically one of 1 or -1
      if (order !== 1 && order !== -1) {
        throw new TypeError(`Invalid order for key "${key}"; expected 1 (i.e. ASC) or -1 (i.e. DESC), received ${order}`);
      }
    });

    // handle options
    options = _.defaults(options, {type: 'index'});

    switch (options.type) {
    case 'index':
      if (_.isNil(options.name)) {
        options.name = 'idx' + (_.size(this._indexKeys) + 1);
      }

      // make sure index is not already defined in unique indices
      if (_.has(this._uniqueKeys, options.name)) {
        throw new Error(`Index ${options.name} is already set as unique index in schema`);
      }

      this._indexKeys[options.name] = keys;
      break;

    case 'unique':
      if (_.isNil(options.name)) {
        options.name = 'uidx' + (_.size(this._uniqueKeys) + 1);
      }

      // make sure index is not already defined in plain indices
      if (_.has(this._indexKeys, options.name)) {
        throw new Error(`Unique index ${options.name} is already set as plain index in schema`);
      }

      this._uniqueKeys[options.name] = keys;
      break;

    case 'primary':
      this._primaryKey = keys;
      break;

    default:
      throw new TypeError(`Invalid type option; expected "index", "unique" or "primary", received "${options.type}"`);
    }

    // allow method chaining
    return this;
  }

  /**
   * Indicates whether the specified key exists in schema.
   * @param {string} key the name of the key.
   * @returns {boolean}
   */
  has(key: string): boolean {
    return _.has(this._keys, key);
  }

  /**
   * Returns an array of keys specified in this schema.
   * @return {Array<string>}
   */
  keys(): Array<string> {
    return _.keys(this._keys);
  }

  /**
   * Returns an array of keys that compose the primary key.
   * @return {Array<string>}
   */
  primaryKey(): Array<string> {
    return _.keys(this._primaryKey);
  }

  /**
   * Returns an array of keys that compose the designate unique key.
   * @param {string} name: the name of the unique key index.
   * @return {Array<string>}
   */
  uniqueKey(name: string): Array<string> {
    const obj = this._uniqueKeys[name];

    if (obj === undefined) {
      throw new Error(`Unknow unique key "${name}"; does not exist in schema`);
    }

    return _.keys(obj);
  }

  /**
   * Returns an array of keys that compose the designate index key.
   * @param {string} name: the name of the index key index.
   * @return {Array<string>}
   */
  indexKey(name: string): Array<string> {
    const obj = this._indexKeys[name];

    if (obj === undefined) {
      throw new Error(`Unknow index key "${name}"; does not exist in schema`);
    }

    return _.keys(obj);
  }

  /**
   * Indicates whether the specified key is automatically incremented.
   * @param {string} key the name of the key.
   * @returns {boolean}
   * @throws {Error} If key does not exist in schema.
   */
  isAutoInc(key: string): boolean {
    const dt = _.get(this._keys, key);

    // make sure key exists
    if (_.isUndefined(dt)) {
      throw new Error(`Key "${key}" not found in schema definition`);
    }

    return dt.props.autoinc === true;
  }

  /**
   * Indicates whether the specified key(s) compose the primary key of this schema.
   * Primary keys may be compound, i.e. composed of multiple keys. Hence the acceptance of multiple params in this function.
   * @param {...string} keys the name of the keys.
   * @returns {boolean}
   */
  isPrimaryKey(...keys): boolean {
    return _.chain(this._primaryKey).keys().xor(keys).value().length === 0;
  }

  /*
   * Indicates whether the specified keys(s) represent a unique key in this schema.
   * Unique keys may be compound, i.e. composed of multiple keys, hence the acceptance of multiple params.
   * @param {...string} keys the name of the keys.
   * @returns {boolean}
   */
  isUniqueKey(...keys): boolean {
    return _.some(this._uniqueKeys, (obj) => {
      return _.xor(_.keys(obj), keys).length === 0;
    });
  }

  /*
   * Indicates whether the specified key(s) represent an index key.
   * Index keys may be compound, i.e. composed of multiple keys, hence the acceptance of multiple params.
   * @param {...string} keys the name of the keys.
   * @returns {boolean}
   */
  isIndexKey(...keys): boolean {
    return _.some(this._indexKeys, function (obj) {
      return _.xor(_.keys(obj), keys).length === 0;
    });
  }

  /**
   * Indicates whether the table has an atomic (i.e. consisted of a single key) auto-incremented primary key.
   * This method will always return false until database is ready.
   * @returns {boolean}
   */
  hasAutoIncPrimaryKey() {
    const keys = _.keys(this._primaryKey);
    return keys.length === 1 && this.isAutoInc(keys[0]);
  }

  /**
   * Validates the designated record against this schema.
   * @param {Object} record the record to validate.
   * @param {Function<Error, Object>} callback an optional callback function.
   * @return {Promise<Object>}
   */
  validate(record: Object, callback: ?Function): Promise {
    // make sure joi exists
    if (!this._joi) {
      this._joi = this.toJoi(); // cache to instance property
    }

    return new Promise((resolve, reject) => {
      Joi.validate(record, this._joi, {convert: false}, (err, value) => {
        if (err) return reject(new CustomError(err, 'ValidationError'));
        resolve(value);
      });
    }).nodeify(callback);
  }

  toMetaData(): Object {
    return this._keys;
  }

  static fromMetaData(obj: Object): Schema {
    return new Schema(obj);
  }

  toJoi() {
    return Joi.object()
      .strict(true)
      .keys(_.mapValues(this._keys, (datatype) => datatype.toJoi()));
  }

  toJSON(): Object {
    return _.mapValues(this._keys, (datatype) => datatype.toJSON());
  }

}

export default Schema;
<|endoftext|>


<commit_before>var Database = (function() {
	var adOpenForwardOnly = 0;
	var adOpenStatic = 3;
	var adLockReadOnly = 1;
	var adLockOptimistic = 3;
	var adVarChar = 200;
	var adParamInput = 1;
    var ObjectCreator = GetObjectCreator();

	return function(connectionString) {
		function __db(connectionString) {
			this.ConnectionString = connectionString;
		}

		__db.prototype.Query = function(sql) {
			var queryParameters = getQueryParameters(arguments);
			var command = createAdodbCommand(sql, queryParameters, this.ConnectionString);
			return createRecordSet(command, true);
		}

		__db.prototype.CreateUpdatableRecordSet = function(sql) {
			var queryParameters = getQueryParameters(arguments);
			var command = createAdodbCommand(sql, queryParameters, this.ConnectionString);
			return createRecordSet(command, false);
		}

		__db.prototype.ExecSql = function(sql){
			var queryParameters = getQueryParameters(arguments);
			var command = createAdodbCommand(sql, queryParameters, this.ConnectionString);
			return command.Execute();
		}

		return new __db(connectionString);
	}

	function getQueryParameters(args) {
		var parameters = new Array();

		for(var i = 1; i < args.length; i++) {
			parameters[i-1] = args[i];
		}

		return parameters;
	}

	function createAdodbCommand(sql, parameters, connectionString){
		var queryCommand = ObjectCreator.CreateObject('ADODB.Command');
		queryCommand.ActiveConnection = connectionString;
		queryCommand.CommandText = sql;

		setCommandParameters(queryCommand, parameters);

		return queryCommand;
	}

	function setCommandParameters(queryCommand, parameters) {
		var queryParameter;
		for(var i = 0; i < parameters.length; i++){
			queryParameter = queryCommand.CreateParameter('', adVarChar, adParamInput, 8000, parameters[i]);
			queryCommand.Parameters.Append(queryParameter);
		}
	}

	function createRecordSet(command, readOnly) {
		var recordSet = ObjectCreator.CreateObject('ADODB.Recordset');
		configureCursorAndLockType(recordSet, readOnly);
		recordSet.Open(command);
		return recordSet;
	}

	function configureCursorAndLockType(recordSet, readOnly) {
		if(readOnly) {
			recordSet.CursorType = adOpenForwardOnly;
			recordSet.LockType = adLockReadOnly;
		} else {
			recordSet.CursorType = adOpenStatic;
			recordSet.LockType = adLockOptimistic;
		}
	}

    function GetObjectCreator() {
        if(typeof(Server) === 'object') {
            return Server;
        }else{
            return WScript;
        }
    }
})();<commit_msg>Refactored.<commit_after>var Database = (function() {
	var adOpenForwardOnly = 0;
	var adOpenStatic = 3;
	var adLockReadOnly = 1;
	var adLockOptimistic = 3;
	var adVarChar = 200;
	var adParamInput = 1;
    var ObjectCreator = GetObjectCreator();

    function __db(connectionString) {
        this.ConnectionString = connectionString;
    }

    __db.prototype.Query = function(sql) {
        var queryParameters = getQueryParameters(arguments);
        var command = createAdodbCommand(sql, queryParameters, this.ConnectionString);
        return createRecordSet(command, true);
    }

    __db.prototype.CreateUpdatableRecordSet = function(sql) {
        var queryParameters = getQueryParameters(arguments);
        var command = createAdodbCommand(sql, queryParameters, this.ConnectionString);
        return createRecordSet(command, false);
    }

    __db.prototype.ExecSql = function(sql){
        var queryParameters = getQueryParameters(arguments);
        var command = createAdodbCommand(sql, queryParameters, this.ConnectionString);
        return command.Execute();
    }

	function getQueryParameters(args) {
		var parameters = new Array();

		for(var i = 1; i < args.length; i++) {
			parameters[i-1] = args[i];
		}

		return parameters;
	}

	function createAdodbCommand(sql, parameters, connectionString){
		var queryCommand = ObjectCreator.CreateObject('ADODB.Command');
		queryCommand.ActiveConnection = connectionString;
		queryCommand.CommandText = sql;

		setCommandParameters(queryCommand, parameters);

		return queryCommand;
	}

	function setCommandParameters(queryCommand, parameters) {
		var queryParameter;
		for(var i = 0; i < parameters.length; i++){
			queryParameter = queryCommand.CreateParameter('', adVarChar, adParamInput, 8000, parameters[i]);
			queryCommand.Parameters.Append(queryParameter);
		}
	}

	function createRecordSet(command, readOnly) {
		var recordSet = ObjectCreator.CreateObject('ADODB.Recordset');
		configureCursorAndLockType(recordSet, readOnly);
		recordSet.Open(command);
		return recordSet;
	}

	function configureCursorAndLockType(recordSet, readOnly) {
		if(readOnly) {
			recordSet.CursorType = adOpenForwardOnly;
			recordSet.LockType = adLockReadOnly;
		} else {
			recordSet.CursorType = adOpenStatic;
			recordSet.LockType = adLockOptimistic;
		}
	}

    function GetObjectCreator() {
        if(typeof(Server) === 'object') {
            return Server;
        }else{
            return WScript;
        }
    }

    return function(connectionString) {
		return new __db(connectionString);
	}
})();<|endoftext|>


<commit_before>const Plugged = require('plugged')
const path = require('path')
const { inherits } = require('util')
const fs = require('fs')
const debug = require('debug')('sekshi:sekshi')
const logChat = require('debug')('sekshi:chat')
const mongoose = require('mongoose')
const Promise = require('promise')
const { User } = require('./models')
const find = require('array-find')
const unescape = require('ent/decode')

// these are fine-tuned to fit pretty much exactly within plug.dj's rate limiting scheme,
// while also being really simple
const CHAT_INTERVAL = 150
const CHAT_BACKOFF_INC = 60
const CHAT_BACKOFF_MAX = 700

export default class Sekshi extends Plugged {
  constructor(args) {
    super()
    this.invokeLogger(this._log)

    this.options = args
    this.db = mongoose.connect(args.mongo)

    this.modules = {}
    this._availableModules = []
    this.delimiter = args.delimiter || '!'
    this.modulePath = args.modulePath || path.join(__dirname, 'modules')

    this._room = args.room

    this.onMessage = this.onMessage.bind(this)

    this._addUnescapeListeners()

    this._chatQueue = []
    this._lastMessage = 0
    this._chatTimer = false
    this._chatBackoff = CHAT_INTERVAL
  }

  _log(msg, verbosity, color) {
    debug(msg)
  }

  // ugly hack to deal with plug.dj's mildly insane html escaping behaviour
  _addUnescapeListeners() {
    const unescapeUser = user => {
      user.username = unescape(user.username)
      user.blurb && (user.blurb = unescape(user.blurb))
    }

    // these handlers are added before any other handlers,
    // hopefully EventEmitter will always fire them in
    // that same order!
    this.on(this.USER_JOIN, unescapeUser)
    this.on(this.ADVANCE, ({}, { media }) => {
      media.author = unescape(media.author)
      media.title = unescape(media.title)
    })
    this.on(this.CHAT, chat => {
      chat.username = unescape(chat.username)
      chat.message = unescape(chat.message)
    })
    this.on(this.JOINED_ROOM, () => {
      this.state.room.users.forEach(unescapeUser)
    })
  }

  // Override Plugged#getUserByName with a case insensitive version
  getUserByName(username, checkCache = false) {
    username = username.toLowerCase()

    if (username === this.getSelf().username.toLowerCase())
      return this.getSelf()

    let user = find(this.getUsers(), user => user.username.toLowerCase() === username)

    if (checkCache && !user) {
      user = find(this.state.usercache, user => user.username.toLowerCase() === username)
    }

    return user || null
  }

  sendChat(message, autodelete = false) {
    const MAX_LENGTH = 256
    while (message.length >= MAX_LENGTH) {
      this._chatQueue.push([ message.slice(0, MAX_LENGTH), autodelete ])
      message = message.slice(MAX_LENGTH)
    }
    this._chatQueue.push([ message, autodelete ])
    if (!this._chatTimer) {
      this._processChatQueue()
    }
  }

  _processChatQueue() {
    if (this._chatQueue.length > 0) {
      if (this._lastMessage + this._chatBackoff <= Date.now()) {
        Plugged.prototype.sendChat.call(this, ...this._chatQueue.shift())
        this._lastMessage = Date.now()
        if (this._chatBackoff < CHAT_BACKOFF_MAX) {
          this._chatBackoff += CHAT_BACKOFF_INC
        }
      }
      setTimeout(() => this._processChatQueue(), this._chatBackoff + 1)
      this._chatTimer = true
    }

    if (this._chatQueue.length === 0) {
      this._chatTimer = false
      this._chatBackoff = CHAT_INTERVAL
    }
  }

  start(creds, cb) {
    this.login(creds)

    this.once(this.LOGIN_SUCCESS, () => {
      this.cacheChat(true)
      this.connect(this.options.room)
      this.loadModules()
    })

    this.on(this.CHAT, this.onMessage)

    this.once(this.JOINED_ROOM, err => {
      if (!err) {
        // ensure that users who are already online are entered into the
        // database
        // TODO move to UserLogger module
        Promise.all(this.getUsers().map(user => User.fromPlugUser(user)))
          .then(users => { debug('updated users', users.length) })
      }
      cb && cb(err)
    })
  }

  stop(cb) {
    this.unloadModules()
    this.logout()
    // should *probably* also wait for this before callback-ing
    mongoose.disconnect()

    this.once(this.LOGOUT_SUCCESS, () => {
      this.removeAllListeners()
      cb()
    })
    this.once(this.LOGOUT_ERROR, e => {
      // TODO figure out something useful to do here
      this.removeAllListeners()
      cb(e)
    })
  }

  setRoom(room) {
    this.options.room = room
    this.connect(room)
  }

  onMessage(msg) {
    if (!this.getCurrentRoomStats()) {
      return
    }

    logChat(msg.username, msg.message)
    if (msg.message.charAt(0) === this.delimiter) {
      let func = null
      let user = msg.id === 'sekshi' ? { role: this.USERROLE.HOST }
                                     : this.getUserByID(msg.id, true)

      // nonexistent user
      if (!user) return

      user.role = user.role || this.USERROLE.NONE

      let args = this.parseArguments(msg.message)

      func = args.shift().replace(this.delimiter, '').toLowerCase()
      args.unshift(user)

      for (let name in this.modules) if (this.modules.hasOwnProperty(name)) {
        let mod = this.modules[name]
        if (mod.enabled() && typeof mod[func] === 'function' && mod.permissions.hasOwnProperty(func)) {
          if (mod.ninjaVanish.indexOf(func) !== -1) {
            this.deleteMessage(msg.cid)
          }
          if (user.role >= mod.permissions[func]) {
            mod[func](...args)
          }
          else {
            this.sendChat(`@${msg.username}: You don't have sufficient permissions to use this command.`, 5 * 1000)
          }
        }
      }
    }
  }

  // Parses space-separated chat command arguments.
  // single words become single arguments.
  //    word → [ 'word ']
  // strings surrounded by double quotes become single arguments.
  //    "quoted words" word → [ 'quoted words', 'word' ]
  // strings prefixed with "@" are matched to the online user list.
  // if any online user's name matches the string, it will be passed instead.
  // this is so that you don't need quotes around usernames with spaces.
  //    @Online User parameter @Offline User → [ 'Online User', 'parameter', '@Offline', 'User' ]
  //
  // feature-bugs:
  // if you forget to close a quoted string it will go until the end of the line (might be unexpected)
  // if you forget to add a space after a quoted string, the rest will be read as a separate parameter
  parseArguments(str = '') {
    let args = []
    let i = 0
    let chunk

    let usernames = str.indexOf('@') !== -1 // might contain a username
      ? [ this.getSelf(), ...this.getUsers() ].map(u => u.username)
      : []

    while (chunk = str.slice(i)) {
      // separator
      if (chunk.charAt(0) === ' ') {
        i++
        continue
      }
      // quoted string
      else if (chunk.charAt(0) === '"') {
        let end = chunk.indexOf('"', 1)
        // end of param string
        if (end === -1) {
          args.push(chunk.slice(1))
          break
        }
        args.push(chunk.slice(1, end))
        i += end + 1
        continue
      }
      // possible username
      else if (chunk.charAt(0) === '@') {
        let username = find(usernames,
                            name => chunk.slice(1, name.length + 1).toLowerCase() === name.toLowerCase())
        if (username) {
          args.push(username)
          i += `@${username}`.length
          continue
        }
      }
      // single parameter word
      let end = chunk.indexOf(' ')
      // end of param string
      if (end === -1) {
        args.push(chunk)
        break
      }
      args.push(chunk.slice(0, end))
      i += end + 1
      continue
    }

    return args
  }

  lockskipDJ(id, position, cb) {
    this.skipDJ(id, e => {
      if (e) cb && cb(e)
      else this.moveDJ(id, position, cb)
    })
  }

  getModule(name) {
    return this.modules[name.toLowerCase()]
  }

  _getModuleFiles(modulePath) {
    const stat = fs.statSync(modulePath)
    if (stat.isDirectory()) {
      let files = fs.readdirSync(modulePath)
      return files.reduce((modules, filename) => {
        return modules.concat(this._getModuleFiles(path.join(modulePath, filename)))
      }, [])
    }
    else if (stat.isFile() && /\.module\.js$/.test(modulePath)) {
      return [ modulePath ]
    }
    return []
  }

  updateAvailableModules() {
    return this._availableModules = this._getModuleFiles(this.modulePath)
      .map(file => file.match(/\/([^\/]+?)\.module\.js$/)[1])
  }

  getAvailableModules() {
    return this._availableModules
  }

  // finds the file path for the given module name, case-insensitive
  getModulePath(name) {
    const lower = name.toLowerCase()
    const match = find(this.getAvailableModules(), avail => avail.toLowerCase() === lower)
    return match ? path.join(this.modulePath, `${match}.module.js`) : null
  }

  enable(name) {
    debug('enable', name)
    const mod = this.getModule(name)
    if (mod) mod.enable()
  }
  disable(name) {
    debug('disable', name)
    const mod = this.getModule(name)
    if (mod) mod.disable()
  }

  loadModule(name) {
    debug('load', name)
    const lName = name.toLowerCase()
    let mod = this.modules[lName]
    if (!mod) {
      const Module = require(this.getModulePath(name))
      mod = new Module(this, {})
      this.modules[lName] = mod
    }

    // the event is fired on nextTick so modules can simply listen for "moduleloaded"
    // and get events for *all* the modules when loadModules() is called, even for those
    // that register earlier
    process.nextTick(() => { this.emit('moduleloaded', mod, lName) })

    // enable system modules by default
    if (lName === 'system' || lName === 'config') mod.enable()

    return mod
  }

  unloadModule(name) {
    debug('unload', name)
    const mod = this.getModule(name)
    if (mod) {
      mod.destroy()
    }
    const file = this.getModulePath(name)
    delete require.cache[path.resolve(file)]
    delete this.modules[name.toLowerCase()]

    process.nextTick(() => { this.emit('moduleunloaded', mod, name.toLowerCase()) })
  }

  reloadModule(name) {
    let mod = this.getModule(name)
    let enabled = false
    if (mod) {
      enabled = mod.enabled()
      mod = null
    }

    this.unloadModule(name)
    this.loadModule(name)

    if (enabled) {
      this.getModule(name).enable()
    }
  }

  loadModules() {
    debug('load all')
    this.updateAvailableModules()
    this.getAvailableModules().forEach(this.loadModule, this)
  }

  unloadModules() {
    debug('unload all')
    for (let name in this.modules) if (this.modules.hasOwnProperty(name)) {
      this.unloadModule(name)
    }
  }

  reloadModules() {
    this.unloadModules()
    this.loadModules()
  }
}<commit_msg>fix lockskip when DJ cycle is disabled<commit_after>const Plugged = require('plugged')
const path = require('path')
const { inherits } = require('util')
const fs = require('fs')
const debug = require('debug')('sekshi:sekshi')
const logChat = require('debug')('sekshi:chat')
const mongoose = require('mongoose')
const Promise = require('promise')
const { User } = require('./models')
const find = require('array-find')
const unescape = require('ent/decode')

// these are fine-tuned to fit pretty much exactly within plug.dj's rate limiting scheme,
// while also being really simple
const CHAT_INTERVAL = 150
const CHAT_BACKOFF_INC = 60
const CHAT_BACKOFF_MAX = 700

export default class Sekshi extends Plugged {
  constructor(args) {
    super()
    this.invokeLogger(this._log)

    this.options = args
    this.db = mongoose.connect(args.mongo)

    this.modules = {}
    this._availableModules = []
    this.delimiter = args.delimiter || '!'
    this.modulePath = args.modulePath || path.join(__dirname, 'modules')

    this._room = args.room

    this.onMessage = this.onMessage.bind(this)

    this._addUnescapeListeners()

    this._chatQueue = []
    this._lastMessage = 0
    this._chatTimer = false
    this._chatBackoff = CHAT_INTERVAL
  }

  _log(msg, verbosity, color) {
    debug(msg)
  }

  // ugly hack to deal with plug.dj's mildly insane html escaping behaviour
  _addUnescapeListeners() {
    const unescapeUser = user => {
      user.username = unescape(user.username)
      user.blurb && (user.blurb = unescape(user.blurb))
    }

    // these handlers are added before any other handlers,
    // hopefully EventEmitter will always fire them in
    // that same order!
    this.on(this.USER_JOIN, unescapeUser)
    this.on(this.ADVANCE, ({}, { media }) => {
      media.author = unescape(media.author)
      media.title = unescape(media.title)
    })
    this.on(this.CHAT, chat => {
      chat.username = unescape(chat.username)
      chat.message = unescape(chat.message)
    })
    this.on(this.JOINED_ROOM, () => {
      this.state.room.users.forEach(unescapeUser)
    })
  }

  // Override Plugged#getUserByName with a case insensitive version
  getUserByName(username, checkCache = false) {
    username = username.toLowerCase()

    if (username === this.getSelf().username.toLowerCase())
      return this.getSelf()

    let user = find(this.getUsers(), user => user.username.toLowerCase() === username)

    if (checkCache && !user) {
      user = find(this.state.usercache, user => user.username.toLowerCase() === username)
    }

    return user || null
  }

  sendChat(message, autodelete = false) {
    const MAX_LENGTH = 256
    while (message.length >= MAX_LENGTH) {
      this._chatQueue.push([ message.slice(0, MAX_LENGTH), autodelete ])
      message = message.slice(MAX_LENGTH)
    }
    this._chatQueue.push([ message, autodelete ])
    if (!this._chatTimer) {
      this._processChatQueue()
    }
  }

  _processChatQueue() {
    if (this._chatQueue.length > 0) {
      if (this._lastMessage + this._chatBackoff <= Date.now()) {
        Plugged.prototype.sendChat.call(this, ...this._chatQueue.shift())
        this._lastMessage = Date.now()
        if (this._chatBackoff < CHAT_BACKOFF_MAX) {
          this._chatBackoff += CHAT_BACKOFF_INC
        }
      }
      setTimeout(() => this._processChatQueue(), this._chatBackoff + 1)
      this._chatTimer = true
    }

    if (this._chatQueue.length === 0) {
      this._chatTimer = false
      this._chatBackoff = CHAT_INTERVAL
    }
  }

  start(creds, cb) {
    this.login(creds)

    this.once(this.LOGIN_SUCCESS, () => {
      this.cacheChat(true)
      this.connect(this.options.room)
      this.loadModules()
    })

    this.on(this.CHAT, this.onMessage)

    this.once(this.JOINED_ROOM, err => {
      if (!err) {
        // ensure that users who are already online are entered into the
        // database
        // TODO move to UserLogger module
        Promise.all(this.getUsers().map(user => User.fromPlugUser(user)))
          .then(users => { debug('updated users', users.length) })
      }
      cb && cb(err)
    })
  }

  stop(cb) {
    this.unloadModules()
    this.logout()
    // should *probably* also wait for this before callback-ing
    mongoose.disconnect()

    this.once(this.LOGOUT_SUCCESS, () => {
      this.removeAllListeners()
      cb()
    })
    this.once(this.LOGOUT_ERROR, e => {
      // TODO figure out something useful to do here
      this.removeAllListeners()
      cb(e)
    })
  }

  setRoom(room) {
    this.options.room = room
    this.connect(room)
  }

  onMessage(msg) {
    if (!this.getCurrentRoomStats()) {
      return
    }

    logChat(msg.username, msg.message)
    if (msg.message.charAt(0) === this.delimiter) {
      let func = null
      let user = msg.id === 'sekshi' ? { role: this.USERROLE.HOST }
                                     : this.getUserByID(msg.id, true)

      // nonexistent user
      if (!user) return

      user.role = user.role || this.USERROLE.NONE

      let args = this.parseArguments(msg.message)

      func = args.shift().replace(this.delimiter, '').toLowerCase()
      args.unshift(user)

      for (let name in this.modules) if (this.modules.hasOwnProperty(name)) {
        let mod = this.modules[name]
        if (mod.enabled() && typeof mod[func] === 'function' && mod.permissions.hasOwnProperty(func)) {
          if (mod.ninjaVanish.indexOf(func) !== -1) {
            this.deleteMessage(msg.cid)
          }
          if (user.role >= mod.permissions[func]) {
            mod[func](...args)
          }
          else {
            this.sendChat(`@${msg.username}: You don't have sufficient permissions to use this command.`, 5 * 1000)
          }
        }
      }
    }
  }

  // Parses space-separated chat command arguments.
  // single words become single arguments.
  //    word → [ 'word ']
  // strings surrounded by double quotes become single arguments.
  //    "quoted words" word → [ 'quoted words', 'word' ]
  // strings prefixed with "@" are matched to the online user list.
  // if any online user's name matches the string, it will be passed instead.
  // this is so that you don't need quotes around usernames with spaces.
  //    @Online User parameter @Offline User → [ 'Online User', 'parameter', '@Offline', 'User' ]
  //
  // feature-bugs:
  // if you forget to close a quoted string it will go until the end of the line (might be unexpected)
  // if you forget to add a space after a quoted string, the rest will be read as a separate parameter
  parseArguments(str = '') {
    let args = []
    let i = 0
    let chunk

    let usernames = str.indexOf('@') !== -1 // might contain a username
      ? [ this.getSelf(), ...this.getUsers() ].map(u => u.username)
      : []

    while (chunk = str.slice(i)) {
      // separator
      if (chunk.charAt(0) === ' ') {
        i++
        continue
      }
      // quoted string
      else if (chunk.charAt(0) === '"') {
        let end = chunk.indexOf('"', 1)
        // end of param string
        if (end === -1) {
          args.push(chunk.slice(1))
          break
        }
        args.push(chunk.slice(1, end))
        i += end + 1
        continue
      }
      // possible username
      else if (chunk.charAt(0) === '@') {
        let username = find(usernames,
                            name => chunk.slice(1, name.length + 1).toLowerCase() === name.toLowerCase())
        if (username) {
          args.push(username)
          i += `@${username}`.length
          continue
        }
      }
      // single parameter word
      let end = chunk.indexOf(' ')
      // end of param string
      if (end === -1) {
        args.push(chunk)
        break
      }
      args.push(chunk.slice(0, end))
      i += end + 1
      continue
    }

    return args
  }

  lockskipDJ(id, position, cb) {
    const skipDJ = Promise.denodeify(this.skipDJ.bind(this))
    const addToWaitlist = Promise.denodeify(this.addToWaitlist.bind(this))
    const moveDJ = Promise.denodeify(this.moveDJ.bind(this))
    const setLock = Promise.denodeify(this.setLock.bind(this))

    if (this.doesWaitlistCycle()) {
      skipDJ(id)
        .then(() => moveDJ(id, position))
        .nodeify(cb)
    }
    else {
      let locked = this.isWaitlistLocked()
      // DJ cycle is off
      // first, lock the wait list so no-one can steal the 50th spot before the DJ is put back
      setLock(true, false)
        // then skip & add the DJ again
        .then(() => skipDJ(id))
        .then(() => addToWaitlist(id))
        .then(() => moveDJ(id, position))
        // and finally revert to the old wait list lock status
        .then(() => setLock(locked, false),
              () => setLock(locked, false))
        .nodeify(cb)
    }
  }

  getModule(name) {
    return this.modules[name.toLowerCase()]
  }

  _getModuleFiles(modulePath) {
    const stat = fs.statSync(modulePath)
    if (stat.isDirectory()) {
      let files = fs.readdirSync(modulePath)
      return files.reduce((modules, filename) => {
        return modules.concat(this._getModuleFiles(path.join(modulePath, filename)))
      }, [])
    }
    else if (stat.isFile() && /\.module\.js$/.test(modulePath)) {
      return [ modulePath ]
    }
    return []
  }

  updateAvailableModules() {
    return this._availableModules = this._getModuleFiles(this.modulePath)
      .map(file => file.match(/\/([^\/]+?)\.module\.js$/)[1])
  }

  getAvailableModules() {
    return this._availableModules
  }

  // finds the file path for the given module name, case-insensitive
  getModulePath(name) {
    const lower = name.toLowerCase()
    const match = find(this.getAvailableModules(), avail => avail.toLowerCase() === lower)
    return match ? path.join(this.modulePath, `${match}.module.js`) : null
  }

  enable(name) {
    debug('enable', name)
    const mod = this.getModule(name)
    if (mod) mod.enable()
  }
  disable(name) {
    debug('disable', name)
    const mod = this.getModule(name)
    if (mod) mod.disable()
  }

  loadModule(name) {
    debug('load', name)
    const lName = name.toLowerCase()
    let mod = this.modules[lName]
    if (!mod) {
      const Module = require(this.getModulePath(name))
      mod = new Module(this, {})
      this.modules[lName] = mod
    }

    // the event is fired on nextTick so modules can simply listen for "moduleloaded"
    // and get events for *all* the modules when loadModules() is called, even for those
    // that register earlier
    process.nextTick(() => { this.emit('moduleloaded', mod, lName) })

    // enable system modules by default
    if (lName === 'system' || lName === 'config') mod.enable()

    return mod
  }

  unloadModule(name) {
    debug('unload', name)
    const mod = this.getModule(name)
    if (mod) {
      mod.destroy()
    }
    const file = this.getModulePath(name)
    delete require.cache[path.resolve(file)]
    delete this.modules[name.toLowerCase()]

    process.nextTick(() => { this.emit('moduleunloaded', mod, name.toLowerCase()) })
  }

  reloadModule(name) {
    let mod = this.getModule(name)
    let enabled = false
    if (mod) {
      enabled = mod.enabled()
      mod = null
    }

    this.unloadModule(name)
    this.loadModule(name)

    if (enabled) {
      this.getModule(name).enable()
    }
  }

  loadModules() {
    debug('load all')
    this.updateAvailableModules()
    this.getAvailableModules().forEach(this.loadModule, this)
  }

  unloadModules() {
    debug('unload all')
    for (let name in this.modules) if (this.modules.hasOwnProperty(name)) {
      this.unloadModule(name)
    }
  }

  reloadModules() {
    this.unloadModules()
    this.loadModules()
  }
}<|endoftext|>