Module: Logidze::Model

Extended by:
ActiveSupport::Concern
Defined in:
lib/logidze/model.rb

Overview

Extends model with methods to browse history

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

TIME_FACTOR =

Use this to convert Ruby time to milliseconds

1_000

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#logidze_requested_tsObject

Returns the value of attribute logidze_requested_ts.



85
86
87
# File 'lib/logidze/model.rb', line 85

def logidze_requested_ts
  @logidze_requested_ts
end

Instance Method Details

#association(name) ⇒ Object

rubocop: disable Metrics/MethodLength



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/logidze/model.rb', line 217

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

#at(time: nil, version: nil) ⇒ Object

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



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/logidze/model.rb', line 91

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

#at!(time: nil, version: nil) ⇒ Object

Revert record to the version at specified time (without saving to DB)

Raises:

  • (ArgumentError)


128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/logidze/model.rb', line 128

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

#at_version(version) ⇒ Object

Return a dirty copy of specified version of record



144
145
146
147
148
149
150
151
152
# File 'lib/logidze/model.rb', line 144

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

#at_version!(version) ⇒ Object

Revert record to the specified version (without saving to DB)

Raises:

  • (ArgumentError)


155
156
157
158
159
160
161
162
# File 'lib/logidze/model.rb', line 155

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

#create_logidze_snapshot!(**opts) ⇒ Object



255
256
257
258
259
# File 'lib/logidze/model.rb', line 255

def create_logidze_snapshot!(**opts)
  self.class.where(self.class.primary_key => id).create_logidze_snapshot(**opts)

  reload_log_data
end

#diff_from(version: nil, time: nil) ⇒ Object

Return diff object representing changes since specified time.

Examples:


post.diff_from(time: 2.days.ago) # or post.diff_from(version: 2)
#=> { "id" => 1, "changes" => { "title" => { "old" => "Hello!", "new" => "World" } } }


170
171
172
173
174
175
176
177
178
179
# File 'lib/logidze/model.rb', line 170

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

#log_sizeObject

rubocop: enable Metrics/MethodLength



241
242
243
# File 'lib/logidze/model.rb', line 241

def log_size
  log_data&.size || 0
end

#logidze_versions(reverse: false, include_self: false) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/logidze/model.rb', line 112

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

#raw_log_dataObject



261
262
263
# File 'lib/logidze/model.rb', line 261

def raw_log_data
  read_attribute_before_type_cast(:log_data)
end

#redo!Object

Restore record to the future version (if ‘undo!` was applied) Return false if no future version found, otherwise return updated record.



192
193
194
195
196
197
# File 'lib/logidze/model.rb', line 192

def redo!
  version = log_data.next_version
  return false if version.nil?

  switch_to!(version.version)
end

#reload_log_dataObject

Loads log_data field from the database, stores to the attributes hash and returns it



246
247
248
# File 'lib/logidze/model.rb', line 246

def reload_log_data
  self.log_data = self.class.where(self.class.primary_key => id).pluck(:"#{self.class.table_name}.log_data").first
end

#reset_log_dataObject

Nullify log_data column for a single record



251
252
253
# File 'lib/logidze/model.rb', line 251

def reset_log_data
  self.class.without_logging { update_column(:log_data, nil) }
end

#switch_to!(version, append: Logidze.append_on_undo) ⇒ Object

Restore record to the specified version. Return false if version is unknown.

Raises:

  • (ArgumentError)


201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/logidze/model.rb', line 201

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

#undo!(append: Logidze.append_on_undo) ⇒ Object

Restore record to the previous version. Return false if no previous version found, otherwise return updated record.



183
184
185
186
187
188
# File 'lib/logidze/model.rb', line 183

def undo!(append: Logidze.append_on_undo)
  version = log_data.previous_version
  return false if version.nil?

  switch_to!(version.version, append: append)
end