puzzle-apartment 2.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +71 -0
  3. data/.github/ISSUE_TEMPLATE.md +21 -0
  4. data/.github/workflows/changelog.yml +63 -0
  5. data/.github/workflows/reviewdog.yml +22 -0
  6. data/.gitignore +15 -0
  7. data/.pryrc +5 -0
  8. data/.rspec +4 -0
  9. data/.rubocop.yml +33 -0
  10. data/.rubocop_todo.yml +418 -0
  11. data/.ruby-version +1 -0
  12. data/.story_branch.yml +5 -0
  13. data/Appraisals +49 -0
  14. data/CHANGELOG.md +963 -0
  15. data/Gemfile +17 -0
  16. data/Guardfile +11 -0
  17. data/HISTORY.md +496 -0
  18. data/README.md +652 -0
  19. data/Rakefile +157 -0
  20. data/TODO.md +50 -0
  21. data/docker-compose.yml +33 -0
  22. data/gemfiles/rails_6_1.gemfile +17 -0
  23. data/gemfiles/rails_7_0.gemfile +17 -0
  24. data/gemfiles/rails_7_1.gemfile +17 -0
  25. data/gemfiles/rails_master.gemfile +17 -0
  26. data/lib/apartment/active_record/connection_handling.rb +34 -0
  27. data/lib/apartment/active_record/internal_metadata.rb +9 -0
  28. data/lib/apartment/active_record/postgresql_adapter.rb +39 -0
  29. data/lib/apartment/active_record/schema_migration.rb +13 -0
  30. data/lib/apartment/adapters/abstract_adapter.rb +275 -0
  31. data/lib/apartment/adapters/abstract_jdbc_adapter.rb +20 -0
  32. data/lib/apartment/adapters/jdbc_mysql_adapter.rb +19 -0
  33. data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +62 -0
  34. data/lib/apartment/adapters/mysql2_adapter.rb +77 -0
  35. data/lib/apartment/adapters/postgis_adapter.rb +13 -0
  36. data/lib/apartment/adapters/postgresql_adapter.rb +284 -0
  37. data/lib/apartment/adapters/sqlite3_adapter.rb +66 -0
  38. data/lib/apartment/console.rb +24 -0
  39. data/lib/apartment/custom_console.rb +42 -0
  40. data/lib/apartment/deprecation.rb +11 -0
  41. data/lib/apartment/elevators/domain.rb +23 -0
  42. data/lib/apartment/elevators/first_subdomain.rb +18 -0
  43. data/lib/apartment/elevators/generic.rb +33 -0
  44. data/lib/apartment/elevators/host.rb +35 -0
  45. data/lib/apartment/elevators/host_hash.rb +26 -0
  46. data/lib/apartment/elevators/subdomain.rb +66 -0
  47. data/lib/apartment/log_subscriber.rb +45 -0
  48. data/lib/apartment/migrator.rb +52 -0
  49. data/lib/apartment/model.rb +29 -0
  50. data/lib/apartment/railtie.rb +68 -0
  51. data/lib/apartment/tasks/enhancements.rb +55 -0
  52. data/lib/apartment/tasks/task_helper.rb +52 -0
  53. data/lib/apartment/tenant.rb +63 -0
  54. data/lib/apartment/version.rb +5 -0
  55. data/lib/apartment.rb +159 -0
  56. data/lib/generators/apartment/install/USAGE +5 -0
  57. data/lib/generators/apartment/install/install_generator.rb +11 -0
  58. data/lib/generators/apartment/install/templates/apartment.rb +116 -0
  59. data/lib/tasks/apartment.rake +106 -0
  60. data/puzzle-apartment.gemspec +59 -0
  61. 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/deprecation'
4
+
5
+ module Apartment
6
+ module Deprecation
7
+ def self.warn(message)
8
+ ActiveSupport::Deprecation.warn message
9
+ end
10
+ end
11
+ 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