hoardable 0.6.0 → 0.9.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dff71dd2ebbebaeedfdc9d6fdd8c56c8faaa5505814cfe41c146c4a565375ff1
4
- data.tar.gz: 6693f3634541bc8308ed5b4329d69851d971df1cec2b50e842fa9332c62425b6
3
+ metadata.gz: 5b85dfaf658e447049fcb09281d34dd25188be79f948eda294398f47b19b8244
4
+ data.tar.gz: 67219612736259e2dbc542230cc1490a156a15bc679340176aca35b76aaf516c
5
5
  SHA512:
6
- metadata.gz: ec2a9df9254cf3623f4fffb0516e99ec30a46ec6496ad0d5e555d4a3ce8324fa81d1880036f42aeb4bbbee136957479f283ccab4b8fb3f2847122bd597483889
7
- data.tar.gz: 25a5040c6dc5b91b0b54020657e436aa775cf2211b56fecf1b277e36c11041b55f5a29861000503d5388a9a3d0280a8ccca99c052a2dc82a45abb8f5c98bb50e
6
+ metadata.gz: cf3c5edfeac5526e7520dc2584caba8bb5399df43acf1ce6bd320b19f44981c154624b9b80c5b415aaeb840fc5d718f1999b4bbaf82eb3ff76149e09621ab0e5
7
+ data.tar.gz: 52112fb9bc55bec2009656cce9571887130333b6460d5478c1a0c113895ccb977ffcc80172954911c6e70d2012f136bf68c64b17bc69ffd7deeaa40a56c97646
data/.rubocop.yml CHANGED
@@ -16,3 +16,6 @@ Metrics/BlockLength:
16
16
 
17
17
  Style/DocumentDynamicEvalDefinition:
18
18
  Enabled: false
19
+
20
+ Naming/PredicateName:
21
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,10 +1,41 @@
1
1
  ## [Unreleased]
2
2
 
3
+ - Stability is coming.
4
+
5
+ ## [0.9.0] - 2022-10-02
6
+
7
+ - **Breaking Change** - `Hoardable.return_everything` was removed in favor of the newly added
8
+ `Hoardable.at`.
9
+
10
+ ## [0.8.0] - 2022-10-01
11
+
12
+ - **Breaking Change** - Due to the performance benefit of using `insert` for database injection of
13
+ versions, and a personal opinion that only an `after_versioned` hook might be needed, the
14
+ `before_versioned` and `around_versioned` ActiveRecord hooks are removed.
15
+
16
+ - **Breaking Change** - Another side effect of the performance benefit gained by using `insert` is
17
+ that a source model will need to be reloaded before a call to `versions` on it can access the
18
+ latest version after an `update` on the source record.
19
+
20
+ - **Breaking Change** - Previously the inherited `_versions` tables did not have a unique index on
21
+ the ID column, though it still pulled from the same sequence as the parent table. Prior to version
22
+ 0.4.0 though, it was possible to have multiple trashed versions with the same ID. Adding unique
23
+ indexes to version tables prior to version 0.4.0 could result in issues.
24
+
25
+ ## [0.7.0] - 2022-09-29
26
+
27
+ - **Breaking Change** - Continuing along with the change below, the `foreign_key` on the `_versions`
28
+ tables is now changed to `hoardable_source_id` instead of the i18n model name dervied foreign key.
29
+ The intent is to never leave room for conflict of foreign keys for existing relationships. This
30
+ can be resolved by renaming the foreign key columns from their i18n model name derived column
31
+ names to `hoardable_source_id`, i.e. `rename_column :post_versions, :post_id, :hoardable_source_id`.
32
+
3
33
  ## [0.6.0] - 2022-09-28
4
34
 
5
35
  - **Breaking Change** - Previously, a source model would `has_many :versions` with an inverse
6
- relationship of the i18n interpreted name of the source model. Now it simply `has_many :versions,
7
- inverse_of :hoardable_source` to not potentially conflict with previously existing relationships.
36
+ relationship based on the i18n interpreted name of the source model. Now it simply `has_many
37
+ :versions, inverse_of :hoardable_source` to not potentially conflict with previously existing
38
+ relationships.
8
39
 
9
40
  ## [0.5.0] - 2022-09-25
10
41
 
data/Gemfile CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
+ gem 'benchmark-ips', '~> 2.10'
5
6
  gem 'debug', '~> 1.6'
6
7
  gem 'minitest', '~> 5.0'
7
8
  gem 'rake', '~> 13.0'
data/README.md CHANGED
@@ -31,6 +31,12 @@ gem 'hoardable'
31
31
 
32
32
  And then execute `bundle install`.
33
33
 
34
+ If you would like to generate an initializer with the global [configuration](#configuration) options:
35
+
36
+ ```
37
+ rails g hoardable:initializer
38
+ ```
39
+
34
40
  ### Model Installation
35
41
 
36
42
  You must include `Hoardable::Model` into an ActiveRecord model that you would like to hoard versions
@@ -64,6 +70,9 @@ _Note:_ If you are on Rails 6.1, you might want to set `config.active_record.sch
64
70
  in `application.rb`, so that the enum type is captured in your schema dump. This is not required in
65
71
  Rails 7.
66
72
 
73
+ _Note:_ Creating an inherited table does not copy over the indexes from the parent table. If you
74
+ need to query versions often, you should add appropriate indexes to the `_versions` tables.
75
+
67
76
  ## Usage
68
77
 
69
78
  ### Overview
@@ -76,7 +85,7 @@ $ irb
76
85
  >> Post
77
86
  => Post(id: integer, body: text, user_id: integer, created_at: datetime)
78
87
  >> PostVersion
79
- => PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, _data: jsonb, _during: tsrange, post_id: integer)
88
+ => PostVersion(id: integer, body: text, user_id: integer, created_at: datetime, _data: jsonb, _during: tsrange, hoardable_source_id: integer)
80
89
  ```
81
90
 
82
91
  A `Post` now `has_many :versions`. With the default configuration, whenever an update and deletion
@@ -86,7 +95,7 @@ of a `Post` occurs, a version is created:
86
95
  post = Post.create!(title: "Title")
87
96
  post.versions.size # => 0
88
97
  post.update!(title: "Revised Title")
89
- post.versions.size # => 1
98
+ post.reload.versions.size # => 1
90
99
  post.versions.first.title # => "Title"
91
100
  post.destroy!
92
101
  post.trashed? # true
@@ -99,15 +108,15 @@ Each `PostVersion` has access to the same attributes, relationships, and other m
99
108
 
100
109
  If you ever need to revert to a specific version, you can call `version.revert!` on it.
101
110
 
102
- ``` ruby
111
+ ```ruby
103
112
  post = Post.create!(title: "Title")
104
113
  post.update!(title: "Whoops")
105
- post.versions.last.revert!
114
+ post.reload.versions.last.revert!
106
115
  post.title # => "Title"
107
116
  ```
108
117
 
109
118
  If you would like to untrash a specific version, you can call `version.untrash!` on it. This will
110
- re-insert the model in the parent class’s table with it’s original primary key.
119
+ re-insert the model in the parent class’s table with the original primary key.
111
120
 
112
121
  ```ruby
113
122
  post = Post.create!(title: "Title")
@@ -134,21 +143,20 @@ If you want to look-up the version of a record at a specific time, you can use t
134
143
  ```ruby
135
144
  post.at(1.day.ago) # => #<PostVersion>
136
145
  # or you can use the scope on the version model class
137
- PostVersion.at(1.day.ago).find_by(post_id: post.id) # => #<PostVersion>
146
+ PostVersion.at(1.day.ago).find_by(hoardable_source_id: post.id) # => #<PostVersion>
138
147
  ```
139
148
 
140
149
  The source model class also has an `.at` method:
141
150
 
142
- ``` ruby
151
+ ```ruby
143
152
  Post.at(1.day.ago) # => [#<Post>, #<Post>]
144
153
  ```
145
154
 
146
- This will return an ActiveRecord scoped query of all `Posts` and `PostVersions` that were valid at
147
- that time, all cast as instances of `Post`.
155
+ This will return an ActiveRecord scoped query of all `Post` and `PostVersion` records that were
156
+ valid at that time, all cast as instances of `Post`.
148
157
 
149
- _Note:_ A `Version` is not created upon initial parent model creation. To accurately track the
150
- beginning of the first temporal period, you will need to ensure the source model table has a
151
- `created_at` timestamp column.
158
+ There is also an `at` method on `Hoardable` itself for more complex temporal resource querying. See
159
+ [Relationships](#relationships) for more.
152
160
 
153
161
  By default, `hoardable` will keep copies of records you have destroyed. You can query them
154
162
  specifically with:
@@ -156,10 +164,12 @@ specifically with:
156
164
  ```ruby
157
165
  PostVersion.trashed
158
166
  Post.version_class.trashed # <- same thing as above
167
+ PostVersion.trashed.first.trashed? # <- true
159
168
  ```
160
169
 
161
- _Note:_ Creating an inherited table does not copy over the indexes from the parent table. If you
162
- need to query versions often, you should add appropriate indexes to the `_versions` tables.
170
+ _Note:_ A `Version` is not created upon initial parent model creation. To accurately track the
171
+ beginning of the first temporal period, you will need to ensure the source model table has a
172
+ `created_at` timestamp column.
163
173
 
164
174
  ### Tracking Contextual Data
165
175
 
@@ -182,7 +192,7 @@ Hoardable.whodunit = -> { Current.user&.id }
182
192
  # somewhere in your app code
183
193
  Current.user = User.find(123)
184
194
  post.update!(status: 'live')
185
- post.versions.last.hoardable_whodunit # => 123
195
+ post.reload.versions.last.hoardable_whodunit # => 123
186
196
  ```
187
197
 
188
198
  You can also set this context manually as well, just remember to clear them afterwards.
@@ -191,7 +201,7 @@ You can also set this context manually as well, just remember to clear them afte
191
201
  Hoardable.note = "reverting due to accidental deletion"
192
202
  post.update!(title: "We’re back!")
193
203
  Hoardable.note = nil
194
- post.versions.last.hoardable_note # => "reverting due to accidental deletion"
204
+ post.reload.versions.last.hoardable_note # => "reverting due to accidental deletion"
195
205
  ```
196
206
 
197
207
  A more useful pattern is to use `Hoardable.with` to set the context around a block. A good example
@@ -225,9 +235,9 @@ version.hoardable_event_uuid
225
235
 
226
236
  ### Model Callbacks
227
237
 
228
- Sometimes you might want to do something with a version before or after it gets inserted to the
229
- database. You can access it in `before/after/around_versioned` callbacks on the source record as
230
- `hoardable_version`. These happen around `.save`, which is enclosed in an ActiveRecord transaction.
238
+ Sometimes you might want to do something with a version after it gets inserted to the database. You
239
+ can access it in `after_versioned` callbacks on the source record as `hoardable_version`. These
240
+ happen within `ActiveRecord`’s `.save`, which is enclosed in an ActiveRecord transaction.
231
241
 
232
242
  There are also `after_reverted` and `after_untrashed` callbacks available as well, which are called
233
243
  on the source record after a version is reverted or untrashed.
@@ -235,14 +245,14 @@ on the source record after a version is reverted or untrashed.
235
245
  ```ruby
236
246
  class User
237
247
  include Hoardable::Model
238
- before_versioned :sanitize_version
248
+ after_versioned :track_versioned_event
239
249
  after_reverted :track_reverted_event
240
250
  after_untrashed :track_untrashed_event
241
251
 
242
252
  private
243
253
 
244
- def sanitize_version
245
- hoardable_version.sanitize_password
254
+ def track_versioned_event
255
+ track_event(:user_versioned, hoardable_version)
246
256
  end
247
257
 
248
258
  def track_reverted_event
@@ -263,7 +273,6 @@ The configurable options are:
263
273
  Hoardable.enabled # => default true
264
274
  Hoardable.version_updates # => default true
265
275
  Hoardable.save_trash # => default true
266
- Hoardable.return_everything # => default false
267
276
  ```
268
277
 
269
278
  `Hoardable.enabled` controls whether versions will be ever be created.
@@ -273,10 +282,6 @@ Hoardable.return_everything # => default false
273
282
  `Hoardable.save_trash` controls whether to create versions upon record deletion. When this is set to
274
283
  `false`, all versions of a record will be deleted when the record is destroyed.
275
284
 
276
- `Hoardable.return_everything` controls whether to include versions when doing queries for source
277
- models. This is typically only useful to set around a block, as explained below in
278
- [Relationships](#relationships).
279
-
280
285
  If you would like to temporarily set a config setting, you can use `Hoardable.with`:
281
286
 
282
287
  ```ruby
@@ -323,8 +328,54 @@ class Comment
323
328
  end
324
329
  ```
325
330
 
326
- Sometimes youll trash something that `has_many :children, dependent: :destroy` and both the parent
327
- and child model classes include `Hoardable::Model`. Whenever a hoardable version is created in a
331
+ Sometimes you'll have a Hoardable record that `has_many` other Hoardable records and you will want
332
+ to know the state of both the parent record and the children at a cetain point in time. You
333
+ accomplish this by establishing a `has_many_hoardable` relationship and using the `Hoardable.at`
334
+ method:
335
+
336
+ ```ruby
337
+ class Post
338
+ include Hoardable::Model
339
+ has_many_hoardable :comments
340
+ end
341
+
342
+ def Comment
343
+ include Hoardable::Model
344
+ end
345
+
346
+ post = Post.create!(title: 'Title')
347
+ comment1 = post.comments.create!(body: 'Comment')
348
+ comment2 = post.comments.create!(body: 'Comment')
349
+ datetime = DateTime.current
350
+ comment2.destroy!
351
+ post.update!(title: 'New Title')
352
+ post_id = post.id # 1
353
+
354
+ Hoardable.at(datetime) do
355
+ post = Post.hoardable.find(post_id)
356
+ post.title # => 'Title'
357
+ post.comments.size # => 2
358
+ post.id # => 2
359
+ post.version? # => true
360
+ post.hoardable_source_id # => 1
361
+ end
362
+ ```
363
+
364
+ There are some additional details to point out above. Firstly, it is important to note that the
365
+ final `post.id` yields a different value than the originally created `Post`. This is because the
366
+ `post` within the `#at` block is actually a temporal version, since it has been subsequently
367
+ updated, but it has been reified as a `Post` for the purposes of your business logic (serialization,
368
+ rendering views, exporting, etc). Don’t fret - you will not be able to commit any updates to the
369
+ version, even though it is masquerading as a `Post`.
370
+
371
+ If you are ever unsure if a Hoardable record is a "source" or a "version", you can be sure by
372
+ calling `version?` on it. If you want to get the true original source record ID, you can call
373
+ `hoardable_source_id`. Finally, if you prepend `.hoardable` to a `.find` call on the source model
374
+ class, you can always find the relevant source or temporal version record using just the original
375
+ source record’s id.
376
+
377
+ Sometimes you’ll trash something that `has_many_hoardable :children, dependent: :destroy` and want
378
+ to untrash everything in a similar dependent manner. Whenever a hoardable version is created in a
328
379
  database transaction, it will create or re-use a unique event UUID for that transaction and tag all
329
380
  versions created with it. That way, when you `untrash!` a record, you can find and `untrash!`
330
381
  records that were trashed with it:
@@ -332,35 +383,18 @@ records that were trashed with it:
332
383
  ```ruby
333
384
  class Post < ActiveRecord::Base
334
385
  include Hoardable::Model
335
- has_many :comments, dependent: :destroy # `Comment` also includes `Hoardable::Model`
386
+ has_many_hoardable :comments, dependent: :destroy # `Comment` also includes `Hoardable::Model`
336
387
 
337
388
  after_untrashed do
338
389
  Comment
339
390
  .version_class
340
391
  .trashed
341
- .where(post_id: id)
342
392
  .with_hoardable_event_uuid(hoardable_event_uuid)
343
393
  .find_each(&:untrash!)
344
394
  end
345
395
  end
346
396
  ```
347
397
 
348
- If there are models that might be related to versions that are trashed or otherwise, and/or might
349
- trashed themselves, you can bypass the inherited tables query handling altogether by using the
350
- `return_everything` configuration variable in `Hoardable.with`. This will ensure that you always see
351
- all records, including update and trashed versions.
352
-
353
- ```ruby
354
- post.destroy!
355
-
356
- Hoardable.with(return_everything: true) do
357
- post = Post.find(post.id) # returns the trashed post as if it was not
358
- post.comments # returns the trashed comments as well
359
- end
360
-
361
- post.reload # raises ActiveRecord::RecordNotFound
362
- ```
363
-
364
398
  ## Gem Comparison
365
399
 
366
400
  ### [`paper_trail`](https://github.com/paper-trail-gem/paper_trail)
@@ -0,0 +1,21 @@
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
@@ -8,11 +8,26 @@ class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%=
8
8
  t.tsrange :_during, null: false
9
9
  t.uuid :_event_uuid, null: false, index: true
10
10
  t.enum :_operation, enum_type: 'hoardable_operation', null: false, index: true
11
- t.<%= foreign_key_type %> :<%= singularized_table_name %>_id, null: false, index: true
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
+ )
27
+ add_index(:<%= singularized_table_name %>_versions, :id, unique: true)
13
28
  add_index(
14
29
  :<%= singularized_table_name %>_versions,
15
- %i[_during <%= singularized_table_name %>_id],
30
+ %i[_during hoardable_source_id],
16
31
  name: 'idx_<%= singularized_table_name %>_versions_temporally'
17
32
  )
18
33
  end
@@ -23,11 +23,26 @@ class Create<%= class_name.singularize %>Versions < ActiveRecord::Migration[<%=
23
23
  t.tsrange :_during, null: false
24
24
  t.uuid :_event_uuid, null: false, index: true
25
25
  t.column :_operation, :hoardable_operation, null: false, index: true
26
- t.<%= foreign_key_type %> :<%= singularized_table_name %>_id, null: false, index: true
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
+ )
42
+ add_index(:<%= singularized_table_name %>_versions, :id, unique: true)
28
43
  add_index(
29
44
  :<%= singularized_table_name %>_versions,
30
- %i[_during <%= singularized_table_name %>_id],
45
+ %i[_during hoardable_source_id],
31
46
  name: 'idx_<%= singularized_table_name %>_versions_temporally'
32
47
  )
33
48
  end
@@ -7,6 +7,26 @@ 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.
12
+ module HasManyScope
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 :HasManyScope
29
+
10
30
  class_methods do
11
31
  # A wrapper for +ActiveRecord+’s +belongs_to+ that allows for falling back to the most recent
12
32
  # trashed +version+, in the case that the related source has been trashed.
@@ -17,9 +37,9 @@ module Hoardable
17
37
 
18
38
  define_method(trashable_relationship_name) do
19
39
  source_reflection = self.class.reflections[name.to_s]
20
- version_class = source_reflection.klass.version_class
40
+ version_class = source_reflection.version_class
21
41
  version_class.trashed.only_most_recent.find_by(
22
- version_class.hoardable_source_foreign_key => source_reflection.foreign_key
42
+ hoardable_source_id: source_reflection.foreign_key
23
43
  )
24
44
  end
25
45
 
@@ -29,6 +49,12 @@ module Hoardable
29
49
  end
30
50
  RUBY
31
51
  end
52
+
53
+ # A wrapper for +ActiveRecord+’s +has_many+ that allows for finding temporal versions of a
54
+ # record cast as instances of the {SourceModel}, when doing a {Hoardable#at} query.
55
+ def has_many_hoardable(name, scope = nil, **options)
56
+ has_many(name, scope, **options) { include HasManyScope }
57
+ end
32
58
  end
33
59
  end
34
60
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hoardable
4
+ # This is a private service class that manages the insertion of {VersionModel}s into the
5
+ # PostgreSQL database.
6
+ class DatabaseClient
7
+ attr_reader :source_record
8
+
9
+ def initialize(source_record)
10
+ @source_record = source_record
11
+ end
12
+
13
+ delegate :version_class, to: :source_record
14
+
15
+ def insert_hoardable_version(operation, &block)
16
+ version = version_class.insert(initialize_version_attributes(operation), returning: :id)
17
+ version_id = version[0]['id']
18
+ source_record.instance_variable_set('@hoardable_version', version_class.find(version_id))
19
+ source_record.run_callbacks(:versioned, &block)
20
+ end
21
+
22
+ def find_or_initialize_hoardable_event_uuid
23
+ Thread.current[:hoardable_event_uuid] ||= ActiveRecord::Base.connection.query('SELECT gen_random_uuid();')[0][0]
24
+ end
25
+
26
+ def hoardable_version_source_id
27
+ @hoardable_version_source_id ||= query_hoardable_version_source_id
28
+ end
29
+
30
+ def query_hoardable_version_source_id
31
+ primary_key = source_record.class.primary_key
32
+ version_class.where(primary_key => source_record.read_attribute(primary_key)).pluck('hoardable_source_id')[0]
33
+ end
34
+
35
+ def initialize_version_attributes(operation)
36
+ source_record.attributes_before_type_cast.without('id').merge(
37
+ source_record.changes.transform_values { |h| h[0] },
38
+ {
39
+ 'hoardable_source_id' => source_record.id,
40
+ '_event_uuid' => find_or_initialize_hoardable_event_uuid,
41
+ '_operation' => operation,
42
+ '_data' => initialize_hoardable_data.merge(changes: source_record.changes),
43
+ '_during' => initialize_temporal_range
44
+ }
45
+ )
46
+ end
47
+
48
+ def initialize_temporal_range
49
+ ((previous_temporal_tsrange_end || hoardable_source_epoch)..Time.now.utc)
50
+ end
51
+
52
+ def initialize_hoardable_data
53
+ DATA_KEYS.to_h do |key|
54
+ [key, assign_hoardable_context(key)]
55
+ end
56
+ end
57
+
58
+ def assign_hoardable_context(key)
59
+ return nil if (value = Hoardable.public_send(key)).nil?
60
+
61
+ value.is_a?(Proc) ? value.call : value
62
+ end
63
+
64
+ def unset_hoardable_version_and_event_uuid
65
+ source_record.instance_variable_set('@hoardable_version', nil)
66
+ return if source_record.class.connection.transaction_open?
67
+
68
+ Thread.current[:hoardable_event_uuid] = nil
69
+ end
70
+
71
+ def previous_temporal_tsrange_end
72
+ source_record.versions.only_most_recent.pluck('_during').first&.end
73
+ end
74
+
75
+ def hoardable_source_epoch
76
+ if source_record.class.column_names.include?('created_at')
77
+ source_record.created_at
78
+ else
79
+ maybe_warn_about_missing_created_at_column
80
+ Time.at(0).utc
81
+ end
82
+ end
83
+
84
+ def maybe_warn_about_missing_created_at_column
85
+ return unless source_record.class.hoardable_config[:warn_on_missing_created_at_column]
86
+
87
+ source_table_name = source_record.class.table_name
88
+ Hoardable.logger.info(
89
+ <<~LOG
90
+ '#{source_table_name}' does not have a 'created_at' column, so the first version’s temporal period
91
+ will begin at the unix epoch instead. Add a 'created_at' column to '#{source_table_name}'
92
+ or set 'Hoardable.warn_on_missing_created_at_column = false' to disable this message.
93
+ LOG
94
+ )
95
+ end
96
+ end
97
+ private_constant :DatabaseClient
98
+ end
@@ -8,7 +8,7 @@ module Hoardable
8
8
 
9
9
  # Symbols for use with setting {Hoardable} configuration. See {file:README.md#configuration
10
10
  # README} for more.
11
- CONFIG_KEYS = %i[enabled version_updates save_trash return_everything warn_on_missing_created_at_column].freeze
11
+ CONFIG_KEYS = %i[enabled version_updates save_trash warn_on_missing_created_at_column].freeze
12
12
 
13
13
  VERSION_CLASS_SUFFIX = 'Version'
14
14
  private_constant :VERSION_CLASS_SUFFIX
@@ -36,7 +36,7 @@ module Hoardable
36
36
 
37
37
  @context = {}
38
38
  @config = CONFIG_KEYS.to_h do |key|
39
- [key, key != :return_everything]
39
+ [key, true]
40
40
  end
41
41
 
42
42
  class << self
@@ -75,6 +75,17 @@ module Hoardable
75
75
  @context = current_context
76
76
  end
77
77
 
78
+ # Allows performing a query for record states at a certain time. Returned {SourceModel}
79
+ # instances within the block may be {SourceModel} or {VersionModel} records.
80
+ #
81
+ # @param datetime [DateTime, Time] the datetime or time to temporally query records at
82
+ def at(datetime)
83
+ @at = datetime
84
+ yield
85
+ ensure
86
+ @at = nil
87
+ end
88
+
78
89
  # @!visibility private
79
90
  def logger
80
91
  @logger ||= ActiveSupport::TaggedLogging.new(Logger.new($stdout))
@@ -46,7 +46,7 @@ module Hoardable
46
46
 
47
47
  included do
48
48
  include Associations
49
- define_model_callbacks :versioned
49
+ define_model_callbacks :versioned, only: :after
50
50
  define_model_callbacks :reverted, only: :after
51
51
  define_model_callbacks :untrashed, only: :after
52
52
 
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- # This concern provides support for PostgreSQL’s tableoid system column to {SourceModel}.
5
- module Tableoid
4
+ # This concern provides support for PostgreSQL’s tableoid system column to {SourceModel} and
5
+ # temporal +ActiveRecord+ scopes.
6
+ module Scopes
6
7
  extend ActiveSupport::Concern
7
8
 
8
9
  TABLEOID_AREL_CONDITIONS = lambda do |arel_table, condition|
@@ -19,10 +20,10 @@ module Hoardable
19
20
 
20
21
  # By default {Hoardable} only returns instances of the parent table, and not the +versions+ in
21
22
  # the inherited table. This can be bypassed by using the {.include_versions} scope or wrapping
22
- # the code in a `Hoardable.with(return_everything: true)` block.
23
+ # the code in a `Hoardable.at(datetime)` block.
23
24
  default_scope do
24
- if hoardable_config[:return_everything]
25
- where(nil)
25
+ if (hoardable_at = Hoardable.instance_variable_get('@at'))
26
+ at(hoardable_at)
26
27
  else
27
28
  exclude_versions
28
29
  end
@@ -51,6 +52,18 @@ module Hoardable
51
52
  # Excludes +versions+ of the parent +ActiveRecord+ class. This is included by default in the
52
53
  # source model’s +default_scope+.
53
54
  scope :exclude_versions, -> { where(TABLEOID_AREL_CONDITIONS.call(arel_table, :eq)) }
55
+
56
+ # @!scope class
57
+ # @!method at
58
+ # @return [ActiveRecord<Object>]
59
+ #
60
+ # Returns instances of the source model and versions that were valid at the supplied
61
+ # +datetime+ or +time+, all cast as instances of the source model.
62
+ scope :at, lambda { |datetime|
63
+ include_versions.where(id: version_class.at(datetime).select('id')).or(
64
+ where.not(id: version_class.select(:hoardable_source_id).where(DURING_QUERY, datetime))
65
+ )
66
+ }
54
67
  end
55
68
 
56
69
  private
@@ -16,56 +16,69 @@ module Hoardable
16
16
  # @return [String] The database operation that created the +version+ - either +update+ or +delete+.
17
17
  delegate :hoardable_event_uuid, :hoardable_operation, to: :hoardable_version, allow_nil: true
18
18
 
19
+ # A module for overriding +ActiveRecord#find_one+’ in the case you are doing a temporal query
20
+ # and the current {SourceModel} record may in fact be a {VersionModel} record.
21
+ module FinderMethods
22
+ def find_one(id)
23
+ conditions = { primary_key => [id, *version_class.where(hoardable_source_id: id).select(primary_key).ids] }
24
+ find_by(conditions) || where(conditions).raise_record_not_found_exception!
25
+ end
26
+ end
27
+ private_constant :FinderMethods
28
+
19
29
  class_methods do
20
30
  # The dynamically generated +Version+ class for this model.
21
31
  def version_class
22
32
  "#{name}#{VERSION_CLASS_SUFFIX}".constantize
23
33
  end
34
+
35
+ # Extends the current {SourceModel} scoping to include Hoardable’s {FinderMethods} overrides.
36
+ def hoardable
37
+ extending(FinderMethods)
38
+ end
24
39
  end
25
40
 
26
41
  included do
27
- include Tableoid
42
+ include Scopes
28
43
 
29
44
  around_update(if: [HOARDABLE_CALLBACKS_ENABLED, HOARDABLE_VERSION_UPDATES]) do |_, block|
30
- hoardable_source_service.insert_hoardable_version('update', &block)
45
+ hoardable_client.insert_hoardable_version('update', &block)
31
46
  end
32
47
 
33
48
  around_destroy(if: [HOARDABLE_CALLBACKS_ENABLED, HOARDABLE_SAVE_TRASH]) do |_, block|
34
- hoardable_source_service.insert_hoardable_version('delete', &block)
49
+ hoardable_client.insert_hoardable_version('delete', &block)
35
50
  end
36
51
 
37
52
  before_destroy(if: HOARDABLE_CALLBACKS_ENABLED, unless: HOARDABLE_SAVE_TRASH) do
38
53
  versions.delete_all(:delete_all)
39
54
  end
40
55
 
41
- after_commit { hoardable_source_service.unset_hoardable_version_and_event_uuid }
56
+ after_commit { hoardable_client.unset_hoardable_version_and_event_uuid }
42
57
 
43
58
  # Returns all +versions+ in ascending order of their temporal timeframes.
44
59
  has_many(
45
60
  :versions, -> { order('UPPER(_during) ASC') },
46
61
  dependent: nil,
47
62
  class_name: version_class.to_s,
48
- inverse_of: :hoardable_source
63
+ inverse_of: :hoardable_source,
64
+ foreign_key: :hoardable_source_id
49
65
  )
50
-
51
- # @!scope class
52
- # @!method at
53
- # @return [ActiveRecord<Object>]
54
- #
55
- # Returns instances of the source model and versions that were valid at the supplied
56
- # +datetime+ or +time+, all cast as instances of the source model.
57
- scope :at, lambda { |datetime|
58
- include_versions.where(id: version_class.at(datetime).select('id')).or(
59
- where.not(id: version_class.select(version_class.hoardable_source_foreign_key).where(DURING_QUERY, datetime))
60
- )
61
- }
62
66
  end
63
67
 
64
- # Returns a boolean of whether the record is actually a trashed +version+.
68
+ # Returns a boolean of whether the record is actually a trashed +version+ cast as an instance of the
69
+ # source model.
65
70
  #
66
71
  # @return [Boolean]
67
72
  def trashed?
68
- versions.trashed.only_most_recent.first&.hoardable_source_foreign_id == id
73
+ versions.trashed.only_most_recent.first&.hoardable_source_id == id
74
+ end
75
+
76
+ # Returns a boolean of whether the record is actually a +version+ cast as an instance of the
77
+ # source model.
78
+ #
79
+ # @return [Boolean]
80
+ def version?
81
+ !!hoardable_client.hoardable_version_source_id
69
82
  end
70
83
 
71
84
  # Returns the +version+ at the supplied +datetime+ or +time+. It will return +self+ if there is
@@ -85,68 +98,24 @@ module Hoardable
85
98
  def revert_to!(datetime)
86
99
  return unless (version = at(datetime))
87
100
 
88
- version.is_a?(self.class.version_class) ? version.revert! : self
101
+ version.is_a?(version_class) ? version.revert! : self
89
102
  end
90
103
 
91
- private
92
-
93
- def hoardable_source_service
94
- @hoardable_source_service ||= Service.new(self)
104
+ # Returns the +hoardable_source_id+ that represents the original {SourceModel} record’s ID. Will
105
+ # return nil if the current {SourceModel} record is not an instance of a {VersionModel} cast as
106
+ # {SourceModel}.
107
+ #
108
+ # @return [Integer, nil]
109
+ def hoardable_source_id
110
+ hoardable_client.hoardable_version_source_id || id
95
111
  end
96
112
 
97
- # This is a private service class that manages the insertion of {VersionModel}s for a
98
- # {SourceModel} into the PostgreSQL database.
99
- class Service
100
- attr_reader :source_model
101
-
102
- def initialize(source_model)
103
- @source_model = source_model
104
- end
105
-
106
- def insert_hoardable_version(operation)
107
- source_model.instance_variable_set('@hoardable_version', initialize_hoardable_version(operation))
108
- source_model.run_callbacks(:versioned) do
109
- yield if block_given?
110
- source_model.hoardable_version.save(validate: false, touch: false)
111
- end
112
- end
113
-
114
- def find_or_initialize_hoardable_event_uuid
115
- Thread.current[:hoardable_event_uuid] ||= ActiveRecord::Base.connection.query('SELECT gen_random_uuid();')[0][0]
116
- end
117
-
118
- def initialize_hoardable_version(operation)
119
- source_model.versions.new(
120
- source_model.attributes_before_type_cast.without('id').merge(
121
- source_model.changes.transform_values { |h| h[0] },
122
- {
123
- _event_uuid: find_or_initialize_hoardable_event_uuid,
124
- _operation: operation,
125
- _data: initialize_hoardable_data.merge(changes: source_model.changes)
126
- }
127
- )
128
- )
129
- end
113
+ delegate :version_class, to: :class
130
114
 
131
- def initialize_hoardable_data
132
- DATA_KEYS.to_h do |key|
133
- [key, assign_hoardable_context(key)]
134
- end
135
- end
136
-
137
- def assign_hoardable_context(key)
138
- return nil if (value = Hoardable.public_send(key)).nil?
139
-
140
- value.is_a?(Proc) ? value.call : value
141
- end
142
-
143
- def unset_hoardable_version_and_event_uuid
144
- source_model.instance_variable_set('@hoardable_version', nil)
145
- return if source_model.class.connection.transaction_open?
115
+ private
146
116
 
147
- Thread.current[:hoardable_event_uuid] = nil
148
- end
117
+ def hoardable_client
118
+ @hoardable_client ||= DatabaseClient.new(self)
149
119
  end
150
- private_constant :Service
151
120
  end
152
121
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hoardable
4
- VERSION = '0.6.0'
4
+ VERSION = '0.9.1'
5
5
  end
@@ -7,9 +7,11 @@ module Hoardable
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  class_methods do
10
- # Returns the foreign column that holds the reference to the source model of the version.
11
- def hoardable_source_foreign_key
12
- @hoardable_source_foreign_key ||= "#{superclass.model_name.i18n_key}_id"
10
+ # This is needed to omit the pseudo row of 'tableoid' when using +ActiveRecord+’s +insert+.
11
+ #
12
+ # @!visibility private
13
+ def scope_attributes
14
+ super.without('tableoid')
13
15
  end
14
16
  end
15
17
 
@@ -18,8 +20,7 @@ module Hoardable
18
20
  belongs_to(
19
21
  :hoardable_source,
20
22
  inverse_of: :versions,
21
- class_name: superclass.model_name,
22
- foreign_key: hoardable_source_foreign_key
23
+ class_name: superclass.model_name
23
24
  )
24
25
 
25
26
  self.table_name = "#{table_name.singularize}#{VERSION_TABLE_SUFFIX}"
@@ -29,8 +30,6 @@ module Hoardable
29
30
  alias_attribute :hoardable_event_uuid, :_event_uuid
30
31
  alias_attribute :hoardable_during, :_during
31
32
 
32
- before_create { hoardable_version_service.assign_temporal_tsrange }
33
-
34
33
  # @!scope class
35
34
  # @!method trashed
36
35
  # @return [ActiveRecord<Object>]
@@ -79,7 +78,7 @@ module Hoardable
79
78
 
80
79
  transaction do
81
80
  hoardable_source.tap do |reverted|
82
- reverted.update!(hoardable_version_service.hoardable_source_attributes.without('id'))
81
+ reverted.update!(hoardable_source_attributes.without('id'))
83
82
  reverted.instance_variable_set(:@hoardable_version, self)
84
83
  reverted.run_callbacks(:reverted)
85
84
  end
@@ -92,10 +91,11 @@ module Hoardable
92
91
  raise(Error, 'Version is not trashed, cannot untrash') unless hoardable_operation == 'delete'
93
92
 
94
93
  transaction do
95
- hoardable_version_service.insert_untrashed_source.tap do |untrashed|
96
- untrashed.send('hoardable_source_service').insert_hoardable_version('insert')
97
- untrashed.instance_variable_set(:@hoardable_version, self)
98
- untrashed.run_callbacks(:untrashed)
94
+ insert_untrashed_source.tap do |untrashed|
95
+ untrashed.send('hoardable_client').insert_hoardable_version('insert') do
96
+ untrashed.instance_variable_set(:@hoardable_version, self)
97
+ untrashed.run_callbacks(:untrashed)
98
+ end
99
99
  end
100
100
  end
101
101
  end
@@ -113,72 +113,24 @@ module Hoardable
113
113
  _data&.dig('changes')
114
114
  end
115
115
 
116
- # Returns the foreign reference that represents the source model of the version.
117
- def hoardable_source_foreign_id
118
- @hoardable_source_foreign_id ||= public_send(hoardable_source_foreign_key)
116
+ # Returns the ID of the {SourceModel} that created this {VersionModel}
117
+ def hoardable_source_id
118
+ read_attribute('hoardable_source_id')
119
119
  end
120
120
 
121
- delegate :hoardable_source_foreign_key, to: :class
121
+ private
122
122
 
123
- def hoardable_version_service
124
- @hoardable_version_service ||= Service.new(self)
123
+ def insert_untrashed_source
124
+ superscope = self.class.superclass.unscoped
125
+ superscope.insert(hoardable_source_attributes.merge('id' => hoardable_source_id))
126
+ superscope.find(hoardable_source_id)
125
127
  end
126
128
 
127
- # This is a private service class that manages the construction of {VersionModel} attributes and
128
- # untrashing / re-insertion into the {SourceModel} table.
129
- class Service
130
- attr_reader :version_model
131
-
132
- def initialize(version_model)
133
- @version_model = version_model
134
- end
135
-
136
- delegate :hoardable_source_foreign_id, :hoardable_source_foreign_key, :hoardable_source, to: :version_model
137
-
138
- def insert_untrashed_source
139
- superscope = version_model.class.superclass.unscoped
140
- superscope.insert(hoardable_source_attributes.merge('id' => hoardable_source_foreign_id))
141
- superscope.find(hoardable_source_foreign_id)
142
- end
143
-
144
- def hoardable_source_attributes
145
- @hoardable_source_attributes ||=
146
- version_model
147
- .attributes_before_type_cast
148
- .without(hoardable_source_foreign_key)
149
- .reject { |k, _v| k.start_with?('_') }
150
- end
151
-
152
- def previous_temporal_tsrange_end
153
- hoardable_source.versions.only_most_recent.pluck('_during').first&.end
154
- end
155
-
156
- def hoardable_source_epoch
157
- if hoardable_source.class.column_names.include?('created_at')
158
- hoardable_source.created_at
159
- else
160
- maybe_warn_about_missing_created_at_column
161
- Time.at(0).utc
162
- end
163
- end
164
-
165
- def assign_temporal_tsrange
166
- version_model._during = ((previous_temporal_tsrange_end || hoardable_source_epoch)..Time.now.utc)
167
- end
168
-
169
- def maybe_warn_about_missing_created_at_column
170
- return unless hoardable_source.class.hoardable_config[:warn_on_missing_created_at_column]
171
-
172
- source_table_name = hoardable_source.class.table_name
173
- Hoardable.logger.info(
174
- <<~LOG
175
- '#{source_table_name}' does not have a 'created_at' column, so the first version’s temporal period
176
- will begin at the unix epoch instead. Add a 'created_at' column to '#{source_table_name}'
177
- or set 'Hoardable.warn_on_missing_created_at_column = false' to disable this message.
178
- LOG
179
- )
180
- end
129
+ def hoardable_source_attributes
130
+ @hoardable_source_attributes ||=
131
+ attributes_before_type_cast
132
+ .without('hoardable_source_id')
133
+ .reject { |k, _v| k.start_with?('_') }
181
134
  end
182
- private_constant :Service
183
135
  end
184
136
  end
data/lib/hoardable.rb CHANGED
@@ -2,10 +2,12 @@
2
2
 
3
3
  require_relative 'hoardable/version'
4
4
  require_relative 'hoardable/hoardable'
5
- require_relative 'hoardable/tableoid'
5
+ require_relative 'hoardable/scopes'
6
6
  require_relative 'hoardable/error'
7
+ require_relative 'hoardable/database_client'
7
8
  require_relative 'hoardable/source_model'
8
9
  require_relative 'hoardable/version_model'
9
10
  require_relative 'hoardable/model'
10
11
  require_relative 'hoardable/associations'
11
12
  require_relative 'generators/hoardable/migration_generator'
13
+ require_relative 'generators/hoardable/initializer_generator'
data/sig/hoardable.rbs CHANGED
@@ -1,7 +1,7 @@
1
1
  module Hoardable
2
2
  VERSION: String
3
3
  DATA_KEYS: [:meta, :whodunit, :note, :event_uuid]
4
- CONFIG_KEYS: [:enabled, :version_updates, :save_trash, :return_everything, :warn_on_missing_created_at_column]
4
+ CONFIG_KEYS: [:enabled, :version_updates, :save_trash, :warn_on_missing_created_at_column]
5
5
  VERSION_CLASS_SUFFIX: String
6
6
  VERSION_TABLE_SUFFIX: String
7
7
  DURING_QUERY: String
@@ -10,12 +10,14 @@ module Hoardable
10
10
  HOARDABLE_VERSION_UPDATES: ^(untyped) -> untyped
11
11
  self.@context: Hash[untyped, untyped]
12
12
  self.@config: untyped
13
+ self.@at: nil
13
14
  self.@logger: untyped
14
15
 
15
16
  def self.with: (untyped hash) -> untyped
17
+ def self.at: (untyped datetime) -> untyped
16
18
  def self.logger: -> untyped
17
19
 
18
- module Tableoid
20
+ module Scopes
19
21
  TABLEOID_AREL_CONDITIONS: Proc
20
22
 
21
23
  private
@@ -28,57 +30,62 @@ module Hoardable
28
30
  class Error < StandardError
29
31
  end
30
32
 
33
+ class DatabaseClient
34
+ @hoardable_version_source_id: untyped
35
+
36
+ attr_reader source_record: SourceModel
37
+ def initialize: (SourceModel source_record) -> void
38
+ def insert_hoardable_version: (untyped operation) -> untyped
39
+ def find_or_initialize_hoardable_event_uuid: -> untyped
40
+ def hoardable_version_source_id: -> untyped
41
+ def query_hoardable_version_source_id: -> untyped
42
+ def initialize_version_attributes: (untyped operation) -> untyped
43
+ def initialize_temporal_range: -> Range
44
+ def initialize_hoardable_data: -> untyped
45
+ def assign_hoardable_context: (:event_uuid | :meta | :note | :whodunit key) -> nil
46
+ def unset_hoardable_version_and_event_uuid: -> nil
47
+ def previous_temporal_tsrange_end: -> untyped
48
+ def hoardable_source_epoch: -> Time
49
+ def maybe_warn_about_missing_created_at_column: -> nil
50
+ end
51
+
31
52
  module SourceModel
32
- include Tableoid
33
- @hoardable_source_service: Service
53
+ include Scopes
54
+ @hoardable_client: DatabaseClient
34
55
 
35
- attr_reader hoardable_version: nil
56
+ attr_reader hoardable_version: untyped
36
57
  def trashed?: -> untyped
58
+ def version?: -> untyped
37
59
  def at: (untyped datetime) -> SourceModel
38
60
  def revert_to!: (untyped datetime) -> SourceModel?
61
+ def hoardable_source_id: -> untyped
39
62
 
40
63
  private
41
- def hoardable_source_service: -> Service
64
+ def hoardable_client: -> DatabaseClient
42
65
 
43
66
  public
44
67
  def version_class: -> untyped
68
+ def hoardable: -> untyped
45
69
 
46
- class Service
47
- attr_reader source_model: SourceModel
48
- def initialize: (SourceModel source_model) -> void
49
- def insert_hoardable_version: (untyped operation) -> untyped
50
- def find_or_initialize_hoardable_event_uuid: -> untyped
51
- def initialize_hoardable_version: (untyped operation) -> untyped
52
- def initialize_hoardable_data: -> untyped
53
- def assign_hoardable_context: (:event_uuid | :meta | :note | :whodunit key) -> nil
54
- def unset_hoardable_version_and_event_uuid: -> nil
70
+ module FinderMethods
71
+ def find_one: (untyped id) -> untyped
55
72
  end
56
73
  end
57
74
 
58
75
  module VersionModel
59
- @hoardable_source_foreign_id: untyped
60
- @hoardable_source_foreign_key: String
61
- @hoardable_version_service: Service
76
+ @hoardable_source_attributes: untyped
62
77
 
63
78
  def revert!: -> untyped
64
79
  def untrash!: -> untyped
65
80
  def changes: -> untyped
66
- def hoardable_source_foreign_id: -> untyped
67
- def hoardable_version_service: -> Service
68
- def hoardable_source_foreign_key: -> String
69
-
70
- class Service
71
- @hoardable_source_attributes: untyped
72
-
73
- attr_reader version_model: VersionModel
74
- def initialize: (VersionModel version_model) -> void
75
- def insert_untrashed_source: -> untyped
76
- def hoardable_source_attributes: -> untyped
77
- def previous_temporal_tsrange_end: -> untyped
78
- def hoardable_source_epoch: -> Time
79
- def assign_temporal_tsrange: -> Range
80
- def maybe_warn_about_missing_created_at_column: -> nil
81
- end
81
+ def hoardable_source_id: -> untyped
82
+
83
+ private
84
+ def insert_untrashed_source: -> untyped
85
+ def hoardable_source_attributes: -> untyped
86
+
87
+ public
88
+ def scope_attributes: -> untyped
82
89
  end
83
90
 
84
91
  module Model
@@ -93,6 +100,17 @@ module Hoardable
93
100
 
94
101
  module Associations
95
102
  def belongs_to_trashable: (untyped name, ?nil scope, **untyped) -> untyped
103
+ def has_many_hoardable: (untyped name, ?nil scope, **untyped) -> untyped
104
+
105
+ module HasManyScope
106
+ @scope: untyped
107
+ @association: bot
108
+
109
+ def scope: -> untyped
110
+
111
+ private
112
+ def hoardable_scope: -> untyped
113
+ end
96
114
  end
97
115
 
98
116
  class MigrationGenerator
@@ -103,4 +121,8 @@ module Hoardable
103
121
  def migration_template_name: -> String
104
122
  def singularized_table_name: -> untyped
105
123
  end
124
+
125
+ class InitializerGenerator
126
+ def create_initializer_file: -> untyped
127
+ end
106
128
  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.6.0
4
+ version: 0.9.1
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-09-28 00:00:00.000000000 Z
11
+ date: 2022-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -104,16 +104,18 @@ files:
104
104
  - LICENSE.txt
105
105
  - README.md
106
106
  - Rakefile
107
+ - lib/generators/hoardable/initializer_generator.rb
107
108
  - lib/generators/hoardable/migration_generator.rb
108
109
  - lib/generators/hoardable/templates/migration.rb.erb
109
110
  - lib/generators/hoardable/templates/migration_6.rb.erb
110
111
  - lib/hoardable.rb
111
112
  - lib/hoardable/associations.rb
113
+ - lib/hoardable/database_client.rb
112
114
  - lib/hoardable/error.rb
113
115
  - lib/hoardable/hoardable.rb
114
116
  - lib/hoardable/model.rb
117
+ - lib/hoardable/scopes.rb
115
118
  - lib/hoardable/source_model.rb
116
- - lib/hoardable/tableoid.rb
117
119
  - lib/hoardable/version.rb
118
120
  - lib/hoardable/version_model.rb
119
121
  - sig/hoardable.rbs