hoardable 0.1.3 → 0.3.0

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: 96952d8928266fc5ae03199a91e85428565ff11385a74851fe61fd3a8eafc881
4
- data.tar.gz: 1b9117cbf4ea08325d29212c16580e368a857b12f8c2d20bc346d9e8f5268d59
3
+ metadata.gz: 50086ef99aac41454b28ab8bff2e8abf94d7870b371da0c706c477e0b296ffc3
4
+ data.tar.gz: 6e6d4ab40470bbfb93e23e96fdbb2d8b086878d24e55d499ac368523125e5397
5
5
  SHA512:
6
- metadata.gz: e533edf9412339fcc90691fa5d10395265914f71f7f7493c9afc226902cf831db87f69661d565630602e2db815549e4209550420abc44f6a7d179ccb23ba7509
7
- data.tar.gz: a8c690b0a3399f3853ed6c65ca7d514af9c82d10569483d7d4bed86c6e7a63d1881ecac61e13d6646facdce3370614dc6c86f88599bc6eb955feba38589c398c
6
+ metadata.gz: 747ac52845c950eb655cb7b0af22794dda49ed08e2b0f1aee472595fd2c63b1f886d4f516e686807607efe4946ca588792913ea92ec1c3152a44c23f8cd84487
7
+ data.tar.gz: 3460ccd66ebe6eed7cdb129bca6150696b82ad9f8613965e80b08fb458ac6e2b8b644e662036533f73f95f52b97a10bf50f2782f76d3fd6671f23f9c2b58df08
data/README.md CHANGED
@@ -3,10 +3,6 @@
3
3
  Hoardable is an ActiveRecord extension for Ruby 2.6+, Rails 6.1+, and PostgreSQL that allows for
4
4
  versioning and soft-deletion of records through the use of _uni-temporal inherited tables_.
5
5
 
6
- [👉 Documentation](https://www.rubydoc.info/gems/hoardable)
7
-
8
- ### huh?
9
-
10
6
  [Temporal tables](https://en.wikipedia.org/wiki/Temporal_database) are a database design pattern
11
7
  where each row of a table contains data along with one or more time ranges. In the case of this gem,
12
8
  each database row has a time range that represents the row’s valid time range - hence
@@ -20,8 +16,10 @@ change is reflected on its descendants.
20
16
  With these concepts combined, `hoardable` offers a simple and effective model versioning system for
21
17
  Rails. Versions of records are stored in separate, inherited tables along with their valid time
22
18
  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.
19
+ be more explicit and obvious on the lower RDBS level while still familiar and convenient to use
20
+ within Ruby on Rails.
21
+
22
+ [👉 Documentation](https://www.rubydoc.info/gems/hoardable)
25
23
 
26
24
  ## Installation
27
25
 
@@ -71,7 +69,7 @@ Rails 7.
71
69
  ### Overview
72
70
 
73
71
  Once you include `Hoardable::Model` into a model, it will dynamically generate a "Version" subclass
74
- of that model. As we continue our example above, :
72
+ of that model. As we continue our example above:
75
73
 
76
74
  ```
77
75
  $ irb
@@ -119,6 +117,10 @@ post.at(1.day.ago) # => #<PostVersion:0x000000010d44fa30>
119
117
  PostVersion.at(1.day.ago).find_by(post_id: post.id) # => #<PostVersion:0x000000010d44fa30>
120
118
  ```
121
119
 
120
+ _Note:_ A `Version` is not created upon initial parent model creation. If you would like to
121
+ accurately capture the valid temporal frame of the first version, make sure your model’s table has a
122
+ `created_at` timestamp field.
123
+
122
124
  By default, `hoardable` will keep copies of records you have destroyed. You can query for them as
123
125
  well:
124
126
 
@@ -129,7 +131,7 @@ PostVersion.trashed
129
131
  _Note:_ Creating an inherited table does not copy over the indexes from the parent table. If you
130
132
  need to query versions often, you should add appropriate indexes to the `_versions` tables.
131
133
 
132
- ### Tracking contextual data
134
+ ### Tracking Contextual Data
133
135
 
134
136
  You’ll often want to track contextual data about the creation of a version. There are 3 optional
135
137
  symbol keys that are provided for tracking contextual information:
@@ -144,7 +146,10 @@ choosing.
144
146
  One convenient way to assign contextual data to these is by defining a proc in an initializer, i.e.:
145
147
 
146
148
  ```ruby
149
+ # config/initiailzers/hoardable.rb
147
150
  Hoardable.whodunit = -> { Current.user&.id }
151
+
152
+ # somewhere in your app code
148
153
  Current.user = User.find(123)
149
154
  post.update!(status: 'live')
150
155
  post.versions.last.hoardable_whodunit # => 123
@@ -222,19 +227,27 @@ end
222
227
 
223
228
  ### Configuration
224
229
 
225
- There are two configurable options currently:
230
+ There are three configurable options currently:
226
231
 
227
232
  ```ruby
228
233
  Hoardable.enabled # => default true
234
+ Hoardable.version_updates # => default true
229
235
  Hoardable.save_trash # => default true
236
+ Hoardable.return_everything # => default false
230
237
  ```
231
238
 
232
- `Hoardable.enabled` controls whether versions will be created at all.
239
+ `Hoardable.enabled` controls whether versions will be ever be created.
240
+
241
+ `Hoardable.version_updates` controls whether versions get created on record updates.
233
242
 
234
243
  `Hoardable.save_trash` controls whether to create versions upon record deletion. When this is set to
235
244
  `false`, all versions of a record will be deleted when the record is destroyed.
236
245
 
237
- If you would like to temporarily set a config setting, you can use `Hoardable.with` as well:
246
+ `Hoardable.return_everything` controls whether to include versions when doing queries for source
247
+ models. This is typically only useful to set around a block, as explained below in
248
+ [Relationships](#relationships).
249
+
250
+ If you would like to temporarily set a config setting, you can use `Hoardable.with`:
238
251
 
239
252
  ```ruby
240
253
  Hoardable.with(enabled: false) do
@@ -242,6 +255,27 @@ Hoardable.with(enabled: false) do
242
255
  end
243
256
  ```
244
257
 
258
+ You can also configure these variables per `ActiveRecord` class as well using `hoardable_config`:
259
+
260
+ ```ruby
261
+ class Comment < ActiveRecord::Base
262
+ include Hoardable::Model
263
+ hoardable_config version_updates: false
264
+ end
265
+ ```
266
+
267
+ If you want to temporarily set the `hoardable_config` for a specific model, you can use
268
+ `with_hoardable_config`:
269
+
270
+ ```ruby
271
+ Comment.with_hoardable_config(version_updates: true) do
272
+ comment.update!(text: "Edited")
273
+ end
274
+ ```
275
+
276
+ If a model-level option exists, it will use that. Otherwise, it will fall back to the global
277
+ `Hoardable` config.
278
+
245
279
  ### Relationships
246
280
 
247
281
  As in life, sometimes relationships can be hard. `hoardable` is still working out best practices and
@@ -250,17 +284,20 @@ features in this area, but here are a couple pointers.
250
284
  Sometimes you’ll have a record that belongs to a record that you’ll trash. Now the child record’s
251
285
  foreign key will point to the non-existent trashed version of the parent. If you would like this
252
286
  `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:
287
+ the `include_versions` scope on the relationship definition:
254
288
 
255
289
  ```ruby
256
- belongs_to :parent, -> { include_versions }
290
+ class Comment
291
+ include Hoardable::Model
292
+ belongs_to :post, -> { include_versions } # `Post` also includes `Hoardable::Model`
293
+ end
257
294
  ```
258
295
 
259
296
  Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and both the parent
260
297
  and child model classes include `Hoardable::Model`. Whenever a hoardable version is created in a
261
298
  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:
299
+ versions created with it. That way, when you `untrash!` a record, you can find and `untrash!`
300
+ records that were trashed with it:
264
301
 
265
302
  ```ruby
266
303
  class Post < ActiveRecord::Base
@@ -278,9 +315,64 @@ class Post < ActiveRecord::Base
278
315
  end
279
316
  ```
280
317
 
318
+ If there are models that might be related to versions that are trashed or otherwise, and/or might
319
+ trashed themselves, you can bypass the inherited tables query handling altogether by using the
320
+ `return_everything` configuration variable in `Hoardable.with`:
321
+
322
+ ```ruby
323
+ post.destroy!
324
+
325
+ Hoardable.with(return_everything: true) do
326
+ post = Post.find(post.id) # returns the trashed post as if it was not
327
+ post.comments # returns the trashed comments as well
328
+ end
329
+
330
+ post.reload # raises ActiveRecord::RecordNotFound
331
+ ```
332
+
333
+ ## Gem Comparison
334
+
335
+ ### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
336
+
337
+ `paper_trail` is maybe the most popular and fully featured gem in this space. It works for other
338
+ database types than PostgeSQL and (by default) stores all versions of all versioned models in a
339
+ single `versions` table. It stores changes in a `text`, `json`, or `jsonb` column. In order to
340
+ efficiently query the `versions` table, a `jsonb` column should be used, which takes up a lot of
341
+ space to index. Unless you customize your configuration, all `versions` for all models types are
342
+ in the same table which is inefficient if you are only interested in querying versions of a single
343
+ model. By contrast, `hoardable` stores versions in smaller, isolated and inherited tables with the
344
+ same database columns as their parents, which are more efficient for querying as well as auditing
345
+ for truncating and dropping. The concept of a `temporal` time-frame does not exist for a single
346
+ version since there is only a `created_at` timestamp.
347
+
348
+ ### [`audited`](https://github.com/collectiveidea/audited)
349
+
350
+ `audited` works in a similar manner as `paper_trail`. It stores all versions for all model types in
351
+ a single table, you must opt into using `jsonb` as the column type to store "changes", in case you
352
+ want to query them, and there is no concept of a `temporal` time-frame for a single version. It
353
+ makes opinionated decisions about contextual data requirements and stores them as top level data
354
+ types on the `audited` table.
355
+
356
+ ### [`discard`](https://github.com/jhawthorn/discard)
357
+
358
+ `discard` only covers soft-deletion. The act of "soft deleting" a record is only captured through
359
+ the time-stamping of a `discarded_at` column on the records table; there is no other capturing of
360
+ the event that caused the soft deletion unless you implement it yourself. Once the "discarded"
361
+ record is restored, the previous "discarded" awareness is lost. Since "discarded" records exist in
362
+ the same table as "undiscarded" records, you must explicitly omit the discarded records from queries
363
+ across your app to keep them from leaking in.
364
+
365
+ ### [`paranoia`](https://github.com/rubysherpas/paranoia)
366
+
367
+ `paranoia` also only covers soft-deletion. In their README, they recommend using `discard` instead
368
+ of `paranoia` because of the fact they override ActiveRecord’s `delete` and `destroy` methods.
369
+ `hoardable` employs callbacks to create trashed versions instead of overriding methods. Otherwise,
370
+ `paranoia` works similarly to `discard` in that it keeps deleted records in the same table and tags
371
+ them with a `deleted_at` timestamp. No other information about the soft-deletion event is stored.
372
+
281
373
  ## Contributing
282
374
 
283
- This gem is currently considered alpha and very open to feedback.
375
+ This gem still quite new and very open to feedback.
284
376
 
285
377
  Bug reports and pull requests are welcome on GitHub at https://github.com/waymondo/hoardable.
286
378
 
@@ -7,7 +7,7 @@ module Hoardable
7
7
  DATA_KEYS = %i[meta whodunit note event_uuid].freeze
8
8
  # Symbols for use with setting {Hoardable} configuration. See {file:README.md#configuration
9
9
  # README} for more.
10
- CONFIG_KEYS = %i[enabled save_trash].freeze
10
+ CONFIG_KEYS = %i[enabled version_updates save_trash return_everything].freeze
11
11
 
12
12
  # @!visibility private
13
13
  VERSION_CLASS_SUFFIX = 'Version'
@@ -15,15 +15,12 @@ module Hoardable
15
15
  # @!visibility private
16
16
  VERSION_TABLE_SUFFIX = "_#{VERSION_CLASS_SUFFIX.tableize}"
17
17
 
18
- # @!visibility private
19
- SAVE_TRASH_ENABLED = -> { Hoardable.save_trash }.freeze
20
-
21
18
  # @!visibility private
22
19
  DURING_QUERY = '_during @> ?::timestamp'
23
20
 
24
21
  @context = {}
25
22
  @config = CONFIG_KEYS.to_h do |key|
26
- [key, true]
23
+ [key, key != :return_everything]
27
24
  end
28
25
 
29
26
  class << self
@@ -49,7 +46,7 @@ module Hoardable
49
46
 
50
47
  # This is a general use method for setting {DATA_KEYS} or {CONFIG_KEYS} around a scoped block.
51
48
  #
52
- # @param hash [Hash] Options and contextual data to set within a block
49
+ # @param hash [Hash] config and contextual data to set within a block
53
50
  def with(hash)
54
51
  current_config = @config
55
52
  current_context = @context
@@ -8,6 +8,42 @@ module Hoardable
8
8
  module Model
9
9
  extend ActiveSupport::Concern
10
10
 
11
+ class_methods do
12
+ # @!visibility private
13
+ attr_reader :_hoardable_config
14
+
15
+ # If called with a hash, this will set the model-level +Hoardable+ configuration variables. If
16
+ # called without an argument it will return the computed +Hoardable+ configuration considering
17
+ # both model-level and global values.
18
+ #
19
+ # @param hash [Hash] The +Hoardable+ configuration for the model. Keys must be present in
20
+ # {CONFIG_KEYS}
21
+ # @return [Hash]
22
+ def hoardable_config(hash = nil)
23
+ if hash
24
+ @_hoardable_config = hash.slice(*Hoardable::CONFIG_KEYS)
25
+ else
26
+ @_hoardable_config ||= {}
27
+ Hoardable::CONFIG_KEYS.to_h do |key|
28
+ [key, @_hoardable_config.key?(key) ? @_hoardable_config[key] : Hoardable.send(key)]
29
+ end
30
+ end
31
+ end
32
+
33
+ # Set the model-level +Hoardable+ configuration variables around a block. The configuration
34
+ # will be reset to it’s previous value afterwards.
35
+ #
36
+ # @param hash [Hash] The +Hoardable+ configuration for the model. Keys must be present in
37
+ # {CONFIG_KEYS}
38
+ def with_hoardable_config(hash)
39
+ current_config = @_hoardable_config
40
+ @_hoardable_config = hash.slice(*Hoardable::CONFIG_KEYS)
41
+ yield
42
+ ensure
43
+ @_hoardable_config = current_config
44
+ end
45
+ end
46
+
11
47
  included do
12
48
  define_model_callbacks :versioned
13
49
  define_model_callbacks :reverted, only: :after
@@ -17,9 +17,9 @@ module Hoardable
17
17
  included do
18
18
  include Tableoid
19
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
20
+ around_update :insert_hoardable_version_on_update, if: %i[hoardable_callbacks_enabled hoardable_version_updates]
21
+ around_destroy :insert_hoardable_version_on_destroy, if: %i[hoardable_callbacks_enabled hoardable_save_trash]
22
+ before_destroy :delete_hoardable_versions, if: :hoardable_callbacks_enabled, unless: :hoardable_save_trash
23
23
  after_commit :unset_hoardable_version_and_event_uuid
24
24
 
25
25
  # This will contain the +Version+ class instance for use within +versioned+, +reverted+, and
@@ -45,7 +45,7 @@ module Hoardable
45
45
  #
46
46
  # @return [Boolean]
47
47
  def trashed?
48
- versions.trashed.limit(1).order(_during: :desc).first&.send(:hoardable_source_attributes) == attributes
48
+ versions.trashed.limit(1).order(_during: :desc).first&.id == id
49
49
  end
50
50
 
51
51
  # Returns the +version+ at the supplied +datetime+ or +time+. It will return +self+ if there is
@@ -71,7 +71,15 @@ module Hoardable
71
71
  private
72
72
 
73
73
  def hoardable_callbacks_enabled
74
- Hoardable.enabled && !self.class.name.end_with?(VERSION_CLASS_SUFFIX)
74
+ self.class.hoardable_config[:enabled] && !self.class.name.end_with?(VERSION_CLASS_SUFFIX)
75
+ end
76
+
77
+ def hoardable_save_trash
78
+ self.class.hoardable_config[:save_trash]
79
+ end
80
+
81
+ def hoardable_version_updates
82
+ self.class.hoardable_config[:version_updates]
75
83
  end
76
84
 
77
85
  def insert_hoardable_version_on_update(&block)
@@ -17,9 +17,16 @@ module Hoardable
17
17
  # @!visibility private
18
18
  attr_writer :tableoid
19
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)) }
20
+ # By default {Hoardable} only returns instances of the parent table, and not the +versions+ in
21
+ # the inherited table. This can be bypassed by using the {.include_versions} scope or wrapping
22
+ # the code in a `Hoardable.with(return_everything: true)` block.
23
+ default_scope do
24
+ if hoardable_config[:return_everything]
25
+ where(nil)
26
+ else
27
+ where(TABLEOID_AREL_CONDITIONS.call(arel_table, :eq))
28
+ end
29
+ end
23
30
 
24
31
  # @!scope class
25
32
  # @!method include_versions
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- VERSION = '0.1.3'
4
+ VERSION = '0.3.0'
5
5
  end
data/sig/hoardable.rbs CHANGED
@@ -1,4 +1,86 @@
1
1
  module Hoardable
2
2
  VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
3
+ DATA_KEYS: [:meta, :whodunit, :note, :event_uuid]
4
+ CONFIG_KEYS: [:enabled, :version_updates, :save_trash, :return_everything]
5
+ VERSION_CLASS_SUFFIX: String
6
+ VERSION_TABLE_SUFFIX: String
7
+ DURING_QUERY: String
8
+ self.@context: Hash[untyped, untyped]
9
+ self.@config: untyped
10
+
11
+ def self.with: (untyped hash) -> untyped
12
+
13
+ module Tableoid
14
+ TABLEOID_AREL_CONDITIONS: Proc
15
+
16
+ private
17
+ def tableoid: -> untyped
18
+
19
+ public
20
+ attr_writer tableoid: untyped
21
+ end
22
+
23
+ class Error < StandardError
24
+ end
25
+
26
+ module SourceModel
27
+ include Tableoid
28
+
29
+ def trashed?: -> untyped
30
+ def at: (untyped datetime) -> SourceModel
31
+ def revert_to!: (untyped datetime) -> SourceModel?
32
+
33
+ private
34
+ def hoardable_callbacks_enabled: -> untyped
35
+ def hoardable_save_trash: -> untyped
36
+ def hoardable_version_updates: -> untyped
37
+ def insert_hoardable_version_on_update: -> untyped
38
+ def insert_hoardable_version_on_destroy: -> untyped
39
+ def insert_hoardable_version: (String operation, untyped attrs) -> untyped
40
+ def find_or_initialize_hoardable_event_uuid: -> untyped
41
+ def initialize_hoardable_version: (String operation, untyped attrs) -> untyped
42
+ def initialize_hoardable_data: -> untyped
43
+ def assign_hoardable_context: (:event_uuid | :meta | :note | :whodunit key) -> nil
44
+ def delete_hoardable_versions: -> untyped
45
+ def unset_hoardable_version_and_event_uuid: -> nil
46
+
47
+ public
48
+ def version_class: -> untyped
49
+ attr_reader hoardable_version: nil
50
+ end
51
+
52
+ module VersionModel
53
+ @hoardable_source_attributes: untyped
54
+ @hoardable_source_foreign_key: String
55
+ @hoardable_source_foreign_id: untyped
56
+
57
+ def revert!: -> untyped
58
+ def untrash!: -> untyped
59
+ def changes: -> untyped
60
+
61
+ private
62
+ def untrashable_hoardable_source_attributes: -> untyped
63
+ def hoardable_source_attributes: -> untyped
64
+ def hoardable_source_foreign_key: -> String
65
+ def hoardable_source_foreign_id: -> untyped
66
+ def previous_temporal_tsrange_end: -> untyped
67
+ def assign_temporal_tsrange: -> Range
68
+ end
69
+
70
+ module Model
71
+ include VersionModel
72
+ include SourceModel
73
+
74
+ def hoardable_config: (?nil hash) -> untyped
75
+ def with_hoardable_config: (untyped hash) -> untyped
76
+ end
77
+
78
+ class MigrationGenerator
79
+ @singularized_table_name: untyped
80
+
81
+ def create_versions_table: -> untyped
82
+ def foreign_key_type: -> String
83
+ def migration_template_name: -> String
84
+ def singularized_table_name: -> untyped
85
+ end
4
86
  end
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.3
4
+ version: 0.3.0
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-08-02 00:00:00.000000000 Z
11
+ date: 2022-08-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord