synerma-apartment 3.1.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/.gitignore +15 -0
- data/.pryrc +5 -0
- data/.rspec +4 -0
- data/.rubocop.yml +79 -0
- data/.ruby-version +1 -0
- data/Appraisals +182 -0
- data/CODE_OF_CONDUCT.md +71 -0
- data/Gemfile +20 -0
- data/Guardfile +11 -0
- data/README.md +671 -0
- data/Rakefile +157 -0
- data/legacy_CHANGELOG.md +965 -0
- data/lib/apartment/active_record/connection_handling.rb +31 -0
- data/lib/apartment/active_record/internal_metadata.rb +9 -0
- data/lib/apartment/active_record/postgres/schema_dumper.rb +20 -0
- data/lib/apartment/active_record/postgresql_adapter.rb +58 -0
- data/lib/apartment/active_record/schema_migration.rb +11 -0
- data/lib/apartment/adapters/abstract_adapter.rb +275 -0
- data/lib/apartment/adapters/abstract_jdbc_adapter.rb +20 -0
- data/lib/apartment/adapters/jdbc_mysql_adapter.rb +19 -0
- data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +62 -0
- data/lib/apartment/adapters/mysql2_adapter.rb +77 -0
- data/lib/apartment/adapters/postgis_adapter.rb +13 -0
- data/lib/apartment/adapters/postgresql_adapter.rb +280 -0
- data/lib/apartment/adapters/sqlite3_adapter.rb +66 -0
- data/lib/apartment/adapters/trilogy_adapter.rb +29 -0
- data/lib/apartment/console.rb +24 -0
- data/lib/apartment/custom_console.rb +42 -0
- data/lib/apartment/deprecation.rb +8 -0
- data/lib/apartment/elevators/domain.rb +23 -0
- data/lib/apartment/elevators/first_subdomain.rb +18 -0
- data/lib/apartment/elevators/generic.rb +33 -0
- data/lib/apartment/elevators/host.rb +35 -0
- data/lib/apartment/elevators/host_hash.rb +26 -0
- data/lib/apartment/elevators/subdomain.rb +66 -0
- data/lib/apartment/log_subscriber.rb +45 -0
- data/lib/apartment/migrator.rb +46 -0
- data/lib/apartment/model.rb +29 -0
- data/lib/apartment/railtie.rb +68 -0
- data/lib/apartment/tasks/enhancements.rb +55 -0
- data/lib/apartment/tasks/task_helper.rb +54 -0
- data/lib/apartment/tenant.rb +63 -0
- data/lib/apartment/version.rb +5 -0
- data/lib/apartment.rb +155 -0
- data/lib/generators/apartment/install/USAGE +5 -0
- data/lib/generators/apartment/install/install_generator.rb +11 -0
- data/lib/generators/apartment/install/templates/apartment.rb +116 -0
- data/lib/tasks/apartment.rake +106 -0
- data/synerma-apartment.gemspec +40 -0
- metadata +198 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord # :nodoc:
|
4
|
+
# This is monkeypatching Active Record to ensure that whenever a new connection is established it
|
5
|
+
# switches to the same tenant as before the connection switching. This problem is more evident when
|
6
|
+
# using read replica in Rails 6
|
7
|
+
module ConnectionHandling
|
8
|
+
if ActiveRecord.version.release <= Gem::Version.new('6.2')
|
9
|
+
def connected_to_with_tenant(database: nil, role: nil, prevent_writes: false, &blk)
|
10
|
+
current_tenant = Apartment::Tenant.current
|
11
|
+
|
12
|
+
connected_to_without_tenant(database: database, role: role, prevent_writes: prevent_writes) do
|
13
|
+
Apartment::Tenant.switch!(current_tenant)
|
14
|
+
yield(blk)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
else
|
18
|
+
def connected_to_with_tenant(role: nil, shard: nil, prevent_writes: false, &blk)
|
19
|
+
current_tenant = Apartment::Tenant.current
|
20
|
+
|
21
|
+
connected_to_without_tenant(role: role, shard: shard, prevent_writes: prevent_writes) do
|
22
|
+
Apartment::Tenant.switch!(current_tenant)
|
23
|
+
yield(blk)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
alias connected_to_without_tenant connected_to
|
29
|
+
alias connected_to connected_to_with_tenant
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This patch prevents `create_schema` from being added to db/schema.rb as schemas are managed by Apartment
|
4
|
+
# not ActiveRecord like they would be in a vanilla Rails setup.
|
5
|
+
|
6
|
+
require 'active_record/connection_adapters/abstract/schema_dumper'
|
7
|
+
require 'active_record/connection_adapters/postgresql/schema_dumper'
|
8
|
+
|
9
|
+
module ActiveRecord
|
10
|
+
module ConnectionAdapters
|
11
|
+
module PostgreSQL
|
12
|
+
class SchemaDumper
|
13
|
+
alias _original_schemas schemas
|
14
|
+
def schemas(stream)
|
15
|
+
_original_schemas(stream) unless Apartment.use_schemas
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Style/ClassAndModuleChildren
|
4
|
+
|
5
|
+
# NOTE: This patch is meant to remove any schema_prefix appart from the ones for
|
6
|
+
# excluded models. The schema_prefix would be resolved by apartment's setting
|
7
|
+
# of search path
|
8
|
+
module Apartment::PostgreSqlAdapterPatch
|
9
|
+
def default_sequence_name(table, _column)
|
10
|
+
res = super
|
11
|
+
|
12
|
+
# for JDBC driver, if rescued in super_method, trim leading and trailing quotes
|
13
|
+
res.delete!('"') if defined?(JRUBY_VERSION)
|
14
|
+
|
15
|
+
schema_prefix = "#{sequence_schema(res)}."
|
16
|
+
|
17
|
+
# NOTE: Excluded models should always access the sequence from the default
|
18
|
+
# tenant schema
|
19
|
+
if excluded_model?(table)
|
20
|
+
default_tenant_prefix = "#{Apartment::Tenant.default_tenant}."
|
21
|
+
|
22
|
+
# Unless the res is already prefixed with the default_tenant_prefix
|
23
|
+
# we should delete the schema_prefix and add the default_tenant_prefix
|
24
|
+
unless res&.starts_with?(default_tenant_prefix)
|
25
|
+
res&.delete_prefix!(schema_prefix)
|
26
|
+
res = default_tenant_prefix + res
|
27
|
+
end
|
28
|
+
|
29
|
+
return res
|
30
|
+
end
|
31
|
+
|
32
|
+
# Delete the schema_prefix from the res if it is present
|
33
|
+
res&.delete_prefix!(schema_prefix)
|
34
|
+
|
35
|
+
res
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def sequence_schema(sequence_name)
|
41
|
+
current = Apartment::Tenant.current
|
42
|
+
return current unless current.is_a?(Array)
|
43
|
+
|
44
|
+
current.find { |schema| sequence_name.starts_with?("#{schema}.") }
|
45
|
+
end
|
46
|
+
|
47
|
+
def excluded_model?(table)
|
48
|
+
Apartment.excluded_models.any? { |m| m.constantize.table_name == table }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
53
|
+
|
54
|
+
# NOTE: inject this into postgresql adapters
|
55
|
+
class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
|
56
|
+
include Apartment::PostgreSqlAdapterPatch
|
57
|
+
end
|
58
|
+
# rubocop:enable Style/ClassAndModuleChildren
|
@@ -0,0 +1,275 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Apartment
|
4
|
+
module Adapters
|
5
|
+
# Abstract adapter from which all the Apartment DB related adapters will inherit the base logic
|
6
|
+
class AbstractAdapter
|
7
|
+
include ActiveSupport::Callbacks
|
8
|
+
define_callbacks :create, :switch
|
9
|
+
|
10
|
+
attr_writer :default_tenant
|
11
|
+
|
12
|
+
# @constructor
|
13
|
+
# @param {Hash} config Database config
|
14
|
+
#
|
15
|
+
def initialize(config)
|
16
|
+
@config = config
|
17
|
+
end
|
18
|
+
|
19
|
+
# Create a new tenant, import schema, seed if appropriate
|
20
|
+
#
|
21
|
+
# @param {String} tenant Tenant name
|
22
|
+
#
|
23
|
+
def create(tenant)
|
24
|
+
run_callbacks :create do
|
25
|
+
create_tenant(tenant)
|
26
|
+
|
27
|
+
switch(tenant) do
|
28
|
+
import_database_schema
|
29
|
+
|
30
|
+
# Seed data if appropriate
|
31
|
+
seed_data if Apartment.seed_after_create
|
32
|
+
|
33
|
+
yield if block_given?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Initialize Apartment config options such as excluded_models
|
39
|
+
#
|
40
|
+
def init
|
41
|
+
process_excluded_models
|
42
|
+
end
|
43
|
+
|
44
|
+
# Note alias_method here doesn't work with inheritence apparently ??
|
45
|
+
#
|
46
|
+
def current
|
47
|
+
Apartment.connection.current_database
|
48
|
+
end
|
49
|
+
|
50
|
+
# Return the original public tenant
|
51
|
+
#
|
52
|
+
# @return {String} default tenant name
|
53
|
+
#
|
54
|
+
def default_tenant
|
55
|
+
@default_tenant || Apartment.default_tenant
|
56
|
+
end
|
57
|
+
|
58
|
+
# Drop the tenant
|
59
|
+
#
|
60
|
+
# @param {String} tenant name
|
61
|
+
#
|
62
|
+
def drop(tenant)
|
63
|
+
with_neutral_connection(tenant) do |conn|
|
64
|
+
drop_command(conn, tenant)
|
65
|
+
end
|
66
|
+
rescue *rescuable_exceptions => e
|
67
|
+
raise_drop_tenant_error!(tenant, e)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Switch to a new tenant
|
71
|
+
#
|
72
|
+
# @param {String} tenant name
|
73
|
+
#
|
74
|
+
def switch!(tenant = nil)
|
75
|
+
run_callbacks :switch do
|
76
|
+
connect_to_new(tenant).tap do
|
77
|
+
Apartment.connection.clear_query_cache
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Connect to tenant, do your biz, switch back to previous tenant
|
83
|
+
#
|
84
|
+
# @param {String?} tenant to connect to
|
85
|
+
#
|
86
|
+
def switch(tenant = nil)
|
87
|
+
previous_tenant = current
|
88
|
+
switch!(tenant)
|
89
|
+
yield
|
90
|
+
ensure
|
91
|
+
begin
|
92
|
+
switch!(previous_tenant)
|
93
|
+
rescue StandardError => _e
|
94
|
+
reset
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Iterate over all tenants, switch to tenant and yield tenant name
|
99
|
+
#
|
100
|
+
def each(tenants = Apartment.tenant_names)
|
101
|
+
tenants.each do |tenant|
|
102
|
+
switch(tenant) { yield tenant }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Establish a new connection for each specific excluded model
|
107
|
+
#
|
108
|
+
def process_excluded_models
|
109
|
+
# All other models will shared a connection (at Apartment.connection_class)
|
110
|
+
# and we can modify at will
|
111
|
+
Apartment.excluded_models.each do |excluded_model|
|
112
|
+
process_excluded_model(excluded_model)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Reset the tenant connection to the default
|
117
|
+
#
|
118
|
+
def reset
|
119
|
+
Apartment.establish_connection @config
|
120
|
+
end
|
121
|
+
|
122
|
+
# Load the rails seed file into the db
|
123
|
+
#
|
124
|
+
def seed_data
|
125
|
+
# Don't log the output of seeding the db
|
126
|
+
silence_warnings { load_or_raise(Apartment.seed_data_file) } if Apartment.seed_data_file
|
127
|
+
end
|
128
|
+
alias seed seed_data
|
129
|
+
|
130
|
+
# Prepend the environment if configured and the environment isn't already there
|
131
|
+
#
|
132
|
+
# @param {String} tenant Database name
|
133
|
+
# @return {String} tenant name with Rails environment *optionally* prepended
|
134
|
+
#
|
135
|
+
def environmentify(tenant)
|
136
|
+
return tenant if tenant.nil? || tenant.include?(Rails.env)
|
137
|
+
|
138
|
+
if Apartment.prepend_environment
|
139
|
+
"#{Rails.env}_#{tenant}"
|
140
|
+
elsif Apartment.append_environment
|
141
|
+
"#{tenant}_#{Rails.env}"
|
142
|
+
else
|
143
|
+
tenant
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
protected
|
148
|
+
|
149
|
+
def process_excluded_model(excluded_model)
|
150
|
+
excluded_model.constantize.establish_connection @config
|
151
|
+
end
|
152
|
+
|
153
|
+
def drop_command(conn, tenant)
|
154
|
+
# connection.drop_database note that drop_database will not throw an exception, so manually execute
|
155
|
+
conn.execute("DROP DATABASE #{conn.quote_table_name(environmentify(tenant))}")
|
156
|
+
end
|
157
|
+
|
158
|
+
# Create the tenant
|
159
|
+
#
|
160
|
+
# @param {String} tenant Database name
|
161
|
+
#
|
162
|
+
def create_tenant(tenant)
|
163
|
+
with_neutral_connection(tenant) do |conn|
|
164
|
+
create_tenant_command(conn, tenant)
|
165
|
+
end
|
166
|
+
rescue *rescuable_exceptions => e
|
167
|
+
raise_create_tenant_error!(tenant, e)
|
168
|
+
end
|
169
|
+
|
170
|
+
def create_tenant_command(conn, tenant)
|
171
|
+
conn.create_database(environmentify(tenant), @config)
|
172
|
+
end
|
173
|
+
|
174
|
+
# Connect to new tenant
|
175
|
+
#
|
176
|
+
# @param {String} tenant Database name
|
177
|
+
#
|
178
|
+
def connect_to_new(tenant)
|
179
|
+
return reset if tenant.nil?
|
180
|
+
|
181
|
+
query_cache_enabled = ActiveRecord::Base.connection.query_cache_enabled
|
182
|
+
|
183
|
+
Apartment.establish_connection multi_tenantify(tenant)
|
184
|
+
Apartment.connection.verify! # call active? to manually check if this connection is valid
|
185
|
+
|
186
|
+
Apartment.connection.enable_query_cache! if query_cache_enabled
|
187
|
+
rescue *rescuable_exceptions => e
|
188
|
+
Apartment::Tenant.reset if reset_on_connection_exception?
|
189
|
+
raise_connect_error!(tenant, e)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Import the database schema
|
193
|
+
#
|
194
|
+
def import_database_schema
|
195
|
+
ActiveRecord::Schema.verbose = false # do not log schema load output.
|
196
|
+
|
197
|
+
load_or_raise(Apartment.database_schema_file) if Apartment.database_schema_file
|
198
|
+
end
|
199
|
+
|
200
|
+
# Return a new config that is multi-tenanted
|
201
|
+
# @param {String} tenant: Database name
|
202
|
+
# @param {Boolean} with_database: if true, use the actual tenant's db name
|
203
|
+
# if false, use the default db name from the db
|
204
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
205
|
+
def multi_tenantify(tenant, with_database = true)
|
206
|
+
db_connection_config(tenant).tap do |config|
|
207
|
+
multi_tenantify_with_tenant_db_name(config, tenant) if with_database
|
208
|
+
end
|
209
|
+
end
|
210
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
211
|
+
|
212
|
+
def multi_tenantify_with_tenant_db_name(config, tenant)
|
213
|
+
config[:database] = environmentify(tenant)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Load a file or raise error if it doesn't exists
|
217
|
+
#
|
218
|
+
def load_or_raise(file)
|
219
|
+
raise FileNotFound, "#{file} doesn't exist yet" unless File.exist?(file)
|
220
|
+
|
221
|
+
load(file)
|
222
|
+
end
|
223
|
+
# Backward compatibility
|
224
|
+
alias load_or_abort load_or_raise
|
225
|
+
|
226
|
+
# Exceptions to rescue from on db operations
|
227
|
+
#
|
228
|
+
def rescuable_exceptions
|
229
|
+
[ActiveRecord::ActiveRecordError] + Array(rescue_from)
|
230
|
+
end
|
231
|
+
|
232
|
+
# Extra exceptions to rescue from
|
233
|
+
#
|
234
|
+
def rescue_from
|
235
|
+
[]
|
236
|
+
end
|
237
|
+
|
238
|
+
def db_connection_config(tenant)
|
239
|
+
Apartment.db_config_for(tenant).dup
|
240
|
+
end
|
241
|
+
|
242
|
+
def with_neutral_connection(tenant, &_block)
|
243
|
+
if Apartment.with_multi_server_setup
|
244
|
+
# neutral connection is necessary whenever you need to create/remove a database from a server.
|
245
|
+
# example: when you use postgresql, you need to connect to the default postgresql database before you create
|
246
|
+
# your own.
|
247
|
+
SeparateDbConnectionHandler.establish_connection(multi_tenantify(tenant, false))
|
248
|
+
yield(SeparateDbConnectionHandler.connection)
|
249
|
+
SeparateDbConnectionHandler.connection.close
|
250
|
+
else
|
251
|
+
yield(Apartment.connection)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def reset_on_connection_exception?
|
256
|
+
false
|
257
|
+
end
|
258
|
+
|
259
|
+
def raise_drop_tenant_error!(tenant, exception)
|
260
|
+
raise TenantNotFound, "Error while dropping tenant #{environmentify(tenant)}: #{exception.message}"
|
261
|
+
end
|
262
|
+
|
263
|
+
def raise_create_tenant_error!(tenant, exception)
|
264
|
+
raise TenantExists, "Error while creating tenant #{environmentify(tenant)}: #{exception.message}"
|
265
|
+
end
|
266
|
+
|
267
|
+
def raise_connect_error!(tenant, exception)
|
268
|
+
raise TenantNotFound, "Error while connecting to tenant #{environmentify(tenant)}: #{exception.message}"
|
269
|
+
end
|
270
|
+
|
271
|
+
class SeparateDbConnectionHandler < ::ActiveRecord::Base
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apartment/adapters/abstract_adapter'
|
4
|
+
|
5
|
+
module Apartment
|
6
|
+
module Adapters
|
7
|
+
# JDBC Abstract adapter
|
8
|
+
class AbstractJDBCAdapter < AbstractAdapter
|
9
|
+
private
|
10
|
+
|
11
|
+
def multi_tenantify_with_tenant_db_name(config, tenant)
|
12
|
+
config[:url] = "#{config[:url].gsub(%r{(\S+)/.+$}, '\1')}/#{environmentify(tenant)}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def rescue_from
|
16
|
+
ActiveRecord::JDBCError
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apartment/adapters/abstract_jdbc_adapter'
|
4
|
+
|
5
|
+
module Apartment
|
6
|
+
module Tenant
|
7
|
+
def self.jdbc_mysql_adapter(config)
|
8
|
+
Adapters::JDBCMysqlAdapter.new config
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Adapters
|
13
|
+
class JDBCMysqlAdapter < AbstractJDBCAdapter
|
14
|
+
def reset_on_connection_exception?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apartment/adapters/postgresql_adapter'
|
4
|
+
|
5
|
+
module Apartment
|
6
|
+
# JDBC helper to decide wether to use JDBC Postgresql Adapter or JDBC Postgresql Adapter with Schemas
|
7
|
+
module Tenant
|
8
|
+
def self.jdbc_postgresql_adapter(config)
|
9
|
+
if Apartment.use_schemas
|
10
|
+
Adapters::JDBCPostgresqlSchemaAdapter.new(config)
|
11
|
+
else
|
12
|
+
Adapters::JDBCPostgresqlAdapter.new(config)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Adapters
|
18
|
+
# Default adapter when not using Postgresql Schemas
|
19
|
+
class JDBCPostgresqlAdapter < PostgresqlAdapter
|
20
|
+
private
|
21
|
+
|
22
|
+
def multi_tenantify_with_tenant_db_name(config, tenant)
|
23
|
+
config[:url] = "#{config[:url].gsub(%r{(\S+)/.+$}, '\1')}/#{environmentify(tenant)}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def create_tenant_command(conn, tenant)
|
27
|
+
conn.create_database(environmentify(tenant), thisisahack: '')
|
28
|
+
end
|
29
|
+
|
30
|
+
def rescue_from
|
31
|
+
ActiveRecord::JDBCError
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Separate Adapter for Postgresql when using schemas
|
36
|
+
class JDBCPostgresqlSchemaAdapter < PostgresqlSchemaAdapter
|
37
|
+
# Set schema search path to new schema
|
38
|
+
#
|
39
|
+
def connect_to_new(tenant = nil)
|
40
|
+
return reset if tenant.nil?
|
41
|
+
raise ActiveRecord::StatementInvalid, "Could not find schema #{tenant}" unless schema_exists?(tenant)
|
42
|
+
|
43
|
+
@current = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
|
44
|
+
Apartment.connection.schema_search_path = full_search_path
|
45
|
+
rescue ActiveRecord::StatementInvalid, ActiveRecord::JDBCError
|
46
|
+
raise TenantNotFound, "One of the following schema(s) is invalid: #{full_search_path}"
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def tenant_exists?(tenant)
|
52
|
+
return true unless Apartment.tenant_presence_check
|
53
|
+
|
54
|
+
Apartment.connection.all_schemas.include? tenant
|
55
|
+
end
|
56
|
+
|
57
|
+
def rescue_from
|
58
|
+
ActiveRecord::JDBCError
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apartment/adapters/abstract_adapter'
|
4
|
+
|
5
|
+
module Apartment
|
6
|
+
# Helper module to decide wether to use mysql2 adapter or mysql2 adapter with schemas
|
7
|
+
module Tenant
|
8
|
+
def self.mysql2_adapter(config)
|
9
|
+
if Apartment.use_schemas
|
10
|
+
Adapters::Mysql2SchemaAdapter.new(config)
|
11
|
+
else
|
12
|
+
Adapters::Mysql2Adapter.new(config)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Adapters
|
18
|
+
# Mysql2 Adapter
|
19
|
+
class Mysql2Adapter < AbstractAdapter
|
20
|
+
def initialize(config)
|
21
|
+
super
|
22
|
+
|
23
|
+
@default_tenant = config[:database]
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def rescue_from
|
29
|
+
Mysql2::Error
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Mysql2 Schemas Adapter
|
34
|
+
class Mysql2SchemaAdapter < AbstractAdapter
|
35
|
+
def initialize(config)
|
36
|
+
super
|
37
|
+
|
38
|
+
@default_tenant = config[:database]
|
39
|
+
reset
|
40
|
+
end
|
41
|
+
|
42
|
+
# Reset current tenant to the default_tenant
|
43
|
+
#
|
44
|
+
def reset
|
45
|
+
return unless default_tenant
|
46
|
+
|
47
|
+
Apartment.connection.execute "use `#{default_tenant}`"
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
# Connect to new tenant
|
53
|
+
#
|
54
|
+
def connect_to_new(tenant)
|
55
|
+
return reset if tenant.nil?
|
56
|
+
|
57
|
+
Apartment.connection.execute "use `#{environmentify(tenant)}`"
|
58
|
+
rescue ActiveRecord::StatementInvalid => e
|
59
|
+
Apartment::Tenant.reset
|
60
|
+
raise_connect_error!(tenant, e)
|
61
|
+
end
|
62
|
+
|
63
|
+
def process_excluded_model(model)
|
64
|
+
model.constantize.tap do |klass|
|
65
|
+
# Ensure that if a schema *was* set, we override
|
66
|
+
table_name = klass.table_name.split('.', 2).last
|
67
|
+
|
68
|
+
klass.table_name = "#{default_tenant}.#{table_name}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def reset_on_connection_exception?
|
73
|
+
true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# handle postgis adapter as if it were postgresql,
|
4
|
+
# only override the adapter_method used for initialization
|
5
|
+
require 'apartment/adapters/postgresql_adapter'
|
6
|
+
|
7
|
+
module Apartment
|
8
|
+
module Tenant
|
9
|
+
def self.postgis_adapter(config)
|
10
|
+
postgresql_adapter(config)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|