snail_trail 0.0.1 → 0.0.2
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.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/lib/generators/snail_trail/install/USAGE +3 -0
- data/lib/generators/snail_trail/install/install_generator.rb +108 -0
- data/lib/generators/snail_trail/install/templates/add_object_changes_to_versions.rb.erb +12 -0
- data/lib/generators/snail_trail/install/templates/add_transaction_id_column_to_versions.rb.erb +12 -0
- data/lib/generators/snail_trail/install/templates/create_versions.rb.erb +41 -0
- data/lib/generators/snail_trail/migration_generator.rb +38 -0
- data/lib/generators/snail_trail/update_item_subtype/USAGE +4 -0
- data/lib/generators/snail_trail/update_item_subtype/templates/update_versions_for_item_subtype.rb.erb +85 -0
- data/lib/generators/snail_trail/update_item_subtype/update_item_subtype_generator.rb +19 -0
- data/lib/snail_trail/attribute_serializers/README.md +10 -0
- data/lib/snail_trail/attribute_serializers/attribute_serializer_factory.rb +41 -0
- data/lib/snail_trail/attribute_serializers/cast_attribute_serializer.rb +51 -0
- data/lib/snail_trail/attribute_serializers/object_attribute.rb +51 -0
- data/lib/snail_trail/attribute_serializers/object_changes_attribute.rb +54 -0
- data/lib/snail_trail/cleaner.rb +60 -0
- data/lib/snail_trail/compatibility.rb +51 -0
- data/lib/snail_trail/config.rb +40 -0
- data/lib/snail_trail/errors.rb +33 -0
- data/lib/snail_trail/events/base.rb +343 -0
- data/lib/snail_trail/events/create.rb +32 -0
- data/lib/snail_trail/events/destroy.rb +42 -0
- data/lib/snail_trail/events/update.rb +76 -0
- data/lib/snail_trail/frameworks/active_record/models/snail_trail/version.rb +16 -0
- data/lib/snail_trail/frameworks/active_record.rb +12 -0
- data/lib/snail_trail/frameworks/cucumber.rb +33 -0
- data/lib/snail_trail/frameworks/rails/controller.rb +103 -0
- data/lib/snail_trail/frameworks/rails/railtie.rb +34 -0
- data/lib/snail_trail/frameworks/rails.rb +3 -0
- data/lib/snail_trail/frameworks/rspec/helpers.rb +29 -0
- data/lib/snail_trail/frameworks/rspec.rb +42 -0
- data/lib/snail_trail/has_snail_trail.rb +92 -0
- data/lib/snail_trail/model_config.rb +265 -0
- data/lib/snail_trail/queries/versions/where_attribute_changes.rb +50 -0
- data/lib/snail_trail/queries/versions/where_object.rb +65 -0
- data/lib/snail_trail/queries/versions/where_object_changes.rb +70 -0
- data/lib/snail_trail/queries/versions/where_object_changes_from.rb +57 -0
- data/lib/snail_trail/queries/versions/where_object_changes_to.rb +57 -0
- data/lib/snail_trail/record_history.rb +51 -0
- data/lib/snail_trail/record_trail.rb +375 -0
- data/lib/snail_trail/reifier.rb +147 -0
- data/lib/snail_trail/request.rb +180 -0
- data/lib/snail_trail/serializers/json.rb +36 -0
- data/lib/snail_trail/serializers/yaml.rb +68 -0
- data/lib/snail_trail/type_serializers/postgres_array_serializer.rb +35 -0
- data/lib/snail_trail/version_concern.rb +407 -0
- data/lib/snail_trail/version_number.rb +23 -0
- data/lib/snail_trail.rb +141 -1
- metadata +369 -13
- data/lib/snail_trail/version.rb +0 -5
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SnailTrail
|
4
|
+
module Queries
|
5
|
+
module Versions
|
6
|
+
# For public API documentation, see `where_object_changes` in
|
7
|
+
# `snail_trail/version_concern.rb`.
|
8
|
+
# @api private
|
9
|
+
class WhereObjectChanges
|
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
|
+
|
17
|
+
# Currently, this `deep_dup` is necessary because the `jsonb` branch
|
18
|
+
# modifies `@attributes`, and that would be a nasty surprise for
|
19
|
+
# consumers of this class.
|
20
|
+
# TODO: Stop modifying `@attributes`, then remove `deep_dup`.
|
21
|
+
@attributes = attributes.deep_dup
|
22
|
+
end
|
23
|
+
|
24
|
+
# @api private
|
25
|
+
def execute
|
26
|
+
if SnailTrail.config.object_changes_adapter.respond_to?(:where_object_changes)
|
27
|
+
return SnailTrail.config.object_changes_adapter.where_object_changes(
|
28
|
+
@version_model_class, @attributes
|
29
|
+
)
|
30
|
+
end
|
31
|
+
column_type = @version_model_class.columns_hash["object_changes"].type
|
32
|
+
case column_type
|
33
|
+
when :jsonb
|
34
|
+
jsonb
|
35
|
+
when :json
|
36
|
+
json
|
37
|
+
else
|
38
|
+
raise UnsupportedColumnType.new(
|
39
|
+
method: "where_object_changes",
|
40
|
+
expected: "json or jsonb",
|
41
|
+
actual: column_type
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
# @api private
|
49
|
+
def json
|
50
|
+
predicates = []
|
51
|
+
values = []
|
52
|
+
@attributes.each do |field, value|
|
53
|
+
predicates.push(
|
54
|
+
"((object_changes->>? ILIKE ?) OR (object_changes->>? ILIKE ?))"
|
55
|
+
)
|
56
|
+
values.push(field, "[#{value.to_json},%", field, "[%,#{value.to_json}]%")
|
57
|
+
end
|
58
|
+
sql = predicates.join(" and ")
|
59
|
+
@version_model_class.where(sql, *values)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @api private
|
63
|
+
def jsonb
|
64
|
+
@attributes.each { |field, value| @attributes[field] = [value] }
|
65
|
+
@version_model_class.where("object_changes @> ?", @attributes.to_json)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SnailTrail
|
4
|
+
module Queries
|
5
|
+
module Versions
|
6
|
+
# For public API documentation, see `where_object_changes_from` in
|
7
|
+
# `snail_trail/version_concern.rb`.
|
8
|
+
# @api private
|
9
|
+
class WhereObjectChangesFrom
|
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 SnailTrail.config.object_changes_adapter.respond_to?(:where_object_changes_from)
|
22
|
+
return SnailTrail.config.object_changes_adapter.where_object_changes_from(
|
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_from",
|
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.push(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
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SnailTrail
|
4
|
+
module Queries
|
5
|
+
module Versions
|
6
|
+
# For public API documentation, see `where_object_changes_to` in
|
7
|
+
# `snail_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 SnailTrail.config.object_changes_adapter.respond_to?(:where_object_changes_to)
|
22
|
+
return SnailTrail.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.push(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
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SnailTrail
|
4
|
+
# Represents the history of a single record.
|
5
|
+
# @api private
|
6
|
+
class RecordHistory
|
7
|
+
# @param versions - ActiveRecord::Relation - All versions of the record.
|
8
|
+
# @param version_class - Class - Usually SnailTrail::Version,
|
9
|
+
# but it could also be a custom version class.
|
10
|
+
# @api private
|
11
|
+
def initialize(versions, version_class)
|
12
|
+
@versions = versions
|
13
|
+
@version_class = version_class
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns ordinal position of `version` in `sequence`.
|
17
|
+
# @api private
|
18
|
+
def index(version)
|
19
|
+
sequence.to_a.index(version)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Returns `@versions` in chronological order.
|
25
|
+
# @api private
|
26
|
+
def sequence
|
27
|
+
if @version_class.primary_key_is_int?
|
28
|
+
@versions.select(primary_key).order(primary_key.asc)
|
29
|
+
else
|
30
|
+
@versions.
|
31
|
+
select([table[:created_at], primary_key]).
|
32
|
+
order(@version_class.timestamp_sort_order)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return - Arel::Attribute - Attribute representing the primary key
|
37
|
+
# of the version table. The column's data type is usually a serial
|
38
|
+
# integer (the rails convention) but not always.
|
39
|
+
# @api private
|
40
|
+
def primary_key
|
41
|
+
table[@version_class.primary_key]
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return - Arel::Table - The version table, usually named `versions`, but
|
45
|
+
# not always.
|
46
|
+
# @api private
|
47
|
+
def table
|
48
|
+
@version_class.arel_table
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,375 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "snail_trail/events/create"
|
4
|
+
require "snail_trail/events/destroy"
|
5
|
+
require "snail_trail/events/update"
|
6
|
+
|
7
|
+
module SnailTrail
|
8
|
+
# Represents the "snail trail" for a single record.
|
9
|
+
class RecordTrail
|
10
|
+
def initialize(record)
|
11
|
+
@record = record
|
12
|
+
end
|
13
|
+
|
14
|
+
# Invoked after rollbacks to ensure versions records are not created for
|
15
|
+
# changes that never actually took place. Optimization: Use lazy `reset`
|
16
|
+
# instead of eager `reload` because, in many use cases, the association will
|
17
|
+
# not be used.
|
18
|
+
def clear_rolled_back_versions
|
19
|
+
versions.reset
|
20
|
+
end
|
21
|
+
|
22
|
+
# Invoked via`after_update` callback for when a previous version is
|
23
|
+
# reified and then saved.
|
24
|
+
def clear_version_instance
|
25
|
+
@record.send(:"#{@record.class.version_association_name}=", nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns true if this instance is the current, live one;
|
29
|
+
# returns false if this instance came from a previous version.
|
30
|
+
def live?
|
31
|
+
source_version.nil?
|
32
|
+
end
|
33
|
+
|
34
|
+
# Returns the object (not a Version) as it became next.
|
35
|
+
# NOTE: if self (the item) was not reified from a version, i.e. it is the
|
36
|
+
# "live" item, we return nil. Perhaps we should return self instead?
|
37
|
+
def next_version
|
38
|
+
subsequent_version = source_version.next
|
39
|
+
subsequent_version ? subsequent_version.reify : @record.class.find(@record.id)
|
40
|
+
rescue StandardError # TODO: Rescue something more specific
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns who put `@record` into its current state.
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def originator
|
48
|
+
(source_version || versions.last).try(:whodunnit)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns the object (not a Version) as it was most recently.
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
def previous_version
|
55
|
+
(source_version ? source_version.previous : versions.last).try(:reify)
|
56
|
+
end
|
57
|
+
|
58
|
+
def record_create
|
59
|
+
return unless enabled?
|
60
|
+
|
61
|
+
build_version_on_create(in_after_callback: true).tap do |version|
|
62
|
+
version.save!
|
63
|
+
|
64
|
+
update_transaction_id(version)
|
65
|
+
|
66
|
+
# Because the version object was created using version_class.new instead
|
67
|
+
# of versions_assoc.build?, the association cache is unaware. So, we
|
68
|
+
# invalidate the `versions` association cache with `reset`.
|
69
|
+
versions.reset
|
70
|
+
rescue StandardError => e
|
71
|
+
handle_version_errors e, version, :create
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# `recording_order` is "after" or "before". See ModelConfig#on_destroy.
|
76
|
+
#
|
77
|
+
# @api private
|
78
|
+
# @return - The created version object, so that plugins can use it, e.g.
|
79
|
+
# snail_trail-association_tracking
|
80
|
+
def record_destroy(recording_order)
|
81
|
+
return unless enabled? && !@record.new_record?
|
82
|
+
in_after_callback = recording_order == "after"
|
83
|
+
event = Events::Destroy.new(@record, in_after_callback)
|
84
|
+
|
85
|
+
# Merge data from `Event` with data from ST-AT. We no longer use
|
86
|
+
# `data_for_destroy` but ST-AT still does.
|
87
|
+
data = event.data.merge(data_for_destroy)
|
88
|
+
|
89
|
+
version = @record.class.snail_trail.version_class.new(data)
|
90
|
+
begin
|
91
|
+
version.save!
|
92
|
+
assign_and_reset_version_association(version)
|
93
|
+
|
94
|
+
if version && version.respond_to?(:errors) && version.errors.empty?
|
95
|
+
update_transaction_id(version)
|
96
|
+
end
|
97
|
+
|
98
|
+
version
|
99
|
+
rescue StandardError => e
|
100
|
+
handle_version_errors e, version, :destroy
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# @api private
|
105
|
+
# @param force [boolean] Insert a `Version` even if `@record` has not
|
106
|
+
# `changed_notably?`.
|
107
|
+
# @param in_after_callback [boolean] True when called from an `after_update`
|
108
|
+
# or `after_touch` callback.
|
109
|
+
# @param is_touch [boolean] True when called from an `after_touch` callback.
|
110
|
+
# @return - The created version object, so that plugins can use it, e.g.
|
111
|
+
# snail_trail-association_tracking
|
112
|
+
def record_update(force:, in_after_callback:, is_touch:)
|
113
|
+
return unless enabled?
|
114
|
+
|
115
|
+
version = build_version_on_update(
|
116
|
+
force: force,
|
117
|
+
in_after_callback: in_after_callback,
|
118
|
+
is_touch: is_touch
|
119
|
+
)
|
120
|
+
return unless version
|
121
|
+
|
122
|
+
begin
|
123
|
+
version.save!
|
124
|
+
# Because the version object was created using version_class.new instead
|
125
|
+
# of versions_assoc.build?, the association cache is unaware. So, we
|
126
|
+
# invalidate the `versions` association cache with `reset`.
|
127
|
+
versions.reset
|
128
|
+
|
129
|
+
if version && version.respond_to?(:errors) && version.errors.empty?
|
130
|
+
update_transaction_id(version)
|
131
|
+
end
|
132
|
+
|
133
|
+
version
|
134
|
+
rescue StandardError => e
|
135
|
+
handle_version_errors e, version, :update
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Invoked via callback when a user attempts to persist a reified
|
140
|
+
# `Version`.
|
141
|
+
def reset_timestamp_attrs_for_update_if_needed
|
142
|
+
return if live?
|
143
|
+
@record.send(:timestamp_attributes_for_update_in_model).each do |column|
|
144
|
+
@record.send(:"restore_#{column}!")
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# AR callback.
|
149
|
+
# @api private
|
150
|
+
def save_version?
|
151
|
+
if_condition = @record.snail_trail_options[:if]
|
152
|
+
unless_condition = @record.snail_trail_options[:unless]
|
153
|
+
(if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record)
|
154
|
+
end
|
155
|
+
|
156
|
+
def source_version
|
157
|
+
version
|
158
|
+
end
|
159
|
+
|
160
|
+
# Save, and create a version record regardless of options such as `:on`,
|
161
|
+
# `:if`, or `:unless`.
|
162
|
+
#
|
163
|
+
# `in_after_callback`: Indicates if this method is being called within an
|
164
|
+
# `after` callback. Defaults to `false`.
|
165
|
+
# `options`: Optional arguments passed to `save`.
|
166
|
+
#
|
167
|
+
# This is an "update" event. That is, we record the same data we would in
|
168
|
+
# the case of a normal AR `update`.
|
169
|
+
def save_with_version(in_after_callback: false, **options)
|
170
|
+
::SnailTrail.request(enabled: false) do
|
171
|
+
@record.save(**options)
|
172
|
+
end
|
173
|
+
record_update(force: true, in_after_callback: in_after_callback, is_touch: false)
|
174
|
+
end
|
175
|
+
|
176
|
+
# Like the `update_column` method from `ActiveRecord::Persistence`, but also
|
177
|
+
# creates a version to record those changes.
|
178
|
+
# @api public
|
179
|
+
def update_column(name, value)
|
180
|
+
update_columns(name => value)
|
181
|
+
end
|
182
|
+
|
183
|
+
# Like the `update_columns` method from `ActiveRecord::Persistence`, but also
|
184
|
+
# creates a version to record those changes.
|
185
|
+
# @api public
|
186
|
+
def update_columns(attributes)
|
187
|
+
# `@record.update_columns` skips dirty-tracking, so we can't just use
|
188
|
+
# `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`.
|
189
|
+
# We need to build our own hash with the changes that will be made
|
190
|
+
# directly to the database.
|
191
|
+
changes = {}
|
192
|
+
attributes.each do |k, v|
|
193
|
+
changes[k] = [@record[k], v]
|
194
|
+
end
|
195
|
+
@record.update_columns(attributes)
|
196
|
+
record_update_columns(changes)
|
197
|
+
end
|
198
|
+
|
199
|
+
# Returns the object (not a Version) as it was at the given timestamp.
|
200
|
+
def version_at(timestamp, reify_options = {})
|
201
|
+
# Because a version stores how its object looked *before* the change,
|
202
|
+
# we need to look for the first version created *after* the timestamp.
|
203
|
+
v = versions.subsequent(timestamp, true).first
|
204
|
+
return v.reify(reify_options) if v
|
205
|
+
@record unless @record.destroyed?
|
206
|
+
end
|
207
|
+
|
208
|
+
# Returns the objects (not Versions) as they were between the given times.
|
209
|
+
def versions_between(start_time, end_time)
|
210
|
+
versions = send(@record.class.versions_association_name).between(start_time, end_time)
|
211
|
+
versions.collect { |version| version_at(version.created_at) }
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
def add_transaction_id_to(data)
|
217
|
+
return unless @record.class.snail_trail.version_class.column_names.include?("transaction_id")
|
218
|
+
data[:transaction_id] = ::SnailTrail.request.transaction_id
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
def update_transaction_id(version)
|
223
|
+
return unless @record.class.snail_trail.version_class.column_names.include?("transaction_id")
|
224
|
+
if ::SnailTrail.transaction? && ::SnailTrail.request.transaction_id.nil?
|
225
|
+
::SnailTrail.request.transaction_id = version.id
|
226
|
+
version.transaction_id = version.id
|
227
|
+
version.save
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# @api private
|
232
|
+
def assign_and_reset_version_association(version)
|
233
|
+
@record.send(:"#{@record.class.version_association_name}=", version)
|
234
|
+
@record.send(@record.class.versions_association_name).reset
|
235
|
+
end
|
236
|
+
|
237
|
+
# @api private
|
238
|
+
def build_version_on_create(in_after_callback:)
|
239
|
+
event = Events::Create.new(@record, in_after_callback)
|
240
|
+
|
241
|
+
# Merge data from `Event` with data from ST-AT. We no longer use
|
242
|
+
# `data_for_create` but ST-AT still does.
|
243
|
+
data = event.data.merge!(data_for_create)
|
244
|
+
|
245
|
+
# Pure `version_class.new` reduces memory usage compared to `versions_assoc.build`
|
246
|
+
@record.class.snail_trail.version_class.new(data)
|
247
|
+
end
|
248
|
+
|
249
|
+
# @api private
|
250
|
+
def build_version_on_update(force:, in_after_callback:, is_touch:)
|
251
|
+
event = Events::Update.new(@record, in_after_callback, is_touch, nil)
|
252
|
+
return unless force || event.changed_notably?
|
253
|
+
data = event.data
|
254
|
+
|
255
|
+
# Copy the (recently set) `updated_at` from the record to the `created_at`
|
256
|
+
# of the `Version`. Without this feature, these two timestamps would
|
257
|
+
# differ by a few milliseconds. To some people, it seems a little
|
258
|
+
# unnatural to tamper with creation timestamps in this way. But, this
|
259
|
+
# feature has existed for a long time, almost a decade now, and some users
|
260
|
+
# may rely on it now.
|
261
|
+
if @record.respond_to?(:updated_at) &&
|
262
|
+
@record.snail_trail_options[:synchronize_version_creation_timestamp] != false
|
263
|
+
data[:created_at] = @record.updated_at
|
264
|
+
end
|
265
|
+
|
266
|
+
# Merge data from `Event` with data from ST-AT. We no longer use
|
267
|
+
# `data_for_update` but ST-AT still does. To save memory, we use `merge!`
|
268
|
+
# instead of `merge`.
|
269
|
+
data.merge!(data_for_update)
|
270
|
+
|
271
|
+
# Using `version_class.new` reduces memory usage compared to
|
272
|
+
# `versions_assoc.build`. It's a trade-off though. We have to clear
|
273
|
+
# the association cache (see `versions.reset`) and that could cause an
|
274
|
+
# additional query in certain applications.
|
275
|
+
@record.class.snail_trail.version_class.new(data)
|
276
|
+
end
|
277
|
+
|
278
|
+
# @api public
|
279
|
+
def data_for_create
|
280
|
+
data = {}
|
281
|
+
add_transaction_id_to(data)
|
282
|
+
data
|
283
|
+
end
|
284
|
+
|
285
|
+
# @api public
|
286
|
+
def data_for_destroy
|
287
|
+
data = {}
|
288
|
+
add_transaction_id_to(data)
|
289
|
+
data
|
290
|
+
end
|
291
|
+
|
292
|
+
# @api public
|
293
|
+
def data_for_update
|
294
|
+
data = {}
|
295
|
+
add_transaction_id_to(data)
|
296
|
+
data
|
297
|
+
end
|
298
|
+
|
299
|
+
# @api public
|
300
|
+
def data_for_update_columns
|
301
|
+
data = {}
|
302
|
+
add_transaction_id_to(data)
|
303
|
+
data
|
304
|
+
end
|
305
|
+
|
306
|
+
# Is ST enabled for this particular record?
|
307
|
+
# @api private
|
308
|
+
def enabled?
|
309
|
+
SnailTrail.enabled? &&
|
310
|
+
SnailTrail.request.enabled? &&
|
311
|
+
SnailTrail.request.enabled_for_model?(@record.class)
|
312
|
+
end
|
313
|
+
|
314
|
+
def log_version_errors(version, action)
|
315
|
+
version.logger&.warn(
|
316
|
+
"Unable to create version for #{action} of #{@record.class.name}" \
|
317
|
+
"##{@record.id}: " + version.errors.full_messages.join(", ")
|
318
|
+
)
|
319
|
+
end
|
320
|
+
|
321
|
+
# Centralized handler for version errors
|
322
|
+
# @api private
|
323
|
+
def handle_version_errors(e, version, action)
|
324
|
+
case SnailTrail.config.version_error_behavior
|
325
|
+
when :legacy
|
326
|
+
# legacy behavior was to raise on create and log on update/delete
|
327
|
+
if action == :create
|
328
|
+
raise e
|
329
|
+
else
|
330
|
+
log_version_errors(version, action)
|
331
|
+
end
|
332
|
+
when :log
|
333
|
+
log_version_errors(version, action)
|
334
|
+
when :exception
|
335
|
+
raise e
|
336
|
+
when :silent
|
337
|
+
# noop
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# @api private
|
342
|
+
# @return - The created version object, so that plugins can use it, e.g.
|
343
|
+
# snail_trail-association_tracking
|
344
|
+
def record_update_columns(changes)
|
345
|
+
return unless enabled?
|
346
|
+
data = Events::Update.new(@record, false, false, changes).data
|
347
|
+
|
348
|
+
# Merge data from `Event` with data from ST-AT. We no longer use
|
349
|
+
# `data_for_update_columns` but ST-AT still does.
|
350
|
+
data.merge!(data_for_update_columns)
|
351
|
+
|
352
|
+
versions_assoc = @record.send(@record.class.versions_association_name)
|
353
|
+
version = versions_assoc.new(data)
|
354
|
+
begin
|
355
|
+
version.save!
|
356
|
+
|
357
|
+
if version && version.respond_to?(:errors) && version.errors.empty?
|
358
|
+
update_transaction_id(version)
|
359
|
+
end
|
360
|
+
|
361
|
+
version
|
362
|
+
rescue StandardError => e
|
363
|
+
handle_version_errors e, version, :update
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
def version
|
368
|
+
@record.public_send(@record.class.version_association_name)
|
369
|
+
end
|
370
|
+
|
371
|
+
def versions
|
372
|
+
@record.public_send(@record.class.versions_association_name)
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|