puzzle-apartment 2.12.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/.circleci/config.yml +71 -0
- data/.github/ISSUE_TEMPLATE.md +21 -0
- data/.github/workflows/changelog.yml +63 -0
- data/.github/workflows/reviewdog.yml +22 -0
- data/.gitignore +15 -0
- data/.pryrc +5 -0
- data/.rspec +4 -0
- data/.rubocop.yml +33 -0
- data/.rubocop_todo.yml +418 -0
- data/.ruby-version +1 -0
- data/.story_branch.yml +5 -0
- data/Appraisals +49 -0
- data/CHANGELOG.md +963 -0
- data/Gemfile +17 -0
- data/Guardfile +11 -0
- data/HISTORY.md +496 -0
- data/README.md +652 -0
- data/Rakefile +157 -0
- data/TODO.md +50 -0
- data/docker-compose.yml +33 -0
- data/gemfiles/rails_6_1.gemfile +17 -0
- data/gemfiles/rails_7_0.gemfile +17 -0
- data/gemfiles/rails_7_1.gemfile +17 -0
- data/gemfiles/rails_master.gemfile +17 -0
- data/lib/apartment/active_record/connection_handling.rb +34 -0
- data/lib/apartment/active_record/internal_metadata.rb +9 -0
- data/lib/apartment/active_record/postgresql_adapter.rb +39 -0
- data/lib/apartment/active_record/schema_migration.rb +13 -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 +284 -0
- data/lib/apartment/adapters/sqlite3_adapter.rb +66 -0
- data/lib/apartment/console.rb +24 -0
- data/lib/apartment/custom_console.rb +42 -0
- data/lib/apartment/deprecation.rb +11 -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 +52 -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 +52 -0
- data/lib/apartment/tenant.rb +63 -0
- data/lib/apartment/version.rb +5 -0
- data/lib/apartment.rb +159 -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/puzzle-apartment.gemspec +59 -0
- metadata +385 -0
@@ -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
|
@@ -0,0 +1,284 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apartment/adapters/abstract_adapter'
|
4
|
+
require 'apartment/active_record/postgresql_adapter'
|
5
|
+
|
6
|
+
module Apartment
|
7
|
+
module Tenant
|
8
|
+
def self.postgresql_adapter(config)
|
9
|
+
adapter = Adapters::PostgresqlAdapter
|
10
|
+
adapter = Adapters::PostgresqlSchemaAdapter if Apartment.use_schemas
|
11
|
+
adapter = Adapters::PostgresqlSchemaFromSqlAdapter if Apartment.use_sql && Apartment.use_schemas
|
12
|
+
adapter.new(config)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module Adapters
|
17
|
+
# Default adapter when not using Postgresql Schemas
|
18
|
+
class PostgresqlAdapter < AbstractAdapter
|
19
|
+
private
|
20
|
+
|
21
|
+
def rescue_from
|
22
|
+
PG::Error
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Separate Adapter for Postgresql when using schemas
|
27
|
+
class PostgresqlSchemaAdapter < AbstractAdapter
|
28
|
+
def initialize(config)
|
29
|
+
super
|
30
|
+
|
31
|
+
reset
|
32
|
+
end
|
33
|
+
|
34
|
+
def default_tenant
|
35
|
+
@default_tenant = Apartment.default_tenant || 'public'
|
36
|
+
end
|
37
|
+
|
38
|
+
# Reset schema search path to the default schema_search_path
|
39
|
+
#
|
40
|
+
# @return {String} default schema search path
|
41
|
+
#
|
42
|
+
def reset
|
43
|
+
@current = default_tenant
|
44
|
+
Apartment.connection.schema_search_path = full_search_path
|
45
|
+
end
|
46
|
+
|
47
|
+
def init
|
48
|
+
super
|
49
|
+
Apartment.connection.schema_search_path = full_search_path
|
50
|
+
end
|
51
|
+
|
52
|
+
def current
|
53
|
+
@current || default_tenant
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
def process_excluded_model(excluded_model)
|
59
|
+
excluded_model.constantize.tap do |klass|
|
60
|
+
# Ensure that if a schema *was* set, we override
|
61
|
+
table_name = klass.table_name.split('.', 2).last
|
62
|
+
|
63
|
+
klass.table_name = "#{default_tenant}.#{table_name}"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def drop_command(conn, tenant)
|
68
|
+
conn.execute(%(DROP SCHEMA "#{tenant}" CASCADE))
|
69
|
+
end
|
70
|
+
|
71
|
+
# Set schema search path to new schema
|
72
|
+
#
|
73
|
+
def connect_to_new(tenant = nil)
|
74
|
+
return reset if tenant.nil?
|
75
|
+
raise ActiveRecord::StatementInvalid, "Could not find schema #{tenant}" unless schema_exists?(tenant)
|
76
|
+
|
77
|
+
@current = tenant.is_a?(Array) ? tenant.map(&:to_s) : tenant.to_s
|
78
|
+
Apartment.connection.schema_search_path = full_search_path
|
79
|
+
|
80
|
+
# When the PostgreSQL version is < 9.3,
|
81
|
+
# there is a issue for prepared statement with changing search_path.
|
82
|
+
# https://www.postgresql.org/docs/9.3/static/sql-prepare.html
|
83
|
+
Apartment.connection.clear_cache! if postgresql_version < 90_300
|
84
|
+
rescue *rescuable_exceptions => e
|
85
|
+
raise_schema_connect_to_new(tenant, e)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def tenant_exists?(tenant)
|
91
|
+
return true unless Apartment.tenant_presence_check
|
92
|
+
|
93
|
+
Apartment.connection.schema_exists?(tenant)
|
94
|
+
end
|
95
|
+
|
96
|
+
def create_tenant_command(conn, tenant)
|
97
|
+
# NOTE: This was causing some tests to fail because of the database strategy for rspec
|
98
|
+
if ActiveRecord::Base.connection.open_transactions.positive?
|
99
|
+
conn.execute(%(CREATE SCHEMA "#{tenant}"))
|
100
|
+
else
|
101
|
+
schema = %(BEGIN;
|
102
|
+
CREATE SCHEMA "#{tenant}";
|
103
|
+
COMMIT;)
|
104
|
+
|
105
|
+
conn.execute(schema)
|
106
|
+
end
|
107
|
+
rescue *rescuable_exceptions => e
|
108
|
+
rollback_transaction(conn)
|
109
|
+
raise e
|
110
|
+
end
|
111
|
+
|
112
|
+
def rollback_transaction(conn)
|
113
|
+
conn.execute('ROLLBACK;')
|
114
|
+
end
|
115
|
+
|
116
|
+
# Generate the final search path to set including persistent_schemas
|
117
|
+
#
|
118
|
+
def full_search_path
|
119
|
+
persistent_schemas.map(&:inspect).join(', ')
|
120
|
+
end
|
121
|
+
|
122
|
+
def persistent_schemas
|
123
|
+
[@current, Apartment.persistent_schemas].flatten
|
124
|
+
end
|
125
|
+
|
126
|
+
def postgresql_version
|
127
|
+
# ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#postgresql_version is
|
128
|
+
# public from Rails 5.0.
|
129
|
+
Apartment.connection.send(:postgresql_version)
|
130
|
+
end
|
131
|
+
|
132
|
+
def schema_exists?(schemas)
|
133
|
+
return true unless Apartment.tenant_presence_check
|
134
|
+
|
135
|
+
Array(schemas).all? { |schema| Apartment.connection.schema_exists?(schema.to_s) }
|
136
|
+
end
|
137
|
+
|
138
|
+
def raise_schema_connect_to_new(tenant, exception)
|
139
|
+
raise TenantNotFound, <<~EXCEPTION_MESSAGE
|
140
|
+
Could not set search path to schemas, they may be invalid: "#{tenant}" #{full_search_path}.
|
141
|
+
Original error: #{exception.class}: #{exception}
|
142
|
+
EXCEPTION_MESSAGE
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Another Adapter for Postgresql when using schemas and SQL
|
147
|
+
class PostgresqlSchemaFromSqlAdapter < PostgresqlSchemaAdapter
|
148
|
+
PSQL_DUMP_BLACKLISTED_STATEMENTS = [
|
149
|
+
/SET search_path/i, # overridden later
|
150
|
+
/SET lock_timeout/i, # new in postgresql 9.3
|
151
|
+
/SET row_security/i, # new in postgresql 9.5
|
152
|
+
/SET idle_in_transaction_session_timeout/i, # new in postgresql 9.6
|
153
|
+
/SET default_table_access_method/i, # new in postgresql 12
|
154
|
+
/CREATE SCHEMA public/i,
|
155
|
+
/COMMENT ON SCHEMA public/i
|
156
|
+
|
157
|
+
].freeze
|
158
|
+
|
159
|
+
def import_database_schema
|
160
|
+
preserving_search_path do
|
161
|
+
clone_pg_schema
|
162
|
+
copy_schema_migrations
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
# Re-set search path after the schema is imported.
|
169
|
+
# Postgres now sets search path to empty before dumping the schema
|
170
|
+
# and it mut be reset
|
171
|
+
#
|
172
|
+
def preserving_search_path
|
173
|
+
search_path = Apartment.connection.execute('show search_path').first['search_path']
|
174
|
+
yield
|
175
|
+
Apartment.connection.execute("set search_path = #{search_path}")
|
176
|
+
end
|
177
|
+
|
178
|
+
# Clone default schema into new schema named after current tenant
|
179
|
+
#
|
180
|
+
def clone_pg_schema
|
181
|
+
pg_schema_sql = patch_search_path(pg_dump_schema)
|
182
|
+
Apartment.connection.execute(pg_schema_sql)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Copy data from schema_migrations into new schema
|
186
|
+
#
|
187
|
+
def copy_schema_migrations
|
188
|
+
pg_migrations_data = patch_search_path(pg_dump_schema_migrations_data)
|
189
|
+
Apartment.connection.execute(pg_migrations_data)
|
190
|
+
end
|
191
|
+
|
192
|
+
# Dump postgres default schema
|
193
|
+
#
|
194
|
+
# @return {String} raw SQL contaning only postgres schema dump
|
195
|
+
#
|
196
|
+
def pg_dump_schema
|
197
|
+
# Skip excluded tables? :/
|
198
|
+
# excluded_tables =
|
199
|
+
# collect_table_names(Apartment.excluded_models)
|
200
|
+
# .map! {|t| "-T #{t}"}
|
201
|
+
# .join(' ')
|
202
|
+
|
203
|
+
# `pg_dump -s -x -O -n #{default_tenant} #{excluded_tables} #{dbname}`
|
204
|
+
|
205
|
+
with_pg_env { `pg_dump -s -x -O -n #{default_tenant} #{dbname}` }
|
206
|
+
end
|
207
|
+
|
208
|
+
# Dump data from schema_migrations table
|
209
|
+
#
|
210
|
+
# @return {String} raw SQL contaning inserts with data from schema_migrations
|
211
|
+
#
|
212
|
+
# rubocop:disable Layout/LineLength
|
213
|
+
def pg_dump_schema_migrations_data
|
214
|
+
with_pg_env { `pg_dump -a --inserts -t #{default_tenant}.schema_migrations -t #{default_tenant}.ar_internal_metadata #{dbname}` }
|
215
|
+
end
|
216
|
+
# rubocop:enable Layout/LineLength
|
217
|
+
|
218
|
+
# Temporary set Postgresql related environment variables if there are in @config
|
219
|
+
#
|
220
|
+
def with_pg_env
|
221
|
+
pghost = ENV['PGHOST']
|
222
|
+
pgport = ENV['PGPORT']
|
223
|
+
pguser = ENV['PGUSER']
|
224
|
+
pgpassword = ENV['PGPASSWORD']
|
225
|
+
|
226
|
+
ENV['PGHOST'] = @config[:host] if @config[:host]
|
227
|
+
ENV['PGPORT'] = @config[:port].to_s if @config[:port]
|
228
|
+
ENV['PGUSER'] = @config[:username].to_s if @config[:username]
|
229
|
+
ENV['PGPASSWORD'] = @config[:password].to_s if @config[:password]
|
230
|
+
|
231
|
+
yield
|
232
|
+
ensure
|
233
|
+
ENV['PGHOST'] = pghost
|
234
|
+
ENV['PGPORT'] = pgport
|
235
|
+
ENV['PGUSER'] = pguser
|
236
|
+
ENV['PGPASSWORD'] = pgpassword
|
237
|
+
end
|
238
|
+
|
239
|
+
# Remove "SET search_path ..." line from SQL dump and prepend search_path set to current tenant
|
240
|
+
#
|
241
|
+
# @return {String} patched raw SQL dump
|
242
|
+
#
|
243
|
+
def patch_search_path(sql)
|
244
|
+
search_path = "SET search_path = \"#{current}\", #{default_tenant};"
|
245
|
+
|
246
|
+
swap_schema_qualifier(sql)
|
247
|
+
.split("\n")
|
248
|
+
.select { |line| check_input_against_regexps(line, PSQL_DUMP_BLACKLISTED_STATEMENTS).empty? }
|
249
|
+
.prepend(search_path)
|
250
|
+
.join("\n")
|
251
|
+
end
|
252
|
+
|
253
|
+
def swap_schema_qualifier(sql)
|
254
|
+
sql.gsub(/#{default_tenant}\.\w*/) do |match|
|
255
|
+
if Apartment.pg_excluded_names.any? { |name| match.include? name }
|
256
|
+
match
|
257
|
+
else
|
258
|
+
match.gsub("#{default_tenant}.", %("#{current}".))
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Checks if any of regexps matches against input
|
264
|
+
#
|
265
|
+
def check_input_against_regexps(input, regexps)
|
266
|
+
regexps.select { |c| input.match c }
|
267
|
+
end
|
268
|
+
|
269
|
+
# Collect table names from AR Models
|
270
|
+
#
|
271
|
+
def collect_table_names(models)
|
272
|
+
models.map do |m|
|
273
|
+
m.constantize.table_name
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Convenience method for current database name
|
278
|
+
#
|
279
|
+
def dbname
|
280
|
+
Apartment.connection_config[:database]
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apartment/adapters/abstract_adapter'
|
4
|
+
|
5
|
+
module Apartment
|
6
|
+
module Tenant
|
7
|
+
def self.sqlite3_adapter(config)
|
8
|
+
Adapters::Sqlite3Adapter.new(config)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Adapters
|
13
|
+
class Sqlite3Adapter < AbstractAdapter
|
14
|
+
def initialize(config)
|
15
|
+
@default_dir = File.expand_path(File.dirname(config[:database]))
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def drop(tenant)
|
21
|
+
unless File.exist?(database_file(tenant))
|
22
|
+
raise TenantNotFound,
|
23
|
+
"The tenant #{environmentify(tenant)} cannot be found."
|
24
|
+
end
|
25
|
+
|
26
|
+
File.delete(database_file(tenant))
|
27
|
+
end
|
28
|
+
|
29
|
+
def current
|
30
|
+
File.basename(Apartment.connection.instance_variable_get(:@config)[:database], '.sqlite3')
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def connect_to_new(tenant)
|
36
|
+
return reset if tenant.nil?
|
37
|
+
|
38
|
+
unless File.exist?(database_file(tenant))
|
39
|
+
raise TenantNotFound,
|
40
|
+
"The tenant #{environmentify(tenant)} cannot be found."
|
41
|
+
end
|
42
|
+
|
43
|
+
super database_file(tenant)
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_tenant(tenant)
|
47
|
+
if File.exist?(database_file(tenant))
|
48
|
+
raise TenantExists,
|
49
|
+
"The tenant #{environmentify(tenant)} already exists."
|
50
|
+
end
|
51
|
+
|
52
|
+
begin
|
53
|
+
f = File.new(database_file(tenant), File::CREAT)
|
54
|
+
ensure
|
55
|
+
f.close
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def database_file(tenant)
|
62
|
+
"#{@default_dir}/#{environmentify(tenant)}.sqlite3"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
def st(schema_name = nil)
|
4
|
+
if schema_name.nil?
|
5
|
+
tenant_list.each { |t| puts t }
|
6
|
+
|
7
|
+
elsif tenant_list.include? schema_name
|
8
|
+
Apartment::Tenant.switch!(schema_name)
|
9
|
+
else
|
10
|
+
puts "Tenant #{schema_name} is not part of the tenant list"
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def tenant_list
|
16
|
+
tenant_list = [Apartment.default_tenant]
|
17
|
+
tenant_list += Apartment.tenant_names
|
18
|
+
tenant_list.uniq
|
19
|
+
end
|
20
|
+
|
21
|
+
def tenant_info_msg
|
22
|
+
puts "Available Tenants: #{tenant_list}\n"
|
23
|
+
puts "Use `st 'tenant'` to switch tenants & `tenant_list` to see list\n"
|
24
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'console'
|
4
|
+
|
5
|
+
module Apartment
|
6
|
+
module CustomConsole
|
7
|
+
begin
|
8
|
+
require 'pry-rails'
|
9
|
+
rescue LoadError
|
10
|
+
# rubocop:disable Layout/LineLength
|
11
|
+
puts '[Failed to load pry-rails] If you want to use Apartment custom prompt you need to add pry-rails to your gemfile'
|
12
|
+
# rubocop:enable Layout/LineLength
|
13
|
+
end
|
14
|
+
|
15
|
+
desc = "Includes the current Rails environment and project folder name.\n" \
|
16
|
+
'[1] [project_name][Rails.env][Apartment::Tenant.current] pry(main)>'
|
17
|
+
|
18
|
+
prompt_procs = [
|
19
|
+
proc { |target_self, nest_level, pry| prompt_contents(pry, target_self, nest_level, '>') },
|
20
|
+
proc { |target_self, nest_level, pry| prompt_contents(pry, target_self, nest_level, '*') }
|
21
|
+
]
|
22
|
+
|
23
|
+
if Gem::Version.new(Pry::VERSION) >= Gem::Version.new('0.13')
|
24
|
+
Pry.config.prompt = Pry::Prompt.new 'ros', desc, prompt_procs
|
25
|
+
else
|
26
|
+
Pry::Prompt.add 'ros', desc, %w[> *] do |target_self, nest_level, pry, sep|
|
27
|
+
prompt_contents(pry, target_self, nest_level, sep)
|
28
|
+
end
|
29
|
+
Pry.config.prompt = Pry::Prompt[:ros][:value]
|
30
|
+
end
|
31
|
+
|
32
|
+
Pry.config.hooks.add_hook(:when_started, 'startup message') do
|
33
|
+
tenant_info_msg
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.prompt_contents(pry, target_self, nest_level, sep)
|
37
|
+
"[#{pry.input_ring.size}] [#{PryRails::Prompt.formatted_env}][#{Apartment::Tenant.current}] " \
|
38
|
+
"#{pry.config.prompt_name}(#{Pry.view_clip(target_self)})" \
|
39
|
+
"#{":#{nest_level}" unless nest_level.zero?}#{sep} "
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apartment/elevators/generic'
|
4
|
+
|
5
|
+
module Apartment
|
6
|
+
module Elevators
|
7
|
+
# Provides a rack based tenant switching solution based on domain
|
8
|
+
# Assumes that tenant name should match domain
|
9
|
+
# Parses request host for second level domain, ignoring www
|
10
|
+
# eg. example.com => example
|
11
|
+
# www.example.bc.ca => example
|
12
|
+
# a.example.bc.ca => a
|
13
|
+
#
|
14
|
+
#
|
15
|
+
class Domain < Generic
|
16
|
+
def parse_tenant_name(request)
|
17
|
+
return nil if request.host.blank?
|
18
|
+
|
19
|
+
request.host.match(/(www\.)?(?<sld>[^.]*)/)['sld']
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'apartment/elevators/subdomain'
|
4
|
+
|
5
|
+
module Apartment
|
6
|
+
module Elevators
|
7
|
+
# Provides a rack based tenant switching solution based on the first subdomain
|
8
|
+
# of a given domain name.
|
9
|
+
# eg:
|
10
|
+
# - example1.domain.com => example1
|
11
|
+
# - example2.something.domain.com => example2
|
12
|
+
class FirstSubdomain < Subdomain
|
13
|
+
def parse_tenant_name(request)
|
14
|
+
super.split('.')[0] unless super.nil?
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack/request'
|
4
|
+
require 'apartment/tenant'
|
5
|
+
|
6
|
+
module Apartment
|
7
|
+
module Elevators
|
8
|
+
# Provides a rack based tenant switching solution based on request
|
9
|
+
#
|
10
|
+
class Generic
|
11
|
+
def initialize(app, processor = nil)
|
12
|
+
@app = app
|
13
|
+
@processor = processor || method(:parse_tenant_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
request = Rack::Request.new(env)
|
18
|
+
|
19
|
+
database = @processor.call(request)
|
20
|
+
|
21
|
+
if database
|
22
|
+
Apartment::Tenant.switch(database) { @app.call(env) }
|
23
|
+
else
|
24
|
+
@app.call(env)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse_tenant_name(_request)
|
29
|
+
raise 'Override'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|