hoardable 0.1.0

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 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: []