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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 214cb14c7afa97c56711b7aaf4d0ed4c3001ba995578de0187432a6951ee8a80
4
- data.tar.gz: 72798e3577020e343768f3ba79cb93adb9c97f8636e3dd869eb6443683e2d2b0
3
+ metadata.gz: 166c7d2f5b7e2595314eab22bd3fc4e8de2b28c7f5c6deb8b7a909e45e3061bb
4
+ data.tar.gz: daad374af6af17a0c14f0726418a72eb22af6d8366429572f70a3658c7a2168a
5
5
  SHA512:
6
- metadata.gz: 330a15dd3e253a01b366445442d4b3164b24bb2e557a7a3ed660459314deec0f847fdfdde0fc945ea9b6093943baf73b757e7faf8848f6b8695846a598b322f4
7
- data.tar.gz: f75841865ec536b0f9aaf2ead65260a8efefd7ffe6062928b8de1095e9d35ce2be97bff4f52e2f0fa52aa764d515ac2b12f33659e6c4089f6f1cf21c231f7e12
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.0.0...master)
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 use the following example code to generate your own diffs.
138
-
137
+ You can obtain the diff between two snapshots like this:
139
138
  ```ruby
140
- snapshot = post.snapshots.find_by!(identifier: "some-identifier")
141
-
142
- snapshot_item = snapshot.snapshot_items.find_by!(item_type: "Post")
143
-
144
- old_attrs = snapshot_item.object
145
- new_attrs = post.attributes # or could be another snapshot object
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
- attrs_not_changed = old_attrs.to_a.intersection(new_attrs.to_a).to_h
150
+ You can also obtain the diff between a snapshot and the current record:
151
+ ```ruby
152
+ from = post.snapshots.last
148
153
 
149
- attrs_changed = new_attrs.to_a - attrs_not_changed.to_a
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 = snapshots.create!({
13
- identifier: identifier,
14
- user_id: (user.id if user),
15
- user_type: (user.class.name if user),
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
- SnapshotItem.upsert_all(new_entries.map{|x| x.delete("id"); x }, returning: false)
19
+ new_entries = new_entries.map { |item| item.except("id").merge(created_at: snapshot.created_at, snapshot_id: snapshot.id) }
36
20
 
37
- snapshot.snapshot_items.reset # clear the association cache otherwise snapshot.valid? returns false
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
- @snapshot_children_proc = block
30
+ self.snapshot_children_proc = block
47
31
  else
48
- @snapshot_children_proc
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?
@@ -1,3 +1,3 @@
1
1
  module ActiveSnapshot
2
- VERSION = "1.0.0".freeze
2
+ VERSION = "1.1.0".freeze
3
3
  end
@@ -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], unique: true
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.0.0
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-01-17 00:00:00.000000000 Z
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