backbone-couchdb.coffee

(c) 2011 Jan Monschke backbone-couchdb.js is licensed under the MIT license.

Backbone.couch_connector = con =

some default config values for the database connections

  config : 
    db_name : "backbone_connect"
    ddoc_name : "backbone_example"
    view_name : "byCollection"

if true, all Collections will have the _changes feed enabled

    global_changes : false

change the databse base_url to be able to fetch from a remote couchdb

    base_url : null
  

some helper methods for the connector

  helpers : 

returns a string representing the collection (needed for the "collection"-field)

    extract_collection_name : (model) ->
      throw new Error("No model has been passed") unless model?
      return "" unless ((model.collection? and model.collection.url?) or model.url?)
      if model.url?
        _name = if _.isFunction(model.url) then model.url() else model.url
      else
        _name = if _.isFunction(model.collection.url) then model.collection.url() else model.collection.url

remove the / at the beginning

      _name = _name.slice(1, _name.length) if _name[0] == "/"
      

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 "/", ""
      _name
    

creates a database instance from the

    make_db : ->
      db = $.couch.db con.config.db_name
      if con.config.base_url?
        db.uri = "#{con.config.base_url}/#{con.config.db_name}/";
      db
      

calls either the read method for collecions or models

  read : (model, opts) ->
    if model.models 
      con.read_collection model, opts 
    else
      con.read_model model, opts

Reads all docs of a collection based on the byCollection view or a custom view specified by the collection

  read_collection : (coll, opts) ->
    _view = @config.view_name
    keys = [@helpers.extract_collection_name coll]
    if coll.db?
      coll.listen_to_changes() if coll.db.changes or @config.global_changes
      if coll.db.view?
        _view = coll.db.view
        keys = coll.db.keys ? null

    @helpers.make_db().view "#{@config.ddoc_name}/#{_view}",
      keys : keys
      success : (data) =>
        _temp = []
        for doc in data.rows
          _temp.push doc.value
        opts.success _temp
      error : ->
        opts.error()

Reads a model from the couchdb by it's ID

  read_model : (model, opts) ->
    throw new Error("The model has no id property, so it can't get fetched from the database") unless model.id
    @helpers.make_db().openDoc model.id,
      success : (doc) -> 
        opts.success(doc)
      error : ->
        opts.error()
  

Creates a model in the db

  create : (model, opts) ->
    vals = model.toJSON()
    coll = @helpers.extract_collection_name model
    vals.collection = coll if coll.length > 0
    @helpers.make_db().saveDoc vals,
      success : (doc) ->
        opts.success
          _id : doc.id
          _rev : doc.rev
      error : ->
        opts.error()

jquery.couch.js uses the same method for updating as it uses for creating a document, so we can use the create method here. ###

  update : (model, opts) ->
    @create(model, opts)

Deletes a model from the db

  del : (model, opts) ->
    @helpers.make_db().removeDoc model.toJSON(),
      success : ->
        opts.success()
      error : (nr, req, e) ->
        if e == "deleted"

The doc does no longer exist on the server

          opts.success()
        else
          opts.error()

Overriding the sync method here to make the connector work ###

Backbone.sync = (method, model, opts) ->
  switch method
    when "read" then con.read model, opts
    when "create" then con.create model, opts
    when "update" then con.update model, opts
    when "delete" then con.del model, opts

Adds some more methods to Collections that are needed for the connector ###

class Backbone.Collection extends Backbone.Collection
  initialize : ->
    @listen_to_changes() if !@_db_changes_enabled && (@db.changes or con.config.global_changes)

Manually start listening to real time updates

  listen_to_changes : ->

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

Stop listening to real time updates

  stop_changes : ->
    @_db_changes_enabled = false
    if @_db_changes_handler?
      @_db_changes_handler.stop()
      @_db_changes_handler = null

  _db_prepared_for_changes : (data) =>
    @_db_update_seq = data.update_seq || 0
    opts = 
      include_docs : true
      collection : con.helpers.extract_collection_name(@)
      filter : "#{con.config.ddoc_name}/by_collection"
    _.extend opts, @db
    _.defer => 
      @_db_changes_handler = @_db_inst.changes(@_db_update_seq, opts)
      @_db_changes_handler.onChange @._db_on_change
    
  _db_on_change : (changes) =>
    for _doc in changes.results
      obj = @get _doc.id

test if collection contains the doc, if not, we add it to the collection

      if obj?

remove from collection if doc has been deleted on the server

        if _doc.deleted
          @remove obj 
        else

set new values if _revs are not the same

          obj.set _doc.doc unless obj.get("_rev") == _doc.doc._rev 
      else
        @add _doc.doc if !_doc.deleted
      

class Backbone.Model extends Backbone.Model

change the idAttribute since CouchDB uses _id

  idAttribute : "_id"