apartment 0.1.3 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +5 -12
- data/HISTORY.md +31 -0
- data/{README.markdown → README.md} +39 -21
- data/Rakefile +44 -47
- data/apartment.gemspec +19 -58
- data/lib/apartment.rb +57 -6
- data/lib/apartment/adapters/abstract_adapter.rb +106 -0
- data/lib/apartment/adapters/mysql_adapter.rb +11 -0
- data/lib/apartment/adapters/postgresql_adapter.rb +55 -0
- data/lib/apartment/database.rb +58 -74
- data/lib/apartment/elevators/subdomain.rb +27 -0
- data/lib/apartment/migrator.rb +23 -0
- data/lib/apartment/railtie.rb +1 -2
- data/lib/apartment/version.rb +3 -0
- data/lib/tasks/apartment.rake +36 -0
- data/spec/apartment_spec.rb +7 -0
- data/spec/config/database.yml +10 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +6 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/models/company.rb +3 -0
- data/spec/dummy/app/models/user.rb +3 -0
- data/spec/dummy/app/views/application/index.html.erb +1 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +47 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +8 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +26 -0
- data/spec/dummy/config/environments/production.rb +49 -0
- data/spec/dummy/config/environments/test.rb +35 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/db/migrate/20110613152810_create_dummy_models.rb +20 -0
- data/spec/dummy/db/schema.rb +26 -0
- data/spec/dummy/db/seeds.rb +8 -0
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/{lib/apartment/associations/multi_tenant_association.rb → spec/dummy/public/favicon.ico} +0 -0
- data/spec/dummy/public/stylesheets/.gitkeep +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/integration/adapters/postgresql_integration_spec.rb +73 -0
- data/spec/integration/apartment_rake_integration_spec.rb +62 -0
- data/spec/integration/database_integration_spec.rb +107 -0
- data/spec/integration/middleware/subdomain_elevator_spec.rb +63 -0
- data/spec/integration/setup_spec.rb +8 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/apartment_helpers.rb +24 -0
- data/spec/support/capybara_sessions.rb +15 -0
- data/spec/support/config.rb +11 -0
- data/spec/support/migrate.rb +9 -0
- data/spec/tasks/apartment_rake_spec.rb +110 -0
- data/spec/unit/config_spec.rb +84 -0
- data/spec/unit/middleware/subdomain_elevator_spec.rb +20 -0
- data/spec/unit/migrator_spec.rb +87 -0
- metadata +112 -62
- data/.bundle/config +0 -2
- data/.project +0 -11
- data/Gemfile.lock +0 -22
- data/VERSION +0 -1
- data/lib/apartment/config.rb +0 -10
- data/lib/apartment/config/default_config.yml +0 -17
- data/lib/tasks/multi_tenant_migrate.rake +0 -14
- data/pkg/apartment-0.1.3.gem +0 -0
@@ -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
|
data/lib/apartment/database.rb
CHANGED
@@ -1,80 +1,64 @@
|
|
1
|
+
require 'active_support/core_ext/string/inflections' # for `constantize`
|
1
2
|
|
2
3
|
module Apartment
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
data/lib/apartment/railtie.rb
CHANGED
@@ -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
|
data/spec/dummy/Rakefile
ADDED
@@ -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 @@
|
|
1
|
+
<h1>Index!!</h1>
|
@@ -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
|