hoardable 0.6.0 → 0.9.1

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: 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