Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ var User = modella('User');
User.use(mongo('Account')); // Uses db.Account
```

## Attribute Configuration

### `{'unique': true}`

This will create a unique index for the attribute

False by default

### `{'atomic': true}`

Uses the `'$inc'` update modifier for mongo, allowing a value to be (de)incremented as needed, rather than using `'$set'` each time.

This only works for number attributes.

False by default

## API

Expand Down
91 changes: 78 additions & 13 deletions lib/mongo.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
'use strict';
/**
* Module dependencies
*/
Expand All @@ -7,33 +8,45 @@ var debug = require('debug')('modella:mongo'),
maggregate = require('./maggregate'),
sync = {};

// CONSTANTS

var FLOAT_REGEXP = /[0-9]*\.[0-9]*/;
var SCIENTIFIC_REGEXP = /[0-9.]+e[0-9]+/;

/**
* Export `Mongo`
*/

module.exports = function(url) {
var mongo = require('mongoskin').db(url, {w: 1}, function() { });

// Use alternate collection name, defaults to modelName
return function(Model) {
return ('string' == typeof Model) ? plugin.bind(null, Model) : plugin(Model.modelName, Model);
};

function plugin(collection, Model) {
var db = mongo.collection(collection);
Model.db = db;

db.open(function(err, col) {
db.open(function(err) {
if (err) throw err;
mquery(Model, db.collection);
maggregate(Model, db.collection);
});

Model.index = db.ensureIndex.bind(db);

Model.prototype.oldAtomics = {};

Model.on('change', function(instance, name, value, previous) {
var options = Model.attrs[name];
if (options.atomic && instance.oldAtomics[name] === undefined) {
instance.oldAtomics[name] = previous;
}
});

Model.once('initialize', function() {
for(var attr in Model.attrs) {
var options = Model.attrs[attr];
if (options.unique) Model.index(attr, { w: 0, unique: true, sparse: true });
if (Model.attrs.hasOwnProperty(attr)) {
var options = Model.attrs[attr];
if (options.unique) Model.index(attr, { w: 0, unique: true, sparse: true });
}
}
});

Expand All @@ -43,7 +56,7 @@ module.exports = function(url) {
var doc = docs ? docs[0] : null;
if(err) {
// Check for duplicate index
if(err.code == 11000) {
if(err.code === 11000) {
var attr = err.message.substring(err.message.indexOf('$') + 1, err.message.indexOf('_1'));
self.error(attr, 'has already been taken');
}
Expand All @@ -67,10 +80,56 @@ module.exports = function(url) {

if(Object.keys(changed).length === 0) { return cb(null, this._attrs); }

return db.findAndModify({_id: id}, {}, {$set: changed}, {new: true}, function(err, doc) {
// set up empty update document
var updateDoc = {
};

// loop through each changed key to see if it has been configured as "atomic"
Object.keys(changed).forEach(function(changedKey) {
var options = Model.attrs[changedKey];
if (options.atomic) {
// if atomic, try parsing it as a number
var numString = changed[changedKey].toString();
var number = NaN;
// detect float strings
if (FLOAT_REGEXP.test(numString) || SCIENTIFIC_REGEXP.test(numString)) {
number = parseFloat(numString);
} else {
// assume base 10?
number = parseInt(numString, 10);
}
// if not actually a number return an error
if (isNaN(number)) {
var errorString = "Atomic property " + changedKey + " set to NaN";
self.error(changedKey, errorString);
}
// get the old value of the atomic variable is available
if (self.oldAtomics[changedKey] !== undefined) {
// get the difference and update the $inc doc on the updateDoc
var delta = number - self.oldAtomics[changedKey];
if (!updateDoc.$inc) updateDoc.$inc = {};
updateDoc.$inc[changedKey] = delta;
} else {
// if there is no old value, just $set it
if (!updateDoc.$set) updateDoc.$set = {};
updateDoc.$set[changedKey] = number;
}
// set the old atomic value to the new value
self.oldAtomics[changedKey] = changed[changedKey];
} else {
if (!updateDoc.$set) updateDoc.$set = {};
updateDoc.$set[changedKey] = changed[changedKey];
}
});

if (self.errors.length) return cb(new Error(self.errors[0].message));

return db.findAndModify({_id: id}, {}, updateDoc, {new: true}, function(err, doc) {
if(err) {
// Check for duplicate index
if(err.code == 11000) {
// test for qwirks/different mongo versions
if (!err.code) err.code = err.lastErrorObject.code;
if(err.code === 11000 || err.code === 11001) {
var attr = err.message.substring(err.message.indexOf('$') + 1, err.message.indexOf('_1'));
self.error(attr, 'has already been taken');
}
Expand Down Expand Up @@ -108,7 +167,7 @@ module.exports = function(url) {

if(!query) return fn(null, false);

if(typeof query == 'string')
if(typeof query === 'string')
query = {_id: db.id(query)};
else
convertStringToIds(query);
Expand All @@ -129,8 +188,14 @@ module.exports = function(url) {
};

function convertStringToIds(query) {
if(typeof query == 'object' && query._id && typeof query._id == 'string')
if(typeof query === 'object' && query._id && typeof query._id === 'string')
query._id = db.id(query._id);
}
}

// Use alternate collection name, defaults to modelName
return function(Model) {
return ('string' === typeof Model) ? plugin.bind(null, Model) : plugin(Model.modelName, Model);
};

};
15 changes: 9 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
"mongo",
"sync"
],
"scripts": {
"test": "make test-once",
"config": {
"blanket": {
"pattern": "lib",
"data-cover-never": "node_modules"
}
},
"scripts": {
"test": "make test-once"
},
"author": "Ryan Schmukler <ryan@slingingcode.com>",
"dependencies": {
"batch": "~0.2.1",
Expand All @@ -23,13 +25,14 @@
"mongoskin": "0.6.1"
},
"devDependencies": {
"modella": "0.2.0",
"mocha": "*",
"expect.js": "*",
"async": "~0.9.0",
"batch": "~0.2.1",
"blanket": "~1.1.5",
"coveralls": "~2.3.0",
"mocha-lcov-reporter": "0.0.1"
"expect.js": "*",
"mocha": "*",
"mocha-lcov-reporter": "0.0.1",
"modella": "0.2.0"
},
"main": "index"
}
108 changes: 104 additions & 4 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
var modella = require('modella'),
mongo = require('../')('localhost:27017/modella-mongo'),
mongoskin = require('mongoskin'),
db = require('mongoskin').db('localhost:27017/modella-mongo', {w: 1}),
mquery = require('mquery'),
maggregate = require('maggregate'),
Batch = require('batch'),
async = require('async'),
expect = require('expect.js');

var User = modella('User')
Expand All @@ -13,7 +15,18 @@ var User = modella('User')
.attr('email', {unique: true})
.attr('password');

var AtomicUser = modella('AtomicUser')
.attr('_id')
.attr('name')
.attr('age', {atomic: true})
.attr('wage', {atomic: true})
.attr('points', {atomic: true})
.attr('email', {unique: true})
.attr('password');


User.use(mongo);
AtomicUser.use(mongo);

/**
* Initialize
Expand All @@ -22,11 +35,14 @@ User.use(mongo);
var user = new User();

var col = db.collection("User");
var atomiccol = db.collection("AtomicUser");


describe("Modella-Mongo", function() {
before(function(done) {
col.remove({}, done);
col.remove({}, function() {
atomiccol.remove({}, done);
});
});

describe("collection", function() {
Expand Down Expand Up @@ -88,6 +104,75 @@ describe("Modella-Mongo", function() {
});
});

it("updates an existing record in the database with a string for _id", function(done) {
var user = new User({_id: (new mongoskin.ObjectID()).toHexString(), name: 'Bob', age: 30});
user.save(function() {
user.name('Eddie');
user.save(function(err, u) {
expect(err).to.not.be.ok();
col.findOne({name: 'Eddie'}, function(err, u) {
expect(u).to.be.ok();
expect(u).to.have.property('name', 'Eddie');
expect(u).to.have.property('age', 30);
done();
});
});
});
});

it("updates an atomic property using $inc", function(done) {
var user = new AtomicUser({name: 'Eddie', age: 30, wage: 7.75});
user.save(function(err) {
expect(err).to.not.be.ok();
async.parallel([
function(finished) {
user.age("29");
user.wage("7.50");
user.save(function(err) {
expect(err).to.not.be.ok();
atomiccol.findOne({name: 'Eddie'}, function(err, u) {
expect(u).to.be.ok();
expect(u).to.have.property('name', 'Eddie');
expect(u).to.have.property('age', 31);
expect(u).to.have.property('wage', 8.25);
finished();
});
});
},
function(finished) {
user.age(31);
user.wage(8.25);
user.points(3);
user.save(function(err, u) {
expect(err).to.not.be.ok();
atomiccol.findOne({name: 'Eddie'}, function(err, u) {
expect(u).to.be.ok();
expect(u).to.have.property('name', 'Eddie');
expect(u).to.have.property('age', 31);
expect(u).to.have.property('wage', 8.25);
});
finished();
});
}
], function() {
done();
});
});
});

it("refuses to update a non-number atomic property", function(done) {
var user = new AtomicUser({name: 'Eddie', age: 30});
user.save(function(err) {
expect(err).to.not.be.ok();
user.age("foo");
user.save(function(err) {
expect(err).to.be.ok();
expect(err.message).to.be("Atomic property age set to NaN");
done();
});
});
});

it("doesn't call mongo if nothing changed (needed for mongo 2.6+)", function(done) {
var user = new User({name: 'Ted'});
user.save(function() {
Expand Down Expand Up @@ -116,9 +201,24 @@ describe("Modella-Mongo", function() {
describe("remove", function() {
it("removes an existing record from the database", function(done) {
var tony = new User({name: 'Tony'});
tony.save(function() {
tony.save(function(err) {
expect(err).to.not.be.ok();
tony.remove(function() {
col.find({name: 'Tony'}).toArray(function(err, docs) {
expect(err).to.not.be.ok();
expect(docs).to.have.length(0);
done();
});
});
});
});
it("removes an existing record from the database with a string _id", function(done) {
var tony = new User({_id: (new mongoskin.ObjectID()).toHexString(), name: 'Tony'});
tony.save(function(err) {
expect(err).to.not.be.ok();
tony.remove(function() {
col.find({name: 'Tony'}).toArray(function(err, docs) {
expect(err).to.not.be.ok();
expect(docs).to.have.length(0);
done();
});
Expand Down Expand Up @@ -219,7 +319,7 @@ describe("Modella-Mongo", function() {
describe("Model.removeAll", function() {
before(function(done) {
var batch = new Batch(),
user;
user;
for(var i = 0; i < 5; ++i) {
user = new User({name: 'soonToBeDeleted'});
batch.push(user.save.bind(user));
Expand All @@ -237,7 +337,7 @@ describe("Modella-Mongo", function() {
describe("Model.query", function() {
it("returns a new instance of mquery", function() {
var queryA = User.query(),
queryB = User.query();
queryB = User.query();
expect(queryA).to.not.be(queryB);
});

Expand Down