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