paper_trail_diff 0.4.0 → 0.5.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 +20 -0
- data/README.md +56 -2
- data/lib/paper_trail_diff/analysis_batch.rb +74 -0
- data/lib/paper_trail_diff/batch_boundary_resolver.rb +137 -0
- data/lib/paper_trail_diff/batched_root_analyzer.rb +50 -0
- data/lib/paper_trail_diff/batched_root_versions.rb +131 -0
- data/lib/paper_trail_diff/comparison_batch.rb +37 -13
- data/lib/paper_trail_diff/historical_snapshot_store.rb +3 -7
- data/lib/paper_trail_diff/paper_trail_adapter.rb +49 -33
- data/lib/paper_trail_diff/timeline_builder.rb +3 -3
- data/lib/paper_trail_diff/timeline_range.rb +9 -2
- data/lib/paper_trail_diff/traversal_preparer.rb +40 -0
- data/lib/paper_trail_diff/version.rb +1 -1
- data/lib/paper_trail_diff.rb +36 -5
- data/sig/generated/paper_trail_diff/analysis_batch.rbs +42 -0
- data/sig/generated/paper_trail_diff/batch_boundary_resolver.rbs +62 -0
- data/sig/generated/paper_trail_diff/batched_root_analyzer.rbs +28 -0
- data/sig/generated/paper_trail_diff/batched_root_versions.rbs +62 -0
- data/sig/generated/paper_trail_diff/comparison_batch.rbs +15 -0
- data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +0 -1
- data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +16 -7
- data/sig/generated/paper_trail_diff/timeline_builder.rbs +2 -2
- data/sig/generated/paper_trail_diff/timeline_range.rbs +6 -2
- data/sig/generated/paper_trail_diff/traversal_preparer.rbs +22 -0
- data/sig/generated/paper_trail_diff.rbs +13 -3
- metadata +14 -4
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
|
-
# PaperTrail/ActiveRecord boundary that produces plain record snapshots.
|
|
6
|
-
class
|
|
5
|
+
# PaperTrail/ActiveRecord boundary that produces plain record snapshots. It is
|
|
6
|
+
# deliberately the widest class here: it fronts every public operation and is
|
|
7
|
+
# the only place allowed to know about both PaperTrail and the pure engine.
|
|
8
|
+
# Reconstruction logic lives in the collaborators it wires together.
|
|
9
|
+
class PaperTrailAdapter # rubocop:disable Metrics/ClassLength
|
|
7
10
|
#: (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
|
|
8
11
|
def initialize(associations:, ignore:, reload_live_endpoints: true)
|
|
9
12
|
@association_tree = AssociationTree.build(associations)
|
|
10
13
|
@ignore_policy = IgnorePolicy.build(ignore, association_paths: @association_tree.paths)
|
|
11
14
|
@traversal = AssociationTraversal.new(@association_tree)
|
|
15
|
+
@traversal_preparer = TraversalPreparer.new(tree: @association_tree, traversal: @traversal)
|
|
12
16
|
@live_endpoints = LiveEndpointProvider.new(
|
|
13
17
|
tree: @association_tree, traversal: @traversal, reload: reload_live_endpoints
|
|
14
18
|
)
|
|
@@ -18,9 +22,7 @@ module PaperTrailDiff
|
|
|
18
22
|
tree: @association_tree, ignore_policy: @ignore_policy,
|
|
19
23
|
traversal: @traversal, pool: @snapshot_pool
|
|
20
24
|
)
|
|
21
|
-
|
|
22
|
-
@timeline_snapshotter = TimelineSnapshotProvider.new(@historical_store)
|
|
23
|
-
@activity_snapshotter = build_activity_snapshotter
|
|
25
|
+
build_snapshotters
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
#: (untyped, untyped) -> Diff
|
|
@@ -40,7 +42,7 @@ module PaperTrailDiff
|
|
|
40
42
|
Instrumentation.instrument('compare_many', payload) do
|
|
41
43
|
ComparisonBatch.new(
|
|
42
44
|
comparisons,
|
|
43
|
-
live_loader: @live_endpoints.method(:call), preparer: method(:
|
|
45
|
+
live_loader: @live_endpoints.method(:call), preparer: @traversal_preparer.method(:call),
|
|
44
46
|
history_preparer: @historical_store.method(:prepare_batch),
|
|
45
47
|
historical_snapshotter: method(:historical_snapshot),
|
|
46
48
|
live_normalizer: method(:normalize_live_snapshot)
|
|
@@ -50,7 +52,7 @@ module PaperTrailDiff
|
|
|
50
52
|
|
|
51
53
|
#: (untyped, from: untyped, to: untyped, within: untyped) -> Array[Step]
|
|
52
54
|
def timeline(record, from:, to:, within:)
|
|
53
|
-
|
|
55
|
+
@traversal_preparer.call(record.class, historical: true)
|
|
54
56
|
builder = TimelineBuilder.new(
|
|
55
57
|
record,
|
|
56
58
|
from: from,
|
|
@@ -65,7 +67,7 @@ module PaperTrailDiff
|
|
|
65
67
|
def activity_timeline(record, from:, to:, within:)
|
|
66
68
|
payload = @instrumentation_payload.merge(model_type: record.class.base_class.name.to_s)
|
|
67
69
|
Instrumentation.instrument('activity_timeline', payload) do
|
|
68
|
-
|
|
70
|
+
@traversal_preparer.call(record.class, historical: true)
|
|
69
71
|
reject_live_habtm_activity!(record.class) if Endpoint.record?(to)
|
|
70
72
|
steps = activity_builder(record, from: from, to: to, within: within).build
|
|
71
73
|
payload[:step_count] = steps.length
|
|
@@ -76,11 +78,11 @@ module PaperTrailDiff
|
|
|
76
78
|
#: (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool) -> Analysis
|
|
77
79
|
def analyze(record, from:, to:, within:, activity: false)
|
|
78
80
|
if activity
|
|
79
|
-
|
|
81
|
+
@traversal_preparer.call(record.class, historical: true)
|
|
80
82
|
return activity_builder(record, from: from, to: to, within: within).analyze
|
|
81
83
|
end
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
@traversal_preparer.call(record.class, historical: true)
|
|
84
86
|
TimelineBuilder.new(
|
|
85
87
|
record,
|
|
86
88
|
from: from,
|
|
@@ -90,6 +92,22 @@ module PaperTrailDiff
|
|
|
90
92
|
).analyze
|
|
91
93
|
end
|
|
92
94
|
|
|
95
|
+
# Analyzes many roots over one shared range, preparing their history once.
|
|
96
|
+
#: (Array[untyped], within: untyped, ?activity: bool) -> Hash[identity, Analysis]
|
|
97
|
+
def analyze_many(records, within:, activity: false)
|
|
98
|
+
count = records.is_a?(Array) ? records.length : 0
|
|
99
|
+
payload = @instrumentation_payload.merge(comparison_count: count)
|
|
100
|
+
Instrumentation.instrument('analyze_many', payload) do
|
|
101
|
+
AnalysisBatch.new(
|
|
102
|
+
records,
|
|
103
|
+
time_range: within.nil? ? nil : TimeRange.new(within),
|
|
104
|
+
live_loader: @live_endpoints.method(:call),
|
|
105
|
+
history_preparer: @historical_store.method(:prepare_batch),
|
|
106
|
+
analyzer: batched_root_analyzer(activity)
|
|
107
|
+
).call
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
93
111
|
private
|
|
94
112
|
|
|
95
113
|
# @rbs @association_tree: AssociationTree
|
|
@@ -97,19 +115,38 @@ module PaperTrailDiff
|
|
|
97
115
|
# @rbs @instrumentation_payload: Hash[Symbol, untyped]
|
|
98
116
|
# @rbs @ignore_policy: IgnorePolicy
|
|
99
117
|
# @rbs @traversal: AssociationTraversal
|
|
118
|
+
# @rbs @traversal_preparer: TraversalPreparer
|
|
100
119
|
# @rbs @snapshot_pool: SnapshotPool
|
|
101
120
|
# @rbs @normalizer: SnapshotNormalizer
|
|
102
121
|
# @rbs @historical_store: HistoricalSnapshotStore
|
|
103
122
|
# @rbs @timeline_snapshotter: TimelineSnapshotProvider
|
|
104
123
|
# @rbs @activity_snapshotter: ActivitySnapshotProvider
|
|
105
124
|
|
|
125
|
+
#: () -> void
|
|
126
|
+
def build_snapshotters
|
|
127
|
+
@historical_store = build_historical_store
|
|
128
|
+
@timeline_snapshotter = TimelineSnapshotProvider.new(@historical_store)
|
|
129
|
+
@activity_snapshotter = build_activity_snapshotter
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
#: (bool) -> BatchedRootAnalyzer
|
|
133
|
+
def batched_root_analyzer(activity)
|
|
134
|
+
BatchedRootAnalyzer.new(
|
|
135
|
+
tree: @association_tree,
|
|
136
|
+
timeline_snapshotter: @timeline_snapshotter,
|
|
137
|
+
activity_snapshotter: @activity_snapshotter,
|
|
138
|
+
preparer: @traversal_preparer.method(:call),
|
|
139
|
+
activity: activity
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
106
143
|
#: () -> HistoricalSnapshotStore
|
|
107
144
|
def build_historical_store
|
|
108
145
|
HistoricalSnapshotStore.new(
|
|
109
146
|
tree: @association_tree,
|
|
110
147
|
traversal: @traversal,
|
|
111
148
|
normalizer: @normalizer,
|
|
112
|
-
preparer: method(:
|
|
149
|
+
preparer: @traversal_preparer.method(:call)
|
|
113
150
|
)
|
|
114
151
|
end
|
|
115
152
|
|
|
@@ -180,30 +217,9 @@ module PaperTrailDiff
|
|
|
180
217
|
|
|
181
218
|
#: (untyped) -> RecordSnapshot
|
|
182
219
|
def normalize_live_snapshot(current)
|
|
183
|
-
|
|
220
|
+
@traversal_preparer.call(current.class, historical: false)
|
|
184
221
|
@normalizer.call(current, reifier: LiveAssociationReader.new) ||
|
|
185
222
|
raise(InvalidEndpointError, 'current record endpoint could not be normalized')
|
|
186
223
|
end
|
|
187
|
-
|
|
188
|
-
#: (untyped, historical: bool) -> void
|
|
189
|
-
def prepare_traversal!(model_class, historical:)
|
|
190
|
-
return if @association_tree.empty?
|
|
191
|
-
|
|
192
|
-
ensure_association_tracking! if historical
|
|
193
|
-
@traversal.validate!(model_class)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
#: () -> void
|
|
197
|
-
def ensure_association_tracking!
|
|
198
|
-
paper_trail = Object.const_get(:PaperTrail) #: untyped
|
|
199
|
-
config = paper_trail.config #: untyped
|
|
200
|
-
available = defined?(::PaperTrailAssociationTracking) &&
|
|
201
|
-
config.respond_to?(:track_associations?) &&
|
|
202
|
-
config.track_associations?
|
|
203
|
-
return if available
|
|
204
|
-
|
|
205
|
-
message = 'association tracking must be loaded and enabled to compare historical associations'
|
|
206
|
-
raise AssociationTrackingUnavailableError, message
|
|
207
|
-
end
|
|
208
224
|
end
|
|
209
225
|
end
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
5
|
# Selects and compares a chronological slice of a record's version history.
|
|
6
6
|
class TimelineBuilder
|
|
7
|
-
#: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped) -> void
|
|
8
|
-
def initialize(record, from:, to:, snapshotter:, within: nil)
|
|
7
|
+
#: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?versions: Array[untyped]?) -> void
|
|
8
|
+
def initialize(record, from:, to:, snapshotter:, within: nil, versions: nil) # rubocop:disable Metrics/ParameterLists
|
|
9
9
|
@record = record
|
|
10
|
-
@range = TimelineRange.new(record, from: from, to: to, within: within)
|
|
10
|
+
@range = TimelineRange.new(record, from: from, to: to, within: within, versions: versions)
|
|
11
11
|
@snapshotter = snapshotter
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -10,9 +10,10 @@ module PaperTrailDiff
|
|
|
10
10
|
attr_reader :to #: untyped
|
|
11
11
|
attr_reader :time_range #: TimeRange?
|
|
12
12
|
|
|
13
|
-
#: (untyped, from: untyped, to: untyped, within: untyped) -> void
|
|
14
|
-
def initialize(record, from:, to:, within:)
|
|
13
|
+
#: (untyped, from: untyped, to: untyped, within: untyped, ?versions: Array[untyped]?) -> void
|
|
14
|
+
def initialize(record, from:, to:, within:, versions: nil)
|
|
15
15
|
@record = record
|
|
16
|
+
@versions = versions&.freeze
|
|
16
17
|
@requested_from = from
|
|
17
18
|
@requested_to = to
|
|
18
19
|
@time_range = build_time_range(within)
|
|
@@ -29,8 +30,13 @@ module PaperTrailDiff
|
|
|
29
30
|
(symbolic?(@requested_from) && @from.nil?) || (symbolic?(@requested_to) && @to.nil?)
|
|
30
31
|
end
|
|
31
32
|
|
|
33
|
+
# A batch may have selected these versions already, in which case reselecting
|
|
34
|
+
# them per record would undo the batching.
|
|
32
35
|
#: (?context_required: bool) -> Array[untyped]
|
|
33
36
|
def select(context_required: false)
|
|
37
|
+
preselected = @versions
|
|
38
|
+
return preselected if preselected
|
|
39
|
+
|
|
34
40
|
range = time_range
|
|
35
41
|
if range
|
|
36
42
|
return TimeVersionRange.new(@record, time_range: range).select(
|
|
@@ -61,6 +67,7 @@ module PaperTrailDiff
|
|
|
61
67
|
private
|
|
62
68
|
|
|
63
69
|
# @rbs @record: untyped
|
|
70
|
+
# @rbs @versions: Array[untyped]?
|
|
64
71
|
# @rbs @requested_from: untyped
|
|
65
72
|
# @rbs @requested_to: untyped
|
|
66
73
|
# @rbs @from: untyped
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module PaperTrailDiff
|
|
5
|
+
# Checks that a model can be traversed before any reconstruction starts, and
|
|
6
|
+
# that association tracking is actually available when history is involved.
|
|
7
|
+
class TraversalPreparer
|
|
8
|
+
#: (tree: AssociationTree, traversal: AssociationTraversal) -> void
|
|
9
|
+
def initialize(tree:, traversal:)
|
|
10
|
+
@tree = tree
|
|
11
|
+
@traversal = traversal
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
#: (untyped, historical: bool) -> void
|
|
15
|
+
def call(model_class, historical:)
|
|
16
|
+
return if @tree.empty?
|
|
17
|
+
|
|
18
|
+
ensure_association_tracking! if historical
|
|
19
|
+
@traversal.validate!(model_class)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# @rbs @tree: AssociationTree
|
|
25
|
+
# @rbs @traversal: AssociationTraversal
|
|
26
|
+
|
|
27
|
+
#: () -> void
|
|
28
|
+
def ensure_association_tracking!
|
|
29
|
+
paper_trail = Object.const_get(:PaperTrail) #: untyped
|
|
30
|
+
config = paper_trail.config #: untyped
|
|
31
|
+
available = defined?(::PaperTrailAssociationTracking) &&
|
|
32
|
+
config.respond_to?(:track_associations?) &&
|
|
33
|
+
config.track_associations?
|
|
34
|
+
return if available
|
|
35
|
+
|
|
36
|
+
message = 'association tracking must be loaded and enabled to compare historical associations'
|
|
37
|
+
raise AssociationTrackingUnavailableError, message
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/paper_trail_diff.rb
CHANGED
|
@@ -10,6 +10,7 @@ require_relative 'paper_trail_diff/errors'
|
|
|
10
10
|
require_relative 'paper_trail_diff/configuration'
|
|
11
11
|
require_relative 'paper_trail_diff/endpoint'
|
|
12
12
|
require_relative 'paper_trail_diff/association_traversal'
|
|
13
|
+
require_relative 'paper_trail_diff/traversal_preparer'
|
|
13
14
|
require_relative 'paper_trail_diff/association_discovery'
|
|
14
15
|
require_relative 'paper_trail_diff/diagnostics'
|
|
15
16
|
require_relative 'paper_trail_diff/collection_identity_index'
|
|
@@ -36,7 +37,9 @@ require_relative 'paper_trail_diff/live_endpoint_batch_loader'
|
|
|
36
37
|
require_relative 'paper_trail_diff/preloaded_endpoint_batch_loader'
|
|
37
38
|
require_relative 'paper_trail_diff/live_endpoint_provider'
|
|
38
39
|
require_relative 'paper_trail_diff/live_graph_collector'
|
|
40
|
+
require_relative 'paper_trail_diff/batch_boundary_resolver'
|
|
39
41
|
require_relative 'paper_trail_diff/comparison_batch'
|
|
42
|
+
require_relative 'paper_trail_diff/batched_root_versions'
|
|
40
43
|
require_relative 'paper_trail_diff/snapshot_normalizer'
|
|
41
44
|
require_relative 'paper_trail_diff/historical_snapshot_store'
|
|
42
45
|
require_relative 'paper_trail_diff/timeline_snapshot_provider'
|
|
@@ -56,6 +59,8 @@ require_relative 'paper_trail_diff/activity_boundary'
|
|
|
56
59
|
require_relative 'paper_trail_diff/step'
|
|
57
60
|
require_relative 'paper_trail_diff/analysis'
|
|
58
61
|
require_relative 'paper_trail_diff/activity_root_steps'
|
|
62
|
+
require_relative 'paper_trail_diff/analysis_batch'
|
|
63
|
+
require_relative 'paper_trail_diff/batched_root_analyzer'
|
|
59
64
|
require_relative 'paper_trail_diff/version_range'
|
|
60
65
|
require_relative 'paper_trail_diff/time_range'
|
|
61
66
|
require_relative 'paper_trail_diff/time_version_range'
|
|
@@ -90,8 +95,9 @@ require_relative 'paper_trail_diff/paper_trail_adapter'
|
|
|
90
95
|
# type traversal_record_path = Array[RecordReference]
|
|
91
96
|
# end
|
|
92
97
|
|
|
93
|
-
# Structured version comparison for PaperTrail.
|
|
94
|
-
|
|
98
|
+
# Structured version comparison for PaperTrail. Each method is a thin,
|
|
99
|
+
# documented entry point, so this reads as an API listing rather than logic.
|
|
100
|
+
module PaperTrailDiff # rubocop:disable Metrics/ModuleLength
|
|
95
101
|
DEFAULT_IGNORED_ATTRIBUTES = ['updated_at'].freeze
|
|
96
102
|
SUPPORTED_ASSOCIATION_MACROS = %i[
|
|
97
103
|
belongs_to
|
|
@@ -154,16 +160,23 @@ module PaperTrailDiff
|
|
|
154
160
|
end
|
|
155
161
|
|
|
156
162
|
# Compares adjacent root and selected-descendant activity boundaries.
|
|
157
|
-
|
|
163
|
+
# `reload_live_endpoints:` applies only when `to:` is a current record; the
|
|
164
|
+
# other range forms never read live state.
|
|
165
|
+
#: (untyped, ?from: untyped, ?to: untyped, ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?reload_live_endpoints: bool) -> Array[ActivityStep]
|
|
158
166
|
def activity_timeline( # rubocop:disable Metrics/ParameterLists
|
|
159
167
|
record,
|
|
160
168
|
from: nil,
|
|
161
169
|
to: nil,
|
|
162
170
|
within: nil,
|
|
163
171
|
associations: [],
|
|
164
|
-
ignore: DEFAULT_IGNORED_ATTRIBUTES
|
|
172
|
+
ignore: DEFAULT_IGNORED_ATTRIBUTES,
|
|
173
|
+
reload_live_endpoints: true
|
|
165
174
|
)
|
|
166
|
-
PaperTrailAdapter.new(
|
|
175
|
+
PaperTrailAdapter.new(
|
|
176
|
+
associations: associations,
|
|
177
|
+
ignore: ignore,
|
|
178
|
+
reload_live_endpoints: reload_live_endpoints
|
|
179
|
+
).activity_timeline(
|
|
167
180
|
record,
|
|
168
181
|
from: from,
|
|
169
182
|
to: to,
|
|
@@ -191,6 +204,24 @@ module PaperTrailDiff
|
|
|
191
204
|
)
|
|
192
205
|
end
|
|
193
206
|
|
|
207
|
+
# Analyzes many roots over one shared time window, preparing their selected
|
|
208
|
+
# history once for the batch instead of once per record. Roots with no
|
|
209
|
+
# versions in the window return an empty `Analysis`.
|
|
210
|
+
#: (Array[untyped], ?within: untyped, ?associations: Array[String | Symbol], ?ignore: ignore_option, ?activity: bool) -> Hash[identity, Analysis]
|
|
211
|
+
def analyze_many(
|
|
212
|
+
records,
|
|
213
|
+
within: nil,
|
|
214
|
+
associations: [],
|
|
215
|
+
ignore: DEFAULT_IGNORED_ATTRIBUTES,
|
|
216
|
+
activity: false
|
|
217
|
+
)
|
|
218
|
+
PaperTrailAdapter.new(associations: associations, ignore: ignore).analyze_many(
|
|
219
|
+
records,
|
|
220
|
+
within: within,
|
|
221
|
+
activity: activity
|
|
222
|
+
)
|
|
223
|
+
end
|
|
224
|
+
|
|
194
225
|
# Returns the association macros this release can normalize.
|
|
195
226
|
#: () -> Array[Symbol]
|
|
196
227
|
def supported_association_macros
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Generated from lib/paper_trail_diff/analysis_batch.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module PaperTrailDiff
|
|
4
|
+
# Builds one Analysis per root over a shared range, selecting every root's
|
|
5
|
+
# versions and preparing their association history once for the whole batch
|
|
6
|
+
# rather than once per root.
|
|
7
|
+
class AnalysisBatch
|
|
8
|
+
# : (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped) -> void
|
|
9
|
+
def initialize: (Array[untyped], time_range: TimeRange?, live_loader: untyped, history_preparer: untyped, analyzer: untyped) -> void
|
|
10
|
+
|
|
11
|
+
# : () -> Hash[identity, Analysis]
|
|
12
|
+
def call: () -> Hash[identity, Analysis]
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
@analyzer: untyped
|
|
17
|
+
|
|
18
|
+
@history_preparer: untyped
|
|
19
|
+
|
|
20
|
+
@live_loader: untyped
|
|
21
|
+
|
|
22
|
+
@records: Array[untyped]
|
|
23
|
+
|
|
24
|
+
@time_range: TimeRange?
|
|
25
|
+
|
|
26
|
+
# : () -> Array[untyped]
|
|
27
|
+
def validated_records: () -> Array[untyped]
|
|
28
|
+
|
|
29
|
+
# Association history is prepared per model class across every selected root
|
|
30
|
+
# version, which is the work that would otherwise repeat for each record.
|
|
31
|
+
# The roots are preloaded first, because preparation reads their current
|
|
32
|
+
# association state as a fallback and would otherwise walk it one root at a
|
|
33
|
+
# time.
|
|
34
|
+
# : (Array[untyped], Hash[Array[String], Array[untyped]]) -> void
|
|
35
|
+
def prepare: (Array[untyped], Hash[Array[String], Array[untyped]]) -> void
|
|
36
|
+
|
|
37
|
+
# A root with no versions in range has nothing to report, which is an empty
|
|
38
|
+
# result rather than a failed request.
|
|
39
|
+
# : (untyped, Array[untyped]) -> Analysis
|
|
40
|
+
def analysis_for: (untyped, Array[untyped]) -> Analysis
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Generated from lib/paper_trail_diff/batch_boundary_resolver.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module PaperTrailDiff
|
|
4
|
+
# Resolves `:first` and `:last` endpoints for a whole batch in two queries per
|
|
5
|
+
# model class. Left to the caller this is a per-root lookup, which reintroduces
|
|
6
|
+
# exactly the queries a batched comparison exists to remove.
|
|
7
|
+
class BatchBoundaryResolver
|
|
8
|
+
BOUNDARIES: untyped
|
|
9
|
+
|
|
10
|
+
# : (untyped) -> bool
|
|
11
|
+
def self.symbolic?: (untyped) -> bool
|
|
12
|
+
|
|
13
|
+
# : (Array[[untyped, untyped]]) -> void
|
|
14
|
+
def initialize: (Array[[ untyped, untyped ]]) -> void
|
|
15
|
+
|
|
16
|
+
# Returns the pairs with every resolvable symbol replaced. A symbol that
|
|
17
|
+
# names history the record does not have is left in place, so the caller can
|
|
18
|
+
# decide what an absent history means rather than being handed nil.
|
|
19
|
+
# : () -> Array[[untyped, untyped]]
|
|
20
|
+
def call: () -> Array[[ untyped, untyped ]]
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
@pairs: Array[[ untyped, untyped ]]
|
|
25
|
+
|
|
26
|
+
# : (untyped) -> bool
|
|
27
|
+
def symbolic?: (untyped) -> bool
|
|
28
|
+
|
|
29
|
+
# : (untyped, untyped, Hash[Array[String], Hash[Symbol, untyped]]) -> untyped
|
|
30
|
+
def resolve: (untyped, untyped, Hash[Array[String], Hash[Symbol, untyped]]) -> untyped
|
|
31
|
+
|
|
32
|
+
# A symbol carries no identity of its own, so the pair's other endpoint has
|
|
33
|
+
# to say which record is meant.
|
|
34
|
+
# : (untyped) -> Array[String]?
|
|
35
|
+
def anchor_identity: (untyped) -> Array[String]?
|
|
36
|
+
|
|
37
|
+
# : () -> Hash[Array[String], Hash[Symbol, untyped]]
|
|
38
|
+
def boundary_index: () -> Hash[Array[String], Hash[Symbol, untyped]]
|
|
39
|
+
|
|
40
|
+
# : () -> Array[[untyped, untyped]]
|
|
41
|
+
def anchors: () -> Array[[ untyped, untyped ]]
|
|
42
|
+
|
|
43
|
+
# : (untyped) -> [untyped, untyped]?
|
|
44
|
+
def anchor: (untyped) -> [ untyped, untyped ]?
|
|
45
|
+
|
|
46
|
+
# : (Hash[Array[String], Hash[Symbol, untyped]], untyped, Array[untyped]) -> void
|
|
47
|
+
def add_model_boundaries: (Hash[Array[String], Hash[Symbol, untyped]], untyped, Array[untyped]) -> void
|
|
48
|
+
|
|
49
|
+
# Two grouped queries name each root's outermost versions, and one more
|
|
50
|
+
# loads them, whatever the size of the batch.
|
|
51
|
+
# : (untyped, Array[untyped]) -> [Hash[untyped, untyped], Hash[untyped, untyped], Hash[untyped, untyped]]
|
|
52
|
+
def boundary_versions: (untyped, Array[untyped]) -> [ Hash[untyped, untyped], Hash[untyped, untyped], Hash[untyped, untyped] ]
|
|
53
|
+
|
|
54
|
+
# Grouped keys come back with whatever type the column uses, so match on the
|
|
55
|
+
# string form rather than assuming integers.
|
|
56
|
+
# : (Hash[untyped, untyped], untyped) -> untyped
|
|
57
|
+
def boundary_key: (Hash[untyped, untyped], untyped) -> untyped
|
|
58
|
+
|
|
59
|
+
# : (untyped, untyped) -> Array[String]
|
|
60
|
+
def identity_key: (untyped, untyped) -> Array[String]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generated from lib/paper_trail_diff/batched_root_analyzer.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module PaperTrailDiff
|
|
4
|
+
# Builds one root's Analysis inside a batch, from versions the batch already
|
|
5
|
+
# selected and history it already prepared.
|
|
6
|
+
class BatchedRootAnalyzer
|
|
7
|
+
# : (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool) -> void
|
|
8
|
+
def initialize: (tree: AssociationTree, timeline_snapshotter: untyped, activity_snapshotter: untyped, preparer: untyped, activity: bool) -> void
|
|
9
|
+
|
|
10
|
+
# : (untyped, Array[untyped]) -> Analysis
|
|
11
|
+
def call: (untyped, Array[untyped]) -> Analysis
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
@activity: bool
|
|
16
|
+
|
|
17
|
+
@activity_snapshotter: untyped
|
|
18
|
+
|
|
19
|
+
@preparer: untyped
|
|
20
|
+
|
|
21
|
+
@timeline_snapshotter: untyped
|
|
22
|
+
|
|
23
|
+
@tree: AssociationTree
|
|
24
|
+
|
|
25
|
+
# : (untyped, Array[untyped]) -> Analysis
|
|
26
|
+
def activity_analysis: (untyped, Array[untyped]) -> Analysis
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Generated from lib/paper_trail_diff/batched_root_versions.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module PaperTrailDiff
|
|
4
|
+
# Selects each root's versions for a batched range in a fixed number of
|
|
5
|
+
# queries. Only the range forms that mean the same thing for every root are
|
|
6
|
+
# supported: a shared wall-clock window, or each root's own whole history.
|
|
7
|
+
class BatchedRootVersions
|
|
8
|
+
# : (Array[untyped], time_range: TimeRange?) -> void
|
|
9
|
+
def initialize: (Array[untyped], time_range: TimeRange?) -> void
|
|
10
|
+
|
|
11
|
+
# Returns root versions per record identity, in chronological order.
|
|
12
|
+
# : () -> Hash[Array[String], Array[untyped]]
|
|
13
|
+
def call: () -> Hash[Array[String], Array[untyped]]
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
@records: Array[untyped]
|
|
18
|
+
|
|
19
|
+
@time_range: TimeRange?
|
|
20
|
+
|
|
21
|
+
# : (untyped, Array[untyped], Hash[Array[String], Array[untyped]]) -> void
|
|
22
|
+
def select_model: (untyped, Array[untyped], Hash[Array[String], Array[untyped]]) -> void
|
|
23
|
+
|
|
24
|
+
# The window's own versions plus, when it has one, the later version needed
|
|
25
|
+
# to reveal the last mutation inside it. A window closing on the root's own
|
|
26
|
+
# destruction needs no later version, because none can exist.
|
|
27
|
+
# : (Array[untyped], untyped) -> Array[untyped]
|
|
28
|
+
def versions_for: (Array[untyped], untyped) -> Array[untyped]
|
|
29
|
+
|
|
30
|
+
# : (Array[untyped]) -> bool
|
|
31
|
+
def terminal_destroy?: (Array[untyped]) -> bool
|
|
32
|
+
|
|
33
|
+
# : (untyped, Array[untyped], TimeRange?) -> Hash[String, Array[untyped]]
|
|
34
|
+
def grouped_versions: (untyped, Array[untyped], TimeRange?) -> Hash[String, Array[untyped]]
|
|
35
|
+
|
|
36
|
+
# One row per root: the earliest version after the window, found without a
|
|
37
|
+
# window function so the query stays portable.
|
|
38
|
+
# : (untyped, Array[untyped], TimeRange) -> Hash[String, untyped]
|
|
39
|
+
def trailing_versions: (untyped, Array[untyped], TimeRange) -> Hash[String, untyped]
|
|
40
|
+
|
|
41
|
+
# : (untyped, TimeRange) -> untyped
|
|
42
|
+
def no_earlier_trailing: (untyped, TimeRange) -> untyped
|
|
43
|
+
|
|
44
|
+
# Mirrors the window's own end handling, so a version sitting exactly on an
|
|
45
|
+
# inclusive boundary stays inside the window instead of masking the trailing
|
|
46
|
+
# version that follows it.
|
|
47
|
+
# : (untyped, untyped, TimeRange) -> untyped
|
|
48
|
+
def preceding: (untyped, untyped, TimeRange) -> untyped
|
|
49
|
+
|
|
50
|
+
# : (untyped, TimeRange) -> untyped
|
|
51
|
+
def after_window: (untyped, TimeRange) -> untyped
|
|
52
|
+
|
|
53
|
+
# : (untyped, Array[untyped]) -> untyped
|
|
54
|
+
def base_scope: (untyped, Array[untyped]) -> untyped
|
|
55
|
+
|
|
56
|
+
# : (untyped) -> Array[untyped]
|
|
57
|
+
def ordered: (untyped) -> Array[untyped]
|
|
58
|
+
|
|
59
|
+
# : (untyped, untyped) -> Array[String]
|
|
60
|
+
def identity: (untyped, untyped) -> Array[String]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -23,6 +23,21 @@ module PaperTrailDiff
|
|
|
23
23
|
|
|
24
24
|
@preparer: untyped
|
|
25
25
|
|
|
26
|
+
# : (Array[[untyped, untyped]]) -> Hash[identity, RecordSnapshot]
|
|
27
|
+
def prepared_live_snapshots: (Array[[ untyped, untyped ]]) -> Hash[identity, RecordSnapshot]
|
|
28
|
+
|
|
29
|
+
# : (untyped) -> bool
|
|
30
|
+
def symbolic?: (untyped) -> bool
|
|
31
|
+
|
|
32
|
+
# A boundary symbol that stayed unresolved means the root has no recorded
|
|
33
|
+
# history. An absent history is an empty result rather than a failed
|
|
34
|
+
# request, matching how the timeline APIs answer the same question.
|
|
35
|
+
# : (Array[[untyped, untyped]], Hash[identity, RecordSnapshot]) -> comparison_results
|
|
36
|
+
def results: (Array[[ untyped, untyped ]], Hash[identity, RecordSnapshot]) -> comparison_results
|
|
37
|
+
|
|
38
|
+
# : (untyped, untyped) -> identity
|
|
39
|
+
def entry_identity: (untyped, untyped) -> identity
|
|
40
|
+
|
|
26
41
|
# : (untyped, untyped, Hash[identity, RecordSnapshot]) -> Diff
|
|
27
42
|
def compare: (untyped, untyped, Hash[identity, RecordSnapshot]) -> Diff
|
|
28
43
|
|
|
@@ -9,7 +9,6 @@ module PaperTrailDiff
|
|
|
9
9
|
# : (untyped, Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
|
|
10
10
|
def prepare: (untyped, Array[untyped], ?start_at: untyped, ?end_at: untyped) -> void
|
|
11
11
|
|
|
12
|
-
# Prepares selected history for several roots of the same model class.
|
|
13
12
|
# : (Array[untyped], Array[untyped]) -> void
|
|
14
13
|
def prepare_batch: (Array[untyped], Array[untyped]) -> void
|
|
15
14
|
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# Generated from lib/paper_trail_diff/paper_trail_adapter.rb with RBS::Inline
|
|
2
2
|
|
|
3
3
|
module PaperTrailDiff
|
|
4
|
-
# PaperTrail/ActiveRecord boundary that produces plain record snapshots.
|
|
4
|
+
# PaperTrail/ActiveRecord boundary that produces plain record snapshots. It is
|
|
5
|
+
# deliberately the widest class here: it fronts every public operation and is
|
|
6
|
+
# the only place allowed to know about both PaperTrail and the pure engine.
|
|
7
|
+
# Reconstruction logic lives in the collaborators it wires together.
|
|
5
8
|
class PaperTrailAdapter
|
|
6
9
|
# : (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
|
|
7
10
|
def initialize: (associations: Array[String | Symbol], ignore: ignore_option, ?reload_live_endpoints: bool) -> void
|
|
@@ -22,6 +25,10 @@ module PaperTrailDiff
|
|
|
22
25
|
# : (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool) -> Analysis
|
|
23
26
|
def analyze: (untyped, from: untyped, to: untyped, within: untyped, ?activity: bool) -> Analysis
|
|
24
27
|
|
|
28
|
+
# Analyzes many roots over one shared range, preparing their history once.
|
|
29
|
+
# : (Array[untyped], within: untyped, ?activity: bool) -> Hash[identity, Analysis]
|
|
30
|
+
def analyze_many: (Array[untyped], within: untyped, ?activity: bool) -> Hash[identity, Analysis]
|
|
31
|
+
|
|
25
32
|
private
|
|
26
33
|
|
|
27
34
|
@activity_snapshotter: ActivitySnapshotProvider
|
|
@@ -44,6 +51,14 @@ module PaperTrailDiff
|
|
|
44
51
|
|
|
45
52
|
@traversal: AssociationTraversal
|
|
46
53
|
|
|
54
|
+
@traversal_preparer: TraversalPreparer
|
|
55
|
+
|
|
56
|
+
# : () -> void
|
|
57
|
+
def build_snapshotters: () -> void
|
|
58
|
+
|
|
59
|
+
# : (bool) -> BatchedRootAnalyzer
|
|
60
|
+
def batched_root_analyzer: (bool) -> BatchedRootAnalyzer
|
|
61
|
+
|
|
47
62
|
# : () -> HistoricalSnapshotStore
|
|
48
63
|
def build_historical_store: () -> HistoricalSnapshotStore
|
|
49
64
|
|
|
@@ -73,11 +88,5 @@ module PaperTrailDiff
|
|
|
73
88
|
|
|
74
89
|
# : (untyped) -> RecordSnapshot
|
|
75
90
|
def normalize_live_snapshot: (untyped) -> RecordSnapshot
|
|
76
|
-
|
|
77
|
-
# : (untyped, historical: bool) -> void
|
|
78
|
-
def prepare_traversal!: (untyped, historical: bool) -> void
|
|
79
|
-
|
|
80
|
-
# : () -> void
|
|
81
|
-
def ensure_association_tracking!: () -> void
|
|
82
91
|
end
|
|
83
92
|
end
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module PaperTrailDiff
|
|
4
4
|
# Selects and compares a chronological slice of a record's version history.
|
|
5
5
|
class TimelineBuilder
|
|
6
|
-
# : (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped) -> void
|
|
7
|
-
def initialize: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped) -> void
|
|
6
|
+
# : (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?versions: Array[untyped]?) -> void
|
|
7
|
+
def initialize: (untyped, from: untyped, to: untyped, snapshotter: untyped, ?within: untyped, ?versions: Array[untyped]?) -> void
|
|
8
8
|
|
|
9
9
|
# : () -> Array[Step]
|
|
10
10
|
def build: () -> Array[Step]
|
|
@@ -11,14 +11,16 @@ module PaperTrailDiff
|
|
|
11
11
|
|
|
12
12
|
attr_reader time_range: TimeRange?
|
|
13
13
|
|
|
14
|
-
# : (untyped, from: untyped, to: untyped, within: untyped) -> void
|
|
15
|
-
def initialize: (untyped, from: untyped, to: untyped, within: untyped) -> void
|
|
14
|
+
# : (untyped, from: untyped, to: untyped, within: untyped, ?versions: Array[untyped]?) -> void
|
|
15
|
+
def initialize: (untyped, from: untyped, to: untyped, within: untyped, ?versions: Array[untyped]?) -> void
|
|
16
16
|
|
|
17
17
|
# A record with no versions has no first or last boundary to resolve, which
|
|
18
18
|
# is an empty history rather than a bad request.
|
|
19
19
|
# : () -> bool
|
|
20
20
|
def unresolved?: () -> bool
|
|
21
21
|
|
|
22
|
+
# A batch may have selected these versions already, in which case reselecting
|
|
23
|
+
# them per record would undo the batching.
|
|
22
24
|
# : (?context_required: bool) -> Array[untyped]
|
|
23
25
|
def select: (?context_required: bool) -> Array[untyped]
|
|
24
26
|
|
|
@@ -43,6 +45,8 @@ module PaperTrailDiff
|
|
|
43
45
|
|
|
44
46
|
@to: untyped
|
|
45
47
|
|
|
48
|
+
@versions: Array[untyped]?
|
|
49
|
+
|
|
46
50
|
# : () -> Array[untyped]
|
|
47
51
|
def empty_versions: () -> Array[untyped]
|
|
48
52
|
|