apartment 0.1.3 → 0.5.0

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