ceritium-relaxdb 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/LICENSE +20 -0
  2. data/README.textile +185 -0
  3. data/Rakefile +58 -0
  4. data/docs/spec_results.html +604 -0
  5. data/lib/more/atomic_bulk_save_support.rb +18 -0
  6. data/lib/more/grapher.rb +48 -0
  7. data/lib/relaxdb.rb +40 -0
  8. data/lib/relaxdb/all_delegator.rb +68 -0
  9. data/lib/relaxdb/belongs_to_proxy.rb +29 -0
  10. data/lib/relaxdb/design_doc.rb +55 -0
  11. data/lib/relaxdb/document.rb +531 -0
  12. data/lib/relaxdb/extlib.rb +15 -0
  13. data/lib/relaxdb/has_many_proxy.rb +104 -0
  14. data/lib/relaxdb/has_one_proxy.rb +45 -0
  15. data/lib/relaxdb/paginate_params.rb +54 -0
  16. data/lib/relaxdb/paginator.rb +78 -0
  17. data/lib/relaxdb/query.rb +75 -0
  18. data/lib/relaxdb/references_many_proxy.rb +101 -0
  19. data/lib/relaxdb/relaxdb.rb +208 -0
  20. data/lib/relaxdb/server.rb +156 -0
  21. data/lib/relaxdb/sorted_by_view.rb +65 -0
  22. data/lib/relaxdb/uuid_generator.rb +21 -0
  23. data/lib/relaxdb/validators.rb +11 -0
  24. data/lib/relaxdb/view_object.rb +34 -0
  25. data/lib/relaxdb/view_result.rb +18 -0
  26. data/lib/relaxdb/view_uploader.rb +47 -0
  27. data/lib/relaxdb/views.rb +54 -0
  28. data/spec/belongs_to_spec.rb +129 -0
  29. data/spec/callbacks_spec.rb +80 -0
  30. data/spec/derived_properties_spec.rb +117 -0
  31. data/spec/design_doc_spec.rb +34 -0
  32. data/spec/document_spec.rb +556 -0
  33. data/spec/has_many_spec.rb +176 -0
  34. data/spec/has_one_spec.rb +128 -0
  35. data/spec/query_spec.rb +80 -0
  36. data/spec/references_many_spec.rb +178 -0
  37. data/spec/relaxdb_spec.rb +226 -0
  38. data/spec/spec.opts +1 -0
  39. data/spec/spec_helper.rb +10 -0
  40. data/spec/spec_models.rb +151 -0
  41. data/spec/view_object_spec.rb +47 -0
  42. metadata +123 -0
@@ -0,0 +1,18 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+ require 'relaxdb'
3
+ require File.dirname(__FILE__) + '/../../spec/spec_models.rb'
4
+
5
+ RelaxDB.configure :host => "localhost", :port => 5984
6
+ RelaxDB.delete_db "relaxdb_spec_db" rescue :ok
7
+ RelaxDB.use_db "relaxdb_spec_db"
8
+
9
+ a1 = Atom.new.save!
10
+ a1_dup = a1.dup
11
+ a1.save!
12
+ begin
13
+ RelaxDB.bulk_save! a1_dup
14
+ puts "Atomic bulk_save _not_ supported"
15
+ rescue RelaxDB::UpdateConflict
16
+ puts "Atomic bulk_save supported"
17
+ end
18
+
@@ -0,0 +1,48 @@
1
+ module RelaxDB
2
+
3
+ #
4
+ # The GraphCreator uses dot to create a graphical model of an entire CouchDB database
5
+ # It probably only makes sense to run it on a database of a limited size
6
+ # The created graphs can be very useful for exploring relationships
7
+ # Run ruby scratch/grapher_demo.rb for an example
8
+ #
9
+ class GraphCreator
10
+
11
+ def self.create
12
+ system "mkdir -p graphs"
13
+
14
+ data = JSON.parse(RelaxDB.db.get("_all_docs").body)
15
+ all_ids = data["rows"].map { |r| r["id"] }
16
+ all_ids = all_ids.reject { |id| id =~ /_/ }
17
+
18
+ dot = "digraph G { \nrankdir=LR;\nnode [shape=record];\n"
19
+ all_ids.each do |id|
20
+ doc = RelaxDB.load(id)
21
+ atts = "#{doc.class}\\l|"
22
+ doc.properties.each do |prop|
23
+ # we don't care about the revision
24
+ next if prop == :_rev
25
+
26
+ prop_val = doc.instance_variable_get("@#{prop}".to_sym)
27
+ atts << "#{prop}\\l#{prop_val}|" if prop_val
28
+ end
29
+ atts = atts[0, atts.length-1]
30
+
31
+ dot << %Q%#{doc._id} [ label ="#{atts}"];\n%
32
+
33
+ doc.class.belongs_to_rels.each do |relationship, opts|
34
+ id = doc.instance_variable_get("@#{relationship}_id".to_sym)
35
+ dot << %Q%#{id} -> #{doc._id} [ label = "#{relationship}"];\n% if id
36
+ end
37
+
38
+ end
39
+ dot << "}"
40
+
41
+ File.open("graphs/data.dot", "w") { |f| f.write(dot) }
42
+
43
+ system "dot -Tpng -o graphs/all_docs.png graphs/data.dot"
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,40 @@
1
+ require 'rubygems'
2
+ require 'extlib'
3
+ require 'json'
4
+ require 'uuid'
5
+
6
+ require 'cgi'
7
+ require 'net/http'
8
+ require 'logger'
9
+ require 'parsedate'
10
+ require 'pp'
11
+ require 'tempfile'
12
+
13
+ $:.unshift(File.dirname(__FILE__)) unless
14
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
15
+
16
+ require 'relaxdb/validators'
17
+
18
+ require 'relaxdb/all_delegator'
19
+ require 'relaxdb/belongs_to_proxy'
20
+ require 'relaxdb/design_doc'
21
+ require 'relaxdb/document'
22
+ require 'relaxdb/extlib'
23
+ require 'relaxdb/has_many_proxy'
24
+ require 'relaxdb/has_one_proxy'
25
+ require 'relaxdb/paginate_params'
26
+ require 'relaxdb/paginator'
27
+ require 'relaxdb/query'
28
+ require 'relaxdb/references_many_proxy'
29
+ require 'relaxdb/relaxdb'
30
+ require 'relaxdb/server'
31
+ require 'relaxdb/sorted_by_view'
32
+ require 'relaxdb/uuid_generator'
33
+ require 'relaxdb/view_object'
34
+ require 'relaxdb/view_result'
35
+ require 'relaxdb/view_uploader'
36
+ require 'relaxdb/views'
37
+ require 'more/grapher.rb'
38
+
39
+ module RelaxDB
40
+ end
@@ -0,0 +1,68 @@
1
+ module RelaxDB
2
+
3
+ #
4
+ # The AllDelegator allows clients to query CouchDB in a natural way
5
+ # FooDoc.all - returns all docs in CouchDB of type FooDoc
6
+ # FooDoc.all.sorted_by(:att1, :att2) - returns all docs in CouchDB of type FooDoc sorted by att1, then att2
7
+ # FooDoc.all.sorted_by(:att1) { |q| q.key("bar") } - returns all docs of type FooDoc where att1 equals "bar"
8
+ # FooDoc.all.destroy! - does what it says on the tin
9
+ # FooDoc.all.size - issues a query to a reduce function that returns the total number of docs for that class
10
+ #
11
+ class AllDelegator < Delegator
12
+
13
+ def initialize(class_name)
14
+ super([])
15
+ @class_name = class_name
16
+ end
17
+
18
+ def __getobj__
19
+ view_path = "_design/#{@class_name}/_view/all?reduce=false"
20
+ map, reduce = ViewCreator.all(@class_name)
21
+
22
+ RelaxDB.retrieve(view_path, @class_name, "all", map, reduce)
23
+ end
24
+
25
+ def sorted_by(*atts)
26
+ view = SortedByView.new(@class_name, *atts)
27
+
28
+ query = Query.new(@class_name, view.view_name)
29
+ yield query if block_given?
30
+
31
+ view.query(query)
32
+ end
33
+
34
+ # Note that this method leaves the corresponding DesignDoc for the associated class intact
35
+ def destroy!
36
+ each do |o|
37
+ # A reload is required for deleting objects with a self referential references_many relationship
38
+ # This makes all.destroy! very slow. Given that references_many is now deprecated and will
39
+ # soon be removed, the required reload is no longer performed.
40
+ # obj = RelaxDB.load(o._id)
41
+ # obj.destroy!
42
+
43
+ o.destroy!
44
+ end
45
+ end
46
+
47
+ # This is pretty ugly - this pattern is now spread over three
48
+ # places (sorted_by_view, relaxdb and here)
49
+ # Consolidation needed
50
+ def size
51
+ view_path = "_design/#{@class_name}/_view/all"
52
+ map, reduce = ViewCreator.all(@class_name)
53
+
54
+ begin
55
+ resp = RelaxDB.db.get(view_path)
56
+ rescue => e
57
+ DesignDocument.get(@class_name).add_map_view("all", map).
58
+ add_reduce_view("all", reduce).save
59
+ resp = RelaxDB.db.get(view_path)
60
+ end
61
+
62
+ data = JSON.parse(resp.body)
63
+ data["rows"][0] ? data["rows"][0]["value"] : 0
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,29 @@
1
+ module RelaxDB
2
+
3
+ class BelongsToProxy
4
+
5
+ attr_reader :target
6
+
7
+ def initialize(client, relationship)
8
+ @client = client
9
+ @relationship = relationship
10
+ @target = nil
11
+ end
12
+
13
+ def target
14
+ return @target if @target
15
+
16
+ id = @client.instance_variable_get("@#{@relationship}_id")
17
+ @target = RelaxDB.load(id) if id
18
+ end
19
+
20
+ def target=(new_target)
21
+ id = new_target ? new_target._id : nil
22
+ @client.instance_variable_set("@#{@relationship}_id", id)
23
+
24
+ @target = new_target
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,55 @@
1
+ module RelaxDB
2
+
3
+ class DesignDocument
4
+
5
+ def initialize(client_class, data)
6
+ @client_class = client_class
7
+ @data = data
8
+ end
9
+
10
+ def add_map_view(view_name, function)
11
+ add_view(view_name, "map", function)
12
+ end
13
+
14
+ def add_reduce_view(view_name, function)
15
+ add_view(view_name, "reduce", function)
16
+ end
17
+
18
+ def add_validation_func(function)
19
+ @data["validate_doc_update"] = function
20
+ self
21
+ end
22
+
23
+ def add_view(view_name, type, function)
24
+ @data["views"] ||= {}
25
+ @data["views"][view_name] ||= {}
26
+ @data["views"][view_name][type] = function
27
+ self
28
+ end
29
+
30
+ def save
31
+ database = RelaxDB.db
32
+ resp = database.put(@data["_id"], @data.to_json)
33
+ @data["_rev"] = JSON.parse(resp.body)["rev"]
34
+ self
35
+ end
36
+
37
+ def self.get(client_class)
38
+ begin
39
+ database = RelaxDB.db
40
+ resp = database.get("_design/#{client_class}")
41
+ DesignDocument.new(client_class, JSON.parse(resp.body))
42
+ rescue HTTP_404
43
+ DesignDocument.new(client_class, {"_id" => "_design/#{client_class}"} )
44
+ end
45
+ end
46
+
47
+ def destroy!
48
+ # Implicitly prevent the object from being resaved by failing to update its revision
49
+ RelaxDB.db.delete("#{@data["_id"]}?rev=#{@data["_rev"]}")
50
+ self
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,531 @@
1
+ module RelaxDB
2
+
3
+ class Document
4
+
5
+ include RelaxDB::Validators
6
+
7
+ # Used to store validation messages
8
+ attr_accessor :errors
9
+
10
+ # Attribute symbols added to this list won't be validated on save
11
+ attr_accessor :validation_skip_list
12
+
13
+ # Define properties and property methods
14
+
15
+ def self.property(prop, opts={})
16
+ # Class instance varibles are not inherited, so the default properties must be explicitly listed
17
+ # Perhaps a better solution exists. Revise. I think extlib contains a solution for this...
18
+ @properties ||= [:_id, :_rev]
19
+ @properties << prop
20
+
21
+ define_method(prop) do
22
+ instance_variable_get("@#{prop}".to_sym)
23
+ end
24
+
25
+ define_method("#{prop}=") do |val|
26
+ instance_variable_set("@#{prop}".to_sym, val)
27
+ end
28
+
29
+ if opts[:default]
30
+ define_method("set_default_#{prop}") do
31
+ default = opts[:default]
32
+ default = default.is_a?(Proc) ? default.call : default
33
+ instance_variable_set("@#{prop}".to_sym, default)
34
+ end
35
+ end
36
+
37
+ if opts[:validator]
38
+ create_validator(prop, opts[:validator])
39
+ end
40
+
41
+ if opts[:validation_msg]
42
+ create_validation_msg(prop, opts[:validation_msg])
43
+ end
44
+
45
+ if opts[:derived]
46
+ add_derived_prop(prop, opts[:derived])
47
+ end
48
+ end
49
+
50
+ def self.properties
51
+ # Ensure that classes that don't define their own properties still function as CouchDB objects
52
+ @properties ||= [:_id, :_rev]
53
+ end
54
+
55
+ def self.create_validator(att, v)
56
+ method_name = "validate_#{att}"
57
+ if v.is_a? Proc
58
+ v.arity == 1 ?
59
+ define_method(method_name) { |att_val| v.call(att_val) } :
60
+ define_method(method_name) { |att_val| v.call(att_val, self) }
61
+ elsif instance_methods.include? "validator_#{v}"
62
+ define_method(method_name) { |att_val| send("validator_#{v}", att_val, self) }
63
+ else
64
+ define_method(method_name) { |att_val| send(v, att_val) }
65
+ end
66
+ end
67
+
68
+ def self.create_validation_msg(att, validation_msg)
69
+ if validation_msg.is_a?(Proc)
70
+ validation_msg.arity == 1 ?
71
+ define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val) } :
72
+ define_method("#{att}_validation_msg") { |att_val| validation_msg.call(att_val, self) }
73
+ else
74
+ define_method("#{att}_validation_msg") { validation_msg }
75
+ end
76
+ end
77
+
78
+ # See derived_properties_spec.rb for usage
79
+ def self.add_derived_prop(prop, deriver)
80
+ source, writer = deriver[0], deriver[1]
81
+ @derived_prop_writers ||= {}
82
+ @derived_prop_writers[source] ||= {}
83
+ @derived_prop_writers[source][prop] = writer
84
+ end
85
+
86
+ def self.derived_prop_writers
87
+ @derived_prop_writers ||= {}
88
+ end
89
+
90
+ #
91
+ # The rationale for rescuing the send below is that the lambda for a derived
92
+ # property shouldn't need to concern itself with checking the validity of
93
+ # the underlying property. Nor, IMO, should clients be exposed to the
94
+ # possibility of a writer raising an exception.
95
+ #
96
+ def write_derived_props(source)
97
+ writers = self.class.derived_prop_writers[source]
98
+ if writers
99
+ writers.each do |prop, writer|
100
+ current_val = send(prop)
101
+ begin
102
+ send("#{prop}=", writer.call(current_val, self))
103
+ rescue => e
104
+ RelaxDB.logger.warn "Deriving #{prop} from #{source} raised #{e}"
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ def properties
111
+ self.class.properties
112
+ end
113
+
114
+ # Specifying these properties here (after property method has been defined)
115
+ # is kinda ugly. Consider a better solution.
116
+ property :_id
117
+ property :_rev
118
+
119
+ def initialize(hash={})
120
+ unless hash["_id"]
121
+ self._id = UuidGenerator.uuid
122
+ end
123
+
124
+ @errors = Errors.new
125
+ @validation_skip_list = []
126
+
127
+ # Set default properties if this object isn't being loaded from CouchDB
128
+ unless hash["_rev"]
129
+ properties.each do |prop|
130
+ if methods.include?("set_default_#{prop}")
131
+ send("set_default_#{prop}")
132
+ end
133
+ end
134
+ end
135
+
136
+ @set_derived_props = hash["_rev"] ? false : true
137
+ set_attributes(hash)
138
+ @set_derived_props = true
139
+ end
140
+
141
+ def set_attributes(data)
142
+ data.each do |key, val|
143
+ # Only set instance variables on creation - object references are resolved on demand
144
+
145
+ # If the variable name ends in _at, _on or _date try to convert it to a Time
146
+ if [/_at$/, /_on$/, /_date$/].inject(nil) { |i, r| i ||= (key =~ r) }
147
+ val = Time.parse(val).utc rescue val
148
+ end
149
+
150
+ # Ignore param keys that don't have a corresponding writer
151
+ # This allows us to comfortably accept a hash containing superflous data
152
+ # such as a params hash in a controller
153
+ send("#{key}=".to_sym, val) if methods.include? "#{key}="
154
+ end
155
+ end
156
+
157
+ def inspect
158
+ s = "#<#{self.class}:#{self.object_id}"
159
+ properties.each do |prop|
160
+ prop_val = instance_variable_get("@#{prop}".to_sym)
161
+ s << ", #{prop}: #{prop_val.inspect}" if prop_val
162
+ end
163
+ self.class.belongs_to_rels.each do |relationship, opts|
164
+ id = instance_variable_get("@#{relationship}_id".to_sym)
165
+ s << ", #{relationship}_id: #{id}" if id
166
+ end
167
+ s << ", errors: #{errors.inspect}" unless errors.empty?
168
+ s << ">"
169
+ end
170
+
171
+ alias_method :to_s, :inspect
172
+
173
+ def to_json
174
+ data = {}
175
+ self.class.belongs_to_rels.each do |relationship, opts|
176
+ id = instance_variable_get("@#{relationship}_id".to_sym)
177
+ data["#{relationship}_id"] = id if id
178
+ end
179
+ properties.each do |prop|
180
+ prop_val = instance_variable_get("@#{prop}".to_sym)
181
+ data["#{prop}"] = prop_val if prop_val
182
+ end
183
+ data["class"] = self.class.name
184
+ data.to_json
185
+ end
186
+
187
+ # Not yet sure of final implemention for hooks - may lean more towards DM than AR
188
+ def save
189
+ if pre_save && save_to_couch
190
+ after_save
191
+ self
192
+ else
193
+ false
194
+ end
195
+ end
196
+
197
+ def save_to_couch
198
+ begin
199
+ resp = RelaxDB.db.put(_id, to_json)
200
+ self._rev = JSON.parse(resp.body)["rev"]
201
+ rescue HTTP_409
202
+ on_update_conflict
203
+ @update_conflict = true
204
+ return false
205
+ end
206
+ end
207
+
208
+ def on_update_conflict
209
+ # override with any behaviour you want to happen when
210
+ # CouchDB returns DocumentConflict on an attempt to save
211
+ end
212
+
213
+ def pre_save
214
+ set_created_at if new_document?
215
+ return false unless validates?
216
+ return false unless before_save
217
+ true
218
+ end
219
+
220
+ def post_save
221
+ after_save
222
+ end
223
+
224
+ def save!
225
+ if save
226
+ self
227
+ elsif update_conflict?
228
+ raise UpdateConflict, self
229
+ else
230
+ raise ValidationFailure, self.errors.to_json
231
+ end
232
+ end
233
+
234
+ def save_all
235
+ RelaxDB.bulk_save(self, *all_children)
236
+ end
237
+
238
+ def save_all!
239
+ RelaxDB.bulk_save!(self, *all_children)
240
+ end
241
+
242
+ def all_children
243
+ ho = self.class.has_one_rels.map { |r| send(r) }
244
+ hm = self.class.has_many_rels.inject([]) { |m,r| m += send(r).children }
245
+ ho + hm
246
+ end
247
+
248
+ def update_conflict?
249
+ @update_conflict
250
+ end
251
+
252
+ def validates?
253
+ props = properties - validation_skip_list
254
+ prop_vals = props.map { |prop| instance_variable_get("@#{prop}") }
255
+
256
+ rels = self.class.belongs_to_rels.keys - validation_skip_list
257
+ rel_vals = rels.map { |rel| instance_variable_get("@#{rel}_id") }
258
+
259
+ att_names = props + rels
260
+ att_vals = prop_vals + rel_vals
261
+
262
+ total_success = true
263
+ att_names.each_index do |i|
264
+ att_name, att_val = att_names[i], att_vals[i]
265
+ if methods.include? "validate_#{att_name}"
266
+ total_success &= validate_att(att_name, att_val)
267
+ end
268
+ end
269
+
270
+ total_success
271
+ end
272
+ alias_method :validate, :validates?
273
+
274
+ def validate_att(att_name, att_val)
275
+ begin
276
+ success = send("validate_#{att_name}", att_val)
277
+ rescue => e
278
+ RelaxDB.logger.warn "Validating #{att_name} with #{att_val} raised #{e}"
279
+ succes = false
280
+ end
281
+
282
+ unless success
283
+ if methods.include? "#{att_name}_validation_msg"
284
+ begin
285
+ @errors[att_name] = send("#{att_name}_validation_msg", att_val)
286
+ rescue => e
287
+ RelaxDB.logger.warn "Validation_msg for #{att_name} with #{att_val} raised #{e}"
288
+ @errors[att_name] = "validation_msg_exception:invalid:#{att_val}"
289
+ end
290
+ elsif @errors[att_name].nil?
291
+ # The above prevents overwriting when validators set their own
292
+ # validation messages
293
+ @errors[att_name] = "invalid:#{att_val}"
294
+ end
295
+ end
296
+ success
297
+ end
298
+
299
+ def new_document?
300
+ @_rev.nil?
301
+ end
302
+ alias_method :new_record?, :new_document?
303
+ alias_method :unsaved?, :new_document?
304
+
305
+ def to_param
306
+ self._id
307
+ end
308
+ alias_method :id, :to_param
309
+
310
+ def set_created_at
311
+ if methods.include? "created_at"
312
+ # Don't override it if it's already been set
313
+ @created_at = Time.now if @created_at.nil?
314
+ end
315
+ end
316
+
317
+ def create_or_get_proxy(klass, relationship, opts=nil)
318
+ proxy_sym = "@proxy_#{relationship}".to_sym
319
+ proxy = instance_variable_get(proxy_sym)
320
+ unless proxy
321
+ proxy = opts ? klass.new(self, relationship, opts) : klass.new(self, relationship)
322
+ instance_variable_set(proxy_sym, proxy)
323
+ end
324
+ proxy
325
+ end
326
+
327
+ # Returns true if CouchDB considers other to be the same as self
328
+ def ==(other)
329
+ other && _id == other._id
330
+ end
331
+
332
+ # If you're using this method, read the specs and make sure you understand
333
+ # how it can be used and how it shouldn't be used
334
+ def self.references_many(relationship, opts={})
335
+ # Treat the representation as a standard property
336
+ properties << relationship
337
+
338
+ # Keep track of the relationship so peers can be disassociated on destroy
339
+ @references_many_rels ||= []
340
+ @references_many_rels << relationship
341
+
342
+ id_arr_sym = "@#{relationship}".to_sym
343
+
344
+ define_method(relationship) do
345
+ instance_variable_set(id_arr_sym, []) unless instance_variable_defined? id_arr_sym
346
+ create_or_get_proxy(ReferencesManyProxy, relationship, opts)
347
+ end
348
+
349
+ define_method("#{relationship}_ids") do
350
+ instance_variable_set(id_arr_sym, []) unless instance_variable_defined? id_arr_sym
351
+ instance_variable_get(id_arr_sym)
352
+ end
353
+
354
+ define_method("#{relationship}=") do |val|
355
+ # Don't invoke this method unless you know what you're doing
356
+ instance_variable_set(id_arr_sym, val)
357
+ end
358
+ end
359
+
360
+ def self.references_many_rels
361
+ @references_many_rels ||= []
362
+ end
363
+
364
+ def self.has_many(relationship, opts={})
365
+ @has_many_rels ||= []
366
+ @has_many_rels << relationship
367
+
368
+ define_method(relationship) do
369
+ create_or_get_proxy(HasManyProxy, relationship, opts)
370
+ end
371
+
372
+ define_method("#{relationship}=") do |children|
373
+ create_or_get_proxy(HasManyProxy, relationship, opts).children = children
374
+ write_derived_props(relationship) if @set_derived_props
375
+ children
376
+ end
377
+ end
378
+
379
+ def self.has_many_rels
380
+ # Don't force clients to check its instantiated
381
+ @has_many_rels ||= []
382
+ end
383
+
384
+ def self.has_one(relationship)
385
+ @has_one_rels ||= []
386
+ @has_one_rels << relationship
387
+
388
+ define_method(relationship) do
389
+ create_or_get_proxy(HasOneProxy, relationship).target
390
+ end
391
+
392
+ define_method("#{relationship}=") do |new_target|
393
+ create_or_get_proxy(HasOneProxy, relationship).target = new_target
394
+ write_derived_props(relationship) if @set_derived_props
395
+ new_target
396
+ end
397
+ end
398
+
399
+ def self.has_one_rels
400
+ @has_one_rels ||= []
401
+ end
402
+
403
+ def self.belongs_to(relationship, opts={})
404
+ @belongs_to_rels ||= {}
405
+ @belongs_to_rels[relationship] = opts
406
+
407
+ define_method(relationship) do
408
+ create_or_get_proxy(BelongsToProxy, relationship).target
409
+ end
410
+
411
+ define_method("#{relationship}=") do |new_target|
412
+ create_or_get_proxy(BelongsToProxy, relationship).target = new_target
413
+ write_derived_props(relationship) if @set_derived_props
414
+ end
415
+
416
+ # Allows all writers to be invoked from the hash passed to initialize
417
+ define_method("#{relationship}_id=") do |id|
418
+ instance_variable_set("@#{relationship}_id".to_sym, id)
419
+ write_derived_props(relationship) if @set_derived_props
420
+ id
421
+ end
422
+
423
+ # Allows belongs_to relationships to be used by the paginator
424
+ define_method("#{relationship}_id") do
425
+ instance_variable_get("@#{relationship}_id")
426
+ end
427
+
428
+ create_validator(relationship, opts[:validator]) if opts[:validator]
429
+
430
+ # Untested below
431
+ create_validation_msg(relationship, opts[:validation_msg]) if opts[:validation_msg]
432
+ end
433
+
434
+ class << self
435
+ alias_method :references, :belongs_to
436
+ end
437
+
438
+ def self.belongs_to_rels
439
+ @belongs_to_rels ||= {}
440
+ end
441
+
442
+ def self.all_relationships
443
+ belongs_to_rels + has_one_rels + has_many_rels + references_many_rels
444
+ end
445
+
446
+ def self.all
447
+ @all_delegator ||= AllDelegator.new(self.name)
448
+ end
449
+
450
+ # destroy! nullifies all relationships with peers and children before deleting
451
+ # itself in CouchDB
452
+ # The nullification and deletion are not performed in a transaction
453
+ #
454
+ # TODO: Current implemention may be inappropriate - causing CouchDB to try to JSON
455
+ # encode undefined. Ensure nil is serialized? See has_many_spec#should nullify its child relationships
456
+ def destroy!
457
+ self.class.references_many_rels.each do |rel|
458
+ send(rel).clear
459
+ end
460
+
461
+ self.class.has_many_rels.each do |rel|
462
+ send(rel).clear
463
+ end
464
+
465
+ self.class.has_one_rels.each do |rel|
466
+ send("#{rel}=".to_sym, nil)
467
+ end
468
+
469
+ # Implicitly prevent the object from being resaved by failing to update its revision
470
+ RelaxDB.db.delete("#{_id}?rev=#{_rev}")
471
+ self
472
+ end
473
+
474
+ #
475
+ # Callbacks - define these in a module and mix'em'in ?
476
+ #
477
+ def self.before_save(callback)
478
+ before_save_callbacks << callback
479
+ end
480
+
481
+ def self.before_save_callbacks
482
+ @before_save ||= []
483
+ end
484
+
485
+ def before_save
486
+ self.class.before_save_callbacks.each do |callback|
487
+ resp = callback.is_a?(Proc) ? callback.call(self) : send(callback)
488
+ if resp == false
489
+ errors[:before_save] = :failed
490
+ return false
491
+ end
492
+ end
493
+ end
494
+
495
+ def self.after_save(callback)
496
+ after_save_callbacks << callback
497
+ end
498
+
499
+ def self.after_save_callbacks
500
+ @after_save_callbacks ||= []
501
+ end
502
+
503
+ def after_save
504
+ self.class.after_save_callbacks.each do |callback|
505
+ callback.is_a?(Proc) ? callback.call(self) : send(callback)
506
+ end
507
+ end
508
+
509
+ def self.paginate_by(page_params, *view_keys)
510
+ paginate_params = PaginateParams.new
511
+ yield paginate_params
512
+ raise paginate_params.error_msg if paginate_params.invalid?
513
+
514
+ paginator = Paginator.new(paginate_params, page_params)
515
+
516
+ design_doc_name = self.name
517
+ view = SortedByView.new(design_doc_name, *view_keys)
518
+ query = Query.new(design_doc_name, view.view_name)
519
+ query.merge(paginate_params)
520
+
521
+ docs = view.query(query)
522
+ docs.reverse! if paginate_params.order_inverted?
523
+
524
+ paginator.add_next_and_prev(docs, design_doc_name, view.view_name, view_keys)
525
+
526
+ docs
527
+ end
528
+
529
+ end
530
+
531
+ end