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 +4 -4
- data/DOCUMENTATION.md +36 -5
- data/README.md +30 -4
- data/lib/active_record/undo/model_extension.rb +9 -0
- data/lib/active_record/undo/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3463f7d5263618417b5dc0bda22a13f8185b349c24d8b4e1610657afc5509564
|
|
4
|
+
data.tar.gz: d89197a34b065986d1f0060a83b6d5284af254ec4e9d2710de48549f60078e55
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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. **
|
|
151
|
-
4. **
|
|
152
|
-
5. **
|
|
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 `#
|
|
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. **
|
|
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?
|