activerecord-multi-tenant 1.0.4 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/CI.yml +63 -0
- data/.gitignore +2 -0
- data/.rspec +1 -0
- data/Appraisals +20 -47
- data/CHANGELOG.md +19 -0
- data/gemfiles/active_record_5.2.gemfile +3 -2
- data/gemfiles/active_record_6.0.gemfile +1 -1
- data/gemfiles/active_record_6.1.gemfile +8 -0
- data/gemfiles/active_record_7.0.gemfile +8 -0
- data/gemfiles/rails_5.2.gemfile +3 -2
- data/gemfiles/rails_6.0.gemfile +1 -1
- data/gemfiles/rails_6.1.gemfile +8 -0
- data/gemfiles/rails_7.0.gemfile +8 -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 +4 -4
- data/lib/activerecord-multi-tenant/migrations.rb +2 -2
- data/lib/activerecord-multi-tenant/model_extensions.rb +9 -9
- data/lib/activerecord-multi-tenant/multi_tenant.rb +1 -0
- data/lib/activerecord-multi-tenant/query_rewriter.rb +32 -75
- data/lib/activerecord-multi-tenant/sidekiq.rb +6 -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 +58 -74
- data/spec/activerecord-multi-tenant/query_rewriter_spec.rb +25 -0
- data/spec/activerecord-multi-tenant/record_finding_spec.rb +36 -0
- data/spec/activerecord-multi-tenant/sidekiq_spec.rb +15 -4
- data/spec/schema.rb +28 -8
- data/spec/spec_helper.rb +1 -6
- metadata +13 -20
- data/.travis.yml +0 -57
- data/Gemfile.lock +0 -181
- data/gemfiles/active_record_5.1.gemfile +0 -15
- data/gemfiles/active_record_5.1.gemfile.lock +0 -180
- data/gemfiles/active_record_5.2.gemfile.lock +0 -188
- data/gemfiles/active_record_6.0.gemfile.lock +0 -198
- data/gemfiles/rails_4.2.gemfile +0 -16
- data/gemfiles/rails_4.2.gemfile.lock +0 -175
- data/gemfiles/rails_5.0.gemfile +0 -15
- data/gemfiles/rails_5.0.gemfile.lock +0 -180
- data/gemfiles/rails_5.1.gemfile +0 -15
- data/gemfiles/rails_5.1.gemfile.lock +0 -180
- data/gemfiles/rails_5.2.gemfile.lock +0 -188
- data/gemfiles/rails_6.0.gemfile.lock +0 -198
@@ -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
|
@@ -120,6 +122,8 @@ module MultiTenant
|
|
120
122
|
alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_OuterJoin
|
121
123
|
alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_OuterJoin
|
122
124
|
|
125
|
+
alias :visit_ActiveModel_Attribute :terminal
|
126
|
+
|
123
127
|
private
|
124
128
|
|
125
129
|
def tenant_relation?(table_name)
|
@@ -224,22 +228,20 @@ module MultiTenant
|
|
224
228
|
delete
|
225
229
|
end
|
226
230
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
|
232
|
-
end
|
233
|
-
super(arel, name, binds)
|
231
|
+
def update(arel, name = nil, binds = [])
|
232
|
+
model = MultiTenant.multi_tenant_model_for_arel(arel)
|
233
|
+
if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
|
234
|
+
arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
|
234
235
|
end
|
236
|
+
super(arel, name, binds)
|
237
|
+
end
|
235
238
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
end
|
241
|
-
super(arel, name, binds)
|
239
|
+
def delete(arel, name = nil, binds = [])
|
240
|
+
model = MultiTenant.multi_tenant_model_for_arel(arel)
|
241
|
+
if model.present? && !MultiTenant.with_write_only_mode_enabled? && MultiTenant.current_tenant_id.present?
|
242
|
+
arel.where(MultiTenant::TenantEnforcementClause.new(model.arel_table[model.partition_key]))
|
242
243
|
end
|
244
|
+
super(arel, name, binds)
|
243
245
|
end
|
244
246
|
end
|
245
247
|
end
|
@@ -274,7 +276,11 @@ module ActiveRecord
|
|
274
276
|
if node.wheres.empty?
|
275
277
|
node.wheres = [enforcement_clause]
|
276
278
|
else
|
277
|
-
node.wheres[0]
|
279
|
+
if node.wheres[0].is_a?(Arel::Nodes::And)
|
280
|
+
node.wheres[0].children << enforcement_clause
|
281
|
+
else
|
282
|
+
node.wheres[0] = enforcement_clause.and(node.wheres[0])
|
283
|
+
end
|
278
284
|
end
|
279
285
|
else
|
280
286
|
raise "UnknownContext"
|
@@ -290,7 +296,7 @@ module ActiveRecord
|
|
290
296
|
|
291
297
|
node_list.select{ |n| n.is_a? Arel::Nodes::Join }.each do |node_join|
|
292
298
|
if (!node_join.right ||
|
293
|
-
(ActiveRecord::VERSION::MAJOR
|
299
|
+
(ActiveRecord::VERSION::MAJOR == 5 &&
|
294
300
|
!node_join.right.expr.right.is_a?(Arel::Attributes::Attribute)))
|
295
301
|
next
|
296
302
|
end
|
@@ -316,7 +322,7 @@ module ActiveRecord
|
|
316
322
|
|
317
323
|
private
|
318
324
|
def relations_from_node_join(node_join)
|
319
|
-
if ActiveRecord::VERSION::MAJOR
|
325
|
+
if ActiveRecord::VERSION::MAJOR == 5 || node_join.right.expr.is_a?(Arel::Nodes::Equality)
|
320
326
|
return node_join.right.expr.right.relation, node_join.right.expr.left.relation
|
321
327
|
end
|
322
328
|
|
@@ -327,71 +333,22 @@ module ActiveRecord
|
|
327
333
|
return nil, nil
|
328
334
|
end
|
329
335
|
|
330
|
-
|
336
|
+
if children[0].right.respond_to?('relation') && children[0].left.respond_to?('relation')
|
337
|
+
return children[0].right.relation, children[0].left.relation
|
338
|
+
end
|
339
|
+
|
340
|
+
return nil, nil
|
331
341
|
end
|
332
342
|
end
|
333
343
|
end
|
334
344
|
|
335
345
|
require 'active_record/base'
|
336
346
|
module MultiTenantFindBy
|
337
|
-
|
338
|
-
|
339
|
-
# way to prevent caching problems here when prepared statements are enabled
|
340
|
-
def find_by(*args)
|
341
|
-
return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
|
342
|
-
|
343
|
-
# This duplicates a bunch of code from AR's find() method
|
344
|
-
return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any?
|
345
|
-
return super if default_scopes.any?
|
346
|
-
|
347
|
-
hash = args.first
|
348
|
-
|
349
|
-
return super if hash.values.any? { |v| v.nil? || Array === v || Hash === v }
|
350
|
-
return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) }
|
351
|
-
|
352
|
-
key = hash.keys
|
347
|
+
def cached_find_by_statement(key, &block)
|
348
|
+
return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
|
353
349
|
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
super
|
358
|
-
end
|
359
|
-
|
360
|
-
def find(*ids)
|
361
|
-
return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
|
362
|
-
|
363
|
-
# This duplicates a bunch of code from AR's find() method
|
364
|
-
return super unless ids.length == 1
|
365
|
-
return super if ids.first.kind_of?(Symbol)
|
366
|
-
return super if block_given? ||
|
367
|
-
primary_key.nil? ||
|
368
|
-
default_scopes.any? ||
|
369
|
-
current_scope ||
|
370
|
-
columns_hash.include?(inheritance_column) ||
|
371
|
-
ids.first.kind_of?(Array)
|
372
|
-
|
373
|
-
id = ids.first
|
374
|
-
if ActiveRecord::Base === id
|
375
|
-
id = id.id
|
376
|
-
ActiveSupport::Deprecation.warn(<<-MSG.squish)
|
377
|
-
You are passing an instance of ActiveRecord::Base to `find`.
|
378
|
-
Please pass the id of the object by calling `.id`
|
379
|
-
MSG
|
380
|
-
end
|
381
|
-
key = primary_key
|
382
|
-
|
383
|
-
# Ensure we never use the cached version
|
384
|
-
find_by_statement_cache.synchronize { find_by_statement_cache[key] = nil }
|
385
|
-
|
386
|
-
super
|
387
|
-
end
|
388
|
-
elsif ActiveRecord::VERSION::MAJOR > 4
|
389
|
-
def cached_find_by_statement(key, &block)
|
390
|
-
return super unless respond_to?(:scoped_by_tenant?) && scoped_by_tenant?
|
391
|
-
|
392
|
-
key = Array.wrap(key) + [MultiTenant.current_tenant_id.to_s]
|
393
|
-
super(key, &block)
|
394
|
-
end
|
350
|
+
key = Array.wrap(key) + [MultiTenant.current_tenant_id.to_s]
|
351
|
+
super(key, &block)
|
395
352
|
end
|
396
353
|
end
|
397
354
|
|
@@ -18,7 +18,12 @@ 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 = begin
|
22
|
+
msg['multi_tenant']['class'].constantize.find(msg['multi_tenant']['id'])
|
23
|
+
rescue ActiveRecord::RecordNotFound
|
24
|
+
msg['multi_tenant']['id']
|
25
|
+
end
|
26
|
+
MultiTenant.with(tenant) do
|
22
27
|
yield
|
23
28
|
end
|
24
29
|
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
|
@@ -348,9 +348,12 @@ describe MultiTenant do
|
|
348
348
|
end
|
349
349
|
|
350
350
|
it "applies the team_id conditions in the where clause" do
|
351
|
-
|
352
|
-
|
353
|
-
|
351
|
+
option1 = <<-sql.strip
|
352
|
+
SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "tasks"."project_id" = 1 AND "sub_tasks"."account_id" = 1 AND "tasks"."account_id" = 1
|
353
|
+
sql
|
354
|
+
option2 = <<-sql.strip
|
355
|
+
SELECT "sub_tasks".* FROM "sub_tasks" INNER JOIN "tasks" ON "sub_tasks"."task_id" = "tasks"."id" AND "sub_tasks"."account_id" = "tasks"."account_id" WHERE "sub_tasks"."account_id" = 1 AND "tasks"."project_id" = 1 AND "tasks"."account_id" = 1
|
356
|
+
sql
|
354
357
|
|
355
358
|
account1 = Account.create! name: 'Account 1'
|
356
359
|
|
@@ -358,7 +361,7 @@ describe MultiTenant do
|
|
358
361
|
project1 = Project.create! name: 'Project 1'
|
359
362
|
task1 = Task.create! name: 'Task 1', project: project1
|
360
363
|
subtask1 = SubTask.create! task: task1
|
361
|
-
expect(project1.sub_tasks.to_sql).to eq(
|
364
|
+
expect(project1.sub_tasks.to_sql).to eq(option1).or(eq(option2))
|
362
365
|
expect(project1.sub_tasks).to include(subtask1)
|
363
366
|
end
|
364
367
|
|
@@ -373,9 +376,13 @@ describe MultiTenant do
|
|
373
376
|
end
|
374
377
|
|
375
378
|
it "tests joins between distributed and reference table" do
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
+
option1 = <<-sql.strip
|
380
|
+
SELECT "categories".* FROM "categories" INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id" WHERE "project_categories"."project_id" = 1 AND "project_categories"."account_id" = 1
|
381
|
+
sql
|
382
|
+
option2 = <<-sql.strip
|
383
|
+
SELECT "categories".* FROM "categories" INNER JOIN "project_categories" ON "categories"."id" = "project_categories"."category_id" WHERE "project_categories"."account_id" = 1 AND "project_categories"."project_id" = 1
|
384
|
+
sql
|
385
|
+
|
379
386
|
account1 = Account.create! name: 'Account 1'
|
380
387
|
category1 = Category.create! name: 'Category 1'
|
381
388
|
|
@@ -383,7 +390,7 @@ describe MultiTenant do
|
|
383
390
|
project1 = Project.create! name: 'Project 1'
|
384
391
|
projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1
|
385
392
|
|
386
|
-
expect(project1.categories.to_sql).to eq(
|
393
|
+
expect(project1.categories.to_sql).to eq(option1).or(eq(option2))
|
387
394
|
expect(project1.categories).to include(category1)
|
388
395
|
expect(project1.project_categories).to include(projectcategory)
|
389
396
|
end
|
@@ -412,25 +419,18 @@ describe MultiTenant do
|
|
412
419
|
account1 = Account.create! name: 'Account 1'
|
413
420
|
category1 = Category.create! name: 'Category 1'
|
414
421
|
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."account_id" = 1 AND "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1
|
422
|
-
sql
|
423
|
-
else
|
424
|
-
<<-sql
|
425
|
-
SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = "projects"."account_id" AND "project_categories"."account_id" = 1 AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1
|
426
|
-
sql
|
427
|
-
end
|
422
|
+
option1 = <<-sql.strip
|
423
|
+
SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."project_id" = "projects"."id" AND "project_categories"."account_id" = 1 AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1
|
424
|
+
sql
|
425
|
+
option2 = <<-sql.strip
|
426
|
+
SELECT "projects"."id" AS t0_r0, "projects"."account_id" AS t0_r1, "projects"."name" AS t0_r2, "categories"."id" AS t1_r0, "categories"."name" AS t1_r1 FROM "projects" LEFT OUTER JOIN "project_categories" ON "project_categories"."account_id" = 1 AND "project_categories"."project_id" = "projects"."id" AND "projects"."account_id" = 1 LEFT OUTER JOIN "categories" ON "categories"."id" = "project_categories"."category_id" AND "project_categories"."account_id" = 1 WHERE "projects"."account_id" = 1
|
427
|
+
sql
|
428
428
|
|
429
429
|
MultiTenant.with(account1) do
|
430
430
|
project1 = Project.create! name: 'Project 1'
|
431
431
|
projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1
|
432
432
|
|
433
|
-
expect(Project.eager_load(:categories).to_sql).to eq(
|
433
|
+
expect(Project.eager_load(:categories).to_sql).to eq(option1).or(eq(option2))
|
434
434
|
|
435
435
|
project = Project.eager_load(:categories).first
|
436
436
|
expect(project.categories).to include(category1)
|
@@ -455,25 +455,18 @@ describe MultiTenant do
|
|
455
455
|
category1 = Category.create! name: 'Category 1'
|
456
456
|
|
457
457
|
MultiTenant.with(account1) do
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."account_id" = 1 AND "projects"."id" = "tasks"."project_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1
|
465
|
-
sql
|
466
|
-
else
|
467
|
-
<<-sql
|
468
|
-
SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = "tasks"."account_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "projects"."account_id" = 1 AND "tasks"."account_id" = 1
|
469
|
-
sql
|
470
|
-
end
|
458
|
+
option1 = <<-sql.strip
|
459
|
+
SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."id" = "tasks"."project_id" AND "projects"."account_id" = 1 LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1
|
460
|
+
sql
|
461
|
+
option2 = <<-sql.strip
|
462
|
+
SELECT "tasks".* FROM "tasks" INNER JOIN "projects" ON "projects"."account_id" = 1 AND "projects"."id" = "tasks"."project_id" LEFT JOIN project_categories pc ON project.category_id = pc.id WHERE "tasks"."account_id" = 1
|
463
|
+
sql
|
471
464
|
|
472
465
|
project1 = Project.create! name: 'Project 1'
|
473
466
|
projectcategory = ProjectCategory.create! name: 'project cat 1', project: project1, category: category1
|
474
467
|
|
475
468
|
project1.tasks.create! name: 'baz'
|
476
|
-
expect(Task.joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql).to eq(
|
469
|
+
expect(Task.joins(:project).joins('LEFT JOIN project_categories pc ON project.category_id = pc.id').to_sql).to eq(option1).or(eq(option2))
|
477
470
|
end
|
478
471
|
|
479
472
|
MultiTenant.without do
|
@@ -493,48 +486,39 @@ describe MultiTenant do
|
|
493
486
|
project2 = Project.create! name: 'Project 2', account: Account.create!(name: 'Account2')
|
494
487
|
|
495
488
|
MultiTenant.with(account) do
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
end
|
513
|
-
|
514
|
-
expect(Project).to receive(:find_by_sql).with(expected_sql, any_args).and_call_original
|
489
|
+
option1 = <<-sql.strip
|
490
|
+
SELECT "projects".* FROM "projects" WHERE "projects"."account_id" = #{account.id} AND "projects"."id" = $1 LIMIT $2
|
491
|
+
sql
|
492
|
+
option2 = <<-sql.strip
|
493
|
+
SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2
|
494
|
+
sql
|
495
|
+
option3 = <<-sql.strip
|
496
|
+
SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 AND "projects"."account_id" = #{account.id} LIMIT $2
|
497
|
+
sql
|
498
|
+
|
499
|
+
# Couldn't make the following line pass for some reason, so came up with an uglier alternative
|
500
|
+
# expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)).or(eq(option3)), any_args).and_call_original
|
501
|
+
expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args|
|
502
|
+
expect(args[0]).to(eq(option1).or(eq(option2)).or(eq(option3)))
|
503
|
+
m.call(args[0], args[1], preparable:args[2][:preparable])
|
504
|
+
end
|
515
505
|
expect(Project.find(project.id)).to eq(project)
|
516
506
|
end
|
517
507
|
|
518
508
|
MultiTenant.without do
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
<<-sql.strip
|
533
|
-
SELECT "projects".* FROM "projects" WHERE "projects"."id" = #{project2.id} LIMIT 1
|
534
|
-
sql
|
535
|
-
end
|
536
|
-
|
537
|
-
expect(Project).to receive(:find_by_sql).with(expected_sql, any_args).and_call_original
|
509
|
+
option1 = <<-sql.strip
|
510
|
+
SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
|
511
|
+
sql
|
512
|
+
option2 = <<-sql.strip
|
513
|
+
SELECT "projects".* FROM "projects" WHERE "projects"."id" = $1 LIMIT $2
|
514
|
+
sql
|
515
|
+
|
516
|
+
# Couldn't make the following line pass for some reason, so came up with an uglier alternative
|
517
|
+
# expect(Project).to receive(:find_by_sql).with(eq(option1).or(eq(option2)), any_args).and_call_original
|
518
|
+
expect(Project).to receive(:find_by_sql).and_wrap_original do |m, *args|
|
519
|
+
expect(args[0]).to(eq(option1).or(eq(option2)))
|
520
|
+
m.call(args[0], args[1], preparable:args[2][:preparable])
|
521
|
+
end
|
538
522
|
expect(Project.find(project2.id)).to eq(project2)
|
539
523
|
end
|
540
524
|
end
|
@@ -90,4 +90,29 @@ describe "Query Rewriter" do
|
|
90
90
|
}.to change { Project.count }.from(3).to(1)
|
91
91
|
end
|
92
92
|
end
|
93
|
+
|
94
|
+
context "when update without arel" do
|
95
|
+
it "can call method" do
|
96
|
+
expect {
|
97
|
+
ActiveRecord::Base.connection.update("SELECT 1")
|
98
|
+
}.not_to raise_error
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context "when joining with a model with a default scope" do
|
103
|
+
let!(:account) { Account.create!(name: "Test Account") }
|
104
|
+
|
105
|
+
it "fetches only records within the default scope" do
|
106
|
+
alive = Domain.create(name: "alive", account: account)
|
107
|
+
deleted = Domain.create(name: "deleted", deleted: true, account: account)
|
108
|
+
page_in_alive_domain = Page.create(name: "alive", account: account, domain: alive)
|
109
|
+
page_in_deleted_domain = Page.create(name: "deleted", account: account, domain: deleted)
|
110
|
+
|
111
|
+
expect(
|
112
|
+
MultiTenant.with(account) do
|
113
|
+
Page.joins(:domain).pluck(:id)
|
114
|
+
end
|
115
|
+
).to eq([page_in_alive_domain.id])
|
116
|
+
end
|
117
|
+
end
|
93
118
|
end
|
@@ -59,4 +59,40 @@ describe MultiTenant, 'Record finding' do
|
|
59
59
|
expect(second_found).to eq(second_record)
|
60
60
|
end
|
61
61
|
end
|
62
|
+
|
63
|
+
context 'model with has_many relation through multi-tenant model' do
|
64
|
+
let(:tenant_1) { Account.create! name: 'Tenant 1' }
|
65
|
+
let(:project_1) { tenant_1.projects.create! }
|
66
|
+
|
67
|
+
let(:tenant_2) { Account.create! name: 'Tenant 2' }
|
68
|
+
let(:project_2) { tenant_2.projects.create! }
|
69
|
+
|
70
|
+
let(:category) { Category.create! name: 'Category' }
|
71
|
+
|
72
|
+
before do
|
73
|
+
ProjectCategory.create! account: tenant_1, name: '1', project: project_1, category: category
|
74
|
+
ProjectCategory.create! account: tenant_2, name: '2', project: project_2, category: category
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'can get model without creating query cache' do
|
78
|
+
MultiTenant.with(tenant_1) do
|
79
|
+
found_category = Project.find(project_1.id).categories.to_a.first
|
80
|
+
expect(found_category).to eq(category)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'can get model for other tenant' do
|
85
|
+
MultiTenant.with(tenant_2) do
|
86
|
+
found_category = Project.find(project_2.id).categories.to_a.first
|
87
|
+
expect(found_category).to eq(category)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'can get model without current_tenant' do
|
92
|
+
MultiTenant.without do
|
93
|
+
found_category = Project.find(project_2.id).categories.to_a.first
|
94
|
+
expect(found_category).to eq(category)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
62
98
|
end
|
@@ -4,14 +4,25 @@ require 'activerecord-multi-tenant/sidekiq'
|
|
4
4
|
|
5
5
|
describe MultiTenant, 'Sidekiq' do
|
6
6
|
let(:server) { Sidekiq::Middleware::MultiTenant::Server.new }
|
7
|
+
let(:account) { Account.create(name: 'test') }
|
8
|
+
let(:deleted_acount) { Account.create(name: 'deleted') }
|
9
|
+
|
10
|
+
before { deleted_acount.destroy! }
|
7
11
|
|
8
12
|
describe 'server middleware' do
|
9
13
|
it 'sets the multitenant context when provided in message' do
|
10
|
-
|
11
|
-
|
12
|
-
'
|
14
|
+
server.call(double,{'bogus' => 'message',
|
15
|
+
'multi_tenant' => { 'class' => account.class.name, 'id' => account.id}},
|
16
|
+
'bogus_queue') do
|
17
|
+
expect(MultiTenant.current_tenant).to eq(account)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'sets the multitenant context (id) even if tenant not found' do
|
22
|
+
server.call(double,{'bogus' => 'message',
|
23
|
+
'multi_tenant' => { 'class' => deleted_acount.class.name, 'id' => deleted_acount.id}},
|
13
24
|
'bogus_queue') do
|
14
|
-
expect(MultiTenant.current_tenant).to eq(
|
25
|
+
expect(MultiTenant.current_tenant).to eq(deleted_acount.id)
|
15
26
|
end
|
16
27
|
end
|
17
28
|
|
data/spec/schema.rb
CHANGED
@@ -89,10 +89,21 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
|
|
89
89
|
t.column :category_id, :integer
|
90
90
|
end
|
91
91
|
|
92
|
-
|
93
92
|
create_table :allowed_places, force: true, id: false do |t|
|
94
|
-
|
95
|
-
|
93
|
+
t.string :account_id, :integer
|
94
|
+
t.string :name, :string
|
95
|
+
end
|
96
|
+
|
97
|
+
create_table :domains, force: true, partition_key: :account_id do |t|
|
98
|
+
t.column :account_id, :integer
|
99
|
+
t.column :name, :string
|
100
|
+
t.column :deleted, :boolean, default: false
|
101
|
+
end
|
102
|
+
|
103
|
+
create_table :pages, force: true, partition_key: :account_id do |t|
|
104
|
+
t.column :account_id, :integer
|
105
|
+
t.column :name, :string
|
106
|
+
t.column :domain_id, :integer
|
96
107
|
end
|
97
108
|
|
98
109
|
create_distributed_table :accounts, :id
|
@@ -108,6 +119,8 @@ ARGV.grep(/\w+_spec\.rb/).empty? && ActiveRecord::Schema.define(version: 1) do
|
|
108
119
|
create_distributed_table :uuid_records, :organization_id
|
109
120
|
create_distributed_table :project_categories, :account_id
|
110
121
|
create_distributed_table :allowed_places, :account_id
|
122
|
+
create_distributed_table :domains, :account_id
|
123
|
+
create_distributed_table :pages, :account_id
|
111
124
|
create_reference_table :categories
|
112
125
|
end
|
113
126
|
|
@@ -181,10 +194,7 @@ end
|
|
181
194
|
class Comment < ActiveRecord::Base
|
182
195
|
multi_tenant :account
|
183
196
|
belongs_to :commentable, polymorphic: true
|
184
|
-
|
185
|
-
if ActiveRecord::VERSION::MAJOR >= 4
|
186
|
-
belongs_to :task, -> { where(comments: { commentable_type: 'Task' }) }, foreign_key: 'commentable_id'
|
187
|
-
end
|
197
|
+
belongs_to :task, -> { where(comments: { commentable_type: 'Task' }) }, foreign_key: 'commentable_id'
|
188
198
|
end
|
189
199
|
|
190
200
|
class Organization < ActiveRecord::Base
|
@@ -207,7 +217,17 @@ class ProjectCategory < ActiveRecord::Base
|
|
207
217
|
belongs_to :account
|
208
218
|
end
|
209
219
|
|
210
|
-
|
211
220
|
class AllowedPlace < ActiveRecord::Base
|
212
221
|
multi_tenant :account
|
213
222
|
end
|
223
|
+
|
224
|
+
class Domain < ActiveRecord::Base
|
225
|
+
multi_tenant :account
|
226
|
+
has_many :pages
|
227
|
+
default_scope { where(deleted: false) }
|
228
|
+
end
|
229
|
+
|
230
|
+
class Page < ActiveRecord::Base
|
231
|
+
multi_tenant :account
|
232
|
+
belongs_to :domain
|
233
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -43,12 +43,7 @@ MultiTenantTest::Application.config.secret_token = 'x' * 40
|
|
43
43
|
MultiTenantTest::Application.config.secret_key_base = 'y' * 40
|
44
44
|
|
45
45
|
def uses_prepared_statements?
|
46
|
-
|
47
|
-
config = ActiveRecord::Base.connection.instance_variable_get(:@config)
|
48
|
-
config.fetch(:prepared_statements, true)
|
49
|
-
else
|
50
|
-
ActiveRecord::Base.connection.prepared_statements
|
51
|
-
end
|
46
|
+
ActiveRecord::Base.connection.prepared_statements
|
52
47
|
end
|
53
48
|
|
54
49
|
require 'schema'
|