Module: SeparateHistory::Core

Extended by:
ActiveSupport::Concern
Defined in:
lib/separate_history/core.rb

Instance Method Summary collapse

Instance Method Details

#has_separate_history(options = {}) ⇒ Object

Raises:

  • (ArgumentError)


10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/separate_history/core.rb', line 10

def has_separate_history(options = {})
  raise ArgumentError, "has_separate_history can not be called on an abstract class" if abstract_class?
  raise ArgumentError, "Options :only and :except can not be used together" if options[:only] && options[:except]

  valid_options = i[only except history_class_name events track_changes]
  invalid_options = options.keys - valid_options
  raise ArgumentError, "Invalid options: #{invalid_options.join(", ")}" if invalid_options.any?

  options[:track_changes] = false if options[:track_changes].nil?
  unless options[:track_changes].is_a?(TrueClass) || options[:track_changes].is_a?(FalseClass)
    raise ArgumentError, "track_changes must be true or false"
  end

  supported_events = i[create update destroy]
  if options[:events]
    events = Array(options[:events])
    invalid_events = events - supported_events
    raise ArgumentError, "Invalid events: #{invalid_events.join(", ")}" if invalid_events.any?
  end

  cattr_accessor :separate_history_options
  self.separate_history_options = options

  class << self
    # Returns the history class (e.g., UserHistory for User)
    def history_class
      history_class_name = separate_history_options.fetch(:history_class_name, "#{name}History")
      history_class_name.safe_constantize
    end

    # Returns the snapshot of the record as it was at or before the given timestamp
    def history_for(id, timestamp = Time.current)
      history_class.where(original_id: id)
                   .where(history_updated_at: ..timestamp)
                   .order(history_updated_at: :desc, id: :desc)
                   .first
    end

    # Alias for readability: get the state of a record as of a point in time
    def history_as_of(id, timestamp)
      history_for(id, timestamp)
    end

    # Returns true if any history exists for the given record ID
    def history_exists_for?(id)
      history_class.where(original_id: id).exists?
    end

    # Returns all history records for the given record ID, ordered by update time
    def all_history_for(id)
      history_class.where(original_id: id)
                   .order(history_updated_at: :asc, id: :asc)
    end

    # Returns the most recent history record for the given record ID
    def latest_history_for(id)
      history_class.where(original_id: id)
                   .order(history_updated_at: :desc, id: :desc)
                   .first
    end

    # Deletes all history for the given record ID.
    # Requires force: true to prevent accidental destruction.
    def clear_history_for(id, force:)
      raise ArgumentError, "Force must be true to clear history." unless force

      history_class.where(original_id: id).delete_all
    end

    # clear all history for all records of this model
    def clear_all_history(force:)
      raise ArgumentError, "Force must be true to clear all history." unless force

      history_class.delete_all
    end
  end

  history_class_name = options.fetch(:history_class_name, "#{name}History")
  history_table_name = history_class_name.tableize
  association_name   = name.demodulize.underscore.to_sym

  # Main association
  # Main association for accessing all history records
  has_many history_table_name.to_sym,
           class_name: history_class_name,
           foreign_key: :original_id,
           inverse_of: association_name

  # Alias association for convenience (points to the same records)
  alias_method :separate_histories, history_table_name.to_sym

  history_class = history_class_name.safe_constantize
  if history_class
    # Set up the belongs_to association on the history class
    unless history_class.reflect_on_association(association_name)
      history_class.belongs_to association_name,
                               class_name: name,
                               foreign_key: :original_id,
                               inverse_of: history_table_name.to_sym,
                               optional: true
    end

    history_class.include SeparateHistory::History
  end

  # Model-level events support
  events = Array(options[:events] || supported_events)
  events.each do |event|
    next unless supported_events.include?(event)

    after_commit :"record_history_#{event}", on: event
  end

  include SeparateHistory::Model
  SeparateHistory.track_model(self)
end