mongo_trails 10.3.1

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 (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