mongoid-audit_log 0.4.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 35867d14fd329fd4f087b5f01dab102b2b83bd9d
4
- data.tar.gz: a98c23806a1fd139755419984fe9d0e8932b59be
3
+ metadata.gz: 76f6838e59f9a32deaa8bb63779f7f5cbb35b280
4
+ data.tar.gz: 52ec79ea1c7f19cac8f7632e3fb9792189f1580f
5
5
  SHA512:
6
- metadata.gz: 2bbbbb0f5b34f99bbc1c7131adf7c8fb4b3482428f2615d3f516de3421a078c5e63908f90b022e590ce056a0bfe6816cc4f61a91d46c2cf160366a788532070f
7
- data.tar.gz: 183f25bf3f728f06b626802cc034f16f75bc54850e225daafdd76bbb0e0a5e9f445a6e8646f732820c763165f3968e34f1200b54bebea9bba86d67e14d1a2a4a
6
+ metadata.gz: 17f735eaaf6b40708cf6ef734699a2356a004496945e9b6851a41bc6b41c9598b3de2efee47e157773f79b6ee89aaed0a4e68a52833d1d656c2142d0b5f1dccc
7
+ data.tar.gz: 10c684e7888e5db8153a1d8e7a49671d9e025a16d83230214a2b4d998e8a8819a22e850b4bfcec6f7823caf80354a4638beeffcdb4e00a0625798e179a1e65c2
data/README.md CHANGED
@@ -128,6 +128,23 @@ model.audit_log_entries.first.create? # => true
128
128
  model.audit_log_entries.first.model_attributes # => {"name"=>"foo bar"}
129
129
  ```
130
130
 
131
+ ### Restoring
132
+
133
+ You can restore models for `Mongoid::AuditLog::Entry` instances for deletions.
134
+ This works for both root and embedded documents.
135
+
136
+ Examples:
137
+ ```ruby
138
+ model = Model.create!(:name => 'foo bar')
139
+ Mongoid::AuditLog.record { model.destroy }
140
+
141
+ entry = Mongoid::AuditLog::Entry.first
142
+ entry.restore!
143
+
144
+ model == Model.find_by(name: 'foo bar') # => true
145
+ ```
146
+
147
+ It's possible to end up in a situation where a destroy entry cannot be restored, e.g. an entry deleting an embedded document for a root document that's already been deleted. In these scenarios, `Mongoid::AuditLog::Restore::InvalidRestore` will be raised.
131
148
 
132
149
  ## Contributing
133
150
 
@@ -3,6 +3,7 @@ require "mongoid/audit_log/config"
3
3
  require "mongoid/audit_log/entry"
4
4
  require "mongoid/audit_log/changes"
5
5
  require "mongoid/audit_log/embedded_changes"
6
+ require "mongoid/audit_log/restore"
6
7
 
7
8
  module Mongoid
8
9
  module AuditLog
@@ -57,7 +58,7 @@ module Mongoid
57
58
  def self.current_modifier
58
59
  Thread.current[:mongoid_audit_log_modifier]
59
60
  end
60
-
61
+
61
62
  def self.current_modifier=(modifier)
62
63
  Thread.current[:mongoid_audit_log_modifier] = modifier
63
64
  end
@@ -9,6 +9,7 @@ module Mongoid
9
9
  field :modifier_id, :type => String
10
10
  field :model_attributes, :type => Hash
11
11
  field :document_path, :type => Array
12
+ field :restored, :type => Boolean, default: false
12
13
 
13
14
  if Gem::Version.new(Mongoid::VERSION) < Gem::Version.new('6.0.0.beta')
14
15
  belongs_to :audited, :polymorphic => true
@@ -82,6 +83,17 @@ module Mongoid
82
83
  end
83
84
  end
84
85
 
86
+ def restore!
87
+ raise Restore::InvalidRestore if restored? || !destroy?
88
+
89
+ Restore.new(self).perform
90
+ update_attributes!(:restored => true)
91
+ end
92
+
93
+ def restorable?
94
+ destroy? && !restored? && Restore.new(self).valid?
95
+ end
96
+
85
97
  def respond_to?(sym, *args)
86
98
  key = sym.to_s
87
99
  (model_attributes.present? && model_attributes.has_key?(key)) || super
@@ -0,0 +1,94 @@
1
+ module Mongoid
2
+ module AuditLog
3
+ class Restore
4
+ class DuplicateError < StandardError; end
5
+ class InvalidRestore < StandardError; end
6
+
7
+ attr_reader :entry
8
+ delegate :name, to: :restored
9
+ delegate :for_embedded_doc?, to: :entry
10
+
11
+ def initialize(entry)
12
+ @entry = entry
13
+ end
14
+
15
+ def valid?
16
+ !entry.for_embedded_doc? || restored_root.present?
17
+ end
18
+
19
+ def perform
20
+ restored.attributes = attributes
21
+ restored.save!
22
+ end
23
+
24
+ def attributes
25
+ @attributes ||=
26
+ begin
27
+ attrs = entry.model_attributes.deep_dup
28
+ restored.send(:process_localized_attributes, model_class, attrs)
29
+ attrs
30
+ end
31
+ end
32
+
33
+ def model_class
34
+ entry.audited_type.constantize
35
+ end
36
+
37
+ def restored
38
+ @restored ||=
39
+ if entry.for_embedded_doc?
40
+ find_embedded_restored
41
+ else
42
+ find_root_restored
43
+ end
44
+ end
45
+
46
+ def find_root_restored
47
+ model_class.new
48
+ end
49
+
50
+ def find_embedded_restored
51
+ raise InvalidRestore if restored_root.blank?
52
+
53
+ last_path = document_path.last
54
+ metadata = restored_root.class.reflect_on_association(last_path['relation'])
55
+ relation = restored_root.send(last_path['relation'])
56
+
57
+ if metadata.many?
58
+ relation.build
59
+ elsif relation.present?
60
+ raise DuplicateError
61
+ else
62
+ restored_root.send("build_#{metadata.name}")
63
+ end
64
+ end
65
+
66
+ def restored_root
67
+ document_path.reduce(entry.root) do |current, path|
68
+ match = if document_path_matches?(path, current)
69
+ current
70
+ elsif current.respond_to?(:detect)
71
+ current.detect do |model|
72
+ document_path_matches?(path, model)
73
+ end
74
+ end
75
+
76
+ if path == document_path.last
77
+ return match
78
+ else
79
+ match.send(path['relation'])
80
+ end
81
+ end
82
+ end
83
+
84
+ def document_path
85
+ # don't need last because that entry represents the deleted doc
86
+ entry.document_path[0..-2]
87
+ end
88
+
89
+ def document_path_matches?(path, object)
90
+ object.class.name == path['class_name'] && object.id == path['id']
91
+ end
92
+ end
93
+ end
94
+ end
@@ -1,5 +1,5 @@
1
1
  module Mongoid
2
2
  module AuditLog
3
- VERSION = "0.4.1"
3
+ VERSION = "0.5.0"
4
4
  end
5
5
  end
@@ -144,6 +144,74 @@ module Mongoid
144
144
  entry.root.should be_nil
145
145
  end
146
146
  end
147
+
148
+ describe '#restore!' do
149
+ it 'marks the entry as restored' do
150
+ product = Product.create!(:name => 'Foo bar')
151
+ AuditLog.record { product.destroy }
152
+
153
+ entry = Entry.desc(:created_at).first
154
+ entry.restore!
155
+ entry.should be_restored
156
+ end
157
+
158
+ it 'raises InvalidRestore if already restored' do
159
+ product = Product.create!(:name => 'Foo bar')
160
+ AuditLog.record { product.destroy }
161
+
162
+ entry = Entry.desc(:created_at).first
163
+ entry.restore!
164
+
165
+ expect { entry.restore! }.to raise_error(Restore::InvalidRestore)
166
+ end
167
+
168
+ it 'raises InvalidRestore if not a destroy' do
169
+ product = Product.create!(:name => 'Foo bar')
170
+ AuditLog.record { product.variants.create! }
171
+
172
+ entry = Entry.desc(:created_at).first
173
+ expect { entry.restore! }.to raise_error(Restore::InvalidRestore)
174
+ end
175
+
176
+ it 'raises InvalidRestore if the root is blank' do
177
+ product = Product.create!(:name => 'Foo bar')
178
+ product.variants.create!
179
+ AuditLog.record { product.variants.first.destroy }
180
+ product.destroy
181
+
182
+ entry = Entry.desc(:created_at).first
183
+ expect { entry.restore! }.to raise_error(Restore::InvalidRestore)
184
+ end
185
+ end
186
+
187
+ describe '#restorable?' do
188
+ it 'is false if already restored' do
189
+ product = Product.create!(:name => 'Foo bar')
190
+ AuditLog.record { product.destroy }
191
+
192
+ entry = Entry.desc(:created_at).first
193
+ entry.restore!
194
+ entry.should_not be_restorable
195
+ end
196
+
197
+ it 'is false if not a destroy' do
198
+ product = Product.create!(:name => 'Foo bar')
199
+ AuditLog.record { product.variants.create! }
200
+
201
+ entry = Entry.desc(:created_at).first
202
+ entry.should_not be_restorable
203
+ end
204
+
205
+ it 'is false is root is blank' do
206
+ product = Product.create!(:name => 'Foo bar')
207
+ product.variants.create!
208
+ AuditLog.record { product.variants.first.destroy }
209
+ product.destroy
210
+
211
+ entry = Entry.desc(:created_at).first
212
+ entry.should_not be_restorable
213
+ end
214
+ end
147
215
  end
148
216
  end
149
217
  end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ module Mongoid
4
+ module AuditLog
5
+ describe Restore do
6
+ before(:all) do
7
+ class ::First
8
+ include Mongoid::Document
9
+ include Mongoid::AuditLog
10
+
11
+ field :name, type: String, localize: true
12
+ embeds_many :seconds
13
+ end
14
+
15
+ class ::Second
16
+ include Mongoid::Document
17
+ include Mongoid::AuditLog
18
+
19
+ field :name, type: String, localize: true
20
+ embedded_in :first
21
+ embeds_many :thirds
22
+ end
23
+
24
+ class ::Third
25
+ include Mongoid::Document
26
+ include Mongoid::AuditLog
27
+
28
+ field :name, type: String, localize: true
29
+ embedded_in :second
30
+ end
31
+ end
32
+
33
+ after(:all) do
34
+ [:First, :Second, :Third].each { |c| Object.send(:remove_const, c) }
35
+ end
36
+
37
+ describe '.perform' do
38
+ it 'restores a root document' do
39
+ root = First.create!(name: 'Foo')
40
+ Mongoid::AuditLog.record { root.destroy }
41
+ restore = Restore.new(Mongoid::AuditLog::Entry.first)
42
+ restore.perform
43
+
44
+ restore.restored.should be_persisted
45
+ First.count.should == 1
46
+ restore.restored.should == First.first
47
+ restore.restored.name.should == 'Foo'
48
+ end
49
+
50
+ it 'restores an embedded array document' do
51
+ root = First.create!(name: 'Foo', seconds: [{ name: 'Bar' }])
52
+ Mongoid::AuditLog.record { root.seconds.first.destroy }
53
+ restore = Restore.new(Mongoid::AuditLog::Entry.first)
54
+ restore.perform
55
+ root.reload
56
+
57
+ restore.restored.should be_persisted
58
+ root.seconds.length.should == 1
59
+ root.seconds.first.should == restore.restored
60
+ root.seconds.first.name.should == 'Bar'
61
+ end
62
+
63
+ it 'restores a nested array document' do
64
+ root = First.create!(
65
+ name: 'Foo',
66
+ seconds: [{ name: 'Bar', thirds: [{ name: 'Baz' }] }]
67
+ )
68
+ Mongoid::AuditLog.record { root.seconds.first.thirds.first.destroy }
69
+ restore = Restore.new(Mongoid::AuditLog::Entry.first)
70
+ restore.perform
71
+ root.reload
72
+
73
+ restore.restored.should be_persisted
74
+ root.seconds.first.thirds.length.should == 1
75
+ root.seconds.first.thirds.first.should == restore.restored
76
+ root.seconds.first.thirds.first.name.should == 'Baz'
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid-audit_log
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Crouse
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-06-19 00:00:00.000000000 Z
11
+ date: 2017-07-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mongoid
@@ -70,10 +70,12 @@ files:
70
70
  - lib/mongoid/audit_log/config.rb
71
71
  - lib/mongoid/audit_log/embedded_changes.rb
72
72
  - lib/mongoid/audit_log/entry.rb
73
+ - lib/mongoid/audit_log/restore.rb
73
74
  - lib/mongoid/audit_log/version.rb
74
75
  - mongoid-audit_log.gemspec
75
76
  - spec/mongoid/audit_log/changes_spec.rb
76
77
  - spec/mongoid/audit_log/entry_spec.rb
78
+ - spec/mongoid/audit_log/restore_spec.rb
77
79
  - spec/mongoid/audit_log_spec.rb
78
80
  - spec/spec_helper.rb
79
81
  - spec/support/models.rb
@@ -105,6 +107,7 @@ summary: No fancy versioning, undo, redo, etc. Just saves changes to Mongoid mod
105
107
  test_files:
106
108
  - spec/mongoid/audit_log/changes_spec.rb
107
109
  - spec/mongoid/audit_log/entry_spec.rb
110
+ - spec/mongoid/audit_log/restore_spec.rb
108
111
  - spec/mongoid/audit_log_spec.rb
109
112
  - spec/spec_helper.rb
110
113
  - spec/support/models.rb