Files
has_aux_table/lib/has_aux_table/aux_table_config.rb
2025-07-29 16:18:37 +00:00

182 lines
5.1 KiB
Ruby

# typed: strict
# frozen_string_literal: true
module HasAuxTable
class AuxTableConfig < T::Struct
extend T::Sig
const :aux_table_name, Symbol
const :aux_association_name, Symbol
const :main, ModelClassHelper
const :aux, ModelClassHelper
const :allow_redefining, T::Array[Symbol]
sig do
params(
main_class: T.class_of(ActiveRecord::Base),
aux_class: T.class_of(ActiveRecord::Base),
aux_association_name: Symbol,
allow_redefining: T::Array[Symbol]
).returns(AuxTableConfig)
end
def self.from_models(
main_class:,
aux_class:,
aux_association_name:,
allow_redefining: []
)
primary_key = aux_class.primary_key
aux_rejected_column_names = [
primary_key,
"created_at",
"updated_at"
].flatten.map(&:to_s).to_set
new(
aux_table_name: aux_class.table_name.to_sym,
aux_association_name:,
main:
ModelClassHelper.new(
klass: main_class,
rejected_column_names: Set.new
),
aux:
ModelClassHelper.new(
klass: aux_class,
rejected_column_names: aux_rejected_column_names
),
allow_redefining:
)
end
sig { returns(T.untyped) }
def load_aux_schema
aux.klass.load_schema
end
sig do
params(main_instance: ActiveRecord::Base).returns(ActiveRecord::Base)
end
def aux_model_for(main_instance)
Util.ensure_is_instance_of!(main_instance, main.klass)
aux_association = main_instance.association(self.aux_association_name)
aux_association.target ||=
(
if main_instance.persisted?
aux_association.load_target || aux_association.build
else
aux_association.build
end
)
end
sig do
params(
name: Symbol,
value: T.untyped,
block:
T
.proc
.params(arel_attr: T.untyped, aux_bind: T.untyped)
.returns(Arel::Nodes::Node)
).returns(Arel::Nodes::Node)
end
def aux_bind_attribute(name, value, &block)
arel_attr = aux.klass.arel_table[name]
aux_bind =
aux.klass.predicate_builder.build_bind_attribute(arel_attr.name, value)
block.call(arel_attr, aux_bind)
end
# Forward method call `method_name` to the aux model
sig { params(method_name: Symbol).void }
def define_aux_attribute_delegate(method_name)
config = self
main
.klass
.define_method(method_name) do |*args, **kwargs, &block|
T.bind(self, ActiveRecord::Base)
aux_model = config.aux_model_for(self)
ret =
T.unsafe(aux_model).public_send(
method_name,
*args,
**kwargs,
&block
)
ret
end
end
sig do
params(conditions: T::Hash[String, T.untyped]).returns(
T::Hash[String, T.untyped]
)
end
def remap_conditions(conditions)
main_conds, aux_conds = partition_by_columns(conditions)
main_conds.merge!(aux_association_name => aux_conds) if aux_conds.any?
main_conds
end
sig do
type_parameters(:K)
.params(
hash:
T::Hash[
T.all(T.type_parameter(:K), T.any(String, Symbol)),
T.untyped
]
)
.returns(
[
T::Hash[
T.all(T.type_parameter(:K), T.any(String, Symbol)),
T.untyped
],
T::Hash[
T.all(T.type_parameter(:K), T.any(String, Symbol)),
T.untyped
]
]
)
end
def partition_by_columns(hash)
main = {}
aux = {}
hash.each do |k, v|
if self.aux.column_names.include?(k.to_s)
# attribute is a column on the aux table
aux[k] = v
next
elsif assoc = self.main.klass.reflect_on_association(k.to_s)
if assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection)
# attribute is an association on the main class
fk = assoc.foreign_key
if self.aux.column_names.include?(fk)
# the association is a column on the aux table, `v` is
# a model, get the primary key of the model
aux[fk] = v && v.send(assoc.association_primary_key)
next
end
elsif assoc.is_a?(ActiveRecord::Reflection::HasOneReflection) ||
assoc.is_a?(ActiveRecord::Reflection::HasManyReflection)
pk = assoc.active_record_primary_key
if self.aux.column_names.include?(pk)
aux[pk] = v && v.send(assoc.foreign_key)
next
end
else
# TODO - add support for ActiveRecord::Reflection::ThroughReflection
raise "unsupported association type: #{assoc.class}"
end
end
# attribute is not a column on the aux table or an association,
# assume it's a column on the main table
main[k] = v
end
[main, aux]
end
end
end