forked from janmonschke/backbone-couchdb
-
Notifications
You must be signed in to change notification settings - Fork 0
/
backbone-couchdb.coffee
231 lines (202 loc) · 8.2 KB
/
backbone-couchdb.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
###
(c) 2011 Jan Monschke
v1.1
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
# 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)
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 "/"
# 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
# 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
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
if coll.db.keys?
keys = coll.db.keys
_opts =
keys : keys
success : (data) =>
_temp = []
for doc in data.rows
_temp.push doc.value
opts.success _temp
error : ->
opts.error()
# delete keys if a custom view is requested but no custom keys
if coll.db? and coll.db.view? and not coll.db.keys?
delete _opts.keys
@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) ->
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 and @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
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 : ->
@_db_changes_enabled = false
if @_db_changes_handler?
@_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 =
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
# 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) =>
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?
# 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"