mongomapper-versioned 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/README.md +106 -0
- data/Rakefile +35 -0
- data/lib/mongomapper-versioned.rb +1 -0
- data/lib/tasks/versioned.rake +16 -0
- data/lib/versioned.rb +20 -0
- data/lib/versioned/meta.rb +3 -0
- data/lib/versioned/version.rb +79 -0
- data/lib/versioned/versioned.rb +100 -0
- data/test/config/config.rb +3 -0
- data/test/config/database.yml +5 -0
- data/test/models/post.rb +8 -0
- data/test/models/user.rb +10 -0
- data/test/performance/read_write.rb +65 -0
- data/test/test_helper.rb +29 -0
- data/test/unit/test_versioning.rb +229 -0
- metadata +155 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
## Versioned
|
2
|
+
|
3
|
+
Automatically store a version of a MongoMapper document every time it's updated. Configure a maximum number of versions you'd like to keep, or even the amount of time a particular document should keep versions around.
|
4
|
+
|
5
|
+
### Setup
|
6
|
+
|
7
|
+
Add the gem to your Gemfile. Add the necessary Mongo indexes: run `Version.create_indexes` in your console, or if you are using Rails run the `rake versioned:create_indexes` command.
|
8
|
+
|
9
|
+
### Basic Usage
|
10
|
+
|
11
|
+
Add the `versioned` declaration to the MongoMapper document models you want to keep versions of. When you make a change to a versioned document, a new version will be stored. Deleting a stored document will delete all the associated versions.
|
12
|
+
|
13
|
+
````ruby
|
14
|
+
class User
|
15
|
+
versioned
|
16
|
+
|
17
|
+
key :name, String
|
18
|
+
key :email, String
|
19
|
+
end
|
20
|
+
````
|
21
|
+
|
22
|
+
### Querying
|
23
|
+
|
24
|
+
Every versioned document model has an association called `versions`. It's a plain old MongoMapper one-to-many association with the `Version` model, sorted in reverse chronological order. You can query for versions using MongoMapper's standard query criteria:
|
25
|
+
|
26
|
+
````ruby
|
27
|
+
@user.versions.where(:created_at.lt > 1.day.ago)
|
28
|
+
````
|
29
|
+
|
30
|
+
### Pruning old versions
|
31
|
+
|
32
|
+
Use the `max` option to specifiy the maximum number of versions you want to keep. When the document is updated, the oldest versions will be pruned away to keep no more than the maximum number you specify.
|
33
|
+
|
34
|
+
````ruby
|
35
|
+
class User
|
36
|
+
versioned max: 10
|
37
|
+
|
38
|
+
key :name, String
|
39
|
+
key :email, String
|
40
|
+
end
|
41
|
+
````
|
42
|
+
|
43
|
+
If you'd rather specify the amount of time the versions of a doc should be kept, implement `keep_versions_for`. It must return the number of seconds to keep each revision. Every time a new version is created, versions will be purged based on their age.
|
44
|
+
|
45
|
+
This doesn't work with the `max` option. Use one or the other.
|
46
|
+
|
47
|
+
````ruby
|
48
|
+
class User
|
49
|
+
versioned
|
50
|
+
|
51
|
+
key :name, String
|
52
|
+
key :email, String
|
53
|
+
|
54
|
+
def keep_versions_for
|
55
|
+
90.days
|
56
|
+
end
|
57
|
+
end
|
58
|
+
````
|
59
|
+
|
60
|
+
### Auditing
|
61
|
+
|
62
|
+
When a versioned document is saved, you can pass the `updater` option to the save method. The associated version will keep a reference to the document passed. This allows you to keep track of who made a change to a versioned document.
|
63
|
+
|
64
|
+
````ruby
|
65
|
+
@reginold = User.find_by_email('reggie@hotmail.com')
|
66
|
+
@user.name = "Bob"
|
67
|
+
@user.save(:updater => @reginold)
|
68
|
+
@user.versions.first.updater
|
69
|
+
=> @reginold
|
70
|
+
````
|
71
|
+
|
72
|
+
### Rolling back
|
73
|
+
|
74
|
+
Each `Version` contains a copy of the document that was versioned. You can roll back to a particular version by calling the `rollback` method on the version you want. Reload the versioned document to pull the changes into the reference you're holding to the document.
|
75
|
+
|
76
|
+
````ruby
|
77
|
+
@user.versions[5].rollback
|
78
|
+
@user.reload
|
79
|
+
````
|
80
|
+
|
81
|
+
### Version IDs
|
82
|
+
|
83
|
+
A versioned document has a `version_id` key. When you update a document, the new Version document takes on the ID of the document's current `version_id` value. The document gets a new `version_id`.
|
84
|
+
|
85
|
+
Rolling back to a previous revision will also roll back the `version_id` value of the document.
|
86
|
+
|
87
|
+
This mechanism allows you to undo changes to a document:
|
88
|
+
|
89
|
+
````ruby
|
90
|
+
version_id = @user.version_id
|
91
|
+
@user.name
|
92
|
+
=> "Roger"
|
93
|
+
|
94
|
+
@user.name = "Frank"
|
95
|
+
@user.save
|
96
|
+
@user.name = "Bob"
|
97
|
+
@user.save
|
98
|
+
@user.versions.find(version_id).rollback
|
99
|
+
|
100
|
+
@user.reload
|
101
|
+
@user.name
|
102
|
+
=> "Roger"
|
103
|
+
@user.version_id == version_id
|
104
|
+
=> true
|
105
|
+
````
|
106
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
require File.join(File.dirname(__FILE__), '/lib/versioned')
|
5
|
+
|
6
|
+
require 'rake/testtask'
|
7
|
+
namespace :test do
|
8
|
+
Rake::TestTask.new(:unit) do |test|
|
9
|
+
test.libs << 'test'
|
10
|
+
test.ruby_opts << '-rubygems'
|
11
|
+
test.pattern = 'test/unit/**/test_*.rb'
|
12
|
+
test.verbose = true
|
13
|
+
end
|
14
|
+
|
15
|
+
Rake::TestTask.new(:performance) do |test|
|
16
|
+
test.libs << 'test'
|
17
|
+
test.ruby_opts << '-rubygems'
|
18
|
+
test.pattern = 'test/performance/**/*.rb'
|
19
|
+
test.verbose = true
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
task :default => 'test:unit'
|
24
|
+
|
25
|
+
desc 'Builds the gem'
|
26
|
+
task :build do
|
27
|
+
sh 'gem build versioned.gemspec'
|
28
|
+
Dir.mkdir('pkg') unless File.directory?('pkg')
|
29
|
+
sh "mv mongomapper-versioned-#{Versioned::VERSION}.gem pkg/mongomapper-versioned-#{Versioned::VERSION}.gem"
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'Builds and Installs the gem'
|
33
|
+
task :install => :build do
|
34
|
+
sh "gem install pkg/mongomapper-versioned-#{Versioned::VERSION}.gem"
|
35
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'versioned')
|
@@ -0,0 +1,16 @@
|
|
1
|
+
namespace :versioned do
|
2
|
+
desc "Check MongoMapper Versioned plugin indexes"
|
3
|
+
task :check_indexes => :environment do
|
4
|
+
missing = Version.missing_indexes
|
5
|
+
if missing.empty?
|
6
|
+
puts "Indexes have already been created."
|
7
|
+
else
|
8
|
+
puts "Indexes have not been created. Run `rake versioned:create_indexes`."
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
desc "Create MongoMapper Versioned plugin indexes"
|
13
|
+
task :create_indexes => :environment do
|
14
|
+
Version.create_indexes
|
15
|
+
end
|
16
|
+
end
|
data/lib/versioned.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'mongo_mapper'
|
2
|
+
require File.join(File.dirname(__FILE__), 'versioned', 'meta')
|
3
|
+
require File.join(File.dirname(__FILE__), 'versioned', 'version')
|
4
|
+
require File.join(File.dirname(__FILE__), 'versioned', 'versioned')
|
5
|
+
|
6
|
+
module Versioned
|
7
|
+
if Kernel.const_defined?(:Rails) && Rails.constants.include?(:Engine)
|
8
|
+
class Engine < ::Rails::Engine
|
9
|
+
engine_name :versioned
|
10
|
+
initializer "versioned.initialize" do |app|
|
11
|
+
MongoMapper::Document.plugin(Versioned)
|
12
|
+
end
|
13
|
+
initializer 'versioned.check_indexes', :after=> :disable_dependency_loading do
|
14
|
+
Version.check_indexes
|
15
|
+
end
|
16
|
+
end
|
17
|
+
else
|
18
|
+
MongoMapper::Document.plugin(Versioned)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'active_support/core_ext/array/conversions'
|
2
|
+
require 'mongomapper_id2'
|
3
|
+
|
4
|
+
class Version
|
5
|
+
include MongoMapper::Document
|
6
|
+
|
7
|
+
auto_increment!
|
8
|
+
|
9
|
+
key :doc, Hash
|
10
|
+
key :created_at, Time
|
11
|
+
|
12
|
+
before_create :set_created_at
|
13
|
+
|
14
|
+
belongs_to :versioned, polymorphic: true
|
15
|
+
belongs_to :updater, polymorphic: true
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def check_indexes
|
19
|
+
unless missing_indexes.empty?
|
20
|
+
msg = "Indexes have not been created for MongoMapper versioned docs. Run `rake versioned:create_indexes`."
|
21
|
+
if Kernel.const_defined?(:IRB)
|
22
|
+
puts "Warning: #{msg}"
|
23
|
+
else
|
24
|
+
::Rails.logger.warn(msg)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def missing_indexes
|
30
|
+
existing_index_names = self.collection.index_information.keys
|
31
|
+
required_index_names = required_indexes.collect do |i|
|
32
|
+
i.first.collect { |k| "#{k[0]}_#{k[1]}" }.join('_')
|
33
|
+
end
|
34
|
+
required_index_names - existing_index_names
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_indexes
|
38
|
+
required_indexes.each do |index|
|
39
|
+
ensure_index *index
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def required_indexes
|
44
|
+
[
|
45
|
+
[[[:versioned_id, 1], [:versioned_type, 1], [:created_at, -1]], background: true ],
|
46
|
+
[[[:versioned_id, 1], [:versioned_type, 1], [:id2, -1]], background: true],
|
47
|
+
[[[:versioned_id, 1], [:versioned_type, 1], [:id2, 1]], background: true]
|
48
|
+
]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def rollback
|
53
|
+
versioned.rollback do
|
54
|
+
trouble = []
|
55
|
+
versioned.version_id = self.id
|
56
|
+
self.doc.each_pair do |attr, val|
|
57
|
+
mutator = "#{attr}="
|
58
|
+
if versioned.respond_to?(mutator)
|
59
|
+
versioned.send(mutator, val)
|
60
|
+
else
|
61
|
+
trouble.push(attr)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
unless trouble.empty?
|
65
|
+
raise "Trying to load a #{versioned.class.name} version that has unsupported attributes: #{trouble.to_sentence}"
|
66
|
+
end
|
67
|
+
versioned.versions.destroy_all(:id2.gte => self.id2)
|
68
|
+
versioned.save
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
# Update the created_at field on the Document to the current time. This is only called on create.
|
74
|
+
def set_created_at
|
75
|
+
unless self.created_at
|
76
|
+
self.created_at = Time.now.utc
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Versioned
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def versioned(opts={})
|
8
|
+
self.send(:include, Versioned::Document)
|
9
|
+
self.max_versions = opts[:max].to_i
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module Document
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
key :version_id, ObjectId, default: proc { BSON::ObjectId.new }
|
18
|
+
|
19
|
+
many :versions, :as => :versioned, :sort => :id2.desc
|
20
|
+
|
21
|
+
before_update :push_version, :unless => :rolling_back?
|
22
|
+
after_update :prune_versions
|
23
|
+
after_destroy :destroy_versions
|
24
|
+
|
25
|
+
cattr_accessor :max_versions
|
26
|
+
attr_accessor :updater, :version_created_at
|
27
|
+
attr_writer :rolling_back
|
28
|
+
end
|
29
|
+
|
30
|
+
def rolling_back?
|
31
|
+
!!@rolling_back
|
32
|
+
end
|
33
|
+
|
34
|
+
def save(options={})
|
35
|
+
options.assert_valid_keys(:updater, :version_created_at)
|
36
|
+
self.updater = options.delete(:updater)
|
37
|
+
self.version_created_at = options.delete(:version_created_at)
|
38
|
+
super
|
39
|
+
end
|
40
|
+
|
41
|
+
def push_version
|
42
|
+
unless self.changes.empty?
|
43
|
+
version = self.versions.create(_id: self.version_id, doc: version_doc, updater: self.updater)
|
44
|
+
self.generate_version_id
|
45
|
+
end
|
46
|
+
ensure
|
47
|
+
self.updater = nil
|
48
|
+
self.version_created_at = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def generate_version_id
|
52
|
+
version_id = BSON::ObjectId.new
|
53
|
+
# don't use #save; that'll generate a new version
|
54
|
+
self.write_attribute(:version_id, version_id)
|
55
|
+
self.collection.update({'_id' => self.id}, { 'version_id' => version_id })
|
56
|
+
self.changed_attributes.clear
|
57
|
+
end
|
58
|
+
|
59
|
+
def prune_versions
|
60
|
+
if self.keep_versions_created_before
|
61
|
+
self.versions.destroy_all(:created_at.lt => self.keep_versions_created_before)
|
62
|
+
elsif self.class.max_versions
|
63
|
+
limit = self.versions.count - self.class.max_versions
|
64
|
+
if limit > 0
|
65
|
+
self.versions.destroy_all(sort: 'id2 asc', limit: limit)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def keep_versions_created_before
|
71
|
+
if self.respond_to?(:keep_versions_for)
|
72
|
+
Time.now - self.keep_versions_for
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def destroy_versions
|
77
|
+
self.versions.destroy_all
|
78
|
+
end
|
79
|
+
|
80
|
+
def version_doc
|
81
|
+
{}.tap do |doc|
|
82
|
+
doc.merge!(self.attributes)
|
83
|
+
self.changes.each_pair do |attr, vals|
|
84
|
+
doc[attr] = vals.first
|
85
|
+
end
|
86
|
+
doc.delete('_id')
|
87
|
+
doc.delete('version_id')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def rollback
|
92
|
+
self.rolling_back = true
|
93
|
+
yield
|
94
|
+
ensure
|
95
|
+
self.rolling_back = false
|
96
|
+
end
|
97
|
+
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
data/test/models/post.rb
ADDED
data/test/models/user.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../lib/versioned')
|
2
|
+
|
3
|
+
require 'benchmark'
|
4
|
+
|
5
|
+
MongoMapper.database = 'versioned_performance'
|
6
|
+
|
7
|
+
class Max
|
8
|
+
include MongoMapper::Document
|
9
|
+
|
10
|
+
versioned max: 10
|
11
|
+
|
12
|
+
key :approved, Boolean
|
13
|
+
key :count, Integer
|
14
|
+
key :approved_at, Time
|
15
|
+
key :expire_on, Date
|
16
|
+
end
|
17
|
+
Max.collection.remove
|
18
|
+
|
19
|
+
class Timed
|
20
|
+
include MongoMapper::Document
|
21
|
+
|
22
|
+
versioned
|
23
|
+
|
24
|
+
key :approved, Boolean
|
25
|
+
key :count, Integer
|
26
|
+
key :approved_at, Time
|
27
|
+
key :expire_on, Date
|
28
|
+
|
29
|
+
def keep_versions_for
|
30
|
+
1.second
|
31
|
+
end
|
32
|
+
end
|
33
|
+
Timed.collection.remove
|
34
|
+
|
35
|
+
class None
|
36
|
+
include MongoMapper::Document
|
37
|
+
|
38
|
+
key :approved, Boolean
|
39
|
+
key :count, Integer
|
40
|
+
key :approved_at, Time
|
41
|
+
key :expire_on, Date
|
42
|
+
end
|
43
|
+
None.collection.remove
|
44
|
+
|
45
|
+
Benchmark.bm(28) do |x|
|
46
|
+
max_ids, timed_ids, ids = [], [], []
|
47
|
+
x.report("write with versioning (max) ") do
|
48
|
+
1000.times { |i| max_ids << Max.create(:count => 0, :approved => true, :approved_at => Time.now, :expire_on => Date.today).id }
|
49
|
+
end
|
50
|
+
x.report("write with versioning (timed)") do
|
51
|
+
1000.times { |i| timed_ids << Timed.create(:count => 0, :approved => true, :approved_at => Time.now, :expire_on => Date.today).id }
|
52
|
+
end
|
53
|
+
x.report("write without versioning ") do
|
54
|
+
1000.times { |i| ids << None.create(:count => 0, :approved => true, :approved_at => Time.now, :expire_on => Date.today).id }
|
55
|
+
end
|
56
|
+
x.report("read with versioning (max) ") do
|
57
|
+
max_ids.each { |id| Max.first(:id => id) }
|
58
|
+
end
|
59
|
+
x.report("read with versioning (timed) ") do
|
60
|
+
timed_ids.each { |id| Timed.first(:id => id) }
|
61
|
+
end
|
62
|
+
x.report("read without versioning ") do
|
63
|
+
ids.each { |id| None.first(:id => id) }
|
64
|
+
end
|
65
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
$LOAD_PATH.unshift('.') unless $LOAD_PATH.include?('.')
|
2
|
+
|
3
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/versioned')
|
4
|
+
require 'test/config/config'
|
5
|
+
|
6
|
+
require 'pp'
|
7
|
+
require 'shoulda'
|
8
|
+
require 'timecop'
|
9
|
+
|
10
|
+
require 'test/models/post'
|
11
|
+
require 'test/models/user'
|
12
|
+
|
13
|
+
|
14
|
+
def create_user
|
15
|
+
User.create!(
|
16
|
+
:name => 'Alex Wolfe',
|
17
|
+
:email => 'alexkwolfe@gmail.com',
|
18
|
+
:posts => []
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def cleanup
|
23
|
+
User.delete_all
|
24
|
+
Version.delete_all
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
|
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class VersioningTest < ActiveSupport::TestCase
|
4
|
+
setup do
|
5
|
+
User.max_versions = nil
|
6
|
+
@user = create_user
|
7
|
+
end
|
8
|
+
|
9
|
+
teardown do
|
10
|
+
cleanup
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'Versioned document' do
|
14
|
+
should 'have versions' do
|
15
|
+
assert @user.respond_to?(:versions)
|
16
|
+
assert_equal [], @user.versions
|
17
|
+
end
|
18
|
+
|
19
|
+
should 'have version id' do
|
20
|
+
assert @user.version_id.is_a?(BSON::ObjectId)
|
21
|
+
end
|
22
|
+
|
23
|
+
should 'not share version ids' do
|
24
|
+
assert_not_equal @user.version_id, create_user.version_id
|
25
|
+
end
|
26
|
+
|
27
|
+
should 'not push version on empty update' do
|
28
|
+
@user.save
|
29
|
+
assert @user.versions.empty?
|
30
|
+
end
|
31
|
+
|
32
|
+
should 'not get a new version id on empty update' do
|
33
|
+
version_id = @user.version_id
|
34
|
+
@user.save
|
35
|
+
assert_equal version_id, @user.version_id
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'that has been updated' do
|
39
|
+
setup do
|
40
|
+
@version_id = @user.version_id
|
41
|
+
@user.name = "alex"
|
42
|
+
@user.save
|
43
|
+
end
|
44
|
+
|
45
|
+
should 'push version' do
|
46
|
+
assert_equal 1, @user.versions.size
|
47
|
+
end
|
48
|
+
|
49
|
+
should 'use version id' do
|
50
|
+
assert_equal @version_id, @user.versions.first.id
|
51
|
+
end
|
52
|
+
|
53
|
+
should 'get a new version id' do
|
54
|
+
assert_not_equal @version_id, @user.version_id
|
55
|
+
end
|
56
|
+
|
57
|
+
should 'rollback to previous version' do
|
58
|
+
@user.versions.first.rollback
|
59
|
+
@user.reload
|
60
|
+
assert_equal "Alex Wolfe", @user.name
|
61
|
+
end
|
62
|
+
|
63
|
+
should 'rollback version id' do
|
64
|
+
@user.versions.first.rollback
|
65
|
+
@user.reload
|
66
|
+
assert_equal @version_id, @user.version_id
|
67
|
+
end
|
68
|
+
|
69
|
+
should 'not store doc id in version doc' do
|
70
|
+
doc = @user.versions.first.doc
|
71
|
+
assert_nil doc['id']
|
72
|
+
assert_nil doc['_id']
|
73
|
+
end
|
74
|
+
|
75
|
+
should 'not store version_id in version doc' do
|
76
|
+
doc = @user.versions.first.doc
|
77
|
+
assert_nil doc['version_id']
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'that has been updated many times' do
|
82
|
+
setup do
|
83
|
+
@version_id = @user.version_id
|
84
|
+
# Versions: Alex 4, Alex 3, Alex 2, Alex 1, Alex Wolfe
|
85
|
+
(1..5).each do |i|
|
86
|
+
@user.name = "Alex #{i}"
|
87
|
+
@user.save!
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
should 'sort versions in reverse chronological order' do
|
92
|
+
assert_equal @user.versions.to_a.sort {|x,y| y.created_at <=> x.created_at }, @user.versions.to_a
|
93
|
+
assert_equal 5, @user.versions.count
|
94
|
+
assert_equal "Alex 4", @user.versions.first.doc['name']
|
95
|
+
assert_equal "Alex Wolfe", @user.versions.last.doc['name']
|
96
|
+
end
|
97
|
+
|
98
|
+
should 'find by version id' do
|
99
|
+
version = @user.versions.find(@version_id)
|
100
|
+
assert_equal @version_id, version.id
|
101
|
+
assert_equal 'Alex Wolfe', version.doc['name']
|
102
|
+
end
|
103
|
+
|
104
|
+
context 'and has been rolled back' do
|
105
|
+
setup do
|
106
|
+
# second to oldest
|
107
|
+
@version = @user.versions[2]
|
108
|
+
@version.rollback
|
109
|
+
@user.reload
|
110
|
+
end
|
111
|
+
|
112
|
+
should 'have original data' do
|
113
|
+
assert_equal "Alex 2", @user.name
|
114
|
+
end
|
115
|
+
|
116
|
+
should 'have original version id' do
|
117
|
+
assert_equal @version.id, @user.version_id
|
118
|
+
end
|
119
|
+
|
120
|
+
should 'have correct versions' do
|
121
|
+
assert_equal 2, @user.versions.count
|
122
|
+
assert_equal 'Alex 1', @user.versions.first.doc['name']
|
123
|
+
assert_equal 'Alex Wolfe', @user.versions.last.doc['name']
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context 'that has been updated by a user' do
|
129
|
+
setup do
|
130
|
+
@updater = User.create!(name: 'Bobby Brown', email: 'bobbeh@brown.com')
|
131
|
+
@user.name = "alex"
|
132
|
+
@user.save(updater: @updater)
|
133
|
+
end
|
134
|
+
|
135
|
+
should 'store user with version' do
|
136
|
+
assert_equal @updater, @user.versions.first.updater
|
137
|
+
end
|
138
|
+
|
139
|
+
should 'forget user after save' do
|
140
|
+
assert @user.updater.nil?
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'Destroyed versioned document' do
|
146
|
+
setup do
|
147
|
+
(1..5).each do |i|
|
148
|
+
@user.name = "#{@user.name} #{i}"
|
149
|
+
@user.save!
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
should 'destroy versions' do
|
154
|
+
@user.destroy
|
155
|
+
assert_equal 0, Version.count
|
156
|
+
end
|
157
|
+
|
158
|
+
should 'not destroy versions of other documents' do
|
159
|
+
assert_equal 5, Version.count
|
160
|
+
@user2 = create_user
|
161
|
+
@user2.name = "Foo"
|
162
|
+
@user2.save
|
163
|
+
@user2.name = "Bar"
|
164
|
+
@user2.save
|
165
|
+
|
166
|
+
assert_equal 7, Version.count
|
167
|
+
|
168
|
+
@user.destroy
|
169
|
+
|
170
|
+
assert_equal 2, Version.count
|
171
|
+
assert_equal 2, @user2.versions.count
|
172
|
+
assert_equal "Foo", @user2.versions.first.doc['name']
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
context 'Versioned document with max versions' do
|
177
|
+
setup do
|
178
|
+
User.max_versions = 5
|
179
|
+
(1..21).each do |i|
|
180
|
+
@user.name = "Alex #{i}"
|
181
|
+
@user.save!
|
182
|
+
end
|
183
|
+
@user.reload
|
184
|
+
end
|
185
|
+
|
186
|
+
should 'have no more than max' do
|
187
|
+
assert_equal 5, @user.versions.count
|
188
|
+
assert_equal ["Alex 20", "Alex 19", "Alex 18", "Alex 17", "Alex 16"], @user.versions.collect{|v| v.doc['name']}
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
context 'Versioned document with time limit' do
|
193
|
+
setup do
|
194
|
+
class << @user
|
195
|
+
def keep_versions_for
|
196
|
+
5.minutes
|
197
|
+
end
|
198
|
+
end
|
199
|
+
(1..20).each do |i|
|
200
|
+
Timecop.freeze(i.minutes.ago) do
|
201
|
+
@user.name = "Alex #{i}"
|
202
|
+
@user.save!
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
should 'calculate time threshold' do
|
208
|
+
assert_equal (Time.now.utc - 5.minutes).to_i, @user.keep_versions_created_before.to_i
|
209
|
+
end
|
210
|
+
|
211
|
+
should 'prune documents older than time limit' do
|
212
|
+
@user.prune_versions
|
213
|
+
assert_equal 4, @user.versions.count
|
214
|
+
@user.versions.each do |v|
|
215
|
+
assert v.created_at >= @user.keep_versions_created_before
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
should "not prune other documents' versions" do
|
220
|
+
Timecop.freeze(10.minutes.ago) do
|
221
|
+
@user2 = create_user
|
222
|
+
@user2.name = "Foo Bar"
|
223
|
+
@user2.save!
|
224
|
+
end
|
225
|
+
@user.prune_versions
|
226
|
+
assert_equal 1, @user2.versions.count
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
metadata
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mongomapper-versioned
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alex Wolfe
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-01-16 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: &70092868791380 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.1.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70092868791380
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: bson_ext
|
27
|
+
requirement: &70092868790820 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70092868790820
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: mongo_mapper
|
38
|
+
requirement: &70092868790140 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.10.1
|
44
|
+
type: :runtime
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70092868790140
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: mongomapper_id2
|
49
|
+
requirement: &70092868789500 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70092868789500
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: rake
|
60
|
+
requirement: &70092868788360 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0'
|
66
|
+
type: :development
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70092868788360
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: shoulda
|
71
|
+
requirement: &70092868787940 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: *70092868787940
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: ruby-debug19
|
82
|
+
requirement: &70092868787380 !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: *70092868787380
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: timecop
|
93
|
+
requirement: &70092868786800 !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: *70092868786800
|
102
|
+
description:
|
103
|
+
email: alexkwolfe@gmail.com
|
104
|
+
executables: []
|
105
|
+
extensions: []
|
106
|
+
extra_rdoc_files: []
|
107
|
+
files:
|
108
|
+
- lib/mongomapper-versioned.rb
|
109
|
+
- lib/tasks/versioned.rake
|
110
|
+
- lib/versioned/meta.rb
|
111
|
+
- lib/versioned/version.rb
|
112
|
+
- lib/versioned/versioned.rb
|
113
|
+
- lib/versioned.rb
|
114
|
+
- Gemfile
|
115
|
+
- Rakefile
|
116
|
+
- README.md
|
117
|
+
- test/config/config.rb
|
118
|
+
- test/config/database.yml
|
119
|
+
- test/models/post.rb
|
120
|
+
- test/models/user.rb
|
121
|
+
- test/performance/read_write.rb
|
122
|
+
- test/test_helper.rb
|
123
|
+
- test/unit/test_versioning.rb
|
124
|
+
homepage: http://github.com/alexkwolfe/mongomapper-versioned
|
125
|
+
licenses: []
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
none: false
|
132
|
+
requirements:
|
133
|
+
- - ! '>='
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
137
|
+
none: false
|
138
|
+
requirements:
|
139
|
+
- - ! '>='
|
140
|
+
- !ruby/object:Gem::Version
|
141
|
+
version: '0'
|
142
|
+
requirements: []
|
143
|
+
rubyforge_project:
|
144
|
+
rubygems_version: 1.8.15
|
145
|
+
signing_key:
|
146
|
+
specification_version: 3
|
147
|
+
summary: A MongoMapper extension adding Versioning
|
148
|
+
test_files:
|
149
|
+
- test/config/config.rb
|
150
|
+
- test/config/database.yml
|
151
|
+
- test/models/post.rb
|
152
|
+
- test/models/user.rb
|
153
|
+
- test/performance/read_write.rb
|
154
|
+
- test/test_helper.rb
|
155
|
+
- test/unit/test_versioning.rb
|