activerecord-multi-tenant 2.2.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/active-record-multi-tenant-tests.yml +83 -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 +8 -0
  8. data/CHANGELOG.md +13 -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 +80 -39
  89. data/lib/activerecord-multi-tenant/multi_tenant.rb +49 -25
  90. data/lib/activerecord-multi-tenant/query_monitor.rb +21 -5
  91. data/lib/activerecord-multi-tenant/query_rewriter.rb +118 -85
  92. data/lib/activerecord-multi-tenant/sidekiq.rb +31 -20
  93. data/lib/activerecord-multi-tenant/table_node.rb +13 -0
  94. data/lib/activerecord-multi-tenant/version.rb +1 -1
  95. data/lib/activerecord-multi-tenant.rb +3 -12
  96. data/lib/activerecord_multi_tenant.rb +13 -0
  97. data/spec/activerecord-multi-tenant/associations_spec.rb +21 -0
  98. data/spec/activerecord-multi-tenant/controller_extensions_spec.rb +3 -2
  99. data/spec/activerecord-multi-tenant/fast_truncate_spec.rb +8 -6
  100. data/spec/activerecord-multi-tenant/model_extensions_spec.rb +233 -153
  101. data/spec/activerecord-multi-tenant/multi_tenant_spec.rb +69 -13
  102. data/spec/activerecord-multi-tenant/query_rewriter_spec.rb +60 -59
  103. data/spec/activerecord-multi-tenant/record_callback_spec.rb +0 -0
  104. data/spec/activerecord-multi-tenant/record_finding_spec.rb +11 -11
  105. data/spec/activerecord-multi-tenant/record_modifications_spec.rb +4 -4
  106. data/spec/activerecord-multi-tenant/sidekiq_spec.rb +10 -10
  107. data/spec/database.yml +0 -0
  108. data/spec/schema.rb +20 -2
  109. data/spec/spec_helper.rb +46 -17
  110. data/spec/support/format_sql.rb +20 -0
  111. metadata +131 -25
  112. data/.github/workflows/CI.yml +0 -47
  113. data/gemfiles/.bundle/config +0 -2
  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_6.0.gemfile +0 -8
  118. data/gemfiles/rails_6.1.gemfile +0 -8
  119. data/gemfiles/rails_7.0.gemfile +0 -8
  120. data/lib/activerecord-multi-tenant/with_lock.rb +0 -15
  121. 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)
@@ -56,42 +67,56 @@ module MultiTenant
56
67
  partition_key = @partition_key
57
68
 
58
69
  # Create an implicit belongs_to association only if tenant class exists
59
- 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])
70
+ if MultiTenant.tenant_klass_defined?(tenant_name, options)
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
- if MultiTenant.tenant_klass_defined?(tenant_name)
106
+ if MultiTenant.tenant_klass_defined?(tenant_name, options)
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
 
@@ -5,27 +5,38 @@ module MultiTenant
5
5
  attribute :tenant
6
6
  end
7
7
 
8
- def self.tenant_klass_defined?(tenant_name)
9
- !!tenant_name.to_s.classify.safe_constantize
8
+ def self.tenant_klass_defined?(tenant_name, options = {})
9
+ class_name = if options[:class_name].present?
10
+ options[:class_name]
11
+ else
12
+ tenant_name.to_s.classify
13
+ end
14
+ !!class_name.safe_constantize
10
15
  end
11
16
 
12
17
  def self.partition_key(tenant_name)
13
- "#{tenant_name.to_s}_id"
18
+ "#{tenant_name}_id"
14
19
  end
15
20
 
21
+ # rubocop:disable Style/ClassVars
16
22
  # 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
23
+ def self.default_tenant_class=(tenant_class)
24
+ @@default_tenant_class = tenant_class
25
+ end
26
+
27
+ def self.default_tenant_class
28
+ @@default_tenant_class ||= nil
29
+ end
19
30
 
20
31
  # Write-only Mode - this only adds the tenant_id to new records, but doesn't
21
32
  # 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
33
+ def self.enable_write_only_mode
34
+ @@enable_write_only_mode = true
35
+ end
24
36
 
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
37
+ def self.with_write_only_mode_enabled?
38
+ @@enable_write_only_mode ||= false
39
+ end
29
40
 
30
41
  # Registry that maps table names to models (used by the query rewriter)
31
42
  def self.register_multi_tenant_model(model_klass)
@@ -38,21 +49,23 @@ module MultiTenant
38
49
  def self.multi_tenant_model_for_table(table_name)
39
50
  @@multi_tenant_models ||= []
40
51
 
41
- if !defined?(@@multi_tenant_model_table_names)
42
- @@multi_tenant_model_table_names = @@multi_tenant_models.map { |model|
52
+ unless defined?(@@multi_tenant_model_table_names)
53
+ @@multi_tenant_model_table_names = @@multi_tenant_models.map do |model|
43
54
  [model.table_name, model] if model.table_name
44
- }.compact.to_h
55
+ end.compact.to_h
45
56
  end
46
57
 
47
58
  @@multi_tenant_model_table_names[table_name.to_s]
59
+ # rubocop:enable Style/ClassVars
48
60
  end
49
61
 
50
62
  def self.multi_tenant_model_for_arel(arel)
51
63
  return nil unless arel.respond_to?(:ast)
64
+
52
65
  if arel.ast.relation.is_a? Arel::Nodes::JoinSource
53
- MultiTenant.multi_tenant_model_for_table(arel.ast.relation.left.table_name)
66
+ MultiTenant.multi_tenant_model_for_table(TableNode.table_name(arel.ast.relation.left))
54
67
  else
55
- MultiTenant.multi_tenant_model_for_table(arel.ast.relation.table_name)
68
+ MultiTenant.multi_tenant_model_for_table(TableNode.table_name(arel.ast.relation))
56
69
  end
57
70
  end
58
71
 
@@ -74,7 +87,7 @@ module MultiTenant
74
87
 
75
88
  def self.current_tenant_class
76
89
  if current_tenant_is_id?
77
- MultiTenant.default_tenant_class || fail('Only have tenant id, and no default tenant class set')
90
+ MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set')
78
91
  elsif current_tenant
79
92
  MultiTenant.current_tenant.class.name
80
93
  end
@@ -83,33 +96,40 @@ module MultiTenant
83
96
  def self.load_current_tenant!
84
97
  return MultiTenant.current_tenant if MultiTenant.current_tenant && !current_tenant_is_id?
85
98
  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')
99
+
100
+ klass = MultiTenant.default_tenant_class || raise('Only have tenant id, and no default tenant class set')
87
101
  self.current_tenant = klass.find(MultiTenant.current_tenant_id)
88
102
  end
89
103
 
90
104
  def self.with(tenant, &block)
91
- return block.call if self.current_tenant == tenant
92
- old_tenant = self.current_tenant
105
+ return block.call if current_tenant == tenant
106
+
107
+ old_tenant = current_tenant
93
108
  begin
94
109
  self.current_tenant = tenant
95
- return block.call
110
+ block.call
96
111
  ensure
97
112
  self.current_tenant = old_tenant
98
113
  end
99
114
  end
100
115
 
101
116
  def self.without(&block)
102
- return block.call if self.current_tenant.nil?
103
- old_tenant = self.current_tenant
117
+ return block.call if current_tenant.nil?
118
+
119
+ old_tenant = current_tenant
104
120
  begin
105
121
  self.current_tenant = nil
106
- return block.call
122
+ block.call
107
123
  ensure
108
124
  self.current_tenant = old_tenant
109
125
  end
110
126
  end
111
127
 
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
128
+ # Wrap calls to any of `method_names` on an instance Class `klass` with MultiTenant.with
129
+ # when `'owner'` (evaluated in context of the klass instance) is a ActiveRecord model instance that is multi-tenant
130
+ # Instruments the methods provided with previously set Multitenant parameters
131
+ # In Ruby 2 using splat (*) operator with `&block` is not supported, so we need to use `method(...)` syntax
132
+ # TODO: Could not understand the use of owner here. Need to check
113
133
  if Gem::Version.create(RUBY_VERSION) < Gem::Version.new('3.0.0')
114
134
  def self.wrap_methods(klass, owner, *method_names)
115
135
  method_names.each do |method_name|
@@ -147,6 +167,10 @@ module MultiTenant
147
167
  # Preserve backward compatibility for people using .with_id
148
168
  singleton_class.send(:alias_method, :with_id, :with)
149
169
 
170
+ # This exception is raised when a there is an attempt to change tenant
150
171
  class TenantIsImmutable < StandardError
151
172
  end
173
+
174
+ class MissingTenantError < StandardError
175
+ end
152
176
  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)