<commit_before>var
  r = require('rethinkdb'),
  async = require('async'),
  util = require('util'),
  _ = require('underscore');

module.exports = RethinkDBProvider;

function RethinkDBProvider(options, helpers, callback) {
  if (!(this instanceof RethinkDBProvider)) return new RethinkDBProvider(options);

  callback = callback || function () {
  };

  var self = this;

  this.name = 'RethinkDB';
  this.options = options;
  this.logger = helpers.logger;
  this.common = helpers.common;

  return this.init(options, function (err) {
    if (err)
      return callback(err);

    return callback(null, self);
  });
}

RethinkDBProvider.prototype.init = function (options, callback) {
  callback = callback || function () {
  };

  var self = this;

  self.logger.info('Initializing connection to provider [' + self.name + '].');

  return self.openConnection(options, callback);
};

RethinkDBProvider.prototype.destroy = function (callback) {
  callback = callback || function () {
  };

  var self = this;

  self.logger.info('Destroying connection to provider [' + self.name + '].');

  return callback(null);
};

RethinkDBProvider.prototype.find = function (options, callback) {
  callback = callback || function () {
  };

  var self = this;

  return callback(null);
};

RethinkDBProvider.prototype.delete = function (options, callback) {
  callback = callback || function () {
  };

  var self = this;

  return callback(null);
};

RethinkDBProvider.prototype.update = function (options, callback) {
  callback = callback || function () {
  };

  var self = this;

  return callback(null);
};

RethinkDBProvider.prototype.insert = function (collection, documents, options, callback) {
  callback = callback || function () {
  };

  var self = this;

  documents.forEach(function (doc, i) {
    documents[i] = self.transform(collection.storeKey, doc);
  });

  var _documents = documents.clone();

  self.client.writePoints(collection.storeKey.replace(/-/ig, '_'), _documents, {}, function (err) {
    if (err)
      return callback(err);

    documents.forEach(function (doc) {
      doc.timestamp = new Date(doc.time);
      doc.saved = true;
      delete doc.time;
    });
    return callback(null, documents);
  });
};

RethinkDBProvider.prototype.buildQueryPlan = function (query, callback) {
  var self = this;
  var plan = {
    uid: self.common.uuid(),
    cost: 0,
    colQueries: {},
    query: query
  };
  var $match = {};
  var $project = {};
  var $group = {};
  var $limit;

  if (!query.dimensions)
    query.dimensions = [];
  if (!query.metrics)
    query.metrics = [];

  if (query.timeframe && !query.timeframe.hasOwnProperty('last_n_items')) {
    if (typeof query.timeframe.start === 'string')
      query.timeframe.start = new Date(query.timeframe.start);
    if (typeof query.timeframe.end === 'string')
      query.timeframe.end = new Date(query.timeframe.end);
    $match.time = {$gt: query.timeframe.start.toISOString().replace('T', ' ').replace('Z', ''), $lt: query.timeframe.end.toISOString().replace('T', ' ').replace('Z', '')};
  }
  else if (query.timeframe && query.timeframe.hasOwnProperty('last_n_items')) {
    $limit = query.timeframe.last_n_items;
  }

  if (query.filter) {
    query.filter.forEach(function (f) {
      //if (f[1] == 'eq')
      //  $match[f[0]] = f[2];
      //else {
      $match[f[0]] = {};
      $match[f[0]]['$' + f[1]] = f[2];
      //}
    });
  }

  switch (query.interval) {
    case 'timebucket.second':
      query.interval = 's';
      break;
    case 'timebucket.minute':
      query.interval = 'm';
      break;
    case 'timebucket.hour':
      query.interval = 'h';
      break;
    case 'timebucket.ddate':
      query.interval = 'd';
      break;
    case 'timebucket.week':
      query.interval = 'w';
      break;
    default:
      break;
  }

  query.dimensions.forEach(function (dimension) {
    switch (dimension.datatype) {
      case 'date':
        $project.time = 'time';
        $group.time = 'time(1' + query.interval + ')';
        query.timeSeries = true;
        break;
      case 'ip':
      case 'number':
      case 'string':
        $project[dimension.key] = (dimension.attribute || dimension.key);
        $group[dimension.key] = (dimension.attribute || dimension.key);
        break;
      case 'geo':
        break;
      default:
        return setImmediate(function () {
          return callback(new Error('Dimension [' + dimension.key + '] has unknown type of [' + dimension.datatype + ']'));
        });
    }
  });

  if (query.metrics.length === 0) {
    try {
      query.metrics.push({
        key: 'fake',
        dependsOn: 'fake',
        collection: query.collection.key || query.dimensions ? query.dimensions[0].collection : null
      });
    }
    catch (ex) {
      query.metrics = [];
    }
  }

  query.metrics.forEach(function (metric) {
    var colQuery = {
      collections: metric.collection ? metric.collection.storeKey : null,
      query: []
    };

    var _$match = self.common.extend({}, $match);
    var _$project = self.common.extend({}, $project);
    var _$group = self.common.extend({}, $group);

    if (metric.filter) {
      metric.filter.forEach(function (f) {
        // if (f[1] === 'eq')
        //   _$match[f[0]] = f[2];
        //else {
        _$match[f[0]] = {};
        _$match[f[0]]['$' + f[1]] = f[2];
        // }
      });
    }
    if (!metric.formula && metric.collection) {
      metric.aggregation = metric.aggregation || 'sum';
      if (metric.aggregation == 'ucount')
        metric.aggregation = 'count(distinct';
      else if (metric.aggregation === 'avg')
        metric.aggregation = 'mean';

      colQuery.key = self.common.hash(colQuery.type + '_' + metric.collection.key + '_' + JSON.stringify(_$match));
      if (plan.colQueries[colQuery.key]) {
        _$group = self.common.extend({}, plan.colQueries[colQuery.key].query.$group);
        _$project = self.common.extend(plan.colQueries[colQuery.key].query.$project, _$project);
      }

      if (metric.key !== 'fake')
        _$project[metric.key] = metric.aggregation + '(' + (metric.attribute || metric.key) + ') ' + (metric.aggregation === 'count(distinct' ? ')' : '') + ' as ' + metric.key;

      colQuery.query = {
        $match: _$match,
        $sort: {time: -1},
        $project: _$project,
        $group: _$group
      };
      if ($limit) {
        colQuery.query.$limit = $limit;
      }

      plan.colQueries[colQuery.key] = colQuery;
    }

  });

  //console.log('plan', require('util').inspect(plan.colQueries, {depth: null, colors: true}));

  plan.dimensions = query.dimensions;
  plan.metrics = query.metrics;

  //console.log(plan);

  return setImmediate(function () {
    return callback(null, plan);
  });
};

RethinkDBProvider.prototype.query = function (context, query, callback) {
  callback = callback || function () {
  };

  var self = this;

  return self.buildQueryPlan(query, function (err, plan) {
    if (err)
      return callback(err);
    //console.log(require('util').inspect(plan.colQueries, {depth: null, colors: true}));

    var calls = [];
    var results = [];
    Object.keys(plan.colQueries).forEach(function (key) {
      var queryPlan = plan.colQueries[key].query;
      var queryPlanKey = key;
      var sql = 'select ';

      if (!queryPlan.$limit) {
        Object.keys(queryPlan.$project).forEach(function (key) {
          var column = queryPlan.$project[key];
          sql += column + ',';
        });
      }
      else {
        Object.keys(queryPlan.$project).forEach(function (key) {
          var column = queryPlan.$project[key];
          sql += 'top(' + (column === 'time' ? '_t' : column) + ',' + queryPlan.$limit + ') as ' + column + ',';
        });
      }
      if (sql.substring(sql.length - 1) === ',')
        sql = sql.substring(0, sql.length - 1);

      sql += ' from ' + plan.colQueries[key].collections.replace(/-/ig, '_');

      if (!queryPlan.$limit) {
        if (Object.keys(queryPlan.$group).length > 0) {
          sql += ' group by ';
          Object.keys(queryPlan.$group).forEach(function (key) {
            var column = queryPlan.$group[key];
            sql += column + ',';
          });
          if (sql.substring(sql.length - 1) === ',')
            sql = sql.substring(0, sql.length - 1);
          //if (query.timeSeries)
          // sql += ' fill(0)';
        }
      }

      if (Object.keys(queryPlan.$match).length > 0) {
        sql += ' where ';
        Object.keys(queryPlan.$match).forEach(function (key) {
          var filter = queryPlan.$match[key];
          var filterName = key;
          Object.keys(filter).forEach(function (key) {
            var f = filter[key];
            sql += filterName + ' ';
            switch (key) {
              case '$gt':
                sql += '> ';
                break;
              case '$gte':
                sql += '>= ';
                break;
              case '$lt':
                sql += '< ';
                break;
              case '$lte':
                sql += '<= ';
                break;
              default:
                sql += '= ';
                break;
            }
            switch (typeof f) {
              case 'number':
                sql += f + ' and ';
                break;
              case 'string':
              default:
                sql += '\'' + f + '\' and ';
                break;
            }
          });
        });
        if (sql.substring(sql.length - 4) === 'and ')
          sql = sql.substring(0, sql.length - 4);
      }
      //if (queryPlan.$limit && queryPlan.$limit > 0)
      //  sql += ' limit ' + queryPlan.$limit;

      //console.log(sql);

      var call = function (callback) {
        self.client.query(sql, function (err, result) {
          if (err)
            return callback(err);

          results.push(result);
          return callback(null);
        });
      };
      calls.push(call);
    });

    async.parallel(calls, function (err) {
      if (err)
        return callback(err);
      var output = {
        dimensions: query.dimensions,
        metrics: query.metrics,
        documents: [],
        queryplan: plan
      };

      var keys = [];
      var final = [];

      if (results && results.length > 0) {
        results.forEach(function (_result) {
          _result = _result[0];
          _result = self.verifyResult(query, _result);
          if (!_result)
            return callback(null, output);
          if (!_result.points)
            _result.points = [];

          var timeIndex = _result.columns.lastIndexOf('time');
          if (timeIndex > -1)
            _result.columns[timeIndex] = 'timestamp';

          _result.points.forEach(function (point) {
            var document = {_id: {}};
            _result.columns.forEach(function (col, i) {
              var dimension = _.find(query.dimensions, function (d) {
                return d.key === col.replace('.', '_');
              });
              if (dimension)
                document._id[col] = point[i];
              document[col] = point[i];
            });

            if (typeof document.timestamp === 'string')
              document.timestamp = parseInt(document.timestamp);
            if (document.timestamp > 0)
              document.timestamp = new Date(document.timestamp);

            var key = self.common.hash(JSON.stringify(document._id));
            var row;

            if (keys.indexOf(key) === -1) {
              row = {};
              Object.keys(document._id).forEach(function (key) {
                row[key] = document._id[key];
              });
              row.key = key;
              keys.push(key);
              final.push(row);
            }
            else {
              row = _.find(final, function (f) {
                return f.key == key;
              });
              if (!row) //TODO: how can this happen?
              {
                console.log('wtf?', key, query);
              }
            }

            Object.keys(document).forEach(function (attribute) {
              if (attribute != '_id') {
                if (document.hasOwnProperty(attribute)) {
                  if (attribute.indexOf('.') > -1) {
                    row[attribute.replace('.', '_')] = document[attribute];
                    delete row[attribute];
                  }
                  else
                    row[attribute] = document[attribute];
                }
                else
                  row[attribute] = '(not set)';
              }
            });
            output.metrics.forEach(function (m) {
              if (!row[m.key])
                row[m.key] = null;
            });

            //delete row.key;
            final[keys.indexOf(key)] = row;
          });
        });

        output.documents = final;

        //console.log(final);

        return setImmediate(function () {
          return callback(null, output);
        });
      }
      else {
        output.dimensions = plan.dimensions;

        output.metrics = plan.metrics;
        output.documents = [];
        return setImmediate(function () {
          return callback(null, output);
        });
      }
    });
  });
};

RethinkDBProvider.prototype.verifyResult = function (query, result) {
  //console.log(result);

  return result;
};

RethinkDBProvider.prototype.openConnection = function (options, callback) {
  callback = callback || function () {
  };

  var self = this;

  r.connect(options, function (err, conn) {
    if (err)
      return callback(err);
    self.client = conn;
    return self.checkConnection(null, callback);
  });
};

RethinkDBProvider.prototype.closeConnection = function (connection, callback) {
  callback = callback || function () {
  };

  var self = this;
  self.client.close();
  return callback(null);
};

RethinkDBProvider.prototype.checkConnection = function (connection, callback) {
  callback = callback || function () {
  };

  var self = this;

  self.client.getDatabaseNames(function (err, names) {
    if (err)
      return callback(err);

    if (names.indexOf('joola') === -1)
      return self.createDatabase(callback);
    return callback(null, connection);
  });
};

RethinkDBProvider.prototype.transform = function (collection, obj) {
  var self = this;
  var result = {};
  obj.time = obj.time || new Date(obj.timestamp).getTime() || new Date().getTime();
  delete obj.timestamp;

  obj.f = 1;
  obj.t = obj.time;
  obj._t = obj.time.toString();

  var flat = self.common.flatten(obj);
  flat.forEach(function (pair) {
    result[pair[0]] = pair[1];
  });
  obj = result;

  return result;
};

RethinkDBProvider.prototype.stats = function (collection, callback) {
  var self = this;

  self.client.query('select count(f) from ' + collection.replace(/-/ig, '_'), function (err, result) {
    if (err)
      return callback(err);

    return callback(null, {count: result});
  });
};

RethinkDBProvider.prototype.drop = function (collection, callback) {
  var self = this;

  self.client.query('drop series ' + collection.replace(/-/ig, '_'), function (err) {
    if (err)
      return callback(err);

    return callback(null);
  });
};

RethinkDBProvider.prototype.createDatabase = function (callback) {
  var self = this;

  self.client.createDatabase('joola', function (err) {
    if (err)
      return callback(err);

    self.client.createUser('joola', 'joola', 'joola', function (err) {
      if (err)
        return callback(err);

      return callback(null);
    });
  });
};

RethinkDBProvider.prototype.purge = function (callback) {
  var self = this;

  self.client.deleteDatabase('joola', function (err) {
    if (err)
      return callback(err);

    return callback(null);
  });
};<commit_msg>#1 preps for query<commit_after>var
  r = require('rethinkdb'),
  async = require('async'),
  util = require('util'),
  _ = require('underscore');

module.exports = RethinkDBProvider;

function RethinkDBProvider(options, helpers, callback) {
  if (!(this instanceof RethinkDBProvider)) return new RethinkDBProvider(options);

  callback = callback || function () {
  };

  var self = this;

  this.name = 'RethinkDB';
  this.options = options;
  this.logger = helpers.logger;
  this.common = helpers.common;

  return this.init(options, function (err) {
    if (err)
      return callback(err);

    return callback(null, self);
  });
}

RethinkDBProvider.prototype.init = function (options, callback) {
  callback = callback || function () {
  };

  var self = this;

  self.logger.info('Initializing connection to provider [' + self.name + '].');

  return self.openConnection(options, callback);
};

RethinkDBProvider.prototype.destroy = function (callback) {
  callback = callback || function () {
  };

  var self = this;

  self.logger.info('Destroying connection to provider [' + self.name + '].');

  return self.closeConnection(options, callback);
};

RethinkDBProvider.prototype.find = function (options, callback) {
  callback = callback || function () {
  };

  var self = this;

  return callback(null);
};

RethinkDBProvider.prototype.delete = function (options, callback) {
  callback = callback || function () {
  };

  var self = this;

  return callback(null);
};

RethinkDBProvider.prototype.update = function (options, callback) {
  callback = callback || function () {
  };

  var self = this;

  return callback(null);
};

RethinkDBProvider.prototype.insert = function (collection, documents, options, callback) {
  callback = callback || function () {
  };

  var self = this;
  r.db(self.options.db).tableCreate(collection.key).run(self.client, function (err, result) {
    //ignore errors if table already exists
    r.table(collection.key).insert(documents).run(self.client, function (err, result) {
      if (err)
        return callback(err);
      return callback(null, result);
    });
  });
};

RethinkDBProvider.prototype.buildQueryPlan = function (query, callback) {
  var self = this;
  var plan = {
    uid: self.common.uuid(),
    cost: 0,
    colQueries: {},
    query: query
  };
  var $match = {};
  var $project = {};
  var $group = {};
  var $limit;

  console.log(query);

  if (!query.dimensions)
    query.dimensions = [];
  if (!query.metrics)
    query.metrics = [];

  if (query.timeframe && !query.timeframe.hasOwnProperty('last_n_items')) {
    if (typeof query.timeframe.start === 'string')
      query.timeframe.start = new Date(query.timeframe.start);
    if (typeof query.timeframe.end === 'string')
      query.timeframe.end = new Date(query.timeframe.end);
    $match.time = {$gt: query.timeframe.start.toISOString().replace('T', ' ').replace('Z', ''), $lt: query.timeframe.end.toISOString().replace('T', ' ').replace('Z', '')};
  }
  else if (query.timeframe && query.timeframe.hasOwnProperty('last_n_items')) {
    $limit = query.timeframe.last_n_items;
  }

  if (query.filter) {
    query.filter.forEach(function (f) {
      //if (f[1] == 'eq')
      //  $match[f[0]] = f[2];
      //else {
      $match[f[0]] = {};
      $match[f[0]]['$' + f[1]] = f[2];
      //}
    });
  }

  switch (query.interval) {
    case 'timebucket.second':
      query.interval = 's';
      break;
    case 'timebucket.minute':
      query.interval = 'm';
      break;
    case 'timebucket.hour':
      query.interval = 'h';
      break;
    case 'timebucket.ddate':
      query.interval = 'd';
      break;
    case 'timebucket.week':
      query.interval = 'w';
      break;
    default:
      break;
  }

  query.dimensions.forEach(function (dimension) {
    switch (dimension.datatype) {
      case 'date':
        $project.time = 'time';
        $group.time = 'time(1' + query.interval + ')';
        query.timeSeries = true;
        break;
      case 'ip':
      case 'number':
      case 'string':
        $project[dimension.key] = (dimension.attribute || dimension.key);
        $group[dimension.key] = (dimension.attribute || dimension.key);
        break;
      case 'geo':
        break;
      default:
        return setImmediate(function () {
          return callback(new Error('Dimension [' + dimension.key + '] has unknown type of [' + dimension.datatype + ']'));
        });
    }
  });

  if (query.metrics.length === 0) {
    try {
      query.metrics.push({
        key: 'fake',
        dependsOn: 'fake',
        collection: query.collection.key || query.dimensions ? query.dimensions[0].collection : null
      });
    }
    catch (ex) {
      query.metrics = [];
    }
  }

  query.metrics.forEach(function (metric) {
    var colQuery = {
      collections: metric.collection ? metric.collection.storeKey : null,
      query: []
    };

    var _$match = self.common.extend({}, $match);
    var _$project = self.common.extend({}, $project);
    var _$group = self.common.extend({}, $group);

    if (metric.filter) {
      metric.filter.forEach(function (f) {
        // if (f[1] === 'eq')
        //   _$match[f[0]] = f[2];
        //else {
        _$match[f[0]] = {};
        _$match[f[0]]['$' + f[1]] = f[2];
        // }
      });
    }
    if (!metric.formula && metric.collection) {
      metric.aggregation = metric.aggregation || 'sum';
      if (metric.aggregation == 'ucount')
        metric.aggregation = 'count(distinct';
      else if (metric.aggregation === 'avg')
        metric.aggregation = 'mean';

      colQuery.key = self.common.hash(colQuery.type + '_' + metric.collection.key + '_' + JSON.stringify(_$match));
      if (plan.colQueries[colQuery.key]) {
        _$group = self.common.extend({}, plan.colQueries[colQuery.key].query.$group);
        _$project = self.common.extend(plan.colQueries[colQuery.key].query.$project, _$project);
      }

      if (metric.key !== 'fake')
        _$project[metric.key] = metric.aggregation + '(' + (metric.attribute || metric.key) + ') ' + (metric.aggregation === 'count(distinct' ? ')' : '') + ' as ' + metric.key;

      colQuery.query = {
        $match: _$match,
        $sort: {time: -1},
        $project: _$project,
        $group: _$group
      };
      if ($limit) {
        colQuery.query.$limit = $limit;
      }

      plan.colQueries[colQuery.key] = colQuery;
    }

  });

  //console.log('plan', require('util').inspect(plan.colQueries, {depth: null, colors: true}));

  plan.dimensions = query.dimensions;
  plan.metrics = query.metrics;

  //console.log(plan);

  return setImmediate(function () {
    return callback(null, plan);
  });
};

RethinkDBProvider.prototype.query = function (context, query, callback) {
  callback = callback || function () {
  };

  var self = this;

  return self.buildQueryPlan(query, function (err, plan) {
    if (err)
      return callback(err);
    //console.log(require('util').inspect(plan.colQueries, {depth: null, colors: true}));

    var calls = [];
    var results = [];
    Object.keys(plan.colQueries).forEach(function (key) {
      var queryPlan = plan.colQueries[key].query;
      var queryPlanKey = key;
      var sql = 'select ';

      if (!queryPlan.$limit) {
        Object.keys(queryPlan.$project).forEach(function (key) {
          var column = queryPlan.$project[key];
          sql += column + ',';
        });
      }
      else {
        Object.keys(queryPlan.$project).forEach(function (key) {
          var column = queryPlan.$project[key];
          sql += 'top(' + (column === 'time' ? '_t' : column) + ',' + queryPlan.$limit + ') as ' + column + ',';
        });
      }
      if (sql.substring(sql.length - 1) === ',')
        sql = sql.substring(0, sql.length - 1);

      sql += ' from ' + plan.colQueries[key].collections.replace(/-/ig, '_');

      if (!queryPlan.$limit) {
        if (Object.keys(queryPlan.$group).length > 0) {
          sql += ' group by ';
          Object.keys(queryPlan.$group).forEach(function (key) {
            var column = queryPlan.$group[key];
            sql += column + ',';
          });
          if (sql.substring(sql.length - 1) === ',')
            sql = sql.substring(0, sql.length - 1);
          //if (query.timeSeries)
          // sql += ' fill(0)';
        }
      }

      if (Object.keys(queryPlan.$match).length > 0) {
        sql += ' where ';
        Object.keys(queryPlan.$match).forEach(function (key) {
          var filter = queryPlan.$match[key];
          var filterName = key;
          Object.keys(filter).forEach(function (key) {
            var f = filter[key];
            sql += filterName + ' ';
            switch (key) {
              case '$gt':
                sql += '> ';
                break;
              case '$gte':
                sql += '>= ';
                break;
              case '$lt':
                sql += '< ';
                break;
              case '$lte':
                sql += '<= ';
                break;
              default:
                sql += '= ';
                break;
            }
            switch (typeof f) {
              case 'number':
                sql += f + ' and ';
                break;
              case 'string':
              default:
                sql += '\'' + f + '\' and ';
                break;
            }
          });
        });
        if (sql.substring(sql.length - 4) === 'and ')
          sql = sql.substring(0, sql.length - 4);
      }
      //if (queryPlan.$limit && queryPlan.$limit > 0)
      //  sql += ' limit ' + queryPlan.$limit;

      //console.log(sql);

      var call = function (callback) {
        self.client.query(sql, function (err, result) {
          if (err)
            return callback(err);

          results.push(result);
          return callback(null);
        });
      };
      calls.push(call);
    });

    async.parallel(calls, function (err) {
      if (err)
        return callback(err);
      var output = {
        dimensions: query.dimensions,
        metrics: query.metrics,
        documents: [],
        queryplan: plan
      };

      var keys = [];
      var final = [];

      if (results && results.length > 0) {
        results.forEach(function (_result) {
          _result = _result[0];
          _result = self.verifyResult(query, _result);
          if (!_result)
            return callback(null, output);
          if (!_result.points)
            _result.points = [];

          var timeIndex = _result.columns.lastIndexOf('time');
          if (timeIndex > -1)
            _result.columns[timeIndex] = 'timestamp';

          _result.points.forEach(function (point) {
            var document = {_id: {}};
            _result.columns.forEach(function (col, i) {
              var dimension = _.find(query.dimensions, function (d) {
                return d.key === col.replace('.', '_');
              });
              if (dimension)
                document._id[col] = point[i];
              document[col] = point[i];
            });

            if (typeof document.timestamp === 'string')
              document.timestamp = parseInt(document.timestamp);
            if (document.timestamp > 0)
              document.timestamp = new Date(document.timestamp);

            var key = self.common.hash(JSON.stringify(document._id));
            var row;

            if (keys.indexOf(key) === -1) {
              row = {};
              Object.keys(document._id).forEach(function (key) {
                row[key] = document._id[key];
              });
              row.key = key;
              keys.push(key);
              final.push(row);
            }
            else {
              row = _.find(final, function (f) {
                return f.key == key;
              });
              if (!row) //TODO: how can this happen?
              {
                console.log('wtf?', key, query);
              }
            }

            Object.keys(document).forEach(function (attribute) {
              if (attribute != '_id') {
                if (document.hasOwnProperty(attribute)) {
                  if (attribute.indexOf('.') > -1) {
                    row[attribute.replace('.', '_')] = document[attribute];
                    delete row[attribute];
                  }
                  else
                    row[attribute] = document[attribute];
                }
                else
                  row[attribute] = '(not set)';
              }
            });
            output.metrics.forEach(function (m) {
              if (!row[m.key])
                row[m.key] = null;
            });

            //delete row.key;
            final[keys.indexOf(key)] = row;
          });
        });

        output.documents = final;

        //console.log(final);

        return setImmediate(function () {
          return callback(null, output);
        });
      }
      else {
        output.dimensions = plan.dimensions;

        output.metrics = plan.metrics;
        output.documents = [];
        return setImmediate(function () {
          return callback(null, output);
        });
      }
    });
  });
};

RethinkDBProvider.prototype.verifyResult = function (query, result) {
  //console.log(result);

  return result;
};

RethinkDBProvider.prototype.openConnection = function (options, callback) {
  callback = callback || function () {
  };

  var self = this;
  r.connect(options, function (err, conn) {
    if (err)
      return callback(err);

    self.client = conn;
    return self.checkConnection(null, callback);
  });
};

RethinkDBProvider.prototype.closeConnection = function (connection, callback) {
  callback = callback || function () {
  };

  var self = this;
  self.client.close();
  return callback(null);
};

RethinkDBProvider.prototype.checkConnection = function (connection, callback) {
  callback = callback || function () {
  };

  var self = this;
  return callback(null, connection);
};

RethinkDBProvider.prototype.stats = function (collection, callback) {
  var self = this;

  self.client.query('select count(f) from ' + collection.replace(/-/ig, '_'), function (err, result) {
    if (err)
      return callback(err);

    return callback(null, {count: result});
  });
};

RethinkDBProvider.prototype.drop = function (collection, callback) {
  var self = this;

  self.client.query('drop series ' + collection.replace(/-/ig, '_'), function (err) {
    if (err)
      return callback(err);

    return callback(null);
  });
};

RethinkDBProvider.prototype.purge = function (callback) {
  var self = this;

  self.client.deleteDatabase('joola', function (err) {
    if (err)
      return callback(err);

    return callback(null);
  });
};<|endoftext|>


<commit_before>/*jslint indent: 2*/
/*global require: true, module: true*/
(function () {
  'use strict';

  var InvalidInputError = require('./input-error.js'),
    transformationFunctions = require('./transforms'),
    transformationKeys = require('./keys'),
    regex = /^(\d*)d(\d+|\%)(([\+\-\/\*bw])(\d+))?$/,
    roll;

  roll = function (random) {
    this.random = random || function () {
      return Math.random();
    };
  };

  roll.prototype.validate = function (s) {
    return regex.test(s);
  };

  roll.prototype.parse = function (s) {
    if (!this.validate(s)) {
      throw new InvalidInputError(s);
    }

    var match = regex.exec(s),
      quantity = match[1], // 2d20+3 => 2
      sides = match[2], // 2d20+3 => 20
      hasTransformation = !!match[3], // 2d20+3 => true
      operator = match[4], // 2d20+3 => "+"
      transformationParameter = match[5]; // 2d20+3 => 3

    return {
      quantity: quantity ? parseInt(quantity, 10) : 1,
      sides: sides === '%' ? 100 : parseInt(sides, 10),
      transformations: hasTransformation ? transformationKeys[operator](parseInt(transformationParameter, 10)) : ['sum'],
      toString: function () {
        return s;
      }
    };
  };

  roll.prototype.roll = function (input) {
    if (!input) {
      throw new InvalidInputError();
    } else if (typeof input === 'string') {
      input = this.parse(input);
    }

    var rolled = [],
     calculations = [];

    while (rolled.length < input.quantity) {
      rolled.push(Math.floor((this.random() * input.sides) + 1));
    }

    calculations = input.transformations.reduce(function (previous, transformation) {
      var transformationFunction,
        transformationAdditionalParameter;

      if (typeof transformation === 'function') { // lets you pass something custom in
        transformationFunction = transformation;
      } else if (typeof transformation === 'string') { // "sum"
        transformationFunction = transformationFunctions[transformation];
      } else if (transformation instanceof Array) { // ["add", 3]
        transformationFunction = transformationFunctions[transformation[0]]; // fn for "add"
        transformationAdditionalParameter = transformation[1]; // 3
      }

      previous.unshift(transformationFunction(previous[0], transformationAdditionalParameter));
      return previous;
    }, [rolled]);

    return {
      input: input,
      calculations: calculations,
      rolled: calculations[calculations.length - 1],
      result: calculations[0]
    };
  };

  module.exports = roll;
  module.exports.InvalidInputError = InvalidInputError;

}());

<commit_msg>Update index.js<commit_after>/*jslint indent: 2*/
/*global require: true, module: true*/
(function () {
  'use strict';

  var InvalidInputError = require('./input-error.js'),
    transformationFunctions = require('./transforms'),
    transformationKeys = require('./keys'),
    regex = /^(\d*)d(\d+|\%)(([\+\-\/\*bw])(\d+))?(([\+\-\/\*])(\d+))?$/,
    roll;

  roll = function (random) {
    this.random = random || function () {
      return Math.random();
    };
  };

  roll.prototype.validate = function (s) {
    return regex.test(s);
  };

  roll.prototype.parse = function (s) {
    if (!this.validate(s)) {
      throw new InvalidInputError(s);
    }

    var match = regex.exec(s),
      quantity = match[1], // 2d20+3 => 2
      sides = match[2], // 2d20+3 => 20
      hasTransformation = !!match[3], // 2d20+3 => true
      operator = match[4], // 2d20+3 => "+"
      transformationParameter = match[5]; // 2d20+3 => 3

    return {
      quantity: quantity ? parseInt(quantity, 10) : 1,
      sides: sides === '%' ? 100 : parseInt(sides, 10),
      transformations: hasTransformation ? transformationKeys[operator](parseInt(transformationParameter, 10)) : ['sum'],
      toString: function () {
        return s;
      }
    };
  };

  roll.prototype.roll = function (input) {
    if (!input) {
      throw new InvalidInputError();
    } else if (typeof input === 'string') {
      input = this.parse(input);
    }

    var rolled = [],
     calculations = [];

    while (rolled.length < input.quantity) {
      rolled.push(Math.floor((this.random() * input.sides) + 1));
    }

    calculations = input.transformations.reduce(function (previous, transformation) {
      var transformationFunction,
        transformationAdditionalParameter;

      if (typeof transformation === 'function') { // lets you pass something custom in
        transformationFunction = transformation;
      } else if (typeof transformation === 'string') { // "sum"
        transformationFunction = transformationFunctions[transformation];
      } else if (transformation instanceof Array) { // ["add", 3]
        transformationFunction = transformationFunctions[transformation[0]]; // fn for "add"
        transformationAdditionalParameter = transformation[1]; // 3
      }

      previous.unshift(transformationFunction(previous[0], transformationAdditionalParameter));
      return previous;
    }, [rolled]);

    return {
      input: input,
      calculations: calculations,
      rolled: calculations[calculations.length - 1],
      result: calculations[0]
    };
  };

  module.exports = roll;
  module.exports.InvalidInputError = InvalidInputError;

}());

<|endoftext|>


<commit_before>var Mehdown;

if (typeof exports === 'object' && typeof require === 'function') {
    Mehdown = exports;
    Mehdown.baseUrl = process.env.NODE_ENV !== 'production' ? 'http://127.0.0.1:8000' : 'https://mediocre.com';
}
else {
    Mehdown = {};
    Mehdown.baseUrl = window.location.protocol + '//' + window.location.host;
}

Mehdown.usernameRegExp = /(^|[^@\w])@(\w{1,15})\b/g;

// Examples of what this will match: http://jsbin.com/eqocuh/552/edit
Mehdown.urlRegExp = /((?:(?:[A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+|(?:[A-Za-z0-9\.\-]+\.(?:com|org|net|gov|edu|co\.uk|io)))(?:(?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)(?:#?[\b\w\/!]+)(?:[\-\.\!\/\w]*))?)/g;

Mehdown.parse = function(text, opts) {
    // Make sure opts is defined
    opts = opts || {};

    text = text.trim();

    // Replace single newlines with
 tags: http://stackoverflow.com/questions/18011260/regex-to-match-single-new-line-regex-to-match-double-new-line
    text = text.replace(/\r\n/g, '\n');
    text = text.replace(/(^|[^\n])\n(?!\n)/g, '$1
');

    // Cleanup line breaks inside
 elements
    text = text.replace(/

/gi, '
');
    text = text.replace(/
<\/blockquote>/gi, '
');
    text = text.replace(/
([^)]+)<\/blockquote>/gi, function(t) {
        return t.replace(/
/gi, ' ');
    });

    // Cleanup line breaks around
 elements
    text = text.replace(/

/g, '
');
    text = text.replace(/<\/li>
/g, '
');

    // Strikethrough
    text = text.replace(/~~(.*)~~/g, '<s>$1</s>');

    // Any URLs that are not already anchored
    text = text.replace(Mehdown.urlRegExp, function() {
        var domain = arguments[1];
        var href = (domain.indexOf('http') < 0) ? 'http://' + domain : domain;
        var preceding = text.substring(0, arguments[2]);
        var insideTagAttrRegex = /<(?!\/)[^>]+(?!>)$/g;
        var insideAnchorTagRegex = /]*>(?!<\/a>)$/g;

        // use the correct protocol if domain is baseUrl domain
        if (href !== Mehdown.baseUrl && href.split('/')[2] === Mehdown.baseUrl.split('/')[2]) {
            href = Mehdown.baseUrl.split('://')[0] + '://' + domain;
        }

        // inside tag attr or anchor tag, abort
        if (preceding.match(insideTagAttrRegex) || preceding.match(insideAnchorTagRegex)) {
            return domain;
        }

        // don't use http protocol on email adresses
        if (domain.indexOf('@') >= 0) {
            var parts = domain.split('@');
            if (parts[1].match(Mehdown.urlRegExp)) {
                href = 'mailto:' + domain;
            } else {
                // this is a wierd something@something-no-tld that the regex thinks is a url, abort
                return domain;
            }
        }

        return '' + domain + '';
    });

    // Usernames
    text = text.replace(Mehdown.usernameRegExp, '$1' + Mehdown.username('$2'));

    // SoundCloud
    text = text.replace(/https?:\/\/(?:soundcloud.com|snd.sc)\/.*<\/a>/gi, Mehdown.soundcloud);

    // Twitter Status: http://stackoverflow.com/questions/4138483/twitter-status-url-regex
    text = text.replace(/https?:\/\/(?:www\.)?(?:twitter\.com\/)(?:#!\/)?(\w+)\/status(es)?\/(\d+)<\/a>/gi, Mehdown.twitter('$1', '$3'));

    // Vimeo
    text = text.replace(/https?:\/\/(?:www\.)?(?:vimeo\.com\/)?(\d+)<\/a>/g, Mehdown.vimeo('$1'));

    // Vine
    text = text.replace(/https?:\/\/(?:www\.)?(?:vine\.co\/v\/)?(\w+)<\/a>/g, Mehdown.vine('$1'));

    // YouTube URLs: http://stackoverflow.com/questions/2964678/jquery-youtube-url-validation-with-regex/10315969#10315969
    text = text.replace(/https?:\/\/(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&(?:amp;)?v=))((\w|-){11})(?:\S+)?<\/a>/g, Mehdown.youtube('$1'));

    //Kickstarter
    text = text.replace(/https?:\/\/(?:www\.)?(?:kickstarter\.com\/projects\/)?([\w-]+)\/([\w-]+).*<\/a>/g, Mehdown.kickstarter('$1', '$2'));

    // Archive.org games
    if (Mehdown.archiveGame) {
        text = text.replace(/https?:\/\/(?:www\.)?(?:archive\.org\/details\/)?(\w+)<\/a>/g, Mehdown.archiveGame);
    }

    // Images
    var imgRegExp = /\1<\/a>/gi;
    text = text.replace(imgRegExp, Mehdown.img('$1'));

    // Anchors
    var anchorRegExp = new RegExp(']*>([^<]+)', 'gi');
    text = text.replace(anchorRegExp, Mehdown.anchor);

    var headerRegExp = new RegExp('<h\\d[^>]*>(.+)</h\\d>', 'gi');
    text = text.replace(headerRegExp, Mehdown.header(opts.suffix));

    return text;
};

Mehdown.anchor = function(anchor) {
    var attributes = anchor.match(/([\w\-.:]+)\s*=\s*("[^"]*"|'[^']*'|[\w\-.:]+)/gi);
    var href = '';
    var isLocal = false;
    var localDomain = Mehdown.baseUrl.substring(Mehdown.baseUrl.indexOf('//') + 2);

    // Find href attribute
    for(var i = 0; i < attributes.length; i++) {
        if(attributes[i].indexOf('href=') === 0) {
            href = attributes[i];
            break;
        }
    }

    href = href.substring(href.indexOf('//') > 0 ? href.indexOf('//') + 2 : 0);

    if(href.indexOf('href="/') === 0 || href.indexOf(localDomain) === 0) {
        isLocal = true;
    }

    // Remove rel attributes
    anchor = anchor.replace(/rel[\s]*=[^\s]*\s*/gi, '');

    // Remove target attributes
    anchor = anchor.replace(/target[\s]*=[^\s]*\s*/gi, '');

    if(!isLocal) {
        anchor = '<a rel="nofollow" target="_blank"' + anchor.substr(2);
    }

    return anchor;
};

if (typeof exports === 'object' && typeof require === 'function') {
    var request = require('sync-request');

    Mehdown.archiveGame = function(match, game) {
        var res = request('GET', 'https://archive.org/details/' + game + '&output=json');

        body = JSON.parse(res.body.toString());

        if (body.metadata && body.metadata.emulator) {
            return '';
        } else if (body.metadata && body.metadata.description) {
            return '' + (body.metadata.description) + '';
        } else {
            return match;
        }
    };
}

Mehdown.header = function(suffix) {
    function toSlug(value) {
        return value
            .toLowerCase()
            .replace(/[^\w ]+/g, '')
            .replace(/ +/g, '-')
            .replace(/^-*/, '')
            .replace(/-*$/, '');
    }

    return function(header) {
        var text = header.replace(/<h\d[^>]*>(.+)<\/h\d>/, '$1');
        text = toSlug(text.replace(/<.*?>/g, ''));

        if (suffix) {
            suffix = toSlug(suffix);
            text = text + '-' + suffix;
        }

        return header.replace(/<h\d/, '$& id="' + text + '"');
    };
};

Mehdown.img = function(url) {
    return '';
};

Mehdown.soundcloud = function(match, url) {
    return '';
};

Mehdown.twitter = function(username, statusId) {
    var html = '

';
    html += '';

    return html;
};

Mehdown.username = function(username) {
    return '@' + username + '';
};

Mehdown.vimeo = function(videoId) {
    return '';
};

Mehdown.kickstarter = function(kickstarterId, slug) {
    return '';
};

Mehdown.vine = function(videoId) {
    return '';
};

Mehdown.youtube = function(videoId) {
    return '';
};
<commit_msg>Failsafe JSON parse<commit_after>var Mehdown;

if (typeof exports === 'object' && typeof require === 'function') {
    Mehdown = exports;
    Mehdown.baseUrl = process.env.NODE_ENV !== 'production' ? 'http://127.0.0.1:8000' : 'https://mediocre.com';
}
else {
    Mehdown = {};
    Mehdown.baseUrl = window.location.protocol + '//' + window.location.host;
}

Mehdown.usernameRegExp = /(^|[^@\w])@(\w{1,15})\b/g;

// Examples of what this will match: http://jsbin.com/eqocuh/552/edit
Mehdown.urlRegExp = /((?:(?:[A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+|(?:[A-Za-z0-9\.\-]+\.(?:com|org|net|gov|edu|co\.uk|io)))(?:(?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)(?:#?[\b\w\/!]+)(?:[\-\.\!\/\w]*))?)/g;

Mehdown.parse = function(text, opts) {
    // Make sure opts is defined
    opts = opts || {};

    text = text.trim();

    // Replace single newlines with
 tags: http://stackoverflow.com/questions/18011260/regex-to-match-single-new-line-regex-to-match-double-new-line
    text = text.replace(/\r\n/g, '\n');
    text = text.replace(/(^|[^\n])\n(?!\n)/g, '$1
');

    // Cleanup line breaks inside
 elements
    text = text.replace(/

/gi, '
');
    text = text.replace(/
<\/blockquote>/gi, '
');
    text = text.replace(/
([^)]+)<\/blockquote>/gi, function(t) {
        return t.replace(/
/gi, ' ');
    });

    // Cleanup line breaks around
 elements
    text = text.replace(/

/g, '
');
    text = text.replace(/<\/li>
/g, '
');

    // Strikethrough
    text = text.replace(/~~(.*)~~/g, '<s>$1</s>');

    // Any URLs that are not already anchored
    text = text.replace(Mehdown.urlRegExp, function() {
        var domain = arguments[1];
        var href = (domain.indexOf('http') < 0) ? 'http://' + domain : domain;
        var preceding = text.substring(0, arguments[2]);
        var insideTagAttrRegex = /<(?!\/)[^>]+(?!>)$/g;
        var insideAnchorTagRegex = /]*>(?!<\/a>)$/g;

        // use the correct protocol if domain is baseUrl domain
        if (href !== Mehdown.baseUrl && href.split('/')[2] === Mehdown.baseUrl.split('/')[2]) {
            href = Mehdown.baseUrl.split('://')[0] + '://' + domain;
        }

        // inside tag attr or anchor tag, abort
        if (preceding.match(insideTagAttrRegex) || preceding.match(insideAnchorTagRegex)) {
            return domain;
        }

        // don't use http protocol on email adresses
        if (domain.indexOf('@') >= 0) {
            var parts = domain.split('@');
            if (parts[1].match(Mehdown.urlRegExp)) {
                href = 'mailto:' + domain;
            } else {
                // this is a wierd something@something-no-tld that the regex thinks is a url, abort
                return domain;
            }
        }

        return '' + domain + '';
    });

    // Usernames
    text = text.replace(Mehdown.usernameRegExp, '$1' + Mehdown.username('$2'));

    // SoundCloud
    text = text.replace(/https?:\/\/(?:soundcloud.com|snd.sc)\/.*<\/a>/gi, Mehdown.soundcloud);

    // Twitter Status: http://stackoverflow.com/questions/4138483/twitter-status-url-regex
    text = text.replace(/https?:\/\/(?:www\.)?(?:twitter\.com\/)(?:#!\/)?(\w+)\/status(es)?\/(\d+)<\/a>/gi, Mehdown.twitter('$1', '$3'));

    // Vimeo
    text = text.replace(/https?:\/\/(?:www\.)?(?:vimeo\.com\/)?(\d+)<\/a>/g, Mehdown.vimeo('$1'));

    // Vine
    text = text.replace(/https?:\/\/(?:www\.)?(?:vine\.co\/v\/)?(\w+)<\/a>/g, Mehdown.vine('$1'));

    // YouTube URLs: http://stackoverflow.com/questions/2964678/jquery-youtube-url-validation-with-regex/10315969#10315969
    text = text.replace(/https?:\/\/(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&(?:amp;)?v=))((\w|-){11})(?:\S+)?<\/a>/g, Mehdown.youtube('$1'));

    //Kickstarter
    text = text.replace(/https?:\/\/(?:www\.)?(?:kickstarter\.com\/projects\/)?([\w-]+)\/([\w-]+).*<\/a>/g, Mehdown.kickstarter('$1', '$2'));

    // Archive.org games
    if (Mehdown.archiveGame) {
        text = text.replace(/https?:\/\/(?:www\.)?(?:archive\.org\/details\/)?(\w+)<\/a>/g, Mehdown.archiveGame);
    }

    // Images
    var imgRegExp = /\1<\/a>/gi;
    text = text.replace(imgRegExp, Mehdown.img('$1'));

    // Anchors
    var anchorRegExp = new RegExp(']*>([^<]+)', 'gi');
    text = text.replace(anchorRegExp, Mehdown.anchor);

    var headerRegExp = new RegExp('<h\\d[^>]*>(.+)</h\\d>', 'gi');
    text = text.replace(headerRegExp, Mehdown.header(opts.suffix));

    return text;
};

Mehdown.anchor = function(anchor) {
    var attributes = anchor.match(/([\w\-.:]+)\s*=\s*("[^"]*"|'[^']*'|[\w\-.:]+)/gi);
    var href = '';
    var isLocal = false;
    var localDomain = Mehdown.baseUrl.substring(Mehdown.baseUrl.indexOf('//') + 2);

    // Find href attribute
    for(var i = 0; i < attributes.length; i++) {
        if(attributes[i].indexOf('href=') === 0) {
            href = attributes[i];
            break;
        }
    }

    href = href.substring(href.indexOf('//') > 0 ? href.indexOf('//') + 2 : 0);

    if(href.indexOf('href="/') === 0 || href.indexOf(localDomain) === 0) {
        isLocal = true;
    }

    // Remove rel attributes
    anchor = anchor.replace(/rel[\s]*=[^\s]*\s*/gi, '');

    // Remove target attributes
    anchor = anchor.replace(/target[\s]*=[^\s]*\s*/gi, '');

    if(!isLocal) {
        anchor = '<a rel="nofollow" target="_blank"' + anchor.substr(2);
    }

    return anchor;
};

if (typeof exports === 'object' && typeof require === 'function') {
    var request = require('sync-request');

    Mehdown.archiveGame = function(match, game) {
        var res = request('GET', 'https://archive.org/details/' + game + '&output=json');

        try {
            body = JSON.parse(res.body.toString());
        } catch {
            return match;
        }

        if (body.metadata && body.metadata.emulator) {
            return '';
        } else if (body.metadata && body.metadata.description) {
            return '' + (body.metadata.description) + '';
        } else {
            return match;
        }
    };
}

Mehdown.header = function(suffix) {
    function toSlug(value) {
        return value
            .toLowerCase()
            .replace(/[^\w ]+/g, '')
            .replace(/ +/g, '-')
            .replace(/^-*/, '')
            .replace(/-*$/, '');
    }

    return function(header) {
        var text = header.replace(/<h\d[^>]*>(.+)<\/h\d>/, '$1');
        text = toSlug(text.replace(/<.*?>/g, ''));

        if (suffix) {
            suffix = toSlug(suffix);
            text = text + '-' + suffix;
        }

        return header.replace(/<h\d/, '$& id="' + text + '"');
    };
};

Mehdown.img = function(url) {
    return '';
};

Mehdown.soundcloud = function(match, url) {
    return '';
};

Mehdown.twitter = function(username, statusId) {
    var html = '

';
    html += '';

    return html;
};

Mehdown.username = function(username) {
    return '@' + username + '';
};

Mehdown.vimeo = function(videoId) {
    return '';
};

Mehdown.kickstarter = function(kickstarterId, slug) {
    return '';
};

Mehdown.vine = function(videoId) {
    return '';
};

Mehdown.youtube = function(videoId) {
    return '';
};
<|endoftext|>


<commit_before>var _ = require('lodash');
var Q = require('q');
var path = require('path');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var crypto = require('crypto');
var mime = require('mime-types');

var types = require('./types');
var errors = require('./errors');
var patch = require('./utils/patch');
var arrayBuffer = require('./utils/arraybuffer');

// Bind and ensure validity of argument for method on ref
function refMethod(fn) {
    return function() {
        var args = _.toArray(arguments);
        if (!_.isPlainObject(_.last(args))) {
            args.push({});
        }

        var opts = args[args.length - 1];

        opts = _.defaults(opts, {
            ref: "master"
        });

        if (!opts.ref) return Q.reject(errors.invalidArgs('Need valid "ref" options'));
        return fn.apply(this, args);
    }
}

// Normalize a path
function normPath(p) {
    p = path.normalize(p);
    if (p[0] == '/') p = p.slice(1);
    if (p[p.length - 1] == '/') p = p.slice(0, -1);
    if (p == '.') p = "";
    return p;
}

// Get gravatar url
function gravatarUrl(email) {
    var shasum = crypto.createHash('md5');
    shasum.update(email);
    return "http://www.gravatar.com/avatar/"+shasum.digest('hex')+"?s=200&d=identicon";
}

function Fs(Driver, opts) {
    if (!(this instanceof Fs)) return (new Fs(Driver, opts));

    this.options = _.defaults(opts || {}, {
        commiter: {}
    });

    if (!this.options.commiter
    || !this.options.commiter.name
    || !this.options.commiter.email) throw "Need a valid 'commiter' as option";

    // Driver client
    this.driver = new Driver(this.options, this);
    this.type = Driver.id;

    // Cache of path => sha
    this.shas = {};

    _.bindAll(this);
}

util.inherits(Fs, EventEmitter);

// Bind a file object (returned from fs.stat or fs.readdir)
Fs.prototype._bindFile = function(file, encoding) {
    encoding = encoding === undefined? 'utf8' : encoding;

    this.shas[file.path] = file.sha;
    file.isDirectory = file.type == types.DIRECTORY;
    file.path = normPath(file.path);
    file.mime = file.mime || mime.lookup(path.extname(file.path)) || 'application/octet-stream';

    if (file.content) {
        if (encoding === null) file.content = arrayBuffer.enforceArrayBuffer(file.content);
        else file.content = arrayBuffer.enforceString(file.content, encoding);
    }

    return file;
};

// Bind an author
Fs.prototype._bindAuthor = function(author) {
    author.avatar = author.avatar || gravatarUrl(author.email);
    return author;
};

// Bind a commit object (returned from fs.listCommits or fs.getCommit)
Fs.prototype._bindCommit = function(commit) {
    commit.date = new Date(commit.date);
    commit.author = this._bindAuthor(commit.author);
    return commit;
};

// Trigger a watcher event
Fs.prototype.emitWatch = function(type, file) {
    file = normPath(file);
    var data = {
        type: type,
        path: file
    };

    this.emit('watch', data);
    this.emit('watch.'+type, data);
};

Fs.prototype.stat = refMethod(function stat(p, opts) {
    var that = this;
    opts = _.defaults(opts, {
        encoding: 'utf8'
    });

    p = normPath(p);

    return Q()
    .then(function() {
        return that.driver.stat(p, opts);
    })
    .then(function(file) {
        return that._bindFile(file, opts.encoding);
    });
});

Fs.prototype.readdir = refMethod(function readdir(p, opts) {
    var that = this;

    if (_.isPlainObject(p)) {
        opts = p;
        p = null;
    }

    p = normPath(p || '');
    opts = _.defaults(opts, {
        encoding: 'utf8'
    });

    return Q()
    .then(function() {
        return that.driver.readdir(p, opts);
    })
    .then(function(files) {
        return _.mapValues(files, function(file) {
            return that._bindFile(file, opts.encoding);
        }, that);
    });
});

Fs.prototype.read = refMethod(function read(p, opts) {
    return this.stat(p, opts).get('content');
});

Fs.prototype.write = refMethod(function write(p, content, opts) {
    var that = this;

    p = normPath(p);
    opts = _.defaults(opts || {}, {
        encoding: 'utf8',
        message: "Update "+p,
        preventOverride: true
    });

    // Force content as arraybuffer
    content = arrayBuffer.enforceArrayBuffer(content, opts.encoding);

    return Q()

    // Prevent ovveride
    .then(function() {
        if (!opts.preventOverride) return;

        // Get sha of file on disk
        return Q(that.driver.stat(p, { ref: opts.ref }))
        .then(function(infos) {
            // Check if different from cache
            if (opts.preventOverride && that.shas[infos.path] && that.shas[infos.path] != infos.sha) {
                throw errors.fileHasBeenModified(infos.path);
            }
        })
    })

    // Write using driver
    .then(function() {
        return that.driver.write(p, content, opts);
    })

    // Return normalized file
    .then(function(file) {
        that.emitWatch('change', p);

        if (!file) return that.stat(p, { ref: opts.ref });
        return  that._bindFile(file, opts.encoding);
    });
});

Fs.prototype.create = refMethod(function create(p, content, opts) {
    var that = this;

    p = normPath(p);
    opts = _.defaults(opts || {}, {
        encoding: 'utf8',
        message: "Create "+p
    });

    // Force content as arraybuffer
    content = arrayBuffer.enforceArrayBuffer(content, opts.encoding);

    return Q()

    // Create file
    .then(function() {
        return that.driver.create(p, content, opts);
    })

    // Return normalized file
    .then(function(file) {
        that.emitWatch('add', p);

        if (!file) return that.stat(p, { ref: opts.ref });
        return  that._bindFile(file, opts.encoding);
    });
});

Fs.prototype.update = refMethod(function update(p, content, opts) {
    var that = this;

    p = normPath(p);
    return that.create(p, content, opts)
    .fail(function(err) {
        if (err.code != 409) throw err;
        return that.write(p, content, opts);
    });
});

Fs.prototype.exists = refMethod(function exists(p, opts) {
    return this.stat(p, opts)
    .then(function() {
        return true;
    }, function() {
        return Q(false);
    });
});

Fs.prototype.unlink = refMethod(function unlink(p, opts) {
    var that = this;

    p = normPath(p);
    opts = _.defaults(opts || {}, {
        message: "Delete "+p
    });

    return Q()
    .then(function() {
        return that.driver.unlink(p, opts);
    })
    .then(function() {
        that.emitWatch('unlink', p);

        return undefined;
    });
});

Fs.prototype.move = refMethod(function move(from, to, opts) {
    var that = this;

    from = normPath(from);
    to = normPath(to);

    return Q.all([
        that.read(from, opts),
        that.exists(to, opts)
    ])
    .spread(function(content, exists) {
        if (exists) throw errors.fileAlreadyExist(to);
        return that.create(to, content, opts);
    })
    .then(function(newFile) {
        return that.unlink(from)
        .thenResolve(newFile);
    });
});
Fs.prototype.rename = Fs.prototype.move;

Fs.prototype.listBranches = function listBranches() {
    var that = this;

    return Q()
    .then(function() {
        return that.driver.listBranches();
    });
};

Fs.prototype.createBranch = function createBranch(name, from) {
    var that = this;
    from = from || "master";

    return Q()
    .then(function() {
        if (!name) throw "Need a branch name";

        return that.driver.createBranch(name, from);
    });
};

Fs.prototype.removeBranch = function removeBranch(name) {
    var that = this;

    return Q()
    .then(function() {
        if (!name) throw "Need a branch name";

        return that.driver.removeBranch(name);
    });
};

Fs.prototype.mergeBranches = function mergeBranches(from, to, opts) {
    var that = this;

    opts = _.defaults(opts || {}, {
        message: "Merge "+from+" into "+to
    });

    return Q()
    .then(function() {
        if (!from) throw "Need a branch to merge";
        if (!to) throw "Need a branch to merge into";

        return that.driver.mergeBranches(from, to, opts);
    });
};

Fs.prototype.listCommits = refMethod(function(opts) {
    var that = this;
    opts = _.defaults(opts || {}, {
        start: 0,
        limit: 100
    });

    return Q()
    .then(function() {
        return that.driver.listCommits(opts);
    })
    .then(function(commits) {
        return _.map(commits, that._bindCommit, that);
    });
});

Fs.prototype.getCommit = function(sha, opts) {
    var that = this;

    opts = _.defaults(opts || {}, {
        parsePatches: true
    });

    return Q()
    .then(function() {
        return that.driver.getCommit(sha);
    })
    .then(function(commit) {
        commit = that._bindCommit(commit);
        commit.files = _.map(commit.files || [], function(file) {
            file.patch = patch.parse(file.patch);
            return file;
        });

        return commit;
    });
};

module.exports = Fs;
module.exports.types = types;
<commit_msg>Add events for branches<commit_after>var _ = require('lodash');
var Q = require('q');
var path = require('path');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var crypto = require('crypto');
var mime = require('mime-types');

var types = require('./types');
var errors = require('./errors');
var patch = require('./utils/patch');
var arrayBuffer = require('./utils/arraybuffer');

// Bind and ensure validity of argument for method on ref
function refMethod(fn) {
    return function() {
        var args = _.toArray(arguments);
        if (!_.isPlainObject(_.last(args))) {
            args.push({});
        }

        var opts = args[args.length - 1];

        opts = _.defaults(opts, {
            ref: "master"
        });

        if (!opts.ref) return Q.reject(errors.invalidArgs('Need valid "ref" options'));
        return fn.apply(this, args);
    }
}

// Normalize a path
function normPath(p) {
    p = path.normalize(p);
    if (p[0] == '/') p = p.slice(1);
    if (p[p.length - 1] == '/') p = p.slice(0, -1);
    if (p == '.') p = "";
    return p;
}

// Get gravatar url
function gravatarUrl(email) {
    var shasum = crypto.createHash('md5');
    shasum.update(email);
    return "http://www.gravatar.com/avatar/"+shasum.digest('hex')+"?s=200&d=identicon";
}

function Fs(Driver, opts) {
    if (!(this instanceof Fs)) return (new Fs(Driver, opts));

    this.options = _.defaults(opts || {}, {
        commiter: {}
    });

    if (!this.options.commiter
    || !this.options.commiter.name
    || !this.options.commiter.email) throw "Need a valid 'commiter' as option";

    // Driver client
    this.driver = new Driver(this.options, this);
    this.type = Driver.id;

    // Cache of path => sha
    this.shas = {};

    _.bindAll(this);
}

util.inherits(Fs, EventEmitter);

// Bind a file object (returned from fs.stat or fs.readdir)
Fs.prototype._bindFile = function(file, encoding) {
    encoding = encoding === undefined? 'utf8' : encoding;

    this.shas[file.path] = file.sha;
    file.isDirectory = file.type == types.DIRECTORY;
    file.path = normPath(file.path);
    file.mime = file.mime || mime.lookup(path.extname(file.path)) || 'application/octet-stream';

    if (file.content) {
        if (encoding === null) file.content = arrayBuffer.enforceArrayBuffer(file.content);
        else file.content = arrayBuffer.enforceString(file.content, encoding);
    }

    return file;
};

// Bind an author
Fs.prototype._bindAuthor = function(author) {
    author.avatar = author.avatar || gravatarUrl(author.email);
    return author;
};

// Bind a commit object (returned from fs.listCommits or fs.getCommit)
Fs.prototype._bindCommit = function(commit) {
    commit.date = new Date(commit.date);
    commit.author = this._bindAuthor(commit.author);
    return commit;
};

// Trigger a watcher event
Fs.prototype.emitWatch = function(type, file) {
    file = normPath(file);
    var data = {
        type: type,
        path: file
    };

    this.emit('watch', data);
    this.emit('watch.'+type, data);
};

// Trigger a branch event
Fs.prototype.emitBranch = function(type, branch) {
    var data = {
        type: type,
        branch: branch
    };

    this.emit('branches', data);
    this.emit('branches.'+type, data);
};

Fs.prototype.stat = refMethod(function stat(p, opts) {
    var that = this;
    opts = _.defaults(opts, {
        encoding: 'utf8'
    });

    p = normPath(p);

    return Q()
    .then(function() {
        return that.driver.stat(p, opts);
    })
    .then(function(file) {
        return that._bindFile(file, opts.encoding);
    });
});

Fs.prototype.readdir = refMethod(function readdir(p, opts) {
    var that = this;

    if (_.isPlainObject(p)) {
        opts = p;
        p = null;
    }

    p = normPath(p || '');
    opts = _.defaults(opts, {
        encoding: 'utf8'
    });

    return Q()
    .then(function() {
        return that.driver.readdir(p, opts);
    })
    .then(function(files) {
        return _.mapValues(files, function(file) {
            return that._bindFile(file, opts.encoding);
        }, that);
    });
});

Fs.prototype.read = refMethod(function read(p, opts) {
    return this.stat(p, opts).get('content');
});

Fs.prototype.write = refMethod(function write(p, content, opts) {
    var that = this;

    p = normPath(p);
    opts = _.defaults(opts || {}, {
        encoding: 'utf8',
        message: "Update "+p,
        preventOverride: true
    });

    // Force content as arraybuffer
    content = arrayBuffer.enforceArrayBuffer(content, opts.encoding);

    return Q()

    // Prevent ovveride
    .then(function() {
        if (!opts.preventOverride) return;

        // Get sha of file on disk
        return Q(that.driver.stat(p, { ref: opts.ref }))
        .then(function(infos) {
            // Check if different from cache
            if (opts.preventOverride && that.shas[infos.path] && that.shas[infos.path] != infos.sha) {
                throw errors.fileHasBeenModified(infos.path);
            }
        })
    })

    // Write using driver
    .then(function() {
        return that.driver.write(p, content, opts);
    })

    // Return normalized file
    .then(function(file) {
        that.emitWatch('change', p);

        if (!file) return that.stat(p, { ref: opts.ref });
        return  that._bindFile(file, opts.encoding);
    });
});

Fs.prototype.create = refMethod(function create(p, content, opts) {
    var that = this;

    p = normPath(p);
    opts = _.defaults(opts || {}, {
        encoding: 'utf8',
        message: "Create "+p
    });

    // Force content as arraybuffer
    content = arrayBuffer.enforceArrayBuffer(content, opts.encoding);

    return Q()

    // Create file
    .then(function() {
        return that.driver.create(p, content, opts);
    })

    // Return normalized file
    .then(function(file) {
        that.emitWatch('add', p);

        if (!file) return that.stat(p, { ref: opts.ref });
        return  that._bindFile(file, opts.encoding);
    });
});

Fs.prototype.update = refMethod(function update(p, content, opts) {
    var that = this;

    p = normPath(p);
    return that.create(p, content, opts)
    .fail(function(err) {
        if (err.code != 409) throw err;
        return that.write(p, content, opts);
    });
});

Fs.prototype.exists = refMethod(function exists(p, opts) {
    return this.stat(p, opts)
    .then(function() {
        return true;
    }, function() {
        return Q(false);
    });
});

Fs.prototype.unlink = refMethod(function unlink(p, opts) {
    var that = this;

    p = normPath(p);
    opts = _.defaults(opts || {}, {
        message: "Delete "+p
    });

    return Q()
    .then(function() {
        return that.driver.unlink(p, opts);
    })
    .then(function() {
        that.emitWatch('unlink', p);

        return undefined;
    });
});

Fs.prototype.move = refMethod(function move(from, to, opts) {
    var that = this;

    from = normPath(from);
    to = normPath(to);

    return Q.all([
        that.read(from, opts),
        that.exists(to, opts)
    ])
    .spread(function(content, exists) {
        if (exists) throw errors.fileAlreadyExist(to);
        return that.create(to, content, opts);
    })
    .then(function(newFile) {
        return that.unlink(from)
        .thenResolve(newFile);
    });
});
Fs.prototype.rename = Fs.prototype.move;

Fs.prototype.listBranches = function listBranches() {
    var that = this;

    return Q()
    .then(function() {
        return that.driver.listBranches();
    });
};

Fs.prototype.createBranch = function createBranch(name, from) {
    var that = this;
    from = from || "master";

    return Q()
    .then(function() {
        if (!name) throw "Need a branch name";

        return that.driver.createBranch(name, from);
    })
    .then(function(branch) {
        that.emitBranch('add', name);
        return branch;
    });
};

Fs.prototype.removeBranch = function removeBranch(name) {
    var that = this;

    return Q()
    .then(function() {
        if (!name) throw "Need a branch name";

        return that.driver.removeBranch(name);
    })
    .then(function() {
        that.emitBranch('remove', name);
    });
};

Fs.prototype.mergeBranches = function mergeBranches(from, to, opts) {
    var that = this;

    opts = _.defaults(opts || {}, {
        message: "Merge "+from+" into "+to
    });

    return Q()
    .then(function() {
        if (!from) throw "Need a branch to merge";
        if (!to) throw "Need a branch to merge into";

        return that.driver.mergeBranches(from, to, opts);
    });
};

Fs.prototype.listCommits = refMethod(function(opts) {
    var that = this;
    opts = _.defaults(opts || {}, {
        start: 0,
        limit: 100
    });

    return Q()
    .then(function() {
        return that.driver.listCommits(opts);
    })
    .then(function(commits) {
        return _.map(commits, that._bindCommit, that);
    });
});

Fs.prototype.getCommit = function(sha, opts) {
    var that = this;

    opts = _.defaults(opts || {}, {
        parsePatches: true
    });

    return Q()
    .then(function() {
        return that.driver.getCommit(sha);
    })
    .then(function(commit) {
        commit = that._bindCommit(commit);
        commit.files = _.map(commit.files || [], function(file) {
            file.patch = patch.parse(file.patch);
            return file;
        });

        return commit;
    });
};

module.exports = Fs;
module.exports.types = types;
<|endoftext|>


<commit_before>'use strict'
const path = require('path')
const Promise = require('bluebird')
const pgpOptions = {promiseLib: Promise}
const pgp = require('pg-promise')(pgpOptions)
const Job = require('./job')
const _ = require('lodash')
const errors = require('./errors')

// var monitor = require('pg-monitor')
// monitor.attach(pgpOptions)
// monitor.setTheme('matrix')

const nextJobSQL = new pgp.QueryFile(path.join(__dirname, './sql/getJob.sql'))
var schemaPath = path.join(__dirname, 'schema.sql')
const schemaFile = new pgp.QueryFile(schemaPath, {minify: true})

function JobQueue(connectionString) {
    this.db = pgp(connectionString)
    this.jobHandlers = {}
    this.shutdown = false
    this.stopProcessingCallback = null
}

/*
Close all connections to the database.
*/
JobQueue.prototype.disconnect = function() {
    pgp.end()
}

/*
Install schema to the database.
*/
JobQueue.prototype.installSchema = function() {
    return this.db.none(schemaFile)
}

/*
Clear the entire job queue.
*/
JobQueue.prototype.clearAllJobs = function() {
    return this.db.none('TRUNCATE table "JobQueue"')
}

/*
Set the job handlers, replacing any previous handlers.
This should be an object with a key of the job type, and value being the function for processing the job.
e.g.
{ sendEmail: () => { sendmail() }
*/
JobQueue.prototype.setHandlers = function(handlers) {
    this.jobHandlers = handlers
}

/*
Add a job to the queue.
*/
JobQueue.prototype.addJob = function(job) {
    if (!_.isObject(job)) {
        throw new TypeError('job is not an object')
    }

    job.scheduledFor = job.scheduledFor || new Date()
    job.maxAttempts = job.maxAttempts || 1
    job.data = job.data || {}

    if (!job.type) {
        throw new TypeError("property 'type' not specified")
    }
    if (!_.isString(job.type)) {
        throw new TypeError("property 'type' is not a string")
    }
    if (!_.isDate(job.scheduledFor)) {
        throw new TypeError("property 'scheduledFor' is not a date")
    }
    if (!_.isNumber(job.maxAttempts)) {
        throw new TypeError("property 'maxAttempts' is not a number")
    }
    else if (job.maxAttempts < 0) {
        throw new TypeError("property 'maxAttempts' is negative")
    }

    var query = `
        INSERT INTO "JobQueue"
        ("type", "data", "scheduledFor", "maxAttempts", "createdAt")
        VALUES ($[type], $[data], $[scheduledFor], $[maxAttempts], NOW() )`

    return this.db.none(query, job)
}

/*
Grab the next job with a known handler, process it.
If no jobs are found, throw errors.JobQueueEmpty
*/
JobQueue.prototype.processNextJob = function() {
    var types = _.keys(this.jobHandlers)

    // we use a task, to ensure the same connection is used
    // this is important because we use a session-level advisory lock
    return this.db.task((t) => {
        // nextJobSQL acquires a pg_advisory_lock
        return t.oneOrNone(nextJobSQL, {types: types})
        .then((result) => {
            if (!result) {
                throw new errors.JobQueueEmpty()
            }
            var job = new Job(result, this.db)

            return Promise.try(() => {
                return this.jobHandlers[result.type](job)
            })
            .catch((e) => {
                return job.fail(e.message)
            })
            .finally(() => {
                // release the advisory lock
                return t.one('SELECT pg_advisory_unlock("id") FROM "JobQueue" WHERE "id"=$1', result.id)
            })
        })
    })
}

/*
Process all jobs (one at a time).
When no jobs are available, throws errors.JobQueueEmpty
*/
JobQueue.prototype.processAllJobs = function() {
    return this.processNextJob().then(() => {
        return this.processAllJobs()
    })
}

/*
Begin an infinite loop of job-processing.
We continually poll for new jobs, waiting for $delay milliseconds between each poll. (default 500 milliseconds)
*/
JobQueue.prototype.startProcessing = function(delay) {
    function loop() {
        if (this.shutdown) {
            if (this.stopProcessingCallback) {
                this.processing = false
                this.stopProcessingCallback()
            }
            return Promise.resolve()
        }
        return this.processAllJobs().catch((e) => {
            // ignore JobQueueEmpty exceptions, we must continue our loop
            if (!(e instanceof errors.JobQueueEmpty)) {
                throw e
            }
        })
        .finally(() => {
            return Promise.delay(delay || 500).then(loop)
        })
    }
    loop = _.bind(loop, this)
    if (this.processing) {
        throw new Error("already processing")
    }
    this.processing = true
    return loop()
}

JobQueue.prototype.stopProcessing = function() {
    if (!this.processing) {
        console.warn('stopProcessing was requested, but we are not currently processing.')
        return
    }
    this.shutdown = true

    return new Promise((fulfill) => {
        this.stopProcessingCallback = fulfill
    })
}

/*
Returns all failed jobs.
*/
JobQueue.prototype.getFailedJobs = function() {
    return this.db.manyOrNone('SELECT * FROM "JobQueue" WHERE "state"=$1', 'failed')
}

/*
Returns current job queue length
*/
JobQueue.prototype.waitingCount = function() {
    return this.db.one('SELECT COUNT(*) FROM "JobQueue" WHERE "state"=$1', 'waiting').then((result) => {
        return _.parseInt(result.count)
    })
}

JobQueue.prototype.errors = require('./errors')

module.exports = JobQueue<commit_msg>fix this.processing would not be set to false<commit_after>'use strict'
const path = require('path')
const Promise = require('bluebird')
const pgpOptions = {promiseLib: Promise}
const pgp = require('pg-promise')(pgpOptions)
const Job = require('./job')
const _ = require('lodash')
const errors = require('./errors')

// var monitor = require('pg-monitor')
// monitor.attach(pgpOptions)
// monitor.setTheme('matrix')

const nextJobSQL = new pgp.QueryFile(path.join(__dirname, './sql/getJob.sql'))
var schemaPath = path.join(__dirname, 'schema.sql')
const schemaFile = new pgp.QueryFile(schemaPath, {minify: true})

function JobQueue(connectionString) {
    this.db = pgp(connectionString)
    this.jobHandlers = {}
    this.shutdown = false
    this.stopProcessingCallback = null
}

/*
Close all connections to the database.
*/
JobQueue.prototype.disconnect = function() {
    pgp.end()
}

/*
Install schema to the database.
*/
JobQueue.prototype.installSchema = function() {
    return this.db.none(schemaFile)
}

/*
Clear the entire job queue.
*/
JobQueue.prototype.clearAllJobs = function() {
    return this.db.none('TRUNCATE table "JobQueue"')
}

/*
Set the job handlers, replacing any previous handlers.
This should be an object with a key of the job type, and value being the function for processing the job.
e.g.
{ sendEmail: () => { sendmail() }
*/
JobQueue.prototype.setHandlers = function(handlers) {
    this.jobHandlers = handlers
}

/*
Add a job to the queue.
*/
JobQueue.prototype.addJob = function(job) {
    if (!_.isObject(job)) {
        throw new TypeError('job is not an object')
    }

    job.scheduledFor = job.scheduledFor || new Date()
    job.maxAttempts = job.maxAttempts || 1
    job.data = job.data || {}

    if (!job.type) {
        throw new TypeError("property 'type' not specified")
    }
    if (!_.isString(job.type)) {
        throw new TypeError("property 'type' is not a string")
    }
    if (!_.isDate(job.scheduledFor)) {
        throw new TypeError("property 'scheduledFor' is not a date")
    }
    if (!_.isNumber(job.maxAttempts)) {
        throw new TypeError("property 'maxAttempts' is not a number")
    }
    else if (job.maxAttempts < 0) {
        throw new TypeError("property 'maxAttempts' is negative")
    }

    var query = `
        INSERT INTO "JobQueue"
        ("type", "data", "scheduledFor", "maxAttempts", "createdAt")
        VALUES ($[type], $[data], $[scheduledFor], $[maxAttempts], NOW() )`

    return this.db.none(query, job)
}

/*
Grab the next job with a known handler, process it.
If no jobs are found, throw errors.JobQueueEmpty
*/
JobQueue.prototype.processNextJob = function() {
    var types = _.keys(this.jobHandlers)

    // we use a task, to ensure the same connection is used
    // this is important because we use a session-level advisory lock
    return this.db.task((t) => {
        // nextJobSQL acquires a pg_advisory_lock
        return t.oneOrNone(nextJobSQL, {types: types})
        .then((result) => {
            if (!result) {
                throw new errors.JobQueueEmpty()
            }
            var job = new Job(result, this.db)

            return Promise.try(() => {
                return this.jobHandlers[result.type](job)
            })
            .catch((e) => {
                return job.fail(e.message)
            })
            .finally(() => {
                // release the advisory lock
                return t.one('SELECT pg_advisory_unlock("id") FROM "JobQueue" WHERE "id"=$1', result.id)
            })
        })
    })
}

/*
Process all jobs (one at a time).
When no jobs are available, throws errors.JobQueueEmpty
*/
JobQueue.prototype.processAllJobs = function() {
    return this.processNextJob().then(() => {
        return this.processAllJobs()
    })
}

/*
Begin an infinite loop of job-processing.
We continually poll for new jobs, waiting for $delay milliseconds between each poll. (default 500 milliseconds)
*/
JobQueue.prototype.startProcessing = function(delay) {
    function loop() {
        if (this.shutdown) {
            this.processing = false
            if (this.stopProcessingCallback) {
                this.stopProcessingCallback()
            }
            return Promise.resolve()
        }
        return this.processAllJobs().catch((e) => {
            // ignore JobQueueEmpty exceptions, we must continue our loop
            if (!(e instanceof errors.JobQueueEmpty)) {
                throw e
            }
        })
        .finally(() => {
            return Promise.delay(delay || 500).then(loop)
        })
    }
    loop = _.bind(loop, this)
    if (this.processing) {
        throw new Error("already processing")
    }
    this.processing = true
    return loop()
}

JobQueue.prototype.stopProcessing = function() {
    if (!this.processing) {
        console.warn('stopProcessing was requested, but we are not currently processing.')
        return
    }
    this.shutdown = true

    return new Promise((fulfill) => {
        this.stopProcessingCallback = fulfill
    })
}

/*
Returns all failed jobs.
*/
JobQueue.prototype.getFailedJobs = function() {
    return this.db.manyOrNone('SELECT * FROM "JobQueue" WHERE "state"=$1', 'failed')
}

/*
Returns current job queue length
*/
JobQueue.prototype.waitingCount = function() {
    return this.db.one('SELECT COUNT(*) FROM "JobQueue" WHERE "state"=$1', 'waiting').then((result) => {
        return _.parseInt(result.count)
    })
}

JobQueue.prototype.errors = require('./errors')

module.exports = JobQueue<|endoftext|>