active_snapshot 0.4.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b48af4ad66af3d9fe9110f544df49f29ec6586969cb7e2c7f62431b54163a84
4
- data.tar.gz: fddc58303f066d5b77b56304c4367874e2b3dda0ea2e76e710d81a0f1705861e
3
+ metadata.gz: d5fd89751eb3c64739f2a3242edfb4a8f3fd1dfce8047bbd4db51b790039ee39
4
+ data.tar.gz: 5cf804fd3bb81be067153fa1120b45f5045febf0eb93921950ae51bea17c65a8
5
5
  SHA512:
6
- metadata.gz: 9a769fccb45b11f7163384b6ee79e5ff056b897cb3f382bb48054eaf9e20dba87371ab6b42da4ac117eafd136ebdc5d2dcd3441c7cf3a397ea087ec481dbc61d
7
- data.tar.gz: 2ca41b8874d1511596e25893ddb9b0b05daeead16a12150ff25b1812b1dd3f2901fdcad501ece2681b4a96161e8909e5fd475d52e8c7eda72d74e83804b38574
6
+ metadata.gz: c079cdc615a35293cecbe0ab3c86df6a0b96e6bc0a1a92beb5b8580735d4a61209aa97623a18d9e06aeffeb109f94bbc742cea119e96421ba725ed9a22aae1eb
7
+ data.tar.gz: dad4d76b90fd8caa0dbd09a2c20d618bd6b8060de77eacbbad4eeda7d79cfa13be48fed5888f80122c607f8ce69acdf67eb7ce48707f54bd08dc3e8f9922604b
data/CHANGELOG.md CHANGED
@@ -2,12 +2,26 @@ CHANGELOG
2
2
  ---------
3
3
 
4
4
  - **Unreleased**
5
- * [View Diff](https://github.com/westonganger/active_snapshot/compare/v0.4.0...master)
5
+ * [View Diff](https://github.com/westonganger/active_snapshot/compare/v0.5.1...master)
6
6
  * Nothing yet
7
7
 
8
+ - **v0.5.1** - Nov 11, 2024
9
+ * [View Diff](https://github.com/westonganger/active_snapshot/compare/v0.5.0...v0.5.1)
10
+ * [#66](https://github.com/westonganger/active_snapshot/pull/66) - Ensure `SnapshotItem#restore_item!` and `Snapshot#fetch_reified_items` bypass assignment for snapshot object data where the associated column no longer exists.
11
+ * [#63](https://github.com/westonganger/active_snapshot/pull/63) - Fix bug when enum value is nil
12
+
13
+ - **v0.5.0** - Nov 8, 2024
14
+ * [View Diff](https://github.com/westonganger/active_snapshot/compare/v0.4.0...v0.5.0)
15
+ * [#61](https://github.com/westonganger/active_snapshot/pull/61) - Ensure snapshot record returned by `create_snapshot!` is `valid?`
16
+ * [#60](https://github.com/westonganger/active_snapshot/pull/60) - Store enum value as integer
17
+ * [#56](https://github.com/westonganger/active_snapshot/pull/56) - Add presence validation for object in SnapshotItem model
18
+ * [#57](https://github.com/westonganger/active_snapshot/pull/57) - Add readonly argument to `Shapshot#fetch_reified_items`
19
+ * [#53](https://github.com/westonganger/active_snapshot/pull/53) - Allow `ActiveSnapshot.config` to be called before ActiveRecord `on_load` hook has occurred
20
+ * [#52](https://github.com/westonganger/active_snapshot/pull/52) - Remove deprecated positional argument on `create_snapshot!`
21
+
8
22
  - **v0.4.0** - July 23, 2024
9
23
  * [View Diff](https://github.com/westonganger/active_snapshot/compare/v0.3.2...v0.4.0)
10
- * [#44](https://github.com/westonganger/active_snapshot/pull/44) - Remove dependency on activerecord-import with vanilla ActiveRecord upsert_all
24
+ * [#44](https://github.com/westonganger/active_snapshot/pull/44) - Remove dependency on `activerecord-import` with vanilla ActiveRecord `upsert_all`
11
25
 
12
26
  - **v0.3.2** - Oct 17, 2023
13
27
  * [View Diff](https://github.com/westonganger/active_snapshot/compare/v0.3.1...v0.3.2)
data/README.md CHANGED
@@ -9,15 +9,15 @@ Simplified snapshots and restoration for ActiveRecord models and associations wi
9
9
  Key Features:
10
10
 
11
11
  - Create and Restore snapshots of a parent record and any specified child records
12
- - Predictible and explicit behaviour provides much needed clarity to your restore logic
12
+ - Predictable and explicit behaviour provides much needed clarity to your restore logic
13
13
  - Snapshots are created upon request only, we do not use any callbacks
14
14
  - Tiny method footprint so its easy to completely override the logic later
15
15
 
16
16
  Why This Library:
17
17
 
18
- Model Versioning and Restoration require concious thought, design, and understanding. You should understand your versioning and restoration process completely. This gem's small API and fully understandable design fully supports this.
18
+ Model Versioning and Restoration require conscious thought, design, and understanding. You should understand your versioning and restoration process completely. This gem's small API and fully understandable design fully supports this.
19
19
 
20
- I do not recommend using paper_trail-association_tracking because it is mostly a blackbox solution which encourages you to set it up and then assume its Just Working<sup>TM</sup>. This makes for major data problems later. Dont fall into this trap. Instead read this gems brief source code completely before use OR copy the code straight into your codebase. Once you know it, then you are free.
20
+ I do not recommend using [paper_trail-association_tracking](https://github.com/westonganger/paper_trail-association_tracking) because it is mostly a blackbox solution which encourages you to set it up and then assume its Just Working<sup>TM</sup>. This makes for major data problems later. Dont fall into this trap. Instead read this gems brief source code completely before use OR copy the code straight into your codebase. Once you know it, then you are free.
21
21
 
22
22
 
23
23
 
@@ -131,20 +131,67 @@ end
131
131
 
132
132
  Now when you run `create_snapshot!` the associations will be tracked accordingly
133
133
 
134
- # Reifying Snapshot Items
134
+ # Reifying Snapshots
135
135
 
136
- You can view all of the reified snapshot items by calling the following method. Its completely up to you on how to use this data.
136
+ A reified record refers to an ActiveRecord instance where the local objects data is set to match the snaphotted data, but the database remains changed.
137
+
138
+ You can view all of the "reified" snapshot items by calling the following method. Its completely up to you on how to use this data.
137
139
 
138
140
  ```ruby
139
141
  reified_parent, reified_children_hash = snapshot.fetch_reified_items
140
142
  ```
141
143
 
142
- As a safety these records have the `@readonly = true` attribute set on them. If you want to perform any write actions on the returned instances you will have to set `@readonly = nil`.
144
+ As a safety these records have the `readonly` attribute set on them.
145
+ If you want to perform any write actions on the returned instances you will have to set the `readonly` attribute to `false`
143
146
 
144
147
  ```ruby
148
+ reified_parent, reified_children_hash = snapshot.fetch_reified_items(readonly: false)
149
+ # or
145
150
  reified_parent, reified_children_hash = snapshot.fetch_reified_items
151
+ reified_children_hash.first.instance_variable_set("@readonly", false)
152
+ ```
153
+
154
+ # Diffing Versions
155
+
156
+ You can use the following example code to generate your own diffs.
157
+
158
+ ```ruby
159
+ snapshot = post.snapshots.find_by!(identifier: "some-identifier")
160
+
161
+ snapshot_item = snapshot.snapshot_items.find_by!(item_type: "Post")
162
+
163
+ old_attrs = snapshot_item.object
164
+ new_attrs = post.attributes # or could be another snapshot object
165
+
166
+ attrs_not_changed = old_attrs.to_a.intersection(new_attrs.to_a).to_h
167
+
168
+ attrs_changed = new_attrs.to_a - attrs_not_changed.to_a
169
+ ```
146
170
 
147
- reified_parent.instance_variable_set("@readonly", false)
171
+ # Important Data Considerations / Warnings
172
+
173
+ ### Dropping columns
174
+
175
+ If you plan to use the snapshot restore capabilities please be aware:
176
+
177
+ Whenever you drop a database column and there already exists snapshots of that model then you are kind of silently breaking your restore mechanism. Because now the application will not be able to assign data to columns that dont exist on the model. We work around this by bypassing the attribute assignment for snapshot item object entries that does not correlate to a current database column.
178
+
179
+ I recommend that you add an entry to this in your applications safe-migrations guidelines.
180
+
181
+ If you would like to detect if this situation has already ocurred you can use the following script:
182
+
183
+ ```ruby
184
+ SnapshotItem.all.each do |snapshot_item|
185
+ snapshot_item.object.keys.each do |key|
186
+ klass = Class.const_get(snapshot_item.item_type)
187
+
188
+ if !klass.column_names.include?(key)
189
+ invalid_data = snapshot_item.object.slice(*klass.column_names)
190
+
191
+ raise "invalid data found - #{invalid_data}"
192
+ end
193
+ end
194
+ end
148
195
  ```
149
196
 
150
197
  # Key Models Provided & Additional Customizations
@@ -8,13 +8,7 @@ module ActiveSnapshot
8
8
  has_many :snapshot_items, as: :item, class_name: 'ActiveSnapshot::SnapshotItem'
9
9
  end
10
10
 
11
- def create_snapshot!(legacy_identifier=nil, identifier: nil, user: nil, metadata: nil)
12
- if identifier.nil? && legacy_identifier
13
- identifier = legacy_identifier
14
-
15
- ActiveSupport::Deprecation.warn(LEGACY_POSITIONAL_ARGUMENT_WARNING)
16
- end
17
-
11
+ def create_snapshot!(identifier: nil, user: nil, metadata: nil)
18
12
  snapshot = snapshots.create!({
19
13
  identifier: identifier,
20
14
  user_id: (user.id if user),
@@ -40,6 +34,8 @@ module ActiveSnapshot
40
34
 
41
35
  SnapshotItem.upsert_all(new_entries.map{|x| x.delete("id"); x }, returning: false)
42
36
 
37
+ snapshot.snapshot_items.reset # clear the association cache otherwise snapshot.valid? returns false
38
+
43
39
  snapshot
44
40
  end
45
41
 
@@ -127,7 +123,5 @@ module ActiveSnapshot
127
123
  end
128
124
  end
129
125
 
130
- LEGACY_POSITIONAL_ARGUMENT_WARNING = "Supplying the snapshots :identifier as a positional argument is now deprecated and will be removed in upcoming versions. Please supply the snapshot identifier using the :identifier keyword argument instead.".freeze
131
-
132
126
  end
133
127
  end
@@ -46,8 +46,18 @@ module ActiveSnapshot
46
46
  end
47
47
 
48
48
  def build_snapshot_item(instance, child_group_name: nil)
49
+ attrs = instance.attributes
50
+
51
+ if instance.class.defined_enums.any?
52
+ instance.class.defined_enums.slice(*attrs.keys).each do |enum_col_name, enum_mapping|
53
+ val = attrs.fetch(enum_col_name)
54
+ next if val.nil?
55
+ attrs[enum_col_name] = enum_mapping.fetch(val)
56
+ end
57
+ end
58
+
49
59
  self.snapshot_items.new({
50
- object: instance.attributes,
60
+ object: attrs,
51
61
  item_id: instance.id,
52
62
  item_type: instance.class.name,
53
63
  child_group_name: child_group_name,
@@ -96,15 +106,25 @@ module ActiveSnapshot
96
106
  return true
97
107
  end
98
108
 
99
- def fetch_reified_items
109
+ def fetch_reified_items(readonly: true)
100
110
  reified_children_hash = {}.with_indifferent_access
101
111
 
102
112
  reified_parent = nil
103
113
 
104
114
  snapshot_items.each do |si|
105
- reified_item = si.item_type.constantize.new(si.object)
115
+ reified_item = si.item_type.constantize.new
106
116
 
107
- reified_item.readonly!
117
+ si.object.each do |k,v|
118
+ if reified_item.respond_to?("#{k}=")
119
+ reified_item[k] = v
120
+ else
121
+ # database column was likely dropped since the snapshot was created
122
+ end
123
+ end
124
+
125
+ if readonly
126
+ reified_item.readonly!
127
+ end
108
128
 
109
129
  key = si.child_group_name
110
130
 
@@ -12,22 +12,21 @@ module ActiveSnapshot
12
12
  validates :snapshot_id, presence: true
13
13
  validates :item_id, presence: true, uniqueness: { scope: [:snapshot_id, :item_type] }
14
14
  validates :item_type, presence: true
15
+ validates :object, presence: true
15
16
 
16
17
  def object
17
18
  return @object if @object
18
19
 
19
20
  if ActiveSnapshot.config.storage_method_json?
20
- @object = JSON.parse(self[:object])
21
+ @object = self[:object] ? JSON.parse(self[:object]) : {}
21
22
  elsif ActiveSnapshot.config.storage_method_yaml?
22
- yaml_method = "unsafe_load"
23
+ yaml_method = YAML.respond_to?(:unsafe_load) ? :unsafe_load : :load
23
24
 
24
- if !YAML.respond_to?("unsafe_load")
25
- yaml_method = "load"
26
- end
27
-
28
- @object = YAML.send(yaml_method, self[:object])
25
+ @object = self[:object] ? YAML.public_send(yaml_method, self[:object]) : {}
29
26
  elsif ActiveSnapshot.config.storage_method_native_json?
30
27
  @object = self[:object]
28
+ else
29
+ raise StandardError, "Unsupported storage_method: `#{ActiveSnapshot.config.storage_method}`"
31
30
  end
32
31
  end
33
32
 
@@ -52,7 +51,13 @@ module ActiveSnapshot
52
51
  self.item = item_klass.new
53
52
  end
54
53
 
55
- item.assign_attributes(object)
54
+ object.each do |k,v|
55
+ if item.respond_to?("#{k}=")
56
+ item[k] = v
57
+ else
58
+ # database column was likely dropped since the snapshot was created
59
+ end
60
+ end
56
61
 
57
62
  item.save!(validate: false, touch: false)
58
63
  end
@@ -1,3 +1,3 @@
1
1
  module ActiveSnapshot
2
- VERSION = "0.4.0".freeze
2
+ VERSION = "0.5.1".freeze
3
3
  end
@@ -3,27 +3,29 @@ require "active_snapshot/config"
3
3
 
4
4
  require 'active_support/lazy_load_hooks'
5
5
 
6
+ module ActiveSnapshot
7
+ @@config = ActiveSnapshot::Config.new
8
+
9
+ def self.config(&block)
10
+ if block_given?
11
+ block.call(@@config)
12
+ else
13
+ return @@config
14
+ end
15
+ end
16
+ end
17
+
6
18
  ActiveSupport.on_load(:active_record) do
7
19
  require "active_snapshot/models/snapshot"
8
20
  require "active_snapshot/models/snapshot_item"
9
21
 
10
22
  require "active_snapshot/models/concerns/snapshots_concern"
11
23
 
12
- module ActiveSnapshot
24
+ ActiveSnapshot.module_eval do
13
25
  extend ActiveSupport::Concern
14
26
 
15
27
  included do
16
28
  include ActiveSnapshot::SnapshotsConcern
17
29
  end
18
-
19
- @@config = ActiveSnapshot::Config.new
20
-
21
- def self.config(&block)
22
- if block_given?
23
- block.call(@@config)
24
- else
25
- return @@config
26
- end
27
- end
28
30
  end
29
31
  end
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: 0.4.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Weston Ganger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-23 00:00:00.000000000 Z
11
+ date: 2024-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord