mongo_trails 10.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitattributes +2 -0
- data/.gitignore +1 -0
- data/.travis.yml +13 -0
- data/Appraisals +7 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +62 -0
- data/LICENSE +20 -0
- data/README.md +36 -0
- data/Rakefile +13 -0
- data/gemfiles/rails_5.gemfile +9 -0
- data/gemfiles/rails_5.gemfile.lock +63 -0
- data/gemfiles/rails_6.gemfile +9 -0
- data/gemfiles/rails_6.gemfile.lock +63 -0
- data/lib/mongo_trails.rb +154 -0
- data/lib/mongo_trails/attribute_serializers/README.md +10 -0
- data/lib/mongo_trails/attribute_serializers/attribute_serializer_factory.rb +27 -0
- data/lib/mongo_trails/attribute_serializers/cast_attribute_serializer.rb +51 -0
- data/lib/mongo_trails/attribute_serializers/object_attribute.rb +41 -0
- data/lib/mongo_trails/attribute_serializers/object_changes_attribute.rb +44 -0
- data/lib/mongo_trails/cleaner.rb +60 -0
- data/lib/mongo_trails/compatibility.rb +51 -0
- data/lib/mongo_trails/config.rb +41 -0
- data/lib/mongo_trails/events/base.rb +323 -0
- data/lib/mongo_trails/events/create.rb +32 -0
- data/lib/mongo_trails/events/destroy.rb +42 -0
- data/lib/mongo_trails/events/update.rb +60 -0
- data/lib/mongo_trails/frameworks/cucumber.rb +33 -0
- data/lib/mongo_trails/frameworks/rails.rb +4 -0
- data/lib/mongo_trails/frameworks/rails/controller.rb +109 -0
- data/lib/mongo_trails/frameworks/rails/engine.rb +43 -0
- data/lib/mongo_trails/frameworks/rspec.rb +43 -0
- data/lib/mongo_trails/frameworks/rspec/helpers.rb +29 -0
- data/lib/mongo_trails/has_paper_trail.rb +86 -0
- data/lib/mongo_trails/model_config.rb +249 -0
- data/lib/mongo_trails/mongo_support/config.rb +9 -0
- data/lib/mongo_trails/mongo_support/version.rb +56 -0
- data/lib/mongo_trails/queries/versions/where_object.rb +65 -0
- data/lib/mongo_trails/queries/versions/where_object_changes.rb +75 -0
- data/lib/mongo_trails/record_history.rb +51 -0
- data/lib/mongo_trails/record_trail.rb +304 -0
- data/lib/mongo_trails/reifier.rb +130 -0
- data/lib/mongo_trails/request.rb +166 -0
- data/lib/mongo_trails/serializers/json.rb +46 -0
- data/lib/mongo_trails/serializers/yaml.rb +43 -0
- data/lib/mongo_trails/type_serializers/postgres_array_serializer.rb +48 -0
- data/lib/mongo_trails/version_concern.rb +336 -0
- data/lib/mongo_trails/version_number.rb +23 -0
- data/mongo_trails.gemspec +38 -0
- 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
|