hoardable 0.10.1 → 0.12.2
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 +60 -19
- data/lib/generators/hoardable/install_generator.rb +33 -0
- data/lib/generators/hoardable/migration_generator.rb +5 -7
- data/lib/generators/hoardable/templates/install.rb.erb +52 -0
- data/lib/generators/hoardable/templates/migration.rb.erb +32 -20
- data/lib/hoardable/associations.rb +5 -98
- data/lib/hoardable/belongs_to.rb +37 -0
- data/lib/hoardable/database_client.rb +12 -13
- data/lib/hoardable/encrypted_rich_text.rb +8 -0
- data/lib/hoardable/{hoardable.rb → engine.rb} +15 -0
- data/lib/hoardable/finder_methods.rb +4 -4
- data/lib/hoardable/has_many.rb +49 -0
- data/lib/hoardable/has_one.rb +28 -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/scopes.rb +2 -2
- data/lib/hoardable/source_model.rb +5 -10
- data/lib/hoardable/version.rb +1 -1
- data/lib/hoardable/version_model.rb +13 -13
- data/lib/hoardable.rb +7 -2
- data/sig/hoardable.rbs +60 -19
- metadata +11 -5
- data/lib/generators/hoardable/initializer_generator.rb +0 -21
- data/lib/generators/hoardable/templates/migration_6.rb.erb +0 -49
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eec6cda69bb2bbb1aeec6a0c460881c33abbe1de123ef9178d2c16378374b2bd
|
4
|
+
data.tar.gz: f3f57007e08f958017483dfffd0275b5b70c0ef1522984ea78da4c595a9e5506
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 04a13fec8198d4c4fda05b5364a7925755e7f2708004e15a564f243b64836577f4393cb502e7dd7d88ea123df29909ccb36a1d603cae8b2e711958250dc45763
|
7
|
+
data.tar.gz: 34a59cb91ed643784e4aa6ab88abac2d3e7a59a59945bcdce6e849aa441bc1490a6870c2661f65c31d246e4cb87bb39c4502e6cb8ce05cb3c42a6ad0e56f1140
|
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
|
|
@@ -83,9 +84,9 @@ of that model. As we continue our example from above:
|
|
83
84
|
```
|
84
85
|
$ irb
|
85
86
|
>> Post
|
86
|
-
=> Post(id: integer, body: text, user_id: integer, created_at: datetime)
|
87
|
+
=> Post(id: integer, body: text, user_id: integer, created_at: datetime, hoardable_id: integer)
|
87
88
|
>> PostVersion
|
88
|
-
=> PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, _data: jsonb, _during: tsrange
|
89
|
+
=> PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, hoardable_id: integer, _data: jsonb, _during: tsrange)
|
89
90
|
```
|
90
91
|
|
91
92
|
A `Post` now `has_many :versions`. With the default configuration, whenever an update and deletion
|
@@ -143,7 +144,7 @@ If you want to look-up the version of a record at a specific time, you can use t
|
|
143
144
|
```ruby
|
144
145
|
post.at(1.day.ago) # => #<PostVersion>
|
145
146
|
# or you can use the scope on the version model class
|
146
|
-
PostVersion.at(1.day.ago).find_by(
|
147
|
+
PostVersion.at(1.day.ago).find_by(hoardable_id: post.id) # => #<PostVersion>
|
147
148
|
```
|
148
149
|
|
149
150
|
The source model class also has an `.at` method:
|
@@ -356,7 +357,7 @@ Hoardable.at(datetime) do
|
|
356
357
|
post.comments.size # => 2
|
357
358
|
post.id # => 2
|
358
359
|
post.version? # => true
|
359
|
-
post.
|
360
|
+
post.hoardable_id # => 1
|
360
361
|
end
|
361
362
|
```
|
362
363
|
|
@@ -369,9 +370,9 @@ version, even though it is masquerading as a `Post`.
|
|
369
370
|
|
370
371
|
If you are ever unsure if a Hoardable record is a "source" or a "version", you can be sure by
|
371
372
|
calling `version?` on it. If you want to get the true original source record ID, you can call
|
372
|
-
`
|
373
|
+
`hoardable_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
|
+
## Action Text
|
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
|
+
post.content.to_plain_text # => 'Goodbye Cruel World'
|
424
|
+
Hoardable.at(datetime) do
|
425
|
+
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 'install.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
|
@@ -12,7 +12,7 @@ module Hoardable
|
|
12
12
|
class_option :foreign_key_type, type: :string
|
13
13
|
|
14
14
|
def create_versions_table
|
15
|
-
migration_template
|
15
|
+
migration_template 'migration.rb.erb', "db/migrate/create_#{singularized_table_name}_versions.rb"
|
16
16
|
end
|
17
17
|
|
18
18
|
no_tasks do
|
@@ -23,12 +23,10 @@ module Hoardable
|
|
23
23
|
'bigint'
|
24
24
|
end
|
25
25
|
|
26
|
-
def
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
'migration.rb.erb'
|
31
|
-
end
|
26
|
+
def primary_key
|
27
|
+
options[:primary_key] || class_name.singularize.constantize.primary_key
|
28
|
+
rescue StandardError
|
29
|
+
'id'
|
32
30
|
end
|
33
31
|
|
34
32
|
def singularized_table_name
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class InstallHoardable < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
4
|
+
def up
|
5
|
+
execute(
|
6
|
+
<<~SQL
|
7
|
+
DO $$
|
8
|
+
BEGIN
|
9
|
+
IF NOT EXISTS (
|
10
|
+
SELECT 1 FROM pg_type t WHERE t.typname = 'hoardable_operation'
|
11
|
+
) THEN
|
12
|
+
CREATE TYPE hoardable_operation AS ENUM ('update', 'delete', 'insert');
|
13
|
+
END IF;
|
14
|
+
END
|
15
|
+
$$;
|
16
|
+
CREATE OR REPLACE FUNCTION hoardable_source_set_id() RETURNS trigger
|
17
|
+
LANGUAGE plpgsql AS
|
18
|
+
$$
|
19
|
+
DECLARE
|
20
|
+
_pk information_schema.constraint_column_usage.column_name%TYPE;
|
21
|
+
_id _pk%TYPE;
|
22
|
+
BEGIN
|
23
|
+
SELECT c.column_name
|
24
|
+
FROM information_schema.table_constraints t
|
25
|
+
JOIN information_schema.constraint_column_usage c
|
26
|
+
ON c.constraint_name = t.constraint_name
|
27
|
+
WHERE c.table_name = TG_TABLE_NAME AND t.constraint_type = 'PRIMARY KEY'
|
28
|
+
LIMIT 1
|
29
|
+
INTO _pk;
|
30
|
+
EXECUTE format('SELECT $1.%I', _pk) INTO _id USING NEW;
|
31
|
+
NEW.hoardable_id = _id;
|
32
|
+
RETURN NEW;
|
33
|
+
END;$$;
|
34
|
+
CREATE OR REPLACE FUNCTION hoardable_version_prevent_update() RETURNS trigger
|
35
|
+
LANGUAGE plpgsql AS
|
36
|
+
$$BEGIN
|
37
|
+
RAISE EXCEPTION 'updating a version is not allowed';
|
38
|
+
RETURN NEW;
|
39
|
+
END;$$;
|
40
|
+
SQL
|
41
|
+
)
|
42
|
+
end
|
43
|
+
def down
|
44
|
+
execute(
|
45
|
+
<<~SQL
|
46
|
+
DROP TYPE IF EXISTS hoardable_operation;
|
47
|
+
DROP FUNCTION IF EXISTS hoardable_version_prevent_update();
|
48
|
+
DROP FUNCTION IF EXISTS hoardable_source_set_id();
|
49
|
+
SQL
|
50
|
+
)
|
51
|
+
end
|
52
|
+
end
|
@@ -1,33 +1,45 @@
|
|
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
|
+
add_column :<%= table_name %>, :hoardable_id, :<%= foreign_key_type %>
|
6
|
+
add_index :<%= table_name %>, :hoardable_id
|
6
7
|
create_table :<%= singularized_table_name %>_versions, id: false, options: 'INHERITS (<%= table_name %>)' do |t|
|
7
8
|
t.jsonb :_data
|
8
9
|
t.tsrange :_during, null: false
|
9
10
|
t.uuid :_event_uuid, null: false, index: true
|
10
|
-
t.
|
11
|
-
t.<%= foreign_key_type %> :hoardable_source_id, null: false, index: true
|
11
|
+
t.column :_operation, :hoardable_operation, null: false, index: true
|
12
12
|
end
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
13
|
+
reversible do |dir|
|
14
|
+
dir.up do
|
15
|
+
execute(
|
16
|
+
<<~SQL
|
17
|
+
UPDATE <%= table_name %> SET hoardable_id = <%= primary_key %>;
|
18
|
+
CREATE TRIGGER <%= singularized_table_name %>_versions_prevent_update
|
19
|
+
BEFORE UPDATE ON <%= singularized_table_name %>_versions FOR EACH ROW
|
20
|
+
EXECUTE PROCEDURE hoardable_version_prevent_update();
|
21
|
+
CREATE TRIGGER <%= table_name %>_set_hoardable_id
|
22
|
+
BEFORE INSERT ON <%= table_name %> FOR EACH ROW
|
23
|
+
EXECUTE PROCEDURE hoardable_source_set_id();
|
24
|
+
SQL
|
25
|
+
)
|
26
|
+
end
|
27
|
+
dir.down do
|
28
|
+
execute(
|
29
|
+
<<~SQL
|
30
|
+
DROP TRIGGER <%= singularized_table_name %>_versions_prevent_update
|
31
|
+
ON <%= singularized_table_name %>_versions;
|
32
|
+
DROP TRIGGER <%= table_name %>_set_hoardable_id
|
33
|
+
ON <%= table_name %>;
|
34
|
+
SQL
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
change_column_null :<%= table_name %>, :hoardable_id, false
|
39
|
+
add_index(:<%= singularized_table_name %>_versions, :<%= primary_key %>, unique: true)
|
28
40
|
add_index(
|
29
41
|
:<%= singularized_table_name %>_versions,
|
30
|
-
%i[_during
|
42
|
+
%i[_during hoardable_id],
|
31
43
|
name: 'idx_<%= singularized_table_name %>_versions_temporally'
|
32
44
|
)
|
33
45
|
end
|
@@ -7,104 +7,11 @@ module Hoardable
|
|
7
7
|
module Associations
|
8
8
|
extend ActiveSupport::Concern
|
9
9
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
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
|
108
15
|
end
|
109
16
|
end
|
110
17
|
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_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
|
@@ -13,30 +13,25 @@ module Hoardable
|
|
13
13
|
delegate :version_class, to: :source_record
|
14
14
|
|
15
15
|
def insert_hoardable_version(operation, &block)
|
16
|
-
version = version_class.insert(initialize_version_attributes(operation), returning:
|
17
|
-
version_id = version[0][
|
16
|
+
version = version_class.insert(initialize_version_attributes(operation), returning: source_primary_key.to_sym)
|
17
|
+
version_id = version[0][source_primary_key]
|
18
18
|
source_record.instance_variable_set('@hoardable_version', version_class.find(version_id))
|
19
19
|
source_record.run_callbacks(:versioned, &block)
|
20
20
|
end
|
21
21
|
|
22
|
-
def
|
23
|
-
|
24
|
-
end
|
25
|
-
|
26
|
-
def hoardable_version_source_id
|
27
|
-
@hoardable_version_source_id ||= query_hoardable_version_source_id
|
22
|
+
def source_primary_key
|
23
|
+
source_record.class.primary_key
|
28
24
|
end
|
29
25
|
|
30
|
-
def
|
31
|
-
|
32
|
-
version_class.where(primary_key => source_record.read_attribute(primary_key)).pluck('hoardable_source_id')[0]
|
26
|
+
def find_or_initialize_hoardable_event_uuid
|
27
|
+
Thread.current[:hoardable_event_uuid] ||= ActiveRecord::Base.connection.query('SELECT gen_random_uuid();')[0][0]
|
33
28
|
end
|
34
29
|
|
35
30
|
def initialize_version_attributes(operation)
|
36
|
-
|
31
|
+
source_attributes_without_primary_key.merge(
|
37
32
|
source_record.changes.transform_values { |h| h[0] },
|
38
33
|
{
|
39
|
-
'
|
34
|
+
'hoardable_id' => source_record.id,
|
40
35
|
'_event_uuid' => find_or_initialize_hoardable_event_uuid,
|
41
36
|
'_operation' => operation,
|
42
37
|
'_data' => initialize_hoardable_data.merge(changes: source_record.changes),
|
@@ -45,6 +40,10 @@ module Hoardable
|
|
45
40
|
)
|
46
41
|
end
|
47
42
|
|
43
|
+
def source_attributes_without_primary_key
|
44
|
+
source_record.attributes_before_type_cast.without(source_primary_key)
|
45
|
+
end
|
46
|
+
|
48
47
|
def initialize_temporal_range
|
49
48
|
((previous_temporal_tsrange_end || hoardable_source_epoch)..Time.now.utc)
|
50
49
|
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,16 @@ 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
|
+
end
|
94
109
|
end
|
@@ -7,18 +7,18 @@ module Hoardable
|
|
7
7
|
# with the class method +hoardable+.
|
8
8
|
module FinderMethods
|
9
9
|
def find_one(id)
|
10
|
-
super(
|
10
|
+
super(hoardable_ids([id])[0])
|
11
11
|
end
|
12
12
|
|
13
13
|
def find_some(ids)
|
14
|
-
super(
|
14
|
+
super(hoardable_ids(ids))
|
15
15
|
end
|
16
16
|
|
17
17
|
private
|
18
18
|
|
19
|
-
def
|
19
|
+
def hoardable_ids(ids)
|
20
20
|
ids.map do |id|
|
21
|
-
version_class.where(
|
21
|
+
version_class.where(hoardable_id: id).select(primary_key).ids[0] || id
|
22
22
|
end
|
23
23
|
end
|
24
24
|
end
|
@@ -0,0 +1,49 @@
|
|
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_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') && (hoardable_id = @association.owner.hoardable_id)
|
19
|
+
if @association.reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
20
|
+
@association.reflection.source_reflection.instance_variable_set(
|
21
|
+
'@active_record_primary_key', 'hoardable_id'
|
22
|
+
)
|
23
|
+
end
|
24
|
+
@association.scope.rewhere(@association.reflection.foreign_key => hoardable_id)
|
25
|
+
else
|
26
|
+
@association.scope
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
private_constant :HasManyExtension
|
31
|
+
|
32
|
+
class_methods do
|
33
|
+
def has_many(*args, &block)
|
34
|
+
options = args.extract_options!
|
35
|
+
options[:extend] = Array(options[:extend]).push(HasManyExtension) if options.delete(:hoardable)
|
36
|
+
super(*args, **options, &block)
|
37
|
+
|
38
|
+
# This hack is needed to force Rails to not use any existing method cache so that the
|
39
|
+
# {HasManyExtension} scope is always used.
|
40
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
41
|
+
def #{args.first}
|
42
|
+
super.extending
|
43
|
+
end
|
44
|
+
RUBY
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
private_constant :HasMany
|
49
|
+
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,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/scopes.rb
CHANGED
@@ -62,10 +62,10 @@ module Hoardable
|
|
62
62
|
scope :at, lambda { |datetime|
|
63
63
|
raise(CreatedAtColumnMissingError, @klass.table_name) unless @klass.column_names.include?('created_at')
|
64
64
|
|
65
|
-
include_versions.where(id: version_class.at(datetime).select(
|
65
|
+
include_versions.where(id: version_class.at(datetime).select(@klass.primary_key)).or(
|
66
66
|
exclude_versions
|
67
67
|
.where("#{table_name}.created_at < ?", datetime)
|
68
|
-
.where.not(id: version_class.select(:
|
68
|
+
.where.not(id: version_class.select(:hoardable_id).where(DURING_QUERY, datetime))
|
69
69
|
).hoardable
|
70
70
|
}
|
71
71
|
end
|
@@ -51,7 +51,7 @@ module Hoardable
|
|
51
51
|
dependent: nil,
|
52
52
|
class_name: version_class.to_s,
|
53
53
|
inverse_of: :hoardable_source,
|
54
|
-
foreign_key: :
|
54
|
+
foreign_key: :hoardable_id
|
55
55
|
)
|
56
56
|
end
|
57
57
|
|
@@ -60,7 +60,7 @@ module Hoardable
|
|
60
60
|
#
|
61
61
|
# @return [Boolean]
|
62
62
|
def trashed?
|
63
|
-
|
63
|
+
!self.class.exists?(self.class.primary_key => id)
|
64
64
|
end
|
65
65
|
|
66
66
|
# Returns a boolean of whether the record is actually a +version+ cast as an instance of the
|
@@ -68,7 +68,7 @@ module Hoardable
|
|
68
68
|
#
|
69
69
|
# @return [Boolean]
|
70
70
|
def version?
|
71
|
-
|
71
|
+
hoardable_id != id
|
72
72
|
end
|
73
73
|
|
74
74
|
# Returns the +version+ at the supplied +datetime+ or +time+, or +self+ if there is none.
|
@@ -98,13 +98,8 @@ module Hoardable
|
|
98
98
|
version.is_a?(version_class) ? version.revert! : self
|
99
99
|
end
|
100
100
|
|
101
|
-
|
102
|
-
|
103
|
-
# {SourceModel}.
|
104
|
-
#
|
105
|
-
# @return [Integer, nil]
|
106
|
-
def hoardable_source_id
|
107
|
-
hoardable_client.hoardable_version_source_id || id
|
101
|
+
def hoardable_id
|
102
|
+
read_attribute('hoardable_id')
|
108
103
|
end
|
109
104
|
|
110
105
|
delegate :version_class, to: :class
|
data/lib/hoardable/version.rb
CHANGED
@@ -7,6 +7,13 @@ module Hoardable
|
|
7
7
|
extend ActiveSupport::Concern
|
8
8
|
|
9
9
|
class_methods do
|
10
|
+
# This is needed to allow {FinderMethods} to work with the version class.
|
11
|
+
#
|
12
|
+
# @!visibility private
|
13
|
+
def version_class
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
10
17
|
# This is needed to omit the pseudo row of 'tableoid' when using +ActiveRecord+’s +insert+.
|
11
18
|
#
|
12
19
|
# @!visibility private
|
@@ -20,6 +27,7 @@ module Hoardable
|
|
20
27
|
belongs_to(
|
21
28
|
:hoardable_source,
|
22
29
|
inverse_of: :versions,
|
30
|
+
foreign_key: :hoardable_id,
|
23
31
|
class_name: superclass.model_name
|
24
32
|
)
|
25
33
|
|
@@ -37,7 +45,7 @@ module Hoardable
|
|
37
45
|
# Returns only trashed +versions+ that are currently orphans.
|
38
46
|
scope :trashed, lambda {
|
39
47
|
left_outer_joins(:hoardable_source)
|
40
|
-
.where(superclass.table_name => {
|
48
|
+
.where(superclass.table_name => { superclass.primary_key => nil })
|
41
49
|
.where(_operation: 'delete')
|
42
50
|
}
|
43
51
|
|
@@ -78,7 +86,7 @@ module Hoardable
|
|
78
86
|
|
79
87
|
transaction do
|
80
88
|
hoardable_source.tap do |reverted|
|
81
|
-
reverted.update!(hoardable_source_attributes.without(
|
89
|
+
reverted.update!(hoardable_source_attributes.without(self.class.superclass.primary_key))
|
82
90
|
reverted.instance_variable_set(:@hoardable_version, self)
|
83
91
|
reverted.run_callbacks(:reverted)
|
84
92
|
end
|
@@ -113,24 +121,16 @@ module Hoardable
|
|
113
121
|
_data&.dig('changes')
|
114
122
|
end
|
115
123
|
|
116
|
-
# Returns the ID of the {SourceModel} that created this {VersionModel}
|
117
|
-
def hoardable_source_id
|
118
|
-
read_attribute('hoardable_source_id')
|
119
|
-
end
|
120
|
-
|
121
124
|
private
|
122
125
|
|
123
126
|
def insert_untrashed_source
|
124
127
|
superscope = self.class.superclass.unscoped
|
125
|
-
superscope.insert(hoardable_source_attributes.merge(
|
126
|
-
superscope.find(
|
128
|
+
superscope.insert(hoardable_source_attributes.merge(superscope.primary_key => hoardable_id))
|
129
|
+
superscope.find(hoardable_id)
|
127
130
|
end
|
128
131
|
|
129
132
|
def hoardable_source_attributes
|
130
|
-
|
131
|
-
attributes_before_type_cast
|
132
|
-
.without('hoardable_source_id')
|
133
|
-
.reject { |k, _v| k.start_with?('_') }
|
133
|
+
attributes_before_type_cast.without(self.class.column_names - self.class.superclass.column_names)
|
134
134
|
end
|
135
135
|
end
|
136
136
|
end
|
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,9 @@ 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'
|
13
18
|
require_relative 'generators/hoardable/migration_generator'
|
14
|
-
require_relative 'generators/hoardable/
|
19
|
+
require_relative 'generators/hoardable/install_generator'
|
data/sig/hoardable.rbs
CHANGED
@@ -8,6 +8,7 @@ module Hoardable
|
|
8
8
|
HOARDABLE_CALLBACKS_ENABLED: ^(untyped) -> untyped
|
9
9
|
HOARDABLE_SAVE_TRASH: ^(untyped) -> untyped
|
10
10
|
HOARDABLE_VERSION_UPDATES: ^(untyped) -> untyped
|
11
|
+
SUPPORTS_ENCRYPTED_ACTION_TEXT: untyped
|
11
12
|
self.@context: Hash[untyped, untyped]
|
12
13
|
self.@config: untyped
|
13
14
|
self.@at: nil
|
@@ -17,8 +18,20 @@ module Hoardable
|
|
17
18
|
def self.at: (untyped datetime) -> untyped
|
18
19
|
def self.logger: -> untyped
|
19
20
|
|
21
|
+
class Engine
|
22
|
+
end
|
23
|
+
|
24
|
+
module FinderMethods
|
25
|
+
def find_one: (untyped id) -> untyped
|
26
|
+
def find_some: (untyped ids) -> untyped
|
27
|
+
|
28
|
+
private
|
29
|
+
def hoardable_ids: ([untyped] ids) -> Array[untyped]
|
30
|
+
end
|
31
|
+
|
20
32
|
module Scopes
|
21
33
|
TABLEOID_AREL_CONDITIONS: Proc
|
34
|
+
self.@klass: bot
|
22
35
|
|
23
36
|
private
|
24
37
|
def tableoid: -> untyped
|
@@ -30,22 +43,24 @@ module Hoardable
|
|
30
43
|
class Error < StandardError
|
31
44
|
end
|
32
45
|
|
33
|
-
class
|
34
|
-
|
46
|
+
class CreatedAtColumnMissingError < Error
|
47
|
+
def initialize: (untyped source_table_name) -> void
|
48
|
+
end
|
35
49
|
|
50
|
+
class DatabaseClient
|
36
51
|
attr_reader source_record: SourceModel
|
37
52
|
def initialize: (SourceModel source_record) -> void
|
38
53
|
def insert_hoardable_version: (untyped operation) -> untyped
|
54
|
+
def source_primary_key: -> untyped
|
39
55
|
def find_or_initialize_hoardable_event_uuid: -> untyped
|
40
|
-
def hoardable_version_source_id: -> untyped
|
41
|
-
def query_hoardable_version_source_id: -> untyped
|
42
56
|
def initialize_version_attributes: (untyped operation) -> untyped
|
57
|
+
def source_attributes_without_primary_key: -> untyped
|
43
58
|
def initialize_temporal_range: -> Range
|
44
59
|
def initialize_hoardable_data: -> untyped
|
45
60
|
def assign_hoardable_context: (:event_uuid | :meta | :note | :whodunit key) -> nil
|
46
61
|
def unset_hoardable_version_and_event_uuid: -> nil
|
47
62
|
def previous_temporal_tsrange_end: -> untyped
|
48
|
-
def hoardable_source_epoch: ->
|
63
|
+
def hoardable_source_epoch: -> untyped
|
49
64
|
end
|
50
65
|
|
51
66
|
module SourceModel
|
@@ -55,9 +70,10 @@ module Hoardable
|
|
55
70
|
attr_reader hoardable_version: untyped
|
56
71
|
def trashed?: -> untyped
|
57
72
|
def version?: -> untyped
|
58
|
-
def at: (untyped datetime) -> SourceModel
|
73
|
+
def at: (untyped datetime) -> SourceModel?
|
74
|
+
def version_at: (untyped datetime) -> untyped
|
59
75
|
def revert_to!: (untyped datetime) -> SourceModel?
|
60
|
-
def
|
76
|
+
def hoardable_id: -> untyped
|
61
77
|
|
62
78
|
private
|
63
79
|
def hoardable_client: -> DatabaseClient
|
@@ -65,25 +81,19 @@ module Hoardable
|
|
65
81
|
public
|
66
82
|
def version_class: -> untyped
|
67
83
|
def hoardable: -> untyped
|
68
|
-
|
69
|
-
module FinderMethods
|
70
|
-
def find_one: (untyped id) -> untyped
|
71
|
-
end
|
72
84
|
end
|
73
85
|
|
74
86
|
module VersionModel
|
75
|
-
@hoardable_source_attributes: untyped
|
76
|
-
|
77
87
|
def revert!: -> untyped
|
78
88
|
def untrash!: -> untyped
|
79
89
|
def changes: -> untyped
|
80
|
-
def hoardable_source_id: -> untyped
|
81
90
|
|
82
91
|
private
|
83
92
|
def insert_untrashed_source: -> untyped
|
84
93
|
def hoardable_source_attributes: -> untyped
|
85
94
|
|
86
95
|
public
|
96
|
+
def version_class: -> VersionModel
|
87
97
|
def scope_attributes: -> untyped
|
88
98
|
end
|
89
99
|
|
@@ -98,10 +108,16 @@ module Hoardable
|
|
98
108
|
end
|
99
109
|
|
100
110
|
module Associations
|
101
|
-
|
102
|
-
|
111
|
+
include HasRichText
|
112
|
+
include BelongsTo
|
113
|
+
include HasOne
|
114
|
+
include HasMany
|
115
|
+
end
|
103
116
|
|
104
|
-
|
117
|
+
module HasMany
|
118
|
+
def has_many: (*untyped args) -> untyped
|
119
|
+
|
120
|
+
module HasManyExtension
|
105
121
|
@scope: untyped
|
106
122
|
@association: bot
|
107
123
|
|
@@ -112,16 +128,41 @@ module Hoardable
|
|
112
128
|
end
|
113
129
|
end
|
114
130
|
|
131
|
+
module BelongsTo
|
132
|
+
def belongs_to: (*untyped args) -> nil
|
133
|
+
|
134
|
+
private
|
135
|
+
def hoardable_override_belongs_to: (untyped name) -> untyped
|
136
|
+
end
|
137
|
+
|
138
|
+
module HasOne
|
139
|
+
def has_one: (*untyped args) -> nil
|
140
|
+
end
|
141
|
+
|
142
|
+
module HasRichText
|
143
|
+
def has_rich_text: (untyped name, ?encrypted: false, ?hoardable: false) -> nil
|
144
|
+
end
|
145
|
+
|
115
146
|
class MigrationGenerator
|
116
147
|
@singularized_table_name: untyped
|
117
148
|
|
118
149
|
def create_versions_table: -> untyped
|
119
150
|
def foreign_key_type: -> String
|
120
|
-
def
|
151
|
+
def primary_key: -> String
|
121
152
|
def singularized_table_name: -> untyped
|
122
153
|
end
|
123
154
|
|
124
|
-
class
|
155
|
+
class InstallGenerator
|
125
156
|
def create_initializer_file: -> untyped
|
157
|
+
def create_migration_file: -> untyped
|
158
|
+
def self.next_migration_number: (untyped dir) -> untyped
|
159
|
+
end
|
160
|
+
|
161
|
+
class RichText
|
162
|
+
include Model
|
163
|
+
end
|
164
|
+
|
165
|
+
class EncryptedRichText
|
166
|
+
include Model
|
126
167
|
end
|
127
168
|
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.12.2
|
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-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -104,17 +104,23 @@ 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/install.rb.erb
|
109
110
|
- lib/generators/hoardable/templates/migration.rb.erb
|
110
|
-
- lib/generators/hoardable/templates/migration_6.rb.erb
|
111
111
|
- lib/hoardable.rb
|
112
112
|
- lib/hoardable/associations.rb
|
113
|
+
- lib/hoardable/belongs_to.rb
|
113
114
|
- lib/hoardable/database_client.rb
|
115
|
+
- lib/hoardable/encrypted_rich_text.rb
|
116
|
+
- lib/hoardable/engine.rb
|
114
117
|
- lib/hoardable/error.rb
|
115
118
|
- lib/hoardable/finder_methods.rb
|
116
|
-
- lib/hoardable/
|
119
|
+
- lib/hoardable/has_many.rb
|
120
|
+
- lib/hoardable/has_one.rb
|
121
|
+
- lib/hoardable/has_rich_text.rb
|
117
122
|
- lib/hoardable/model.rb
|
123
|
+
- lib/hoardable/rich_text.rb
|
118
124
|
- lib/hoardable/scopes.rb
|
119
125
|
- lib/hoardable/source_model.rb
|
120
126
|
- 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
|
@@ -1,49 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
4
|
-
def change
|
5
|
-
reversible do |dir|
|
6
|
-
dir.up do
|
7
|
-
execute <<~SQL
|
8
|
-
DO $$
|
9
|
-
BEGIN
|
10
|
-
IF NOT EXISTS (
|
11
|
-
SELECT 1 FROM pg_type t
|
12
|
-
WHERE t.typname = 'hoardable_operation'
|
13
|
-
) THEN
|
14
|
-
CREATE TYPE hoardable_operation AS ENUM ('update', 'delete', 'insert');
|
15
|
-
END IF;
|
16
|
-
END
|
17
|
-
$$;
|
18
|
-
SQL
|
19
|
-
end
|
20
|
-
end
|
21
|
-
create_table :<%= singularized_table_name %>_versions, id: false, options: 'INHERITS (<%= table_name %>)' do |t|
|
22
|
-
t.jsonb :_data
|
23
|
-
t.tsrange :_during, null: false
|
24
|
-
t.uuid :_event_uuid, null: false, index: true
|
25
|
-
t.column :_operation, :hoardable_operation, null: false, index: true
|
26
|
-
t.<%= foreign_key_type %> :hoardable_source_id, null: false, index: true
|
27
|
-
end
|
28
|
-
execute(
|
29
|
-
<<~SQL
|
30
|
-
CREATE OR REPLACE FUNCTION hoardable_version_prevent_update() RETURNS trigger
|
31
|
-
LANGUAGE plpgsql AS
|
32
|
-
$$BEGIN
|
33
|
-
RAISE EXCEPTION 'updating a version is not allowed';
|
34
|
-
RETURN NEW;
|
35
|
-
END;$$;
|
36
|
-
|
37
|
-
CREATE TRIGGER <%= singularized_table_name %>_versions_prevent_update
|
38
|
-
BEFORE UPDATE ON <%= singularized_table_name %>_versions FOR EACH ROW
|
39
|
-
EXECUTE PROCEDURE hoardable_version_prevent_update();
|
40
|
-
SQL
|
41
|
-
)
|
42
|
-
add_index(:<%= singularized_table_name %>_versions, :id, unique: true)
|
43
|
-
add_index(
|
44
|
-
:<%= singularized_table_name %>_versions,
|
45
|
-
%i[_during hoardable_source_id],
|
46
|
-
name: 'idx_<%= singularized_table_name %>_versions_temporally'
|
47
|
-
)
|
48
|
-
end
|
49
|
-
end
|