apartment 0.1.3 → 0.5.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 (75) hide show
  1. data/.gitignore +6 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +5 -12
  5. data/HISTORY.md +31 -0
  6. data/{README.markdown → README.md} +39 -21
  7. data/Rakefile +44 -47
  8. data/apartment.gemspec +19 -58
  9. data/lib/apartment.rb +57 -6
  10. data/lib/apartment/adapters/abstract_adapter.rb +106 -0
  11. data/lib/apartment/adapters/mysql_adapter.rb +11 -0
  12. data/lib/apartment/adapters/postgresql_adapter.rb +55 -0
  13. data/lib/apartment/database.rb +58 -74
  14. data/lib/apartment/elevators/subdomain.rb +27 -0
  15. data/lib/apartment/migrator.rb +23 -0
  16. data/lib/apartment/railtie.rb +1 -2
  17. data/lib/apartment/version.rb +3 -0
  18. data/lib/tasks/apartment.rake +36 -0
  19. data/spec/apartment_spec.rb +7 -0
  20. data/spec/config/database.yml +10 -0
  21. data/spec/dummy/Rakefile +7 -0
  22. data/spec/dummy/app/controllers/application_controller.rb +6 -0
  23. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  24. data/spec/dummy/app/models/company.rb +3 -0
  25. data/spec/dummy/app/models/user.rb +3 -0
  26. data/spec/dummy/app/views/application/index.html.erb +1 -0
  27. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  28. data/spec/dummy/config.ru +4 -0
  29. data/spec/dummy/config/application.rb +47 -0
  30. data/spec/dummy/config/boot.rb +10 -0
  31. data/spec/dummy/config/database.yml +8 -0
  32. data/spec/dummy/config/environment.rb +5 -0
  33. data/spec/dummy/config/environments/development.rb +26 -0
  34. data/spec/dummy/config/environments/production.rb +49 -0
  35. data/spec/dummy/config/environments/test.rb +35 -0
  36. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  37. data/spec/dummy/config/initializers/inflections.rb +10 -0
  38. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  39. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  40. data/spec/dummy/config/initializers/session_store.rb +8 -0
  41. data/spec/dummy/config/locales/en.yml +5 -0
  42. data/spec/dummy/config/routes.rb +3 -0
  43. data/spec/dummy/db/migrate/20110613152810_create_dummy_models.rb +20 -0
  44. data/spec/dummy/db/schema.rb +26 -0
  45. data/spec/dummy/db/seeds.rb +8 -0
  46. data/spec/dummy/db/test.sqlite3 +0 -0
  47. data/spec/dummy/public/404.html +26 -0
  48. data/spec/dummy/public/422.html +26 -0
  49. data/spec/dummy/public/500.html +26 -0
  50. data/{lib/apartment/associations/multi_tenant_association.rb → spec/dummy/public/favicon.ico} +0 -0
  51. data/spec/dummy/public/stylesheets/.gitkeep +0 -0
  52. data/spec/dummy/script/rails +6 -0
  53. data/spec/integration/adapters/postgresql_integration_spec.rb +73 -0
  54. data/spec/integration/apartment_rake_integration_spec.rb +62 -0
  55. data/spec/integration/database_integration_spec.rb +107 -0
  56. data/spec/integration/middleware/subdomain_elevator_spec.rb +63 -0
  57. data/spec/integration/setup_spec.rb +8 -0
  58. data/spec/spec_helper.rb +26 -0
  59. data/spec/support/apartment_helpers.rb +24 -0
  60. data/spec/support/capybara_sessions.rb +15 -0
  61. data/spec/support/config.rb +11 -0
  62. data/spec/support/migrate.rb +9 -0
  63. data/spec/tasks/apartment_rake_spec.rb +110 -0
  64. data/spec/unit/config_spec.rb +84 -0
  65. data/spec/unit/middleware/subdomain_elevator_spec.rb +20 -0
  66. data/spec/unit/migrator_spec.rb +87 -0
  67. metadata +112 -62
  68. data/.bundle/config +0 -2
  69. data/.project +0 -11
  70. data/Gemfile.lock +0 -22
  71. data/VERSION +0 -1
  72. data/lib/apartment/config.rb +0 -10
  73. data/lib/apartment/config/default_config.yml +0 -17
  74. data/lib/tasks/multi_tenant_migrate.rake +0 -14
  75. data/pkg/apartment-0.1.3.gem +0 -0
@@ -0,0 +1,11 @@
1
+ module Apartment
2
+
3
+ module Adapters
4
+
5
+ class MysqlAdapter < AbstractAdapter
6
+
7
+ end
8
+
9
+ end
10
+
11
+ end
@@ -0,0 +1,55 @@
1
+ module Apartment
2
+
3
+ module Database
4
+
5
+ def self.postgresql_adapter(config)
6
+ Adapters::PostgresqlAdapter.new config, :schema_search_path => ActiveRecord::Base.connection.schema_search_path
7
+ end
8
+ end
9
+
10
+ module Adapters
11
+
12
+ class PostgresqlAdapter < AbstractAdapter
13
+
14
+ # Set schema path or connect to new db
15
+ def connect_to_new(database)
16
+ return ActiveRecord::Base.connection.schema_search_path = database if using_schemas?
17
+
18
+ super
19
+ rescue ActiveRecord::StatementInvalid => e
20
+ raise SchemaNotFound, e
21
+ end
22
+
23
+ def create(database)
24
+ reset
25
+ # Postgres will (optionally) use 'schemas' instead of actual dbs, create a new schema while connected to main (global) db
26
+ create_schema(database) if using_schemas?
27
+ super(database)
28
+ end
29
+
30
+ def reset
31
+ if using_schemas?
32
+ ActiveRecord::Base.connection.schema_search_path = @defaults[:schema_search_path]
33
+ else
34
+ super
35
+ end
36
+ end
37
+
38
+ protected
39
+
40
+ def create_schema(database)
41
+ reset
42
+
43
+ ActiveRecord::Base.connection.execute("CREATE SCHEMA #{sanitize(database)}")
44
+ rescue Exception => e
45
+ raise SchemaExists, e
46
+ end
47
+
48
+ def using_schemas?
49
+ Apartment.use_postgres_schemas
50
+ end
51
+
52
+ end
53
+
54
+ end
55
+ end
@@ -1,80 +1,64 @@
1
+ require 'active_support/core_ext/string/inflections' # for `constantize`
1
2
 
2
3
  module Apartment
3
- class Database
4
- def self.switch(database)
5
-
6
- config = get_default_database
7
-
8
- if database.nil?
9
- ActiveRecord::Base.establish_connection(config)
10
- return
11
- end
4
+ module Database
5
+
6
+ MULTI_TENANT_METHODS = [:create, :switch, :reset, :connect_and_reset]
7
+
8
+ class << self
12
9
 
13
- switched_config = multi_tenantify(config, database)
14
-
15
- puts switched_config.to_yaml
16
-
17
- ActiveRecord::Base.establish_connection(switched_config)
18
-
19
- puts Apartment::Config.excluded_models
20
-
21
- Apartment::Config.excluded_models.each do |m|
22
- klass = Kernel
23
- m.split("::").each do |i|
24
- klass = klass.const_get(i)
25
- end
26
-
27
- raise "Excluded class #{klass} could not be found." if klass.nil?
28
-
29
- puts "Excluding class #{m}"
30
-
31
- klass.establish_connection(config)
32
- end
33
- end
34
-
35
- def self.create(database)
36
- config = get_default_database
37
-
38
- switched_config = multi_tenantify(config, database)
39
-
40
- ActiveRecord::Base.establish_connection(switched_config)
41
-
42
- if config["adapter"] == "postgresql"
43
- ActiveRecord::Base.connection.execute('create table schema_migrations(version varchar(255))')
44
- end
45
-
46
- migrate(database)
47
- end
48
-
49
- def self.migrate(database)
50
-
51
- config = get_default_database
52
- switched_config = multi_tenantify(config, database)
53
-
54
- ActiveRecord::Base.establish_connection(switched_config)
55
-
56
-
57
- ActiveRecord::Migrator.migrate(File.join(Rails.root, 'db', 'migrate'))
58
-
59
- ActiveRecord::Base.establish_connection(config)
60
- end
61
-
62
- protected
63
- def self.get_default_database
64
- Rails.configuration.database_configuration[Rails.env]
65
- end
66
-
67
- def self.multi_tenantify(configuration, database)
68
- new_config = configuration.clone
69
-
70
- if new_config['adapter'] == "postgresql"
71
- new_config['schema_search_path'] = database
72
- else
73
- new_config['database'] = new_config['database'].gsub(Rails.env.to_s, "#{database}_#{Rails.env}")
74
- end
75
-
76
- new_config
77
- end
10
+ # Call init to establish a connection to the public schema on all excluded models
11
+ # This must be done before creating any new schemas or switching
12
+ def init
13
+ connect_exclusions
14
+ end
15
+
16
+ MULTI_TENANT_METHODS.each do |method|
17
+ class_eval <<-RUBY
18
+ def #{method}(*args, &block)
19
+ adapter.send(:#{method}, *args, &block)
20
+ end
21
+ RUBY
22
+ end
23
+
24
+ def adapter
25
+ @adapter ||= begin
26
+ adapter_method = "#{config[:adapter]}_adapter"
27
+
28
+ begin
29
+ require "apartment/adapters/#{adapter_method}"
30
+ rescue LoadError => e
31
+ raise "The adapter `#{config[:adapter]}` is not yet supported"
32
+ end
33
+
34
+ unless respond_to?(adapter_method)
35
+ raise AdapterNotFound, "database configuration specifies nonexistent #{config[:adapter]} adapter"
36
+ end
37
+
38
+ send(adapter_method, config)
39
+ end
40
+ end
41
+
42
+ def reload!
43
+ @adapter = nil
44
+ @config = nil
45
+ end
46
+
47
+ private
48
+
49
+ def connect_exclusions
50
+ # Establish a connection for each specific excluded model
51
+ # Thus all other models will shared a connection (at ActiveRecord::Base) and we can modify at will
52
+ Apartment.excluded_models.each do |excluded_model|
53
+ excluded_model.establish_connection config
54
+ end
55
+ end
56
+
57
+ def config
58
+ @config ||= Rails.configuration.database_configuration[Rails.env].symbolize_keys
59
+ end
60
+ end
61
+
78
62
  end
79
63
 
80
64
  end
@@ -0,0 +1,27 @@
1
+ module Apartment
2
+ module Elevators
3
+ # Provides a rack based db switching solution based on subdomains
4
+ # Assumes that database name should match subdomain
5
+ class Subdomain
6
+
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ request = ActionDispatch::Request.new(env)
13
+
14
+ database = subdomain(request)
15
+
16
+ Apartment::Database.switch database if database
17
+
18
+ @app.call(env)
19
+ end
20
+
21
+ def subdomain(request)
22
+ request.subdomain.present? && request.subdomain || nil
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ module Apartment
2
+
3
+ module Migrator
4
+
5
+ extend self
6
+
7
+ # Migrate to latest
8
+ def migrate(database)
9
+ Database.connect_and_reset(database){ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_path) }
10
+ end
11
+
12
+ # Migrate up/down to a specific version
13
+ def run(direction, database, version)
14
+ Database.connect_and_reset(database){ ActiveRecord::Migrator.run(direction, ActiveRecord::Migrator.migrations_path, version) }
15
+ end
16
+
17
+ # rollback latest migration `step` number of times
18
+ def rollback(database, step = 1)
19
+ Database.connect_and_reset(database){ ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_path, step) }
20
+ end
21
+ end
22
+
23
+ end
@@ -1,10 +1,9 @@
1
- require 'apartment'
2
1
  require 'rails'
3
2
 
4
3
  module Apartment
5
4
  class Railtie < Rails::Railtie
6
5
  rake_tasks do
7
- load 'tasks/multi_tenant_migrate.rake'
6
+ load 'tasks/apartment.rake'
8
7
  end
9
8
  end
10
9
  end
@@ -0,0 +1,3 @@
1
+ module Apartment
2
+ VERSION = "0.5.0"
3
+ end
@@ -0,0 +1,36 @@
1
+ namespace :apartment do
2
+
3
+ desc "Migrate all multi-tenant databases"
4
+ task :migrate => :environment do
5
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_path)
6
+
7
+ Apartment.database_names.each{ |db| Apartment::Migrator.migrate db }
8
+ end
9
+
10
+ desc "Rolls the schema back to the previous version (specify steps w/ STEP=n) across all multi-tenant dbs."
11
+ task :rollback => :environment do
12
+ step = ENV['STEP'] ? ENV['STEP'].to_i : 1
13
+ Apartment.database_names.each{ |db| Apartment::Migrator.rollback db, step }
14
+ end
15
+
16
+ namespace :migrate do
17
+
18
+ desc 'Runs the "up" for a given migration VERSION across all multi-tenant dbs.'
19
+ task :up => :environment do
20
+ version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
21
+ raise 'VERSION is required' unless version
22
+
23
+ Apartment.database_names.each{ |db| Apartment::Migrator.run :up, db, version }
24
+ end
25
+
26
+ desc 'Runs the "down" for a given migration VERSION across all multi-tenant dbs.'
27
+ task :down => :environment do
28
+ version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
29
+ raise 'VERSION is required' unless version
30
+
31
+ Apartment.database_names.each{ |db| Apartment::Migrator.run :down, db, version }
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Apartment do
4
+ it "should be valid" do
5
+ Apartment.should be_a(Module)
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ connections:
2
+ postgresql:
3
+ adapter: postgresql
4
+ database: apartment_postgresql_test
5
+ username: root
6
+ password:
7
+
8
+ mysql:
9
+ adapater: mysql
10
+ database: apartment_mysql_test
@@ -0,0 +1,7 @@
1
+ # Add your own tasks in files placed in lib/tasks ending in .rake,
2
+ # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3
+
4
+ require File.expand_path('../config/application', __FILE__)
5
+ require 'rake'
6
+
7
+ Dummy::Application.load_tasks
@@ -0,0 +1,6 @@
1
+ class ApplicationController < ActionController::Base
2
+ protect_from_forgery
3
+
4
+ def index
5
+ end
6
+ end
@@ -0,0 +1,2 @@
1
+ module ApplicationHelper
2
+ end
@@ -0,0 +1,3 @@
1
+ class Company < ActiveRecord::Base
2
+ # Dummy models
3
+ end
@@ -0,0 +1,3 @@
1
+ class User < ActiveRecord::Base
2
+ # Dummy models
3
+ end
@@ -0,0 +1 @@
1
+ <h1>Index!!</h1>
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Dummy</title>
5
+ <%= stylesheet_link_tag :all %>
6
+ <%= javascript_include_tag :defaults %>
7
+ <%= csrf_meta_tag %>
8
+ </head>
9
+ <body>
10
+
11
+ <%= yield %>
12
+
13
+ </body>
14
+ </html>
@@ -0,0 +1,4 @@
1
+ # This file is used by Rack-based servers to start the application.
2
+
3
+ require ::File.expand_path('../config/environment', __FILE__)
4
+ run Dummy::Application
@@ -0,0 +1,47 @@
1
+ require File.expand_path('../boot', __FILE__)
2
+
3
+ require "active_model/railtie"
4
+ require "active_record/railtie"
5
+ require "action_controller/railtie"
6
+ require "action_view/railtie"
7
+ require "action_mailer/railtie"
8
+
9
+ Bundler.require
10
+ require "apartment"
11
+
12
+ module Dummy
13
+ class Application < Rails::Application
14
+ # Settings in config/environments/* take precedence over those specified here.
15
+ # Application configuration should go into files in config/initializers
16
+ # -- all .rb files in that directory are automatically loaded.
17
+
18
+ config.middleware.use 'Apartment::Elevators::Subdomain'
19
+
20
+ # Custom directories with classes and modules you want to be autoloadable.
21
+ # config.autoload_paths += %W(#{config.root}/extras)
22
+
23
+ # Only load the plugins named here, in the order given (default is alphabetical).
24
+ # :all can be used as a placeholder for all plugins not explicitly named.
25
+ # config.plugins = [ :exception_notification, :ssl_requirement, :all ]
26
+
27
+ # Activate observers that should always be running.
28
+ # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
29
+
30
+ # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
31
+ # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
32
+ # config.time_zone = 'Central Time (US & Canada)'
33
+
34
+ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
35
+ # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
36
+ # config.i18n.default_locale = :de
37
+
38
+ # JavaScript files you want as :defaults (application.js is always included).
39
+ # config.action_view.javascript_expansions[:defaults] = %w(jquery rails)
40
+
41
+ # Configure the default encoding used in templates for Ruby 1.9.
42
+ config.encoding = "utf-8"
43
+
44
+ # Configure sensitive parameters which will be filtered from the log file.
45
+ config.filter_parameters += [:password]
46
+ end
47
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ gemfile = File.expand_path('../../../../Gemfile', __FILE__)
3
+
4
+ if File.exist?(gemfile)
5
+ ENV['BUNDLE_GEMFILE'] = gemfile
6
+ require 'bundler'
7
+ Bundler.setup
8
+ end
9
+
10
+ $:.unshift File.expand_path('../../../../lib', __FILE__)