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,6 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ *.log
6
+
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --format documentation
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 1.9.2@apartment
data/Gemfile CHANGED
@@ -1,15 +1,8 @@
1
1
  source "http://rubygems.org"
2
- # Add dependencies required to use your gem here.
3
- # Example:
4
2
 
5
- gem "activesupport", ">= 2.3.5"
3
+ # Uses patched version of postgresql adapter to enable apartment usage.
4
+ # Current postgresql adapter in rails doesn't properly support the use of postgresql schemas
5
+ # Pull request sent => https://github.com/rails/rails/pull/1604
6
+ gem 'rails', '3.0.8', :git => 'git://github.com/bradrobertson/rails', :branch => '3-0-stable'
6
7
 
7
-
8
- # Add dependencies to develop your gem here.
9
- # Include everything needed to run rake, tests, features, etc.
10
- group :development do
11
- gem "shoulda", ">= 0"
12
- gem "bundler", "~> 1.0.0"
13
- gem "jeweler", "~> 1.5.1"
14
- gem "rcov", ">= 0"
15
- end
8
+ gemspec
@@ -0,0 +1,31 @@
1
+ # 0.5.0
2
+ * June 20, 2011
3
+
4
+ - Added the concept of an "Elevator", a rack based strategy for db switching
5
+ - Added the Subdomain Elevator middleware to enabled db switching based on subdomain
6
+
7
+ # 0.4.0
8
+ * June 14, 2011
9
+
10
+ - Added `configure` method on Apartment instead of using yml file, allows for dynamic setting of db names to migrate for rake task
11
+ - Added `seed_after_create` config option to import seed data to new db on create
12
+
13
+ # 0.3.0
14
+ * June 10, 2011
15
+
16
+ - Added full support for database migration
17
+ - Added in method to establish new connection for excluded models on startup rather than on each switch
18
+
19
+ # 0.2.0
20
+ * June 6, 2011 *
21
+
22
+ - Refactor to use more rails/active_support functionality
23
+ - Refactor config to lazily load apartment.yml if exists
24
+ - Remove OStruct and just use hashes for fetching methods
25
+ - Added schema load on create instead of migrating from scratch
26
+
27
+ # 0.1.3
28
+ * March 30, 2011 *
29
+
30
+ - Original pass from Ryan
31
+
@@ -51,7 +51,7 @@ would not allow a full new database to be created.
51
51
 
52
52
  To switch databases using Apartment, use the following command:
53
53
 
54
- Apartment::Database.switch('database_name')
54
+ Apartment::Database.switch('database_name')
55
55
 
56
56
  When switch is called, all requests coming to ActiveRecord will be routed to the database
57
57
  you specify (with the exception of excluded models, see below). To return to the 'root'
@@ -59,32 +59,50 @@ database, call switch with no arguments.
59
59
 
60
60
  ### Switching Databases per request
61
61
 
62
- You can have Apartment route to the appropriate database per request by adding a warden task
63
- in application.rb. For instance, if you want to route to a particular database depending on
64
- user, and you have a database attribute on your user model, you could do the following:
65
-
66
- Warden::Manager.on_request do |proxy|
67
- if session[:user_id]
68
- u = User.find(session[:user_id])
69
- Apartment::Database.switch(u.database)
70
- end
71
- end
72
-
62
+ You can have Apartment route to the appropriate database by adding some Rack middleware.
63
+ Apartment can support many different "Elevators" that can take care of this routing to your data.
64
+ In house, we use the subdomain elevator, which analyzes the subdomain of the request and switches
65
+ to a database schema of the same name. It can be used like so:
66
+
67
+ # application.rb
68
+ module My Application
69
+ class Application < Rails::Application
70
+
71
+ config.middleware.use 'Apartment::Elevators::Subdomain'
72
+ end
73
+ end
74
+
73
75
  ### Excluding models
74
76
 
75
- If you have some models that should always access the 'root' database, you can specify this in
76
- a configuration file. Apartment will look for a config file called 'apartment.yml' in the /config
77
- directory of your Rails application. To exclude certain models, do the following:
77
+ If you have some models that should always access the 'root' database, you can specify this by configuring
78
+ Apartment using `Apartment.configure`. This will yield a config object for you. You can set excluded models like so:
78
79
 
79
- excluded_models: [User, Company]
80
+ Apartment.configure do |config|
81
+ config.excluded_models = [User, Company] # these models will not be multi-tenanted, but remain in the global (public) namespace
82
+ end
80
83
 
81
84
  ### Managing Migrations
82
85
 
83
- Currently, you will need to migrate each database individually. I'll be working on code to
84
- migrate all known databases soon. You can migrate any database up to the current version by
85
- calling:
86
-
87
- Apartment::Database.migrate('database_name')
86
+ In order to migrate all of your databases (or posgresql schemas) you need to provide a list
87
+ of dbs to Apartment. You can make this dynamic by providing a Proc object to be called on migrations.
88
+ This object should yield an array of string representing each database name. Example:
89
+
90
+ # Dynamically get database names to migrate
91
+ Apartment.configure do |config|
92
+ config.database_names = lambda{ Company.all.collect(&:database_name) }
93
+ end
94
+
95
+ # Use a static list of database names for migrate
96
+ Apartment.configure do |config|
97
+ config.database_names = ['db1', 'db2']
98
+ end
99
+
100
+ You can then migration your databases using the rake task:
101
+
102
+ rake apartment:migrate
103
+
104
+ This basically invokes `Apartment::Database.migrate(#{db_name})` for each database name supplied
105
+ from `Apartment.database_names`
88
106
 
89
107
  ## TODO
90
108
 
data/Rakefile CHANGED
@@ -1,54 +1,51 @@
1
- require 'rubygems'
2
- require 'bundler'
3
- begin
4
- Bundler.setup(:default, :development)
5
- rescue Bundler::BundlerError => e
6
- $stderr.puts e.message
7
- $stderr.puts "Run `bundle install` to install missing gems"
8
- exit e.status_code
9
- end
10
- require 'rake'
1
+ require 'bundler' rescue 'You must `gem install bundler` and `bundle install` to run rake tasks'
2
+ Bundler.setup
3
+ Bundler::GemHelper.install_tasks
11
4
 
12
- require 'jeweler'
13
- Jeweler::Tasks.new do |gem|
14
- # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
- gem.name = "apartment"
16
- gem.homepage = "http://github.com/ryanbrunner/apartment"
17
- gem.license = "MIT"
18
- gem.summary = %Q{A Ruby gem for managing multitenancy in Rails applications}
19
- gem.description = %Q{Apartment allows Rails applications to deal with
20
- multitenancy.}
21
- gem.email = "ryan@ryanbrunner.com"
22
- gem.authors = ["Ryan Brunner"]
23
- # Include your dependencies below. Runtime dependencies are required when using your gem,
24
- # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
25
- # gem.add_runtime_dependency 'jabber4r', '> 0.1'
26
- # gem.add_development_dependency 'rspec', '> 1.2.3'
27
- end
28
- Jeweler::RubygemsDotOrgTasks.new
5
+ require "rspec"
6
+ require "rspec/core/rake_task"
29
7
 
30
- require 'rake/testtask'
31
- Rake::TestTask.new(:test) do |test|
32
- test.libs << 'lib' << 'test'
33
- test.pattern = 'test/**/test_*.rb'
34
- test.verbose = true
8
+ RSpec::Core::RakeTask.new(:spec) do |spec|
9
+ spec.pattern = "spec/**/*_spec.rb"
35
10
  end
36
11
 
37
- require 'rcov/rcovtask'
38
- Rcov::RcovTask.new do |test|
39
- test.libs << 'test'
40
- test.pattern = 'test/**/test_*.rb'
41
- test.verbose = true
42
- end
12
+ namespace :spec do
13
+
14
+ [:tasks, :unit, :integration].each do |type|
15
+ RSpec::Core::RakeTask.new(type) do |spec|
16
+ spec.pattern = "spec/#{type}/**/*_spec.rb"
17
+ end
18
+ end
19
+
20
+ namespace :unit do
21
+ RSpec::Core::RakeTask.new(:adapters) do |spec|
22
+ spec.pattern = "spec/unit/adapters/**/*_spec.rb"
23
+ end
24
+ end
43
25
 
44
- task :default => :test
26
+ end
45
27
 
46
- require 'rake/rdoctask'
47
- Rake::RDocTask.new do |rdoc|
48
- version = File.exist?('VERSION') ? File.read('VERSION') : ""
28
+ task :default => :spec
49
29
 
50
- rdoc.rdoc_dir = 'rdoc'
51
- rdoc.title = "apartment #{version}"
52
- rdoc.rdoc_files.include('README*')
53
- rdoc.rdoc_files.include('lib/**/*.rb')
54
- end
30
+ namespace :postgres do
31
+ require 'active_record'
32
+ require "#{File.join(File.dirname(__FILE__), 'spec', 'support', 'config')}"
33
+
34
+ desc 'Build the PostgreSQL test databases'
35
+ task :build_db do
36
+ %x{ createdb -E UTF8 #{config['database']} } rescue "test db already exists"
37
+ ActiveRecord::Base.establish_connection config
38
+ load 'spec/dummy/db/schema.rb'
39
+ end
40
+
41
+ desc "drop the PostgreSQL test database"
42
+ task :drop_db do
43
+ puts "dropping database #{config['database']}"
44
+ %x{ dropdb #{config['database']} }
45
+ end
46
+
47
+ def config
48
+ Apartment::Test.config['connections']['postgresql']
49
+ end
50
+
51
+ end
@@ -1,68 +1,29 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
1
  # -*- encoding: utf-8 -*-
2
+ $: << File.expand_path("../lib", __FILE__)
3
+ require "apartment/version"
5
4
 
6
5
  Gem::Specification.new do |s|
7
6
  s.name = %q{apartment}
8
- s.version = "0.1.3"
7
+ s.version = Apartment::VERSION
9
8
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Ryan Brunner"]
12
- s.date = %q{2011-03-04}
13
- s.description = %q{Apartment allows Rails applications to deal with
14
- multitenancy.}
15
- s.email = %q{ryan@ryanbrunner.com}
16
- s.extra_rdoc_files = [
17
- "README.markdown"
18
- ]
19
- s.files = [
20
- ".bundle/config",
21
- ".project",
22
- "Gemfile",
23
- "Gemfile.lock",
24
- "README.markdown",
25
- "Rakefile",
26
- "VERSION",
27
- "apartment.gemspec",
28
- "lib/apartment.rb",
29
- "lib/apartment/associations/multi_tenant_association.rb",
30
- "lib/apartment/config.rb",
31
- "lib/apartment/config/default_config.yml",
32
- "lib/apartment/database.rb",
33
- "lib/apartment/railtie.rb",
34
- "lib/tasks/multi_tenant_migrate.rake",
35
- "pkg/apartment-0.1.3.gem"
36
- ]
9
+ s.authors = ["Ryan Brunner", "Brad Robertson"]
10
+ s.date = %q{2011-04-18}
11
+ s.summary = %q{A Ruby gem for managing database multitenancy in Rails applications}
12
+ s.description = %q{Apartment allows Rails applications to deal with database multitenancy}
13
+ s.email = %w{ryan@ryanbrunner.com bradleyrobertson@gmail.com}
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {spec}/*`.split("\n")
16
+
37
17
  s.homepage = %q{http://github.com/ryanbrunner/apartment}
38
18
  s.licenses = ["MIT"]
39
19
  s.require_paths = ["lib"]
40
20
  s.rubygems_version = %q{1.3.7}
41
- s.summary = %q{A Ruby gem for managing multitenancy in Rails applications}
42
-
43
- if s.respond_to? :specification_version then
44
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
45
- s.specification_version = 3
46
-
47
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
48
- s.add_runtime_dependency(%q<activesupport>, [">= 3.0.5"])
49
- s.add_development_dependency(%q<shoulda>, [">= 0"])
50
- s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
51
- s.add_development_dependency(%q<jeweler>, ["~> 1.5.1"])
52
- s.add_development_dependency(%q<rcov>, [">= 0"])
53
- else
54
- s.add_dependency(%q<activesupport>, [">= 3.0.5"])
55
- s.add_dependency(%q<shoulda>, [">= 0"])
56
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
57
- s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
58
- s.add_dependency(%q<rcov>, [">= 0"])
59
- end
60
- else
61
- s.add_dependency(%q<activesupport>, [">= 3.0.5"])
62
- s.add_dependency(%q<shoulda>, [">= 0"])
63
- s.add_dependency(%q<bundler>, ["~> 1.0.0"])
64
- s.add_dependency(%q<jeweler>, ["~> 1.5.1"])
65
- s.add_dependency(%q<rcov>, [">= 0"])
66
- end
21
+
22
+ s.add_dependency 'rails', '~> 3.0.8'
23
+ s.add_development_dependency 'sqlite3'
24
+ s.add_development_dependency 'rspec', '~> 2.6.0'
25
+ s.add_development_dependency 'rspec-rails', '~> 2.6.1'
26
+ s.add_development_dependency 'capybara', '1.0.0'
27
+ s.add_development_dependency 'pg', '~> 0.11.0'
28
+ s.add_development_dependency "silent-postgres", "~> 0.0.8"
67
29
  end
68
-
@@ -1,11 +1,62 @@
1
+ require 'apartment/railtie'
1
2
 
2
3
  module Apartment
3
- require 'apartment/railtie'
4
- require 'apartment/config'
5
- require 'apartment/database'
6
-
7
- def self.included(base)
8
- base.extend Apartment::ClassMethods
4
+
5
+ class << self
6
+ attr_accessor :use_postgres_schemas, :seed_after_create
7
+ attr_writer :database_names, :excluded_models
8
+
9
+ # configure apartment with available options
10
+ def configure
11
+ yield self if block_given?
12
+ Database.init
13
+ end
14
+
15
+ # Be careful not to use `return` here so both Proc and lambda can be used without breaking
16
+ def database_names
17
+ if @database_names.respond_to?(:call)
18
+ @database_names.call
19
+ else
20
+ @database_names
21
+ end
22
+ end
23
+
24
+ # Default to none
25
+ def excluded_models
26
+ @excluded_models || []
27
+ end
28
+
29
+ end
30
+
31
+ autoload :Database, 'apartment/database'
32
+ autoload :Migrator, 'apartment/migrator'
33
+
34
+ module Adapters
35
+ autoload :AbstractAdapter, 'apartment/adapters/abstract_adapter'
36
+ # Specific adapters will be loaded dynamically based on adapter in config
37
+ end
38
+
39
+ module Elevators
40
+ autoload :Subdomain, 'apartment/elevators/subdomain'
9
41
  end
42
+
43
+ # Exceptions
44
+ class ApartmentError < StandardError; end
45
+
46
+ # Raised when apartment cannot find the adapter specified in <tt>config/database.yml</tt>
47
+ class AdapterNotFound < ApartmentError; end
48
+
49
+ # Raised when database cannot find the specified schema
50
+ class SchemaNotFound < ApartmentError; end
51
+
52
+ # Raised when trying to create a schema that already exists
53
+ class SchemaExists < ApartmentError; end
54
+
10
55
  end
11
56
 
57
+ Apartment.configure do |config|
58
+ config.excluded_models = []
59
+ config.use_postgres_schemas = true
60
+ config.database_names = []
61
+ config.seed_after_create = false
62
+ end
@@ -0,0 +1,106 @@
1
+ require 'active_record'
2
+
3
+ module Apartment
4
+
5
+ module Adapters
6
+
7
+ class AbstractAdapter
8
+
9
+ # @constructor
10
+ # @param {Hash} config Database config
11
+ # @param {Hash} defaults Some default options
12
+ #
13
+ def initialize(config, defaults)
14
+ @config = config
15
+ @defaults = defaults
16
+ end
17
+
18
+ # Connect to db or schema, do stuff, reset
19
+ #
20
+ # @param {String} database Database or schema to connect to
21
+ def connect_and_reset(database)
22
+ connect_to_new(database)
23
+ yield if block_given?
24
+ ensure
25
+ reset
26
+ end
27
+
28
+ # Create new postgres schema
29
+ #
30
+ # @param {String} database Database name
31
+ def create(database)
32
+ # TODO create_database unless using_schemas?
33
+
34
+ connect_and_reset(database) do
35
+ import_database_schema
36
+
37
+ # Manually init schema migrations table (apparently there were issues with Postgres when this isn't done)
38
+ ActiveRecord::Base.connection.initialize_schema_migrations_table
39
+
40
+ # Seed data if appropriate
41
+ seed_data if Apartment.seed_after_create
42
+ end
43
+ end
44
+
45
+ # Reset the base connection
46
+ def reset
47
+ ActiveRecord::Base.establish_connection @config
48
+ end
49
+
50
+ # Switch to new connection (or schema if appopriate)
51
+ def switch(database = nil)
52
+ # Just connect to default db and return
53
+ return reset if database.nil?
54
+
55
+ connect_to_new(database)
56
+ end
57
+
58
+ protected
59
+
60
+ def create_schema
61
+ # noop
62
+ end
63
+
64
+ def connect_to_new(database)
65
+ ActiveRecord::Base.establish_connection multi_tenantify(database)
66
+ end
67
+
68
+ def import_database_schema
69
+ load_or_abort("#{Rails.root}/db/schema.rb")
70
+ end
71
+
72
+ def seed_data
73
+ load_or_abort("#{Rails.root}/db/seeds.rb")
74
+ end
75
+
76
+ # Return a new config that is multi-tenanted
77
+ def multi_tenantify(database)
78
+ @config.clone.tap do |config|
79
+ config['database'].gsub!(Rails.env.to_s, "#{database}_#{Rails.env}")
80
+ end
81
+ end
82
+
83
+ # Remove all non-alphanumeric characters
84
+ def sanitize(database)
85
+ database.gsub(/[\W]/,'')
86
+ end
87
+
88
+ # Whether or not to use postgresql schemas
89
+ def using_schemas?
90
+ false
91
+ end
92
+
93
+ def load_or_abort(file)
94
+ if File.exists?(file)
95
+ load(file)
96
+ else
97
+ abort %{#{file} doesn't exist yet}
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+
104
+
105
+ end
106
+ end