hoardable 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24eee9ce1b76bd2d5a40a6b527ef4e048c25850ea454f75b2838f45b21339c25
4
- data.tar.gz: e464f7418ec2527ed6fe598b4575481a01d50fea236631749d0722bdd328c66f
3
+ metadata.gz: 7380fb64f7dd3132bec65fd1cfc0c8317a52e6cb6f26895d0b83d7b78391c193
4
+ data.tar.gz: 71241a1edc81c57d1bb0549685da47642036fc65ff8d659284c31a9784b0f571
5
5
  SHA512:
6
- metadata.gz: a617926045371fa040ae329241fe37a5ea58ea4fdcd43c2c66f0c3fe2b89cc0b5c2723371c88c9946b4bda4cf20d477a936494a32f6e8e39b9a1b5e727c7e9b4
7
- data.tar.gz: 283ee7c613a1d07b227e32dc823914ea2cdc9b144a9b9d03c0155adf21cdeeacecd92ba8d173d2c0c4bf17febbd1343571898db7c5e24ea828e0f261ae82cbb7
6
+ metadata.gz: 4f473b92097e223dd535512ec5850a6c00d9c22faba482787ffbf2ae7b0f6130d37b0a3ed34a46701f53ec4958e7293a69626bdc9693f1f7a51c0e35ff62bb2e
7
+ data.tar.gz: 530e609fd4535b4d37f82fc4a39ce3322209451bdd0fa67079e74ed2ef658bd252d9a87ca7189a9332e0303b20c8cb073765bc1c99fcdb647c346f893a133ecd
data/Gemfile CHANGED
@@ -2,11 +2,11 @@
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
+
12
+ gemspec
data/README.md CHANGED
@@ -1,23 +1,25 @@
1
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 **uni-temporal inherited tables**.
4
+ versioning and soft-deletion of records through the use of _uni-temporal inherited tables_.
5
5
 
6
- ### Huh?
6
+ #### nice... huh?
7
7
 
8
8
  [Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern
9
- where each row contains data as well as one or more time ranges. In the case of a temporal table
10
- representing versions, each row has one time range representing the row’s valid time range, hence
9
+ where each row of a table contains data along with one or more time ranges. In the case of this gem,
10
+ each database row has a time range that represents the row’s valid time range - hence
11
11
  "uni-temporal".
12
12
 
13
13
  [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 another table. The descendant table’s schema will stay
15
- in sync with all columns that it inherits from it’s parent. If a new column or removed from the
16
- parent, the schema change is reflected on its descendants.
14
+ that allows a table to inherit all columns of a parent table. The descendant table’s schema will
15
+ stay in sync with its parent. If a new column is added to or removed from the parent, the schema
16
+ change is reflected on its descendants.
17
17
 
18
- With these principles combined, `hoardable` offers a simple and effective model versioning system,
19
- where versions of records are stored in a separate, inherited table with the validity time range and
20
- other versioning data.
18
+ With these concepts combined, `hoardable` offers a simple and effective model versioning system for
19
+ Rails. Versions of records are stored in separate, inherited tables along with their valid time
20
+ ranges and contextual data. Compared to other Rails-oriented versioning systems, this gem strives to
21
+ be more explicit and obvious on the lower RDBS level while still familiar and convenient within Ruby
22
+ on Rails.
21
23
 
22
24
  ## Installation
23
25
 
@@ -31,12 +33,15 @@ And then execute `bundle install`.
31
33
 
32
34
  ### Model Installation
33
35
 
34
- First, include `Hoardable::Model` into a model you would like to hoard versions of:
36
+ You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions
37
+ of:
35
38
 
36
39
  ```ruby
37
40
  class Post < ActiveRecord::Base
38
41
  include Hoardable::Model
39
42
  belongs_to :user
43
+ has_many :comments, dependent: :destroy
44
+ ...
40
45
  end
41
46
  ```
42
47
 
@@ -47,52 +52,60 @@ bin/rails g hoardable:migration posts
47
52
  bin/rails db:migrate
48
53
  ```
49
54
 
55
+ _Note:_ If you are on Rails 6.1, you might want to set `config.active_record.schema_format = :sql`
56
+ in `application.rb`, so that the enum type is captured in your schema dump. This is not required in
57
+ Rails 7.
58
+
50
59
  ## Usage
51
60
 
52
- ### Basics
61
+ ### Overview
53
62
 
54
63
  Once you include `Hoardable::Model` into a model, it will dynamically generate a "Version" subclass
55
- of that model. Continuing our example above:
64
+ of that model. As we continue our example above, :
56
65
 
57
66
  ```
67
+ $ irb
58
68
  >> Post
59
69
  => Post(id: integer, body: text, user_id: integer, created_at: datetime)
60
70
  >> PostVersion
61
71
  => PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, _data: jsonb, _during: tsrange, post_id: integer)
62
72
  ```
63
73
 
64
- A `Post` now `has_many :versions` which are created on every update and deletion of a `Post` (by
65
- default):
74
+ A `Post` now `has_many :versions`. Whenever an update and deletion of a `Post` occurs, a version is
75
+ created (by default):
66
76
 
67
77
  ```ruby
68
- post_id = post.id
78
+ post = Post.create!(attributes)
69
79
  post.versions.size # => 0
70
80
  post.update!(title: "Title")
71
81
  post.versions.size # => 1
72
82
  post.destroy!
73
- post.reload # => ActiveRecord::RecordNotFound
74
- PostVersion.where(post_id: post_id).size # => 2
83
+ post.trashed? # true
84
+ post.versions.size # => 2
85
+ Post.find(post.id) # raises ActiveRecord::RecordNotFound
75
86
  ```
76
87
 
77
88
  Each `PostVersion` has access to the same attributes, relationships, and other model behavior that
78
- `Post` has, but is a read-only record.
89
+ `Post` has, but as a read-only record.
79
90
 
80
- If you ever need to revert to a specific version, you can call `version.revert!` on it. If the
81
- source post had been deleted, this will untrash it with it’s original primary key.
91
+ If you ever need to revert to a specific version, you can call `version.revert!` on it. If you would
92
+ like to untrash a specific version, you can call `version.untrash!` on it. This will re-insert the
93
+ model in the parent class’ table with it’s original primary key.
82
94
 
83
95
  ### Querying and Temporal Lookup
84
96
 
85
- Since a `PostVersion` is just a normal `ActiveRecord`, you can query them like another model
86
- resource, i.e:
97
+ Since a `PostVersion` is an `ActiveRecord` class, you can query them like another model resource:
87
98
 
88
99
  ```ruby
89
- post.versions.where(user_id: Current.user.id, body: nil)
100
+ post.versions.where(user_id: Current.user.id, body: "Cool!")
90
101
  ```
91
102
 
92
- If you want to look-up the version of a `Post` at a specific time, you can use the `.at` method:
103
+ If you want to look-up the version of a record at a specific time, you can use the `.at` method:
93
104
 
94
105
  ```ruby
95
106
  post.at(1.day.ago) # => #<PostVersion:0x000000010d44fa30>
107
+ # or
108
+ PostVersion.at(1.day.ago).find_by(post_id: post.id) # => #<PostVersion:0x000000010d44fa30>
96
109
  ```
97
110
 
98
111
  By default, `hoardable` will keep copies of records you have destroyed. You can query for them as
@@ -103,14 +116,17 @@ PostVersion.trashed
103
116
  ```
104
117
 
105
118
  _Note:_ Creating an inherited table does not copy over the indexes from the parent table. If you
106
- need to query versions often, you will need to add those indexes to the `_versions` tables manually.
119
+ need to query versions often, you should add appropriate indexes to the `_versions` tables.
107
120
 
108
121
  ### Tracking contextual data
109
122
 
110
- You’ll often want to track contextual data about a version. `hoardable` will automatically capture
111
- the ActiveRecord `changes` hash and `operation` that cause the version (`update` or `delete`).
123
+ You’ll often want to track contextual data about the creation of a version. `hoardable` will
124
+ automatically capture the ActiveRecord
125
+ [changes](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changes) hash and the
126
+ `operation` that cause the version (`update` or `delete`). It will also tag all versions created in
127
+ the same database transaction with a shared and unique `event_id`.
112
128
 
113
- There are also 3 other optional keys that are provided for tracking contextual information:
129
+ There 3 other optional keys that are provided for tracking contextual information:
114
130
 
115
131
  - `whodunit` - an identifier for who is responsible for creating the version
116
132
  - `note` - a string containing a description regarding the versioning
@@ -119,10 +135,13 @@ There are also 3 other optional keys that are provided for tracking contextual i
119
135
  This information is stored in a `jsonb` column. Each key’s value can be in the format of your
120
136
  choosing.
121
137
 
122
- One convenient way to assign this contextual data is with a proc in an initializer, i.e.:
138
+ One convenient way to assign contextual data to these is by defining a proc in an initializer, i.e.:
123
139
 
124
140
  ```ruby
125
141
  Hoardable.whodunit = -> { Current.user&.id }
142
+ Current.user = User.find(123)
143
+ post.update!(status: 'live')
144
+ post.versions.last.hoardable_whodunit # => 123
126
145
  ```
127
146
 
128
147
  You can also set this context manually as well, just remember to clear them afterwards.
@@ -134,7 +153,7 @@ Hoardable.note = nil
134
153
  post.versions.last.hoardable_note # => "reverting due to accidental deletion"
135
154
  ```
136
155
 
137
- Another useful pattern is to use `Hoardable.with` to set the context around a block. A good example
156
+ A more useful pattern is to use `Hoardable.with` to set the context around a block. A good example
138
157
  of this would be in `ApplicationController`:
139
158
 
140
159
  ```ruby
@@ -147,38 +166,48 @@ class ApplicationController < ActionController::Base
147
166
  Hoardable.with(whodunit: current_user.id, meta: { request_uuid: request.uuid }) do
148
167
  yield
149
168
  end
169
+ # `Hoardable.whodunit` is back to nil or the previously set value
150
170
  end
151
171
  end
152
172
  ```
153
173
 
154
174
  ### Model Callbacks
155
175
 
156
- Sometimes you might want to do something with a version before it gets saved. You can access it in a
157
- `before_save` callback as `hoardable_version`. There is also an `after_reverted` callback available
158
- as well.
176
+ Sometimes you might want to do something with a version before or after it gets inserted to the
177
+ database. You can access it in `before/after/around_versioned` callbacks on the source record as
178
+ `hoardable_version`. These happen around `.save`, which is enclosed in an ActiveRecord transaction.
179
+
180
+ There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called
181
+ on the source record after a version is reverted or untrashed.
159
182
 
160
- ``` ruby
183
+ ```ruby
161
184
  class User
162
- before_save :sanitize_version
185
+ include Hoardable::Model
186
+ before_versioned :sanitize_version
163
187
  after_reverted :track_reverted_event
188
+ after_untrashed :track_untrashed_event
164
189
 
165
190
  private
166
191
 
167
192
  def sanitize_version
168
193
  hoardable_version.sanitize_password
169
- end
194
+ end
170
195
 
171
196
  def track_reverted_event
172
197
  track_event(:user_reverted, self)
173
198
  end
199
+
200
+ def track_untrashed_event
201
+ track_event(:user_untrashed, self)
202
+ end
174
203
  end
175
204
  ```
176
205
 
177
206
  ### Configuration
178
207
 
179
- There are two available options:
208
+ There are two configurable options currently:
180
209
 
181
- ``` ruby
210
+ ```ruby
182
211
  Hoardable.enabled # => default true
183
212
  Hoardable.save_trash # => default true
184
213
  ```
@@ -10,10 +10,18 @@ module Hoardable
10
10
  include Rails::Generators::Migration
11
11
 
12
12
  def create_versions_table
13
- migration_template 'migration.rb.erb', "db/migrate/create_#{singularized_table_name}_versions.rb"
13
+ migration_template migration_template_name, "db/migrate/create_#{singularized_table_name}_versions.rb"
14
14
  end
15
15
 
16
16
  no_tasks do
17
+ def migration_template_name
18
+ if Gem::Version.new(ActiveRecord::Migration.current_version.to_s) < Gem::Version.new('7')
19
+ 'migration_6.rb.erb'
20
+ else
21
+ 'migration.rb.erb'
22
+ end
23
+ end
24
+
17
25
  def singularized_table_name
18
26
  @singularized_table_name ||= table_name.singularize
19
27
  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
9
+ t.enum :_operation, enum_type: 'hoardable_operation', null: false
8
10
  t.bigint :<%= singularized_table_name %>_id, null: false, index: true
9
11
  end
10
- add_index :<%= singularized_table_name %>_versions, %i[_during <%= singularized_table_name %>_id]
12
+ add_index(
13
+ :<%= singularized_table_name %>_versions,
14
+ %i[_during <%= singularized_table_name %>_id],
15
+ name: 'idx_<%= singularized_table_name %>_versions_temporally'
16
+ )
17
+ add_index :<%= singularized_table_name %>_versions, :_operation
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.column :_operation, :hoardable_operation, null: false
25
+ t.bigint :<%= singularized_table_name %>_id, null: false, index: true
26
+ end
27
+ add_index(
28
+ :<%= singularized_table_name %>_versions,
29
+ %i[_during <%= singularized_table_name %>_id],
30
+ name: 'idx_<%= singularized_table_name %>_versions_temporally'
31
+ )
32
+ add_index :<%= singularized_table_name %>_versions, :_operation
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # A subclass of StandardError for general use within Hoardable
5
+ class Error < StandardError; end
6
+ end
@@ -2,10 +2,14 @@
2
2
 
3
3
  # An ActiveRecord extension for keeping versions of records in temporal inherited tables
4
4
  module Hoardable
5
- VERSION = '0.1.0'
6
- DATA_KEYS = %i[changes meta whodunit note operation].freeze
5
+ DATA_KEYS = %i[meta whodunit note event_id].freeze
7
6
  CONFIG_KEYS = %i[enabled save_trash].freeze
8
7
 
8
+ VERSION_CLASS_SUFFIX = 'Version'
9
+ VERSION_TABLE_SUFFIX = "_#{VERSION_CLASS_SUFFIX.tableize}"
10
+ SAVE_TRASH_ENABLED = -> { Hoardable.save_trash }.freeze
11
+ DURING_QUERY = '_during @> ?::timestamp'
12
+
9
13
  @context = {}
10
14
  @config = CONFIG_KEYS.to_h do |key|
11
15
  [key, true]
@@ -1,78 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- # This concern includes the Hoardable API methods on ActiveRecord instances and dynamically
5
- # generates the Version variant of the class
4
+ # This concern dynamically generates the Version variant of the class module Model and includes
5
+ # the API methods and relationships on the source model
6
6
  module Model
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  included do
10
- default_scope { where("#{table_name}.tableoid = '#{table_name}'::regclass") }
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
-
10
+ define_model_callbacks :versioned
20
11
  define_model_callbacks :reverted, only: :after
12
+ define_model_callbacks :untrashed, only: :after
21
13
 
22
14
  TracePoint.new(:end) do |trace|
23
15
  next unless self == trace.self
24
16
 
25
- version_class_name = "#{name}Version"
17
+ version_class_name = "#{name}#{VERSION_CLASS_SUFFIX}"
26
18
  next if Object.const_defined?(version_class_name)
27
19
 
28
20
  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
- trace.disable
36
- end.enable
37
- end
38
-
39
- def at(datetime)
40
- versions.find_by('_during @> ?::timestamp', datetime) || self
41
- end
42
21
 
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
22
+ include SourceModel
55
23
 
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)
24
+ trace.disable
25
+ end.enable
76
26
  end
77
27
  end
78
28
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # This concern contains the relationships, callbacks, and API methods for a Source model
5
+ module SourceModel
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def version_class
10
+ "#{name}#{VERSION_CLASS_SUFFIX}".constantize
11
+ end
12
+ end
13
+
14
+ included do
15
+ include Tableoid
16
+
17
+ around_update :insert_hoardable_version_on_update, if: :hoardable_callbacks_enabled
18
+ around_destroy :insert_hoardable_version_on_destroy, if: [:hoardable_callbacks_enabled, SAVE_TRASH_ENABLED]
19
+ before_destroy :delete_hoardable_versions, if: :hoardable_callbacks_enabled, unless: SAVE_TRASH_ENABLED
20
+ after_commit :unset_hoardable_version_and_event_id
21
+
22
+ attr_reader :hoardable_version
23
+
24
+ has_many(
25
+ :versions, -> { order(:_during) },
26
+ dependent: nil,
27
+ class_name: version_class.to_s,
28
+ inverse_of: model_name.i18n_key
29
+ )
30
+ end
31
+
32
+ def trashed?
33
+ versions.trashed.limit(1).order(_during: :desc).first&.send(:hoardable_source_attributes) == attributes
34
+ end
35
+
36
+ def at(datetime)
37
+ versions.find_by(DURING_QUERY, datetime) || self
38
+ end
39
+
40
+ private
41
+
42
+ def hoardable_callbacks_enabled
43
+ Hoardable.enabled && !self.class.name.end_with?(VERSION_CLASS_SUFFIX)
44
+ end
45
+
46
+ def insert_hoardable_version_on_update(&block)
47
+ insert_hoardable_version('update', attributes_before_type_cast.without('id'), &block)
48
+ end
49
+
50
+ def insert_hoardable_version_on_destroy(&block)
51
+ insert_hoardable_version('delete', attributes_before_type_cast, &block)
52
+ end
53
+
54
+ def insert_hoardable_version(operation, attrs)
55
+ event_id = find_or_initialize_hoardable_event_id
56
+ Hoardable.with(event_id: event_id) do
57
+ @hoardable_version = initialize_hoardable_version(operation, attrs)
58
+ run_callbacks(:versioned) do
59
+ yield
60
+ hoardable_version.save(validate: false, touch: false)
61
+ end
62
+ end
63
+ end
64
+
65
+ def find_or_initialize_hoardable_event_id
66
+ Thread.current[:hoardable_event_id] ||= SecureRandom.hex
67
+ end
68
+
69
+ def initialize_hoardable_version(operation, attrs)
70
+ versions.new(
71
+ attrs.merge(
72
+ changes.transform_values { |h| h[0] },
73
+ {
74
+ _operation: operation,
75
+ _data: initialize_hoardable_data.merge(changes: changes)
76
+ }
77
+ )
78
+ )
79
+ end
80
+
81
+ def initialize_hoardable_data
82
+ DATA_KEYS.to_h do |key|
83
+ [key, assign_hoardable_context(key)]
84
+ end
85
+ end
86
+
87
+ def assign_hoardable_context(key)
88
+ return nil if (value = Hoardable.public_send(key)).nil?
89
+
90
+ value.is_a?(Proc) ? value.call : value
91
+ end
92
+
93
+ def delete_hoardable_versions
94
+ versions.delete_all(:delete_all)
95
+ end
96
+
97
+ def unset_hoardable_version_and_event_id
98
+ @hoardable_version = nil
99
+ return if ActiveRecord::Base.connection.transaction_open?
100
+
101
+ Thread.current[:hoardable_event_id] = nil
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # This concern provides support for PostgreSQL's tableoid system column
5
+ module Tableoid
6
+ extend ActiveSupport::Concern
7
+
8
+ TABLEOID_AREL_CONDITIONS = lambda do |arel_table, condition|
9
+ arel_table[:tableoid].send(
10
+ condition,
11
+ Arel::Nodes::NamedFunction.new('CAST', [Arel::Nodes::Quoted.new(arel_table.name).as('regclass')])
12
+ )
13
+ end.freeze
14
+
15
+ included do
16
+ attr_writer :tableoid
17
+
18
+ default_scope { where(TABLEOID_AREL_CONDITIONS.call(arel_table, :eq)) }
19
+ scope :include_versions, -> { unscope(where: [:tableoid]) }
20
+ scope :versions, -> { include_versions.where(TABLEOID_AREL_CONDITIONS.call(arel_table, :not_eq)) }
21
+ end
22
+
23
+ def tableoid
24
+ connection.execute("SELECT oid FROM pg_class WHERE relname = '#{table_name}'")[0]['oid']
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ VERSION = '0.1.1'
5
+ end
@@ -10,7 +10,7 @@ module Hoardable
10
10
  belongs_to hoardable_source_key, inverse_of: :versions
11
11
  alias_method :hoardable_source, hoardable_source_key
12
12
 
13
- self.table_name = "#{table_name.singularize}_versions"
13
+ self.table_name = "#{table_name.singularize}#{Hoardable::VERSION_TABLE_SUFFIX}"
14
14
 
15
15
  alias_method :readonly?, :persisted?
16
16
 
@@ -19,17 +19,32 @@ module Hoardable
19
19
  scope :trashed, lambda {
20
20
  left_outer_joins(hoardable_source_key)
21
21
  .where(superclass.table_name => { id: nil })
22
- .where("_data ->> 'operation' = 'delete'")
22
+ .where(_operation: 'delete')
23
23
  }
24
+ scope :at, ->(datetime) { where(DURING_QUERY, datetime) }
24
25
  end
25
26
 
26
27
  def revert!
28
+ raise(Error, 'Version is trashed, cannot revert') unless _operation == 'update'
29
+
30
+ transaction do
31
+ hoardable_source.tap do |reverted|
32
+ reverted.update!(hoardable_source_attributes.without('id'))
33
+ reverted.instance_variable_set(:@hoardable_version, self)
34
+ reverted.run_callbacks(:reverted)
35
+ end
36
+ end
37
+ end
38
+
39
+ def untrash!
40
+ raise(Error, 'Version is not trashed, cannot untrash') unless _operation == 'delete'
41
+
27
42
  transaction do
28
- (
29
- hoardable_source&.tap { |tapped| tapped.update!(hoardable_source_attributes.without('id')) } ||
30
- untrash
31
- ).tap do |tapped|
32
- tapped.run_callbacks(:reverted)
43
+ superscope = self.class.superclass.unscoped
44
+ superscope.insert(untrashable_hoardable_source_attributes)
45
+ superscope.find(hoardable_source_foreign_id).tap do |untrashed|
46
+ untrashed.instance_variable_set(:@hoardable_version, self)
47
+ untrashed.run_callbacks(:untrashed)
33
48
  end
34
49
  end
35
50
  end
@@ -40,14 +55,16 @@ module Hoardable
40
55
  end
41
56
  end
42
57
 
43
- alias changes hoardable_changes
58
+ def changes
59
+ _data&.dig('changes')
60
+ end
44
61
 
45
62
  private
46
63
 
47
- def untrash
48
- foreign_id = public_send(hoardable_source_foreign_key)
49
- self.class.superclass.insert(hoardable_source_attributes.merge('id' => foreign_id, 'updated_at' => Time.now))
50
- self.class.superclass.find(foreign_id)
64
+ def untrashable_hoardable_source_attributes
65
+ hoardable_source_attributes.merge('id' => hoardable_source_foreign_id).tap do |hash|
66
+ hash['updated_at'] = Time.now if self.class.column_names.include?('updated_at')
67
+ end
51
68
  end
52
69
 
53
70
  def hoardable_source_attributes
@@ -61,12 +78,24 @@ module Hoardable
61
78
  @hoardable_source_foreign_key ||= "#{self.class.superclass.model_name.i18n_key}_id"
62
79
  end
63
80
 
81
+ def hoardable_source_foreign_id
82
+ @hoardable_source_foreign_id ||= public_send(hoardable_source_foreign_key)
83
+ end
84
+
64
85
  def previous_temporal_tsrange_end
65
86
  hoardable_source.versions.limit(1).order(_during: :desc).pluck('_during').first&.end
66
87
  end
67
88
 
68
89
  def assign_temporal_tsrange
69
- self._during = ((previous_temporal_tsrange_end || hoardable_source.created_at)..Time.now)
90
+ range_start = (
91
+ previous_temporal_tsrange_end ||
92
+ if hoardable_source.class.column_names.include?('created_at')
93
+ hoardable_source.created_at
94
+ else
95
+ Time.at(0)
96
+ end
97
+ )
98
+ self._during = (range_start..Time.now)
70
99
  end
71
100
  end
72
101
  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.0
4
+ version: 0.1.1
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-07-26 00:00:00.000000000 Z
11
+ date: 2022-07-28 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: pg
54
+ name: railties
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - ">="
58
58
  - !ruby/object:Gem::Version
59
- version: '1.0'
59
+ version: '6.1'
60
60
  - - "<"
61
61
  - !ruby/object:Gem::Version
62
- version: '2'
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.0'
69
+ version: '6.1'
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
- version: '2'
72
+ version: '8'
73
73
  - !ruby/object:Gem::Dependency
74
- name: railties
74
+ name: pg
75
75
  requirement: !ruby/object:Gem::Requirement
76
76
  requirements:
77
77
  - - ">="
78
78
  - !ruby/object:Gem::Version
79
- version: '6.1'
79
+ version: '1'
80
80
  - - "<"
81
81
  - !ruby/object:Gem::Version
82
- version: '8'
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: '6.1'
89
+ version: '1'
90
90
  - - "<"
91
91
  - !ruby/object:Gem::Version
92
- version: '8'
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