182 lines
5.1 KiB
Ruby
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
|