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