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.
@@ -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