puzzle-apartment 2.12.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|