mcmartins/jsondbfs

View on GitHub
lib/collection.js

Summary

Maintainability
F
4 days
Test Coverage
/**
 * (C) Copyright 2015 Manuel Martins.
 *
 * This module is inspired by json_file_system.
 * (json_file_system is Copyright (c) 2014 Jalal Hejazi,
 *  Licensed under the MIT license.)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Created by: ManuelMartins
 * Created on: 11-09-2015
 *
 */

'use strict';

require("babel-polyfill");

var util = require('./util');
var async = require('async');
var _ = require('underscore');
var DataHandler = require('./dataHandler');
var matchCriteria = require('json-criteria').test;

/**
 * Defines a Collection.
 * Holds access methods to the current collection.
 *
 * @param {object} options
 * @param {string} options.db the database object
 * @param {string} options.file the path of the collections
 * @constructor
 */
function Collection(options) {
  options = options || {};
  this._dataHandler = new DataHandler(options);
}

/**
 * Handles callback behaviour.
 * If call back is not provided or is not a function a 'no op callback' is used
 *
 * @param callback
 * @returns {*}
 */
function checkCallback(callback) {
  if (!callback || typeof callback !== 'function') {
    return function noopCallback() {
    };
  } else {
    return callback;
  }
}

/**
 * Filters the collection using mongodb criteria
 *
 * @param criteria the criteria to match
 * @param {object} options
 * @param {boolean} options.multi returns multiple if matches
 * @param callback
 */
Collection.prototype.find = function find(criteria, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }
  if (!options) {
    options = {
      multi: true
    };
  }
  if (typeof criteria === 'function') {
    callback = criteria;
    criteria = undefined;
  }
  callback = checkCallback(callback);
  var self = this;
  self._dataHandler.get(function afterReadFile(err, documents) {
    if (err) {
      return callback(err);
    }
    if (!criteria) {
      return callback(undefined, documents);
    } else {
      var filteredDocuments = [];
      async.each(documents, function filter(document, next) {
        if (matchCriteria(document, criteria)) {
          if (!options.multi) {
            return next(document);
          }
          filteredDocuments.push(document);
        }
        return next();
      }, function afterFiltering(result) {
        if (_.isError(result)) {
          return callback(result);
        } else if (result) {
          // ok, is not an error, so is the single object filtered
          return callback(undefined, result);
        } else {
          return callback(undefined, filteredDocuments);
        }
      });
    }
  });
};

/**
 * Filters the collection using mongodb criteria and returns the first matched document
 *
 * @param criteria the criteria to match
 * @param callback
 * @returns {*}
 */
Collection.prototype.findOne = function findOne(criteria, callback) {
  if (typeof criteria === 'function') {
    // must be the callback, but this method does not work without criteria
    callback = criteria;
    criteria = undefined;
  }
  callback = checkCallback(callback);
  if (!criteria) {
    return callback(new Error('No criteria specified!'));
  }
  this.find(criteria, {multi: false}, callback);
};

/**
 * Filters the collection using mongodb criteria updates the document(s) and returns the changed ones
 *
 * @param criteria th criteria to match
 * @param updateCriteria the criteria to update
 * @param {object} options
 * @param {boolean} options.multi updates multiple if matches
 * @param {boolean} options.retObj true to return the changed object(s), returns an array with one or more match depending on options
 * @param callback
 * @returns {*}
 */
Collection.prototype.findAndModify = function findAndModify(criteria, updateCriteria, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }
  if (!options) {
    options = {
      multi: true,
      retObj: true
    };
  }
  if (typeof criteria === 'function') {
    // must be the callback, but this method does not work without criteria
    callback = criteria;
    criteria = undefined;
  }
  if (typeof updateCriteria === 'function') {
    // must be the callback, but this method does not work without update criteria
    callback = updateCriteria;
    updateCriteria = undefined;
  }
  callback = checkCallback(callback);
  if (!criteria) {
    return callback(new Error('No criteria specified!'));
  }
  if (!updateCriteria) {
    return callback(new Error('No update criteria specified!'));
  }
  var self = this;
  self.update(criteria, updateCriteria, options, callback);
};

/**
 * Inserts a new document in the collection
 *
 * @param data the object to insert
 * @param callback
 */
Collection.prototype.insert = function insert(data, callback) {
  if (typeof data === 'function' || !data) {
    callback = data;
    data = undefined;
  }
  callback = checkCallback(callback);
  if (!data) {
    return callback(new Error('No data passed to persist!'));
  }
  var self = this;
  // generate unique internal id for each document
  data._id = util.generateUUID();
  self._dataHandler.lock(function afterLockFile(err) {
    if (err) {
      return callback(err);
    }
    self._dataHandler.get(function afterReadFile(err, documents) {
      if (err) {
        self._dataHandler.unlock();
        return callback(err);
      }
      documents.push(data);
      if (_.isArray(data)) {
        documents = _.flatten(documents);
      }
      self._dataHandler.set(documents, function afterWriteFile(err) {
        self._dataHandler.unlock();
        if (err) {
          return callback(err);
        }
        return callback(undefined, data);
      });
    });
  });
};

/**
 * Updates documents based on mongodb criteria
 *
 * @param criteria the criteria to match
 * @param updateCriteria the update criteria
 * @param {object} options
 * @param {boolean} options.multi update multiple if matches
 * @param {boolean} options.upsert insert if no matches were found to update
 * @param {boolean} options.retObj true to return the changed object(s), returns an array with one or more match depending on options
 * @param callback
 */
Collection.prototype.update = function update(criteria, updateCriteria, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }
  if (!options) {
    options = {
      upsert: false,
      multi: true,
      retObj: false
    };
  }
  if (typeof criteria === 'function') {
    // must be the callback, but this method does not work without criteria
    callback = criteria;
    criteria = undefined;
  }
  if (typeof updateCriteria === 'function') {
    // must be the callback, but this method does not work without update criteria
    callback = updateCriteria;
    updateCriteria = undefined;
  }
  callback = checkCallback(callback);
  if (!criteria) {
    return callback(new Error('No criteria specified!'));
  }
  if (!updateCriteria) {
    return callback(new Error('No update criteria specified!'));
  }
  var ret = {
    nMatched: 0,
    nModified: 0,
    nUpserted: 0
  };
  var self = this;
  self._dataHandler.lock(function afterLockFile(err) {
    if (err) {
      return callback(err);
    }
    self._dataHandler.get(function afterReadFile(err, documents) {
      if (err) {
        self._dataHandler.unlock();
        return callback(err);
      }
      async.map(documents, function matches(document, next) {
        if (matchCriteria(document, criteria)) {
          for (var propertyName in updateCriteria) {
            document[propertyName] = updateCriteria[propertyName];
          }
          ret.nModified++;
          ret.nMatched++;
        }
        return next(undefined, document);
      }, function afterFilter(err, filteredDocuments) {
        if (err) {
          self._dataHandler.unlock();
          return callback(err);
        }
        if (!options.multi) {
          filteredDocuments = [filteredDocuments[0]];
        }
        if (ret.nModified > 0) {
          self._dataHandler.set(filteredDocuments, function afterWriteFile(err) {
            self._dataHandler.unlock();
            if (err) {
              return callback(err);
            }
            return callback(undefined, options.retObj ? filteredDocuments : ret);
          });
        } else {
          self._dataHandler.unlock();
          if (options.upsert) {
            self.insert(updateCriteria, function afterInsert(err) {
              if (err) {
                return callback(err);
              }
              ret.nUpserted = 1;
              return callback(undefined, options.retObj ? updateCriteria : ret);
            });
          } else {
            return callback(undefined, options.retObj ? updateCriteria : ret);
          }
        }
      });
    });
  });
};

/**
 * Removes documents based in mongodb criteria
 *
 * @param criteria the criteria to match
 * @param {object} options
 * @param {boolean} options.multi remove multiple if matches
 * @param callback
 * @returns {*}
 */
Collection.prototype.remove = function remove(criteria, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = {
      multi: true
    };
  }
  if (typeof criteria === 'function') {
    // must be the callback, but this method does not work without criteria
    // should we allow to remove everything?
    callback = criteria;
    criteria = undefined;

  }
  callback = checkCallback(callback);
  if (!criteria) {
    return callback(new Error('No criteria specified!'));
  }
  var self = this;
  self._dataHandler.lock(function afterLockFile(err) {
    if (err) {
      return callback(err);
    }
    self._dataHandler.get(function afterReadFile(err, documents) {
      if (err) {
        self._dataHandler.unlock();
        return callback(err);
      }
      async.reject(documents, function matches(document, next) {
        return next(matchCriteria(document, criteria));
      }, function afterFilter(filteredDocuments) {
        self._dataHandler.set(filteredDocuments, function afterWriteFile(err) {
          self._dataHandler.unlock();
          if (err) {
            return callback(err);
          } else {
            return callback(undefined, true);
          }
        });
      });
    });
  });
};

/**
 * Counts the number of documents in the collection
 *
 * @param criteria the criteria to match
 * @param callback
 */
Collection.prototype.count = function count(criteria, callback) {
  if (typeof criteria === 'function') {
    callback = criteria;
    criteria = undefined;
  }
  callback = checkCallback(callback);
  var self = this;
  if (!criteria) {
    self._dataHandler.get(function afterReadFile(err, documents) {
      return callback(err, documents.length);
    });
  } else {
    self.find(criteria, function afterFind(err, filteredDocuments) {
      if (err) {
        return callback(err);
      }
      return callback(undefined, filteredDocuments.length);
    });
  }
};

module.exports = Collection;