cohitre-relaxdb 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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