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 +4 -4
- data/README.md +17 -0
- data/lib/mongoid/audit_log.rb +2 -1
- data/lib/mongoid/audit_log/entry.rb +12 -0
- data/lib/mongoid/audit_log/restore.rb +94 -0
- data/lib/mongoid/audit_log/version.rb +1 -1
- data/spec/mongoid/audit_log/entry_spec.rb +68 -0
- data/spec/mongoid/audit_log/restore_spec.rb +81 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 76f6838e59f9a32deaa8bb63779f7f5cbb35b280
|
4
|
+
data.tar.gz: 52ec79ea1c7f19cac8f7632e3fb9792189f1580f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/lib/mongoid/audit_log.rb
CHANGED
@@ -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
|
@@ -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
|
+
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-
|
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
|