active_record-undo 0.1.3 → 0.1.4

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: 3463f7d5263618417b5dc0bda22a13f8185b349c24d8b4e1610657afc5509564
4
- data.tar.gz: d89197a34b065986d1f0060a83b6d5284af254ec4e9d2710de48549f60078e55
3
+ metadata.gz: aedb1b7356b0530c7316c1c94a4f2681cf4c103cb50986e24dfaa1f5d1eec553
4
+ data.tar.gz: 33f779d1c31b5c80126fa4838b8f080f512e25dca9dd372d88e3ff1d9a1b4eaa
5
5
  SHA512:
6
- metadata.gz: 6daed055a8e31d9ce9386c92943b9e07133ca69aa0f782412d032e090491cfcc080054359fa7e9340b78563addf9dc17cf3cc6dabed4baffda12a611bf32e5ff
7
- data.tar.gz: 3259b19bec3a4519ad5dda5b3d56dc94ff3ff6c4d4853fb81cdde33556cbbc530689875863a00893ac277bd042f9153261f4413ff0ad6a9dd87fcc26d09d6091
6
+ metadata.gz: 982b77e10592f7e0599d080b74d5c9daa4658c9df14e8a068b95dd00d51cacd33e813e5250e88b3fedc134fbfbe66fde57a6ad62c089b0dd2ce32a56c2597332
7
+ data.tar.gz: 177f71075ba0e65a2e372fbfc9afb4783248c29eb164955cf7449706257e82759d38e86b0eefad18b89a4b4a17ee335c8d589be65b6d288ff0126ccf9fafd7ab
data/DOCUMENTATION.md CHANGED
@@ -49,6 +49,10 @@ graph TD
49
49
  | **Audit Log Parent** | `lib/active_record/undo/undo_log.rb` | `UndoLog` | Represents the top-level deletion event and manages atomic batch restoration |
50
50
  | **Audit Log Child** | `lib/active_record/undo/undo_log_item.rb` | `UndoLogItem` | Maps polymorphic targets (`item_type`, `item_id`) to original deleted entities |
51
51
  | **Engine Link** | `lib/active_record/undo/engine.rb` | `Engine` | Appends `db/migrate/` directly to host app migration paths |
52
+ | **Configuration** | `lib/active_record/undo/configuration.rb` | `Configuration` | Houses retention_period settings |
53
+ | **Purger Service** | `lib/active_record/undo/purger.rb` | `Purger` | Deletes expired UndoLog/Items and soft-deleted model records in batches |
54
+ | **Purge Job** | `lib/active_record/undo/purge_job.rb` | `PurgeJob` | ActiveJob background runner invoking Purger |
55
+ | **Rake Task** | `lib/active_record/undo/tasks/purge.rake` | Rake Task | Exposes `active_record_undo:purge_expired` command |
52
56
 
53
57
  ---
54
58
 
@@ -202,6 +206,7 @@ sequenceDiagram
202
206
  - `undoable_column`: Caches the name of the column (defaults to `:deleted_at`).
203
207
  - `kept` scope: Returns records that are not soft-deleted (`where(column => nil)`).
204
208
  - `soft_deleted` scope: Returns records that are soft-deleted (`where.not(column => nil)`).
209
+ - `expired` scope: Returns records soft-deleted before the global retention period threshold.
205
210
  * **`soft_deleted?`**
206
211
  - *Function*: Checks if the current record instance has been soft-deleted. Returns `true` if the configured deletion column is populated with a timestamp.
207
212
  * **`undoable?`**
@@ -227,6 +232,7 @@ sequenceDiagram
227
232
  3. Resolves the latest `UndoLogItem` that records the soft-deletion of this instance.
228
233
  4. If a log item is found, it calls `restore!` on the parent `UndoLog` (which restores the entire deleted tree).
229
234
  5. If no log item is found, it falls back to a simple, direct restore by setting the deletion column back to `nil`.
235
+ 6. Automatically reloads the instance attributes in-memory (using `reload`) to refresh the model state before returning `true`.
230
236
  * **`ensure_undoable_column_exists!` (Private)**
231
237
  - *Function*: Asserts that the configured soft-delete column exists in the database schema table. Raises `ActiveRecord::Undo::Error` if missing.
232
238
  * **`find_latest_undo_log_item` (Private)**
@@ -289,4 +295,31 @@ sequenceDiagram
289
295
  * **`ensure_column_exists!(klass, column_name)` (Private)**
290
296
  - *Function*: Confirms that the target soft-delete column exists in the class's table schema. Throws `ActiveRecord::Undo::Error` if missing.
291
297
  * **`reset_soft_delete_column!(target, column_name)` (Private)**
292
- - *Function*: Bypasses standard callbacks and validations to write a `nil` value to the soft-delete column directly in the database.
298
+ - *Function*: Bypasses standard callbacks and validations to write a `nil` value to the soft-delete column directly in the database.
299
+
300
+ ### 6.9 `lib/active_record/undo/configuration.rb` (Global Configuration)
301
+
302
+ * **`Configuration#initialize`**
303
+ - *Function*: Instantiates default configurations for the gem.
304
+ - *Details*: Sets default `@retention_period` to `30.days`.
305
+
306
+ ### 6.10 `lib/active_record/undo/purger.rb` (Hard Purging Engine)
307
+
308
+ * **`Purger.purge_expired!(batch_size: 1000)`**
309
+ - *Function*: Cleans up all database records and logs that are past their retention limits.
310
+ - *Steps*:
311
+ 1. Delegates to `purge_expired_logs!(batch_size)` to find and clean up expired `UndoLog` and `UndoLogItem` entries in batches using direct SQL `delete_all` execution.
312
+ 2. Delegates to `purge_expired_records!(batch_size)` to eager-load the host Rails application models (preventing Zeitwerk lazy-load gaps), identify expired records via the `.expired` scope, and hard-delete them and their cascading dependent associations in batches.
313
+ - *Relational Integrity & Constraint Protection*: Bottom-up recursive cascading deletes are performed first for associations marked as `:destroy`, `:delete_all`, or `:soft_delete`. For `:nullify` configurations, foreign key values are updated to `nil`, falling back to cascading deletion if the column contains a database-level `NOT NULL` constraint.
314
+
315
+ ### 6.11 `lib/active_record/undo/purge_job.rb` (Background Task Worker)
316
+
317
+ * **`PurgeJob#perform(batch_size: 1000)`**
318
+ - *Function*: Runs inside ActiveJob (when available) to execute purging asynchronously.
319
+ - *Details*: Invokes `ActiveRecord::Undo::Purger.purge_expired!(batch_size: batch_size)`.
320
+
321
+ ### 6.12 `lib/active_record/undo/tasks/purge.rake` (Command Line Utility)
322
+
323
+ * **`active_record_undo:purge_expired`**
324
+ - *Function*: Exposes CLI Rake task for the purger engine.
325
+ - *Details*: Checks `ENV['BATCH_SIZE']` for custom batch sizes, falling back to 1000, and triggers `ActiveRecord::Undo::Purger.purge_expired!`.
data/README.md CHANGED
@@ -18,6 +18,7 @@ Unlike conventional soft-deletion gems, `active_record-undo` automatically captu
18
18
  - 📦 **Polymorphic Tracking:** Records deletion events via native `UndoLog` and `UndoLogItem` models—no messy JSON payload parsing required.
19
19
  - 🚂 **Zero Generator Setup:** Built on top of `Rails::Engine`. Migrations automatically hook into `rails db:migrate`.
20
20
  - 🔍 **Default Scopes & Helpers:** Provides `.kept`, `.soft_deleted`, `#soft_deleted?`, and `#undoable?` query methods out of the box.
21
+ - 🧹 **Automatic Expiration & Purging:** Configurable global retention window with automated background purging (`PurgeJob` and Rake task) to clean up old soft-deleted records and logs.
21
22
 
22
23
  ---
23
24
 
@@ -158,11 +159,11 @@ To restore a deleted object tree, you can invoke `restore!` either on the corres
158
159
  Calling `restore!` directly on the soft-deleted model automatically resolves its latest deletion event log, performs the cascading restore, and cleans up the log database rows:
159
160
 
160
161
  ```ruby
161
- # Restores the post and all comments deleted in the same batch
162
+ # Restores the post and all comments deleted in the same batch (automatically reloads in-memory)
162
163
  post.restore!
163
164
 
164
- post.reload.soft_deleted? # => false
165
- post.comments.count # => 2
165
+ post.soft_deleted? # => false
166
+ post.comments.count # => 2
166
167
  ```
167
168
 
168
169
  #### Option B: Restore from the `UndoLog`
@@ -199,6 +200,75 @@ Post.unscoped.where(id: 1)
199
200
 
200
201
  ---
201
202
 
203
+ ## Expiration, Retention & Auto-Purging
204
+
205
+ To prevent database bloat, `active_record-undo` supports automated retention periods, expiration checks, and background purging for both soft-deleted records and their associated `UndoLog` audit entries.
206
+
207
+ ### 1. Global Configuration
208
+
209
+ You can configure a global retention period inside a Rails initializer:
210
+
211
+ ```ruby
212
+ # config/initializers/active_record_undo.rb
213
+ ActiveRecord::Undo.configure do |config|
214
+ # Default retention period for all soft-deleted records and undo logs (defaults to 30.days)
215
+ config.retention_period = 30.days
216
+ end
217
+ ```
218
+
219
+ If `retention_period` is set to `nil`, records and logs will never expire.
220
+
221
+ ### 2. Expiration Scopes & Helpers
222
+
223
+ The gem provides query scopes and instance predicate helpers:
224
+
225
+ - **Model `.expired` Scope**: Returns soft-deleted records older than the configured retention period.
226
+ ```ruby
227
+ Post.expired # => ActiveRecord::Relation of posts soft-deleted > 30 days ago
228
+ ```
229
+ - **Model `#expired?` Predicate**: Checks if a record is soft-deleted and past the retention period.
230
+ ```ruby
231
+ post.expired? # => true/false
232
+ ```
233
+ - **`UndoLog.expired` Scope**: Returns undo log entries older than the retention period.
234
+ ```ruby
235
+ ActiveRecord::Undo::UndoLog.expired # => logs created > 30 days ago
236
+ ```
237
+
238
+ ### 3. Background Purging
239
+
240
+ #### Purger Service
241
+ The `ActiveRecord::Undo::Purger` class performs hard SQL deletes on expired records and logs using `delete_all` (bypassing callbacks and validations for efficiency):
242
+
243
+ ```ruby
244
+ # Purge all expired soft-deleted records and undo logs in batches
245
+ ActiveRecord::Undo::Purger.purge_expired!(batch_size: 1000)
246
+ ```
247
+
248
+ > [!NOTE]
249
+ > **Relational Integrity & Constraint Protection**: To prevent database-level foreign key constraint failures (e.g. `FOREIGN KEY constraint failed`), `Purger` dynamically resolves dependent associations (such as `dependent: :destroy`, `dependent: :delete_all`, or `dependent: :soft_delete`) and recursively purges associated child records bottom-up before deleting the parent record. For associations configured with `dependent: :nullify`, it nullifies the foreign key (or cascades the deletion if the foreign key column is database-restricted to be `NOT NULL`).
250
+
251
+ #### ActiveJob Background Job
252
+ The gem provides an ActiveJob class that calls the Purger service:
253
+
254
+ ```ruby
255
+ # Enqueue the purge job to run in the background
256
+ ActiveRecord::Undo::PurgeJob.perform_later(batch_size: 1000)
257
+ ```
258
+
259
+ #### Engine Rake Task
260
+ You can run the purge task via Rake. This task is automatically loaded into host applications:
261
+
262
+ ```bash
263
+ # Run with the default batch size of 1000
264
+ $ rails active_record_undo:purge_expired
265
+
266
+ # Run with a custom batch size
267
+ $ BATCH_SIZE=500 rails active_record_undo:purge_expired
268
+ ```
269
+
270
+ ---
271
+
202
272
  ## How It Works
203
273
 
204
274
  1. **Cascade Inspection:** When `soft_delete!` is called, `ActiveRecord::Undo::CascadeHandler` reflects on `has_many`, `has_one`, and `belongs_to` associations configured with `dependent: :destroy` or `:delete_all`.
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Undo
5
+ class Configuration
6
+ attr_accessor :retention_period
7
+
8
+ def initialize
9
+ @retention_period = 30.days
10
+ end
11
+ end
12
+ end
13
+ end
@@ -14,6 +14,11 @@ module ActiveRecord
14
14
  end
15
15
  end
16
16
  end
17
+
18
+ rake_tasks do
19
+ path = root.join('lib', 'tasks', 'active_record_undo_tasks.rake')
20
+ load path if File.exist?(path)
21
+ end
17
22
  end
18
23
  end
19
24
  end
@@ -8,18 +8,44 @@ module ActiveRecord
8
8
  module ModelExtension
9
9
  extend ActiveSupport::Concern
10
10
 
11
- class_methods do
11
+ module ClassMethods
12
12
  # Accept a custom column parameter, defaulting to :deleted_at
13
13
  def acts_as_undoable(column: :deleted_at)
14
14
  include InstanceMethods
15
15
 
16
+ define_undo_attributes!(column)
17
+ define_undo_scopes!
18
+
19
+ ActiveRecord::Undo.registered_models << self
20
+ end
21
+
22
+ private
23
+
24
+ def define_undo_attributes!(column)
16
25
  class_attribute :undoable_column
17
26
  self.undoable_column = column.to_sym
27
+ end
28
+
29
+ def define_undo_scopes!
30
+ define_basic_scopes!
31
+ define_expired_scope!
32
+ end
18
33
 
19
- # Dynamic scopes using the configured column
34
+ def define_basic_scopes!
20
35
  scope :kept, -> { where(undoable_column => nil) }
21
36
  scope :soft_deleted, -> { where.not(undoable_column => nil) }
22
37
  end
38
+
39
+ def define_expired_scope!
40
+ scope :expired, lambda {
41
+ period = ActiveRecord::Undo.config.retention_period
42
+ if period
43
+ where("#{table_name}.#{undoable_column} < ?", Time.current - period)
44
+ else
45
+ none
46
+ end
47
+ }
48
+ end
23
49
  end
24
50
 
25
51
  module InstanceMethods
@@ -27,6 +53,16 @@ module ActiveRecord
27
53
  public_send(self.class.undoable_column).present?
28
54
  end
29
55
 
56
+ def expired?
57
+ return false unless soft_deleted?
58
+
59
+ period = ActiveRecord::Undo.config.retention_period
60
+ return false unless period
61
+
62
+ timestamp = public_send(self.class.undoable_column)
63
+ timestamp && timestamp < Time.current - period
64
+ end
65
+
30
66
  def undoable?
31
67
  return false unless soft_deleted?
32
68
 
@@ -52,20 +88,16 @@ module ActiveRecord
52
88
  undo_log
53
89
  end
54
90
 
91
+ # rubocop:disable Naming/PredicateMethod
55
92
  def restore!
56
93
  ensure_undoable_column_exists!
57
94
  return false unless soft_deleted?
58
95
 
59
- log_item = find_latest_undo_log_item
60
-
61
- if log_item
62
- log_item.undo_log.restore!
63
- else
64
- column_name = self.class.undoable_column
65
- update_columns(column_name => nil, updated_at: Time.current)
66
- true
67
- end
96
+ restore_internally!(find_latest_undo_log_item)
97
+ reload
98
+ true
68
99
  end
100
+ # rubocop:enable Naming/PredicateMethod
69
101
 
70
102
  private
71
103
 
@@ -88,6 +120,15 @@ module ActiveRecord
88
120
  def soft_delete_cascade_internal!(timestamp, undo_log)
89
121
  CascadeHandler.new(self).soft_delete_with_cascade!(timestamp, undo_log)
90
122
  end
123
+
124
+ def restore_internally!(log_item)
125
+ if log_item
126
+ log_item.undo_log.restore!
127
+ else
128
+ column_name = self.class.undoable_column
129
+ update_columns(column_name => nil, updated_at: Time.current)
130
+ end
131
+ end
91
132
  end
92
133
  end
93
134
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'active_job'
5
+ rescue LoadError
6
+ # ActiveJob is not available
7
+ end
8
+
9
+ if defined?(ActiveJob::Base)
10
+ module ActiveRecord
11
+ module Undo
12
+ class PurgeJob < ActiveJob::Base
13
+ queue_as :default
14
+
15
+ def perform(batch_size: 1000)
16
+ ActiveRecord::Undo::Purger.purge_expired!(batch_size: batch_size)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Undo
5
+ class Purger
6
+ class << self
7
+ def purge_expired!(batch_size: 1000)
8
+ purge_expired_logs!(batch_size)
9
+ purge_expired_records!(batch_size)
10
+ end
11
+
12
+ private
13
+
14
+ def purge_expired_logs!(batch_size)
15
+ loop do
16
+ log_ids = UndoLog.expired.limit(batch_size).pluck(:id)
17
+ break if log_ids.empty?
18
+
19
+ UndoLogItem.where(undo_log_id: log_ids).delete_all
20
+ UndoLog.where(id: log_ids).delete_all
21
+ end
22
+ end
23
+
24
+ def purge_expired_records!(batch_size)
25
+ period = ActiveRecord::Undo.config.retention_period
26
+ return unless period
27
+
28
+ ActiveRecord::Undo.undoable_models.each do |model|
29
+ purge_model_expired_records!(model, batch_size)
30
+ end
31
+ end
32
+
33
+ def purge_model_expired_records!(model, batch_size)
34
+ primary_key = model.primary_key
35
+ loop do
36
+ expired_ids = model.expired.limit(batch_size).pluck(primary_key)
37
+ break if expired_ids.empty?
38
+
39
+ purge_records_with_cascade!(model, expired_ids, batch_size)
40
+ end
41
+ end
42
+
43
+ def purge_records_with_cascade!(model, record_ids, batch_size)
44
+ return if record_ids.empty?
45
+
46
+ nullify_dependent_associations!(model, record_ids)
47
+ purge_cascading_associations!(model, record_ids, batch_size)
48
+ delete_records!(model, record_ids)
49
+ end
50
+
51
+ def nullify_dependent_associations!(model, record_ids)
52
+ nullify_reflections(model).each do |reflection|
53
+ child_query_for(model, reflection, record_ids).update_all(reflection.foreign_key => nil)
54
+ end
55
+ end
56
+
57
+ def purge_cascading_associations!(model, record_ids, batch_size)
58
+ cascade_reflections(model).each do |reflection|
59
+ query = child_query_for(model, reflection, record_ids)
60
+ primary_key = reflection.klass.primary_key
61
+
62
+ query.pluck(primary_key).each_slice(batch_size) do |child_ids|
63
+ purge_records_with_cascade!(reflection.klass, child_ids, batch_size)
64
+ end
65
+ end
66
+ end
67
+
68
+ def child_query_for(model, reflection, record_ids)
69
+ query = reflection.klass.unscoped.where(reflection.foreign_key => record_ids)
70
+ query = query.where(reflection.type => model.name) if reflection.type
71
+ query
72
+ end
73
+
74
+ def delete_records!(model, record_ids)
75
+ model.unscoped.where(model.primary_key => record_ids).delete_all
76
+ end
77
+
78
+ def nullify_reflections(model)
79
+ model.reflections.values.select do |ref|
80
+ %i[has_many has_one].include?(ref.macro) &&
81
+ ref.options[:dependent] == :nullify &&
82
+ foreign_key_nullable?(ref)
83
+ end
84
+ end
85
+
86
+ def cascade_reflections(model)
87
+ model.reflections.values.select do |ref|
88
+ next false unless %i[has_many has_one].include?(ref.macro)
89
+
90
+ dependent = ref.options[:dependent]
91
+ %i[destroy soft_delete delete_all].include?(dependent) ||
92
+ non_nullable_nullify?(ref)
93
+ end
94
+ end
95
+
96
+ def non_nullable_nullify?(ref)
97
+ ref.options[:dependent] == :nullify && !foreign_key_nullable?(ref)
98
+ end
99
+
100
+ def foreign_key_nullable?(ref)
101
+ column = ref.klass.columns_hash[ref.foreign_key.to_s]
102
+ column.nil? || column.null
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :active_record_undo do
4
+ desc 'Purge expired soft-deleted records and undo logs'
5
+ task purge_expired: :environment do
6
+ batch_size = ENV['BATCH_SIZE'] ? ENV['BATCH_SIZE'].to_i : 1000
7
+ ActiveRecord::Undo::Purger.purge_expired!(batch_size: batch_size)
8
+ end
9
+ end
@@ -8,6 +8,15 @@ module ActiveRecord
8
8
 
9
9
  has_many :undo_log_items, class_name: 'ActiveRecord::Undo::UndoLogItem', dependent: :destroy
10
10
 
11
+ scope :expired, lambda {
12
+ period = ActiveRecord::Undo.config.retention_period
13
+ if period
14
+ where('created_at < ?', Time.current - period)
15
+ else
16
+ none
17
+ end
18
+ }
19
+
11
20
  # Restores all records associated with this deletion batch
12
21
  def restore!
13
22
  transaction do
@@ -3,6 +3,6 @@
3
3
 
4
4
  module ActiveRecord
5
5
  module Undo
6
- VERSION = '0.1.3'
6
+ VERSION = '0.1.4'
7
7
  end
8
8
  end
@@ -9,15 +9,52 @@ rescue LoadError => e
9
9
  "Please add 'activerecord' to your Gemfile. (Original error: #{e.message})"
10
10
  end
11
11
 
12
+ require 'set'
12
13
  require_relative 'undo/version'
13
14
  require_relative 'undo/engine' if defined?(Rails::Engine)
15
+ require_relative 'undo/configuration'
14
16
  require_relative 'undo/model_extension'
15
17
  require_relative 'undo/undo_log'
16
18
  require_relative 'undo/undo_log_item'
19
+ require_relative 'undo/purger'
20
+ require_relative 'undo/purge_job'
17
21
 
18
22
  module ActiveRecord
19
23
  module Undo
20
24
  class Error < StandardError; end
25
+
26
+ class << self
27
+ def config
28
+ @config ||= Configuration.new
29
+ end
30
+
31
+ def configure
32
+ yield(config)
33
+ end
34
+
35
+ def registered_models
36
+ @registered_models ||= Set.new
37
+ end
38
+
39
+ def undoable_models
40
+ eager_load_rails!
41
+ models = registered_models.to_a
42
+ if defined?(ActiveRecord::Base)
43
+ models += ActiveRecord::Base.descendants.select { |m| m.respond_to?(:undoable_column) }
44
+ end
45
+ models.uniq
46
+ end
47
+
48
+ private
49
+
50
+ def eager_load_rails!
51
+ return unless defined?(Rails) && Rails.application
52
+
53
+ Rails.application.eager_load!
54
+ rescue StandardError
55
+ # Safe fallback if Rails eager loading fails in test environments
56
+ end
57
+ end
21
58
  end
22
59
  end
23
60
 
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ path = File.expand_path('../active_record/undo/tasks/purge.rake', __dir__)
4
+ load path if File.exist?(path)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record-undo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saurabh Sharma
@@ -141,11 +141,16 @@ files:
141
141
  - lib/active_record/undo/cascade_handler.rb
142
142
  - lib/active_record/undo/cascade_handler/association_finder.rb
143
143
  - lib/active_record/undo/cascade_handler/record_updater.rb
144
+ - lib/active_record/undo/configuration.rb
144
145
  - lib/active_record/undo/engine.rb
145
146
  - lib/active_record/undo/model_extension.rb
147
+ - lib/active_record/undo/purge_job.rb
148
+ - lib/active_record/undo/purger.rb
149
+ - lib/active_record/undo/tasks/purge.rake
146
150
  - lib/active_record/undo/undo_log.rb
147
151
  - lib/active_record/undo/undo_log_item.rb
148
152
  - lib/active_record/undo/version.rb
153
+ - lib/tasks/active_record_undo_tasks.rake
149
154
  - sig/active_record/undo.rbs
150
155
  homepage: https://github.com/saurabh-activecode/active_record-undo
151
156
  licenses: