paper_trail_diff 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/README.md +9 -0
- data/lib/paper_trail_diff/activity_belongs_to_event_applier.rb +126 -0
- data/lib/paper_trail_diff/activity_collection_event_applier.rb +137 -0
- data/lib/paper_trail_diff/activity_collection_record_updater.rb +67 -0
- data/lib/paper_trail_diff/activity_collection_route_change.rb +21 -0
- data/lib/paper_trail_diff/activity_collection_route_updater.rb +128 -0
- data/lib/paper_trail_diff/activity_event_record_normalizer.rb +42 -0
- data/lib/paper_trail_diff/activity_event_record_resolver.rb +120 -0
- data/lib/paper_trail_diff/activity_event_route_finder.rb +112 -0
- data/lib/paper_trail_diff/activity_event_snapshot_refresher.rb +56 -650
- data/lib/paper_trail_diff/activity_relationship.rb +55 -0
- data/lib/paper_trail_diff/branch_snapshot_refresher.rb +2 -0
- data/lib/paper_trail_diff/version.rb +1 -1
- data/lib/paper_trail_diff.rb +9 -0
- data/sig/generated/paper_trail_diff/activity_belongs_to_event_applier.rbs +41 -0
- data/sig/generated/paper_trail_diff/activity_collection_event_applier.rbs +46 -0
- data/sig/generated/paper_trail_diff/activity_collection_record_updater.rbs +25 -0
- data/sig/generated/paper_trail_diff/activity_collection_route_change.rbs +17 -0
- data/sig/generated/paper_trail_diff/activity_collection_route_updater.rbs +44 -0
- data/sig/generated/paper_trail_diff/activity_event_record_normalizer.rbs +19 -0
- data/sig/generated/paper_trail_diff/activity_event_record_resolver.rbs +44 -0
- data/sig/generated/paper_trail_diff/activity_event_route_finder.rbs +54 -0
- data/sig/generated/paper_trail_diff/activity_event_snapshot_refresher.rbs +18 -110
- data/sig/generated/paper_trail_diff/activity_relationship.rbs +23 -0
- metadata +22 -4
|
@@ -2,26 +2,53 @@
|
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
|
-
#
|
|
6
|
-
# rubocop:disable Metrics/ClassLength
|
|
5
|
+
# Dispatches safe descendant events to focused immutable snapshot appliers.
|
|
7
6
|
class ActivityEventSnapshotRefresher
|
|
8
|
-
RECORD_AFTER_HANDLERS = {
|
|
9
|
-
'create' => :created_record_after,
|
|
10
|
-
'update' => :updated_record_after
|
|
11
|
-
}.freeze
|
|
12
|
-
private_constant :RECORD_AFTER_HANDLERS
|
|
13
|
-
|
|
14
7
|
#: (traversal: AssociationTraversal, pool: SnapshotPool, components: untyped, ?association_reader: untyped, ?record_transition: untyped) -> void
|
|
15
8
|
def initialize(traversal:, pool:, components:, association_reader: nil, record_transition: nil)
|
|
16
|
-
@traversal = traversal
|
|
17
|
-
@pool = pool
|
|
18
9
|
@components = components
|
|
19
|
-
@
|
|
20
|
-
@
|
|
10
|
+
@record_resolver = ActivityEventRecordResolver.new(record_transition: record_transition)
|
|
11
|
+
@route_finder = ActivityEventRouteFinder.new(traversal)
|
|
12
|
+
@relationship = ActivityRelationship.new
|
|
13
|
+
configure_appliers(pool, association_reader)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
#: (SnapshotPool, untyped) -> void
|
|
17
|
+
def configure_appliers(pool, association_reader)
|
|
18
|
+
route_updater = collection_updater(pool)
|
|
19
|
+
record_normalizer = ActivityEventRecordNormalizer.new(
|
|
20
|
+
association_reader: association_reader
|
|
21
|
+
)
|
|
22
|
+
@collection_applier = collection_applier(record_normalizer, route_updater)
|
|
23
|
+
@belongs_to_applier = belongs_to_applier(record_normalizer)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#: (SnapshotPool) -> ActivityCollectionRouteUpdater
|
|
27
|
+
def collection_updater(pool)
|
|
28
|
+
ActivityCollectionRouteUpdater.new(pool: pool, relationship: @relationship)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#: (ActivityEventRecordNormalizer, ActivityCollectionRouteUpdater) -> ActivityCollectionEventApplier
|
|
32
|
+
def collection_applier(record_normalizer, route_updater)
|
|
33
|
+
ActivityCollectionEventApplier.new(
|
|
34
|
+
record_resolver: @record_resolver,
|
|
35
|
+
record_normalizer: record_normalizer,
|
|
36
|
+
route_updater: route_updater,
|
|
37
|
+
relationship: @relationship
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#: (ActivityEventRecordNormalizer) -> ActivityBelongsToEventApplier
|
|
42
|
+
def belongs_to_applier(record_normalizer)
|
|
43
|
+
ActivityBelongsToEventApplier.new(
|
|
44
|
+
route_finder: @route_finder,
|
|
45
|
+
record_resolver: @record_resolver,
|
|
46
|
+
record_normalizer: record_normalizer
|
|
47
|
+
)
|
|
21
48
|
end
|
|
22
49
|
|
|
23
50
|
#: (untyped, untyped, RecordSnapshot, Array[String], ActivityEvent?) -> [bool, RecordSnapshot?]
|
|
24
|
-
def call( # rubocop:disable Metrics/
|
|
51
|
+
def call( # rubocop:disable Metrics/MethodLength
|
|
25
52
|
root_endpoint,
|
|
26
53
|
context_endpoint,
|
|
27
54
|
previous,
|
|
@@ -31,29 +58,25 @@ module PaperTrailDiff
|
|
|
31
58
|
return [false, nil] unless event && branches.one?
|
|
32
59
|
|
|
33
60
|
tree, normalizer = @components.call(branches)
|
|
34
|
-
|
|
35
|
-
endpoint_model_class(root_endpoint),
|
|
36
|
-
tree,
|
|
37
|
-
event.version.item_type.to_s
|
|
61
|
+
routes = @route_finder.collection_routes(
|
|
62
|
+
endpoint_model_class(root_endpoint), tree, event.version.item_type.to_s
|
|
38
63
|
)
|
|
39
|
-
unless
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
delta = activity_snapshot_delta(event.version)
|
|
43
|
-
return [false, nil] if unsafe_nested_membership_delta?(collection_routes, delta)
|
|
64
|
+
unless routes.empty?
|
|
65
|
+
delta = @record_resolver.snapshot_delta(event.version)
|
|
66
|
+
return [false, nil] unless @collection_applier.safe?(routes, event.version, delta)
|
|
44
67
|
|
|
45
|
-
return
|
|
68
|
+
return @collection_applier.call(
|
|
46
69
|
root_endpoint,
|
|
47
70
|
context_endpoint,
|
|
48
71
|
previous,
|
|
49
72
|
normalizer,
|
|
50
|
-
|
|
73
|
+
routes,
|
|
51
74
|
event.version,
|
|
52
75
|
delta
|
|
53
76
|
)
|
|
54
77
|
end
|
|
55
78
|
|
|
56
|
-
|
|
79
|
+
@belongs_to_applier.call(
|
|
57
80
|
root_endpoint,
|
|
58
81
|
context_endpoint,
|
|
59
82
|
previous,
|
|
@@ -65,639 +88,23 @@ module PaperTrailDiff
|
|
|
65
88
|
|
|
66
89
|
private
|
|
67
90
|
|
|
68
|
-
# @rbs @traversal: AssociationTraversal
|
|
69
|
-
# @rbs @pool: SnapshotPool
|
|
70
91
|
# @rbs @components: untyped
|
|
71
|
-
# @rbs @
|
|
72
|
-
# @rbs @
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
return false if version.event.to_s == 'update'
|
|
77
|
-
|
|
78
|
-
routes.any? do |route|
|
|
79
|
-
route.first(route.length - 1).any? do |entry|
|
|
80
|
-
reflection = entry.fetch(1)
|
|
81
|
-
reflection.options[:through]
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
#: (Array[Array[untyped]], ActivitySnapshotDelta?) -> bool
|
|
87
|
-
def unsafe_nested_membership_delta?(routes, delta)
|
|
88
|
-
return false unless delta
|
|
89
|
-
|
|
90
|
-
routes.any? do |route|
|
|
91
|
-
route.length > 1 && delta.relationship_changed?(route.last.fetch(1))
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
#: (untyped, untyped, RecordSnapshot, SnapshotNormalizer, Array[Array[untyped]], untyped, ActivitySnapshotDelta?) -> [bool, RecordSnapshot]
|
|
96
|
-
def collection_target_snapshot( # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
97
|
-
root_endpoint,
|
|
98
|
-
context_endpoint,
|
|
99
|
-
previous,
|
|
100
|
-
normalizer,
|
|
101
|
-
routes,
|
|
102
|
-
version,
|
|
103
|
-
delta
|
|
104
|
-
)
|
|
105
|
-
fallback_record = [] #: Array[untyped]
|
|
106
|
-
snapshot = previous
|
|
107
|
-
routes.each do |route|
|
|
108
|
-
record, replacement = collection_route_event_state(
|
|
109
|
-
snapshot,
|
|
110
|
-
route,
|
|
111
|
-
version,
|
|
112
|
-
delta,
|
|
113
|
-
fallback_record,
|
|
114
|
-
normalizer,
|
|
115
|
-
root_endpoint,
|
|
116
|
-
context_endpoint
|
|
117
|
-
)
|
|
118
|
-
snapshot, = replace_collection_route(snapshot, route, version, record, replacement)
|
|
119
|
-
end
|
|
120
|
-
[true, snapshot]
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
#: (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?, Array[untyped], SnapshotNormalizer, untyped, untyped) -> [untyped, RecordSnapshot?]
|
|
124
|
-
def collection_route_event_state( # rubocop:disable Metrics/ParameterLists
|
|
125
|
-
snapshot,
|
|
126
|
-
route,
|
|
127
|
-
version,
|
|
128
|
-
delta,
|
|
129
|
-
fallback_record,
|
|
130
|
-
normalizer,
|
|
131
|
-
root_endpoint,
|
|
132
|
-
context_endpoint
|
|
133
|
-
)
|
|
134
|
-
existing = existing_delta_record(snapshot, route, version, delta) if delta
|
|
135
|
-
return [delta, delta.apply(existing)] if delta && existing
|
|
136
|
-
|
|
137
|
-
fallback_record << record_after(version) if fallback_record.empty?
|
|
138
|
-
record = fallback_record.first
|
|
139
|
-
return [record, nil] unless record
|
|
140
|
-
|
|
141
|
-
[record, normalized_collection_event_record(
|
|
142
|
-
record, route, normalizer, root_endpoint, context_endpoint
|
|
143
|
-
)]
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
#: (untyped, Array[untyped], SnapshotNormalizer, untyped, untyped) -> RecordSnapshot?
|
|
147
|
-
def normalized_collection_event_record(
|
|
148
|
-
record,
|
|
149
|
-
route,
|
|
150
|
-
normalizer,
|
|
151
|
-
root_endpoint,
|
|
152
|
-
context_endpoint
|
|
153
|
-
)
|
|
154
|
-
_name, reflection, subtree, path = route.last
|
|
155
|
-
normalize_event_record(
|
|
156
|
-
record,
|
|
157
|
-
reflection,
|
|
158
|
-
subtree,
|
|
159
|
-
path,
|
|
160
|
-
normalizer,
|
|
161
|
-
root_endpoint,
|
|
162
|
-
context_endpoint
|
|
163
|
-
)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
#: (RecordSnapshot, Array[untyped], untyped, ActivitySnapshotDelta?) -> RecordSnapshot?
|
|
167
|
-
def existing_delta_record(snapshot, route, version, delta)
|
|
168
|
-
return unless delta && route.length.between?(1, 2)
|
|
169
|
-
|
|
170
|
-
owner = route.length == 1 ? snapshot : nested_delta_owner(snapshot, route, delta)
|
|
171
|
-
return unless owner
|
|
172
|
-
|
|
173
|
-
association = owner.associations.fetch(route.last.fetch(0))
|
|
174
|
-
position = association.position_for_id(version.item_id)
|
|
175
|
-
association.records.fetch(position) if position
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
#: (RecordSnapshot, Array[untyped], ActivitySnapshotDelta) -> RecordSnapshot?
|
|
179
|
-
def nested_delta_owner(snapshot, route, delta)
|
|
180
|
-
parent_name = route.first.fetch(0)
|
|
181
|
-
reflection = route.last.fetch(1)
|
|
182
|
-
association = snapshot.associations.fetch(parent_name)
|
|
183
|
-
owner_id = relationship_owner_id(delta, reflection, state: :before)
|
|
184
|
-
position = association.position_for_id(owner_id)
|
|
185
|
-
return unless position
|
|
186
|
-
|
|
187
|
-
owner = association.records.fetch(position)
|
|
188
|
-
owner if member_of_owner?(delta, reflection, owner, state: :before)
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
#: (untyped, AssociationTree, String, ?path: String) -> Array[Array[untyped]]
|
|
192
|
-
def direct_collection_routes( # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
193
|
-
model_class,
|
|
194
|
-
tree,
|
|
195
|
-
target_type,
|
|
196
|
-
path: ''
|
|
197
|
-
)
|
|
198
|
-
@traversal.reflections_for(model_class, tree, path: path).flat_map do |reflection|
|
|
199
|
-
name = reflection.name.to_s
|
|
200
|
-
subtree = tree.child(name)
|
|
201
|
-
next [] unless subtree
|
|
202
|
-
|
|
203
|
-
child_path = Support.association_path(path, name)
|
|
204
|
-
route_entry = [name, reflection, subtree, child_path]
|
|
205
|
-
routes = [] #: Array[Array[untyped]]
|
|
206
|
-
if reflection.macro == :has_many && !reflection.options[:through] &&
|
|
207
|
-
reflection.klass.base_class.name.to_s == target_type
|
|
208
|
-
routes << [route_entry]
|
|
209
|
-
end
|
|
210
|
-
unless subtree.empty? || reflection.polymorphic?
|
|
211
|
-
nested = direct_collection_routes(
|
|
212
|
-
reflection.klass,
|
|
213
|
-
subtree,
|
|
214
|
-
target_type,
|
|
215
|
-
path: child_path
|
|
216
|
-
)
|
|
217
|
-
routes.concat(nested.map { |route| [route_entry, *route] })
|
|
218
|
-
end
|
|
219
|
-
routes
|
|
220
|
-
end
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
#: (RecordSnapshot, Array[untyped], untyped, untyped, RecordSnapshot?, ?depth: Integer) -> [RecordSnapshot, bool]
|
|
224
|
-
def replace_collection_route( # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
225
|
-
snapshot,
|
|
226
|
-
route,
|
|
227
|
-
version,
|
|
228
|
-
record,
|
|
229
|
-
replacement,
|
|
230
|
-
depth: 0
|
|
231
|
-
)
|
|
232
|
-
name, reflection, _subtree, path = route.fetch(depth)
|
|
233
|
-
association = snapshot.associations.fetch(name)
|
|
234
|
-
changed = false
|
|
235
|
-
transition_before = nil #: RecordSnapshot?
|
|
236
|
-
transition_after = nil #: RecordSnapshot?
|
|
237
|
-
membership_preserved = true
|
|
238
|
-
updated_records = if depth == route.length - 1
|
|
239
|
-
child = collection_event_child(
|
|
240
|
-
association,
|
|
241
|
-
version,
|
|
242
|
-
record,
|
|
243
|
-
reflection,
|
|
244
|
-
snapshot,
|
|
245
|
-
replacement
|
|
246
|
-
)
|
|
247
|
-
records, transition_before, transition_after, membership_preserved =
|
|
248
|
-
replace_collection_record(association, version, child)
|
|
249
|
-
records.tap { |value| changed = !value.equal?(association.records) }
|
|
250
|
-
elsif (targeted = replace_targeted_collection_parent(
|
|
251
|
-
association,
|
|
252
|
-
route,
|
|
253
|
-
version,
|
|
254
|
-
record,
|
|
255
|
-
replacement,
|
|
256
|
-
depth
|
|
257
|
-
))
|
|
258
|
-
records, transition_before, transition_after, changed = targeted
|
|
259
|
-
records
|
|
260
|
-
else
|
|
261
|
-
association.records.map do |child_snapshot|
|
|
262
|
-
updated, child_changed = replace_collection_route(
|
|
263
|
-
child_snapshot,
|
|
264
|
-
route,
|
|
265
|
-
version,
|
|
266
|
-
record,
|
|
267
|
-
replacement,
|
|
268
|
-
depth: depth + 1
|
|
269
|
-
)
|
|
270
|
-
if child_changed
|
|
271
|
-
transition_before = nil if changed
|
|
272
|
-
transition_after = nil if changed
|
|
273
|
-
unless changed
|
|
274
|
-
transition_before = child_snapshot
|
|
275
|
-
transition_after = updated
|
|
276
|
-
end
|
|
277
|
-
changed = true
|
|
278
|
-
end
|
|
279
|
-
updated
|
|
280
|
-
end.freeze
|
|
281
|
-
end
|
|
282
|
-
return [snapshot, false] unless changed
|
|
283
|
-
|
|
284
|
-
updated_association = @pool.association(
|
|
285
|
-
path,
|
|
286
|
-
association.transition_to(
|
|
287
|
-
updated_records,
|
|
288
|
-
before: transition_before,
|
|
289
|
-
after: transition_after,
|
|
290
|
-
membership_preserved: membership_preserved
|
|
291
|
-
)
|
|
292
|
-
)
|
|
293
|
-
updated_snapshot = RecordSnapshot.new(
|
|
294
|
-
type: snapshot.type,
|
|
295
|
-
id: snapshot.id,
|
|
296
|
-
attributes: snapshot.attributes,
|
|
297
|
-
associations: snapshot.associations.merge(name => updated_association)
|
|
298
|
-
)
|
|
299
|
-
parent_path = depth.zero? ? '' : route.fetch(depth - 1).fetch(3)
|
|
300
|
-
updated_snapshot = @pool.record(parent_path, updated_snapshot) unless parent_path.empty?
|
|
301
|
-
[updated_snapshot, true]
|
|
302
|
-
end
|
|
303
|
-
|
|
304
|
-
#: (AssociationSnapshot, untyped, untyped, untyped, RecordSnapshot, RecordSnapshot?) -> RecordSnapshot?
|
|
305
|
-
def collection_event_child( # rubocop:disable Metrics/ParameterLists
|
|
306
|
-
association, version, record, reflection, owner, replacement
|
|
307
|
-
)
|
|
308
|
-
return unless record
|
|
309
|
-
|
|
310
|
-
unless record.is_a?(ActivitySnapshotDelta)
|
|
311
|
-
return replacement if member_of_owner?(record, reflection, owner)
|
|
312
|
-
|
|
313
|
-
return
|
|
314
|
-
end
|
|
315
|
-
unless record.relationship_changed?(reflection)
|
|
316
|
-
return replacement if association.position_for_id(version.item_id)
|
|
317
|
-
|
|
318
|
-
return
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
replacement if member_of_owner?(record, reflection, owner)
|
|
322
|
-
end
|
|
323
|
-
|
|
324
|
-
#: (AssociationSnapshot, Array[untyped], untyped, untyped, RecordSnapshot?, Integer) -> [Array[RecordSnapshot], RecordSnapshot?, RecordSnapshot?, bool]?
|
|
325
|
-
def replace_targeted_collection_parent( # rubocop:disable Metrics/ParameterLists
|
|
326
|
-
association,
|
|
327
|
-
route,
|
|
328
|
-
version,
|
|
329
|
-
record,
|
|
330
|
-
replacement,
|
|
331
|
-
depth
|
|
332
|
-
)
|
|
333
|
-
return unless depth == route.length - 2
|
|
334
|
-
|
|
335
|
-
reflection = route.fetch(depth + 1).fetch(1)
|
|
336
|
-
position = nested_owner_position(association, reflection, version, record)
|
|
337
|
-
return unless position
|
|
338
|
-
|
|
339
|
-
before = association.records.fetch(position)
|
|
340
|
-
after, changed = replace_collection_route(
|
|
341
|
-
before,
|
|
342
|
-
route,
|
|
343
|
-
version,
|
|
344
|
-
record,
|
|
345
|
-
replacement,
|
|
346
|
-
depth: depth + 1
|
|
347
|
-
)
|
|
348
|
-
targeted_parent_replacement(association.records, position, before, after, changed)
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
#: (Array[RecordSnapshot], Integer, RecordSnapshot, RecordSnapshot, bool) -> [Array[RecordSnapshot], RecordSnapshot?, RecordSnapshot?, bool]
|
|
352
|
-
def targeted_parent_replacement(records, position, before, after, changed)
|
|
353
|
-
return [records, nil, nil, false] unless changed
|
|
354
|
-
|
|
355
|
-
updated = records.dup
|
|
356
|
-
updated[position] = after
|
|
357
|
-
[updated.freeze, before, after, true]
|
|
358
|
-
end
|
|
359
|
-
|
|
360
|
-
#: (AssociationSnapshot, untyped, untyped, untyped) -> Integer?
|
|
361
|
-
def nested_owner_position(association, reflection, version, record)
|
|
362
|
-
membership_record = record || version.reify(dup: true)
|
|
363
|
-
return unless membership_record
|
|
364
|
-
return if membership_record.is_a?(ActivitySnapshotDelta) &&
|
|
365
|
-
membership_record.relationship_changed?(reflection)
|
|
366
|
-
|
|
367
|
-
owner_id = relationship_owner_id(membership_record, reflection, state: :after)
|
|
368
|
-
association.position_for_id(owner_id)
|
|
369
|
-
end
|
|
370
|
-
|
|
371
|
-
#: (untyped, untyped, RecordSnapshot, AssociationTree, SnapshotNormalizer, untyped) -> [bool, RecordSnapshot?]
|
|
372
|
-
def belongs_to_target_snapshot( # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
373
|
-
root_endpoint,
|
|
374
|
-
context_endpoint,
|
|
375
|
-
previous,
|
|
376
|
-
tree,
|
|
377
|
-
normalizer,
|
|
378
|
-
version
|
|
379
|
-
)
|
|
380
|
-
return [false, nil] unless %w[update destroy].include?(version.event.to_s)
|
|
92
|
+
# @rbs @record_resolver: ActivityEventRecordResolver
|
|
93
|
+
# @rbs @route_finder: ActivityEventRouteFinder
|
|
94
|
+
# @rbs @relationship: ActivityRelationship
|
|
95
|
+
# @rbs @collection_applier: ActivityCollectionEventApplier
|
|
96
|
+
# @rbs @belongs_to_applier: ActivityBelongsToEventApplier
|
|
381
97
|
|
|
382
|
-
|
|
383
|
-
routes = belongs_to_routes(root_class, tree, version.item_type.to_s)
|
|
384
|
-
return [false, nil] if routes.empty?
|
|
385
|
-
|
|
386
|
-
snapshot = previous
|
|
387
|
-
routes.each do |route|
|
|
388
|
-
replacement = normalized_route_record(
|
|
389
|
-
route,
|
|
390
|
-
version,
|
|
391
|
-
normalizer,
|
|
392
|
-
root_endpoint,
|
|
393
|
-
context_endpoint
|
|
394
|
-
)
|
|
395
|
-
snapshot, = replace_route(snapshot, route, version, replacement)
|
|
396
|
-
end
|
|
397
|
-
[true, snapshot]
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
#: (untyped, AssociationTree, String, ?path: String) -> Array[Array[untyped]]
|
|
401
|
-
def belongs_to_routes( # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
402
|
-
model_class,
|
|
403
|
-
tree,
|
|
404
|
-
target_type,
|
|
405
|
-
path: ''
|
|
406
|
-
)
|
|
407
|
-
@traversal.reflections_for(model_class, tree, path: path).flat_map do |reflection|
|
|
408
|
-
name = reflection.name.to_s
|
|
409
|
-
subtree = tree.child(name)
|
|
410
|
-
next [] unless subtree
|
|
411
|
-
|
|
412
|
-
child_path = Support.association_path(path, name)
|
|
413
|
-
route_entry = [name, reflection, subtree, child_path]
|
|
414
|
-
routes = [] #: Array[Array[untyped]]
|
|
415
|
-
if reflection.macro == :belongs_to && !reflection.polymorphic? &&
|
|
416
|
-
reflection.klass.base_class.name.to_s == target_type
|
|
417
|
-
routes << [route_entry]
|
|
418
|
-
end
|
|
419
|
-
unless subtree.empty? || reflection.polymorphic?
|
|
420
|
-
nested = belongs_to_routes(reflection.klass, subtree, target_type, path: child_path)
|
|
421
|
-
routes.concat(nested.map { |route| [route_entry, *route] })
|
|
422
|
-
end
|
|
423
|
-
routes
|
|
424
|
-
end
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
#: (Array[untyped], untyped, SnapshotNormalizer, untyped, untyped) -> RecordSnapshot?
|
|
428
|
-
def normalized_route_record(route, version, normalizer, root_endpoint, context_endpoint)
|
|
429
|
-
record = record_after(version)
|
|
430
|
-
return unless record
|
|
431
|
-
|
|
432
|
-
_name, reflection, subtree, path = route.last
|
|
433
|
-
normalize_event_record(
|
|
434
|
-
record,
|
|
435
|
-
reflection,
|
|
436
|
-
subtree,
|
|
437
|
-
path,
|
|
438
|
-
normalizer,
|
|
439
|
-
root_endpoint,
|
|
440
|
-
context_endpoint
|
|
441
|
-
)
|
|
442
|
-
end
|
|
443
|
-
|
|
444
|
-
#: (RecordSnapshot, Array[untyped], untyped, RecordSnapshot?, ?depth: Integer) -> [RecordSnapshot, bool]
|
|
445
|
-
def replace_route( # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
446
|
-
snapshot,
|
|
447
|
-
route,
|
|
448
|
-
version,
|
|
449
|
-
replacement,
|
|
450
|
-
depth: 0
|
|
451
|
-
)
|
|
452
|
-
name, _reflection, _subtree, = route.fetch(depth)
|
|
453
|
-
association = snapshot.associations.fetch(name)
|
|
454
|
-
|
|
455
|
-
records = association.records
|
|
456
|
-
changed = false
|
|
457
|
-
updated_records = if depth == route.length - 1
|
|
458
|
-
replace_existing_target(records, version, replacement).tap do |value|
|
|
459
|
-
changed = !value.equal?(records)
|
|
460
|
-
end
|
|
461
|
-
else
|
|
462
|
-
records.map do |record|
|
|
463
|
-
updated, record_changed = replace_route(
|
|
464
|
-
record,
|
|
465
|
-
route,
|
|
466
|
-
version,
|
|
467
|
-
replacement,
|
|
468
|
-
depth: depth + 1
|
|
469
|
-
)
|
|
470
|
-
changed ||= record_changed
|
|
471
|
-
updated
|
|
472
|
-
end.freeze
|
|
473
|
-
end
|
|
474
|
-
return [snapshot, false] unless changed
|
|
475
|
-
|
|
476
|
-
updated_association = AssociationSnapshot.new(
|
|
477
|
-
kind: association.kind,
|
|
478
|
-
records: updated_records
|
|
479
|
-
)
|
|
480
|
-
updated_snapshot = RecordSnapshot.new(
|
|
481
|
-
type: snapshot.type,
|
|
482
|
-
id: snapshot.id,
|
|
483
|
-
attributes: snapshot.attributes,
|
|
484
|
-
associations: snapshot.associations.merge(name => updated_association)
|
|
485
|
-
)
|
|
486
|
-
[updated_snapshot, true]
|
|
487
|
-
end
|
|
488
|
-
|
|
489
|
-
#: (Array[RecordSnapshot], untyped, RecordSnapshot?) -> Array[RecordSnapshot]
|
|
490
|
-
def replace_existing_target(records, version, replacement)
|
|
491
|
-
index = records.index do |record|
|
|
492
|
-
record.id.to_s == version.item_id.to_s &&
|
|
493
|
-
record.type.to_s == replacement_type(version, replacement)
|
|
494
|
-
end
|
|
495
|
-
return records unless index
|
|
496
|
-
|
|
497
|
-
updated = records.dup
|
|
498
|
-
replacement ? updated[index] = replacement : updated.delete_at(index)
|
|
499
|
-
updated.freeze
|
|
500
|
-
end
|
|
98
|
+
private :belongs_to_applier, :collection_applier, :collection_updater, :configure_appliers
|
|
501
99
|
|
|
502
100
|
#: (untyped) -> untyped
|
|
503
101
|
def record_after(version)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
changed_record = changed_record_after(version)
|
|
507
|
-
return changed_record if changed_record
|
|
508
|
-
|
|
509
|
-
successor = version.next
|
|
510
|
-
record = successor&.reify(dup: true)
|
|
511
|
-
return record if record
|
|
512
|
-
|
|
513
|
-
model_class = Endpoint.model_class(version)
|
|
514
|
-
criteria = { model_class.primary_key => version.item_id } #: Hash[untyped, untyped]
|
|
515
|
-
model_class.unscoped.find_by(criteria)
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
# PaperTrail create/update versions contain deserialized change pairs, so
|
|
519
|
-
# their post-event records need no successor query.
|
|
520
|
-
#: (untyped) -> untyped
|
|
521
|
-
def changed_record_after(version)
|
|
522
|
-
handler = RECORD_AFTER_HANDLERS[version.event.to_s]
|
|
523
|
-
send(handler, version) if handler
|
|
524
|
-
end
|
|
525
|
-
|
|
526
|
-
#: (untyped) -> untyped
|
|
527
|
-
def created_record_after(version)
|
|
528
|
-
model_class = Endpoint.model_class(version)
|
|
529
|
-
changes = deserialized_changeset(version, model_class)
|
|
530
|
-
return unless changes.respond_to?(:each) && !changes.empty?
|
|
531
|
-
|
|
532
|
-
attributes = after_attributes(changes, model_class)
|
|
533
|
-
model_class.new(attributes)
|
|
534
|
-
end
|
|
535
|
-
|
|
536
|
-
#: (untyped) -> untyped
|
|
537
|
-
def updated_record_after(version)
|
|
538
|
-
record = version.reify(dup: true)
|
|
539
|
-
changes = deserialized_changeset(version, record&.class)
|
|
540
|
-
apply_changes(record, changes)
|
|
541
|
-
end
|
|
542
|
-
|
|
543
|
-
#: (untyped, untyped) -> Hash[untyped, untyped]
|
|
544
|
-
def after_attributes(changes, model_class)
|
|
545
|
-
names = model_class.attribute_names
|
|
546
|
-
attributes = {} #: Hash[untyped, untyped]
|
|
547
|
-
changes.each do |name, values|
|
|
548
|
-
next unless names.include?(name.to_s)
|
|
549
|
-
|
|
550
|
-
attributes[name] = values.last
|
|
551
|
-
end
|
|
552
|
-
attributes
|
|
102
|
+
@record_resolver.record_after(version)
|
|
553
103
|
end
|
|
554
104
|
|
|
555
|
-
#: (untyped, untyped) -> untyped
|
|
556
|
-
def apply_changes(record, changes)
|
|
557
|
-
return unless record && changes.respond_to?(:each) && !changes.empty?
|
|
558
|
-
|
|
559
|
-
changes.each do |name, values|
|
|
560
|
-
next unless record.has_attribute?(name) && values.respond_to?(:last)
|
|
561
|
-
|
|
562
|
-
record[name] = values.last
|
|
563
|
-
end
|
|
564
|
-
record
|
|
565
|
-
end
|
|
566
|
-
|
|
567
|
-
# Mirrors PaperTrail's changeset deserialization without resolving
|
|
568
|
-
# +version.item+, which would issue one live-record query per event.
|
|
569
105
|
#: (untyped, untyped) -> untyped
|
|
570
106
|
def deserialized_changeset(version, model_class)
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
paper_trail = Object.const_get(:PaperTrail) #: untyped
|
|
574
|
-
adapter = paper_trail.config.object_changes_adapter
|
|
575
|
-
return adapter.load_changeset(version) if adapter.respond_to?(:load_changeset)
|
|
576
|
-
|
|
577
|
-
standard_changeset(paper_trail, version, model_class)
|
|
578
|
-
end
|
|
579
|
-
|
|
580
|
-
#: (untyped, untyped, untyped) -> untyped
|
|
581
|
-
def standard_changeset(paper_trail, version, model_class)
|
|
582
|
-
raw_changes = version.send(:object_changes_deserialized)
|
|
583
|
-
active_support = Object.const_get(:ActiveSupport) #: untyped
|
|
584
|
-
changes = active_support.const_get(:HashWithIndifferentAccess).new(raw_changes)
|
|
585
|
-
serializers = paper_trail.const_get(:AttributeSerializers)
|
|
586
|
-
serializers.const_get(:ObjectChangesAttribute).new(model_class).deserialize(changes)
|
|
587
|
-
changes
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
#: (untyped) -> ActivitySnapshotDelta?
|
|
591
|
-
def activity_snapshot_delta(version)
|
|
592
|
-
return unless version.event.to_s == 'update' && version.object
|
|
593
|
-
|
|
594
|
-
transition = @record_transition&.call(version)
|
|
595
|
-
return unless transition
|
|
596
|
-
|
|
597
|
-
ActivitySnapshotDelta.new(
|
|
598
|
-
before_attributes: transition.fetch(0),
|
|
599
|
-
after_attributes: transition.fetch(1)
|
|
600
|
-
)
|
|
601
|
-
end
|
|
602
|
-
|
|
603
|
-
#: (untyped, untyped, RecordSnapshot, ?state: Symbol) -> bool
|
|
604
|
-
def member_of_owner?(record, reflection, owner, state: :after) # rubocop:disable Metrics/AbcSize
|
|
605
|
-
foreign_keys = Array(reflection.foreign_key)
|
|
606
|
-
actual_ids = foreign_keys.map { |key| event_attribute(record, key, state: state) }
|
|
607
|
-
expected_ids = Array(owner.id)
|
|
608
|
-
# @type var expected_ids: Array[untyped]
|
|
609
|
-
# Explicit blocks keep the inline RBS checker from inferring an unusable Proc type.
|
|
610
|
-
actual = actual_ids.map { |id| id.to_s } # rubocop:disable Style/SymbolProc
|
|
611
|
-
expected = expected_ids.map { |id| id.to_s } # rubocop:disable Style/SymbolProc
|
|
612
|
-
return false unless actual == expected
|
|
613
|
-
return true unless reflection.options[:as]
|
|
614
|
-
|
|
615
|
-
type = event_attribute(record, reflection.type, state: state).to_s
|
|
616
|
-
owner_types = [
|
|
617
|
-
owner.type.to_s,
|
|
618
|
-
reflection.active_record.name.to_s,
|
|
619
|
-
reflection.active_record.base_class.name.to_s
|
|
620
|
-
]
|
|
621
|
-
owner_types.include?(type)
|
|
622
|
-
end
|
|
623
|
-
|
|
624
|
-
#: (untyped, untyped, state: Symbol) -> untyped
|
|
625
|
-
def event_attribute(record, name, state:)
|
|
626
|
-
if record.is_a?(ActivitySnapshotDelta)
|
|
627
|
-
return state == :before ? record.before_value(name) : record.after_value(name)
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
record.public_send(name)
|
|
631
|
-
end
|
|
632
|
-
|
|
633
|
-
#: (untyped, untyped, state: Symbol) -> untyped
|
|
634
|
-
def relationship_owner_id(record, reflection, state:)
|
|
635
|
-
owner_ids = Array(reflection.foreign_key).map do |foreign_key|
|
|
636
|
-
event_attribute(record, foreign_key, state: state)
|
|
637
|
-
end
|
|
638
|
-
owner_ids.one? ? owner_ids.first : owner_ids
|
|
639
|
-
end
|
|
640
|
-
|
|
641
|
-
#: (untyped, untyped, AssociationTree, String, SnapshotNormalizer, untyped, untyped) -> RecordSnapshot?
|
|
642
|
-
def normalize_event_record( # rubocop:disable Metrics/ParameterLists
|
|
643
|
-
record,
|
|
644
|
-
reflection,
|
|
645
|
-
subtree,
|
|
646
|
-
path,
|
|
647
|
-
normalizer,
|
|
648
|
-
root_endpoint,
|
|
649
|
-
context_endpoint
|
|
650
|
-
)
|
|
651
|
-
habtm_boundary = Endpoint.version?(root_endpoint) ? root_endpoint : context_endpoint
|
|
652
|
-
reifier = @association_reader.call(
|
|
653
|
-
context_endpoint,
|
|
654
|
-
habtm_version: habtm_boundary
|
|
655
|
-
)
|
|
656
|
-
normalizer.call_child(
|
|
657
|
-
record,
|
|
658
|
-
tree: subtree,
|
|
659
|
-
path: path,
|
|
660
|
-
incoming: reflection,
|
|
661
|
-
reifier: reifier
|
|
662
|
-
)
|
|
663
|
-
end
|
|
664
|
-
|
|
665
|
-
#: (untyped, habtm_version: untyped) -> HistoricalAssociationReifier
|
|
666
|
-
def historical_reader(context_endpoint, habtm_version:)
|
|
667
|
-
HistoricalAssociationReifier.new(context_endpoint, habtm_version: habtm_version)
|
|
668
|
-
end
|
|
669
|
-
|
|
670
|
-
#: (AssociationSnapshot, untyped, RecordSnapshot?) -> [Array[RecordSnapshot], RecordSnapshot?, RecordSnapshot?, bool]
|
|
671
|
-
def replace_collection_record(association, version, replacement)
|
|
672
|
-
records = association.records
|
|
673
|
-
position = association.position(
|
|
674
|
-
replacement_type(version, replacement),
|
|
675
|
-
version.item_id
|
|
676
|
-
)
|
|
677
|
-
return [records, nil, nil, true] unless replacement || position
|
|
678
|
-
|
|
679
|
-
before = records.fetch(position) if position
|
|
680
|
-
updated = replacement_collection_records(records, position, replacement)
|
|
681
|
-
[updated, before, replacement, !before.nil? && !replacement.nil?]
|
|
682
|
-
end
|
|
683
|
-
|
|
684
|
-
#: (Array[RecordSnapshot], Integer?, RecordSnapshot?) -> Array[RecordSnapshot]
|
|
685
|
-
def replacement_collection_records(records, position, replacement)
|
|
686
|
-
updated = records.dup
|
|
687
|
-
if replacement
|
|
688
|
-
position ? updated[position] = replacement : updated << replacement
|
|
689
|
-
else
|
|
690
|
-
position ||= raise(ConfigurationError, 'missing collection event identity')
|
|
691
|
-
updated.delete_at(position)
|
|
692
|
-
end
|
|
693
|
-
updated.freeze
|
|
694
|
-
end
|
|
695
|
-
|
|
696
|
-
#: (untyped, RecordSnapshot?) -> String
|
|
697
|
-
def replacement_type(version, replacement)
|
|
698
|
-
return replacement.type.to_s if replacement
|
|
699
|
-
|
|
700
|
-
Endpoint.model_class(version).name.to_s
|
|
107
|
+
@record_resolver.deserialized_changeset(version, model_class)
|
|
701
108
|
end
|
|
702
109
|
|
|
703
110
|
#: (untyped) -> untyped
|
|
@@ -705,5 +112,4 @@ module PaperTrailDiff
|
|
|
705
112
|
Endpoint.version?(endpoint) ? Endpoint.model_class(endpoint) : endpoint.class
|
|
706
113
|
end
|
|
707
114
|
end
|
|
708
|
-
# rubocop:enable Metrics/ClassLength
|
|
709
115
|
end
|