cohitre-relaxdb 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,38 @@
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/all_delegator'
17
+ require 'relaxdb/belongs_to_proxy'
18
+ require 'relaxdb/design_doc'
19
+ require 'relaxdb/document'
20
+ require 'relaxdb/extlib'
21
+ require 'relaxdb/has_many_proxy'
22
+ require 'relaxdb/has_one_proxy'
23
+ require 'relaxdb/paginate_params'
24
+ require 'relaxdb/paginator'
25
+ require 'relaxdb/query'
26
+ require 'relaxdb/references_many_proxy'
27
+ require 'relaxdb/relaxdb'
28
+ require 'relaxdb/server'
29
+ require 'relaxdb/sorted_by_view'
30
+ require 'relaxdb/uuid_generator'
31
+ require 'relaxdb/view_object'
32
+ require 'relaxdb/view_result'
33
+ require 'relaxdb/view_uploader'
34
+ require 'relaxdb/views'
35
+ require 'more/grapher.rb'
36
+
37
+ module RelaxDB
38
+ end
@@ -0,0 +1,48 @@
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
+ #
10
+ class AllDelegator < Delegator
11
+
12
+ def initialize(class_name)
13
+ super([])
14
+ @class_name = class_name
15
+ end
16
+
17
+ def __getobj__
18
+ view_path = "_view/#{@class_name}/all"
19
+ map_function = ViewCreator.all(@class_name)
20
+
21
+ @all = RelaxDB.retrieve(view_path, @class_name, "all", map_function)
22
+ end
23
+
24
+ def sorted_by(*atts)
25
+ view = SortedByView.new(@class_name, *atts)
26
+
27
+ query = Query.new(@class_name, view.view_name)
28
+ yield query if block_given?
29
+
30
+ view.query(query)
31
+ end
32
+
33
+ # Note that this method leaves the corresponding DesignDoc for the associated class intact
34
+ def destroy!
35
+ each do |o|
36
+ # A reload is required for deleting objects with a self referential references_many relationship
37
+ # This makes all.destroy! very slow. Given that references_many is now deprecated and will
38
+ # soon be removed, the required reload is no longer performed.
39
+ # obj = RelaxDB.load(o._id)
40
+ # obj.destroy!
41
+
42
+ o.destroy!
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ 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,50 @@
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_view(view_name, type, function)
19
+ @data["views"] ||= {}
20
+ @data["views"][view_name] ||= {}
21
+ @data["views"][view_name][type] = function
22
+ self
23
+ end
24
+
25
+ def save
26
+ database = RelaxDB.db
27
+ resp = database.put(::CGI::escape(@data["_id"]), @data.to_json)
28
+ @data["_rev"] = JSON.parse(resp.body)["rev"]
29
+ self
30
+ end
31
+
32
+ def self.get(client_class)
33
+ begin
34
+ database = RelaxDB.db
35
+ resp = database.get(::CGI::escape("_design/#{client_class}"))
36
+ DesignDocument.new(client_class, JSON.parse(resp.body))
37
+ rescue => e
38
+ DesignDocument.new(client_class, {"_id" => "_design/#{client_class}"} )
39
+ end
40
+ end
41
+
42
+ def destroy!
43
+ # Implicitly prevent the object from being resaved by failing to update its revision
44
+ RelaxDB.db.delete("#{::CGI::escape(@data["_id"])}?rev=#{@data["_rev"]}")
45
+ self
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,386 @@
1
+ module RelaxDB
2
+
3
+ class Document
4
+
5
+ # Used to store validation messages
6
+ attr_accessor :errors
7
+
8
+ # Define properties and property methods
9
+
10
+ def self.property(prop, opts={})
11
+ # Class instance varibles are not inherited, so the default properties must be explicitly listed
12
+ # Perhaps a better solution exists. Revise. I think extlib contains a solution for this...
13
+ @properties ||= [:_id, :_rev]
14
+ @properties << prop
15
+
16
+ define_method(prop) do
17
+ instance_variable_get("@#{prop}".to_sym)
18
+ end
19
+
20
+ define_method("#{prop}=") do |val|
21
+ instance_variable_set("@#{prop}".to_sym, val)
22
+ end
23
+
24
+ if opts[:default]
25
+ define_method("set_default_#{prop}") do
26
+ default = opts[:default]
27
+ default = default.is_a?(Proc) ? default.call : default
28
+ instance_variable_set("@#{prop}".to_sym, default)
29
+ end
30
+ end
31
+
32
+ if opts[:validator]
33
+ define_method("validate_#{prop}") do |prop_val|
34
+ opts[:validator].call(prop_val)
35
+ end
36
+ end
37
+
38
+ if opts[:validation_msg]
39
+ define_method("#{prop}_validation_msg") do
40
+ opts[:validation_msg]
41
+ end
42
+ end
43
+
44
+ end
45
+
46
+ def self.properties
47
+ # Ensure that classes that don't define their own properties still function as CouchDB objects
48
+ @properties ||= [:_id, :_rev]
49
+ end
50
+
51
+ def properties
52
+ self.class.properties
53
+ end
54
+
55
+ # Specifying these properties here (after property method has been defined)
56
+ # is kinda ugly. Consider a better solution.
57
+ property :_id
58
+ property :_rev
59
+
60
+ def initialize(hash={})
61
+ # The default _id will be overwritten if loaded from CouchDB
62
+ self._id = UuidGenerator.uuid
63
+
64
+ @errors = Errors.new
65
+
66
+ # Set default properties if this object has not known CouchDB
67
+ unless hash["_rev"]
68
+ properties.each do |prop|
69
+ if methods.include?("set_default_#{prop}")
70
+ send("set_default_#{prop}")
71
+ end
72
+ end
73
+ end
74
+
75
+ set_attributes(hash)
76
+ end
77
+
78
+ def set_attributes(data)
79
+ data.each do |key, val|
80
+ # Only set instance variables on creation - object references are resolved on demand
81
+
82
+ # If the variable name ends in _at try to convert it to a Time
83
+ if key =~ /_at$/
84
+ val = Time.local(*ParseDate.parsedate(val)) rescue val
85
+ end
86
+
87
+ # Ignore param keys that don't have a corresponding writer
88
+ # This allows us to comfortably accept a hash containing superflous data
89
+ # such as a params hash in a controller
90
+ if methods.include? "#{key}="
91
+ send("#{key}=".to_sym, val)
92
+ end
93
+
94
+ end
95
+ end
96
+
97
+ def inspect
98
+ s = "#<#{self.class}:#{self.object_id}"
99
+ properties.each do |prop|
100
+ prop_val = instance_variable_get("@#{prop}".to_sym)
101
+ s << ", #{prop}: #{prop_val.inspect}" if prop_val
102
+ end
103
+ self.class.belongs_to_rels.each do |relationship|
104
+ id = instance_variable_get("@#{relationship}_id".to_sym)
105
+ s << ", #{relationship}_id: #{id}" if id
106
+ end
107
+ s << ">"
108
+ end
109
+
110
+ def to_json
111
+ data = {}
112
+ self.class.belongs_to_rels.each do |relationship, opts|
113
+ id = instance_variable_get("@#{relationship}_id".to_sym)
114
+ data["#{relationship}_id"] = id if id
115
+ if opts[:denormalise]
116
+ add_denormalised_data(data, relationship, opts)
117
+ end
118
+ end
119
+ properties.each do |prop|
120
+ prop_val = instance_variable_get("@#{prop}".to_sym)
121
+ data["#{prop}"] = prop_val if prop_val
122
+ end
123
+ data["class"] = self.class.name
124
+ data.to_json
125
+ end
126
+
127
+ # quick n' dirty denormalisation - explicit denormalisation will probably become a
128
+ # permanent fixture of RelaxDB, but quite likely in a different form to this one
129
+ def add_denormalised_data(data, relationship, opts)
130
+ obj = send(relationship)
131
+ if obj
132
+ opts[:denormalise].each do |prop_name|
133
+ val = obj.send(prop_name)
134
+ data["#{relationship}_#{prop_name}"] = val
135
+ end
136
+ end
137
+ end
138
+
139
+ # Order changed as of 30/10/2008 to be consistent with ActiveRecord
140
+ # Not yet sure of final implemention for hooks - may lean more towards DM than AR
141
+ def save
142
+ return false unless validates?
143
+ return false unless before_save
144
+
145
+ set_created_at if unsaved?
146
+
147
+ resp = RelaxDB.db.put("#{_id}", to_json)
148
+ self._rev = JSON.parse(resp.body)["rev"]
149
+
150
+ after_save
151
+
152
+ self
153
+ end
154
+
155
+ def validates?
156
+ total_success = true
157
+ properties.each do |prop|
158
+ if methods.include? "validate_#{prop}"
159
+ prop_val = instance_variable_get("@#{prop}")
160
+ success = send("validate_#{prop}", prop_val) rescue false
161
+ unless success
162
+ if methods.include? "#{prop}_validation_msg"
163
+ @errors["#{prop}".to_sym] = send("#{prop}_validation_msg")
164
+ end
165
+ end
166
+ total_success &= success
167
+ end
168
+ end
169
+ total_success &= validate
170
+ total_success
171
+ end
172
+
173
+ def validate
174
+ true
175
+ end
176
+
177
+ # Hmm. Rename... never_saved? newnew?
178
+ def unsaved?
179
+ @_rev.nil?
180
+ end
181
+ alias_method :new_record?, :unsaved?
182
+ alias_method :new_document?, :unsaved?
183
+
184
+ def to_param
185
+ self._id
186
+ end
187
+ alias_method :id, :to_param
188
+
189
+ def set_created_at
190
+ if methods.include? "created_at"
191
+ # Don't override it if it's already been set
192
+ @created_at = Time.now if @created_at.nil?
193
+ end
194
+ end
195
+
196
+ def create_or_get_proxy(klass, relationship, opts=nil)
197
+ proxy_sym = "@proxy_#{relationship}".to_sym
198
+ proxy = instance_variable_get(proxy_sym)
199
+ unless proxy
200
+ proxy = opts ? klass.new(self, relationship, opts) : klass.new(self, relationship)
201
+ end
202
+ instance_variable_set(proxy_sym, proxy)
203
+ proxy
204
+ end
205
+
206
+ # Returns true if CouchDB considers other to be the same as self
207
+ def ==(other)
208
+ other && _id == other._id
209
+ end
210
+
211
+ # Deprecated. This method was experimental and will be removed
212
+ # once multi key GETs are available in CouchDB.
213
+ def self.references_many(relationship, opts={})
214
+ # Treat the representation as a standard property
215
+ properties << relationship
216
+
217
+ # Keep track of the relationship so peers can be disassociated on destroy
218
+ @references_many_rels ||= []
219
+ @references_many_rels << relationship
220
+
221
+ define_method(relationship) do
222
+ array_sym = "@#{relationship}".to_sym
223
+ instance_variable_set(array_sym, []) unless instance_variable_defined? array_sym
224
+
225
+ create_or_get_proxy(RelaxDB::ReferencesManyProxy, relationship, opts)
226
+ end
227
+
228
+ define_method("#{relationship}=") do |val|
229
+ # Sharp edge - do not invoke this method
230
+ instance_variable_set("@#{relationship}".to_sym, val)
231
+ end
232
+ end
233
+
234
+ def self.references_many_rels
235
+ # Don't force clients to check its instantiated
236
+ @references_many_rels ||= []
237
+ end
238
+
239
+ def self.has_many(relationship, opts={})
240
+ @has_many_rels ||= []
241
+ @has_many_rels << relationship
242
+
243
+ define_method(relationship) do
244
+ create_or_get_proxy(HasManyProxy, relationship, opts)
245
+ end
246
+
247
+ define_method("#{relationship}=") do
248
+ raise "You may not currently assign to a has_many relationship - may be implemented"
249
+ end
250
+ end
251
+
252
+ def self.has_many_rels
253
+ # Don't force clients to check its instantiated
254
+ @has_many_rels ||= []
255
+ end
256
+
257
+ def self.has_one(relationship)
258
+ @has_one_rels ||= []
259
+ @has_one_rels << relationship
260
+
261
+ define_method(relationship) do
262
+ create_or_get_proxy(HasOneProxy, relationship).target
263
+ end
264
+
265
+ define_method("#{relationship}=") do |new_target|
266
+ create_or_get_proxy(HasOneProxy, relationship).target = new_target
267
+ end
268
+ end
269
+
270
+ def self.has_one_rels
271
+ @has_one_rels ||= []
272
+ end
273
+
274
+ def self.belongs_to(relationship, opts={})
275
+ @belongs_to_rels ||= {}
276
+ @belongs_to_rels[relationship] = opts
277
+
278
+ define_method(relationship) do
279
+ create_or_get_proxy(BelongsToProxy, relationship).target
280
+ end
281
+
282
+ define_method("#{relationship}=") do |new_target|
283
+ create_or_get_proxy(BelongsToProxy, relationship).target = new_target
284
+ end
285
+
286
+ # Allows all writers to be invoked from the hash passed to initialize
287
+ define_method("#{relationship}_id=") do |id|
288
+ instance_variable_set("@#{relationship}_id".to_sym, id)
289
+ end
290
+
291
+ # Allows belongs_to relationships to be used by the paginator
292
+ define_method("#{relationship}_id") do
293
+ instance_variable_get("@#{relationship}_id")
294
+ end
295
+
296
+ end
297
+
298
+ def self.belongs_to_rels
299
+ # Don't force clients to check that it's instantiated
300
+ @belongs_to_rels ||= {}
301
+ end
302
+
303
+ def self.all_relationships
304
+ belongs_to_rels + has_one_rels + has_many_rels + references_many_rels
305
+ end
306
+
307
+ def self.all
308
+ @all_delegator ||= AllDelegator.new(self.name)
309
+ end
310
+
311
+ # destroy! nullifies all relationships with peers and children before deleting
312
+ # itself in CouchDB
313
+ # The nullification and deletion are not performed in a transaction
314
+ def destroy!
315
+ self.class.references_many_rels.each do |rel|
316
+ send(rel).clear
317
+ end
318
+
319
+ self.class.has_many_rels.each do |rel|
320
+ send(rel).clear
321
+ end
322
+
323
+ self.class.has_one_rels.each do |rel|
324
+ send("#{rel}=".to_sym, nil)
325
+ end
326
+
327
+ # Implicitly prevent the object from being resaved by failing to update its revision
328
+ RelaxDB.db.delete("#{_id}?rev=#{_rev}")
329
+ self
330
+ end
331
+
332
+ #
333
+ # Callbacks - define these in a module and mix'em'in ?
334
+ #
335
+ def self.before_save(callback)
336
+ before_save_callbacks << callback
337
+ end
338
+
339
+ def self.before_save_callbacks
340
+ @before_save ||= []
341
+ end
342
+
343
+ def before_save
344
+ self.class.before_save_callbacks.each do |callback|
345
+ resp = callback.is_a?(Proc) ? callback.call(self) : send(callback)
346
+ return false unless resp
347
+ end
348
+ end
349
+
350
+ def self.after_save(callback)
351
+ after_save_callbacks << callback
352
+ end
353
+
354
+ def self.after_save_callbacks
355
+ @after_save_callbacks ||= []
356
+ end
357
+
358
+ def after_save
359
+ self.class.after_save_callbacks.each do |callback|
360
+ callback.is_a?(Proc) ? callback.call(self) : send(callback)
361
+ end
362
+ end
363
+
364
+ def self.paginate_by(page_params, *view_keys)
365
+ paginate_params = PaginateParams.new
366
+ yield paginate_params
367
+ raise paginate_params.error_msg if paginate_params.invalid?
368
+
369
+ paginator = Paginator.new(paginate_params, page_params)
370
+
371
+ design_doc_name = self.name
372
+ view = SortedByView.new(design_doc_name, *view_keys)
373
+ query = Query.new(design_doc_name, view.view_name)
374
+ query.merge(paginate_params)
375
+
376
+ docs = view.query(query)
377
+ docs.reverse! if paginate_params.order_inverted?
378
+
379
+ paginator.add_next_and_prev(docs, design_doc_name, view.view_name, view_keys)
380
+
381
+ docs
382
+ end
383
+
384
+ end
385
+
386
+ end