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.
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