mongoid-multitenancy 0.4.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|