lib/mongoid/association/referenced/syncable.rb
# frozen_string_literal: true
# rubocop:todo all
module Mongoid
module Association
module Referenced
# This module handles the behavior for synchronizing foreign keys between
# both sides of a many to many associations.
module Syncable
# Is the document able to be synced on the inverse side? This is only if
# the key has changed and the association bindings have not been run.
#
# @example Are the foreign keys syncable?
# document._syncable?(association)
#
# @param [ Mongoid::Association::Relatable ] association The association metadata.
#
# @return [ true | false ] If we can sync.
def _syncable?(association)
!_synced?(association.foreign_key) && send(association.foreign_key_check)
end
# Get the synced foreign keys.
#
# @example Get the synced foreign keys.
# document._synced
#
# @return [ Hash ] The synced foreign keys.
def _synced
@_synced ||= {}
end
# Has the document been synced for the foreign key?
#
# @example Has the document been synced?
# document._synced?
#
# @param [ String ] foreign_key The foreign key.
#
# @return [ true | false ] If we can sync.
def _synced?(foreign_key)
!!_synced[foreign_key]
end
# Update the inverse keys on destroy.
#
# @example Update the inverse keys.
# document.remove_inverse_keys(association)
#
# @param [ Mongoid::Association::Relatable ] association The association metadata.
#
# @return [ Object ] The updated values.
def remove_inverse_keys(association)
foreign_keys = send(association.foreign_key)
unless foreign_keys.nil? || foreign_keys.empty?
association.criteria(self, foreign_keys).pull(association.inverse_foreign_key => _id)
end
end
# Update the inverse keys for the association.
#
# @example Update the inverse keys
# document.update_inverse_keys(association)
#
# @param [ Mongoid::Association::Relatable ] association The association metadata.
#
# @return [ Object ] The updated values.
def update_inverse_keys(association)
if previous_changes.has_key?(association.foreign_key)
old, new = previous_changes[association.foreign_key]
adds, subs = new - (old || []), (old || []) - new
# If we are autosaving we don't want a duplicate to get added - the
# $addToSet would run previously and then the $push and $each from the
# inverse on the autosave would cause this. We delete each id from
# what's in memory in case a mix of id addition and object addition
# had occurred.
if association.autosave?
send(association.name).in_memory.each do |doc|
adds.delete_one(doc._id)
end
end
unless adds.empty?
association.criteria(self, adds).without_options.add_to_set(association.inverse_foreign_key => _id)
end
unless subs.empty?
association.criteria(self, subs).without_options.pull(association.inverse_foreign_key => _id)
end
end
end
module ClassMethods
# Set up the syncing of many to many foreign keys.
#
# @example Set up the syncing.
# Person._synced(association)
#
# @param [ Mongoid::Association::Relatable ] association The association metadata.
def _synced(association)
unless association.forced_nil_inverse?
synced_save(association)
synced_destroy(association)
end
end
private
# Set up the sync of inverse keys that needs to happen on a save.
#
# If the foreign key field has changed and the document is not
# synced, $addToSet the new ids, $pull the ones no longer in the
# array from the inverse side.
#
# @example Set up the save syncing.
# Person.synced_save(association)
#
# @param [ Mongoid::Association::Relatable ] association The association metadata.
#
# @return [ Class ] The class getting set up.
def synced_save(association)
set_callback(
:save,
:after,
if: ->(doc) { doc._syncable?(association) }
) do |doc|
doc.update_inverse_keys(association)
end
self
end
# Set up the sync of inverse keys that needs to happen on a destroy.
#
# @example Set up the destroy syncing.
# Person.synced_destroy(association)
#
# @param [ Mongoid::Association::Relatable ] association The association metadata.
#
# @return [ Class ] The class getting set up.
def synced_destroy(association)
set_callback(
:destroy,
:after
) do |doc|
doc.remove_inverse_keys(association)
end
self
end
end
end
end
end
end