yaanno-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
data/lib/relaxdb.rb ADDED
@@ -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,377 @@
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
+ def save
140
+ return false unless before_save
141
+ return false unless validates?
142
+
143
+ set_created_at if unsaved?
144
+
145
+ resp = RelaxDB.db.put("#{_id}", to_json)
146
+ self._rev = JSON.parse(resp.body)["rev"]
147
+
148
+ after_save
149
+
150
+ self
151
+ end
152
+
153
+ def validates?
154
+ success = true
155
+ properties.each do |prop|
156
+ if methods.include? "validate_#{prop}"
157
+ prop_val = instance_variable_get("@#{prop}")
158
+ success = send("validate_#{prop}", prop_val) rescue false
159
+ unless success
160
+ if methods.include? "#{prop}_validation_msg"
161
+ @errors["#{prop}".to_sym] = send("#{prop}_validation_msg")
162
+ end
163
+ end
164
+ end
165
+ end
166
+ success
167
+ end
168
+
169
+ # Hmm. Rename... never_saved? newnew?
170
+ def unsaved?
171
+ @_rev.nil?
172
+ end
173
+ alias_method :new_record?, :unsaved?
174
+
175
+ def to_param
176
+ self._id
177
+ end
178
+ alias_method :id, :to_param
179
+
180
+ def set_created_at
181
+ if methods.include? "created_at"
182
+ # Don't override it if it's already been set
183
+ @created_at = Time.now if @created_at.nil?
184
+ end
185
+ end
186
+
187
+ def create_or_get_proxy(klass, relationship, opts=nil)
188
+ proxy_sym = "@proxy_#{relationship}".to_sym
189
+ proxy = instance_variable_get(proxy_sym)
190
+ unless proxy
191
+ proxy = opts ? klass.new(self, relationship, opts) : klass.new(self, relationship)
192
+ end
193
+ instance_variable_set(proxy_sym, proxy)
194
+ proxy
195
+ end
196
+
197
+ # Returns true if CouchDB considers other to be the same as self
198
+ def ==(other)
199
+ other && _id == other._id
200
+ end
201
+
202
+ # Deprecated. This method was experimental and will be removed
203
+ # once multi key GETs are available in CouchDB.
204
+ def self.references_many(relationship, opts={})
205
+ # Treat the representation as a standard property
206
+ properties << relationship
207
+
208
+ # Keep track of the relationship so peers can be disassociated on destroy
209
+ @references_many_rels ||= []
210
+ @references_many_rels << relationship
211
+
212
+ define_method(relationship) do
213
+ array_sym = "@#{relationship}".to_sym
214
+ instance_variable_set(array_sym, []) unless instance_variable_defined? array_sym
215
+
216
+ create_or_get_proxy(RelaxDB::ReferencesManyProxy, relationship, opts)
217
+ end
218
+
219
+ define_method("#{relationship}=") do |val|
220
+ # Sharp edge - do not invoke this method
221
+ instance_variable_set("@#{relationship}".to_sym, val)
222
+ end
223
+ end
224
+
225
+ def self.references_many_rels
226
+ # Don't force clients to check its instantiated
227
+ @references_many_rels ||= []
228
+ end
229
+
230
+ def self.has_many(relationship, opts={})
231
+ @has_many_rels ||= []
232
+ @has_many_rels << relationship
233
+
234
+ define_method(relationship) do
235
+ create_or_get_proxy(HasManyProxy, relationship, opts)
236
+ end
237
+
238
+ define_method("#{relationship}=") do
239
+ raise "You may not currently assign to a has_many relationship - may be implemented"
240
+ end
241
+ end
242
+
243
+ def self.has_many_rels
244
+ # Don't force clients to check its instantiated
245
+ @has_many_rels ||= []
246
+ end
247
+
248
+ def self.has_one(relationship)
249
+ @has_one_rels ||= []
250
+ @has_one_rels << relationship
251
+
252
+ define_method(relationship) do
253
+ create_or_get_proxy(HasOneProxy, relationship).target
254
+ end
255
+
256
+ define_method("#{relationship}=") do |new_target|
257
+ create_or_get_proxy(HasOneProxy, relationship).target = new_target
258
+ end
259
+ end
260
+
261
+ def self.has_one_rels
262
+ @has_one_rels ||= []
263
+ end
264
+
265
+ def self.belongs_to(relationship, opts={})
266
+ @belongs_to_rels ||= {}
267
+ @belongs_to_rels[relationship] = opts
268
+
269
+ define_method(relationship) do
270
+ create_or_get_proxy(BelongsToProxy, relationship).target
271
+ end
272
+
273
+ define_method("#{relationship}=") do |new_target|
274
+ create_or_get_proxy(BelongsToProxy, relationship).target = new_target
275
+ end
276
+
277
+ # Allows all writers to be invoked from the hash passed to initialize
278
+ define_method("#{relationship}_id=") do |id|
279
+ instance_variable_set("@#{relationship}_id".to_sym, id)
280
+ end
281
+
282
+ # Allows belongs_to relationships to be used by the paginator
283
+ define_method("#{relationship}_id") do
284
+ instance_variable_get("@#{relationship}_id")
285
+ end
286
+
287
+ end
288
+
289
+ def self.belongs_to_rels
290
+ # Don't force clients to check that it's instantiated
291
+ @belongs_to_rels ||= {}
292
+ end
293
+
294
+ def self.all_relationships
295
+ belongs_to_rels + has_one_rels + has_many_rels + references_many_rels
296
+ end
297
+
298
+ def self.all
299
+ @all_delegator ||= AllDelegator.new(self.name)
300
+ end
301
+
302
+ # destroy! nullifies all relationships with peers and children before deleting
303
+ # itself in CouchDB
304
+ # The nullification and deletion are not performed in a transaction
305
+ def destroy!
306
+ self.class.references_many_rels.each do |rel|
307
+ send(rel).clear
308
+ end
309
+
310
+ self.class.has_many_rels.each do |rel|
311
+ send(rel).clear
312
+ end
313
+
314
+ self.class.has_one_rels.each do |rel|
315
+ send("#{rel}=".to_sym, nil)
316
+ end
317
+
318
+ # Implicitly prevent the object from being resaved by failing to update its revision
319
+ RelaxDB.db.delete("#{_id}?rev=#{_rev}")
320
+ self
321
+ end
322
+
323
+ #
324
+ # Callbacks - define these in a module and mix'em'in ?
325
+ #
326
+ def self.before_save(callback)
327
+ before_save_callbacks << callback
328
+ end
329
+
330
+ def self.before_save_callbacks
331
+ @before_save ||= []
332
+ end
333
+
334
+ def before_save
335
+ self.class.before_save_callbacks.each do |callback|
336
+ resp = callback.is_a?(Proc) ? callback.call(self) : send(callback)
337
+ return false unless resp
338
+ end
339
+ end
340
+
341
+ def self.after_save(callback)
342
+ after_save_callbacks << callback
343
+ end
344
+
345
+ def self.after_save_callbacks
346
+ @after_save_callbacks ||= []
347
+ end
348
+
349
+ def after_save
350
+ self.class.after_save_callbacks.each do |callback|
351
+ callback.is_a?(Proc) ? callback.call(self) : send(callback)
352
+ end
353
+ end
354
+
355
+ def self.paginate_by(page_params, *view_keys)
356
+ paginate_params = PaginateParams.new
357
+ yield paginate_params
358
+ raise paginate_params.error_msg if paginate_params.invalid?
359
+
360
+ paginator = Paginator.new(paginate_params, page_params)
361
+
362
+ design_doc_name = self.name
363
+ view = SortedByView.new(design_doc_name, *view_keys)
364
+ query = Query.new(design_doc_name, view.view_name)
365
+ query.merge(paginate_params)
366
+
367
+ docs = view.query(query)
368
+ docs.reverse! if paginate_params.order_inverted?
369
+
370
+ paginator.add_next_and_prev(docs, design_doc_name, view.view_name, view_keys)
371
+
372
+ docs
373
+ end
374
+
375
+ end
376
+
377
+ end