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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9cdae6ddf14e715ae80d9c932ca1d6425e6f56e1
4
- data.tar.gz: 8162a81d9eb38bf3992314a4d6fddad14bbc55a8
3
+ metadata.gz: 5edda0f57e98bfa6166bb6bd4fcea9368cdc2963
4
+ data.tar.gz: 8fdda9e1bbee41a48490ae0c61dc6bc18d5084c4
5
5
  SHA512:
6
- metadata.gz: 8683c9692ee16ece0b1876937e2ad79d25c057106c1f750e077386f5c0d04e822b5318baf250b10bcb0f9918bbddabdde6a53aaddd9637e9de0ccb9f33748208
7
- data.tar.gz: b76295e730aa02ac1f5a89b53459fed7499f70821bc0292321d2b2f7dc8fc036dc441995a25334f8207519102f5375063f6b99099902713db2c935f241e98238
6
+ metadata.gz: 7450954792e78f6bd27bb8c4a6414853331562554d027cb54703746f59f1f255d898b8966349640494c7574917924ab3faa690ef07d295791d50693c56b6a0eb
7
+ data.tar.gz: 9ffd46752338eb87341c0deb314395e0f8223beb5744a2ade9a632be6234f6ec68184a1ab01744b404978e11b25f80d03314645510cda7fb8875a1f86809c86d
@@ -1 +1 @@
1
- ruby-2.2
1
+ ruby-2.3
@@ -1,25 +1,22 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 1.9.3
4
3
  - 2.0.0
5
- - 2.1.0
6
- - 2.2.0
7
- - jruby-19mode
8
- - jruby-head
4
+ - 2.1.9
5
+ - 2.2.4
6
+ - 2.3.1
7
+ - jruby-9.0.5.0
9
8
  gemfile:
10
9
  - gemfiles/rails_3_2.gemfile
11
10
  - gemfiles/rails_4_0.gemfile
12
11
  - gemfiles/rails_4_1.gemfile
13
12
  - gemfiles/rails_4_2.gemfile
14
- bundler_args: --without local --verbose
13
+ bundler_args: --without local
15
14
  before_install:
16
15
  - gem install bundler -v '> 1.5.0'
17
16
  env:
18
17
  RUBY_GC_MALLOC_LIMIT: 90000000
19
18
  RUBY_FREE_MIN: 200000
20
19
  matrix:
21
- allow_failures:
22
- - rvm: jruby-head
23
20
  exclude:
24
21
  - rvm: 2.2.0
25
22
  gemfile: gemfiles/rails_3_2.gemfile
data/Gemfile CHANGED
@@ -6,4 +6,5 @@ gem 'rails', '>= 3.1.2'
6
6
 
7
7
  group :local do
8
8
  gem 'pry'
9
+ gem 'guard-rspec', '~> 4.2'
9
10
  end
data/README.md CHANGED
@@ -8,14 +8,22 @@ Apartment provides tools to help you deal with multiple tenants in your Rails
8
8
  application. If you need to have certain data sequestered based on account or company,
9
9
  but still allow some data to exist in a common tenant, Apartment can help.
10
10
 
11
+ ## HELP!
12
+
13
+ In order to help drive the direction of development and clean up the codebase, we'd like to take a poll
14
+ on how people are currently using Apartment. If you can take 5 seconds (1 question) to answer
15
+ this poll, we'd greatly appreciated it.
16
+
17
+ [View Poll](http://www.poll-maker.com/poll391552x4Bfb41a9-15)
18
+
11
19
  ## Excessive Memory Issues on ActiveRecord 4.x
12
20
 
13
21
  > If you're noticing ever growing memory issues (ie growing with each tenant you add)
14
22
  > when using Apartment, that's because there's [an issue](https://github.com/rails/rails/issues/19578)
15
23
  > with how ActiveRecord maps Postgresql data types into AR data types.
16
- > This has been patched and will be release for AR 4.2.2. It's apparently hard
24
+ > This has been patched and will be released for AR 4.2.2. It's apparently hard
17
25
  > to backport to 4.1 unfortunately.
18
- > If you want to use this today, you can use our [4.2.1 patched version](https://github.com/influitive/rails/tree/v4.2.1.memfix) on our github account using the code sample below.
26
+ > If you're noticing high memory usage from ActiveRecord with Apartment please upgrade.
19
27
 
20
28
  ```ruby
21
29
  gem 'rails', '4.2.1', github: 'influitive/rails', tag: 'v4.2.1.memfix'
@@ -118,7 +126,16 @@ module MyApplication
118
126
  end
119
127
  ```
120
128
 
121
- If you want to exclude a domain, for example if you don't want your application to treate www like a subdomain, in an initializer in your application, you can set the following:
129
+ By default, the subdomain elevator assumes that the parent domain consists of two segments, e.g. 'example.com'. If this isn't the case, you can adjust the `tld_length` (top level domain length) configuration variable, which defaults to 1. For example, if you are using 'localhost' in development:
130
+ ```ruby
131
+ # config/initializers/apartment.rb
132
+ Apartment.configure do |config|
133
+ ...
134
+ config.tld_length = 0 if Rails.env == 'development'
135
+ end
136
+ ```
137
+
138
+ If you want to exclude a domain, for example if you don't want your application to treat www like a subdomain, in an initializer in your application, you can set the following:
122
139
 
123
140
  ```ruby
124
141
  # config/initializers/apartment/subdomain_exclusions.rb
@@ -127,6 +144,27 @@ Apartment::Elevators::Subdomain.excluded_subdomains = ['www']
127
144
 
128
145
  This functions much in the same way as Apartment.excluded_models. This example will prevent switching your tenant when the subdomain is www. Handy for subdomains like: "public", "www", and "admin" :)
129
146
 
147
+ **Switch on first subdomain**
148
+ To switch on the first subdomain, which analyzes the chain of subdomains of the request and switches to a tenant schema of the first name in the chain (e.g. owls.birds.animals.com would switch to "owl"). It can be used like so:
149
+
150
+ ```ruby
151
+ # application.rb
152
+ module MyApplication
153
+ class Application < Rails::Application
154
+ config.middleware.use 'Apartment::Elevators::FirstSubdomain'
155
+ end
156
+ end
157
+ ```
158
+
159
+ If you want to exclude a domain, for example if you don't want your application to treate www like a subdomain, in an initializer in your application, you can set the following:
160
+
161
+ ```ruby
162
+ # config/initializers/apartment/subdomain_exclusions.rb
163
+ Apartment::Elevators::FirstSubdomain.excluded_subdomains = ['www']
164
+ ```
165
+
166
+ This functions much in the same way as the Subdomain elevator.
167
+
130
168
  **Switch on domain**
131
169
  To switch based on full domain (excluding subdomains *ie 'www'* and top level domains *ie '.com'* ) use the following:
132
170
 
@@ -187,6 +225,16 @@ class MyCustomElevator < Apartment::Elevators::Generic
187
225
  end
188
226
  ```
189
227
 
228
+ ### Dropping Tenants
229
+
230
+ To drop tenants using Apartment, use the following command:
231
+
232
+ ```ruby
233
+ Apartment::Tenant.drop('tenant_name')
234
+ ```
235
+
236
+ When method is called, the schema is dropped and all data from itself will be lost. Be careful with this method.
237
+
190
238
  ## Config
191
239
 
192
240
  The following config options should be set up in a Rails initializer such as:
@@ -225,7 +273,7 @@ By default, ActiveRecord will use `"$user", public` as the default `schema_searc
225
273
  config.default_schema = "some_other_schema"
226
274
  ```
227
275
 
228
- With that set, all excluded models will use this schema as the table name prefix instead of `public` and `reset` on `Apartment::Tenant` will return to this schema also
276
+ With that set, all excluded models will use this schema as the table name prefix instead of `public` and `reset` on `Apartment::Tenant` will return to this schema as well.
229
277
 
230
278
  ## Persistent Schemas
231
279
  Apartment will normally just switch the `schema_search_path` whole hog to the one passed in. This can lead to problems if you want other schemas to always be searched as well. Enter `persistent_schemas`. You can configure a list of other schemas that will always remain in the search path, while the default gets swapped out:
@@ -377,6 +425,33 @@ and test environments. If you wish to turn this option off in production, you c
377
425
  config.prepend_environment = !Rails.env.production?
378
426
  ```
379
427
 
428
+ ## Tenants on different servers
429
+
430
+ You can store your tenants in different databases on one or more servers.
431
+ To do it, specify your `tenant_names` as a hash, keys being the actual tenant names,
432
+ values being a hash with the database configuration to use.
433
+
434
+ Example:
435
+
436
+ ```ruby
437
+ config.tenant_names = {
438
+ 'tenant1' => {
439
+ adapter: 'postgresql',
440
+ host: 'some_server',
441
+ port: 5555,
442
+ database: 'postgres' # this is not the name of the tenant's db
443
+ # but the name of the database to connect to, before creating the tenant's db
444
+ # mandatory in postgresql
445
+ }
446
+ }
447
+ # or using a lambda:
448
+ config.tenant_names = lambda do
449
+ Tenant.all.each_with_object({}) do |tenant, hash|
450
+ hash[tenant.name] = tenant.db_configuration
451
+ end
452
+ end
453
+ ```
454
+
380
455
  ## Delayed::Job
381
456
  ### Has been removed... See apartment-sidekiq for a better backgrounding experience
382
457
 
@@ -40,8 +40,8 @@ Gem::Specification.new do |s|
40
40
 
41
41
  s.add_development_dependency 'appraisal'
42
42
  s.add_development_dependency 'rake', '~> 0.9'
43
- s.add_development_dependency 'rspec-rails', '~> 2.14'
44
- s.add_development_dependency 'guard-rspec', '~> 4.2'
43
+ s.add_development_dependency 'rspec', '~> 3.4'
44
+ s.add_development_dependency 'rspec-rails', '~> 3.4'
45
45
  s.add_development_dependency 'capybara', '~> 2.0'
46
46
 
47
47
  if defined?(JRUBY_VERSION)
@@ -9,3 +9,5 @@ group :local do
9
9
  end
10
10
 
11
11
  gemspec :path => "../"
12
+
13
+ gem "test-unit"
@@ -24,9 +24,16 @@ module Apartment
24
24
  yield self if block_given?
25
25
  end
26
26
 
27
- # Be careful not to use `return` here so both Proc and lambda can be used without breaking
28
27
  def tenant_names
29
- @tenant_names.respond_to?(:call) ? @tenant_names.call : @tenant_names
28
+ extract_tenant_config.keys.map(&:to_s)
29
+ end
30
+
31
+ def tenants_with_config
32
+ extract_tenant_config
33
+ end
34
+
35
+ def db_config_for(tenant)
36
+ (tenants_with_config[tenant] || connection_config).with_indifferent_access
30
37
  end
31
38
 
32
39
  # Whether or not db:migrate should also migrate tenants
@@ -96,6 +103,19 @@ module Apartment
96
103
  Apartment::Deprecation.warn "[Deprecation Warning] `use_postgresql_schemas=` is now deprecated, please use `use_schemas=`"
97
104
  self.use_schemas = to_use_or_not_to_use
98
105
  end
106
+
107
+ def extract_tenant_config
108
+ return {} unless @tenant_names
109
+ values = @tenant_names.respond_to?(:call) ? @tenant_names.call : @tenant_names
110
+ unless values.is_a? Hash
111
+ values = values.each_with_object({}) do |tenant, hash|
112
+ hash[tenant] = connection_config
113
+ end
114
+ end
115
+ values.with_indifferent_access
116
+ rescue ActiveRecord::StatementInvalid
117
+ {}
118
+ end
99
119
  end
100
120
 
101
121
  # Exceptions
@@ -67,11 +67,12 @@ module Apartment
67
67
  # @param {String} tenant name
68
68
  #
69
69
  def drop(tenant)
70
- # Apartment.connection.drop_database note that drop_database will not throw an exception, so manually execute
71
- Apartment.connection.execute("DROP DATABASE #{environmentify(tenant)}" )
70
+ with_neutral_connection(tenant) do |conn|
71
+ drop_command(conn, tenant)
72
+ end
72
73
 
73
- rescue *rescuable_exceptions
74
- raise TenantNotFound, "The tenant #{environmentify(tenant)} cannot be found"
74
+ rescue *rescuable_exceptions => exception
75
+ raise_drop_tenant_error!(tenant, exception)
75
76
  end
76
77
 
77
78
  # Switch to a new tenant
@@ -125,7 +126,7 @@ module Apartment
125
126
  def process_excluded_models
126
127
  # All other models will shared a connection (at Apartment.connection_class) and we can modify at will
127
128
  Apartment.excluded_models.each do |excluded_model|
128
- excluded_model.constantize.establish_connection @config
129
+ process_excluded_model(excluded_model)
129
130
  end
130
131
  end
131
132
 
@@ -139,21 +140,38 @@ module Apartment
139
140
  #
140
141
  def seed_data
141
142
  # Don't log the output of seeding the db
142
- silence_stream(STDOUT){ load_or_abort(Apartment.seed_data_file) } if Apartment.seed_data_file
143
+ silence_warnings{ load_or_abort(Apartment.seed_data_file) } if Apartment.seed_data_file
143
144
  end
144
145
  alias_method :seed, :seed_data
145
146
 
146
147
  protected
147
148
 
149
+ def process_excluded_model(excluded_model)
150
+ excluded_model.constantize.establish_connection @config
151
+ end
152
+
153
+ def drop_command(conn, tenant)
154
+ # connection.drop_database note that drop_database will not throw an exception, so manually execute
155
+ conn.execute("DROP DATABASE #{environmentify(tenant)}")
156
+ end
157
+
158
+ class SeparateDbConnectionHandler < ::ActiveRecord::Base
159
+ end
160
+
148
161
  # Create the tenant
149
162
  #
150
163
  # @param {String} tenant Database name
151
164
  #
152
165
  def create_tenant(tenant)
153
- Apartment.connection.create_database( environmentify(tenant) )
166
+ with_neutral_connection(tenant) do |conn|
167
+ create_tenant_command(conn, tenant)
168
+ end
169
+ rescue *rescuable_exceptions => exception
170
+ raise_create_tenant_error!(tenant, exception)
171
+ end
154
172
 
155
- rescue *rescuable_exceptions
156
- raise TenantExists, "The tenant #{environmentify(tenant)} already exists."
173
+ def create_tenant_command(conn, tenant)
174
+ conn.create_database(environmentify(tenant))
157
175
  end
158
176
 
159
177
  # Connect to new tenant
@@ -163,9 +181,9 @@ module Apartment
163
181
  def connect_to_new(tenant)
164
182
  Apartment.establish_connection multi_tenantify(tenant)
165
183
  Apartment.connection.active? # call active? to manually check if this connection is valid
166
-
167
- rescue *rescuable_exceptions
168
- raise TenantNotFound, "The tenant #{environmentify(tenant)} cannot be found."
184
+ rescue *rescuable_exceptions => exception
185
+ Apartment::Tenant.reset if reset_on_connection_exception?
186
+ raise_connect_error!(tenant, exception)
169
187
  end
170
188
 
171
189
  # Prepend the environment if configured and the environment isn't already there
@@ -196,13 +214,21 @@ module Apartment
196
214
  end
197
215
 
198
216
  # Return a new config that is multi-tenanted
199
- #
200
- def multi_tenantify(tenant)
201
- @config.clone.tap do |config|
202
- config[:database] = environmentify(tenant)
217
+ # @param {String} tenant: Database name
218
+ # @param {Boolean} with_database: if true, use the actual tenant's db name
219
+ # if false, use the default db name from the db
220
+ def multi_tenantify(tenant, with_database = true)
221
+ db_connection_config(tenant).tap do |config|
222
+ if with_database
223
+ multi_tenantify_with_tenant_db_name(config, tenant)
224
+ end
203
225
  end
204
226
  end
205
227
 
228
+ def multi_tenantify_with_tenant_db_name(config, tenant)
229
+ config[:database] = environmentify(tenant)
230
+ end
231
+
206
232
  # Load a file or abort if it doesn't exists
207
233
  #
208
234
  def load_or_abort(file)
@@ -224,6 +250,34 @@ module Apartment
224
250
  def rescue_from
225
251
  []
226
252
  end
253
+
254
+ def db_connection_config(tenant)
255
+ Apartment.db_config_for(tenant).clone
256
+ end
257
+
258
+ # neutral connection is necessary whenever you need to create/remove a database from a server.
259
+ # example: when you use postgresql, you need to connect to the default postgresql database before you create your own.
260
+ def with_neutral_connection(tenant, &block)
261
+ SeparateDbConnectionHandler.establish_connection(multi_tenantify(tenant, false))
262
+ yield(SeparateDbConnectionHandler.connection)
263
+ SeparateDbConnectionHandler.connection.close
264
+ end
265
+
266
+ def reset_on_connection_exception?
267
+ false
268
+ end
269
+
270
+ def raise_drop_tenant_error!(tenant, exception)
271
+ raise TenantNotFound, "Error while dropping tenant #{environmentify(tenant)}: #{ exception.message }"
272
+ end
273
+
274
+ def raise_create_tenant_error!(tenant, exception)
275
+ raise TenantExists, "Error while creating tenant #{environmentify(tenant)}: #{ exception.message }"
276
+ end
277
+
278
+ def raise_connect_error!(tenant, exception)
279
+ raise TenantNotFound, "Error while connecting to tenant #{environmentify(tenant)}: #{ exception.message }"
280
+ end
227
281
  end
228
282
  end
229
283
  end
@@ -4,20 +4,15 @@ module Apartment
4
4
  module Adapters
5
5
  class AbstractJDBCAdapter < AbstractAdapter
6
6
 
7
- protected
7
+ private
8
8
 
9
- # Return a new config that is multi-tenanted
10
- #
11
- def multi_tenantify(database)
12
- @config.clone.tap do |config|
13
- config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(database)}"
14
- end
9
+ def multi_tenantify_with_tenant_db_name(config, tenant)
10
+ config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(tenant)}"
15
11
  end
16
- private
17
12
 
18
13
  def rescue_from
19
14
  ActiveRecord::JDBCError
20
15
  end
21
16
  end
22
17
  end
23
- end
18
+ end
@@ -11,19 +11,8 @@ module Apartment
11
11
  module Adapters
12
12
  class JDBCMysqlAdapter < AbstractJDBCAdapter
13
13
 
14
- protected
15
-
16
- # Connect to new database
17
- # Abstract adapter will catch generic ActiveRecord error
18
- # Catch specific adapter errors here
19
- #
20
- # @param {String} database Database name
21
- #
22
- def connect_to_new(database)
23
- super
24
- rescue TenantNotFound
25
- Apartment::Tenant.reset
26
- raise TenantNotFound, "Cannot find database #{environmentify(database)}"
14
+ def reset_on_connection_exception?
15
+ true
27
16
  end
28
17
  end
29
18
  end
@@ -15,27 +15,16 @@ module Apartment
15
15
  # Default adapter when not using Postgresql Schemas
16
16
  class JDBCPostgresqlAdapter < PostgresqlAdapter
17
17
 
18
- protected
19
-
20
- def create_tenant(tenant)
21
- # There is a bug in activerecord-jdbcpostgresql-adapter (1.2.5) that will cause
22
- # an exception if no options are passed into the create_database call.
23
- Apartment.connection.create_database(environmentify(tenant), { :thisisahack => '' })
18
+ private
24
19
 
25
- rescue *rescuable_exceptions
26
- raise TenantExists, "The tenant #{environmentify(tenant)} already exists."
20
+ def multi_tenantify_with_tenant_db_name(config, tenant)
21
+ config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(tenant)}"
27
22
  end
28
23
 
29
- # Return a new config that is multi-tenanted
30
- #
31
- def multi_tenantify(tenant)
32
- @config.clone.tap do |config|
33
- config[:url] = "#{config[:url].gsub(/(\S+)\/.+$/, '\1')}/#{environmentify(tenant)}"
34
- end
24
+ def create_tenant_command(conn, tenant)
25
+ conn.create_database(environmentify(tenant), { :thisisahack => '' })
35
26
  end
36
27
 
37
- private
38
-
39
28
  def rescue_from
40
29
  ActiveRecord::JDBCError
41
30
  end