Class: RailsAuditLog::AuditLogEntry

Inherits:
ApplicationRecord show all
Defined in:
app/models/rails_audit_log/audit_log_entry.rb

Overview

Represents a single audited event (create, update, or destroy) for one ActiveRecord record.

Columns

event

One of "create", "update", "destroy".

item_type

Class name of the audited record (e.g. "Article").

item_id

Primary key of the audited record.

object_changes

JSON hash of attribute changes in [from, to] form. All three event types use the same format: create stores [nil, new] for every column, update stores [old, new] for changed columns only, destroy stores [final, nil] for every column.

object

JSON snapshot of the record’s full attributes before the change (stored when #store_snapshot is true).

whodunnit_snapshot

Display name of the actor at the time of the change.

actor_type / actor_id

Polymorphic reference to the actor record.

reason

Optional free-text reason string.

metadata

Arbitrary JSON hash (request IP, custom lambdas, etc.).

Constant Summary collapse

EVENTS =
%w[create update destroy].freeze
BLOB_COLUMNS =
%w[object_changes object metadata].freeze
PERIODS =
{ "1h" => 1.hour, "24h" => 24.hours, "7d" => 7.days }.freeze
ENCRYPTION_MARKER =
"__ral_enc__"

Event scopes collapse

Actor / resource scopes collapse

Time scopes collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.configure_connection!Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



30
31
32
33
34
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 30

def self.configure_connection!
  return unless (opts = RailsAuditLog.connects_to)

  connects_to(**opts)
end

Instance Method Details

#by_actorActiveRecord::Relation

Entries written by a specific actor.

Examples:

AuditLogEntry.by_actor(current_user)

Parameters:

  • actor (ActiveRecord::Base)

    the actor record to filter by

Returns:

  • (ActiveRecord::Relation)


75
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 75

scope :by_actor, ->(actor) { where(actor_type: actor.class.name, actor_id: actor.id) }

#changed_attributesArray<String>

Returns the list of attribute (and association) names that changed in this entry, derived from the keys of object_changes.

Returns:

  • (Array<String>)


215
216
217
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 215

def changed_attributes
  object_changes&.keys || []
end

#created_eventsActiveRecord::Relation

Entries for create events.

Returns:

  • (ActiveRecord::Relation)


48
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 48

scope :created_events,  -> { where(event: "create") }

#destroyed_eventsActiveRecord::Relation

Entries for destroy events.

Returns:

  • (ActiveRecord::Relation)


56
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 56

scope :destroyed_events, -> { where(event: "destroy") }

#diffHash{String => Hash}

Returns object_changes in a named-key format convenient for display.

Examples:

entry.diff
# => { "title" => { from: "Old", to: "New" }, ... }

Returns:

  • (Hash{String => Hash})

    keys are attribute names; values are hashes with :from and :to keys



226
227
228
229
230
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 226

def diff
  return {} unless object_changes

  object_changes.transform_values { |from_to| { from: from_to[0], to: from_to[1] } }
end

#for_periodActiveRecord::Relation

Entries within a named period. Valid keys: "1h", "24h", "7d".

Parameters:

  • period (String)

    one of PERIODS.keys

Returns:

  • (ActiveRecord::Relation)


114
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 114

scope :for_period, ->(period) { where(created_at: PERIODS[period].ago..) }

#for_resourceActiveRecord::Relation

Entries for a specific resource class or instance. Pass a class to get all entries for that type; pass an instance for one record.

Examples:

All entries for Article

AuditLogEntry.for_resource(Article)

All entries for one article

AuditLogEntry.for_resource(article)

Parameters:

  • resource (Class, ActiveRecord::Base)

Returns:

  • (ActiveRecord::Relation)


86
87
88
89
90
91
92
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 86

scope :for_resource, lambda { |resource|
  if resource.is_a?(Class)
    where(item_type: resource.name)
  else
    where(item_type: resource.class.name, item_id: resource.id)
  end
}

#nextAuditLogEntry?

Returns the entry immediately after this one in the version chain for the same record (higher id), or nil if this is the last entry.

Returns:



192
193
194
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 192

def next
  self.class.where(item_type: item_type, item_id: item_id).where("id > ?", id).order(id: :asc).first
end

#objectHash?

Returns the decrypted object snapshot hash. Transparent to callers.

Returns:

  • (Hash, nil)


207
208
209
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 207

def object
  decrypt_if_encrypted(super)
end

#object_changesHash?

Returns the decrypted object_changes hash. Transparent to callers —encrypted and non-encrypted entries behave identically.

Returns:

  • (Hash, nil)


200
201
202
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 200

def object_changes
  decrypt_if_encrypted(super)
end

#previousAuditLogEntry?

Returns the entry immediately before this one in the version chain for the same record (lower id), or nil if this is the first entry.

Returns:



184
185
186
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 184

def previous
  self.class.where(item_type: item_type, item_id: item_id).where("id < ?", id).order(id: :desc).first
end

#reifyActiveRecord::Base?

Reconstructs and returns the record’s state before this entry’s change. Uses the object snapshot when available; falls back to deriving prior state from object_changes (or the live record for update entries).

Returns:

  • (ActiveRecord::Base, nil)

    an unpersisted instance; nil for create entries (there is no prior state)



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 149

def reify
  return nil if event == "create"

  klass = item_type.constantize

  if object.present?
    instance = klass.new
    instance.assign_attributes(object.except("id"))
    instance.id = object.fetch("id") { item_id }
    return instance
  end

  # Fallback: diff-only mode or entries recorded before snapshot support.
  # Filter to column names so association-change entries (e.g. tags, comments)
  # don't get assigned to the record as if they were scalar attributes.
  column_names = klass.column_names.map(&:to_s)
  from_attrs = (object_changes || {})
    .select { |k, _| column_names.include?(k) }
    .transform_values { |from_to| from_to[0] }

  if event == "update"
    record = klass.find_by(id: item_id)
    from_attrs = record.attributes.merge(from_attrs) if record
  end

  instance = klass.new
  instance.assign_attributes(from_attrs.except("id"))
  instance.id = from_attrs.fetch("id") { item_id }
  instance
end

#sinceActiveRecord::Relation

Entries created at or after time.

Parameters:

  • time (Time)

Returns:

  • (ActiveRecord::Relation)


102
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 102

scope :since, ->(time) { where(created_at: time..) }

#slimActiveRecord::Relation

Omits the three JSON blob columns (object_changes, object, metadata) from the SELECT. Use on index/listing queries where blobs are not displayed to reduce I/O and avoid deserializing large payloads.

Returns:

  • (ActiveRecord::Relation)


123
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 123

scope :slim, -> { select(column_names - BLOB_COLUMNS) }

#touchingActiveRecord::Relation

Entries where object_changes contains a key matching attribute. Uses json_extract on SQLite/MySQL and ->> on PostgreSQL.

Examples:

AuditLogEntry.touching(:title)
post.audit_log_entries.updated_events.touching(:published_at)

Parameters:

  • attribute (Symbol, String)

    the attribute name to filter on

Returns:

  • (ActiveRecord::Relation)


133
134
135
136
137
138
139
140
141
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 133

scope :touching, ->(attribute) {
  if connection.adapter_name =~ /PostgreSQL/i
    # :nocov:
    where("object_changes->>? IS NOT NULL", attribute.to_s)
    # :nocov:
  else
    where("json_extract(object_changes, ?) IS NOT NULL", "$.#{attribute}")
  end
}

#untilActiveRecord::Relation

Entries created at or before time.

Parameters:

  • time (Time)

Returns:

  • (ActiveRecord::Relation)


108
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 108

scope :until, ->(time) { where(created_at: ..time) }

#updated_eventsActiveRecord::Relation

Entries for update events.

Returns:

  • (ActiveRecord::Relation)


52
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 52

scope :updated_events,  -> { where(event: "update") }