versioned_record 0.2.0 → 0.2.1
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 +29 -2
- data/lib/versioned_record.rb +86 -75
- data/lib/versioned_record/attribute_builder.rb +3 -3
- data/lib/versioned_record/composite_predicates.rb +40 -0
- data/lib/versioned_record/version.rb +1 -1
- data/spec/spec_helper.rb +2 -0
- data/spec/versions_spec.rb +1 -1
- data/versioned_record.gemspec +1 -0
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8d961aed7862c149a656c89584e5e39a36eab865
|
4
|
+
data.tar.gz: efac3b9ce83e333511c5dba682d0ca4b238384b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 496b5f7cc0c2d15098bf17b5316253f128e1a7442bb1143c7f6bfdf781586f1b5abbbe1f42b827660d3b79424a4f483089bde85114bfe50de3387f39310ed56a
|
7
|
+
data.tar.gz: 3ff2e0d96885ea548736e846dd77e8385c9448850affd79d8967905ff227933b7400e53d5e85b83a3bc1ffd44bc5597af6f0d4f339b7f1df740e768a4aebfd0c
|
data/README.md
CHANGED
@@ -70,7 +70,32 @@ Also, the `is_current_version` flag is unset for the old version and set for the
|
|
70
70
|
product.is_current_version? => false
|
71
71
|
product_v2.is_current_version? => true
|
72
72
|
|
73
|
-
##
|
73
|
+
## Associations
|
74
|
+
|
75
|
+
### `belongs_to` A versioned model, but references the latest version (general case)
|
76
|
+
|
77
|
+
A simple `belongs_to` will work
|
78
|
+
|
79
|
+
### `belongs_to` A versioned model but references a specific version
|
80
|
+
|
81
|
+
`belongs_to` must specify the `foreign_key` and primary key settings
|
82
|
+
|
83
|
+
class Contract < ActiveRecord::Base
|
84
|
+
include VersionedRecord
|
85
|
+
has_many :apprentices, :foreign_key => [:contract_id, :contract_version], primary_key: [:id, :version ]
|
86
|
+
end
|
87
|
+
|
88
|
+
class Apprentice < ActiveRecord::Base
|
89
|
+
belongs_to :contract, :foreign_key => [:contract_id, :contract_version], primary_key: [ :id, version ]
|
90
|
+
end
|
91
|
+
|
92
|
+
### `belongs_to` a non-versioned model
|
93
|
+
|
94
|
+
Normal behaviour
|
95
|
+
|
96
|
+
### `belongs_to` when this model is version
|
97
|
+
|
98
|
+
As per above cases (versioning here has no effect)
|
74
99
|
|
75
100
|
|
76
101
|
## Database Support
|
@@ -79,7 +104,9 @@ Right now, only PostgreSQL has been tested. MySQL may or may not work but if you
|
|
79
104
|
|
80
105
|
## Limitations
|
81
106
|
|
82
|
-
Does not currently work with ActiveRecord 4.1+
|
107
|
+
* Does not currently work with ActiveRecord 4.1+
|
108
|
+
* Calling reload on a model will load the latest _version_ of that record, not the specific one. (This is because id will return just the id and not the id/version composite.)
|
109
|
+
|
83
110
|
|
84
111
|
## Author
|
85
112
|
|
data/lib/versioned_record.rb
CHANGED
@@ -7,102 +7,113 @@ require 'versioned_record/attribute_builder'
|
|
7
7
|
require 'versioned_record/class_methods'
|
8
8
|
require 'versioned_record/connection_adapters/postgresql'
|
9
9
|
require 'versioned_record/version'
|
10
|
+
require 'versioned_record/composite_predicates'
|
11
|
+
|
12
|
+
ActiveRecord::Associations::AssociationScope.include(VersionedRecord::CompositePredicates)
|
10
13
|
|
11
14
|
module VersionedRecord
|
12
15
|
def self.included(model_class)
|
13
|
-
model_class.extend ClassMethods
|
14
16
|
model_class.primary_keys = :id, :version
|
15
17
|
model_class.after_save :ensure_version_deprecation!, on: :create
|
18
|
+
model_class.send :alias_method, :id_with_version, :id
|
19
|
+
model_class.extend ClassMethods
|
20
|
+
model_class.include InstanceMethods
|
16
21
|
end
|
17
22
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
#
|
22
|
-
# Attributes that are not specified here will be copied to the new version from
|
23
|
-
# the previous version
|
24
|
-
#
|
25
|
-
# This method will still fire ActiveRecord callbacks for save/create etc as per
|
26
|
-
# normal record creation
|
27
|
-
#
|
28
|
-
# @example
|
29
|
-
#
|
30
|
-
# person_v1 = Person.create(name: 'Dan')
|
31
|
-
# person_v2 = person_v1.create_version!(name: 'Daniel')
|
32
|
-
#
|
33
|
-
def create_version!(new_attrs = {})
|
34
|
-
create_operation do
|
35
|
-
self.class.create!(new_version_attrs(new_attrs))
|
23
|
+
module InstanceMethods
|
24
|
+
def id
|
25
|
+
id_with_version[0]
|
36
26
|
end
|
37
|
-
end
|
38
27
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
28
|
+
# Create a new version of the existing record
|
29
|
+
# A new version can only be created once for a given record and subsequent
|
30
|
+
# versions must be created by calling create_version! on the latest version
|
31
|
+
#
|
32
|
+
# Attributes that are not specified here will be copied to the new version from
|
33
|
+
# the previous version
|
34
|
+
#
|
35
|
+
# This method will still fire ActiveRecord callbacks for save/create etc as per
|
36
|
+
# normal record creation
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
#
|
40
|
+
# person_v1 = Person.create(name: 'Dan')
|
41
|
+
# person_v2 = person_v1.create_version!(name: 'Daniel')
|
42
|
+
#
|
43
|
+
def create_version!(new_attrs = {})
|
44
|
+
create_operation do
|
45
|
+
self.class.create!(new_version_attrs(new_attrs))
|
46
|
+
end
|
45
47
|
end
|
46
|
-
end
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
# new_version = first_version.build_version
|
56
|
-
# new_version.save
|
57
|
-
#
|
58
|
-
def build_version(new_attrs = {})
|
59
|
-
new_version = self.class.new(new_version_attrs(new_attrs)).tap do |built|
|
60
|
-
built.deprecate_old_versions_after_create!
|
49
|
+
# Same as #create_version! but will not raise if the record is invalid
|
50
|
+
# @see VersionedRecord#create_version!
|
51
|
+
#
|
52
|
+
def create_version(new_attrs = {})
|
53
|
+
create_operation do
|
54
|
+
self.class.create(new_version_attrs(new_attrs))
|
55
|
+
end
|
61
56
|
end
|
62
|
-
end
|
63
|
-
|
64
|
-
# Retrieve all versions of this record
|
65
|
-
# Can be chained with other scopes
|
66
|
-
#
|
67
|
-
# @example Versions ordered by version number
|
68
|
-
#
|
69
|
-
# person.versions.order(:version)
|
70
|
-
#
|
71
|
-
def versions
|
72
|
-
self.class.where(id: self.id)
|
73
|
-
end
|
74
57
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
58
|
+
# Build (but do not save) a new version of the record
|
59
|
+
# This allows you to use the object in forms etc
|
60
|
+
# After the record is saved, all previous versions will be deprecated
|
61
|
+
# and this record will be marked as current
|
62
|
+
#
|
63
|
+
# @example
|
64
|
+
#
|
65
|
+
# new_version = first_version.build_version
|
66
|
+
# new_version.save
|
67
|
+
#
|
68
|
+
def build_version(new_attrs = {})
|
69
|
+
new_version = self.class.new(new_version_attrs(new_attrs)).tap do |built|
|
70
|
+
built.deprecate_old_versions_after_create!
|
71
|
+
end
|
72
|
+
end
|
80
73
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
74
|
+
# Retrieve all versions of this record
|
75
|
+
# Can be chained with other scopes
|
76
|
+
#
|
77
|
+
# @example Versions ordered by version number
|
78
|
+
#
|
79
|
+
# person.versions.order(:version)
|
80
|
+
#
|
81
|
+
def versions
|
82
|
+
self.class.where(id: self.id)
|
85
83
|
end
|
86
84
|
|
87
|
-
|
88
|
-
|
85
|
+
# Ensure that old versions are deprecated when we save
|
86
|
+
# (only applies on create)
|
87
|
+
def deprecate_old_versions_after_create!
|
88
|
+
@deprecate_old_versions_after_create = true
|
89
89
|
end
|
90
90
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
91
|
+
private
|
92
|
+
def new_version_attrs(new_attrs)
|
93
|
+
attr_builder = AttributeBuilder.new(self)
|
94
|
+
attr_builder.attributes(new_attrs)
|
95
|
+
end
|
96
|
+
|
97
|
+
def deprecate_old_versions(current_version)
|
98
|
+
versions.exclude(current_version).update_all(is_current_version: false)
|
99
|
+
end
|
100
|
+
|
101
|
+
def create_operation
|
102
|
+
self.class.transaction do
|
103
|
+
yield.tap do |created|
|
104
|
+
deprecate_old_versions(created) if created.persisted?
|
105
|
+
end
|
95
106
|
end
|
96
107
|
end
|
97
|
-
end
|
98
108
|
|
99
|
-
|
100
|
-
|
101
|
-
|
109
|
+
def deprecate_old_versions_after_create?
|
110
|
+
@deprecate_old_versions_after_create
|
111
|
+
end
|
102
112
|
|
103
|
-
|
104
|
-
|
105
|
-
|
113
|
+
def ensure_version_deprecation!
|
114
|
+
if deprecate_old_versions_after_create?
|
115
|
+
deprecate_old_versions(self)
|
116
|
+
end
|
106
117
|
end
|
107
|
-
|
118
|
+
end
|
108
119
|
end
|
@@ -14,10 +14,10 @@ module VersionedRecord
|
|
14
14
|
#
|
15
15
|
# @param [Hash] new_attrs are the attributes we are changing in this version
|
16
16
|
def attributes(new_attrs)
|
17
|
-
@new_attrs = new_attrs.
|
18
|
-
@record.attributes.merge(@new_attrs.merge({
|
17
|
+
@new_attrs = new_attrs.symbolize_keys
|
18
|
+
attrs = @record.attributes.symbolize_keys.merge(@new_attrs.merge({
|
19
19
|
is_current_version: true,
|
20
|
-
id: @record.
|
20
|
+
id: @record.id_with_version,
|
21
21
|
version: @record.version + 1
|
22
22
|
}))
|
23
23
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module VersionedRecord
|
2
|
+
module CompositePredicates
|
3
|
+
|
4
|
+
# Lets say location belongs to a company and company is versioned
|
5
|
+
#
|
6
|
+
# class Company < ActiveRecord::Base
|
7
|
+
# include VersionedRecord
|
8
|
+
# has_many :locations
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# class Location < ActiveRecord::Base
|
12
|
+
# belongs_to :company
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# When we want to load the company from the location we do:
|
16
|
+
#
|
17
|
+
# location.company
|
18
|
+
#
|
19
|
+
# This will do:
|
20
|
+
#
|
21
|
+
# Company.where(id: location.company.id, is_current_version: true)
|
22
|
+
#
|
23
|
+
# @param [Arel::Table] association_table the associated_table (in the example, company)
|
24
|
+
# @param [Array,String] association_key the value of the pkey of associated_table (in the example, company)
|
25
|
+
# @param [Arel::Table] table (in the example, location)
|
26
|
+
# @param [Array,String] key reference to the association
|
27
|
+
#
|
28
|
+
def cpk_join_predicate(association_table, association_key, table, key)
|
29
|
+
fields = Array(key).map { |key| table[key] }
|
30
|
+
association_fields = Array(association_key).map { |key| association_table[key] }
|
31
|
+
|
32
|
+
if fields.size == 1
|
33
|
+
eq_predicates = [ association_fields[0].eq(fields[0]), association_table[:is_current_version].eq(true) ]
|
34
|
+
cpk_and_predicate(eq_predicates)
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/versions_spec.rb
CHANGED
@@ -10,7 +10,7 @@ describe VersionedProduct do
|
|
10
10
|
|
11
11
|
it 'unsets current version on first version' do
|
12
12
|
perform
|
13
|
-
expect(first_version.
|
13
|
+
expect(VersionedProduct.find(first_version.id, 0)).to_not be_is_current_version
|
14
14
|
end
|
15
15
|
|
16
16
|
describe 'the new version' do
|
data/versioned_record.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: versioned_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dan Draper
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-04-
|
11
|
+
date: 2014-04-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -136,6 +136,20 @@ dependencies:
|
|
136
136
|
- - ">="
|
137
137
|
- !ruby/object:Gem::Version
|
138
138
|
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: byebug
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :development
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - ">="
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
139
153
|
description: Version ActiveRecord models using composite primary keys
|
140
154
|
email:
|
141
155
|
- daniel@codehire.com
|
@@ -155,6 +169,7 @@ files:
|
|
155
169
|
- lib/versioned_record.rb
|
156
170
|
- lib/versioned_record/attribute_builder.rb
|
157
171
|
- lib/versioned_record/class_methods.rb
|
172
|
+
- lib/versioned_record/composite_predicates.rb
|
158
173
|
- lib/versioned_record/connection_adapters/postgresql.rb
|
159
174
|
- lib/versioned_record/version.rb
|
160
175
|
- spec/spec_helper.rb
|