apartment 0.26.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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'