Files
nairobi/app/models/tenant.rb
Javi Martín 118c2bf5e0 Move custom ActiveStorage service to $LOAD_PATH
We moved this file to `app/lib/` in commit cb477149c so it would be in
the autoload_paths. However, this class is loaded by ActiveStorage, with
the following method:

```
def resolve(class_name)
  require "active_storage/service/#{class_name.to_s.underscore}_service"
  ActiveStorage::Service.const_get(:"#{class_name.camelize}Service")
rescue LoadError
  raise "Missing service adapter for #{class_name.inspect}"
end
``

So this file needs to be in the $LOAD_PATH, or else ActiveStorage won't
be able to load it when we disable the `add_autoload_paths_to_load_path`
option, which is the default in Rails 7.1 [1].

Moving it to the `lib` folder solves the issue; as mentioned in the
guide to upgrade to Rails 7.1 [2]:

> The lib directory is not affected by this flag, it is added to
> $LOAD_PATH always.

However, we were also referencing this class in the `Tenant` model,
meaning we needed to autoload it as well somehow. So, instead of
directly referencing this class, we're using `respond_to?` in the Tenant
model.

We're changing the test so it fails when the code calls
`is_a?(ActiveStorage::Service::TenantDiskService)`. We need to change
the active storage configurations in the test because, otherwise, the
moment `ActiveStorage::Blob` is loaded, the `TenantDiskService` class is
also loaded, meaning the test will pass when using `is_a?`.

Note that, since this class isn't in the autoload paths anymore, we need
to add a `require` in the tests. We could add an initializer to require
it; we're not doing it in order to be consistent with what ActiveStorage
does: it only loads the service that's going to be used in the current
Rails environment. If somebody changed their production environment in
order to use (for example), S3, and we added an initializer to require
the TenantDiskService, we would still load the TenantDiskService even if
it isn't going to be used.

[1] https://guides.rubyonrails.org/v7.1/configuring.html#config-add-autoload-paths-to-load-path
[2] https://guides.rubyonrails.org/v7.1/upgrading_ruby_on_rails.html#autoloaded-paths-are-no-longer-in-$load-path
2024-04-17 15:18:41 +02:00

201 lines
4.2 KiB
Ruby

class Tenant < ApplicationRecord
enum schema_type: %w[subdomain domain]
validates :schema,
presence: true,
uniqueness: true,
exclusion: { in: ->(*) { excluded_subdomains }},
format: { with: URI::DEFAULT_PARSER.regexp[:HOST] }
validates :name, presence: true, uniqueness: true
after_create :create_schema
after_update :rename_schema
after_destroy :destroy_schema
scope :only_hidden, -> { where.not(hidden_at: nil) }
def self.find_by_domain(host)
domain.find_by(schema: host)
end
def self.resolve_host(host)
return nil if Rails.application.config.multitenancy.blank?
return nil if host.blank? || host.match?(Resolv::AddressRegex)
schema = schema_for(host)
if schema && only_hidden.find_by(schema: schema)
raise Apartment::TenantNotFound
else
schema
end
end
def self.schema_for(host)
host_without_www = host.delete_prefix("www.")
if find_by_domain(host)
host
elsif find_by_domain(host_without_www)
host_without_www
else
host_domain = allowed_domains.find { |domain| host == domain || host.ends_with?(".#{domain}") }
schema = host_without_www.sub(/\.?#{host_domain}\Z/, "").presence
if find_by_domain(schema)
raise Apartment::TenantNotFound
else
schema
end
end
end
def self.allowed_domains
dev_domains = %w[localhost lvh.me example.com]
dev_domains + [default_host]
end
def self.excluded_subdomains
%w[mail public shared_extensions www]
end
def self.default_url_options
ActionMailer::Base.default_url_options
end
def self.default_host
default_url_options[:host]
end
def self.default_domain
if default_host == "localhost"
"lvh.me"
else
default_host
end
end
def self.current_url_options
default_url_options.merge(host: current_host)
end
def self.current_host
host_for(current_schema)
end
def self.host_for(schema)
if schema == "public"
default_host
elsif find_by_domain(schema)
schema
else
"#{schema}.#{default_domain}"
end
end
def self.current_secrets
if default?
Rails.application.secrets
else
@secrets ||= {}
@cached_rails_secrets ||= Rails.application.secrets
if @cached_rails_secrets != Rails.application.secrets
@secrets = {}
@cached_rails_secrets = Rails.application.secrets
end
@secrets[current_schema] ||= Rails.application.secrets.merge(
Rails.application.secrets.dig(:tenants, current_schema.to_sym).to_h
)
end
end
def self.subfolder_path
subfolder_path_for(current_schema)
end
def self.subfolder_path_for(schema)
if schema == "public"
""
else
File.join("tenants", schema)
end
end
def self.path_with_subfolder(filename_or_folder)
File.join(subfolder_path, filename_or_folder).delete_prefix("/")
end
def self.default?
current_schema == "public"
end
def self.current_schema
Apartment::Tenant.current
end
def self.current
find_by(schema: current_schema)
end
def self.switch(...)
Apartment::Tenant.switch(...)
end
def self.run_on_each(&)
["public"].union(Apartment.tenant_names).each do |schema|
switch(schema, &)
end
end
def host
self.class.host_for(schema)
end
def hide
update_attribute(:hidden_at, Time.current)
end
def restore
update_attribute(:hidden_at, nil)
end
def hidden?
hidden_at.present?
end
private
def create_schema
Apartment::Tenant.create(schema)
end
def rename_schema
if saved_change_to_schema?
ActiveRecord::Base.connection.execute(
"ALTER SCHEMA \"#{schema_before_last_save}\" RENAME TO \"#{schema}\";"
)
rename_storage
end
end
def rename_storage
service = ActiveStorage::Blob.service
return unless service.respond_to?(:tenant_root_for)
old_storage = service.tenant_root_for(schema_before_last_save)
return unless File.directory?(old_storage)
new_storage = service.tenant_root_for(schema)
File.rename(old_storage, new_storage)
end
def destroy_schema
Apartment::Tenant.drop(schema)
end
end