activerecord-multi-tenant 1.0.1 → 1.1.1

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/.travis.yml +8 -14
  4. data/Appraisals +28 -20
  5. data/CHANGELOG.md +29 -0
  6. data/Gemfile.lock +85 -67
  7. data/README.md +1 -1
  8. data/activerecord-multi-tenant.gemspec +1 -1
  9. data/gemfiles/.bundle/config +2 -0
  10. data/gemfiles/active_record_5.2.gemfile +10 -2
  11. data/gemfiles/active_record_5.2.gemfile.lock +104 -98
  12. data/gemfiles/{active_record_5.1.gemfile → active_record_6.0.gemfile} +2 -2
  13. data/gemfiles/active_record_6.0.gemfile.lock +198 -0
  14. data/gemfiles/{rails_4.1.gemfile → active_record_6.1.gemfile} +2 -2
  15. data/gemfiles/active_record_6.1.gemfile.lock +198 -0
  16. data/gemfiles/rails_5.2.gemfile +10 -2
  17. data/gemfiles/rails_5.2.gemfile.lock +109 -103
  18. data/gemfiles/{rails_5.0.gemfile → rails_6.0.gemfile} +2 -2
  19. data/gemfiles/rails_6.0.gemfile.lock +198 -0
  20. data/gemfiles/{rails_5.1.gemfile → rails_6.1.gemfile} +2 -2
  21. data/gemfiles/rails_6.1.gemfile.lock +198 -0
  22. data/lib/activerecord-multi-tenant.rb +1 -0
  23. data/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb +200 -0
  24. data/lib/activerecord-multi-tenant/controller_extensions.rb +2 -6
  25. data/lib/activerecord-multi-tenant/copy_from_client.rb +2 -2
  26. data/lib/activerecord-multi-tenant/migrations.rb +2 -2
  27. data/lib/activerecord-multi-tenant/model_extensions.rb +13 -14
  28. data/lib/activerecord-multi-tenant/multi_tenant.rb +9 -0
  29. data/lib/activerecord-multi-tenant/persistence_extension.rb +13 -0
  30. data/lib/activerecord-multi-tenant/query_rewriter.rb +60 -104
  31. data/lib/activerecord-multi-tenant/sidekiq.rb +2 -1
  32. data/lib/activerecord-multi-tenant/version.rb +1 -1
  33. data/spec/activerecord-multi-tenant/controller_extensions_spec.rb +19 -24
  34. data/spec/activerecord-multi-tenant/model_extensions_spec.rb +54 -104
  35. data/spec/activerecord-multi-tenant/query_rewriter_spec.rb +8 -0
  36. data/spec/activerecord-multi-tenant/record_finding_spec.rb +36 -0
  37. data/spec/activerecord-multi-tenant/record_modifications_spec.rb +60 -3
  38. data/spec/activerecord-multi-tenant/schema_dumper_tester.rb +0 -0
  39. data/spec/activerecord-multi-tenant/sidekiq_spec.rb +4 -4
  40. data/spec/schema.rb +1 -4
  41. data/spec/spec_helper.rb +1 -6
  42. metadata +17 -17
  43. data/gemfiles/active_record_5.1.gemfile.lock +0 -173
  44. data/gemfiles/rails_4.0.gemfile +0 -8
  45. data/gemfiles/rails_4.0.gemfile.lock +0 -141
  46. data/gemfiles/rails_4.1.gemfile.lock +0 -146
  47. data/gemfiles/rails_4.2.gemfile +0 -8
  48. data/gemfiles/rails_4.2.gemfile.lock +0 -169
  49. data/gemfiles/rails_5.0.gemfile.lock +0 -175
  50. data/gemfiles/rails_5.1.gemfile.lock +0 -175
@@ -20,10 +20,6 @@ module MultiTenant
20
20
  end
21
21
  end
22
22
 
23
- if defined?(ActionController::Base)
24
- ActionController::Base.extend MultiTenant::ControllerExtensions
25
- end
26
-
27
- if defined?(ActionController::API)
28
- ActionController::API.extend MultiTenant::ControllerExtensions
23
+ ActiveSupport.on_load(:action_controller) do |base|
24
+ base.extend MultiTenant::ControllerExtensions
29
25
  end
@@ -28,6 +28,6 @@ module MultiTenant
28
28
  end
29
29
  end
30
30
 
31
- if defined?(ActiveRecord::Base)
32
- ActiveRecord::Base.extend(MultiTenant::CopyFromClient)
31
+ ActiveSupport.on_load(:active_record) do |base|
32
+ base.extend(MultiTenant::CopyFromClient)
33
33
  end
@@ -44,10 +44,10 @@ module ActiveRecord
44
44
  module SchemaStatements
45
45
  alias :orig_create_table :create_table
46
46
  def create_table(table_name, options = {}, &block)
47
- ret = orig_create_table(table_name, options.except(:partition_key), &block)
47
+ ret = orig_create_table(table_name, **options.except(:partition_key), &block)
48
48
  if options[:partition_key] && options[:partition_key].to_s != 'id'
49
49
  execute "ALTER TABLE #{table_name} DROP CONSTRAINT #{table_name}_pkey"
50
- execute "ALTER TABLE #{table_name} ADD PRIMARY KEY(id, \"#{options[:partition_key]}\")"
50
+ execute "ALTER TABLE #{table_name} ADD PRIMARY KEY(\"#{options[:partition_key]}\", id)"
51
51
  end
52
52
  ret
53
53
  end
@@ -24,11 +24,6 @@ module MultiTenant
24
24
  def primary_key
25
25
  return @primary_key if @primary_key
26
26
 
27
- if ::ActiveRecord::VERSION::MAJOR < 5
28
- @primary_key = super || DEFAULT_ID_FIELD
29
- return @primary_key if connection.schema_cache.columns_hash(table_name).include? @primary_key
30
- end
31
-
32
27
  primary_object_keys = Array.wrap(connection.schema_cache.primary_keys(table_name)) - [partition_key]
33
28
 
34
29
  if primary_object_keys.size == 1
@@ -54,7 +49,7 @@ module MultiTenant
54
49
 
55
50
  # Create an implicit belongs_to association only if tenant class exists
56
51
  if MultiTenant.tenant_klass_defined?(tenant_name)
57
- belongs_to tenant_name, options.slice(:class_name, :inverse_of).merge(foreign_key: options[:partition_key])
52
+ belongs_to tenant_name, **options.slice(:class_name, :inverse_of).merge(foreign_key: options[:partition_key])
58
53
  end
59
54
 
60
55
  # New instances should have the tenant set
@@ -126,16 +121,20 @@ module MultiTenant
126
121
  end
127
122
  end
128
123
 
129
- if defined?(ActiveRecord::Base)
130
- ActiveRecord::Base.extend(MultiTenant::ModelExtensionsClassMethods)
124
+ ActiveSupport.on_load(:active_record) do |base|
125
+ base.extend MultiTenant::ModelExtensionsClassMethods
131
126
  end
132
127
 
133
- if ActiveRecord::VERSION::MAJOR > 4 || (ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR >= 2)
134
- class ActiveRecord::Associations::Association
135
- alias skip_statement_cache_orig skip_statement_cache?
136
- def skip_statement_cache?(*scope)
137
- return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant?
138
- skip_statement_cache_orig(*scope)
128
+ class ActiveRecord::Associations::Association
129
+ alias skip_statement_cache_orig skip_statement_cache?
130
+ def skip_statement_cache?(*scope)
131
+ return true if klass.respond_to?(:scoped_by_tenant?) && klass.scoped_by_tenant?
132
+
133
+ if reflection.through_reflection
134
+ through_klass = reflection.through_reflection.klass
135
+ return true if through_klass.respond_to?(:scoped_by_tenant?) && through_klass.scoped_by_tenant?
139
136
  end
137
+
138
+ skip_statement_cache_orig(*scope)
140
139
  end
141
140
  end
@@ -33,6 +33,15 @@ module MultiTenant
33
33
  @@multi_tenant_models[table_name.to_s]
34
34
  end
35
35
 
36
+ def self.multi_tenant_model_for_arel(arel)
37
+ return nil unless arel.respond_to?(:ast)
38
+ if arel.ast.relation.is_a? Arel::Nodes::JoinSource
39
+ MultiTenant.multi_tenant_model_for_table(arel.ast.relation.left.table_name)
40
+ else
41
+ MultiTenant.multi_tenant_model_for_table(arel.ast.relation.table_name)
42
+ end
43
+ end
44
+
36
45
  def self.current_tenant=(tenant)
37
46
  RequestStore.store[:current_tenant] = tenant
38
47
  end
@@ -0,0 +1,13 @@
1
+ module ActiveRecord
2
+ module Persistence
3
+ alias :delete_orig :delete
4
+
5
+ def delete
6
+ if MultiTenant.multi_tenant_model_for_table(self.class.table_name).present? && persisted? && MultiTenant.current_tenant_id.nil?
7
+ MultiTenant.with(self.public_send(self.class.partition_key)) { delete_orig }
8
+ else
9
+ delete_orig
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,4 +1,5 @@
1
1
  require 'active_record'
2
+ require_relative "./arel_visitors_depth_first.rb" unless Arel::Visitors.const_defined?(:DepthFirst)
2
3
 
3
4
  module MultiTenant
4
5
  class Table
@@ -31,6 +32,7 @@ module MultiTenant
31
32
  @arel_node = arel_node
32
33
  @known_relations = []
33
34
  @handled_relations = []
35
+ @discovering = false
34
36
  end
35
37
 
36
38
  def discover_relations
@@ -54,7 +56,7 @@ module MultiTenant
54
56
  end
55
57
  end
56
58
 
57
- class ArelTenantVisitor < Arel::Visitors::DepthFirst
59
+ class ArelTenantVisitor < Arel::Visitors.const_defined?(:DepthFirst) ? Arel::Visitors::DepthFirst : ::MultiTenant::ArelVisitorsDepthFirst
58
60
  def initialize(arel)
59
61
  super(Proc.new {})
60
62
  @statement_node_id = nil
@@ -149,21 +151,27 @@ module MultiTenant
149
151
  end
150
152
  end
151
153
 
152
- class TenantEnforcementClause < Arel::Nodes::Node
154
+ class BaseTenantEnforcementClause < Arel::Nodes::Node
153
155
  attr_reader :tenant_attribute
154
156
  def initialize(tenant_attribute)
155
157
  @tenant_attribute = tenant_attribute
158
+ @tenant_model = MultiTenant.multi_tenant_model_for_table(tenant_attribute.relation.table_name)
156
159
  end
157
160
 
158
161
  def to_s; to_sql; end
159
162
  def to_str; to_sql; end
160
163
 
161
164
  def to_sql(*)
162
- tenant_arel.to_sql
165
+ collector = Arel::Collectors::SQLString.new
166
+ collector = @tenant_model.connection.visitor.accept tenant_arel, collector
167
+ collector.value
163
168
  end
164
169
 
165
- private
166
170
 
171
+ end
172
+
173
+ class TenantEnforcementClause < BaseTenantEnforcementClause
174
+ private
167
175
  def tenant_arel
168
176
  if defined?(Arel::Nodes::Quoted)
169
177
  @tenant_attribute.eq(Arel::Nodes::Quoted.new(MultiTenant.current_tenant_id))
@@ -174,24 +182,15 @@ module MultiTenant
174
182
  end
175
183
 
176
184
 
177
- class TenantJoinEnforcementClause < Arel::Nodes::Node
178
- attr_reader :tenant_attribute
185
+ class TenantJoinEnforcementClause < BaseTenantEnforcementClause
179
186
  attr_reader :table_left
180
187
  def initialize(tenant_attribute, table_left)
188
+ super(tenant_attribute)
181
189
  @table_left = table_left
182
190
  @model_left = MultiTenant.multi_tenant_model_for_table(table_left.table_name)
183
- @tenant_attribute = tenant_attribute
184
- end
185
-
186
- def to_s; to_sql; end
187
- def to_str; to_sql; end
188
-
189
- def to_sql(*)
190
- tenant_arel.to_sql
191
191
  end
192
192
 
193
193
  private
194
-
195
194
  def tenant_arel
196
195
  @tenant_attribute.eq(@table_left[@model_left.partition_key])
197
196
  end
@@ -199,23 +198,12 @@ module MultiTenant
199
198
 
200
199
 
201
200
  module TenantValueVisitor
202
- if ActiveRecord::VERSION::MAJOR > 4 || (ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR >= 2)
203
- def visit_MultiTenant_TenantEnforcementClause(o, collector)
204
- collector << o
205
- end
206
-
207
- def visit_MultiTenant_TenantJoinEnforcementClause(o, collector)
208
- collector << o
209
- end
210
-
211
- else
212
- def visit_MultiTenant_TenantEnforcementClause(o, a = nil)
213
- o
214
- end
201
+ def visit_MultiTenant_TenantEnforcementClause(o, collector)
202
+ collector << o
203
+ end
215
204
 
216
- def visit_MultiTenant_TenantJoinEnforcementClause(o, a = nil)
217
- o
218
- end
205
+ def visit_MultiTenant_TenantJoinEnforcementClause(o, collector)
206
+ collector << o
219
207
  end
220
208
  end
221
209
 
@@ -238,22 +226,20 @@ module MultiTenant
238
226
  delete
239
227
  end
240
228
 
241
- if ActiveRecord::VERSION::MAJOR >= 5 && ActiveRecord::VERSION::MINOR >= 2
242
- def update(arel, name = nil, binds = [])
243
- model = MultiTenant.multi_tenant_model_for_table(arel.ast.relation.table_name)
244
- if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
245
- arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
246
- end
247
- super(arel, name, binds)
229
+ def update(arel, name = nil, binds = [])
230
+ model = MultiTenant.multi_tenant_model_for_arel(arel)
231
+ if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
232
+ arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
248
233
  end
234
+ super(arel, name, binds)
235
+ end
249
236
 
250
- def delete(arel, name = nil, binds = [])
251
- model = MultiTenant.multi_tenant_model_for_table(arel.ast.left.table_name)
252
- if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
253
- arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
254
- end
255
- super(arel, name, binds)
237
+ def delete(arel, name = nil, binds = [])
238
+ model = MultiTenant.multi_tenant_model_for_arel(arel)
239
+ if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
240
+ arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
256
241
  end
242
+ super(arel, name, binds)
257
243
  end
258
244
  end
259
245
  end
@@ -296,7 +282,6 @@ module ActiveRecord
296
282
  end
297
283
 
298
284
  if node.is_a?(Arel::Nodes::SelectCore) || node.is_a?(Arel::Nodes::Join)
299
-
300
285
  if node.is_a?Arel::Nodes::Join
301
286
  node_list = [node]
302
287
  else
@@ -304,14 +289,18 @@ module ActiveRecord
304
289
  end
305
290
 
306
291
  node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
307
- if !node_join.right || !node_join.right.expr.right.is_a?(Arel::Attributes::Attribute)
292
+ if (!node_join.right ||
293
+ (ActiveRecord::VERSION::MAJOR == 5 &&
294
+ !node_join.right.expr.right.is_a?(Arel::Attributes::Attribute)))
308
295
  next
309
296
  end
310
297
 
311
- relation_right = node_join.right.expr.right.relation
312
- relation_left = node_join.right.expr.left.relation
313
- model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name)
298
+ relation_right, relation_left = relations_from_node_join(node_join)
299
+
300
+ next unless relation_right && relation_left
301
+
314
302
  model_right = MultiTenant.multi_tenant_model_for_table(relation_left.table_name)
303
+ model_left = MultiTenant.multi_tenant_model_for_table(relation_right.table_name)
315
304
  if model_right && model_left
316
305
  join_enforcement_clause = MultiTenant::TenantJoinEnforcementClause.new(relation_left[model_left.partition_key], relation_right)
317
306
  node_join.right.expr = node_join.right.expr.and(join_enforcement_clause)
@@ -324,69 +313,36 @@ module ActiveRecord
324
313
 
325
314
  arel
326
315
  end
327
- end
328
- end
329
316
 
330
- require 'active_record/base'
331
- module MultiTenantFindBy
332
- if ActiveRecord::VERSION::MAJOR == 4 && ActiveRecord::VERSION::MINOR >= 2
333
- # Disable caching for find and find_by in Rails 4.2 - we don't have a good
334
- # way to prevent caching problems here when prepared statements are enabled
335
- def find_by(*args)
336
- return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
337
-
338
- # This duplicates a bunch of code from AR's find() method
339
- return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any?
340
- return super if default_scopes.any?
341
-
342
- hash = args.first
317
+ private
318
+ def relations_from_node_join(node_join)
319
+ if ActiveRecord::VERSION::MAJOR == 5 || node_join.right.expr.is_a?(Arel::Nodes::Equality)
320
+ return node_join.right.expr.right.relation, node_join.right.expr.left.relation
321
+ end
343
322
 
344
- return super if hash.values.any? { |v| v.nil? || Array === v || Hash === v }
345
- return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) }
323
+ children = node_join.right.expr.children
346
324
 
347
- key = hash.keys
325
+ tenant_applied = children.any?(MultiTenant::TenantEnforcementClause) || children.any?(MultiTenant::TenantJoinEnforcementClause)
326
+ if tenant_applied || children.empty?
327
+ return nil, nil
328
+ end
348
329
 
349
- # Ensure we never use the cached version
350
- find_by_statement_cache.synchronize { find_by_statement_cache[key] = nil }
330
+ if children[0].right.respond_to?('relation') && children[0].left.respond_to?('relation')
331
+ return children[0].right.relation, children[0].left.relation
332
+ end
351
333
 
352
- super
334
+ return nil, nil
353
335
  end
336
+ end
337
+ end
354
338
 
355
- def find(*ids)
356
- return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
357
-
358
- # This duplicates a bunch of code from AR's find() method
359
- return super unless ids.length == 1
360
- return super if ids.first.kind_of?(Symbol)
361
- return super if block_given? ||
362
- primary_key.nil? ||
363
- default_scopes.any? ||
364
- current_scope ||
365
- columns_hash.include?(inheritance_column) ||
366
- ids.first.kind_of?(Array)
367
-
368
- id = ids.first
369
- if ActiveRecord::Base === id
370
- id = id.id
371
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
372
- You are passing an instance of ActiveRecord::Base to `find`.
373
- Please pass the id of the object by calling `.id`
374
- MSG
375
- end
376
- key = primary_key
377
-
378
- # Ensure we never use the cached version
379
- find_by_statement_cache.synchronize { find_by_statement_cache[key] = nil }
380
-
381
- super
382
- end
383
- elsif ActiveRecord::VERSION::MAJOR > 4
384
- def cached_find_by_statement(key, &block)
385
- return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
339
+ require 'active_record/base'
340
+ module MultiTenantFindBy
341
+ def cached_find_by_statement(key, &block)
342
+ return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
386
343
 
387
- key = Array.wrap(key) + [MultiTenant.current_tenant_id.to_s]
388
- super(key, &block)
389
- end
344
+ key = Array.wrap(key) + [MultiTenant.current_tenant_id.to_s]
345
+ super(key, &block)
390
346
  end
391
347
  end
392
348
 
@@ -18,7 +18,8 @@ module Sidekiq::Middleware::MultiTenant
18
18
  class Server
19
19
  def call(worker_class, msg, queue)
20
20
  if msg.has_key?('multi_tenant')
21
- MultiTenant.with(msg['multi_tenant']['id']) do
21
+ tenant = msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id'])
22
+ MultiTenant.with(tenant) do
22
23
  yield
23
24
  end
24
25
  else
@@ -1,3 +1,3 @@
1
1
  module MultiTenant
2
- VERSION = '1.0.1'
2
+ VERSION = '1.1.1'
3
3
  end
@@ -21,11 +21,7 @@ describe "Controller Extensions", type: :controller do
21
21
  describe ApplicationController, type: :controller do
22
22
  controller do
23
23
  def index
24
- if ActionPack::VERSION::MAJOR >= 5
25
- render body: 'custom called'
26
- else
27
- render text: 'custom called'
28
- end
24
+ render body: 'custom called'
29
25
  end
30
26
  end
31
27
 
@@ -35,30 +31,29 @@ describe "Controller Extensions", type: :controller do
35
31
  end
36
32
  end
37
33
 
38
- if ActionPack::VERSION::MAJOR >= 5
39
- class APIApplicationController < ActionController::API
40
- include Rails.application.routes.url_helpers
41
- set_current_tenant_through_filter
42
- before_action :your_method_that_finds_the_current_tenant
43
34
 
44
- def your_method_that_finds_the_current_tenant
45
- current_account = Account.new
46
- current_account.name = 'account1'
47
- set_current_tenant(current_account)
48
- end
35
+ class APIApplicationController < ActionController::API
36
+ include Rails.application.routes.url_helpers
37
+ set_current_tenant_through_filter
38
+ before_action :your_method_that_finds_the_current_tenant
39
+
40
+ def your_method_that_finds_the_current_tenant
41
+ current_account = Account.new
42
+ current_account.name = 'account1'
43
+ set_current_tenant(current_account)
49
44
  end
45
+ end
50
46
 
51
- describe APIApplicationController, type: :controller do
52
- controller do
53
- def index
54
- render body: 'custom called'
55
- end
47
+ describe APIApplicationController, type: :controller do
48
+ controller do
49
+ def index
50
+ render body: 'custom called'
56
51
  end
52
+ end
57
53
 
58
- it 'Finds the correct tenant using the filter command' do
59
- get :index
60
- expect(MultiTenant.current_tenant.name).to eq 'account1'
61
- end
54
+ it 'Finds the correct tenant using the filter command' do
55
+ get :index
56
+ expect(MultiTenant.current_tenant.name).to eq 'account1'
62
57
  end
63
58
  end
64
59
  end