rocking_chair 0.2.5 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/rocking_chair/view.rb +39 -1
- data/test/fixtures/simply_stored_fixtures.rb +24 -0
- data/test/simply_stored_test.rb +107 -0
- data/test/view_test.rb +81 -3
- metadata +5 -5
data/lib/rocking_chair/view.rb
CHANGED
@@ -59,6 +59,8 @@ module RockingChair
|
|
59
59
|
find_by_attribute(match[1])
|
60
60
|
elsif match = view_name.match(/\Aassociation_#{design_document_name}_belongs_to_(\w+)\Z/)
|
61
61
|
find_belongs_to(match[1])
|
62
|
+
elsif match = view_name.match(/\Aassociation_#{design_document_name}_has_and_belongs_to_many_(\w+)\Z/)
|
63
|
+
find_has_and_belongs_to_many(match[1])
|
62
64
|
else
|
63
65
|
raise "Unknown View implementation for view #{view_name.inspect} in design document _design/#{design_document_name}"
|
64
66
|
end
|
@@ -126,6 +128,17 @@ module RockingChair
|
|
126
128
|
filter_deleted_items if options['without_deleted'].to_s == 'true'
|
127
129
|
sort_by_attribute('created_at')
|
128
130
|
end
|
131
|
+
|
132
|
+
def find_has_and_belongs_to_many(belongs_to)
|
133
|
+
if foreign_keys_are_stored_on_my_class?(belongs_to)
|
134
|
+
filter_items_by_key_in_attribute_group(foreign_key_array_id(belongs_to))
|
135
|
+
filter_items_without_correct_ruby_class
|
136
|
+
else
|
137
|
+
filter_items_by_query_document_attributes(belongs_to)
|
138
|
+
end
|
139
|
+
filter_deleted_items if options['without_deleted'].to_s == 'true'
|
140
|
+
sort_by_attribute('created_at')
|
141
|
+
end
|
129
142
|
|
130
143
|
def find_by_attribute(attribute_string)
|
131
144
|
attributes = attribute_string.split("_and_")
|
@@ -148,7 +161,12 @@ module RockingChair
|
|
148
161
|
end
|
149
162
|
@view_name = view_name.gsub(/_withoutdeleted\Z/, '').gsub(/_without_deleted\Z/, '').gsub(/_withdeleted\Z/, '').gsub(/_with_deleted\Z/, '')
|
150
163
|
end
|
151
|
-
|
164
|
+
|
165
|
+
def foreign_keys_are_stored_on_my_class?(belongs_to)
|
166
|
+
reduce_function = @view_document['reduce']
|
167
|
+
reduce_function == "_sum"
|
168
|
+
end
|
169
|
+
|
152
170
|
def initialize_ruby_store
|
153
171
|
@ruby_store = database.storage.dup
|
154
172
|
@ruby_store.each{|doc_id, json_document| ruby_store[doc_id] = JSON.parse(json_document, :create_additions => false) }
|
@@ -188,6 +206,22 @@ module RockingChair
|
|
188
206
|
end
|
189
207
|
end
|
190
208
|
|
209
|
+
def filter_items_by_key_in_attribute_group(attribute)
|
210
|
+
filter_key = (options['key'] || options['startkey']).to_s
|
211
|
+
@keys = keys.select do |key|
|
212
|
+
document = ruby_store[key]
|
213
|
+
document_attribute = RockingChair::Helper.access(attribute, document)
|
214
|
+
document_attribute && document_attribute.is_a?(Array) && document_attribute.include?(filter_key)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def filter_items_by_query_document_attributes(belongs_to)
|
219
|
+
filter_key = options['key'] || options['startkey']
|
220
|
+
filtering_document = ruby_store[filter_key.to_s]
|
221
|
+
reverse_foreign_key = foreign_key_array_id(design_document_name)
|
222
|
+
@keys = RockingChair::Helper.access(reverse_foreign_key, filtering_document) || []
|
223
|
+
end
|
224
|
+
|
191
225
|
def filter_deleted_items
|
192
226
|
@keys = keys.delete_if do |key|
|
193
227
|
document = ruby_store[key]
|
@@ -258,6 +292,10 @@ module RockingChair
|
|
258
292
|
def foreign_key_id(name)
|
259
293
|
name.underscore.gsub('/','__').gsub('::','__') + "_id"
|
260
294
|
end
|
295
|
+
|
296
|
+
def foreign_key_array_id(name)
|
297
|
+
name.underscore.singularize.gsub('/','__').gsub('::','__') + "_ids"
|
298
|
+
end
|
261
299
|
|
262
300
|
def key_description
|
263
301
|
description = {'key' => options['key']}
|
@@ -6,6 +6,7 @@ class User
|
|
6
6
|
property :firstname
|
7
7
|
property :lastname
|
8
8
|
belongs_to :project
|
9
|
+
has_and_belongs_to_many :groups, :storing_keys => true
|
9
10
|
|
10
11
|
enable_soft_delete
|
11
12
|
|
@@ -33,4 +34,27 @@ class CustomViewUser
|
|
33
34
|
|
34
35
|
property :tags
|
35
36
|
view :by_tags, :type => SimplyStored::Couch::Views::ArrayPropertyViewSpec, :key => :tags
|
37
|
+
end
|
38
|
+
|
39
|
+
class Group
|
40
|
+
include SimplyStored::Couch
|
41
|
+
|
42
|
+
property :name
|
43
|
+
has_and_belongs_to_many :users, :storing_keys => false
|
44
|
+
end
|
45
|
+
|
46
|
+
class Server
|
47
|
+
include SimplyStored::Couch
|
48
|
+
|
49
|
+
property :hostname
|
50
|
+
|
51
|
+
has_and_belongs_to_many :networks, :storing_keys => true
|
52
|
+
end
|
53
|
+
|
54
|
+
class Network
|
55
|
+
include SimplyStored::Couch
|
56
|
+
|
57
|
+
property :klass
|
58
|
+
|
59
|
+
has_and_belongs_to_many :servers, :storing_keys => false
|
36
60
|
end
|
data/test/simply_stored_test.rb
CHANGED
@@ -279,6 +279,113 @@ class SimplyStoredTest < Test::Unit::TestCase
|
|
279
279
|
end
|
280
280
|
end
|
281
281
|
|
282
|
+
context "when handling n:m relations using has_and_belongs_to_many" do
|
283
|
+
should "work relations from both sides" do
|
284
|
+
network_a = Network.create(:klass => "A")
|
285
|
+
network_b = Network.create(:klass => "B")
|
286
|
+
3.times {
|
287
|
+
server = Server.new
|
288
|
+
server.add_network(network_a)
|
289
|
+
server.add_network(network_b)
|
290
|
+
}
|
291
|
+
assert_equal 3, network_a.servers.size
|
292
|
+
network_a.servers.each do |server|
|
293
|
+
assert_equal 2, server.networks.size
|
294
|
+
end
|
295
|
+
assert_equal 3, network_b.servers.size
|
296
|
+
network_b.servers.each do |server|
|
297
|
+
assert_equal 2, server.networks.size
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
should "work relations from both sides - regardless from where the add was called" do
|
302
|
+
network_a = Network.create(:klass => "A")
|
303
|
+
network_b = Network.create(:klass => "B")
|
304
|
+
3.times {
|
305
|
+
server = Server.new
|
306
|
+
network_a.add_server(server)
|
307
|
+
network_b.add_server(server)
|
308
|
+
}
|
309
|
+
assert_equal 3, network_a.servers.size
|
310
|
+
network_a.servers.each do |server|
|
311
|
+
assert_equal 2, server.networks.size, server.network_ids.inspect
|
312
|
+
end
|
313
|
+
assert_equal 3, network_b.servers.size
|
314
|
+
network_b.servers.each do |server|
|
315
|
+
assert_equal 2, server.networks.size
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
should "cound correctly - regardless of the side of the relation" do
|
320
|
+
network_a = Network.create(:klass => "A")
|
321
|
+
network_b = Network.create(:klass => "B")
|
322
|
+
3.times {
|
323
|
+
server = Server.new
|
324
|
+
network_a.add_server(server)
|
325
|
+
network_b.add_server(server)
|
326
|
+
}
|
327
|
+
assert_equal 3, network_a.server_count
|
328
|
+
assert_equal 3, network_b.server_count
|
329
|
+
assert_equal 2, network_a.servers.first.network_count
|
330
|
+
assert_equal 2, network_b.servers.first.network_count
|
331
|
+
end
|
332
|
+
|
333
|
+
should "support mixing order and limit" do
|
334
|
+
network_1 = Network.find(Network.create!(:klass => "A").id)
|
335
|
+
network_1.created_at = Time.local(2001)
|
336
|
+
network_1.save!
|
337
|
+
|
338
|
+
network_2 = Network.find(Network.create!(:klass => "B").id)
|
339
|
+
network_2.created_at = Time.local(2002)
|
340
|
+
network_2.save!
|
341
|
+
|
342
|
+
server_1 = Server.find(Server.create!(:hostname => 'www.example.com').id)
|
343
|
+
server_1.created_at = Time.local(2003)
|
344
|
+
network_1.add_server(server_1)
|
345
|
+
network_2.add_server(server_1)
|
346
|
+
|
347
|
+
server_2 = Server.find(Server.create!(:hostname => 'foo.com').id)
|
348
|
+
server_2.created_at = Time.local(2004)
|
349
|
+
network_1.add_server(server_2)
|
350
|
+
network_2.add_server(server_2)
|
351
|
+
|
352
|
+
assert_equal ['www.example.com', 'foo.com'], network_1.servers(:order => :asc).map(&:hostname)
|
353
|
+
assert_equal ['www.example.com', 'foo.com'].reverse, network_1.servers(:order => :desc).map(&:hostname)
|
354
|
+
|
355
|
+
assert_equal ['A', 'B'], server_2.networks(:order => :asc).map(&:klass)
|
356
|
+
assert_equal ['A', 'B'].reverse, server_2.networks(:order => :desc).map(&:klass)
|
357
|
+
|
358
|
+
assert_equal ['www.example.com'], network_1.servers(:order => :asc, :limit => 1).map(&:hostname)
|
359
|
+
assert_equal ['foo.com'], network_1.servers(:order => :desc, :limit => 1).map(&:hostname)
|
360
|
+
|
361
|
+
assert_equal ['A'], server_2.networks(:order => :asc, :limit => 1).map(&:klass)
|
362
|
+
assert_equal ['B'], server_2.networks(:order => :desc, :limit => 1).map(&:klass)
|
363
|
+
end
|
364
|
+
|
365
|
+
should "when counting cache the result" do
|
366
|
+
@network = Network.create(:klass => "C")
|
367
|
+
@server = Server.create
|
368
|
+
assert_equal 0, @network.server_count
|
369
|
+
Server.create(:network_ids => [@network.id])
|
370
|
+
assert_equal 0, @network.server_count
|
371
|
+
assert_equal 0, @network.instance_variable_get("@server_count")
|
372
|
+
@network.instance_variable_set("@server_count", nil)
|
373
|
+
assert_equal 1, @network.server_count
|
374
|
+
end
|
375
|
+
|
376
|
+
should "when counting cache the result - from both directions" do
|
377
|
+
@network = Network.create(:klass => "C")
|
378
|
+
@server = Server.create
|
379
|
+
assert_equal 0, @server.network_count
|
380
|
+
@server.network_ids = [@network.id]
|
381
|
+
@server.save!
|
382
|
+
assert_equal 0, @server.network_count
|
383
|
+
assert_equal 0, @server.instance_variable_get("@network_count")
|
384
|
+
@server.instance_variable_set("@network_count", nil)
|
385
|
+
assert_equal 1, @server.network_count
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
282
389
|
context "when deleting all design docs" do
|
283
390
|
should "reset all design docs" do
|
284
391
|
User.find_all_by_firstname('a')
|
data/test/view_test.rb
CHANGED
@@ -46,15 +46,19 @@ class ViewTest < Test::Unit::TestCase
|
|
46
46
|
'map' => "function(item){emit(item)}"
|
47
47
|
},
|
48
48
|
'by_firstname' => {
|
49
|
-
'reduce' => "
|
49
|
+
'reduce' => "_sum",
|
50
50
|
"map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
|
51
51
|
},
|
52
52
|
'by_firstname_and_lastname' => {
|
53
|
-
'reduce' => "
|
53
|
+
'reduce' => "_sum",
|
54
54
|
"map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
|
55
55
|
},
|
56
56
|
'association_user_belongs_to_project' => {
|
57
|
-
'reduce' => "
|
57
|
+
'reduce' => "_sum",
|
58
|
+
"map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
|
59
|
+
},
|
60
|
+
'association_user_has_and_belongs_to_many_groups' => {
|
61
|
+
'reduce' => "_sum",
|
58
62
|
"map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
|
59
63
|
}
|
60
64
|
}}
|
@@ -366,6 +370,80 @@ class ViewTest < Test::Unit::TestCase
|
|
366
370
|
end
|
367
371
|
end
|
368
372
|
|
373
|
+
context "has and belongs to many views" do
|
374
|
+
setup do
|
375
|
+
@db['group_1'] = {"name" => 'A', 'ruby_class' => 'Group'}
|
376
|
+
@db['group_2'] = {"name" => 'B', 'ruby_class' => 'Group'}
|
377
|
+
@db['user_1'] = {"group_ids" => ['group_1', 'group_2'], 'firstname' => 'Bert', 'ruby_class' => 'User'}
|
378
|
+
@db['user_2'] = {"group_ids" => ['group_1'], 'firstname' => 'Alf', 'ruby_class' => 'User'}
|
379
|
+
@db['_design/group'] = { 'language' => 'javascript', 'views' => {
|
380
|
+
'all_documents' => {
|
381
|
+
'reduce' => nil,
|
382
|
+
'map' => "function(item){emit(item)}"
|
383
|
+
},
|
384
|
+
'association_group_has_and_belongs_to_many_users' => {
|
385
|
+
'reduce' => "function(key, values){ return values.length }",
|
386
|
+
"map" => "function(doc) {\n if(doc.ruby_class && doc.ruby_class == 'Instance') {\n emit(doc['created_at'], null);\n }\n }"
|
387
|
+
}
|
388
|
+
}}
|
389
|
+
end
|
390
|
+
|
391
|
+
should "return all item not storing keys" do
|
392
|
+
assert_equal(JSON.parse({
|
393
|
+
"total_rows" => 2,
|
394
|
+
"rows" => [
|
395
|
+
{"doc" => {
|
396
|
+
"_rev" => "the-rev",
|
397
|
+
"group_ids" => ["group_1","group_2"],
|
398
|
+
"_id" => "user_1",
|
399
|
+
"firstname" => "Bert",
|
400
|
+
"ruby_class" => "User"
|
401
|
+
},
|
402
|
+
"id" => "user_1",
|
403
|
+
"value" => nil,
|
404
|
+
"key" => "group_1"
|
405
|
+
},{
|
406
|
+
"doc" => {
|
407
|
+
"_rev" => "the-rev",
|
408
|
+
"group_ids" => ["group_1"],
|
409
|
+
"_id" => "user_2",
|
410
|
+
"firstname" => "Alf",
|
411
|
+
"ruby_class" => "User"
|
412
|
+
},
|
413
|
+
"id" => "user_2",
|
414
|
+
"value" => nil,
|
415
|
+
"key" => "group_1"
|
416
|
+
}],
|
417
|
+
"offset" => 0}.to_json), JSON.parse(@db.view('user', 'association_user_has_and_belongs_to_many_groups', 'key' => "group_1".to_json, 'include_docs' => 'true')))
|
418
|
+
end
|
419
|
+
|
420
|
+
should "return all item storing keys" do
|
421
|
+
assert_equal(JSON.parse({
|
422
|
+
"total_rows" => 2,
|
423
|
+
"rows" => [
|
424
|
+
{"doc" => {
|
425
|
+
"_rev" => "the-rev",
|
426
|
+
"_id" => "group_1",
|
427
|
+
"name" => "A",
|
428
|
+
"ruby_class" => "Group"
|
429
|
+
},
|
430
|
+
"id" => "group_1",
|
431
|
+
"value" => nil,
|
432
|
+
"key" => "user_1"
|
433
|
+
},{
|
434
|
+
"doc" => {
|
435
|
+
"_rev" => "the-rev",
|
436
|
+
"_id" => "group_2",
|
437
|
+
"name" => "B",
|
438
|
+
"ruby_class" => "Group"
|
439
|
+
},
|
440
|
+
"id" => "group_2",
|
441
|
+
"value" => nil,
|
442
|
+
"key" => "user_1"
|
443
|
+
}],
|
444
|
+
"offset" => 0}.to_json), JSON.parse(@db.view('group', 'association_group_has_and_belongs_to_many_users', 'key' => "user_1".to_json, 'include_docs' => 'true')))
|
445
|
+
end
|
446
|
+
end
|
369
447
|
end
|
370
448
|
|
371
449
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rocking_chair
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 3
|
9
|
+
- 0
|
10
|
+
version: 0.3.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Jonathan Weiss
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date:
|
18
|
+
date: 2011-02-22 00:00:00 +01:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|