join_collection 0.0.1
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/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +98 -0
- data/Rakefile +1 -0
- data/join_collection.gemspec +25 -0
- data/lib/join_collection.rb +82 -0
- data/spec/join_collection_spec.rb +116 -0
- data/spec/spec_helper.rb +9 -0
- metadata +121 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Mason Chang
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# JoinCollection
|
2
|
+
|
3
|
+
Join an array of mongoid docs with target objects by specified relation and delegation fields
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
gem 'join_collection'
|
11
|
+
|
12
|
+
And then execute:
|
13
|
+
|
14
|
+
$ bundle
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
|
18
|
+
$ gem install join_collection
|
19
|
+
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
### Initialize a JoinCollection object
|
24
|
+
|
25
|
+
The class JoinCollection itself is a wrapper around an array of mongoid docs, so to initialize it, simply put
|
26
|
+
|
27
|
+
user_collection = JoinCollection.new(users) # where users is an array of mongoid docs
|
28
|
+
|
29
|
+
We will call what we put inside the initilizer the source objects.
|
30
|
+
|
31
|
+
### Specifiy parameters to join docs
|
32
|
+
|
33
|
+
To use any of the `join_*` functions, make sure you provide the following parameters.
|
34
|
+
|
35
|
+
1. The 1st parameter is the `target_name`, which is used as the prefix for the delegation fields.
|
36
|
+
2. The 2nd parameter is the `target_class`, which is used to query target objects.
|
37
|
+
3. The 3rd parameter is the `options`, which is not optional and it must be a hash containing a `:relation` key and a `:delegation` key.
|
38
|
+
- The key `:relation` points to another hash, which specifies the foreign key to primary key for the type of join relation.
|
39
|
+
- The key `:delegation` also points to a hash, which specifies the `:fields` of the target object to be delegated to the doc in the source objects.
|
40
|
+
In a has_many relation, you can also privide a `:if` conditional block to specify which target object to delegate if there are many target objects.
|
41
|
+
|
42
|
+
## Examples
|
43
|
+
|
44
|
+
### Join docs with `belongs_to` relation
|
45
|
+
|
46
|
+
Assume we have a `user belongs_to site` relationship, and the Site class has the field `url`.
|
47
|
+
|
48
|
+
user_collection = JoinCollection.new(users)
|
49
|
+
user_collection.join_to(:site, Site, :relation => {:site_id => :id}, :delegation => {:fields => [:url]})
|
50
|
+
|
51
|
+
After this, all the source objects, the users, in the user_collection will have the field `site_url`
|
52
|
+
|
53
|
+
user_collection.source_objects.first.site_url # => "http://..."
|
54
|
+
|
55
|
+
### Join docs with `has_one` relation
|
56
|
+
|
57
|
+
Assume we have a `user has_one profile` relationship, and the Profile class has the field `twitter`.
|
58
|
+
|
59
|
+
user_collection = JoinCollection.new(users)
|
60
|
+
user_collection.join_one(:profile, Profile, :relation => {:user_id => :id}, :delegation => {:fields => [:twitter]})
|
61
|
+
|
62
|
+
After this, all the source objects, the users, in the user_collection will have the field `profile_twitter`
|
63
|
+
|
64
|
+
user_collection.source_objects.first.profile_twitter # => "https://twitter.com/..."
|
65
|
+
|
66
|
+
### Join docs with `has_many` relation
|
67
|
+
|
68
|
+
Assume we have a `user has_many contacts` relationship, and the Contact class has the field `phone`.
|
69
|
+
|
70
|
+
user_collection = JoinCollection.new(users)
|
71
|
+
user_collection.join_many(:contacts, Contact, :relation => {:user_id => :id},
|
72
|
+
:delegation => {:if => lambda { |x| x.is_active? }, :fields => [:phone]})
|
73
|
+
|
74
|
+
After this, all the source objects, the users, in the user_collection will have the field `contact_phone`
|
75
|
+
|
76
|
+
user_collection.source_objects.first.contact_phone # => "0987-654-321"
|
77
|
+
|
78
|
+
### Notes
|
79
|
+
|
80
|
+
If you specify a delegation field which is identical to the target name of that join type, the whole object(s) will be captured.
|
81
|
+
|
82
|
+
user_collection.join_to(:site, Site, :relation => {:site_id => :id}, :delegation => {:fields => [:site]})
|
83
|
+
user_collection.source_objects.first.site # get the site the user belongs to
|
84
|
+
|
85
|
+
user_collection.join_one(:profile, Profile, :relation => {:user_id => :id}, :delegation => {:fields => [:profile]})
|
86
|
+
user_collection.source_objects.first.profile # get the profile the user has
|
87
|
+
|
88
|
+
user_collection.join_many(:contacts, Contact, :relation => {:user_id => :id}, :delegation => {:fields => [:contacts]})
|
89
|
+
user_collection.source_objects.first.contacts # get all contacts the user has
|
90
|
+
|
91
|
+
|
92
|
+
## Contributing
|
93
|
+
|
94
|
+
1. Fork it
|
95
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
96
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
97
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
98
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'join_collection'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "join_collection"
|
8
|
+
spec.version = JoinCollection::VERSION
|
9
|
+
spec.authors = ["Mason Chang"]
|
10
|
+
spec.email = ["changmason@gmail.com"]
|
11
|
+
spec.description = %q{Joining mongoid docs with specified relation}
|
12
|
+
spec.summary = %q{Joining mongoid docs with specified relation}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "mongoid"
|
23
|
+
spec.add_development_dependency "rake"
|
24
|
+
spec.add_development_dependency "rspec"
|
25
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
class JoinCollection
|
2
|
+
|
3
|
+
VERSION = "0.0.1"
|
4
|
+
|
5
|
+
attr_reader :source_objects
|
6
|
+
attr_accessor :join_type, :singular_target, :plural_target
|
7
|
+
|
8
|
+
def initialize(collection)
|
9
|
+
@source_objects = collection
|
10
|
+
end
|
11
|
+
|
12
|
+
def join_to(target, target_class, options)
|
13
|
+
self.join_type = :join_to
|
14
|
+
fk, pk, delegate_if, delegate_fields = extract_options(target, options)
|
15
|
+
|
16
|
+
source_fks = source_objects.map(&fk).compact
|
17
|
+
target_objects = target_class.where(pk.in => source_fks).to_a
|
18
|
+
|
19
|
+
mapper = target_objects.group_by(&pk)
|
20
|
+
mapper.default = []
|
21
|
+
join_data(mapper, fk, delegate_if, delegate_fields)
|
22
|
+
end
|
23
|
+
|
24
|
+
def join_one(target, target_class, options)
|
25
|
+
self.join_type = :join_one
|
26
|
+
join_many(target, target_class, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def join_many(target, target_class, options)
|
30
|
+
self.join_type = :join_many unless self.join_type == :join_one
|
31
|
+
fk, pk, delegate_if, delegate_fields = extract_options(target, options)
|
32
|
+
|
33
|
+
source_pks = source_objects.map(&pk).compact
|
34
|
+
target_objects = target_class.where(fk.in => source_pks).to_a
|
35
|
+
|
36
|
+
mapper = target_objects.group_by(&fk)
|
37
|
+
mapper.default = []
|
38
|
+
join_data(mapper, pk, delegate_if, delegate_fields)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def extract_options(target, options)
|
44
|
+
self.singular_target = target.to_s.singularize.to_sym
|
45
|
+
self.plural_target = target.to_s.pluralize.to_sym
|
46
|
+
|
47
|
+
relation = options[:relation]
|
48
|
+
delegation = options[:delegation]
|
49
|
+
raise ArgumentError.new('Relation hash not found in options') unless relation.is_a?(Hash)
|
50
|
+
raise ArgumentError.new('Delegation hash not found in options') unless delegation.is_a?(Hash)
|
51
|
+
|
52
|
+
fk = relation.keys.first
|
53
|
+
pk = relation.values.first
|
54
|
+
|
55
|
+
if_block = delegation[:if] || lambda { |x| true }
|
56
|
+
fields = delegation[:fields] || []
|
57
|
+
|
58
|
+
return fk, pk, if_block, fields
|
59
|
+
end
|
60
|
+
|
61
|
+
def join_data(mapper, key, delegate_if, delegate_fields)
|
62
|
+
join_type = self.join_type
|
63
|
+
singular_target = self.singular_target
|
64
|
+
plural_target = self.plural_target
|
65
|
+
|
66
|
+
source_objects.each do |doc|
|
67
|
+
target_objects = mapper[doc[key]]
|
68
|
+
target_object = (target_objects.find &delegate_if) || target_objects.first
|
69
|
+
|
70
|
+
delegate_fields.each do |field|
|
71
|
+
case field
|
72
|
+
when singular_target
|
73
|
+
doc[singular_target] = target_object unless join_type == :join_many
|
74
|
+
when plural_target
|
75
|
+
doc[plural_target] = target_objects if join_type == :join_many
|
76
|
+
else
|
77
|
+
doc["#{singular_target}_#{field}"] = target_object.send(field)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
|
4
|
+
class User; include Mongoid::Document; end
|
5
|
+
class Post; include Mongoid::Document; end
|
6
|
+
|
7
|
+
describe JoinCollection do
|
8
|
+
let!(:user1) { User.new :mysql_id => 1, :name => 'Bob' }
|
9
|
+
let!(:user2) { User.new :mysql_id => 2, :name => 'Joe' }
|
10
|
+
|
11
|
+
let!(:post1) { Post.new :mysql_id => 1, :user_id => 1, :content => 'text 1', :published => true }
|
12
|
+
let!(:post2) { Post.new :mysql_id => 2, :user_id => 2, :content => 'text 2', :published => true }
|
13
|
+
let!(:post3) { Post.new :mysql_id => 3, :user_id => 2, :content => 'text 3', :published => false }
|
14
|
+
|
15
|
+
|
16
|
+
context 'extract options' do
|
17
|
+
before do
|
18
|
+
@user_collection = JoinCollection.new([user1])
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'raise an argument error if relation hash is not found in options' do
|
22
|
+
expect{@user_collection.join_many(:post, Post, :delegation => {})}.to raise_error(ArgumentError)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'raise an argument error if delegation hash is not found in options' do
|
26
|
+
expect{@user_collection.join_many(:post, Post, :relation => {})}.to raise_error(ArgumentError)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# post1 belongs_to user1
|
31
|
+
describe '#join_to' do
|
32
|
+
before do
|
33
|
+
@post_collection = JoinCollection.new([post1])
|
34
|
+
User.stub(:where).and_return([user1])
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should call User.where' do
|
38
|
+
User.should_receive(:where).with(:mysql_id.in => [1]).and_return([user1])
|
39
|
+
@post_collection.join_to(:user, User, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:mysql_id]})
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should catch the target object if the delegation field name equals to the target' do
|
43
|
+
@post_collection.join_to(:user, User, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:user]})
|
44
|
+
expect(@post_collection.source_objects.first.user).to eq(user1)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should have correct value in delegation field if the target object has the field' do
|
48
|
+
@post_collection.join_to(:user, User, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:name]})
|
49
|
+
expect(@post_collection.source_objects.first.user_name).to eq('Bob')
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should raise no method error if the target object does not have the field' do
|
53
|
+
expect{
|
54
|
+
@post_collection.join_to(:user, User, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:email]})
|
55
|
+
}.to raise_error(NoMethodError)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# user1 has_one post1
|
60
|
+
describe '#join_one' do
|
61
|
+
before do
|
62
|
+
@user_collection = JoinCollection.new([user1])
|
63
|
+
Post.stub(:where).and_return([post1])
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'should call Post.where' do
|
67
|
+
Post.should_receive(:where).with(:user_id.in => [1]).and_return([post1])
|
68
|
+
@user_collection.join_one(:post, Post, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:mysql_id]})
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should catch the target object if the delegation field name equals to the target name' do
|
72
|
+
@user_collection.join_one(:post, Post, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:post]})
|
73
|
+
expect(@user_collection.source_objects.first.post).to eq(post1)
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'should have correct value in delegation field if the target object has the field' do
|
77
|
+
@user_collection.join_one(:post, Post, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:content]})
|
78
|
+
expect(@user_collection.source_objects.first.post_content).to eq('text 1')
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should raise no method error if the target object does not have the field' do
|
82
|
+
expect{
|
83
|
+
@user_collection.join_one(:post, Post, :relation => {:user_id => :mysql_id}, :delegation => {:fields => [:location]})
|
84
|
+
}.to raise_error(NoMethodError)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# user2 has_many posts which are post2 and post3
|
89
|
+
describe '#join_many' do
|
90
|
+
before do
|
91
|
+
@user_collection = JoinCollection.new([user2])
|
92
|
+
Post.stub(:where).and_return([post2, post3])
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'should catch the whole target objects if the delegation field name equals to the plural target name' do
|
96
|
+
@user_collection.join_many(:post, Post,
|
97
|
+
:relation => {:user_id => :mysql_id}, :delegation => {:fields => [:posts]})
|
98
|
+
expect(@user_collection.source_objects.first.posts).to eq([post2, post3])
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should have correct values in delegation fields if no conditional block given' do
|
102
|
+
@user_collection.join_many(:post, Post,
|
103
|
+
:relation => {:user_id => :mysql_id}, :delegation => {:fields => [:content, :published]})
|
104
|
+
expect(@user_collection.source_objects.first.post_content).to eq('text 2')
|
105
|
+
expect(@user_collection.source_objects.first.post_published).to be_true
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'should have correct values in delegation fields if a conditional block given' do
|
109
|
+
@user_collection.join_many(:post, Post,
|
110
|
+
:relation => {:user_id => :mysql_id},
|
111
|
+
:delegation => {:if => lambda { |x| x.published == false }, :fields => [:content, :published]})
|
112
|
+
expect(@user_collection.source_objects.first.post_content).to eq('text 3')
|
113
|
+
expect(@user_collection.source_objects.first.post_published).to be_false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: join_collection
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mason Chang
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-07-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: mongoid
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rspec
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Joining mongoid docs with specified relation
|
79
|
+
email:
|
80
|
+
- changmason@gmail.com
|
81
|
+
executables: []
|
82
|
+
extensions: []
|
83
|
+
extra_rdoc_files: []
|
84
|
+
files:
|
85
|
+
- .gitignore
|
86
|
+
- Gemfile
|
87
|
+
- LICENSE.txt
|
88
|
+
- README.md
|
89
|
+
- Rakefile
|
90
|
+
- join_collection.gemspec
|
91
|
+
- lib/join_collection.rb
|
92
|
+
- spec/join_collection_spec.rb
|
93
|
+
- spec/spec_helper.rb
|
94
|
+
homepage: ''
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options: []
|
99
|
+
require_paths:
|
100
|
+
- lib
|
101
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
none: false
|
109
|
+
requirements:
|
110
|
+
- - ! '>='
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 1.8.23
|
116
|
+
signing_key:
|
117
|
+
specification_version: 3
|
118
|
+
summary: Joining mongoid docs with specified relation
|
119
|
+
test_files:
|
120
|
+
- spec/join_collection_spec.rb
|
121
|
+
- spec/spec_helper.rb
|