multitenancy 0.0.4 → 0.0.5
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.
- data/README.md +16 -3
- data/Rakefile +9 -1
- data/lib/multitenancy.rb +16 -1
- data/lib/multitenancy/active_record/switch_db.rb +30 -0
- data/lib/multitenancy/version.rb +1 -1
- data/test/database.yml +5 -2
- data/test/{multitenancy.sqlite3 → db1.sqlite3} +0 -0
- data/test/db2.sqlite3 +0 -0
- data/test/test_config.rb +3 -1
- data/test/unit/multitenancy_test.rb +52 -0
- data/test/unit/rest_client_test.rb +17 -14
- data/test/unit/switch_db_test.rb +41 -0
- data/test/unit/tenant_test.rb +4 -0
- metadata +9 -4
data/README.md
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
# Multitenancy
|
2
2
|
|
3
|
-
Multitenancy gem nicely plugs in to activerecord to provide multitenant support within a
|
3
|
+
Multitenancy gem nicely plugs in to activerecord to provide multitenant support within a shared schema as well dedicated schema. It allows multitenancy at two levels, tenant and sub-tenant. For instance you can have SAAS application where the primary tenant could be an organization and sub-tenant will be users in that organization.
|
4
|
+
|
5
|
+
Shared Schema: All tennats will be part of the same instance. Additional columns (tenant_id and sub_tenant_id) will be added to your tables to support multitenancy
|
6
|
+
Dedicated Schema: Every tenant will have its own instance of db
|
4
7
|
|
5
8
|
## Installation
|
6
9
|
|
@@ -17,14 +20,13 @@ Or install it yourself as:
|
|
17
20
|
$ gem install multitenancy
|
18
21
|
|
19
22
|
## Usage
|
20
|
-
|
21
23
|
This gem expects the tenant and sub-tenant values passed in the request header. But you can enhace this to support other logic as well.
|
22
24
|
|
23
25
|
You use it in your padrino/sinatra application, add the below lines to the config.ru
|
24
26
|
|
25
27
|
Multitenancy.init(:tenant_header => 'X_COMPANY_ID', :sub_tenant_header => 'X_USER_ID')
|
26
28
|
Padrino.use Multitenancy::Filter
|
27
|
-
|
29
|
+
|
28
30
|
You can also outside of a filter or in a standalone application,
|
29
31
|
|
30
32
|
tenant = Multitenancy::Tenant.new('flipkart', 'ganeshs')
|
@@ -33,6 +35,17 @@ You can also outside of a filter or in a standalone application,
|
|
33
35
|
end
|
34
36
|
|
35
37
|
Any active record query executed within the tenant block, will be tenant/sub-tenant scoped. New records will persist the tenant and sub-tenant ids, find queries will be scoped to teanant and sub-tenant ids. If the sub-tenant id is not specified, it will be de-scoped.
|
38
|
+
|
39
|
+
### Shared Instance
|
40
|
+
You can pass on the db_type as shared while initializing multitenancy, which is by default set when db_type is not.
|
41
|
+
|
42
|
+
Multitenancy.init(:tenant_header => 'X_COMPANY_ID', :sub_tenant_header => 'X_USER_ID', :db_type => :shared)
|
43
|
+
|
44
|
+
### Dedicated Instance
|
45
|
+
|
46
|
+
Dedicated instance config takes in additional optional params,
|
47
|
+
|
48
|
+
Multitenancy.init(:tenant_header => 'X_COMPANY_ID', :sub_tenant_header => 'X_USER_ID', :db_type => :shared, :db_config_prefix => 'some_prefix_', :db_config_suffix => 'production')
|
36
49
|
|
37
50
|
## Contributing
|
38
51
|
|
data/Rakefile
CHANGED
data/lib/multitenancy.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require "active_record"
|
2
2
|
require "active_model"
|
3
3
|
require "multitenancy/version"
|
4
|
+
require "multitenancy/active_record/switch_db"
|
4
5
|
require "multitenancy/tenant"
|
5
6
|
require "multitenancy/rack/filter"
|
6
7
|
require "multitenancy/model_extensions"
|
@@ -12,6 +13,9 @@ module Multitenancy
|
|
12
13
|
@@sub_tenant_header = 'X_SUB_TENANT_ID'
|
13
14
|
@@append_headers_to_rest_calls = true
|
14
15
|
@@logger = (logger rescue nil) || Logger.new(STDOUT)
|
16
|
+
@@db_config_prefix = ''
|
17
|
+
@@db_config_suffix = '_development'
|
18
|
+
@@db_type = :shared # or :dedicated
|
15
19
|
|
16
20
|
class << self
|
17
21
|
def init(config)
|
@@ -19,6 +23,13 @@ module Multitenancy
|
|
19
23
|
@@sub_tenant_header = config[:sub_tenant_header]
|
20
24
|
@@logger = config[:logger] if config[:logger]
|
21
25
|
@@append_headers_to_rest_calls = config[:append_headers_to_rest_calls] unless config[:append_headers_to_rest_calls].nil?
|
26
|
+
@@db_config_prefix = config[:db_config_prefix] unless config[:db_config_prefix].nil?
|
27
|
+
@@db_config_suffix = config[:db_config_suffix] unless config[:db_config_suffix].nil?
|
28
|
+
@@db_type = (config[:db_type].nil? || ![:shared, :dedicated].include?(config[:db_type])) ? :shared : config[:db_type]
|
29
|
+
end
|
30
|
+
|
31
|
+
def db_type
|
32
|
+
@@db_type
|
22
33
|
end
|
23
34
|
|
24
35
|
def logger
|
@@ -45,7 +56,11 @@ module Multitenancy
|
|
45
56
|
old_tenant = self.current_tenant
|
46
57
|
self.current_tenant = tenant
|
47
58
|
begin
|
48
|
-
|
59
|
+
if db_type == :shared
|
60
|
+
return block.call
|
61
|
+
else
|
62
|
+
return ActiveRecord::Base.switch_db("#{@@db_config_prefix}#{tenant.tenant_id}#{@@db_config_suffix}".to_sym, &block)
|
63
|
+
end
|
49
64
|
ensure
|
50
65
|
self.current_tenant = old_tenant
|
51
66
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
class ActiveRecord::Base
|
2
|
+
class << self
|
3
|
+
|
4
|
+
@@connection_handlers ||= {}
|
5
|
+
|
6
|
+
def connection_handler_with_multi_db_support(spec_symbol = nil)
|
7
|
+
return @@connection_handlers[spec_symbol] if spec_symbol
|
8
|
+
if Thread.current[:current_db]
|
9
|
+
@@connection_handlers[Thread.current[:current_db]] ||= ActiveRecord::ConnectionAdapters::ConnectionHandler.new
|
10
|
+
else
|
11
|
+
connection_handler_without_multi_db_support
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
alias_method :connection_handler_without_multi_db_support, :connection_handler
|
16
|
+
alias_method :connection_handler, :connection_handler_with_multi_db_support
|
17
|
+
|
18
|
+
def switch_db(db, &block)
|
19
|
+
Thread.current[:current_db] = db
|
20
|
+
unless ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base)
|
21
|
+
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[db])
|
22
|
+
end
|
23
|
+
yield
|
24
|
+
ensure
|
25
|
+
ActiveRecord::Base.connection_handler.clear_active_connections! rescue puts "supressing error while clearing connections - #{$!.inspect}"
|
26
|
+
Thread.current[:current_db] = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
data/lib/multitenancy/version.rb
CHANGED
data/test/database.yml
CHANGED
Binary file
|
data/test/db2.sqlite3
ADDED
Binary file
|
data/test/test_config.rb
CHANGED
@@ -15,7 +15,9 @@ $:.unshift(ROOT + "/lib")
|
|
15
15
|
|
16
16
|
config = YAML::load(IO.read(File.join(File.dirname(__FILE__), 'database.yml')))
|
17
17
|
ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
|
18
|
-
ActiveRecord::Base.
|
18
|
+
ActiveRecord::Base.configurations[:db1] = config[ENV['DB1'] || 'db1']
|
19
|
+
ActiveRecord::Base.configurations[:db2] = config[ENV['DB2'] || 'db2']
|
20
|
+
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[:db1])
|
19
21
|
|
20
22
|
class ActiveSupport::TestCase
|
21
23
|
|
@@ -8,6 +8,23 @@ module Multitenancy
|
|
8
8
|
assert_equal 'tenant_id', Multitenancy.tenant_header
|
9
9
|
end
|
10
10
|
|
11
|
+
context "configure db type" do
|
12
|
+
should "default to shared" do
|
13
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id')
|
14
|
+
assert_equal :shared, Multitenancy.db_type
|
15
|
+
end
|
16
|
+
|
17
|
+
should "set to shared on junk value" do
|
18
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id', :db_type => :junk)
|
19
|
+
assert_equal :shared, Multitenancy.db_type
|
20
|
+
end
|
21
|
+
|
22
|
+
should "set to dedicated" do
|
23
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id', :db_type => :dedicated)
|
24
|
+
assert_equal :dedicated, Multitenancy.db_type
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
11
28
|
should "return current tenant" do
|
12
29
|
tenant = Tenant.new('tenant_id', 'seller_id')
|
13
30
|
Multitenancy.current_tenant = tenant
|
@@ -16,6 +33,7 @@ module Multitenancy
|
|
16
33
|
|
17
34
|
context "block within context" do
|
18
35
|
setup do
|
36
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id')
|
19
37
|
tenant = Tenant.new('tenant_id', 'seller_id')
|
20
38
|
Multitenancy.current_tenant = tenant
|
21
39
|
end
|
@@ -26,6 +44,40 @@ module Multitenancy
|
|
26
44
|
assert_equal Multitenancy.current_tenant.sub_tenant_id, 'dummy_seller_id'
|
27
45
|
end
|
28
46
|
end
|
47
|
+
|
48
|
+
context "for dedicated db" do
|
49
|
+
setup do
|
50
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id', :db_type => :dedicated)
|
51
|
+
end
|
52
|
+
|
53
|
+
should "switch db" do
|
54
|
+
ActiveRecord::Base.expects(:switch_db).with(:db1_development)
|
55
|
+
Multitenancy.with_tenant(Tenant.new('db1')) do
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
should "call block" do
|
60
|
+
test = false
|
61
|
+
ActiveRecord::Base.stubs(:establish_connection)
|
62
|
+
Multitenancy.with_tenant(Tenant.new('db1')) do
|
63
|
+
test = true
|
64
|
+
end
|
65
|
+
assert_true test
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
context "for dedicated type prefix and suffix" do
|
72
|
+
setup do
|
73
|
+
Multitenancy.init(:tenant_header => 'tenant_id', :sub_tenant_header => 'seller_id', :db_type => :dedicated, :db_config_prefix => 'db_', :db_config_suffix => '_suffix')
|
74
|
+
end
|
75
|
+
|
76
|
+
should "be used to switch db" do
|
77
|
+
ActiveRecord::Base.expects(:switch_db).with(:db_db1_suffix)
|
78
|
+
Multitenancy.with_tenant(Tenant.new('db1')) do
|
79
|
+
end
|
80
|
+
end
|
29
81
|
end
|
30
82
|
end
|
31
83
|
end
|
@@ -2,22 +2,25 @@ require File.expand_path(File.dirname(__FILE__) + '/../test_config.rb')
|
|
2
2
|
|
3
3
|
class RestClientTest < Test::Unit::TestCase
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
5
|
+
context "tenant headers in rest calls" do
|
6
|
+
setup do
|
7
|
+
Multitenancy.init({:tenant_header => 'X_COMPANY_ID', :sub_tenant_header => 'X_USER_ID', :append_headers_to_rest_calls => true})
|
8
|
+
end
|
9
|
+
|
10
|
+
should "not append tenant headers to rest calls outside of a tenant scope" do
|
11
|
+
Multitenancy.current_tenant = nil
|
12
|
+
RestClient::Request.any_instance.expects(:make_headers).with() {|headers| headers[Multitenancy.tenant_header].nil? && headers[Multitenancy.sub_tenant_header].nil?}
|
13
|
+
RestClient::Request.any_instance.expects(:execute)
|
13
14
|
RestClient.get('http://some/resource')
|
14
15
|
end
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
16
|
+
|
17
|
+
should "append tenant headers to rest calls within a tenant scope" do
|
18
|
+
RestClient::Request.any_instance.expects(:make_headers).with() {|headers| headers[Multitenancy.tenant_header] == 'Flipkart' && headers[Multitenancy.sub_tenant_header] == 'ganeshs'}
|
19
|
+
RestClient::Request.any_instance.expects(:execute)
|
20
|
+
Multitenancy.with_tenant(Multitenancy::Tenant.new('Flipkart', 'ganeshs')) do
|
21
|
+
RestClient.get('http://some/resource')
|
22
|
+
end
|
23
|
+
end
|
21
24
|
end
|
22
25
|
|
23
26
|
context "no tenant headers in rest calls" do
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../test_config.rb')
|
2
|
+
|
3
|
+
2.times do |i|
|
4
|
+
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["db#{i + 1}".to_sym])
|
5
|
+
ActiveRecord::Schema.define(:version => 1) do
|
6
|
+
create_table :dummy, :force => true do |t|
|
7
|
+
t.column :org_id, :string
|
8
|
+
t.column :name, :string
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Dummy < ActiveRecord::Base
|
14
|
+
end
|
15
|
+
|
16
|
+
class SwitchDBTest < Test::Unit::TestCase
|
17
|
+
|
18
|
+
should "set current db name on switch db" do
|
19
|
+
Dummy.switch_db(:db1) do
|
20
|
+
assert_equal(:db1, Thread.current[:current_db])
|
21
|
+
end
|
22
|
+
Dummy.switch_db(:db2) do
|
23
|
+
assert_equal(:db2, Thread.current[:current_db])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
should "reset current db after switch db is completed" do
|
28
|
+
Dummy.switch_db(:db1) do
|
29
|
+
assert_equal(:db1, Thread.current[:current_db])
|
30
|
+
end
|
31
|
+
assert_nil(Thread.current[:current_db])
|
32
|
+
end
|
33
|
+
|
34
|
+
should "clear active connections after switch db block execution" do
|
35
|
+
ActiveRecord::ConnectionAdapters::ConnectionHandler.any_instance.expects(:clear_active_connections!)
|
36
|
+
Dummy.switch_db(:db1) do
|
37
|
+
assert_equal(:db1, Thread.current[:current_db])
|
38
|
+
end
|
39
|
+
assert_nil(Thread.current[:current_db])
|
40
|
+
end
|
41
|
+
end
|
data/test/unit/tenant_test.rb
CHANGED
@@ -2,6 +2,10 @@ require File.expand_path(File.dirname(__FILE__) + '/../test_config.rb')
|
|
2
2
|
|
3
3
|
module Multitenancy
|
4
4
|
class TenantTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
setup do
|
7
|
+
Multitenancy.init(:tenant_header => 'X_TENANT_ID', :sub_tenant_header => 'X_SUB_TENANT_ID')
|
8
|
+
end
|
5
9
|
|
6
10
|
should "create tenant" do
|
7
11
|
tenant = Tenant.new('tenant_id', 'seller_id')
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: multitenancy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-02-05 00:00:00.000000000Z
|
13
13
|
dependencies: []
|
14
14
|
description: Support multitennacy with active record
|
15
15
|
email:
|
@@ -24,6 +24,7 @@ files:
|
|
24
24
|
- README.md
|
25
25
|
- Rakefile
|
26
26
|
- lib/multitenancy.rb
|
27
|
+
- lib/multitenancy/active_record/switch_db.rb
|
27
28
|
- lib/multitenancy/model_extensions.rb
|
28
29
|
- lib/multitenancy/rack/filter.rb
|
29
30
|
- lib/multitenancy/rest_client/rest_client.rb
|
@@ -31,12 +32,14 @@ files:
|
|
31
32
|
- lib/multitenancy/version.rb
|
32
33
|
- multitenancy.gemspec
|
33
34
|
- test/database.yml
|
34
|
-
- test/
|
35
|
+
- test/db1.sqlite3
|
36
|
+
- test/db2.sqlite3
|
35
37
|
- test/test_config.rb
|
36
38
|
- test/unit/filter_test.rb
|
37
39
|
- test/unit/model_extensions_test.rb
|
38
40
|
- test/unit/multitenancy_test.rb
|
39
41
|
- test/unit/rest_client_test.rb
|
42
|
+
- test/unit/switch_db_test.rb
|
40
43
|
- test/unit/tenant_test.rb
|
41
44
|
homepage: https://github.com/Flipkart/multitenancy
|
42
45
|
licenses: []
|
@@ -64,11 +67,13 @@ specification_version: 3
|
|
64
67
|
summary: Support multitennacy with active record at tenant and sub-tenant level
|
65
68
|
test_files:
|
66
69
|
- test/database.yml
|
67
|
-
- test/
|
70
|
+
- test/db1.sqlite3
|
71
|
+
- test/db2.sqlite3
|
68
72
|
- test/test_config.rb
|
69
73
|
- test/unit/filter_test.rb
|
70
74
|
- test/unit/model_extensions_test.rb
|
71
75
|
- test/unit/multitenancy_test.rb
|
72
76
|
- test/unit/rest_client_test.rb
|
77
|
+
- test/unit/switch_db_test.rb
|
73
78
|
- test/unit/tenant_test.rb
|
74
79
|
has_rdoc:
|