module ActiveRecord class Base def self.reflect_on_all_associations @associations ||= superclass.instance_eval { @associations&.dup || [] } end def self.reflect_on_association(attr) reflection_finder { |assoc| assoc.attribute == attr } end def self.reflect_on_association_by_foreign_key(key) reflection_finder { |assoc| assoc.association_foreign_key == key && assoc.macro != :has_many } end def self.reflection_finder(&block) found = reflect_on_all_associations.detect do |assoc| assoc.owner_class == self && yield(assoc) end if found found elsif superclass == Base nil else superclass.reflection_finder(&block) end end end module Associations class AssociationReflection attr_reader :klass_name attr_reader :association_foreign_key attr_reader :attribute attr_reader :macro attr_reader :owner_class attr_reader :source attr_reader :source_type attr_reader :options attr_reader :polymorphic_type_attribute def initialize(owner_class, macro, name, options = {}) owner_class.reflect_on_all_associations << self @owner_class = owner_class @macro = macro @options = options unless options[:polymorphic] @klass_name = options[:class_name] || (collection? && name.camelize.singularize) || name.camelize end if @klass_name < ActiveRecord::Base @klass = @klass_name @klass_name = @klass_name.name end rescue nil @association_foreign_key = options[:foreign_key] || (macro == :belongs_to && "#{name}_id") || (options[:as] && "#{options[:as]}_id") || (options[:polymorphic] && "#{name}_id") || "#{@owner_class.name.underscore}_id" if options[:through] @source = options[:source] || @klass_name.underscore @source_type = options[:source_type] || @klass_name end @polymorphic_type_attribute = "#{name}_type" if options[:polymorphic] @attribute = name @through_associations = Hash.new { |_h, k| [] unless k } end def collection? @macro == :has_many end def singular? @macro != :has_many end def habtm? through_association&.klass_name =~ /^HyperstackInternalHabtm/ end def through_association return unless @options[:through] @through_association ||= @owner_class.reflect_on_all_associations.detect do |association| association.attribute == @options[:through] end raise "Through association #{@options[:through]} for "\ "#{@owner_class}.#{attribute} not found." unless @through_association @through_association end alias through_association? through_association # class Membership < ActiveRecord::Base # belongs_to :uzer # belongs_to :memerable, polymorphic: true # end # # class Project < ActiveRecord::Base # has_many :memberships, as: :memerable, dependent: :destroy # has_many :uzers, through: :memberships # end # # class Group < ActiveRecord::Base # has_many :memberships, as: :memerable, dependent: :destroy # has_many :uzers, through: :memberships # end # # class Uzer < ActiveRecord::Base # has_many :memberships # has_many :groups, through: :memberships, source: :memerable, source_type: 'Group' # has_many :projects, through: :memberships, source: :memerable, source_type: 'Project' # end # so find the belongs_to relationship whose attribute == ta.source # now find the inverse of that relationship using source_value as the model # now find any has many through relationships that use that relationship as there source. # each of those attributes in the source_value have to be updated. # self is the through association def through_associations(model) # given self is a belongs_to association currently pointing to model # find all associations that use the inverse association as the through association # that is find all associations that are using this association in a through relationship the_klass = klass(model) @through_associations[the_klass] ||= the_klass.reflect_on_all_associations.select do |assoc| assoc.through_association&.inverse == self end end def source_belongs_to_association # private # given self is a has_many_through association return the corresponding belongs_to association # for the source @source_belongs_to_association ||= through_association.inverse.owner_class.reflect_on_all_associations.detect do |sibling| sibling.attribute == source end end def source_associations(model) # given self is a has_many_through association find the source_association for the given model source_belongs_to_association.through_associations(model) end alias :polymorphic? polymorphic_type_attribute def inverse(model = nil) return @inverse if @inverse ta = through_association found = ta ? ta.inverse : find_inverse(model) @inverse = found unless polymorphic? found end def inverse_of(model = nil) inverse(model).attribute end def find_inverse(model) # private the_klass = klass(model) the_klass.reflect_on_all_associations.each do |association| next if association == self next if association.association_foreign_key != @association_foreign_key next if association.attribute == attribute return association if association.polymorphic? || association.klass == owner_class end raise "could not find inverse of polymorphic belongs_to: #{model.inspect} #{self.inspect}" if options[:polymorphic] # instead of raising an error go ahead and create the inverse relationship if it does not exist. # https://github.com/hyperstack-org/hyperstack/issues/89 if macro == :belongs_to Hyperstack::Component::IsomorphicHelpers.log "**** warning dynamically adding relationship: #{the_klass}.has_many :#{@owner_class.name.underscore.pluralize}, foreign_key: #{@association_foreign_key}", :warning the_klass.has_many @owner_class.name.underscore.pluralize, foreign_key: @association_foreign_key elsif options[:as] Hyperstack::Component::IsomorphicHelpers.log "**** warning dynamically adding relationship: #{the_klass}.belongs_to :#{options[:as]}, polymorphic: true", :warning the_klass.belongs_to options[:as], polymorphic: true else Hyperstack::Component::IsomorphicHelpers.log "**** warning dynamically adding relationship: #{the_klass}.belongs_to :#{@owner_class.name.underscore}, foreign_key: #{@association_foreign_key}", :warning the_klass.belongs_to @owner_class.name.underscore, foreign_key: @association_foreign_key end end def klass(model = nil) @klass ||= Object.const_get(@klass_name) if @klass_name if @klass && model && !(model.class <= @klass || @klass <= model.class) # TODO: added || @klass <= model.class can both cases really happen I guess so raise "internal error: provided model #{model} is not subclass of #{@klass}" end raise 'no model supplied for polymorphic relationship' unless @klass || model @klass || model.class end def collection? [:has_many].include? @macro end def remove_member(member, owner) collection = owner.attributes[attribute] return if collection.nil? collection.delete(member) end def add_member(member, owner) owner.attributes[attribute] ||= ReactiveRecord::Collection.new(owner_class, owner, self) owner.attributes[attribute]._internal_push member end end end end