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.
@@ -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
@@ -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' => "function(key, values){ return values.length }",
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' => "function(key, values){ return values.length }",
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' => "function(key, values){ return values.length }",
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: 29
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 5
10
- version: 0.2.5
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: 2010-09-30 00:00:00 +02:00
18
+ date: 2011-02-22 00:00:00 +01:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency