├── .gitignore ├── README.md ├── backbone-mongodb.js ├── index.js ├── lib ├── db.js └── mongodb-document.js ├── package.json └── spec ├── collection_spec.js ├── document_spec.js ├── embedded_spec.js ├── helper.js └── validation_spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Extensions to Backbone.js to support MongoDB use as a back-end data store. 2 | 3 | # Overview 4 | 5 | Adds Backbone.Document and Backbone.EmbeddedDocument for working with MongoDB data: 6 | 7 | 1. embedded documents are wrapped in the correct objects if desired; nesting is supported 8 | 2. embedded documents support a minimum of features for accessing the root 9 | 3. all changes are made through the root document 10 | 11 | Co-exists with Backbone Models, which remain unchanged. 12 | 13 | # Server Side (node.js) 14 | 15 | When loaded on a node.js server (where node-mongodb-native is available), provides: 16 | 17 | 1. save, fetch, and delete methods that follow the standard node callback pattern: callback(err, response) 18 | 2. support for loading from and saving to the mongodb 19 | 20 | 21 | # Client Side 22 | 23 | When loaded on the browser, provides: 24 | 25 | 1. access to the same methods (validation, etc) that the server has 26 | 2. Backbone.js sync support for document-level 27 | 28 | 29 | # Credit 30 | 31 | Structural credit and general props for writing beautiful code to jashkenas and the Backbone.js crew. -------------------------------------------------------------------------------- /backbone-mongodb.js: -------------------------------------------------------------------------------- 1 | // backbone-mongodb.js 2 | // (c) 2011 Done. 3 | 4 | (function() { 5 | 6 | // Save a reference to the global object. 7 | var root = this; 8 | 9 | // Require Backbone and Underscore if we're on the server, and it's not already present 10 | var isServer = (typeof require !== 'undefined'); 11 | 12 | var Backbone = root.Backbone; 13 | var _ = root._; 14 | var MongoDBDocument; 15 | 16 | if (!Backbone && isServer) Backbone = require('backbone'); 17 | if (!_ && isServer) _ = require('underscore'); 18 | 19 | var MongoDb = {}; 20 | 21 | // MongoDb models 22 | // -------------- 23 | MongoDb.models = { 24 | 25 | Document: Backbone.Model.extend({ 26 | idAttribute: '_id', // provides the mongo _id for documents 27 | models: {}, // mapping of attributes to models (optional) 28 | 29 | get : function(attr) { 30 | return this._prepareAttribute(attr); 31 | }, 32 | 33 | // Create the attribute with the right 34 | _prepareAttribute : function(attr) { 35 | var value = this.attributes[attr]; 36 | if(attr in this.models) { 37 | value = new this.models[attr](value); 38 | value.container = this; 39 | } 40 | return value; 41 | } 42 | }), 43 | }; 44 | 45 | // Patch Backbone 46 | // -------------- 47 | 48 | // Add mongoDB behavior to the Document 49 | if (isServer) { 50 | /* 51 | var MongoDBDocument = require('./lib/mongodb-document'); 52 | _.extend(MongoDb.models.Document.prototype, MongoDBDocument); 53 | */ 54 | } 55 | 56 | Backbone.MongoDb = MongoDb; 57 | _.extend(Backbone, MongoDb.models); 58 | 59 | }).call(this); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // backbone-mongodb index.js 2 | // (c) 2011 Done. 3 | 4 | exports.Db = require('./lib/db'); 5 | 6 | -------------------------------------------------------------------------------- /lib/db.js: -------------------------------------------------------------------------------- 1 | // backbone-mongodb db.js 2 | // (c) 2011 Done. 3 | 4 | var util = require('util'), 5 | events = require('events'), 6 | Mongo = require('mongodb').Db, 7 | Server = require('mongodb').Server; 8 | 9 | var _connection = null; 10 | 11 | // Database interface for the MongoDB 12 | // 13 | // Options hash: 14 | // name, host, port 15 | // debug --- TODO: does MongoDB driver support debug? how much? 16 | // 17 | // Opens the database and emits an 'open' event on success, or an 'error' event if there was a problem. 18 | var Database = module.exports = function(options) { 19 | var self = this, 20 | db = new Mongo(options.name, new Server(options.host, options.port, {})); 21 | 22 | if (!_connection) { 23 | db.open(function(err, database) { 24 | if(err) { 25 | self.emit('database', 'error', err); 26 | } else { 27 | _connection = database; 28 | self.emit('database', 'open'); 29 | } 30 | }); 31 | } 32 | }; 33 | 34 | // Support events 35 | util.inherits(Database, events.EventEmitter); 36 | 37 | // Returns a connection to the database, or null if the database is not (yet) open 38 | Database.getConnection = function() { 39 | return _connection; 40 | }; 41 | -------------------------------------------------------------------------------- /lib/mongodb-document.js: -------------------------------------------------------------------------------- 1 | // backbone-mongodb mongodb-document.js 2 | // (c) 2011 Done. 3 | var _ = require('underscore')._, 4 | Backbone = require('backbone'), 5 | Db = require('./db'); 6 | 7 | // Document provides the server-side implementation of various functions for the MongoDocument. 8 | // It is included in the backbone-mongodb implementation when running on the server. 9 | var Document = module.exports = { 10 | 11 | // Document Public API 12 | // ------------------- 13 | initialize : function() {}, // called on subclasses 14 | 15 | // Refresh the contents of this document from the database 16 | fetch : function(callback) { 17 | var self = this; 18 | 19 | if (!self._requireRoot()) return; 20 | 21 | self._withCollection(function(err, collection) { 22 | if (err) { return callback(err); } 23 | 24 | collection.findOne({ _id: self.id }, function(err, dbModel) { 25 | if (!dbModel) { 26 | err = 'Could not find id ' + self.id; 27 | } else if(!err) { 28 | self.set(dbModel); 29 | } 30 | callback(err, self); 31 | }); 32 | }); 33 | }, 34 | 35 | // Validate and save the contents of this document to the database 36 | save : function(attrs, callback) { 37 | var self = this, 38 | options = { 39 | error: function(model, error, options) { 40 | callback(error, model); 41 | } 42 | }; 43 | 44 | if (!self._requireRoot()) return; 45 | 46 | // options.error configures the callback 47 | if (attrs) { 48 | if (!this.set(attrs, options)) return; 49 | } else { 50 | if (self.validate && !self._performValidation(self.attributes, options)) return; 51 | } 52 | 53 | self._withCollection(function(err, collection) { 54 | if (err) { return callback(err); } 55 | 56 | if (self.isNew()) { 57 | collection.insert(self.attributes, function(err, dbModel) { 58 | if(!err) { self.set(dbModel[0]); } 59 | callback(err, self); 60 | }); 61 | } else { 62 | collection.update({ _id: self.id }, self.attributes, function(err) { 63 | callback(err, self); 64 | }); 65 | } 66 | }); 67 | }, 68 | 69 | // Remove this document from the database 70 | destroy : function(callback) { 71 | var self = this; 72 | 73 | if (!self._requireRoot()) return; 74 | 75 | self._withCollection(function(err, collection) { 76 | if (err) { return callback(err); } 77 | 78 | collection.remove({ _id: self.id }, callback); 79 | }); 80 | }, 81 | 82 | // Private API functions 83 | // --------------------- 84 | 85 | // Obtain a database connection or die 86 | _requireConnection : function() { 87 | var connection = Db.getConnection(); 88 | if (!connection) { 89 | throw 'FATAL: Database not connected', this; 90 | } 91 | return connection; 92 | }, 93 | 94 | // Request the Database collection associated with this Document 95 | _withCollection : function(callback) { 96 | var connection = this._requireConnection(); 97 | connection.collection(this.collectionName, function(err, collection) { 98 | callback(err, collection); 99 | }); 100 | }, 101 | 102 | // Must be the root 103 | _requireRoot : function(callback) { 104 | if (this.container) { 105 | callback('This function cannot be called on an embedded document'); 106 | return false; 107 | } 108 | return true; 109 | } 110 | 111 | }; 112 | 113 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backbone-mongodb", 3 | "version": "0.0.1", 4 | "description": "Extensions to Backbone.js to support MongoDB use as a back-end data store.", 5 | "main" : "./backbone-mongodb.js", 6 | "keywords": [ 7 | "backbone", 8 | "mongodb" 9 | ], 10 | "dependencies": { 11 | "backbone": "0.5.x", 12 | "underscore": "1.1.x", 13 | "mongodb": "0.9.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spec/collection_spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | _ = require('underscore')._, 3 | vows = require('vows'), 4 | helper = require('./helper'), 5 | BackboneMongoDb = require('../backbone-mongodb'), 6 | Backbone = require('backbone'); 7 | 8 | var Monkey = Backbone.Document.extend({ 9 | }); 10 | 11 | var MonkeyCollection = Backbone.Collection.extend({ 12 | model : Monkey, 13 | }); 14 | 15 | var Document = Backbone.Document.extend({ 16 | collectionName: 'document', 17 | models : { 'monkeys': MonkeyCollection }, 18 | }); 19 | 20 | vows.describe('Collection').addBatch({ 21 | 22 | // Set up the database 23 | // ------------------- 24 | 25 | 'open database': { 26 | topic: function() { helper.db(this.callback); }, 27 | 'is available': function(err, db) { 28 | assert.isNull(err); 29 | assert.isNotNull(db); 30 | } 31 | } 32 | 33 | // Validate object type assignment 34 | // ------------------------------- 35 | }).addBatch({ 36 | 'a new document with monkeys': { 37 | topic: function() { 38 | var document = new Document({ monkeys: [ { name: 'Monkey 1' }, { name : 'Monkey 2' } ] }); 39 | document.save(null, this.callback); 40 | }, 41 | 'should have a monkey collection': function(err, document) { 42 | assert.isTrue(document.get('monkeys') instanceof MonkeyCollection); 43 | }, 44 | 'monkey collection should have monkeys': function(err, document) { 45 | assert.isTrue(document.get('monkeys').at(0) instanceof Monkey); 46 | }, 47 | 'monkey in collection should be the right monkey': function(err, document) { 48 | assert.equal(document.get('monkeys').at(0).get('name'), 'Monkey 1'); 49 | } 50 | } 51 | 52 | }).export(module); 53 | -------------------------------------------------------------------------------- /spec/document_spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | vows = require('vows'), 3 | helper = require('./helper'), 4 | BackboneMongoDb = require('../backbone-mongodb'), 5 | Backbone = require('backbone'); 6 | 7 | var TestDocument = Backbone.Document.extend({ 8 | collectionName: 'TestDocument', 9 | }); 10 | 11 | vows.describe('Document').addBatch({ 12 | 13 | // Set up the database 14 | // ------------------- 15 | 16 | 'open database': { 17 | topic: function() { helper.db(this.callback); }, 18 | 'is available': function(err, db) { 19 | assert.isNull(err); 20 | assert.isNotNull(db); 21 | } 22 | } 23 | 24 | // Saving documents 25 | // ---------------- 26 | 27 | }).addBatch({ 28 | 'an unsaved Document': { 29 | topic: new TestDocument(), 30 | 'is new': function(document) { 31 | assert.isTrue(document.isNew()); 32 | }, 33 | 'when saved': { 34 | topic: function(document) { 35 | document.save(null, this.callback); 36 | }, 37 | 'is not new': function(err, document) { 38 | assert.isFalse(document.isNew()); 39 | }, 40 | 'has assigned the id': function(err, document) { 41 | assert.ok(document.id); 42 | }, 43 | }, 44 | 'when updated inside save': { 45 | topic: function(document) { 46 | document.save({spaceMonkeyCaptain: true}, this.callback); 47 | }, 48 | 'has parameter saved': function(err, document) { 49 | assert.isNull(err); 50 | assert.isTrue(document.get('spaceMonkeyCaptain')); 51 | } 52 | }, 53 | 'when updated': { 54 | topic: function(document) { 55 | document.set({spaceMonkeyTrainee: true}); 56 | document.save({}, this.callback); 57 | }, 58 | 'has parameter saved': function(err, document) { 59 | assert.isNull(err); 60 | assert.isTrue(document.get('spaceMonkeyTrainee')); 61 | } 62 | } 63 | } 64 | 65 | // Fetching documents 66 | // ------------------ 67 | 68 | }).addBatch({ 69 | 'an existing document': { 70 | topic: function() { 71 | var document = new TestDocument(); 72 | document.save({spaceMonkeyCaptain: true}, this.callback); 73 | }, 74 | 'exists': function(document) { 75 | assert.isFalse(document.isNew()); 76 | assert.isTrue(document.get('spaceMonkeyCaptain')); 77 | }, 78 | 'when fetched': { 79 | topic: function(document) { 80 | var fetchedDocument = new TestDocument(); 81 | fetchedDocument.id = document.id; 82 | fetchedDocument.fetch(this.callback); 83 | }, 84 | 'is found': function(err, fetched) { 85 | assert.isNull(err); 86 | assert.ok(fetched); 87 | }, 88 | 'is populated': function(err, fetched) { 89 | assert.isTrue(fetched.get('spaceMonkeyCaptain')); 90 | } 91 | } 92 | } 93 | 94 | // Removing documents 95 | // ------------------ 96 | 97 | }).addBatch({ 98 | 'an existing document': { 99 | topic: function() { 100 | var document = new TestDocument(); 101 | document.save({}, this.callback); 102 | }, 103 | 'exists': function(document) { 104 | assert.isFalse(document.isNew()); 105 | }, 106 | 'when deleted': { 107 | topic: function(document) { 108 | document.destroy(this.callback); 109 | }, 110 | 'succeeds': function(err) { 111 | // without 'safe' mode the return is undefined 112 | assert.isUndefined(err); 113 | }, 114 | 'cannot be fetched': { 115 | topic: function(document) { 116 | document.fetch(this.callback); 117 | }, 118 | 'fail': function(err, document) { 119 | assert.ok(err); 120 | } 121 | } 122 | } 123 | } 124 | 125 | }).export(module); 126 | -------------------------------------------------------------------------------- /spec/embedded_spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | vows = require('vows'), 3 | helper = require('./helper'), 4 | BackboneMongoDb = require('../backbone-mongodb'), 5 | Backbone = require('backbone'); 6 | 7 | var Monkey = Backbone.Document.extend({}); 8 | 9 | var TestDocument = Backbone.Document.extend({ 10 | collectionName: 'TestDocument', 11 | models: { 'monkey': Monkey }, 12 | }); 13 | 14 | vows.describe('Embedded Document').addBatch({ 15 | 16 | // Set up the database 17 | // ------------------- 18 | 19 | 'open database': { 20 | topic: function() { helper.db(this.callback); }, 21 | 'is available': function(err, db) { 22 | assert.isNull(err); 23 | assert.isNotNull(db); 24 | } 25 | } 26 | 27 | // Attributes that have models 28 | // --------------------------- 29 | 30 | }).addBatch({ 31 | 'document attribute with a model': { 32 | topic: new TestDocument(), 33 | 'valid data set from an object': { 34 | topic: function(document) { 35 | document.set({ monkey: { name: 'Super Monkey' } }); 36 | this.callback(null, document); 37 | }, 38 | 'has correct value': function(err, document) { 39 | assert.equal(document.get('monkey').get('name'), 'Super Monkey'); 40 | }, 41 | 'has correct model type': function(err, document) { 42 | assert.isTrue(document.get('monkey') instanceof Monkey); 43 | }, 44 | 'has correct container': function(err, document) { 45 | assert.equal(document.get('monkey').container, document); 46 | }, 47 | 'when saved and fetched': { 48 | topic: function(document) { 49 | var self = this; 50 | document.save(null, function(err, document) { 51 | document.fetch(self.callback); 52 | }); 53 | }, 54 | 'has correct data': function(err, document) { 55 | assert.equal(document.get('monkey').get('name'), 'Super Monkey'); 56 | } 57 | } 58 | }, 59 | 'set from a Document': { 60 | topic: function(document) { 61 | document.set({ monkey: new Monkey({ name: 'Super Monkey' }) }); 62 | this.callback(null, document); 63 | }, 64 | 'has correct value': function(err, document) { 65 | assert.equal(document.get('monkey').get('name'), 'Super Monkey'); 66 | }, 67 | 'has correct model type': function(err, document) { 68 | assert.isTrue(document.get('monkey') instanceof Monkey); 69 | }, 70 | 'has correct container': function(err, document) { 71 | assert.equal(document.get('monkey').container, document); 72 | }, 73 | 'when saved and fetched': { 74 | topic: function(document) { 75 | var self = this; 76 | document.save(null, function(err, document) { 77 | document.fetch(self.callback); 78 | }); 79 | }, 80 | 'has correct data': function(err, document) { 81 | assert.equal(document.get('monkey').get('name'), 'Super Monkey'); 82 | } 83 | } 84 | }, 85 | } 86 | 87 | // Changing values of attribute models from the sub-model 88 | // ------------------------------------------------------ 89 | 90 | }).addBatch({ 91 | 'document attribute with a model': { 92 | topic: new TestDocument(), 93 | 'set from the attribute model': { 94 | topic: function(document) { 95 | document.set({ monkey: new Monkey({ name: 'Super Monkey' }) }); 96 | document.get('monkey').set({ name: 'Lame Monkey' }); 97 | this.callback(null, document); 98 | }, 99 | 'should not change': function(err, document) { 100 | assert.equal(document.get('monkey').get('name'), 'Super Monkey'); 101 | }, 102 | } 103 | } 104 | 105 | 106 | }).export(module); 107 | -------------------------------------------------------------------------------- /spec/helper.js: -------------------------------------------------------------------------------- 1 | var Db = require('../lib/db'); 2 | 3 | var _connection = null; 4 | 5 | var database = new Db({ 6 | name: 'test', 7 | host: '127.0.0.1', 8 | port: 27017 9 | }); 10 | 11 | exports.db = function(callback) { 12 | if (_connection) { 13 | return callback(null, _connection); 14 | } 15 | 16 | database.on('database', function(status) { 17 | var error = status === 'open' ? null : status; 18 | _connection = Db.getConnection(); 19 | callback(error, _connection); 20 | }); 21 | } -------------------------------------------------------------------------------- /spec/validation_spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | _ = require('underscore')._, 3 | vows = require('vows'), 4 | helper = require('./helper'), 5 | BackboneMongoDb = require('../backbone-mongodb'), 6 | Backbone = require('backbone'); 7 | 8 | var Monkey = Backbone.Document.extend({}); 9 | 10 | var TestDocument = Backbone.Document.extend({ 11 | collectionName: 'TestDocument', 12 | models: { 'monkey': Monkey }, 13 | validate : function(attrs) { 14 | var errors = {}; 15 | 16 | if (!attrs.name || attrs.name.length === 0) { 17 | errors.name = 'Document name must not be blank'; 18 | } 19 | 20 | if(attrs.monkey) { 21 | if(!attrs.monkey.name || attrs.monkey.name.length === 0) { 22 | errors.monkey = {}; 23 | errors.monkey.name = "Monkeys must have a name"; 24 | } 25 | } 26 | return _.keys(errors).length === 0 ? null : errors; 27 | } 28 | }); 29 | 30 | vows.describe('Validation').addBatch({ 31 | 32 | // Set up the database 33 | // ------------------- 34 | 35 | 'open database': { 36 | topic: function() { helper.db(this.callback); }, 37 | 'is available': function(err, db) { 38 | assert.isNull(err); 39 | assert.isNotNull(db); 40 | } 41 | } 42 | 43 | // Validate the top level document 44 | // ------------------------------- 45 | 46 | }).addBatch({ 47 | 'an unsaved Document': { 48 | topic: new TestDocument(), 49 | 'is new': function(document) { 50 | assert.isTrue(document.isNew()); 51 | }, 52 | 'when not valid': { 53 | topic: function(document) { 54 | document.save(null, this.callback); 55 | }, 56 | 'should have an error': function(err, document) { 57 | assert.ok(err); 58 | }, 59 | 'should have an error on name': function(err, document) { 60 | assert.equal(err.name, "Document name must not be blank"); 61 | } 62 | }, 63 | } 64 | }).addBatch({ 65 | 'an unsaved Document': { 66 | topic: new TestDocument(), 67 | 'is new': function(document) { 68 | assert.isTrue(document.isNew()); 69 | }, 70 | 'when valid': { 71 | topic: function(document) { 72 | document.save({ name: 'Dox' }, this.callback); 73 | }, 74 | 'should not have an error': function(err, document) { 75 | assert.isNull(err); 76 | } 77 | } 78 | } 79 | 80 | // Validate the embedded document 81 | // ------------------------------ 82 | 83 | }).addBatch({ 84 | 'an unsaved Document': { 85 | topic: new TestDocument({ name: 'Dox' }), 86 | 'is new': function(document) { 87 | assert.isTrue(document.isNew()); 88 | }, 89 | 'when embedded value is not valid': { 90 | topic: function(document) { 91 | document.save({ monkey: {} }, this.callback); 92 | }, 93 | 'should have an error': function(err, document) { 94 | assert.ok(err); 95 | }, 96 | 'should have an error on name': function(err, document) { 97 | assert.equal(err.monkey.name, 'Monkeys must have a name'); 98 | } 99 | } 100 | } 101 | 102 | }).export(module); 103 | --------------------------------------------------------------------------------