Files
has_aux_table/lib/has_aux_table/relation_extensions.rb
2025-07-24 04:04:08 +00:00

145 lines
4.1 KiB
Ruby

# typed: true
# frozen_string_literal: true
require "active_record/associations/association_scope"
class ActiveRecord::Associations::AssociationScope
def get_chain(reflection, association, tracker)
name = reflection.name
chain =
T.let(
[
ActiveRecord::Reflection::RuntimeReflection.new(
reflection,
association
)
],
T.untyped
)
reflection
.chain
.drop(1)
.each do |refl|
refl_klass = T.cast(refl.klass, T.class_of(ActiveRecord::Base))
if refl_klass.is_a?(HasAuxTable::ClassMethods) &&
(aux_config = refl_klass.aux_table_for(refl.foreign_key))
aliased_table = aux_config.aux.klass.arel_table
chain << ReflectionProxy.new(refl, aliased_table)
else
aliased_table =
tracker.aliased_table_for(refl.klass.arel_table) do
refl.alias_candidate(name)
end
chain << ReflectionProxy.new(refl, aliased_table)
end
end
chain
end
def scope(association)
klass = association.klass
reflection = association.reflection
scope = klass.unscoped
owner = association.owner
chain = get_chain(reflection, association, scope.alias_tracker)
scope.extending! reflection.extensions
scope = add_constraints(scope, owner, chain)
scope.limit!(1) unless reflection.collection?
chain.each do |refl|
klass = refl.klass
next unless klass.is_a?(HasAuxTable::ClassMethods)
next unless aux_config = klass.aux_table_for(refl.join_primary_key)
aux_table = aux_config.aux.klass.table_name
main_table = aux_config.main.klass.table_name
main_keys =
aux_config.main.primary_keys.map { |key| "'#{main_table}'.'#{key}'" }
aux_keys =
aux_config.aux.primary_keys.map { |key| "'#{aux_table}'.'#{key}'" }
join_clause =
main_keys
.zip(aux_keys)
.map { |(main_key, aux_key)| "#{main_key} = #{aux_key}" }
.join(" AND ")
scope.joins!("INNER JOIN '#{main_table}' ON (#{join_clause})")
end if association.is_a?(
ActiveRecord::Associations::HasManyThroughAssociation
)
scope
end
end
module HasAuxTable
module RelationExtensions
extend T::Sig
Util = HasAuxTable::Util
sig { params(aux_config: AuxTableConfig).void }
def setup_relation_extensions!(aux_config)
main_class = aux_config.main.klass
Util.hook_method(main_class, :where, false) do |original, *args|
if args.length == 1 && args.first.is_a?(Hash)
opts_remapped = aux_config.remap_conditions(args.first)
original.call(opts_remapped)
else
original.call(*args)
end
end
Util.hook_method(
main_class,
:all,
false
) do |original, *args, **kwargs, &block|
original.call(*args, **kwargs, &block).eager_load(
aux_config.aux_association_name
)
end
Util.hook_method(
main_class,
:unscoped,
false
) do |original, *args, **kwargs, &block|
ret = original.call(*args, **kwargs, &block)
if ret.is_a?(ActiveRecord::Relation)
ret.eager_load!(aux_config.aux_association_name)
end
ret
end
relation_class =
main_class.relation_delegate_class(ActiveRecord::Relation)
collection_proxy_class =
main_class.relation_delegate_class(
ActiveRecord::Associations::CollectionProxy
)
[
[relation_class, :build_where_clause],
[collection_proxy_class, :where]
].each do |klass, method_name|
Util.hook_method(klass, method_name, true) do |original, opts, *rest|
if opts.is_a?(Hash)
opts_remapped = aux_config.remap_conditions(opts)
T.unsafe(original).call(opts_remapped, *rest)
else
T.unsafe(original).call(opts, *rest)
end
end
end
Util.hook_method(
relation_class,
:bind_attribute,
true
) do |original, name, value, &block|
aux_config.aux_bind_attribute(name, value, &block)
end
end
end
end