apartment 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +5 -8
  4. data/Gemfile +1 -0
  5. data/README.md +79 -4
  6. data/apartment.gemspec +2 -2
  7. data/gemfiles/rails_3_2.gemfile +2 -0
  8. data/lib/apartment.rb +22 -2
  9. data/lib/apartment/adapters/abstract_adapter.rb +70 -16
  10. data/lib/apartment/adapters/abstract_jdbc_adapter.rb +4 -9
  11. data/lib/apartment/adapters/jdbc_mysql_adapter.rb +2 -13
  12. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +5 -16
  13. data/lib/apartment/adapters/mysql2_adapter.rb +8 -19
  14. data/lib/apartment/adapters/postgresql_adapter.rb +16 -41
  15. data/lib/apartment/adapters/sqlite3_adapter.rb +6 -3
  16. data/lib/apartment/elevators/first_subdomain.rb +1 -1
  17. data/lib/apartment/elevators/generic.rb +5 -3
  18. data/lib/apartment/version.rb +1 -1
  19. data/lib/generators/apartment/install/templates/apartment.rb +26 -1
  20. data/lib/tasks/apartment.rake +0 -1
  21. data/spec/adapters/jdbc_mysql_adapter_spec.rb +1 -1
  22. data/spec/adapters/jdbc_postgresql_adapter_spec.rb +1 -1
  23. data/spec/adapters/mysql2_adapter_spec.rb +2 -1
  24. data/spec/adapters/postgresql_adapter_spec.rb +1 -0
  25. data/spec/adapters/sqlite3_adapter_spec.rb +56 -0
  26. data/spec/apartment_spec.rb +2 -2
  27. data/spec/examples/connection_adapter_examples.rb +1 -1
  28. data/spec/examples/generic_adapter_custom_configuration_example.rb +90 -0
  29. data/spec/examples/generic_adapter_examples.rb +15 -15
  30. data/spec/examples/schema_adapter_examples.rb +25 -25
  31. data/spec/integration/apartment_rake_integration_spec.rb +4 -4
  32. data/spec/integration/query_caching_spec.rb +2 -2
  33. data/spec/spec_helper.rb +11 -0
  34. data/spec/support/apartment_helpers.rb +8 -2
  35. data/spec/support/setup.rb +3 -3
  36. data/spec/tasks/apartment_rake_spec.rb +11 -11
  37. data/spec/tenant_spec.rb +12 -12
  38. data/spec/unit/config_spec.rb +53 -23
  39. data/spec/unit/elevators/domain_spec.rb +4 -4
  40. data/spec/unit/elevators/first_subdomain_spec.rb +7 -2
  41. data/spec/unit/elevators/generic_spec.rb +19 -2
  42. data/spec/unit/elevators/host_hash_spec.rb +2 -2
  43. data/spec/unit/elevators/subdomain_spec.rb +6 -6
  44. data/spec/unit/migrator_spec.rb +1 -1
  45. data/spec/unit/reloader_spec.rb +2 -2
  46. metadata +11 -9
@@ -21,17 +21,8 @@ module Apartment
21
21
 
22
22
  protected
23
23
 
24
- # Connect to new tenant
25
- # Abstract adapter will catch generic ActiveRecord error
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
- raise TenantNotFound, "Cannot find tenant #{environmentify(tenant)}"
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
- # Create the new schema
96
- #
97
- def create_tenant(tenant)
98
- Apartment.connection.execute(%{CREATE SCHEMA "#{tenant}"})
75
+ private
99
76
 
100
- rescue *rescuable_exceptions
101
- raise TenantExists, "The schema #{tenant} already exists."
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
- f = File.new(database_file(tenant), File::CREAT)
43
- f.close
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
@@ -10,7 +10,7 @@ module Apartment
10
10
  class FirstSubdomain < Subdomain
11
11
 
12
12
  def parse_tenant_name(request)
13
- super.split('.')[0]
13
+ super.split('.')[0] unless super.nil?
14
14
  end
15
15
  end
16
16
  end
@@ -18,9 +18,11 @@ module Apartment
18
18
 
19
19
  database = @processor.call(request)
20
20
 
21
- Apartment::Tenant.switch! database if database
22
-
23
- @app.call(env)
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)
@@ -1,3 +1,3 @@
1
1
  module Apartment
2
- VERSION = "1.0.2"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -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 an array of strings representing each Tenant name.
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'
@@ -17,7 +17,6 @@ apartment_namespace = namespace :apartment do
17
17
  desc "Migrate all tenants"
18
18
  task :migrate do
19
19
  warn_if_tenants_empty
20
-
21
20
  tenants.each do |tenant|
22
21
  begin
23
22
  puts("Migrating #{tenant} tenant")
@@ -1,7 +1,7 @@
1
1
  if defined?(JRUBY_VERSION)
2
2
 
3
3
  require 'spec_helper'
4
- require 'lib/apartment/adapters/jdbc_mysql_adapter'
4
+ require 'apartment/adapters/jdbc_mysql_adapter'
5
5
 
6
6
  describe Apartment::Adapters::JDBCMysqlAdapter, database: :mysql do
7
7
 
@@ -1,7 +1,7 @@
1
1
  if defined?(JRUBY_VERSION)
2
2
 
3
3
  require 'spec_helper'
4
- require 'lib/apartment/adapters/jdbc_postgresql_adapter'
4
+ require 'apartment/adapters/jdbc_postgresql_adapter'
5
5
 
6
6
  describe Apartment::Adapters::JDBCPostgresqlAdapter, database: :postgresql do
7
7
 
@@ -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.should == "#{default_tenant}.companies"
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
@@ -2,11 +2,11 @@ require 'spec_helper'
2
2
 
3
3
  describe Apartment do
4
4
  it "should be valid" do
5
- Apartment.should be_a(Module)
5
+ expect(Apartment).to be_a(Module)
6
6
  end
7
7
 
8
8
  it "should be a valid app" do
9
- ::Rails.application.should be_a(Dummy::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.should_not == ActiveRecord::Base.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