active_snapshot 1.0.0 → 1.1.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 +9 -1
- data/README.md +15 -10
- data/lib/active_snapshot/models/concerns/snapshots_concern.rb +10 -27
- data/lib/active_snapshot/models/snapshot.rb +113 -1
- data/lib/active_snapshot/version.rb +1 -1
- data/lib/generators/active_snapshot/install/templates/create_snapshots_tables.rb.erb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 166c7d2f5b7e2595314eab22bd3fc4e8de2b28c7f5c6deb8b7a909e45e3061bb
|
|
4
|
+
data.tar.gz: daad374af6af17a0c14f0726418a72eb22af6d8366429572f70a3658c7a2168a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6c648c31574aca219686d0ba73ceb44c074fc6787dfe0f3df3197bdf047604cee57938eecc3599168f35676d3f4230582114857a7d0d5c6db7e962afbf57b499
|
|
7
|
+
data.tar.gz: e4904b1ed1f105cda6a60cb2cc243f35c03509e04b399b4412f05228c122f04aa94e32500c7af8eb68d76263bf0f41b1e49273af6d497b4ccc26f1c093cb8f05
|
data/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,17 @@ CHANGELOG
|
|
|
2
2
|
---------
|
|
3
3
|
|
|
4
4
|
- **Unreleased**
|
|
5
|
-
* [View Diff](https://github.com/westonganger/active_snapshot/compare/v1.
|
|
5
|
+
* [View Diff](https://github.com/westonganger/active_snapshot/compare/v1.1.0...master)
|
|
6
6
|
* Nothing yet
|
|
7
7
|
|
|
8
|
+
- **v1.1.0** - Dec 28 2025
|
|
9
|
+
* [View Diff](https://github.com/westonganger/active_snapshot/compare/v1.0.0...v1.1.0)
|
|
10
|
+
* [#77](https://github.com/westonganger/active_snapshot/pull/77) - Remove uniqueness constraint from `snapshot_items` table migration
|
|
11
|
+
- Upgrade Instructions: Create a DB migration with `remove_index :snapshot_items, [:snapshot_id, :item_id, :item_type], unique: true`
|
|
12
|
+
* [#76](https://github.com/westonganger/active_snapshot/pull/76) - Add full STI support (inherit snapshot children definition from base class, and allow overriding in STI child classes)
|
|
13
|
+
* [#74](https://github.com/westonganger/active_snapshot/pull/74) - Ensure no exception is raised when class does not have method defined_enums
|
|
14
|
+
* [#72](https://github.com/westonganger/active_snapshot/pull/72) - Adds `ActiveSnapshot::Snapshot.diff(from, to)` to get the difference between two snapshots or a snapshot and the current record.
|
|
15
|
+
|
|
8
16
|
- **v1.0.0** - Jan 17 2025
|
|
9
17
|
* [View Diff](https://github.com/westonganger/active_snapshot/compare/v0.5.2...v1.0.0)
|
|
10
18
|
* There are no functional changes. This release v1.0.0 is to signal that its stable and ready for widespread usage.
|
data/README.md
CHANGED
|
@@ -134,19 +134,24 @@ reified_children_hash.first.instance_variable_set("@readonly", false)
|
|
|
134
134
|
|
|
135
135
|
# Diffing Versions
|
|
136
136
|
|
|
137
|
-
You can
|
|
138
|
-
|
|
137
|
+
You can obtain the diff between two snapshots like this:
|
|
139
138
|
```ruby
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
from = post.snapshots.first
|
|
140
|
+
to = post.snapshots.second
|
|
141
|
+
|
|
142
|
+
ActiveSnapshot::Snapshot.diff(from, to)
|
|
143
|
+
# [
|
|
144
|
+
# {action: :update, item_type: "Post", item_id: 1, changes: {name: ["Old Name", "New Name"]}},
|
|
145
|
+
# {action: :destroy, item_type: "Comment", item_id: 1, changes: {id: [1, nil], content: ["Some Content", nil]}},
|
|
146
|
+
# {action: :create, item_type: "Comment", item_id: 2, changes: {id: [nil, 1], content: [nil, "New Content"]}}
|
|
147
|
+
# ]
|
|
148
|
+
```
|
|
146
149
|
|
|
147
|
-
|
|
150
|
+
You can also obtain the diff between a snapshot and the current record:
|
|
151
|
+
```ruby
|
|
152
|
+
from = post.snapshots.last
|
|
148
153
|
|
|
149
|
-
|
|
154
|
+
ActiveSnapshot::Snapshot.diff(from, post)
|
|
150
155
|
```
|
|
151
156
|
|
|
152
157
|
# Important Data Considerations / Warnings
|
|
@@ -6,35 +6,19 @@ module ActiveSnapshot
|
|
|
6
6
|
### We do NOT mark these as dependent: :destroy, the developer must manually destroy the snapshots or individual snapshot items
|
|
7
7
|
has_many :snapshots, as: :item, class_name: 'ActiveSnapshot::Snapshot'
|
|
8
8
|
has_many :snapshot_items, as: :item, class_name: 'ActiveSnapshot::SnapshotItem'
|
|
9
|
+
|
|
10
|
+
class_attribute :snapshot_children_proc
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
def create_snapshot!(identifier: nil, user: nil, metadata: nil)
|
|
12
|
-
snapshot =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
metadata: (metadata || {}),
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
new_entries = []
|
|
20
|
-
|
|
21
|
-
current_time = Time.now
|
|
22
|
-
|
|
23
|
-
new_entries << snapshot.build_snapshot_item(self).attributes.merge(created_at: current_time)
|
|
24
|
-
|
|
25
|
-
snapshot_children = self.children_to_snapshot
|
|
26
|
-
|
|
27
|
-
if snapshot_children
|
|
28
|
-
snapshot_children.each do |child_group_name, h|
|
|
29
|
-
h[:records].each do |child_item|
|
|
30
|
-
new_entries << snapshot.build_snapshot_item(child_item, child_group_name: child_group_name).attributes.merge(created_at: current_time)
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
14
|
+
snapshot = Snapshot.build_snapshot(self, identifier: identifier, user: user, metadata: metadata)
|
|
15
|
+
new_entries = snapshot.snapshot_items.map(&:attributes)
|
|
16
|
+
snapshot.snapshot_items.reset # clear the association cache otherwise snapshot.valid? returns false
|
|
17
|
+
snapshot.save!
|
|
34
18
|
|
|
35
|
-
|
|
19
|
+
new_entries = new_entries.map { |item| item.except("id").merge(created_at: snapshot.created_at, snapshot_id: snapshot.id) }
|
|
36
20
|
|
|
37
|
-
|
|
21
|
+
SnapshotItem.upsert_all(new_entries, returning: false)
|
|
38
22
|
|
|
39
23
|
snapshot
|
|
40
24
|
end
|
|
@@ -43,9 +27,9 @@ module ActiveSnapshot
|
|
|
43
27
|
|
|
44
28
|
def has_snapshot_children(&block)
|
|
45
29
|
if block_given?
|
|
46
|
-
|
|
30
|
+
self.snapshot_children_proc = block
|
|
47
31
|
else
|
|
48
|
-
|
|
32
|
+
self.snapshot_children_proc
|
|
49
33
|
end
|
|
50
34
|
end
|
|
51
35
|
|
|
@@ -122,6 +106,5 @@ module ActiveSnapshot
|
|
|
122
106
|
return snapshot_children
|
|
123
107
|
end
|
|
124
108
|
end
|
|
125
|
-
|
|
126
109
|
end
|
|
127
110
|
end
|
|
@@ -15,6 +15,118 @@ module ActiveSnapshot
|
|
|
15
15
|
validates :identifier, uniqueness: { scope: [:item_id, :item_type], allow_nil: true}
|
|
16
16
|
validates :user_type, presence: true, if: :user_id
|
|
17
17
|
|
|
18
|
+
class << self
|
|
19
|
+
def build_snapshot(resource, identifier: nil, user: nil, metadata: nil)
|
|
20
|
+
snapshot = resource.snapshots.build({
|
|
21
|
+
identifier: identifier,
|
|
22
|
+
user_id: (user.id if user),
|
|
23
|
+
user_type: (user.class.name if user),
|
|
24
|
+
metadata: (metadata || {}),
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
snapshot.build_snapshot_item(resource)
|
|
28
|
+
|
|
29
|
+
snapshot_children = resource.children_to_snapshot
|
|
30
|
+
|
|
31
|
+
snapshot_children&.each do |child_group_name, h|
|
|
32
|
+
h[:records].each do |child_item|
|
|
33
|
+
snapshot.build_snapshot_item(child_item, child_group_name: child_group_name)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
snapshot
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def diff(from, to)
|
|
41
|
+
if !from.is_a?(Snapshot)
|
|
42
|
+
raise ArgumentError.new("'from' must be an ActiveSnapshot::Snapshot")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
to_item_id, to_item_type = to.is_a?(Snapshot) ? [to.item_id, to.item_type] : [to.id, to.class.polymorphic_name]
|
|
46
|
+
|
|
47
|
+
if from.item_id != to_item_id || from.item_type != to_item_type
|
|
48
|
+
raise ArgumentError.new("Both records must reference the same item")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if to.is_a?(Snapshot) && from.created_at > to.created_at
|
|
52
|
+
raise ArgumentError.new("'to' must be a newer snapshot than 'from'")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
from_snapshot = from
|
|
56
|
+
to_snapshot = to.is_a?(Snapshot) ? to : build_snapshot(to)
|
|
57
|
+
|
|
58
|
+
from_snapshot_items = from_snapshot.snapshot_items
|
|
59
|
+
to_snapshot_items = to_snapshot.snapshot_items
|
|
60
|
+
|
|
61
|
+
diffs = []
|
|
62
|
+
|
|
63
|
+
from_snapshot_items.each do |from_snapshot_item|
|
|
64
|
+
to_snapshot_item = to_snapshot_items.find do |item|
|
|
65
|
+
item.item_id == from_snapshot_item.item_id && item.item_type == from_snapshot_item.item_type
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if to_snapshot_item.nil?
|
|
69
|
+
diffs << {
|
|
70
|
+
action: :destroy,
|
|
71
|
+
item_id: from_snapshot_item.item_id,
|
|
72
|
+
item_type: from_snapshot_item.item_type,
|
|
73
|
+
changes: snapshot_item_changes(from_snapshot_item, nil)
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
changes = snapshot_item_changes(from_snapshot_item, to_snapshot_item)
|
|
77
|
+
|
|
78
|
+
next if changes.empty?
|
|
79
|
+
|
|
80
|
+
diffs << {
|
|
81
|
+
action: :update,
|
|
82
|
+
item_id: from_snapshot_item.item_id,
|
|
83
|
+
item_type: from_snapshot_item.item_type,
|
|
84
|
+
changes: changes
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
to_snapshot_items.each do |to_snapshot_item|
|
|
90
|
+
from_snapshot_item = from_snapshot_items.find do |item|
|
|
91
|
+
item.item_id == to_snapshot_item.item_id && item.item_type == to_snapshot_item.item_type
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
next if from_snapshot_item.present?
|
|
95
|
+
|
|
96
|
+
diffs << {
|
|
97
|
+
action: :create,
|
|
98
|
+
item_id: to_snapshot_item.item_id,
|
|
99
|
+
item_type: to_snapshot_item.item_type,
|
|
100
|
+
changes: snapshot_item_changes(nil, to_snapshot_item)
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
diffs
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def snapshot_item_changes(from, to)
|
|
110
|
+
from_object = from ? from.object : {}
|
|
111
|
+
to_object = to ? to.object : {}
|
|
112
|
+
|
|
113
|
+
keys = (from_object.keys + to_object.keys).uniq
|
|
114
|
+
|
|
115
|
+
changes = {}
|
|
116
|
+
|
|
117
|
+
keys.each do |key|
|
|
118
|
+
from_value = from_object[key]
|
|
119
|
+
to_value = to_object[key]
|
|
120
|
+
|
|
121
|
+
next if to_value == from_value
|
|
122
|
+
|
|
123
|
+
changes[key.to_sym] = [from_value, to_value]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
changes
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
18
130
|
def metadata
|
|
19
131
|
return @metadata if @metadata
|
|
20
132
|
|
|
@@ -52,7 +164,7 @@ module ActiveSnapshot
|
|
|
52
164
|
def build_snapshot_item(instance, child_group_name: nil)
|
|
53
165
|
attrs = instance.attributes
|
|
54
166
|
|
|
55
|
-
if instance.class.defined_enums.any?
|
|
167
|
+
if instance.class.respond_to?(:defined_enums) && instance.class.defined_enums.any?
|
|
56
168
|
instance.class.defined_enums.slice(*attrs.keys).each do |enum_col_name, enum_mapping|
|
|
57
169
|
val = attrs.fetch(enum_col_name)
|
|
58
170
|
next if val.nil?
|
|
@@ -16,7 +16,7 @@ class <%= migration_name %> < ActiveRecord::Migration::Current
|
|
|
16
16
|
create_table :snapshot_items<%= table_options %> do |t|
|
|
17
17
|
t.belongs_to :snapshot, null: false, index: true
|
|
18
18
|
t.belongs_to :item, polymorphic: true, null: false, index: true
|
|
19
|
-
t.index [:snapshot_id, :item_id, :item_type]
|
|
19
|
+
t.index [:snapshot_id, :item_id, :item_type]
|
|
20
20
|
|
|
21
21
|
t.json :object, null: false
|
|
22
22
|
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: active_snapshot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Weston Ganger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-12-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -56,16 +56,16 @@ dependencies:
|
|
|
56
56
|
name: minitest
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
|
-
- - "
|
|
59
|
+
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '0'
|
|
61
|
+
version: '5.0'
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
|
-
- - "
|
|
66
|
+
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '0'
|
|
68
|
+
version: '5.0'
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: minitest-reporters
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|