mongoid-multitenancy 0.4.4 → 1.0.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.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.travis.yml +4 -3
- data/CHANGELOG.md +13 -0
- data/Gemfile +5 -5
- data/README.md +44 -15
- data/Rakefile +1 -1
- data/gemfiles/Gemfile.mongoid-4.0 +3 -4
- data/gemfiles/{Gemfile.mongoid-3.0 → Gemfile.mongoid-5.0} +3 -4
- data/lib/mongoid/multitenancy.rb +6 -10
- data/lib/mongoid/multitenancy/document.rb +117 -49
- data/lib/mongoid/multitenancy/validators/tenancy.rb +36 -0
- data/lib/mongoid/multitenancy/validators/tenant_uniqueness.rb +72 -0
- data/lib/mongoid/multitenancy/version.rb +2 -1
- data/mongoid-multitenancy.gemspec +8 -8
- data/spec/immutable_spec.rb +30 -18
- data/spec/indexable_spec.rb +6 -6
- data/spec/inheritance_spec.rb +11 -6
- data/spec/mandatory_spec.rb +70 -44
- data/spec/models/immutable.rb +3 -3
- data/spec/models/indexable.rb +2 -2
- data/spec/models/mandatory.rb +5 -5
- data/spec/models/mutable.rb +3 -3
- data/spec/models/optional.rb +5 -5
- data/spec/mongoid-multitenancy_spec.rb +17 -11
- data/spec/mutable_child_spec.rb +31 -17
- data/spec/mutable_spec.rb +34 -18
- data/spec/optional_spec.rb +88 -46
- data/spec/spec_helper.rb +16 -60
- data/spec/support/database_cleaner.rb +38 -0
- data/spec/support/mongoid_matchers.rb +17 -0
- data/spec/support/shared_examples.rb +81 -0
- metadata +14 -8
- data/lib/mongoid/validators/tenant_validator.rb +0 -20
- data/spec/support/mongoid.rb +0 -30
@@ -0,0 +1,36 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Multitenancy
|
3
|
+
# Validates whether or not a tenant field is correct.
|
4
|
+
#
|
5
|
+
# @example Define the tenant validator
|
6
|
+
#
|
7
|
+
# class Person
|
8
|
+
# include Mongoid::Document
|
9
|
+
# include Mongoid::Multitenancy::Document
|
10
|
+
# field :title
|
11
|
+
# tenant :client
|
12
|
+
#
|
13
|
+
# validates_tenancy_of :client
|
14
|
+
# end
|
15
|
+
class TenancyValidator < ActiveModel::EachValidator
|
16
|
+
def validate_each(object, attribute, value)
|
17
|
+
# Immutable Check
|
18
|
+
if options[:immutable]
|
19
|
+
if object.send(:attribute_changed?, attribute) and object.send(:attribute_was, attribute)
|
20
|
+
object.errors.add(attribute, 'is immutable and cannot be updated')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Ownership check
|
25
|
+
if value and Mongoid::Multitenancy.current_tenant and value != Mongoid::Multitenancy.current_tenant.id
|
26
|
+
object.errors.add(attribute, "not authorized")
|
27
|
+
end
|
28
|
+
|
29
|
+
# Optional Check
|
30
|
+
if !options[:optional] and value.nil?
|
31
|
+
object.errors.add(attribute, 'is mandatory')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Multitenancy
|
3
|
+
# Validates whether or not a field is unique against the documents in the
|
4
|
+
# database.
|
5
|
+
#
|
6
|
+
# @example Define the tenant uniqueness validator.
|
7
|
+
#
|
8
|
+
# class Person
|
9
|
+
# include Mongoid::Document
|
10
|
+
# include Mongoid::Multitenancy::Document
|
11
|
+
# field :title
|
12
|
+
# tenant :client
|
13
|
+
#
|
14
|
+
# validates_tenant_uniqueness_of :title
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# It is also possible to limit the uniqueness constraint to a set of
|
18
|
+
# records matching certain conditions:
|
19
|
+
# class Person
|
20
|
+
# include Mongoid::Document
|
21
|
+
# include Mongoid::Multitenancy::Document
|
22
|
+
# field :title
|
23
|
+
# field :active, type: Boolean
|
24
|
+
# tenant :client
|
25
|
+
#
|
26
|
+
# validates_tenant_uniqueness_of :title, conditions: -> {where(active: true)}
|
27
|
+
# end
|
28
|
+
class TenantUniquenessValidator < Mongoid::Validatable::UniquenessValidator
|
29
|
+
# Validate a tenant root document.
|
30
|
+
def validate_root(document, attribute, value)
|
31
|
+
klass = document.class
|
32
|
+
|
33
|
+
while klass.superclass.respond_to?(:validators) && klass.superclass.validators.include?(self)
|
34
|
+
klass = klass.superclass
|
35
|
+
end
|
36
|
+
criteria = create_criteria(klass, document, attribute, value)
|
37
|
+
|
38
|
+
# <<Add the tenant Criteria>>
|
39
|
+
add_tenant_criterion(criteria, klass, document)
|
40
|
+
|
41
|
+
criteria = criteria.merge(options[:conditions].call) if options[:conditions]
|
42
|
+
|
43
|
+
if Mongoid::VERSION.start_with?('4')
|
44
|
+
if criteria.with(persistence_options(criteria)).exists?
|
45
|
+
add_error(document, attribute, value)
|
46
|
+
end
|
47
|
+
else
|
48
|
+
if criteria.with(criteria.persistence_options).read(mode: :primary).exists?
|
49
|
+
add_error(document, attribute, value)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Create the validation criteria for a tenant model.
|
55
|
+
#
|
56
|
+
# @api private
|
57
|
+
def add_tenant_criterion(criteria, base, document)
|
58
|
+
tenant_value = document.send(base.tenant_field.to_sym)
|
59
|
+
|
60
|
+
if document.class.tenant_options[:optional]
|
61
|
+
if tenant_value
|
62
|
+
criteria.selector.update(criterion(document, base.tenant_field, {'$in' => [tenant_value, nil].mongoize}))
|
63
|
+
end
|
64
|
+
else
|
65
|
+
criteria.selector.update(criterion(document, base.tenant_field, tenant_value.mongoize))
|
66
|
+
end
|
67
|
+
|
68
|
+
criteria
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -2,18 +2,18 @@
|
|
2
2
|
require File.expand_path('../lib/mongoid/multitenancy/version', __FILE__)
|
3
3
|
|
4
4
|
Gem::Specification.new do |gem|
|
5
|
-
gem.authors = [
|
6
|
-
gem.email = [
|
7
|
-
gem.description =
|
8
|
-
gem.summary =
|
9
|
-
gem.homepage =
|
5
|
+
gem.authors = ['Aymeric Brisse']
|
6
|
+
gem.email = ['aymeric.brisse@mperfect-memory.com']
|
7
|
+
gem.description = 'MultiTenancy with Mongoid'
|
8
|
+
gem.summary = 'Support of a multi-tenant database with Mongoid'
|
9
|
+
gem.homepage = 'https://github.com/PerfectMemory/mongoid-multitenancy'
|
10
10
|
gem.license = 'MIT'
|
11
11
|
gem.files = `git ls-files`.split($\)
|
12
12
|
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
13
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
-
gem.name =
|
15
|
-
gem.require_paths = [
|
14
|
+
gem.name = 'mongoid-multitenancy'
|
15
|
+
gem.require_paths = ['lib']
|
16
16
|
gem.version = Mongoid::Multitenancy::VERSION
|
17
17
|
|
18
|
-
gem.add_dependency('mongoid', '>=
|
18
|
+
gem.add_dependency('mongoid', '>= 4.0')
|
19
19
|
end
|
data/spec/immutable_spec.rb
CHANGED
@@ -1,34 +1,46 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Immutable do
|
4
|
+
let(:client) do
|
5
|
+
Account.create!(name: 'client')
|
6
|
+
end
|
4
7
|
|
5
|
-
|
8
|
+
let(:another_client) do
|
9
|
+
Account.create!(name: 'another client')
|
10
|
+
end
|
6
11
|
|
7
|
-
let(:
|
8
|
-
|
12
|
+
let(:item) do
|
13
|
+
Immutable.new(title: 'title X', slug: 'page-x')
|
14
|
+
end
|
9
15
|
|
10
|
-
|
11
|
-
before { Mongoid::Multitenancy.current_tenant = client; }
|
12
|
-
after { Mongoid::Multitenancy.current_tenant = nil }
|
16
|
+
it_behaves_like 'a tenantable model'
|
13
17
|
|
14
|
-
|
18
|
+
describe '#valid?' do
|
19
|
+
before do
|
20
|
+
Mongoid::Multitenancy.current_tenant = client
|
21
|
+
end
|
15
22
|
|
16
|
-
|
23
|
+
context 'when the tenant has not changed' do
|
24
|
+
before do
|
25
|
+
item.save!
|
26
|
+
end
|
17
27
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
item.title = "title X (2)"
|
22
|
-
item.should be_valid
|
28
|
+
it 'is valid' do
|
29
|
+
item.title = 'title X (2)'
|
30
|
+
expect(item).to be_valid
|
23
31
|
end
|
24
32
|
end
|
25
33
|
|
26
|
-
context
|
27
|
-
before
|
28
|
-
|
34
|
+
context 'when the tenant has changed' do
|
35
|
+
before do
|
36
|
+
item.save!
|
37
|
+
Mongoid::Multitenancy.current_tenant = another_client
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'is not valid' do
|
29
41
|
item.client = another_client
|
30
|
-
item.
|
42
|
+
expect(item).not_to be_valid
|
31
43
|
end
|
32
44
|
end
|
33
45
|
end
|
34
|
-
end
|
46
|
+
end
|
data/spec/indexable_spec.rb
CHANGED
@@ -3,7 +3,7 @@ require 'spec_helper'
|
|
3
3
|
describe 'tenant' do
|
4
4
|
|
5
5
|
let(:client) do
|
6
|
-
Account.create!(:
|
6
|
+
Account.create!(name: 'client')
|
7
7
|
end
|
8
8
|
|
9
9
|
before do
|
@@ -12,26 +12,26 @@ describe 'tenant' do
|
|
12
12
|
|
13
13
|
context 'without index: true' do
|
14
14
|
it 'does not create an index' do
|
15
|
-
Immutable.
|
15
|
+
expect(Immutable).not_to have_index_for(:client_id => 1)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
19
|
context 'with index: true' do
|
20
20
|
it 'creates an index' do
|
21
|
-
Indexable.
|
21
|
+
expect(Indexable).to have_index_for(:client_id => 1)
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
25
|
context 'with full_indexes: true' do
|
26
26
|
it 'add the tenant field on each index' do
|
27
|
-
Immutable.
|
27
|
+
expect(Immutable).to have_index_for(:client_id => 1, title: 1)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
31
|
context 'with full_indexes: false' do
|
32
32
|
it 'does not add the tenant field on each index' do
|
33
|
-
Indexable.
|
33
|
+
expect(Indexable).not_to have_index_for(:client_id => 1, title: 1)
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
-
end
|
37
|
+
end
|
data/spec/inheritance_spec.rb
CHANGED
@@ -1,17 +1,22 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe 'Inheritance' do
|
4
|
+
let(:client) do
|
5
|
+
Account.create!(name: 'client')
|
6
|
+
end
|
4
7
|
|
5
|
-
|
8
|
+
before do
|
9
|
+
Mongoid::Multitenancy.current_tenant = client
|
10
|
+
end
|
6
11
|
|
7
|
-
describe
|
8
|
-
it '
|
9
|
-
MutableChild.create!(:
|
12
|
+
describe 'class' do
|
13
|
+
it 'uses inheritance pattern' do
|
14
|
+
MutableChild.create!(title: 'title X', slug: 'page-x')
|
10
15
|
expect(Mutable.last).to be_a MutableChild
|
11
16
|
end
|
12
17
|
|
13
|
-
it '
|
14
|
-
AnotherMutableChild.new(:
|
18
|
+
it 'keeps options' do
|
19
|
+
expect(AnotherMutableChild.new(title: 'title X', slug: 'page-x')).to be_valid
|
15
20
|
end
|
16
21
|
end
|
17
22
|
end
|
data/spec/mandatory_spec.rb
CHANGED
@@ -2,81 +2,107 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Mandatory do
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
let(:client) do
|
6
|
+
Account.create!(name: 'client')
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:another_client) do
|
10
|
+
Account.create!(name: 'another client')
|
11
|
+
end
|
7
12
|
|
8
|
-
let(:
|
9
|
-
|
13
|
+
let(:item) do
|
14
|
+
Mandatory.new(title: 'title X', slug: 'page-x')
|
15
|
+
end
|
16
|
+
|
17
|
+
it_behaves_like 'a tenantable model'
|
18
|
+
it { is_expected.to validate_tenant_uniqueness_of(:slug) }
|
10
19
|
|
11
|
-
describe
|
12
|
-
before
|
13
|
-
Mongoid::Multitenancy.with_tenant(client) { @itemX = Mandatory.create!(:
|
14
|
-
Mongoid::Multitenancy.with_tenant(another_client) { @itemY = Mandatory.create!(:
|
15
|
-
|
20
|
+
describe '.default_scope' do
|
21
|
+
before do
|
22
|
+
Mongoid::Multitenancy.with_tenant(client) { @itemX = Mandatory.create!(title: 'title X', slug: 'article-x') }
|
23
|
+
Mongoid::Multitenancy.with_tenant(another_client) { @itemY = Mandatory.create!(title: 'title Y', slug: 'article-y') }
|
24
|
+
end
|
16
25
|
|
17
|
-
context
|
18
|
-
before
|
19
|
-
|
26
|
+
context 'with a current tenant' do
|
27
|
+
before do
|
28
|
+
Mongoid::Multitenancy.current_tenant = another_client
|
29
|
+
end
|
20
30
|
|
21
|
-
it
|
22
|
-
Mandatory.all.to_a.
|
31
|
+
it 'filters on the current tenant' do
|
32
|
+
expect(Mandatory.all.to_a).to match_array [@itemY]
|
23
33
|
end
|
24
34
|
end
|
25
35
|
|
26
|
-
context
|
27
|
-
before
|
36
|
+
context 'without a current tenant' do
|
37
|
+
before do
|
38
|
+
Mongoid::Multitenancy.current_tenant = nil
|
39
|
+
end
|
28
40
|
|
29
|
-
it
|
30
|
-
Mandatory.all.to_a.
|
41
|
+
it 'does not filter on any tenant' do
|
42
|
+
expect(Mandatory.all.to_a).to match_array [@itemX, @itemY]
|
31
43
|
end
|
32
44
|
end
|
33
45
|
end
|
34
46
|
|
35
|
-
describe
|
36
|
-
before
|
37
|
-
Mongoid::Multitenancy.with_tenant(client) { @itemX = Mandatory.create!(:
|
38
|
-
Mongoid::Multitenancy.with_tenant(another_client) { @itemY = Mandatory.create!(:
|
39
|
-
|
47
|
+
describe '#delete_all' do
|
48
|
+
before do
|
49
|
+
Mongoid::Multitenancy.with_tenant(client) { @itemX = Mandatory.create!(title: 'title X', slug: 'article-x') }
|
50
|
+
Mongoid::Multitenancy.with_tenant(another_client) { @itemY = Mandatory.create!(title: 'title Y', slug: 'article-y') }
|
51
|
+
end
|
40
52
|
|
41
|
-
context
|
42
|
-
it
|
53
|
+
context 'with a current tenant' do
|
54
|
+
it 'only deletes the current tenant' do
|
43
55
|
Mongoid::Multitenancy.with_tenant(another_client) { Mandatory.delete_all }
|
44
|
-
Mandatory.all.to_a.
|
56
|
+
expect(Mandatory.all.to_a).to match_array [@itemX]
|
45
57
|
end
|
46
58
|
end
|
47
59
|
|
48
|
-
context
|
49
|
-
it
|
60
|
+
context 'without a current tenant' do
|
61
|
+
it 'deletes all the items' do
|
50
62
|
Mandatory.delete_all
|
51
|
-
Mandatory.all.to_a.
|
63
|
+
expect(Mandatory.all.to_a).to be_empty
|
52
64
|
end
|
53
65
|
end
|
54
66
|
end
|
55
67
|
|
56
|
-
describe
|
57
|
-
|
68
|
+
describe '#valid?' do
|
69
|
+
context 'with a tenant' do
|
70
|
+
before do
|
71
|
+
item.client = client
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'is valid' do
|
75
|
+
expect(item).to be_valid
|
76
|
+
end
|
58
77
|
|
59
|
-
|
78
|
+
context 'with a uniqueness constraint' do
|
79
|
+
let(:duplicate) do
|
80
|
+
Mandatory.new(title: 'title Y', slug: 'page-x')
|
81
|
+
end
|
60
82
|
|
61
|
-
|
83
|
+
before do
|
84
|
+
item.save!
|
85
|
+
end
|
62
86
|
|
63
|
-
|
64
|
-
|
87
|
+
it 'does not allow duplicates on the same tenant' do
|
88
|
+
expect(duplicate).not_to be_valid
|
89
|
+
end
|
65
90
|
|
66
|
-
|
67
|
-
|
68
|
-
|
91
|
+
it 'allow duplicates on a different same tenant' do
|
92
|
+
Mongoid::Multitenancy.with_tenant(another_client) do
|
93
|
+
expect(duplicate).to be_valid
|
94
|
+
end
|
95
|
+
end
|
69
96
|
end
|
70
97
|
end
|
71
98
|
|
72
|
-
context
|
73
|
-
|
74
|
-
item.
|
75
|
-
item.client.should be_nil
|
99
|
+
context 'without a tenant' do
|
100
|
+
before do
|
101
|
+
item.client = nil
|
76
102
|
end
|
77
103
|
|
78
|
-
it
|
79
|
-
item.
|
104
|
+
it 'is not valid' do
|
105
|
+
expect(item).not_to be_valid
|
80
106
|
end
|
81
107
|
end
|
82
108
|
end
|
data/spec/models/immutable.rb
CHANGED
@@ -2,14 +2,14 @@ class Immutable
|
|
2
2
|
include Mongoid::Document
|
3
3
|
include Mongoid::Multitenancy::Document
|
4
4
|
|
5
|
-
tenant(:client, :
|
5
|
+
tenant(:client, class_name: 'Account', immutable: true)
|
6
6
|
|
7
7
|
field :slug, :type => String
|
8
8
|
field :title, :type => String
|
9
9
|
|
10
|
-
|
10
|
+
validates_tenant_uniqueness_of :slug
|
11
11
|
validates_presence_of :slug
|
12
12
|
validates_presence_of :title
|
13
13
|
|
14
|
-
index({ :
|
14
|
+
index({ title: 1 })
|
15
15
|
end
|