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.
Files changed (120) 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 -0
  8. data/CHANGELOG.md +6 -0
  9. data/Gemfile +3 -1
  10. data/LICENSE +0 -0
  11. data/README.md +2 -1
  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 +18 -8
  88. data/lib/activerecord-multi-tenant/model_extensions.rb +78 -37
  89. data/lib/activerecord-multi-tenant/multi_tenant.rb +40 -21
  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 +233 -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 +130 -25
  111. data/.github/workflows/CI.yml +0 -47
  112. data/gemfiles/.bundle/config +0 -2
  113. data/gemfiles/active_record_6.0.gemfile +0 -8
  114. data/gemfiles/active_record_6.1.gemfile +0 -8
  115. data/gemfiles/active_record_7.0.gemfile +0 -8
  116. data/gemfiles/rails_6.0.gemfile +0 -8
  117. data/gemfiles/rails_6.1.gemfile +0 -8
  118. data/gemfiles/rails_7.0.gemfile +0 -8
  119. data/lib/activerecord-multi-tenant/with_lock.rb +0 -15
  120. 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,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).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
96
  was = send("#{partition_key}_was")
79
- 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
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
- 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
+
89
113
  model
90
114
  end
91
115
 
92
- define_method "#{tenant_name}" do
93
- if !association(tenant_name.to_sym).loaded? && !MultiTenant.current_tenant_is_id? && MultiTenant.current_tenant_id && public_send(partition_key) == MultiTenant.current_tenant_id
94
- 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
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
- around_save -> (record, block) {
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 -> (record, block) {
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 -> (record, block) {
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, 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
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 any queries have the correct current_tenant_id in WHERE clause
144
- ActiveRecord::Associations::CollectionProxy.alias_method :equals_mt, :== # Hack to prevent syntax error due to invalid method name
145
- ActiveRecord::Associations::CollectionProxy.alias_method :append_mt, :<< # Hack to prevent syntax error due to invalid method name
146
- 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)
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.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|
@@ -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)