forked from datamapper/dm-is-versioned
/
versioned.rb
158 lines (139 loc) · 5.03 KB
/
versioned.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
module DataMapper
module Is
##
# = Is Versioned
# The Versioned module will configure a model to be versioned.
#
# The is-versioned plugin functions differently from other versioning
# solutions (such as acts_as_versioned), but can be configured to
# function like it if you so desire.
#
# The biggest difference is that there is not an incrementing 'version'
# field, but rather, any field of your choosing which will be unique
# on update.
#
# == Setup
# For simplicity, I will assume that you have loaded dm-timestamps to
# automatically update your :updated_at field. See versioned_spec for
# and example of updating the versioned field yourself.
#
# class Story
# include DataMapper::Resource
# property :id, Serial
# property :title, String
# property :updated_at, DateTime
#
# is_versioned :on => [:updated_at]
# end
#
# == Auto Upgrading and Auto Migrating
#
# Story.auto_migrate! # => will run auto_migrate! on Story::Version, too
# Story.auto_upgrade! # => will run auto_upgrade! on Story::Version, too
#
# == Usage
#
# story = Story.get(1)
# story.title = "New Title"
# story.save # => Saves this story and creates a new version with the
# # original values.
# story.versions.size # => 1
#
# story.title = "A Different New Title"
# story.save
# story.versions.size # => 2
#
# TODO: enable replacing a current version with an old version.
module Versioned
def is_versioned(options = {})
@is_versioned_on = options[:on]
@is_versioned_ignore = options[:ignore]
extend(Migration) if respond_to?(:auto_migrate!)
before :save do
if dirty? && !new?
@pending_version_attributes = {}
original_attributes.each do |k,v|
# Skip associations
unless k.is_a? DataMapper::Associations::Relationship
@pending_version_attributes[k.name] = v
end
end
else
@pending_version_attributes = nil
end
end
after :save do
if clean? && @pending_version_attributes
version_attributes = attributes.merge(@pending_version_attributes)
# Check if we were asked to ignore certain attributes when checking for changes
ignore = self.class.instance_variable_get(:@is_versioned_ignore)
if ignore
wanted_keys = attributes.keys - ignore
# Compare old and new attributes while ignoring some attributes
if version_attributes.values_at(*wanted_keys) != attributes.values_at(*wanted_keys)
model::Version.create!(version_attributes)
end
else
# Assume DataMapper already figured out whether anything changed
model::Version.create!(version_attributes)
end
end
@pending_version_attributes = nil
end
extend ClassMethods
include InstanceMethods
end
module ClassMethods
def const_missing(name)
if name == :Version
model = DataMapper::Model.new(name, self)
properties.each do |property|
type = case property
when DataMapper::Property::Discriminator then Class
when DataMapper::Property::Serial then Integer
else
property.class
end
options = property.options.merge(:key => property.name == @is_versioned_on)
# Replace keys for plain indices
options[:index] = true if options[:key]
# these options are dangerous and break the versioning system
[:unique, :key, :serial].each { |option| options.delete(option) }
model.property(property.name, type, options)
end
model.property('is_versioned_id', DataMapper::Property::Serial, :key => true)
model.finalize
model
else
super
end
end
end # ClassMethods
module InstanceMethods
##
# Returns a collection of other versions of this resource.
# The versions are related on the models keys, and ordered
# by the version field.
#
# --
# @return <Collection>
def versions
version_model = model.const_get(:Version)
query = Hash[ model.key.zip(key).map { |p, v| [ p.name, v ] } ]
query.merge!(:order => :is_versioned_id.desc)
version_model.all(query)
end
end # InstanceMethods
module Migration
def auto_migrate!(repository_name = self.repository_name)
super
self::Version.auto_migrate!
end
def auto_upgrade!(repository_name = self.repository_name)
super
self::Version.auto_upgrade!
end
end # Migration
end # Versioned
end # Is
end # DataMapper