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 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: