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.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/active-record-multi-tenant-tests.yml +80 -0
  3. data/.gitignore +6 -0
  4. data/.readthedocs.yaml +15 -0
  5. data/.rspec +0 -0
  6. data/.rubocop.yml +51 -0
  7. data/Appraisals +0 -24
  8. data/CHANGELOG.md +12 -0
  9. data/Gemfile +3 -1
  10. data/LICENSE +0 -0
  11. data/README.md +3 -2
  12. data/Rakefile +1 -1
  13. data/activerecord-multi-tenant.gemspec +28 -22
  14. data/docker-compose.yml +24 -18
  15. data/docs/.gitignore +3 -0
  16. data/docs/Makefile +28 -0
  17. data/docs/api-reference.sh +10 -0
  18. data/docs/requirements.in +4 -0
  19. data/docs/requirements.txt +62 -0
  20. data/docs/source/_static/api-reference/ActiveRecord/Associations/Association.html +285 -0
  21. data/docs/source/_static/api-reference/ActiveRecord/Associations/ClassMethods.html +255 -0
  22. data/docs/source/_static/api-reference/ActiveRecord/Associations.html +117 -0
  23. data/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters/SchemaStatements.html +232 -0
  24. data/docs/source/_static/api-reference/ActiveRecord/ConnectionAdapters.html +126 -0
  25. data/docs/source/_static/api-reference/ActiveRecord/QueryMethods.html +336 -0
  26. data/docs/source/_static/api-reference/ActiveRecord/SchemaDumper.html +121 -0
  27. data/docs/source/_static/api-reference/ActiveRecord.html +130 -0
  28. data/docs/source/_static/api-reference/MultiTenant/ArelTenantVisitor.html +755 -0
  29. data/docs/source/_static/api-reference/MultiTenant/ArelVisitorsDepthFirst.html +208 -0
  30. data/docs/source/_static/api-reference/MultiTenant/BaseTenantEnforcementClause.html +462 -0
  31. data/docs/source/_static/api-reference/MultiTenant/Context.html +659 -0
  32. data/docs/source/_static/api-reference/MultiTenant/ControllerExtensions.html +202 -0
  33. data/docs/source/_static/api-reference/MultiTenant/CopyFromClient.html +186 -0
  34. data/docs/source/_static/api-reference/MultiTenant/CopyFromClientHelper.html +362 -0
  35. data/docs/source/_static/api-reference/MultiTenant/Current.html +124 -0
  36. data/docs/source/_static/api-reference/MultiTenant/DatabaseStatements.html +366 -0
  37. data/docs/source/_static/api-reference/MultiTenant/FastTruncate.html +226 -0
  38. data/docs/source/_static/api-reference/MultiTenant/MigrationExtensions.html +554 -0
  39. data/docs/source/_static/api-reference/MultiTenant/MissingTenantError.html +124 -0
  40. data/docs/source/_static/api-reference/MultiTenant/ModelExtensionsClassMethods.html +492 -0
  41. data/docs/source/_static/api-reference/MultiTenant/QueryMonitor.html +257 -0
  42. data/docs/source/_static/api-reference/MultiTenant/Table.html +419 -0
  43. data/docs/source/_static/api-reference/MultiTenant/TenantEnforcementClause.html +148 -0
  44. data/docs/source/_static/api-reference/MultiTenant/TenantIsImmutable.html +135 -0
  45. data/docs/source/_static/api-reference/MultiTenant/TenantJoinEnforcementClause.html +310 -0
  46. data/docs/source/_static/api-reference/MultiTenant/TenantValueVisitor.html +239 -0
  47. data/docs/source/_static/api-reference/MultiTenant.html +1454 -0
  48. data/docs/source/_static/api-reference/MultiTenantFindBy.html +180 -0
  49. data/docs/source/_static/api-reference/Sidekiq/Client.html +302 -0
  50. data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Client.html +217 -0
  51. data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant/Server.html +219 -0
  52. data/docs/source/_static/api-reference/Sidekiq/Middleware/MultiTenant.html +126 -0
  53. data/docs/source/_static/api-reference/Sidekiq.html +126 -0
  54. data/docs/source/_static/api-reference/_index.html +399 -0
  55. data/docs/source/_static/api-reference/class_list.html +51 -0
  56. data/docs/source/_static/api-reference/css/common.css +1 -0
  57. data/docs/source/_static/api-reference/css/full_list.css +58 -0
  58. data/docs/source/_static/api-reference/css/style.css +497 -0
  59. data/docs/source/_static/api-reference/file.README.html +167 -0
  60. data/docs/source/_static/api-reference/file_list.html +56 -0
  61. data/docs/source/_static/api-reference/frames.html +17 -0
  62. data/docs/source/_static/api-reference/index.html +167 -0
  63. data/docs/source/_static/api-reference/js/app.js +314 -0
  64. data/docs/source/_static/api-reference/js/full_list.js +216 -0
  65. data/docs/source/_static/api-reference/js/jquery.js +4 -0
  66. data/docs/source/_static/api-reference/method_list.html +715 -0
  67. data/docs/source/_static/api-reference/top-level-namespace.html +126 -0
  68. data/docs/source/_templates/.gitignore +4 -0
  69. data/docs/source/api-reference.rst +8 -0
  70. data/docs/source/appendix.rst +26 -0
  71. data/docs/source/changelog.rst +8 -0
  72. data/docs/source/community-and-support.rst +26 -0
  73. data/docs/source/conf.py +30 -0
  74. data/docs/source/contributing.rst +70 -0
  75. data/docs/source/getting-started.rst +37 -0
  76. data/docs/source/guides-and-tutorials.rst +129 -0
  77. data/docs/source/index.rst +54 -0
  78. data/docs/source/introduction.rst +33 -0
  79. data/docs/source/license.rst +22 -0
  80. data/docs/source/troubleshooting.rst +41 -0
  81. data/docs/source/usage-guide.rst +59 -0
  82. data/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb +183 -174
  83. data/lib/activerecord-multi-tenant/controller_extensions.rb +15 -4
  84. data/lib/activerecord-multi-tenant/copy_from_client.rb +4 -0
  85. data/lib/activerecord-multi-tenant/fast_truncate.rb +4 -2
  86. data/lib/activerecord-multi-tenant/habtm.rb +50 -0
  87. data/lib/activerecord-multi-tenant/migrations.rb +19 -9
  88. data/lib/activerecord-multi-tenant/model_extensions.rb +82 -40
  89. data/lib/activerecord-multi-tenant/multi_tenant.rb +42 -23
  90. data/lib/activerecord-multi-tenant/query_monitor.rb +21 -5
  91. data/lib/activerecord-multi-tenant/query_rewriter.rb +111 -80
  92. data/lib/activerecord-multi-tenant/sidekiq.rb +31 -20
  93. data/lib/activerecord-multi-tenant/version.rb +1 -1
  94. data/lib/activerecord-multi-tenant.rb +3 -12
  95. data/lib/activerecord_multi_tenant.rb +12 -0
  96. data/spec/activerecord-multi-tenant/associations_spec.rb +21 -0
  97. data/spec/activerecord-multi-tenant/controller_extensions_spec.rb +3 -2
  98. data/spec/activerecord-multi-tenant/fast_truncate_spec.rb +8 -6
  99. data/spec/activerecord-multi-tenant/model_extensions_spec.rb +243 -153
  100. data/spec/activerecord-multi-tenant/multi_tenant_spec.rb +15 -13
  101. data/spec/activerecord-multi-tenant/query_rewriter_spec.rb +60 -59
  102. data/spec/activerecord-multi-tenant/record_callback_spec.rb +0 -0
  103. data/spec/activerecord-multi-tenant/record_finding_spec.rb +11 -11
  104. data/spec/activerecord-multi-tenant/record_modifications_spec.rb +4 -4
  105. data/spec/activerecord-multi-tenant/sidekiq_spec.rb +10 -10
  106. data/spec/database.yml +0 -0
  107. data/spec/schema.rb +20 -2
  108. data/spec/spec_helper.rb +46 -17
  109. data/spec/support/format_sql.rb +20 -0
  110. metadata +132 -29
  111. data/.github/workflows/CI.yml +0 -63
  112. data/gemfiles/.bundle/config +0 -2
  113. data/gemfiles/active_record_5.2.gemfile +0 -16
  114. data/gemfiles/active_record_6.0.gemfile +0 -8
  115. data/gemfiles/active_record_6.1.gemfile +0 -8
  116. data/gemfiles/active_record_7.0.gemfile +0 -8
  117. data/gemfiles/rails_5.2.gemfile +0 -16
  118. data/gemfiles/rails_6.0.gemfile +0 -8
  119. data/gemfiles/rails_6.1.gemfile +0 -8
  120. data/gemfiles/rails_7.0.gemfile +0 -8
  121. data/lib/activerecord-multi-tenant/with_lock.rb +0 -15
  122. 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 -> do
12
- if self.class.columns_hash[self.class.primary_key].type == :uuid
13
- self.id ||= SecureRandom.uuid
14
- else
15
- self.id ||= self.class.connection.select_value("SELECT nextval('#{self.class.table_name}_#{self.class.primary_key}_seq'::regclass)")
16
- end
17
- end
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
- .try(:instance_variable_get, :@partition_key)
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
- return @primary_key if @primary_key
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
- @primary_key = primary_object_keys.first
39
- elsif connection.schema_cache.columns_hash(table_name).include? DEFAULT_ID_FIELD
40
- @primary_key = DEFAULT_ID_FIELD
41
- else
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).merge(foreign_key: options[:partition_key])
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 Proc.new { |record|
76
+ after_initialize proc { |record|
65
77
  if MultiTenant.current_tenant_id &&
66
- (!record.attribute_present?(partition_key) || record.public_send(partition_key.to_sym).nil?)
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("#{partition_key}", tenant_id)
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.class == Object
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
- raise MultiTenant::TenantIsImmutable if send("#{partition_key}_changed?") && persisted? && !send("#{partition_key}_was").nil?
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 "#{tenant_name}" do
94
- if !association(tenant_name.to_sym).loaded? && !MultiTenant.current_tenant_is_id? && MultiTenant.current_tenant_id && public_send(partition_key) == MultiTenant.current_tenant_id
95
- return MultiTenant.current_tenant
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
- around_save -> (record, block) {
105
- if persisted? && MultiTenant.current_tenant_id.nil?
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 -> (record, block) {
113
- if MultiTenant.current_tenant_id.nil?
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 -> (record, block) {
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, or update_columns without callbacks is called
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 any queries have the correct current_tenant_id in WHERE clause
143
- ActiveRecord::Associations::CollectionProxy.alias_method :equals_mt, :== # Hack to prevent syntax error due to invalid method name
144
- ActiveRecord::Associations::CollectionProxy.alias_method :append_mt, :<< # Hack to prevent syntax error due to invalid method name
145
- MultiTenant.wrap_methods(ActiveRecord::Associations::CollectionProxy, '@association.owner', :find, :last, :take, :build, :create, :create!, :replace, :delete_all, :destroy_all, :delete, :destroy, :calculate, :pluck, :size, :empty?, :include?, :equals_mt, :records, :append_mt, :find_nth_with_limit, :find_nth_from_last, :null_scope?, :find_from_target?, :exec_queries)
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.to_s}_id"
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); @@default_tenant_class = tenant_class; end
18
- def self.default_tenant_class; @@default_tenant_class ||= nil; end
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; @@enable_write_only_mode = true; end
23
- def self.with_write_only_mode_enabled?; @@enable_write_only_mode ||= false; end
28
+ def self.enable_write_only_mode
29
+ @@enable_write_only_mode = true
30
+ end
24
31
 
25
- # Workaroud to make "with_lock" work until https://github.com/citusdata/citus/issues/1236 is fixed
26
- @@enable_with_lock_workaround = false
27
- def self.enable_with_lock_workaround; @@enable_with_lock_workaround = true; end
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
- if !defined?(@@multi_tenant_model_table_names)
42
- @@multi_tenant_model_table_names = @@multi_tenant_models.map { |model|
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
- }.compact.to_h
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 || fail('Only have tenant id, and no default tenant class set')
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
- klass = MultiTenant.default_tenant_class || fail('Only have tenant id, and no default tenant class set')
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 self.current_tenant == tenant
92
- old_tenant = self.current_tenant
100
+ return block.call if current_tenant == tenant
101
+
102
+ old_tenant = current_tenant
93
103
  begin
94
104
  self.current_tenant = tenant
95
- return block.call
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 self.current_tenant.nil?
103
- old_tenant = self.current_tenant
112
+ return block.call if current_tenant.nil?
113
+
114
+ old_tenant = current_tenant
104
115
  begin
105
116
  self.current_tenant = nil
106
- return block.call
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 when `'owner'` (evaluated in context of the klass instance) is a ActiveRecord model instance that is multi-tenant
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(name, id, payload); end
10
- def finish(name, id, payload)
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)