paulcarey-relaxdb 0.1.0
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.
- data/LICENSE +20 -0
- data/README.textile +98 -0
- data/Rakefile +52 -0
- data/docs/spec_results.html +602 -0
- data/lib/relaxdb/all_delegator.rb +48 -0
- data/lib/relaxdb/belongs_to_proxy.rb +29 -0
- data/lib/relaxdb/design_doc.rb +50 -0
- data/lib/relaxdb/document.rb +298 -0
- data/lib/relaxdb/has_many_proxy.rb +81 -0
- data/lib/relaxdb/has_one_proxy.rb +45 -0
- data/lib/relaxdb/query.rb +48 -0
- data/lib/relaxdb/references_many_proxy.rb +99 -0
- data/lib/relaxdb/relaxdb.rb +106 -0
- data/lib/relaxdb/server.rb +112 -0
- data/lib/relaxdb/sorted_by_view.rb +42 -0
- data/lib/relaxdb/uuid_generator.rb +21 -0
- data/lib/relaxdb/view_object.rb +34 -0
- data/lib/relaxdb/view_uploader.rb +47 -0
- data/lib/relaxdb/views.rb +42 -0
- data/lib/relaxdb.rb +33 -0
- data/spec/belongs_to_spec.rb +80 -0
- data/spec/design_doc_spec.rb +34 -0
- data/spec/document_spec.rb +301 -0
- data/spec/has_many_spec.rb +139 -0
- data/spec/has_one_spec.rb +121 -0
- data/spec/query_spec.rb +46 -0
- data/spec/references_many_spec.rb +141 -0
- data/spec/relaxdb_spec.rb +50 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/spec_models.rb +104 -0
- data/spec/view_object_spec.rb +47 -0
- metadata +111 -0
@@ -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("#{@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("_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("#{@data["_id"]}?rev=#{@data["_rev"]}")
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,298 @@
|
|
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 = {}
|
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|
|
113
|
+
id = instance_variable_get("@#{relationship}_id".to_sym)
|
114
|
+
data["#{relationship}_id"] = id if id
|
115
|
+
end
|
116
|
+
properties.each do |prop|
|
117
|
+
prop_val = instance_variable_get("@#{prop}".to_sym)
|
118
|
+
data["#{prop}"] = prop_val if prop_val
|
119
|
+
end
|
120
|
+
data["class"] = self.class.name
|
121
|
+
data.to_json
|
122
|
+
end
|
123
|
+
|
124
|
+
def save
|
125
|
+
set_created_at_if_new
|
126
|
+
|
127
|
+
if validates?
|
128
|
+
resp = RelaxDB.db.put("#{_id}", to_json)
|
129
|
+
self._rev = JSON.parse(resp.body)["rev"]
|
130
|
+
self
|
131
|
+
else
|
132
|
+
false
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def validates?
|
137
|
+
success = true
|
138
|
+
properties.each do |prop|
|
139
|
+
if methods.include? "validate_#{prop}"
|
140
|
+
prop_val = instance_variable_get("@#{prop}")
|
141
|
+
unless send("validate_#{prop}", prop_val)
|
142
|
+
success = false
|
143
|
+
if methods.include? "#{prop}_validation_msg"
|
144
|
+
@errors["#{prop}".to_sym] = send("#{prop}_validation_msg")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
success
|
150
|
+
end
|
151
|
+
|
152
|
+
def unsaved?
|
153
|
+
instance_variable_get(:@_rev).nil?
|
154
|
+
end
|
155
|
+
|
156
|
+
def set_created_at_if_new
|
157
|
+
if unsaved? and methods.include? "created_at"
|
158
|
+
# Don't override it if it's already been set
|
159
|
+
unless instance_variable_get(:@created_at)
|
160
|
+
instance_variable_set(:@created_at, Time.now)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def create_or_get_proxy(klass, relationship, opts=nil)
|
166
|
+
proxy_sym = "@proxy_#{relationship}".to_sym
|
167
|
+
proxy = instance_variable_get(proxy_sym)
|
168
|
+
unless proxy
|
169
|
+
proxy = opts ? klass.new(self, relationship, opts) : klass.new(self, relationship)
|
170
|
+
end
|
171
|
+
instance_variable_set(proxy_sym, proxy)
|
172
|
+
proxy
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns true if CouchDB considers other to be the same as self
|
176
|
+
def ==(other)
|
177
|
+
other && _id == other._id
|
178
|
+
end
|
179
|
+
|
180
|
+
# Deprecated. This method was experimental and will be removed
|
181
|
+
# once multi key GETs are available in CouchDB.
|
182
|
+
def self.references_many(relationship, opts={})
|
183
|
+
# Treat the representation as a standard property
|
184
|
+
properties << relationship
|
185
|
+
|
186
|
+
# Keep track of the relationship so peers can be disassociated on destroy
|
187
|
+
@references_many_rels ||= []
|
188
|
+
@references_many_rels << relationship
|
189
|
+
|
190
|
+
define_method(relationship) do
|
191
|
+
array_sym = "@#{relationship}".to_sym
|
192
|
+
instance_variable_set(array_sym, []) unless instance_variable_defined? array_sym
|
193
|
+
|
194
|
+
create_or_get_proxy(RelaxDB::ReferencesManyProxy, relationship, opts)
|
195
|
+
end
|
196
|
+
|
197
|
+
define_method("#{relationship}=") do |val|
|
198
|
+
# Sharp edge - do not invoke this method
|
199
|
+
instance_variable_set("@#{relationship}".to_sym, val)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def self.references_many_rels
|
204
|
+
# Don't force clients to check its instantiated
|
205
|
+
@references_many_rels ||= []
|
206
|
+
end
|
207
|
+
|
208
|
+
def self.has_many(relationship, opts={})
|
209
|
+
@has_many_rels ||= []
|
210
|
+
@has_many_rels << relationship
|
211
|
+
|
212
|
+
define_method(relationship) do
|
213
|
+
create_or_get_proxy(HasManyProxy, relationship, opts)
|
214
|
+
end
|
215
|
+
|
216
|
+
define_method("#{relationship}=") do
|
217
|
+
raise "You may not currently assign to a has_many relationship - may be implemented"
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.has_many_rels
|
222
|
+
# Don't force clients to check its instantiated
|
223
|
+
@has_many_rels ||= []
|
224
|
+
end
|
225
|
+
|
226
|
+
def self.has_one(relationship)
|
227
|
+
@has_one_rels ||= []
|
228
|
+
@has_one_rels << relationship
|
229
|
+
|
230
|
+
define_method(relationship) do
|
231
|
+
create_or_get_proxy(HasOneProxy, relationship).target
|
232
|
+
end
|
233
|
+
|
234
|
+
define_method("#{relationship}=") do |new_target|
|
235
|
+
create_or_get_proxy(HasOneProxy, relationship).target = new_target
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def self.has_one_rels
|
240
|
+
@has_one_rels ||= []
|
241
|
+
end
|
242
|
+
|
243
|
+
def self.belongs_to(relationship)
|
244
|
+
@belongs_to_rels ||= []
|
245
|
+
@belongs_to_rels << relationship
|
246
|
+
|
247
|
+
define_method(relationship) do
|
248
|
+
create_or_get_proxy(BelongsToProxy, relationship).target
|
249
|
+
end
|
250
|
+
|
251
|
+
define_method("#{relationship}=") do |new_target|
|
252
|
+
create_or_get_proxy(BelongsToProxy, relationship).target = new_target
|
253
|
+
end
|
254
|
+
|
255
|
+
# Allows all writers to be invoked from the hash passed to initialize
|
256
|
+
define_method("#{relationship}_id=") do |id|
|
257
|
+
instance_variable_set("@#{relationship}_id".to_sym, id)
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
def self.belongs_to_rels
|
263
|
+
# Don't force clients to check that it's instantiated
|
264
|
+
@belongs_to_rels ||= []
|
265
|
+
end
|
266
|
+
|
267
|
+
def self.all_relationships
|
268
|
+
belongs_to_rels + has_one_rels + has_many_rels + references_many_rels
|
269
|
+
end
|
270
|
+
|
271
|
+
def self.all
|
272
|
+
@all_delegator ||= AllDelegator.new(self)
|
273
|
+
end
|
274
|
+
|
275
|
+
# destroy! nullifies all relationships with peers and children before deleting
|
276
|
+
# itself in CouchDB
|
277
|
+
# The nullification and deletion are not performed in a transaction
|
278
|
+
def destroy!
|
279
|
+
self.class.references_many_rels.each do |rel|
|
280
|
+
send(rel).clear
|
281
|
+
end
|
282
|
+
|
283
|
+
self.class.has_many_rels.each do |rel|
|
284
|
+
send(rel).clear
|
285
|
+
end
|
286
|
+
|
287
|
+
self.class.has_one_rels.each do |rel|
|
288
|
+
send("#{rel}=".to_sym, nil)
|
289
|
+
end
|
290
|
+
|
291
|
+
# Implicitly prevent the object from being resaved by failing to update its revision
|
292
|
+
RelaxDB.db.delete("#{_id}?rev=#{_rev}")
|
293
|
+
self
|
294
|
+
end
|
295
|
+
|
296
|
+
end
|
297
|
+
|
298
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class HasManyProxy
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(client, relationship, opts)
|
8
|
+
@client = client
|
9
|
+
@relationship = relationship
|
10
|
+
@opts = opts
|
11
|
+
|
12
|
+
@target_class = opts[:class]
|
13
|
+
@relationship_as_viewed_by_target = (opts[:known_as] || client.class.name.downcase).to_s
|
14
|
+
|
15
|
+
@children = load_children
|
16
|
+
end
|
17
|
+
|
18
|
+
def <<(obj)
|
19
|
+
return false unless obj.validates?
|
20
|
+
return false if @children.include?(obj)
|
21
|
+
|
22
|
+
obj.send("#{@relationship_as_viewed_by_target}=".to_sym, @client)
|
23
|
+
obj.save
|
24
|
+
@children << obj
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def clear
|
29
|
+
@children.each do |c|
|
30
|
+
break_back_link c
|
31
|
+
end
|
32
|
+
@children.clear
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(obj)
|
36
|
+
obj = @children.delete(obj)
|
37
|
+
break_back_link(obj) if obj
|
38
|
+
end
|
39
|
+
|
40
|
+
def break_back_link(obj)
|
41
|
+
if obj
|
42
|
+
obj.send("#{@relationship_as_viewed_by_target}=".to_sym, nil)
|
43
|
+
obj.save
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def empty?
|
48
|
+
@children.empty?
|
49
|
+
end
|
50
|
+
|
51
|
+
def size
|
52
|
+
@children.size
|
53
|
+
end
|
54
|
+
|
55
|
+
def [](*args)
|
56
|
+
@children[*args]
|
57
|
+
end
|
58
|
+
|
59
|
+
def each(&blk)
|
60
|
+
@children.each(&blk)
|
61
|
+
end
|
62
|
+
|
63
|
+
def reload
|
64
|
+
@children = load_children
|
65
|
+
end
|
66
|
+
|
67
|
+
def load_children
|
68
|
+
view_path = "_view/#{@client.class}/#{@relationship}?key=\"#{@client._id}\""
|
69
|
+
design_doc = @client.class
|
70
|
+
view_name = @relationship
|
71
|
+
map_function = ViewCreator.has_n(@target_class, @relationship_as_viewed_by_target)
|
72
|
+
@children = RelaxDB.retrieve(view_path, design_doc, view_name, map_function)
|
73
|
+
end
|
74
|
+
|
75
|
+
def inspect
|
76
|
+
@children.inspect
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class HasOneProxy
|
4
|
+
|
5
|
+
def initialize(client, relationship)
|
6
|
+
@client = client
|
7
|
+
@relationship = relationship
|
8
|
+
@target_class = @relationship.to_s.capitalize
|
9
|
+
@relationship_as_viewed_by_target = client.class.to_s.downcase
|
10
|
+
|
11
|
+
@target = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def target
|
15
|
+
return @target if @target
|
16
|
+
@target = load_target
|
17
|
+
end
|
18
|
+
|
19
|
+
# All database changes performed by this method would ideally be done in a transaction
|
20
|
+
def target=(new_target)
|
21
|
+
# Nullify any existing relationship on assignment
|
22
|
+
old_target = target
|
23
|
+
if old_target
|
24
|
+
old_target.send("#{@relationship_as_viewed_by_target}=".to_sym, nil)
|
25
|
+
old_target.save
|
26
|
+
end
|
27
|
+
|
28
|
+
@target = new_target
|
29
|
+
unless @target.nil?
|
30
|
+
@target.send("#{@relationship_as_viewed_by_target}=".to_sym, @client)
|
31
|
+
@target.save
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_target
|
36
|
+
design_doc = @client.class
|
37
|
+
view_name = @relationship
|
38
|
+
view_path = "_view/#{design_doc}/#{view_name}?key=\"#{@client._id}\""
|
39
|
+
map_function = ViewCreator.has_n(@target_class, @relationship_as_viewed_by_target)
|
40
|
+
RelaxDB.retrieve(view_path, design_doc, view_name, map_function)[0]
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
# A Query is used to build the query string made against a view
|
4
|
+
# All parameter values are first JSON encoded and then URL encoded
|
5
|
+
# Nil values are set to the empty string
|
6
|
+
# All parameter calls return self so calls may be chained => q.startkey("foo").endkey("bar").count(2)
|
7
|
+
|
8
|
+
#
|
9
|
+
# The query object is currently inconsistent with the RelaxDB object idiom. Consider
|
10
|
+
# paul = User.new(:name => "paul").save; Event.new(:host=>paul).save
|
11
|
+
# but an event query requires
|
12
|
+
# Event.all.sorted_by(:host_id) { |q| q.key(paul._id) }
|
13
|
+
# rather than
|
14
|
+
# Event.all.sorted_by(:host) { |q| q.key(paul) }
|
15
|
+
# I feel that both forms should be supported
|
16
|
+
#
|
17
|
+
class Query
|
18
|
+
|
19
|
+
@@params = %w(key startkey startkey_docid endkey endkey_docid count update descending skip group)
|
20
|
+
|
21
|
+
@@params.each do |param|
|
22
|
+
define_method(param.to_sym) do |val|
|
23
|
+
val ||= ""
|
24
|
+
instance_variable_set("@#{param}".to_sym, val)
|
25
|
+
self
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(design_doc, view_name)
|
30
|
+
@design_doc = design_doc
|
31
|
+
@view_name = view_name
|
32
|
+
end
|
33
|
+
|
34
|
+
def view_path
|
35
|
+
uri = "_view/#{@design_doc}/#{@view_name}"
|
36
|
+
|
37
|
+
query = ""
|
38
|
+
@@params.each do |param|
|
39
|
+
val = instance_variable_get("@#{param}")
|
40
|
+
query << "&#{param}=#{::CGI::escape(val.to_json)}" if val
|
41
|
+
end
|
42
|
+
|
43
|
+
uri << query.sub(/^&/, "?")
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module RelaxDB
|
2
|
+
|
3
|
+
class ReferencesManyProxy
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(client, relationship, opts)
|
8
|
+
@client = client
|
9
|
+
@relationship = relationship
|
10
|
+
|
11
|
+
@target_class = opts[:class]
|
12
|
+
@relationship_as_viewed_by_target = opts[:known_as].to_s
|
13
|
+
end
|
14
|
+
|
15
|
+
def <<(obj, reciprocal_invocation=false)
|
16
|
+
return false if peer_ids.include? obj._id
|
17
|
+
|
18
|
+
@peers << obj if @peers
|
19
|
+
peer_ids << obj._id
|
20
|
+
|
21
|
+
unless reciprocal_invocation
|
22
|
+
# Set the other side of the relationship, ensuring this method isn't called again
|
23
|
+
obj.send(@relationship_as_viewed_by_target).send(:<<, @client, true)
|
24
|
+
|
25
|
+
# Bulk save to ensure relationship is persisted on both sides
|
26
|
+
RelaxDB.bulk_save(@client, obj)
|
27
|
+
end
|
28
|
+
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
resolve
|
34
|
+
@peers.each do |peer|
|
35
|
+
peer.send(@relationship_as_viewed_by_target).send(:delete_from_self, @client)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Important to resolve in the database before in memory, although an examination of the
|
39
|
+
# contents of the bulk_save will look wrong as this object will still list all its peers
|
40
|
+
RelaxDB.bulk_save(@client, *@peers)
|
41
|
+
|
42
|
+
peer_ids.clear
|
43
|
+
@peers.clear
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete(obj)
|
47
|
+
deleted = obj.send(@relationship_as_viewed_by_target).send(:delete_from_self, @client)
|
48
|
+
if deleted
|
49
|
+
delete_from_self(obj)
|
50
|
+
RelaxDB.bulk_save(@client, obj)
|
51
|
+
end
|
52
|
+
deleted
|
53
|
+
end
|
54
|
+
|
55
|
+
def delete_from_self(obj)
|
56
|
+
@peers.delete(obj) if @peers
|
57
|
+
peer_ids.delete(obj._id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def empty?
|
61
|
+
peer_ids.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def size
|
65
|
+
peer_ids.size
|
66
|
+
end
|
67
|
+
|
68
|
+
def [](*args)
|
69
|
+
resolve
|
70
|
+
@peers[*args]
|
71
|
+
end
|
72
|
+
|
73
|
+
def each(&blk)
|
74
|
+
resolve
|
75
|
+
@peers.each(&blk)
|
76
|
+
end
|
77
|
+
|
78
|
+
def inspect
|
79
|
+
@client.instance_variable_get("@#{@relationship}".to_sym).inspect
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def peer_ids
|
85
|
+
@client.instance_variable_get("@#{@relationship}".to_sym)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Resolves the actual ids into real objects via a single GET to CouchDB. Called internally by each
|
89
|
+
def resolve
|
90
|
+
design_doc = @client.class
|
91
|
+
view_name = @relationship
|
92
|
+
view_path = "_view/#{design_doc}/#{view_name}?key=\"#{@client._id}\""
|
93
|
+
map_function = ViewCreator.has_many_through(@target_class, @relationship_as_viewed_by_target)
|
94
|
+
@peers = RelaxDB.retrieve(view_path, design_doc, view_name, map_function)
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|