apartment 0.26.1 → 1.0.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -1
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +9 -0
  5. data/HISTORY.md +8 -0
  6. data/README.md +30 -7
  7. data/TODO.md +9 -0
  8. data/apartment.gemspec +9 -8
  9. data/lib/apartment.rb +9 -18
  10. data/lib/apartment/adapters/abstract_adapter.rb +63 -30
  11. data/lib/apartment/adapters/jdbc_mysql_adapter.rb +2 -2
  12. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +3 -3
  13. data/lib/apartment/adapters/mysql2_adapter.rb +10 -6
  14. data/lib/apartment/adapters/postgresql_adapter.rb +15 -21
  15. data/lib/apartment/adapters/sqlite3_adapter.rb +4 -4
  16. data/lib/apartment/deprecation.rb +13 -0
  17. data/lib/apartment/elevators/generic.rb +3 -2
  18. data/lib/apartment/elevators/host_hash.rb +1 -1
  19. data/lib/apartment/migrator.rb +3 -3
  20. data/lib/apartment/tasks/enhancements.rb +31 -21
  21. data/lib/apartment/tenant.rb +10 -7
  22. data/lib/apartment/version.rb +1 -1
  23. data/lib/generators/apartment/install/templates/apartment.rb +41 -22
  24. data/lib/tasks/apartment.rake +1 -1
  25. data/spec/adapters/jdbc_mysql_adapter_spec.rb +1 -1
  26. data/spec/adapters/jdbc_postgresql_adapter_spec.rb +2 -2
  27. data/spec/adapters/mysql2_adapter_spec.rb +4 -2
  28. data/spec/adapters/postgresql_adapter_spec.rb +3 -3
  29. data/spec/adapters/sqlite3_adapter_spec.rb +1 -1
  30. data/spec/dummy_engine/.gitignore +8 -0
  31. data/spec/dummy_engine/Gemfile +15 -0
  32. data/spec/dummy_engine/Rakefile +34 -0
  33. data/spec/dummy_engine/bin/rails +12 -0
  34. data/spec/dummy_engine/config/initializers/apartment.rb +51 -0
  35. data/spec/dummy_engine/dummy_engine.gemspec +24 -0
  36. data/spec/dummy_engine/lib/dummy_engine.rb +4 -0
  37. data/spec/dummy_engine/lib/dummy_engine/engine.rb +4 -0
  38. data/spec/dummy_engine/lib/dummy_engine/version.rb +3 -0
  39. data/spec/dummy_engine/test/dummy/Rakefile +6 -0
  40. data/spec/dummy_engine/test/dummy/config.ru +4 -0
  41. data/spec/dummy_engine/test/dummy/config/application.rb +22 -0
  42. data/spec/dummy_engine/test/dummy/config/boot.rb +5 -0
  43. data/spec/dummy_engine/test/dummy/config/database.yml +25 -0
  44. data/spec/dummy_engine/test/dummy/config/environment.rb +5 -0
  45. data/spec/dummy_engine/test/dummy/config/environments/development.rb +37 -0
  46. data/spec/dummy_engine/test/dummy/config/environments/production.rb +78 -0
  47. data/spec/dummy_engine/test/dummy/config/environments/test.rb +39 -0
  48. data/spec/dummy_engine/test/dummy/config/initializers/assets.rb +8 -0
  49. data/spec/dummy_engine/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  50. data/spec/dummy_engine/test/dummy/config/initializers/cookies_serializer.rb +3 -0
  51. data/spec/dummy_engine/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  52. data/spec/dummy_engine/test/dummy/config/initializers/inflections.rb +16 -0
  53. data/spec/dummy_engine/test/dummy/config/initializers/mime_types.rb +4 -0
  54. data/spec/dummy_engine/test/dummy/config/initializers/session_store.rb +3 -0
  55. data/spec/dummy_engine/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  56. data/spec/dummy_engine/test/dummy/config/locales/en.yml +23 -0
  57. data/spec/dummy_engine/test/dummy/config/routes.rb +56 -0
  58. data/spec/dummy_engine/test/dummy/config/secrets.yml +22 -0
  59. data/spec/examples/connection_adapter_examples.rb +5 -5
  60. data/spec/examples/generic_adapter_examples.rb +75 -37
  61. data/spec/examples/schema_adapter_examples.rb +19 -24
  62. data/spec/integration/query_caching_spec.rb +3 -3
  63. data/spec/integration/use_within_an_engine_spec.rb +28 -0
  64. data/spec/spec_helper.rb +1 -1
  65. data/spec/support/setup.rb +9 -9
  66. data/spec/{database_spec.rb → tenant_spec.rb} +20 -30
  67. data/spec/unit/config_spec.rb +2 -2
  68. data/spec/unit/elevators/domain_spec.rb +1 -1
  69. data/spec/unit/elevators/generic_spec.rb +2 -2
  70. data/spec/unit/elevators/host_hash_spec.rb +5 -5
  71. data/spec/unit/elevators/subdomain_spec.rb +2 -2
  72. data/spec/unit/migrator_spec.rb +7 -7
  73. metadata +104 -43
@@ -13,6 +13,12 @@ module Apartment
13
13
  module Adapters
14
14
  class Mysql2Adapter < AbstractAdapter
15
15
 
16
+ def initialize(config)
17
+ super
18
+
19
+ @default_tenant = config[:database]
20
+ end
21
+
16
22
  protected
17
23
 
18
24
  # Connect to new tenant
@@ -25,13 +31,11 @@ module Apartment
25
31
  super
26
32
  rescue Mysql2::Error
27
33
  Apartment::Tenant.reset
28
- raise DatabaseNotFound, "Cannot find tenant #{environmentify(tenant)}"
34
+ raise TenantNotFound, "Cannot find tenant #{environmentify(tenant)}"
29
35
  end
30
36
  end
31
37
 
32
38
  class Mysql2SchemaAdapter < AbstractAdapter
33
- attr_reader :default_tenant
34
-
35
39
  def initialize(config)
36
40
  super
37
41
 
@@ -39,7 +43,7 @@ module Apartment
39
43
  reset
40
44
  end
41
45
 
42
- # Reset current_tenant to the default_tenant
46
+ # Reset current tenant to the default_tenant
43
47
  #
44
48
  def reset
45
49
  Apartment.connection.execute "use #{default_tenant}"
@@ -53,7 +57,7 @@ module Apartment
53
57
 
54
58
  protected
55
59
 
56
- # Set schema current_tenant to new db
60
+ # Connect to new tenant
57
61
  #
58
62
  def connect_to_new(tenant)
59
63
  return reset if tenant.nil?
@@ -62,7 +66,7 @@ module Apartment
62
66
 
63
67
  rescue ActiveRecord::StatementInvalid
64
68
  Apartment::Tenant.reset
65
- raise DatabaseNotFound, "Cannot find tenant #{environmentify(tenant)}"
69
+ raise TenantNotFound, "Cannot find tenant #{environmentify(tenant)}"
66
70
  end
67
71
 
68
72
  def process_excluded_model(model)
@@ -20,7 +20,7 @@ module Apartment
20
20
  Apartment.connection.execute(%{DROP DATABASE "#{tenant}"})
21
21
 
22
22
  rescue *rescuable_exceptions
23
- raise DatabaseNotFound, "The tenant #{tenant} cannot be found"
23
+ raise TenantNotFound, "The tenant #{tenant} cannot be found"
24
24
  end
25
25
 
26
26
  private
@@ -47,7 +47,7 @@ module Apartment
47
47
  Apartment.connection.execute(%{DROP SCHEMA "#{tenant}" CASCADE})
48
48
 
49
49
  rescue *rescuable_exceptions
50
- raise SchemaNotFound, "The schema #{tenant.inspect} cannot be found."
50
+ raise TenantNotFound, "The schema #{tenant.inspect} cannot be found."
51
51
  end
52
52
 
53
53
  # Reset search path to default search_path
@@ -59,7 +59,7 @@ module Apartment
59
59
  # Ensure that if a schema *was* set, we override
60
60
  table_name = klass.table_name.split('.', 2).last
61
61
 
62
- klass.table_name = "#{Apartment.default_schema}.#{table_name}"
62
+ klass.table_name = "#{default_tenant}.#{table_name}"
63
63
  end
64
64
  end
65
65
  end
@@ -69,12 +69,12 @@ module Apartment
69
69
  # @return {String} default schema search path
70
70
  #
71
71
  def reset
72
- @current_tenant = Apartment.default_schema
72
+ @current = default_tenant
73
73
  Apartment.connection.schema_search_path = full_search_path
74
74
  end
75
75
 
76
- def current_tenant
77
- @current_tenant || Apartment.default_schema
76
+ def current
77
+ @current || default_tenant
78
78
  end
79
79
 
80
80
  protected
@@ -85,11 +85,11 @@ module Apartment
85
85
  return reset if tenant.nil?
86
86
  raise ActiveRecord::StatementInvalid.new("Could not find schema #{tenant}") unless Apartment.connection.schema_exists? tenant
87
87
 
88
- @current_tenant = tenant.to_s
88
+ @current = tenant.to_s
89
89
  Apartment.connection.schema_search_path = full_search_path
90
90
 
91
91
  rescue *rescuable_exceptions
92
- raise SchemaNotFound, "One of the following schema(s) is invalid: #{tenant}, #{full_search_path}"
92
+ raise TenantNotFound, "One of the following schema(s) is invalid: \"#{tenant}\" #{full_search_path}"
93
93
  end
94
94
 
95
95
  # Create the new schema
@@ -98,7 +98,7 @@ module Apartment
98
98
  Apartment.connection.execute(%{CREATE SCHEMA "#{tenant}"})
99
99
 
100
100
  rescue *rescuable_exceptions
101
- raise SchemaExists, "The schema #{tenant} already exists."
101
+ raise TenantExists, "The schema #{tenant} already exists."
102
102
  end
103
103
 
104
104
  private
@@ -110,7 +110,7 @@ module Apartment
110
110
  end
111
111
 
112
112
  def persistent_schemas
113
- [@current_tenant, Apartment.persistent_schemas].flatten
113
+ [@current, Apartment.persistent_schemas].flatten
114
114
  end
115
115
  end
116
116
 
@@ -155,9 +155,9 @@ module Apartment
155
155
  # .map! {|t| "-T #{t}"}
156
156
  # .join(' ')
157
157
 
158
- # `pg_dump -s -x -O -n #{default_schema} #{excluded_tables} #{dbname}`
158
+ # `pg_dump -s -x -O -n #{default_tenant} #{excluded_tables} #{dbname}`
159
159
 
160
- `pg_dump -s -x -O -n #{default_schema} #{dbname}`
160
+ `pg_dump -s -x -O -n #{default_tenant} #{dbname}`
161
161
  end
162
162
 
163
163
  # Dump data from schema_migrations table
@@ -165,7 +165,7 @@ module Apartment
165
165
  # @return {String} raw SQL contaning inserts with data from schema_migrations
166
166
  #
167
167
  def pg_dump_schema_migrations_data
168
- `pg_dump -a --inserts -t schema_migrations -n #{default_schema} #{dbname}`
168
+ `pg_dump -a --inserts -t schema_migrations -n #{default_tenant} #{dbname}`
169
169
  end
170
170
 
171
171
  # Remove "SET search_path ..." line from SQL dump and prepend search_path set to current tenant
@@ -173,7 +173,7 @@ module Apartment
173
173
  # @return {String} patched raw SQL dump
174
174
  #
175
175
  def patch_search_path(sql)
176
- search_path = "SET search_path = \"#{current}\", #{default_schema};"
176
+ search_path = "SET search_path = \"#{current}\", #{default_tenant};"
177
177
 
178
178
  sql
179
179
  .split("\n")
@@ -199,13 +199,7 @@ module Apartment
199
199
  # Convenience method for current database name
200
200
  #
201
201
  def dbname
202
- ActiveRecord::Base.connection_config[:database]
203
- end
204
-
205
- # Convenience method for the default schema
206
- #
207
- def default_schema
208
- Apartment.default_schema
202
+ Apartment.connection_config[:database]
209
203
  end
210
204
  end
211
205
  end
@@ -16,27 +16,27 @@ module Apartment
16
16
  end
17
17
 
18
18
  def drop(tenant)
19
- raise DatabaseNotFound,
19
+ raise TenantNotFound,
20
20
  "The tenant #{environmentify(tenant)} cannot be found." unless File.exists?(database_file(tenant))
21
21
 
22
22
  File.delete(database_file(tenant))
23
23
  end
24
24
 
25
- def current_tenant
25
+ def current
26
26
  File.basename(Apartment.connection.instance_variable_get(:@config)[:database], '.sqlite3')
27
27
  end
28
28
 
29
29
  protected
30
30
 
31
31
  def connect_to_new(tenant)
32
- raise DatabaseNotFound,
32
+ raise TenantNotFound,
33
33
  "The tenant #{environmentify(tenant)} cannot be found." unless File.exists?(database_file(tenant))
34
34
 
35
35
  super database_file(tenant)
36
36
  end
37
37
 
38
38
  def create_tenant(tenant)
39
- raise DatabaseExists,
39
+ raise TenantExists,
40
40
  "The tenant #{environmentify(tenant)} already exists." if File.exists?(database_file(tenant))
41
41
 
42
42
  f = File.new(database_file(tenant), File::CREAT)
@@ -0,0 +1,13 @@
1
+ module Apartment
2
+ module Deprecation
3
+
4
+ def self.warn(message)
5
+ begin
6
+ ActiveSupport::Deprecation.warn message
7
+ rescue
8
+ warn message
9
+ end
10
+ end
11
+
12
+ end
13
+ end
@@ -1,5 +1,6 @@
1
1
  require 'rack/request'
2
2
  require 'apartment/tenant'
3
+ require 'apartment/deprecation'
3
4
 
4
5
  module Apartment
5
6
  module Elevators
@@ -17,7 +18,7 @@ module Apartment
17
18
 
18
19
  database = @processor.call(request)
19
20
 
20
- Apartment::Tenant.switch database if database
21
+ Apartment::Tenant.switch! database if database
21
22
 
22
23
  @app.call(env)
23
24
  end
@@ -41,7 +42,7 @@ module Apartment
41
42
  end
42
43
 
43
44
  def deprecation_warning
44
- warn "[DEPRECATED::Apartment] Use #parse_tenant_name instead of #parse_database_name -> #{self.class.name}"
45
+ Apartment::Deprecation.warn "[DEPRECATED::Apartment] Use #parse_tenant_name instead of #parse_database_name -> #{self.class.name}"
45
46
  end
46
47
  end
47
48
  end
@@ -12,7 +12,7 @@ module Apartment
12
12
  end
13
13
 
14
14
  def parse_tenant_name(request)
15
- raise DatabaseNotFound,
15
+ raise TenantNotFound,
16
16
  "Cannot find tenant for host #{request.host}" unless @hash.has_key?(request.host)
17
17
 
18
18
  @hash[request.host]
@@ -7,7 +7,7 @@ module Apartment
7
7
 
8
8
  # Migrate to latest
9
9
  def migrate(database)
10
- Tenant.process(database) do
10
+ Tenant.switch(database) do
11
11
  version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
12
12
 
13
13
  ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, version) do |migration|
@@ -18,14 +18,14 @@ module Apartment
18
18
 
19
19
  # Migrate up/down to a specific version
20
20
  def run(direction, database, version)
21
- Tenant.process(database) do
21
+ Tenant.switch(database) do
22
22
  ActiveRecord::Migrator.run(direction, ActiveRecord::Migrator.migrations_paths, version)
23
23
  end
24
24
  end
25
25
 
26
26
  # rollback latest migration `step` number of times
27
27
  def rollback(database, step = 1)
28
- Tenant.process(database) do
28
+ Tenant.switch(database) do
29
29
  ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
30
30
  end
31
31
  end
@@ -1,26 +1,36 @@
1
1
  # Require this file to append Apartment rake tasks to ActiveRecord db rake tasks
2
2
  # Enabled by default in the initializer
3
3
 
4
- Rake::Task["db:migrate"].enhance do
5
- Rake::Task["apartment:migrate"].invoke
4
+ module Apartment
5
+ class RakeTaskEnhancer
6
+
7
+ TASKS = %w(db:migrate db:rollback db:migrate:up db:migrate:down db:migrate:redo db:seed)
8
+
9
+ # This is a bit convoluted, but helps solve problems when using Apartment within an engine
10
+ # See spec/integration/use_within_an_engine.rb
11
+
12
+ class << self
13
+ def enhance!
14
+ TASKS.each do |name|
15
+ task = Rake::Task[name]
16
+ task.enhance do
17
+ if should_enhance?
18
+ enhance_task(task)
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def should_enhance?
25
+ Apartment.db_migrate_tenants
26
+ end
27
+
28
+ def enhance_task(task)
29
+ Rake::Task[task.name.sub(/db:/, 'apartment:')].invoke
30
+ end
31
+ end
32
+
33
+ end
6
34
  end
7
35
 
8
- Rake::Task["db:rollback"].enhance do
9
- Rake::Task["apartment:rollback"].invoke
10
- end
11
-
12
- Rake::Task["db:migrate:up"].enhance do
13
- Rake::Task["apartment:migrate:up"].invoke
14
- end
15
-
16
- Rake::Task["db:migrate:down"].enhance do
17
- Rake::Task["apartment:migrate:down"].invoke
18
- end
19
-
20
- Rake::Task["db:migrate:redo"].enhance do
21
- Rake::Task["apartment:migrate:redo"].invoke
22
- end
23
-
24
- Rake::Task["db:seed"].enhance do
25
- Rake::Task["apartment:seed"].invoke
26
- end
36
+ Apartment::RakeTaskEnhancer.enhance!
@@ -1,4 +1,5 @@
1
1
  require 'forwardable'
2
+ require 'apartment/deprecation'
2
3
 
3
4
  module Apartment
4
5
  # The main entry point to Apartment functions
@@ -8,7 +9,7 @@ module Apartment
8
9
  extend self
9
10
  extend Forwardable
10
11
 
11
- def_delegators :adapter, :create, :current_tenant, :current, :current_database, :drop, :process, :process_excluded_models, :reset, :seed, :switch
12
+ def_delegators :adapter, :create, :current_tenant, :current, :current_database, :default_tenant, :drop, :switch, :process_excluded_models, :reset, :seed, :switch!
12
13
 
13
14
  attr_writer :config
14
15
 
@@ -60,14 +61,16 @@ module Apartment
60
61
  # Fetch the rails database configuration
61
62
  #
62
63
  def config
63
- @config ||= (ActiveRecord::Base.configurations[Rails.env] ||
64
- Rails.application.config.database_configuration[Rails.env]).symbolize_keys
64
+ @config ||= Apartment.connection_config
65
65
  end
66
66
  end
67
67
 
68
68
  def self.const_missing(const_name)
69
- super unless const_name == :Database
70
- warn "`Apartment::Database` has been deprecated. Use `Apartment::Tenant` instead."
71
- Tenant
69
+ if const_name == :Database
70
+ Apartment::Deprecation.warn "`Apartment::Database` has been deprecated. Use `Apartment::Tenant` instead."
71
+ Tenant
72
+ else
73
+ super
74
+ end
72
75
  end
73
- end
76
+ end
@@ -1,3 +1,3 @@
1
1
  module Apartment
2
- VERSION = "0.26.1"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -1,4 +1,6 @@
1
- # Require whichever elevator you're using below here...
1
+ # You can have Apartment route to the appropriate Tenant by adding some Rack middleware.
2
+ # Apartment can support many different "Elevators" that can take care of this routing to your data.
3
+ # Require whichever Elevator you're using below or none if you have a custom one.
2
4
  #
3
5
  # require 'apartment/elevators/generic'
4
6
  # require 'apartment/elevators/domain'
@@ -9,40 +11,57 @@ require 'apartment/elevators/subdomain'
9
11
  #
10
12
  Apartment.configure do |config|
11
13
 
12
- # These models will not be multi-tenanted,
13
- # but remain in the global (public) namespace
14
+ # Add any models that you do not want to be multi-tenanted, but remain in the global (public) namespace.
15
+ # A typical example would be a Customer or Tenant model that stores each Tenant's information.
14
16
  #
15
- # An example might be a Customer or Tenant model that stores each tenant information
16
- # ex:
17
+ # config.excluded_models = %w{ Tenant }
18
+
19
+ # In order to migrate all of your Tenants you need to provide a list of Tenant names to Apartment.
20
+ # 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.
17
22
  #
18
- # config.excluded_models = %w{Tenant}
23
+ # config.tenant_names = lambda{ Customer.pluck(:tenant_name) }
24
+ # config.tenant_names = ['tenant1', 'tenant2']
19
25
  #
20
- config.excluded_models = %w{}
26
+ config.tenant_names = lambda { ToDo_Tenant_Or_User_Model.pluck :database }
21
27
 
22
- # use postgres schemas?
23
- config.use_schemas = true
28
+ #
29
+ # ==> PostgreSQL only options
24
30
 
25
- # use raw SQL dumps for creating postgres schemas? (only appies with use_schemas set to true)
26
- #config.use_sql = true
31
+ # Specifies whether to use PostgreSQL schemas or create a new database per Tenant.
32
+ # The default behaviour is true.
33
+ #
34
+ # config.use_schemas = true
35
+
36
+ # Apartment can be forced to use raw SQL dumps instead of schema.rb for creating new schemas.
37
+ # Use this when you are using some extra features in PostgreSQL that can't be respresented in
38
+ # schema.rb, like materialized views etc. (only applies with use_schemas set to true).
39
+ # (Note: this option doesn't use db/structure.sql, it creates SQL dump by executing pg_dump)
40
+ #
41
+ # config.use_sql = false
27
42
 
28
- # configure persistent schemas (E.g. hstore )
43
+ # There are cases where you might want some schemas to always be in your search_path
44
+ # e.g when using a PostgreSQL extension like hstore.
45
+ # Any schemas added here will be available along with your selected Tenant.
46
+ #
29
47
  # config.persistent_schemas = %w{ hstore }
30
48
 
31
- # add the Rails environment to database names?
32
- # config.prepend_environment = true
33
- # config.append_environment = true
49
+ # <== PostgreSQL only options
50
+ #
34
51
 
35
- # supply list of database names for migrations to run on
36
- config.tenant_names = lambda{ ToDo_Tenant_Or_User_Model.pluck :database }
52
+ # By default, and only when not using PostgreSQL schemas, Apartment will prepend the environment
53
+ # to the tenant name to ensure there is no conflict between your environments.
54
+ # This is mainly for the benefit of your development and test environments.
55
+ # Uncomment the line below if you want to disable this behaviour in production.
56
+ #
57
+ # config.prepend_environment = !Rails.env.production?
37
58
  end
38
59
 
39
- ##
40
- # Elevator Configuration
41
-
60
+ # Setup a custom Tenant switching middleware. The Proc should return the name of the Tenant that
61
+ # you want to switch to.
42
62
  # Rails.application.config.middleware.use 'Apartment::Elevators::Generic', lambda { |request|
43
- # # TODO: supply generic implementation
63
+ # request.host.split('.').first
44
64
  # }
45
65
 
46
66
  # Rails.application.config.middleware.use 'Apartment::Elevators::Domain'
47
-
48
67
  Rails.application.config.middleware.use 'Apartment::Elevators::Subdomain'