activerecord-multi-tenant 2.1.6 → 2.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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 -24
- data/CHANGELOG.md +12 -0
- data/Gemfile +3 -1
- data/LICENSE +0 -0
- data/README.md +3 -2
- 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 +19 -9
- data/lib/activerecord-multi-tenant/model_extensions.rb +82 -40
- data/lib/activerecord-multi-tenant/multi_tenant.rb +42 -23
- 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 +243 -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 +132 -29
- data/.github/workflows/CI.yml +0 -63
- data/gemfiles/.bundle/config +0 -2
- data/gemfiles/active_record_5.2.gemfile +0 -16
- 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_5.2.gemfile +0 -16
- 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,42 +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
|
-
# This is still true after the Rails 5.2 refactor
|
79
96
|
was = send("#{partition_key}_was")
|
80
|
-
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
|
81
102
|
|
82
|
-
raise MultiTenant::TenantIsImmutable if send("#{partition_key}_changed?") && persisted? && !was_nil_or_skipped
|
83
103
|
tenant_id
|
84
104
|
end
|
85
105
|
|
86
106
|
if MultiTenant.tenant_klass_defined?(tenant_name)
|
87
107
|
define_method "#{tenant_name}=" do |model|
|
88
108
|
super(model)
|
89
|
-
|
109
|
+
if send("#{partition_key}_changed?") && persisted? && !send("#{partition_key}_was").nil?
|
110
|
+
raise MultiTenant::TenantIsImmutable
|
111
|
+
end
|
112
|
+
|
90
113
|
model
|
91
114
|
end
|
92
115
|
|
93
|
-
define_method
|
94
|
-
if !association(tenant_name.to_sym).loaded? && !MultiTenant.current_tenant_is_id? &&
|
95
|
-
|
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
|
96
120
|
else
|
97
121
|
super()
|
98
122
|
end
|
@@ -101,23 +125,28 @@ module MultiTenant
|
|
101
125
|
end
|
102
126
|
include to_include
|
103
127
|
|
104
|
-
|
105
|
-
|
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|
|
132
|
+
record_tenant = record.attribute_was(partition_key)
|
133
|
+
if persisted? && MultiTenant.current_tenant_id.nil? && !record_tenant.nil?
|
106
134
|
MultiTenant.with(record.public_send(partition_key)) { block.call }
|
107
135
|
else
|
108
136
|
block.call
|
109
137
|
end
|
110
138
|
}
|
111
139
|
|
112
|
-
around_update
|
113
|
-
|
140
|
+
around_update lambda { |record, block|
|
141
|
+
record_tenant = record.attribute_was(partition_key)
|
142
|
+
if MultiTenant.current_tenant_id.nil? && !record_tenant.nil?
|
114
143
|
MultiTenant.with(record.public_send(partition_key)) { block.call }
|
115
144
|
else
|
116
145
|
block.call
|
117
146
|
end
|
118
147
|
}
|
119
148
|
|
120
|
-
around_destroy
|
149
|
+
around_destroy lambda { |record, block|
|
121
150
|
if MultiTenant.current_tenant_id.nil?
|
122
151
|
MultiTenant.with(record.public_send(partition_key)) { block.call }
|
123
152
|
else
|
@@ -129,26 +158,39 @@ module MultiTenant
|
|
129
158
|
end
|
130
159
|
end
|
131
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.
|
132
164
|
ActiveSupport.on_load(:active_record) do |base|
|
133
165
|
base.extend MultiTenant::ModelExtensionsClassMethods
|
134
166
|
|
135
|
-
# 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
|
136
169
|
MultiTenant.wrap_methods(ActiveRecord::Base, 'self', :delete, :reload, :update_columns)
|
137
170
|
|
138
171
|
# Any queuries fired for fetching a singular association have the correct current_tenant_id in WHERE clause
|
139
172
|
# reload is called anytime any record's association is accessed
|
140
173
|
MultiTenant.wrap_methods(ActiveRecord::Associations::Association, 'owner', :reload)
|
141
174
|
|
142
|
-
# For collection associations, we need to wrap multiple methods in returned proxy so that
|
143
|
-
|
144
|
-
ActiveRecord::Associations::CollectionProxy.alias_method
|
145
|
-
|
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)
|
146
186
|
ActiveRecord::Associations::CollectionProxy.alias_method :==, :equals_mt
|
147
187
|
ActiveRecord::Associations::CollectionProxy.alias_method :<<, :append_mt
|
148
188
|
end
|
149
189
|
|
190
|
+
# skips statement caching for classes that is Multi-tenant or has a multi-tenant relation
|
150
191
|
class ActiveRecord::Associations::Association
|
151
192
|
alias skip_statement_cache_orig skip_statement_cache?
|
193
|
+
|
152
194
|
def skip_statement_cache?(*scope)
|
153
195
|
return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant?
|
154
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|
|
@@ -117,7 +132,7 @@ module MultiTenant
|
|
117
132
|
klass.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
118
133
|
alias_method :#{original_method_name}, :#{method_name}
|
119
134
|
def #{method_name}(*args, &block)
|
120
|
-
if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil?
|
135
|
+
if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? && #{owner}.class.respond_to?(:partition_key) && #{owner}.attributes.include?(#{owner}.class.partition_key)
|
121
136
|
MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(*args, &block) }
|
122
137
|
else
|
123
138
|
#{original_method_name}(*args, &block)
|
@@ -133,7 +148,7 @@ module MultiTenant
|
|
133
148
|
klass.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
134
149
|
alias_method :#{original_method_name}, :#{method_name}
|
135
150
|
def #{method_name}(...)
|
136
|
-
if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil?
|
151
|
+
if MultiTenant.multi_tenant_model_for_table(#{owner}.class.table_name).present? && #{owner}.persisted? && MultiTenant.current_tenant_id.nil? && #{owner}.class.respond_to?(:partition_key) && #{owner}.attributes.include?(#{owner}.class.partition_key)
|
137
152
|
MultiTenant.with(#{owner}.public_send(#{owner}.class.partition_key)) { #{original_method_name}(...) }
|
138
153
|
else
|
139
154
|
#{original_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)
|