Class: SnailTrail::RecordTrail
- Inherits:
-
Object
- Object
- SnailTrail::RecordTrail
- Defined in:
- lib/snail_trail/record_trail.rb
Overview
Represents the “snail trail” for a single record.
Instance Method Summary collapse
-
#clear_rolled_back_versions ⇒ Object
Invoked after rollbacks to ensure versions records are not created for changes that never actually took place.
-
#clear_version_instance ⇒ Object
Invoked via`after_update` callback for when a previous version is reified and then saved.
-
#initialize(record) ⇒ RecordTrail
constructor
A new instance of RecordTrail.
-
#live? ⇒ Boolean
Returns true if this instance is the current, live one; returns false if this instance came from a previous version.
-
#next_version ⇒ Object
Returns the object (not a Version) as it became next.
-
#originator ⇒ Object
Returns who put ‘@record` into its current state.
-
#previous_version ⇒ Object
Returns the object (not a Version) as it was most recently.
- #record_create ⇒ Object
-
#record_destroy(recording_order) ⇒ Object
private
‘recording_order` is “after” or “before”.
-
#record_performance_data(force = false) ⇒ void
When in ‘performance_mode?`, this will save the `performance_mode_data` array to the “versions” table In batches of 1_000.
-
#record_update(force:, in_after_callback:, is_touch:) ⇒ Object
private
snail_trail-association_tracking.
-
#reset_timestamp_attrs_for_update_if_needed ⇒ Object
Invoked via callback when a user attempts to persist a reified ‘Version`.
-
#save_version? ⇒ Boolean
private
AR callback.
-
#save_with_version(in_after_callback: false, **options) ⇒ Object
Save, and create a version record regardless of options such as ‘:on`, `:if`, or `:unless`.
- #source_version ⇒ Object
-
#update_column(name, value) ⇒ Object
Like the ‘update_column` method from `ActiveRecord::Persistence`, but also creates a version to record those changes.
-
#update_columns(attributes) ⇒ Object
Like the ‘update_columns` method from `ActiveRecord::Persistence`, but also creates a version to record those changes.
-
#version_at(timestamp, reify_options = {}) ⇒ Object
Returns the object (not a Version) as it was at the given timestamp.
-
#versions_between(start_time, end_time) ⇒ Object
Returns the objects (not Versions) as they were between the given times.
Constructor Details
#initialize(record) ⇒ RecordTrail
Returns a new instance of RecordTrail.
10 11 12 |
# File 'lib/snail_trail/record_trail.rb', line 10 def initialize(record) @record = record end |
Instance Method Details
#clear_rolled_back_versions ⇒ Object
Invoked after rollbacks to ensure versions records are not created for changes that never actually took place. Optimization: Use lazy ‘reset` instead of eager `reload` because, in many use cases, the association will not be used.
18 19 20 |
# File 'lib/snail_trail/record_trail.rb', line 18 def clear_rolled_back_versions versions.reset end |
#clear_version_instance ⇒ Object
Invoked via`after_update` callback for when a previous version is reified and then saved.
24 25 26 |
# File 'lib/snail_trail/record_trail.rb', line 24 def clear_version_instance @record.send(:"#{@record.class.version_association_name}=", nil) end |
#live? ⇒ Boolean
Returns true if this instance is the current, live one; returns false if this instance came from a previous version.
30 31 32 |
# File 'lib/snail_trail/record_trail.rb', line 30 def live? source_version.nil? end |
#next_version ⇒ Object
Returns the object (not a Version) as it became next. NOTE: if self (the item) was not reified from a version, i.e. it is the
"live" item, we return nil. Perhaps we should return self instead?
37 38 39 40 41 42 |
# File 'lib/snail_trail/record_trail.rb', line 37 def next_version subsequent_version = source_version.next subsequent_version ? subsequent_version.reify : @record.class.find(@record.id) rescue StandardError # TODO: Rescue something more specific nil end |
#originator ⇒ Object
Returns who put ‘@record` into its current state.
47 48 49 |
# File 'lib/snail_trail/record_trail.rb', line 47 def originator (source_version || versions.last).try(:whodunnit) end |
#previous_version ⇒ Object
Returns the object (not a Version) as it was most recently.
54 55 56 |
# File 'lib/snail_trail/record_trail.rb', line 54 def previous_version (source_version ? source_version.previous : versions.last).try(:reify) end |
#record_create ⇒ Object
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/snail_trail/record_trail.rb', line 58 def record_create return unless enabled? version = build_version_on_create(in_after_callback: true) if ::SnailTrail.request.performance_mode? ::SnailTrail.request.performance_mode_data << version.attributes.symbolize_keys else begin version.save! update_transaction_id(version) # Because the version object was created using version_class.new instead # of versions_assoc.build?, the association cache is unaware. So, we # invalidate the `versions` association cache with `reset`. versions.reset rescue StandardError => e handle_version_errors e, version, :create end end end |
#record_destroy(recording_order) ⇒ 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.
‘recording_order` is “after” or “before”. See ModelConfig#on_destroy.
snail_trail-association_tracking
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 |
# File 'lib/snail_trail/record_trail.rb', line 86 def record_destroy(recording_order) return unless enabled? && !@record.new_record? in_after_callback = recording_order == "after" event = Events::Destroy.new(@record, in_after_callback) data = event.data.merge(data_for_destroy) version = @record.class.snail_trail.version_class.new(data) if ::SnailTrail.request.performance_mode? ::SnailTrail.request.performance_mode_data << version.attributes.symbolize_keys else begin version.save! assign_and_reset_version_association(version) if version && version.respond_to?(:errors) && version.errors.empty? update_transaction_id(version) end version rescue StandardError => e handle_version_errors e, version, :destroy end end end |
#record_performance_data(force = false) ⇒ void
This method returns an undefined value.
When in ‘performance_mode?`, this will save the `performance_mode_data` array to the “versions” table
In batches of 1_000
157 158 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 |
# File 'lib/snail_trail/record_trail.rb', line 157 def record_performance_data(force = false) return unless ::SnailTrail.request.performance_mode? all_data = ::SnailTrail.request.performance_mode_data first_data = all_data.shift version = @record.class.snail_trail.version_class.create(first_data) update_transaction_id(version, force) all_data.each_slice(1_000) do |datas| = Time.now.utc.strftime("%Y-%m-%d %H:%M:%S") datas.each do |d| add_transaction_id_to(d) d[:created_at] = d[:event] = "batch_#{d[:event]}" end sql = ["INSERT INTO #{version.class.table_name} (#{datas.first.keys.join(", ")}) VALUES"] datas.map do |data| values = data.map { |k, v| @record.class.connection.quote(v.is_a?(Hash) ? JSON.generate(v) : v) } sql << "(#{values.join(", ")})," end sql.last.chop! @record.class.snail_trail.version_class.connection.execute(sql.join("\n")) end end |
#record_update(force:, in_after_callback:, is_touch:) ⇒ 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.
snail_trail-association_tracking
121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/snail_trail/record_trail.rb', line 121 def record_update(force:, in_after_callback:, is_touch:) return unless enabled? version = build_version_on_update( force: force, in_after_callback: in_after_callback, is_touch: is_touch ) return unless version if ::SnailTrail.request.performance_mode? ::SnailTrail.request.performance_mode_data << version.attributes.symbolize_keys else begin version.save! # Because the version object was created using version_class.new instead # of versions_assoc.build?, the association cache is unaware. So, we # invalidate the `versions` association cache with `reset`. versions.reset if version && version.respond_to?(:errors) && version.errors.empty? update_transaction_id(version) end version rescue StandardError => e handle_version_errors e, version, :update end end end |
#reset_timestamp_attrs_for_update_if_needed ⇒ Object
Invoked via callback when a user attempts to persist a reified ‘Version`.
190 191 192 193 194 195 |
# File 'lib/snail_trail/record_trail.rb', line 190 def return if live? @record.send(:timestamp_attributes_for_update_in_model).each do |column| @record.send(:"restore_#{column}!") end end |
#save_version? ⇒ Boolean
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.
AR callback.
199 200 201 202 203 |
# File 'lib/snail_trail/record_trail.rb', line 199 def save_version? if_condition = @record.[:if] unless_condition = @record.[:unless] (if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record) end |
#save_with_version(in_after_callback: false, **options) ⇒ Object
Save, and create a version record regardless of options such as ‘:on`, `:if`, or `:unless`.
‘in_after_callback`: Indicates if this method is being called within an
`after` callback. Defaults to `false`.
‘options`: Optional arguments passed to `save`.
This is an “update” event. That is, we record the same data we would in the case of a normal AR ‘update`.
218 219 220 221 222 223 |
# File 'lib/snail_trail/record_trail.rb', line 218 def save_with_version(in_after_callback: false, **) ::SnailTrail.request(enabled: false) do @record.save(**) end record_update(force: true, in_after_callback: in_after_callback, is_touch: false) end |
#source_version ⇒ Object
205 206 207 |
# File 'lib/snail_trail/record_trail.rb', line 205 def source_version version end |
#update_column(name, value) ⇒ Object
Like the ‘update_column` method from `ActiveRecord::Persistence`, but also creates a version to record those changes.
228 229 230 |
# File 'lib/snail_trail/record_trail.rb', line 228 def update_column(name, value) update_columns(name => value) end |
#update_columns(attributes) ⇒ Object
Like the ‘update_columns` method from `ActiveRecord::Persistence`, but also creates a version to record those changes.
235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/snail_trail/record_trail.rb', line 235 def update_columns(attributes) # `@record.update_columns` skips dirty-tracking, so we can't just use # `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`. # We need to build our own hash with the changes that will be made # directly to the database. changes = {} attributes.each do |k, v| changes[k] = [@record[k], v] end @record.update_columns(attributes) record_update_columns(changes) end |
#version_at(timestamp, reify_options = {}) ⇒ Object
Returns the object (not a Version) as it was at the given timestamp.
249 250 251 252 253 254 255 |
# File 'lib/snail_trail/record_trail.rb', line 249 def version_at(, = {}) # Because a version stores how its object looked *before* the change, # we need to look for the first version created *after* the timestamp. v = versions.subsequent(, true).first return v.reify() if v @record unless @record.destroyed? end |
#versions_between(start_time, end_time) ⇒ Object
Returns the objects (not Versions) as they were between the given times.
258 259 260 261 |
# File 'lib/snail_trail/record_trail.rb', line 258 def versions_between(start_time, end_time) versions = send(@record.class.versions_association_name).between(start_time, end_time) versions.collect { |version| version_at(version.created_at) } end |