From b52ec039d78bb2d63e3044566be70c1444e5930b Mon Sep 17 00:00:00 2001 From: Aaron Hamid Date: Sun, 6 Nov 2011 02:16:37 -0500 Subject: [PATCH 1/2] determine collection from model more conservatively - supports nested collections / names with paths - https://github.com/janmonschke/backbone-couchdb/issues/17 --- backbone-couchdb.coffee | 15 ++++++++++++--- backbone-couchdb.js | 25 +++++++++++++++++-------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/backbone-couchdb.coffee b/backbone-couchdb.coffee index b5c8545..d8d8c33 100644 --- a/backbone-couchdb.coffee +++ b/backbone-couchdb.coffee @@ -31,8 +31,17 @@ Backbone.couch_connector = con = # jquery.couch.js adds the id itself, so we delete the id if it is in the url. # "collection/:id" -> "collection" _splitted = _name.split "/" - _name = if _splitted.length > 0 then _splitted[0] else _name - _name = _name.replace "/", "" + + # only pop off the last component if it is the id + if (_splitted.length > 0) + if (model.id == _splitted[_splitted.length - 1]) + _splitted.pop() + _name = _splitted.join('/') + + # remove any leading slash + if (_name.indexOf("/") == 0) + _name = _name.replace("/", "") + _name # creates a database instance from the @@ -172,4 +181,4 @@ class Backbone.Collection extends Backbone.Collection class Backbone.Model extends Backbone.Model # change the idAttribute since CouchDB uses _id - idAttribute : "_id" \ No newline at end of file + idAttribute : "_id" diff --git a/backbone-couchdb.js b/backbone-couchdb.js index 6a4cde8..b929137 100644 --- a/backbone-couchdb.js +++ b/backbone-couchdb.js @@ -1,9 +1,10 @@ (function() { /* (c) 2011 Jan Monschke - v1.0 + v1.1 backbone-couchdb.js is licensed under the MIT license. - */ var con; + */ + var con; var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } @@ -38,8 +39,15 @@ _name = _name.slice(1, _name.length); } _splitted = _name.split("/"); - _name = _splitted.length > 0 ? _splitted[0] : _name; - _name = _name.replace("/", ""); + if (_splitted.length > 0) { + if (model.id === _splitted[_splitted.length - 1]) { + _splitted.pop(); + } + _name = _splitted.join('/'); + } + if (_name.indexOf("/") === 0) { + _name = _name.replace("/", ""); + } return _name; }, make_db: function() { @@ -157,11 +165,12 @@ } }; Backbone.Collection = (function() { + __extends(Collection, Backbone.Collection); function Collection() { - this._db_on_change = __bind(this._db_on_change, this);; - this._db_prepared_for_changes = __bind(this._db_prepared_for_changes, this);; Collection.__super__.constructor.apply(this, arguments); + this._db_on_change = __bind(this._db_on_change, this); + this._db_prepared_for_changes = __bind(this._db_prepared_for_changes, this); + Collection.__super__.constructor.apply(this, arguments); } - __extends(Collection, Backbone.Collection); Collection.prototype.initialize = function() { if (!this._db_changes_enabled && ((this.db && this.db.changes) || con.config.global_changes)) { return this.listen_to_changes(); @@ -213,10 +222,10 @@ return Collection; })(); Backbone.Model = (function() { + __extends(Model, Backbone.Model); function Model() { Model.__super__.constructor.apply(this, arguments); } - __extends(Model, Backbone.Model); Model.prototype.idAttribute = "_id"; return Model; })(); From de023494fb3079e82de16dd6ab05cfa538943971 Mon Sep 17 00:00:00 2001 From: Aaron Hamid Date: Sun, 13 Nov 2011 22:02:17 -0500 Subject: [PATCH 2/2] added support for single global handler coupled with local collection filter functions --- backbone-couchdb.coffee | 57 ++++++++++++-- backbone-couchdb.js | 170 +++++++++++++++++++++++++++++----------- 2 files changed, 177 insertions(+), 50 deletions(-) diff --git a/backbone-couchdb.coffee b/backbone-couchdb.coffee index d8d8c33..2a37cb2 100644 --- a/backbone-couchdb.coffee +++ b/backbone-couchdb.coffee @@ -12,9 +12,16 @@ Backbone.couch_connector = con = view_name : "byCollection" # if true, all Collections will have the _changes feed enabled global_changes : false + # if true, a single changes feed connection will be used + single_feed : false # change the databse base_url to be able to fetch from a remote couchdb base_url : null + # global changes feed for all collections + _global_db_inst: null + _global_changes_handler: null + _global_changes_callbacks: [] + # some helper methods for the connector helpers : # returns a string representing the collection (needed for the "collection"-field) @@ -44,6 +51,10 @@ Backbone.couch_connector = con = _name + # default local filter which selects documents of a given collection + filter_collection : (results, collection_name) -> + entry for entry in results when (entry.deleted == true) || (entry.doc?.collection == collection_name) + # creates a database instance from the make_db : -> db = $.couch.db con.config.db_name @@ -85,6 +96,27 @@ Backbone.couch_connector = con = @helpers.make_db().view "#{@config.ddoc_name}/#{_view}", _opts + # initializes the single global changes handler + init_global_changes_handler : (callback) -> + @_global_db_inst = con.helpers.make_db() + @_global_db_inst.info + "success" : (data) => + # initialize the global changes handler + opts = _.extend { include_docs : true }, con.config.global_changes_opts + @_global_changes_handler = @_global_db_inst.changes (data.update_seq || 0), opts + # register a callback which delegates to every registered collection + @_global_changes_handler.onChange (changes) => + cb(changes) for cb in @_global_changes_callbacks + callback() + + # registers a collection callback with the global changes feed + register_global_changes_callback : (callback) -> + return unless callback? + if !@_global_db_inst? + @init_global_changes_handler => + @_global_changes_callbacks.push callback + else + @_global_changes_callbacks.push callback # Reads a model from the couchdb by it's ID read_model : (model, opts) -> @@ -142,9 +174,14 @@ class Backbone.Collection extends Backbone.Collection # don't enable changes feed a second time unless @_db_changes_enabled @_db_changes_enabled = true - @_db_inst = con.helpers.make_db() unless @_db_inst - @_db_inst.info - "success" : @_db_prepared_for_changes + if con.config.single_feed + # if we are using a single feed, don't set up a separate connection for the collection + # register a callback with the global changes handler + @_db_prepared_for_global_changes() + else + @_db_inst = con.helpers.make_db() unless @_db_inst + @_db_inst.info + "success" : @_db_prepared_for_changes # Stop listening to real time updates stop_changes : -> @@ -153,6 +190,7 @@ class Backbone.Collection extends Backbone.Collection @_db_changes_handler.stop() @_db_changes_handler = null + # sets up a new changes feed for this collection _db_prepared_for_changes : (data) => @_db_update_seq = data.update_seq || 0 opts = @@ -163,9 +201,18 @@ class Backbone.Collection extends Backbone.Collection _.defer => @_db_changes_handler = @_db_inst.changes(@_db_update_seq, opts) @_db_changes_handler.onChange @._db_on_change - + + # registers this collection's change handler with the global change feed + _db_prepared_for_global_changes : => + con.register_global_changes_callback(@_db_on_change) + _db_on_change : (changes) => - for _doc in changes.results + results = changes.results + if @db and @db.local_filter # if a local filter has been defined on the collection, use it + results = @db.local_filter(results) + else if con.config.single_feed # otherwise, if we are using a single feed, use the default global changes collection filter + results = con.helpers.filter_collection(results, con.helpers.extract_collection_name(@)) + for _doc in results obj = @get _doc.id # test if collection contains the doc, if not, we add it to the collection if obj? diff --git a/backbone-couchdb.js b/backbone-couchdb.js index b929137..3be2f2a 100644 --- a/backbone-couchdb.js +++ b/backbone-couchdb.js @@ -1,32 +1,30 @@ (function() { + /* (c) 2011 Jan Monschke v1.1 backbone-couchdb.js is licensed under the MIT license. */ + var con; - var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { - for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } - function ctor() { this.constructor = child; } - ctor.prototype = parent.prototype; - child.prototype = new ctor; - child.__super__ = parent.prototype; - return child; - }; + var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor; child.__super__ = parent.prototype; return child; }; + Backbone.couch_connector = con = { config: { db_name: "backbone_connect", ddoc_name: "backbone_example", view_name: "byCollection", global_changes: false, + single_feed: false, base_url: null }, + _global_db_inst: null, + _global_changes_handler: null, + _global_changes_callbacks: [], helpers: { extract_collection_name: function(model) { var _name, _splitted; - if (model == null) { - throw new Error("No model has been passed"); - } + if (model == null) throw new Error("No model has been passed"); if (!(((model.collection != null) && (model.collection.url != null)) || (model.url != null))) { return ""; } @@ -35,21 +33,26 @@ } else { _name = _.isFunction(model.collection.url) ? model.collection.url() : model.collection.url; } - if (_name[0] === "/") { - _name = _name.slice(1, _name.length); - } + if (_name[0] === "/") _name = _name.slice(1, _name.length); _splitted = _name.split("/"); if (_splitted.length > 0) { - if (model.id === _splitted[_splitted.length - 1]) { - _splitted.pop(); - } + if (model.id === _splitted[_splitted.length - 1]) _splitted.pop(); _name = _splitted.join('/'); } - if (_name.indexOf("/") === 0) { - _name = _name.replace("/", ""); - } + if (_name.indexOf("/") === 0) _name = _name.replace("/", ""); return _name; }, + filter_collection: function(results, collection_name) { + var entry, _i, _len, _ref, _results; + _results = []; + for (_i = 0, _len = results.length; _i < _len; _i++) { + entry = results[_i]; + if ((entry.deleted === true) || (((_ref = entry.doc) != null ? _ref.collection : void 0) === collection_name)) { + _results.push(entry); + } + } + return _results; + }, make_db: function() { var db; db = $.couch.db(con.config.db_name); @@ -68,22 +71,19 @@ }, read_collection: function(coll, opts) { var keys, _opts, _view; + var _this = this; _view = this.config.view_name; keys = [this.helpers.extract_collection_name(coll)]; if (coll.db != null) { if (coll.db.changes || this.config.global_changes) { coll.listen_to_changes(); } - if (coll.db.view != null) { - _view = coll.db.view; - } - if (coll.db.keys != null) { - keys = coll.db.keys; - } + if (coll.db.view != null) _view = coll.db.view; + if (coll.db.keys != null) keys = coll.db.keys; } _opts = { keys: keys, - success: __bind(function(data) { + success: function(data) { var doc, _i, _len, _ref, _temp; _temp = []; _ref = data.rows; @@ -92,7 +92,7 @@ _temp.push(doc.value); } return opts.success(_temp); - }, this), + }, error: function() { return opts.error(); } @@ -102,6 +102,41 @@ } return this.helpers.make_db().view("" + this.config.ddoc_name + "/" + _view, _opts); }, + init_global_changes_handler: function(callback) { + var _this = this; + this._global_db_inst = con.helpers.make_db(); + return this._global_db_inst.info({ + "success": function(data) { + var opts; + opts = _.extend({ + include_docs: true + }, con.config.global_changes_opts); + _this._global_changes_handler = _this._global_db_inst.changes(data.update_seq || 0, opts); + _this._global_changes_handler.onChange(function(changes) { + var cb, _i, _len, _ref, _results; + _ref = _this._global_changes_callbacks; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + cb = _ref[_i]; + _results.push(cb(changes)); + } + return _results; + }); + return callback(); + } + }); + }, + register_global_changes_callback: function(callback) { + var _this = this; + if (callback == null) return; + if (!(this._global_db_inst != null)) { + return this.init_global_changes_handler(function() { + return _this._global_changes_callbacks.push(callback); + }); + } else { + return this._global_changes_callbacks.push(callback); + } + }, read_model: function(model, opts) { if (!model.id) { throw new Error("The model has no id property, so it can't get fetched from the database"); @@ -119,9 +154,7 @@ var coll, vals; vals = model.toJSON(); coll = this.helpers.extract_collection_name(model); - if (coll.length > 0) { - vals.collection = coll; - } + if (coll.length > 0) vals.collection = coll; return this.helpers.make_db().saveDoc(vals, { success: function(doc) { return opts.success({ @@ -152,6 +185,7 @@ }); } }; + Backbone.sync = function(method, model, opts) { switch (method) { case "read": @@ -164,29 +198,38 @@ return con.del(model, opts); } }; + Backbone.Collection = (function() { + __extends(Collection, Backbone.Collection); + function Collection() { this._db_on_change = __bind(this._db_on_change, this); + this._db_prepared_for_global_changes = __bind(this._db_prepared_for_global_changes, this); this._db_prepared_for_changes = __bind(this._db_prepared_for_changes, this); Collection.__super__.constructor.apply(this, arguments); } + Collection.prototype.initialize = function() { if (!this._db_changes_enabled && ((this.db && this.db.changes) || con.config.global_changes)) { return this.listen_to_changes(); } }; + Collection.prototype.listen_to_changes = function() { if (!this._db_changes_enabled) { this._db_changes_enabled = true; - if (!this._db_inst) { - this._db_inst = con.helpers.make_db(); + if (con.config.single_feed) { + return this._db_prepared_for_global_changes(); + } else { + if (!this._db_inst) this._db_inst = con.helpers.make_db(); + return this._db_inst.info({ + "success": this._db_prepared_for_changes + }); } - return this._db_inst.info({ - "success": this._db_prepared_for_changes - }); } }; + Collection.prototype.stop_changes = function() { this._db_changes_enabled = false; if (this._db_changes_handler != null) { @@ -194,8 +237,10 @@ return this._db_changes_handler = null; } }; + Collection.prototype._db_prepared_for_changes = function(data) { var opts; + var _this = this; this._db_update_seq = data.update_seq || 0; opts = { include_docs: true, @@ -203,30 +248,65 @@ filter: "" + con.config.ddoc_name + "/by_collection" }; _.extend(opts, this.db); - return _.defer(__bind(function() { - this._db_changes_handler = this._db_inst.changes(this._db_update_seq, opts); - return this._db_changes_handler.onChange(this._db_on_change); - }, this)); + return _.defer(function() { + _this._db_changes_handler = _this._db_inst.changes(_this._db_update_seq, opts); + return _this._db_changes_handler.onChange(_this._db_on_change); + }); }; + + Collection.prototype._db_prepared_for_global_changes = function() { + return con.register_global_changes_callback(this._db_on_change); + }; + Collection.prototype._db_on_change = function(changes) { - var obj, _doc, _i, _len, _ref, _results; - _ref = changes.results; + var obj, results, _doc, _i, _len, _results; + results = changes.results; + if (this.db && this.db.local_filter) { + results = this.db.local_filter(results); + } else if (con.config.single_feed) { + results = con.helpers.filter_collection(results, con.helpers.extract_collection_name(this)); + } _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - _doc = _ref[_i]; + for (_i = 0, _len = results.length; _i < _len; _i++) { + _doc = results[_i]; obj = this.get(_doc.id); - _results.push(obj != null ? _doc.deleted ? this.remove(obj) : obj.get("_rev") !== _doc.doc._rev ? obj.set(_doc.doc) : void 0 : !_doc.deleted ? this.add(_doc.doc) : void 0); + if (obj != null) { + if (_doc.deleted) { + _results.push(this.remove(obj)); + } else { + if (obj.get("_rev") !== _doc.doc._rev) { + _results.push(obj.set(_doc.doc)); + } else { + _results.push(void 0); + } + } + } else { + if (!_doc.deleted) { + _results.push(this.add(_doc.doc)); + } else { + _results.push(void 0); + } + } } return _results; }; + return Collection; + })(); + Backbone.Model = (function() { + __extends(Model, Backbone.Model); + function Model() { Model.__super__.constructor.apply(this, arguments); } + Model.prototype.idAttribute = "_id"; + return Model; + })(); + }).call(this);