hoardable 0.10.1 → 0.11.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 +4 -4
- data/Gemfile +1 -0
- data/README.md +55 -14
- data/lib/generators/hoardable/install_generator.rb +33 -0
- data/lib/generators/hoardable/templates/functions.rb.erb +16 -0
- data/lib/generators/hoardable/templates/migration.rb.erb +20 -15
- data/lib/generators/hoardable/templates/migration_6.rb.erb +20 -15
- data/lib/hoardable/associations.rb +6 -98
- data/lib/hoardable/attachment.rb +16 -0
- data/lib/hoardable/belongs_to.rb +37 -0
- data/lib/hoardable/encrypted_rich_text.rb +8 -0
- data/lib/hoardable/{hoardable.rb → engine.rb} +21 -0
- data/lib/hoardable/has_many.rb +45 -0
- data/lib/hoardable/has_one.rb +28 -0
- data/lib/hoardable/has_one_attached.rb +33 -0
- data/lib/hoardable/has_rich_text.rb +23 -0
- data/lib/hoardable/model.rb +10 -4
- data/lib/hoardable/rich_text.rb +8 -0
- data/lib/hoardable/source_model.rb +2 -0
- data/lib/hoardable/version.rb +1 -1
- data/lib/hoardable.rb +8 -2
- data/sig/hoardable.rbs +36 -10
- metadata +13 -4
- data/lib/generators/hoardable/initializer_generator.rb +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2d9f9ea3a4559d3a4515357eabc0c7ee05526ee1308a4e6eef111f2d6207aef6
|
4
|
+
data.tar.gz: be0011cb1ea9d3fcdcd5d1a4f4951d7185371cc1d05c91dfd27ea163b59620b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20ebc8d7ec2924e43e6bbd806e7aaf08350653b695dc4aef16de28359ceb95d8de16da4c19790e8b1212bc8d8d202caeb3614e05b326977bd470defe48e4b731
|
7
|
+
data.tar.gz: fc3fa92ae0ab25bee4122456b889f86eec75b0eee4efba742dced8737d7bc9ab3a99e63e63839f49089183900df8ed54a315a4561aad634c0e2ea3fa56aa8732
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -29,14 +29,19 @@ Add this line to your application's Gemfile:
|
|
29
29
|
gem 'hoardable'
|
30
30
|
```
|
31
31
|
|
32
|
-
|
33
|
-
|
34
|
-
If you would like to generate an initializer with the global [configuration](#configuration) options:
|
32
|
+
Run `bundle install`, and then run:
|
35
33
|
|
36
34
|
```
|
37
|
-
rails g hoardable:
|
35
|
+
bin/rails g hoardable:install
|
36
|
+
bin/rails db:migrate
|
38
37
|
```
|
39
38
|
|
39
|
+
This will generate a PostgreSQL function and an initiailzer.
|
40
|
+
|
41
|
+
_Note:_ It is recommended to set `config.active_record.schema_format = :sql` in `application.rb`, so
|
42
|
+
that the function and triggers in the migrations that prevent updates to the versions table get
|
43
|
+
captured in your schema.
|
44
|
+
|
40
45
|
### Model Installation
|
41
46
|
|
42
47
|
You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions
|
@@ -66,10 +71,6 @@ explicitly, you can do so:
|
|
66
71
|
bin/rails g hoardable:migration Post --foreign-key-type uuid
|
67
72
|
```
|
68
73
|
|
69
|
-
_Note:_ If you are on Rails 6.1, you might want to set `config.active_record.schema_format = :sql`
|
70
|
-
in `application.rb`, so that the enum type is captured in your schema dump. This is not required in
|
71
|
-
Rails 7.
|
72
|
-
|
73
74
|
_Note:_ Creating an inherited table does not copy over the indexes from the parent table. If you
|
74
75
|
need to query versions often, you should add appropriate indexes to the `_versions` tables.
|
75
76
|
|
@@ -371,7 +372,7 @@ If you are ever unsure if a Hoardable record is a "source" or a "version", you c
|
|
371
372
|
calling `version?` on it. If you want to get the true original source record ID, you can call
|
372
373
|
`hoardable_source_id`.
|
373
374
|
|
374
|
-
Sometimes you’ll trash something that `
|
375
|
+
Sometimes you’ll trash something that `has_many :children, dependent: :destroy` and want
|
375
376
|
to untrash everything in a similar dependent manner. Whenever a hoardable version is created in a
|
376
377
|
database transaction, it will create or re-use a unique event UUID for that transaction and tag all
|
377
378
|
versions created with it. That way, when you `untrash!` a record, you can find and `untrash!`
|
@@ -380,7 +381,7 @@ records that were trashed with it:
|
|
380
381
|
```ruby
|
381
382
|
class Post < ActiveRecord::Base
|
382
383
|
include Hoardable::Model
|
383
|
-
|
384
|
+
has_many :comments, hoardable: true, dependent: :destroy # `Comment` also includes `Hoardable::Model`
|
384
385
|
|
385
386
|
after_untrashed do
|
386
387
|
Comment
|
@@ -392,9 +393,42 @@ class Post < ActiveRecord::Base
|
|
392
393
|
end
|
393
394
|
```
|
394
395
|
|
396
|
+
## ActionText
|
397
|
+
|
398
|
+
Hoardable provides support for ActiveRecord models with `has_rich_text`. First, you must create a
|
399
|
+
temporal table for `ActionText::RichText`:
|
400
|
+
|
401
|
+
```
|
402
|
+
bin/rails g hoardable:migration ActionText::RichText
|
403
|
+
bin/rails db:migrate
|
404
|
+
```
|
405
|
+
|
406
|
+
Then in your model, include `Hoardable::Model` and provide the `hoardable: true` keyword to
|
407
|
+
`has_rich_text`:
|
408
|
+
|
409
|
+
``` ruby
|
410
|
+
class Post < ActiveRecord::Base
|
411
|
+
include Hoardable::Model # or `Hoardable::Associations` if you don't need `PostVersion`
|
412
|
+
has_rich_text :content, hoardable: true
|
413
|
+
end
|
414
|
+
```
|
415
|
+
|
416
|
+
Now the `rich_text_content` relationship will be managed as a Hoardable `has_one` relationship:
|
417
|
+
|
418
|
+
``` ruby
|
419
|
+
post = Post.create!(content: '<div>Hello World</div>')
|
420
|
+
datetime = DateTime.current
|
421
|
+
post.update!(content: '<div>Goodbye Cruel World</div>')
|
422
|
+
post.content.versions.size # => 1
|
423
|
+
assert_equal post.content.to_plain_text, 'Goodbye Cruel World'
|
424
|
+
Hoardable.at(datetime) do
|
425
|
+
assert_equal post.content.to_plain_text, 'Hello World'
|
426
|
+
end
|
427
|
+
```
|
428
|
+
|
395
429
|
## Gem Comparison
|
396
430
|
|
397
|
-
|
431
|
+
#### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
|
398
432
|
|
399
433
|
`paper_trail` is maybe the most popular and fully featured gem in this space. It works for other
|
400
434
|
database types than PostgeSQL and (by default) stores all versions of all versioned models in a
|
@@ -407,7 +441,7 @@ same database columns as their parents, which are more efficient for querying as
|
|
407
441
|
for truncating and dropping. The concept of a `temporal` time-frame does not exist for a single
|
408
442
|
version since there is only a `created_at` timestamp.
|
409
443
|
|
410
|
-
|
444
|
+
#### [`audited`](https://github.com/collectiveidea/audited)
|
411
445
|
|
412
446
|
`audited` works in a similar manner as `paper_trail`. It stores all versions for all model types in
|
413
447
|
a single table, you must opt into using `jsonb` as the column type to store "changes", in case you
|
@@ -415,7 +449,7 @@ want to query them, and there is no concept of a `temporal` time-frame for a sin
|
|
415
449
|
makes opinionated decisions about contextual data requirements and stores them as top level data
|
416
450
|
types on the `audited` table.
|
417
451
|
|
418
|
-
|
452
|
+
#### [`discard`](https://github.com/jhawthorn/discard)
|
419
453
|
|
420
454
|
`discard` only covers soft-deletion. The act of "soft deleting" a record is only captured through
|
421
455
|
the time-stamping of a `discarded_at` column on the records table; there is no other capturing of
|
@@ -424,7 +458,7 @@ record is restored, the previous "discarded" awareness is lost. Since "discarded
|
|
424
458
|
the same table as "undiscarded" records, you must explicitly omit the discarded records from queries
|
425
459
|
across your app to keep them from leaking in.
|
426
460
|
|
427
|
-
|
461
|
+
#### [`paranoia`](https://github.com/rubysherpas/paranoia)
|
428
462
|
|
429
463
|
`paranoia` also only covers soft-deletion. In their README, they recommend using `discard` instead
|
430
464
|
of `paranoia` because of the fact they override ActiveRecord’s `delete` and `destroy` methods.
|
@@ -432,6 +466,13 @@ of `paranoia` because of the fact they override ActiveRecord’s `delete` and `d
|
|
432
466
|
`paranoia` works similarly to `discard` in that it keeps deleted records in the same table and tags
|
433
467
|
them with a `deleted_at` timestamp. No other information about the soft-deletion event is stored.
|
434
468
|
|
469
|
+
#### [`logidze`](https://github.com/palkan/logidze)
|
470
|
+
|
471
|
+
`logidze` is an interesting versioning alternative that leverages the power of PostgreSQL triggers.
|
472
|
+
Instead of storing the previous versions or changes in a separate table, it stores them in a
|
473
|
+
proprietary JSON format directly on the database row of the record itself. If does not support soft
|
474
|
+
deletion.
|
475
|
+
|
435
476
|
## Contributing
|
436
477
|
|
437
478
|
This gem still quite new and very open to feedback.
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators'
|
4
|
+
|
5
|
+
module Hoardable
|
6
|
+
# Generates an initializer file for {Hoardable} configuration and a migration with a PostgreSQL
|
7
|
+
# function.
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
10
|
+
include Rails::Generators::Migration
|
11
|
+
|
12
|
+
def create_initializer_file
|
13
|
+
create_file(
|
14
|
+
'config/initializers/hoardable.rb',
|
15
|
+
<<~TEXT
|
16
|
+
# Hoardable configuration defaults are below. Learn more at https://github.com/waymondo/hoardable#configuration
|
17
|
+
#
|
18
|
+
# Hoardable.enabled = true
|
19
|
+
# Hoardable.version_updates = true
|
20
|
+
# Hoardable.save_trash = true
|
21
|
+
TEXT
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_migration_file
|
26
|
+
migration_template 'functions.rb.erb', 'db/migrate/install_hoardable.rb'
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.next_migration_number(dir)
|
30
|
+
::ActiveRecord::Generators::Base.next_migration_number(dir)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class InstallHoardable < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
4
|
+
def up
|
5
|
+
execute(
|
6
|
+
<<~SQL
|
7
|
+
CREATE OR REPLACE FUNCTION hoardable_version_prevent_update() RETURNS trigger
|
8
|
+
LANGUAGE plpgsql AS
|
9
|
+
$$BEGIN
|
10
|
+
RAISE EXCEPTION 'updating a version is not allowed';
|
11
|
+
RETURN NEW;
|
12
|
+
END;$$;
|
13
|
+
SQL
|
14
|
+
)
|
15
|
+
end
|
16
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
3
|
+
class Create<%= class_name.singularize.delete(':') %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
4
4
|
def change
|
5
5
|
create_enum :hoardable_operation, %w[update delete insert]
|
6
6
|
create_table :<%= singularized_table_name %>_versions, id: false, options: 'INHERITS (<%= table_name %>)' do |t|
|
@@ -10,20 +10,25 @@ class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%=
|
|
10
10
|
t.enum :_operation, enum_type: 'hoardable_operation', null: false, index: true
|
11
11
|
t.<%= foreign_key_type %> :hoardable_source_id, null: false, index: true
|
12
12
|
end
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
13
|
+
reversible do |dir|
|
14
|
+
dir.up do
|
15
|
+
execute(
|
16
|
+
<<~SQL
|
17
|
+
CREATE TRIGGER <%= singularized_table_name %>_versions_prevent_update
|
18
|
+
BEFORE UPDATE ON <%= singularized_table_name %>_versions FOR EACH ROW
|
19
|
+
EXECUTE PROCEDURE hoardable_version_prevent_update();
|
20
|
+
SQL
|
21
|
+
)
|
22
|
+
end
|
23
|
+
dir.down do
|
24
|
+
execute(
|
25
|
+
<<~SQL
|
26
|
+
DROP TRIGGER <%= singularized_table_name %>_versions_prevent_update
|
27
|
+
ON <%= singularized_table_name %>_versions;
|
28
|
+
SQL
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
27
32
|
add_index(:<%= singularized_table_name %>_versions, :id, unique: true)
|
28
33
|
add_index(
|
29
34
|
:<%= singularized_table_name %>_versions,
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
3
|
+
class Create<%= class_name.singularize.delete(':') %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
4
4
|
def change
|
5
5
|
reversible do |dir|
|
6
6
|
dir.up do
|
@@ -25,20 +25,25 @@ class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%=
|
|
25
25
|
t.column :_operation, :hoardable_operation, null: false, index: true
|
26
26
|
t.<%= foreign_key_type %> :hoardable_source_id, null: false, index: true
|
27
27
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
28
|
+
reversible do |dir|
|
29
|
+
dir.up do
|
30
|
+
execute(
|
31
|
+
<<~SQL
|
32
|
+
CREATE TRIGGER <%= singularized_table_name %>_versions_prevent_update
|
33
|
+
BEFORE UPDATE ON <%= singularized_table_name %>_versions FOR EACH ROW
|
34
|
+
EXECUTE PROCEDURE hoardable_version_prevent_update();
|
35
|
+
SQL
|
36
|
+
)
|
37
|
+
end
|
38
|
+
dir.down do
|
39
|
+
execute(
|
40
|
+
<<~SQL
|
41
|
+
DROP TRIGGER <%= singularized_table_name %>_versions_prevent_update
|
42
|
+
ON <%= singularized_table_name %>_versions;
|
43
|
+
SQL
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
42
47
|
add_index(:<%= singularized_table_name %>_versions, :id, unique: true)
|
43
48
|
add_index(
|
44
49
|
:<%= singularized_table_name %>_versions,
|
@@ -7,104 +7,12 @@ module Hoardable
|
|
7
7
|
module Associations
|
8
8
|
extend ActiveSupport::Concern
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
private
|
18
|
-
|
19
|
-
def hoardable_scope
|
20
|
-
if Hoardable.instance_variable_get('@at') &&
|
21
|
-
(hoardable_source_id = @association.owner.hoardable_source_id)
|
22
|
-
@association.scope.rewhere(@association.reflection.foreign_key => hoardable_source_id)
|
23
|
-
else
|
24
|
-
@association.scope
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
private_constant :HasManyExtension
|
29
|
-
|
30
|
-
# A private service class for installing +ActiveRecord+ association overrides.
|
31
|
-
class Overrider
|
32
|
-
attr_reader :klass
|
33
|
-
|
34
|
-
def initialize(klass)
|
35
|
-
@klass = klass
|
36
|
-
end
|
37
|
-
|
38
|
-
def override_belongs_to(name)
|
39
|
-
klass.define_method("trashable_#{name}") do
|
40
|
-
source_reflection = klass.reflections[name.to_s]
|
41
|
-
source_reflection.version_class.trashed.only_most_recent.find_by(
|
42
|
-
hoardable_source_id: source_reflection.foreign_key
|
43
|
-
)
|
44
|
-
end
|
45
|
-
|
46
|
-
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
47
|
-
def #{name}
|
48
|
-
super || trashable_#{name}
|
49
|
-
end
|
50
|
-
RUBY
|
51
|
-
end
|
52
|
-
|
53
|
-
def override_has_one(name)
|
54
|
-
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
55
|
-
def #{name}
|
56
|
-
return super unless (at = Hoardable.instance_variable_get('@at'))
|
57
|
-
|
58
|
-
super&.version_at(at) ||
|
59
|
-
_reflections["profile"].klass.where(_reflections["profile"].foreign_key => id).first
|
60
|
-
end
|
61
|
-
RUBY
|
62
|
-
end
|
63
|
-
|
64
|
-
def override_has_many(name)
|
65
|
-
# This hack is needed to force Rails to not use any existing method cache so that the
|
66
|
-
# {HasManyExtension} scope is always used.
|
67
|
-
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
68
|
-
def #{name}
|
69
|
-
super.extending
|
70
|
-
end
|
71
|
-
RUBY
|
72
|
-
end
|
73
|
-
end
|
74
|
-
private_constant :Overrider
|
75
|
-
|
76
|
-
class_methods do
|
77
|
-
def belongs_to(*args)
|
78
|
-
options = args.extract_options!
|
79
|
-
trashable = options.delete(:trashable)
|
80
|
-
super(*args, **options)
|
81
|
-
return unless trashable
|
82
|
-
|
83
|
-
hoardable_association_overrider.override_belongs_to(args.first)
|
84
|
-
end
|
85
|
-
|
86
|
-
def has_one(*args)
|
87
|
-
options = args.extract_options!
|
88
|
-
hoardable = options.delete(:hoardable)
|
89
|
-
super(*args, **options)
|
90
|
-
return unless hoardable
|
91
|
-
|
92
|
-
hoardable_association_overrider.override_has_one(args.first)
|
93
|
-
end
|
94
|
-
|
95
|
-
def has_many(*args, &block)
|
96
|
-
options = args.extract_options!
|
97
|
-
options[:extend] = Array(options[:extend]).push(HasManyExtension) if options.delete(:hoardable)
|
98
|
-
super(*args, **options, &block)
|
99
|
-
|
100
|
-
hoardable_association_overrider.override_has_many(args.first)
|
101
|
-
end
|
102
|
-
|
103
|
-
private
|
104
|
-
|
105
|
-
def hoardable_association_overrider
|
106
|
-
@hoardable_association_overrider ||= Overrider.new(self)
|
107
|
-
end
|
10
|
+
included do
|
11
|
+
include HasMany
|
12
|
+
include HasOne
|
13
|
+
include BelongsTo
|
14
|
+
include HasRichText
|
15
|
+
include HasOneAttached
|
108
16
|
end
|
109
17
|
end
|
110
18
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hoardable
|
4
|
+
# A {Hoardable} subclass of {ActiveStorage::Attachment}
|
5
|
+
class Attachment < ActiveStorage::Attachment
|
6
|
+
include Model
|
7
|
+
|
8
|
+
class CreateOne < ActiveStorage::Attached::Changes::CreateOne
|
9
|
+
private
|
10
|
+
|
11
|
+
def build_attachment
|
12
|
+
Attachment.new(record: record, name: name, blob: blob)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hoardable
|
4
|
+
# Provides awareness of trashed source records to +belongs_to+ relationships.
|
5
|
+
module BelongsTo
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def belongs_to(*args)
|
10
|
+
options = args.extract_options!
|
11
|
+
trashable = options.delete(:trashable)
|
12
|
+
super(*args, **options)
|
13
|
+
return unless trashable
|
14
|
+
|
15
|
+
hoardable_override_belongs_to(args.first)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def hoardable_override_belongs_to(name)
|
21
|
+
define_method("trashed_#{name}") do
|
22
|
+
source_reflection = reflections[name.to_s]
|
23
|
+
source_reflection.version_class.trashed.only_most_recent.find_by(
|
24
|
+
hoardable_source_id: source_reflection.foreign_key
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
29
|
+
def #{name}
|
30
|
+
super || trashed_#{name}
|
31
|
+
end
|
32
|
+
RUBY
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
private_constant :BelongsTo
|
37
|
+
end
|
@@ -34,6 +34,9 @@ module Hoardable
|
|
34
34
|
end.freeze
|
35
35
|
private_constant :HOARDABLE_VERSION_UPDATES
|
36
36
|
|
37
|
+
SUPPORTS_ENCRYPTED_ACTION_TEXT = ActiveRecord.version >= ::Gem::Version.new('7.0')
|
38
|
+
private_constant :SUPPORTS_ENCRYPTED_ACTION_TEXT
|
39
|
+
|
37
40
|
@context = {}
|
38
41
|
@config = CONFIG_KEYS.to_h do |key|
|
39
42
|
[key, true]
|
@@ -91,4 +94,22 @@ module Hoardable
|
|
91
94
|
@logger ||= ActiveSupport::TaggedLogging.new(Logger.new($stdout))
|
92
95
|
end
|
93
96
|
end
|
97
|
+
|
98
|
+
# A +Rails+ engine for providing support for +ActionText+
|
99
|
+
class Engine < ::Rails::Engine
|
100
|
+
isolate_namespace Hoardable
|
101
|
+
|
102
|
+
initializer 'hoardable.action_text' do
|
103
|
+
ActiveSupport.on_load(:action_text_rich_text) do
|
104
|
+
require_relative 'rich_text'
|
105
|
+
require_relative 'encrypted_rich_text' if SUPPORTS_ENCRYPTED_ACTION_TEXT
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
initializer 'hoardable.active_storage' do
|
110
|
+
ActiveSupport.on_load(:active_storage_attachment) do
|
111
|
+
require_relative 'attachment'
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
94
115
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hoardable
|
4
|
+
# Provides temporal version awareness to +has_many+ relationships.
|
5
|
+
module HasMany
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
# An +ActiveRecord+ extension that allows looking up {VersionModel}s by +hoardable_source_id+ as
|
9
|
+
# if they were {SourceModel}s when using {Hoardable#at}.
|
10
|
+
module HasManyExtension
|
11
|
+
def scope
|
12
|
+
@scope ||= hoardable_scope
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def hoardable_scope
|
18
|
+
if Hoardable.instance_variable_get('@at') &&
|
19
|
+
(hoardable_source_id = @association.owner.hoardable_source_id)
|
20
|
+
@association.scope.rewhere(@association.reflection.foreign_key => hoardable_source_id)
|
21
|
+
else
|
22
|
+
@association.scope
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
private_constant :HasManyExtension
|
27
|
+
|
28
|
+
class_methods do
|
29
|
+
def has_many(*args, &block)
|
30
|
+
options = args.extract_options!
|
31
|
+
options[:extend] = Array(options[:extend]).push(HasManyExtension) if options.delete(:hoardable)
|
32
|
+
super(*args, **options, &block)
|
33
|
+
|
34
|
+
# This hack is needed to force Rails to not use any existing method cache so that the
|
35
|
+
# {HasManyExtension} scope is always used.
|
36
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
37
|
+
def #{args.first}
|
38
|
+
super.extending
|
39
|
+
end
|
40
|
+
RUBY
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
private_constant :HasMany
|
45
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hoardable
|
4
|
+
# Provides temporal version awareness to +has_one+ relationships.
|
5
|
+
module HasOne
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def has_one(*args)
|
10
|
+
options = args.extract_options!
|
11
|
+
hoardable = options.delete(:hoardable)
|
12
|
+
association = super(*args, **options)
|
13
|
+
name = args.first
|
14
|
+
return unless hoardable || association[name.to_s].options[:class_name].match?(/RichText$/)
|
15
|
+
|
16
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
17
|
+
def #{name}
|
18
|
+
return super unless (at = Hoardable.instance_variable_get('@at'))
|
19
|
+
|
20
|
+
super&.version_at(at) ||
|
21
|
+
_reflections['profile'].klass.where(_reflections['profile'].foreign_key => id).first
|
22
|
+
end
|
23
|
+
RUBY
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
private_constant :HasOne
|
28
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hoardable
|
4
|
+
# Provides temporal version awareness for +ActionText+.
|
5
|
+
module HasOneAttached
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def has_one_attached(name, **options, &block)
|
10
|
+
hoardable = options.delete(:hoardable)
|
11
|
+
super(name, **options, &block)
|
12
|
+
return unless hoardable
|
13
|
+
|
14
|
+
'ActiveStorage::Attachment'.constantize
|
15
|
+
reflection_options = reflections["#{name}_attachment"].options
|
16
|
+
reflection_options[:class_name] = reflection_options[:class_name].sub(/ActiveStorage/, 'Hoardable')
|
17
|
+
|
18
|
+
generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
19
|
+
# frozen_string_literal: true
|
20
|
+
def #{name}=(attachable)
|
21
|
+
attachment_changes["#{name}"] =
|
22
|
+
if attachable.nil?
|
23
|
+
ActiveStorage::Attached::Changes::DeleteOne.new("#{name}", self)
|
24
|
+
else
|
25
|
+
Hoardable::Attachment::CreateOne.new("#{name}", self, attachable)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
CODE
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
private_constant :HasOneAttached
|
33
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Hoardable
|
4
|
+
# Provides temporal version awareness for +ActionText+.
|
5
|
+
module HasRichText
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def has_rich_text(name, encrypted: false, hoardable: false)
|
10
|
+
if SUPPORTS_ENCRYPTED_ACTION_TEXT
|
11
|
+
super(name, encrypted: encrypted)
|
12
|
+
else
|
13
|
+
super(name)
|
14
|
+
end
|
15
|
+
return unless hoardable
|
16
|
+
|
17
|
+
reflection_options = reflections["rich_text_#{name}"].options
|
18
|
+
reflection_options[:class_name] = reflection_options[:class_name].sub(/ActionText/, 'Hoardable')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
private_constant :HasRichText
|
23
|
+
end
|
data/lib/hoardable/model.rb
CHANGED
@@ -53,11 +53,17 @@ module Hoardable
|
|
53
53
|
TracePoint.new(:end) do |trace|
|
54
54
|
next unless self == trace.self
|
55
55
|
|
56
|
-
|
57
|
-
|
58
|
-
|
56
|
+
full_version_class_name = "#{name}#{VERSION_CLASS_SUFFIX}"
|
57
|
+
if (namespace_match = full_version_class_name.match(/(.*)::(.*)/))
|
58
|
+
object_namespace = namespace_match[1].constantize
|
59
|
+
version_class_name = namespace_match[2]
|
60
|
+
else
|
61
|
+
object_namespace = Object
|
62
|
+
version_class_name = full_version_class_name
|
63
|
+
end
|
64
|
+
unless Object.const_defined?(full_version_class_name)
|
65
|
+
object_namespace.const_set(version_class_name, Class.new(self) { include VersionModel })
|
59
66
|
end
|
60
|
-
|
61
67
|
include SourceModel
|
62
68
|
|
63
69
|
trace.disable
|
data/lib/hoardable/version.rb
CHANGED
data/lib/hoardable.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'active_record'
|
3
4
|
require_relative 'hoardable/version'
|
4
|
-
require_relative 'hoardable/
|
5
|
+
require_relative 'hoardable/engine'
|
5
6
|
require_relative 'hoardable/finder_methods'
|
6
7
|
require_relative 'hoardable/scopes'
|
7
8
|
require_relative 'hoardable/error'
|
@@ -10,5 +11,10 @@ require_relative 'hoardable/source_model'
|
|
10
11
|
require_relative 'hoardable/version_model'
|
11
12
|
require_relative 'hoardable/model'
|
12
13
|
require_relative 'hoardable/associations'
|
14
|
+
require_relative 'hoardable/has_many'
|
15
|
+
require_relative 'hoardable/belongs_to'
|
16
|
+
require_relative 'hoardable/has_one'
|
17
|
+
require_relative 'hoardable/has_rich_text'
|
18
|
+
require_relative 'hoardable/has_one_attached'
|
13
19
|
require_relative 'generators/hoardable/migration_generator'
|
14
|
-
require_relative 'generators/hoardable/
|
20
|
+
require_relative 'generators/hoardable/install_generator'
|
data/sig/hoardable.rbs
CHANGED
@@ -17,8 +17,17 @@ module Hoardable
|
|
17
17
|
def self.at: (untyped datetime) -> untyped
|
18
18
|
def self.logger: -> untyped
|
19
19
|
|
20
|
+
module FinderMethods
|
21
|
+
def find_one: (untyped id) -> untyped
|
22
|
+
def find_some: (untyped ids) -> untyped
|
23
|
+
|
24
|
+
private
|
25
|
+
def hoardable_source_ids: ([untyped] ids) -> Array[untyped]
|
26
|
+
end
|
27
|
+
|
20
28
|
module Scopes
|
21
29
|
TABLEOID_AREL_CONDITIONS: Proc
|
30
|
+
self.@klass: bot
|
22
31
|
|
23
32
|
private
|
24
33
|
def tableoid: -> untyped
|
@@ -30,6 +39,10 @@ module Hoardable
|
|
30
39
|
class Error < StandardError
|
31
40
|
end
|
32
41
|
|
42
|
+
class CreatedAtColumnMissingError < Error
|
43
|
+
def initialize: (untyped source_table_name) -> void
|
44
|
+
end
|
45
|
+
|
33
46
|
class DatabaseClient
|
34
47
|
@hoardable_version_source_id: untyped
|
35
48
|
|
@@ -45,7 +58,7 @@ module Hoardable
|
|
45
58
|
def assign_hoardable_context: (:event_uuid | :meta | :note | :whodunit key) -> nil
|
46
59
|
def unset_hoardable_version_and_event_uuid: -> nil
|
47
60
|
def previous_temporal_tsrange_end: -> untyped
|
48
|
-
def hoardable_source_epoch: ->
|
61
|
+
def hoardable_source_epoch: -> untyped
|
49
62
|
end
|
50
63
|
|
51
64
|
module SourceModel
|
@@ -55,7 +68,8 @@ module Hoardable
|
|
55
68
|
attr_reader hoardable_version: untyped
|
56
69
|
def trashed?: -> untyped
|
57
70
|
def version?: -> untyped
|
58
|
-
def at: (untyped datetime) -> SourceModel
|
71
|
+
def at: (untyped datetime) -> SourceModel?
|
72
|
+
def version_at: (untyped datetime) -> untyped
|
59
73
|
def revert_to!: (untyped datetime) -> SourceModel?
|
60
74
|
def hoardable_source_id: -> untyped
|
61
75
|
|
@@ -65,10 +79,6 @@ module Hoardable
|
|
65
79
|
public
|
66
80
|
def version_class: -> untyped
|
67
81
|
def hoardable: -> untyped
|
68
|
-
|
69
|
-
module FinderMethods
|
70
|
-
def find_one: (untyped id) -> untyped
|
71
|
-
end
|
72
82
|
end
|
73
83
|
|
74
84
|
module VersionModel
|
@@ -98,10 +108,16 @@ module Hoardable
|
|
98
108
|
end
|
99
109
|
|
100
110
|
module Associations
|
101
|
-
|
102
|
-
|
111
|
+
@hoardable_association_overrider: Overrider
|
112
|
+
|
113
|
+
def belongs_to: (*untyped args) -> nil
|
114
|
+
def has_one: (*untyped args) -> nil
|
115
|
+
def has_many: (*untyped args) -> untyped
|
116
|
+
|
117
|
+
private
|
118
|
+
def hoardable_association_overrider: -> Overrider
|
103
119
|
|
104
|
-
module
|
120
|
+
module HasManyExtension
|
105
121
|
@scope: untyped
|
106
122
|
@association: bot
|
107
123
|
|
@@ -110,6 +126,14 @@ module Hoardable
|
|
110
126
|
private
|
111
127
|
def hoardable_scope: -> untyped
|
112
128
|
end
|
129
|
+
|
130
|
+
class Overrider
|
131
|
+
attr_reader klass: Associations
|
132
|
+
def initialize: (Associations klass) -> void
|
133
|
+
def override_belongs_to: (untyped name) -> untyped
|
134
|
+
def override_has_one: (untyped name) -> untyped
|
135
|
+
def override_has_many: (untyped name) -> untyped
|
136
|
+
end
|
113
137
|
end
|
114
138
|
|
115
139
|
class MigrationGenerator
|
@@ -121,7 +145,9 @@ module Hoardable
|
|
121
145
|
def singularized_table_name: -> untyped
|
122
146
|
end
|
123
147
|
|
124
|
-
class
|
148
|
+
class InstallGenerator
|
125
149
|
def create_initializer_file: -> untyped
|
150
|
+
def create_migration_file: -> untyped
|
151
|
+
def self.next_migration_number: (untyped dir) -> untyped
|
126
152
|
end
|
127
153
|
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.
|
4
|
+
version: 0.11.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-10-
|
11
|
+
date: 2022-10-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -104,17 +104,26 @@ files:
|
|
104
104
|
- LICENSE.txt
|
105
105
|
- README.md
|
106
106
|
- Rakefile
|
107
|
-
- lib/generators/hoardable/
|
107
|
+
- lib/generators/hoardable/install_generator.rb
|
108
108
|
- lib/generators/hoardable/migration_generator.rb
|
109
|
+
- lib/generators/hoardable/templates/functions.rb.erb
|
109
110
|
- lib/generators/hoardable/templates/migration.rb.erb
|
110
111
|
- lib/generators/hoardable/templates/migration_6.rb.erb
|
111
112
|
- lib/hoardable.rb
|
112
113
|
- lib/hoardable/associations.rb
|
114
|
+
- lib/hoardable/attachment.rb
|
115
|
+
- lib/hoardable/belongs_to.rb
|
113
116
|
- lib/hoardable/database_client.rb
|
117
|
+
- lib/hoardable/encrypted_rich_text.rb
|
118
|
+
- lib/hoardable/engine.rb
|
114
119
|
- lib/hoardable/error.rb
|
115
120
|
- lib/hoardable/finder_methods.rb
|
116
|
-
- lib/hoardable/
|
121
|
+
- lib/hoardable/has_many.rb
|
122
|
+
- lib/hoardable/has_one.rb
|
123
|
+
- lib/hoardable/has_one_attached.rb
|
124
|
+
- lib/hoardable/has_rich_text.rb
|
117
125
|
- lib/hoardable/model.rb
|
126
|
+
- lib/hoardable/rich_text.rb
|
118
127
|
- lib/hoardable/scopes.rb
|
119
128
|
- lib/hoardable/source_model.rb
|
120
129
|
- lib/hoardable/version.rb
|
@@ -1,21 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rails/generators'
|
4
|
-
|
5
|
-
module Hoardable
|
6
|
-
# Generates an initializer file for {Hoardable} configuration.
|
7
|
-
class InitializerGenerator < Rails::Generators::Base
|
8
|
-
def create_initializer_file
|
9
|
-
create_file(
|
10
|
-
'config/initializers/hoardable.rb',
|
11
|
-
<<~TEXT
|
12
|
-
# Hoardable configuration defaults are below. Learn more at https://github.com/waymondo/hoardable#configuration
|
13
|
-
#
|
14
|
-
# Hoardable.enabled = true
|
15
|
-
# Hoardable.version_updates = true
|
16
|
-
# Hoardable.save_trash = true
|
17
|
-
TEXT
|
18
|
-
)
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|