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,35 @@
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 the host
8
+ # Assumes that tenant name should match host
9
+ # Strips/ignores first subdomains in ignored_first_subdomains
10
+ # eg. example.com => example.com
11
+ # www.example.bc.ca => www.example.bc.ca
12
+ # if ignored_first_subdomains = ['www']
13
+ # www.example.bc.ca => example.bc.ca
14
+ # www.a.b.c.d.com => a.b.c.d.com
15
+ #
16
+ class Host < Generic
17
+ def self.ignored_first_subdomains
18
+ @ignored_first_subdomains ||= []
19
+ end
20
+
21
+ # rubocop:disable Style/TrivialAccessors
22
+ def self.ignored_first_subdomains=(arg)
23
+ @ignored_first_subdomains = arg
24
+ end
25
+ # rubocop:enable Style/TrivialAccessors
26
+
27
+ def parse_tenant_name(request)
28
+ return nil if request.host.blank?
29
+
30
+ parts = request.host.split('.')
31
+ self.class.ignored_first_subdomains.include?(parts[0]) ? parts.drop(1).join('.') : request.host
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
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 hosts
8
+ # Uses a hash to find the corresponding tenant name for the host
9
+ #
10
+ class HostHash < Generic
11
+ def initialize(app, hash = {}, processor = nil)
12
+ super app, processor
13
+ @hash = hash
14
+ end
15
+
16
+ def parse_tenant_name(request)
17
+ unless @hash.key?(request.host)
18
+ raise TenantNotFound,
19
+ "Cannot find tenant for host #{request.host}"
20
+ end
21
+
22
+ @hash[request.host]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apartment/elevators/generic'
4
+ require 'public_suffix'
5
+
6
+ module Apartment
7
+ module Elevators
8
+ # Provides a rack based tenant switching solution based on subdomains
9
+ # Assumes that tenant name should match subdomain
10
+ #
11
+ class Subdomain < Generic
12
+ def self.excluded_subdomains
13
+ @excluded_subdomains ||= []
14
+ end
15
+
16
+ # rubocop:disable Style/TrivialAccessors
17
+ def self.excluded_subdomains=(arg)
18
+ @excluded_subdomains = arg
19
+ end
20
+ # rubocop:enable Style/TrivialAccessors
21
+
22
+ def parse_tenant_name(request)
23
+ request_subdomain = subdomain(request.host)
24
+
25
+ # If the domain acquired is set to be excluded, set the tenant to whatever is currently
26
+ # next in line in the schema search path.
27
+ tenant = if self.class.excluded_subdomains.include?(request_subdomain)
28
+ nil
29
+ else
30
+ request_subdomain
31
+ end
32
+
33
+ tenant.presence
34
+ end
35
+
36
+ protected
37
+
38
+ # *Almost* a direct ripoff of ActionDispatch::Request subdomain methods
39
+
40
+ # Only care about the first subdomain for the database name
41
+ def subdomain(host)
42
+ subdomains(host).first
43
+ end
44
+
45
+ def subdomains(host)
46
+ host_valid?(host) ? parse_host(host) : []
47
+ end
48
+
49
+ def host_valid?(host)
50
+ !ip_host?(host) && domain_valid?(host)
51
+ end
52
+
53
+ def ip_host?(host)
54
+ !/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.match(host).nil?
55
+ end
56
+
57
+ def domain_valid?(host)
58
+ PublicSuffix.valid?(host, ignore_private: true)
59
+ end
60
+
61
+ def parse_host(host)
62
+ (PublicSuffix.parse(host, ignore_private: true).trd || '').split('.')
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_record/log_subscriber'
4
+
5
+ module Apartment
6
+ # Custom Log subscriber to include database name and schema name in sql logs
7
+ class LogSubscriber < ActiveRecord::LogSubscriber
8
+ # NOTE: for some reason, if the method definition is not here, then the custom debug method is not called
9
+ # rubocop:disable Lint/UselessMethodDefinition
10
+ def sql(event)
11
+ super(event)
12
+ end
13
+ # rubocop:enable Lint/UselessMethodDefinition
14
+
15
+ private
16
+
17
+ def debug(progname = nil, &block)
18
+ progname = " #{apartment_log}#{progname}" unless progname.nil?
19
+
20
+ super(progname, &block)
21
+ end
22
+
23
+ def apartment_log
24
+ database = color("[#{database_name}] ", ActiveSupport::LogSubscriber::MAGENTA, true)
25
+ schema = current_search_path
26
+ schema = color("[#{schema.tr('"', '')}] ", ActiveSupport::LogSubscriber::YELLOW, true) unless schema.nil?
27
+ "#{database}#{schema}"
28
+ end
29
+
30
+ def current_search_path
31
+ if Apartment.connection.respond_to?(:schema_search_path)
32
+ Apartment.connection.schema_search_path
33
+ else
34
+ Apartment::Tenant.current # all others
35
+ end
36
+ end
37
+
38
+ def database_name
39
+ db_name = Apartment.connection.raw_connection.try(:db) # PostgreSQL, PostGIS
40
+ db_name ||= Apartment.connection.raw_connection.try(:query_options)&.dig(:database) # Mysql
41
+ db_name ||= Apartment.connection.current_database # Failover
42
+ db_name
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apartment/tenant'
4
+
5
+ module Apartment
6
+ module Migrator
7
+ extend self
8
+
9
+ # Migrate to latest
10
+ def migrate(database)
11
+ Tenant.switch(database) do
12
+ version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
13
+
14
+ migration_scope_block = ->(migration) { ENV['SCOPE'].blank? || (ENV['SCOPE'] == migration.scope) }
15
+
16
+ if activerecord_below_5_2?
17
+ ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, version, &migration_scope_block)
18
+ else
19
+ ActiveRecord::Base.connection.migration_context.migrate(version, &migration_scope_block)
20
+ end
21
+ end
22
+ end
23
+
24
+ # Migrate up/down to a specific version
25
+ def run(direction, database, version)
26
+ Tenant.switch(database) do
27
+ if activerecord_below_5_2?
28
+ ActiveRecord::Migrator.run(direction, ActiveRecord::Migrator.migrations_paths, version)
29
+ else
30
+ ActiveRecord::Base.connection.migration_context.run(direction, version)
31
+ end
32
+ end
33
+ end
34
+
35
+ # rollback latest migration `step` number of times
36
+ def rollback(database, step = 1)
37
+ Tenant.switch(database) do
38
+ if activerecord_below_5_2?
39
+ ActiveRecord::Migrator.rollback(ActiveRecord::Migrator.migrations_paths, step)
40
+ else
41
+ ActiveRecord::Base.connection.migration_context.rollback(step)
42
+ end
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def activerecord_below_5_2?
49
+ ActiveRecord.version.release < Gem::Version.new('5.2.0')
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apartment
4
+ module Model
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ # NOTE: key can either be an array of symbols or a single value.
9
+ # E.g. If we run the following query:
10
+ # `Setting.find_by(key: 'something', value: 'amazing')` key will have an array of symbols: `[:key, :something]`
11
+ # while if we run:
12
+ # `Setting.find(10)` key will have the value 'id'
13
+ def cached_find_by_statement(key, &block)
14
+ # Modifying the cache key to have a reference to the current tenant,
15
+ # so the cached statement is referring only to the tenant in which we've
16
+ # executed this
17
+ cache_key = if key.is_a? String
18
+ "#{Apartment::Tenant.current}_#{key}"
19
+ else
20
+ # NOTE: In Rails 6.0.4 we start receiving an ActiveRecord::Reflection::BelongsToReflection
21
+ # as the key, which wouldn't work well with an array.
22
+ [Apartment::Tenant.current] + Array.wrap(key)
23
+ end
24
+ cache = @find_by_statement_cache[connection.prepared_statements]
25
+ cache.compute_if_absent(cache_key) { ActiveRecord::StatementCache.create(connection, &block) }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+ require 'apartment/tenant'
5
+
6
+ module Apartment
7
+ class Railtie < Rails::Railtie
8
+ #
9
+ # Set up our default config options
10
+ # Do this before the app initializers run so we don't override custom settings
11
+ #
12
+ config.before_initialize do
13
+ Apartment.configure do |config|
14
+ config.excluded_models = []
15
+ config.use_schemas = true
16
+ config.tenant_names = []
17
+ config.seed_after_create = false
18
+ config.prepend_environment = false
19
+ config.append_environment = false
20
+ config.tenant_presence_check = true
21
+ config.active_record_log = false
22
+ end
23
+
24
+ ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
25
+ end
26
+
27
+ # Hook into ActionDispatch::Reloader to ensure Apartment is properly initialized
28
+ # Note that this doesn't entirely work as expected in Development,
29
+ # because this is called before classes are reloaded
30
+ # See the middleware/console declarations below to help with this. Hope to fix that soon.
31
+ #
32
+ config.to_prepare do
33
+ next if ARGV.any? { |arg| arg =~ /\Aassets:(?:precompile|clean)\z/ }
34
+ next if ARGV.any?('webpacker:compile')
35
+ next if ENV['APARTMENT_DISABLE_INIT']
36
+
37
+ begin
38
+ Apartment.connection_class.connection_pool.with_connection do
39
+ Apartment::Tenant.init
40
+ end
41
+ rescue ::ActiveRecord::NoDatabaseError
42
+ # Since `db:create` and other tasks invoke this block from Rails 5.2.0,
43
+ # we need to swallow the error to execute `db:create` properly.
44
+ end
45
+ end
46
+
47
+ config.after_initialize do
48
+ # NOTE: Load the custom log subscriber if enabled
49
+ if Apartment.active_record_log
50
+ ActiveSupport::Notifications.notifier.listeners_for('sql.active_record').each do |listener|
51
+ next unless listener.instance_variable_get('@delegate').is_a?(ActiveRecord::LogSubscriber)
52
+
53
+ ActiveSupport::Notifications.unsubscribe listener
54
+ end
55
+
56
+ Apartment::LogSubscriber.attach_to :active_record
57
+ end
58
+ end
59
+
60
+ #
61
+ # Ensure rake tasks are loaded
62
+ #
63
+ rake_tasks do
64
+ load 'tasks/apartment.rake'
65
+ require 'apartment/tasks/enhancements' if Apartment.db_migrate_tenants
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Require this file to append Apartment rake tasks to ActiveRecord db rake tasks
4
+ # Enabled by default in the initializer
5
+
6
+ module Apartment
7
+ class RakeTaskEnhancer
8
+ module TASKS
9
+ ENHANCE_BEFORE = %w[db:drop].freeze
10
+ ENHANCE_AFTER = %w[db:migrate db:rollback db:migrate:up db:migrate:down db:migrate:redo db:seed].freeze
11
+ freeze
12
+ end
13
+
14
+ # This is a bit convoluted, but helps solve problems when using Apartment within an engine
15
+ # See spec/integration/use_within_an_engine.rb
16
+
17
+ class << self
18
+ def enhance!
19
+ return unless should_enhance?
20
+
21
+ # insert task before
22
+ TASKS::ENHANCE_BEFORE.each do |name|
23
+ task = Rake::Task[name]
24
+ enhance_before_task(task)
25
+ end
26
+
27
+ # insert task after
28
+ TASKS::ENHANCE_AFTER.each do |name|
29
+ task = Rake::Task[name]
30
+ enhance_after_task(task)
31
+ end
32
+ end
33
+
34
+ def should_enhance?
35
+ Apartment.db_migrate_tenants
36
+ end
37
+
38
+ def enhance_before_task(task)
39
+ task.enhance([inserted_task_name(task)])
40
+ end
41
+
42
+ def enhance_after_task(task)
43
+ task.enhance do
44
+ Rake::Task[inserted_task_name(task)].invoke
45
+ end
46
+ end
47
+
48
+ def inserted_task_name(task)
49
+ task.name.sub(/db:/, 'apartment:')
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ Apartment::RakeTaskEnhancer.enhance!
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apartment
4
+ module TaskHelper
5
+ def self.each_tenant(&block)
6
+ Parallel.each(tenants_without_default, in_threads: Apartment.parallel_migration_threads) do |tenant|
7
+ block.call(tenant)
8
+ end
9
+ end
10
+
11
+ def self.tenants_without_default
12
+ tenants - [Apartment.default_tenant]
13
+ end
14
+
15
+ def self.tenants
16
+ ENV['DB'] ? ENV['DB'].split(',').map(&:strip) : Apartment.tenant_names || []
17
+ end
18
+
19
+ def self.warn_if_tenants_empty
20
+ return unless tenants.empty? && ENV['IGNORE_EMPTY_TENANTS'] != 'true'
21
+
22
+ puts <<-WARNING
23
+ [WARNING] - The list of tenants to migrate appears to be empty. This could mean a few things:
24
+
25
+ 1. You may not have created any, in which case you can ignore this message
26
+ 2. You've run `apartment:migrate` directly without loading the Rails environment
27
+ * `apartment:migrate` is now deprecated. Tenants will automatically be migrated with `db:migrate`
28
+
29
+ Note that your tenants currently haven't been migrated. You'll need to run `db:migrate` to rectify this.
30
+ WARNING
31
+ end
32
+
33
+ def self.create_tenant(tenant_name)
34
+ puts("Creating #{tenant_name} tenant")
35
+ Apartment::Tenant.create(tenant_name)
36
+ rescue Apartment::TenantExists => e
37
+ puts "Tried to create already existing tenant: #{e}"
38
+ end
39
+
40
+ def self.migrate_tenant(tenant_name)
41
+ strategy = Apartment.db_migrate_tenant_missing_strategy
42
+ create_tenant(tenant_name) if strategy == :create_tenant
43
+
44
+ puts("Migrating #{tenant_name} tenant")
45
+ Apartment::Migrator.migrate tenant_name
46
+ rescue Apartment::TenantNotFound => e
47
+ raise e if strategy == :raise_exception
48
+
49
+ puts e.message
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Apartment
6
+ # The main entry point to Apartment functions
7
+ #
8
+ module Tenant
9
+ extend self
10
+ extend Forwardable
11
+
12
+ def_delegators :adapter, :create, :drop, :switch, :switch!, :current, :each,
13
+ :reset, :init, :set_callback, :seed, :default_tenant, :environmentify
14
+
15
+ attr_writer :config
16
+
17
+ # Fetch the proper multi-tenant adapter based on Rails config
18
+ #
19
+ # @return {subclass of Apartment::AbstractAdapter}
20
+ #
21
+ def adapter
22
+ Thread.current[:apartment_adapter] ||= begin
23
+ adapter_method = "#{config[:adapter]}_adapter"
24
+
25
+ if defined?(JRUBY_VERSION)
26
+ case config[:adapter]
27
+ when /mysql/
28
+ adapter_method = 'jdbc_mysql_adapter'
29
+ when /postgresql/
30
+ adapter_method = 'jdbc_postgresql_adapter'
31
+ end
32
+ end
33
+
34
+ begin
35
+ require "apartment/adapters/#{adapter_method}"
36
+ rescue LoadError
37
+ raise "The adapter `#{adapter_method}` is not yet supported"
38
+ end
39
+
40
+ unless respond_to?(adapter_method)
41
+ raise AdapterNotFound, "database configuration specifies nonexistent #{config[:adapter]} adapter"
42
+ end
43
+
44
+ send(adapter_method, config)
45
+ end
46
+ end
47
+
48
+ # Reset config and adapter so they are regenerated
49
+ #
50
+ def reload!(config = nil)
51
+ Thread.current[:apartment_adapter] = nil
52
+ @config = config
53
+ end
54
+
55
+ private
56
+
57
+ # Fetch the rails database configuration
58
+ #
59
+ def config
60
+ @config ||= Apartment.connection_config
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apartment
4
+ VERSION = '2.12.0'
5
+ end
data/lib/apartment.rb ADDED
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'apartment/railtie' if defined?(Rails)
4
+ require 'active_support/core_ext/object/blank'
5
+ require 'forwardable'
6
+ require 'active_record'
7
+ require 'apartment/tenant'
8
+
9
+ require_relative 'apartment/log_subscriber'
10
+
11
+ if ActiveRecord.version.release >= Gem::Version.new('6.0')
12
+ require_relative 'apartment/active_record/connection_handling'
13
+ end
14
+
15
+ if ActiveRecord.version.release >= Gem::Version.new('6.1')
16
+ require_relative 'apartment/active_record/schema_migration'
17
+ require_relative 'apartment/active_record/internal_metadata'
18
+ end
19
+
20
+ # Apartment main definitions
21
+ module Apartment
22
+ class << self
23
+ extend Forwardable
24
+
25
+ ACCESSOR_METHODS = %i[use_schemas use_sql seed_after_create prepend_environment default_tenant
26
+ append_environment with_multi_server_setup tenant_presence_check active_record_log].freeze
27
+
28
+ WRITER_METHODS = %i[tenant_names database_schema_file excluded_models
29
+ persistent_schemas connection_class
30
+ db_migrate_tenants db_migrate_tenant_missing_strategy seed_data_file
31
+ parallel_migration_threads pg_excluded_names].freeze
32
+
33
+ attr_accessor(*ACCESSOR_METHODS)
34
+ attr_writer(*WRITER_METHODS)
35
+
36
+ if ActiveRecord.version.release >= Gem::Version.new('6.1')
37
+ def_delegators :connection_class, :connection, :connection_db_config, :establish_connection
38
+
39
+ def connection_config
40
+ connection_db_config.configuration_hash
41
+ end
42
+ else
43
+ def_delegators :connection_class, :connection, :connection_config, :establish_connection
44
+ end
45
+
46
+ # configure apartment with available options
47
+ def configure
48
+ yield self if block_given?
49
+ end
50
+
51
+ def tenant_names
52
+ extract_tenant_config.keys.map(&:to_s)
53
+ end
54
+
55
+ def tenants_with_config
56
+ extract_tenant_config
57
+ end
58
+
59
+ def tld_length=(_)
60
+ Apartment::Deprecation.warn('`config.tld_length` have no effect because it was removed in https://github.com/influitive/apartment/pull/309')
61
+ end
62
+
63
+ def db_config_for(tenant)
64
+ (tenants_with_config[tenant] || connection_config)
65
+ end
66
+
67
+ # Whether or not db:migrate should also migrate tenants
68
+ # defaults to true
69
+ def db_migrate_tenants
70
+ return @db_migrate_tenants if defined?(@db_migrate_tenants)
71
+
72
+ @db_migrate_tenants = true
73
+ end
74
+
75
+ # How to handle tenant missing on db:migrate
76
+ # defaults to :rescue_exception
77
+ # available options: rescue_exception, raise_exception, create_tenant
78
+ def db_migrate_tenant_missing_strategy
79
+ valid = %i[rescue_exception raise_exception create_tenant]
80
+ value = @db_migrate_tenant_missing_strategy || :rescue_exception
81
+
82
+ return value if valid.include?(value)
83
+
84
+ key_name = 'config.db_migrate_tenant_missing_strategy'
85
+ opt_names = valid.join(', ')
86
+
87
+ raise ApartmentError, "Option #{value} not valid for `#{key_name}`. Use one of #{opt_names}"
88
+ end
89
+
90
+ # Default to empty array
91
+ def excluded_models
92
+ @excluded_models || []
93
+ end
94
+
95
+ def parallel_migration_threads
96
+ @parallel_migration_threads || 0
97
+ end
98
+
99
+ def persistent_schemas
100
+ @persistent_schemas || []
101
+ end
102
+
103
+ def connection_class
104
+ @connection_class || ActiveRecord::Base
105
+ end
106
+
107
+ def database_schema_file
108
+ return @database_schema_file if defined?(@database_schema_file)
109
+
110
+ @database_schema_file = Rails.root.join('db/schema.rb')
111
+ end
112
+
113
+ def seed_data_file
114
+ return @seed_data_file if defined?(@seed_data_file)
115
+
116
+ @seed_data_file = Rails.root.join('db/seeds.rb')
117
+ end
118
+
119
+ def pg_excluded_names
120
+ @pg_excluded_names || []
121
+ end
122
+
123
+ # Reset all the config for Apartment
124
+ def reset
125
+ (ACCESSOR_METHODS + WRITER_METHODS).each do |method|
126
+ remove_instance_variable(:"@#{method}") if instance_variable_defined?(:"@#{method}")
127
+ end
128
+ end
129
+
130
+ def extract_tenant_config
131
+ return {} unless @tenant_names
132
+
133
+ values = @tenant_names.respond_to?(:call) ? @tenant_names.call : @tenant_names
134
+ unless values.is_a? Hash
135
+ values = values.each_with_object({}) do |tenant, hash|
136
+ hash[tenant] = connection_config
137
+ end
138
+ end
139
+ values.with_indifferent_access
140
+ rescue ActiveRecord::StatementInvalid
141
+ {}
142
+ end
143
+ end
144
+
145
+ # Exceptions
146
+ ApartmentError = Class.new(StandardError)
147
+
148
+ # Raised when apartment cannot find the adapter specified in <tt>config/database.yml</tt>
149
+ AdapterNotFound = Class.new(ApartmentError)
150
+
151
+ # Raised when apartment cannot find the file to be loaded
152
+ FileNotFound = Class.new(ApartmentError)
153
+
154
+ # Tenant specified is unknown
155
+ TenantNotFound = Class.new(ApartmentError)
156
+
157
+ # The Tenant attempting to be created already exists
158
+ TenantExists = Class.new(ApartmentError)
159
+ end
@@ -0,0 +1,5 @@
1
+ Description:
2
+ Creates an initializer for apartment.
3
+
4
+ Example:
5
+ `rails generate apartment:install`
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apartment
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('templates', __dir__)
6
+
7
+ def copy_files
8
+ template 'apartment.rb', File.join('config', 'initializers', 'apartment.rb')
9
+ end
10
+ end
11
+ end