module Logidze::Model
Extends model with methods to browse history
Constants
- TIME_FACTOR
Use this to convert Ruby time to milliseconds
Attributes
Public Instance Methods
rubocop: disable Metrics/MethodLength
# File lib/logidze/model.rb, line 203 def association(name) association = super return association unless Logidze.associations_versioning should_apply_logidze = logidze_past? && association.klass.respond_to?(:has_logidze?) && !association.singleton_class.include?(Logidze::VersionedAssociation) return association unless should_apply_logidze association.singleton_class.prepend Logidze::VersionedAssociation if association.is_a? ActiveRecord::Associations::CollectionAssociation association.singleton_class.prepend( Logidze::VersionedAssociation::CollectionAssociation ) end association end
Return a dirty copy of record at specified time If time/version is less then the first version, then return nil. If time/version is greater then the last version, then return self. rubocop: disable Metrics/MethodLength
# File lib/logidze/model.rb, line 77 def at(time: nil, version: nil) return at_version(version) if version time = parse_time(time) unless log_data return Logidze.return_self_if_log_data_is_empty ? self : nil end return nil unless log_data.exists_ts?(time) if log_data.current_ts?(time) self.logidze_requested_ts = time return self end log_entry = log_data.find_by_time(time) build_dup(log_entry, time) end
Revert record to the version at specified time (without saving to DB)
# File lib/logidze/model.rb, line 114 def at!(time: nil, version: nil) return at_version!(version) if version raise ArgumentError, "#log_data is empty" unless log_data time = parse_time(time) return self if log_data.current_ts?(time) return false unless log_data.exists_ts?(time) version = log_data.find_by_time(time).version apply_diff(version, log_data.changes_to(version: version)) end
Return a dirty copy of specified version of record
# File lib/logidze/model.rb, line 130 def at_version(version) return nil unless log_data return self if log_data.version == version log_entry = log_data.find_by_version(version) return nil unless log_entry build_dup(log_entry) end
Revert record to the specified version (without saving to DB)
# File lib/logidze/model.rb, line 141 def at_version!(version) raise ArgumentError, "#log_data is empty" unless log_data return self if log_data.version == version return false unless log_data.find_by_version(version) apply_diff(version, log_data.changes_to(version: version)) end
# File lib/logidze/model.rb, line 241 def create_logidze_snapshot!(**opts) self.class.where(self.class.primary_key => id).create_logidze_snapshot(**opts) reload_log_data end
Return diff object representing changes since specified time.
@example
post.diff_from(time: 2.days.ago) # or post.diff_from(version: 2) #=> { "id" => 1, "changes" => { "title" => { "old" => "Hello!", "new" => "World" } } }
# File lib/logidze/model.rb, line 156 def diff_from(version: nil, time: nil) time = parse_time(time) if time changes = log_data&.diff_from(time: time, version: version)&.tap do |v| deserialize_changes!(v) end || {} changes.delete_if { |k, _v| deleted_column?(k) } {"id" => id, "changes" => changes} end
rubocop: enable Metrics/MethodLength
# File lib/logidze/model.rb, line 227 def log_size log_data&.size || 0 end
# File lib/logidze/model.rb, line 98 def logidze_versions(reverse: false, include_self: false) versions_meta = log_data.versions.dup if reverse versions_meta.reverse! versions_meta.shift unless include_self else versions_meta.pop unless include_self end Enumerator.new { |yielder| versions_meta.each { yielder << at(version: _1.version) } } end
Restore record to the future version (if ‘undo!` was applied) Return false if no future version found, otherwise return updated record.
# File lib/logidze/model.rb, line 178 def redo! version = log_data.next_version return false if version.nil? switch_to!(version.version) end
Loads log_data field from the database, stores to the attributes hash and returns it
# File lib/logidze/model.rb, line 232 def reload_log_data self.log_data = self.class.where(self.class.primary_key => id).pluck(:"#{self.class.table_name}.log_data").first end
Nullify log_data column for a single record
# File lib/logidze/model.rb, line 237 def reset_log_data self.class.without_logging { update_column(:log_data, nil) } end
Restore record to the specified version. Return false if version is unknown.
# File lib/logidze/model.rb, line 187 def switch_to!(version, append: Logidze.append_on_undo) raise ArgumentError, "#log_data is empty" unless log_data return false unless at_version(version) if append && version < log_version changes = log_data.changes_to(version: version) changes.each { |c, v| changes[c] = deserialize_value(c, v) } update!(changes) else at_version!(version) self.class.without_logging { save! } end end
Restore record to the previous version. Return false if no previous version found, otherwise return updated record.
# File lib/logidze/model.rb, line 169 def undo!(append: Logidze.append_on_undo) version = log_data.previous_version return false if version.nil? switch_to!(version.version, append: append) end
Protected Instance Methods
# File lib/logidze/model.rb, line 258 def apply_column_diff(column, value) return if deleted_column?(column) || column == "log_data" write_attribute column, deserialize_value(column, value) end
# File lib/logidze/model.rb, line 249 def apply_diff(version, diff) diff.each do |k, v| apply_column_diff(k, v) end log_data.version = version self end
# File lib/logidze/model.rb, line 264 def build_dup(log_entry, requested_ts = log_entry.time) object_at = dup object_at.apply_diff(log_entry.version, log_data.changes_to(version: log_entry.version)) object_at.id = id object_at.logidze_requested_ts = requested_ts object_at end
# File lib/logidze/model.rb, line 277 def deleted_column?(column) !@attributes.key?(column) end
# File lib/logidze/model.rb, line 281 def deserialize_changes!(diff) diff.each do |k, v| v["old"] = deserialize_value(k, v["old"]) v["new"] = deserialize_value(k, v["new"]) end end
# File lib/logidze/model.rb, line 273 def deserialize_value(column, value) @attributes[column].type.deserialize(value) end
# File lib/logidze/model.rb, line 288 def logidze_past? return false unless @logidze_requested_ts @logidze_requested_ts < Time.now.to_i * TIME_FACTOR end
# File lib/logidze/model.rb, line 294 def parse_time(ts) case ts when Numeric ts.to_i when String (Time.parse(ts).to_r * TIME_FACTOR).to_i when Date (ts.to_time.to_r * TIME_FACTOR).to_i when Time (ts.to_r * TIME_FACTOR).to_i end end