paper_trail_diff 0.13.1 → 0.14.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 +287 -0
- data/QUICKSTART.md +9 -6
- data/README.md +84 -9
- data/docs/architecture.md +32 -9
- data/docs/performance.md +106 -13
- data/docs/reference.md +242 -29
- data/docs/testing.md +34 -11
- data/lib/paper_trail_diff/activity_belongs_to_event_applier.rb +19 -9
- data/lib/paper_trail_diff/activity_collection_event_applier.rb +11 -1
- data/lib/paper_trail_diff/activity_collection_record_updater.rb +16 -2
- data/lib/paper_trail_diff/activity_event_record_resolver.rb +20 -1
- data/lib/paper_trail_diff/activity_event_route_finder.rb +2 -1
- data/lib/paper_trail_diff/activity_event_snapshot_refresher.rb +1 -1
- data/lib/paper_trail_diff/activity_range.rb +14 -2
- data/lib/paper_trail_diff/activity_relationship.rb +12 -0
- data/lib/paper_trail_diff/activity_root_snapshot_refresher.rb +5 -2
- data/lib/paper_trail_diff/activity_target_candidate_loader.rb +131 -0
- data/lib/paper_trail_diff/activity_timeline_builder.rb +30 -24
- data/lib/paper_trail_diff/activity_version_collector.rb +31 -20
- data/lib/paper_trail_diff/association_activity_edge.rb +236 -0
- data/lib/paper_trail_diff/association_activity_selection.rb +197 -0
- data/lib/paper_trail_diff/association_discovery.rb +8 -13
- data/lib/paper_trail_diff/association_traversal.rb +169 -45
- data/lib/paper_trail_diff/batch_safety.rb +23 -15
- data/lib/paper_trail_diff/batched_root_versions.rb +21 -39
- data/lib/paper_trail_diff/comparison_batch.rb +37 -10
- data/lib/paper_trail_diff/deleted_analysis_batch.rb +34 -16
- data/lib/paper_trail_diff/diagnostics.rb +26 -8
- data/lib/paper_trail_diff/direct_record_state_loader.rb +10 -1
- data/lib/paper_trail_diff/errors.rb +2 -1
- data/lib/paper_trail_diff/habtm_transaction_owners.rb +68 -0
- data/lib/paper_trail_diff/historical_association_reifier.rb +32 -2
- data/lib/paper_trail_diff/historical_snapshot_store.rb +6 -9
- data/lib/paper_trail_diff/live_endpoint_batch_loader.rb +35 -2
- data/lib/paper_trail_diff/live_graph_collector.rb +1 -1
- data/lib/paper_trail_diff/paper_trail_adapter.rb +22 -6
- data/lib/paper_trail_diff/preloaded_endpoint_batch_loader.rb +1 -1
- data/lib/paper_trail_diff/prepared_activity_index.rb +110 -36
- data/lib/paper_trail_diff/prepared_edge_loader.rb +29 -45
- data/lib/paper_trail_diff/prepared_history.rb +63 -54
- data/lib/paper_trail_diff/prepared_history_loader.rb +33 -43
- data/lib/paper_trail_diff/prepared_record_index.rb +26 -0
- data/lib/paper_trail_diff/prepared_snapshot_builder.rb +1 -1
- data/lib/paper_trail_diff/prepared_state_association_reader.rb +119 -29
- data/lib/paper_trail_diff/recorded_changes.rb +83 -0
- data/lib/paper_trail_diff/recorded_class.rb +53 -0
- data/lib/paper_trail_diff/scope_condition.rb +331 -0
- data/lib/paper_trail_diff/scope_ranking.rb +196 -0
- data/lib/paper_trail_diff/scoped_analysis_builder.rb +4 -3
- data/lib/paper_trail_diff/scoped_root_selection.rb +93 -43
- data/lib/paper_trail_diff/snapshot_normalizer.rb +10 -3
- data/lib/paper_trail_diff/support.rb +9 -4
- data/lib/paper_trail_diff/time_activity_timeline_builder.rb +0 -1
- data/lib/paper_trail_diff/time_range.rb +9 -12
- data/lib/paper_trail_diff/time_version_range.rb +5 -5
- data/lib/paper_trail_diff/version.rb +1 -1
- data/lib/paper_trail_diff/version_association_candidate_scope.rb +13 -3
- data/lib/paper_trail_diff/version_range.rb +8 -15
- data/lib/paper_trail_diff/version_scope_filter.rb +11 -3
- data/lib/paper_trail_diff.rb +31 -2
- data/sig/generated/paper_trail_diff/activity_belongs_to_event_applier.rbs +10 -2
- data/sig/generated/paper_trail_diff/activity_collection_event_applier.rbs +5 -0
- data/sig/generated/paper_trail_diff/activity_collection_record_updater.rbs +5 -0
- data/sig/generated/paper_trail_diff/activity_event_record_resolver.rbs +7 -0
- data/sig/generated/paper_trail_diff/activity_range.rbs +6 -2
- data/sig/generated/paper_trail_diff/activity_relationship.rbs +3 -0
- data/sig/generated/paper_trail_diff/activity_root_snapshot_refresher.rbs +2 -0
- data/sig/generated/paper_trail_diff/activity_target_candidate_loader.rbs +79 -0
- data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +11 -5
- data/sig/generated/paper_trail_diff/activity_version_collector.rbs +10 -4
- data/sig/generated/paper_trail_diff/association_activity_edge.rbs +148 -0
- data/sig/generated/paper_trail_diff/association_activity_selection.rbs +112 -0
- data/sig/generated/paper_trail_diff/association_discovery.rbs +3 -5
- data/sig/generated/paper_trail_diff/association_traversal.rbs +84 -9
- data/sig/generated/paper_trail_diff/batch_safety.rbs +10 -4
- data/sig/generated/paper_trail_diff/batched_root_versions.rbs +2 -8
- data/sig/generated/paper_trail_diff/comparison_batch.rbs +22 -4
- data/sig/generated/paper_trail_diff/deleted_analysis_batch.rbs +16 -4
- data/sig/generated/paper_trail_diff/diagnostics.rbs +7 -2
- data/sig/generated/paper_trail_diff/direct_record_state_loader.rbs +6 -0
- data/sig/generated/paper_trail_diff/errors.rbs +2 -1
- data/sig/generated/paper_trail_diff/habtm_transaction_owners.rbs +40 -0
- data/sig/generated/paper_trail_diff/historical_association_reifier.rbs +13 -0
- data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +3 -2
- data/sig/generated/paper_trail_diff/live_endpoint_batch_loader.rbs +17 -0
- data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +5 -0
- data/sig/generated/paper_trail_diff/prepared_activity_index.rbs +52 -10
- data/sig/generated/paper_trail_diff/prepared_edge_loader.rbs +4 -12
- data/sig/generated/paper_trail_diff/prepared_history.rbs +33 -22
- data/sig/generated/paper_trail_diff/prepared_history_loader.rbs +15 -13
- data/sig/generated/paper_trail_diff/prepared_record_index.rbs +15 -0
- data/sig/generated/paper_trail_diff/prepared_state_association_reader.rbs +56 -11
- data/sig/generated/paper_trail_diff/recorded_changes.rbs +47 -0
- data/sig/generated/paper_trail_diff/recorded_class.rbs +22 -0
- data/sig/generated/paper_trail_diff/scope_condition.rbs +185 -0
- data/sig/generated/paper_trail_diff/scope_ranking.rbs +117 -0
- data/sig/generated/paper_trail_diff/scoped_analysis_builder.rbs +2 -2
- data/sig/generated/paper_trail_diff/scoped_root_selection.rbs +41 -7
- data/sig/generated/paper_trail_diff/snapshot_normalizer.rbs +6 -0
- data/sig/generated/paper_trail_diff/support.rbs +5 -2
- data/sig/generated/paper_trail_diff/time_range.rbs +4 -0
- data/sig/generated/paper_trail_diff/time_version_range.rbs +4 -4
- data/sig/generated/paper_trail_diff/version_association_candidate_scope.rbs +7 -0
- data/sig/generated/paper_trail_diff/version_range.rbs +4 -2
- data/sig/generated/paper_trail_diff/version_scope_filter.rbs +4 -0
- data/sig/generated/paper_trail_diff.rbs +21 -2
- metadata +20 -4
|
@@ -4,25 +4,21 @@
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
5
|
# Loads every historical identity that can participate in one association edge.
|
|
6
6
|
class PreparedEdgeLoader
|
|
7
|
-
EDGE_LOADERS = {
|
|
8
|
-
belongs_to: :belongs_to_edge,
|
|
9
|
-
has_one: :child_edge,
|
|
10
|
-
has_many: :child_edge,
|
|
11
|
-
has_and_belongs_to_many: :habtm_edge
|
|
12
|
-
}.freeze
|
|
13
|
-
private_constant :EDGE_LOADERS
|
|
14
|
-
|
|
15
7
|
#: (PreparedRecordIndex, Array[untyped], ?start_at: untyped) -> void
|
|
16
8
|
def initialize(records, root_versions, start_at: root_versions.first&.created_at)
|
|
17
9
|
@records = records
|
|
18
|
-
@transaction_ids = root_versions.filter_map(&:transaction_id)
|
|
10
|
+
@transaction_ids = root_versions.filter_map(&:transaction_id)
|
|
19
11
|
@start_at = start_at
|
|
20
12
|
end
|
|
21
13
|
|
|
14
|
+
# HABTM alone also returns each root transaction's members.
|
|
22
15
|
#: (untyped, Array[untyped], untyped) -> [Hash[String, Hash[Symbol, untyped]], Hash[String, Array[untyped]]?]
|
|
23
16
|
def call(owner_class, owner_ids, reflection)
|
|
24
|
-
|
|
25
|
-
|
|
17
|
+
case reflection.macro
|
|
18
|
+
when :belongs_to then [belongs_to_groups(owner_class, owner_ids, reflection), nil]
|
|
19
|
+
when :has_and_belongs_to_many then habtm_groups(owner_class, reflection)
|
|
20
|
+
else [child_groups(owner_class, owner_ids, reflection), nil]
|
|
21
|
+
end
|
|
26
22
|
end
|
|
27
23
|
|
|
28
24
|
private
|
|
@@ -31,21 +27,6 @@ module PaperTrailDiff
|
|
|
31
27
|
# @rbs @transaction_ids: Array[untyped]
|
|
32
28
|
# @rbs @start_at: untyped
|
|
33
29
|
|
|
34
|
-
#: (untyped, Array[untyped], untyped) -> [Hash[String, Hash[Symbol, untyped]], nil]
|
|
35
|
-
def belongs_to_edge(owner_class, owner_ids, reflection)
|
|
36
|
-
[belongs_to_groups(owner_class, owner_ids, reflection), nil]
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
#: (untyped, Array[untyped], untyped) -> [Hash[String, Hash[Symbol, untyped]], nil]
|
|
40
|
-
def child_edge(owner_class, owner_ids, reflection)
|
|
41
|
-
[child_groups(owner_class, owner_ids, reflection), nil]
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
#: (untyped, Array[untyped], untyped) -> [Hash[String, Hash[Symbol, untyped]], Hash[String, Array[untyped]]]
|
|
45
|
-
def habtm_edge(owner_class, _owner_ids, reflection)
|
|
46
|
-
habtm_groups(owner_class, reflection)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
30
|
#: (untyped, Array[untyped], untyped) -> Hash[String, Hash[Symbol, untyped]]
|
|
50
31
|
def belongs_to_groups(owner_class, owner_ids, reflection)
|
|
51
32
|
key = scalar_foreign_key(reflection)
|
|
@@ -56,9 +37,12 @@ module PaperTrailDiff
|
|
|
56
37
|
end
|
|
57
38
|
|
|
58
39
|
grouped = {} #: Hash[String, Hash[Symbol, untyped]]
|
|
40
|
+
# Grouping only decides what to preload, so a type that names no class is
|
|
41
|
+
# left to raise where that state is read.
|
|
59
42
|
@records.records_for(owner_class, owner_ids).each do |owner|
|
|
60
43
|
id = owner.public_send(key)
|
|
61
|
-
|
|
44
|
+
type = owner.public_send(reflection.foreign_type).to_s
|
|
45
|
+
model_class = AssociationTraversal.named_class(type)
|
|
62
46
|
add_identity(grouped, model_class, id) if model_class && id
|
|
63
47
|
end
|
|
64
48
|
grouped
|
|
@@ -75,28 +59,31 @@ module PaperTrailDiff
|
|
|
75
59
|
group_for(reflection.klass, owners.values.flatten, owners: owners)
|
|
76
60
|
end
|
|
77
61
|
|
|
62
|
+
# A transaction more than one owner recorded members in is left out, so
|
|
63
|
+
# its read falls back to HistoricalAssociationReifier, which raises.
|
|
78
64
|
#: (untyped, untyped) -> [Hash[String, Hash[Symbol, untyped]], Hash[String, Array[untyped]]]
|
|
79
65
|
def habtm_groups(owner_class, reflection)
|
|
80
66
|
rows = habtm_rows(owner_class, reflection)
|
|
67
|
+
shared = HabtmTransactionOwners.new(owner_class, reflection).shared(rows.map(&:version_id))
|
|
81
68
|
memberships = @transaction_ids.to_h do |transaction_id|
|
|
82
69
|
[transaction_id.to_s, membership_ids(rows, transaction_id)]
|
|
83
70
|
end
|
|
84
|
-
[group_for(reflection.klass, rows.
|
|
71
|
+
[group_for(reflection.klass, rows.map(&:foreign_key_id)), memberships.except(*shared)]
|
|
85
72
|
end
|
|
86
73
|
|
|
87
74
|
#: (untyped, untyped) -> Array[untyped]
|
|
88
75
|
def habtm_rows(owner_class, reflection)
|
|
89
76
|
owner_class.paper_trail.version_association_class.where(
|
|
90
77
|
version_id: @transaction_ids,
|
|
91
|
-
foreign_key_name: reflection.name
|
|
92
|
-
)
|
|
78
|
+
foreign_key_name: reflection.name
|
|
79
|
+
)
|
|
93
80
|
end
|
|
94
81
|
|
|
95
82
|
#: (Array[untyped], untyped) -> Array[untyped]
|
|
96
83
|
def membership_ids(rows, transaction_id)
|
|
97
84
|
rows.filter_map do |row|
|
|
98
85
|
row.foreign_key_id if row.version_id.to_s == transaction_id.to_s
|
|
99
|
-
end
|
|
86
|
+
end
|
|
100
87
|
end
|
|
101
88
|
|
|
102
89
|
#: (untyped, Array[untyped], untyped, String) -> Hash[String, Array[untyped]]
|
|
@@ -109,7 +96,7 @@ module PaperTrailDiff
|
|
|
109
96
|
version_class = owner_class.paper_trail.version_class
|
|
110
97
|
association_class = owner_class.paper_trail.version_association_class
|
|
111
98
|
relation = historical_child_relation(owner_class, owner_ids, reflection, foreign_key)
|
|
112
|
-
pairs = child_candidate_scope(version_class).call(relation).
|
|
99
|
+
pairs = child_candidate_scope(version_class).call(relation).pluck(
|
|
113
100
|
association_class.arel_table[:foreign_key_id],
|
|
114
101
|
version_class.arel_table[:item_id]
|
|
115
102
|
)
|
|
@@ -141,10 +128,10 @@ module PaperTrailDiff
|
|
|
141
128
|
|
|
142
129
|
#: (untyped, Array[untyped], untyped, String) -> Hash[String, Array[untyped]]
|
|
143
130
|
def live_child_ids(owner_class, owner_ids, reflection, foreign_key)
|
|
131
|
+
# Every row of the table, as history reads them: an STI subclass's
|
|
132
|
+
# siblings are left out where children are resolved.
|
|
144
133
|
relation = reflection.klass.base_class.unscoped.where(foreign_key => owner_ids)
|
|
145
|
-
if reflection.
|
|
146
|
-
relation = relation.where(reflection.type => parent_types(owner_class))
|
|
147
|
-
end
|
|
134
|
+
relation = relation.where(reflection.type => parent_types(owner_class)) if reflection.type
|
|
148
135
|
group_ids_by_owner(relation.pluck(foreign_key, reflection.klass.primary_key))
|
|
149
136
|
end
|
|
150
137
|
|
|
@@ -161,18 +148,15 @@ module PaperTrailDiff
|
|
|
161
148
|
|
|
162
149
|
group = { model: model_class, ids: unique } #: Hash[Symbol, untyped]
|
|
163
150
|
group[:owners] = owners if owners
|
|
164
|
-
{ model_class.name
|
|
151
|
+
{ model_class.name => group }
|
|
165
152
|
end
|
|
166
153
|
|
|
167
|
-
#
|
|
154
|
+
# A set rather than a scan, so that a wide edge stays linear in its width.
|
|
168
155
|
#: (Array[Array[untyped]]) -> Hash[String, Array[untyped]]
|
|
169
156
|
def group_ids_by_owner(pairs)
|
|
170
|
-
grouped = {} #: Hash[String,
|
|
171
|
-
pairs.each
|
|
172
|
-
|
|
173
|
-
ids[child_id] = true
|
|
174
|
-
end
|
|
175
|
-
grouped.transform_values(&:keys)
|
|
157
|
+
grouped = {} #: Hash[String, Set[untyped]]
|
|
158
|
+
pairs.each { |owner_id, child_id| (grouped[owner_id.to_s] ||= Set.new) << child_id }
|
|
159
|
+
grouped.transform_values(&:to_a)
|
|
176
160
|
end
|
|
177
161
|
|
|
178
162
|
#: (Hash[String, Array[untyped]], Hash[String, Array[untyped]]) -> Hash[String, Array[untyped]]
|
|
@@ -183,12 +167,12 @@ module PaperTrailDiff
|
|
|
183
167
|
#: (untyped) -> String?
|
|
184
168
|
def scalar_foreign_key(reflection)
|
|
185
169
|
key = reflection.foreign_key
|
|
186
|
-
key
|
|
170
|
+
key unless key.is_a?(Array)
|
|
187
171
|
end
|
|
188
172
|
|
|
189
173
|
#: (untyped) -> Array[String?]
|
|
190
174
|
def parent_types(owner_class)
|
|
191
|
-
[nil, '', owner_class.name
|
|
175
|
+
[nil, '', owner_class.name, owner_class.base_class.name]
|
|
192
176
|
end
|
|
193
177
|
end
|
|
194
178
|
end
|
|
@@ -17,14 +17,16 @@ module PaperTrailDiff
|
|
|
17
17
|
#: (PreparedRecordIndex, ?activity_index: PreparedActivityIndex?) -> void
|
|
18
18
|
def initialize(records, activity_index: nil)
|
|
19
19
|
@records = records
|
|
20
|
-
@
|
|
20
|
+
@conditions = ScopeCondition::Cache.new
|
|
21
|
+
@state_reader = PreparedStateAssociationReader.new(records, method(:edge), @conditions)
|
|
21
22
|
@activity_index = activity_index
|
|
22
23
|
@edges = {} #: Hash[Array[String], Hash[String, Hash[Symbol, untyped]]]
|
|
23
24
|
@unsupported = {} #: Hash[Array[String], bool]
|
|
24
25
|
@habtm = {} #: Hash[Array[String], Hash[String, Array[untyped]]]
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
# Returns every group the edge now holds.
|
|
29
|
+
#: (untyped, untyped, Hash[String, Hash[Symbol, untyped]]) -> Hash[String, Hash[Symbol, untyped]]
|
|
28
30
|
def merge_edge(owner_class, reflection, groups)
|
|
29
31
|
key = edge_key(owner_class, reflection)
|
|
30
32
|
empty = {} #: Hash[String, Hash[Symbol, untyped]]
|
|
@@ -62,14 +64,16 @@ module PaperTrailDiff
|
|
|
62
64
|
@state_reader.call(owner, reflection, boundary)
|
|
63
65
|
end
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
# Unranked, as a part of a has_many through is read, it returns every row
|
|
68
|
+
# the part's conditions admit, a record reached twice appearing twice.
|
|
69
|
+
#: (untyped, untyped, untyped, habtm_boundary: untyped, ?ranked: bool) -> [bool, Array[untyped]]
|
|
70
|
+
def resolve(record, reflection, boundary, habtm_boundary:, ranked: true)
|
|
71
|
+
return [false, []] unless supported?(record.class, reflection, ranked)
|
|
68
72
|
|
|
69
73
|
records = if reflection.options[:through]
|
|
70
|
-
through_records(record, reflection, boundary, habtm_boundary)
|
|
74
|
+
through_records(record, reflection, boundary, habtm_boundary, ranked)
|
|
71
75
|
else
|
|
72
|
-
direct_records(record, reflection, boundary, habtm_boundary)
|
|
76
|
+
direct_records(record, reflection, boundary, habtm_boundary, ranked)
|
|
73
77
|
end
|
|
74
78
|
records ? [true, records] : [false, []]
|
|
75
79
|
end
|
|
@@ -81,31 +85,34 @@ module PaperTrailDiff
|
|
|
81
85
|
# @rbs @edges: Hash[Array[String], Hash[String, Hash[Symbol, untyped]]]
|
|
82
86
|
# @rbs @unsupported: Hash[Array[String], bool]
|
|
83
87
|
# @rbs @habtm: Hash[Array[String], Hash[String, Array[untyped]]]
|
|
88
|
+
# @rbs @conditions: ScopeCondition::Cache
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
# A polymorphic target's class is known only once a record names it, so
|
|
91
|
+
# belongs_to_records reads its scope for each record instead.
|
|
92
|
+
#: (untyped, untyped, bool) -> bool
|
|
93
|
+
def supported?(owner_class, reflection, ranked)
|
|
87
94
|
!@unsupported[edge_key(owner_class, reflection)] &&
|
|
88
|
-
reflection.
|
|
95
|
+
(reflection.polymorphic? || !@conditions.call(reflection, ranked: ranked).nil?)
|
|
89
96
|
end
|
|
90
97
|
|
|
91
|
-
#: (untyped, untyped, untyped, untyped) -> Array[untyped]?
|
|
92
|
-
def direct_records(record, reflection, boundary, habtm_boundary)
|
|
98
|
+
#: (untyped, untyped, untyped, untyped, bool) -> Array[untyped]?
|
|
99
|
+
def direct_records(record, reflection, boundary, habtm_boundary, ranked)
|
|
93
100
|
resolver = DIRECT_RESOLVERS.fetch(reflection.macro)
|
|
94
|
-
send(resolver, record, reflection, boundary, habtm_boundary)
|
|
101
|
+
send(resolver, record, reflection, boundary, habtm_boundary, ranked)
|
|
95
102
|
end
|
|
96
103
|
|
|
97
|
-
#: (untyped, untyped, untyped, untyped) -> Array[untyped]?
|
|
98
|
-
def resolve_belongs_to(record, reflection, boundary, _habtm_boundary)
|
|
99
|
-
belongs_to_records(record, reflection, boundary)
|
|
104
|
+
#: (untyped, untyped, untyped, untyped, bool) -> Array[untyped]?
|
|
105
|
+
def resolve_belongs_to(record, reflection, boundary, _habtm_boundary, ranked)
|
|
106
|
+
belongs_to_records(record, reflection, boundary, ranked)
|
|
100
107
|
end
|
|
101
108
|
|
|
102
|
-
#: (untyped, untyped, untyped, untyped) -> Array[untyped]
|
|
103
|
-
def resolve_children(record, reflection, boundary, _habtm_boundary)
|
|
104
|
-
child_records(record, reflection, boundary)
|
|
109
|
+
#: (untyped, untyped, untyped, untyped, bool) -> Array[untyped]
|
|
110
|
+
def resolve_children(record, reflection, boundary, _habtm_boundary, ranked)
|
|
111
|
+
child_records(record, reflection, boundary, ranked)
|
|
105
112
|
end
|
|
106
113
|
|
|
107
|
-
#: (untyped, untyped, untyped) -> Array[untyped]?
|
|
108
|
-
def belongs_to_records(record, reflection, boundary)
|
|
114
|
+
#: (untyped, untyped, untyped, bool) -> Array[untyped]?
|
|
115
|
+
def belongs_to_records(record, reflection, boundary, ranked)
|
|
109
116
|
foreign_key = scalar_foreign_key(reflection)
|
|
110
117
|
return unless foreign_key
|
|
111
118
|
|
|
@@ -113,20 +120,23 @@ module PaperTrailDiff
|
|
|
113
120
|
return [] if id.nil?
|
|
114
121
|
|
|
115
122
|
model_class = AssociationTraversal.belongs_to_target(reflection, record)
|
|
116
|
-
return unless model_class && @
|
|
123
|
+
return unless model_class && @conditions.call(reflection, model_class, ranked: ranked) &&
|
|
124
|
+
@records.loaded?(model_class, id)
|
|
117
125
|
|
|
118
|
-
Array(@records.record_before(model_class, id, boundary)).compact
|
|
126
|
+
target = Array(@records.record_before(model_class, id, boundary)).compact
|
|
127
|
+
@conditions.select(reflection, target, model_class, ranked: ranked)
|
|
119
128
|
end
|
|
120
129
|
|
|
121
|
-
#: (untyped, untyped, untyped) -> Array[untyped]
|
|
122
|
-
def child_records(record, reflection, boundary)
|
|
123
|
-
edge(record.class, reflection).values.flat_map do |group|
|
|
130
|
+
#: (untyped, untyped, untyped, bool) -> Array[untyped]
|
|
131
|
+
def child_records(record, reflection, boundary, ranked)
|
|
132
|
+
children = edge(record.class, reflection).values.flat_map do |group|
|
|
124
133
|
model_class = group.fetch(:model)
|
|
125
134
|
child_ids_for(group, record).filter_map do |id|
|
|
126
135
|
child = @records.record_before(model_class, id, boundary)
|
|
127
136
|
child if child && member_of?(child, reflection, record)
|
|
128
137
|
end
|
|
129
138
|
end
|
|
139
|
+
@conditions.select(reflection, children, ranked: ranked)
|
|
130
140
|
end
|
|
131
141
|
|
|
132
142
|
#: (Hash[Symbol, untyped], untyped) -> Array[untyped]
|
|
@@ -136,8 +146,8 @@ module PaperTrailDiff
|
|
|
136
146
|
owners ? owners.fetch(owner.id.to_s, empty) : group.fetch(:ids)
|
|
137
147
|
end
|
|
138
148
|
|
|
139
|
-
#: (untyped, untyped, untyped, untyped) -> Array[untyped]?
|
|
140
|
-
def habtm_records(record, reflection, boundary, habtm_boundary)
|
|
149
|
+
#: (untyped, untyped, untyped, untyped, bool) -> Array[untyped]?
|
|
150
|
+
def habtm_records(record, reflection, boundary, habtm_boundary, ranked)
|
|
141
151
|
transaction_id = transaction_id_for(habtm_boundary)
|
|
142
152
|
return unless transaction_id
|
|
143
153
|
|
|
@@ -145,37 +155,35 @@ module PaperTrailDiff
|
|
|
145
155
|
ids = @habtm.fetch(edge_key(record.class, reflection), empty)[transaction_id.to_s]
|
|
146
156
|
return unless ids
|
|
147
157
|
|
|
148
|
-
ids.filter_map
|
|
149
|
-
|
|
150
|
-
end
|
|
158
|
+
members = ids.filter_map { |id| @records.record_before(reflection.klass, id, boundary) }
|
|
159
|
+
@conditions.select(reflection, members, ranked: ranked)
|
|
151
160
|
end
|
|
152
161
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
habtm_boundary: habtm_boundary
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return unless resolved.all?(&:first)
|
|
162
|
+
# A has_many through reads its parts unranked, as its query does. Its own
|
|
163
|
+
# scope applies to each row, and a ranked read then keeps a record reached
|
|
164
|
+
# twice as one member.
|
|
165
|
+
#: (untyped, untyped, untyped, untyped, bool) -> Array[untyped]?
|
|
166
|
+
def through_records(record, reflection, boundary, habtm_boundary, ranked)
|
|
167
|
+
read = lambda do |owner, part|
|
|
168
|
+
resolve(owner, part, boundary, habtm_boundary: habtm_boundary,
|
|
169
|
+
ranked: ranked && !reflection.collection?)
|
|
170
|
+
end
|
|
171
|
+
rows = through_rows(record, reflection, read)
|
|
172
|
+
return unless rows
|
|
165
173
|
|
|
166
|
-
|
|
174
|
+
rows = @conditions.select(reflection, rows, ranked: ranked)
|
|
175
|
+
ranked ? unique_records(rows) : rows
|
|
167
176
|
end
|
|
168
177
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
end
|
|
178
|
+
# The source records of each join record, once for each join.
|
|
179
|
+
#: (untyped, untyped, ^(untyped, untyped) -> [bool, Array[untyped]]) -> Array[untyped]?
|
|
180
|
+
def through_rows(record, reflection, read)
|
|
181
|
+
handled, joins = read.call(record, reflection.through_reflection)
|
|
182
|
+
return unless handled
|
|
183
|
+
|
|
184
|
+
joins = joins.select { |join| AssociationTraversal.source_type_join?(reflection, join) }
|
|
185
|
+
resolved = joins.map { |join| read.call(join, reflection.source_reflection) }
|
|
186
|
+
resolved.flat_map(&:last) if resolved.all?(&:first)
|
|
179
187
|
end
|
|
180
188
|
|
|
181
189
|
#: (untyped, untyped, untyped) -> bool
|
|
@@ -183,6 +191,7 @@ module PaperTrailDiff
|
|
|
183
191
|
foreign_key = scalar_foreign_key(reflection)
|
|
184
192
|
return false unless foreign_key
|
|
185
193
|
return false unless child.public_send(foreign_key).to_s == owner.id.to_s
|
|
194
|
+
return false unless AssociationTraversal.sti_member?(reflection.klass, child.attributes)
|
|
186
195
|
return true unless reflection.options[:as]
|
|
187
196
|
|
|
188
197
|
owner_types(reflection, owner).include?(child.public_send(reflection.type).to_s)
|
|
@@ -4,16 +4,17 @@
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
5
|
# Expands the explicit association tree into a request-scoped PreparedHistory.
|
|
6
6
|
class PreparedHistoryLoader
|
|
7
|
-
|
|
7
|
+
# Without `end_at`, the activity range closes on the last root version.
|
|
8
|
+
#: (untyped, root_ids: Array[untyped], root_versions: Array[untyped], tree: AssociationTree, traversal: AssociationTraversal, start_at: untyped, end_at: untyped, live_records: Array[untyped]) -> void
|
|
8
9
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
9
10
|
record,
|
|
11
|
+
root_ids:,
|
|
10
12
|
root_versions:,
|
|
11
13
|
tree:,
|
|
12
14
|
traversal:,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
live_records: []
|
|
15
|
+
start_at:,
|
|
16
|
+
end_at:,
|
|
17
|
+
live_records:
|
|
17
18
|
)
|
|
18
19
|
@record = record
|
|
19
20
|
@root_ids = root_ids
|
|
@@ -21,12 +22,12 @@ module PaperTrailDiff
|
|
|
21
22
|
@tree = tree
|
|
22
23
|
@traversal = traversal
|
|
23
24
|
@records = PreparedRecordIndex.new(start_at, end_at: end_at, live_records: live_records)
|
|
24
|
-
activity = PreparedActivityIndex.new(
|
|
25
|
+
@activity = PreparedActivityIndex.new(
|
|
25
26
|
@records, root_versions, start_at: start_at, end_at: end_at || root_versions.last.created_at
|
|
26
27
|
)
|
|
27
|
-
@history = PreparedHistory.new(@records, activity_index: activity)
|
|
28
|
+
@history = PreparedHistory.new(@records, activity_index: @activity)
|
|
28
29
|
@edges = PreparedEdgeLoader.new(@records, root_versions, start_at: start_at)
|
|
29
|
-
@prepared = {} #: Hash[
|
|
30
|
+
@prepared = {} #: Hash[untyped, Array[String]]
|
|
30
31
|
end
|
|
31
32
|
|
|
32
33
|
#: () -> PreparedHistory
|
|
@@ -44,20 +45,18 @@ module PaperTrailDiff
|
|
|
44
45
|
# @rbs @tree: AssociationTree
|
|
45
46
|
# @rbs @traversal: AssociationTraversal
|
|
46
47
|
# @rbs @records: PreparedRecordIndex
|
|
48
|
+
# @rbs @activity: PreparedActivityIndex
|
|
47
49
|
# @rbs @history: PreparedHistory
|
|
48
50
|
# @rbs @edges: PreparedEdgeLoader
|
|
49
|
-
# @rbs @prepared: Hash[
|
|
51
|
+
# @rbs @prepared: Hash[untyped, Array[String]]
|
|
50
52
|
|
|
51
53
|
#: (untyped, Array[untyped], AssociationTree, path: String) -> void
|
|
52
54
|
def load_node(model_class, ids, tree, path:)
|
|
53
|
-
@
|
|
55
|
+
@activity.register(model_class, ids)
|
|
54
56
|
@traversal.reflections_for(model_class, tree, path: path).each do |reflection|
|
|
55
|
-
subtree = tree.child(reflection.name)
|
|
56
|
-
next unless subtree
|
|
57
|
-
|
|
57
|
+
subtree = tree.child(reflection.name) #: AssociationTree
|
|
58
58
|
groups = prepare_reflection(model_class, ids, reflection)
|
|
59
|
-
|
|
60
|
-
child_path = Support.association_path(path, reflection.name.to_s)
|
|
59
|
+
child_path = Support.association_path(path, reflection.name)
|
|
61
60
|
groups.each_value do |group|
|
|
62
61
|
load_node(group.fetch(:model), group.fetch(:ids), subtree, path: child_path)
|
|
63
62
|
end
|
|
@@ -66,70 +65,66 @@ module PaperTrailDiff
|
|
|
66
65
|
|
|
67
66
|
#: (untyped, Array[untyped], untyped) -> Hash[String, Hash[Symbol, untyped]]
|
|
68
67
|
def prepare_reflection(owner_class, owner_ids, reflection)
|
|
69
|
-
return prepare_through(owner_class, owner_ids, reflection) if reflection.
|
|
68
|
+
return prepare_through(owner_class, owner_ids, reflection) if reflection.through_reflection?
|
|
70
69
|
|
|
71
70
|
prepare_edge(owner_class, owner_ids, reflection)
|
|
72
71
|
end
|
|
73
72
|
|
|
73
|
+
# Either part can itself be a through, which has no key of its own to
|
|
74
|
+
# read, so each part is prepared as what it is. Like an edge, it returns
|
|
75
|
+
# every group it holds, not only those of these owners.
|
|
74
76
|
#: (untyped, Array[untyped], untyped) -> Hash[String, Hash[Symbol, untyped]]
|
|
75
77
|
def prepare_through(owner_class, owner_ids, reflection)
|
|
76
|
-
through_groups =
|
|
78
|
+
through_groups = prepare_reflection(owner_class, owner_ids, reflection.through_reflection)
|
|
77
79
|
targets = {} #: Hash[String, Hash[Symbol, untyped]]
|
|
78
80
|
through_groups.each_value do |group|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
source = prepare_reflection(group.fetch(:model), group.fetch(:ids),
|
|
82
|
+
reflection.source_reflection)
|
|
81
83
|
targets = merge_groups(targets, source)
|
|
82
84
|
end
|
|
83
85
|
@history.merge_edge(owner_class, reflection, targets)
|
|
84
|
-
@history.mark_unsupported(owner_class, reflection) if deferred_macro?(reflection)
|
|
85
|
-
targets
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
#: (untyped, Array[untyped], untyped) -> Hash[String, Hash[Symbol, untyped]]
|
|
89
89
|
def prepare_edge(owner_class, owner_ids, reflection)
|
|
90
|
-
missing = unprepared_ids(
|
|
90
|
+
missing = unprepared_ids(owner_ids, reflection)
|
|
91
91
|
return @history.edge(owner_class, reflection) if missing.empty?
|
|
92
92
|
|
|
93
|
-
load_records(owner_class, missing) if reflection.macro == :belongs_to
|
|
94
93
|
groups, memberships = @edges.call(owner_class, missing, reflection)
|
|
95
94
|
@history.merge_edge(owner_class, reflection, groups)
|
|
96
95
|
@history.register_habtm(owner_class, reflection, memberships) if memberships
|
|
97
|
-
mark_prepared(
|
|
96
|
+
mark_prepared(missing, reflection)
|
|
98
97
|
mark_unsupported(owner_class, reflection, groups)
|
|
99
98
|
load_groups(groups)
|
|
100
99
|
@history.edge(owner_class, reflection)
|
|
101
100
|
end
|
|
102
101
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
# A reflection belongs to the class that declares it, so it alone names
|
|
103
|
+
# the edge; a subclass that inherits it reads the same records.
|
|
104
|
+
#: (Array[untyped], untyped) -> Array[untyped]
|
|
105
|
+
def unprepared_ids(ids, reflection)
|
|
106
|
+
prepared = @prepared.fetch(reflection, [])
|
|
106
107
|
ids.reject { |id| prepared.include?(id.to_s) }
|
|
107
108
|
end
|
|
108
109
|
|
|
109
|
-
#: (
|
|
110
|
-
def mark_prepared(
|
|
111
|
-
|
|
112
|
-
@prepared[key] = (@prepared.fetch(key, []) | ids.map(&:to_s))
|
|
110
|
+
#: (Array[untyped], untyped) -> void
|
|
111
|
+
def mark_prepared(ids, reflection)
|
|
112
|
+
@prepared[reflection] = (@prepared.fetch(reflection, []) | ids.map(&:to_s))
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
#: (untyped, untyped, Hash[String, Hash[Symbol, untyped]]) -> void
|
|
116
116
|
def mark_unsupported(owner_class, reflection, groups)
|
|
117
|
-
unsupported =
|
|
117
|
+
unsupported = reflection.foreign_key.is_a?(Array) ||
|
|
118
118
|
groups.values.any? { |group| !versioned?(group.fetch(:model)) }
|
|
119
119
|
@history.mark_unsupported(owner_class, reflection) if unsupported
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
#: (untyped) -> bool
|
|
123
|
-
def deferred_macro?(reflection)
|
|
124
|
-
reflection.options[:through] && !reflection.source_reflection.belongs_to?
|
|
125
|
-
end
|
|
126
|
-
|
|
127
122
|
#: (Hash[String, Hash[Symbol, untyped]]) -> void
|
|
128
123
|
def load_groups(groups)
|
|
129
124
|
groups.each_value do |group|
|
|
130
125
|
model = group.fetch(:model)
|
|
131
126
|
ids = group.fetch(:ids)
|
|
132
|
-
@
|
|
127
|
+
@activity.register(model, ids)
|
|
133
128
|
load_records(model, ids)
|
|
134
129
|
end
|
|
135
130
|
end
|
|
@@ -144,11 +139,6 @@ module PaperTrailDiff
|
|
|
144
139
|
Support.versioned?(model_class)
|
|
145
140
|
end
|
|
146
141
|
|
|
147
|
-
#: (untyped, untyped) -> Array[String]
|
|
148
|
-
def edge_key(owner_class, reflection)
|
|
149
|
-
[owner_class.base_class.name.to_s, reflection.name.to_s]
|
|
150
|
-
end
|
|
151
|
-
|
|
152
142
|
#: (Hash[String, Hash[Symbol, untyped]], Hash[String, Hash[Symbol, untyped]]) -> Hash[String, Hash[Symbol, untyped]]
|
|
153
143
|
def merge_groups(left, right)
|
|
154
144
|
Support.merge_record_groups(left, right)
|
|
@@ -163,6 +163,22 @@ module PaperTrailDiff
|
|
|
163
163
|
live ? [*recorded, live] : recorded
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
+
# The classes the record had at any point, or either side of one version.
|
|
167
|
+
#: (?untyped) -> Array[untyped]
|
|
168
|
+
def model_classes(version = nil)
|
|
169
|
+
return states.map(&:model_class) unless version
|
|
170
|
+
|
|
171
|
+
states_around(version).map(&:model_class)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The states either side of one version; a create has none before it and a
|
|
175
|
+
# destroy none after.
|
|
176
|
+
#: (untyped) -> Array[PreparedRecordState]
|
|
177
|
+
def states_around(version)
|
|
178
|
+
index = @version_positions.fetch(version.id.to_s)
|
|
179
|
+
[state_for(version), state_after(index)].compact
|
|
180
|
+
end
|
|
181
|
+
|
|
166
182
|
#: (untyped) -> [Hash[untyped, untyped], Hash[untyped, untyped]]?
|
|
167
183
|
def transition(version)
|
|
168
184
|
index = @version_positions[version.id.to_s]
|
|
@@ -337,6 +353,16 @@ module PaperTrailDiff
|
|
|
337
353
|
@series.fetch(identity(model_class, id)).versions
|
|
338
354
|
end
|
|
339
355
|
|
|
356
|
+
#: (untyped, untyped, ?untyped) -> Array[untyped]
|
|
357
|
+
def model_classes(model_class, id, version = nil)
|
|
358
|
+
@series.fetch(identity(model_class, id)).model_classes(version)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
#: (untyped, untyped, untyped) -> Array[PreparedRecordState]
|
|
362
|
+
def states_around(model_class, id, version)
|
|
363
|
+
@series.fetch(identity(model_class, id)).states_around(version)
|
|
364
|
+
end
|
|
365
|
+
|
|
340
366
|
#: (untyped, untyped) -> bool
|
|
341
367
|
def loaded?(model_class, id)
|
|
342
368
|
@series.key?(identity(model_class, id))
|
|
@@ -49,7 +49,7 @@ module PaperTrailDiff
|
|
|
49
49
|
subtree = tree.child(reflection.name)
|
|
50
50
|
return [false, nil] unless subtree
|
|
51
51
|
|
|
52
|
-
child_path = Support.association_path(path, reflection.name
|
|
52
|
+
child_path = Support.association_path(path, reflection.name)
|
|
53
53
|
records = [] #: Array[RecordSnapshot]
|
|
54
54
|
states.each do |state|
|
|
55
55
|
supported, snapshot = build(state, boundary, subtree, normalizer, history,
|