mongoid-audit_log 0.4.1 → 0.5.0

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
  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