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.
@@ -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
+
@@ -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,3 @@
1
+ require 'motel/sources/default'
2
+ require 'motel/sources/database'
3
+ require 'motel/sources/redis'
@@ -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
+