mongo_trails 10.3.1 → 14.0.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.
- checksums.yaml +4 -4
- data/.github/workflows/test.yml +53 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +19 -0
- data/.ruby-version +1 -0
- data/Appraisals +10 -4
- data/Gemfile +8 -1
- data/README.md +28 -6
- data/Rakefile +17 -0
- data/bin/bump-version +67 -0
- data/gemfiles/rails_7.gemfile +15 -0
- data/gemfiles/rails_7.gemfile.lock +212 -0
- data/gemfiles/rails_8.gemfile +15 -0
- data/gemfiles/rails_8.gemfile.lock +213 -0
- data/gemfiles/rails_8_1.gemfile +15 -0
- data/gemfiles/rails_8_1.gemfile.lock +143 -0
- data/lib/mongo_trails/callback_propagation.rb +77 -0
- data/lib/mongo_trails/config.rb +7 -24
- data/lib/mongo_trails/events/base.rb +5 -308
- data/lib/mongo_trails/model_config.rb +1 -232
- data/lib/mongo_trails/mongo_support/config.rb +7 -1
- data/lib/mongo_trails/mongo_support/criteria.rb +7 -0
- data/lib/mongo_trails/mongo_support/version.rb +249 -19
- data/lib/mongo_trails/mongo_support/version_commit_wrap.rb +37 -0
- data/lib/mongo_trails/mongo_support/write_version_worker.rb +11 -0
- data/lib/mongo_trails/record_trail.rb +21 -262
- data/lib/mongo_trails/version.rb +5 -0
- data/lib/mongo_trails/version_concern.rb +4 -293
- data/lib/mongo_trails.rb +13 -149
- data/mongo_trails.gemspec +19 -20
- metadata +51 -85
- data/.travis.yml +0 -13
- data/Gemfile.lock +0 -62
- data/gemfiles/rails_5.gemfile +0 -9
- data/gemfiles/rails_5.gemfile.lock +0 -63
- data/gemfiles/rails_6.gemfile +0 -9
- data/gemfiles/rails_6.gemfile.lock +0 -63
- data/lib/mongo_trails/attribute_serializers/README.md +0 -10
- data/lib/mongo_trails/attribute_serializers/attribute_serializer_factory.rb +0 -27
- data/lib/mongo_trails/attribute_serializers/cast_attribute_serializer.rb +0 -51
- data/lib/mongo_trails/attribute_serializers/object_attribute.rb +0 -41
- data/lib/mongo_trails/attribute_serializers/object_changes_attribute.rb +0 -44
- data/lib/mongo_trails/cleaner.rb +0 -60
- data/lib/mongo_trails/compatibility.rb +0 -51
- data/lib/mongo_trails/events/create.rb +0 -32
- data/lib/mongo_trails/events/destroy.rb +0 -42
- data/lib/mongo_trails/events/update.rb +0 -60
- data/lib/mongo_trails/frameworks/cucumber.rb +0 -33
- data/lib/mongo_trails/frameworks/rails/controller.rb +0 -109
- data/lib/mongo_trails/frameworks/rails/engine.rb +0 -43
- data/lib/mongo_trails/frameworks/rails.rb +0 -4
- data/lib/mongo_trails/frameworks/rspec/helpers.rb +0 -29
- data/lib/mongo_trails/frameworks/rspec.rb +0 -43
- data/lib/mongo_trails/has_paper_trail.rb +0 -86
- data/lib/mongo_trails/queries/versions/where_object.rb +0 -65
- data/lib/mongo_trails/queries/versions/where_object_changes.rb +0 -75
- data/lib/mongo_trails/record_history.rb +0 -51
- data/lib/mongo_trails/reifier.rb +0 -130
- data/lib/mongo_trails/request.rb +0 -166
- data/lib/mongo_trails/serializers/json.rb +0 -46
- data/lib/mongo_trails/serializers/yaml.rb +0 -43
- data/lib/mongo_trails/type_serializers/postgres_array_serializer.rb +0 -48
- data/lib/mongo_trails/version_number.rb +0 -23
|
@@ -1,20 +1,136 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mongoid'
|
|
4
|
+
require 'time'
|
|
5
|
+
require 'mongo_trails/mongo_support/version_commit_wrap'
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
require 'sidekiq'
|
|
9
|
+
require 'mongo_trails/mongo_support/write_version_worker'
|
|
10
|
+
require 'mongo_trails/mongo_support/criteria'
|
|
11
|
+
rescue LoadError
|
|
12
|
+
# Continue without Sidekiq
|
|
13
|
+
end
|
|
3
14
|
|
|
4
15
|
class AutoIncrementCounters
|
|
5
16
|
include Mongoid::Document
|
|
17
|
+
|
|
18
|
+
store_in collection: 'auto_increment_counters'
|
|
19
|
+
|
|
20
|
+
field :sequence, type: Integer, default: 0
|
|
6
21
|
end
|
|
7
22
|
|
|
8
|
-
|
|
23
|
+
PaperTrail.config.has_paper_trail_defaults = { versions: { class_name: 'MongoTrails::Version' } }
|
|
24
|
+
|
|
25
|
+
module MongoTrails
|
|
9
26
|
class Version
|
|
27
|
+
class << self
|
|
28
|
+
def find(id)
|
|
29
|
+
find_by(integer_id: id)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def incrementing_fields
|
|
33
|
+
@incrementing_fields ||= {
|
|
34
|
+
integer_id: {
|
|
35
|
+
scope: -> { MongoTrails::Version.prefix_map },
|
|
36
|
+
step: 1
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def prefix_map
|
|
42
|
+
(PaperTrail.config.mongo_prefix.is_a?(Proc) && PaperTrail.config.mongo_prefix.call) || 'paper_trail'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def next_integer_id
|
|
46
|
+
next_integer_ids(1).first
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def next_integer_ids(count, scope: prefix_map)
|
|
50
|
+
return [] if count.to_i <= 0
|
|
51
|
+
|
|
52
|
+
counter_id = counter_key(scope)
|
|
53
|
+
ensure_counter_initialized(counter_id, scope)
|
|
54
|
+
|
|
55
|
+
counter = AutoIncrementCounters.collection.find(_id: counter_id).find_one_and_update(
|
|
56
|
+
{ '$inc' => { sequence: count } },
|
|
57
|
+
upsert: true,
|
|
58
|
+
return_document: :after
|
|
59
|
+
)
|
|
60
|
+
max_integer_id = counter.fetch('sequence')
|
|
61
|
+
|
|
62
|
+
((max_integer_id - count + 1)..max_integer_id).to_a
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def bulk_insert_attributes(version)
|
|
66
|
+
version = version.is_a?(self) ? version : new(version)
|
|
67
|
+
attrs = version.attributes.dup
|
|
68
|
+
attrs.delete('_id') if attrs['_id'].nil?
|
|
69
|
+
attrs
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def table_name; end
|
|
73
|
+
|
|
74
|
+
def abstract_class?
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def columns_hash
|
|
79
|
+
fields
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def column_names
|
|
83
|
+
fields.keys
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def belongs_to(_name, _options = {}, &block); end
|
|
87
|
+
|
|
88
|
+
def validates_presence_of(_name); end
|
|
89
|
+
|
|
90
|
+
def after_create(_name); end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def counter_key(scope)
|
|
95
|
+
"#{name}:#{scope || prefix_map}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def ensure_counter_initialized(counter_id, _scope)
|
|
99
|
+
existing_counter = AutoIncrementCounters.collection.find(_id: counter_id).first
|
|
100
|
+
return if existing_counter.present?
|
|
101
|
+
|
|
102
|
+
max_id = maximum_existing_integer_id
|
|
103
|
+
initialize_counter(counter_id, max_id)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def initialize_counter(counter_id, max_id)
|
|
107
|
+
AutoIncrementCounters.collection.find(_id: counter_id).find_one_and_update(
|
|
108
|
+
{ '$setOnInsert' => { sequence: max_id } },
|
|
109
|
+
upsert: true,
|
|
110
|
+
return_document: :after
|
|
111
|
+
)
|
|
112
|
+
rescue Mongo::Error::OperationFailure => e
|
|
113
|
+
# DocumentDB can return E11000 when another writer creates this exact counter after the
|
|
114
|
+
# existence check. The caller can safely increment the counter created by that writer.
|
|
115
|
+
raise unless e.code == 11_000
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def maximum_existing_integer_id
|
|
119
|
+
pipeline = [
|
|
120
|
+
{ '$match' => {} },
|
|
121
|
+
{ '$group' => { _id: nil, max_integer_id: { '$max' => '$integer_id' } } }
|
|
122
|
+
]
|
|
123
|
+
Version.collection.aggregate(pipeline).first&.dig('max_integer_id').to_i
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
10
127
|
include PaperTrail::VersionConcern
|
|
11
128
|
include Mongoid::Document
|
|
12
|
-
include Mongoid::Autoinc
|
|
13
129
|
|
|
14
|
-
store_in collection: ->
|
|
130
|
+
store_in collection: -> { "#{MongoTrails::Version.prefix_map}_versions" }
|
|
15
131
|
|
|
16
132
|
field :item_type, type: String
|
|
17
|
-
field :item_id, type:
|
|
133
|
+
field :item_id, type: String
|
|
18
134
|
field :event, type: String
|
|
19
135
|
field :whodunnit, type: String
|
|
20
136
|
field :object, type: Hash
|
|
@@ -22,35 +138,149 @@ module PaperTrail
|
|
|
22
138
|
field :created_at, type: DateTime
|
|
23
139
|
field :integer_id, type: Integer
|
|
24
140
|
|
|
25
|
-
|
|
141
|
+
index({ item_type: -1, item_id: -1 }, { background: true })
|
|
26
142
|
|
|
27
|
-
|
|
28
|
-
def reset
|
|
29
|
-
Mongoid::QueryCache.clear_cache
|
|
30
|
-
end
|
|
143
|
+
before_create :assign_integer_id
|
|
31
144
|
|
|
32
|
-
|
|
33
|
-
find_by(integer_id: id)
|
|
34
|
-
end
|
|
145
|
+
attr_accessor :mongo_trails_source_item
|
|
35
146
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
147
|
+
def save_version
|
|
148
|
+
schedule_version_persistence(bang: false)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def save_version!
|
|
152
|
+
schedule_version_persistence(bang: true)
|
|
39
153
|
end
|
|
40
154
|
|
|
41
155
|
def initialize(data)
|
|
156
|
+
data = data.to_h.deep_symbolize_keys
|
|
42
157
|
item = data.delete(:item)
|
|
43
158
|
if item.present?
|
|
44
159
|
data[:item_type] = item.class.name
|
|
45
160
|
data[:item_id] = item.id
|
|
46
161
|
end
|
|
47
|
-
data[:created_at] =
|
|
162
|
+
data[:created_at] = normalize_created_at(data[:created_at])
|
|
48
163
|
|
|
49
164
|
super
|
|
50
165
|
end
|
|
51
166
|
|
|
52
167
|
def item
|
|
53
|
-
item_type.constantize.
|
|
168
|
+
item_type.constantize.find_by(id: item_id)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def object=(value)
|
|
172
|
+
super(escape_value(value))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def object
|
|
176
|
+
unescape_value(super)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def object_changes
|
|
180
|
+
unescape_value(super)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def object_changes=(value)
|
|
184
|
+
super(escape_value(value))
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def unescape_value(value)
|
|
188
|
+
value&.deep_transform_keys { |key| parser.unescape(force_utf8(key)) }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def escape_value(value)
|
|
192
|
+
value&.deep_transform_keys { |key| parser.escape(force_utf8(key.to_s), /[$.]/) }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
def schedule_version_persistence(bang:)
|
|
198
|
+
connection = transaction_connection
|
|
199
|
+
return persist_version(self, bang:) unless connection.transaction_open?
|
|
200
|
+
unless mongo_trails_source_item
|
|
201
|
+
return VersionCommitWrap.new { persist_version(self, bang:) }.add_to_transaction(connection)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if (propagation = callback_propagation)
|
|
205
|
+
merge_callback_propagation(propagation, connection:, bang:)
|
|
206
|
+
else
|
|
207
|
+
register_callback_propagation(connection:, bang:)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def transaction_connection
|
|
212
|
+
mongo_trails_source_item ? mongo_trails_source_item.class.connection : ActiveRecord::Base.connection
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def callback_propagation
|
|
216
|
+
return callback_propagations[event] unless event == 'update'
|
|
217
|
+
|
|
218
|
+
callback_propagations[event] || callback_propagations['create']
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def callback_propagations
|
|
222
|
+
mongo_trails_source_item.instance_variable_get(:@mongo_trails_callback_propagations) ||
|
|
223
|
+
mongo_trails_source_item.instance_variable_set(:@mongo_trails_callback_propagations, {})
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def register_callback_propagation(connection:, bang:)
|
|
227
|
+
propagation = CallbackPropagation.new(mongo_trails_source_item, self, bang:)
|
|
228
|
+
callback_propagations[event] = propagation
|
|
229
|
+
VersionCommitWrap.new(rollback: -> { propagation.clear }) { propagation.persist }.add_to_transaction(connection)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def merge_callback_propagation(propagation, connection:, bang:)
|
|
233
|
+
snapshot = propagation.snapshot
|
|
234
|
+
VersionCommitWrap.new(rollback: -> { propagation.restore!(snapshot) }) {}.add_to_transaction(connection)
|
|
235
|
+
propagation.merge!(self, bang:)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def assign_integer_id
|
|
239
|
+
self.integer_id ||= self.class.next_integer_id
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def normalize_created_at(value)
|
|
243
|
+
return value if value.is_a?(Time) || value.is_a?(DateTime)
|
|
244
|
+
return current_time if value.nil?
|
|
245
|
+
|
|
246
|
+
parse_time(value)
|
|
247
|
+
rescue ArgumentError
|
|
248
|
+
current_time
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def persist_version(version, bang: false)
|
|
252
|
+
return version.send(:async_save!) if sidekiq_enabled?
|
|
253
|
+
|
|
254
|
+
bang ? version.save! : version.save
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def parse_time(value)
|
|
258
|
+
time_zone&.parse(value.to_s) || Time.parse(value.to_s)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def current_time = time_zone&.now || Time.now
|
|
262
|
+
def time_zone = Time.zone
|
|
263
|
+
|
|
264
|
+
def sidekiq_enabled?
|
|
265
|
+
defined?(Sidekiq) && PaperTrail.config.enable_sidekiq
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def force_utf8(value)
|
|
269
|
+
value.respond_to?(:force_encoding) ? value.dup.force_encoding('UTF-8') : value
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def parser
|
|
273
|
+
@parser ||= URI::Parser.new
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def async_save!
|
|
277
|
+
worker = defined?(PaperTrail.config.sidekiq_worker.queue) ? PaperTrail.config.sidekiq_worker : PaperTrail::WriteVersionWorker
|
|
278
|
+
args = attributes.as_json
|
|
279
|
+
if worker == PaperTrail::WriteVersionWorker
|
|
280
|
+
worker.set(PaperTrail.config.sidekiq_options).perform_async(args)
|
|
281
|
+
else
|
|
282
|
+
worker.perform_async(args)
|
|
283
|
+
end
|
|
54
284
|
end
|
|
55
285
|
end
|
|
56
286
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MongoTrails
|
|
4
|
+
class VersionCommitWrap
|
|
5
|
+
def initialize(rollback: nil, &callback)
|
|
6
|
+
@callback = callback
|
|
7
|
+
@rollback = rollback
|
|
8
|
+
@executed = false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def has_transactional_callbacks? # rubocop:disable Naming/PredicatePrefix
|
|
12
|
+
true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def trigger_transactional_callbacks?
|
|
16
|
+
true
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def before_committed!; end
|
|
20
|
+
|
|
21
|
+
def rolledback!(*)
|
|
22
|
+
@executed = true
|
|
23
|
+
@rollback&.call
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def committed!(**)
|
|
27
|
+
return if @executed
|
|
28
|
+
|
|
29
|
+
@executed = true
|
|
30
|
+
@callback.call
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def add_to_transaction(connection = ActiveRecord::Base.connection)
|
|
34
|
+
connection.add_transaction_record(self)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -1,126 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "mongo_trails/events/create"
|
|
4
|
-
require "mongo_trails/events/destroy"
|
|
5
|
-
require "mongo_trails/events/update"
|
|
6
|
-
|
|
7
3
|
module PaperTrail
|
|
8
|
-
# Represents the "paper trail" for a single record.
|
|
9
4
|
class RecordTrail
|
|
10
|
-
RAILS_GTE_5_1 = ::ActiveRecord.gem_version >= ::Gem::Version.new("5.1.0.beta1")
|
|
11
|
-
|
|
12
|
-
def initialize(record)
|
|
13
|
-
@record = record
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# Invoked after rollbacks to ensure versions records are not created for
|
|
17
|
-
# changes that never actually took place. Optimization: Use lazy `reset`
|
|
18
|
-
# instead of eager `reload` because, in many use cases, the association will
|
|
19
|
-
# not be used.
|
|
20
|
-
def clear_rolled_back_versions
|
|
21
|
-
versions_reset
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def versions_reset
|
|
25
|
-
@record.class.paper_trail.version_class.reset
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Invoked via`after_update` callback for when a previous version is
|
|
29
|
-
# reified and then saved.
|
|
30
|
-
def clear_version_instance
|
|
31
|
-
@record.send("#{@record.class.version_association_name}=", nil)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Is PT enabled for this particular record?
|
|
35
|
-
# @api private
|
|
36
|
-
def enabled?
|
|
37
|
-
PaperTrail.enabled? &&
|
|
38
|
-
PaperTrail.request.enabled? &&
|
|
39
|
-
PaperTrail.request.enabled_for_model?(@record.class)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Returns true if this instance is the current, live one;
|
|
43
|
-
# returns false if this instance came from a previous version.
|
|
44
|
-
def live?
|
|
45
|
-
source_version.nil?
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Returns the object (not a Version) as it became next.
|
|
49
|
-
# NOTE: if self (the item) was not reified from a version, i.e. it is the
|
|
50
|
-
# "live" item, we return nil. Perhaps we should return self instead?
|
|
51
|
-
def next_version
|
|
52
|
-
subsequent_version = source_version.next
|
|
53
|
-
subsequent_version ? subsequent_version.reify : @record.class.find(@record.id)
|
|
54
|
-
rescue StandardError # TODO: Rescue something more specific
|
|
55
|
-
nil
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Returns who put `@record` into its current state.
|
|
59
|
-
#
|
|
60
|
-
# @api public
|
|
61
|
-
def originator
|
|
62
|
-
(source_version || versions.last).try(:whodunnit)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Returns the object (not a Version) as it was most recently.
|
|
66
|
-
#
|
|
67
|
-
# @api public
|
|
68
|
-
def previous_version
|
|
69
|
-
(source_version ? source_version.previous : versions.last).try(:reify)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
5
|
def record_create
|
|
73
6
|
return unless enabled?
|
|
74
7
|
|
|
75
|
-
build_version_on_create(in_after_callback: true)
|
|
76
|
-
|
|
77
|
-
# Because the version object was created using version_class.new instead
|
|
78
|
-
# of versions_assoc.build?, the association cache is unaware. So, we
|
|
79
|
-
# invalidate the `versions` association cache with `reset`.
|
|
80
|
-
versions_reset
|
|
81
|
-
end
|
|
82
|
-
end
|
|
8
|
+
version = build_version_on_create(in_after_callback: true)
|
|
9
|
+
return if exceeds_record_size_limit?(version)
|
|
83
10
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
# @api private
|
|
87
|
-
def data_for_create
|
|
88
|
-
{}
|
|
11
|
+
version.mongo_trails_source_item = @record
|
|
12
|
+
version.save_version!
|
|
89
13
|
end
|
|
90
14
|
|
|
91
|
-
# `recording_order` is "after" or "before". See ModelConfig#on_destroy.
|
|
92
|
-
#
|
|
93
|
-
# @api private
|
|
94
|
-
# @return - The created version object, so that plugins can use it, e.g.
|
|
95
|
-
# paper_trail-association_tracking
|
|
96
15
|
def record_destroy(recording_order)
|
|
97
16
|
return unless enabled? && !@record.new_record?
|
|
98
|
-
in_after_callback = recording_order == "after"
|
|
99
|
-
event = Events::Destroy.new(@record, in_after_callback)
|
|
100
17
|
|
|
101
|
-
|
|
102
|
-
|
|
18
|
+
in_after_callback = recording_order == 'after'
|
|
19
|
+
event = Events::Destroy.new(@record, in_after_callback)
|
|
103
20
|
data = event.data.merge(data_for_destroy)
|
|
104
21
|
|
|
105
|
-
version = @record.class.paper_trail.version_class.
|
|
106
|
-
if version
|
|
107
|
-
log_version_errors(version, :destroy)
|
|
108
|
-
else
|
|
109
|
-
assign_and_reset_version_association(version)
|
|
110
|
-
version
|
|
111
|
-
end
|
|
112
|
-
end
|
|
22
|
+
version = @record.class.paper_trail.version_class.new(data)
|
|
23
|
+
return if exceeds_record_size_limit?(version)
|
|
113
24
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
# @api private
|
|
117
|
-
def data_for_destroy
|
|
118
|
-
{}
|
|
25
|
+
version.mongo_trails_source_item = @record
|
|
26
|
+
version.save_version
|
|
119
27
|
end
|
|
120
28
|
|
|
121
|
-
# @api private
|
|
122
|
-
# @return - The created version object, so that plugins can use it, e.g.
|
|
123
|
-
# paper_trail-association_tracking
|
|
124
29
|
def record_update(force:, in_after_callback:, is_touch:)
|
|
125
30
|
return unless enabled?
|
|
126
31
|
|
|
@@ -129,176 +34,30 @@ module PaperTrail
|
|
|
129
34
|
in_after_callback: in_after_callback,
|
|
130
35
|
is_touch: is_touch
|
|
131
36
|
)
|
|
132
|
-
return unless version
|
|
37
|
+
return unless version && !exceeds_record_size_limit?(version)
|
|
133
38
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# of versions_assoc.build?, the association cache is unaware. So, we
|
|
137
|
-
# invalidate the `versions` association cache with `reset`.
|
|
138
|
-
versions_reset
|
|
139
|
-
version
|
|
140
|
-
else
|
|
141
|
-
log_version_errors(version, :update)
|
|
142
|
-
end
|
|
39
|
+
version.mongo_trails_source_item = @record
|
|
40
|
+
version.save_version
|
|
143
41
|
end
|
|
144
42
|
|
|
145
|
-
# PT-AT extends this method to add its transaction id.
|
|
146
|
-
#
|
|
147
|
-
# @api private
|
|
148
|
-
def data_for_update
|
|
149
|
-
{}
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# @api private
|
|
153
|
-
# @return - The created version object, so that plugins can use it, e.g.
|
|
154
|
-
# paper_trail-association_tracking
|
|
155
43
|
def record_update_columns(changes)
|
|
156
44
|
return unless enabled?
|
|
157
|
-
event = Events::Update.new(@record, false, false, changes)
|
|
158
45
|
|
|
159
|
-
|
|
160
|
-
# `data_for_update_columns` but PT-AT still does.
|
|
46
|
+
event = Events::Update.new(@record, false, false, changes)
|
|
161
47
|
data = event.data.merge(data_for_update_columns)
|
|
162
|
-
|
|
163
48
|
versions_assoc = @record.send(@record.class.versions_association_name)
|
|
164
|
-
version = versions_assoc.
|
|
165
|
-
if version
|
|
166
|
-
log_version_errors(version, :update)
|
|
167
|
-
else
|
|
168
|
-
version
|
|
169
|
-
end
|
|
170
|
-
end
|
|
49
|
+
version = versions_assoc.new(data)
|
|
50
|
+
return if exceeds_record_size_limit?(version)
|
|
171
51
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
# @api private
|
|
175
|
-
def data_for_update_columns
|
|
176
|
-
{}
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
# Invoked via callback when a user attempts to persist a reified
|
|
180
|
-
# `Version`.
|
|
181
|
-
def reset_timestamp_attrs_for_update_if_needed
|
|
182
|
-
return if live?
|
|
183
|
-
@record.send(:timestamp_attributes_for_update_in_model).each do |column|
|
|
184
|
-
@record.send("restore_#{column}!")
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# AR callback.
|
|
189
|
-
# @api private
|
|
190
|
-
def save_version?
|
|
191
|
-
if_condition = @record.paper_trail_options[:if]
|
|
192
|
-
unless_condition = @record.paper_trail_options[:unless]
|
|
193
|
-
(if_condition.blank? || if_condition.call(@record)) && !unless_condition.try(:call, @record)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def source_version
|
|
197
|
-
version
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
# Save, and create a version record regardless of options such as `:on`,
|
|
201
|
-
# `:if`, or `:unless`.
|
|
202
|
-
#
|
|
203
|
-
# Arguments are passed to `save`.
|
|
204
|
-
#
|
|
205
|
-
# This is an "update" event. That is, we record the same data we would in
|
|
206
|
-
# the case of a normal AR `update`.
|
|
207
|
-
def save_with_version(*args)
|
|
208
|
-
::PaperTrail.request(enabled: false) do
|
|
209
|
-
@record.save(*args)
|
|
210
|
-
end
|
|
211
|
-
record_update(force: true, in_after_callback: false, is_touch: false)
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
# Like the `update_column` method from `ActiveRecord::Persistence`, but also
|
|
215
|
-
# creates a version to record those changes.
|
|
216
|
-
# @api public
|
|
217
|
-
def update_column(name, value)
|
|
218
|
-
update_columns(name => value)
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
# Like the `update_columns` method from `ActiveRecord::Persistence`, but also
|
|
222
|
-
# creates a version to record those changes.
|
|
223
|
-
# @api public
|
|
224
|
-
def update_columns(attributes)
|
|
225
|
-
# `@record.update_columns` skips dirty-tracking, so we can't just use
|
|
226
|
-
# `@record.changes` or @record.saved_changes` from `ActiveModel::Dirty`.
|
|
227
|
-
# We need to build our own hash with the changes that will be made
|
|
228
|
-
# directly to the database.
|
|
229
|
-
changes = {}
|
|
230
|
-
attributes.each do |k, v|
|
|
231
|
-
changes[k] = [@record[k], v]
|
|
232
|
-
end
|
|
233
|
-
@record.update_columns(attributes)
|
|
234
|
-
record_update_columns(changes)
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# Returns the object (not a Version) as it was at the given timestamp.
|
|
238
|
-
def version_at(timestamp, reify_options = {})
|
|
239
|
-
# Because a version stores how its object looked *before* the change,
|
|
240
|
-
# we need to look for the first version created *after* the timestamp.
|
|
241
|
-
v = versions.subsequent(timestamp, true).first
|
|
242
|
-
return v.reify(reify_options) if v
|
|
243
|
-
@record unless @record.destroyed?
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
# Returns the objects (not Versions) as they were between the given times.
|
|
247
|
-
def versions_between(start_time, end_time)
|
|
248
|
-
versions = send(@record.class.versions_association_name).between(start_time, end_time)
|
|
249
|
-
versions.collect { |version| version_at(version.created_at) }
|
|
52
|
+
version.mongo_trails_source_item = @record
|
|
53
|
+
version.save_version
|
|
250
54
|
end
|
|
251
55
|
|
|
252
56
|
private
|
|
253
57
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
@record.send(@record.class.versions_association_name).reset
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
# @api private
|
|
261
|
-
def build_version_on_create(in_after_callback:)
|
|
262
|
-
event = Events::Create.new(@record, in_after_callback)
|
|
263
|
-
|
|
264
|
-
# Merge data from `Event` with data from PT-AT. We no longer use
|
|
265
|
-
# `data_for_create` but PT-AT still does.
|
|
266
|
-
data = event.data.merge!(data_for_create)
|
|
267
|
-
|
|
268
|
-
# Pure `version_class.new` reduces memory usage compared to `versions_assoc.build`
|
|
269
|
-
@record.class.paper_trail.version_class.new(data)
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# @api private
|
|
273
|
-
def build_version_on_update(force:, in_after_callback:, is_touch:)
|
|
274
|
-
event = Events::Update.new(@record, in_after_callback, is_touch, nil)
|
|
275
|
-
return unless force || event.changed_notably?
|
|
276
|
-
|
|
277
|
-
# Merge data from `Event` with data from PT-AT. We no longer use
|
|
278
|
-
# `data_for_update` but PT-AT still does. To save memory, we use `merge!`
|
|
279
|
-
# instead of `merge`.
|
|
280
|
-
data = event.data.merge!(data_for_update)
|
|
281
|
-
|
|
282
|
-
# Using `version_class.new` reduces memory usage compared to
|
|
283
|
-
# `versions_assoc.build`. It's a trade-off though. We have to clear
|
|
284
|
-
# the association cache (see `versions.reset`) and that could cause an
|
|
285
|
-
# additional query in certain applications.
|
|
286
|
-
@record.class.paper_trail.version_class.new(data)
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
def log_version_errors(version, action)
|
|
290
|
-
version.logger&.warn(
|
|
291
|
-
"Unable to create version for #{action} of #{@record.class.name}" \
|
|
292
|
-
"##{@record.id}: " + version.errors.full_messages.join(", ")
|
|
293
|
-
)
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
def version
|
|
297
|
-
@record.public_send(@record.class.version_association_name)
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def versions
|
|
301
|
-
@record.public_send(@record.class.versions_association_name)
|
|
58
|
+
def exceeds_record_size_limit?(version)
|
|
59
|
+
size_limit = PaperTrail.config.mongo_trails_config&.dig(:record_size_limit)
|
|
60
|
+
size_limit && size_limit.to_i < version.to_json.to_s.bytesize
|
|
302
61
|
end
|
|
303
62
|
end
|
|
304
63
|
end
|