mongo_trails 10.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +2 -0
  3. data/.gitignore +1 -0
  4. data/.travis.yml +13 -0
  5. data/Appraisals +7 -0
  6. data/Gemfile +7 -0
  7. data/Gemfile.lock +62 -0
  8. data/LICENSE +20 -0
  9. data/README.md +36 -0
  10. data/Rakefile +13 -0
  11. data/gemfiles/rails_5.gemfile +9 -0
  12. data/gemfiles/rails_5.gemfile.lock +63 -0
  13. data/gemfiles/rails_6.gemfile +9 -0
  14. data/gemfiles/rails_6.gemfile.lock +63 -0
  15. data/lib/mongo_trails.rb +154 -0
  16. data/lib/mongo_trails/attribute_serializers/README.md +10 -0
  17. data/lib/mongo_trails/attribute_serializers/attribute_serializer_factory.rb +27 -0
  18. data/lib/mongo_trails/attribute_serializers/cast_attribute_serializer.rb +51 -0
  19. data/lib/mongo_trails/attribute_serializers/object_attribute.rb +41 -0
  20. data/lib/mongo_trails/attribute_serializers/object_changes_attribute.rb +44 -0
  21. data/lib/mongo_trails/cleaner.rb +60 -0
  22. data/lib/mongo_trails/compatibility.rb +51 -0
  23. data/lib/mongo_trails/config.rb +41 -0
  24. data/lib/mongo_trails/events/base.rb +323 -0
  25. data/lib/mongo_trails/events/create.rb +32 -0
  26. data/lib/mongo_trails/events/destroy.rb +42 -0
  27. data/lib/mongo_trails/events/update.rb +60 -0
  28. data/lib/mongo_trails/frameworks/cucumber.rb +33 -0
  29. data/lib/mongo_trails/frameworks/rails.rb +4 -0
  30. data/lib/mongo_trails/frameworks/rails/controller.rb +109 -0
  31. data/lib/mongo_trails/frameworks/rails/engine.rb +43 -0
  32. data/lib/mongo_trails/frameworks/rspec.rb +43 -0
  33. data/lib/mongo_trails/frameworks/rspec/helpers.rb +29 -0
  34. data/lib/mongo_trails/has_paper_trail.rb +86 -0
  35. data/lib/mongo_trails/model_config.rb +249 -0
  36. data/lib/mongo_trails/mongo_support/config.rb +9 -0
  37. data/lib/mongo_trails/mongo_support/version.rb +56 -0
  38. data/lib/mongo_trails/queries/versions/where_object.rb +65 -0
  39. data/lib/mongo_trails/queries/versions/where_object_changes.rb +75 -0
  40. data/lib/mongo_trails/record_history.rb +51 -0
  41. data/lib/mongo_trails/record_trail.rb +304 -0
  42. data/lib/mongo_trails/reifier.rb +130 -0
  43. data/lib/mongo_trails/request.rb +166 -0
  44. data/lib/mongo_trails/serializers/json.rb +46 -0
  45. data/lib/mongo_trails/serializers/yaml.rb +43 -0
  46. data/lib/mongo_trails/type_serializers/postgres_array_serializer.rb +48 -0
  47. data/lib/mongo_trails/version_concern.rb +336 -0
  48. data/lib/mongo_trails/version_number.rb +23 -0
  49. data/mongo_trails.gemspec +38 -0
  50. metadata +180 -0
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "request_store"
4
+
5
+ module PaperTrail
6
+ # Manages variables that affect the current HTTP request, such as `whodunnit`.
7
+ #
8
+ # Please do not use `PaperTrail::Request` directly, use `PaperTrail.request`.
9
+ # Currently, `Request` is a `Module`, but in the future it is quite possible
10
+ # we may make it a `Class`. If we make such a choice, we will not provide any
11
+ # warning and will not treat it as a breaking change. You've been warned :)
12
+ #
13
+ # @api private
14
+ module Request
15
+ class InvalidOption < RuntimeError
16
+ end
17
+
18
+ class << self
19
+ # Sets any data from the controller that you want PaperTrail to store.
20
+ # See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
21
+ #
22
+ # PaperTrail.request.controller_info = { ip: request_user_ip }
23
+ # PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
24
+ #
25
+ # @api public
26
+ def controller_info=(value)
27
+ store[:controller_info] = value
28
+ end
29
+
30
+ # Returns the data from the controller that you want PaperTrail to store.
31
+ # See also `PaperTrail::Rails::Controller#info_for_paper_trail`.
32
+ #
33
+ # PaperTrail.request.controller_info = { ip: request_user_ip }
34
+ # PaperTrail.request.controller_info # => { ip: '127.0.0.1' }
35
+ #
36
+ # @api public
37
+ def controller_info
38
+ store[:controller_info]
39
+ end
40
+
41
+ # Switches PaperTrail off for the given model.
42
+ # @api public
43
+ def disable_model(model_class)
44
+ enabled_for_model(model_class, false)
45
+ end
46
+
47
+ # Switches PaperTrail on for the given model.
48
+ # @api public
49
+ def enable_model(model_class)
50
+ enabled_for_model(model_class, true)
51
+ end
52
+
53
+ # Sets whether PaperTrail is enabled or disabled for the current request.
54
+ # @api public
55
+ def enabled=(value)
56
+ store[:enabled] = value
57
+ end
58
+
59
+ # Returns `true` if PaperTrail is enabled for the request, `false` otherwise.
60
+ # See `PaperTrail::Rails::Controller#paper_trail_enabled_for_controller`.
61
+ # @api public
62
+ def enabled?
63
+ !!store[:enabled]
64
+ end
65
+
66
+ # Sets whether PaperTrail is enabled or disabled for this model in the
67
+ # current request.
68
+ # @api public
69
+ def enabled_for_model(model, value)
70
+ store[:"enabled_for_#{model}"] = value
71
+ end
72
+
73
+ # Returns `true` if PaperTrail is enabled for this model in the current
74
+ # request, `false` otherwise.
75
+ # @api public
76
+ def enabled_for_model?(model)
77
+ model.include?(::PaperTrail::Model::InstanceMethods) &&
78
+ !!store.fetch(:"enabled_for_#{model}", true)
79
+ end
80
+
81
+ # @api private
82
+ def merge(options)
83
+ options.to_h.each do |k, v|
84
+ store[k] = v
85
+ end
86
+ end
87
+
88
+ # @api private
89
+ def set(options)
90
+ store.clear
91
+ merge(options)
92
+ end
93
+
94
+ # Returns a deep copy of the internal hash from our RequestStore. Keys are
95
+ # all symbols. Values are mostly primitives, but whodunnit can be a Proc.
96
+ # We cannot use Marshal.dump here because it doesn't support Proc. It is
97
+ # unclear exactly how `deep_dup` handles a Proc, but it doesn't complain.
98
+ # @api private
99
+ def to_h
100
+ store.deep_dup
101
+ end
102
+
103
+ # Temporarily set `options` and execute a block.
104
+ # @api private
105
+ def with(options)
106
+ return unless block_given?
107
+ validate_public_options(options)
108
+ before = to_h
109
+ merge(options)
110
+ yield
111
+ ensure
112
+ set(before)
113
+ end
114
+
115
+ # Sets who is responsible for any changes that occur during request. You
116
+ # would normally use this in a migration or on the console, when working
117
+ # with models directly.
118
+ #
119
+ # `value` is usually a string, the name of a person, but you can set
120
+ # anything that responds to `to_s`. You can also set a Proc, which will
121
+ # not be evaluated until `whodunnit` is called later, usually right before
122
+ # inserting a `Version` record.
123
+ #
124
+ # @api public
125
+ def whodunnit=(value)
126
+ store[:whodunnit] = value
127
+ end
128
+
129
+ # Returns who is reponsible for any changes that occur during request.
130
+ #
131
+ # @api public
132
+ def whodunnit
133
+ who = store[:whodunnit]
134
+ who.respond_to?(:call) ? who.call : who
135
+ end
136
+
137
+ private
138
+
139
+ # Returns a Hash, initializing with default values if necessary.
140
+ # @api private
141
+ def store
142
+ RequestStore.store[:paper_trail] ||= {
143
+ enabled: true
144
+ }
145
+ end
146
+
147
+ # Provide a helpful error message if someone has a typo in one of their
148
+ # option keys. We don't validate option values here. That's traditionally
149
+ # been handled with casting (`to_s`, `!!`) in the accessor method.
150
+ # @api private
151
+ def validate_public_options(options)
152
+ options.each do |k, _v|
153
+ case k
154
+ when :controller_info,
155
+ /enabled_for_/,
156
+ :enabled,
157
+ :whodunnit
158
+ next
159
+ else
160
+ raise InvalidOption, "Invalid option: #{k}"
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Serializers
5
+ # An alternate serializer for, e.g. `versions.object`.
6
+ module JSON
7
+ extend self # makes all instance methods become module methods as well
8
+
9
+ def load(string)
10
+ ActiveSupport::JSON.decode string
11
+ end
12
+
13
+ def dump(object)
14
+ ActiveSupport::JSON.encode object
15
+ end
16
+
17
+ # Returns a SQL LIKE condition to be used to match the given field and
18
+ # value in the serialized object.
19
+ def where_object_condition(arel_field, field, value)
20
+ # Convert to JSON to handle strings and nulls correctly.
21
+ json_value = value.to_json
22
+
23
+ # If the value is a number, we need to ensure that we find the next
24
+ # character too, which is either `,` or `}`, to ensure that searching
25
+ # for the value 12 doesn't yield false positives when the value is
26
+ # 123.
27
+ if value.is_a? Numeric
28
+ arel_field.matches("%\"#{field}\":#{json_value},%").
29
+ or(arel_field.matches("%\"#{field}\":#{json_value}}%"))
30
+ else
31
+ arel_field.matches("%\"#{field}\":#{json_value}%")
32
+ end
33
+ end
34
+
35
+ def where_object_changes_condition(*)
36
+ raise <<-STR.squish.freeze
37
+ where_object_changes no longer supports reading JSON from a text
38
+ column. The old implementation was inaccurate, returning more records
39
+ than you wanted. This feature was deprecated in 7.1.0 and removed in
40
+ 8.0.0. The json and jsonb datatypes are still supported. See the
41
+ discussion at https://github.com/paper-trail-gem/paper_trail/issues/803
42
+ STR
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module PaperTrail
6
+ module Serializers
7
+ # The default serializer for, e.g. `versions.object`.
8
+ module YAML
9
+ extend self # makes all instance methods become module methods as well
10
+
11
+ def load(string)
12
+ ::YAML.load string
13
+ end
14
+
15
+ # @param object (Hash | HashWithIndifferentAccess) - Coming from
16
+ # `recordable_object` `object` will be a plain `Hash`. However, due to
17
+ # recent [memory optimizations](https://git.io/fjeYv), when coming from
18
+ # `recordable_object_changes`, it will be a `HashWithIndifferentAccess`.
19
+ def dump(object)
20
+ object = object.to_hash if object.is_a?(HashWithIndifferentAccess)
21
+ ::YAML.dump object
22
+ end
23
+
24
+ # Returns a SQL LIKE condition to be used to match the given field and
25
+ # value in the serialized object.
26
+ def where_object_condition(arel_field, field, value)
27
+ arel_field.matches("%\n#{field}: #{value}\n%")
28
+ end
29
+
30
+ # Returns a SQL LIKE condition to be used to match the given field and
31
+ # value in the serialized `object_changes`.
32
+ def where_object_changes_condition(*)
33
+ raise <<-STR.squish.freeze
34
+ where_object_changes no longer supports reading YAML from a text
35
+ column. The old implementation was inaccurate, returning more records
36
+ than you wanted. This feature was deprecated in 8.1.0 and removed in
37
+ 9.0.0. The json and jsonb datatypes are still supported. See
38
+ discussion at https://github.com/paper-trail-gem/paper_trail/pull/997
39
+ STR
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module TypeSerializers
5
+ # Provides an alternative method of serialization
6
+ # and deserialization of PostgreSQL array columns.
7
+ class PostgresArraySerializer
8
+ def initialize(subtype, delimiter)
9
+ @subtype = subtype
10
+ @delimiter = delimiter
11
+ end
12
+
13
+ def serialize(array)
14
+ return serialize_with_ar(array) if active_record_pre_502?
15
+ array
16
+ end
17
+
18
+ def deserialize(array)
19
+ return deserialize_with_ar(array) if active_record_pre_502?
20
+
21
+ case array
22
+ # Needed for legacy reasons. If serialized array is a string
23
+ # then it was serialized with Rails < 5.0.2.
24
+ when ::String then deserialize_with_ar(array)
25
+ else array
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def active_record_pre_502?
32
+ ::ActiveRecord.gem_version < Gem::Version.new("5.0.2")
33
+ end
34
+
35
+ def serialize_with_ar(array)
36
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
37
+ new(@subtype, @delimiter).
38
+ serialize(array)
39
+ end
40
+
41
+ def deserialize_with_ar(array)
42
+ ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.
43
+ new(@subtype, @delimiter).
44
+ deserialize(array)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
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
+ 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
+ module VersionConcern
13
+ extend ::ActiveSupport::Concern
14
+
15
+ # :nodoc:
16
+ module ClassMethods
17
+ def item_subtype_column_present?
18
+ column_names.include?("item_subtype")
19
+ end
20
+
21
+ 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"
39
+ end
40
+
41
+ def between(start_time, end_time)
42
+ where(:created_at.gt => start_time).and(:created_at.lt => end_time).order(timestamp_sort_order)
43
+ end
44
+
45
+ # Defaults to using the primary key as the secondary sort order if
46
+ # possible.
47
+ def timestamp_sort_order(direction = "asc")
48
+ { created_at: direction.downcase }
49
+ end
50
+
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
+ def object_col_is_json?
116
+ # %i[json jsonb].include?(columns_hash["object"].type)
117
+ true
118
+ end
119
+
120
+ # Returns whether the `object_changes` column is using the `json` type
121
+ # supported by PostgreSQL.
122
+ def object_changes_col_is_json?
123
+ # %i[json jsonb].include?(columns_hash["object_changes"].try(:type))
124
+ true
125
+ end
126
+
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
+ def preceding_by_id(obj)
161
+ where(:integer_id.lt => obj.integer_id).order(integer_id: :desc)
162
+ end
163
+
164
+ # @api private
165
+ def preceding_by_timestamp(obj)
166
+ obj = obj.send(:created_at) if obj.is_a?(self)
167
+ where(:created_at.lt => obj).order(timestamp_sort_order("desc"))
168
+ end
169
+
170
+ # @api private
171
+ def subsequent_by_id(version)
172
+ where(:integer_id.gt => version.integer_id).order(integer_id: :asc)
173
+ end
174
+
175
+ # @api private
176
+ def subsequent_by_timestamp(obj)
177
+ obj = obj.send(:created_at) if obj.is_a?(self)
178
+ where(:created_at.gt => obj).order(timestamp_sort_order)
179
+ end
180
+ 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
+ end
336
+ end