hoardable 0.10.1 → 0.11.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: dad9a103d70ce10905528d1d7e56e67a4a8fb89e675b3097165ccede6262a015
4
- data.tar.gz: a2dc95873a08175254cca315caa3de9b087674df1e4dadd17f42fb26a9080a9d
3
+ metadata.gz: 2d9f9ea3a4559d3a4515357eabc0c7ee05526ee1308a4e6eef111f2d6207aef6
4
+ data.tar.gz: be0011cb1ea9d3fcdcd5d1a4f4951d7185371cc1d05c91dfd27ea163b59620b7
5
5
  SHA512:
6
- metadata.gz: 02b542ba1fe562750eb16bf6f310b08e2340610e01e7db68eaedb9185e4da4a1d7e7a5a42885fb09376759bf936b8b527de4bedec8399b71119541162e57604f
7
- data.tar.gz: 77edd48d420fea18333996280d6e31e0daba198b96e67fdc49b4fc7fe1884cb86cc88416e1f486691e2292465818fb50e3c4fc4367d83cc6d751d0adcb39acf6
6
+ metadata.gz: 20ebc8d7ec2924e43e6bbd806e7aaf08350653b695dc4aef16de28359ceb95d8de16da4c19790e8b1212bc8d8d202caeb3614e05b326977bd470defe48e4b731
7
+ data.tar.gz: fc3fa92ae0ab25bee4122456b889f86eec75b0eee4efba742dced8737d7bc9ab3a99e63e63839f49089183900df8ed54a315a4561aad634c0e2ea3fa56aa8732
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ source 'https://rubygems.org'
5
5
  gem 'benchmark-ips', '~> 2.10'
6
6
  gem 'debug', '~> 1.6'
7
7
  gem 'minitest', '~> 5.0'
8
+ gem 'rails', '>= 6.1'
8
9
  gem 'rake', '~> 13.0'
9
10
  gem 'rubocop', '~> 1.21'
10
11
  gem 'rubocop-minitest', '~> 0.20'
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
- And then execute `bundle install`.
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:initializer
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 `has_many_hoardable :children, dependent: :destroy` and want
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
- has_many_hoardable :comments, dependent: :destroy # `Comment` also includes `Hoardable::Model`
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
- ### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
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
- ### [`audited`](https://github.com/collectiveidea/audited)
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
- ### [`discard`](https://github.com/jhawthorn/discard)
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
- ### [`paranoia`](https://github.com/rubysherpas/paranoia)
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
- execute(
14
- <<~SQL
15
- CREATE OR REPLACE FUNCTION hoardable_version_prevent_update() RETURNS trigger
16
- LANGUAGE plpgsql AS
17
- $$BEGIN
18
- RAISE EXCEPTION 'updating a version is not allowed';
19
- RETURN NEW;
20
- END;$$;
21
-
22
- CREATE TRIGGER <%= singularized_table_name %>_versions_prevent_update
23
- BEFORE UPDATE ON <%= singularized_table_name %>_versions FOR EACH ROW
24
- EXECUTE PROCEDURE hoardable_version_prevent_update();
25
- SQL
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
- 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
- )
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
- # An +ActiveRecord+ extension that allows looking up {VersionModel}s by +hoardable_source_id+ as
11
- # if they were {SourceModel}s when using {Hoardable#at}.
12
- module HasManyExtension
13
- def scope
14
- @scope ||= hoardable_scope
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
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # A {Hoardable} subclass of {ActionText::EncryptedRichText}.
5
+ class EncryptedRichText < ActionText::EncryptedRichText
6
+ include Model
7
+ end
8
+ 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
@@ -53,11 +53,17 @@ module Hoardable
53
53
  TracePoint.new(:end) do |trace|
54
54
  next unless self == trace.self
55
55
 
56
- version_class_name = "#{name}#{VERSION_CLASS_SUFFIX}"
57
- unless Object.const_defined?(version_class_name)
58
- Object.const_set(version_class_name, Class.new(self) { include VersionModel })
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # A {Hoardable} subclass of {ActionText::RichText}
5
+ class RichText < ActionText::RichText
6
+ include Model
7
+ end
8
+ end
@@ -32,6 +32,8 @@ module Hoardable
32
32
  include Scopes
33
33
 
34
34
  around_update(if: [HOARDABLE_CALLBACKS_ENABLED, HOARDABLE_VERSION_UPDATES]) do |_, block|
35
+ next if self.is_a?(Hoardable::Attachment)
36
+
35
37
  hoardable_client.insert_hoardable_version('update', &block)
36
38
  end
37
39
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- VERSION = '0.10.1'
4
+ VERSION = '0.11.0'
5
5
  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/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/initializer_generator'
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: -> Time
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
- def belongs_to_trashable: (untyped name, ?nil scope, **untyped) -> untyped
102
- def has_many_hoardable: (untyped name, ?nil scope, **untyped) -> untyped
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 HasManyScope
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 InitializerGenerator
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.10.1
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-07 00:00:00.000000000 Z
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/initializer_generator.rb
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/hoardable.rb
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