bengl/mongosmash

View on GitHub
src/mongosmash.js

Summary

Maintainability
A
45 mins
Test Coverage
import observed from 'observed';
import {queryGenerator} from './queryGenerator';

class NeDBWrapper {

  constructor (nedb) {
    this._dbs = {};
    this.nedb = nedb;
  }

  collection (name) {
    if (this._dbs[name]) return this._dbs[name];

    this._dbs[name] = new this.nedb();
    return this._dbs[name];
  }

}

export class MongoSmash {

  constructor (db) {
    if (!(this instanceof MongoSmash)) return new MongoSmash(db);
    if (!db) throw Error('An NeDB or MongoDB connection is required!');

    this.isNeDB = typeof db.collection !== 'function';
    this.db = (this.isNeDB ? new NeDBWrapper(db) : db);
    this.changelists = new Map();
    this.modelnames = new Map();
    this.observers = new Map(); // hang on to references so we don't lose the observers
    this.collections = {};
  }

  _handleChanges (obj, change) {
    let lists = this.changelists;
    if (!lists.has(obj)) lists.set(obj, [change]);
    else lists.get(obj).push(change);
  }

  _dbOp (model, op, args) {
    args = Array.prototype.slice.call(args, 1);
    var col;
    if (!this.collections[model]) {
      col = this.db.collection(model);
      this.collections[model] = col;
    } else {
      col = this.collections[model];
    }
    return new Promise(function(resolve, reject){
      args.push(promisifier(resolve, reject));
      col[op].apply(col, args);
    });
  }

  _observe (obj, model) {
    this.modelnames.set(obj, model);
    let observer = observed(obj);
    observer.on('change', changes => this._handleChanges(obj, changes));
    let oldObserver = this.observers.get(obj);
    if (oldObserver) oldObserver.stop();
    this.observers.set(obj, observer);
    return obj;
  }

  _observeResults (model) {
    return results => {
      if (!results) return results;
      if (Array.isArray(results))
        return results.map(this._observeResults(model));
      else if (results.toArray)
        return toArrayPromise(results).then(this._observeResults(model));
      else
        return this._observe(results, model);
    };
  };

  _insert (model) {
    return this._dbOp(model, 'insert', arguments);
  }

  _update (model) {
    return this._dbOp(model, 'update', arguments);
  }

  _remove (model) {
    return this._dbOp(model, 'remove', arguments);
  }

  _find (model) {
    return this._dbOp(model, 'find', arguments);
  }

  _findOne (model) {
    return this._dbOp(model, 'findOne', arguments);
  }

  find (model, query, cb) {
    return nodeify(this._find(model, query).then(this._observeResults(model)), cb);
  }

  findOne (model, query, cb) {
    return nodeify(this._findOne(model, query).then(this._observeResults(model)), cb);
  }

  new (model, obj) {
    this.changelists.set(obj, ['insert']);
    this._observe(obj, model);
  }

  create (model, obj, cb) {
    this.new(model, obj);
    return nodeify(this.save(obj), cb);
  }

  delete (obj, cb) {
    let paramTwo = this.isNeDB ? {} : true;
    return nodeify(this._remove(this.modelnames.get(obj), idOf(obj), paramTwo)
      .then(() => this.changelists.set(obj, [])), cb);
  }

  save (obj, cb) {
    let model;
    this.observers.get(obj).deliverChanges();
    let q = queryGenerator(this.changelists.get(obj) || [], this);
    if (q.insert || q.update) model = this.modelnames.get(obj);
    if (!this.isNeDB && hasUserPrototype(obj)) {
      obj = shallowCopy(obj); // otherwise mongodb saves the prototype properties
    }
    return nodeify((
      q.insert ? this._insert(model, obj) :
      q.update ? this._update(model, idOf(obj), q.update) :
      Promise.resolve()
    ).then(result => {
      this.changelists.delete(obj);
      return Array.isArray(result) ? result[0] : result;
    }), cb);
  }

}

function idOf(obj) { return {_id: obj._id}; }

function promisifier(resolve, reject) {
  return function (err, result) {
    if (err) reject(err);
    else resolve(result);
  }
}

function toArrayPromise(result) {
  return new Promise(function(resolve, reject){
    result.toArray(promisifier(resolve, reject));
  });
}

function nodeify(p, cb) {
  if (cb) {
    p.then(function(result) {
      cb(null, result);
    }, function(err) {
      cb(err);
    });
  }
  return p;
}

function hasUserPrototype(obj) {
  return Object.getPrototypeOf(obj) !== Object.prototype;
}

function shallowCopy(obj) {
  var copy = {};
  Object.keys(obj).forEach((k) => copy[k] = obj[k]);
  return copy;
}