puzzle-apartment 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +71 -0
- data/.github/ISSUE_TEMPLATE.md +21 -0
- data/.github/workflows/changelog.yml +63 -0
- data/.github/workflows/reviewdog.yml +22 -0
- data/.gitignore +15 -0
- data/.pryrc +5 -0
- data/.rspec +4 -0
- data/.rubocop.yml +33 -0
- data/.rubocop_todo.yml +418 -0
- data/.ruby-version +1 -0
- data/.story_branch.yml +5 -0
- data/Appraisals +49 -0
- data/CHANGELOG.md +963 -0
- data/Gemfile +17 -0
- data/Guardfile +11 -0
- data/HISTORY.md +496 -0
- data/README.md +652 -0
- data/Rakefile +157 -0
- data/TODO.md +50 -0
- data/docker-compose.yml +33 -0
- data/gemfiles/rails_6_1.gemfile +17 -0
- data/gemfiles/rails_7_0.gemfile +17 -0
- data/gemfiles/rails_7_1.gemfile +17 -0
- data/gemfiles/rails_master.gemfile +17 -0
- data/lib/apartment/active_record/connection_handling.rb +34 -0
- data/lib/apartment/active_record/internal_metadata.rb +9 -0
- data/lib/apartment/active_record/postgresql_adapter.rb +39 -0
- data/lib/apartment/active_record/schema_migration.rb +13 -0
- data/lib/apartment/adapters/abstract_adapter.rb +275 -0
- data/lib/apartment/adapters/abstract_jdbc_adapter.rb +20 -0
- data/lib/apartment/adapters/jdbc_mysql_adapter.rb +19 -0
- data/lib/apartment/adapters/jdbc_postgresql_adapter.rb +62 -0
- data/lib/apartment/adapters/mysql2_adapter.rb +77 -0
- data/lib/apartment/adapters/postgis_adapter.rb +13 -0
- data/lib/apartment/adapters/postgresql_adapter.rb +284 -0
- data/lib/apartment/adapters/sqlite3_adapter.rb +66 -0
- data/lib/apartment/console.rb +24 -0
- data/lib/apartment/custom_console.rb +42 -0
- data/lib/apartment/deprecation.rb +11 -0
- data/lib/apartment/elevators/domain.rb +23 -0
- data/lib/apartment/elevators/first_subdomain.rb +18 -0
- data/lib/apartment/elevators/generic.rb +33 -0
- data/lib/apartment/elevators/host.rb +35 -0
- data/lib/apartment/elevators/host_hash.rb +26 -0
- data/lib/apartment/elevators/subdomain.rb +66 -0
- data/lib/apartment/log_subscriber.rb +45 -0
- data/lib/apartment/migrator.rb +52 -0
- data/lib/apartment/model.rb +29 -0
- data/lib/apartment/railtie.rb +68 -0
- data/lib/apartment/tasks/enhancements.rb +55 -0
- data/lib/apartment/tasks/task_helper.rb +52 -0
- data/lib/apartment/tenant.rb +63 -0
- data/lib/apartment/version.rb +5 -0
- data/lib/apartment.rb +159 -0
- data/lib/generators/apartment/install/USAGE +5 -0
- data/lib/generators/apartment/install/install_generator.rb +11 -0
- data/lib/generators/apartment/install/templates/apartment.rb +116 -0
- data/lib/tasks/apartment.rake +106 -0
- data/puzzle-apartment.gemspec +59 -0
- metadata +385 -0
@@ -0,0 +1,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
|
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,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
|