multitenancy 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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 single 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.
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
@@ -1,2 +1,10 @@
1
1
  #!/usr/bin/env rake
2
- require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << 'test'
6
+ t.test_files = FileList['test/**/*.rb']
7
+ end
8
+
9
+ desc "Run tests"
10
+ task :default => :test
@@ -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
- return block.call
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
@@ -1,3 +1,3 @@
1
1
  module Multitenancy
2
- VERSION = "0.0.4"
2
+ VERSION = "0.0.5"
3
3
  end
@@ -1,3 +1,6 @@
1
- sqlite:
1
+ db1:
2
2
  adapter: sqlite3
3
- database: test/multitenancy.sqlite3
3
+ database: test/db1.sqlite3
4
+ db2:
5
+ adapter: sqlite3
6
+ database: test/db2.sqlite3
Binary file
@@ -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.establish_connection(config[ENV['DB'] || 'sqlite'])
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
- setup do
6
- Multitenancy.init({:tenant_header => 'X_COMPANY_ID', :sub_tenant_header => 'X_USER_ID'})
7
- end
8
-
9
- should "append tenant headers to rest calls within a tenant scope" do
10
- RestClient::Request.any_instance.expects(:make_headers).with() {|headers| headers[Multitenancy.tenant_header] == 'Flipkart' && headers[Multitenancy.sub_tenant_header] == 'ganeshs'}
11
- RestClient::Request.any_instance.expects(:execute)
12
- Multitenancy.with_tenant(Multitenancy::Tenant.new('Flipkart', 'ganeshs')) do
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
- end
16
-
17
- should "not append tenant headers to rest calls outside of a tenant scope" do
18
- RestClient::Request.any_instance.expects(:make_headers).with() {|headers| headers[Multitenancy.tenant_header].nil? && headers[Multitenancy.sub_tenant_header].nil? }
19
- RestClient::Request.any_instance.expects(:execute)
20
- RestClient.get('http://some/resource')
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
@@ -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
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: 2012-12-11 00:00:00.000000000Z
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/multitenancy.sqlite3
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/multitenancy.sqlite3
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: