lib/mongoid/association/depending.rb
# frozen_string_literal: true
# rubocop:todo all
module Mongoid
module Association
# This module defines the behavior for setting up cascading deletes and
# nullifies for associations, and how to delegate to the appropriate strategy.
module Depending
extend ActiveSupport::Concern
included do
class_attribute :dependents
# @api private
class_attribute :dependents_owner
self.dependents = []
self.dependents_owner = self
end
class_methods do
# Returns all dependent association metadata objects.
#
# @return [ Array<Mongoid::Association::Relatable> ] The dependent
# association metadata.
#
# @api private
def _all_dependents
superclass_dependents = superclass.respond_to?(:_all_dependents) ? superclass._all_dependents : []
dependents + superclass_dependents.reject do |new_dep|
dependents.any? do |old_dep| old_dep.name == new_dep.name
end
end
end
end
# The valid dependent strategies.
STRATEGIES = [
:delete_all,
:destroy,
:nullify,
:restrict_with_exception,
:restrict_with_error
]
# Attempt to add the cascading information for the document to know how
# to handle associated documents on a removal.
#
# @example Set up cascading information
# Mongoid::Association::Depending.define_dependency!(association)
#
# @param [ Mongoid::Association::Relatable ] association The association metadata.
#
# @return [ Class ] The class of the document.
def self.define_dependency!(association)
validate!(association)
association.inverse_class.tap do |klass|
if klass.dependents_owner != klass
klass.dependents = []
klass.dependents_owner = klass
end
if association.dependent && !klass.dependents.include?(association)
klass.dependents.push(association)
end
end
end
# Validates that an association's dependent strategy is
# within the allowed enumeration.
#
# @param [ Mongoid::Association::Relatable ] association
# The association to validate.
#
# @raises [ Mongoid::Errors::InvalidDependentStrategy ]
# Error if invalid.
def self.validate!(association)
unless STRATEGIES.include?(association.dependent)
raise Errors::InvalidDependentStrategy.new(association,
association.dependent,
STRATEGIES)
end
end
# Perform all cascading deletes, destroys, or nullifies. Will delegate to
# the appropriate strategy to perform the operation.
#
# @example Execute cascades.
# document.apply_destroy_dependencies!
def apply_destroy_dependencies!
self.class._all_dependents.each do |association|
if dependent = association.try(:dependent)
send("_dependent_#{dependent}!", association)
end
end
end
private
def _dependent_delete_all!(association)
if relation = send(association.name)
if relation.respond_to?(:dependents) && relation.dependents.blank?
relation.clear
else
::Array.wrap(send(association.name)).each { |rel| rel.delete }
end
end
end
def _dependent_destroy!(association)
if relation = send(association.name)
if relation.is_a?(Enumerable)
relation.entries
relation.each { |doc| doc.destroy }
else
relation.destroy
end
end
end
def _dependent_nullify!(association)
if relation = send(association.name)
relation.nullify
end
end
def _dependent_restrict_with_exception!(association)
if (relation = send(association.name)) && !relation.blank?
raise Errors::DeleteRestriction.new(relation, association.name)
end
end
def _dependent_restrict_with_error!(association)
if (relation = send(association.name)) && !relation.blank?
errors.add(association.name, :destroy_restrict_with_error_dependencies_exist)
throw(:abort, false)
end
end
end
end
end