notduncansmith/summit

View on GitHub
lib/collection.js

Summary

Maintainability
C
1 day
Test Coverage
var _ = require('lodash')
  , makeForm = require('./collection/forms.js')
  , Promise = require('bluebird')
  , Item = require('./collection/item')
  , UserCollection = require('./collection/user')
  , GroupCollection = require('./collection/group')
  , routeHelpers = require('./collection/collection_routes')
  , helpers = require('./collection/collection_helpers')
  , EventEmitter = require('events').EventEmitter
  , moment = require('moment');

module.exports = Collection;

function Collection (collection, app) {
  this.router = app.router();

  this._ee = new EventEmitter();
  this.name = collection.name;
  this.app = app;
  this.db = app.db;
  this.collection = collection;
  this.mergeCollection(collection);
  this.dbSafeName = helpers.dbSafeName(this.collection.name);

  if (collection.restful) {
    routeHelpers.apiRoutes(this);
    routeHelpers.methodRoutes(this);
  }

  _.extend(this, this.methods);
}

Collection.prototype.fields = {};

Collection.prototype.design = {
  updates: {
    merge: function (doc, req) {
      if (!doc) {
        return [null, 'Error: Merge called on nonexistent doc'];
      }

      if (req.form.type !== doc.type) {
        return [null, 'Error: Doc type mismatch'];
      }

      for (var field in req.form) {
        doc[field] = req.form[field];
      }

      return [doc, toJSON(doc)];
    },

    write: function (doc, req) {
      var newDoc = req.form;
      var doc = doc || {type: '{{name}}'};

      if (req.form.type !== doc.type) {
        return [null, 'Error: doc type mismatch'];
      }

      newDoc._id = doc._id || req.uuid;
      newDoc._rev = doc._rev;

      return [newDoc, toJSON(newDoc)];
    },
  },
  lists: {},
  shows: {},
  views: {
    all: {
      map: function (doc) {
        if (doc.type === '{{name}}') {
          emit(doc._id, doc);
        }
      }
    }
  }
};

Collection.prototype.applyCollectionType = function (collectionType) {
  _.extend(this, collectionType.prototype);
  this.design = _.merge({}, this.design, collectionType.design || {});
  this.fields = _.extend({}, this.fields, collectionType.fields);
};

Collection.prototype.mergeCollection = function (collection) {
  if (collection.isUserType) {
    this.applyCollectionType(UserCollection);
    this.isUserType = true;
  }

  if (collection.isGroupType) {
    this.applyCollectionType(GroupCollection);
    this.isGroupType = true;
  }

  this.design = _.merge({}, this.design, collection.design || {});
  this.design = _.merge({}, this.design, _.pick(collection, ['views', 'filters', 'updates', 'lists', 'shows']));
  this.fields = _.extend({}, this.fields, collection.fields || {});
  this.methods = _.merge({}, this.methods, collection.methods || {});

  if (collection.timestamps !== false) {
    this.timestamps = true;
  }

  if (collection.setup) {
    this.setup = collection.setup;
  }
};

Collection.prototype.get = function (id, opts) {
  return this.db.get(id)
  .then(function (results) {
    if ((!opts || !opts.raw) && (typeof id === 'string')) {
      return results[0];
    }
    else {
      return results;
    }
  });
};

Collection.prototype.all = function (params) {
  return this.view('all', params);
};

Collection.prototype.update = function (updateName, doc, opts) {
  var self = this;
  doc.type = this.name;

  if (this.timestamps) {
    doc.updatedAt = moment().toISOString();
    if (!doc._id) {
      doc.createdAt = doc.updatedAt;
    }
  }

  this.emit('update:before', {updateName: updateName, doc: doc});

  var name = this.dbSafeName;

  return this.db.update(name, updateName, doc)
  .then(function (results) {
    self.emit('update', {updateName: updateName, doc: doc, results: results});
    self.emit('update:after', {updateName: updateName, doc: doc, results: results});
    var newRev = results[1]['x-couch-update-newrev'];

    if (!opts || !opts.raw) {
      results[0]._rev = newRev;
      return results[0];
    }

    return results;
  });
};

Collection.prototype.write = function (doc, opts) {
  return this.update('write', doc, opts);
};

Collection.prototype.merge = function (doc, opts) {
  return this.update('merge', doc, opts);
};

Collection.prototype.view = function (viewName, params, opts) {
  var name = this.dbSafeName
    , params = params || {}
    , opts = opts || {};

  // Backwards compatibility
  if (opts.include_docs || opts.includeDocs) {
    params.include_docs = true
  };

  return this.db.view(name, viewName, params)
  .then(function (results) {

    if (opts.raw) {
      return results;
    }
    else {
      return results[0].rows.map(function (i) {
        if (params.include_docs) {
          return i.doc;
        }
        return i.value;
      });
    }
  });
};

Collection.prototype.pages = function (opts) {

};

Collection.prototype.put = function (obj) {
  var item = new Item(obj, this);
  var self = this;
  this.emit('put:before', {data: obj, item: item});

  return item.save()
  .then(function (results) {
    self.emit('put', {data: obj, item: item, results: results}); // in case someone just attaches to 'put'
    self.emit('put:after', {data: obj, item: item, results: results});
    return results;
  });
};

Collection.prototype.form = function (opts) {
  return makeForm(this, opts);
};

Collection.prototype.setup = function () {
  var doc = helpers.designDoc(this);
  var self = this;
  var ensureStaticDocs = [];

  if (this.collection.staticDocs) {
    ensureStaticDocs = this.collection.staticDocs.map(function (staticDoc) {
      return self.db.get(staticDoc._id)
      .catch(function(){
        return self.db.put(staticDoc);
      });
    });
  }

  return Promise.all(ensureStaticDocs)
  .then(function(){
    return self.db.get(doc._id)
    .then(function (curr) {
      if (!curr._rev) {
        curr = curr[0];
      }

      doc._rev = curr._rev;

      var str = JSON.stringify(doc);
      var curr = JSON.stringify(curr);

      if (str !== curr) {
        return self.db.put(doc._id, doc);
      }
      else {
        return curr;
      }
    })
    .catch(function (err) {
      if (err.message === 'missing' || err.message === 'deleted') {
        return self.db.put(doc._id, doc);
      }
      else {
        throw err;
      }
    });
  });
};

Collection.prototype.delete = function (id) {
  this.emit('delete', id);
  return this.merge({_id: id, _deleted: true});
};

Collection.prototype.destroy = function (id) {
  this.emit('destroy', id);
  return this.db.destroy(id);
};

Collection.prototype.fetch = function (ids) {
  return this.db.fetch(ids);
};

Collection.prototype.bulk = function (docs, params) {
  return this.db.bulk(docs, params);
};

Collection.prototype.follow = function (opts, handler) {
  var opts = opts || {}
    , args = {}
    , handler = handler || function(){}
    , feed;

  if (opts.view) {
    args.filter = '_view';
    args.view = this.name + '/' + opts.view;
  }
  else if (opts.filter) {
    if (opts.params) {
      args.query_params = opts.params;
    }
  }
  else if (typeof opts === 'function') {
    handler = opts;
    args.filter = '_view';
    args.view = this.name + '/all';
  }
  else {
    throw new Error('You must specify a handler function');
  }

  args.since = opts.since || 'now';

  feed = this.db.follow(args, handler);

  feed.on('error', function (err) {
    console.log('Error while watching feed: ', err);
  });

  return feed;
};

Collection.prototype.__search = function (opts) {
  /*
    This is a WIP and should not be used.
  */
  var fields = this.fields;
  var searchableFields = _.keys(fields)
  .filter(function (k) {
    return (fields[k] === 'string' || fields[k] === 'text');
  });

  if (typeof opts === 'string') {
    opts = {fields: searchableFields, query: opts};
  }

  opts = _.extend({}, {collection: this.name}, opts);
  return this.app.search(opts);
};

Collection.prototype.search = function (opts, filtered) {
  /*
    Only supports ElasticSearch since
    that's what apps currently in
    production were using before the
    Pouch refactor.
  */

  var matchField = opts.exact === false ? 'term' : 'match';
  delete opts.exact;

  var args = {
    filtered: {
      query: {},
      filter: {
        and:[]
      }
    }
  };
  if (!filtered){
    delete args.filtered.filter.and;
    args.filtered.filter = {query:{match:{collection: this.name}}};
  }else{
    filtered.map(function(filteredCollection){
      args.filtered.filter.and.push({match_all:{collection: filteredCollection}});
    })
  }

  if (_.isArray(opts)) {
    return Promise.all(opts.map(this.search.bind(this)));
  }

  if (typeof opts === 'string') {
    args.filtered.query.query_string = {query: opts, phrase_slop: 3};
    return this.app.search({query: args});
  }

  args.filtered.query[matchField] = opts;

  if (opts.query) {
    return this.app.search(opts.query);
  }

  if (opts.size) {
    args.size = opts.size;
  }

  if (opts.page) {
    args.size = args.size || 10;
    args.from = args.size * (opts.page - 1);
  }

  return this.app.search({query: args});
};

Collection.prototype.seed = function (num, persist) {
  var runs = _.times(num, function () {
    var item = this.app.invoke(this.fake || this.collection.fake);

    if (persist !== false) {
      return this.put(item);
    }
    else {
      return Promise.resolve(item);
    }
  }, this);

  return Promise.all(runs);
};

Collection.prototype.emit = function (name, data) {
  this._ee.emit(name, data);
};

Collection.prototype.on = function (name, handler) {
  this._ee.on(name, handler);
};

Collection.prototype.extend = function (obj) {
  var base = _.extend({}, this.collection);
  var source = _.extend({}, obj);
  return _.merge({}, base, source);
};

Collection.prototype.using = function (db) {
  var newColl = new Collection(this.collection, this.app);
  newColl.db = db;
  return newColl;
};