paper_trail 12.0.0 → 13.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2ce7f019e3413c485beff4eefa957f759b084fe0d29b0d9f3451b6ba3e4befe
4
- data.tar.gz: 3f73870b42c582f683936eee21fe288b17d915d1bfbff5327ac50dd8e9766732
3
+ metadata.gz: 776e912c4a06b036014b0301b30a1438974c00b8e63b6e0b78fbffe4dfca7824
4
+ data.tar.gz: 691060404fbb2b4a3f66cf40c0b6187df7a9e8439854097ffbad433a7bdd6e75
5
5
  SHA512:
6
- metadata.gz: 6134547d0c2c69f94962aea4f868aec8d456bbb0afc4588b9b07971074a8d50476f29a59acb41d3e058dea581678d0c7129d8e92a04c370fa2016dbd6bf544b7
7
- data.tar.gz: a6ca4e41cf2f6bce774894a9927dc0b3e4abbcbbdbb39fbf4671d029398f26999ce8927143f1f14279a6ad8aa70194025f5b6e653780560a0cc4870dce9b754e
6
+ metadata.gz: 7110ad57841a431cb50318a79db55ba65f279b839d72009dc2aef961a57c9b044d4d8d706062bbff4cea7fe9b7b2db0a3d83c966518eeffddd66cca6f9e63ae3
7
+ data.tar.gz: d5ebe8b0a5f8dd4df889b1e6a71e81ab5fb1e752a034dd400decd5ef1bdb91441bce9b6061980db7d443796309995c745063175bf7cdb85819028f45fc20dc88
@@ -20,6 +20,12 @@ module PaperTrail
20
20
  default: false,
21
21
  desc: "Store changeset (diff) with each version"
22
22
  )
23
+ class_option(
24
+ :uuid,
25
+ type: :boolean,
26
+ default: false,
27
+ desc: "Use uuid instead of bigint for item_id type (use only if tables use UUIDs)"
28
+ )
23
29
 
24
30
  desc "Generates (but does not run) a migration to add a versions table." \
25
31
  " See section 5.c. Generators in README.md for more information."
@@ -28,7 +34,8 @@ module PaperTrail
28
34
  add_paper_trail_migration(
29
35
  "create_versions",
30
36
  item_type_options: item_type_options,
31
- versions_table_options: versions_table_options
37
+ versions_table_options: versions_table_options,
38
+ item_id_type_options: item_id_type_options
32
39
  )
33
40
  if options.with_changes?
34
41
  add_paper_trail_migration("add_object_changes_to_versions")
@@ -37,13 +44,18 @@ module PaperTrail
37
44
 
38
45
  private
39
46
 
47
+ # To use uuid instead of integer for primary key
48
+ def item_id_type_options
49
+ options.uuid? ? "string" : "bigint"
50
+ end
51
+
40
52
  # MySQL 5.6 utf8mb4 limit is 191 chars for keys used in indexes.
41
53
  # See https://github.com/paper-trail-gem/paper_trail/issues/651
42
54
  def item_type_options
43
55
  if mysql?
44
- ", { null: false, limit: 191 }"
56
+ ", null: false, limit: 191"
45
57
  else
46
- ", { null: false }"
58
+ ", null: false"
47
59
  end
48
60
  end
49
61
 
@@ -11,7 +11,7 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
11
11
  def change
12
12
  create_table :versions<%= versions_table_options %> do |t|
13
13
  t.string :item_type<%= item_type_options %>
14
- t.bigint :item_id, null: false
14
+ t.<%= item_id_type_options %> :item_id, null: false
15
15
  t.string :event, null: false
16
16
  t.string :whodunnit
17
17
  t.text :object, limit: TEXT_BYTES
@@ -29,8 +29,10 @@ class CreateVersions < ActiveRecord::Migration<%= migration_version %>
29
29
  # version of ActiveRecord with support for fractional seconds in MySQL.
30
30
  # (https://github.com/rails/rails/pull/14359)
31
31
  #
32
+ # MySQL users should use the following line for `created_at`
33
+ # t.datetime :created_at, limit: 6
32
34
  t.datetime :created_at
33
35
  end
34
- add_index :versions, %i(item_type item_id)
36
+ add_index :versions, %i[item_type item_id]
35
37
  end
36
38
  end
@@ -7,8 +7,10 @@ module PaperTrail
7
7
  class UpdateItemSubtypeGenerator < MigrationGenerator
8
8
  source_root File.expand_path("templates", __dir__)
9
9
 
10
- desc "Generates (but does not run) a migration to update item_subtype for STI entries in an "\
11
- "existing versions table."
10
+ desc(
11
+ "Generates (but does not run) a migration to update item_subtype for "\
12
+ "STI entries in an existing versions table."
13
+ )
12
14
 
13
15
  def create_migration_file
14
16
  add_paper_trail_migration("update_versions_for_item_subtype", sti_type_options: options)
@@ -32,6 +32,12 @@ module PaperTrail
32
32
  if defined_enums[attr] && val.is_a?(::String)
33
33
  # Because PT 4 used to save the string version of enums to `object_changes`
34
34
  val
35
+ elsif PaperTrail::RAILS_GTE_7_0 && val.is_a?(ActiveRecord::Type::Time::Value)
36
+ # Because Rails 7 time attribute throws a delegation error when you deserialize
37
+ # it with the factory.
38
+ # See ActiveRecord::Type::Time::Value crashes when loaded from YAML on rails 7.0
39
+ # https://github.com/rails/rails/issues/43966
40
+ val.instance_variable_get(:@time)
35
41
  else
36
42
  AttributeSerializerFactory.for(@klass, attr).deserialize(val)
37
43
  end
@@ -8,7 +8,7 @@ module PaperTrail
8
8
  #
9
9
  # It is not safe to assume that a new version of rails will be compatible with
10
10
  # PaperTrail. PT is only compatible with the versions of rails that it is
11
- # tested against. See `.travis.yml`.
11
+ # tested against. See `.github/workflows/test.yml`.
12
12
  #
13
13
  # However, as of
14
14
  # [#1213](https://github.com/paper-trail-gem/paper_trail/pull/1213) our
@@ -18,7 +18,7 @@ module PaperTrail
18
18
  # versions.
19
19
  module Compatibility
20
20
  ACTIVERECORD_GTE = ">= 5.2" # enforced in gemspec
21
- ACTIVERECORD_LT = "< 6.2" # not enforced in gemspec
21
+ ACTIVERECORD_LT = "< 7.1" # not enforced in gemspec
22
22
 
23
23
  E_INCOMPATIBLE_AR = <<-EOS
24
24
  PaperTrail %s is not compatible with ActiveRecord %s. We allow PT
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ # Generic PaperTrail exception.
5
+ # @api public
6
+ class Error < StandardError
7
+ end
8
+
9
+ # An unexpected option, perhaps a typo, was passed to a public API method.
10
+ # @api public
11
+ class InvalidOption < Error
12
+ end
13
+
14
+ # The application's database schema is not supported.
15
+ # @api public
16
+ class UnsupportedSchema < Error
17
+ end
18
+
19
+ # The application's database column type is not supported.
20
+ # @api public
21
+ class UnsupportedColumnType < UnsupportedSchema
22
+ def initialize(method:, expected:, actual:)
23
+ super(
24
+ format(
25
+ "%s expected %s column, got %s",
26
+ method,
27
+ expected,
28
+ actual
29
+ )
30
+ )
31
+ end
32
+ end
33
+ end
@@ -109,19 +109,25 @@ module PaperTrail
109
109
  @changed_in_latest_version ||= changes_in_latest_version.keys
110
110
  end
111
111
 
112
- # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
113
- # https://github.com/paper-trail-gem/paper_trail/pull/899
112
+ # Memoized to reduce memory usage
114
113
  #
115
114
  # @api private
116
115
  def changes_in_latest_version
117
- # Memoized to reduce memory usage
118
- @changes_in_latest_version ||= begin
119
- if @in_after_callback
120
- @record.saved_changes
121
- else
122
- @record.changes
123
- end
116
+ @changes_in_latest_version ||= load_changes_in_latest_version
117
+ end
118
+
119
+ # @api private
120
+ def evaluate_only
121
+ only = @record.paper_trail_options[:only].dup
122
+ # Remove Hash arguments and then evaluate whether the attributes (the
123
+ # keys of the hash) should also get pushed into the collection.
124
+ only.delete_if do |obj|
125
+ obj.is_a?(Hash) &&
126
+ obj.each { |attr, condition|
127
+ only << attr if condition.respond_to?(:call) && condition.call(@record)
128
+ }
124
129
  end
130
+ only
125
131
  end
126
132
 
127
133
  # An attributed is "ignored" if it is listed in the `:ignore` option
@@ -134,6 +140,18 @@ module PaperTrail
134
140
  ignored.any? && (changed_in_latest_version & ignored).any?
135
141
  end
136
142
 
143
+ # Rails 5.1 changed the API of `ActiveRecord::Dirty`. See
144
+ # https://github.com/paper-trail-gem/paper_trail/pull/899
145
+ #
146
+ # @api private
147
+ def load_changes_in_latest_version
148
+ if @in_after_callback
149
+ @record.saved_changes
150
+ else
151
+ @record.changes
152
+ end
153
+ end
154
+
137
155
  # PT 10 has a new optional column, `item_subtype`
138
156
  #
139
157
  # @api private
@@ -178,20 +196,28 @@ module PaperTrail
178
196
  if value.respond_to?(:call)
179
197
  value.call(@record)
180
198
  elsif value.is_a?(Symbol) && @record.respond_to?(value, true)
181
- # If it is an attribute that is changing in an existing object,
182
- # be sure to grab the current version.
183
- if event != "create" &&
184
- @record.has_attribute?(value) &&
185
- attribute_changed_in_latest_version?(value)
186
- attribute_in_previous_version(value, false)
187
- else
188
- @record.send(value)
189
- end
199
+ metadatum_from_model_method(event, value)
190
200
  else
191
201
  value
192
202
  end
193
203
  end
194
204
 
205
+ # The model method can either be an attribute or a non-attribute method.
206
+ #
207
+ # If it is an attribute that is changing in an existing object,
208
+ # be sure to grab the correct version.
209
+ #
210
+ # @api private
211
+ def metadatum_from_model_method(event, method)
212
+ if event != "create" &&
213
+ @record.has_attribute?(method) &&
214
+ attribute_changed_in_latest_version?(method)
215
+ attribute_in_previous_version(method, false)
216
+ else
217
+ @record.send(method)
218
+ end
219
+ end
220
+
195
221
  # @api private
196
222
  def notable_changes
197
223
  changes_in_latest_version.delete_if { |k, _v|
@@ -203,16 +229,9 @@ module PaperTrail
203
229
  def notably_changed
204
230
  # Memoized to reduce memory usage
205
231
  @notably_changed ||= begin
206
- only = @record.paper_trail_options[:only].dup
207
- # Remove Hash arguments and then evaluate whether the attributes (the
208
- # keys of the hash) should also get pushed into the collection.
209
- only.delete_if do |obj|
210
- obj.is_a?(Hash) &&
211
- obj.each { |attr, condition|
212
- only << attr if condition.respond_to?(:call) && condition.call(@record)
213
- }
214
- end
215
- only.empty? ? changed_and_not_ignored : (changed_and_not_ignored & only)
232
+ only = evaluate_only
233
+ cani = changed_and_not_ignored
234
+ only.empty? ? cani : (cani & only)
216
235
  end
217
236
  end
218
237
 
@@ -35,16 +35,35 @@ module PaperTrail
35
35
  if record_object?
36
36
  data[:object] = recordable_object(@is_touch)
37
37
  end
38
- if record_object_changes?
39
- changes = @force_changes.nil? ? notable_changes : @force_changes
40
- data[:object_changes] = prepare_object_changes(changes)
41
- end
38
+ merge_object_changes_into(data)
42
39
  merge_item_subtype_into(data)
43
40
  merge_metadata_into(data)
44
41
  end
45
42
 
43
+ # If it is a touch event, and changed are empty, it is assumed to be
44
+ # implicit `touch` mutation, and will a version is created.
45
+ #
46
+ # See https://github.com/rails/rails/commit/dcb825902d79d0f6baba956f7c6ec5767611353e
47
+ #
48
+ # @api private
49
+ def changed_notably?
50
+ if @is_touch && changes_in_latest_version.empty?
51
+ true
52
+ else
53
+ super
54
+ end
55
+ end
56
+
46
57
  private
47
58
 
59
+ # @api private
60
+ def merge_object_changes_into(data)
61
+ if record_object_changes?
62
+ changes = @force_changes.nil? ? notable_changes : @force_changes
63
+ data[:object_changes] = prepare_object_changes(changes)
64
+ end
65
+ end
66
+
48
67
  # `touch` cannot record `object_changes` because rails' `touch` does not
49
68
  # perform dirty-tracking. Specifically, methods from `Dirty`, like
50
69
  # `saved_changes`, return the same values before and after `touch`.
@@ -18,11 +18,6 @@ module PaperTrail
18
18
  `abstract_class`. This is fine, but all application models must be
19
19
  configured to use concrete (not abstract) version models.
20
20
  STR
21
- E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE = <<~STR.squish.freeze
22
- To use PaperTrail's per-model limit in your %s model, you must have an
23
- item_subtype column in your versions table. See documentation sections
24
- 2.e.1 Per-model limit, and 4.b.1 The optional item_subtype column.
25
- STR
26
21
  DPR_PASSING_ASSOC_NAME_DIRECTLY_TO_VERSIONS_OPTION = <<~STR.squish
27
22
  Passing versions association name as `has_paper_trail versions: %{versions_name}`
28
23
  is deprecated. Use `has_paper_trail versions: {name: %{versions_name}}` instead.
@@ -45,22 +40,14 @@ module PaperTrail
45
40
  @model_class.after_create { |r|
46
41
  r.paper_trail.record_create if r.paper_trail.save_version?
47
42
  }
48
- return if @model_class.paper_trail_options[:on].include?(:create)
49
- @model_class.paper_trail_options[:on] << :create
43
+ append_option_uniquely(:on, :create)
50
44
  end
51
45
 
52
46
  # Adds a callback that records a version before or after a "destroy" event.
53
47
  #
54
48
  # @api public
55
49
  def on_destroy(recording_order = "before")
56
- unless %w[after before].include?(recording_order.to_s)
57
- raise ArgumentError, 'recording order can only be "after" or "before"'
58
- end
59
-
60
- if recording_order.to_s == "after" && cannot_record_after_destroy?
61
- raise E_CANNOT_RECORD_AFTER_DESTROY
62
- end
63
-
50
+ assert_valid_recording_order_for_on_destroy(recording_order)
64
51
  @model_class.send(
65
52
  "#{recording_order}_destroy",
66
53
  lambda do |r|
@@ -68,9 +55,7 @@ module PaperTrail
68
55
  r.paper_trail.record_destroy(recording_order)
69
56
  end
70
57
  )
71
-
72
- return if @model_class.paper_trail_options[:on].include?(:destroy)
73
- @model_class.paper_trail_options[:on] << :destroy
58
+ append_option_uniquely(:on, :destroy)
74
59
  end
75
60
 
76
61
  # Adds a callback that records a version after an "update" event.
@@ -92,19 +77,27 @@ module PaperTrail
92
77
  @model_class.after_update { |r|
93
78
  r.paper_trail.clear_version_instance
94
79
  }
95
- return if @model_class.paper_trail_options[:on].include?(:update)
96
- @model_class.paper_trail_options[:on] << :update
80
+ append_option_uniquely(:on, :update)
97
81
  end
98
82
 
99
83
  # Adds a callback that records a version after a "touch" event.
84
+ #
85
+ # Rails < 6.0 has a bug where dirty-tracking does not occur during
86
+ # a `touch`. (https://github.com/rails/rails/issues/33429) See also:
87
+ # https://github.com/paper-trail-gem/paper_trail/issues/1121
88
+ # https://github.com/paper-trail-gem/paper_trail/issues/1161
89
+ # https://github.com/paper-trail-gem/paper_trail/pull/1285
90
+ #
100
91
  # @api public
101
92
  def on_touch
102
93
  @model_class.after_touch { |r|
103
- r.paper_trail.record_update(
104
- force: true,
105
- in_after_callback: true,
106
- is_touch: true
107
- )
94
+ if r.paper_trail.save_version?
95
+ r.paper_trail.record_update(
96
+ force: RAILS_LT_6_0,
97
+ in_after_callback: true,
98
+ is_touch: true
99
+ )
100
+ end
108
101
  }
109
102
  end
110
103
 
@@ -117,7 +110,6 @@ module PaperTrail
117
110
  @model_class.send :include, ::PaperTrail::Model::InstanceMethods
118
111
  setup_options(options)
119
112
  setup_associations(options)
120
- check_presence_of_item_subtype_column(options)
121
113
  @model_class.after_rollback { paper_trail.clear_rolled_back_versions }
122
114
  setup_callbacks_from_options options[:on]
123
115
  end
@@ -129,26 +121,37 @@ module PaperTrail
129
121
 
130
122
  private
131
123
 
124
+ RAILS_LT_6_0 = ::ActiveRecord.gem_version < ::Gem::Version.new("6.0.0")
125
+ private_constant :RAILS_LT_6_0
126
+
127
+ # @api private
128
+ def append_option_uniquely(option, value)
129
+ collection = @model_class.paper_trail_options.fetch(option)
130
+ return if collection.include?(value)
131
+ collection << value
132
+ end
133
+
132
134
  # Raises an error if the provided class is an `abstract_class`.
133
135
  # @api private
134
136
  def assert_concrete_activerecord_class(class_name)
135
137
  if class_name.constantize.abstract_class?
136
- raise format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
138
+ raise Error, format(E_HPT_ABSTRACT_CLASS, @model_class, class_name)
137
139
  end
138
140
  end
139
141
 
140
- def cannot_record_after_destroy?
141
- ::ActiveRecord::Base.belongs_to_required_by_default
142
+ # @api private
143
+ def assert_valid_recording_order_for_on_destroy(recording_order)
144
+ unless %w[after before].include?(recording_order.to_s)
145
+ raise ArgumentError, 'recording order can only be "after" or "before"'
146
+ end
147
+
148
+ if recording_order.to_s == "after" && cannot_record_after_destroy?
149
+ raise Error, E_CANNOT_RECORD_AFTER_DESTROY
150
+ end
142
151
  end
143
152
 
144
- # Some options require the presence of the `item_subtype` column. Currently
145
- # only `limit`, but in the future there may be others.
146
- #
147
- # @api private
148
- def check_presence_of_item_subtype_column(options)
149
- return unless options.key?(:limit)
150
- return if version_class.item_subtype_column_present?
151
- raise format(E_MODEL_LIMIT_REQUIRES_ITEM_SUBTYPE, @model_class.name)
153
+ def cannot_record_after_destroy?
154
+ ::ActiveRecord::Base.belongs_to_required_by_default
152
155
  end
153
156
 
154
157
  def check_version_class_name(options)
@@ -206,6 +209,14 @@ module PaperTrail
206
209
  options
207
210
  end
208
211
 
212
+ # Process an `ignore`, `skip`, or `only` option.
213
+ def event_attribute_option(option_name)
214
+ [@model_class.paper_trail_options[option_name]].
215
+ flatten.
216
+ compact.
217
+ map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
218
+ end
219
+
209
220
  def get_versions_scope(options)
210
221
  options[:versions][:scope] || -> { order(model.timestamp_sort_order) }
211
222
  end
@@ -240,12 +251,8 @@ module PaperTrail
240
251
  @model_class.paper_trail_options = options.dup
241
252
 
242
253
  %i[ignore skip only].each do |k|
243
- @model_class.paper_trail_options[k] = [@model_class.paper_trail_options[k]].
244
- flatten.
245
- compact.
246
- map { |attr| attr.is_a?(Hash) ? attr.stringify_keys : attr.to_s }
254
+ @model_class.paper_trail_options[k] = event_attribute_option(k)
247
255
  end
248
-
249
256
  @model_class.paper_trail_options[:meta] ||= {}
250
257
  end
251
258
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Queries
5
+ module Versions
6
+ # For public API documentation, see `where_attribute_changes` in
7
+ # `paper_trail/version_concern.rb`.
8
+ # @api private
9
+ class WhereAttributeChanges
10
+ # - version_model_class - The class that VersionConcern was mixed into.
11
+ # - attribute - An attribute that changed. See the public API
12
+ # documentation for details.
13
+ # @api private
14
+ def initialize(version_model_class, attribute)
15
+ @version_model_class = version_model_class
16
+ @attribute = attribute
17
+ end
18
+
19
+ # @api private
20
+ def execute
21
+ if PaperTrail.config.object_changes_adapter.respond_to?(:where_attribute_changes)
22
+ return PaperTrail.config.object_changes_adapter.where_attribute_changes(
23
+ @version_model_class, @attribute
24
+ )
25
+ end
26
+ column_type = @version_model_class.columns_hash["object_changes"].type
27
+ case column_type
28
+ when :jsonb, :json
29
+ json
30
+ else
31
+ raise UnsupportedColumnType.new(
32
+ method: "where_attribute_changes",
33
+ expected: "json or jsonb",
34
+ actual: column_type
35
+ )
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # @api private
42
+ def json
43
+ sql = "object_changes -> ? IS NOT NULL"
44
+
45
+ @version_model_class.where(sql, @attribute)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -19,7 +19,7 @@ module PaperTrail
19
19
  # @api private
20
20
  def execute
21
21
  column = @version_model_class.columns_hash["object"]
22
- raise "where_object can't be called without an object column" unless column
22
+ raise Error, "where_object requires an object column" unless column
23
23
 
24
24
  case column.type
25
25
  when :jsonb
@@ -28,13 +28,18 @@ module PaperTrail
28
28
  @version_model_class, @attributes
29
29
  )
30
30
  end
31
- case @version_model_class.columns_hash["object_changes"].type
31
+ column_type = @version_model_class.columns_hash["object_changes"].type
32
+ case column_type
32
33
  when :jsonb
33
34
  jsonb
34
35
  when :json
35
36
  json
36
37
  else
37
- text
38
+ raise UnsupportedColumnType.new(
39
+ method: "where_object_changes",
40
+ expected: "json or jsonb",
41
+ actual: column_type
42
+ )
38
43
  end
39
44
  end
40
45
 
@@ -59,16 +64,6 @@ module PaperTrail
59
64
  @attributes.each { |field, value| @attributes[field] = [value] }
60
65
  @version_model_class.where("object_changes @> ?", @attributes.to_json)
61
66
  end
62
-
63
- # @api private
64
- def text
65
- arel_field = @version_model_class.arel_table[:object_changes]
66
- where_conditions = @attributes.map { |field, value|
67
- ::PaperTrail.serializer.where_object_changes_condition(arel_field, field, value)
68
- }
69
- where_conditions = where_conditions.reduce { |a, e| a.and(e) }
70
- @version_model_class.where(where_conditions)
71
- end
72
67
  end
73
68
  end
74
69
  end
@@ -23,12 +23,16 @@ module PaperTrail
23
23
  @version_model_class, @attributes
24
24
  )
25
25
  end
26
-
27
- case @version_model_class.columns_hash["object_changes"].type
26
+ column_type = @version_model_class.columns_hash["object_changes"].type
27
+ case column_type
28
28
  when :jsonb, :json
29
29
  json
30
30
  else
31
- text
31
+ raise UnsupportedColumnType.new(
32
+ method: "where_object_changes_from",
33
+ expected: "json or jsonb",
34
+ actual: column_type
35
+ )
32
36
  end
33
37
  end
34
38
 
@@ -47,18 +51,6 @@ module PaperTrail
47
51
  sql = predicates.join(" and ")
48
52
  @version_model_class.where(sql, *values)
49
53
  end
50
-
51
- # @api private
52
- def text
53
- arel_field = @version_model_class.arel_table[:object_changes]
54
-
55
- where_conditions = @attributes.map do |field, value|
56
- ::PaperTrail.serializer.where_object_changes_from_condition(arel_field, field, value)
57
- end
58
-
59
- where_conditions = where_conditions.reduce { |a, e| a.and(e) }
60
- @version_model_class.where(where_conditions)
61
- end
62
54
  end
63
55
  end
64
56
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PaperTrail
4
+ module Queries
5
+ module Versions
6
+ # For public API documentation, see `where_object_changes_to` in
7
+ # `paper_trail/version_concern.rb`.
8
+ # @api private
9
+ class WhereObjectChangesTo
10
+ # - version_model_class - The class that VersionConcern was mixed into.
11
+ # - attributes - A `Hash` of attributes and values. See the public API
12
+ # documentation for details.
13
+ # @api private
14
+ def initialize(version_model_class, attributes)
15
+ @version_model_class = version_model_class
16
+ @attributes = attributes
17
+ end
18
+
19
+ # @api private
20
+ def execute
21
+ if PaperTrail.config.object_changes_adapter.respond_to?(:where_object_changes_to)
22
+ return PaperTrail.config.object_changes_adapter.where_object_changes_to(
23
+ @version_model_class, @attributes
24
+ )
25
+ end
26
+ column_type = @version_model_class.columns_hash["object_changes"].type
27
+ case column_type
28
+ when :jsonb, :json
29
+ json
30
+ else
31
+ raise UnsupportedColumnType.new(
32
+ method: "where_object_changes_to",
33
+ expected: "json or jsonb",
34
+ actual: column_type
35
+ )
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # @api private
42
+ def json
43
+ predicates = []
44
+ values = []
45
+ @attributes.each do |field, value|
46
+ predicates.push(
47
+ "(object_changes->>? ILIKE ?)"
48
+ )
49
+ values.concat([field, "[%#{value.to_json}]"])
50
+ end
51
+ sql = predicates.join(" and ")
52
+ @version_model_class.where(sql, *values)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end