module PermanentRecords::ActiveRecord

This module defines the public api that you can use in your model instances.

Public Class Methods

included(base) click to toggle source
# File lib/permanent_records.rb, line 16
def self.included(base)
  base.extend Scopes
  base.extend IsPermanent

  base.instance_eval do
    define_model_callbacks :revive
  end
end

Public Instance Methods

deleted?() click to toggle source
# File lib/permanent_records.rb, line 29
def deleted?
  if is_permanent?
    !!deleted_at # rubocop:disable Style/DoubleNegation
  else
    destroyed?
  end
end
destroy(force = nil) click to toggle source
Calls superclass method
# File lib/permanent_records.rb, line 49
def destroy(force = nil)
  with_transaction_returning_status do
    if !is_permanent? || PermanentRecords.should_force_destroy?(force)
      permanently_delete_records_after { super() }
    else
      destroy_with_permanent_records(force)
    end
  end
end
is_permanent?() click to toggle source
# File lib/permanent_records.rb, line 25
def is_permanent? # rubocop:disable Naming/PredicateName
  respond_to?(:deleted_at)
end
revive(options = nil) click to toggle source
# File lib/permanent_records.rb, line 37
def revive(options = nil)
  with_transaction_returning_status do
    if PermanentRecords.should_revive_parent_first?(options)
      revival.reverse
    else
      revival
    end.each { |p| p.call(options) }

    self
  end
end

Private Instance Methods

add_record_window(_request, name, reflection) click to toggle source

rubocop:enable Metrics/MethodLength

# File lib/permanent_records.rb, line 141
def add_record_window(_request, name, reflection)
  send(name).unscope(where: :deleted_at).where(
    [
      "#{reflection.klass.quoted_table_name}.deleted_at > ?" \
      ' AND ' \
      "#{reflection.klass.quoted_table_name}.deleted_at < ?",
      deleted_at - PermanentRecords.dependent_record_window,
      deleted_at + PermanentRecords.dependent_record_window
    ]
  )
end
attempt_notifying_observers(callback) click to toggle source

rubocop:enable Metrics/MethodLength

# File lib/permanent_records.rb, line 178
def attempt_notifying_observers(callback)
  notify_observers(callback)
rescue NoMethodError # rubocop:disable Lint/HandleExceptions
  # do nothing: this model isn't being observed
end
dependent_record_ids() click to toggle source

return the records corresponding to an association with the `:dependent

> :destroy` option

# File lib/permanent_records.rb, line 186
def dependent_record_ids
  # check which dependent records are to be destroyed
  PermanentRecords.dependent_reflections(self.class)
                  .reduce({}) do |records, (key, _)|
    found = Array(send(key)).compact
    next records if found.empty?

    records.update found.first.class => found.map(&:id)
  end
end
destroy_with_permanent_records(force = nil) click to toggle source

rubocop:disable Metrics/MethodLength

# File lib/permanent_records.rb, line 124
def destroy_with_permanent_records(force = nil)
  run_callbacks(:destroy) do
    if deleted? || new_record?
      save
    else
      set_deleted_at(Time.now, force)
      # decrement all associated counters for counter cache
      each_counter_cache do |assoc_class, counter_cache_column, assoc_id|
        assoc_class.decrement_counter(counter_cache_column, assoc_id)
      end
    end
    true
  end
  deleted? ? self : false
end
destroyed_dependent_relations() click to toggle source

rubocop:disable Metrics/MethodLength

# File lib/permanent_records.rb, line 162
def destroyed_dependent_relations
  PermanentRecords.dependent_permanent_reflections(self.class).map do |name, relation|
    case relation.macro.to_sym
    when :has_many
      if deleted_at
        add_record_window(send(name), name, relation)
      else
        send(name).unscope(where: :deleted_at)
      end
    when :has_one, :belongs_to
      self.class.unscoped { Array(send(name)) }
    end
  end
end
each_counter_cache() { |associated_class, counter_cache_column, send(foreign_key)| ... } click to toggle source

rubocop:enable Metrics/MethodLength

# File lib/permanent_records.rb, line 111
def each_counter_cache
  _reflections.each do |name, reflection|
    association = respond_to?(name.to_sym) ? send(name.to_sym) : nil
    next if association.nil?
    next unless reflection.belongs_to? && reflection.counter_cache_column

    associated_class = association.class

    yield(associated_class, reflection.counter_cache_column, send(reflection.foreign_key))
  end
end
get_deleted_record() click to toggle source
# File lib/permanent_records.rb, line 79
def get_deleted_record # rubocop:disable Naming/AccessorMethodName
  self.class.unscoped.find(id)
end
permanently_delete_records(dependent_records) click to toggle source

permanently delete the records (i.e. remove from database)

# File lib/permanent_records.rb, line 212
def permanently_delete_records(dependent_records)
  dependent_records.each do |klass, ids|
    ids.each do |id|
      record = klass.unscoped.where(klass.primary_key => id).first
      next unless record

      record.deleted_at = nil
      record.destroy(:force)
    end
  end
end
permanently_delete_records_after() { || ... } click to toggle source

If we force the destruction of the record, we will need to force the destruction of dependent records if the user specified `:dependent => :destroy` in the model. By default, the call to super/destroy_with_permanent_records (i.e. the &block param) will only soft delete the dependent records; we keep track of the dependent records that have `:dependent => :destroy` and call destroy(force) on them after the call to super

# File lib/permanent_records.rb, line 204
def permanently_delete_records_after(&_block)
  dependent_records = dependent_record_ids
  result = yield
  permanently_delete_records(dependent_records) if result
  result
end
revival() click to toggle source
# File lib/permanent_records.rb, line 61
def revival # rubocop:disable Metrics/MethodLength
  [
    lambda do |validate|
      revive_destroyed_dependent_records(validate)
    end,
    lambda do |validate|
      run_callbacks(:revive) do
        set_deleted_at(nil, validate)
        # increment all associated counters for counter cache
        each_counter_cache do |assoc_class, counter_cache_column, assoc_id|
          assoc_class.increment_counter(counter_cache_column, assoc_id)
        end
        true
      end
    end
  ]
end
revive_destroyed_dependent_records(force = nil) click to toggle source

TODO: Feel free to refactor this without polluting the ActiveRecord namespace.

# File lib/permanent_records.rb, line 154
def revive_destroyed_dependent_records(force = nil)
  destroyed_dependent_relations.each do |relation|
    relation.to_a.each { |destroyed_dependent_record| destroyed_dependent_record.try(:revive, force) }
  end
  reload
end
set_deleted_at(value, force = nil) click to toggle source

rubocop:disable Metrics/MethodLength

# File lib/permanent_records.rb, line 84
def set_deleted_at(value, force = nil)
  return self unless is_permanent?

  record = get_deleted_record
  record.deleted_at = value
  begin
    # we call save! instead of update_attribute so an
    # ActiveRecord::RecordInvalid error will be raised if the record isn't
    # valid. (This prevents reviving records that disregard validation
    # constraints,)
    if PermanentRecords.should_ignore_validations?(force)
      record.save(validate: false)
    else
      record.save!
    end

    @attributes = record.instance_variable_get('@attributes')
  rescue StandardError => e
    # trigger dependent record destruction (they were revived before this
    # record, which cannot be revived due to validations)
    record.destroy
    raise e
  end
end