hoardable 0.1.0 → 0.1.3
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/Gemfile +3 -2
- data/README.md +130 -46
- data/lib/generators/hoardable/migration_generator.rb +19 -2
- data/lib/generators/hoardable/templates/migration.rb.erb +9 -2
- data/lib/generators/hoardable/templates/migration_6.rb.erb +34 -0
- data/lib/hoardable/error.rb +6 -0
- data/lib/hoardable/hoardable.rb +21 -3
- data/lib/hoardable/model.rb +12 -60
- data/lib/hoardable/source_model.rb +133 -0
- data/lib/hoardable/tableoid.rb +47 -0
- data/lib/hoardable/version.rb +5 -0
- data/lib/hoardable/version_model.rb +75 -14
- data/lib/hoardable.rb +4 -0
- metadata +17 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96952d8928266fc5ae03199a91e85428565ff11385a74851fe61fd3a8eafc881
|
|
4
|
+
data.tar.gz: 1b9117cbf4ea08325d29212c16580e368a857b12f8c2d20bc346d9e8f5268d59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e533edf9412339fcc90691fa5d10395265914f71f7f7493c9afc226902cf831db87f69661d565630602e2db815549e4209550420abc44f6a7d179ccb23ba7509
|
|
7
|
+
data.tar.gz: a8c690b0a3399f3853ed6c65ca7d514af9c82d10569483d7d4bed86c6e7a63d1881ecac61e13d6646facdce3370614dc6c86f88599bc6eb955feba38589c398c
|
data/Gemfile
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
source 'https://rubygems.org'
|
|
4
4
|
|
|
5
|
-
gemspec
|
|
6
|
-
|
|
7
5
|
gem 'debug', '~> 1.6'
|
|
8
6
|
gem 'minitest', '~> 5.0'
|
|
9
7
|
gem 'rake', '~> 13.0'
|
|
10
8
|
gem 'rubocop', '~> 1.21'
|
|
11
9
|
gem 'rubocop-minitest', '~> 0.20'
|
|
12
10
|
gem 'rubocop-rake', '~> 0.6'
|
|
11
|
+
gem 'yard', '~> 0.9'
|
|
12
|
+
|
|
13
|
+
gemspec
|
data/README.md
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
# Hoardable
|
|
1
|
+
# Hoardable 
|
|
2
2
|
|
|
3
3
|
Hoardable is an ActiveRecord extension for Ruby 2.6+, Rails 6.1+, and PostgreSQL that allows for
|
|
4
|
-
versioning and soft-deletion of records through the use of
|
|
4
|
+
versioning and soft-deletion of records through the use of _uni-temporal inherited tables_.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
[👉 Documentation](https://www.rubydoc.info/gems/hoardable)
|
|
7
|
+
|
|
8
|
+
### huh?
|
|
7
9
|
|
|
8
10
|
[Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern
|
|
9
|
-
where each row contains data
|
|
10
|
-
|
|
11
|
+
where each row of a table contains data along with one or more time ranges. In the case of this gem,
|
|
12
|
+
each database row has a time range that represents the row’s valid time range - hence
|
|
11
13
|
"uni-temporal".
|
|
12
14
|
|
|
13
15
|
[Table inheritance](https://www.postgresql.org/docs/14/ddl-inherit.html) is a feature of PostgreSQL
|
|
14
|
-
that allows a table to inherit all columns of
|
|
15
|
-
in sync with
|
|
16
|
-
|
|
16
|
+
that allows a table to inherit all columns of a parent table. The descendant table’s schema will
|
|
17
|
+
stay in sync with its parent. If a new column is added to or removed from the parent, the schema
|
|
18
|
+
change is reflected on its descendants.
|
|
17
19
|
|
|
18
|
-
With these
|
|
19
|
-
|
|
20
|
-
other versioning
|
|
20
|
+
With these concepts combined, `hoardable` offers a simple and effective model versioning system for
|
|
21
|
+
Rails. Versions of records are stored in separate, inherited tables along with their valid time
|
|
22
|
+
ranges and contextual data. Compared to other Rails-oriented versioning systems, this gem strives to
|
|
23
|
+
be more explicit and obvious on the lower RDBS level while still familiar and convenient within Ruby
|
|
24
|
+
on Rails.
|
|
21
25
|
|
|
22
26
|
## Installation
|
|
23
27
|
|
|
@@ -31,68 +35,88 @@ And then execute `bundle install`.
|
|
|
31
35
|
|
|
32
36
|
### Model Installation
|
|
33
37
|
|
|
34
|
-
|
|
38
|
+
You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions
|
|
39
|
+
of:
|
|
35
40
|
|
|
36
41
|
```ruby
|
|
37
42
|
class Post < ActiveRecord::Base
|
|
38
43
|
include Hoardable::Model
|
|
39
44
|
belongs_to :user
|
|
45
|
+
has_many :comments, dependent: :destroy
|
|
46
|
+
...
|
|
40
47
|
end
|
|
41
48
|
```
|
|
42
49
|
|
|
43
50
|
Then, run the generator command to create a database migration and migrate it:
|
|
44
51
|
|
|
45
52
|
```
|
|
46
|
-
bin/rails g hoardable:migration
|
|
53
|
+
bin/rails g hoardable:migration Post
|
|
47
54
|
bin/rails db:migrate
|
|
48
55
|
```
|
|
49
56
|
|
|
57
|
+
By default, it will try to guess the foreign key type for the `_versions` table based on the primary
|
|
58
|
+
key of the model specified in the migration generator above. If you want/need to specify this
|
|
59
|
+
explicitly, you can do so:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
bin/rails g hoardable:migration Post --foreign-key-type uuid
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
_Note:_ If you are on Rails 6.1, you might want to set `config.active_record.schema_format = :sql`
|
|
66
|
+
in `application.rb`, so that the enum type is captured in your schema dump. This is not required in
|
|
67
|
+
Rails 7.
|
|
68
|
+
|
|
50
69
|
## Usage
|
|
51
70
|
|
|
52
|
-
###
|
|
71
|
+
### Overview
|
|
53
72
|
|
|
54
73
|
Once you include `Hoardable::Model` into a model, it will dynamically generate a "Version" subclass
|
|
55
|
-
of that model.
|
|
74
|
+
of that model. As we continue our example above, :
|
|
56
75
|
|
|
57
76
|
```
|
|
77
|
+
$ irb
|
|
58
78
|
>> Post
|
|
59
79
|
=> Post(id: integer, body: text, user_id: integer, created_at: datetime)
|
|
60
80
|
>> PostVersion
|
|
61
81
|
=> PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, _data: jsonb, _during: tsrange, post_id: integer)
|
|
62
82
|
```
|
|
63
83
|
|
|
64
|
-
A `Post` now `has_many :versions
|
|
65
|
-
default):
|
|
84
|
+
A `Post` now `has_many :versions`. Whenever an update and deletion of a `Post` occurs, a version is
|
|
85
|
+
created (by default):
|
|
66
86
|
|
|
67
87
|
```ruby
|
|
68
|
-
|
|
88
|
+
post = Post.create!(title: "Title")
|
|
69
89
|
post.versions.size # => 0
|
|
70
|
-
post.update!(title: "Title")
|
|
90
|
+
post.update!(title: "Revised Title")
|
|
71
91
|
post.versions.size # => 1
|
|
92
|
+
post.versions.first.title # => "Title"
|
|
72
93
|
post.destroy!
|
|
73
|
-
post.
|
|
74
|
-
|
|
94
|
+
post.trashed? # true
|
|
95
|
+
post.versions.size # => 2
|
|
96
|
+
Post.find(post.id) # raises ActiveRecord::RecordNotFound
|
|
75
97
|
```
|
|
76
98
|
|
|
77
99
|
Each `PostVersion` has access to the same attributes, relationships, and other model behavior that
|
|
78
|
-
`Post` has, but
|
|
100
|
+
`Post` has, but as a read-only record.
|
|
79
101
|
|
|
80
|
-
If you ever need to revert to a specific version, you can call `version.revert!` on it. If
|
|
81
|
-
|
|
102
|
+
If you ever need to revert to a specific version, you can call `version.revert!` on it. If you would
|
|
103
|
+
like to untrash a specific version, you can call `version.untrash!` on it. This will re-insert the
|
|
104
|
+
model in the parent class’ table with it’s original primary key.
|
|
82
105
|
|
|
83
106
|
### Querying and Temporal Lookup
|
|
84
107
|
|
|
85
|
-
Since a `PostVersion` is
|
|
86
|
-
resource, i.e:
|
|
108
|
+
Since a `PostVersion` is an `ActiveRecord` class, you can query them like another model resource:
|
|
87
109
|
|
|
88
110
|
```ruby
|
|
89
|
-
post.versions.where(user_id: Current.user.id, body:
|
|
111
|
+
post.versions.where(user_id: Current.user.id, body: "Cool!")
|
|
90
112
|
```
|
|
91
113
|
|
|
92
|
-
If you want to look-up the version of a
|
|
114
|
+
If you want to look-up the version of a record at a specific time, you can use the `.at` method:
|
|
93
115
|
|
|
94
116
|
```ruby
|
|
95
117
|
post.at(1.day.ago) # => #<PostVersion:0x000000010d44fa30>
|
|
118
|
+
# or
|
|
119
|
+
PostVersion.at(1.day.ago).find_by(post_id: post.id) # => #<PostVersion:0x000000010d44fa30>
|
|
96
120
|
```
|
|
97
121
|
|
|
98
122
|
By default, `hoardable` will keep copies of records you have destroyed. You can query for them as
|
|
@@ -103,26 +127,27 @@ PostVersion.trashed
|
|
|
103
127
|
```
|
|
104
128
|
|
|
105
129
|
_Note:_ Creating an inherited table does not copy over the indexes from the parent table. If you
|
|
106
|
-
need to query versions often, you
|
|
130
|
+
need to query versions often, you should add appropriate indexes to the `_versions` tables.
|
|
107
131
|
|
|
108
132
|
### Tracking contextual data
|
|
109
133
|
|
|
110
|
-
You’ll often want to track contextual data about a version.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
There are also 3 other optional keys that are provided for tracking contextual information:
|
|
134
|
+
You’ll often want to track contextual data about the creation of a version. There are 3 optional
|
|
135
|
+
symbol keys that are provided for tracking contextual information:
|
|
114
136
|
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
-
|
|
137
|
+
- `:whodunit` - an identifier for who is responsible for creating the version
|
|
138
|
+
- `:note` - a description regarding the versioning
|
|
139
|
+
- `:meta` - any other contextual information you’d like to store along with the version
|
|
118
140
|
|
|
119
141
|
This information is stored in a `jsonb` column. Each key’s value can be in the format of your
|
|
120
142
|
choosing.
|
|
121
143
|
|
|
122
|
-
One convenient way to assign
|
|
144
|
+
One convenient way to assign contextual data to these is by defining a proc in an initializer, i.e.:
|
|
123
145
|
|
|
124
146
|
```ruby
|
|
125
147
|
Hoardable.whodunit = -> { Current.user&.id }
|
|
148
|
+
Current.user = User.find(123)
|
|
149
|
+
post.update!(status: 'live')
|
|
150
|
+
post.versions.last.hoardable_whodunit # => 123
|
|
126
151
|
```
|
|
127
152
|
|
|
128
153
|
You can also set this context manually as well, just remember to clear them afterwards.
|
|
@@ -134,7 +159,7 @@ Hoardable.note = nil
|
|
|
134
159
|
post.versions.last.hoardable_note # => "reverting due to accidental deletion"
|
|
135
160
|
```
|
|
136
161
|
|
|
137
|
-
|
|
162
|
+
A more useful pattern is to use `Hoardable.with` to set the context around a block. A good example
|
|
138
163
|
of this would be in `ApplicationController`:
|
|
139
164
|
|
|
140
165
|
```ruby
|
|
@@ -147,38 +172,59 @@ class ApplicationController < ActionController::Base
|
|
|
147
172
|
Hoardable.with(whodunit: current_user.id, meta: { request_uuid: request.uuid }) do
|
|
148
173
|
yield
|
|
149
174
|
end
|
|
175
|
+
# `Hoardable.whodunit` is back to nil or the previously set value
|
|
150
176
|
end
|
|
151
177
|
end
|
|
152
178
|
```
|
|
153
179
|
|
|
180
|
+
`hoardable` will also automatically capture the ActiveRecord
|
|
181
|
+
[changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash, the
|
|
182
|
+
`operation` that cause the version (`update` or `delete`), and it will also tag all versions created
|
|
183
|
+
in the same database transaction with a shared and unique `event_uuid`. These are available as:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
version.changes
|
|
187
|
+
version.hoardable_operation
|
|
188
|
+
version.hoardable_event_uuid
|
|
189
|
+
```
|
|
190
|
+
|
|
154
191
|
### Model Callbacks
|
|
155
192
|
|
|
156
|
-
Sometimes you might want to do something with a version before it gets
|
|
157
|
-
|
|
158
|
-
|
|
193
|
+
Sometimes you might want to do something with a version before or after it gets inserted to the
|
|
194
|
+
database. You can access it in `before/after/around_versioned` callbacks on the source record as
|
|
195
|
+
`hoardable_version`. These happen around `.save`, which is enclosed in an ActiveRecord transaction.
|
|
159
196
|
|
|
160
|
-
|
|
197
|
+
There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called
|
|
198
|
+
on the source record after a version is reverted or untrashed.
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
161
201
|
class User
|
|
162
|
-
|
|
202
|
+
include Hoardable::Model
|
|
203
|
+
before_versioned :sanitize_version
|
|
163
204
|
after_reverted :track_reverted_event
|
|
205
|
+
after_untrashed :track_untrashed_event
|
|
164
206
|
|
|
165
207
|
private
|
|
166
208
|
|
|
167
209
|
def sanitize_version
|
|
168
210
|
hoardable_version.sanitize_password
|
|
169
|
-
end
|
|
211
|
+
end
|
|
170
212
|
|
|
171
213
|
def track_reverted_event
|
|
172
214
|
track_event(:user_reverted, self)
|
|
173
215
|
end
|
|
216
|
+
|
|
217
|
+
def track_untrashed_event
|
|
218
|
+
track_event(:user_untrashed, self)
|
|
219
|
+
end
|
|
174
220
|
end
|
|
175
221
|
```
|
|
176
222
|
|
|
177
223
|
### Configuration
|
|
178
224
|
|
|
179
|
-
There are two
|
|
225
|
+
There are two configurable options currently:
|
|
180
226
|
|
|
181
|
-
```
|
|
227
|
+
```ruby
|
|
182
228
|
Hoardable.enabled # => default true
|
|
183
229
|
Hoardable.save_trash # => default true
|
|
184
230
|
```
|
|
@@ -196,8 +242,46 @@ Hoardable.with(enabled: false) do
|
|
|
196
242
|
end
|
|
197
243
|
```
|
|
198
244
|
|
|
245
|
+
### Relationships
|
|
246
|
+
|
|
247
|
+
As in life, sometimes relationships can be hard. `hoardable` is still working out best practices and
|
|
248
|
+
features in this area, but here are a couple pointers.
|
|
249
|
+
|
|
250
|
+
Sometimes you’ll have a record that belongs to a record that you’ll trash. Now the child record’s
|
|
251
|
+
foreign key will point to the non-existent trashed version of the parent. If you would like this
|
|
252
|
+
`belongs_to` relationship to always resolve to the parent as if it was not trashed, you can include
|
|
253
|
+
the scope on the relationship definition:
|
|
254
|
+
|
|
255
|
+
```ruby
|
|
256
|
+
belongs_to :parent, -> { include_versions }
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and both the parent
|
|
260
|
+
and child model classes include `Hoardable::Model`. Whenever a hoardable version is created in a
|
|
261
|
+
database transaction, it will create or re-use a unique event UUID for that transaction and tag all
|
|
262
|
+
versions created with it. That way, when you `untrash!` a parent object, you can find and `untrash!`
|
|
263
|
+
the children like so:
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
class Post < ActiveRecord::Base
|
|
267
|
+
include Hoardable::Model
|
|
268
|
+
has_many :comments, dependent: :destroy # `Comment` also includes `Hoardable::Model`
|
|
269
|
+
|
|
270
|
+
after_untrashed do
|
|
271
|
+
Comment
|
|
272
|
+
.version_class
|
|
273
|
+
.trashed
|
|
274
|
+
.where(post_id: id)
|
|
275
|
+
.with_hoardable_event_uuid(hoardable_event_uuid)
|
|
276
|
+
.find_each(&:untrash!)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
```
|
|
280
|
+
|
|
199
281
|
## Contributing
|
|
200
282
|
|
|
283
|
+
This gem is currently considered alpha and very open to feedback.
|
|
284
|
+
|
|
201
285
|
Bug reports and pull requests are welcome on GitHub at https://github.com/waymondo/hoardable.
|
|
202
286
|
|
|
203
287
|
## License
|
|
@@ -4,16 +4,33 @@ require 'rails/generators'
|
|
|
4
4
|
require 'rails/generators/active_record/migration/migration_generator'
|
|
5
5
|
|
|
6
6
|
module Hoardable
|
|
7
|
-
# Generates a migration
|
|
7
|
+
# Generates a migration to create an inherited uni-temporal table of a model including
|
|
8
|
+
# {Hoardable::Model}, for the storage of +versions+.
|
|
8
9
|
class MigrationGenerator < ActiveRecord::Generators::Base
|
|
9
10
|
source_root File.expand_path('templates', __dir__)
|
|
10
11
|
include Rails::Generators::Migration
|
|
12
|
+
class_option :foreign_key_type, type: :string
|
|
11
13
|
|
|
12
14
|
def create_versions_table
|
|
13
|
-
migration_template
|
|
15
|
+
migration_template migration_template_name, "db/migrate/create_#{singularized_table_name}_versions.rb"
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
no_tasks do
|
|
19
|
+
def foreign_key_type
|
|
20
|
+
options[:foreign_key_type] ||
|
|
21
|
+
class_name.singularize.constantize.columns.find { |col| col.name == 'id' }.sql_type
|
|
22
|
+
rescue StandardError
|
|
23
|
+
'bigint'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def migration_template_name
|
|
27
|
+
if Gem::Version.new(ActiveRecord::Migration.current_version.to_s) < Gem::Version.new('7')
|
|
28
|
+
'migration_6.rb.erb'
|
|
29
|
+
else
|
|
30
|
+
'migration.rb.erb'
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
17
34
|
def singularized_table_name
|
|
18
35
|
@singularized_table_name ||= table_name.singularize
|
|
19
36
|
end
|
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
4
4
|
def change
|
|
5
|
+
create_enum :hoardable_operation, %w[update delete]
|
|
5
6
|
create_table :<%= singularized_table_name %>_versions, id: false, options: 'INHERITS (<%= table_name %>)' do |t|
|
|
6
7
|
t.jsonb :_data
|
|
7
8
|
t.tsrange :_during, null: false
|
|
8
|
-
t.
|
|
9
|
+
t.uuid :_event_uuid, null: false, index: true
|
|
10
|
+
t.enum :_operation, enum_type: 'hoardable_operation', null: false, index: true
|
|
11
|
+
t.<%= foreign_key_type %> :<%= singularized_table_name %>_id, null: false, index: true
|
|
9
12
|
end
|
|
10
|
-
add_index
|
|
13
|
+
add_index(
|
|
14
|
+
:<%= singularized_table_name %>_versions,
|
|
15
|
+
%i[_during <%= singularized_table_name %>_id],
|
|
16
|
+
name: 'idx_<%= singularized_table_name %>_versions_temporally'
|
|
17
|
+
)
|
|
11
18
|
end
|
|
12
19
|
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
4
|
+
def change
|
|
5
|
+
reversible do |dir|
|
|
6
|
+
dir.up do
|
|
7
|
+
execute <<~SQL
|
|
8
|
+
DO $$
|
|
9
|
+
BEGIN
|
|
10
|
+
IF NOT EXISTS (
|
|
11
|
+
SELECT 1 FROM pg_type t
|
|
12
|
+
WHERE t.typname = 'hoardable_operation'
|
|
13
|
+
) THEN
|
|
14
|
+
CREATE TYPE hoardable_operation AS ENUM ('update', 'delete');
|
|
15
|
+
END IF;
|
|
16
|
+
END
|
|
17
|
+
$$;
|
|
18
|
+
SQL
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
create_table :<%= singularized_table_name %>_versions, id: false, options: 'INHERITS (<%= table_name %>)' do |t|
|
|
22
|
+
t.jsonb :_data
|
|
23
|
+
t.tsrange :_during, null: false
|
|
24
|
+
t.uuid :_event_uuid, null: false, index: true
|
|
25
|
+
t.column :_operation, :hoardable_operation, null: false, index: true
|
|
26
|
+
t.<%= foreign_key_type %> :<%= singularized_table_name %>_id, null: false, index: true
|
|
27
|
+
end
|
|
28
|
+
add_index(
|
|
29
|
+
:<%= singularized_table_name %>_versions,
|
|
30
|
+
%i[_during <%= singularized_table_name %>_id],
|
|
31
|
+
name: 'idx_<%= singularized_table_name %>_versions_temporally'
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/hoardable/hoardable.rb
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# An ActiveRecord extension for keeping versions of records in temporal inherited tables
|
|
3
|
+
# An +ActiveRecord+ extension for keeping versions of records in uni-temporal inherited tables.
|
|
4
4
|
module Hoardable
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
# Symbols for use with setting contextual data, when creating versions. See
|
|
6
|
+
# {file:README.md#tracking-contextual-data README} for more.
|
|
7
|
+
DATA_KEYS = %i[meta whodunit note event_uuid].freeze
|
|
8
|
+
# Symbols for use with setting {Hoardable} configuration. See {file:README.md#configuration
|
|
9
|
+
# README} for more.
|
|
7
10
|
CONFIG_KEYS = %i[enabled save_trash].freeze
|
|
8
11
|
|
|
12
|
+
# @!visibility private
|
|
13
|
+
VERSION_CLASS_SUFFIX = 'Version'
|
|
14
|
+
|
|
15
|
+
# @!visibility private
|
|
16
|
+
VERSION_TABLE_SUFFIX = "_#{VERSION_CLASS_SUFFIX.tableize}"
|
|
17
|
+
|
|
18
|
+
# @!visibility private
|
|
19
|
+
SAVE_TRASH_ENABLED = -> { Hoardable.save_trash }.freeze
|
|
20
|
+
|
|
21
|
+
# @!visibility private
|
|
22
|
+
DURING_QUERY = '_during @> ?::timestamp'
|
|
23
|
+
|
|
9
24
|
@context = {}
|
|
10
25
|
@config = CONFIG_KEYS.to_h do |key|
|
|
11
26
|
[key, true]
|
|
@@ -32,6 +47,9 @@ module Hoardable
|
|
|
32
47
|
end
|
|
33
48
|
end
|
|
34
49
|
|
|
50
|
+
# This is a general use method for setting {DATA_KEYS} or {CONFIG_KEYS} around a scoped block.
|
|
51
|
+
#
|
|
52
|
+
# @param hash [Hash] Options and contextual data to set within a block
|
|
35
53
|
def with(hash)
|
|
36
54
|
current_config = @config
|
|
37
55
|
current_context = @context
|
data/lib/hoardable/model.rb
CHANGED
|
@@ -1,78 +1,30 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Hoardable
|
|
4
|
-
# This concern
|
|
5
|
-
# generates the Version variant of
|
|
4
|
+
# This concern is the main entrypoint for using {Hoardable}. When included into an +ActiveRecord+
|
|
5
|
+
# class, it dynamically generates the +Version+ variant of that class (with {VersionModel}) and
|
|
6
|
+
# includes the {Hoardable} API methods and relationships on the source model class (through
|
|
7
|
+
# {SourceModel}).
|
|
6
8
|
module Model
|
|
7
9
|
extend ActiveSupport::Concern
|
|
8
10
|
|
|
9
11
|
included do
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
before_update :initialize_hoardable_version, if: -> { Hoardable.enabled }
|
|
13
|
-
before_destroy :initialize_hoardable_version, if: -> { Hoardable.enabled && Hoardable.save_trash }
|
|
14
|
-
after_update :save_hoardable_version, if: -> { Hoardable.enabled }
|
|
15
|
-
before_destroy :delete_hoardable_versions, if: -> { Hoardable.enabled && !Hoardable.save_trash }
|
|
16
|
-
after_destroy :save_hoardable_version, if: -> { Hoardable.enabled && Hoardable.save_trash }
|
|
17
|
-
|
|
18
|
-
attr_reader :hoardable_version
|
|
19
|
-
|
|
12
|
+
define_model_callbacks :versioned
|
|
20
13
|
define_model_callbacks :reverted, only: :after
|
|
14
|
+
define_model_callbacks :untrashed, only: :after
|
|
21
15
|
|
|
22
16
|
TracePoint.new(:end) do |trace|
|
|
23
17
|
next unless self == trace.self
|
|
24
18
|
|
|
25
|
-
version_class_name = "#{name}
|
|
26
|
-
|
|
19
|
+
version_class_name = "#{name}#{VERSION_CLASS_SUFFIX}"
|
|
20
|
+
unless Object.const_defined?(version_class_name)
|
|
21
|
+
Object.const_set(version_class_name, Class.new(self) { include VersionModel })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
include SourceModel
|
|
27
25
|
|
|
28
|
-
Object.const_set(version_class_name, Class.new(self) { include VersionModel })
|
|
29
|
-
has_many(
|
|
30
|
-
:versions, -> { order(:_during) },
|
|
31
|
-
dependent: nil,
|
|
32
|
-
class_name: version_class_name,
|
|
33
|
-
inverse_of: model_name.i18n_key
|
|
34
|
-
)
|
|
35
26
|
trace.disable
|
|
36
27
|
end.enable
|
|
37
28
|
end
|
|
38
|
-
|
|
39
|
-
def at(datetime)
|
|
40
|
-
versions.find_by('_during @> ?::timestamp', datetime) || self
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def initialize_hoardable_version
|
|
46
|
-
Hoardable.with(changes: changes) do
|
|
47
|
-
@hoardable_version = versions.new(
|
|
48
|
-
attributes_before_type_cast
|
|
49
|
-
.without('id')
|
|
50
|
-
.merge(changes.transform_values { |h| h[0] })
|
|
51
|
-
.merge(_data: initialize_hoardable_data)
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def initialize_hoardable_data
|
|
57
|
-
DATA_KEYS.to_h do |key|
|
|
58
|
-
[key, assign_hoardable_context(key)]
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def assign_hoardable_context(key)
|
|
63
|
-
return nil if (value = Hoardable.public_send(key)).nil?
|
|
64
|
-
|
|
65
|
-
value.is_a?(Proc) ? value.call : value
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def save_hoardable_version
|
|
69
|
-
hoardable_version._data['operation'] = persisted? ? 'update' : 'delete'
|
|
70
|
-
hoardable_version.save!(validate: false, touch: false)
|
|
71
|
-
@hoardable_version = nil
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def delete_hoardable_versions
|
|
75
|
-
versions.delete_all(:delete_all)
|
|
76
|
-
end
|
|
77
29
|
end
|
|
78
30
|
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hoardable
|
|
4
|
+
# This concern contains the {Hoardable} relationships, callbacks, and API methods for an
|
|
5
|
+
# +ActiveRecord+. It is included by {Hoardable::Model} after the dynamic generation of the
|
|
6
|
+
# +Version+ class variant.
|
|
7
|
+
module SourceModel
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
class_methods do
|
|
11
|
+
# The dynamically generated +Version+ class for this model.
|
|
12
|
+
def version_class
|
|
13
|
+
"#{name}#{VERSION_CLASS_SUFFIX}".constantize
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
included do
|
|
18
|
+
include Tableoid
|
|
19
|
+
|
|
20
|
+
around_update :insert_hoardable_version_on_update, if: :hoardable_callbacks_enabled
|
|
21
|
+
around_destroy :insert_hoardable_version_on_destroy, if: [:hoardable_callbacks_enabled, SAVE_TRASH_ENABLED]
|
|
22
|
+
before_destroy :delete_hoardable_versions, if: :hoardable_callbacks_enabled, unless: SAVE_TRASH_ENABLED
|
|
23
|
+
after_commit :unset_hoardable_version_and_event_uuid
|
|
24
|
+
|
|
25
|
+
# This will contain the +Version+ class instance for use within +versioned+, +reverted+, and
|
|
26
|
+
# +untrashed+ callbacks.
|
|
27
|
+
attr_reader :hoardable_version
|
|
28
|
+
|
|
29
|
+
# @!attribute [r] hoardable_event_uuid
|
|
30
|
+
# @return [String] A postgres UUID that represents the +version+’s +ActiveRecord+ database transaction
|
|
31
|
+
# @!attribute [r] hoardable_operation
|
|
32
|
+
# @return [String] The database operation that created the +version+ - either +update+ or +delete+.
|
|
33
|
+
delegate :hoardable_event_uuid, :hoardable_operation, to: :hoardable_version, allow_nil: true
|
|
34
|
+
|
|
35
|
+
# Returns all +versions+ in ascending order of their temporal timeframes.
|
|
36
|
+
has_many(
|
|
37
|
+
:versions, -> { order(:_during) },
|
|
38
|
+
dependent: nil,
|
|
39
|
+
class_name: version_class.to_s,
|
|
40
|
+
inverse_of: model_name.i18n_key
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Returns a boolean of whether the record is actually a trashed +version+.
|
|
45
|
+
#
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
def trashed?
|
|
48
|
+
versions.trashed.limit(1).order(_during: :desc).first&.send(:hoardable_source_attributes) == attributes
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns the +version+ at the supplied +datetime+ or +time+. It will return +self+ if there is
|
|
52
|
+
# none. This will raise an error if you try to find a version in the future.
|
|
53
|
+
#
|
|
54
|
+
# @param datetime [DateTime, Time]
|
|
55
|
+
def at(datetime)
|
|
56
|
+
raise(Error, 'Future state cannot be known') if datetime.future?
|
|
57
|
+
|
|
58
|
+
versions.find_by(DURING_QUERY, datetime) || self
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# If a version is found at the supplied datetime, it will +revert!+ to it and return it. This
|
|
62
|
+
# will raise an error if you try to revert to a version in the future.
|
|
63
|
+
#
|
|
64
|
+
# @param datetime [DateTime, Time]
|
|
65
|
+
def revert_to!(datetime)
|
|
66
|
+
return unless (version = at(datetime))
|
|
67
|
+
|
|
68
|
+
version.is_a?(self.class.version_class) ? version.revert! : self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def hoardable_callbacks_enabled
|
|
74
|
+
Hoardable.enabled && !self.class.name.end_with?(VERSION_CLASS_SUFFIX)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def insert_hoardable_version_on_update(&block)
|
|
78
|
+
insert_hoardable_version('update', attributes_before_type_cast.without('id'), &block)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def insert_hoardable_version_on_destroy(&block)
|
|
82
|
+
insert_hoardable_version('delete', attributes_before_type_cast, &block)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def insert_hoardable_version(operation, attrs)
|
|
86
|
+
@hoardable_version = initialize_hoardable_version(operation, attrs)
|
|
87
|
+
run_callbacks(:versioned) do
|
|
88
|
+
yield
|
|
89
|
+
hoardable_version.save(validate: false, touch: false)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def find_or_initialize_hoardable_event_uuid
|
|
94
|
+
Thread.current[:hoardable_event_uuid] ||= ActiveRecord::Base.connection.query('SELECT gen_random_uuid();')[0][0]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def initialize_hoardable_version(operation, attrs)
|
|
98
|
+
versions.new(
|
|
99
|
+
attrs.merge(
|
|
100
|
+
changes.transform_values { |h| h[0] },
|
|
101
|
+
{
|
|
102
|
+
_event_uuid: find_or_initialize_hoardable_event_uuid,
|
|
103
|
+
_operation: operation,
|
|
104
|
+
_data: initialize_hoardable_data.merge(changes: changes)
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def initialize_hoardable_data
|
|
111
|
+
DATA_KEYS.to_h do |key|
|
|
112
|
+
[key, assign_hoardable_context(key)]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def assign_hoardable_context(key)
|
|
117
|
+
return nil if (value = Hoardable.public_send(key)).nil?
|
|
118
|
+
|
|
119
|
+
value.is_a?(Proc) ? value.call : value
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def delete_hoardable_versions
|
|
123
|
+
versions.delete_all(:delete_all)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def unset_hoardable_version_and_event_uuid
|
|
127
|
+
@hoardable_version = nil
|
|
128
|
+
return if ActiveRecord::Base.connection.transaction_open?
|
|
129
|
+
|
|
130
|
+
Thread.current[:hoardable_event_uuid] = nil
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hoardable
|
|
4
|
+
# This concern provides support for PostgreSQL’s tableoid system column to {SourceModel}.
|
|
5
|
+
module Tableoid
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
# @!visibility private
|
|
9
|
+
TABLEOID_AREL_CONDITIONS = lambda do |arel_table, condition|
|
|
10
|
+
arel_table[:tableoid].send(
|
|
11
|
+
condition,
|
|
12
|
+
Arel::Nodes::NamedFunction.new('CAST', [Arel::Nodes::Quoted.new(arel_table.name).as('regclass')])
|
|
13
|
+
)
|
|
14
|
+
end.freeze
|
|
15
|
+
|
|
16
|
+
included do
|
|
17
|
+
# @!visibility private
|
|
18
|
+
attr_writer :tableoid
|
|
19
|
+
|
|
20
|
+
# By default, {Hoardable} only returns instances of the parent table, and not the +versions+
|
|
21
|
+
# in the inherited table.
|
|
22
|
+
default_scope { where(TABLEOID_AREL_CONDITIONS.call(arel_table, :eq)) }
|
|
23
|
+
|
|
24
|
+
# @!scope class
|
|
25
|
+
# @!method include_versions
|
|
26
|
+
# @return [ActiveRecord<Object>]
|
|
27
|
+
#
|
|
28
|
+
# Returns +versions+ along with instances of the source models, all cast as instances of the
|
|
29
|
+
# source model’s class.
|
|
30
|
+
scope :include_versions, -> { unscope(where: [:tableoid]) }
|
|
31
|
+
|
|
32
|
+
# @!scope class
|
|
33
|
+
# @!method versions
|
|
34
|
+
# @return [ActiveRecord<Object>]
|
|
35
|
+
#
|
|
36
|
+
# Returns only +versions+ of the parent +ActiveRecord+ class, cast as instances of the source
|
|
37
|
+
# model’s class.
|
|
38
|
+
scope :versions, -> { include_versions.where(TABLEOID_AREL_CONDITIONS.call(arel_table, :not_eq)) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def tableoid
|
|
44
|
+
connection.execute("SELECT oid FROM pg_class WHERE relname = '#{table_name}'")[0]['oid']
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -1,35 +1,79 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Hoardable
|
|
4
|
-
# This concern is included into the dynamically generated Version
|
|
4
|
+
# This concern is included into the dynamically generated +Version+ kind of the parent
|
|
5
|
+
# +ActiveRecord+ class.
|
|
5
6
|
module VersionModel
|
|
6
7
|
extend ActiveSupport::Concern
|
|
7
8
|
|
|
8
9
|
included do
|
|
9
10
|
hoardable_source_key = superclass.model_name.i18n_key
|
|
11
|
+
|
|
12
|
+
# A +version+ belongs to it’s parent +ActiveRecord+ source.
|
|
10
13
|
belongs_to hoardable_source_key, inverse_of: :versions
|
|
11
14
|
alias_method :hoardable_source, hoardable_source_key
|
|
12
15
|
|
|
13
|
-
self.table_name = "#{table_name.singularize}
|
|
16
|
+
self.table_name = "#{table_name.singularize}#{Hoardable::VERSION_TABLE_SUFFIX}"
|
|
14
17
|
|
|
15
18
|
alias_method :readonly?, :persisted?
|
|
19
|
+
alias_attribute :hoardable_operation, :_operation
|
|
20
|
+
alias_attribute :hoardable_event_uuid, :_event_uuid
|
|
21
|
+
alias_attribute :hoardable_during, :_during
|
|
16
22
|
|
|
17
23
|
before_create :assign_temporal_tsrange
|
|
18
24
|
|
|
25
|
+
# @!scope class
|
|
26
|
+
# @!method trashed
|
|
27
|
+
# @return [ActiveRecord<Object>]
|
|
28
|
+
#
|
|
29
|
+
# Returns only trashed +versions+ that are orphans.
|
|
19
30
|
scope :trashed, lambda {
|
|
20
31
|
left_outer_joins(hoardable_source_key)
|
|
21
32
|
.where(superclass.table_name => { id: nil })
|
|
22
|
-
.where(
|
|
33
|
+
.where(_operation: 'delete')
|
|
23
34
|
}
|
|
35
|
+
|
|
36
|
+
# @!scope class
|
|
37
|
+
# @!method at
|
|
38
|
+
# @return [ActiveRecord<Object>]
|
|
39
|
+
#
|
|
40
|
+
# Returns +versions+ that were valid at the supplied +datetime+ or +time+.
|
|
41
|
+
scope :at, ->(datetime) { where(DURING_QUERY, datetime) }
|
|
42
|
+
|
|
43
|
+
# @!scope class
|
|
44
|
+
# @!method with_hoardable_event_uuid
|
|
45
|
+
# @return [ActiveRecord<Object>]
|
|
46
|
+
#
|
|
47
|
+
# Returns all +versions+ that were created as part of the same +ActiveRecord+ database
|
|
48
|
+
# transaction of the supplied +event_uuid+. Useful in +reverted+ and +untrashed+ callbacks.
|
|
49
|
+
scope :with_hoardable_event_uuid, ->(event_uuid) { where(_event_uuid: event_uuid) }
|
|
24
50
|
end
|
|
25
51
|
|
|
52
|
+
# Reverts the parent +ActiveRecord+ instance to the saved attributes of this +version+. Raises
|
|
53
|
+
# an error if the version is trashed.
|
|
26
54
|
def revert!
|
|
55
|
+
raise(Error, 'Version is trashed, cannot revert') unless hoardable_operation == 'update'
|
|
56
|
+
|
|
27
57
|
transaction do
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
58
|
+
hoardable_source.tap do |reverted|
|
|
59
|
+
reverted.update!(hoardable_source_attributes.without('id'))
|
|
60
|
+
reverted.instance_variable_set(:@hoardable_version, self)
|
|
61
|
+
reverted.run_callbacks(:reverted)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Inserts a trashed +version+ back into its parent +ActiveRecord+ table with its original
|
|
67
|
+
# primary key. Raises an error if the version is not trashed.
|
|
68
|
+
def untrash!
|
|
69
|
+
raise(Error, 'Version is not trashed, cannot untrash') unless hoardable_operation == 'delete'
|
|
70
|
+
|
|
71
|
+
transaction do
|
|
72
|
+
superscope = self.class.superclass.unscoped
|
|
73
|
+
superscope.insert(untrashable_hoardable_source_attributes)
|
|
74
|
+
superscope.find(hoardable_source_foreign_id).tap do |untrashed|
|
|
75
|
+
untrashed.instance_variable_set(:@hoardable_version, self)
|
|
76
|
+
untrashed.run_callbacks(:untrashed)
|
|
33
77
|
end
|
|
34
78
|
end
|
|
35
79
|
end
|
|
@@ -40,14 +84,19 @@ module Hoardable
|
|
|
40
84
|
end
|
|
41
85
|
end
|
|
42
86
|
|
|
43
|
-
|
|
87
|
+
# Returns the +ActiveRecord+
|
|
88
|
+
# {https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes changes} that
|
|
89
|
+
# were present during version creation.
|
|
90
|
+
def changes
|
|
91
|
+
_data&.dig('changes')
|
|
92
|
+
end
|
|
44
93
|
|
|
45
94
|
private
|
|
46
95
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
96
|
+
def untrashable_hoardable_source_attributes
|
|
97
|
+
hoardable_source_attributes.merge('id' => hoardable_source_foreign_id).tap do |hash|
|
|
98
|
+
hash['updated_at'] = Time.now if self.class.column_names.include?('updated_at')
|
|
99
|
+
end
|
|
51
100
|
end
|
|
52
101
|
|
|
53
102
|
def hoardable_source_attributes
|
|
@@ -61,12 +110,24 @@ module Hoardable
|
|
|
61
110
|
@hoardable_source_foreign_key ||= "#{self.class.superclass.model_name.i18n_key}_id"
|
|
62
111
|
end
|
|
63
112
|
|
|
113
|
+
def hoardable_source_foreign_id
|
|
114
|
+
@hoardable_source_foreign_id ||= public_send(hoardable_source_foreign_key)
|
|
115
|
+
end
|
|
116
|
+
|
|
64
117
|
def previous_temporal_tsrange_end
|
|
65
118
|
hoardable_source.versions.limit(1).order(_during: :desc).pluck('_during').first&.end
|
|
66
119
|
end
|
|
67
120
|
|
|
68
121
|
def assign_temporal_tsrange
|
|
69
|
-
|
|
122
|
+
range_start = (
|
|
123
|
+
previous_temporal_tsrange_end ||
|
|
124
|
+
if hoardable_source.class.column_names.include?('created_at')
|
|
125
|
+
hoardable_source.created_at
|
|
126
|
+
else
|
|
127
|
+
Time.at(0)
|
|
128
|
+
end
|
|
129
|
+
)
|
|
130
|
+
self._during = (range_start..Time.now)
|
|
70
131
|
end
|
|
71
132
|
end
|
|
72
133
|
end
|
data/lib/hoardable.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'hoardable/version'
|
|
3
4
|
require_relative 'hoardable/hoardable'
|
|
5
|
+
require_relative 'hoardable/tableoid'
|
|
6
|
+
require_relative 'hoardable/error'
|
|
7
|
+
require_relative 'hoardable/source_model'
|
|
4
8
|
require_relative 'hoardable/version_model'
|
|
5
9
|
require_relative 'hoardable/model'
|
|
6
10
|
require_relative 'generators/hoardable/migration_generator'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hoardable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- justin talbott
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-
|
|
11
|
+
date: 2022-08-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -51,45 +51,45 @@ dependencies:
|
|
|
51
51
|
- !ruby/object:Gem::Version
|
|
52
52
|
version: '8'
|
|
53
53
|
- !ruby/object:Gem::Dependency
|
|
54
|
-
name:
|
|
54
|
+
name: railties
|
|
55
55
|
requirement: !ruby/object:Gem::Requirement
|
|
56
56
|
requirements:
|
|
57
57
|
- - ">="
|
|
58
58
|
- !ruby/object:Gem::Version
|
|
59
|
-
version: '1
|
|
59
|
+
version: '6.1'
|
|
60
60
|
- - "<"
|
|
61
61
|
- !ruby/object:Gem::Version
|
|
62
|
-
version: '
|
|
62
|
+
version: '8'
|
|
63
63
|
type: :runtime
|
|
64
64
|
prerelease: false
|
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
|
66
66
|
requirements:
|
|
67
67
|
- - ">="
|
|
68
68
|
- !ruby/object:Gem::Version
|
|
69
|
-
version: '1
|
|
69
|
+
version: '6.1'
|
|
70
70
|
- - "<"
|
|
71
71
|
- !ruby/object:Gem::Version
|
|
72
|
-
version: '
|
|
72
|
+
version: '8'
|
|
73
73
|
- !ruby/object:Gem::Dependency
|
|
74
|
-
name:
|
|
74
|
+
name: pg
|
|
75
75
|
requirement: !ruby/object:Gem::Requirement
|
|
76
76
|
requirements:
|
|
77
77
|
- - ">="
|
|
78
78
|
- !ruby/object:Gem::Version
|
|
79
|
-
version: '
|
|
79
|
+
version: '1'
|
|
80
80
|
- - "<"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '
|
|
82
|
+
version: '2'
|
|
83
83
|
type: :runtime
|
|
84
84
|
prerelease: false
|
|
85
85
|
version_requirements: !ruby/object:Gem::Requirement
|
|
86
86
|
requirements:
|
|
87
87
|
- - ">="
|
|
88
88
|
- !ruby/object:Gem::Version
|
|
89
|
-
version: '
|
|
89
|
+
version: '1'
|
|
90
90
|
- - "<"
|
|
91
91
|
- !ruby/object:Gem::Version
|
|
92
|
-
version: '
|
|
92
|
+
version: '2'
|
|
93
93
|
description: Rails model versioning with the power of uni-temporal inherited tables
|
|
94
94
|
email:
|
|
95
95
|
- justin@waymondo.com
|
|
@@ -106,9 +106,14 @@ files:
|
|
|
106
106
|
- Rakefile
|
|
107
107
|
- lib/generators/hoardable/migration_generator.rb
|
|
108
108
|
- lib/generators/hoardable/templates/migration.rb.erb
|
|
109
|
+
- lib/generators/hoardable/templates/migration_6.rb.erb
|
|
109
110
|
- lib/hoardable.rb
|
|
111
|
+
- lib/hoardable/error.rb
|
|
110
112
|
- lib/hoardable/hoardable.rb
|
|
111
113
|
- lib/hoardable/model.rb
|
|
114
|
+
- lib/hoardable/source_model.rb
|
|
115
|
+
- lib/hoardable/tableoid.rb
|
|
116
|
+
- lib/hoardable/version.rb
|
|
112
117
|
- lib/hoardable/version_model.rb
|
|
113
118
|
- sig/hoardable.rbs
|
|
114
119
|
homepage: https://github.com/waymondo/hoardable
|