motel-activerecord 1.0.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.
- checksums.yaml +7 -0
- data/README.md +225 -0
- data/VERSION +1 -0
- data/lib/motel-activerecord.rb +10 -0
- data/lib/motel/connection_adapters.rb +3 -0
- data/lib/motel/connection_adapters/connection_handler.rb +96 -0
- data/lib/motel/connection_adapters/connection_specification.rb +2 -0
- data/lib/motel/connection_adapters/connection_specification/resolver.rb +92 -0
- data/lib/motel/errors.rb +28 -0
- data/lib/motel/lobby.rb +47 -0
- data/lib/motel/manager.rb +69 -0
- data/lib/motel/multi_tenant.rb +53 -0
- data/lib/motel/railtie.rb +63 -0
- data/lib/motel/sources.rb +3 -0
- data/lib/motel/sources/database.rb +120 -0
- data/lib/motel/sources/default.rb +54 -0
- data/lib/motel/sources/redis.rb +80 -0
- data/lib/motel/version.rb +6 -0
- data/spec/lib/motel/connection_adapters/connection_handler_spec.rb +184 -0
- data/spec/lib/motel/connection_adapters/connection_specification/resolver_spec.rb +120 -0
- data/spec/lib/motel/lobby_spec.rb +155 -0
- data/spec/lib/motel/manager_spec.rb +238 -0
- data/spec/lib/motel/multi_tenant_spec.rb +169 -0
- data/spec/lib/motel/sources/database_spec.rb +246 -0
- data/spec/lib/motel/sources/default_spec.rb +167 -0
- data/spec/lib/motel/sources/redis_spec.rb +188 -0
- data/spec/spec_helper.rb +14 -0
- data/spec/tmp/foo.sqlite3 +0 -0
- data/spec/tmp/tenants.sqlite3 +0 -0
- metadata +144 -0
data/lib/motel/errors.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
module Motel
|
2
|
+
|
3
|
+
class ExistingTenantError < StandardError
|
4
|
+
def initialize(msg = "Existing tenant")
|
5
|
+
super(msg)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class NonexistentTenantError < StandardError
|
10
|
+
def initialize(msg = "Nonexistent tenant")
|
11
|
+
super(msg)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class NoCurrentTenantError < StandardError
|
16
|
+
def initialize(msg = "No current tenant")
|
17
|
+
super(msg)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class AnonymousTenantError < StandardError
|
22
|
+
def initialize(msg = "Anonymous tenant is not allowed")
|
23
|
+
super(msg)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
data/lib/motel/lobby.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module Motel
|
4
|
+
|
5
|
+
class Lobby
|
6
|
+
|
7
|
+
def initialize(app)
|
8
|
+
@app = app
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
request = Rack::Request.new(env)
|
13
|
+
|
14
|
+
name = tenant_name(request)
|
15
|
+
|
16
|
+
if name
|
17
|
+
if ActiveRecord::Base.motel.tenant?(name)
|
18
|
+
ActiveRecord::Base.motel.current_tenant = name
|
19
|
+
@app.call(env)
|
20
|
+
else
|
21
|
+
path = ActiveRecord::Base.motel.nonexistent_tenant_page
|
22
|
+
file = File.expand_path(path) if path
|
23
|
+
body = (File.exists?(file.to_s)) ? File.read(file) : "Nonexistent #{name} tenant"
|
24
|
+
[404, {"Content-Type" => "text/html", "Content-Length" => body.size.to_s}, [body]]
|
25
|
+
end
|
26
|
+
else
|
27
|
+
ActiveRecord::Base.motel.current_tenant = nil
|
28
|
+
@app.call(env)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def tenant_name(request)
|
35
|
+
if ActiveRecord::Base.motel.admission_criteria
|
36
|
+
regex = Regexp.new(ActiveRecord::Base.motel.admission_criteria)
|
37
|
+
name = request.path.match(regex)
|
38
|
+
name[1] if name
|
39
|
+
else
|
40
|
+
request.host.split('.').first
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'active_support/core_ext/module/attribute_accessors'
|
2
|
+
require 'active_support/core_ext/string/inflections'
|
3
|
+
require 'active_record'
|
4
|
+
|
5
|
+
module Motel
|
6
|
+
module Manager
|
7
|
+
|
8
|
+
mattr_accessor :nonexistent_tenant_page
|
9
|
+
mattr_accessor :admission_criteria
|
10
|
+
mattr_accessor :default_tenant
|
11
|
+
mattr_accessor :current_tenant
|
12
|
+
|
13
|
+
class << self
|
14
|
+
|
15
|
+
def tenants_source_configurations(config)
|
16
|
+
source_type = config[:source] || 'default'
|
17
|
+
source_class = "Motel::Sources::#{source_type.to_s.camelize}".constantize
|
18
|
+
|
19
|
+
source_instance = source_class.new(config)
|
20
|
+
|
21
|
+
ActiveRecord::Base.connection_handler.tenants_source = source_instance
|
22
|
+
end
|
23
|
+
|
24
|
+
def tenants
|
25
|
+
tenants_source.tenants
|
26
|
+
end
|
27
|
+
|
28
|
+
def tenant(name)
|
29
|
+
tenants_source.tenant(name)
|
30
|
+
end
|
31
|
+
|
32
|
+
def tenant?(name)
|
33
|
+
active_tenants.include?(name) || tenants_source.tenant?(name)
|
34
|
+
end
|
35
|
+
|
36
|
+
def add_tenant(name, spec)
|
37
|
+
tenants_source.add_tenant(name, spec)
|
38
|
+
tenant?(name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def update_tenant(name, spec)
|
42
|
+
ActiveRecord::Base.remove_connection(name)
|
43
|
+
tenants_source.update_tenant(name, spec)
|
44
|
+
tenant(name)
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete_tenant(name)
|
48
|
+
ActiveRecord::Base.remove_connection(name)
|
49
|
+
tenants_source.delete_tenant(name)
|
50
|
+
!tenant?(name)
|
51
|
+
end
|
52
|
+
|
53
|
+
def active_tenants
|
54
|
+
ActiveRecord::Base.connection_handler.active_tenants
|
55
|
+
end
|
56
|
+
|
57
|
+
def determines_tenant
|
58
|
+
ENV['TENANT'] || current_tenant || default_tenant
|
59
|
+
end
|
60
|
+
|
61
|
+
def tenants_source
|
62
|
+
ActiveRecord::Base.connection_handler.tenants_source
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Motel
|
2
|
+
module MultiTenant
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
|
7
|
+
mattr_accessor :motel, instance_writer: false
|
8
|
+
self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
|
9
|
+
self.motel = Manager
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
|
15
|
+
def establish_connection(config)
|
16
|
+
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(motel.tenants)
|
17
|
+
spec = resolver.spec(config)
|
18
|
+
connection_handler.establish_connection (ENV['TENANT'] || self.name), spec
|
19
|
+
end
|
20
|
+
|
21
|
+
def connection_pool
|
22
|
+
connection_handler.retrieve_connection_pool(current_tenant)
|
23
|
+
end
|
24
|
+
|
25
|
+
def retrieve_connection
|
26
|
+
connection_handler.retrieve_connection(current_tenant)
|
27
|
+
end
|
28
|
+
|
29
|
+
def connected?
|
30
|
+
connection_handler.connected?(current_tenant)
|
31
|
+
end
|
32
|
+
|
33
|
+
def remove_connection(tenant_name = current_tenant)
|
34
|
+
connection_handler.remove_connection(tenant_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def arel_engine
|
38
|
+
ActiveRecord::Base
|
39
|
+
end
|
40
|
+
|
41
|
+
def current_tenant
|
42
|
+
motel.determines_tenant or raise Motel::NoCurrentTenantError
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
ActiveSupport.on_load(:active_record) do
|
51
|
+
include Motel::MultiTenant
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'active_support/ordered_options'
|
2
|
+
require 'motel/manager'
|
3
|
+
require 'rails'
|
4
|
+
|
5
|
+
module Motel
|
6
|
+
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
INIT_TO_DELETE = %w(active_record.initialize_database active_record.set_reloader_hooks)
|
9
|
+
|
10
|
+
config.motel = ActiveSupport::OrderedOptions.new
|
11
|
+
|
12
|
+
rake_tasks do
|
13
|
+
namespace :db do
|
14
|
+
task :load_config do
|
15
|
+
Motel::Manager.tenants_source_configurations(
|
16
|
+
Rails.application.config.motel.tenants_source_configurations
|
17
|
+
)
|
18
|
+
|
19
|
+
Motel::Manager.current_tenant = "ActiveRecord::Base"
|
20
|
+
|
21
|
+
ActiveRecord::Tasks::DatabaseTasks.database_configuration = Motel::Manager.tenants
|
22
|
+
ActiveRecord::Tasks::DatabaseTasks.env = Motel::Manager.determines_tenant
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
ActiveRecord::Railtie.initializers.delete_if do |i|
|
28
|
+
INIT_TO_DELETE.include?(i.name)
|
29
|
+
end
|
30
|
+
|
31
|
+
initializer "motel.general_configuration" do
|
32
|
+
motel_config = Rails.application.config.motel
|
33
|
+
|
34
|
+
Motel::Manager.nonexistent_tenant_page = motel_config.nonexistent_tenant_page || 'public/404.html'
|
35
|
+
Motel::Manager.admission_criteria = motel_config.admission_criteria
|
36
|
+
Motel::Manager.default_tenant = motel_config.default_tenant
|
37
|
+
Motel::Manager.current_tenant = motel_config.current_tenant
|
38
|
+
Motel::Manager.tenants_source_configurations(motel_config.tenants_source_configurations)
|
39
|
+
end
|
40
|
+
|
41
|
+
initializer "motel.configure_middleware" do |app|
|
42
|
+
unless Rails.application.config.motel.disable_middleware
|
43
|
+
app.config.middleware.insert_before ActiveRecord::Migration::CheckPending, Lobby
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
initializer "active_record.set_reloader_hooks" do |app|
|
48
|
+
hook = app.config.reload_classes_only_on_change ? :to_prepare : :to_cleanup
|
49
|
+
|
50
|
+
ActiveSupport.on_load(:active_record) do
|
51
|
+
ActionDispatch::Reloader.send(hook) do
|
52
|
+
if ActiveRecord::Base.motel.active_tenants.any?
|
53
|
+
ActiveRecord::Base.clear_reloadable_connections!
|
54
|
+
ActiveRecord::Base.clear_cache!
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Motel
|
4
|
+
module Sources
|
5
|
+
|
6
|
+
class Database
|
7
|
+
|
8
|
+
attr_accessor :source_spec, :table_name
|
9
|
+
|
10
|
+
def initialize(config = {})
|
11
|
+
@source_spec = config[:source_spec]
|
12
|
+
@table_name = config[:table_name]
|
13
|
+
end
|
14
|
+
|
15
|
+
def tenants
|
16
|
+
query_result.inject({}) do |hash, tenant|
|
17
|
+
name = tenant.delete('name')
|
18
|
+
|
19
|
+
tenant.each do |field, value|
|
20
|
+
if table[field].respond_to? :column
|
21
|
+
tenant[field] = table[field].column.type_cast(value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
hash[name] = tenant
|
26
|
+
hash
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def tenant(name)
|
31
|
+
tenants[name]
|
32
|
+
end
|
33
|
+
|
34
|
+
def tenant?(name)
|
35
|
+
tenants.key?(name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def add_tenant(name, spec)
|
39
|
+
raise ExistingTenantError if tenant?(name)
|
40
|
+
|
41
|
+
spec = spec.merge(:name => name.to_s)
|
42
|
+
spec.delete_if{ |c,v| v.nil? }
|
43
|
+
|
44
|
+
sql = <<-SQL
|
45
|
+
INSERT INTO #{table_name} (#{spec.keys.map{|c| "\`#{c}\`"}.join(',')})
|
46
|
+
VALUES (#{spec.values.map(&:inspect).join(',')})
|
47
|
+
SQL
|
48
|
+
|
49
|
+
connection_pool.with_connection { |conn| conn.execute(sql) }
|
50
|
+
end
|
51
|
+
|
52
|
+
def update_tenant(name, spec)
|
53
|
+
raise NonexistentTenantError unless tenant?(name)
|
54
|
+
|
55
|
+
spec = spec.merge(:name => name.to_s)
|
56
|
+
spec.delete_if{ |c,v| v.nil? }
|
57
|
+
|
58
|
+
sql = <<-SQL
|
59
|
+
UPDATE #{table_name}
|
60
|
+
SET #{spec.map{|c, v| "\`#{c}\` = \"#{v}\""}.join(',')}
|
61
|
+
WHERE name = "#{name}"
|
62
|
+
SQL
|
63
|
+
|
64
|
+
connection_pool.with_connection { |conn| conn.execute(sql) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def delete_tenant(name)
|
68
|
+
if tenant?(name)
|
69
|
+
sql = <<-SQL
|
70
|
+
DELETE FROM #{table_name} WHERE name = "#{name}"
|
71
|
+
SQL
|
72
|
+
|
73
|
+
connection_pool.with_connection { |conn| conn.execute(sql) }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def connection
|
78
|
+
connection_pool.connection
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def table
|
84
|
+
@table ||= Arel::Table.new(table_name, self)
|
85
|
+
end
|
86
|
+
|
87
|
+
def query
|
88
|
+
@query ||= table.project('*')
|
89
|
+
end
|
90
|
+
|
91
|
+
def query_result
|
92
|
+
connection_pool.with_connection do |conn|
|
93
|
+
conn.select_all(query.to_sql)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def spec
|
98
|
+
@spec ||= begin
|
99
|
+
resolver = Motel::ConnectionAdapters::ConnectionSpecification::Resolver.new
|
100
|
+
resolver.spec(source_spec)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def connection_handler
|
105
|
+
@connection_handler ||= begin
|
106
|
+
handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
|
107
|
+
handler.establish_connection self.class, spec
|
108
|
+
handler
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def connection_pool
|
113
|
+
connection_handler.retrieve_connection_pool self.class
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Motel
|
4
|
+
module Sources
|
5
|
+
|
6
|
+
class Default
|
7
|
+
|
8
|
+
attr_accessor :tenants
|
9
|
+
|
10
|
+
alias :configurations= :tenants=
|
11
|
+
|
12
|
+
def initialize(config = {})
|
13
|
+
@tenants = config[:configurations] || {}
|
14
|
+
end
|
15
|
+
|
16
|
+
def tenant(name)
|
17
|
+
tenants[name]
|
18
|
+
end
|
19
|
+
|
20
|
+
def tenant?(name)
|
21
|
+
tenants.key?(name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_tenant(name, spec)
|
25
|
+
raise ExistingTenantError if tenant?(name)
|
26
|
+
|
27
|
+
tenants[name] = keys_to_string(spec)
|
28
|
+
end
|
29
|
+
|
30
|
+
def update_tenant(name, spec)
|
31
|
+
raise NonexistentTenantError unless tenant?(name)
|
32
|
+
|
33
|
+
spec = keys_to_string(spec)
|
34
|
+
tenants[name].merge!(spec)
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete_tenant(name)
|
38
|
+
tenants.delete(name)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def keys_to_string(hash)
|
44
|
+
hash = hash.inject({}) do |h, (k, v)|
|
45
|
+
h[k.to_s] = v
|
46
|
+
h
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|