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>)


225
226
227
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 225

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



236
237
238
239
240
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 236

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)


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

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
}

#for_tenantActiveRecord::Relation

Entries belonging to a specific tenant. Composable with all other scopes.

Examples:

AuditLogEntry.for_tenant("acme")
AuditLogEntry.for_tenant(Current.tenant_id).updated_events

Parameters:

  • id (String, Integer)

    the tenant identifier stored in tenant_id

Returns:

  • (ActiveRecord::Relation)


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

scope :for_tenant, ->(id) { where(tenant_id: id) }

#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:



202
203
204
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 202

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)


217
218
219
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 217

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)


210
211
212
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 210

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:



194
195
196
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 194

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)



159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 159

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)


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

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)


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

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)


143
144
145
146
147
148
149
150
151
# File 'app/models/rails_audit_log/audit_log_entry.rb', line 143

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)


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

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") }