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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 4d99bb0326610a2417b22820a6f19fe41267d345
4
+ data.tar.gz: 5758e9d8179ff47709fe4b3f4633e55842a386ed
5
+ SHA512:
6
+ metadata.gz: 19f4d633c32fe7a15744f7c3c3599a2adb43c5e04bd1f6be0ce7e1c6d8f12ecf1303707c68f8b58bd5e8ce59fc6906a633b336cfafbc59d60771dc4a3f3ce9f3
7
+ data.tar.gz: e1900055fe03cb927f8daaf1d6ad4eaf887fe941dbacbe6e371f2ffcd0d6c2e7d0db139ebcf0e69740f7b97d06032a3bad297b24fbbb7c0f3588662583ff6ee7
data/README.md ADDED
@@ -0,0 +1,225 @@
1
+ Motel ActiveRecord
2
+ ===================
3
+
4
+ Motel is a gem that adds functionality to ActiveRecord to use
5
+ connections to multiple databases, one for each tenant.
6
+
7
+ # Features
8
+
9
+ * Adds multi-tenant functionality to ActiveRecord.
10
+ * Multiple databases, one for each tenant.
11
+ * Tenant connection details are stored keying them by the name on a database or redis server.
12
+ * Use with or without Rails.
13
+
14
+ # Installing
15
+
16
+ ```ruby
17
+ gem install motel-activerecord
18
+ ```
19
+
20
+ or add the following line to Gemfile:
21
+
22
+ ```ruby
23
+ gem 'motel-activerecord'
24
+ ```
25
+
26
+ and run `bundle install` from your shell.
27
+
28
+ # Supported Ruby and Rails versions
29
+ The gem motel-activerecord supports MRI Ruby 2.0 or greater and Rails 4.0 or greater.
30
+
31
+ # Configuration
32
+
33
+ ## Use with Rails
34
+
35
+ In your app/application.rb file write this:
36
+
37
+ ### Specifying database as a source of tenants
38
+
39
+ ```ruby
40
+ config.motel.tenants_source_configurations = {
41
+ source: :database,
42
+ source_spec: { adapter: 'sqlite3', database: 'db/tenants.sqlite3' },
43
+ table_name: 'tenant'
44
+ }
45
+ ```
46
+
47
+ You can specify the source by providing a hash of the
48
+ connection specification. Example:
49
+
50
+ ```ruby
51
+ source_spec: {adapter: 'sqlite3', database: 'db/tenants.sqlite3'}
52
+ ```
53
+
54
+ Table name where are stored the connection details of tenants:
55
+
56
+ ```ruby
57
+ table_name: 'tenant'
58
+ ```
59
+
60
+ Note: The columns of the table must contain connection details and
61
+ thad are according with the information needed to connect to a database,
62
+ including the name column to store the tenant name. Example columns
63
+ are showed below:
64
+
65
+ |Name |Type |
66
+ | ----------|:---------:|
67
+ | name | String |
68
+ | adapter | String |
69
+ | sockect | String |
70
+ | port | Integer |
71
+ | pool | Integer |
72
+ | host | Integer |
73
+ | username | Integer |
74
+ | password | Integer |
75
+ | database | Integer |
76
+ | url | String |
77
+
78
+
79
+ ### Specifying a redis-server as a source of tenants
80
+
81
+ ```ruby
82
+ config.motel.tenants_source_configurations = {
83
+ source: :redis,
84
+ host: 127.0.0.1,
85
+ port: 6380,
86
+ password: 'redis_password'
87
+ }
88
+ ```
89
+ To connect to Redis listening on a Unix socket, try use 'path'
90
+ option.
91
+
92
+ ### Default source of tenants
93
+
94
+ Also you can use the gem without specify a source configuration.
95
+
96
+ If you want to assing dirently the tenants specificactions you can do it:
97
+
98
+ ```ruby
99
+ config.motel.tenants_source_configurations = {
100
+ tenant: { 'foo' => { adapter: 'sqlite3', database: 'db/foo.sqlite3' }}
101
+ }
102
+ ```
103
+
104
+ Assing tenants from database.yml file:
105
+
106
+ ```ruby
107
+ config.motel.tenants_source_configurations = {
108
+ tenant: Rails.application.config.database_configuration
109
+ }
110
+ ```
111
+
112
+ Note: The methods like `add_tenant`, `update_tenant` and
113
+ `delete_tenant` dosen't store permanently tenants.
114
+
115
+ ### More configurations
116
+
117
+ You can assign a default tenant if the current tenant is null:
118
+
119
+ ```ruby
120
+ config.motel.default_tenant = 'my_default_tenant'
121
+ ```
122
+
123
+ Tenants switching is done via the subdomain of the url, you can
124
+ specify a criteria to identify the tenant providing a regex as a
125
+ string. Example, to get the tenant `foo` from the following url
126
+ `http://www.example.com/foo/index` you should write:
127
+
128
+ ```ruby
129
+ config.motel.admission_criteria = '\/(\w*)\/'
130
+ ```
131
+
132
+ To disable automatic switching between tenants by url you must
133
+ disable the middleware:
134
+
135
+ ```ruby
136
+ config.motel.disable_middleware = true
137
+ ```
138
+
139
+ Path of the html page to show if tenant doesn't exist. Default is
140
+ `public/404.html`.
141
+
142
+ ```ruby
143
+ config.motel.nonexistent_tenant_page = 'new_path'
144
+ ```
145
+
146
+ ## Use without Rails
147
+
148
+ ### Specifying the source of tenants
149
+
150
+ You can set the source of the tenants in the same way as with Rails, use the method `tenants_source_configurations` of `ActiveRecord::Base.motel`:
151
+
152
+ ```ruby
153
+ ActiveRecord::Base.motel.tenants_source_configurations({
154
+ source: :database,
155
+ source_spec: { adapter: 'sqlite3', database: 'db/tenants.sqlite3' },
156
+ table_name: 'tenant'
157
+ })
158
+ ```
159
+
160
+ # Available methods
161
+
162
+ Set a tenats source configurations
163
+ ```ruby
164
+ ActiveRecord::Base.motel.tenants_source_configurations(config)
165
+ ```
166
+
167
+ Set the admission criterio for the middleware
168
+ ```ruby
169
+ ActiveRecord::Base.motel.admission_criterio
170
+ ```
171
+
172
+ Set a default tenant
173
+ ```ruby
174
+ ActiveRecord::Base.motel.default_tenant
175
+ ```
176
+
177
+ Set the html page to show if tenant doesn't exist
178
+ ```ruby
179
+ ActiveRecord::Base.motel.nonexistent_tenant_page
180
+ ```
181
+
182
+ Set a current tenant
183
+ ```ruby
184
+ ActiveRecord::Base.motel.current_tenant
185
+ ```
186
+
187
+ Retrieve the connection details of all tenants
188
+ ```ruby
189
+ ActiveRecord::Base.motel.tenants
190
+ ```
191
+
192
+ Retrieve a tenant
193
+ ```ruby
194
+ ActiveRecord::Base.motel.tenant(name)
195
+ ```
196
+
197
+ Determine if a tenant exists
198
+ ```ruby
199
+ ActiveRecord::Base.motel.tenant?(name)
200
+ ```
201
+
202
+ Add tenant
203
+ ```ruby
204
+ ActiveRecord::Base.motel.add_tenant(name, spec)
205
+ ```
206
+
207
+ Update tenant
208
+ ```ruby
209
+ ActiveRecord::Base.motel.update_tenant(name, spec)
210
+ ```
211
+
212
+ Delete tenant
213
+ ```ruby
214
+ ActiveRecord::Base.motel.delete_tenant(name)
215
+ ```
216
+
217
+ Retrieve the names of the tenants of active connections
218
+ ```ruby
219
+ ActiveRecord::Base.motel.active_tenants
220
+ ```
221
+
222
+ Determine the tenant to use for the connection
223
+ ```ruby
224
+ ActiveRecord::Base.motel.determines_tenant
225
+ ```
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,10 @@
1
+ require 'motel/connection_adapters'
2
+ require 'motel/lobby'
3
+ require 'motel/manager'
4
+ require 'motel/sources'
5
+ require 'motel/multi_tenant'
6
+ require 'motel/errors'
7
+ require 'motel/version'
8
+
9
+ require 'motel/railtie' if defined? Rails
10
+
@@ -0,0 +1,3 @@
1
+ require 'motel/connection_adapters/connection_handler'
2
+ require 'motel/connection_adapters/connection_specification'
3
+
@@ -0,0 +1,96 @@
1
+ require 'active_record'
2
+
3
+ module Motel
4
+ module ConnectionAdapters
5
+
6
+ class ConnectionHandler < ActiveRecord::ConnectionAdapters::ConnectionHandler
7
+
8
+ attr_accessor :tenants_source
9
+
10
+ def initialize(tenants_source = Sources::Default.new)
11
+ @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
12
+ h[k] = ThreadSafe::Cache.new(:initial_capacity => 2)
13
+ end
14
+ @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
15
+ h[k] = ThreadSafe::Cache.new
16
+ end
17
+
18
+ @tenants_source = tenants_source
19
+ end
20
+
21
+ def establish_connection(tenant_name, spec = nil)
22
+ spec ||= connection_especification(tenant_name)
23
+ @class_to_pool.clear
24
+ owner_to_pool[tenant_name] = ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec)
25
+ end
26
+
27
+ def retrieve_connection(tenant_name)
28
+ pool = retrieve_connection_pool(tenant_name)
29
+
30
+ unless pool.connection
31
+ establish_connection tenant_name
32
+ pool = retrieve_connection_pool(tenant_name)
33
+ end
34
+
35
+ pool.connection
36
+ end
37
+
38
+ def retrieve_connection_pool(tenant_name)
39
+ class_to_pool[tenant_name] ||= begin
40
+ pool = pool_for(tenant_name)
41
+
42
+ class_to_pool[tenant_name] = pool
43
+ end
44
+ end
45
+
46
+ def remove_connection(tenant_name)
47
+ if pool = owner_to_pool.delete(tenant_name)
48
+ @class_to_pool.clear
49
+ pool.automatic_reconnect = false
50
+ pool.disconnect!
51
+ pool.spec.config
52
+ end
53
+ end
54
+
55
+ def connected?(tenant_name)
56
+ conn = retrieve_connection_pool(tenant_name)
57
+ conn && conn.connected?
58
+ end
59
+
60
+ def pool_for(tenant_name)
61
+ owner_to_pool.fetch(tenant_name) {
62
+ establish_connection tenant_name
63
+ }
64
+ end
65
+
66
+ def pool_from_any_process_for(tenant_name)
67
+ owner_to_pool = @owner_to_pool.values.find { |v| v[tenant_name] }
68
+ owner_to_pool && owner_to_pool[tenant_name]
69
+ end
70
+
71
+ def active_tenants
72
+ owner_to_pool.keys
73
+ end
74
+
75
+ private
76
+
77
+ def connection_especification(tenant_name)
78
+ unless tenants_source.tenant?(tenant_name)
79
+ raise NonexistentTenantError, "Nonexistent #{tenant_name} tenant"
80
+ end
81
+
82
+ resolver = ConnectionSpecification::Resolver.new
83
+ spec = resolver.spec(tenants_source.tenant(tenant_name))
84
+
85
+ unless ActiveRecord::Base.respond_to?(spec.adapter_method)
86
+ raise ActiveRecord::AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
87
+ end
88
+
89
+ spec
90
+ end
91
+
92
+ end
93
+
94
+ end
95
+ end
96
+
@@ -0,0 +1,2 @@
1
+ require 'motel/connection_adapters/connection_specification/resolver'
2
+
@@ -0,0 +1,92 @@
1
+ require 'uri'
2
+ require 'active_record'
3
+ require 'active_support/core_ext/hash/keys'
4
+
5
+ module Motel
6
+ module ConnectionAdapters
7
+ module ConnectionSpecification
8
+ class Resolver
9
+
10
+ attr_accessor :configurations
11
+
12
+ def initialize(configurations = nil)
13
+ @configurations = configurations || {}
14
+ end
15
+
16
+ def spec(config)
17
+ spec = resolve(config).symbolize_keys
18
+
19
+ raise(ActiveRecord::AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
20
+
21
+ path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
22
+ begin
23
+ require path_to_adapter
24
+ rescue Gem::LoadError => e
25
+ raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
26
+ rescue LoadError => e
27
+ raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
28
+ end
29
+
30
+ adapter_method = "#{spec[:adapter]}_connection"
31
+ ActiveRecord::ConnectionAdapters::ConnectionSpecification.new(spec, adapter_method)
32
+ end
33
+
34
+ private
35
+
36
+ def resolve(config)
37
+ case config
38
+ when nil
39
+ raise ActiveRecord::AdapterNotSpecified
40
+ when String, Symbol
41
+ resolve_string_connection config.to_s
42
+ when Hash
43
+ resolve_hash_connection config
44
+ end
45
+ end
46
+
47
+ def resolve_hash_connection(spec)
48
+ if url = spec.delete("url")
49
+ connection_hash = resolve_string_connection(url)
50
+ spec.merge!(connection_hash)
51
+ end
52
+ spec
53
+ end
54
+
55
+ def resolve_string_connection(spec)
56
+ hash = configurations.fetch(spec) do |k|
57
+ connection_url_to_hash(k)
58
+ end
59
+
60
+ resolve_hash_connection hash
61
+ end
62
+
63
+ def connection_url_to_hash(url)
64
+ config = URI.parse url
65
+ adapter = config.scheme
66
+ adapter = "postgresql" if adapter == "postgres"
67
+ spec = { :adapter => adapter,
68
+ :username => config.user,
69
+ :password => config.password,
70
+ :port => config.port,
71
+ :database => config.path.sub(%r{^/},""),
72
+ :host => config.host }
73
+
74
+ spec.reject!{ |_,value| value.blank? }
75
+
76
+ uri_parser = URI::Parser.new
77
+
78
+ spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) }
79
+
80
+ if config.query
81
+ options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
82
+
83
+ spec.merge!(options)
84
+ end
85
+
86
+ spec
87
+ end
88
+
89
+ end
90
+ end
91
+ end
92
+ end