mongoid-multitenancy-2 2.0.3
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 +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +54 -0
- data/Gemfile +15 -0
- data/LICENSE.TXT +20 -0
- data/README.md +283 -0
- data/Rakefile +8 -0
- data/gemfiles/Gemfile.mongoid-6 +14 -0
- data/gemfiles/Gemfile.mongoid-7 +14 -0
- data/lib/mongoid-multitenancy.rb +1 -0
- data/lib/mongoid/multitenancy.rb +34 -0
- data/lib/mongoid/multitenancy/document.rb +176 -0
- data/lib/mongoid/multitenancy/validators/tenancy.rb +36 -0
- data/lib/mongoid/multitenancy/validators/tenant_uniqueness.rb +67 -0
- data/lib/mongoid/multitenancy/version.rb +6 -0
- data/mongoid-multitenancy.gemspec +19 -0
- data/spec/immutable_spec.rb +46 -0
- data/spec/indexable_spec.rb +35 -0
- data/spec/inheritance_spec.rb +22 -0
- data/spec/mandatory_spec.rb +116 -0
- data/spec/models/account.rb +5 -0
- data/spec/models/immutable.rb +15 -0
- data/spec/models/indexable.rb +10 -0
- data/spec/models/mandatory.rb +15 -0
- data/spec/models/mutable.rb +15 -0
- data/spec/models/mutable_child.rb +9 -0
- data/spec/models/no_scopable.rb +11 -0
- data/spec/models/optional.rb +15 -0
- data/spec/models/optional_exclude.rb +15 -0
- data/spec/mongoid-multitenancy_spec.rb +37 -0
- data/spec/mutable_child_spec.rb +46 -0
- data/spec/mutable_spec.rb +50 -0
- data/spec/optional_exclude_spec.rb +71 -0
- data/spec/optional_spec.rb +207 -0
- data/spec/scopable_spec.rb +29 -0
- data/spec/spec_helper.rb +44 -0
- data/spec/support/mongoid_matchers.rb +17 -0
- data/spec/support/shared_examples.rb +80 -0
- metadata +119 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
gem 'mongoid', '~> 7.0'
|
4
|
+
|
5
|
+
gem 'rake'
|
6
|
+
|
7
|
+
group :test do
|
8
|
+
gem 'database_cleaner'
|
9
|
+
gem 'coveralls', require: false
|
10
|
+
gem 'rspec', '~> 3.1'
|
11
|
+
gem 'yard'
|
12
|
+
gem 'mongoid-rspec', git: 'https://github.com/mongoid-rspec/mongoid-rspec.git'
|
13
|
+
gem 'rubocop', require: false
|
14
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'mongoid/multitenancy'
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'mongoid'
|
2
|
+
require 'mongoid/multitenancy/document'
|
3
|
+
require 'mongoid/multitenancy/version'
|
4
|
+
require 'mongoid/multitenancy/validators/tenancy'
|
5
|
+
require 'mongoid/multitenancy/validators/tenant_uniqueness'
|
6
|
+
|
7
|
+
module Mongoid
|
8
|
+
module Multitenancy
|
9
|
+
class << self
|
10
|
+
# Set the current tenant. Make it Thread aware
|
11
|
+
def current_tenant=(tenant)
|
12
|
+
Thread.current[:current_tenant] = tenant
|
13
|
+
end
|
14
|
+
|
15
|
+
# Returns the current tenant
|
16
|
+
def current_tenant
|
17
|
+
Thread.current[:current_tenant]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Affects a tenant temporary for a block execution
|
21
|
+
def with_tenant(tenant, &block)
|
22
|
+
raise ArgumentError, 'block required' if block.nil?
|
23
|
+
|
24
|
+
begin
|
25
|
+
old_tenant = current_tenant
|
26
|
+
self.current_tenant = tenant
|
27
|
+
yield
|
28
|
+
ensure
|
29
|
+
self.current_tenant = old_tenant
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
module Mongoid
|
2
|
+
module Multitenancy
|
3
|
+
module Document
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
attr_accessor :tenant_field, :tenant_options
|
8
|
+
|
9
|
+
# List of authorized options
|
10
|
+
MULTITENANCY_OPTIONS = [:optional, :immutable, :full_indexes, :index, :scopes].freeze
|
11
|
+
|
12
|
+
# Defines the tenant field for the document.
|
13
|
+
#
|
14
|
+
# @example Define a tenant.
|
15
|
+
# tenant :client, optional: false, immutable: true, full_indexes: true
|
16
|
+
#
|
17
|
+
# @param [ Symbol ] name The name of the relation.
|
18
|
+
# @param [ Hash ] options The relation options.
|
19
|
+
# All the belongs_to options are allowed plus the following ones:
|
20
|
+
#
|
21
|
+
# @option options [ Boolean ] :full_indexes If true the tenant field
|
22
|
+
# will be added for each index.
|
23
|
+
# @option options [ Boolean ] :immutable If true changing the tenant
|
24
|
+
# wil raise an Exception.
|
25
|
+
# @option options [ Boolean ] :optional If true allow the document
|
26
|
+
# to be shared among all the tenants.
|
27
|
+
# @option options [ Boolean ] :index If true build an index for
|
28
|
+
# the tenant field itself.
|
29
|
+
# @option options [ Boolean ] :scopes If true create scopes :shared
|
30
|
+
# and :unshared.
|
31
|
+
# @return [ Field ] The generated field
|
32
|
+
def tenant(association = :account, options = {})
|
33
|
+
options = { full_indexes: true, immutable: true, scopes: true }.merge!(options)
|
34
|
+
assoc_options, multitenant_options = build_options(options)
|
35
|
+
|
36
|
+
# Setup the association between the class and the tenant class
|
37
|
+
belongs_to association, assoc_options
|
38
|
+
|
39
|
+
# Get the tenant model and its foreign key
|
40
|
+
self.tenant_field = reflect_on_association(association).foreign_key.to_sym
|
41
|
+
self.tenant_options = multitenant_options
|
42
|
+
|
43
|
+
# Validates the tenant field
|
44
|
+
validates_tenancy_of tenant_field, multitenant_options
|
45
|
+
|
46
|
+
define_scopes if multitenant_options[:scopes]
|
47
|
+
define_initializer association
|
48
|
+
define_inherited association, options
|
49
|
+
define_index if multitenant_options[:index]
|
50
|
+
end
|
51
|
+
|
52
|
+
# Validates whether or not a field is unique against the documents in the
|
53
|
+
# database.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
#
|
57
|
+
# class Person
|
58
|
+
# include Mongoid::Document
|
59
|
+
# include Mongoid::Multitenancy::Document
|
60
|
+
# field :title
|
61
|
+
#
|
62
|
+
# validates_tenant_uniqueness_of :title
|
63
|
+
# end
|
64
|
+
#
|
65
|
+
# @param [ Array ] *args The arguments to pass to the validator.
|
66
|
+
def validates_tenant_uniqueness_of(*args)
|
67
|
+
validates_with(TenantUniquenessValidator, _merge_attributes(args))
|
68
|
+
end
|
69
|
+
|
70
|
+
# Validates whether or not a tenant field is correct.
|
71
|
+
#
|
72
|
+
# @example Define the tenant validator
|
73
|
+
#
|
74
|
+
# class Person
|
75
|
+
# include Mongoid::Document
|
76
|
+
# include Mongoid::Multitenancy::Document
|
77
|
+
# field :title
|
78
|
+
# tenant :client
|
79
|
+
#
|
80
|
+
# validates_tenant_of :client
|
81
|
+
# end
|
82
|
+
#
|
83
|
+
# @param [ Array ] *args The arguments to pass to the validator.
|
84
|
+
def validates_tenancy_of(*args)
|
85
|
+
validates_with(TenancyValidator, _merge_attributes(args))
|
86
|
+
end
|
87
|
+
|
88
|
+
# Redefine 'index' to include the tenant field in first position
|
89
|
+
def index(spec, options = nil)
|
90
|
+
if tenant_options[:full_indexes]
|
91
|
+
spec = { tenant_field => 1 }.merge(spec)
|
92
|
+
end
|
93
|
+
|
94
|
+
super(spec, options)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Redefine 'delete_all' to take in account the default scope
|
98
|
+
def delete_all(conditions = nil)
|
99
|
+
scope = scoped
|
100
|
+
scope = scopewhere(conditions) if conditions
|
101
|
+
scope.delete
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
# @private
|
107
|
+
def build_options(options)
|
108
|
+
assoc_options = {}
|
109
|
+
multitenant_options = {}
|
110
|
+
|
111
|
+
options.each do |k, v|
|
112
|
+
if MULTITENANCY_OPTIONS.include?(k)
|
113
|
+
multitenant_options[k] = v
|
114
|
+
assoc_options[k] = v if k == :optional
|
115
|
+
else
|
116
|
+
assoc_options[k] = v
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
[assoc_options, multitenant_options]
|
121
|
+
end
|
122
|
+
|
123
|
+
# @private
|
124
|
+
#
|
125
|
+
# Define the after_initialize
|
126
|
+
def define_initializer(association)
|
127
|
+
# Apply the default value when the default scope is complex (optional tenant)
|
128
|
+
after_initialize lambda {
|
129
|
+
if Multitenancy.current_tenant && new_record?
|
130
|
+
send "#{association}=".to_sym, Multitenancy.current_tenant
|
131
|
+
end
|
132
|
+
}
|
133
|
+
end
|
134
|
+
|
135
|
+
# @private
|
136
|
+
#
|
137
|
+
# Define the inherited method
|
138
|
+
def define_inherited(association, options)
|
139
|
+
define_singleton_method(:inherited) do |child|
|
140
|
+
child.tenant association, options.merge(scopes: false)
|
141
|
+
super(child)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# @private
|
146
|
+
#
|
147
|
+
# Define the scopes
|
148
|
+
def define_scopes
|
149
|
+
# Set the default_scope to scope to current tenant
|
150
|
+
default_scope lambda {
|
151
|
+
if Multitenancy.current_tenant
|
152
|
+
tenant_id = Multitenancy.current_tenant.id
|
153
|
+
if tenant_options[:optional]
|
154
|
+
where(tenant_field.in => [tenant_id, nil])
|
155
|
+
else
|
156
|
+
where(tenant_field => tenant_id)
|
157
|
+
end
|
158
|
+
else
|
159
|
+
all
|
160
|
+
end
|
161
|
+
}
|
162
|
+
|
163
|
+
scope :shared, -> { where(tenant_field => nil) }
|
164
|
+
scope :unshared, -> { where(tenant_field => Multitenancy.current_tenant.id) }
|
165
|
+
end
|
166
|
+
|
167
|
+
# @private
|
168
|
+
#
|
169
|
+
# Create the index
|
170
|
+
def define_index
|
171
|
+
index({ tenant_field => 1 }, background: true)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -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) && 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 && Mongoid::Multitenancy.current_tenant && value != Mongoid::Multitenancy.current_tenant.id
|
26
|
+
object.errors.add(attribute, 'not authorized')
|
27
|
+
end
|
28
|
+
|
29
|
+
# Optional Check
|
30
|
+
if !options[:optional] && value.nil?
|
31
|
+
object.errors.add(attribute, 'is mandatory')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,67 @@
|
|
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
|
+
criteria = with_tenant_criterion(criteria, klass, document)
|
40
|
+
criteria = criteria.merge(options[:conditions].call) if options[:conditions]
|
41
|
+
|
42
|
+
if criteria.read(mode: :primary).exists?
|
43
|
+
add_error(document, attribute, value)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Add the scope criteria for a tenant model criteria.
|
48
|
+
#
|
49
|
+
# @api private
|
50
|
+
def with_tenant_criterion(criteria, base, document)
|
51
|
+
item = base.tenant_field.to_sym
|
52
|
+
name = document.database_field_name(item)
|
53
|
+
tenant_value = document.attributes[name]
|
54
|
+
|
55
|
+
if document.class.tenant_options[:optional] && !options[:exclude_shared]
|
56
|
+
if tenant_value
|
57
|
+
criteria = criteria.where(:"#{item}".in => [tenant_value, nil])
|
58
|
+
end
|
59
|
+
else
|
60
|
+
criteria = criteria.where(item => tenant_value)
|
61
|
+
end
|
62
|
+
|
63
|
+
criteria
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/mongoid/multitenancy/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ['Botond Orban', 'Aymeric Brisse']
|
6
|
+
gem.email = ['botondorban@gmail.com']
|
7
|
+
gem.description = 'MultiTenancy with Mongoid'
|
8
|
+
gem.summary = 'Support of a multi-tenant database with Mongoid'
|
9
|
+
gem.homepage = 'https://github.com/orbanbotond/mongoid-multitenancy'
|
10
|
+
gem.license = 'MIT'
|
11
|
+
gem.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = 'mongoid-multitenancy-2'
|
15
|
+
gem.require_paths = ['lib']
|
16
|
+
gem.version = Mongoid::Multitenancy::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency('mongoid', '~> 7')
|
19
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Immutable do
|
4
|
+
let(:client) do
|
5
|
+
Account.create!(name: 'client')
|
6
|
+
end
|
7
|
+
|
8
|
+
let(:another_client) do
|
9
|
+
Account.create!(name: 'another client')
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:item) do
|
13
|
+
Immutable.new(title: 'title X', slug: 'page-x')
|
14
|
+
end
|
15
|
+
|
16
|
+
it_behaves_like 'a tenantable model'
|
17
|
+
|
18
|
+
describe '#valid?' do
|
19
|
+
before do
|
20
|
+
Mongoid::Multitenancy.current_tenant = client
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when the tenant has not changed' do
|
24
|
+
before do
|
25
|
+
item.save!
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'is valid' do
|
29
|
+
item.title = 'title X (2)'
|
30
|
+
expect(item).to be_valid
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
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
|
41
|
+
item.tenant = another_client
|
42
|
+
expect(item).not_to be_valid
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'tenant' do
|
4
|
+
let(:client) do
|
5
|
+
Account.create!(name: 'client')
|
6
|
+
end
|
7
|
+
|
8
|
+
before do
|
9
|
+
Mongoid::Multitenancy.current_tenant = client
|
10
|
+
end
|
11
|
+
|
12
|
+
context 'without index: true' do
|
13
|
+
it 'does not create an index' do
|
14
|
+
expect(Immutable).not_to have_index_for(tenant_id: 1)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'with index: true' do
|
19
|
+
it 'creates an index' do
|
20
|
+
expect(Indexable).to have_index_for(tenant_id: 1)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'with full_indexes: true' do
|
25
|
+
it 'add the tenant field on each index' do
|
26
|
+
expect(Immutable).to have_index_for(tenant_id: 1, title: 1)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
context 'with full_indexes: false' do
|
31
|
+
it 'does not add the tenant field on each index' do
|
32
|
+
expect(Indexable).not_to have_index_for(tenant_id: 1, title: 1)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|