rocking_chair 0.2.5 → 0.3.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/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
|