paper_trail_diff 0.12.0 → 0.13.0
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 +127 -0
- data/QUICKSTART.md +149 -79
- data/README.md +179 -1388
- data/docs/architecture.md +88 -0
- data/docs/performance.md +309 -0
- data/docs/reference.md +977 -0
- data/docs/testing.md +146 -0
- data/lib/paper_trail_diff/activity_belongs_to_event_applier.rb +2 -12
- data/lib/paper_trail_diff/activity_boundary.rb +20 -8
- data/lib/paper_trail_diff/activity_child_candidate_loader.rb +29 -0
- data/lib/paper_trail_diff/activity_collection_event_applier.rb +10 -7
- data/lib/paper_trail_diff/activity_collection_route_updater.rb +1 -11
- data/lib/paper_trail_diff/activity_event_record_normalizer.rb +4 -0
- data/lib/paper_trail_diff/activity_event_record_resolver.rb +66 -6
- data/lib/paper_trail_diff/activity_event_snapshot_refresher.rb +6 -4
- data/lib/paper_trail_diff/activity_grouping.rb +12 -0
- data/lib/paper_trail_diff/activity_history.rb +9 -3
- data/lib/paper_trail_diff/activity_relationship.rb +2 -0
- data/lib/paper_trail_diff/activity_root_snapshot_refresher.rb +1 -0
- data/lib/paper_trail_diff/activity_snapshot_sequence.rb +9 -2
- data/lib/paper_trail_diff/activity_timeline_builder.rb +39 -35
- data/lib/paper_trail_diff/activity_version_collector.rb +27 -18
- data/lib/paper_trail_diff/analysis.rb +9 -4
- data/lib/paper_trail_diff/analysis_batch.rb +28 -7
- data/lib/paper_trail_diff/association_traversal.rb +23 -0
- data/lib/paper_trail_diff/batch_boundary_resolver.rb +18 -26
- data/lib/paper_trail_diff/batch_safety.rb +55 -0
- data/lib/paper_trail_diff/batched_root_analyzer.rb +14 -5
- data/lib/paper_trail_diff/batched_root_versions.rb +9 -37
- data/lib/paper_trail_diff/branch_snapshot_refresher.rb +7 -8
- data/lib/paper_trail_diff/deleted_analysis_batch.rb +93 -0
- data/lib/paper_trail_diff/direct_record_state_loader.rb +81 -0
- data/lib/paper_trail_diff/endpoint.rb +10 -0
- data/lib/paper_trail_diff/engine.rb +4 -2
- data/lib/paper_trail_diff/errors.rb +9 -0
- data/lib/paper_trail_diff/historical_association_reifier.rb +26 -1
- data/lib/paper_trail_diff/historical_filter.rb +181 -0
- data/lib/paper_trail_diff/historical_snapshot_store.rb +51 -35
- data/lib/paper_trail_diff/instrumentation.rb +2 -2
- data/lib/paper_trail_diff/nested_comparator.rb +39 -15
- data/lib/paper_trail_diff/paper_trail_adapter.rb +109 -55
- data/lib/paper_trail_diff/prepared_activity_index.rb +121 -0
- data/lib/paper_trail_diff/prepared_edge_loader.rb +11 -11
- data/lib/paper_trail_diff/prepared_history.rb +21 -30
- data/lib/paper_trail_diff/prepared_history_loader.rb +14 -2
- data/lib/paper_trail_diff/prepared_record_index.rb +102 -29
- data/lib/paper_trail_diff/prepared_snapshot_builder.rb +64 -0
- data/lib/paper_trail_diff/prepared_state_association_reader.rb +77 -0
- data/lib/paper_trail_diff/root_version_plan.rb +25 -2
- data/lib/paper_trail_diff/root_version_selection.rb +16 -5
- data/lib/paper_trail_diff/scoped_analysis.rb +17 -2
- data/lib/paper_trail_diff/scoped_analysis_builder.rb +47 -0
- data/lib/paper_trail_diff/scoped_root_selection.rb +106 -13
- data/lib/paper_trail_diff/snapshot.rb +9 -0
- data/lib/paper_trail_diff/snapshot_normalizer.rb +41 -2
- data/lib/paper_trail_diff/step.rb +10 -5
- data/lib/paper_trail_diff/stored_time_precision.rb +68 -0
- data/lib/paper_trail_diff/support.rb +13 -3
- data/lib/paper_trail_diff/time_activity_timeline_builder.rb +19 -7
- data/lib/paper_trail_diff/time_range.rb +22 -2
- data/lib/paper_trail_diff/time_version_range.rb +1 -15
- data/lib/paper_trail_diff/timeline_builder.rb +5 -4
- data/lib/paper_trail_diff/timeline_range.rb +25 -43
- data/lib/paper_trail_diff/timeline_snapshot_provider.rb +13 -2
- data/lib/paper_trail_diff/traversal.rb +1 -1
- data/lib/paper_trail_diff/version.rb +1 -1
- data/lib/paper_trail_diff/version_association_candidate_scope.rb +1 -1
- data/lib/paper_trail_diff/version_boundary.rb +48 -0
- data/lib/paper_trail_diff/version_query.rb +33 -0
- data/lib/paper_trail_diff/version_range.rb +10 -38
- data/lib/paper_trail_diff.rb +78 -42
- data/sig/generated/paper_trail_diff/activity_belongs_to_event_applier.rbs +0 -3
- data/sig/generated/paper_trail_diff/activity_boundary.rbs +9 -4
- data/sig/generated/paper_trail_diff/activity_child_candidate_loader.rbs +11 -0
- data/sig/generated/paper_trail_diff/activity_collection_event_applier.rbs +4 -4
- data/sig/generated/paper_trail_diff/activity_collection_route_updater.rbs +0 -3
- data/sig/generated/paper_trail_diff/activity_event_record_resolver.rbs +19 -3
- data/sig/generated/paper_trail_diff/activity_event_snapshot_refresher.rbs +2 -2
- data/sig/generated/paper_trail_diff/activity_grouping.rbs +3 -0
- data/sig/generated/paper_trail_diff/activity_history.rbs +6 -2
- data/sig/generated/paper_trail_diff/activity_snapshot_sequence.rbs +7 -2
- data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +8 -3
- data/sig/generated/paper_trail_diff/activity_version_collector.rbs +4 -8
- data/sig/generated/paper_trail_diff/analysis.rbs +2 -2
- data/sig/generated/paper_trail_diff/analysis_batch.rbs +14 -3
- data/sig/generated/paper_trail_diff/association_traversal.rbs +11 -0
- data/sig/generated/paper_trail_diff/batch_boundary_resolver.rbs +7 -10
- data/sig/generated/paper_trail_diff/batch_safety.rbs +24 -0
- data/sig/generated/paper_trail_diff/batched_root_analyzer.rbs +8 -2
- data/sig/generated/paper_trail_diff/batched_root_versions.rbs +4 -16
- data/sig/generated/paper_trail_diff/branch_snapshot_refresher.rbs +2 -2
- data/sig/generated/paper_trail_diff/deleted_analysis_batch.rbs +44 -0
- data/sig/generated/paper_trail_diff/direct_record_state_loader.rbs +42 -0
- data/sig/generated/paper_trail_diff/endpoint.rbs +5 -0
- data/sig/generated/paper_trail_diff/engine.rbs +4 -2
- data/sig/generated/paper_trail_diff/errors.rbs +12 -0
- data/sig/generated/paper_trail_diff/historical_association_reifier.rbs +8 -0
- data/sig/generated/paper_trail_diff/historical_filter.rbs +75 -0
- data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +17 -4
- data/sig/generated/paper_trail_diff/nested_comparator.rbs +15 -7
- data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +36 -15
- data/sig/generated/paper_trail_diff/prepared_activity_index.rbs +56 -0
- data/sig/generated/paper_trail_diff/prepared_edge_loader.rbs +3 -3
- data/sig/generated/paper_trail_diff/prepared_history.rbs +12 -14
- data/sig/generated/paper_trail_diff/prepared_history_loader.rbs +2 -0
- data/sig/generated/paper_trail_diff/prepared_record_index.rbs +43 -6
- data/sig/generated/paper_trail_diff/prepared_snapshot_builder.rbs +24 -0
- data/sig/generated/paper_trail_diff/prepared_state_association_reader.rbs +33 -0
- data/sig/generated/paper_trail_diff/root_version_plan.rbs +15 -2
- data/sig/generated/paper_trail_diff/root_version_selection.rbs +8 -1
- data/sig/generated/paper_trail_diff/scoped_analysis.rbs +14 -2
- data/sig/generated/paper_trail_diff/scoped_analysis_builder.rbs +22 -0
- data/sig/generated/paper_trail_diff/scoped_root_selection.rbs +46 -6
- data/sig/generated/paper_trail_diff/snapshot.rbs +4 -0
- data/sig/generated/paper_trail_diff/snapshot_normalizer.rbs +13 -0
- data/sig/generated/paper_trail_diff/step.rbs +8 -2
- data/sig/generated/paper_trail_diff/stored_time_precision.rbs +36 -0
- data/sig/generated/paper_trail_diff/support.rbs +7 -0
- data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +7 -2
- data/sig/generated/paper_trail_diff/time_range.rbs +9 -0
- data/sig/generated/paper_trail_diff/time_version_range.rbs +0 -6
- data/sig/generated/paper_trail_diff/timeline_builder.rbs +2 -2
- data/sig/generated/paper_trail_diff/timeline_range.rbs +10 -14
- data/sig/generated/paper_trail_diff/timeline_snapshot_provider.rbs +2 -0
- data/sig/generated/paper_trail_diff/version_boundary.rbs +23 -0
- data/sig/generated/paper_trail_diff/version_query.rbs +15 -0
- data/sig/generated/paper_trail_diff/version_range.rbs +5 -14
- data/sig/generated/paper_trail_diff.rbs +31 -25
- metadata +31 -5
|
@@ -27,10 +27,16 @@ module PaperTrailDiff
|
|
|
27
27
|
# one of them, so absence is its own value rather than nil.
|
|
28
28
|
class NestedComparator
|
|
29
29
|
# Stands in for a key that was not there at all.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
class Absent
|
|
31
|
+
#: () -> String
|
|
32
|
+
def inspect = '#<PaperTrailDiff absent>'
|
|
33
|
+
|
|
34
|
+
#: () -> String
|
|
35
|
+
def to_s = 'absent'
|
|
36
|
+
end
|
|
37
|
+
private_constant :Absent
|
|
38
|
+
|
|
39
|
+
ABSENT = Absent.new.freeze
|
|
34
40
|
|
|
35
41
|
#: (untyped, untyped) -> nested_changes
|
|
36
42
|
def self.call(from_value, to_value)
|
|
@@ -49,9 +55,9 @@ module PaperTrailDiff
|
|
|
49
55
|
#: () -> nested_changes
|
|
50
56
|
def call
|
|
51
57
|
from_structure, to_structure = structures
|
|
52
|
-
return {} unless from_structure && to_structure
|
|
53
|
-
|
|
54
58
|
changes = {} #: nested_changes
|
|
59
|
+
return changes.freeze unless from_structure && to_structure
|
|
60
|
+
|
|
55
61
|
walk(from_structure, to_structure, [], changes)
|
|
56
62
|
changes.freeze
|
|
57
63
|
end
|
|
@@ -76,20 +82,23 @@ module PaperTrailDiff
|
|
|
76
82
|
|
|
77
83
|
parsed = begin
|
|
78
84
|
JSON.parse(value)
|
|
79
|
-
rescue JSON::ParserError
|
|
85
|
+
rescue JSON::ParserError
|
|
80
86
|
nil
|
|
81
87
|
end
|
|
82
|
-
|
|
88
|
+
# JSON parses objects into plain Hashes, never a subclass.
|
|
89
|
+
parsed if parsed.instance_of?(Hash)
|
|
83
90
|
end
|
|
84
91
|
|
|
85
92
|
#: (Hash[untyped, untyped], Hash[untyped, untyped], Array[String], nested_changes) -> void
|
|
86
93
|
def walk(from_hash, to_hash, path, changes)
|
|
94
|
+
from_hash = normalize_keys(from_hash)
|
|
95
|
+
to_hash = normalize_keys(to_hash)
|
|
87
96
|
keys(from_hash, to_hash).each do |key|
|
|
88
|
-
from_item = from_hash.
|
|
89
|
-
to_item = to_hash.
|
|
97
|
+
from_item = from_hash.fetch(key, ABSENT)
|
|
98
|
+
to_item = to_hash.fetch(key, ABSENT)
|
|
90
99
|
next if from_item == to_item
|
|
91
100
|
|
|
92
|
-
here =
|
|
101
|
+
here = (path + [key]).freeze
|
|
93
102
|
if from_item.is_a?(Hash) && to_item.is_a?(Hash)
|
|
94
103
|
walk(from_item, to_item, here, changes)
|
|
95
104
|
else
|
|
@@ -98,6 +107,20 @@ module PaperTrailDiff
|
|
|
98
107
|
end
|
|
99
108
|
end
|
|
100
109
|
|
|
110
|
+
#: (Hash[untyped, untyped]) -> Hash[String, untyped]
|
|
111
|
+
def normalize_keys(hash)
|
|
112
|
+
normalized = {} #: Hash[String, untyped]
|
|
113
|
+
hash.each do |key, value|
|
|
114
|
+
name = key.to_s
|
|
115
|
+
if normalized.key?(name)
|
|
116
|
+
raise AmbiguousNestedKeyError, "duplicate nested key after normalization: #{name.inspect}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
normalized[name] = value
|
|
120
|
+
end
|
|
121
|
+
normalized
|
|
122
|
+
end
|
|
123
|
+
|
|
101
124
|
# Two arrays are a membership change; anything else is a change to the
|
|
102
125
|
# value as a whole, including an array facing something that is not one.
|
|
103
126
|
#: (untyped, untyped) -> nested_change
|
|
@@ -109,11 +132,12 @@ module PaperTrailDiff
|
|
|
109
132
|
end
|
|
110
133
|
end
|
|
111
134
|
|
|
112
|
-
# Sorted so a report reads the same twice
|
|
113
|
-
# loaded from JSON and one built in Ruby can key the
|
|
114
|
-
|
|
135
|
+
# Sorted so a report reads the same twice. Both hashes are already keyed by
|
|
136
|
+
# strings, because a hash loaded from JSON and one built in Ruby can key the
|
|
137
|
+
# same field differently.
|
|
138
|
+
#: (Hash[String, untyped], Hash[String, untyped]) -> Array[String]
|
|
115
139
|
def keys(from_hash, to_hash)
|
|
116
|
-
(from_hash.keys | to_hash.keys).
|
|
140
|
+
(from_hash.keys | to_hash.keys).sort
|
|
117
141
|
end
|
|
118
142
|
end
|
|
119
143
|
end
|
|
@@ -10,11 +10,12 @@ module PaperTrailDiff
|
|
|
10
10
|
# The only thing a wall-clock window can close on besides a later version.
|
|
11
11
|
CLOSE_ON_CURRENT = :current
|
|
12
12
|
|
|
13
|
-
#: (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
|
|
14
|
-
def initialize(associations:, ignore:, reload_live_endpoints: true)
|
|
13
|
+
#: (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool, ?allow_expensive: bool) -> void
|
|
14
|
+
def initialize(associations:, ignore:, reload_live_endpoints: true, allow_expensive: false)
|
|
15
15
|
@association_tree = AssociationTree.build(associations)
|
|
16
16
|
@ignore_policy = IgnorePolicy.build(ignore, association_paths: @association_tree.paths)
|
|
17
17
|
@traversal = AssociationTraversal.new(@association_tree)
|
|
18
|
+
@batch_safety = BatchSafety.new(@traversal, allow_expensive: allow_expensive)
|
|
18
19
|
@traversal_preparer = TraversalPreparer.new(tree: @association_tree, traversal: @traversal)
|
|
19
20
|
@live_endpoints = LiveEndpointProvider.new(
|
|
20
21
|
tree: @association_tree, traversal: @traversal, reload: reload_live_endpoints
|
|
@@ -34,7 +35,14 @@ module PaperTrailDiff
|
|
|
34
35
|
Instrumentation.instrument('compare', payload) do
|
|
35
36
|
Endpoint.validate_pair!(from_endpoint, to_endpoint)
|
|
36
37
|
notify_ambiguous_association_boundary(from_endpoint, to_endpoint)
|
|
37
|
-
|
|
38
|
+
if @association_tree.empty?
|
|
39
|
+
Engine.compare(snapshot_for_endpoint(from_endpoint), snapshot_for_endpoint(to_endpoint))
|
|
40
|
+
else
|
|
41
|
+
# Prepared history orders versions that share a timestamp; reading
|
|
42
|
+
# associations straight from PT-AT would resolve them by time alone.
|
|
43
|
+
comparison_batch([{ from: from_endpoint, to: to_endpoint }],
|
|
44
|
+
preparer: @traversal_preparer.method(:call)).call.values.fetch(0)
|
|
45
|
+
end
|
|
38
46
|
end
|
|
39
47
|
end
|
|
40
48
|
|
|
@@ -44,19 +52,14 @@ module PaperTrailDiff
|
|
|
44
52
|
count = comparisons.is_a?(Array) ? comparisons.length : 0
|
|
45
53
|
payload = @instrumentation_payload.merge(comparison_count: count)
|
|
46
54
|
Instrumentation.instrument('compare_many', payload) do
|
|
47
|
-
|
|
48
|
-
comparisons,
|
|
49
|
-
live_loader: @live_endpoints.method(:call), preparer: @traversal_preparer.method(:call),
|
|
50
|
-
history_preparer: @historical_store.method(:prepare_batch),
|
|
51
|
-
historical_snapshotter: method(:historical_snapshot),
|
|
52
|
-
live_normalizer: method(:normalize_live_snapshot)
|
|
53
|
-
).call
|
|
55
|
+
comparison_batch(comparisons, preparer: method(:prepare_batch_model)).call
|
|
54
56
|
end
|
|
55
57
|
end
|
|
56
58
|
|
|
57
59
|
#: (untyped, from: untyped, to: untyped, within: untyped, ?version_scope: untyped, ?close_on: Symbol?) -> Array[Step]
|
|
58
60
|
def timeline(record, from:, to:, within:, version_scope: nil, close_on: nil) # rubocop:disable Metrics/ParameterLists
|
|
59
61
|
@traversal_preparer.call(record.class, historical: true)
|
|
62
|
+
from, to = default_range(record, from, to, within)
|
|
60
63
|
TimelineBuilder.new(
|
|
61
64
|
record,
|
|
62
65
|
from: from,
|
|
@@ -73,8 +76,8 @@ module PaperTrailDiff
|
|
|
73
76
|
payload = @instrumentation_payload.merge(model_type: record.class.base_class.name.to_s)
|
|
74
77
|
Instrumentation.instrument('activity_timeline', payload) do
|
|
75
78
|
@traversal_preparer.call(record.class, historical: true)
|
|
79
|
+
from, to = default_range(record, from, to, within)
|
|
76
80
|
live = live_endpoint_for(record, close_on, within)
|
|
77
|
-
reject_live_habtm_activity!(record.class) if Endpoint.record?(to) || live
|
|
78
81
|
steps = activity_builder(
|
|
79
82
|
record, from: from, to: to, within: within, version_scope: version_scope,
|
|
80
83
|
live_endpoint: live, snapshots: snapshots, group: group
|
|
@@ -86,7 +89,9 @@ module PaperTrailDiff
|
|
|
86
89
|
|
|
87
90
|
#: (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?) -> Analysis
|
|
88
91
|
def analyze(record, from:, to:, within:, activity: false, version_scope: nil, close_on: nil, snapshots: false, group: nil) # rubocop:disable Metrics/ParameterLists, Layout/LineLength
|
|
92
|
+
ActivityGrouping.validate!(group, activity: activity, snapshots: snapshots)
|
|
89
93
|
@traversal_preparer.call(record.class, historical: true)
|
|
94
|
+
from, to = default_range(record, from, to, within)
|
|
90
95
|
live = live_endpoint_for(record, close_on, within)
|
|
91
96
|
if activity
|
|
92
97
|
return analyze_activity(
|
|
@@ -102,19 +107,21 @@ module PaperTrailDiff
|
|
|
102
107
|
end
|
|
103
108
|
|
|
104
109
|
# Analyzes many roots over one shared range, preparing their history once.
|
|
105
|
-
#: (Array[untyped], within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> Hash[identity, Analysis]
|
|
106
|
-
def analyze_many(records, within:, activity: false, version_scope: nil, close_on: nil)
|
|
110
|
+
#: (Array[untyped], within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?) -> Hash[identity, Analysis]
|
|
111
|
+
def analyze_many(records, within:, activity: false, version_scope: nil, close_on: nil, snapshots: false, group: nil) # rubocop:disable Metrics/ParameterLists, Layout/LineLength
|
|
112
|
+
ActivityGrouping.validate!(group, activity: activity, snapshots: snapshots)
|
|
107
113
|
count = records.is_a?(Array) ? records.length : 0
|
|
108
114
|
payload = @instrumentation_payload.merge(comparison_count: count)
|
|
109
115
|
Instrumentation.instrument('analyze_many', payload) do
|
|
110
116
|
AnalysisBatch.new(
|
|
111
117
|
records,
|
|
112
|
-
time_range: within.nil? ? nil : TimeRange.new(within),
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
live_loader:
|
|
118
|
+
time_range: within.nil? ? nil : TimeRange.new(within), version_scope: version_scope,
|
|
119
|
+
close_on_current: close_on_current?(close_on, within) || within.nil?,
|
|
120
|
+
context_required: activity && !@association_tree.empty?,
|
|
121
|
+
live_loader: method(:load_batch_endpoints),
|
|
116
122
|
history_preparer: @historical_store.method(:prepare_batch),
|
|
117
|
-
analyzer: batched_root_analyzer(activity
|
|
123
|
+
analyzer: batched_root_analyzer(activity, within: within, snapshots: snapshots,
|
|
124
|
+
group: group)
|
|
118
125
|
).call
|
|
119
126
|
end
|
|
120
127
|
end
|
|
@@ -123,22 +130,65 @@ module PaperTrailDiff
|
|
|
123
130
|
# caller reporting on a population does not have to rediscover which of its
|
|
124
131
|
# members changed. The roots the relation could not reach come back named
|
|
125
132
|
# rather than dropped -- see `PaperTrailDiff.analyze_scope`.
|
|
126
|
-
#: (untyped, limit: Integer?, within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?) -> ScopedAnalysis
|
|
127
|
-
def analyze_scope(scope, limit:, within:, activity: false, version_scope: nil, close_on: nil) # rubocop:disable Metrics/ParameterLists
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
#: (untyped, limit: Integer?, within: untyped, ?activity: bool, ?version_scope: untyped, ?close_on: Symbol?, ?snapshots: bool, ?group: Symbol?, ?historical_filter: untyped, ?max_filter_versions: Integer, ?include_deleted: bool) -> ScopedAnalysis
|
|
134
|
+
def analyze_scope(scope, limit:, within:, activity: false, version_scope: nil, close_on: nil, snapshots: false, group: nil, historical_filter: nil, max_filter_versions: HistoricalFilter::DEFAULT_VERSION_LIMIT, include_deleted: false) # rubocop:disable Metrics/ParameterLists, Layout/LineLength
|
|
135
|
+
ActivityGrouping.validate!(group, activity: activity, snapshots: snapshots)
|
|
136
|
+
@batch_safety.call(scope.all.model) if scope.respond_to?(:all) && scope.respond_to?(:where)
|
|
137
|
+
# @type var options: report_options
|
|
138
|
+
options = { within: within, activity: activity, version_scope: version_scope,
|
|
139
|
+
close_on: close_on, snapshots: snapshots, group: group }
|
|
140
|
+
ScopedAnalysisBuilder.new(
|
|
141
|
+
scope, limit: limit, within: within, historical_filter: historical_filter,
|
|
142
|
+
max_filter_versions: max_filter_versions, include_deleted: include_deleted,
|
|
143
|
+
analyzer: ->(records) { analyze_many(records, **options, within: within) },
|
|
144
|
+
deleted_analyzer: lambda { |identities|
|
|
145
|
+
analyze_deleted(scope.all.model, identities, options)
|
|
146
|
+
}
|
|
132
147
|
).call
|
|
133
|
-
ScopedAnalysis.new(
|
|
134
|
-
analyses: analyze_many(selection.records, within: within, activity: activity,
|
|
135
|
-
version_scope: version_scope, close_on: close_on),
|
|
136
|
-
unreachable: selection.unreachable
|
|
137
|
-
)
|
|
138
148
|
end
|
|
139
149
|
|
|
140
150
|
private
|
|
141
151
|
|
|
152
|
+
#: (Array[comparison_input], preparer: untyped) -> ComparisonBatch
|
|
153
|
+
def comparison_batch(comparisons, preparer:)
|
|
154
|
+
ComparisonBatch.new(
|
|
155
|
+
comparisons,
|
|
156
|
+
live_loader: @live_endpoints, preparer: preparer,
|
|
157
|
+
history_preparer: @historical_store.method(:prepare_batch),
|
|
158
|
+
historical_snapshotter: method(:historical_snapshot),
|
|
159
|
+
live_normalizer: method(:normalize_live_snapshot)
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
#: (Array[untyped]) -> Hash[identity, untyped]
|
|
164
|
+
def load_batch_endpoints(records)
|
|
165
|
+
records.map(&:class).uniq.each { |model| @batch_safety.call(model) }
|
|
166
|
+
@live_endpoints.call(records)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
#: (untyped, historical: bool) -> void
|
|
170
|
+
def prepare_batch_model(model, historical:)
|
|
171
|
+
@batch_safety.call(model, historical: historical)
|
|
172
|
+
@traversal_preparer.call(model, historical: historical)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @rbs @batch_safety: BatchSafety
|
|
176
|
+
|
|
177
|
+
#: (untyped, Array[Array[String]], Hash[Symbol, untyped]) -> Hash[Array[String], Analysis?]
|
|
178
|
+
def analyze_deleted(model, identities, options)
|
|
179
|
+
within = options.fetch(:within)
|
|
180
|
+
analyzer = batched_root_analyzer(
|
|
181
|
+
options.fetch(:activity), within: within, snapshots: options.fetch(:snapshots),
|
|
182
|
+
group: options.fetch(:group)
|
|
183
|
+
)
|
|
184
|
+
DeletedAnalysisBatch.new(
|
|
185
|
+
model, identities: identities, time_range: within.nil? ? nil : TimeRange.new(within),
|
|
186
|
+
version_scope: options.fetch(:version_scope),
|
|
187
|
+
history_preparer: @historical_store.method(:prepare_deleted_batch),
|
|
188
|
+
analyzer: analyzer
|
|
189
|
+
).call
|
|
190
|
+
end
|
|
191
|
+
|
|
142
192
|
# @rbs @association_tree: AssociationTree
|
|
143
193
|
# @rbs @live_endpoints: LiveEndpointProvider
|
|
144
194
|
# @rbs @instrumentation_payload: Hash[Symbol, untyped]
|
|
@@ -153,7 +203,6 @@ module PaperTrailDiff
|
|
|
153
203
|
|
|
154
204
|
#: (untyped, from: untyped, to: untyped, within: untyped, version_scope: untyped, live_endpoint: untyped, ?snapshots: bool, ?group: Symbol?) -> Analysis
|
|
155
205
|
def analyze_activity(record, from:, to:, within:, version_scope:, live_endpoint:, snapshots: false, group: nil) # rubocop:disable Metrics/ParameterLists, Layout/LineLength
|
|
156
|
-
reject_live_habtm_activity!(record.class) if live_endpoint
|
|
157
206
|
activity_builder(
|
|
158
207
|
record, from: from, to: to, within: within, version_scope: version_scope,
|
|
159
208
|
live_endpoint: live_endpoint, snapshots: snapshots, group: group
|
|
@@ -181,25 +230,38 @@ module PaperTrailDiff
|
|
|
181
230
|
)
|
|
182
231
|
end
|
|
183
232
|
|
|
184
|
-
#
|
|
185
|
-
#
|
|
233
|
+
# A window with nothing recorded after its last change ends at the live
|
|
234
|
+
# record, which holds the state that change produced -- whether the window
|
|
235
|
+
# ended a moment ago or long before. `close_on: :current` asked for this
|
|
236
|
+
# before it was the default and is still accepted. It names what ends a
|
|
237
|
+
# wall-clock window, so it is meaningless for explicit endpoints.
|
|
186
238
|
#: (Symbol?, untyped) -> bool
|
|
187
239
|
def close_on_current?(close_on, within)
|
|
188
|
-
|
|
189
|
-
unless close_on == CLOSE_ON_CURRENT
|
|
240
|
+
unless close_on.nil? || close_on == CLOSE_ON_CURRENT
|
|
190
241
|
raise ConfigurationError, "close_on: must be #{CLOSE_ON_CURRENT.inspect} or nil"
|
|
191
242
|
end
|
|
192
|
-
raise ConfigurationError, 'close_on: requires `within`' if within.nil?
|
|
243
|
+
raise ConfigurationError, 'close_on: requires `within`' if close_on && within.nil?
|
|
193
244
|
|
|
194
|
-
|
|
245
|
+
!within.nil?
|
|
195
246
|
end
|
|
196
247
|
|
|
197
|
-
#
|
|
198
|
-
#
|
|
248
|
+
# With neither a window nor endpoints, a range is the record's whole
|
|
249
|
+
# history through its current saved state; a missing endpoint takes that
|
|
250
|
+
# side of the same default.
|
|
251
|
+
#: (untyped, untyped, untyped, untyped) -> [untyped, untyped]
|
|
252
|
+
def default_range(record, from, to, within)
|
|
253
|
+
return [from, to] unless within.nil?
|
|
254
|
+
|
|
255
|
+
[from.nil? ? :first : from, to.nil? ? record : to]
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Only a saved row has current state to close on. A destroyed root's own
|
|
259
|
+
# destroy version already terminates its history, and a reified or unsaved
|
|
260
|
+
# instance stands for a state that is not in the table.
|
|
199
261
|
#: (untyped, Symbol?, untyped) -> untyped
|
|
200
262
|
def live_endpoint_for(record, close_on, within)
|
|
201
263
|
return unless close_on_current?(close_on, within)
|
|
202
|
-
return unless Endpoint.record?(record) &&
|
|
264
|
+
return unless Endpoint.record?(record) && record.persisted?
|
|
203
265
|
|
|
204
266
|
record
|
|
205
267
|
end
|
|
@@ -213,14 +275,14 @@ module PaperTrailDiff
|
|
|
213
275
|
@activity_snapshotter = build_activity_snapshotter
|
|
214
276
|
end
|
|
215
277
|
|
|
216
|
-
#: (bool) -> BatchedRootAnalyzer
|
|
217
|
-
def batched_root_analyzer(activity)
|
|
278
|
+
#: (bool, within: untyped, snapshots: bool, group: Symbol?) -> BatchedRootAnalyzer
|
|
279
|
+
def batched_root_analyzer(activity, within:, snapshots:, group:)
|
|
218
280
|
BatchedRootAnalyzer.new(
|
|
219
281
|
tree: @association_tree,
|
|
220
282
|
timeline_snapshotter: @timeline_snapshotter,
|
|
221
283
|
activity_snapshotter: @activity_snapshotter,
|
|
222
284
|
preparer: @traversal_preparer.method(:call),
|
|
223
|
-
activity: activity
|
|
285
|
+
activity: activity, within: within, snapshots: snapshots, group: group
|
|
224
286
|
)
|
|
225
287
|
end
|
|
226
288
|
|
|
@@ -243,12 +305,13 @@ module PaperTrailDiff
|
|
|
243
305
|
full_snapshotter: method(:snapshot_at),
|
|
244
306
|
partial_snapshotter: @historical_store.method(:custom),
|
|
245
307
|
association_reader: @historical_store.method(:association_reader),
|
|
246
|
-
record_transition: @historical_store.method(:record_transition)
|
|
308
|
+
record_transition: @historical_store.method(:record_transition),
|
|
309
|
+
prepared_record_after: @historical_store.method(:record_after)
|
|
247
310
|
)
|
|
248
311
|
ActivitySnapshotProvider.new(
|
|
249
|
-
snapshotter: method(:snapshot_at),
|
|
250
|
-
|
|
251
|
-
|
|
312
|
+
snapshotter: method(:snapshot_at), refresher: refresher,
|
|
313
|
+
preparer: @historical_store.method(:prepare),
|
|
314
|
+
activity_index: @historical_store.method(:activity_index)
|
|
252
315
|
)
|
|
253
316
|
end
|
|
254
317
|
|
|
@@ -267,15 +330,6 @@ module PaperTrailDiff
|
|
|
267
330
|
)
|
|
268
331
|
end
|
|
269
332
|
|
|
270
|
-
#: (untyped) -> void
|
|
271
|
-
def reject_live_habtm_activity!(model_class)
|
|
272
|
-
paths = @traversal.habtm_paths(model_class)
|
|
273
|
-
return if paths.empty?
|
|
274
|
-
|
|
275
|
-
message = "live-ended activity cannot reconstruct HABTM event boundaries: #{paths.join(', ')}"
|
|
276
|
-
raise UnsupportedLiveActivityError, message
|
|
277
|
-
end
|
|
278
|
-
|
|
279
333
|
#: (untyped) -> RecordSnapshot?
|
|
280
334
|
def snapshot_for_endpoint(endpoint)
|
|
281
335
|
Endpoint.version?(endpoint) ? historical_snapshot(endpoint) : live_snapshot(endpoint)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module PaperTrailDiff
|
|
5
|
+
# Shares association event discovery across every root in a prepared graph.
|
|
6
|
+
class PreparedActivityIndex
|
|
7
|
+
#: (PreparedRecordIndex, Array[untyped], start_at: untyped, end_at: untyped) -> void
|
|
8
|
+
def initialize(records, root_versions, start_at:, end_at:)
|
|
9
|
+
@records = records
|
|
10
|
+
@range = ActivityRange.new(start_at, end_at)
|
|
11
|
+
@roots = root_versions.group_by { |version| [version.item_type, version.item_id.to_s] }
|
|
12
|
+
@nodes = {} #: Hash[String, Array[untyped]]
|
|
13
|
+
@children = {} #: Hash[Array[String], untyped]
|
|
14
|
+
@rows = {} #: Hash[Array[String], Hash[String, Array[untyped]]]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#: (untyped, Array[untyped]) -> void
|
|
18
|
+
def register(model, ids)
|
|
19
|
+
name = model.base_class.name.to_s
|
|
20
|
+
@nodes[name] = (@nodes.fetch(name, []) | ids)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
#: (untyped, Array[untyped]) -> Array[untyped]
|
|
24
|
+
def versions(model, ids)
|
|
25
|
+
ids.flat_map do |id|
|
|
26
|
+
key = [model.base_class.name.to_s, id.to_s]
|
|
27
|
+
@roots[key] || @records.versions_for_id(model, id)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
#: (untyped, Array[untyped], untyped, ActivityRange) -> Array[untyped]
|
|
32
|
+
def child_ids(model, ids, reflection, range)
|
|
33
|
+
key = [model.base_class.name.to_s, reflection.name.to_s]
|
|
34
|
+
historical, live = @children[key] ||= child_rows(model, reflection)
|
|
35
|
+
empty = [] #: Array[untyped]
|
|
36
|
+
selected = historical_ids(reflection.klass, ids, historical, range)
|
|
37
|
+
selected |= ids.flat_map { |id| live.fetch(id.to_s, empty) } if range.current_end?
|
|
38
|
+
selected.uniq
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#: (untyped, Array[untyped], untyped, ?transactions: bool) -> Array[untyped]
|
|
42
|
+
def association_rows(model, selected_versions, reflection, transactions: false)
|
|
43
|
+
key = [model.base_class.name.to_s, reflection.name.to_s]
|
|
44
|
+
rows = @rows[key] ||= load_association_rows(model, reflection, transactions)
|
|
45
|
+
version_ids(selected_versions, transactions).flat_map { |id| rows.fetch(id.to_s, []) }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# @rbs @records: PreparedRecordIndex
|
|
51
|
+
# @rbs @range: ActivityRange
|
|
52
|
+
# @rbs @roots: Hash[Array[String], Array[untyped]]
|
|
53
|
+
# @rbs @nodes: Hash[String, Array[untyped]]
|
|
54
|
+
# @rbs @children: Hash[Array[String], untyped]
|
|
55
|
+
# @rbs @rows: Hash[Array[String], Hash[String, Array[untyped]]]
|
|
56
|
+
|
|
57
|
+
#: (untyped, Array[untyped], untyped, ActivityRange) -> Array[untyped]
|
|
58
|
+
def historical_ids(model, ids, historical, range)
|
|
59
|
+
empty = [] #: Array[untyped]
|
|
60
|
+
ids.flat_map do |id|
|
|
61
|
+
historical.fetch(id.to_s, empty).filter_map do |row|
|
|
62
|
+
row[1] if eligible_child?(model, row, range)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#: (untyped) -> Array[untyped]
|
|
68
|
+
def node_ids(model)
|
|
69
|
+
name = model.base_class.name.to_s
|
|
70
|
+
@nodes.fetch(name) do
|
|
71
|
+
raise ConfigurationError,
|
|
72
|
+
"prepared activity graph is missing #{name}; " \
|
|
73
|
+
'register the model before collecting activity'
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
#: (untyped, untyped) -> untyped
|
|
78
|
+
def child_rows(model, reflection)
|
|
79
|
+
loader = ActivityChildCandidateLoader.new(
|
|
80
|
+
model, node_ids(model), reflection, @range
|
|
81
|
+
)
|
|
82
|
+
[loader.historical_rows.group_by { |row| row.first.to_s },
|
|
83
|
+
loader.current_pairs.group_by { |row| row.first.to_s }
|
|
84
|
+
.transform_values { |rows| rows.map(&:last) }]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Mirrors VersionAssociationCandidateScope's before-window rule. The loaded
|
|
88
|
+
# series includes changes to other owners too, so an old membership cannot
|
|
89
|
+
# survive a later pre-window reassignment just because that owner is absent.
|
|
90
|
+
#: (untyped, Array[untyped], ActivityRange) -> bool
|
|
91
|
+
def eligible_child?(model, row, range)
|
|
92
|
+
_owner, id, timestamp, event = row
|
|
93
|
+
return timestamp <= range.end_time if timestamp >= range.start_time
|
|
94
|
+
return false if event == 'destroy'
|
|
95
|
+
|
|
96
|
+
later = @records.versions_for_id(model, id)
|
|
97
|
+
.bsearch { |version| version.created_at > timestamp }
|
|
98
|
+
!later || later.created_at >= range.start_time
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
#: (untyped, untyped, bool) -> Hash[String, Array[untyped]]
|
|
102
|
+
def load_association_rows(model, reflection, transactions)
|
|
103
|
+
all_versions = versions(model, node_ids(model))
|
|
104
|
+
foreign_key = transactions ? reflection.name.to_s : reflection.foreign_key.to_s
|
|
105
|
+
model.paper_trail.version_association_class.where(
|
|
106
|
+
version_id: version_ids(all_versions, transactions), foreign_key_name: foreign_key
|
|
107
|
+
).to_a.group_by { |row| row.version_id.to_s }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
#: (Array[untyped], bool) -> Array[untyped]
|
|
111
|
+
def version_ids(versions, transactions)
|
|
112
|
+
if transactions
|
|
113
|
+
versions.filter_map do |version|
|
|
114
|
+
version.transaction_id if version.respond_to?(:transaction_id)
|
|
115
|
+
end.uniq
|
|
116
|
+
else
|
|
117
|
+
versions.map(&:id)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -50,11 +50,15 @@ module PaperTrailDiff
|
|
|
50
50
|
def belongs_to_groups(owner_class, owner_ids, reflection)
|
|
51
51
|
key = scalar_foreign_key(reflection)
|
|
52
52
|
return {} unless key
|
|
53
|
+
unless reflection.polymorphic?
|
|
54
|
+
return group_for(reflection.klass,
|
|
55
|
+
@records.attribute_values_for(owner_class, owner_ids, key))
|
|
56
|
+
end
|
|
53
57
|
|
|
54
58
|
grouped = {} #: Hash[String, Hash[Symbol, untyped]]
|
|
55
59
|
@records.records_for(owner_class, owner_ids).each do |owner|
|
|
56
60
|
id = owner.public_send(key)
|
|
57
|
-
model_class =
|
|
61
|
+
model_class = AssociationTraversal.belongs_to_target(reflection, owner)
|
|
58
62
|
add_identity(grouped, model_class, id) if model_class && id
|
|
59
63
|
end
|
|
60
64
|
grouped
|
|
@@ -109,7 +113,12 @@ module PaperTrailDiff
|
|
|
109
113
|
association_class.arel_table[:foreign_key_id],
|
|
110
114
|
version_class.arel_table[:item_id]
|
|
111
115
|
)
|
|
112
|
-
group_ids_by_owner(pairs)
|
|
116
|
+
group_ids_by_owner(typed_child_ids(reflection.klass, pairs))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
#: (untyped, Array[Array[untyped]]) -> Array[Array[untyped]]
|
|
120
|
+
def typed_child_ids(model_class, pairs)
|
|
121
|
+
pairs.map { |owner_id, child_id| [owner_id, Support.record_id(model_class, child_id)] }
|
|
113
122
|
end
|
|
114
123
|
|
|
115
124
|
#: (untyped, Array[untyped], untyped, String) -> untyped
|
|
@@ -139,15 +148,6 @@ module PaperTrailDiff
|
|
|
139
148
|
group_ids_by_owner(relation.pluck(foreign_key, reflection.klass.primary_key))
|
|
140
149
|
end
|
|
141
150
|
|
|
142
|
-
#: (untyped, untyped) -> untyped
|
|
143
|
-
def target_class(reflection, owner)
|
|
144
|
-
return reflection.klass unless reflection.polymorphic?
|
|
145
|
-
|
|
146
|
-
Object.const_get(owner.public_send(reflection.foreign_type).to_s)
|
|
147
|
-
rescue NameError
|
|
148
|
-
nil
|
|
149
|
-
end
|
|
150
|
-
|
|
151
151
|
#: (Hash[String, Hash[Symbol, untyped]], untyped, untyped) -> void
|
|
152
152
|
def add_identity(groups, model_class, id)
|
|
153
153
|
group = groups[model_class.name.to_s] ||= { model: model_class, ids: [] }
|
|
@@ -4,17 +4,21 @@
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
5
|
# Batched record states and relationship candidates for one activity range.
|
|
6
6
|
class PreparedHistory
|
|
7
|
+
attr_reader :activity_index #: PreparedActivityIndex?
|
|
8
|
+
|
|
7
9
|
DIRECT_RESOLVERS = {
|
|
8
10
|
belongs_to: :resolve_belongs_to,
|
|
9
11
|
has_one: :resolve_children,
|
|
10
12
|
has_many: :resolve_children,
|
|
11
|
-
has_and_belongs_to_many: :
|
|
13
|
+
has_and_belongs_to_many: :habtm_records
|
|
12
14
|
}.freeze
|
|
13
15
|
private_constant :DIRECT_RESOLVERS
|
|
14
16
|
|
|
15
|
-
#: (PreparedRecordIndex) -> void
|
|
16
|
-
def initialize(records)
|
|
17
|
+
#: (PreparedRecordIndex, ?activity_index: PreparedActivityIndex?) -> void
|
|
18
|
+
def initialize(records, activity_index: nil)
|
|
17
19
|
@records = records
|
|
20
|
+
@state_reader = PreparedStateAssociationReader.new(records, method(:edge))
|
|
21
|
+
@activity_index = activity_index
|
|
18
22
|
@edges = {} #: Hash[Array[String], Hash[String, Hash[Symbol, untyped]]]
|
|
19
23
|
@unsupported = {} #: Hash[Array[String], bool]
|
|
20
24
|
@habtm = {} #: Hash[Array[String], Hash[String, Array[untyped]]]
|
|
@@ -24,7 +28,7 @@ module PaperTrailDiff
|
|
|
24
28
|
def merge_edge(owner_class, reflection, groups)
|
|
25
29
|
key = edge_key(owner_class, reflection)
|
|
26
30
|
empty = {} #: Hash[String, Hash[Symbol, untyped]]
|
|
27
|
-
@edges[key] =
|
|
31
|
+
@edges[key] = Support.merge_record_groups(@edges.fetch(key, empty), groups)
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
#: (untyped, untyped) -> Hash[String, Hash[Symbol, untyped]]
|
|
@@ -48,6 +52,16 @@ module PaperTrailDiff
|
|
|
48
52
|
@records.transition(model_class, id, version)
|
|
49
53
|
end
|
|
50
54
|
|
|
55
|
+
#: (untyped, untyped, untyped, ?direct: bool) -> [bool, untyped]
|
|
56
|
+
def record_after(model_class, id, version, direct: false)
|
|
57
|
+
@records.record_after(model_class, id, version, direct: direct)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
#: (PreparedRecordState, untyped, untyped) -> [bool, Array[PreparedRecordState]]
|
|
61
|
+
def snapshot_states(owner, reflection, boundary)
|
|
62
|
+
@state_reader.call(owner, reflection, boundary)
|
|
63
|
+
end
|
|
64
|
+
|
|
51
65
|
#: (untyped, untyped, untyped, habtm_boundary: untyped) -> [bool, Array[untyped]]
|
|
52
66
|
def resolve(record, reflection, boundary, habtm_boundary:)
|
|
53
67
|
return [false, []] unless supported?(record.class, reflection)
|
|
@@ -63,6 +77,7 @@ module PaperTrailDiff
|
|
|
63
77
|
private
|
|
64
78
|
|
|
65
79
|
# @rbs @records: PreparedRecordIndex
|
|
80
|
+
# @rbs @state_reader: PreparedStateAssociationReader
|
|
66
81
|
# @rbs @edges: Hash[Array[String], Hash[String, Hash[Symbol, untyped]]]
|
|
67
82
|
# @rbs @unsupported: Hash[Array[String], bool]
|
|
68
83
|
# @rbs @habtm: Hash[Array[String], Hash[String, Array[untyped]]]
|
|
@@ -70,12 +85,7 @@ module PaperTrailDiff
|
|
|
70
85
|
#: (untyped, untyped) -> bool
|
|
71
86
|
def supported?(owner_class, reflection)
|
|
72
87
|
!@unsupported[edge_key(owner_class, reflection)] &&
|
|
73
|
-
reflection.scope.nil? && !
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
#: (untyped) -> bool
|
|
77
|
-
def default_scoped?(model_class)
|
|
78
|
-
model_class.default_scopes?
|
|
88
|
+
reflection.scope.nil? && !reflection.klass.default_scopes?
|
|
79
89
|
end
|
|
80
90
|
|
|
81
91
|
#: (untyped, untyped, untyped, untyped) -> Array[untyped]?
|
|
@@ -94,11 +104,6 @@ module PaperTrailDiff
|
|
|
94
104
|
child_records(record, reflection, boundary)
|
|
95
105
|
end
|
|
96
106
|
|
|
97
|
-
#: (untyped, untyped, untyped, untyped) -> Array[untyped]?
|
|
98
|
-
def resolve_habtm(record, reflection, boundary, habtm_boundary)
|
|
99
|
-
habtm_records(record, reflection, boundary, habtm_boundary)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
107
|
#: (untyped, untyped, untyped) -> Array[untyped]?
|
|
103
108
|
def belongs_to_records(record, reflection, boundary)
|
|
104
109
|
foreign_key = scalar_foreign_key(reflection)
|
|
@@ -107,7 +112,7 @@ module PaperTrailDiff
|
|
|
107
112
|
id = record.public_send(foreign_key)
|
|
108
113
|
return [] if id.nil?
|
|
109
114
|
|
|
110
|
-
model_class =
|
|
115
|
+
model_class = AssociationTraversal.belongs_to_target(reflection, record)
|
|
111
116
|
return unless model_class && @records.loaded?(model_class, id)
|
|
112
117
|
|
|
113
118
|
Array(@records.record_before(model_class, id, boundary)).compact
|
|
@@ -189,15 +194,6 @@ module PaperTrailDiff
|
|
|
189
194
|
key.to_s unless key.is_a?(Array)
|
|
190
195
|
end
|
|
191
196
|
|
|
192
|
-
#: (untyped, untyped) -> untyped
|
|
193
|
-
def belongs_to_class(record, reflection)
|
|
194
|
-
return reflection.klass unless reflection.polymorphic?
|
|
195
|
-
|
|
196
|
-
Object.const_get(record.public_send(reflection.foreign_type).to_s)
|
|
197
|
-
rescue NameError
|
|
198
|
-
nil
|
|
199
|
-
end
|
|
200
|
-
|
|
201
197
|
#: (untyped, untyped) -> Array[String]
|
|
202
198
|
def owner_types(reflection, owner)
|
|
203
199
|
types = [owner.class.name, owner.class.base_class.name, reflection.active_record.name]
|
|
@@ -218,10 +214,5 @@ module PaperTrailDiff
|
|
|
218
214
|
def edge_key(owner_class, reflection)
|
|
219
215
|
[owner_class.base_class.name.to_s, reflection.name.to_s]
|
|
220
216
|
end
|
|
221
|
-
|
|
222
|
-
#: (Hash[String, Hash[Symbol, untyped]], Hash[String, Hash[Symbol, untyped]]) -> Hash[String, Hash[Symbol, untyped]]
|
|
223
|
-
def merge_groups(existing, incoming)
|
|
224
|
-
Support.merge_record_groups(existing, incoming)
|
|
225
|
-
end
|
|
226
217
|
end
|
|
227
218
|
end
|