apartment 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/.travis.yml +5 -8
- data/Gemfile +1 -0
- data/README.md +79 -4
- data/apartment.gemspec +2 -2
- data/gemfiles/rails_3_2.gemfile +2 -0
- data/lib/apartment.rb +22 -2
- data/lib/apartment/adapters/abstract_adapter.rb +70 -16
- data/lib/apartment/adapters/abstract_jdbc_adapter.rb +4 -9
- data/lib/apartment/adapters/jdbc_mysql_adapter.rb +2 -13
- data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +5 -16
- data/lib/apartment/adapters/mysql2_adapter.rb +8 -19
- data/lib/apartment/adapters/postgresql_adapter.rb +16 -41
- data/lib/apartment/adapters/sqlite3_adapter.rb +6 -3
- data/lib/apartment/elevators/first_subdomain.rb +1 -1
- data/lib/apartment/elevators/generic.rb +5 -3
- data/lib/apartment/version.rb +1 -1
- data/lib/generators/apartment/install/templates/apartment.rb +26 -1
- data/lib/tasks/apartment.rake +0 -1
- data/spec/adapters/jdbc_mysql_adapter_spec.rb +1 -1
- data/spec/adapters/jdbc_postgresql_adapter_spec.rb +1 -1
- data/spec/adapters/mysql2_adapter_spec.rb +2 -1
- data/spec/adapters/postgresql_adapter_spec.rb +1 -0
- data/spec/adapters/sqlite3_adapter_spec.rb +56 -0
- data/spec/apartment_spec.rb +2 -2
- data/spec/examples/connection_adapter_examples.rb +1 -1
- data/spec/examples/generic_adapter_custom_configuration_example.rb +90 -0
- data/spec/examples/generic_adapter_examples.rb +15 -15
- data/spec/examples/schema_adapter_examples.rb +25 -25
- data/spec/integration/apartment_rake_integration_spec.rb +4 -4
- data/spec/integration/query_caching_spec.rb +2 -2
- data/spec/spec_helper.rb +11 -0
- data/spec/support/apartment_helpers.rb +8 -2
- data/spec/support/setup.rb +3 -3
- data/spec/tasks/apartment_rake_spec.rb +11 -11
- data/spec/tenant_spec.rb +12 -12
- data/spec/unit/config_spec.rb +53 -23
- data/spec/unit/elevators/domain_spec.rb +4 -4
- data/spec/unit/elevators/first_subdomain_spec.rb +7 -2
- data/spec/unit/elevators/generic_spec.rb +19 -2
- data/spec/unit/elevators/host_hash_spec.rb +2 -2
- data/spec/unit/elevators/subdomain_spec.rb +6 -6
- data/spec/unit/migrator_spec.rb +1 -1
- data/spec/unit/reloader_spec.rb +2 -2
- metadata +11 -9
@@ -21,17 +21,8 @@ module Apartment
|
|
21
21
|
|
22
22
|
protected
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
# Catch specific adapter errors here
|
27
|
-
#
|
28
|
-
# @param {String} tenant Tenant name
|
29
|
-
#
|
30
|
-
def connect_to_new(tenant = nil)
|
31
|
-
super
|
32
|
-
rescue Mysql2::Error
|
33
|
-
Apartment::Tenant.reset
|
34
|
-
raise TenantNotFound, "Cannot find tenant #{environmentify(tenant)}"
|
24
|
+
def rescue_from
|
25
|
+
Mysql2::Error
|
35
26
|
end
|
36
27
|
end
|
37
28
|
|
@@ -49,12 +40,6 @@ module Apartment
|
|
49
40
|
Apartment.connection.execute "use `#{default_tenant}`"
|
50
41
|
end
|
51
42
|
|
52
|
-
# Set the table_name to always use the default tenant for excluded models
|
53
|
-
#
|
54
|
-
def process_excluded_models
|
55
|
-
Apartment.excluded_models.each{ |model| process_excluded_model(model) }
|
56
|
-
end
|
57
|
-
|
58
43
|
protected
|
59
44
|
|
60
45
|
# Connect to new tenant
|
@@ -64,9 +49,9 @@ module Apartment
|
|
64
49
|
|
65
50
|
Apartment.connection.execute "use `#{environmentify(tenant)}`"
|
66
51
|
|
67
|
-
rescue ActiveRecord::StatementInvalid
|
52
|
+
rescue ActiveRecord::StatementInvalid => exception
|
68
53
|
Apartment::Tenant.reset
|
69
|
-
|
54
|
+
raise_connect_error!(tenant, exception)
|
70
55
|
end
|
71
56
|
|
72
57
|
def process_excluded_model(model)
|
@@ -77,6 +62,10 @@ module Apartment
|
|
77
62
|
klass.table_name = "#{default_tenant}.#{table_name}"
|
78
63
|
end
|
79
64
|
end
|
65
|
+
|
66
|
+
def reset_on_connection_exception?
|
67
|
+
true
|
68
|
+
end
|
80
69
|
end
|
81
70
|
end
|
82
71
|
end
|
@@ -15,14 +15,6 @@ module Apartment
|
|
15
15
|
# Default adapter when not using Postgresql Schemas
|
16
16
|
class PostgresqlAdapter < AbstractAdapter
|
17
17
|
|
18
|
-
def drop(tenant)
|
19
|
-
# Apartment.connection.drop_database note that drop_database will not throw an exception, so manually execute
|
20
|
-
Apartment.connection.execute(%{DROP DATABASE "#{tenant}"})
|
21
|
-
|
22
|
-
rescue *rescuable_exceptions
|
23
|
-
raise TenantNotFound, "The tenant #{tenant} cannot be found"
|
24
|
-
end
|
25
|
-
|
26
18
|
private
|
27
19
|
|
28
20
|
def rescue_from
|
@@ -39,31 +31,6 @@ module Apartment
|
|
39
31
|
reset
|
40
32
|
end
|
41
33
|
|
42
|
-
# Drop the tenant
|
43
|
-
#
|
44
|
-
# @param {String} tenant Database (schema) to drop
|
45
|
-
#
|
46
|
-
def drop(tenant)
|
47
|
-
Apartment.connection.execute(%{DROP SCHEMA "#{tenant}" CASCADE})
|
48
|
-
|
49
|
-
rescue *rescuable_exceptions
|
50
|
-
raise TenantNotFound, "The schema #{tenant.inspect} cannot be found."
|
51
|
-
end
|
52
|
-
|
53
|
-
# Reset search path to default search_path
|
54
|
-
# Set the table_name to always use the default namespace for excluded models
|
55
|
-
#
|
56
|
-
def process_excluded_models
|
57
|
-
Apartment.excluded_models.each do |excluded_model|
|
58
|
-
excluded_model.constantize.tap do |klass|
|
59
|
-
# Ensure that if a schema *was* set, we override
|
60
|
-
table_name = klass.table_name.split('.', 2).last
|
61
|
-
|
62
|
-
klass.table_name = "#{default_tenant}.#{table_name}"
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
34
|
# Reset schema search path to the default schema_search_path
|
68
35
|
#
|
69
36
|
# @return {String} default schema search path
|
@@ -79,6 +46,19 @@ module Apartment
|
|
79
46
|
|
80
47
|
protected
|
81
48
|
|
49
|
+
def process_excluded_model(excluded_model)
|
50
|
+
excluded_model.constantize.tap do |klass|
|
51
|
+
# Ensure that if a schema *was* set, we override
|
52
|
+
table_name = klass.table_name.split('.', 2).last
|
53
|
+
|
54
|
+
klass.table_name = "#{default_tenant}.#{table_name}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def drop_command(conn, tenant)
|
59
|
+
conn.execute(%{DROP SCHEMA "#{tenant}" CASCADE})
|
60
|
+
end
|
61
|
+
|
82
62
|
# Set schema search path to new schema
|
83
63
|
#
|
84
64
|
def connect_to_new(tenant = nil)
|
@@ -92,17 +72,12 @@ module Apartment
|
|
92
72
|
raise TenantNotFound, "One of the following schema(s) is invalid: \"#{tenant}\" #{full_search_path}"
|
93
73
|
end
|
94
74
|
|
95
|
-
|
96
|
-
#
|
97
|
-
def create_tenant(tenant)
|
98
|
-
Apartment.connection.execute(%{CREATE SCHEMA "#{tenant}"})
|
75
|
+
private
|
99
76
|
|
100
|
-
|
101
|
-
|
77
|
+
def create_tenant_command(conn, tenant)
|
78
|
+
conn.execute(%{CREATE SCHEMA "#{tenant}"})
|
102
79
|
end
|
103
80
|
|
104
|
-
private
|
105
|
-
|
106
81
|
# Generate the final search path to set including persistent_schemas
|
107
82
|
#
|
108
83
|
def full_search_path
|
@@ -39,14 +39,17 @@ module Apartment
|
|
39
39
|
raise TenantExists,
|
40
40
|
"The tenant #{environmentify(tenant)} already exists." if File.exists?(database_file(tenant))
|
41
41
|
|
42
|
-
|
43
|
-
|
42
|
+
begin
|
43
|
+
f = File.new(database_file(tenant), File::CREAT)
|
44
|
+
ensure
|
45
|
+
f.close
|
46
|
+
end
|
44
47
|
end
|
45
48
|
|
46
49
|
private
|
47
50
|
|
48
51
|
def database_file(tenant)
|
49
|
-
"#{@default_dir}/#{tenant}.sqlite3"
|
52
|
+
"#{@default_dir}/#{environmentify(tenant)}.sqlite3"
|
50
53
|
end
|
51
54
|
end
|
52
55
|
end
|
@@ -18,9 +18,11 @@ module Apartment
|
|
18
18
|
|
19
19
|
database = @processor.call(request)
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
21
|
+
if database
|
22
|
+
Apartment::Tenant.switch(database) { @app.call(env) }
|
23
|
+
else
|
24
|
+
@app.call(env)
|
25
|
+
end
|
24
26
|
end
|
25
27
|
|
26
28
|
def parse_database_name(request)
|
data/lib/apartment/version.rb
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
# require 'apartment/elevators/generic'
|
6
6
|
# require 'apartment/elevators/domain'
|
7
7
|
require 'apartment/elevators/subdomain'
|
8
|
+
# require 'apartment/elevators/first_subdomain'
|
8
9
|
|
9
10
|
#
|
10
11
|
# Apartment Configuration
|
@@ -18,10 +19,33 @@ Apartment.configure do |config|
|
|
18
19
|
|
19
20
|
# In order to migrate all of your Tenants you need to provide a list of Tenant names to Apartment.
|
20
21
|
# You can make this dynamic by providing a Proc object to be called on migrations.
|
21
|
-
# This object should yield
|
22
|
+
# This object should yield either:
|
23
|
+
# - an array of strings representing each Tenant name.
|
24
|
+
# - a hash which keys are tenant names, and values custom db config (must contain all key/values required in database.yml)
|
22
25
|
#
|
23
26
|
# config.tenant_names = lambda{ Customer.pluck(:tenant_name) }
|
24
27
|
# config.tenant_names = ['tenant1', 'tenant2']
|
28
|
+
# config.tenant_names = {
|
29
|
+
# 'tenant1' => {
|
30
|
+
# adapter: 'postgresql',
|
31
|
+
# host: 'some_server',
|
32
|
+
# port: 5555,
|
33
|
+
# database: 'postgres' # this is not the name of the tenant's db
|
34
|
+
# # but the name of the database to connect to before creating the tenant's db
|
35
|
+
# # mandatory in postgresql
|
36
|
+
# },
|
37
|
+
# 'tenant2' => {
|
38
|
+
# adapter: 'postgresql',
|
39
|
+
# database: 'postgres' # this is not the name of the tenant's db
|
40
|
+
# # but the name of the database to connect to before creating the tenant's db
|
41
|
+
# # mandatory in postgresql
|
42
|
+
# }
|
43
|
+
# }
|
44
|
+
# config.tenant_names = lambda do
|
45
|
+
# Tenant.all.each_with_object({}) do |tenant, hash|
|
46
|
+
# hash[tenant.name] = tenant.db_configuration
|
47
|
+
# end
|
48
|
+
# end
|
25
49
|
#
|
26
50
|
config.tenant_names = lambda { ToDo_Tenant_Or_User_Model.pluck :database }
|
27
51
|
|
@@ -65,3 +89,4 @@ end
|
|
65
89
|
|
66
90
|
# Rails.application.config.middleware.use 'Apartment::Elevators::Domain'
|
67
91
|
Rails.application.config.middleware.use 'Apartment::Elevators::Subdomain'
|
92
|
+
# Rails.application.config.middleware.use 'Apartment::Elevators::FirstSubdomain'
|
data/lib/tasks/apartment.rake
CHANGED
@@ -35,7 +35,7 @@ describe Apartment::Adapters::Mysql2Adapter, database: :mysql do
|
|
35
35
|
it "should process model exclusions" do
|
36
36
|
Apartment::Tenant.init
|
37
37
|
|
38
|
-
Company.table_name.
|
38
|
+
expect(Company.table_name).to eq("#{default_tenant}.companies")
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
@@ -44,6 +44,7 @@ describe Apartment::Adapters::Mysql2Adapter, database: :mysql do
|
|
44
44
|
before { Apartment.use_schemas = false }
|
45
45
|
|
46
46
|
it_should_behave_like "a generic apartment adapter"
|
47
|
+
it_should_behave_like "a generic apartment adapter able to handle custom configuration"
|
47
48
|
it_should_behave_like "a connection based apartment adapter"
|
48
49
|
end
|
49
50
|
end
|
@@ -54,6 +54,7 @@ describe Apartment::Adapters::PostgresqlAdapter, database: :postgresql do
|
|
54
54
|
let(:default_tenant) { subject.switch { ActiveRecord::Base.connection.current_database } }
|
55
55
|
|
56
56
|
it_should_behave_like "a generic apartment adapter"
|
57
|
+
it_should_behave_like "a generic apartment adapter able to handle custom configuration"
|
57
58
|
it_should_behave_like "a connection based apartment adapter"
|
58
59
|
end
|
59
60
|
end
|
@@ -23,5 +23,61 @@ describe Apartment::Adapters::Sqlite3Adapter, database: :sqlite do
|
|
23
23
|
File.delete(Apartment::Test.config['connections']['sqlite']['database'])
|
24
24
|
end
|
25
25
|
end
|
26
|
+
|
27
|
+
context "with prepend and append" do
|
28
|
+
let(:default_dir) { File.expand_path(File.dirname(config[:database])) }
|
29
|
+
describe "#prepend" do
|
30
|
+
let (:db_name) { "db_with_prefix" }
|
31
|
+
before do
|
32
|
+
Apartment.configure do |config|
|
33
|
+
config.prepend_environment = true
|
34
|
+
config.append_environment = false
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
after { subject.drop db_name rescue nil }
|
39
|
+
|
40
|
+
it "should create a new database" do
|
41
|
+
subject.create db_name
|
42
|
+
|
43
|
+
expect(File.exists?("#{default_dir}/#{Rails.env}_#{db_name}.sqlite3")).to eq true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#neither" do
|
48
|
+
let (:db_name) { "db_without_prefix_suffix" }
|
49
|
+
before do
|
50
|
+
Apartment.configure { |config| config.prepend_environment = config.append_environment = false }
|
51
|
+
end
|
52
|
+
|
53
|
+
after { subject.drop db_name rescue nil }
|
54
|
+
|
55
|
+
it "should create a new database" do
|
56
|
+
subject.create db_name
|
57
|
+
|
58
|
+
expect(File.exists?("#{default_dir}/#{db_name}.sqlite3")).to eq true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#append" do
|
63
|
+
let (:db_name) { "db_with_suffix" }
|
64
|
+
before do
|
65
|
+
Apartment.configure do |config|
|
66
|
+
config.prepend_environment = false
|
67
|
+
config.append_environment = true
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
after { subject.drop db_name rescue nil }
|
72
|
+
|
73
|
+
it "should create a new database" do
|
74
|
+
subject.create db_name
|
75
|
+
|
76
|
+
expect(File.exists?("#{default_dir}/#{db_name}_#{Rails.env}.sqlite3")).to eq true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
26
82
|
end
|
27
83
|
end
|
data/spec/apartment_spec.rb
CHANGED
@@ -2,11 +2,11 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Apartment do
|
4
4
|
it "should be valid" do
|
5
|
-
Apartment.
|
5
|
+
expect(Apartment).to be_a(Module)
|
6
6
|
end
|
7
7
|
|
8
8
|
it "should be a valid app" do
|
9
|
-
::Rails.application.
|
9
|
+
expect(::Rails.application).to be_a(Dummy::Application)
|
10
10
|
end
|
11
11
|
|
12
12
|
it "should deprecate Apartment::Database in favor of Apartment::Tenant" do
|
@@ -12,7 +12,7 @@ shared_examples_for "a connection based apartment adapter" do
|
|
12
12
|
end
|
13
13
|
Apartment::Tenant.init
|
14
14
|
|
15
|
-
Company.connection.object_id.
|
15
|
+
expect(Company.connection.object_id).not_to eq(ActiveRecord::Base.connection.object_id)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
shared_examples_for "a generic apartment adapter able to handle custom configuration" do
|
4
|
+
|
5
|
+
let(:custom_tenant_name) { 'test_tenantwwww' }
|
6
|
+
let(:db) { |example| example.metadata[:database]}
|
7
|
+
let(:custom_tenant_names) do
|
8
|
+
{
|
9
|
+
custom_tenant_name => get_custom_db_conf
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
before do
|
14
|
+
Apartment.tenant_names = custom_tenant_names
|
15
|
+
end
|
16
|
+
|
17
|
+
context "database key taken from specific config" do
|
18
|
+
|
19
|
+
let(:expected_args) { get_custom_db_conf }
|
20
|
+
|
21
|
+
describe "#create" do
|
22
|
+
it "should establish_connection with the separate connection with expected args" do
|
23
|
+
expect(Apartment::Adapters::AbstractAdapter::SeparateDbConnectionHandler).to receive(:establish_connection).with(expected_args).and_call_original
|
24
|
+
|
25
|
+
# because we dont have another server to connect to it errors
|
26
|
+
# what matters is establish_connection receives proper args
|
27
|
+
expect { subject.create(custom_tenant_name) }.to raise_error(Apartment::TenantExists)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#drop" do
|
32
|
+
it "should establish_connection with the separate connection with expected args" do
|
33
|
+
expect(Apartment::Adapters::AbstractAdapter::SeparateDbConnectionHandler).to receive(:establish_connection).with(expected_args).and_call_original
|
34
|
+
|
35
|
+
# because we dont have another server to connect to it errors
|
36
|
+
# what matters is establish_connection receives proper args
|
37
|
+
expect { subject.drop(custom_tenant_name) }.to raise_error(Apartment::TenantNotFound)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "database key from tenant name" do
|
43
|
+
|
44
|
+
let(:expected_args) {
|
45
|
+
get_custom_db_conf.tap {|args| args.delete(:database) }
|
46
|
+
}
|
47
|
+
|
48
|
+
describe "#switch!" do
|
49
|
+
|
50
|
+
it "should connect to new db" do
|
51
|
+
expect(Apartment).to receive(:establish_connection) do |args|
|
52
|
+
db_name = args.delete(:database)
|
53
|
+
|
54
|
+
expect(args).to eq expected_args
|
55
|
+
expect(db_name).to match custom_tenant_name
|
56
|
+
|
57
|
+
# we only need to check args, then we short circuit
|
58
|
+
# in order to avoid the mess due to the `establish_connection` override
|
59
|
+
raise ActiveRecord::ActiveRecordError
|
60
|
+
end
|
61
|
+
|
62
|
+
expect { subject.switch!(custom_tenant_name) }.to raise_error(Apartment::TenantNotFound)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def specific_connection
|
68
|
+
{
|
69
|
+
postgresql: {
|
70
|
+
adapter: 'postgresql',
|
71
|
+
database: 'override_database',
|
72
|
+
password: 'override_password',
|
73
|
+
username: 'overridepostgres'
|
74
|
+
},
|
75
|
+
mysql: {
|
76
|
+
adapter: 'mysql2',
|
77
|
+
database: 'override_database',
|
78
|
+
username: 'root'
|
79
|
+
},
|
80
|
+
sqlite: {
|
81
|
+
adapter: 'sqlite3',
|
82
|
+
database: 'override_database'
|
83
|
+
}
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
def get_custom_db_conf
|
88
|
+
specific_connection[db.to_sym].with_indifferent_access
|
89
|
+
end
|
90
|
+
end
|