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