active_record-undo 0.1.2 β†’ 0.1.3

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: 30415ed66666364750a948cbbb06c6edffb88bff916bc5b7fc42d78ae027038d
4
- data.tar.gz: 83f4abe357af730e23bf89f85e93e009cd6f6207a8bb313bb5972b67c1b6488e
3
+ metadata.gz: 3463f7d5263618417b5dc0bda22a13f8185b349c24d8b4e1610657afc5509564
4
+ data.tar.gz: d89197a34b065986d1f0060a83b6d5284af254ec4e9d2710de48549f60078e55
5
5
  SHA512:
6
- metadata.gz: 4fc93992835bec03fff811b669ea856230b8498bb53e81b62be8e85e6d7d047d0f644f9568de5bf337d77bd4b2ed20f2a36b00155f8b8021c3a358e6bbb0b710
7
- data.tar.gz: a1afbb6efc8ce17b31b8f2a8b863e94c0c5d1f04ad4eb475d21a5599f525845002ced413793b8139d1241e936aaac2d3754b89ff11a3a4b2e139c42aa7756bec
6
+ metadata.gz: 6daed055a8e31d9ce9386c92943b9e07133ca69aa0f782412d032e090491cfcc080054359fa7e9340b78563addf9dc17cf3cc6dabed4baffda12a611bf32e5ff
7
+ data.tar.gz: 3259b19bec3a4519ad5dda5b3d56dc94ff3ff6c4d4853fb81cdde33556cbbc530689875863a00893ac277bd042f9153261f4413ff0ad6a9dd87fcc26d09d6091
data/DOCUMENTATION.md CHANGED
@@ -12,6 +12,7 @@ graph TD
12
12
 
13
13
  subgraph GemCore[ActiveRecord::Undo Core Engine]
14
14
  ModelExt -->|Calls soft_delete!| TxBoundary[ActiveRecord::Base.transaction]
15
+ ModelExt -->|Calls undoable?| CheckQuery[SQL EXISTS Query across UndoLogItem & UndoLog]
15
16
 
16
17
  subgraph TxBoundary
17
18
  CreateLog[Create UndoLog Parent]
@@ -28,6 +29,7 @@ graph TD
28
29
  subgraph DatabaseStorage[Persistence Layer]
29
30
  AppendItem --> UndoLogTable[(undo_logs)]
30
31
  AppendItem --> UndoItemTable[(undo_log_items)]
32
+ CheckQuery --> UndoItemTable
31
33
  end
32
34
  ```
33
35
 
@@ -40,7 +42,7 @@ graph TD
40
42
  | Component | File Path | Class / Module | Core Responsibility |
41
43
  | :--- | :--- | :--- | :--- |
42
44
  | **Main Hook** | `lib/active_record/undo.rb` | `ActiveRecord::Undo` | Hooks into `ActiveSupport.on_load(:active_record)` |
43
- | **Model Extension** | `lib/active_record/undo/model_extension.rb` | `ModelExtension` | Injects DSL (`acts_as_undoable`), scopes (`kept`, `soft_deleted`), and methods (`soft_delete!`) |
45
+ | **Model Extension** | `lib/active_record/undo/model_extension.rb` | `ModelExtension` | Injects DSL (`acts_as_undoable`), scopes (`kept`, `soft_deleted`), and methods (`soft_delete!`, `undoable?`, `restore!`) |
44
46
  | **Cascade Engine** | `lib/active_record/undo/cascade_handler.rb` | `CascadeHandler` | Inspects ActiveRecord reflections (`reflections`) and executes DFS traversal |
45
47
  | **Cascade Association Finder** | `lib/active_record/undo/cascade_handler/association_finder.rb` | `AssociationFinder` | Resolves which records should cascade based on dependency configuration |
46
48
  | **Cascade Record Updater** | `lib/active_record/undo/cascade_handler/record_updater.rb` | `RecordUpdater` | Updates the database timestamps directly bypassing callbacks |
@@ -117,6 +119,28 @@ sequenceDiagram
117
119
  Model-->>User: Returns UndoLog Instance
118
120
  ```
119
121
 
122
+ ### Restoration Verification Flow (`#undoable?`)
123
+
124
+ ```mermaid
125
+ sequenceDiagram
126
+ autonumber
127
+ actor User
128
+ participant Model as Post Model
129
+ participant Ext as ModelExtension
130
+ participant DB as SQL Engine
131
+
132
+ User->>Model: post.undoable?
133
+ Model->>Ext: Check soft_deleted?
134
+
135
+ alt Not Soft Deleted
136
+ Ext-->>User: returns false
137
+ else Is Soft Deleted
138
+ Ext->>DB: SELECT 1 FROM undo_log_items INNER JOIN undo_logs ... LIMIT 1
139
+ DB-->>Ext: Record Exists (true / false)
140
+ Ext-->>User: returns boolean
141
+ end
142
+ ```
143
+
120
144
  ### Restoration Flow (`#restore!`)
121
145
 
122
146
  ```mermaid
@@ -147,9 +171,10 @@ sequenceDiagram
147
171
 
148
172
  1. **Depth-First Traversal Order:** Cascading deletes traverse downward to child records before updating the parent node. Child item associations are appended to `undo_log_items` first, and the parent record is appended last.
149
173
  2. **Reverse Restoration Order:** `#restore!` calls `undo_log_items.reverse_each`. This ensures the parent node is restored first before restoring its dependent records, maintaining database relational integrity.
150
- 3. **Bypassing Callbacks:** Soft-deletion updates use `update_columns`. This executes a direct SQL `UPDATE` query without firing standard ActiveRecord persistence callbacks (`save`, `validate`), preventing unintended side effects during soft deletes.
151
- 4. **Unscoped Model Resolution:** `#restore_item!` uses `klass.unscoped.find_by(id: item_id)` to locate records. This guarantees records are retrieved even when models define default scopes that filter out soft-deleted records.
152
- 5. **Class Inheritance Security Check:** When constantizing stored class strings, the gem validates that target models inherit from `ActiveRecord::Base` to prevent arbitrary non-model constant manipulation.
174
+ 3. **Optimized `#undoable?` Query Execution:** Calling `#undoable?` executes a single optimized `EXISTS` query (`joins(:undo_log).exists?(item_type: self.class.name, item_id: id)`). This checks for the presence of both the audit item and the valid parent log without loading records into Ruby memory.
175
+ 4. **Bypassing Callbacks:** Soft-deletion updates use `update_columns`. This executes a direct SQL `UPDATE` query without firing standard ActiveRecord persistence callbacks (`save`, `validate`), preventing unintended side effects during soft deletes.
176
+ 5. **Unscoped Model Resolution:** `#restore_item!` uses `klass.unscoped.find_by(id: item_id)` to locate records. This guarantees records are retrieved even when models define default scopes that filter out soft-deleted records.
177
+ 6. **Class Inheritance Security Check:** When constantizing stored class strings, the gem validates that target models inherit from `ActiveRecord::Base` to prevent arbitrary non-model constant manipulation.
153
178
 
154
179
  ---
155
180
 
@@ -179,6 +204,12 @@ sequenceDiagram
179
204
  - `soft_deleted` scope: Returns records that are soft-deleted (`where.not(column => nil)`).
180
205
  * **`soft_deleted?`**
181
206
  - *Function*: Checks if the current record instance has been soft-deleted. Returns `true` if the configured deletion column is populated with a timestamp.
207
+ * **`undoable?`**
208
+ - *Function*: Checks if the soft-deleted record has a valid undo log entry available for restoration.
209
+ - *Steps*:
210
+ 1. Returns `false` immediately if `soft_deleted?` is `false`.
211
+ 2. Performs a lightweight SQL `EXISTS` query (`ActiveRecord::Undo::UndoLogItem.joins(:undo_log).exists?(item_type: self.class.name, item_id: id)`).
212
+ 3. Returns `true` if both the log item and its parent `UndoLog` exist in the database.
182
213
  * **`soft_delete!`**
183
214
  - *Function*: Starts the cascade soft-deletion sequence for the record.
184
215
  - *Steps*:
@@ -258,4 +289,4 @@ sequenceDiagram
258
289
  * **`ensure_column_exists!(klass, column_name)` (Private)**
259
290
  - *Function*: Confirms that the target soft-delete column exists in the class's table schema. Throws `ActiveRecord::Undo::Error` if missing.
260
291
  * **`reset_soft_delete_column!(target, column_name)` (Private)**
261
- - *Function*: Bypasses standard callbacks and validations to write a `nil` value to the soft-delete column directly in the database.
292
+ - *Function*: Bypasses standard callbacks and validations to write a `nil` value to the soft-delete column directly in the database.
data/README.md CHANGED
@@ -12,11 +12,12 @@ Unlike conventional soft-deletion gems, `active_record-undo` automatically captu
12
12
  ## Features
13
13
 
14
14
  - πŸ”„ **Cascading Soft Deletes:** Soft deletes parent models along with dependent associations (`dependent: :destroy` / `:delete_all`).
15
- - βͺ **Atomic Restores:** Reverses soft deletion for an entire object tree (`undo_log.restore!`) within a single database transaction.
15
+ - βͺ **Atomic Restores:** Reverses soft deletion for an entire object tree (`undo_log.restore!` or `record.restore!`) within a single database transaction.
16
+ - πŸ” **Restoration Verification:** Provides `#undoable?` to check if a record is soft-deleted and has a valid undo log entry available for restoration.
16
17
  - βš™οΈ **Configurable Columns:** Supports custom soft-delete columns (e.g., `:archived_at`, `:discarded_at`) per model while defaulting to `:deleted_at`.
17
18
  - πŸ“¦ **Polymorphic Tracking:** Records deletion events via native `UndoLog` and `UndoLogItem` modelsβ€”no messy JSON payload parsing required.
18
19
  - πŸš‚ **Zero Generator Setup:** Built on top of `Rails::Engine`. Migrations automatically hook into `rails db:migrate`.
19
- - πŸ” **Default Scopes & Helpers:** Provides `.kept`, `.soft_deleted`, and `#soft_deleted?` query methods out of the box.
20
+ - πŸ” **Default Scopes & Helpers:** Provides `.kept`, `.soft_deleted`, `#soft_deleted?`, and `#undoable?` query methods out of the box.
20
21
 
21
22
  ---
22
23
 
@@ -121,6 +122,24 @@ item.soft_deleted? # => true
121
122
  item.archived_at # => 2026-08-08 22:20:16 UTC
122
123
  ```
123
124
 
125
+ ### Checking Restoration Eligibility (`#undoable?`)
126
+
127
+ Use `#undoable?` to verify if a record is soft-deleted and has a corresponding `UndoLog` entry available in the database. This is ideal for conditionally rendering UI elements or validating controller actions:
128
+
129
+ ```ruby
130
+ post = Post.unscoped.find(1)
131
+
132
+ if post.undoable?
133
+ # Render "Undo Deletion" button or execute restore
134
+ post.restore!
135
+ end
136
+ ```
137
+
138
+ `#undoable?` returns `false` if:
139
+ * The record is currently active (not soft deleted).
140
+ * The record was soft deleted manually via direct SQL/column updates without generating an undo log.
141
+ * The corresponding `UndoLog` record was purged or already restored.
142
+
124
143
  ### Inspect Deletion Logs
125
144
 
126
145
  Inspect affected records through standard Rails associations on the returned `UndoLog`:
@@ -159,7 +178,7 @@ post.comments.count # => 2
159
178
 
160
179
  ## Scopes & Querying
161
180
 
162
- `ActiveRecord::Undo` provides scopes for filtering records based on the configured column:
181
+ `ActiveRecord::Undo` provides scopes and predicate helpers for filtering and checking records based on the configured column:
163
182
 
164
183
  ```ruby
165
184
  # Fetch only active (non-deleted) records
@@ -168,6 +187,12 @@ Post.kept
168
187
  # Fetch soft-deleted records
169
188
  Post.soft_deleted
170
189
 
190
+ # Check if a record is soft-deleted
191
+ post.soft_deleted?
192
+
193
+ # Check if a record is soft-deleted AND can be restored via an undo log
194
+ post.undoable?
195
+
171
196
  # Retrieve records including soft-deleted ones via unscoped
172
197
  Post.unscoped.where(id: 1)
173
198
  ```
@@ -179,7 +204,8 @@ Post.unscoped.where(id: 1)
179
204
  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`.
180
205
  2. **Dynamic Column Resolution:** The handler checks `record.class.undoable_column` to set the correct timestamp column (`:deleted_at`, `:archived_at`, etc.) across all affected models.
181
206
  3. **Polymorphic Logging:** An `ActiveRecord::Undo::UndoLog` record is created alongside multiple `ActiveRecord::Undo::UndoLogItem` entries mapping polymorphic references (`item_type`, `item_id`) to every affected record.
182
- 4. **Atomic Operation:** All updates and log creations take place within an `ActiveRecord::Base.transaction`.
207
+ 4. **Restoration Verification:** Calling `#undoable?` executes an efficient SQL query joining `undo_log_items` and `undo_logs` to ensure the entity is soft-deleted and its associated log entry exists before restoration.
208
+ 5. **Atomic Operation:** All updates and log creations take place within an `ActiveRecord::Base.transaction`.
183
209
 
184
210
  ---
185
211
 
@@ -27,6 +27,15 @@ module ActiveRecord
27
27
  public_send(self.class.undoable_column).present?
28
28
  end
29
29
 
30
+ def undoable?
31
+ return false unless soft_deleted?
32
+
33
+ ActiveRecord::Undo::UndoLogItem.joins(:undo_log).exists?(
34
+ item_type: self.class.name,
35
+ item_id: id
36
+ )
37
+ end
38
+
30
39
  def soft_delete!
31
40
  ensure_undoable_column_exists!
32
41
  return false if soft_deleted?
@@ -3,6 +3,6 @@
3
3
 
4
4
  module ActiveRecord
5
5
  module Undo
6
- VERSION = '0.1.2'
6
+ VERSION = '0.1.3'
7
7
  end
8
8
  end
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.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saurabh Sharma