apartment 1.0.2 → 1.1.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/.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
|