activerecord-multi-tenant 1.0.1 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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