mongoid-multitenancy-2 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,6 @@
1
+ module Mongoid
2
+ module Multitenancy
3
+ # Version
4
+ VERSION = '2.0.3'.freeze
5
+ end
6
+ 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