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.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +8 -14
- data/Appraisals +28 -20
- data/CHANGELOG.md +29 -0
- data/Gemfile.lock +85 -67
- data/README.md +1 -1
- data/activerecord-multi-tenant.gemspec +1 -1
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/active_record_5.2.gemfile +10 -2
- data/gemfiles/active_record_5.2.gemfile.lock +104 -98
- data/gemfiles/{active_record_5.1.gemfile → active_record_6.0.gemfile} +2 -2
- data/gemfiles/active_record_6.0.gemfile.lock +198 -0
- data/gemfiles/{rails_4.1.gemfile → active_record_6.1.gemfile} +2 -2
- data/gemfiles/active_record_6.1.gemfile.lock +198 -0
- data/gemfiles/rails_5.2.gemfile +10 -2
- data/gemfiles/rails_5.2.gemfile.lock +109 -103
- data/gemfiles/{rails_5.0.gemfile → rails_6.0.gemfile} +2 -2
- data/gemfiles/rails_6.0.gemfile.lock +198 -0
- data/gemfiles/{rails_5.1.gemfile → rails_6.1.gemfile} +2 -2
- data/gemfiles/rails_6.1.gemfile.lock +198 -0
- data/lib/activerecord-multi-tenant.rb +1 -0
- data/lib/activerecord-multi-tenant/arel_visitors_depth_first.rb +200 -0
- data/lib/activerecord-multi-tenant/controller_extensions.rb +2 -6
- data/lib/activerecord-multi-tenant/copy_from_client.rb +2 -2
- data/lib/activerecord-multi-tenant/migrations.rb +2 -2
- data/lib/activerecord-multi-tenant/model_extensions.rb +13 -14
- data/lib/activerecord-multi-tenant/multi_tenant.rb +9 -0
- data/lib/activerecord-multi-tenant/persistence_extension.rb +13 -0
- data/lib/activerecord-multi-tenant/query_rewriter.rb +60 -104
- data/lib/activerecord-multi-tenant/sidekiq.rb +2 -1
- data/lib/activerecord-multi-tenant/version.rb +1 -1
- data/spec/activerecord-multi-tenant/controller_extensions_spec.rb +19 -24
- data/spec/activerecord-multi-tenant/model_extensions_spec.rb +54 -104
- data/spec/activerecord-multi-tenant/query_rewriter_spec.rb +8 -0
- data/spec/activerecord-multi-tenant/record_finding_spec.rb +36 -0
- data/spec/activerecord-multi-tenant/record_modifications_spec.rb +60 -3
- data/spec/activerecord-multi-tenant/schema_dumper_tester.rb +0 -0
- data/spec/activerecord-multi-tenant/sidekiq_spec.rb +4 -4
- data/spec/schema.rb +1 -4
- data/spec/spec_helper.rb +1 -6
- metadata +17 -17
- data/gemfiles/active_record_5.1.gemfile.lock +0 -173
- data/gemfiles/rails_4.0.gemfile +0 -8
- data/gemfiles/rails_4.0.gemfile.lock +0 -141
- data/gemfiles/rails_4.1.gemfile.lock +0 -146
- data/gemfiles/rails_4.2.gemfile +0 -8
- data/gemfiles/rails_4.2.gemfile.lock +0 -169
- data/gemfiles/rails_5.0.gemfile.lock +0 -175
- data/gemfiles/rails_5.1.gemfile.lock +0 -175
@@ -20,10 +20,6 @@ module MultiTenant
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
|
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
|
@@ -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(
|
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
|
-
|
130
|
-
|
124
|
+
ActiveSupport.on_load(:active_record) do |base|
|
125
|
+
base.extend MultiTenant::ModelExtensionsClassMethods
|
131
126
|
end
|
132
127
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
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
|
-
|
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 <
|
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
|
-
|
203
|
-
|
204
|
-
|
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
|
-
|
217
|
-
|
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
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
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
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
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 ||
|
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
|
312
|
-
|
313
|
-
|
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
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
350
|
-
|
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
|
-
|
334
|
+
return nil, nil
|
353
335
|
end
|
336
|
+
end
|
337
|
+
end
|
354
338
|
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
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
|
-
|
388
|
-
|
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
|
-
|
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
|
@@ -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
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
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
|