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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module PaperTrailDiff
|
|
5
|
+
# Reconstructs an event's post-change record or immutable scalar delta.
|
|
6
|
+
class ActivityEventRecordResolver
|
|
7
|
+
RECORD_AFTER_HANDLERS = {
|
|
8
|
+
'create' => :created_record_after,
|
|
9
|
+
'update' => :updated_record_after
|
|
10
|
+
}.freeze
|
|
11
|
+
private_constant :RECORD_AFTER_HANDLERS
|
|
12
|
+
|
|
13
|
+
#: (?record_transition: untyped) -> void
|
|
14
|
+
def initialize(record_transition: nil)
|
|
15
|
+
@record_transition = record_transition
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
#: (untyped) -> untyped
|
|
19
|
+
def record_after(version)
|
|
20
|
+
return if version.event.to_s == 'destroy'
|
|
21
|
+
|
|
22
|
+
changed_record = changed_record_after(version)
|
|
23
|
+
return changed_record if changed_record
|
|
24
|
+
|
|
25
|
+
successor = version.next
|
|
26
|
+
record = successor&.reify(dup: true)
|
|
27
|
+
return record if record
|
|
28
|
+
|
|
29
|
+
model_class = Endpoint.model_class(version)
|
|
30
|
+
criteria = { model_class.primary_key => version.item_id } #: Hash[untyped, untyped]
|
|
31
|
+
model_class.unscoped.find_by(criteria)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#: (untyped) -> ActivitySnapshotDelta?
|
|
35
|
+
def snapshot_delta(version)
|
|
36
|
+
return unless version.event.to_s == 'update' && version.object
|
|
37
|
+
|
|
38
|
+
transition = @record_transition&.call(version)
|
|
39
|
+
return unless transition
|
|
40
|
+
|
|
41
|
+
ActivitySnapshotDelta.new(
|
|
42
|
+
before_attributes: transition.fetch(0),
|
|
43
|
+
after_attributes: transition.fetch(1)
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#: (untyped) -> untyped
|
|
48
|
+
def changed_record_after(version)
|
|
49
|
+
handler = RECORD_AFTER_HANDLERS[version.event.to_s]
|
|
50
|
+
send(handler, version) if handler
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#: (untyped) -> untyped
|
|
54
|
+
def created_record_after(version)
|
|
55
|
+
model_class = Endpoint.model_class(version)
|
|
56
|
+
changes = deserialized_changeset(version, model_class)
|
|
57
|
+
return unless changes.respond_to?(:each) && !changes.empty?
|
|
58
|
+
|
|
59
|
+
model_class.new(after_attributes(changes, model_class))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
#: (untyped) -> untyped
|
|
63
|
+
def updated_record_after(version)
|
|
64
|
+
record = version.reify(dup: true)
|
|
65
|
+
changes = deserialized_changeset(version, record&.class)
|
|
66
|
+
apply_changes(record, changes)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Mirrors PaperTrail's changeset deserialization without resolving
|
|
70
|
+
# +version.item+, which would issue one live-record query per event.
|
|
71
|
+
#: (untyped, untyped) -> untyped
|
|
72
|
+
def deserialized_changeset(version, model_class)
|
|
73
|
+
return unless model_class && version.class.column_names.include?('object_changes')
|
|
74
|
+
|
|
75
|
+
paper_trail = Object.const_get(:PaperTrail) #: untyped
|
|
76
|
+
adapter = paper_trail.config.object_changes_adapter
|
|
77
|
+
return adapter.load_changeset(version) if adapter.respond_to?(:load_changeset)
|
|
78
|
+
|
|
79
|
+
standard_changeset(paper_trail, version, model_class)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# @rbs @record_transition: untyped
|
|
85
|
+
|
|
86
|
+
#: (untyped, untyped) -> Hash[untyped, untyped]
|
|
87
|
+
def after_attributes(changes, model_class)
|
|
88
|
+
names = model_class.attribute_names
|
|
89
|
+
attributes = {} #: Hash[untyped, untyped]
|
|
90
|
+
changes.each do |name, values|
|
|
91
|
+
next unless names.include?(name.to_s)
|
|
92
|
+
|
|
93
|
+
attributes[name] = values.last
|
|
94
|
+
end
|
|
95
|
+
attributes
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#: (untyped, untyped) -> untyped
|
|
99
|
+
def apply_changes(record, changes)
|
|
100
|
+
return unless record && changes.respond_to?(:each) && !changes.empty?
|
|
101
|
+
|
|
102
|
+
changes.each do |name, values|
|
|
103
|
+
next unless record.has_attribute?(name) && values.respond_to?(:last)
|
|
104
|
+
|
|
105
|
+
record[name] = values.last
|
|
106
|
+
end
|
|
107
|
+
record
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
#: (untyped, untyped, untyped) -> untyped
|
|
111
|
+
def standard_changeset(paper_trail, version, model_class)
|
|
112
|
+
raw_changes = version.send(:object_changes_deserialized)
|
|
113
|
+
active_support = Object.const_get(:ActiveSupport) #: untyped
|
|
114
|
+
changes = active_support.const_get(:HashWithIndifferentAccess).new(raw_changes)
|
|
115
|
+
serializers = paper_trail.const_get(:AttributeSerializers)
|
|
116
|
+
serializers.const_get(:ObjectChangesAttribute).new(model_class).deserialize(changes)
|
|
117
|
+
changes
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module PaperTrailDiff
|
|
5
|
+
# Finds explicitly selected collection and belongs-to routes for one event type.
|
|
6
|
+
class ActivityEventRouteFinder
|
|
7
|
+
#: (AssociationTraversal) -> void
|
|
8
|
+
def initialize(traversal)
|
|
9
|
+
@traversal = traversal
|
|
10
|
+
@collection_model = nil
|
|
11
|
+
@collection_tree = nil #: AssociationTree?
|
|
12
|
+
@collection_type = nil #: String?
|
|
13
|
+
@collection_path = nil #: String?
|
|
14
|
+
@collection_routes = nil #: Array[Array[untyped]]?
|
|
15
|
+
@belongs_to_model = nil
|
|
16
|
+
@belongs_to_tree = nil #: AssociationTree?
|
|
17
|
+
@belongs_to_type = nil #: String?
|
|
18
|
+
@belongs_to_path = nil #: String?
|
|
19
|
+
@belongs_to_routes = nil #: Array[Array[untyped]]?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
#: (untyped, AssociationTree, String, ?path: String) -> Array[Array[untyped]]
|
|
23
|
+
def collection_routes(model_class, tree, target_type, path: '')
|
|
24
|
+
cached = @collection_routes
|
|
25
|
+
if cached && @collection_model.equal?(model_class) && @collection_tree.equal?(tree) &&
|
|
26
|
+
@collection_type == target_type && @collection_path == path
|
|
27
|
+
return cached
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
computed = freeze_routes(routes(model_class, tree, target_type, :has_many, path: path))
|
|
31
|
+
@collection_model = model_class
|
|
32
|
+
@collection_tree = tree
|
|
33
|
+
@collection_type = Support.immutable_copy(target_type)
|
|
34
|
+
@collection_path = Support.immutable_copy(path)
|
|
35
|
+
@collection_routes = computed
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
#: (untyped, AssociationTree, String, ?path: String) -> Array[Array[untyped]]
|
|
39
|
+
def belongs_to_routes(model_class, tree, target_type, path: '')
|
|
40
|
+
cached = @belongs_to_routes
|
|
41
|
+
if cached && @belongs_to_model.equal?(model_class) && @belongs_to_tree.equal?(tree) &&
|
|
42
|
+
@belongs_to_type == target_type && @belongs_to_path == path
|
|
43
|
+
return cached
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
computed = freeze_routes(routes(model_class, tree, target_type, :belongs_to, path: path))
|
|
47
|
+
@belongs_to_model = model_class
|
|
48
|
+
@belongs_to_tree = tree
|
|
49
|
+
@belongs_to_type = Support.immutable_copy(target_type)
|
|
50
|
+
@belongs_to_path = Support.immutable_copy(path)
|
|
51
|
+
@belongs_to_routes = computed
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# @rbs @traversal: AssociationTraversal
|
|
57
|
+
# @rbs @collection_model: untyped
|
|
58
|
+
# @rbs @collection_tree: AssociationTree?
|
|
59
|
+
# @rbs @collection_type: String?
|
|
60
|
+
# @rbs @collection_path: String?
|
|
61
|
+
# @rbs @collection_routes: Array[Array[untyped]]?
|
|
62
|
+
# @rbs @belongs_to_model: untyped
|
|
63
|
+
# @rbs @belongs_to_tree: AssociationTree?
|
|
64
|
+
# @rbs @belongs_to_type: String?
|
|
65
|
+
# @rbs @belongs_to_path: String?
|
|
66
|
+
# @rbs @belongs_to_routes: Array[Array[untyped]]?
|
|
67
|
+
|
|
68
|
+
#: (Array[Array[untyped]]) -> Array[Array[untyped]]
|
|
69
|
+
def freeze_routes(routes)
|
|
70
|
+
routes.each do |route|
|
|
71
|
+
route.each(&:freeze)
|
|
72
|
+
route.freeze
|
|
73
|
+
end.freeze
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
#: (untyped, AssociationTree, String, Symbol, path: String) -> Array[Array[untyped]]
|
|
77
|
+
def routes(model_class, tree, target_type, macro, path: '')
|
|
78
|
+
@traversal.reflections_for(model_class, tree, path: path).flat_map do |reflection|
|
|
79
|
+
routes_for_reflection(reflection, tree, target_type, macro, path)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
#: (untyped, AssociationTree, String, Symbol, String) -> Array[Array[untyped]]
|
|
84
|
+
def routes_for_reflection(reflection, tree, target_type, macro, path)
|
|
85
|
+
name = reflection.name.to_s
|
|
86
|
+
subtree = tree.child(name)
|
|
87
|
+
return [] unless subtree
|
|
88
|
+
|
|
89
|
+
child_path = Support.association_path(path, name)
|
|
90
|
+
entry = [name, reflection, subtree, child_path]
|
|
91
|
+
direct = [] #: Array[Array[untyped]]
|
|
92
|
+
direct << [entry] if direct_route?(reflection, target_type, macro)
|
|
93
|
+
return direct if subtree.empty? || reflection.polymorphic?
|
|
94
|
+
|
|
95
|
+
nested = routes(reflection.klass, subtree, target_type, macro, path: child_path)
|
|
96
|
+
direct.concat(nested.map { |route| [entry, *route] })
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
#: (untyped, String, Symbol) -> bool
|
|
100
|
+
def direct_route?(reflection, target_type, macro)
|
|
101
|
+
reflection.macro == macro && supported_direct_reflection?(reflection, macro) &&
|
|
102
|
+
reflection.klass.base_class.name.to_s == target_type
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
#: (untyped, Symbol) -> bool
|
|
106
|
+
def supported_direct_reflection?(reflection, macro)
|
|
107
|
+
return !reflection.options[:through] if macro == :has_many
|
|
108
|
+
|
|
109
|
+
!reflection.polymorphic?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|