activerecord-multi-tenant 2.2.0 → 2.3.0
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 +4 -4
- data/.github/workflows/active-record-multi-tenant-tests.yml +80 -0
- data/.gitignore +6 -0
- data/.readthedocs.yaml +15 -0
- data/.rspec +0 -0
- data/.rubocop.yml +51 -0
- data/Appraisals +0 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +3 -1
- data/LICENSE +0 -0
- data/README.md +2 -1
- data/Rakefile +1 -1
- data/activerecord-multi-tenant.gemspec +28 -22
- data/docker-compose.yml +24 -18
- data/docs/.gitignore +3 -0
- data/docs/Makefile +28 -0
- data/docs/api-reference.sh +10 -0
- data/docs/requirements.in +4 -0
- data/docs/requirements.txt +62 -0
- data/docs/source/_static/api-reference/ActiveRecord/Associations/Association.html +285 -0
- data/docs/source/_static/api-reference/ActiveRecord/Associations/ClassMethods.html +255 -0
- data/docs/source/_static/api-reference/ActiveRecord/Associations.html +117 -0
- data/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters/SchemaStatements.html +232 -0
- data/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters.html +126 -0
- data/docs/source/_static/api-reference/ActiveRecord/QueryMethods.html +336 -0
- data/docs/source/_static/api-reference/ActiveRecord/SchemaDumper.html +121 -0
- data/docs/source/_static/api-reference/ActiveRecord.html +130 -0
- data/docs/source/_static/api-reference/MultiTenant/ArelTenantVisitor.html +755 -0
- data/docs/source/_static/api-reference/MultiTenant/ArelVisitorsDepthFirst.html +208 -0
- data/docs/source/_static/api-reference/MultiTenant/BaseTenantEnforcementClause.html +462 -0
- data/docs/source/_static/api-reference/MultiTenant/Context.html +659 -0
- data/docs/source/_static/api-reference/MultiTenant/ControllerExtensions.html +202 -0
- data/docs/source/_static/api-reference/MultiTenant/CopyFromClient.html +186 -0
- data/docs/source/_static/api-reference/MultiTenant/CopyFromClientHelper.html +362 -0
- data/docs/source/_static/api-reference/MultiTenant/Current.html +124 -0
- data/docs/source/_static/api-reference/MultiTenant/DatabaseStatements.html +366 -0
- data/docs/source/_static/api-reference/MultiTenant/FastTruncate.html +226 -0
- data/docs/source/_static/api-reference/MultiTenant/MigrationExtensions.html +554 -0
- data/docs/source/_static/api-reference/MultiTenant/MissingTenantError.html +124 -0
- data/docs/source/_static/api-reference/MultiTenant/ModelExtensionsClassMethods.html +492 -0
- data/docs/source/_static/api-reference/MultiTenant/QueryMonitor.html +257 -0
- data/docs/source/_static/api-reference/MultiTenant/Table.html +419 -0
- data/docs/source/_static/api-reference/MultiTenant/TenantEnforcementClause.html +148 -0
- data/docs/source/_static/api-reference/MultiTenant/TenantIsImmutable.html +135 -0
- data/docs/source/_static/api-reference/MultiTenant/TenantJoinEnforcementClause.html +310 -0
- data/docs/source/_static/api-reference/MultiTenant/TenantValueVisitor.html +239 -0
- data/docs/source/_static/api-reference/MultiTenant.html +1454 -0
- data/docs/source/_static/api-reference/MultiTenantFindBy.html +180 -0
- data/docs/source/_static/api-reference/Sidekiq/Client.html +302 -0
- data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Client.html +217 -0
- data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Server.html +219 -0
- data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant.html +126 -0
- data/docs/source/_static/api-reference/Sidekiq.html +126 -0
- data/docs/source/_static/api-reference/_index.html +399 -0
- data/docs/source/_static/api-reference/class_list.html +51 -0
- data/docs/source/_static/api-reference/css/common.css +1 -0
- data/docs/source/_static/api-reference/css/full_list.css +58 -0
- data/docs/source/_static/api-reference/css/style.css +497 -0
- data/docs/source/_static/api-reference/file.README.html +167 -0
- data/docs/source/_static/api-reference/file_list.html +56 -0
- data/docs/source/_static/api-reference/frames.html +17 -0
- data/docs/source/_static/api-reference/index.html +167 -0
- data/docs/source/_static/api-reference/js/app.js +314 -0
- data/docs/source/_static/api-reference/js/full_list.js +216 -0
- data/docs/source/_static/api-reference/js/jquery.js +4 -0
- data/docs/source/_static/api-reference/method_list.html +715 -0
- data/docs/source/_static/api-reference/top-level-namespace.html +126 -0
- data/docs/source/_templates/.gitignore +4 -0
- data/docs/source/api-reference.rst +8 -0
- data/docs/source/appendix.rst +26 -0
- data/docs/source/changelog.rst +8 -0
- data/docs/source/community-and-support.rst +26 -0
- data/docs/source/conf.py +30 -0
- data/docs/source/contributing.rst +70 -0
- data/docs/source/getting-started.rst +37 -0
- data/docs/source/guides-and-tutorials.rst +129 -0
- data/docs/source/index.rst +54 -0
- data/docs/source/introduction.rst +33 -0
- data/docs/source/license.rst +22 -0
- data/docs/source/troubleshooting.rst +41 -0
- data/docs/source/usage-guide.rst +59 -0
- data/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb +183 -174
- data/lib/activerecord-multi-tenant/controller_extensions.rb +15 -4
- data/lib/activerecord-multi-tenant/copy_from_client.rb +4 -0
- data/lib/activerecord-multi-tenant/fast_truncate.rb +4 -2
- data/lib/activerecord-multi-tenant/habtm.rb +50 -0
- data/lib/activerecord-multi-tenant/migrations.rb +18 -8
- data/lib/activerecord-multi-tenant/model_extensions.rb +78 -37
- data/lib/activerecord-multi-tenant/multi_tenant.rb +40 -21
- data/lib/activerecord-multi-tenant/query_monitor.rb +21 -5
- data/lib/activerecord-multi-tenant/query_rewriter.rb +111 -80
- data/lib/activerecord-multi-tenant/sidekiq.rb +31 -20
- data/lib/activerecord-multi-tenant/version.rb +1 -1
- data/lib/activerecord-multi-tenant.rb +3 -12
- data/lib/activerecord_multi_tenant.rb +12 -0
- data/spec/activerecord-multi-tenant/associations_spec.rb +21 -0
- data/spec/activerecord-multi-tenant/controller_extensions_spec.rb +3 -2
- data/spec/activerecord-multi-tenant/fast_truncate_spec.rb +8 -6
- data/spec/activerecord-multi-tenant/model_extensions_spec.rb +233 -153
- data/spec/activerecord-multi-tenant/multi_tenant_spec.rb +15 -13
- data/spec/activerecord-multi-tenant/query_rewriter_spec.rb +60 -59
- data/spec/activerecord-multi-tenant/record_callback_spec.rb +0 -0
- data/spec/activerecord-multi-tenant/record_finding_spec.rb +11 -11
- data/spec/activerecord-multi-tenant/record_modifications_spec.rb +4 -4
- data/spec/activerecord-multi-tenant/sidekiq_spec.rb +10 -10
- data/spec/database.yml +0 -0
- data/spec/schema.rb +20 -2
- data/spec/spec_helper.rb +46 -17
- data/spec/support/format_sql.rb +20 -0
- metadata +130 -25
- data/.github/workflows/CI.yml +0 -47
- data/gemfiles/.bundle/config +0 -2
- data/gemfiles/active_record_6.0.gemfile +0 -8
- data/gemfiles/active_record_6.1.gemfile +0 -8
- data/gemfiles/active_record_7.0.gemfile +0 -8
- data/gemfiles/rails_6.0.gemfile +0 -8
- data/gemfiles/rails_6.1.gemfile +0 -8
- data/gemfiles/rails_7.0.gemfile +0 -8
- data/lib/activerecord-multi-tenant/with_lock.rb +0 -15
- data/spec/activerecord-multi-tenant/schema_dumper_tester.rb +0 -0
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
require_relative './multi_tenant'
|
|
2
2
|
|
|
3
3
|
module MultiTenant
|
|
4
|
+
# Extension to the model to allow scoping of models to the current tenant. This is done by adding
|
|
5
|
+
# the multitenant method to the models that need to be scoped. This method is called in the
|
|
6
|
+
# model declaration.
|
|
7
|
+
# Adds scoped_by_tenant? partition_key, primary_key and inherited methods to the model
|
|
4
8
|
module ModelExtensionsClassMethods
|
|
5
9
|
DEFAULT_ID_FIELD = 'id'.freeze
|
|
6
|
-
|
|
10
|
+
# executes when multi_tenant method is called in the model. This method adds the following
|
|
11
|
+
# methods to the model that calls it.
|
|
12
|
+
# scoped_by_tenant? - returns true if the model is scoped by tenant
|
|
13
|
+
# partition_key - returns the partition key for the model
|
|
14
|
+
# primary_key - returns the primary key for the model
|
|
15
|
+
#
|
|
7
16
|
def multi_tenant(tenant_name, options = {})
|
|
8
17
|
if to_s.underscore.to_sym == tenant_name || (!table_name.nil? && table_name.singularize.to_sym == tenant_name)
|
|
9
18
|
unless MultiTenant.with_write_only_mode_enabled?
|
|
10
19
|
# This is the tenant model itself. Workaround for https://github.com/citusdata/citus/issues/687
|
|
11
|
-
before_create
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
before_create lambda {
|
|
21
|
+
id = if self.class.columns_hash[self.class.primary_key].type == :uuid
|
|
22
|
+
SecureRandom.uuid
|
|
23
|
+
else
|
|
24
|
+
self.class.connection.select_value(
|
|
25
|
+
"SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
self.id ||= id
|
|
29
|
+
}
|
|
18
30
|
end
|
|
19
31
|
else
|
|
20
32
|
class << self
|
|
@@ -24,24 +36,23 @@ module MultiTenant
|
|
|
24
36
|
|
|
25
37
|
# Allow partition_key to be set from a superclass if not already set in this class
|
|
26
38
|
def partition_key
|
|
27
|
-
@partition_key ||= ancestors.detect{ |k| k.instance_variable_get(:@partition_key) }
|
|
28
|
-
|
|
39
|
+
@partition_key ||= ancestors.detect { |k| k.instance_variable_get(:@partition_key) }
|
|
40
|
+
.try(:instance_variable_get, :@partition_key)
|
|
29
41
|
end
|
|
30
42
|
|
|
31
43
|
# Avoid primary_key errors when using composite primary keys (e.g. id, tenant_id)
|
|
32
44
|
def primary_key
|
|
33
|
-
|
|
45
|
+
if defined?(PRIMARY_KEY_NOT_SET) ? !PRIMARY_KEY_NOT_SET.equal?(@primary_key) : @primary_key
|
|
46
|
+
return @primary_key
|
|
47
|
+
end
|
|
34
48
|
|
|
35
49
|
primary_object_keys = Array.wrap(connection.schema_cache.primary_keys(table_name)) - [partition_key]
|
|
36
50
|
|
|
37
|
-
if primary_object_keys.size == 1
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# table without a primary key and DEFAULT_ID_FIELD is not present in the table
|
|
43
|
-
@primary_key = nil
|
|
44
|
-
end
|
|
51
|
+
@primary_key = if primary_object_keys.size == 1
|
|
52
|
+
primary_object_keys.first
|
|
53
|
+
elsif connection.schema_cache.columns_hash(table_name).include? DEFAULT_ID_FIELD
|
|
54
|
+
DEFAULT_ID_FIELD
|
|
55
|
+
end
|
|
45
56
|
end
|
|
46
57
|
|
|
47
58
|
def inherited(subclass)
|
|
@@ -57,41 +68,55 @@ module MultiTenant
|
|
|
57
68
|
|
|
58
69
|
# Create an implicit belongs_to association only if tenant class exists
|
|
59
70
|
if MultiTenant.tenant_klass_defined?(tenant_name)
|
|
60
|
-
belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional)
|
|
71
|
+
belongs_to tenant_name, **options.slice(:class_name, :inverse_of, :optional)
|
|
72
|
+
.merge(foreign_key: options[:partition_key])
|
|
61
73
|
end
|
|
62
74
|
|
|
63
75
|
# New instances should have the tenant set
|
|
64
|
-
after_initialize
|
|
76
|
+
after_initialize proc { |record|
|
|
65
77
|
if MultiTenant.current_tenant_id &&
|
|
66
|
-
|
|
78
|
+
(!record.attribute_present?(partition_key) || record.public_send(partition_key.to_sym).nil?)
|
|
67
79
|
record.public_send("#{partition_key}=".to_sym, MultiTenant.current_tenant_id)
|
|
68
80
|
end
|
|
69
81
|
}
|
|
70
82
|
|
|
83
|
+
# Below block adds the following methods to the model that calls it.
|
|
84
|
+
# partition_key= - returns the partition key for the model.class << self 'partition' method defined above
|
|
85
|
+
# is the getter method. Here, there is additional check to assure that the tenant id is not changed once set
|
|
86
|
+
# tenant_name- returns the name of the tenant model. Its setter and getter methods defined separately
|
|
87
|
+
# Getter checks for the tenant association and if it is not loaded, returns the current tenant id set
|
|
88
|
+
# in the MultiTenant module
|
|
71
89
|
to_include = Module.new do
|
|
72
90
|
define_method "#{partition_key}=" do |tenant_id|
|
|
73
|
-
write_attribute(
|
|
91
|
+
write_attribute(partition_key.to_s, tenant_id)
|
|
74
92
|
|
|
75
93
|
# Rails 5 `attribute_will_change!` uses the attribute-method-call rather than `read_attribute`
|
|
76
94
|
# and will raise ActiveModel::MissingAttributeError if that column was not selected.
|
|
77
95
|
# This is rescued as NoMethodError and in MRI attribute_was is assigned an arbitrary Object
|
|
78
96
|
was = send("#{partition_key}_was")
|
|
79
|
-
was_nil_or_skipped = was.nil? || was.
|
|
97
|
+
was_nil_or_skipped = was.nil? || was.instance_of?(Object)
|
|
98
|
+
|
|
99
|
+
if send("#{partition_key}_changed?") && persisted? && !was_nil_or_skipped
|
|
100
|
+
raise MultiTenant::TenantIsImmutable
|
|
101
|
+
end
|
|
80
102
|
|
|
81
|
-
raise MultiTenant::TenantIsImmutable if send("#{partition_key}_changed?") && persisted? && !was_nil_or_skipped
|
|
82
103
|
tenant_id
|
|
83
104
|
end
|
|
84
105
|
|
|
85
106
|
if MultiTenant.tenant_klass_defined?(tenant_name)
|
|
86
107
|
define_method "#{tenant_name}=" do |model|
|
|
87
108
|
super(model)
|
|
88
|
-
|
|
109
|
+
if send("#{partition_key}_changed?") && persisted? && !send("#{partition_key}_was").nil?
|
|
110
|
+
raise MultiTenant::TenantIsImmutable
|
|
111
|
+
end
|
|
112
|
+
|
|
89
113
|
model
|
|
90
114
|
end
|
|
91
115
|
|
|
92
|
-
define_method
|
|
93
|
-
if !association(tenant_name.to_sym).loaded? && !MultiTenant.current_tenant_is_id? &&
|
|
94
|
-
|
|
116
|
+
define_method tenant_name.to_s do
|
|
117
|
+
if !association(tenant_name.to_sym).loaded? && !MultiTenant.current_tenant_is_id? &&
|
|
118
|
+
MultiTenant.current_tenant_id && public_send(partition_key) == MultiTenant.current_tenant_id
|
|
119
|
+
MultiTenant.current_tenant
|
|
95
120
|
else
|
|
96
121
|
super()
|
|
97
122
|
end
|
|
@@ -100,7 +125,10 @@ module MultiTenant
|
|
|
100
125
|
end
|
|
101
126
|
include to_include
|
|
102
127
|
|
|
103
|
-
|
|
128
|
+
# Below blocks sets tenant_id for the current session with the tenant_id of the record
|
|
129
|
+
# If the tenant is not set for the `session.After` the save operation current session tenant is set to nil
|
|
130
|
+
# If tenant is set for the session, save operation is performed as it is
|
|
131
|
+
around_save lambda { |record, block|
|
|
104
132
|
record_tenant = record.attribute_was(partition_key)
|
|
105
133
|
if persisted? && MultiTenant.current_tenant_id.nil? && !record_tenant.nil?
|
|
106
134
|
MultiTenant.with(record.public_send(partition_key)) { block.call }
|
|
@@ -109,7 +137,7 @@ module MultiTenant
|
|
|
109
137
|
end
|
|
110
138
|
}
|
|
111
139
|
|
|
112
|
-
around_update
|
|
140
|
+
around_update lambda { |record, block|
|
|
113
141
|
record_tenant = record.attribute_was(partition_key)
|
|
114
142
|
if MultiTenant.current_tenant_id.nil? && !record_tenant.nil?
|
|
115
143
|
MultiTenant.with(record.public_send(partition_key)) { block.call }
|
|
@@ -118,7 +146,7 @@ module MultiTenant
|
|
|
118
146
|
end
|
|
119
147
|
}
|
|
120
148
|
|
|
121
|
-
around_destroy
|
|
149
|
+
around_destroy lambda { |record, block|
|
|
122
150
|
if MultiTenant.current_tenant_id.nil?
|
|
123
151
|
MultiTenant.with(record.public_send(partition_key)) { block.call }
|
|
124
152
|
else
|
|
@@ -130,26 +158,39 @@ module MultiTenant
|
|
|
130
158
|
end
|
|
131
159
|
end
|
|
132
160
|
|
|
161
|
+
# Below code block is executed on Model, Associations and CollectionProxy objects
|
|
162
|
+
# when ActiveRecord is loaded and decorates defined methods with MultiTenant.with function.
|
|
163
|
+
# Additionally, adds aliases for some operators.
|
|
133
164
|
ActiveSupport.on_load(:active_record) do |base|
|
|
134
165
|
base.extend MultiTenant::ModelExtensionsClassMethods
|
|
135
166
|
|
|
136
|
-
# Ensure we have current_tenant_id in where clause when a cached ActiveRecord instance is being reloaded,
|
|
167
|
+
# Ensure we have current_tenant_id in where clause when a cached ActiveRecord instance is being reloaded,
|
|
168
|
+
# or update_columns without callbacks is called
|
|
137
169
|
MultiTenant.wrap_methods(ActiveRecord::Base, 'self', :delete, :reload, :update_columns)
|
|
138
170
|
|
|
139
171
|
# Any queuries fired for fetching a singular association have the correct current_tenant_id in WHERE clause
|
|
140
172
|
# reload is called anytime any record's association is accessed
|
|
141
173
|
MultiTenant.wrap_methods(ActiveRecord::Associations::Association, 'owner', :reload)
|
|
142
174
|
|
|
143
|
-
# For collection associations, we need to wrap multiple methods in returned proxy so that
|
|
144
|
-
|
|
145
|
-
ActiveRecord::Associations::CollectionProxy.alias_method
|
|
146
|
-
|
|
175
|
+
# For collection associations, we need to wrap multiple methods in returned proxy so that
|
|
176
|
+
# any queries have the correct current_tenant_id in WHERE clause
|
|
177
|
+
ActiveRecord::Associations::CollectionProxy.alias_method \
|
|
178
|
+
:equals_mt, :== # Hack to prevent syntax error due to invalid method name
|
|
179
|
+
ActiveRecord::Associations::CollectionProxy.alias_method \
|
|
180
|
+
:append_mt, :<< # Hack to prevent syntax error due to invalid method name
|
|
181
|
+
MultiTenant.wrap_methods(ActiveRecord::Associations::CollectionProxy, '@association.owner',
|
|
182
|
+
:find, :last, :take, :build, :create, :create!, :replace, :delete_all,
|
|
183
|
+
:destroy_all, :delete, :destroy, :calculate, :pluck, :size, :empty?, :include?, :equals_mt,
|
|
184
|
+
:records, :append_mt, :find_nth_with_limit, :find_nth_from_last, :null_scope?,
|
|
185
|
+
:find_from_target?, :exec_queries)
|
|
147
186
|
ActiveRecord::Associations::CollectionProxy.alias_method :==, :equals_mt
|
|
148
187
|
ActiveRecord::Associations::CollectionProxy.alias_method :<<, :append_mt
|
|
149
188
|
end
|
|
150
189
|
|
|
190
|
+
# skips statement caching for classes that is Multi-tenant or has a multi-tenant relation
|
|
151
191
|
class ActiveRecord::Associations::Association
|
|
152
192
|
alias skip_statement_cache_orig skip_statement_cache?
|
|
193
|
+
|
|
153
194
|
def skip_statement_cache?(*scope)
|
|
154
195
|
return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant?
|
|
155
196
|
|
|
@@ -10,22 +10,28 @@ module MultiTenant
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def self.partition_key(tenant_name)
|
|
13
|
-
"#{tenant_name
|
|
13
|
+
"#{tenant_name}_id"
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
# rubocop:disable Style/ClassVars
|
|
16
17
|
# In some cases we only have an ID - if defined we'll return the default tenant class in such cases
|
|
17
|
-
def self.default_tenant_class=(tenant_class)
|
|
18
|
-
|
|
18
|
+
def self.default_tenant_class=(tenant_class)
|
|
19
|
+
@@default_tenant_class = tenant_class
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.default_tenant_class
|
|
23
|
+
@@default_tenant_class ||= nil
|
|
24
|
+
end
|
|
19
25
|
|
|
20
26
|
# Write-only Mode - this only adds the tenant_id to new records, but doesn't
|
|
21
27
|
# require its presence for SELECTs/UPDATEs/DELETEs
|
|
22
|
-
def self.enable_write_only_mode
|
|
23
|
-
|
|
28
|
+
def self.enable_write_only_mode
|
|
29
|
+
@@enable_write_only_mode = true
|
|
30
|
+
end
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def self.with_lock_workaround_enabled?; @@enable_with_lock_workaround; end
|
|
32
|
+
def self.with_write_only_mode_enabled?
|
|
33
|
+
@@enable_write_only_mode ||= false
|
|
34
|
+
end
|
|
29
35
|
|
|
30
36
|
# Registry that maps table names to models (used by the query rewriter)
|
|
31
37
|
def self.register_multi_tenant_model(model_klass)
|
|
@@ -38,17 +44,19 @@ module MultiTenant
|
|
|
38
44
|
def self.multi_tenant_model_for_table(table_name)
|
|
39
45
|
@@multi_tenant_models ||= []
|
|
40
46
|
|
|
41
|
-
|
|
42
|
-
@@multi_tenant_model_table_names = @@multi_tenant_models.map
|
|
47
|
+
unless defined?(@@multi_tenant_model_table_names)
|
|
48
|
+
@@multi_tenant_model_table_names = @@multi_tenant_models.map do |model|
|
|
43
49
|
[model.table_name, model] if model.table_name
|
|
44
|
-
|
|
50
|
+
end.compact.to_h
|
|
45
51
|
end
|
|
46
52
|
|
|
47
53
|
@@multi_tenant_model_table_names[table_name.to_s]
|
|
54
|
+
# rubocop:enable Style/ClassVars
|
|
48
55
|
end
|
|
49
56
|
|
|
50
57
|
def self.multi_tenant_model_for_arel(arel)
|
|
51
58
|
return nil unless arel.respond_to?(:ast)
|
|
59
|
+
|
|
52
60
|
if arel.ast.relation.is_a? Arel::Nodes::JoinSource
|
|
53
61
|
MultiTenant.multi_tenant_model_for_table(arel.ast.relation.left.table_name)
|
|
54
62
|
else
|
|
@@ -74,7 +82,7 @@ module MultiTenant
|
|
|
74
82
|
|
|
75
83
|
def self.current_tenant_class
|
|
76
84
|
if current_tenant_is_id?
|
|
77
|
-
MultiTenant.default_tenant_class ||
|
|
85
|
+
MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set')
|
|
78
86
|
elsif current_tenant
|
|
79
87
|
MultiTenant.current_tenant.class.name
|
|
80
88
|
end
|
|
@@ -83,33 +91,40 @@ module MultiTenant
|
|
|
83
91
|
def self.load_current_tenant!
|
|
84
92
|
return MultiTenant.current_tenant if MultiTenant.current_tenant && !current_tenant_is_id?
|
|
85
93
|
raise 'MultiTenant.current_tenant must be set to load' if MultiTenant.current_tenant.nil?
|
|
86
|
-
|
|
94
|
+
|
|
95
|
+
klass = MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set')
|
|
87
96
|
self.current_tenant = klass.find(MultiTenant.current_tenant_id)
|
|
88
97
|
end
|
|
89
98
|
|
|
90
99
|
def self.with(tenant, &block)
|
|
91
|
-
return block.call if
|
|
92
|
-
|
|
100
|
+
return block.call if current_tenant == tenant
|
|
101
|
+
|
|
102
|
+
old_tenant = current_tenant
|
|
93
103
|
begin
|
|
94
104
|
self.current_tenant = tenant
|
|
95
|
-
|
|
105
|
+
block.call
|
|
96
106
|
ensure
|
|
97
107
|
self.current_tenant = old_tenant
|
|
98
108
|
end
|
|
99
109
|
end
|
|
100
110
|
|
|
101
111
|
def self.without(&block)
|
|
102
|
-
return block.call if
|
|
103
|
-
|
|
112
|
+
return block.call if current_tenant.nil?
|
|
113
|
+
|
|
114
|
+
old_tenant = current_tenant
|
|
104
115
|
begin
|
|
105
116
|
self.current_tenant = nil
|
|
106
|
-
|
|
117
|
+
block.call
|
|
107
118
|
ensure
|
|
108
119
|
self.current_tenant = old_tenant
|
|
109
120
|
end
|
|
110
121
|
end
|
|
111
122
|
|
|
112
|
-
# Wrap calls to any of `method_names` on an instance Class `klass` with MultiTenant.with
|
|
123
|
+
# Wrap calls to any of `method_names` on an instance Class `klass` with MultiTenant.with
|
|
124
|
+
# when `'owner'` (evaluated in context of the klass instance) is a ActiveRecord model instance that is multi-tenant
|
|
125
|
+
# Instruments the methods provided with previously set Multitenant parameters
|
|
126
|
+
# In Ruby 2 using splat (*) operator with `&block` is not supported, so we need to use `method(...)` syntax
|
|
127
|
+
# TODO: Could not understand the use of owner here. Need to check
|
|
113
128
|
if Gem::Version.create(RUBY_VERSION) < Gem::Version.new('3.0.0')
|
|
114
129
|
def self.wrap_methods(klass, owner, *method_names)
|
|
115
130
|
method_names.each do |method_name|
|
|
@@ -147,6 +162,10 @@ module MultiTenant
|
|
|
147
162
|
# Preserve backward compatibility for people using .with_id
|
|
148
163
|
singleton_class.send(:alias_method, :with_id, :with)
|
|
149
164
|
|
|
165
|
+
# This exception is raised when a there is an attempt to change tenant
|
|
150
166
|
class TenantIsImmutable < StandardError
|
|
151
167
|
end
|
|
168
|
+
|
|
169
|
+
class MissingTenantError < StandardError
|
|
170
|
+
end
|
|
152
171
|
end
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
# Add generic warning when queries fail and there is no tenant set
|
|
2
|
+
# To handle this case, a QueryMonitor hook is created and registered
|
|
3
|
+
# to sql.active_record. This hook will log a warning when a query fails
|
|
4
|
+
# This hook is executed after the query is executed.
|
|
2
5
|
module MultiTenant
|
|
6
|
+
# rubocop:disable Style/ClassVars
|
|
3
7
|
# Option to enable query monitor
|
|
4
8
|
@@enable_query_monitor = false
|
|
5
|
-
def self.enable_query_monitor; @@enable_query_monitor = true; end
|
|
6
|
-
def self.query_monitor_enabled?; @@enable_query_monitor; end
|
|
7
9
|
|
|
10
|
+
def self.enable_query_monitor
|
|
11
|
+
@@enable_query_monitor = true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.query_monitor_enabled?
|
|
15
|
+
@@enable_query_monitor
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# rubocop:enable Style/ClassVars
|
|
19
|
+
# QueryMonitor class to log a warning when a query fails and there is no tenant set
|
|
20
|
+
# start and finish methods are required to be register sql.active_record hook
|
|
8
21
|
class QueryMonitor
|
|
9
|
-
def start(
|
|
10
|
-
|
|
22
|
+
def start(_name, _id, _payload) end
|
|
23
|
+
|
|
24
|
+
def finish(_name, _id, payload)
|
|
11
25
|
return unless MultiTenant.query_monitor_enabled?
|
|
26
|
+
|
|
12
27
|
return unless payload[:exception].present? && MultiTenant.current_tenant_id.nil?
|
|
28
|
+
|
|
13
29
|
Rails.logger.info 'WARNING: Tenant not present - make sure to add MultiTenant.with(tenant) { ... }'
|
|
14
30
|
end
|
|
15
31
|
end
|
|
16
32
|
end
|
|
17
|
-
|
|
33
|
+
# Actual code to register the hook.
|
|
18
34
|
ActiveSupport::Notifications.subscribe('sql.active_record', MultiTenant::QueryMonitor.new)
|