activerecord-multi-tenant 1.0.4 → 1.2.0
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/.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'
|