hoardable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 24eee9ce1b76bd2d5a40a6b527ef4e048c25850ea454f75b2838f45b21339c25
4
+ data.tar.gz: e464f7418ec2527ed6fe598b4575481a01d50fea236631749d0722bdd328c66f
5
+ SHA512:
6
+ metadata.gz: a617926045371fa040ae329241fe37a5ea58ea4fdcd43c2c66f0c3fe2b89cc0b5c2723371c88c9946b4bda4cf20d477a936494a32f6e8e39b9a1b5e727c7e9b4
7
+ data.tar.gz: 283ee7c613a1d07b227e32dc823914ea2cdc9b144a9b9d03c0155adf21cdeeacecd92ba8d173d2c0c4bf17febbd1343571898db7c5e24ea828e0f261ae82cbb7
data/.rubocop.yml ADDED
@@ -0,0 +1,10 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.6
3
+ NewCops: enable
4
+
5
+ Layout/LineLength:
6
+ Max: 120
7
+
8
+ Metrics/ClassLength:
9
+ Exclude:
10
+ - 'test/**/*.rb'
data/.tool-versions ADDED
@@ -0,0 +1,2 @@
1
+ ruby 3.1.2
2
+ postgres 14.4
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2022-07-23
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'debug', '~> 1.6'
8
+ gem 'minitest', '~> 5.0'
9
+ gem 'rake', '~> 13.0'
10
+ gem 'rubocop', '~> 1.21'
11
+ gem 'rubocop-minitest', '~> 0.20'
12
+ gem 'rubocop-rake', '~> 0.6'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 justin talbott
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # Hoardable
2
+
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**.
5
+
6
+ ### Huh?
7
+
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
11
+ "uni-temporal".
12
+
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.
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.
21
+
22
+ ## Installation
23
+
24
+ Add this line to your application's Gemfile:
25
+
26
+ ```ruby
27
+ gem 'hoardable'
28
+ ```
29
+
30
+ And then execute `bundle install`.
31
+
32
+ ### Model Installation
33
+
34
+ First, include `Hoardable::Model` into a model you would like to hoard versions of:
35
+
36
+ ```ruby
37
+ class Post < ActiveRecord::Base
38
+ include Hoardable::Model
39
+ belongs_to :user
40
+ end
41
+ ```
42
+
43
+ Then, run the generator command to create a database migration and migrate it:
44
+
45
+ ```
46
+ bin/rails g hoardable:migration posts
47
+ bin/rails db:migrate
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ ### Basics
53
+
54
+ Once you include `Hoardable::Model` into a model, it will dynamically generate a "Version" subclass
55
+ of that model. Continuing our example above:
56
+
57
+ ```
58
+ >> Post
59
+ => Post(id: integer, body: text, user_id: integer, created_at: datetime)
60
+ >> PostVersion
61
+ => PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, _data: jsonb, _during: tsrange, post_id: integer)
62
+ ```
63
+
64
+ A `Post` now `has_many :versions` which are created on every update and deletion of a `Post` (by
65
+ default):
66
+
67
+ ```ruby
68
+ post_id = post.id
69
+ post.versions.size # => 0
70
+ post.update!(title: "Title")
71
+ post.versions.size # => 1
72
+ post.destroy!
73
+ post.reload # => ActiveRecord::RecordNotFound
74
+ PostVersion.where(post_id: post_id).size # => 2
75
+ ```
76
+
77
+ Each `PostVersion` has access to the same attributes, relationships, and other model behavior that
78
+ `Post` has, but is a read-only record.
79
+
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.
82
+
83
+ ### Querying and Temporal Lookup
84
+
85
+ Since a `PostVersion` is just a normal `ActiveRecord`, you can query them like another model
86
+ resource, i.e:
87
+
88
+ ```ruby
89
+ post.versions.where(user_id: Current.user.id, body: nil)
90
+ ```
91
+
92
+ If you want to look-up the version of a `Post` at a specific time, you can use the `.at` method:
93
+
94
+ ```ruby
95
+ post.at(1.day.ago) # => #<PostVersion:0x000000010d44fa30>
96
+ ```
97
+
98
+ By default, `hoardable` will keep copies of records you have destroyed. You can query for them as
99
+ well:
100
+
101
+ ```ruby
102
+ PostVersion.trashed
103
+ ```
104
+
105
+ _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.
107
+
108
+ ### Tracking contextual data
109
+
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`).
112
+
113
+ There are also 3 other optional keys that are provided for tracking contextual information:
114
+
115
+ - `whodunit` - an identifier for who is responsible for creating the version
116
+ - `note` - a string containing a description regarding the versioning
117
+ - `meta` - any other contextual information you’d like to store along with the version
118
+
119
+ This information is stored in a `jsonb` column. Each key’s value can be in the format of your
120
+ choosing.
121
+
122
+ One convenient way to assign this contextual data is with a proc in an initializer, i.e.:
123
+
124
+ ```ruby
125
+ Hoardable.whodunit = -> { Current.user&.id }
126
+ ```
127
+
128
+ You can also set this context manually as well, just remember to clear them afterwards.
129
+
130
+ ```ruby
131
+ Hoardable.note = "reverting due to accidental deletion"
132
+ post.update!(title: "We’re back!")
133
+ Hoardable.note = nil
134
+ post.versions.last.hoardable_note # => "reverting due to accidental deletion"
135
+ ```
136
+
137
+ Another useful pattern is to use `Hoardable.with` to set the context around a block. A good example
138
+ of this would be in `ApplicationController`:
139
+
140
+ ```ruby
141
+ class ApplicationController < ActionController::Base
142
+ around_action :use_hoardable_context
143
+
144
+ private
145
+
146
+ def use_hoardable_context
147
+ Hoardable.with(whodunit: current_user.id, meta: { request_uuid: request.uuid }) do
148
+ yield
149
+ end
150
+ end
151
+ end
152
+ ```
153
+
154
+ ### Model Callbacks
155
+
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.
159
+
160
+ ``` ruby
161
+ class User
162
+ before_save :sanitize_version
163
+ after_reverted :track_reverted_event
164
+
165
+ private
166
+
167
+ def sanitize_version
168
+ hoardable_version.sanitize_password
169
+ end
170
+
171
+ def track_reverted_event
172
+ track_event(:user_reverted, self)
173
+ end
174
+ end
175
+ ```
176
+
177
+ ### Configuration
178
+
179
+ There are two available options:
180
+
181
+ ``` ruby
182
+ Hoardable.enabled # => default true
183
+ Hoardable.save_trash # => default true
184
+ ```
185
+
186
+ `Hoardable.enabled` controls whether versions will be created at all.
187
+
188
+ `Hoardable.save_trash` controls whether to create versions upon record deletion. When this is set to
189
+ `false`, all versions of a record will be deleted when the record is destroyed.
190
+
191
+ If you would like to temporarily set a config setting, you can use `Hoardable.with` as well:
192
+
193
+ ```ruby
194
+ Hoardable.with(enabled: false) do
195
+ post.update!(title: 'unimportant change to create version for')
196
+ end
197
+ ```
198
+
199
+ ## Contributing
200
+
201
+ Bug reports and pull requests are welcome on GitHub at https://github.com/waymondo/hoardable.
202
+
203
+ ## License
204
+
205
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/test_*.rb']
10
+ end
11
+
12
+ require 'rubocop/rake_task'
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record/migration/migration_generator'
5
+
6
+ module Hoardable
7
+ # Generates a migration for an inherited temporal table of a model including {Hoardable::Model}
8
+ class MigrationGenerator < ActiveRecord::Generators::Base
9
+ source_root File.expand_path('templates', __dir__)
10
+ include Rails::Generators::Migration
11
+
12
+ def create_versions_table
13
+ migration_template 'migration.rb.erb', "db/migrate/create_#{singularized_table_name}_versions.rb"
14
+ end
15
+
16
+ no_tasks do
17
+ def singularized_table_name
18
+ @singularized_table_name ||= table_name.singularize
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
4
+ def change
5
+ create_table :<%= singularized_table_name %>_versions, id: false, options: 'INHERITS (<%= table_name %>)' do |t|
6
+ t.jsonb :_data
7
+ t.tsrange :_during, null: false
8
+ t.bigint :<%= singularized_table_name %>_id, null: false, index: true
9
+ end
10
+ add_index :<%= singularized_table_name %>_versions, %i[_during <%= singularized_table_name %>_id]
11
+ end
12
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # An ActiveRecord extension for keeping versions of records in temporal inherited tables
4
+ module Hoardable
5
+ VERSION = '0.1.0'
6
+ DATA_KEYS = %i[changes meta whodunit note operation].freeze
7
+ CONFIG_KEYS = %i[enabled save_trash].freeze
8
+
9
+ @context = {}
10
+ @config = CONFIG_KEYS.to_h do |key|
11
+ [key, true]
12
+ end
13
+
14
+ class << self
15
+ CONFIG_KEYS.each do |key|
16
+ define_method(key) do
17
+ @config[key]
18
+ end
19
+
20
+ define_method("#{key}=") do |value|
21
+ @config[key] = value
22
+ end
23
+ end
24
+
25
+ DATA_KEYS.each do |key|
26
+ define_method(key) do
27
+ @context[key]
28
+ end
29
+
30
+ define_method("#{key}=") do |value|
31
+ @context[key] = value
32
+ end
33
+ end
34
+
35
+ def with(hash)
36
+ current_config = @config
37
+ current_context = @context
38
+ @config = current_config.merge(hash.slice(*CONFIG_KEYS))
39
+ @context = current_context.merge(hash.slice(*DATA_KEYS))
40
+ yield
41
+ ensure
42
+ @config = current_config
43
+ @context = current_context
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # This concern includes the Hoardable API methods on ActiveRecord instances and dynamically
5
+ # generates the Version variant of the class
6
+ module Model
7
+ extend ActiveSupport::Concern
8
+
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
+
20
+ define_model_callbacks :reverted, only: :after
21
+
22
+ TracePoint.new(:end) do |trace|
23
+ next unless self == trace.self
24
+
25
+ version_class_name = "#{name}Version"
26
+ next if Object.const_defined?(version_class_name)
27
+
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
+ trace.disable
36
+ end.enable
37
+ 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
+ end
78
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # This concern is included into the dynamically generated Version models.
5
+ module VersionModel
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ hoardable_source_key = superclass.model_name.i18n_key
10
+ belongs_to hoardable_source_key, inverse_of: :versions
11
+ alias_method :hoardable_source, hoardable_source_key
12
+
13
+ self.table_name = "#{table_name.singularize}_versions"
14
+
15
+ alias_method :readonly?, :persisted?
16
+
17
+ before_create :assign_temporal_tsrange
18
+
19
+ scope :trashed, lambda {
20
+ left_outer_joins(hoardable_source_key)
21
+ .where(superclass.table_name => { id: nil })
22
+ .where("_data ->> 'operation' = 'delete'")
23
+ }
24
+ end
25
+
26
+ def revert!
27
+ 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)
33
+ end
34
+ end
35
+ end
36
+
37
+ DATA_KEYS.each do |key|
38
+ define_method("hoardable_#{key}") do
39
+ _data&.dig(key.to_s)
40
+ end
41
+ end
42
+
43
+ alias changes hoardable_changes
44
+
45
+ private
46
+
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)
51
+ end
52
+
53
+ def hoardable_source_attributes
54
+ @hoardable_source_attributes ||=
55
+ attributes_before_type_cast
56
+ .without(hoardable_source_foreign_key)
57
+ .reject { |k, _v| k.start_with?('_') }
58
+ end
59
+
60
+ def hoardable_source_foreign_key
61
+ @hoardable_source_foreign_key ||= "#{self.class.superclass.model_name.i18n_key}_id"
62
+ end
63
+
64
+ def previous_temporal_tsrange_end
65
+ hoardable_source.versions.limit(1).order(_during: :desc).pluck('_during').first&.end
66
+ end
67
+
68
+ def assign_temporal_tsrange
69
+ self._during = ((previous_temporal_tsrange_end || hoardable_source.created_at)..Time.now)
70
+ end
71
+ end
72
+ end
data/lib/hoardable.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'hoardable/hoardable'
4
+ require_relative 'hoardable/version_model'
5
+ require_relative 'hoardable/model'
6
+ require_relative 'generators/hoardable/migration_generator'
data/sig/hoardable.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Hoardable
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hoardable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - justin talbott
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-07-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '6.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activesupport
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '6.1'
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: '8'
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '6.1'
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: '8'
53
+ - !ruby/object:Gem::Dependency
54
+ name: pg
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '1.0'
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: '2'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '1.0'
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: '2'
73
+ - !ruby/object:Gem::Dependency
74
+ name: railties
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '6.1'
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: '8'
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '6.1'
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: '8'
93
+ description: Rails model versioning with the power of uni-temporal inherited tables
94
+ email:
95
+ - justin@waymondo.com
96
+ executables: []
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - ".rubocop.yml"
101
+ - ".tool-versions"
102
+ - CHANGELOG.md
103
+ - Gemfile
104
+ - LICENSE.txt
105
+ - README.md
106
+ - Rakefile
107
+ - lib/generators/hoardable/migration_generator.rb
108
+ - lib/generators/hoardable/templates/migration.rb.erb
109
+ - lib/hoardable.rb
110
+ - lib/hoardable/hoardable.rb
111
+ - lib/hoardable/model.rb
112
+ - lib/hoardable/version_model.rb
113
+ - sig/hoardable.rbs
114
+ homepage: https://github.com/waymondo/hoardable
115
+ licenses:
116
+ - MIT
117
+ metadata:
118
+ homepage_uri: https://github.com/waymondo/hoardable
119
+ source_code_uri: https://github.com/waymondo/hoardable
120
+ rubygems_mfa_required: 'true'
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: 2.6.0
130
+ required_rubygems_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ requirements: []
136
+ rubygems_version: 3.3.7
137
+ signing_key:
138
+ specification_version: 4
139
+ summary: An ActiveRecord extension for versioning and soft-deletion of records in
140
+ Postgres
141
+ test_files: []