module Sequel::Plugins::ThroughAssociations::ClassMethods

Public Instance Methods

associate_through(type, name, opts, &block) click to toggle source

Associates a related model with the current model using another association as the intermediary.

# File lib/sequel/plugins/through_associations.rb, line 44
def associate_through type, name, opts, &block

  unless assoc_type = Sequel.synchronize{ASSOCIATION_THROUGH_TYPES[type]}
    raise Error, "#{type} does not support through associations"
  end

  result = find_association_path(**opts, name: name, models: self, from_through: true)

  # Remove the last table if it matches the destination table
  dest_model = result[:models].pop
  result[:tables].pop if result[:tables].last == dest_model.table_name

  # Build the association path
  path = []
  left_key = result[:keys].shift
  result[:tables].each do |table|
    path.push [table, result[:keys].shift, result[:keys].shift]
  end

  # Create the association
  if assoc_type.to_s.end_with? "_through_many"
    # *_through_many has a path argument
    self.send(assoc_type,
      name,
      path,
      left_primary_key: left_key,
      right_primary_key: result[:keys].shift,
      class: dest_model,
      **opts,
      originally_through: opts[:through],
      &block
    )
  else
    # *_through_one does not have a path argument
    self.send(assoc_type,
      name,
      left_primary_key: left_key,
      right_primary_key: result[:keys].shift,
      class: dest_model,
      **opts,
      originally_through: opts[:through],
      &block
    )
  end
end
find_association_path(**opts) click to toggle source

Recurses through associations until a path to the destination is completed

# File lib/sequel/plugins/through_associations.rb, line 91
def find_association_path **opts

  # Initialize arguments
  [:tables, :keys, :through, :models, :assocs].each do |k|
    opts[k] ||= []
    opts[k] = [opts[k]] unless Array === opts[k]
    opts[k] = opts[k].dup
  end

  # Find the linked association
  assoc = \
    opts[:models].last.association_reflection(opts[:through].last.to_s.pluralize.to_sym) \
    || opts[:models].last.association_reflection(opts[:through].last.to_s.singularize.to_sym)

  # Short circuit if association does not exist
  unless assoc

    # Determine if finished or if the last relation is missing
    if opts[:from_through]

      m = opts[:models].pop
      t = opts[:through].pop
      path = [m]

      opts[:models].zip(opts[:through]).each do |model, through|
        path.push "#{model}.#{through}"
      end

      raise MissingAssociation, "#{m} is missing through association :#{t} from #{path.join " -> "}"

    else

      if opts[:assocs].last[:name].to_s.singularize != (opts[:using] || opts[:name]).to_s.singularize
        text = "#{opts[:models].first}.#{opts[:name]} could not be resolved through path #{opts[:models].zip(opts[:through]).map{|model, through| "#{model}.#{through}"}.join " -> "}"
        raise MissingAssociation, text
      end

      return opts

    end

  end

  # Store the association
  opts[:assocs].push assoc

  # Handle *_through_many associations
  if assoc[:type].to_s.end_with? "_through_many"

    opts[:through].push assoc[:originally_through]
    opts[:from_through] = true

    # Search through the existing model first, falling back to the associated model
    search = [
      opts[:models].last,
      assoc[:class] || assoc[:class_name].constantize
    ]
    return begin
      model = search.shift
      raise NoAssociationPath, opts unless model
      self.find_association_path(**opts, models: opts[:models] + [model])
    rescue MissingAssociation
      # Try the next model in the search path
      retry
    end

  end

  # Move to the new model
  opts[:models].push assoc[:class] || assoc[:class_name].constantize

  # Read the through association if present
  if assoc[:through]
    opts[:through].push assoc[:using] || assoc[:through]
    opts[:from_through] = true
    opts[:using] = nil
    return self.find_association_path(**opts)
  end

  # Otherwise, add the new table to the stack
  if opts[:from_through] && opts[:models].last.respond_to?(:cti_tables)
    opts[:tables].push opts[:models].last.cti_tables.first
  else
    opts[:tables].push opts[:models].last.table_name
  end

  # Left side
  case assoc[:type]
    when :one_to_many, :one_to_one # 1:_
      opts[:keys].push assoc.primary_key
    when :many_to_one # n:_
      opts[:keys].push assoc[:key]
    else
      raise
  end

  # Right side
  case assoc[:type]
    when :many_to_one, :one_to_one # _:1
      opts[:keys].push assoc.primary_key
    when :one_to_many # _:n
      opts[:keys].push assoc[:key]
    else
      raise
  end

  # Check for a source association
  opts[:through].push opts[:using] || opts[:name]
  opts[:from_through] = false
  return self.find_association_path(**opts)

end