apartment 1.2.0 → 2.0.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/.travis.yml +45 -4
- data/Appraisals +12 -9
- data/HISTORY.md +18 -0
- data/README.md +69 -42
- data/Rakefile +24 -5
- data/apartment.gemspec +6 -16
- data/docker-compose.yml +14 -0
- data/gemfiles/rails_4_0.gemfile +1 -1
- data/gemfiles/rails_4_1.gemfile +1 -1
- data/gemfiles/rails_4_2.gemfile +1 -1
- data/gemfiles/rails_5_0.gemfile +1 -1
- data/gemfiles/{rails_3_2.gemfile → rails_5_1.gemfile} +2 -3
- data/gemfiles/rails_master.gemfile +12 -0
- data/lib/apartment.rb +3 -25
- data/lib/apartment/adapters/abstract_adapter.rb +19 -44
- data/lib/apartment/adapters/postgresql_adapter.rb +17 -3
- data/lib/apartment/elevators/generic.rb +1 -20
- data/lib/apartment/elevators/subdomain.rb +15 -4
- data/lib/apartment/railtie.rb +5 -2
- data/lib/apartment/tenant.rb +0 -10
- data/lib/apartment/version.rb +1 -1
- data/lib/generators/apartment/install/templates/apartment.rb +13 -8
- data/lib/tasks/apartment.rake +2 -2
- data/spec/adapters/mysql2_adapter_spec.rb +8 -0
- data/spec/apartment_spec.rb +1 -5
- data/spec/dummy/config/application.rb +1 -1
- data/spec/dummy/config/database.yml.sample +3 -1
- data/spec/dummy/db/migrate/20110613152810_create_dummy_models.rb +2 -1
- data/spec/dummy/db/migrate/20111202022214_create_table_books.rb +2 -1
- data/spec/dummy_engine/config/initializers/apartment.rb +3 -3
- data/spec/examples/connection_adapter_examples.rb +8 -0
- data/spec/examples/generic_adapter_examples.rb +35 -17
- data/spec/examples/schema_adapter_examples.rb +8 -0
- data/spec/integration/query_caching_spec.rb +63 -23
- data/spec/spec_helper.rb +12 -0
- data/spec/tenant_spec.rb +10 -3
- data/spec/unit/config_spec.rb +0 -7
- data/spec/unit/elevators/subdomain_spec.rb +28 -8
- metadata +20 -17
data/lib/apartment/version.rb
CHANGED
@@ -49,16 +49,21 @@ Apartment.configure do |config|
|
|
49
49
|
#
|
50
50
|
config.tenant_names = lambda { ToDo_Tenant_Or_User_Model.pluck :database }
|
51
51
|
|
52
|
+
# PostgreSQL:
|
53
|
+
# Specifies whether to use PostgreSQL schemas or create a new database per Tenant.
|
54
|
+
#
|
55
|
+
# MySQL:
|
56
|
+
# Specifies whether to switch databases by using `use` statement or re-establish connection.
|
52
57
|
#
|
53
|
-
# ==> PostgreSQL only options
|
54
|
-
|
55
|
-
# Specifies whether to use PostgreSQL schemas or create a new database per Tenant.
|
56
58
|
# The default behaviour is true.
|
57
59
|
#
|
58
60
|
# config.use_schemas = true
|
59
61
|
|
62
|
+
#
|
63
|
+
# ==> PostgreSQL only options
|
64
|
+
|
60
65
|
# Apartment can be forced to use raw SQL dumps instead of schema.rb for creating new schemas.
|
61
|
-
# Use this when you are using some extra features in PostgreSQL that can't be
|
66
|
+
# Use this when you are using some extra features in PostgreSQL that can't be represented in
|
62
67
|
# schema.rb, like materialized views etc. (only applies with use_schemas set to true).
|
63
68
|
# (Note: this option doesn't use db/structure.sql, it creates SQL dump by executing pg_dump)
|
64
69
|
#
|
@@ -83,10 +88,10 @@ end
|
|
83
88
|
|
84
89
|
# Setup a custom Tenant switching middleware. The Proc should return the name of the Tenant that
|
85
90
|
# you want to switch to.
|
86
|
-
# Rails.application.config.middleware.use
|
91
|
+
# Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request|
|
87
92
|
# request.host.split('.').first
|
88
93
|
# }
|
89
94
|
|
90
|
-
# Rails.application.config.middleware.use
|
91
|
-
Rails.application.config.middleware.use
|
92
|
-
# Rails.application.config.middleware.use
|
95
|
+
# Rails.application.config.middleware.use Apartment::Elevators::Domain
|
96
|
+
Rails.application.config.middleware.use Apartment::Elevators::Subdomain
|
97
|
+
# Rails.application.config.middleware.use Apartment::Elevators::FirstSubdomain
|
data/lib/tasks/apartment.rake
CHANGED
@@ -3,11 +3,11 @@ require 'apartment/migrator'
|
|
3
3
|
apartment_namespace = namespace :apartment do
|
4
4
|
|
5
5
|
desc "Create all tenants"
|
6
|
-
task create
|
6
|
+
task :create do
|
7
7
|
tenants.each do |tenant|
|
8
8
|
begin
|
9
9
|
puts("Creating #{tenant} tenant")
|
10
|
-
|
10
|
+
Apartment::Tenant.create(tenant)
|
11
11
|
rescue Apartment::TenantExists => e
|
12
12
|
puts e.message
|
13
13
|
end
|
@@ -32,6 +32,14 @@ describe Apartment::Adapters::Mysql2Adapter, database: :mysql do
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
+
after do
|
36
|
+
# Apartment::Tenant.init creates per model connection.
|
37
|
+
# Remove the connection after testing not to unintentionally keep the connection across tests.
|
38
|
+
Apartment.excluded_models.each do |excluded_model|
|
39
|
+
excluded_model.constantize.remove_connection
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
35
43
|
it "should process model exclusions" do
|
36
44
|
Apartment::Tenant.init
|
37
45
|
|
data/spec/apartment_spec.rb
CHANGED
@@ -8,8 +8,4 @@ describe Apartment do
|
|
8
8
|
it "should be a valid app" do
|
9
9
|
expect(::Rails.application).to be_a(Dummy::Application)
|
10
10
|
end
|
11
|
-
|
12
|
-
it "should deprecate Apartment::Database in favor of Apartment::Tenant" do
|
13
|
-
expect(Apartment::Database).to eq(Apartment::Tenant)
|
14
|
-
end
|
15
|
-
end
|
11
|
+
end
|
@@ -17,7 +17,7 @@ module Dummy
|
|
17
17
|
require 'apartment/elevators/subdomain'
|
18
18
|
require 'apartment/elevators/domain'
|
19
19
|
|
20
|
-
config.middleware.use
|
20
|
+
config.middleware.use Apartment::Elevators::Subdomain
|
21
21
|
|
22
22
|
# Custom directories with classes and modules you want to be autoloadable.
|
23
23
|
config.autoload_paths += %W(#{config.root}/lib)
|
@@ -25,6 +25,7 @@ development:
|
|
25
25
|
test:
|
26
26
|
adapter: postgresql
|
27
27
|
database: apartment_postgresql_test
|
28
|
+
username: postgres
|
28
29
|
min_messages: WARNING
|
29
30
|
pool: 5
|
30
31
|
timeout: 5000
|
@@ -32,7 +33,8 @@ test:
|
|
32
33
|
development:
|
33
34
|
adapter: postgresql
|
34
35
|
database: apartment_postgresql_development
|
36
|
+
username: postgres
|
35
37
|
min_messages: WARNING
|
36
38
|
pool: 5
|
37
39
|
timeout: 5000
|
38
|
-
<% end %>
|
40
|
+
<% end %>
|
@@ -1,4 +1,5 @@
|
|
1
|
-
|
1
|
+
migration_class = (ActiveRecord::VERSION::MAJOR >= 5) ? ActiveRecord::Migration[4.2] : ActiveRecord::Migration
|
2
|
+
class CreateDummyModels < migration_class
|
2
3
|
def self.up
|
3
4
|
create_table :companies do |t|
|
4
5
|
t.boolean :dummy
|
@@ -42,10 +42,10 @@ end
|
|
42
42
|
##
|
43
43
|
# Elevator Configuration
|
44
44
|
|
45
|
-
# Rails.application.config.middleware.use
|
45
|
+
# Rails.application.config.middleware.use Apartment::Elevators::Generic, lambda { |request|
|
46
46
|
# # TODO: supply generic implementation
|
47
47
|
# }
|
48
48
|
|
49
|
-
# Rails.application.config.middleware.use
|
49
|
+
# Rails.application.config.middleware.use Apartment::Elevators::Domain
|
50
50
|
|
51
|
-
Rails.application.config.middleware.use
|
51
|
+
Rails.application.config.middleware.use Apartment::Elevators::Subdomain
|
@@ -6,6 +6,14 @@ shared_examples_for "a connection based apartment adapter" do
|
|
6
6
|
let(:default_tenant){ subject.switch{ ActiveRecord::Base.connection.current_database } }
|
7
7
|
|
8
8
|
describe "#init" do
|
9
|
+
after do
|
10
|
+
# Apartment::Tenant.init creates per model connection.
|
11
|
+
# Remove the connection after testing not to unintentionally keep the connection across tests.
|
12
|
+
Apartment.excluded_models.each do |excluded_model|
|
13
|
+
excluded_model.constantize.remove_connection
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
9
17
|
it "should process model exclusions" do
|
10
18
|
Apartment.configure do |config|
|
11
19
|
config.excluded_models = ["Company"]
|
@@ -8,6 +8,25 @@ shared_examples_for "a generic apartment adapter" do
|
|
8
8
|
Apartment.append_environment = false
|
9
9
|
}
|
10
10
|
|
11
|
+
describe "#init" do
|
12
|
+
it "should not retain a connection after railtie" do
|
13
|
+
# this test should work on rails >= 4, the connection pool code is
|
14
|
+
# completely different for 3.2 so we'd have to have a messy conditional..
|
15
|
+
unless Rails::VERSION::MAJOR < 4
|
16
|
+
ActiveRecord::Base.connection_pool.disconnect!
|
17
|
+
|
18
|
+
Apartment::Railtie.config.to_prepare_blocks.map(&:call)
|
19
|
+
|
20
|
+
num_available_connections = Apartment.connection_class.connection_pool
|
21
|
+
.instance_variable_get(:@available)
|
22
|
+
.instance_variable_get(:@queue)
|
23
|
+
.size
|
24
|
+
|
25
|
+
expect(num_available_connections).to eq(1)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
11
30
|
#
|
12
31
|
# Creates happen already in our before_filter
|
13
32
|
#
|
@@ -39,6 +58,22 @@ shared_examples_for "a generic apartment adapter" do
|
|
39
58
|
|
40
59
|
subject.switch(db2){ expect(User.count).to eq(@count + 1) }
|
41
60
|
end
|
61
|
+
|
62
|
+
it "should raise error when the schema.rb is missing unless Apartment.use_sql is set to true" do
|
63
|
+
next if Apartment.use_sql
|
64
|
+
|
65
|
+
subject.drop(db1)
|
66
|
+
begin
|
67
|
+
Dir.mktmpdir do |tmpdir|
|
68
|
+
Apartment.database_schema_file = "#{tmpdir}/schema.rb"
|
69
|
+
expect {
|
70
|
+
subject.create(db1)
|
71
|
+
}.to raise_error(Apartment::FileNotFound)
|
72
|
+
end
|
73
|
+
ensure
|
74
|
+
Apartment.remove_instance_variable(:@database_schema_file)
|
75
|
+
end
|
76
|
+
end
|
42
77
|
end
|
43
78
|
|
44
79
|
describe "#drop" do
|
@@ -83,23 +118,6 @@ shared_examples_for "a generic apartment adapter" do
|
|
83
118
|
subject.switch(db1){ subject.drop(db2) }
|
84
119
|
}.to_not raise_error
|
85
120
|
end
|
86
|
-
|
87
|
-
it "warns if no block is given, but calls switch!" do
|
88
|
-
expect(Apartment::Deprecation).to receive(:warn)
|
89
|
-
|
90
|
-
subject.switch(db1)
|
91
|
-
expect(subject.current).to eq(db1)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
describe "#process" do
|
96
|
-
it "is deprecated" do
|
97
|
-
expect(Apartment::Deprecation).to receive(:warn)
|
98
|
-
|
99
|
-
subject.process(db1) do
|
100
|
-
expect(subject.current).to eq(db1)
|
101
|
-
end
|
102
|
-
end
|
103
121
|
end
|
104
122
|
|
105
123
|
describe "#reset" do
|
@@ -15,6 +15,14 @@ shared_examples_for "a schema based apartment adapter" do
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
+
after do
|
19
|
+
# Apartment::Tenant.init creates per model connection.
|
20
|
+
# Remove the connection after testing not to unintentionally keep the connection across tests.
|
21
|
+
Apartment.excluded_models.each do |excluded_model|
|
22
|
+
excluded_model.constantize.remove_connection
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
18
26
|
it "should process model exclusions" do
|
19
27
|
Apartment::Tenant.init
|
20
28
|
|
@@ -1,41 +1,81 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe 'query caching' do
|
4
|
-
|
4
|
+
describe 'when use_schemas = true' do
|
5
|
+
let(:db_names) { [db1, db2] }
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
before do
|
8
|
+
Apartment.configure do |config|
|
9
|
+
config.excluded_models = ["Company"]
|
10
|
+
config.tenant_names = lambda{ Company.pluck(:database) }
|
11
|
+
config.use_schemas = true
|
12
|
+
end
|
13
|
+
|
14
|
+
Apartment::Tenant.reload!(config)
|
15
|
+
|
16
|
+
db_names.each do |db_name|
|
17
|
+
Apartment::Tenant.create(db_name)
|
18
|
+
Company.create database: db_name
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
after do
|
23
|
+
db_names.each{ |db| Apartment::Tenant.drop(db) }
|
24
|
+
Apartment::Tenant.reset
|
25
|
+
Company.delete_all
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'clears the ActiveRecord::QueryCache after switching databases' do
|
29
|
+
db_names.each do |db_name|
|
30
|
+
Apartment::Tenant.switch! db_name
|
31
|
+
User.create! name: db_name
|
32
|
+
end
|
33
|
+
|
34
|
+
ActiveRecord::Base.connection.enable_query_cache!
|
35
|
+
|
36
|
+
Apartment::Tenant.switch! db_names.first
|
37
|
+
expect(User.find_by_name(db_names.first).name).to eq(db_names.first)
|
38
|
+
|
39
|
+
Apartment::Tenant.switch! db_names.last
|
40
|
+
expect(User.find_by_name(db_names.first)).to be_nil
|
11
41
|
end
|
42
|
+
end
|
12
43
|
|
13
|
-
|
44
|
+
describe 'when use_schemas = false' do
|
45
|
+
let(:db_name) { db1 }
|
46
|
+
|
47
|
+
before do
|
48
|
+
Apartment.configure do |config|
|
49
|
+
config.excluded_models = ["Company"]
|
50
|
+
config.tenant_names = lambda{ Company.pluck(:database) }
|
51
|
+
config.use_schemas = false
|
52
|
+
end
|
53
|
+
|
54
|
+
Apartment::Tenant.reload!(config)
|
14
55
|
|
15
|
-
db_names.each do |db_name|
|
16
56
|
Apartment::Tenant.create(db_name)
|
17
57
|
Company.create database: db_name
|
18
58
|
end
|
19
|
-
end
|
20
59
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
Company.delete_all
|
25
|
-
end
|
60
|
+
after do
|
61
|
+
# Avoid cannot drop the currently open database. Maybe there is a better way to handle this.
|
62
|
+
Apartment::Tenant.switch! 'template1'
|
26
63
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
User.create! name: db_name
|
64
|
+
Apartment::Tenant.drop(db_name)
|
65
|
+
Apartment::Tenant.reset
|
66
|
+
Company.delete_all
|
31
67
|
end
|
32
68
|
|
33
|
-
|
69
|
+
it "configuration value is kept after switching databases" do
|
70
|
+
ActiveRecord::Base.connection.enable_query_cache!
|
34
71
|
|
35
|
-
|
36
|
-
|
72
|
+
Apartment::Tenant.switch! db_name
|
73
|
+
expect(Apartment.connection.query_cache_enabled).to be true
|
74
|
+
|
75
|
+
ActiveRecord::Base.connection.disable_query_cache!
|
37
76
|
|
38
|
-
|
39
|
-
|
77
|
+
Apartment::Tenant.switch! db_name
|
78
|
+
expect(Apartment.connection.query_cache_enabled).to be false
|
79
|
+
end
|
40
80
|
end
|
41
81
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -4,6 +4,18 @@ $LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
4
4
|
ENV["RAILS_ENV"] = "test"
|
5
5
|
|
6
6
|
require File.expand_path("../dummy/config/environment.rb", __FILE__)
|
7
|
+
|
8
|
+
# Loading dummy applications affects table_name of each excluded models
|
9
|
+
# defined in `spec/dummy/config/initializers/apartment.rb`.
|
10
|
+
# To make them pristine, we need to execute below lines.
|
11
|
+
Apartment.excluded_models.each do |model|
|
12
|
+
klass = model.constantize
|
13
|
+
|
14
|
+
Apartment.connection_class.remove_connection(klass)
|
15
|
+
klass.clear_all_connections!
|
16
|
+
klass.reset_table_name
|
17
|
+
end
|
18
|
+
|
7
19
|
require "rspec/rails"
|
8
20
|
require 'capybara/rspec'
|
9
21
|
require 'capybara/rails'
|
data/spec/tenant_spec.rb
CHANGED
@@ -59,8 +59,7 @@ describe Apartment::Tenant do
|
|
59
59
|
|
60
60
|
describe "#adapter" do
|
61
61
|
it "should load postgresql adapter" do
|
62
|
-
subject.adapter
|
63
|
-
expect(Apartment::Adapters::PostgresqlAdapter).to be_a(Class)
|
62
|
+
expect(subject.adapter).to be_a(Apartment::Adapters::PostgresqlSchemaAdapter)
|
64
63
|
end
|
65
64
|
|
66
65
|
it "raises exception with invalid adapter specified" do
|
@@ -68,7 +67,7 @@ describe Apartment::Tenant do
|
|
68
67
|
|
69
68
|
expect {
|
70
69
|
Apartment::Tenant.adapter
|
71
|
-
}.to raise_error
|
70
|
+
}.to raise_error(RuntimeError)
|
72
71
|
end
|
73
72
|
|
74
73
|
context "threadsafety" do
|
@@ -137,6 +136,14 @@ describe Apartment::Tenant do
|
|
137
136
|
subject.init
|
138
137
|
end
|
139
138
|
|
139
|
+
after do
|
140
|
+
# Apartment::Tenant.init creates per model connection.
|
141
|
+
# Remove the connection after testing not to unintentionally keep the connection across tests.
|
142
|
+
Apartment.excluded_models.each do |excluded_model|
|
143
|
+
excluded_model.constantize.remove_connection
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
140
147
|
it "should create excluded models in public schema" do
|
141
148
|
subject.reset # ensure we're on public schema
|
142
149
|
count = Company.count + x.times{ Company.create }
|
data/spec/unit/config_spec.rb
CHANGED
@@ -50,13 +50,6 @@ describe Apartment do
|
|
50
50
|
expect(Apartment.seed_after_create).to be true
|
51
51
|
end
|
52
52
|
|
53
|
-
it "should set tld_length" do
|
54
|
-
Apartment.configure do |config|
|
55
|
-
config.tld_length = 2
|
56
|
-
end
|
57
|
-
expect(Apartment.tld_length).to eq(2)
|
58
|
-
end
|
59
|
-
|
60
53
|
context "databases" do
|
61
54
|
let(:users_conf_hash) { { port: 5444 } }
|
62
55
|
|
@@ -6,7 +6,7 @@ describe Apartment::Elevators::Subdomain do
|
|
6
6
|
subject(:elevator){ described_class.new(Proc.new{}) }
|
7
7
|
|
8
8
|
describe "#parse_tenant_name" do
|
9
|
-
context "assuming
|
9
|
+
context "assuming one tld" do
|
10
10
|
it "should parse subdomain" do
|
11
11
|
request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.bar.com')
|
12
12
|
expect(elevator.parse_tenant_name(request)).to eq('foo')
|
@@ -18,13 +18,7 @@ describe Apartment::Elevators::Subdomain do
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
-
context "assuming
|
22
|
-
before do
|
23
|
-
Apartment.configure do |config|
|
24
|
-
config.tld_length = 2
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
21
|
+
context "assuming two tlds" do
|
28
22
|
it "should parse subdomain in the third level domain" do
|
29
23
|
request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.bar.co.uk')
|
30
24
|
expect(elevator.parse_tenant_name(request)).to eq("foo")
|
@@ -35,6 +29,32 @@ describe Apartment::Elevators::Subdomain do
|
|
35
29
|
expect(elevator.parse_tenant_name(request)).to be_nil
|
36
30
|
end
|
37
31
|
end
|
32
|
+
|
33
|
+
context "assuming two subdomains" do
|
34
|
+
it "should parse two subdomains in the two level domain" do
|
35
|
+
request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.xyz.bar.com')
|
36
|
+
expect(elevator.parse_tenant_name(request)).to eq("foo")
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should parse two subdomains in the third level domain" do
|
40
|
+
request = ActionDispatch::Request.new('HTTP_HOST' => 'foo.xyz.bar.co.uk')
|
41
|
+
expect(elevator.parse_tenant_name(request)).to eq("foo")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "assuming localhost" do
|
46
|
+
it "should return nil for localhost" do
|
47
|
+
request = ActionDispatch::Request.new('HTTP_HOST' => 'localhost')
|
48
|
+
expect(elevator.parse_tenant_name(request)).to be_nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context "assuming ip address" do
|
53
|
+
it "should return nil for an ip address" do
|
54
|
+
request = ActionDispatch::Request.new('HTTP_HOST' => '127.0.0.1')
|
55
|
+
expect(elevator.parse_tenant_name(request)).to be_nil
|
56
|
+
end
|
57
|
+
end
|
38
58
|
end
|
39
59
|
|
40
60
|
describe "#call" do
|