mongo_trails 10.3.1 → 14.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +53 -0
  3. data/.gitignore +2 -0
  4. data/.rubocop.yml +19 -0
  5. data/.ruby-version +1 -0
  6. data/Appraisals +10 -4
  7. data/Gemfile +8 -1
  8. data/README.md +27 -6
  9. data/Rakefile +17 -0
  10. data/gemfiles/rails_7.gemfile +15 -0
  11. data/gemfiles/rails_7.gemfile.lock +212 -0
  12. data/gemfiles/rails_8.gemfile +15 -0
  13. data/gemfiles/rails_8.gemfile.lock +213 -0
  14. data/gemfiles/rails_8_1.gemfile +15 -0
  15. data/gemfiles/rails_8_1.gemfile.lock +143 -0
  16. data/lib/mongo_trails/callback_propagation.rb +77 -0
  17. data/lib/mongo_trails/config.rb +7 -24
  18. data/lib/mongo_trails/events/base.rb +5 -308
  19. data/lib/mongo_trails/model_config.rb +1 -232
  20. data/lib/mongo_trails/mongo_support/config.rb +7 -1
  21. data/lib/mongo_trails/mongo_support/criteria.rb +7 -0
  22. data/lib/mongo_trails/mongo_support/version.rb +242 -19
  23. data/lib/mongo_trails/mongo_support/version_commit_wrap.rb +37 -0
  24. data/lib/mongo_trails/mongo_support/write_version_worker.rb +11 -0
  25. data/lib/mongo_trails/record_trail.rb +21 -262
  26. data/lib/mongo_trails/version.rb +5 -0
  27. data/lib/mongo_trails/version_concern.rb +4 -293
  28. data/lib/mongo_trails.rb +13 -149
  29. data/mongo_trails.gemspec +19 -20
  30. metadata +50 -85
  31. data/.travis.yml +0 -13
  32. data/Gemfile.lock +0 -62
  33. data/gemfiles/rails_5.gemfile +0 -9
  34. data/gemfiles/rails_5.gemfile.lock +0 -63
  35. data/gemfiles/rails_6.gemfile +0 -9
  36. data/gemfiles/rails_6.gemfile.lock +0 -63
  37. data/lib/mongo_trails/attribute_serializers/README.md +0 -10
  38. data/lib/mongo_trails/attribute_serializers/attribute_serializer_factory.rb +0 -27
  39. data/lib/mongo_trails/attribute_serializers/cast_attribute_serializer.rb +0 -51
  40. data/lib/mongo_trails/attribute_serializers/object_attribute.rb +0 -41
  41. data/lib/mongo_trails/attribute_serializers/object_changes_attribute.rb +0 -44
  42. data/lib/mongo_trails/cleaner.rb +0 -60
  43. data/lib/mongo_trails/compatibility.rb +0 -51
  44. data/lib/mongo_trails/events/create.rb +0 -32
  45. data/lib/mongo_trails/events/destroy.rb +0 -42
  46. data/lib/mongo_trails/events/update.rb +0 -60
  47. data/lib/mongo_trails/frameworks/cucumber.rb +0 -33
  48. data/lib/mongo_trails/frameworks/rails/controller.rb +0 -109
  49. data/lib/mongo_trails/frameworks/rails/engine.rb +0 -43
  50. data/lib/mongo_trails/frameworks/rails.rb +0 -4
  51. data/lib/mongo_trails/frameworks/rspec/helpers.rb +0 -29
  52. data/lib/mongo_trails/frameworks/rspec.rb +0 -43
  53. data/lib/mongo_trails/has_paper_trail.rb +0 -86
  54. data/lib/mongo_trails/queries/versions/where_object.rb +0 -65
  55. data/lib/mongo_trails/queries/versions/where_object_changes.rb +0 -75
  56. data/lib/mongo_trails/record_history.rb +0 -51
  57. data/lib/mongo_trails/reifier.rb +0 -130
  58. data/lib/mongo_trails/request.rb +0 -166
  59. data/lib/mongo_trails/serializers/json.rb +0 -46
  60. data/lib/mongo_trails/serializers/yaml.rb +0 -43
  61. data/lib/mongo_trails/type_serializers/postgres_array_serializer.rb +0 -48
  62. data/lib/mongo_trails/version_number.rb +0 -23
@@ -1,126 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mongo_trails/events/create"
4
- require "mongo_trails/events/destroy"
5
- require "mongo_trails/events/update"
6
-
7
3
  module PaperTrail
8
- # Represents the "paper trail" for a single record.
9
4
  class RecordTrail
10
- RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
11
-
12
- def initialize(record)
13
- @record = record
14
- end
15
-
16
- # Invoked after rollbacks to ensure versions records are not created for
17
- # changes that never actually took place. Optimization: Use lazy `reset`
18
- # instead of eager `reload` because, in many use cases, the association will
19
- # not be used.
20
- def clear_rolled_back_versions
21
- versions_reset
22
- end
23
-
24
- def versions_reset
25
- @record.class.paper_trail.version_class.reset
26
- end
27
-
28
- # Invoked via`after_update` callback for when a previous version is
29
- # reified and then saved.
30
- def clear_version_instance
31
- @record.send("#{@record.class.version_association_name}=", nil)
32
- end
33
-
34
- # Is PT enabled for this particular record?
35
- # @api private
36
- def enabled?
37
- PaperTrail.enabled? &&
38
- PaperTrail.request.enabled? &&
39
- PaperTrail.request.enabled_for_model?(@record.class)
40
- end
41
-
42
- # Returns true if this instance is the current, live one;
43
- # returns false if this instance came from a previous version.
44
- def live?
45
- source_version.nil?
46
- end
47
-
48
- # Returns the object (not a Version) as it became next.
49
- # NOTE: if self (the item) was not reified from a version, i.e. it is the
50
- # "live" item, we return nil. Perhaps we should return self instead?
51
- def next_version
52
- subsequent_version = source_version.next
53
- subsequent_version ? subsequent_version.reify : @record.class.find(@record.id)
54
- rescue StandardError # TODO: Rescue something more specific
55
- nil
56
- end
57
-
58
- # Returns who put `@record` into its current state.
59
- #
60
- # @api public
61
- def originator
62
- (source_version || versions.last).try(:whodunnit)
63
- end
64
-
65
- # Returns the object (not a Version) as it was most recently.
66
- #
67
- # @api public
68
- def previous_version
69
- (source_version ? source_version.previous : versions.last).try(:reify)
70
- end
71
-
72
5
  def record_create
73
6
  return unless enabled?
74
7
 
75
- build_version_on_create(in_after_callback: true).tap do |version|
76
- version.save!
77
- # Because the version object was created using version_class.new instead
78
- # of versions_assoc.build?, the association cache is unaware. So, we
79
- # invalidate the `versions` association cache with `reset`.
80
- versions_reset
81
- end
82
- end
8
+ version = build_version_on_create(in_after_callback: true)
9
+ return if exceeds_record_size_limit?(version)
83
10
 
84
- # PT-AT extends this method to add its transaction id.
85
- #
86
- # @api private
87
- def data_for_create
88
- {}
11
+ version.mongo_trails_source_item = @record
12
+ version.save_version!
89
13
  end
90
14
 
91
- # `recording_order` is "after" or "before". See ModelConfig#on_destroy.
92
- #
93
- # @api private
94
- # @return - The created version object, so that plugins can use it, e.g.
95
- # paper_trail-association_tracking
96
15
  def record_destroy(recording_order)
97
16
  return unless enabled? && !@record.new_record?
98
- in_after_callback = recording_order == "after"
99
- event = Events::Destroy.new(@record, in_after_callback)
100
17
 
101
- # Merge data from `Event` with data from PT-AT. We no longer use
102
- # `data_for_destroy` but PT-AT still does.
18
+ in_after_callback = recording_order == 'after'
19
+ event = Events::Destroy.new(@record, in_after_callback)
103
20
  data = event.data.merge(data_for_destroy)
104
21
 
105
- version = @record.class.paper_trail.version_class.create(data)
106
- if version.errors.any?
107
- log_version_errors(version, :destroy)
108
- else
109
- assign_and_reset_version_association(version)
110
- version
111
- end
112
- end
22
+ version = @record.class.paper_trail.version_class.new(data)
23
+ return if exceeds_record_size_limit?(version)
113
24
 
114
- # PT-AT extends this method to add its transaction id.
115
- #
116
- # @api private
117
- def data_for_destroy
118
- {}
25
+ version.mongo_trails_source_item = @record
26
+ version.save_version
119
27
  end
120
28
 
121
- # @api private
122
- # @return - The created version object, so that plugins can use it, e.g.
123
- # paper_trail-association_tracking
124
29
  def record_update(force:, in_after_callback:, is_touch:)
125
30
  return unless enabled?
126
31
 
@@ -129,176 +34,30 @@ module PaperTrail
129
34
  in_after_callback: in_after_callback,
130
35
  is_touch: is_touch
131
36
  )
132
- return unless version
37
+ return unless version && !exceeds_record_size_limit?(version)
133
38
 
134
- if version.save
135
- # Because the version object was created using version_class.new instead
136
- # of versions_assoc.build?, the association cache is unaware. So, we
137
- # invalidate the `versions` association cache with `reset`.
138
- versions_reset
139
- version
140
- else
141
- log_version_errors(version, :update)
142
- end
39
+ version.mongo_trails_source_item = @record
40
+ version.save_version
143
41
  end
144
42
 
145
- # PT-AT extends this method to add its transaction id.
146
- #
147
- # @api private
148
- def data_for_update
149
- {}
150
- end
151
-
152
- # @api private
153
- # @return - The created version object, so that plugins can use it, e.g.
154
- # paper_trail-association_tracking
155
43
  def record_update_columns(changes)
156
44
  return unless enabled?
157
- event = Events::Update.new(@record, false, false, changes)
158
45
 
159
- # Merge data from `Event` with data from PT-AT. We no longer use
160
- # `data_for_update_columns` but PT-AT still does.
46
+ event = Events::Update.new(@record, false, false, changes)
161
47
  data = event.data.merge(data_for_update_columns)
162
-
163
48
  versions_assoc = @record.send(@record.class.versions_association_name)
164
- version = versions_assoc.create(data)
165
- if version.errors.any?
166
- log_version_errors(version, :update)
167
- else
168
- version
169
- end
170
- end
49
+ version = versions_assoc.new(data)
50
+ return if exceeds_record_size_limit?(version)
171
51
 
172
- # PT-AT extends this method to add its transaction id.
173
- #
174
- # @api private
175
- def data_for_update_columns
176
- {}
177
- end
178
-
179
- # Invoked via callback when a user attempts to persist a reified
180
- # `Version`.
181
- def reset_timestamp_attrs_for_update_if_needed
182
- return if live?
183
- @record.send(:timestamp_attributes_for_update_in_model).each do |column|
184
- @record.send("restore_#{column}!")
185
- end
186
- end
187
-
188
- # AR callback.
189
- # @api private
190
- def save_version?
191
- if_condition = @record.paper_trail_options[:if]
192
- unless_condition = @record.paper_trail_options[:unless]
193
- (if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record)
194
- end
195
-
196
- def source_version
197
- version
198
- end
199
-
200
- # Save, and create a version record regardless of options such as `:on`,
201
- # `:if`, or `:unless`.
202
- #
203
- # Arguments are passed to `save`.
204
- #
205
- # This is an "update" event. That is, we record the same data we would in
206
- # the case of a normal AR `update`.
207
- def save_with_version(*args)
208
- ::PaperTrail.request(enabled: false) do
209
- @record.save(*args)
210
- end
211
- record_update(force: true, in_after_callback: false, is_touch: false)
212
- end
213
-
214
- # Like the `update_column` method from `ActiveRecord::Persistence`, but also
215
- # creates a version to record those changes.
216
- # @api public
217
- def update_column(name, value)
218
- update_columns(name => value)
219
- end
220
-
221
- # Like the `update_columns` method from `ActiveRecord::Persistence`, but also
222
- # creates a version to record those changes.
223
- # @api public
224
- def update_columns(attributes)
225
- # `@record.update_columns` skips dirty-tracking, so we can't just use
226
- # `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`.
227
- # We need to build our own hash with the changes that will be made
228
- # directly to the database.
229
- changes = {}
230
- attributes.each do |k, v|
231
- changes[k] = [@record[k], v]
232
- end
233
- @record.update_columns(attributes)
234
- record_update_columns(changes)
235
- end
236
-
237
- # Returns the object (not a Version) as it was at the given timestamp.
238
- def version_at(timestamp, reify_options = {})
239
- # Because a version stores how its object looked *before* the change,
240
- # we need to look for the first version created *after* the timestamp.
241
- v = versions.subsequent(timestamp, true).first
242
- return v.reify(reify_options) if v
243
- @record unless @record.destroyed?
244
- end
245
-
246
- # Returns the objects (not Versions) as they were between the given times.
247
- def versions_between(start_time, end_time)
248
- versions = send(@record.class.versions_association_name).between(start_time, end_time)
249
- versions.collect { |version| version_at(version.created_at) }
52
+ version.mongo_trails_source_item = @record
53
+ version.save_version
250
54
  end
251
55
 
252
56
  private
253
57
 
254
- # @api private
255
- def assign_and_reset_version_association(version)
256
- @record.send("#{@record.class.version_association_name}=", version)
257
- @record.send(@record.class.versions_association_name).reset
258
- end
259
-
260
- # @api private
261
- def build_version_on_create(in_after_callback:)
262
- event = Events::Create.new(@record, in_after_callback)
263
-
264
- # Merge data from `Event` with data from PT-AT. We no longer use
265
- # `data_for_create` but PT-AT still does.
266
- data = event.data.merge!(data_for_create)
267
-
268
- # Pure `version_class.new` reduces memory usage compared to `versions_assoc.build`
269
- @record.class.paper_trail.version_class.new(data)
270
- end
271
-
272
- # @api private
273
- def build_version_on_update(force:, in_after_callback:, is_touch:)
274
- event = Events::Update.new(@record, in_after_callback, is_touch, nil)
275
- return unless force || event.changed_notably?
276
-
277
- # Merge data from `Event` with data from PT-AT. We no longer use
278
- # `data_for_update` but PT-AT still does. To save memory, we use `merge!`
279
- # instead of `merge`.
280
- data = event.data.merge!(data_for_update)
281
-
282
- # Using `version_class.new` reduces memory usage compared to
283
- # `versions_assoc.build`. It's a trade-off though. We have to clear
284
- # the association cache (see `versions.reset`) and that could cause an
285
- # additional query in certain applications.
286
- @record.class.paper_trail.version_class.new(data)
287
- end
288
-
289
- def log_version_errors(version, action)
290
- version.logger&.warn(
291
- "Unable to create version for #{action} of #{@record.class.name}" \
292
- "##{@record.id}: " + version.errors.full_messages.join(", ")
293
- )
294
- end
295
-
296
- def version
297
- @record.public_send(@record.class.version_association_name)
298
- end
299
-
300
- def versions
301
- @record.public_send(@record.class.versions_association_name)
58
+ def exceeds_record_size_limit?(version)
59
+ size_limit = PaperTrail.config.mongo_trails_config&.dig(:record_size_limit)
60
+ size_limit && size_limit.to_i < version.to_json.to_s.bytesize
302
61
  end
303
62
  end
304
63
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MongoTrails
4
+ VERSION = '14.0.0'
5
+ end
@@ -1,336 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "mongo_trails/attribute_serializers/object_changes_attribute"
4
- require "mongo_trails/queries/versions/where_object"
5
- require "mongo_trails/queries/versions/where_object_changes"
6
-
7
3
  module PaperTrail
8
- # Originally, PaperTrail did not provide this module, and all of this
9
- # functionality was in `PaperTrail::Version`. That model still exists (and is
10
- # used by most apps) but by moving the functionality to this module, people
11
- # can include this concern instead of sub-classing the `Version` model.
12
4
  module VersionConcern
13
5
  extend ::ActiveSupport::Concern
14
6
 
15
- # :nodoc:
16
7
  module ClassMethods
17
- def item_subtype_column_present?
18
- column_names.include?("item_subtype")
19
- end
20
-
21
8
  def with_item_keys(item_type, item_id)
22
- where(item_type: item_type).and(item_id: item_id)
23
- end
24
-
25
- def creates
26
- where event: "create"
27
- end
28
-
29
- def updates
30
- where event: "update"
31
- end
32
-
33
- def destroys
34
- where event: "destroy"
35
- end
36
-
37
- def not_creates
38
- where "event <> ?", "create"
9
+ where(item_type: item_type, item_id: item_id)
39
10
  end
40
11
 
41
12
  def between(start_time, end_time)
42
- where(:created_at.gt => start_time).and(:created_at.lt => end_time).order(timestamp_sort_order)
13
+ where(:created_at.gt => start_time, :created_at.lt => end_time).order(timestamp_sort_order)
43
14
  end
44
15
 
45
- # Defaults to using the primary key as the secondary sort order if
46
- # possible.
47
- def timestamp_sort_order(direction = "asc")
16
+ def timestamp_sort_order(direction = 'asc')
48
17
  { created_at: direction.downcase }
49
18
  end
50
19
 
51
- # Given a hash of attributes like `name: 'Joan'`, query the
52
- # `versions.objects` column.
53
- #
54
- # ```
55
- # SELECT "versions".*
56
- # FROM "versions"
57
- # WHERE ("versions"."object" LIKE '%
58
- # name: Joan
59
- # %')
60
- # ```
61
- #
62
- # This is useful for finding versions where a given attribute had a given
63
- # value. Imagine, in the example above, that Joan had changed her name
64
- # and we wanted to find the versions before that change.
65
- #
66
- # Based on the data type of the `object` column, the appropriate SQL
67
- # operator is used. For example, a text column will use `like`, and a
68
- # jsonb column will use `@>`.
69
- #
70
- # @api public
71
- def where_object(args = {})
72
- raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
73
- Queries::Versions::WhereObject.new(self, args).execute
74
- end
75
-
76
- # Given a hash of attributes like `name: 'Joan'`, query the
77
- # `versions.objects_changes` column.
78
- #
79
- # ```
80
- # SELECT "versions".*
81
- # FROM "versions"
82
- # WHERE .. ("versions"."object_changes" LIKE '%
83
- # name:
84
- # - Joan
85
- # %' OR "versions"."object_changes" LIKE '%
86
- # name:
87
- # -%
88
- # - Joan
89
- # %')
90
- # ```
91
- #
92
- # This is useful for finding versions immediately before and after a given
93
- # attribute had a given value. Imagine, in the example above, that someone
94
- # changed their name to Joan and we wanted to find the versions
95
- # immediately before and after that change.
96
- #
97
- # Based on the data type of the `object` column, the appropriate SQL
98
- # operator is used. For example, a text column will use `like`, and a
99
- # jsonb column will use `@>`.
100
- #
101
- # @api public
102
- def where_object_changes(args = {})
103
- raise ArgumentError, "expected to receive a Hash" unless args.is_a?(Hash)
104
- Queries::Versions::WhereObjectChanges.new(self, args).execute
105
- end
106
-
107
- def primary_key_is_int?
108
- @primary_key_is_int ||= columns_hash[primary_key].type == :integer
109
- rescue StandardError # TODO: Rescue something more specific
110
- true
111
- end
112
-
113
- # Returns whether the `object` column is using the `json` type supported
114
- # by PostgreSQL.
115
20
  def object_col_is_json?
116
- # %i[json jsonb].include?(columns_hash["object"].type)
117
21
  true
118
22
  end
119
23
 
120
- # Returns whether the `object_changes` column is using the `json` type
121
- # supported by PostgreSQL.
122
24
  def object_changes_col_is_json?
123
- # %i[json jsonb].include?(columns_hash["object_changes"].try(:type))
124
25
  true
125
26
  end
126
27
 
127
- # Returns versions before `obj`.
128
- #
129
- # @param obj - a `Version` or a timestamp
130
- # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
131
- # Default: false.
132
- # @return `ActiveRecord::Relation`
133
- # @api public
134
- def preceding(obj, timestamp_arg = false)
135
- if timestamp_arg != true && primary_key_is_int?
136
- preceding_by_id(obj)
137
- else
138
- preceding_by_timestamp(obj)
139
- end
140
- end
141
-
142
- # Returns versions after `obj`.
143
- #
144
- # @param obj - a `Version` or a timestamp
145
- # @param timestamp_arg - boolean - When true, `obj` is a timestamp.
146
- # Default: false.
147
- # @return `ActiveRecord::Relation`
148
- # @api public
149
- def subsequent(obj, timestamp_arg = false)
150
- if timestamp_arg != true && primary_key_is_int?
151
- subsequent_by_id(obj)
152
- else
153
- subsequent_by_timestamp(obj)
154
- end
155
- end
156
-
157
- private
158
-
159
- # @api private
160
28
  def preceding_by_id(obj)
161
29
  where(:integer_id.lt => obj.integer_id).order(integer_id: :desc)
162
30
  end
163
31
 
164
- # @api private
165
32
  def preceding_by_timestamp(obj)
166
33
  obj = obj.send(:created_at) if obj.is_a?(self)
167
- where(:created_at.lt => obj).order(timestamp_sort_order("desc"))
34
+ where(:created_at.lt => obj).order(timestamp_sort_order('desc'))
168
35
  end
169
36
 
170
- # @api private
171
37
  def subsequent_by_id(version)
172
38
  where(:integer_id.gt => version.integer_id).order(integer_id: :asc)
173
39
  end
174
40
 
175
- # @api private
176
41
  def subsequent_by_timestamp(obj)
177
42
  obj = obj.send(:created_at) if obj.is_a?(self)
178
43
  where(:created_at.gt => obj).order(timestamp_sort_order)
179
44
  end
180
45
  end
181
-
182
- # @api private
183
- def object_deserialized
184
- if self.class.object_col_is_json?
185
- object
186
- else
187
- PaperTrail.serializer.load(object)
188
- end
189
- end
190
-
191
- # Restore the item from this version.
192
- #
193
- # Options:
194
- #
195
- # - :mark_for_destruction
196
- # - `true` - Mark the has_one/has_many associations that did not exist in
197
- # the reified version for destruction, instead of removing them.
198
- # - `false` - Default. Useful for persisting the reified version.
199
- # - :dup
200
- # - `false` - Default.
201
- # - `true` - Always create a new object instance. Useful for
202
- # comparing two versions of the same object.
203
- # - :unversioned_attributes
204
- # - `:nil` - Default. Attributes undefined in version record are set to
205
- # nil in reified record.
206
- # - `:preserve` - Attributes undefined in version record are not modified.
207
- #
208
- def reify(options = {})
209
- unless self.class.fields.keys.include? "object"
210
- raise "reify can't be called without an object column"
211
- end
212
- return nil if object.nil?
213
- ::PaperTrail::Reifier.reify(self, options)
214
- end
215
-
216
- # Returns what changed in this version of the item.
217
- # `ActiveModel::Dirty#changes`. returns `nil` if your `versions` table does
218
- # not have an `object_changes` text column.
219
- def changeset
220
- return nil unless self.class.fields.keys.include? "object_changes"
221
- @changeset ||= load_changeset
222
- end
223
-
224
- # Returns who put the item into the state stored in this version.
225
- def paper_trail_originator
226
- @paper_trail_originator ||= previous.try(:whodunnit)
227
- end
228
-
229
- # Returns who changed the item from the state it had in this version. This
230
- # is an alias for `whodunnit`.
231
- def terminator
232
- @terminator ||= whodunnit
233
- end
234
- alias version_author terminator
235
-
236
- def sibling_versions(reload = false)
237
- if reload || !defined?(@sibling_versions) || @sibling_versions.nil?
238
- @sibling_versions = self.class.with_item_keys(item_type, item_id)
239
- end
240
- @sibling_versions
241
- end
242
-
243
- def next
244
- @next ||= sibling_versions.subsequent(self).first
245
- end
246
-
247
- def previous
248
- @previous ||= sibling_versions.preceding(self).first
249
- end
250
-
251
- # Returns an integer representing the chronological position of the
252
- # version among its siblings (see `sibling_versions`). The "create" event,
253
- # for example, has an index of 0.
254
- # @api public
255
- def index
256
- @index ||= RecordHistory.new(sibling_versions, self.class).index(self)
257
- end
258
-
259
- private
260
-
261
- # @api private
262
- def load_changeset
263
- if PaperTrail.config.object_changes_adapter&.respond_to?(:load_changeset)
264
- return PaperTrail.config.object_changes_adapter.load_changeset(self)
265
- end
266
-
267
- # First, deserialize the `object_changes` column.
268
- changes = HashWithIndifferentAccess.new(object_changes_deserialized)
269
-
270
- # The next step is, perhaps unfortunately, called "de-serialization",
271
- # and appears to be responsible for custom attribute serializers. For an
272
- # example of a custom attribute serializer, see
273
- # `Person::TimeZoneSerializer` in the test suite.
274
- #
275
- # Is `item.class` good enough? Does it handle `inheritance_column`
276
- # as well as `Reifier#version_reification_class`? We were using
277
- # `item_type.constantize`, but that is problematic when the STI parent
278
- # is not versioned. (See `Vehicle` and `Car` in the test suite).
279
- #
280
- # Note: `item` returns nil if `event` is "destroy".
281
- unless item.nil?
282
- AttributeSerializers::ObjectChangesAttribute.
283
- new(item.class).
284
- deserialize(changes)
285
- end
286
-
287
- # Finally, return a Hash mapping each attribute name to
288
- # a two-element array representing before and after.
289
- changes
290
- end
291
-
292
- # If the `object_changes` column is a Postgres JSON column, then
293
- # ActiveRecord will deserialize it for us. Otherwise, it's a string column
294
- # and we must deserialize it ourselves.
295
- # @api private
296
- def object_changes_deserialized
297
- if self.class.object_changes_col_is_json?
298
- object_changes
299
- else
300
- begin
301
- PaperTrail.serializer.load(object_changes)
302
- rescue StandardError # TODO: Rescue something more specific
303
- {}
304
- end
305
- end
306
- end
307
-
308
- # Enforces the `version_limit`, if set. Default: no limit.
309
- # @api private
310
- def enforce_version_limit!
311
- limit = version_limit
312
- return unless limit.is_a? Numeric
313
- previous_versions = sibling_versions.not_creates.
314
- order(self.class.timestamp_sort_order("asc"))
315
- return unless previous_versions.size > limit
316
- excess_versions = previous_versions - previous_versions.last(limit)
317
- excess_versions.map(&:destroy)
318
- end
319
-
320
- # See docs section 2.e. Limiting the Number of Versions Created.
321
- # The version limit can be global or per-model.
322
- #
323
- # @api private
324
- #
325
- # TODO: Duplication: similar `constantize` in Reifier#version_reification_class
326
- def version_limit
327
- if self.class.item_subtype_column_present?
328
- klass = (item_subtype || item_type).constantize
329
- if klass&.paper_trail_options&.key?(:limit)
330
- return klass.paper_trail_options[:limit]
331
- end
332
- end
333
- PaperTrail.config.version_limit
334
- end
335
46
  end
336
47
  end