db-hijacker 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/Gemfile.lock +61 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +67 -0
- data/Rakefile +33 -0
- data/VERSION +1 -0
- data/example_root_schema.rb +16 -0
- data/hijacker.gemspec +70 -0
- data/init.rb +7 -0
- data/install.rb +1 -0
- data/lib/hijacker.rb +218 -0
- data/lib/hijacker/active_record_ext.rb +8 -0
- data/lib/hijacker/alias.rb +7 -0
- data/lib/hijacker/controller_methods.rb +42 -0
- data/lib/hijacker/database.rb +91 -0
- data/lib/hijacker/middleware.rb +18 -0
- data/spec/hijacker/alias_spec.rb +7 -0
- data/spec/hijacker/database_spec.rb +46 -0
- data/spec/hijacker/middleware_spec.rb +33 -0
- data/spec/hijacker_spec.rb +176 -0
- data/spec/spec_helper.rb +28 -0
- data/tasks/hijacker_tasks.rake +4 -0
- data/uninstall.rb +1 -0
- metadata +205 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
db-hijacker (0.3.1)
|
5
|
+
rails (~> 2.3.14)
|
6
|
+
|
7
|
+
GEM
|
8
|
+
remote: https://rubygems.org/
|
9
|
+
specs:
|
10
|
+
actionmailer (2.3.14)
|
11
|
+
actionpack (= 2.3.14)
|
12
|
+
actionpack (2.3.14)
|
13
|
+
activesupport (= 2.3.14)
|
14
|
+
rack (~> 1.1.0)
|
15
|
+
activerecord (2.3.14)
|
16
|
+
activesupport (= 2.3.14)
|
17
|
+
activeresource (2.3.14)
|
18
|
+
activesupport (= 2.3.14)
|
19
|
+
activesupport (2.3.14)
|
20
|
+
columnize (0.3.6)
|
21
|
+
diff-lcs (1.1.3)
|
22
|
+
linecache (0.46)
|
23
|
+
rbx-require-relative (> 0.0.4)
|
24
|
+
rack (1.1.3)
|
25
|
+
rack-test (0.6.1)
|
26
|
+
rack (>= 1.0)
|
27
|
+
rails (2.3.14)
|
28
|
+
actionmailer (= 2.3.14)
|
29
|
+
actionpack (= 2.3.14)
|
30
|
+
activerecord (= 2.3.14)
|
31
|
+
activeresource (= 2.3.14)
|
32
|
+
activesupport (= 2.3.14)
|
33
|
+
rake (>= 0.8.3)
|
34
|
+
rake (0.9.2.2)
|
35
|
+
rbx-require-relative (0.0.5)
|
36
|
+
rspec (2.8.0)
|
37
|
+
rspec-core (~> 2.8.0)
|
38
|
+
rspec-expectations (~> 2.8.0)
|
39
|
+
rspec-mocks (~> 2.8.0)
|
40
|
+
rspec-core (2.8.0)
|
41
|
+
rspec-expectations (2.8.0)
|
42
|
+
diff-lcs (~> 1.1.2)
|
43
|
+
rspec-mocks (2.8.0)
|
44
|
+
ruby-debug (0.10.4)
|
45
|
+
columnize (>= 0.1)
|
46
|
+
ruby-debug-base (~> 0.10.4.0)
|
47
|
+
ruby-debug-base (0.10.4)
|
48
|
+
linecache (>= 0.3)
|
49
|
+
sqlite3 (1.3.5)
|
50
|
+
|
51
|
+
PLATFORMS
|
52
|
+
ruby
|
53
|
+
|
54
|
+
DEPENDENCIES
|
55
|
+
db-hijacker!
|
56
|
+
rack (~> 1.1.0)
|
57
|
+
rack-test (~> 0.6.1)
|
58
|
+
rake (~> 0.9.2)
|
59
|
+
rspec (~> 2.8.0)
|
60
|
+
ruby-debug (~> 0.10.4)
|
61
|
+
sqlite3 (~> 1.3.5)
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 [name of plugin creator]
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
Hijacker
|
2
|
+
========
|
3
|
+
|
4
|
+
One application, multiple client databases. Although customizable, by default uses a combination of database and regular expression matching against the host domain to figure out which database to connect to.
|
5
|
+
|
6
|
+
Example
|
7
|
+
=======
|
8
|
+
|
9
|
+
class ApplicationController < ActionController::Base
|
10
|
+
hijack_connection({
|
11
|
+
# First thing it does is look for static routes. If this option
|
12
|
+
# exists and returns a string, it'll use that as the database
|
13
|
+
:static_routes => Proc.new {
|
14
|
+
case RAILS_ENV
|
15
|
+
when "development" then "site_development"
|
16
|
+
when "test" then "site_test"
|
17
|
+
end
|
18
|
+
},
|
19
|
+
# If it can't find the host in root.databases, it'll try pattern matching.
|
20
|
+
# Grabs $1 after a successful match.
|
21
|
+
:domain_patterns => [
|
22
|
+
/^(.+)\.domain\.com/, /^.+\.(.+)\..+/, /^(.+)\..+/
|
23
|
+
],
|
24
|
+
:after_hijack => Proc.new {
|
25
|
+
# Classes using acts_as_nested_set load the table info when preloading code in production.
|
26
|
+
# This is wrong 'cause at that point AR is connected to the root database.
|
27
|
+
Category.reset_column_information
|
28
|
+
}
|
29
|
+
})
|
30
|
+
end
|
31
|
+
|
32
|
+
For copy/pasters, a shorter version:
|
33
|
+
|
34
|
+
hijack_connection({
|
35
|
+
:static_routes => Proc.new { "site_#{Rails.env}" if !(Rails.env == "production") },
|
36
|
+
:domain_patterns => [/^(.+)\.site\.com/, /^.+\.(.+)\..+/, /^(.+)\..+/],
|
37
|
+
:after_hijack => Proc.new { Category.reset_column_information }
|
38
|
+
})
|
39
|
+
|
40
|
+
Configuration
|
41
|
+
=============
|
42
|
+
|
43
|
+
Your database.yml needs a "root" connection like so:
|
44
|
+
|
45
|
+
...
|
46
|
+
|
47
|
+
root: &root
|
48
|
+
database: root
|
49
|
+
<<: *defaults
|
50
|
+
|
51
|
+
production:
|
52
|
+
<<: *root
|
53
|
+
|
54
|
+
...
|
55
|
+
|
56
|
+
Other parts of database.yml will remain the same (development, test) but production
|
57
|
+
apps will initially start up on this root database, then hijack when the first connection
|
58
|
+
comes in.
|
59
|
+
|
60
|
+
Running tests
|
61
|
+
=============
|
62
|
+
|
63
|
+
To run the tests, just invoke RSpec:
|
64
|
+
|
65
|
+
rspec spec
|
66
|
+
|
67
|
+
Copyright (c) 2012 Michael Xavier, Donald Plummer, Woody Peterson, released under the MIT license
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
desc 'Test the hijacker plugin.'
|
9
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
10
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Generate documentation for the hijacker plugin.'
|
14
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
15
|
+
rdoc.rdoc_dir = 'rdoc'
|
16
|
+
rdoc.title = 'Hijacker'
|
17
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
18
|
+
rdoc.rdoc_files.include('README')
|
19
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
20
|
+
end
|
21
|
+
|
22
|
+
begin
|
23
|
+
require 'jeweler'
|
24
|
+
Jeweler::Tasks.new do |gemspec|
|
25
|
+
gemspec.name = "hijacker"
|
26
|
+
gemspec.summary = "One application, multiple client databases"
|
27
|
+
gemspec.description = "Allows a single Rails appliation to access many different databases"
|
28
|
+
gemspec.email = "woody@crystalcommerce.com"
|
29
|
+
gemspec.authors = ["Woody Peterson"]
|
30
|
+
end
|
31
|
+
rescue LoadError
|
32
|
+
puts "Jeweler not available. Install it with: sudo gem install jeweler"
|
33
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,16 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 1) do
|
2
|
+
create_table "databases", :force => true do |t|
|
3
|
+
t.string "database"
|
4
|
+
t.integer "master_id"
|
5
|
+
end
|
6
|
+
|
7
|
+
add_index "databases", "database"
|
8
|
+
add_index "databases", "master_id"
|
9
|
+
|
10
|
+
create_table "aliases", :force => true do |t|
|
11
|
+
t.integer "database_id"
|
12
|
+
t.string "name"
|
13
|
+
end
|
14
|
+
|
15
|
+
add_index "aliases", "name"
|
16
|
+
end
|
data/hijacker.gemspec
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{db-hijacker}
|
5
|
+
s.homepage = "https://github.com/crystalcommerce/hijacker"
|
6
|
+
s.version = "0.3.1"
|
7
|
+
|
8
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
9
|
+
s.authors = ["Michael Xavier", "Donald Plummer", "Woody Peterson"]
|
10
|
+
s.date = %q{2012-03-21}
|
11
|
+
s.description = %q{Allows a single Rails appliation to access many different databases}
|
12
|
+
s.email = %q{developers@crystalcommerce.com}
|
13
|
+
s.add_dependency("rails", "~>2.3.14")
|
14
|
+
s.add_development_dependency("rake", "~>0.9.2")
|
15
|
+
s.add_development_dependency("rack-test", "~>0.6.1")
|
16
|
+
s.add_development_dependency("rack", "~>1.1.0")
|
17
|
+
s.add_development_dependency("rspec", "~>2.8.0")
|
18
|
+
s.add_development_dependency("sqlite3", "~>1.3.5")
|
19
|
+
s.add_development_dependency("ruby-debug", "~>0.10.4")
|
20
|
+
s.extra_rdoc_files = [
|
21
|
+
"README.rdoc"
|
22
|
+
]
|
23
|
+
s.files = %w{
|
24
|
+
Gemfile
|
25
|
+
Gemfile.lock
|
26
|
+
MIT-LICENSE
|
27
|
+
README.rdoc
|
28
|
+
Rakefile
|
29
|
+
VERSION
|
30
|
+
example_root_schema.rb
|
31
|
+
hijacker.gemspec
|
32
|
+
init.rb
|
33
|
+
install.rb
|
34
|
+
lib/hijacker.rb
|
35
|
+
lib/hijacker/active_record_ext.rb
|
36
|
+
lib/hijacker/alias.rb
|
37
|
+
lib/hijacker/controller_methods.rb
|
38
|
+
lib/hijacker/database.rb
|
39
|
+
lib/hijacker/middleware.rb
|
40
|
+
spec/hijacker/alias_spec.rb
|
41
|
+
spec/hijacker/database_spec.rb
|
42
|
+
spec/hijacker/middleware_spec.rb
|
43
|
+
spec/hijacker_spec.rb
|
44
|
+
spec/spec_helper.rb
|
45
|
+
tasks/hijacker_tasks.rake
|
46
|
+
uninstall.rb
|
47
|
+
}
|
48
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
49
|
+
s.require_paths = ["lib"]
|
50
|
+
s.rubygems_version = %q{1.8.15}
|
51
|
+
s.summary = %q{One application, multiple client databases}
|
52
|
+
s.test_files = %w{
|
53
|
+
spec/hijacker/alias_spec.rb
|
54
|
+
spec/hijacker/database_spec.rb
|
55
|
+
spec/hijacker/middleware_spec.rb
|
56
|
+
spec/hijacker_spec.rb
|
57
|
+
spec/spec_helper.rb
|
58
|
+
}
|
59
|
+
|
60
|
+
if s.respond_to? :specification_version then
|
61
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
62
|
+
s.specification_version = 3
|
63
|
+
|
64
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
65
|
+
else
|
66
|
+
end
|
67
|
+
else
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
data/init.rb
ADDED
data/install.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Install hook code here
|
data/lib/hijacker.rb
ADDED
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'hijacker/active_record_ext'
|
2
|
+
require 'active_record'
|
3
|
+
require 'action_controller'
|
4
|
+
|
5
|
+
module Hijacker
|
6
|
+
class UnparseableURL < StandardError;end
|
7
|
+
class InvalidDatabase < StandardError;end
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :config, :master, :sister
|
11
|
+
attr_writer :valid_routes
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.valid_routes
|
15
|
+
@valid_routes ||= {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.connect_to_master(db_name)
|
19
|
+
connect(*Hijacker::Database.find_master_and_sister_for(db_name))
|
20
|
+
end
|
21
|
+
|
22
|
+
# Manually establishes a new connection to the database.
|
23
|
+
#
|
24
|
+
# Background: every time rails gets information
|
25
|
+
# from the database, it uses the last established connection. So,
|
26
|
+
# although we've already established a connection to a "dummy" db
|
27
|
+
# ("crystal", in this case), if we establish a new connection, all
|
28
|
+
# subsequent database calls will use these settings instead (well,
|
29
|
+
# until it's called again when it gets another request).
|
30
|
+
#
|
31
|
+
# Note that you can manually call this from script/console (or wherever)
|
32
|
+
# to connect to the database you want, ex Hijacker.connect("database")
|
33
|
+
def self.connect(target_name, sister_name = nil, options = {})
|
34
|
+
raise InvalidDatabase, 'master cannot be nil' if target_name.nil?
|
35
|
+
|
36
|
+
target_name = target_name.downcase
|
37
|
+
sister_name = sister_name.downcase unless sister_name.nil?
|
38
|
+
|
39
|
+
return if already_connected?(target_name, sister_name) || test?
|
40
|
+
|
41
|
+
verify = options.fetch(:verify, Hijacker.do_hijacking?)
|
42
|
+
|
43
|
+
db_name = determine_database_name(target_name, sister_name, verify)
|
44
|
+
|
45
|
+
establish_connection_to_database(db_name)
|
46
|
+
|
47
|
+
check_connection
|
48
|
+
|
49
|
+
self.master = db_name
|
50
|
+
self.sister = sister_name
|
51
|
+
|
52
|
+
# don't cache sister site
|
53
|
+
cache_database_route(target_name, db_name) unless sister_name
|
54
|
+
|
55
|
+
connect_sister_site_models(target_name)
|
56
|
+
|
57
|
+
reenable_query_caching
|
58
|
+
|
59
|
+
self.config[:after_hijack].call if self.config[:after_hijack]
|
60
|
+
rescue
|
61
|
+
self.establish_root_connection
|
62
|
+
raise
|
63
|
+
end
|
64
|
+
|
65
|
+
# very small chance this will raise, but if it does, we will still handle it the
|
66
|
+
# same as +Hijacker.connect+ so we don't lock up the app.
|
67
|
+
#
|
68
|
+
# Also note that sister site models share a connection via minor management of
|
69
|
+
# AR's connection_pool stuff, and will use ActiveRecord::Base.connection_pool if
|
70
|
+
# we're not in a sister-site situation
|
71
|
+
def self.connect_sister_site_models(db)
|
72
|
+
return if db.nil?
|
73
|
+
|
74
|
+
sister_db_connection_pool = self.processing_sister_site? ? nil : ActiveRecord::Base.connection_pool
|
75
|
+
self.config[:sister_site_models].each do |model_name|
|
76
|
+
ar_model = model_name.constantize
|
77
|
+
|
78
|
+
if !sister_db_connection_pool
|
79
|
+
ar_model.establish_connection(self.root_connection.config.merge(:database => db))
|
80
|
+
begin
|
81
|
+
ar_model.connection
|
82
|
+
rescue
|
83
|
+
ar_model.establish_connection(self.root_connection.config)
|
84
|
+
raise Hijacker::InvalidDatabase, db
|
85
|
+
end
|
86
|
+
sister_db_connection_pool = ar_model.connection_pool
|
87
|
+
else
|
88
|
+
ActiveRecord::Base.connection_handler.connection_pools[model_name] = sister_db_connection_pool
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# connects the sister_site_models to +db+ while calling the block
|
94
|
+
# if +db+ and self.master differ
|
95
|
+
def self.temporary_sister_connect(db, &block)
|
96
|
+
processing_sister_site = (db != self.master && db != self.sister)
|
97
|
+
self.sister = db if processing_sister_site
|
98
|
+
self.connect_sister_site_models(db) if processing_sister_site
|
99
|
+
result = block.call
|
100
|
+
self.connect_sister_site_models(self.master) if processing_sister_site
|
101
|
+
self.sister = nil if processing_sister_site
|
102
|
+
return result
|
103
|
+
end
|
104
|
+
|
105
|
+
# maintains and returns a connection to the "dummy" database.
|
106
|
+
#
|
107
|
+
# The advantage of using this over just calling
|
108
|
+
# ActiveRecord::Base.establish_connection (without arguments) to reconnect
|
109
|
+
# to the dummy database is that reusing the same connection greatly reduces
|
110
|
+
# context switching overhead etc involved with establishing a connection to
|
111
|
+
# the database. It may seem trivial, but it actually seems to speed things
|
112
|
+
# up by ~ 1/3 for already fast requests (probably less noticeable on slower
|
113
|
+
# pages).
|
114
|
+
#
|
115
|
+
# Note: does not hijack, just returns the root connection (i.e. AR::Base will
|
116
|
+
# maintain its connection)
|
117
|
+
def self.root_connection
|
118
|
+
unless $hijacker_root_connection
|
119
|
+
current_config = ActiveRecord::Base.connection.config
|
120
|
+
ActiveRecord::Base.establish_connection('root') # establish with defaults
|
121
|
+
$hijacker_root_connection = ActiveRecord::Base.connection
|
122
|
+
ActiveRecord::Base.establish_connection(current_config) # reconnect, we don't intend to hijack
|
123
|
+
end
|
124
|
+
|
125
|
+
return $hijacker_root_connection
|
126
|
+
end
|
127
|
+
|
128
|
+
def self.root_config
|
129
|
+
ActiveRecord::Base.configurations['root']
|
130
|
+
end
|
131
|
+
|
132
|
+
# this should establish a connection to a database containing the bare minimum
|
133
|
+
# for loading the app, usually a sessions table if using sql-based sessions.
|
134
|
+
def self.establish_root_connection
|
135
|
+
ActiveRecord::Base.establish_connection('root')
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.processing_sister_site?
|
139
|
+
!sister.nil?
|
140
|
+
end
|
141
|
+
|
142
|
+
def self.master
|
143
|
+
@master || ActiveRecord::Base.configurations[ENV['RAILS_ENV']]['database']
|
144
|
+
end
|
145
|
+
|
146
|
+
def self.current_client
|
147
|
+
sister || master
|
148
|
+
end
|
149
|
+
|
150
|
+
def self.do_hijacking?
|
151
|
+
(Hijacker.config[:hosted_environments] || %w[staging production]).
|
152
|
+
include?(ENV['RAILS_ENV'])
|
153
|
+
end
|
154
|
+
|
155
|
+
# just calling establish_connection doesn't actually check to see if
|
156
|
+
# we've established a VALID connection. a call to connection will check
|
157
|
+
# this, and throw an error if the connection's invalid. It is important
|
158
|
+
# to catch the error and reconnect to a known valid database or rails
|
159
|
+
# will get stuck. This is because once we establish a connection to an
|
160
|
+
# invalid database, the next request will do a courteousy touch to the
|
161
|
+
# invalid database before reaching establish_connection and throw an error,
|
162
|
+
# preventing us from retrying to establish a valid connection and effectively
|
163
|
+
# locking us out of the app.
|
164
|
+
def self.check_connection
|
165
|
+
::ActiveRecord::Base.connection
|
166
|
+
end
|
167
|
+
|
168
|
+
def self.test?
|
169
|
+
['test', 'cucumber'].include?(RAILS_ENV)
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
def self.already_connected?(new_master, new_sister)
|
175
|
+
current_client == new_master && sister == new_sister
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.determine_database_name(target_name, sister_name, verify)
|
179
|
+
if sister_name
|
180
|
+
raise(Hijacker::InvalidDatabase, sister_name) unless Hijacker::Database.exists?(:database => sister_name)
|
181
|
+
db_name = sister_name
|
182
|
+
elsif valid_routes[target_name]
|
183
|
+
db_name = valid_routes[target_name] # cached valid name
|
184
|
+
else
|
185
|
+
db_name = target_name unless verify
|
186
|
+
db_name ||= Hijacker::Alias.find_by_name(target_name).try(:database).try(:database)
|
187
|
+
db_name ||= Hijacker::Database.find_by_database(target_name).try(:database)
|
188
|
+
raise(Hijacker::InvalidDatabase, target_name) if db_name.nil?
|
189
|
+
end
|
190
|
+
db_name
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.cache_database_route(requested_db_name, actual_db_name)
|
194
|
+
valid_routes[requested_db_name] ||= actual_db_name
|
195
|
+
end
|
196
|
+
|
197
|
+
def self.establish_connection_to_database(db_name)
|
198
|
+
hijacked_config = self.root_connection.config.dup
|
199
|
+
::ActiveRecord::Base.establish_connection(hijacked_config.merge(:database => db_name))
|
200
|
+
end
|
201
|
+
|
202
|
+
# This is a hack to get query caching back on. For some reason when we
|
203
|
+
# reconnect the database during the request, it stops doing query caching.
|
204
|
+
# We couldn't find how it's used by rails originally, but if you turn on
|
205
|
+
# query caching then start a cache block to initialize the @query_cache
|
206
|
+
# instance variable in the connection, AR will from then on build on that
|
207
|
+
# empty @query_cache hash. You have to do both 'cuz without the latter there
|
208
|
+
# will be no @query_cache available. Maybe someday we'll submit a ticket to Rails.
|
209
|
+
def self.reenable_query_caching
|
210
|
+
if ::ActionController::Base.perform_caching
|
211
|
+
::ActiveRecord::Base.connection.instance_variable_set("@query_cache_enabled", true)
|
212
|
+
::ActiveRecord::Base.connection.cache do;end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
require 'hijacker/database'
|
218
|
+
require 'hijacker/alias'
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Hijacker::ControllerMethods
|
2
|
+
module Instance
|
3
|
+
def hijack_connection
|
4
|
+
host = request.host
|
5
|
+
|
6
|
+
master, sister = determine_databases(host)
|
7
|
+
|
8
|
+
Hijacker.connect(master, sister)
|
9
|
+
|
10
|
+
return true
|
11
|
+
rescue Hijacker::InvalidDatabase => e
|
12
|
+
render_invalid_db
|
13
|
+
|
14
|
+
# If we've encountered a bad database connection, we don't want
|
15
|
+
# to continue rendering the rest of the before_filters on this, which it will
|
16
|
+
# try to do even when just rendering the bit of text above. If any filters
|
17
|
+
# return false, though, it will halt the filter chain.
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
|
21
|
+
# Returns 2-member array of the main database to connect to, and the sister
|
22
|
+
# (sister will be nil if no master is found, which means we are on the master).
|
23
|
+
def determine_databases(host)
|
24
|
+
if Hijacker.do_hijacking?
|
25
|
+
Hijacker.config[:domain_patterns].find {|pattern| host =~ pattern}
|
26
|
+
client = $1
|
27
|
+
else # development, test, etc
|
28
|
+
client = ActiveRecord::Base.configurations[Rails.env]['database']
|
29
|
+
end
|
30
|
+
|
31
|
+
raise Hijacker::UnparseableURL, "cannot parse '#{host}'" if client.nil?
|
32
|
+
|
33
|
+
master, sister = Hijacker::Database.find_master_and_sister_for(client)
|
34
|
+
|
35
|
+
return [master, sister]
|
36
|
+
end
|
37
|
+
|
38
|
+
def render_invalid_db
|
39
|
+
render :text => "You do not appear to have an account with us (#{request.host})"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
class Hijacker::Database < ActiveRecord::Base
|
2
|
+
establish_connection(Hijacker.root_config)
|
3
|
+
|
4
|
+
validates_uniqueness_of :database
|
5
|
+
|
6
|
+
has_many :aliases, :class_name => "Hijacker::Alias"
|
7
|
+
belongs_to :master, :foreign_key => 'master_id', :class_name => 'Hijacker::Database'
|
8
|
+
has_many :sisters, :foreign_key => 'master_id', :class_name => 'Hijacker::Database'
|
9
|
+
|
10
|
+
def self.current
|
11
|
+
find(:first, :conditions => {:database => Hijacker.current_client})
|
12
|
+
end
|
13
|
+
|
14
|
+
# returns a string or nil
|
15
|
+
def self.find_master_for(client)
|
16
|
+
@masters ||= {}
|
17
|
+
@masters[client] ||= self.connection.select_values(
|
18
|
+
"SELECT master.database
|
19
|
+
FROM `databases` AS master, `databases` AS sister
|
20
|
+
WHERE sister.database = #{ActiveRecord::Base.connection.quote(client)}
|
21
|
+
AND sister.master_id = master.id"
|
22
|
+
).first
|
23
|
+
end
|
24
|
+
|
25
|
+
# always returns a master, sister can be nil
|
26
|
+
def self.find_master_and_sister_for(client)
|
27
|
+
master = self.find_master_for(client)
|
28
|
+
sister = master.nil? ? nil : client
|
29
|
+
master ||= client
|
30
|
+
|
31
|
+
return master, sister
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.shared_sites
|
35
|
+
self.find_shared_sites_for(Hijacker.current_client)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.connect_to_each_shared_site(&block)
|
39
|
+
connect_each(find_shared_sites_for(Hijacker.current_client), &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.connect_to_each_sister_site(&block)
|
43
|
+
sites = find_shared_sites_for(Hijacker.current_client)
|
44
|
+
sites.delete(Hijacker.current_client)
|
45
|
+
connect_each(sites, &block)
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.find_shared_sites_for(client)
|
49
|
+
@shared_sites ||= {}
|
50
|
+
return @shared_sites[client] if @shared_sites[client].present?
|
51
|
+
|
52
|
+
current = self.find(:first, :conditions => {:database => client})
|
53
|
+
master_id = current.master_id || current.id
|
54
|
+
|
55
|
+
@shared_sites[client] = self.connection.select_values(
|
56
|
+
"SELECT `database`
|
57
|
+
FROM `databases`
|
58
|
+
WHERE master_id = '#{master_id}' OR id = '#{master_id}'
|
59
|
+
ORDER BY id"
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.connect_each(sites = all.map(&:database))
|
64
|
+
original_database = Hijacker.current_client
|
65
|
+
begin
|
66
|
+
sites.each do |db|
|
67
|
+
Hijacker.connect_to_master(db)
|
68
|
+
yield db
|
69
|
+
end
|
70
|
+
ensure
|
71
|
+
begin
|
72
|
+
Hijacker.connect_to_master(original_database)
|
73
|
+
rescue Hijacker::InvalidDatabase
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.disabled_databases
|
79
|
+
Hijacker::Database.connection.select_values("SELECT `database_name` FROM `disabled_databases`")
|
80
|
+
end
|
81
|
+
|
82
|
+
def disable!
|
83
|
+
Hijacker::Database.connection.
|
84
|
+
execute("REPLACE INTO `disabled_databases` (`database_name`) VALUES ('#{database}')")
|
85
|
+
end
|
86
|
+
|
87
|
+
def enable!
|
88
|
+
Hijacker::Database.connection.
|
89
|
+
execute("DELETE FROM `disabled_databases` WHERE `database_name` = '#{database}'")
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Hijacker
|
2
|
+
class Middleware
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
if env['HTTP_X_HIJACKER_DB'].present?
|
9
|
+
begin
|
10
|
+
Hijacker.connect(env['HTTP_X_HIJACKER_DB'])
|
11
|
+
rescue Hijacker::InvalidDatabase => e
|
12
|
+
return [404, {}, ""]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
@app.call(env)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Hijacker
|
4
|
+
describe Database do
|
5
|
+
it "has many aliases" do
|
6
|
+
lambda { subject.aliases }.should_not raise_error
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "#connect_each" do
|
10
|
+
def db(name)
|
11
|
+
mock("#{name}_db", :database => name)
|
12
|
+
end
|
13
|
+
|
14
|
+
before (:each) do
|
15
|
+
Database.stub!(:all).and_return([ db("one"), db("two"), db("three") ])
|
16
|
+
Hijacker.stub!(:connect)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "Calls the block once for each database" do
|
20
|
+
count = 0
|
21
|
+
Database.connect_each do |db|
|
22
|
+
count += 1
|
23
|
+
end
|
24
|
+
count.should == Database.all.size
|
25
|
+
end
|
26
|
+
|
27
|
+
it "Passes the name of the database to the block" do
|
28
|
+
db_names = []
|
29
|
+
Database.connect_each do |db|
|
30
|
+
db_names << db
|
31
|
+
end
|
32
|
+
db_names.should == Database.all.map(&:database)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "connects to each of the database and reconnects to the original" do
|
36
|
+
original_db = Hijacker::Database.current
|
37
|
+
Hijacker.should_receive(:connect).exactly(Database.all.size + 1).times
|
38
|
+
Database.connect_each do |db|
|
39
|
+
# noop
|
40
|
+
end
|
41
|
+
|
42
|
+
Hijacker::Database.current.should == original_db
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'rack/test'
|
3
|
+
|
4
|
+
require 'hijacker/middleware'
|
5
|
+
|
6
|
+
module Hijacker
|
7
|
+
describe Middleware do
|
8
|
+
include Rack::Test::Methods
|
9
|
+
|
10
|
+
def app
|
11
|
+
Rack::Builder.new do
|
12
|
+
use Hijacker::Middleware
|
13
|
+
run lambda { |env| [200, { 'blah' => 'blah' }, "success"] }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#call" do
|
18
|
+
context "When the 'X-Hijacker-DB' header is set" do
|
19
|
+
it "connects to the database from the header" do
|
20
|
+
Hijacker.should_receive(:connect).with("sample-db")
|
21
|
+
get '/',{}, 'HTTP_X_HIJACKER_DB' => 'sample-db'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "When the 'X-Hijacker-DB' header is not set" do
|
26
|
+
it "doesn't connect to any database" do
|
27
|
+
Hijacker.should_not_receive(:connect)
|
28
|
+
get '/',{}, "x-not-db-header" => "something"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Hijacker do
|
4
|
+
let(:hosted_environments) { %w[staging production] }
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
Hijacker.config = {
|
8
|
+
:hosted_environments => hosted_environments
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
let!(:master) { Hijacker::Database.create(:database => "master_db") }
|
13
|
+
|
14
|
+
describe ".find_shared_sites_for" do
|
15
|
+
let!(:sister) {Hijacker::Database.create(:database => "sister_db",
|
16
|
+
:master => master)}
|
17
|
+
let!(:sister2) {Hijacker::Database.create(:database => "sister_db2",
|
18
|
+
:master => master)}
|
19
|
+
let!(:unrelated) {Hijacker::Database.create(:database => "unrelated_db")}
|
20
|
+
let!(:unrelated_sister) {Hijacker::Database.create(:database => "unrelated_sister",
|
21
|
+
:master => unrelated)}
|
22
|
+
|
23
|
+
it "find shared sites given a master or sister database" do
|
24
|
+
dbs = ["master_db","sister_db","sister_db2"]
|
25
|
+
Hijacker::Database.find_shared_sites_for("master_db").should == dbs
|
26
|
+
Hijacker::Database.find_shared_sites_for("sister_db").should == dbs
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe "class methods" do
|
31
|
+
subject { Hijacker }
|
32
|
+
|
33
|
+
describe ".connect" do
|
34
|
+
let(:perform_caching) { false }
|
35
|
+
|
36
|
+
|
37
|
+
before(:each) do
|
38
|
+
subject.master = nil
|
39
|
+
subject.sister = nil
|
40
|
+
subject.valid_routes = {}
|
41
|
+
subject.stub(:test?).and_return(false)
|
42
|
+
ActiveRecord::Base.stub(:establish_connection)
|
43
|
+
subject.stub(:root_connection).and_return(stub(:config => {}))
|
44
|
+
subject.stub(:connect_sister_site_models)
|
45
|
+
Hijacker.stub(:do_hijacking?).and_return(true)
|
46
|
+
::ActionController::Base.stub(:perform_caching).
|
47
|
+
and_return(perform_caching)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "raises an InvalidDatabase exception if master is nil" do
|
51
|
+
expect { subject.connect(nil) }.to raise_error(Hijacker::InvalidDatabase)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "establishes a connection merging in the db name" do
|
55
|
+
Hijacker::Database.create!(:database => 'elsewhere')
|
56
|
+
ActiveRecord::Base.should_receive(:establish_connection).
|
57
|
+
with({:database => 'elsewhere'})
|
58
|
+
subject.connect('elsewhere')
|
59
|
+
end
|
60
|
+
|
61
|
+
it "checks the connection by calling ActiveRecord::Base.connection" do
|
62
|
+
subject.should_receive(:check_connection)
|
63
|
+
subject.connect("master_db")
|
64
|
+
end
|
65
|
+
|
66
|
+
it "attempts to find an alias" do
|
67
|
+
Hijacker::Alias.should_receive(:find_by_name).with('alias_db')
|
68
|
+
subject.connect('alias_db') rescue nil
|
69
|
+
end
|
70
|
+
|
71
|
+
it "caches the valid route at the class level :(" do
|
72
|
+
subject.connect('master_db')
|
73
|
+
subject.valid_routes['master_db'].should == 'master_db'
|
74
|
+
end
|
75
|
+
|
76
|
+
context "there's an alias for the master" do
|
77
|
+
let!(:alias_db) { Hijacker::Alias.create(:name => 'alias_db', :database => master)}
|
78
|
+
|
79
|
+
it "connects with the alias" do
|
80
|
+
ActiveRecord::Base.should_receive(:establish_connection).
|
81
|
+
with({:database => 'master_db'})
|
82
|
+
subject.connect('alias_db')
|
83
|
+
end
|
84
|
+
|
85
|
+
it "caches the valid route at the class level :(" do
|
86
|
+
subject.connect('alias_db')
|
87
|
+
subject.valid_routes['alias_db'].should == 'master_db'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "ActiveRecord reports the connection is invalid" do
|
92
|
+
before(:each) do
|
93
|
+
subject.stub(:check_connection).and_raise("oh no you didn't")
|
94
|
+
end
|
95
|
+
|
96
|
+
it "reestablishes the root connection" do
|
97
|
+
ActiveRecord::Base.should_receive(:establish_connection).with('root')
|
98
|
+
subject.connect('master_db') rescue nil
|
99
|
+
end
|
100
|
+
|
101
|
+
it "re-raises the error" do
|
102
|
+
expect { subject.connect("master_db") }.to raise_error("oh no you didn't")
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "already connected to database" do
|
107
|
+
before(:each) do
|
108
|
+
Hijacker.master = 'master_db'
|
109
|
+
Hijacker.sister = nil
|
110
|
+
end
|
111
|
+
|
112
|
+
after(:each) do
|
113
|
+
Hijacker.master = nil
|
114
|
+
end
|
115
|
+
|
116
|
+
it "does not reconnect" do
|
117
|
+
ActiveRecord::Base.should_not_receive(:establish_connection)
|
118
|
+
subject.connect('master_db')
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context "sister site specified" do
|
123
|
+
let!(:sister_db) { Hijacker::Database.create!(:database => 'sister_db',
|
124
|
+
:master => master)}
|
125
|
+
it "does reconnect if specifying a different sister" do
|
126
|
+
ActiveRecord::Base.should_receive(:establish_connection)
|
127
|
+
subject.connect('master_db', 'sister_db')
|
128
|
+
end
|
129
|
+
|
130
|
+
it "does not cache the route" do
|
131
|
+
subject.connect('master_db', 'sister_db')
|
132
|
+
subject.valid_routes.should_not have_key('sister_db')
|
133
|
+
end
|
134
|
+
|
135
|
+
it "raises InvalidDatabase if the sister does not exist" do
|
136
|
+
expect do
|
137
|
+
subject.connect("master_db", "adopted_sister_db")
|
138
|
+
end.to raise_error(Hijacker::InvalidDatabase)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context "actioncontroller configured for caching" do
|
143
|
+
let(:perform_caching) { true }
|
144
|
+
|
145
|
+
it "enables the query cache on ActiveRecord::Base" do
|
146
|
+
subject.connect('master_db')
|
147
|
+
::ActiveRecord::Base.connection.query_cache_enabled.should be_true
|
148
|
+
end
|
149
|
+
|
150
|
+
it "calls cache on the connection" do
|
151
|
+
::ActiveRecord::Base.connection.should_receive(:cache)
|
152
|
+
subject.connect('master_db')
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "after_hijack call specified" do
|
157
|
+
let(:spy) { stub.as_null_object }
|
158
|
+
before(:each) do
|
159
|
+
Hijacker.config.merge!(:after_hijack => spy)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "calls the callback" do
|
163
|
+
spy.should_receive(:call)
|
164
|
+
subject.connect('master_db')
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe ".check_connection" do
|
170
|
+
it "calls connection on ActiveRecord::Base" do
|
171
|
+
::ActiveRecord::Base.should_receive(:connection)
|
172
|
+
subject.check_connection
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'active_support'
|
3
|
+
require 'active_support/test_case'
|
4
|
+
require 'ruby-debug'
|
5
|
+
|
6
|
+
require 'active_record'
|
7
|
+
|
8
|
+
RAILS_ENV="test"
|
9
|
+
ENV['RAILS_ENV'] = 'test'
|
10
|
+
$:.unshift '../lib'
|
11
|
+
ActiveRecord::Base.configurations = {
|
12
|
+
"test" => {
|
13
|
+
:adapter => 'sqlite3',
|
14
|
+
:database => File.dirname(__FILE__) + "/test_database.sqlite3"
|
15
|
+
}
|
16
|
+
}
|
17
|
+
|
18
|
+
ActiveRecord::Base.establish_connection
|
19
|
+
require File.dirname(__FILE__) + "/../example_root_schema"
|
20
|
+
|
21
|
+
require 'hijacker'
|
22
|
+
|
23
|
+
RSpec.configure do |config|
|
24
|
+
config.before(:each) do
|
25
|
+
Hijacker::Database.delete_all
|
26
|
+
Hijacker::Alias.delete_all
|
27
|
+
end
|
28
|
+
end
|
data/uninstall.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# Uninstall hook code here
|
metadata
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: db-hijacker
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 17
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 3
|
9
|
+
- 1
|
10
|
+
version: 0.3.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Michael Xavier
|
14
|
+
- Donald Plummer
|
15
|
+
- Woody Peterson
|
16
|
+
autorequire:
|
17
|
+
bindir: bin
|
18
|
+
cert_chain: []
|
19
|
+
|
20
|
+
date: 2012-03-21 00:00:00 Z
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: rails
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 31
|
31
|
+
segments:
|
32
|
+
- 2
|
33
|
+
- 3
|
34
|
+
- 14
|
35
|
+
version: 2.3.14
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id001
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: rake
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
hash: 63
|
47
|
+
segments:
|
48
|
+
- 0
|
49
|
+
- 9
|
50
|
+
- 2
|
51
|
+
version: 0.9.2
|
52
|
+
type: :development
|
53
|
+
version_requirements: *id002
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rack-test
|
56
|
+
prerelease: false
|
57
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ~>
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
hash: 5
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
- 6
|
66
|
+
- 1
|
67
|
+
version: 0.6.1
|
68
|
+
type: :development
|
69
|
+
version_requirements: *id003
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: rack
|
72
|
+
prerelease: false
|
73
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ~>
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
hash: 19
|
79
|
+
segments:
|
80
|
+
- 1
|
81
|
+
- 1
|
82
|
+
- 0
|
83
|
+
version: 1.1.0
|
84
|
+
type: :development
|
85
|
+
version_requirements: *id004
|
86
|
+
- !ruby/object:Gem::Dependency
|
87
|
+
name: rspec
|
88
|
+
prerelease: false
|
89
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ~>
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
hash: 47
|
95
|
+
segments:
|
96
|
+
- 2
|
97
|
+
- 8
|
98
|
+
- 0
|
99
|
+
version: 2.8.0
|
100
|
+
type: :development
|
101
|
+
version_requirements: *id005
|
102
|
+
- !ruby/object:Gem::Dependency
|
103
|
+
name: sqlite3
|
104
|
+
prerelease: false
|
105
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ~>
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
hash: 17
|
111
|
+
segments:
|
112
|
+
- 1
|
113
|
+
- 3
|
114
|
+
- 5
|
115
|
+
version: 1.3.5
|
116
|
+
type: :development
|
117
|
+
version_requirements: *id006
|
118
|
+
- !ruby/object:Gem::Dependency
|
119
|
+
name: ruby-debug
|
120
|
+
prerelease: false
|
121
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ~>
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
hash: 63
|
127
|
+
segments:
|
128
|
+
- 0
|
129
|
+
- 10
|
130
|
+
- 4
|
131
|
+
version: 0.10.4
|
132
|
+
type: :development
|
133
|
+
version_requirements: *id007
|
134
|
+
description: Allows a single Rails appliation to access many different databases
|
135
|
+
email: developers@crystalcommerce.com
|
136
|
+
executables: []
|
137
|
+
|
138
|
+
extensions: []
|
139
|
+
|
140
|
+
extra_rdoc_files:
|
141
|
+
- README.rdoc
|
142
|
+
files:
|
143
|
+
- Gemfile
|
144
|
+
- Gemfile.lock
|
145
|
+
- MIT-LICENSE
|
146
|
+
- README.rdoc
|
147
|
+
- Rakefile
|
148
|
+
- VERSION
|
149
|
+
- example_root_schema.rb
|
150
|
+
- hijacker.gemspec
|
151
|
+
- init.rb
|
152
|
+
- install.rb
|
153
|
+
- lib/hijacker.rb
|
154
|
+
- lib/hijacker/active_record_ext.rb
|
155
|
+
- lib/hijacker/alias.rb
|
156
|
+
- lib/hijacker/controller_methods.rb
|
157
|
+
- lib/hijacker/database.rb
|
158
|
+
- lib/hijacker/middleware.rb
|
159
|
+
- spec/hijacker/alias_spec.rb
|
160
|
+
- spec/hijacker/database_spec.rb
|
161
|
+
- spec/hijacker/middleware_spec.rb
|
162
|
+
- spec/hijacker_spec.rb
|
163
|
+
- spec/spec_helper.rb
|
164
|
+
- tasks/hijacker_tasks.rake
|
165
|
+
- uninstall.rb
|
166
|
+
homepage: https://github.com/crystalcommerce/hijacker
|
167
|
+
licenses: []
|
168
|
+
|
169
|
+
post_install_message:
|
170
|
+
rdoc_options:
|
171
|
+
- --charset=UTF-8
|
172
|
+
require_paths:
|
173
|
+
- lib
|
174
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
175
|
+
none: false
|
176
|
+
requirements:
|
177
|
+
- - ">="
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
hash: 3
|
180
|
+
segments:
|
181
|
+
- 0
|
182
|
+
version: "0"
|
183
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
184
|
+
none: false
|
185
|
+
requirements:
|
186
|
+
- - ">="
|
187
|
+
- !ruby/object:Gem::Version
|
188
|
+
hash: 3
|
189
|
+
segments:
|
190
|
+
- 0
|
191
|
+
version: "0"
|
192
|
+
requirements: []
|
193
|
+
|
194
|
+
rubyforge_project:
|
195
|
+
rubygems_version: 1.8.15
|
196
|
+
signing_key:
|
197
|
+
specification_version: 3
|
198
|
+
summary: One application, multiple client databases
|
199
|
+
test_files:
|
200
|
+
- spec/hijacker/alias_spec.rb
|
201
|
+
- spec/hijacker/database_spec.rb
|
202
|
+
- spec/hijacker/middleware_spec.rb
|
203
|
+
- spec/hijacker_spec.rb
|
204
|
+
- spec/spec_helper.rb
|
205
|
+
has_rdoc:
|