<|start_filename|>test/helpers_update_document_test.js<|end_filename|>
/*globals describe, it*/
var assert = require('assert')
  , UpdateDocument = require('./../lib/helpers/update_document');

describe('namespacer', function () {

  it('should return update document if its not an special operation', function () {
    var update = {foo: 'bar'}
      , doc = {foo: 'zemba', hey: 'joe'}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, update);
  });

  it('`$inc` increases doc value', function () {
    var update = {'$inc': {foo: 3}}
      , doc = {foo: 1, hey: 'joe'}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: 4, hey: 'joe'});
  });

  it('`$inc` increases doc value using `dot_notation`', function () {
    var update = {'$inc': {'foo.bar': 3}}
      , doc = {foo: {bar: 1}}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: 4}});
  });

  it('`$inc` sets doc value using `dot_notation` on `inexistant` elements', function () {
    var update = {'$inc': {'foo.bar': 3}}
      , doc = {}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: 3}});
  });

  it('`$set` sets doc value', function () {
    var update = {'$set': {foo: 'zemba'}}
      , doc = {foo: 'bar', hey: 'joe'}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: 'zemba', hey: 'joe'});
  });

  it('`$set` sets doc value using `dot_notation`', function () {
    var update = {'$set': {'foo.bar': 'zemba'}}
      , doc = {foo: {bar: 'joe'}}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: 'zemba'}});
  });

  it('`$set` sets doc value using `dot_notation` on `inexistant` elements', function () {
    var update = {'$set': {'foo.bar': 'zemba'}}
      , doc = {}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: 'zemba'}});
  });

  it('`$unset` unsets doc value', function () {
    var update = {'$unset': {foo: 1}}
      , doc = {foo: 'bar', hey: 'joe'}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {hey: 'joe'});
  });

  it('`$unset` unsets doc value using `dot_notation`', function () {
    var update = {'$unset': {'foo.bar.hey': 1}}
      , doc = {foo: {bar: {hey: 'joe'}}}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: {}}});
  });

  it('`$unset` unsets doc value using `dot_notation` on `inexistant` elements', function () {
    var update = {'$unset': {'foo.bar.hey': 1}}
      , doc = {}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {});
  });

  it('`$push` pushes doc value', function () {
    var update = {'$push': {foo: 'fleiba'}}
      , doc = {foo: ['bar'], hey: 'joe'}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: ['bar', 'fleiba'], hey: 'joe'});
  });

  it('`$push` pushes doc value using `dot_notation`', function () {
    var update = {'$push': {'foo.bar': 'fleiba'}}
      , doc = {foo: {bar: ['bar']}}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: ['bar', 'fleiba']}});
  });

  it('`$push` pushes doc value using `dot_notation` on `inexistant` elements', function () {
    var update = {'$push': {'foo.bar': 'fleiba'}}
      , doc = {}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: ['fleiba']}});
  });

  it('`$pushAll` pushes doc array', function () {
    var update = {'$pushAll': {foo: ['zemba', 'fleiba']}}
      , doc = {foo: ['bar'], hey: 'joe'}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: ['bar', 'zemba', 'fleiba'], hey: 'joe'});
  });

  it('`$pushAll` pushes doc array using `dot_notation`', function () {
    var update = {'$pushAll': {'foo.bar': ['zemba', 'fleiba']}}
      , doc = {foo: {bar: ['bar']}}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: ['bar', 'zemba', 'fleiba']}});
  });

  it('`$pushAll` pushes doc array using `dot_notation` on `inexistant` elements', function () {
    var update = {'$pushAll': {'foo.bar': ['zemba', 'fleiba']}}
      , doc = {}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: ['zemba', 'fleiba']}});
  });

  it('`$addToSet` pushes doc value if not present', function () {
    [0, 1].forEach(function () {
      var update = {'$addToSet': {foo: 'zemba'}}
        , doc = {foo: ['bar'], hey: 'joe'}
        , ret = UpdateDocument({document: doc, update: update});

      assert.deepEqual(ret, {foo: ['bar', 'zemba'], hey: 'joe'});
    });
  });

  it('`$addToSet` pushes doc value if not present using `dot_notation`', function () {
    [0, 1].forEach(function () {
      var update = {'$addToSet': {'foo.bar': 'zemba'}}
        , doc = {foo: {bar: ['bar']}}
        , ret = UpdateDocument({document: doc, update: update});

      assert.deepEqual(ret, {foo: {bar: ['bar', 'zemba']}});
    });
  });

  it('`$addToSet` pushes doc value if not present using `dot_notation` on `inexistant` elements', function () {
    [0, 1].forEach(function () {
      var update = {'$addToSet': {'foo.bar': 'zemba'}}
        , doc = {}
        , ret = UpdateDocument({document: doc, update: update});

      assert.deepEqual(ret, {foo: {bar: ['zemba']}});
    });
  });

  it('`$pop` pops array element', function () {
    var update = {'$pop': {foo: 1}}
      , doc = {foo: ['bar', 'zemba', 'fleiba'], hey: 'joe'}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: ['bar', 'zemba'], hey: 'joe'});

    update = {'$pop': {foo: -1}};
    doc = {foo: ['bar', 'zemba', 'fleiba'], hey: 'joe'};
    ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: ['zemba', 'fleiba'], hey: 'joe'});
  });

  it('`$pop` pops array element using `dot_notation`', function () {
    var update = {'$pop': {'foo.bar': 1}}
      , doc = {foo: {bar: ['bar', 'zemba', 'fleiba']}}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: ['bar', 'zemba']}});

    update = {'$pop': {'foo.bar': -1}};
    doc = {foo: {bar: ['bar', 'zemba', 'fleiba']}};
    ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: ['zemba', 'fleiba']}});
  });

  it('`$pop` pops array element using `dot_notation` on `inexistant` elements', function () {
    var update = {'$pop': {'foo.bar': 1}}
      , doc = {}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {});
  });

  it('`$pull` removes array elements matchin a value', function () {
    var update = {'$pull': {foo: 'zemba'}}
      , doc = {foo: ['bar', 'zemba', 'zemba', 'fleiba'], hey: 'joe'}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: ['bar', 'fleiba'], hey: 'joe'});
  });

  it('`$pull` removes array elements matchin a value using `dot_notation`', function () {
    var update = {'$pull': {'foo.bar': 'zemba'}}
      , doc = {foo: {bar: ['bar', 'zemba', 'zemba', 'fleiba']}}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: ['bar', 'fleiba']}});
  });

  it('`$pull` removes array elements matchin a value using `dot_notation` on `inexistant` elements', function () {
    var update = {'$pull': {'foo.bar': 'zemba'}}
      , doc = {}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {});
  });

  it('`$pullAll` removes array elements in a array', function () {
    var update = {'$pullAll': {foo: ['fleiba', 'zemba']}}
      , doc = {foo: ['bar', 'zemba', 'zemba', 'fleiba'], hey: 'joe'}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: ['bar'], hey: 'joe'});
  });

  it('`$pullAll` removes array elements in a array using `dot_notation`', function () {
    var update = {'$pullAll': {'foo.bar': ['fleiba', 'zemba']}}
      , doc = {foo: {bar: ['bar', 'zemba', 'zemba', 'fleiba']}}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {bar: ['bar']}});
  });

  it('`$pullAll` removes array elements in a array using `dot_notation` on `inexistant` elements', function () {
    var update = {'$pullAll': {'foo.bar': ['fleiba', 'zemba']}}
      , doc = {}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {});
  });

  it('`$rename` renames a field', function () {
    var update = {'$rename': {foo: 'bar'}}
      , doc = {foo: 'foo', hey: 'joe'}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {bar: 'foo', hey: 'joe'});
  });

  it('`$rename` renames a field using `dot_notation`', function () {
    var update = {'$rename': {'foo.bar': 'far'}}
      , doc = {foo: {bar: 'foo'}}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {foo: {far: 'foo'}});
  });

  it('`$rename` renames a field using `dot_notation` on `inexistant` elements', function () {
    var update = {'$rename': {'foo.bar': 'far'}}
      , doc = {}
      , ret = UpdateDocument({document: doc, update: update});

    assert.deepEqual(ret, {});
  });
});


<|start_filename|>example/controllers/public.js<|end_filename|>
module.exports = function (APP) {
  var app = APP.app
    , User = APP.loadModel('user')
    , Post = APP.loadModel('post')
    , Comment = APP.loadModel('comment');

  app.get('/', function (req, res) {
    var funk = require('funk')();

    Post().mongo('findArray', {}, funk.result('posts'));
    User().mongo('findArray', {}, funk.result('users'));
    Comment().mongo('findArray', {}, funk.result('comments'));

    funk.parallel(function () {
      if (this.errors) throw Error(this.errors[0]);

      res.render('public/index', {
        title: 'home'
      , users: this.users
      , posts: this.posts
      , comments: this.comments
      });
    });
  });
};


<|start_filename|>example/controllers/posts.js<|end_filename|>
module.exports = function (APP) {
  var app = APP.app
    , User = APP.loadModel('user')
    , Post = APP.loadModel('post')
    , ObjectID = APP.ObjectID

    , embedAuthor = function (document, callback) {
        User().mongo('findOne', {_id: new ObjectID(document.author._id)}, function (error, user) {
          if (error) callback(error);

          document.author = Post().getEmbeddedDocument('author', user);
          callback(null, document);
        });
      };

  // show post
  app.get('/posts/:id', function (req, res) {
    var id = req.param('id')
      , funk = require('funk')();

    User().mongo('findArray', {}, funk.result('users'));
    Post().mongo('findOne', {_id: new ObjectID(id)}, funk.result('post'));

    funk.parallel(function () {
      res.render('posts/show', {
        title: this.post.title
      , post: this.post
      , users: this.users
      });
    });
  });

  // create post
  app.post('/posts', function (req, res) {
    var document = req.param('post')
      , funk = require('funk')();

    Post().getCollection(funk.add(function (error, collection) {
      document._id = collection.pkFactory.createPk();
    }));
    embedAuthor(document, funk.nothing());

    funk.parallel(function () {
      Post().validateAndInsert(document, function (error, validator) {
        if (error) throw error;

        if (validator.hasErrors()) {
          // Show flash error
          console.log(validator.errors);
          res.redirect('/');
        } else {
          res.redirect('/posts/' + document._id + '/');
        }
      });
    });
  });

  // update post
  app.post('/posts/:id', function (req, res) {
    var id = req.param('id')
      , update = req.param('post');

    embedAuthor(update, function (error, update) {
      Post().validateAndUpdate({_id: new ObjectID(id)}, {'$set': update}, function (error, validator) {
        if (error) throw error;
        if (validator.hasErrors()) console.log(validator.errors);

        res.redirect('/posts/' + id + '/');
      });
    });
  });

  // create comment
  app.post('/posts/:id/comments', function (req, res) {
    var id = req.param('id')
      , comment = req.param('comment');

    Post().mongo('findOne', {_id: new ObjectID(id)}, function (error, post) {
      if (error) throw error;
      Post().addComment(post, comment, function (error, document) {
        if (error) throw error;
        res.redirect('/posts/' + id + '/');
      });
    });
  });
};


<|start_filename|>lib/validator.js<|end_filename|>
module.exports = function (document, update) {
  var VALIDATOR = {},
      _ = require('underscore'),
      utils = require('./utils'),
      UpdateDocument = require('./helpers/update_document');

  // private methods
  function _getKeys(object) {
    return _.intersect(Object.keys(object), Object.keys(VALIDATOR.updated_document));
  }

  if (!document) {
    throw Error("Must provide a document");
  }

  if (!update) {
    throw Error("Must provide an update object");
  }

  VALIDATOR.document = document || {};
  VALIDATOR.update = update || {};
  VALIDATOR.updated_document = UpdateDocument(VALIDATOR);
  VALIDATOR.errors = {};

  VALIDATOR.regex =  {
    login: /^[A-Za-z](?=[A-Za-z0-9_.]{3,11}$)[a-zA-Z0-9_]*\.?[a-zA-Z0-9_]*$/,
    username: /^[A-Za-z0-9][A-Za-z0-9_@&$. \-]{3,31}[a-zA-Z0-9_]$/,
    title: /^[A-Za-z0-9].{3,50}/,
    description: /.{10,300}/,
    email: /^\S+@\S+\.\S+$/,
    password: <PASSWORD>}/,
    url: /((http|https|ftp):\/\/(\S*?\.\S*?))(\s|\;|\)|\]|\[|\{|\}|,|\"|'|:|\<|$|\.\s)/i
  };

  /* is the model being updated?
   *
   * @return {Boolean}
   */
  VALIDATOR.isUpdating = function () {
    return !_.isEmpty(this.document);
  };

  /* is the model being inserted?
   *
   * @return {Boolean}
   */
  VALIDATOR.isInserting = function () {
    return _.isEmpty(this.document);
  };

  /* did the attribute change?
   *
   * @param {String}
   * @return {Boolean}
   */
  VALIDATOR.attrChanged = function (attr) {
    return this.document[attr] !== this.updated_document[attr];
  };

  /* gets or creates an error bucket fot the given attribute
   *
   * @param {String} attr
   * @param {Boolean} create
   *
   * @return {Object}
   */
  VALIDATOR.getErrorBucket = function (attr, create) {
    var attr_parts = attr.split('.')
      , error_bucket, key;

    if (attr_parts.length === 1) {
      if (create && !this.errors[attr]) {
        this.errors[attr] = [];
      }
      return this.errors[attr] || null;
    } else {
      error_bucket = this.errors;
      for (;attr_parts.length > 0;) {
        key = attr_parts.shift();

        if (!error_bucket[key]) {
          if (create) {
            error_bucket[key] = attr_parts.length === 0 ? [] : {};
          } else {
            return null;
          }
        }
        error_bucket = error_bucket[key];
      }
    }
    return error_bucket;
  };

  /* does attr have any error?
   *
   * @param {String}
   * @return {Boolean}
   */
  VALIDATOR.hasError = function (attr) {
    return this.hasErrors() && this.getErrorBucket(attr) !== null;
  };

  /* does the model have any error?
   *
   * @return {Boolean}
   */
  VALIDATOR.hasErrors = function () {
    return !_.isEmpty(this.errors);
  };

  /* validates existence of some attributes
   *
   * {attr1: 'error message', attr2: ...}
   *
   * @param {Object} validations
   * @return self
   */
  VALIDATOR.validateExistence = function (validations) {
    Object.keys(validations).forEach(function (key, i) {
      var parsed = utils.inspect(this.updated_document, key)
        , obj = parsed[0]
        , attr = parsed[1];

      if (!obj[attr]) {
        this.addError(key, validations[key]);
      }
    }, this);
    return VALIDATOR;
  };

  /* validates regex matching of some attributes
   *
   * {attr1: [/regex/, 'error message'], attr2: ...}
   *
   * @param {Object} validations
   * @return self
   */
  VALIDATOR.validateRegex = function (validations) {
    Object.keys(validations).forEach(function (key, i) {
      var parsed = utils.inspect(this.updated_document, key)
        , obj = parsed[0]
        , attr = parsed[1];

      if (!obj[attr] || !validations[key][0].test(obj[attr])) {
        this.addError(key, validations[key][1]);
      }
    }, this);
    return VALIDATOR;
  };

  /* validates confirmation of some attributes
   *
   * {attr1: ['confirmation_key', 'error message'], attr2: ...}
   *
   * @param {Object} validations
   * @return self
   */
  VALIDATOR.validateConfirmation = function (validations) {
    Object.keys(validations).forEach(function (key, i) {
      var parsed = utils.inspect(this.updated_document, key)
        , obj = parsed[0], attr = parsed[1]
        , parsed2 = utils.inspect(this.updated_document, validations[key][0])
        , obj2 = parsed2[0], attr2 = parsed2[1];

      if (!obj[attr] || !obj2[attr2] || obj[attr] !== obj2[attr2]) {
        this.addError(key, validations[key][1]);
        this.addError(validations[key][0], validations[key][1]);
      }
    }, this);
    return VALIDATOR;
  };

  /* validates that a query returns something or nothing
   *
   * {attr1: [Model, query, true|false, 'error message'], attr2: ...}
   *
   * @param {Object} validations
   * @param {Function} callback
   * @return self
   */
  VALIDATOR.validateQuery = function (validations, callback) {
    var funk = require('funk')();

    _getKeys(validations).forEach(function (key, i) {
      validations[key][0].mongo('findOne', validations[key][1], funk.add(function (errors, doc) {
        if ((validations[key][2] === true && !doc || validations[key][2] === false && doc)) {
          VALIDATOR.addError(key, validations[key][3]);
        }
      }));
    });

    funk.parallel(callback);
    return VALIDATOR;
  };

  /* adds an error to the error bucket
   *
   * @param {String} name
   * @param {*} value
   * @return self
   */
  VALIDATOR.addError = function (name, value) {
    var error_bucket = this.getErrorBucket(name, true);

    error_bucket.push(value);
    return VALIDATOR;
  };

  return VALIDATOR;
};


<|start_filename|>test/helpers_collection_proxy_test.js<|end_filename|>
/*globals describe, it, beforeEach, afterEach*/
var assert = require('assert')
  , sinon = require('sinon')
  , CollectionProxy = require('./../lib/helpers/collection_proxy')
  , noop = function () {
      return function () {};
    }
  , Collection = { update: noop(), insert: noop(), findArray: noop()
                 , find: noop(), findAndModify: noop(), mapReduce: noop(), remove: noop()}
  , Model;

function stubGetCollection(sinon) {
  var stub = sinon.stub(Model, 'getCollection', function (_callback) {
    _callback(null, Collection);
    stub.restore();
  });
}

describe('collection proxy test', function () {

  beforeEach(function () {
    Model = require('../').model({}, 'models');
  });

  describe('proxy', function () {
    it('delegates every call to collection_proxy or native driver collection functions', sinon.test(function () {
      var cb = function cb() {}
        , self = this
        , stub
        , args = ['zemba', cb];

      ['update', 'insert', 'findArray'].forEach(function (method) {
        stubGetCollection(self);
        stub = self.stub(CollectionProxy, method).withArgs(Model, Collection, args, cb);
        CollectionProxy.proxy(Model, {method: method, hooks: true}, args, cb);
        sinon.assert.calledOnce(stub);
      });

      ['find', 'update', 'insert'].forEach(function (method) {
        stubGetCollection(self);
        stub = self.stub(Collection, method).withArgs(args[0], args[1]);
        CollectionProxy.proxy(Model, {method: method, hooks: false}, args, cb);
        sinon.assert.calledOnce(stub);
      });
    }));

    describe('namespace', function () {
      var cb, args;

      beforeEach(function () {
        cb = function () {};
        args = [{foo: 'bar'}, cb];

        Model.namespaces = {foo: ['zemba', 'fleiba']};
      });

      it('modifies the arguments if `namespace` is set to `true`', sinon.test(function () {
        var self = this, stub1, stub2;

        stubGetCollection(self);
        stub1 = self.stub(CollectionProxy.namespacer, 'filter').withArgs(Model.namespaces, 'foo', 'find', args);
        stub2 = self.stub(Collection, 'find').withArgs(args[0], args[1]);

        CollectionProxy.proxy(Model, {method: 'find', namespace: 'foo', namespacing: true}, args, cb);
        sinon.assert.calledOnce(stub1);
        sinon.assert.calledOnce(stub2);
      }));

      it('doesnt modify the arguments if `namespace` is set to `false`', sinon.test(function () {
        var self = this, stub;

        stubGetCollection(self);
        stub = self.stub(Collection, 'find').withArgs(args[0], args[1]);

        CollectionProxy.proxy(Model, {method: 'find', namespace: 'foo', namespacing: false}, args, cb);
        sinon.assert.calledOnce(stub);
      }));
    });

    describe('maps', function () {
      var cb, args;

      beforeEach(function () {
        cb = function () {};
        args = [{foo: 'bar', _id: '3'}, cb];

        Model.maps = {foo: Boolean};
      });

      it('maps the arguments according if the `mapping` option is set to `true`', sinon.test(function () {
        var self = this, stub1, stub2;

        stubGetCollection(self);
        stub1 = self.stub(CollectionProxy.mapper, 'map').withArgs({foo: Boolean}, 'find', args);
        stub2 = self.stub(Collection, 'find').withArgs(args[0], args[1]);

        CollectionProxy.proxy(Model, {method: 'find', mapping: true}, args, cb);
        sinon.assert.calledOnce(stub1);
        sinon.assert.calledOnce(stub2);
      }));

      it('does not map the arguments if the `mapping` option is set to `false`', sinon.test(function () {
        var self = this, stub;

        stubGetCollection(self);
        stub = self.stub(Collection, 'find').withArgs(args[0], args[1]);

        CollectionProxy.proxy(Model, {method: 'find', mapping: false}, args, cb);
        sinon.assert.calledOnce(stub);
      }));
    });

    it('can be called with no callback', sinon.test(function () {
      var args = ['zemba']
        , stub
        , self = this;

      ['update', 'insert', 'findArray'].forEach(function (method) {
        stubGetCollection(self);
        stub = self.stub(CollectionProxy, method, function (m, c, a, callback) {
          assert.equal(typeof callback, 'function');
        });

        CollectionProxy.proxy(Model, {method: method, hooks: true}, args);
        sinon.assert.calledOnce(stub);
      });

      ['find', 'update', 'insert'].forEach(function (method) {
        stubGetCollection(self);
        stub = self.stub(Collection, method, function (args, callback) {
          assert.equal(typeof callback, 'undefined');
        });

        CollectionProxy.proxy(Model, {method: method, hooks: false}, args);
        sinon.assert.calledOnce(stub);
      });
    }));
  });

  describe('findArray', function () {
    it('calls find on a collection with some arguments', sinon.test(function () {
      var cb = function (error, cursor) {}
        , self = this
        , stub1, stub2
        , cursor = {toArray: function () {}}
        , error_result = null
        , args = ['fleiba', cb];

      stub1 = self.stub(Collection, 'find', function (_collection, _args) {
        assert.deepEqual(_collection, args[0]);
        stub2 = self.stub(cursor, 'toArray');
        args[args.length - 1](null, cursor);
      });

      CollectionProxy.findArray(Model, Collection, args, cb);
      sinon.assert.calledOnce(stub1);
      sinon.assert.calledOnce(stub2);
    }));
  });

  describe('insert', function () {
    it('inserts a record', sinon.test(function () {
      var callback_called = false
        , self = this
        , stub1, stub2, stub3
        , cb = function (error, ret) {
            assert.deepEqual(error, null);
            assert.deepEqual(ret, [1, 2, 3]);
            callback_called = true;
          }
        , args = ['fleiba', cb];

      stub1 = self.stub(Model, 'beforeInsert', function (ar, callback) {
        assert.deepEqual(ar, args[0]);

        stub2 = self.stub(Collection.insert, 'apply', function (_collection, _args) {
          assert.deepEqual(_collection, Collection);
          assert.deepEqual(_args[0], ['document1', 'document2']);

          stub3 = self.stub(Model, 'afterInsert', function (_docs, _callback) {
            assert.deepEqual(_docs, ['document1', 'document2']);
            _callback(null);
            assert.ok(callback_called);
          });

          _args[1](null, [1, 2, 3]);
        });

        callback(null, ['document1', 'document2']);
      });

      CollectionProxy.insert(Model, Collection, args, cb);
      sinon.assert.calledOnce(stub1);
      sinon.assert.calledOnce(stub2);
      sinon.assert.calledOnce(stub3);
    }));
  });

  describe('update', function () {
    it('finds and modifies a record', sinon.test(function () {
      var callback_called = false
        , self = this
        , stub1, stub2, stub3
        , cb = function (error, ret) {
            assert.deepEqual(error, null);
            assert.deepEqual(ret, [1, 2, 3]);
            callback_called = true;
          }
        , args = [{name: 'zemba'}, {'$set': {name: 'foo'}}, {}, cb];

      stub1 = sinon.stub(Model, 'beforeUpdate', function (_query, _update, _callback) {
        assert.deepEqual(_query, args[0]);
        assert.deepEqual(_update, args[1]);
        _update.$set.updated_at = 123;

        stub2 = sinon.stub(Collection.update, 'apply', function (_collection, _args) {
          assert.deepEqual(_collection, Collection);
          assert.deepEqual(_args[0], args[0]);
          assert.deepEqual(_args[1].$set.updated_at, 123);

          stub3 = sinon.stub(Model, 'afterUpdate', function (_doc, _update, _callback) {
            assert.deepEqual(_doc, args[0]);
            assert.deepEqual(_update, args[1]);
            _callback(null);
            assert.ok(callback_called);
          });

          _args[3](null, [1, 2, 3]);
        });

        _callback(null, _query, _update);
      });

      CollectionProxy.update(Model, Collection, args, cb);
      sinon.assert.calledOnce(stub1);
      sinon.assert.calledOnce(stub2);
      sinon.assert.calledOnce(stub3);
    }));
  });

  describe('findAndModify', function () {
    it('finds and modifies a record', sinon.test(function () {
      var callback_called = false
        , self = this
        , stub1, stub2, stub3
        , cb = function (error, ret) {
            assert.deepEqual(error, null);
            assert.deepEqual(ret, [1, 2, 3]);
            callback_called = true;
          }
        , args = [{name: 'zemba'}, [], {'$set': {name: 'foo'}}, {}, cb];

      stub1 = self.stub(Model, 'beforeUpdate', function (_query, _update, _callback) {
        assert.deepEqual(_query, args[0]);
        assert.deepEqual(_update, args[2]);
        _update.$set.updated_at = 123;

        stub2 = self.stub(Collection.findAndModify, 'apply', function (_collection, _args) {
          assert.deepEqual(_collection, Collection);
          assert.deepEqual(_args[0], args[0]);
          assert.deepEqual(_args[2].$set.updated_at, 123);

          stub3 = self.stub(Model, 'afterUpdate', function (_doc, _update, _callback) {
            assert.deepEqual(_doc, args[0]);
            assert.deepEqual(_update, args[2]);
            _callback(null);
            assert.ok(callback_called);
          });

          _args[4](null, [1, 2, 3]);
        });

        _callback(null, _query, _update);
      });

      CollectionProxy.findAndModify(Model, Collection, args, cb);
      sinon.assert.calledOnce(stub1);
      sinon.assert.calledOnce(stub2);
      sinon.assert.calledOnce(stub3);
    }));
  });

  describe('mapReduceCursor', function () {
    var args, stub1, stub2, coll, cb;

    beforeEach(function () {
      args = ['a', 'b'];
      coll = {find: function () {}};
    });

    it('calls `mapReduce` returning a cursor if no intermediate errors occur', sinon.test(function () {
      var self = this;

      stub1 = self.stub(Collection.mapReduce, 'apply', function (_collection, _args) {
        assert.equal(_collection, Collection);
        assert.equal(_args, args);

        stub2 = self.stub(coll, 'find', function (callback) {
          assert.ok(callback);
        });

        _args[1](null, coll);
      });

      cb = function () {};

      CollectionProxy.mapReduceCursor(Model, Collection, args, cb);
      sinon.assert.calledOnce(stub1);
      sinon.assert.calledOnce(stub2);
    }));

    it('calls `mapReduce` returning an error if an intermediate error occurs', sinon.test(function () {
      var self = this
        , error = 'could not access the DB';

      stub1 = self.stub(Collection.mapReduce, 'apply', function (_collection, _args) {
        assert.equal(_collection, Collection);
        assert.equal(_args, args);
        _args[1](error, coll);
      });

      stub2 = self.spy().withArgs(error, null);

      CollectionProxy.mapReduceCursor(Model, Collection, args, stub2);
      sinon.assert.calledOnce(stub1);
      sinon.assert.calledOnce(stub2);
    }));
  });

  describe('mapReduceArray', function () {
    it('returns a `mapReduceCursor` to Array', sinon.test(function () {
      var cb = function () {}
        , self = this
        , stub1, stub2
        , args = ['fleiba', cb]
        , cursor = {toArray: function () {}};

      stub1 = self.stub(CollectionProxy, 'mapReduceCursor', function (_model, _collection, _args, _callback) {
        assert.equal(_model, Model);
        assert.equal(_collection, Collection);
        assert.equal(_args, args);

        stub2 = self.stub(cursor, 'toArray').withArgs(cb);

        _callback(null, cursor);
      });

      CollectionProxy.mapReduceArray(Model, Collection, args, cb);
      sinon.assert.calledOnce(stub1);
      sinon.assert.calledOnce(stub2);
    }));
  });

  describe('remove', function () {
    it('removes a document', sinon.test(function () {
      var callback_called = false
        , self = this
        , stub1, stub2, stub3
        , cb = function (error, ret) {
            assert.deepEqual(error, null);
            assert.deepEqual(ret, [1, 2, 3]);
            callback_called = true;
          }
        , args = ['fleiba', cb];

      stub1 = self.stub(Model, 'beforeRemove', function (_query, _callback) {
        assert.deepEqual(_query, args[0]);

        stub2 = self.stub(Collection.remove, 'apply', function (_collection, _args) {
          assert.deepEqual(Collection, _collection);
          assert.deepEqual(_args[0], args[0]);

          stub3 = self.stub(Model, 'afterRemove', function (_query, _callback) {
            assert.deepEqual(_query, args[0]);
            _callback(null);
            assert.ok(callback_called);
          });

          _args[1](null, [1, 2, 3]);
        });

        _callback(null, _query);
      });

      CollectionProxy.remove(Model, Collection, args, cb);
      sinon.assert.calledOnce(stub1);
      sinon.assert.calledOnce(stub2);
      sinon.assert.calledOnce(stub3);
    }));
  });
});


<|start_filename|>lib/helpers/mapper.js<|end_filename|>
var MAPPER = {}
  , _ = require('underscore');

MAPPER.mapDocument = function (maps, arg) {

  /* 0: dont match
   * 1: partial matching, needs more inspection
   * 2: fully matching
   */
  function isMatching(maps, attr, level) {
    var ret = 0;

    Object.keys(maps).some(function (el) {
      var match;
      if (attr.indexOf('.') !== -1) {
        match = attr.match(new RegExp(el + "(\..*)?"));
        if (match) {
          ret = 2;
        }
      } else {
        match = el === attr;
        if (match) {
          ret = typeof maps[el] === 'object' ? 1 : 2;
        }
      }
      return match;
    });

    return ret;
  }

  function getObject(obj, attr) {
    // dot notation
    if (attr.indexOf('.') !== -1) {
      return attr.split('.').reduce(function (memo, part) {
        memo = memo[part];
        return memo;
      }, obj);
    } else {
      return obj[attr];
    }
  }

  function filter(maps, el, level) {
    var attr, is_matching;

    for (attr in el) {
      if (el.hasOwnProperty(attr)) {
        // special op
        if (attr[0] === '$') {
          filter(maps, el[attr], level);
        } else {
          is_matching = isMatching(maps, attr, level);
          if (is_matching === 2) {
            if (Array.isArray(el[attr])) {
              el[attr].forEach(function(value, i) {
                el[attr][i] = getObject(maps, attr)(el[attr][i]);
              });
            } else {
              el[attr] = getObject(maps, attr)(el[attr]);
            }
          } else {
            if (is_matching === 1 && typeof el[attr] === 'object') {
              filter(maps[attr], el[attr], level + 1);
            }
          }
        }
      }
    }
  }

  if (!_.isEmpty(arg)) {
    if (Array.isArray(arg)) {
      arg.forEach(function (el) {
        filter(maps, el, 0);
      });
    } else {
      filter(maps, arg, 0);
    }
  }
};

MAPPER.map = function (maps, fn, args) {
  if (fn === 'insert') {
    MAPPER.mapDocument(maps, args[0]);
  } else if (fn === 'update') {
    MAPPER.mapDocument(maps, args[1]);
  }
};

module.exports = MAPPER;


<|start_filename|>index.js<|end_filename|>
exports.model = require('./lib/model');
exports.validator = require('./lib/validator');


<|start_filename|>example/public/css/global.css<|end_filename|>
body {
  font-family: Helvetica, serif;
  font-size: 12pt;
  color: #aa99aa;
  background: #18171a url(/images/back.png) repeat-x top left;
}
h1 {
  font-size: 42pt;
  font-weight: normal;
  margin: 10px 0px 20px;
  font-family: Monaco, console;
  float: left;
}
h1 a:hover {
  text-decoration: none;
}
h2 {
  font-size: 28pt;
  font-weight: normal;
  color: #aa99aa;
  margin-bottom: 10px;
}
a {
  color: #ffaadd;
  text-decoration: none;
  text-shadow: 1px 1px 0px #dd0085;
}
a:hover {
  text-decoration: underline;
  color: #ffddf1;
  text-shadow: 1px 1px 0px #ff44b4;
}
table thead {
  text-shadow: 0px 1px 0px #fff;
}
table th, table td {
  padding: 5px 10px;
  border: 1px solid #9b869b;
}
.section {
  margin-bottom: 20px;
}
.floated {
  float: left;
}
html, body {
  height: 100%;
}
#wrapper {
  width: 960px;
  margin: 0px auto;
  min-height: 100%;
  position: relative;
}
#wrapper #sticky {
  padding-bottom: 90px;
}
#wrapper #sticky #login {
  position: absolute;
  top: 8px;
  right: 10px;
}
#wrapper #sticky > nav {
  -moz-box-shadow: 0px 1px 0px #443344;
  -webkit-box-shadow: 0px 1px 0px #443344;
  box-shadow: 0px 1px 0px #443344;
  height: 75px;
  margin-bottom: 25px;
  padding-bottom: 5px;
  border-bottom: 1px solid #27252a;
}
#wrapper #sticky > nav #logo {
  display: block;
  text-indent: -999px;
  overflow: hidden;
  background-repeat: none;
  background: url(/images/sprite.png) no-repeat 0px 5px;
  height: 45px;
  width: 120px;
  margin-right: 20px;
  float: left;
}
#wrapper #sticky > nav menu#header_left {
  float: left;
  width: 512px;
}
#wrapper #sticky > nav menu#header_right {
  float: right;
  font-size: 10pt;
  text-align: right;
  width: 310px;
}
#wrapper #sticky > nav #logged_email {
  color: #aa99aa;
}
#wrapper #sticky > nav #logged_email, #wrapper #sticky > nav #edit_profile, #wrapper #sticky > nav #logout {
  margin-left: 20px;
  line-height: 45px;
}
form {
  width: 100%;
}
form fieldset {
  padding: 10px;
}
form fieldset.floated {
  float: left;
  width: 250px;
}
form fieldset h2 {
  margin-bottom: 10px;
}
form fieldset .field {
  border-bottom: 1px solid #18171a;
  -moz-box-shadow: 0px 1px 0px #443344;
  -webkit-box-shadow: 0px 1px 0px #443344;
  box-shadow: 0px 1px 0px #443344;
  padding: 10px;
  width: 562px;
}
form fieldset .field.floated {
  float: left;
  width: 250px;
}
form fieldset .field.errors {
  -webkit-border-radius: 8px;
  -moz-border-radius: 8px;
  border-radius: 8px;
  background: #b33c3c;
  border-bottom-color: #8c2f2f;
  position: relative;
}
form fieldset .field.errors input, form fieldset .field.errors textarea {
  border-color: #662222;
  border-top-color: #531c1c;
  border-left-color: #531c1c;
  color: #662222;
}
form fieldset .field.errors input:hover, form fieldset .field.errors textarea:hover {
  border-color: #2d0f0f;
}
form fieldset .field.errors input:focus, form fieldset .field.errors textarea:focus {
  border-color: #060202;
}
form fieldset .field.errors label {
  color: #190808;
}
form fieldset .field.errors ul.error {
  -webkit-border-radius: 8px;
  -moz-border-radius: 8px;
  border-radius: 8px;
  position: absolute;
  right: -315px;
  top: 0px;
  width: 275px;
  padding: 10px;
  background: #401515;
  color: #fff;
}
form label {
  text-align: right;
  padding-right: 20px;
  float: left;
  width: 100px;
  height: 34px;
  font-size: 108%;
  color: #aa99aa;
  font-size: 11pt;
}
form label.disabled {
  color: #665566;
}
form input, form textarea {
  -webkit-border-radius: 8px;
  -moz-border-radius: 8px;
  border-radius: 8px;
  -moz-box-shadow: 1px 1px 0px #533e53;
  -webkit-box-shadow: 1px 1px 0px #533e53;
  box-shadow: 1px 1px 0px #533e53;
  border: 1px solid #18171a;
  padding: 7px 10px;
  color: #aa99aa;
  background: #312f35;
}
form input:hover, form textarea:hover {
  -moz-box-shadow: 1px 1px 0px #614961;
  -webkit-box-shadow: 1px 1px 0px #614961;
  box-shadow: 1px 1px 0px #614961;
  background: #3d3b43;
  color: #aa99aa;
}
form input:focus, form textarea:focus {
  background: #4a4750;
  color: #aa99aa;
}
form textarea {
  width: 400px;
}
form .operations {
  text-align: right;
  padding: 10px;
  width: 562px;
}

#forms {
  float: left;
  margin-right: 40px;
}

#lists {
  float: left;
}

.button {
  -webkit-border-radius: 8px;
  -moz-border-radius: 8px;
  border-radius: 8px;
  -moz-box-shadow: 1px 1px 0px #7e5f7e inset;
  -webkit-box-shadow: 1px 1px 0px #7e5f7e inset;
  box-shadow: 1px 1px 0px #7e5f7e inset;
  background: #614961;
  border: 1px solid #271d27;
  border-bottom-color: #000000;
  border-right-color: #000000;
  cursor: pointer;
  display: inline-block;
}
.button:hover {
  background: #7e5f7e;
  -moz-box-shadow: 1px 1px 0px #997799 inset;
  -webkit-box-shadow: 1px 1px 0px #997799 inset;
  box-shadow: 1px 1px 0px #997799 inset;
  border-bottom-color: #000000;
  border-right-color: #000000;
  text-decoration: none;
}
.button:hover input {
  -moz-box-shadow: 0px 0px 0px #7e5f7e;
  -webkit-box-shadow: 0px 0px 0px #7e5f7e;
  box-shadow: 0px 0px 0px #7e5f7e;
  text-shadow: 0px 1px 0px #997799;
  background: #7e5f7e;
  color: #18171a;
}
.button.desactivated {
  cursor: default;
}
.button.desactivated:hover {
  background: url(/images/sprite_x.png) repeat-x 0px -28px #dddddd;
  border: 1px solid #ccc;
}
.button.desactivated span {
  opacity: 0.3;
}
.button span, .button input {
  -moz-box-shadow: 0px 0px 0px #614961;
  -webkit-box-shadow: 0px 0px 0px #614961;
  box-shadow: 0px 0px 0px #614961;
  display: inline-block;
  line-height: 18px;
  padding: 5px 10px 3px;
  text-shadow: 0px 1px 0px #7e5f7e;
  color: #18171a;
  font-weight: bold;
  border: none;
  background-color: transparent;
}
.button span.icon, .button input.icon {
  background-position: 6px;
  padding-left: 28px;
}
.button.small span, .button.small input {
  padding: 3px 7px 0;
}
.button.small span.icon, .button.small input.icon {
  background-position: 2px 2px;
  padding-left: 21px;
}
table.tabular {
  border: none;
  background: none;
}
table.tabular th, table.tabular td {
  border: none;
  background: none;
}
table.tabular th {
  text-align: right;
}
.box {
  -webkit-border-radius: 8px;
  -moz-border-radius: 8px;
  border-radius: 8px;
  padding: 10px 20px;
  margin-bottom: 10px;
  background: #443344;
  color: #9e8b9e;
}
.box.info {
  background: #d2f8cb;
  color: #6a8;
}
.box.info h2 {
  color: #7fd56e;
  border-bottom: 1px solid #bff4b5;
}
.box.info table {
  background: #e6fbe1;
}
.box.info table th, .box.info table td {
  border-color: #d2f8cb;
}
.box.error {
  background: #9f3535;
  color: #fff;
  text-shadow: 0px -1px 0px #662222;
}
.box.error h2 {
  color: #401515;
  text-shadow: 0px 1px 0px #c14646;
}
.box.warning {
  background: #fffad6;
  color: #999988;
}
.box.warning h2 {
  color: #c6b853;
  border-bottom: 1px solid #fff080;
}
.box h2 {
  margin-bottom: 5px;
}

ul.list {
  margin-bottom: 50px;
}

ul.list li{
  margin: 10px 0px;
  border-left: 4px solid #625;
  padding-left: 10px;
}


<|start_filename|>example/models/post.js<|end_filename|>
module.exports = function (APP) {
  var POST = APP.model(APP.db, 'posts'),
      Comment = APP.loadModel('comment'),
      ObjectID = APP.ObjectID;

  POST.skeletons = {
    author: ['_id', 'name'],
    comments: ['_id', 'body']
  };

  POST.validate = function (document, update, callback) {
    var validator = APP.validator(document, update);

    validator.validateExistence({
      title: 'Title is mandatory',
      body: 'Body is mandatory',
      author: 'A Post needs an author'
    });

    callback(null, validator);
  };

  POST.addComment = function (post, comment, callback) {
    comment.post = Comment().getEmbeddedDocument('post', post);
    Comment().validateAndInsert(comment, callback);
  };

  POST.afterUpdate = function (query, update, callback) {
    Comment().updateEmbeddedDocument({_id: query._id}, 'post', update, {}, callback);
  };

  return POST;
};


<|start_filename|>example/public/css/reset.css<|end_filename|>
html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, figure, footer, header, hgroup, nav, section, menu, time, mark, audio, video {
  margin: 0;
  padding: 0;
  border: 0;
  outline: 0;
  font-size: 100%;
  vertical-align: baseline;
  background: transparent;
}

article, aside, figure, footer, header, hgroup, nav, section { display: block; }
fieldset {border: none;}

menu, ul { list-style: none; }
ul, ol { list-style-position: inside; }
blockquote, q { quotes: none; }
blockquote:before, blockquote:after, q:before, q:after { content: ''; content: none; }
a {
  margin: 0;
  padding: 0;
  border: 0;
  font-size: 100%;
  vertical-align: baseline;
  background: transparent;
}

ins { background-color: #ff9; color: #000; text-decoration: none; }

mark {
  background-color: #ff9;
  color: #000;
  font-style: italic;
  font-weight: bold;
}
del {text-decoration: line-through;}

abbr[title], dfn[title] {border-bottom: 1px dotted #000; cursor: help;}

/* tables still need cellspacing="0" in the markup */
table {border-collapse: collapse; border-spacing: 0;}
hr {
  display: block;
  height: 1px;
  border: 0;
  border-top: 1px solid #ccc;
  margin: 1em 0;
  padding: 0;
}
input, select {
  vertical-align: middle;
}
/*
fonts.css from the YUI Library: developer.yahoo.com/yui/
          Please refer to developer.yahoo.com/yui/fonts/ for font sizing percentages

There are two custom edits:
 * make the line-height relative and unit-less
 * remove the pre,code styles as we use a recommendation from Eric Meyer instead
*/
body {
  font: 13px arial, helvetica, "Nimbus Sans L", "Bitstream Vera Sans", sans-serif;
  *font-size: small;
  *font: x-small;
  line-height: 1.22;
}
table { font-size: inherit; font: 100%; }
select, input, textarea { font: 99% arial, helvetica, "Nimbus Sans L", "Bitstream Vera Sans", sans-serif; outline: none; }
/* normalize monospace sizing
 * meyerweb.com/eric/thoughts/2010/02/12/fixed-monospace-sizing/
 */
code, pre { font-family: "Courier New", monospace, serif; font-size: 1em; }
/*
 * minimal base styles
 */
/* #444 looks better than black: twitter.com/H_FJ/statuses/11800719859 */
body, select, input, textarea { color: #444; }
/* Headers (h1,h2,etc) have no default font-size or margin,
   you'll want to define those yourself. */
/* www.aestheticallyloyal.com/public/optimize-legibility/ */
h1, h2, h3, h4, h5, h6 { font-weight: bold; text-rendering: optimizeLegibility; }
/* maxvoltar.com/archive/-webkit-font-smoothing */
html { -webkit-font-smoothing: antialiased; }
/* Accessible focus treatment: people.opera.com/patrickl/experiments/keyboard/test */
a:active { outline: none; }
a:focus { outline: none; }
small { font-size: 85%; }
strong, th { font-weight: bold; }
td, td img { vertical-align: top; }
sub { vertical-align: sub; font-size: smaller; }
sup { vertical-align: super; font-size: smaller; }
pre {
  padding: 15px;
  /* www.pathf.com/blogs/2008/05/formatting-quoted-code-in-blog-posts-css21-white-space-pre-wrap/ */

  white-space: pre;
  /* CSS2 */

  white-space: pre-wrap;
  /* CSS 2.1 */

  white-space: pre-line;
  /* CSS 3 (and 2.1 as well, actually) */

  word-wrap: break-word;
  /* IE */
}
/* align checkboxes, radios, text inputs with their label
   by: <NAME> tjkdesign.com/ez-css/css/base.css  */
input[type="radio"] { vertical-align: text-bottom; }
input[type="checkbox"] { vertical-align: bottom; *vertical-align: baseline; }
.ie6 input { vertical-align: text-bottom; }
/* hand cursor on clickable input elements */
label, input[type=button], input[type=submit], button { cursor: pointer; }
/* always force a scrollbar in non-IE */
html { overflow-y: scroll; }
/* make buttons play nice in IE:
   www.viget.com/inspire/styling-the-button-element-in-internet-explorer/ */
button { width: auto; overflow: visible; }
/* bicubic resizing for non-native sized IMG:
   code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ */
.ie7 img { -ms-interpolation-mode: bicubic; }
/*
 * Non-semantic helper classes
 */
/* Hide for both screenreaders and browsers
   css-discuss.incutio.com/wiki/Screenreader_Visibility */
.hidden { display: none; visibility: hidden; }
/* Hide only visually, but have it available for screenreaders
   www.webaim.org/techniques/css/invisiblecontent/
   Solution from: j.mp/visuallyhidden - Thanks <NAME>! */
.visuallyhidden {
  position: absolute !important;
  clip: rect(1px 1px 1px 1px);
  /* IE6, IE7 */

  clip: rect(1px, 1px, 1px, 1px);
}
/* Hide visually and from screenreaders, but maintain layout */
.invisible { visibility: hidden; }
.clearer { clear: both; }


<|start_filename|>lib/helpers/namespacer.js<|end_filename|>
var NAMESPACER = {}
  , _ = require('underscore')
  , _getFields = function (visibility) {
      return visibility.reduce(function (memo, el) {
        memo[el] = 1;
        return memo;
      }, {});
    };

NAMESPACER.resolveNamespace = function (namespaces, namespace) {
  var ret = []
    , ns = namespaces[namespace]
    , attr;

  if (Array.isArray(ns)) {
    return ns;
  } else {
    if (ns.extend) {
      ret = namespaces[ns.extend];
    }

    if (ns.add) {
      ret = ret.concat(ns.add);
    }

    if (ns.remove) {
      ret = _.filter(ret, function (value) {
        return !_.include(ns.remove, value);
      });
    }
    return ret;
  }
};

NAMESPACER.addFieldFind = function (visibility, args) {
  var current_fields = args.length >= 3 ? args[1] : undefined
    , fields = _getFields(visibility)
    , tests = ['limit', 'sort', 'fields', 'skip', 'hint', 'explain', 'snapshot', 'timeout', 'tailable', 'batchSize']
    , is_option = args.length === 3 && tests.some(function (test) {
        return test in current_fields;
      });

  // selector, callback
  if (args.length < 3) {
    args.splice(1, 0, fields);
  } else if (args.length === 3 && is_option) {
    args.splice(1, 0, fields);
  // selector, fields, options, callback
  } else if (args.length >= 3) {
    args[1] = _.defaults(fields, args[1]);
  }
};

NAMESPACER.addFieldFindOne = function (visibility, args) {
  var fields = _getFields(visibility);

  // selector, callback
  if (args.length <= 2) {
    args.splice(1, 0, { fields: fields });
  // selector, fields, callback
  } else {
    args[1] = _.defaults({ fields: fields }, args[1]);
  }
};

NAMESPACER.addFieldFindAndModify = function (visibility, args) {
  var fields = _getFields(visibility);

  if (args.length === 2) {
    args.splice(1, 0, []);
  }

  if (args.length === 3) {
    args.splice(2, 0, null);
  }

  if (args.length === 4) {
    args.splice(3, 0, {});
  }

  args[3].fields = _.defaults(fields, args[3].fields);
};

NAMESPACER.filterUpdate = function (visibility, arg) {

  /* 0: not visible
   * 1: partial visible, needs more inspection
   * 2: fully visible
   */
  function isVisible(visibility, attr, level) {
    var ret = 0;

    visibility.some(function (el) {
      var match;
      if (attr.indexOf('.') !== -1) {
        match = attr.match(new RegExp(el + "(\..*)?"));
        if (match) {
          ret = 2;
        }
      } else {
        el = el.split('.');
        match = el[level] === attr;
        if (match) {
          ret = el.length === level + 1 ? 2 : 1;
        }
      }
      return match;
    });

    return ret;
  }

  function filter(el, level) {
    var attr, is_visible;

    for (attr in el) {
      if (el.hasOwnProperty(attr)) {
        // special op
        if (attr[0] === '$') {
          filter(el[attr], level);
        } else {
          is_visible = isVisible(visibility, attr, level);
          if (is_visible === 0) {
            delete el[attr];
          } else {
            if (is_visible === 1 && typeof el[attr] === 'object') {
              filter(el[attr], level + 1);
            }
          }
        }
      }
    }
  }

  if (!_.isEmpty(arg)) {
    if (Array.isArray(arg)) {
      arg.forEach(function (el) {
        filter(el, 0);
      });
    } else {
      filter(arg, 0);
    }
  }
};

NAMESPACER.filter = function (namespaces, namespace, fn, args) {
  var visibility = NAMESPACER.resolveNamespace(namespaces, namespace);

  if (fn === 'findAndModify') {
    NAMESPACER.addFieldFindAndModify(visibility, args);
    NAMESPACER.filterUpdate(visibility, args[2]);
  } else if (fn === 'findOne') {
    NAMESPACER.addFieldFindOne(visibility, args);
  } else if (fn.match(/^find/)) {
    NAMESPACER.addFieldFind(visibility, args);
  } else if (fn === 'insert') {
    NAMESPACER.filterUpdate(visibility, args[0]);
  } else if (fn === 'update') {
    NAMESPACER.filterUpdate(visibility, args[1]);
  }
};

module.exports = NAMESPACER;


<|start_filename|>lib/helpers/update_document.js<|end_filename|>
/* updates a document client side
 * so you can have a representation of that document
 * after being updated */
var _ = require('underscore')
  , utils = require('../utils');

function _strategy(op, ret, key, value) {
  var is_destructive = ['$unset', '$pop', '$pull', '$pullAll', '$rename'].indexOf(op) !== -1
    , parsed = utils.inspect(ret, key, !is_destructive);

  ret = parsed[0];
  key = parsed[1];

  switch (op) {
  case '$inc':
    ret[key] = ret[key] ? ret[key] + value : value;
    break;
  case '$set':
    ret[key] = value;
    break;
  case '$unset':
    if (typeof ret !== 'undefined') {
      delete ret[key];
    }
    break;
  case '$push':
    if (ret[key]) {
      ret[key].push(value);
    } else {
      ret[key] = [value];
    }
    break;
  case '$pushAll':
    if (ret[key]) {
      value.forEach(function (el) {
        ret[key].push(el);
      });
    } else {
      ret[key] = value;
    }
    break;
  case '$addToSet':
    if (ret[key]) {
      if (ret[key].indexOf(value) === -1) {
        ret[key].push(value);
      }
    } else {
      ret[key] = [value];
    }
    break;
  case '$pop':
    if (typeof ret !== 'undefined') {
      if (value === 1) {
        ret[key].pop(value);
      } else {
        ret[key].shift(value);
      }
    }
    break;
  case '$pull':
    if (typeof ret !== 'undefined') {
      ret[key] = ret[key].filter(function (f) {
        return f !== value;
      });
    }
    break;
  case '$pullAll':
    if (typeof ret !== 'undefined') {
      ret[key] = ret[key].filter(function (f) {
        return value.indexOf(f) === -1;
      });
    }
    break;
  case '$rename':
    if (typeof ret !== 'undefined') {
      ret[value] = ret[key];
      delete ret[key];
    }
    break;
  }
}

module.exports = function (VALIDATOR) {
  var keys = Object.keys(VALIDATOR.update)
    , special_op = keys[0] && keys[0][0] === '$'
    , ret = special_op ? _.clone(VALIDATOR.document) : _.clone(VALIDATOR.update)
    , op, key;


  if (special_op) {
    for (op in VALIDATOR.update) {
      if (VALIDATOR.update.hasOwnProperty(op)) {
        for (key in VALIDATOR.update[op]) {
          if (VALIDATOR.update[op].hasOwnProperty(key)) {
            _strategy(op, ret, key, VALIDATOR.update[op][key]);
          }
        }
      }
    }
  }

  return ret;
};


<|start_filename|>test/helpers_namespacer_test.js<|end_filename|>
/*globals describe, it*/
var assert = require('assert')
  , Namespacer = require('./../lib/helpers/namespacer');

describe('namespacer', function () {
  it('`resolveNamespace` implements the whole namespace API', function () {
    var test = Namespacer.resolveNamespace
      , namespaces = { jazz: ['mile.davis', 'coltrane', 'ellington', 'fitzgerald']
                     , modern: { extend: 'jazz'
                               , add: ['cohen', 'corea']
                               , remove: ['ellington', 'fitzgerald'] }};

    assert.deepEqual(test(namespaces, 'jazz'), namespaces.jazz);
    assert.deepEqual(test(namespaces, 'modern'), ['mile.davis', 'coltrane', 'cohen', 'corea']);
  });

  it('`addFieldFindOne` adds `fields` to findOne method if using a namespace', function () {
    var test = Namespacer.addFieldFindOne
      , args
      , cb = function () {}
      , fields = {fields: {zemba: 1, fleiba: 1, 'nested.attribute': 1}}
      , visibility = ['nested.attribute', 'zemba', 'fleiba'];

    args = [{_id: 1}, cb];
    test(visibility, args);
    assert.deepEqual(args[1], fields);

    args = [{_id: 1}, {timeout: 1}, cb];
    test(visibility, args);
    assert.deepEqual(args[1].fields, fields.fields);
    assert.deepEqual(args[1].timeout, 1);

    args = [{_id: 1}, {fields: {zemba: -1}}, cb];
    test(visibility, args);
    assert.deepEqual(args[1], fields);
  });

  it('`addFieldFind` adds `fields` to find methods if using a namespace', function () {
    var test = Namespacer.addFieldFind
      , args
      , cb = function () {}
      , fields = {zemba: 1, fleiba: 1, 'nested.attribute': 1}
      , visibility = ['nested.attribute', 'zemba', 'fleiba'];

    args = [{_id: 1}, cb];
    test(visibility, args);
    assert.deepEqual(args[1], fields);

    args = [{_id: 1}, {skip: 1}, cb];
    test(visibility, args);
    assert.deepEqual(args[1], fields);

    args = [{_id: 1}, {zemba: -1}, cb];
    test(visibility, args);
    assert.deepEqual(args[1], fields);

    args = [{_id: 1}, {zemba: -1}, {skip: 1}, cb];
    test(visibility, args);
    assert.deepEqual(args[1], fields);
  });

  it('`addFieldFindAndModify` adds `fields` to findAndModify method if using a namespace', function () {
    var test = Namespacer.addFieldFindAndModify
      , args
      , cb = function () {}
      , fields = {fields: {zemba: 1, fleiba: 1, 'nested.attribute': 1}}
      , visibility = ['nested.attribute', 'zemba', 'fleiba'];

    args = [{_id: 1}, cb];
    test(visibility, args);
    assert.deepEqual(args[3], fields);

    args = [{_id: 1}, [], cb];
    test(visibility, args);
    assert.deepEqual(args[3], fields);

    args = [{_id: 1}, [], {foo: 'bar'}, cb];
    test(visibility, args);
    assert.deepEqual(args[3], fields);

    args = [{_id: 1}, [], {foo: 'bar'}, {fields:  {zemba: -1}}, cb];
    test(visibility, args);
    assert.deepEqual(args[3], fields);
  });

  it('`filterUpdate` should filter documents before being inserted or updated', function () {
    var test = Namespacer.filterUpdate
      , arg
      , insert = {django: {reinhardt: 'guitar', framework: null}, charlie: {parker: 'saxophone'}}
      , update = {django: {reinhardt: 'guitar', framework: null}, 'charlie.parker': 'saxophone'}
      , visibility = ['django', 'charlie.parker'];

    // insert/update
    arg = {django: {reinhardt: 'guitar', framework: null}, charlie: {christian: 'guitar', parker: 'saxophone'}};
    test(visibility, arg);
    assert.deepEqual(arg, insert);

    // insert/update
    arg = { django: {reinhardt: 'guitar', framework: null}, will_not: 'be_inserted', charlie: { christian: 'guitar', parker: 'saxophone'}};
    test(visibility, arg);
    assert.deepEqual(arg, insert);

    // insert/update
    arg = [
      {django: {reinhardt: 'guitar', framework: null}, will_not: 'be_inserted', charlie: { christian: 'guitar', parker: 'saxophone'}}
    , {django: {reinhardt: 'guitar', framework: null}, will_not: 'be_inserted'}
    ];
    test(visibility, arg);
    assert.deepEqual(arg[0], {django: {reinhardt: 'guitar', framework: null}, charlie: {parker: 'saxophone'}});
    assert.deepEqual(arg[1], {django: {reinhardt: 'guitar', framework: null}});

    // update with sepcial ops
    arg = {'$set': {django: {reinhardt: 'guitar', framework: null}, 'charlie.christian': 'guitar', 'charlie.parker': 'saxophone'}};
    test(visibility, arg);
    assert.deepEqual(arg, {'$set': update});

    // update with sepcial ops dot notation
    arg = {'$set': {'django.reinhardt': 'piano'}};
    test(visibility, arg);
    assert.deepEqual(arg, {'$set': {'django.reinhardt': 'piano'}});

    // update with sepcial ops
    arg = {'$set': {django: {reinhardt: 'guitar', framework: null}, will_not: 'be_update', charlie: { christian: 'guitar', parker: 'saxophone'}}};
    test(visibility, arg);
    assert.deepEqual(arg, {'$set': {django: {reinhardt: 'guitar', framework: null}, charlie: {parker: 'saxophone'}}});
  });
});


<|start_filename|>lib/model.js<|end_filename|>
var _ = require('underscore');

module.exports = function (db, collection_name) {

  if (!db) {
    throw (Error('You must specify a db'));
  }

  if (!collection_name) {
    throw (Error('You must specify a collection name'));
  }

  var MODEL = {
    db: db
  , collection_proxy: require('./helpers/collection_proxy')
  };

  /**
   * Gets the mongo collection
   *
   * @param {Function} callback
   * @returns itself
   */
  MODEL.getCollection = function (callback) {
    db.collection(collection_name, callback);

    return MODEL;
  };

  MODEL.validator = require('./validator');

  /**
   * Proxies `collection` calls and then:
   *
   *  + Namespacing: Filters sent and received documents according to namespaces.
   *  + Mapping: Applies functions to sent and received document attributes.
   *  + Hooks: Calls some before* and after* hooks
   *
   * This functionalities are enabled by default, but you can disable them by providing
   * options as the first param.
   *
   * Adds custom `findArray`, `mapReduceCursor` and `mapReduceArray`.
   *
   * @param {String|Object} options
   * @param {Mixed} args
   * @param {Function} callback
   * @return itself
   */
  MODEL.mongo = function (args) {
    var parsed_method, options, callback;

    args = _.toArray(arguments);
    options = args.shift();

    if (typeof options === 'string') {
      parsed_method = options.match(/([a-zA-Z]*)(?::([a-zA-Z]*))?/); // extract the namespace
      options = {
        method: parsed_method[1]
      , namespace: parsed_method[2]
      };
    }

    if (!options.method) {
      throw (Error('`method` is mandatory!'));
    }

    options = _.extend({
      hooks: true
    , namespacing: true
    , mapping: true
    }, options);

    MODEL.collection_proxy.proxy(MODEL, options, args);

    return MODEL;
  };

  /**
   * Validates a mongo document
   *
   * @param {Object} document
   * @param {Object} update
   * @param {Function} callback
   */
  MODEL.validate = function (document, update, callback) {
    callback(null, MODEL.validator(document, update));
  };

  /**
   * Validates a mongo document and inserts it
   *
   * TODO: Refactor
   *
   * @param {Object} document
   * @param {Object} options
   * @param {Function} callback
   * @returns itself
   */
  MODEL.validateAndInsert = function (document, options, callback) {
    // options are optional
    if (typeof options === 'function') {
      callback = options;
      options = {};
    } else {
      options = options || {};
    }

    var fn = {method: 'insert', namespacing: false, mapping: false}
      , args = [document];

    if (options.namespace) {
      fn.namespace = options.namespace;
    }

    if (MODEL.namespaces && MODEL.namespaces[options.namespace]) {
      // has side effects, alters args
      MODEL.collection_proxy.namespacer.filter(MODEL.namespaces, options.namespace, 'insert', args);
    }

    if (MODEL.maps) {
      // has side effects, alters args
      MODEL.collection_proxy.mapper.map(MODEL.maps, 'insert', args);
    }

    MODEL.validate({}, document, function (error, validator) {
      if (error) {
        return callback(error);
      }

      if (validator.hasErrors()) {
        return callback(null, validator);
      }

      args.push(function cb(error, documents) {
        if (error) {
          return callback(error);
        } else {
          if (documents) {
            validator.updated_document = documents[0];
          }
          callback(error, validator);
        }
      });
      args.unshift(fn);
      MODEL.mongo.apply(MODEL, args);
    });

    return MODEL;
  };

  /**
   * Hook triggered before inserting a document
   *
   * @param {Array} documents
   * @param {Function} callback
   */
  MODEL.beforeInsert = function (documents, callback) {
    documents.forEach(function (document) {
      if (!document.created_at) {
        document.created_at = new Date();
      }
    });
    callback(null, documents);
  };

  /**
   * Hook triggered after inserting a document
   *
   * @param {Array} documents
   * @param {Function} callback
   */
  MODEL.afterInsert = function (documents, callback) {
    callback(null, documents);
  };

  /**
   * Hook triggered before updating a document
   *
   * @param {Object} query
   * @param {Object} update
   * @param {Function} callback
   */
  MODEL.beforeUpdate = function (query, update, callback) {
    update.$set = update.$set || {};
    update.$set.updated_at = new Date();
    callback(null, query, update);
  };

  /**
   * Hook triggered after updating a document
   *
   * @param {Object} query
   * @param {Object} update
   * @param {Function} callback
   */
  MODEL.afterUpdate = function (query, update, callback) {
    callback(null, query, update);
  };

  /**
   * Hook triggered before removing a document
   *
   * @param {Object} query
   * @param {Function} callback
   */
  MODEL.beforeRemove = function (query, callback) {
    callback(null, query);
  };

  /**
   * Hook triggered after removing a document
   *
   * @param {Object} query
   * @param {Function} callback
   */
  MODEL.afterRemove = function (query, callback) {
    callback(null, query);
  };

  /**
   * Validates a mongo document and updates it
   *
   * @param {Object} query      - Document query
   * @param {Object} update     - Update object
   * @param {Object} options    - Update options. Defaults to {upsert: false, multi: false}
   * @param {Function} callback - The callback returns a validator
   *
   * @returns itself
   */
  MODEL.validateAndUpdate = function (query, update, options, callback) {
    // options are optional
    if (typeof options === 'function') {
      callback = options;
      options = {};
    } else {
      options = options || {};
    }

    options = _.defaults(options, {upsert: false, multi: false});

    var fn = {method: 'update', namespacing: false, mapping: false}
      , args = [query, update, options];

    if (options.namespace) {
      fn.namespace = options.namespace;
    }

    delete options.namespace;

    if (MODEL.namespaces && MODEL.namespaces[options.namespace]) {
      // has side effects, alters args
      MODEL.collection_proxy.namespacer.filter(MODEL.namespaces, options.namespace, 'update', args);
    }

    if (MODEL.maps) {
      // has side effects, alters args
      MODEL.collection_proxy.mapper.map(MODEL.maps, 'update', args);
    }

    MODEL.mongo('findOne', query, function (error, document) {
      if (error) {
        return callback(error);
      }

      MODEL.validate(document, update, function (error, validator) {
        if (error) {
          return callback(error);
        }

        if (validator.hasErrors()) {
          callback(null, validator);
        } else {
          args.push(function cb(error, _) {
            callback(error, validator);
          });

          args.unshift(fn);
          args[1] = {_id: document._id};
          MODEL.mongo.apply(MODEL, args);
        }
      });
    });

    return MODEL;
  };

  /**
   * Get the document ready to embed to this model according to the skeleton
   *
   * TODO: Refactor this method! Or take it away as a helper
   *
   * @param {String} name
   * @param {Object} document
   * @param {String} scope         - Optional field to scope the resulting document
   * @param {Boolean} dot_notation - Return the nested object or using dot_notation ready for mongo
   *
   * @returns document with applied skeleton
   */
  MODEL.getEmbeddedDocument = function (name, document, scope, dot_notation) {
    var filtered = _.clone(document)
      , result = {}
      , pointer = result;


    function parseAttr(scope, attr) {
      return dot_notation ? ((scope ? scope + '.' : '') + attr) : attr;
    }

    /**
     * 0: not visible
     * 1: partial visible, needs more inspection
     * 2: fully visible
     */
    function isVisible(attr, level) {
      var ret = 0;

      if (MODEL.skeletons && MODEL.skeletons[name]) {
        _.some(MODEL.skeletons[name], function (el) {
          var match;
          if (attr.indexOf('.') !== -1) {
            match = attr.match(new RegExp(el + '(\..*)?'));
            if (match) {
              ret = 2;
            }
          } else {
            el = el.split('.');
            match = el[level] === attr;
            if (match) {
              ret = el.length === level + 1 ? 2 : 1;
            }
          }
          return match;
        });
      } else {
        return 2; // if no skeleton available, everything is visible!
      }

      return ret;
    }

    function filter(el, level, accumulated_scope) {
      var attr, is_visible;

      for (attr in el) {
        if (el.hasOwnProperty(attr)) {
          is_visible = isVisible(attr, level);
          if (is_visible === 0) {
            delete el[attr];
          } else {

            if (is_visible === 1 && typeof el[attr] === 'object') {
              filter(el[attr], level + 1, accumulated_scope ? accumulated_scope + '.' + attr : attr);
              if (dot_notation) {
                delete el[attr];
              }
            } else {
              if (dot_notation && parseAttr(attr) !== attr) {
                if (accumulated_scope) {
                  filtered[parseAttr(parseAttr(scope, accumulated_scope), attr)] = el[attr];
                } else {
                  el[parseAttr(scope, attr)] = el[attr];
                }
                delete el[attr];
              }
            }
          }
        }
      }
    }

    filter(filtered, 0);

    if (scope && !dot_notation) {
      scope.split('.').forEach(function (level, i, array) {
        pointer = pointer[level] = (i === array.length - 1) ? filtered : {};
      });
      return result;
    } else {
      return filtered;
    }
  };

  /**
   * Update all the embedded objects
   *
   * @param {ObjectID} id          - Id of the embedded document to be updated
   * @param {String} document_name - Name for the embedded document
   * @param {Object} document      - Document to be embedded
   * @param {Object} options       - Update options. Defaults to {upsert: false, multi: true}
   * @param {Function} callback
   *
   * @returns itself
   */
  MODEL.updateEmbeddedDocument = function (query, document_name, document, options, callback) {
    var update = {},
        scoped_query = {};

    document = document.$set || document;

    update = MODEL.getEmbeddedDocument(document_name, document, document_name, true);

    // options are optional
    if (typeof options === 'function') {
      callback = options;
      options = {};
    } else {
      options = options || {};
    }

    options = _.extend({upsert: false, multi: true}, options);
    Object.keys(query).forEach(function (attr) {
      scoped_query[document_name + '.' + attr] = query[attr];
    });

    MODEL.mongo({method: 'update', hooks: false}, scoped_query, {'$set': update}, options, callback);

    return MODEL;
  };

  /**
   * Push an embedded document.
   *
   * @param {Object} query          - Query to match the objects to push the embedded doc
   * @param {String} document_name  - Name for the embedded document array
   * @param {Object} document       - Document to be push
   * @param {Object} options        - Update options. Defaults to {upsert: false, multi: true}
   * @param {Function} callback
   *
   * @returns itself
   */
  MODEL.pushEmbeddedDocument = function (query, document_name, document, options, callback) {
    var update = {};

    update[document_name] = MODEL.getEmbeddedDocument(document_name, document);

    // options are optional
    if (typeof options === 'function') {
      callback = options;
      options = {};
    } else {
      options = options || {};
    }

    options = _.extend({upsert: false, multi: true}, options);
    MODEL.mongo({method: 'update', hooks: false}, query, {'$push': update}, options, callback);

    return MODEL;
  };

  /**
   * Add some shortcuts for collection methods
   */
  _.each([ 'find', 'findArray', 'insert', 'update', 'remove', 'rename', 'insertAll'
         , 'save', 'distinct', 'count', 'drop', 'findAndModify', 'findAndRemove'
         , 'findOne', 'mapReduce', 'mapReduceArray', 'mapReduceCursor', 'group'
         , 'aggregate', 'stats'], function (method) {
    MODEL[method] = function () {
      MODEL.mongo.apply(MODEL, [method].concat(_.toArray(arguments)));
    };
  });

  return MODEL;
};


<|start_filename|>example/controllers/users.js<|end_filename|>
module.exports = function (APP) {
  var app = APP.app
    , User = APP.loadModel('user')
    , ObjectID = APP.ObjectID;

  // show user
  app.get('/users/:id', function (req, res) {
    var id = req.param('id');

    User().mongo('findOne', {_id: new ObjectID(id)}, function (error, user) {
      if (error) throw error;

      res.render('users/show', {
        title: 'Paco'
      , user: user
      , posts: user.posts
      });
    });
  });

  // create user
  app.post('/users', function (req, res) {
    var document = req.param('user');

    User().getCollection(function (error, collection) {
      document._id = collection.pkFactory.createPk();

      User().validateAndInsert(document, function (error, validator) {
        if (error) throw error;

        if (validator.hasErrors()) {
          // Show flash error
          console.log(validator.errors);
          res.redirect('/');
        } else {
          res.redirect('/users/' + document._id + '/');
        }
      });
    });
  });

  // update user
  app.post('/users/:id', function (req, res) {
    var id = req.param('id')
      , update = req.param('user');

    User().validateAndUpdate({_id: new ObjectID(id)}, {'$set': update}, function (error, validator) {
      if (error) throw error;
      if (validator.hasErrors()) console.log(validator.errors);

      res.redirect('/users/' + id + '/');
    });
  });
};


<|start_filename|>Makefile<|end_filename|>
MOCHA = ./node_modules/.bin/mocha --reporter dot

test:
	@$(MOCHA)

test_model:
	@$(MOCHA) test/model_test

test_validator:
	@$(MOCHA) test/validator_test.js

test_update_document:
	@$(MOCHA) test/helpers_update_document_test.js

test_namespacer:
	@$(MOCHA) test/helpers_namespacer_test.js

test_mapper:
	@$(MOCHA) test/helpers_mapper_test.js

test_proxy:
	@$(MOCHA) test/helpers_collection_proxy_test.js

test_integration:
	@$(MOCHA) test/integration_test.js

.PHONY: test


<|start_filename|>test/integration_test.js<|end_filename|>
/*globals describe, it, before*/
var assert = require('assert')

  , Model = require('./../lib/model')
  , mongodb = require('mongodb')
  , Db = mongodb.Db
  , Server = mongodb.Server
  , db = new Db('mongolia_test', new Server('localhost', 27017, {auto_reconnect: true, native_parser: true}), {})

  , remove_users = function (cb) {
      db.collection('users', function (error, collection) {
        collection.remove({}, function () {
          db.collection('countries', function (error, collection) {
            collection.remove({}, cb);
          });
        });
      });
    };

describe('Integration test', function () {

  before(function (done) {
    db.open(function (error) {
      if (error) {
        throw error;
      }

      remove_users(done);
    });
  });

  it('`Insert` documents with `before/afterInsert` hooks', function (done) {
    var User = Model(db, 'users'),
        Country = Model(db, 'countries');

    User.beforeInsert = function (documents, callback) {
      documents.forEach(function (document) {
        document.has_country = true;
      });
      callback(null, documents);
    };

    User.afterInsert = function (documents, callback) {
      documents.forEach(function (document) {
        document.comments = [];
      });
      callback(null, documents);
    };

    User.namespaces = {
      'public': ['name', 'country']
    };

    Country.maps = {
      name: function capitalize(val) {
              return val.charAt(0).toUpperCase() + val.slice(1);
            }
    , iso: {id: Number}
    };

    Country.mongo('insert', {name: 'andorra', iso: {id: '123'}}, function (error, docs) {
      var doc = docs[0];
      assert.equal(doc.name, 'Andorra');
      assert.ok(doc.created_at);
      assert.ok(doc.iso.id, 123);

      User.mongo('insert:public', {name: 'zemba', country: doc, password: '<PASSWORD>'}, function (error, docs) {
        var doc = docs[0];
        assert.equal(doc.name, 'zemba');
        assert.equal(doc.has_country, true);
        assert.deepEqual(doc.country.name, 'Andorra');
        assert.equal(doc.password, null); // namespaced!
        assert.deepEqual(doc.comments, []);
        done();
      });
    });
  });

  it('`Update embedded` documents', function (done) {
    var User = Model(db, 'users'),
        Country = Model(db, 'countries');

    Country.findOne({name: 'Andorra'}, function (error, doc) {
      User.updateEmbeddedDocument({_id: doc._id}, 'country', {name: 'France'}, {}, function (error) {
        User.mongo('findOne', {name: 'zemba'}, function (error, doc) {
          assert.equal(doc.country.name, 'France');
          assert.equal(doc.country.iso.id, '123');
          done();
        });
      });
    });
  });

  it('`Push embedded` documents', function (done) {
    var User = Model(db, 'users'),
        funk = require('funk')(),
        query = {name: 'zemba'};

    User.pushEmbeddedDocument(query, 'comments', {body: 'bla bla bla'}, {}, funk.nothing());
    User.pushEmbeddedDocument(query, 'comments', {body: 'trolling bla'}, {}, funk.nothing());

    funk.parallel(function () {
      User.mongo('findOne', {name: 'zemba'}, function (error, doc) {
        assert.deepEqual(doc.comments, [
          {body: 'bla bla bla'}
        , {body: 'trolling bla'}
        ]);
        done();
      });
    });
  });

  it('`findAndModify` documents with `before/afterUpdate` hooks', function (done) {
    var User = Model(db, 'users'),
        Country = Model(db, 'countries'),
        query = {name: 'zemba'},
        update = {'$set': {name: 'fleiba'}},
        calledBefore = false,
        calledAfter = false;


    User.beforeUpdate = function (_query, _update, _callback) {
      calledBefore = true;

      _update.$set.updated_at = new Date();

      Country.mongo('findOne', {name: 'Andorra'}, function (error, doc) {
        _update.$set['country.name'] = doc.name;

        assert.deepEqual(_query, query);
        assert.deepEqual(_update.$set.name, update.$set.name);
        assert.deepEqual(_update.$set['country.name'], 'Andorra');

        _callback(error, _query, _update);
      });
    };

    User.afterUpdate = function (_query, _update, _callback) {
      calledAfter = true;

      assert.deepEqual(_query, query);
      assert.deepEqual(_update.$set.name, update.$set.name);
      assert.deepEqual(_update.$set['country.name'], 'Andorra');

      _callback(null, _query, _update);
    };

    User.findAndModify(query, [], update, {'new': true}, function (error, doc) {
      assert.ok(calledBefore);
      assert.ok(calledAfter);
      assert.deepEqual(doc.country.name, 'Andorra');
      assert.deepEqual(doc.name, 'fleiba');
      done();
    });
  });

  it('`update` documents with `before/afterUpdate` hooks', function (done) {
    var User = Model(db, 'users'),
        Country = Model(db, 'countries'),
        query = {name: 'fleiba'},
        update = {'$set': {name: 'zemba'}},
        calledBefore = false,
        calledAfter = false;


    User.beforeUpdate = function (_query, _update, _callback) {
      calledBefore = true;

      _update.$set.updated_at = new Date();

      Country.mongo('findOne', {name: 'Andorra'}, function (error, doc) {
        _update.$set['country.name'] = 'France';

        assert.deepEqual(_query, query);
        assert.deepEqual(_update.$set.name, update.$set.name);

        _callback(error, _query, _update);
      });
    };

    User.afterUpdate = function (_query, _update, _callback) {
      calledAfter = true;

      assert.deepEqual(_query, query);
      assert.deepEqual(_update.$set.name, update.$set.name);
      assert.deepEqual(_update.$set['country.name'], 'France');

      _callback(null, _query, _update);
    };

    User.update(query, update, function (error, doc) {
      assert.ok(calledBefore);
      assert.ok(calledAfter);
      User.mongo('findArray', update.$set, function (error, docs) {
        var doc = docs[0];
        assert.deepEqual(doc.country.name, 'France');
        assert.deepEqual(doc.name, 'zemba');
        done();
      });
    });
  });

  it('`Remove` documents with `before/afterRemove` hooks', function (done) {
    var User = Model(db, 'users'),
        query = {name: 'zemba'},
        calledBefore = false,
        calledAfter = false;

    User.beforeRemove = function (_query, callback) {
      calledBefore = true;
      assert.deepEqual(query, _query);
      callback(null, _query);
    };

    User.afterRemove = function (_query, callback) {
      calledAfter = true;
      assert.deepEqual(query, _query);
      callback(null);
    };

    User.remove(query, function (error, ret) {
      assert.ok(calledBefore);
      assert.ok(calledAfter);
      User.mongo('findArray', {}, function (error, docs) {
        assert.deepEqual(docs, []);
        done();
      });
    });
  });

  it('`validateAndInsert` validates and inserts', function (done) {
    var User = Model(db, 'users');

    User.validate = function (document, update, callback) {
      var validator = require('./../lib/validator')(document, update);

      if (update.name !== 'zemba') {
        validator.addError('name', 'We only love Zemba here');
      }

      callback(null, validator);
    };

    User.validateAndInsert({name: 'zemba'}, function (error, validation) {
      assert.equal(validation.updated_document.name, 'zemba');
      assert.deepEqual(validation.errors, {});

      // Try to insert an invalid record
      User.validateAndInsert({name: 'barbaz'}, function (error, validation) {
        assert.deepEqual(validation.errors.name, ['We only love Zemba here']);
        done();
      });
    });
  });

  it('`validateAndUpdate` validates and updates', function (done) {
    var User = Model(db, 'users');

    User.mongo('insert', {name: '<NAME>', age: 30}, function (errors, documents) {});

    User.validate = function (document, update, callback) {
      var validator = require('./../lib/validator')(document, update);

      if (validator.updated_document.name !== 'zemba') {
        validator.addError('name', 'We only love Zemba here');
      }

      callback(null, validator);
    };

    User.validateAndUpdate({name: '<NAME>'}, {'$set': {name: 'foobar'}}, function (error, validation) {
      assert.deepEqual(validation.errors.name, ['We only love Zemba here']);
      assert.deepEqual(validation.updated_document.name, 'foobar');

      User.validateAndUpdate({name: '<NAME>'}, {'$set': {name: 'zemba'}}, function (error, validation) {
        assert.deepEqual(validation.errors, {});
        assert.deepEqual(validation.updated_document.name, 'zemba');
        done();
      });
    });
  });
});


<|start_filename|>test/helpers_mapper_test.js<|end_filename|>
/*global describe, it*/
var assert = require('assert')
  , Mapper = require('./../lib/helpers/mapper');

describe('mapper', function () {
  it('`mapDocument` should filter documents before being inserted or updated', function () {
    var test = Mapper.mapDocument
      , arg
      , toUpper = function (val) {
          return val.toUpperCase();
        }
      , update = {zemba: 'FOO', nested: {id: 123, name: '300'}, list: [1, 2, 3], foo: true}
      , maps = { zemba: toUpper
               , nested: { id: Number
                         , name: String
                         }
               , list: Number
               , foo: Boolean
               };

    // document
    arg = {zemba: 'foo', nested: {id: '123', name: 300}, list: [1, 2, 3], foo: 'true'};
    test(maps, arg);
    assert.deepEqual(arg, update);
    assert.equal(typeof arg.nested.id, typeof update.nested.id);
    assert.equal(typeof arg.nested.name, typeof update.nested.name);

    // array
    arg = [{zemba: 'fleiba'}, {zemba: 'foo', nested: {id: '123', name: 300}, list: [1, 2, 3], foo: 'true'}];
    test(maps, arg);
    assert.deepEqual(arg[0], {zemba: 'FLEIBA'});
    assert.deepEqual(arg[1], update);
    assert.equal(typeof arg[1].nested.id, typeof update.nested.id);
    assert.equal(typeof arg[1].nested.name, typeof update.nested.name);

    // with sepcial ops
    arg = {'$set': {zemba: 'foo', nested: {id: '123', name: 300}, list: [1, 2, 3], foo: 'true'}};
    test(maps, arg);
    assert.deepEqual(arg, {'$set': update});
    assert.equal(typeof arg.$set.nested.id, typeof update.nested.id);
    assert.equal(typeof arg.$set.nested.name, typeof update.nested.name);

    // dot notation
    arg = {'$set': {'nested.id': '123', 'nested.name': 300, foo: 'true'}};
    test(maps, arg);
    assert.equal(typeof arg.$set['nested.id'], typeof update.nested.id);
    assert.equal(typeof arg.$set['nested.name'], typeof update.nested.name);

    // array values
    arg = {list: ['1', '2', 3]};
    test(maps, arg);
    assert.equal(typeof arg.list[0], typeof update.list[0]);
    assert.equal(typeof arg.list[1], typeof update.list[1]);
    assert.equal(typeof arg.list[2], typeof update.list[2]);
  });
});



<|start_filename|>test/validator_test.js<|end_filename|>
/*globals describe, beforeEach, it*/
var assert = require('assert')
  , Validator = require('./../lib/validator');

describe('validators', function () {

  it('initial status', function () {
    var update = {'$set': {foo: 'bar'}}
      , doc = {foo: 'zemba', hey: 'joe'}
      , val = Validator({}, update)
      , val2 = Validator(doc, update);

    assert.deepEqual(val.errors, {});
    assert.deepEqual(val.document, {});
    assert.deepEqual(val.update, update);
    assert.deepEqual(val.updated_document, {foo: 'bar'});

    assert.deepEqual(val2.errors, {});
    assert.deepEqual(val2.document, doc);
    assert.deepEqual(val2.update, update);
    assert.deepEqual(val2.updated_document, {foo: 'bar', hey: 'joe'});
  });

  it('`addError` adds a validation error', function () {
    var val = Validator({}, {foo: 'bar'})
      , error = 'foo error';

    val.addError('foo', error);
    assert.equal(val.errors.foo[0], error);
  });

  it('`hasErrors` returns true if the validator has errors', function () {
    var val = Validator({}, {foo: 'bar'});

    val.addError('foo', 'foo error');
    assert.ok(val.hasErrors());
  });

  it('`hasError` returns true if the validator has a particular error', function () {
    var val = Validator({}, {foo: 'bar'});

    val.addError('foo', 'foo error');
    assert.ok(val.hasError('foo'));
  });

  it('nested `addError` adds a nested error', function () {
    var val = Validator({}, {foo: 'bar'});

    val.addError('foo.zemba', 'foo error');
    assert.equal(val.errors.foo.zemba[0], 'foo error');
  });

  it('`hasErrors` returns whether the validator has nested errors', function () {
    var val = Validator({}, {foo: 'bar'});

    val.addError('foo.zemba', 'foo error');
    assert.ok(val.hasErrors());
  });

  it('`hasError` returns whether the validator has a particular nested error', function () {
    var val = Validator({}, {foo: 'bar'});

    val.addError('foo.zemba', 'foo error');
    assert.ok(val.hasError('foo.zemba'));
  });

  it('`hasError` and `hasErrors` return false when there are no errors', function () {
    var val = Validator({}, {foo: 'bar'});

    assert.equal(val.hasError('foo.zemba'), false);
    assert.equal(val.hasErrors(), false);
  });

  it('multiple errors per field', function () {
    var val = Validator({}, {foo: 'bar'});

    val.addError('foo.zemba', 'error1');
    assert.ok(val.hasError('foo.zemba'));

    val.addError('foo.zemba', 'error2');
    assert.deepEqual(val.errors.foo.zemba, ['error1', 'error2']);
    assert.equal(val.hasError('foo.zemba'), true);

    val.addError('foo.bla', 'error3');
    assert.deepEqual(val.errors.foo.zemba, ['error1', 'error2']);
    assert.deepEqual(val.errors.foo.bla, ['error3']);
  });

  it('`isUpdating` returns whether the model is being updated', function () {
    var update = {foo: 'bar'}
      , val = Validator({}, update)
      , val2 = Validator({zemba: 'fleiba'}, update);

    assert.equal(val.isUpdating(), false);
    assert.equal(val2.isUpdating(), true);
  });

  it('`isInserting` returns whether the model is being inserted', function () {
    var update = {foo: 'bar'}
      , val = Validator({}, update)
      , val2 = Validator({zemba: 'fleiba'}, update);

    assert.equal(val.isInserting(), true);
    assert.equal(val2.isInserting(), false);
  });

  it('`attrChanged` returns whether the attribute changed', function () {
    var update = {foo: 'bar'}
      , val = Validator({}, update)
      , val2 = Validator({foo: 'zemba'}, update)
      , val3 = Validator({foo: 'bar'}, update);

    assert.equal(val.attrChanged('foo'), true);
    assert.equal(val2.attrChanged('foo'), true);
    assert.equal(val3.attrChanged('foo'), false);
  });

  it('`getErrorBucket` gets or sets the error bucket for the attribute', function () {
    var update = {foo: 'bar'}
      , val = Validator({}, update);

    assert.deepEqual(val.errors, {});
    val.getErrorBucket('foo', true);
    assert.deepEqual(val.errors, {foo: []});
    assert.deepEqual(val.getErrorBucket('foo'), []);
    val.addError('foo', 'ERROR');
    assert.deepEqual(val.getErrorBucket('foo'), ['ERROR']);
  });

  it('`getErrorBucket` also works with nested attributes', function () {
    var update = {'$set': {'foo.bar': 'zemba'}}
      , val = Validator({}, update);

    assert.deepEqual(val.errors, {});
    val.getErrorBucket('foo.bar', true);
    assert.deepEqual(val.errors, {foo: {bar: []}});
    assert.deepEqual(val.getErrorBucket('foo.bar'), []);
    val.addError('foo.bar', 'ERROR');
    assert.deepEqual(val.getErrorBucket('foo.bar'), ['ERROR']);
  });

  it('`validateExistence` validates existence of attributes', function () {
    var update = {'$set': {hey: 'joe', 'foo.bar': 'zemba'}}
      , val = Validator({}, update);

    assert.deepEqual(val.hasErrors(), false);

    val.validateExistence({
      hey: 'hey error'
    , 'foo.bar': 'foo.bar error'
    , 'foo.boo': 'foo.boo error'
    , inexistant: 'inexistant error'
    });

    assert.ok(val.hasError('foo.boo'));
    assert.ok(val.hasError('inexistant'));
    assert.deepEqual(val.getErrorBucket('foo.boo'), ['foo.boo error']);
    assert.deepEqual(val.getErrorBucket('inexistant'), ['inexistant error']);
  });

  it('`validateRegex` validates attributes matching regexes', function () {
    var update = {'$set': {hey: 'joe', 'foo.bar': 'zemba'}}
      , val = Validator({}, update);

    assert.deepEqual(val.hasErrors(), false);

    val.validateRegex({
      hey: [/joe/, 'hey error']
    , 'foo.bar': [/[0-9]+/, 'foo.bar error']
    , 'foo.boo': [/.*/, 'foo.boo error']
    , inexistant: [/.*/, 'inexistant error']
    });

    assert.equal(val.hasError('hey'), false);
    assert.ok(val.hasError('foo.bar'));
    assert.ok(val.hasError('foo.boo'));
    assert.ok(val.hasError('inexistant'));
    assert.deepEqual(val.getErrorBucket('foo.bar'), ['foo.bar error']);
    assert.deepEqual(val.getErrorBucket('foo.boo'), ['foo.boo error']);
    assert.deepEqual(val.getErrorBucket('inexistant'), ['inexistant error']);
  });

  it('`validateConfirmation` validates attributes being equal', function () {
    var update = {'$set': {hey: 'joe', 'foo.bar': 'zemba', 'foo.boo': 'zemba'}}
      , val = Validator({}, update);

    assert.deepEqual(val.hasErrors(), false);

    val.validateConfirmation({
      hey: ['foo.cla', 'hey error']
    , 'foo.bar': ['foo.boo', 'foo.bar error']
    , inexistant: ['another inexistant', 'inexistant error']
    });

    assert.ok(val.hasError('hey'));
    assert.equal(val.hasError('foo.bar'), false);
    assert.ok(val.hasError('inexistant'));
    assert.deepEqual(val.getErrorBucket('hey'), ['hey error']);
    assert.deepEqual(val.getErrorBucket('inexistant'), ['inexistant error']);
  });
});


<|start_filename|>example/models/comment.js<|end_filename|>
module.exports = function (APP) {
  var COMMENT = APP.model(APP.db, 'comments'),
      Post = APP.loadModel('post');

  COMMENT.skeletons = {
    post: ['_id', 'title']
  };

  COMMENT.validate = function (document, update, callback) {
    var validator = APP.validator(document, update);

    validator.validateExistence({
      body: 'Body is mandatory',
      post: 'A comment needs a post'
    });

    callback(null, validator);
  };

  COMMENT.afterInsert = function (documents, callback) {
    var funk = require('funk')();

    documents.forEach(function (document) {
      Post().pushEmbeddedDocument({_id: document.post._id}, 'comments', document, funk.nothing());
    });

    funk.parallel(callback);
  };

  return COMMENT;
};


<|start_filename|>example/app.js<|end_filename|>
require('colors');

var mongodb = require('mongodb')
  , Db = mongodb.Db
  , Server = mongodb.Server
  , express = require('express')

  , model = require('./../lib/model')
  , validator = require('./../lib/validator')

  , db = new Db('blog', new Server('localhost', 27017, {auto_reconnect: true, native_parser: true}), {})
  , ObjectID = db.bson_serializer.ObjectID

  , app = express.createServer()

  , APP = {
      app: app
    , db: db
    , ObjectID: ObjectID
    , options: {port: 3000}
    , model: model
    , validator: validator
    };

APP.loadModel = function (str) {
  return function () {
    return require('./models/' + str)(APP);
  };
};

// Configure
app.configure(function () {
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');

  // middlewares
  app.use(express.favicon());
  app.use(express['static'](__dirname + '/public'));
  app.use(express.bodyParser());
  app.use(app.router);
});

// Controllers
require('./controllers/public')(APP);
require('./controllers/users')(APP);
require('./controllers/posts')(APP);

console.log('Opening database '.blue);
db.open(function () {
  app.listen(APP.options.port);
  console.log('Listening port '.green + (APP.options.port).toString().yellow);
});


<|start_filename|>example/models/user.js<|end_filename|>
module.exports = function (APP) {
  var USER = APP.model(APP.db, 'users'),
      Post = APP.loadModel('post');

  USER.validate = function (document, update, callback) {
    var validator = APP.validator(document, update);

    validator.validateRegex({
      name: [/[a-zA-Z_\s]{3,20}/, 'Incorrect name'],
      email: [validator.regex.email, 'Incorrect email'],
      password: [validator.regex.password, 'Incorrect password']
    });

    if (validator.attrChanged('password')) {
      validator.validateConfirmation({
        password: ['password_confirmation', 'Passwords must match']
      });
    }

    if (!validator.isUpdating()) {
      validator.validateQuery({
        name: [this, {name: update.name}, false, 'There is already a user with this name'],
        email: [this, {email: update.email}, false, 'There is already a user with this email']
      }, function () {
        callback(null, validator);
      });
    } else {
      callback(null, validator);
    }
  };

  USER.afterUpdate = function (query, update, callback) {
    Post().updateEmbeddedDocument({'_id': query._id}, 'author', update, callback);
  };

  return USER;
};


<|start_filename|>lib/helpers/collection_proxy.js<|end_filename|>
var PROXY = {}
  , _ = require('underscore');

function _apply(collection, fn, args) {
  collection[fn].apply(collection, args);
}

Object.defineProperty(PROXY, 'namespacer', {value: require('./namespacer'), writable: true});
Object.defineProperty(PROXY, 'mapper', {value: require('./mapper'), writable: true});

/**
 * Proxies calls
 *
 * @param {Object} model
 * @param {Object} options
 * @param {Array} args
 */
PROXY.proxy = function (model, options, args) {
  var fn = options.method
    , callback
    , arguments_length = arguments.length;

  model.getCollection(function (error, collection) {
    var is_overwritten = typeof PROXY[fn] !== 'undefined'
      , is_native = typeof collection[fn] !== 'undefined';

    if (typeof args[args.length - 1] === 'function') {
      callback =  args[args.length - 1];
    } else {
      callback = function () {};
    }

    if (error) {
      return callback(error, null);
    }

    if (options.namespacing && model.namespaces && model.namespaces[options.namespace]) {
      // has side effects, alters args
      PROXY.namespacer.filter(model.namespaces, options.namespace, fn, args);
    }

    if (options.mapping && model.maps) {
      // has side effects, alters args
      PROXY.mapper.map(model.maps, fn, args);
    }

    // overwritten method with hooks, or custom method
    if (is_overwritten && (is_native ? options.hooks : true)) {
      PROXY[fn](model, collection, args, callback);
    // driver method or hooks disabled
    } else {
      _apply(collection, fn, args);
    }
  });
};

/**
 * Calls `find` + `toArray`
 *
 * @param {Object} model
 * @param {Object} collection
 * @param {Array} args
 * @param {Function} callback
 */
PROXY.findArray = function (model, collection, args, callback) {
  args[args.length - 1] = function (error, cursor) {
    cursor.toArray(callback);
  };
  _apply(collection, 'find', args);
};

/**
 * Calls `insert`
 * and triggers the `beforeInsert` and `afterInsert` hooks
 *
 * @param {Object} model
 * @param {Object} collection
 * @param {Array} args
 * @param {Function} callback
 */
PROXY.insert = function (model, collection, args, callback) {
  args[args.length - 1] = function (error, ret) {
    if (error) return callback(error, null);

    model.afterInsert(args[0], function (error, _) {
      callback(error, ret);
    });
  };

  if (!Array.isArray(args[0])) {
    args[0] = [args[0]];
  }

  model.beforeInsert(args[0], function (error, documents) {
    if (error) return callback(error, null);

    args[0] = documents;
    _apply(collection, 'insert', args);
  });
};

/**
 * Calls `findAndModify` or `update`
 * and triggers the `beforeUpdate` and `afterUpdate` hooks
 *
 * @param {Object} model
 * @param {Object} collection
 * @param {Array} args
 * @param {Function} callback
 */
['findAndModify', 'update'].forEach(function (method) {
  PROXY[method] = function (model, collection, args, callback) {
    var update_index = method === 'update' ? 1 : 2;

    args[args.length - 1] = function (error, ret) {
      if (error) {
        return callback(error, null);
      }
      model.afterUpdate(args[0], args[update_index], function (error, _) {
        callback(error, ret);
      });
    };

    model.beforeUpdate(args[0], args[update_index], function (error, _query, _update) {
      if (error) {
        return callback(error, null);
      }

      args[0] = _query;
      args[update_index] = _update;

      _apply(collection, method, args);
    });
  };
});

/**
 * Calls `remove`
 * and triggers the `beforeRemove` and `afterRemove` hooks
 *
 * @param {Object} model
 * @param {Object} collection
 * @param {Array} args
 * @param {Function} callback
 */
PROXY.remove = function (model, collection, args, callback) {
  args[args.length - 1] = function (error, ret) {
    if (error) {
      return callback(error, null);
    }

    model.afterRemove(args[0], function (error, _) {
      callback(error, ret);
    });
  };

  model.beforeRemove(args[0], function (error, _query) {
    if (error) {
      return callback(error, null);
    }

    args[0] = _query;

    _apply(collection, 'remove', args);
  });
};

/**
 * Calls `mapReduce` + `find`
 *
 * @param {Object} model
 * @param {Object} collection
 * @param {Array} args
 * @param {Function} callback
 */
PROXY.mapReduceCursor = function (model, collection, args, callback) {
  args[args.length - 1] = function (error, collection) {
    if (error) {
      callback(error, null);
    } else {
      collection.find(callback);
    }
  };
  _apply(collection, 'mapReduce', args);
};

/**
 * Calls `mapReduce` + `find` + `toArray`
 *
 * @param {Object} model
 * @param {Object} collection
 * @param {Array} args
 * @param {Function} callback
 */
PROXY.mapReduceArray = function (model, collection, args, callback) {
  PROXY.mapReduceCursor(model, collection, args, function (error, cursor) {
    if (error) {
      callback(error, null);
    } else {
      cursor.toArray(callback);
    }
  });
};

module.exports = PROXY;


<|start_filename|>lib/utils.js<|end_filename|>
var UTILS = {};

/* explores an object given an attribute
 * or a `dot_notation` attribute
 *
 * @param {Object} obj
 * @param {String} attr
 * @param {Boolean} create
 *
 * @return {Array} [obj, attr]
 */
UTILS.inspect = function (obj, attr, create) {
  var attr_parts = attr.split('.')
    , last, current;

  if (attr_parts.length === 1) {
    return [obj, attr];
  } else {
    last = obj;
    for (;attr_parts.length > 0;) {
      current = attr_parts.shift();

      if (!last) {
        return [undefined, current];
      }

      if (create && !last[current]) {
        last[current] = attr_parts.length ? {} : undefined;
      }

      if (!attr_parts.length) {
        return [last, current];
      }

      last = last[current];
    }
  }
};

//exports
module.exports = UTILS;


<|start_filename|>test/model_test.js<|end_filename|>
/*globals describe, beforeEach, it*/
var assert = require('assert')
  , sinon = require('sinon')
  , Model = require('./../lib/model')
  , Validator = require('./../lib/validator')

  , _db = {bson_serializer: {}, collection: function () {}}
  , _mock_validator = function (ret) {
      return {
        hasErrors: function () {
          return ret;
        }
      };
    }
  , User;

describe('Models', function () {

  beforeEach(function () {
    User = Model(_db, 'users');
  });

  it('`core` throws an error when there is no db', function () {
    assert.throws(function () {
      Model(null);
    }, 'You must specify a db');
  });

  it('`core` throws an error when collection is missing', function () {
    assert.throws(function () {
      Model(_db);
    }, 'You must specify a collection name');
  });

  it('`getCollection` returns a document collection', sinon.test(function () {
    var cb = function () {}
      , stub, self = this;

    stub = self.stub(_db, 'collection').withArgs('users', cb);

    User.getCollection(cb);
    sinon.assert.calledOnce(stub);
  }));

  it('`mongo` proxies collection calls', sinon.test(function () {
    var callback = function (error, doc) {}
      , stub, self = this
      , query = {name: 'zemba'};

    stub = self.stub(User.collection_proxy, 'proxy', function (_model, _options, _args) {
      assert.deepEqual(_options, {hooks: true, namespacing: true, mapping: true, method: 'findArray'});
      assert.deepEqual(_args[0], query);
      assert.deepEqual(_args[1], callback);
    });

    User.mongo('findArray', query, callback);
    sinon.assert.calledOnce(stub);
  }));

  it('`mongo` proxies namespaced collection calls', sinon.test(function () {
    var callback = function (error, doc) {}
      , stub, self = this
      , query = {name: 'zemba'};

    stub = self.stub(User.collection_proxy, 'proxy', function (_model, _options, _args) {
      assert.deepEqual(_options, {
        hooks: true
      , namespacing: true
      , mapping: true
      , method: 'findArray'
      , namespace: 'public'
      });
      assert.deepEqual(_args[0], query);
      assert.deepEqual(_args[1], callback);
    });

    User.mongo('findArray:public', query, callback);
    sinon.assert.calledOnce(stub);
  }));

  it('`mongo` proxies with options', sinon.test(function () {
    var callback = function (error, doc) {}
      , stub, self = this
      , query = {name: 'zemba'};

    stub = self.stub(User.collection_proxy, 'proxy', function (_model, _options, _args) {
      assert.deepEqual(_options, {
        hooks: false
      , namespacing: true
      , mapping: true
      , method: 'findArray'
      , namespace: 'public'
      });
      assert.deepEqual(_args[0], query);
      assert.deepEqual(_args[1], callback);
    });

    User.mongo({method: 'findArray', namespace: 'public', hooks: false}, query, callback);
    sinon.assert.calledOnce(stub);
  }));

  it('`validate` validates a mongo document', sinon.test(function () {
    var document = {}
      , self = this, stub
      , update = {name: 'Pau'}
      , validator = {data: 'foo'}
      , callback;

    stub = self.stub(User, 'validator').withArgs(document, update).returns(validator);
    callback = self.spy().withArgs(null, validator);

    User.validate(document, update, callback);
    sinon.assert.calledOnce(stub);
    sinon.assert.calledOnce(callback);
  }));

  it('`validateAndInsert` when the model is invalid does not insert it', sinon.test(function () {
    var document = {}
      , self = this, stub
      , validator = _mock_validator(true)
      , callback;

    stub = self.stub(User, 'validate', function (_document, _data, _callback) {
      _callback(null, validator);
    });
    callback = self.spy().withArgs(null, validator);

    User.validateAndInsert(document, callback);
    sinon.assert.calledOnce(stub);
    sinon.assert.calledOnce(callback);
  }));

  it('`validateAndInsert` when the model is valid inserts it afterwards', sinon.test(function () {
    var document = {foo: 'bar'}
      , self = this, stub1, stub2
      , validator = _mock_validator(false)
      , callback;

    User.maps = {
      foo: function (el) {
        return el.toUpperCase();
      }
    };

    stub1 = self.stub(User, 'validate', function (_document, _data, _callback) {
      _callback(null, validator);
    });

    stub2 = self.stub(User, 'mongo', function (_action, _document, _callback) {
      assert.deepEqual(_action, {method: 'insert', namespacing: false, mapping: false});
      assert.deepEqual(_document.foo, 'BAR');
      _callback(null, _document);
    });

    callback = self.spy().withArgs(null, validator);

    User.validateAndInsert(document, callback);
    sinon.assert.calledOnce(stub1);
    sinon.assert.calledOnce(stub2);
    sinon.assert.calledOnce(callback);
  }));

  it('`beforeInsert` default hook sets the created_at date', sinon.test(function () {
    var documents = [{name: 'zemba'}, {foo: 'bar'}];

    User.beforeInsert(documents, function (_error, _documents) {
      _documents.forEach(function (document) {
        assert.ok(document.created_at);
        assert.equal(document.created_at.constructor, (new Date()).constructor);
      });
    });
  }));

  it('`beforeUpdate` default hook updated the updated_at date', sinon.test(function () {
    var query = {foo: 'bar'}
      , update = {'$set': {fleiba: 'zemba'}};

    User.beforeUpdate(query, update, function (error, _query, _update) {
      assert.ok(_update.$set);
      assert.ok(_update.$set.updated_at);
      assert.equal(_update.$set.updated_at.constructor, (new Date()).constructor);
    });
  }));

  it('`validateAndUpdate` when the model is invalid does not update it', sinon.test(function () {
    var query = {foo: 'bar'}
      , self = this, stub1, stub2
      , document = {foo: 'bar', fleiba: 'foo'}
      , update = {fleiba: 'zemba'}
      , validator = _mock_validator(true)
      , options = {}
      , callback;

    stub1 = self.stub(User, 'mongo', function (_method, _query, _callback) {
      assert.equal(_method, 'findOne');
      assert.deepEqual(_query, query);
      _callback(null, document);
    });
    stub2 = self.stub(User, 'validate', function (_document, _data, _callback) {
      _callback(null, validator);
    });
    callback = self.spy().withArgs(null, validator);

    User.validateAndUpdate(query, update, options, callback);
    sinon.assert.calledOnce(stub1);
    sinon.assert.calledOnce(stub2);
    sinon.assert.calledOnce(callback);
  }));

  it('`validateAndUpdate` when the model is valid updates it afterwards', sinon.test(function () {
    var query = {foo: 'bar'}
      , self = this, stub1, stub2, stub3
      , document = {_id: '123', foo: 'bar'}
      , update = {'$set': {fleiba: 'John'}}
      , validator = _mock_validator(false)
      , options = {}
      , callback;

    User.maps = {
      fleiba: function (el) {
        return el.toLowerCase();
      }
    };

    stub1 = self.stub(User, 'mongo', function (_method, _query, _callback) {
      assert.equal(_method, 'findOne');
      assert.deepEqual(_query, query);

      sinon.assert.calledOnce(stub1);
      stub1.restore();
      stub3 = self.stub(User, 'mongo', function (_action, _document, _update, _options, _callback) {
        assert.deepEqual(_action, {method: 'update', namespacing: false, mapping: false});
        assert.deepEqual(_document._id, document._id);
        assert.deepEqual(_update.$set.fleiba, 'john');
        assert.deepEqual(_options, options);
        _callback(null, _document);
      });

      _callback(null, document);
    });

    User.beforeUpdate = function (_query, _update, _callback) {
      _callback(null, document);
    };

    stub2 = self.stub(User, 'validate', function (_document, _data, _callback) {
      _callback(null, validator);
    });

    callback = self.spy().withArgs(null, validator);

    User.validateAndUpdate(query, update, options, callback);
    sinon.assert.calledOnce(stub2);
    sinon.assert.calledOnce(stub3);
    sinon.assert.calledOnce(callback);
  }));

  it('`getEmbeddedDocument` filters the document following the skeletons directive', sinon.test(function () {
    var comment = {_id: 1, title: 'foo', body: 'Lorem ipsum'};

    User.skeletons = {
      comment: ['_id', 'title']
    };

    assert.deepEqual(User.getEmbeddedDocument('comment', comment), { _id: 1, title: 'foo' });
    assert.deepEqual(
      User.getEmbeddedDocument('comment', comment, 'post.comment')
    , {post: {comment: {_id: 1, title: 'foo'}}}
    );
  }));

  it('`getEmbeddedDocument` filters the document following recursive skeletons directives', sinon.test(function () {
    var post = {_id: 1, title: 'foo', body: 'Lorem ipsum', comment: {body: 'comment body!', created_at: Date.now()}};

    User.skeletons = {
      post: ['_id', 'title', 'comment.body']
    };

    assert.deepEqual(User.getEmbeddedDocument('post', post), {_id: 1, title: 'foo', comment: {body: 'comment body!'}});
    assert.deepEqual(
      User.getEmbeddedDocument('post', post, 'post')
    , {post: {_id: 1, title: 'foo', comment: {body: 'comment body!'}}}
    );
  }));

  it('`getEmbeddedDocument` returns appropiate `dot_notation` strings', sinon.test(function () {
    var comment = {_id: 1, title: 'foo', body: 'Lorem ipsum'};

    User.skeletons = {
      comment: ['_id', 'title']
    };

    assert.deepEqual(User.getEmbeddedDocument('comment', comment), {_id: 1, title: 'foo'});
    assert.deepEqual(
      User.getEmbeddedDocument('comment', comment, 'post', true)
    , {'post._id': 1, 'post.title': 'foo'}
    );
    assert.deepEqual(
      User.getEmbeddedDocument('comment', comment, 'post.comment', true)
    , {'post.comment._id': 1, 'post.comment.title': 'foo'}
    );
  }));

  it('`getEmbeddedDocument` returns appropiate `dot_notation` strings using rescursive stuff', sinon.test(function () {
    var post = {_id: 1, title: 'foo', body: 'Lorem ipsum', comment: {body: 'comment body!', created_at: Date.now()}};

    User.skeletons = {
      post: ['_id', 'title', 'comment.body']
    };

    assert.deepEqual(
      User.getEmbeddedDocument('post', post, 'user.post', true)
    , {'user.post._id': 1, 'user.post.title': 'foo', 'user.post.comment.body': 'comment body!'}
    );
  }));

  it('`getEmbeddedDocument` works without specifying the skeletons', sinon.test(function () {
    var comment = {_id: 1, title: 'foo', body: 'Lorem ipsum'};

    User.skeletons = null;

    assert.deepEqual(User.getEmbeddedDocument('comment', comment), { _id: 1, title: 'foo', body: 'Lorem ipsum'});
    assert.deepEqual(
      User.getEmbeddedDocument('comment', comment, 'post.comment')
    , {post: {comment: comment}}
    );
  }));

  it('`updateEmbeddedDocument` updates embedded objects', sinon.test(function () {
    var embeddedDocument = {name: 'john', surname: 'snow', bo: 'vale'}
      , self = this, stub
      , options = {upsert: true}
      , callback = function () {};

    User.skeletons = {
      author: ['_id', 'name', 'surname']
    };

    stub = self.stub(User, 'mongo', function (_opts, _query, _update, _options, _callback) {
      assert.deepEqual(_opts, {method: 'update', hooks: false});
      assert.deepEqual(_query, {'author._id': 1});
      assert.deepEqual(_update, {'$set': {'author.name': 'john', 'author.surname': 'snow'}});
      assert.deepEqual(_options, {upsert: true, multi: true});
      assert.equal(_callback, callback);
    });

    User.updateEmbeddedDocument({_id: 1}, 'author', embeddedDocument, options, callback);
    sinon.assert.calledOnce(stub);
  }));

  it('`pushEmbeddedDocument` pushes embedded objects', sinon.test(function () {
    var embeddedDocument = {name: 'john'}
      , self = this, stub1, stub2
      , collection = {foo: 'bar'};

    stub1 = self.stub(User, 'getEmbeddedDocument', function (_name, _doc, _scope, _dot_notation) {
      assert.equal(_name, 'author');
      assert.deepEqual(_doc, embeddedDocument);
      assert.ifError(_scope);
      assert.ifError(_dot_notation);
      return embeddedDocument;
    });

    stub2 = self.stub(User, 'mongo', function (_opts, _query, _update, _options, _callback) {
      assert.deepEqual(_opts, {method: 'update', hooks: false});
      assert.deepEqual(_query, {_id: 1});
      assert.deepEqual(_update, {'$push': {author: embeddedDocument}});
      assert.deepEqual(_options, {upsert: false, multi: true});
    });

    User.pushEmbeddedDocument({_id: 1}, 'author', embeddedDocument);
    sinon.assert.calledOnce(stub1);
    sinon.assert.calledOnce(stub2);
  }));
});
