active_record-undo 0.2.0 → 0.3.0
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/CHANGELOG.md +78 -2
- data/DOCUMENTATION.md +311 -178
- data/README.md +140 -10
- data/app/controllers/active_record/undo/application_controller.rb +9 -0
- data/app/controllers/active_record/undo/logs_controller/async_handler.rb +50 -0
- data/app/controllers/active_record/undo/logs_controller.rb +32 -3
- data/app/jobs/active_record/undo/restore_job.rb +72 -0
- data/app/jobs/active_record/undo/soft_delete_job.rb +75 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20260920000000_add_status_and_error_message_to_undo_logs.rb +15 -0
- data/lib/active_record/undo/cascade_handler.rb +66 -0
- data/lib/active_record/undo/configuration.rb +2 -1
- data/lib/active_record/undo/model_extension/async_methods.rb +68 -0
- data/lib/active_record/undo/model_extension.rb +7 -4
- data/lib/active_record/undo/status_predicates.rb +39 -0
- data/lib/active_record/undo/undo_log/token_verifier.rb +48 -0
- data/lib/active_record/undo/undo_log.rb +49 -32
- data/lib/active_record/undo/version.rb +1 -1
- data/lib/active_record/undo/view_helpers/status_badge_helper.rb +84 -0
- data/lib/active_record/undo/view_helpers.rb +4 -0
- data/lib/active_record/undo.rb +24 -1
- data/sig/active_record/undo.rbs +50 -1
- metadata +23 -1
data/DOCUMENTATION.md
CHANGED
|
@@ -39,29 +39,32 @@ graph TD
|
|
|
39
39
|
|
|
40
40
|
### Component Summary
|
|
41
41
|
|
|
42
|
-
| Component | File Path | Class / Module
|
|
43
|
-
| :----------------------------- | :-------------------------------------------------------------- |
|
|
44
|
-
| **Main Hook & Context** | `lib/active_record/undo.rb` | `ActiveRecord::Undo`
|
|
45
|
-
| **Model Extension** | `lib/active_record/undo/model_extension.rb` | `ModelExtension`
|
|
46
|
-
| **Model Attribution Helper** | `lib/active_record/undo/model_extension/attribution_helper.rb` | `AttributionHelper`
|
|
47
|
-
| **Tenant Verification** | `lib/active_record/undo/model_extension/tenant_verification.rb` | `TenantVerification`
|
|
48
|
-
| **Cascade Engine** | `lib/active_record/undo/cascade_handler.rb` | `CascadeHandler`
|
|
49
|
-
| **Cascade Association Finder** | `lib/active_record/undo/cascade_handler/association_finder.rb` | `AssociationFinder`
|
|
50
|
-
| **Cascade Record Updater** | `lib/active_record/undo/cascade_handler/record_updater.rb` | `RecordUpdater`
|
|
51
|
-
| **Audit Log Parent** | `lib/active_record/undo/undo_log.rb` | `UndoLog`
|
|
52
|
-
| **Audit Log Child** | `lib/active_record/undo/undo_log_item.rb` | `UndoLogItem`
|
|
53
|
-
| **Engine Link** | `lib/active_record/undo/engine.rb` | `Engine`
|
|
54
|
-
| **Configuration** | `lib/active_record/undo/configuration.rb` | `Configuration`
|
|
42
|
+
| Component | File Path | Class / Module | Core Responsibility |
|
|
43
|
+
| :----------------------------- | :-------------------------------------------------------------- | :---------------------- | :------------------------------------------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| **Main Hook & Context** | `lib/active_record/undo.rb` | `ActiveRecord::Undo` | Hooks into `ActiveSupport.on_load(:active_record)`, manages ambient thread context (`whodunnit`, `current_tenant`), and defines errors |
|
|
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!`) |
|
|
46
|
+
| **Model Attribution Helper** | `lib/active_record/undo/model_extension/attribution_helper.rb` | `AttributionHelper` | Resolves user attribution and tenant context from parameters, configured procs, or thread contexts |
|
|
47
|
+
| **Tenant Verification** | `lib/active_record/undo/model_extension/tenant_verification.rb` | `TenantVerification` | Validates initiating context against log tenant on `#restore!`, raising `SecurityError` on mismatch |
|
|
48
|
+
| **Cascade Engine** | `lib/active_record/undo/cascade_handler.rb` | `CascadeHandler` | Inspects ActiveRecord reflections (`reflections`) and executes DFS traversal |
|
|
49
|
+
| **Cascade Association Finder** | `lib/active_record/undo/cascade_handler/association_finder.rb` | `AssociationFinder` | Resolves which records should cascade based on dependency configuration |
|
|
50
|
+
| **Cascade Record Updater** | `lib/active_record/undo/cascade_handler/record_updater.rb` | `RecordUpdater` | Updates the database timestamps directly bypassing callbacks |
|
|
51
|
+
| **Audit Log Parent** | `lib/active_record/undo/undo_log.rb` | `UndoLog` | Represents the top-level deletion event and manages atomic batch restoration, tenancy, and user attribution |
|
|
52
|
+
| **Audit Log Child** | `lib/active_record/undo/undo_log_item.rb` | `UndoLogItem` | Maps polymorphic targets (`item_type`, `item_id`) to original deleted entities |
|
|
53
|
+
| **Engine Link** | `lib/active_record/undo/engine.rb` | `Engine` | Appends `db/migrate/` directly to host app migration paths |
|
|
54
|
+
| **Configuration** | `lib/active_record/undo/configuration.rb` | `Configuration` | Houses retention_period, current_user_method, and current_tenant_method settings |
|
|
55
55
|
| **Purger Service** | `lib/active_record/undo/purger.rb` | `Purger` | Deletes expired UndoLog/Items and soft-deleted model records in batches, scoped to tenant context when present |
|
|
56
56
|
| **Purger Reflection Helper** | `lib/active_record/undo/purger/reflection_helper.rb` | `ReflectionHelper` | Determines cascade and nullify reflection behavior for purge routines |
|
|
57
57
|
| **Purge Job** | `lib/active_record/undo/purge_job.rb` | `PurgeJob` | ActiveJob background runner invoking Purger |
|
|
58
|
+
| **Soft Delete Job** | `app/jobs/active_record/undo/soft_delete_job.rb` | `SoftDeleteJob` | ActiveJob worker executing batched cascade soft deletes asynchronously (`soft_delete_later!`) |
|
|
59
|
+
| **Restore Job** | `app/jobs/active_record/undo/restore_job.rb` | `RestoreJob` | ActiveJob worker executing bottom-up batch restores with row locking (`restore_later!`) |
|
|
60
|
+
| **Status Predicates** | `lib/active_record/undo/status_predicates.rb` | `StatusPredicates` | Shared predicates for checking soft delete & restore progress (`soft_delete_processing?`, `restore_processing?`, etc.) |
|
|
58
61
|
| **Rake Task** | `lib/active_record/undo/tasks/purge.rake` | Rake Task | Exposes `active_record_undo:purge_expired` command |
|
|
59
62
|
| **Mountable Engine** | `lib/active_record/undo/engine.rb` | `Engine` | Defines mountable Rails engine route namespace, view helper hooks, and migrations |
|
|
60
63
|
| **Open-Redirect Guard** | `lib/active_record/undo/safe_redirect.rb` | `SafeRedirect` | Enforces URL safety, same-host/port validation, CRLF blocking, and safe fallback paths |
|
|
61
|
-
| **View Helpers** | `lib/active_record/undo/view_helpers.rb` | `ViewHelpers` | Provides `undo_button_to
|
|
64
|
+
| **View Helpers** | `lib/active_record/undo/view_helpers.rb` | `ViewHelpers` | Provides `undo_button_to`, `undo_link_to`, `undo_status_badge`, `soft_delete_status_badge`, and `restore_status_badge` helpers |
|
|
62
65
|
| **Engine Base Controller** | `app/controllers/active_record/undo/application_controller.rb` | `ApplicationController` | Base engine controller handling CSRF, attribution, tenancy, and multi-format error responses |
|
|
63
|
-
| **HTTP Restoration Endpoints** | `app/controllers/active_record/undo/logs_controller.rb` | `LogsController` | Handles restore actions
|
|
64
|
-
| **Signed Restore Endpoint** | `app/controllers/active_record/undo/restores_controller.rb` | `RestoresController` | Dedicated route endpoint for signed token restorations (`POST /undo/restore/:token`)
|
|
66
|
+
| **HTTP Restoration Endpoints** | `app/controllers/active_record/undo/logs_controller.rb` | `LogsController` | Handles restore actions (sync & async) and status polling (`GET /undo/logs/:id/status`) |
|
|
67
|
+
| **Signed Restore Endpoint** | `app/controllers/active_record/undo/restores_controller.rb` | `RestoresController` | Dedicated route endpoint for signed token restorations (`POST /undo/restore/:token`, `GET /undo/status/:token`) |
|
|
65
68
|
|
|
66
69
|
---
|
|
67
70
|
|
|
@@ -76,6 +79,9 @@ erDiagram
|
|
|
76
79
|
|
|
77
80
|
UNDO_LOGS {
|
|
78
81
|
bigint id PK
|
|
82
|
+
integer status "0: pending, 1: processing, 2: completed, 3: failed"
|
|
83
|
+
integer action "0: soft_delete, 1: restore"
|
|
84
|
+
text error_message "nullable"
|
|
79
85
|
string whodunnit_type "optional"
|
|
80
86
|
bigint whodunnit_id "optional"
|
|
81
87
|
string tenant_type "optional"
|
|
@@ -184,6 +190,80 @@ sequenceDiagram
|
|
|
184
190
|
Log-->>User: Restoration Complete
|
|
185
191
|
```
|
|
186
192
|
|
|
193
|
+
### Asynchronous Soft Deletion & Failure Handling (`#soft_delete_later!`)
|
|
194
|
+
|
|
195
|
+
```mermaid
|
|
196
|
+
sequenceDiagram
|
|
197
|
+
autonumber
|
|
198
|
+
actor User
|
|
199
|
+
participant Model as Post Model
|
|
200
|
+
participant DB as Database
|
|
201
|
+
participant Queue as ActiveJob / Queue
|
|
202
|
+
participant Worker as SoftDeleteJob
|
|
203
|
+
participant Log as UndoLog
|
|
204
|
+
participant Cascade as CascadeHandler
|
|
205
|
+
|
|
206
|
+
User->>Model: post.soft_delete_later!
|
|
207
|
+
Model->>DB: update_columns(deleted_at: now)
|
|
208
|
+
note over Model, DB: Root Record Immediately Hidden from .kept
|
|
209
|
+
Model->>Log: UndoLog.create!(action: :soft_delete, status: :pending)
|
|
210
|
+
Model->>Queue: SoftDeleteJob.perform_later(Post, id, log_id)
|
|
211
|
+
Model-->>User: returns UndoLog (status: :pending)
|
|
212
|
+
|
|
213
|
+
rect rgb(240, 240, 240)
|
|
214
|
+
note over Worker, Cascade: Asynchronous Worker Execution
|
|
215
|
+
Queue->>Worker: perform(Post, id, log_id)
|
|
216
|
+
Worker->>Log: update!(status: :processing)
|
|
217
|
+
Worker->>Cascade: CascadeHandler#soft_delete_associations_batched!
|
|
218
|
+
|
|
219
|
+
alt Success Path
|
|
220
|
+
Cascade->>DB: in_batches update child records & create undo_log_items
|
|
221
|
+
Worker->>Log: update!(status: :completed)
|
|
222
|
+
else Failure Path (Deadlock / Timeout / Error)
|
|
223
|
+
Cascade--xWorker: StandardError (Exception)
|
|
224
|
+
Worker->>Log: update!(status: :failed, error_message: e.message)
|
|
225
|
+
Worker-->>Queue: re-raise e (triggers queue retries & error alerts)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Asynchronous Restoration with Row-Locking & Failure Recovery (`#restore_later!`)
|
|
231
|
+
|
|
232
|
+
```mermaid
|
|
233
|
+
sequenceDiagram
|
|
234
|
+
autonumber
|
|
235
|
+
actor User
|
|
236
|
+
participant Model as Post Model
|
|
237
|
+
participant Queue as ActiveJob / Queue
|
|
238
|
+
participant Worker as RestoreJob
|
|
239
|
+
participant Log as UndoLog
|
|
240
|
+
participant Items as UndoLogItems
|
|
241
|
+
participant DB as Target Tables
|
|
242
|
+
|
|
243
|
+
User->>Model: post.restore_later!
|
|
244
|
+
Model->>Log: update!(action: :restore, status: :pending)
|
|
245
|
+
Model->>Queue: RestoreJob.perform_later(log.id)
|
|
246
|
+
Model-->>User: Enqueued Successfully
|
|
247
|
+
|
|
248
|
+
rect rgb(240, 240, 240)
|
|
249
|
+
note over Worker, DB: Row-Locked Worker Execution
|
|
250
|
+
Queue->>Worker: perform(log.id)
|
|
251
|
+
Worker->>Log: with_lock (SELECT FOR UPDATE)
|
|
252
|
+
Worker->>Log: update!(status: :processing)
|
|
253
|
+
|
|
254
|
+
alt Success Path
|
|
255
|
+
Worker->>Items: find_each(batch_size: 1000, order: :desc)
|
|
256
|
+
Items->>DB: update_columns(deleted_at: nil)
|
|
257
|
+
Worker->>Items: in_batches(of: 1000).delete_all
|
|
258
|
+
Worker->>Log: destroy!
|
|
259
|
+
else Failure Path (Lock Timeout / Exception)
|
|
260
|
+
Items--xWorker: StandardError (Exception)
|
|
261
|
+
Worker->>Log: update!(status: :failed, error_message: e.message)
|
|
262
|
+
Worker-->>Queue: re-raise e (Preserves log & items for retry/inspection)
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
```
|
|
266
|
+
|
|
187
267
|
---
|
|
188
268
|
|
|
189
269
|
## 5. Key Technical Considerations
|
|
@@ -198,6 +278,12 @@ sequenceDiagram
|
|
|
198
278
|
8. **User Attribution Transparency:** `#soft_delete!` and `#restore!` support transparent user tracking via explicit keyword arguments or ambient context resolution (`Current.user` / Thread context).
|
|
199
279
|
9. **Configured Context Enforcement:** When `current_user_method` or `current_tenant_method` is configured via `ActiveRecord::Undo.configure`, operations verify that the evaluated context is not `nil`. If any configured method evaluates to `nil`, `soft_delete!` and `restore!` immediately raise `ActiveRecord::Undo::SecurityError` to prevent unauthenticated or tenantless mutations.
|
|
200
280
|
10. **Cryptographic Restore Tokens & Single-Use Semantics:** `UndoLog#signed_token` and `ModelExtension#signed_token` generate HMAC-SHA256 signed tokens using Rails' `message_verifier(:active_record_undo)` (configured with `ActiveRecord::Undo.config.token_expires_in`). The token encapsulates the log's ID with a purpose lock (`purpose: :restore`) and timestamp expiration (`exp`). When verified via `UndoLog.find_by_signed_token`, any payload alteration, purpose mismatch, or expiration automatically invalidates the token. Furthermore, because `#restore!` destroys the `UndoLog` database record upon completion, tokens automatically provide single-use replay protection: once restored, subsequent attempts to verify the same token find no corresponding database record, resulting in a `404 Not Found`. Even with a cryptographically authentic token, tenant boundary validation (`TenantVerification`) remains strictly enforced during restoration to prevent cross-tenant attacks.
|
|
281
|
+
11. **Asynchronous Execution State & Persistent Error Logging:** `UndoLog` maintains an explicit state machine (`:pending`, `:processing`, `:completed`, `:failed`), operation indicator (`:soft_delete`, `:restore`), and `error_message`. When background workers hit unhandled exceptions, the error message is persisted directly onto `undo_logs` before re-raising the exception.
|
|
282
|
+
12. **Worker Re-raising for ActiveJob Queue Resiliency:** `SoftDeleteJob` and `RestoreJob` re-raise trapped exceptions after logging the failure state. This ensures that queue backends (Sidekiq, Solid Queue, GoodJob) register the failure, trigger configured retry strategies (`retry_on`), update worker metrics, and report error backtraces to error monitoring services (Sentry, Bugsnag, Datadog).
|
|
283
|
+
13. **Idempotent Re-entrancy & Row Locking:**
|
|
284
|
+
- `SoftDeleteJob` uses batched DFS traversal (`CascadeHandler#soft_delete_associations_batched!`) with `associated.reload.soft_deleted?` pre-checks. If a job is aborted or retried, previously processed associations are safely skipped, preventing duplicate work and redundant `undo_log_items`.
|
|
285
|
+
- `RestoreJob` employs database row-level locking (`with_lock`) to eliminate race conditions between multiple workers. Bottom-up batched restoration sets `deleted_at: nil` idempotently, and `UndoLog` / `undo_log_items` are only deleted upon complete cascade success. If restoration fails mid-run, all audit records remain intact for diagnosis or retrying.
|
|
286
|
+
14. **Frontend Polling & Zero-Downtime Status Observability:** Dedicated endpoints (`/undo/logs/:id/status` and `/undo/status/:token`) allow frontends to poll operation progress asynchronously. The returned JSON exposes execution state, action, and failure messages (`{"status": "failed", "error_message": "..."}`), while view helpers (`undo_status_badge`, `soft_delete_status_badge`, `restore_status_badge`) dynamically apply `.undo-status-failed` classes to notify users.
|
|
201
287
|
|
|
202
288
|
---
|
|
203
289
|
|
|
@@ -205,63 +291,63 @@ sequenceDiagram
|
|
|
205
291
|
|
|
206
292
|
### 6.1 `lib/active_record/undo.rb` (Entrypoint & Context)
|
|
207
293
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
294
|
+
- **`require 'active_record'` Loader Check**
|
|
295
|
+
- *Function*: Safely imports ActiveRecord. If ActiveRecord is not in the load path, it catches the `LoadError` and throws a detailed error instructing the developer to add `activerecord` to their `Gemfile`.
|
|
296
|
+
- **Loader Hook Block**
|
|
297
|
+
- *Function*: Detects if `ActiveSupport` is loaded:
|
|
298
|
+
- If present, registers `ActiveSupport.on_load(:active_record)` to inject the extension module when ActiveRecord boots.
|
|
299
|
+
- If absent (e.g. running in simple Ruby scripts), directly includes `ModelExtension` into `ActiveRecord::Base` as a fallback.
|
|
300
|
+
- **`whodunnit` / `whodunnit=`**
|
|
301
|
+
- *Function*: Thread/Fiber-safe accessor for the active actor performing deletions or restorations.
|
|
302
|
+
- **`current_tenant` / `current_tenant=`**
|
|
303
|
+
- *Function*: Thread/Fiber-safe accessor for the active tenant context.
|
|
304
|
+
- **`verify_configured_context!`**
|
|
305
|
+
- *Function*: Enforces that configured `current_user_method` and `current_tenant_method` do not return `nil`. Raises `ActiveRecord::Undo::SecurityError` if any configured method evaluates to `nil`.
|
|
306
|
+
- **`SecurityError` Exception Class**
|
|
307
|
+
- *Function*: Custom error inheriting from `ActiveRecord::Undo::Error`, raised when a cross-tenant restoration attempt, missing tenant context, or configured context violation occurs.
|
|
222
308
|
|
|
223
309
|
### 6.2 `lib/active_record/undo/engine.rb` (Rails Integration)
|
|
224
310
|
|
|
225
|
-
|
|
226
|
-
|
|
311
|
+
- **`initializer 'active_record_undo.migrations'`**
|
|
312
|
+
- *Function*: Automatically runs on Rails boot to append the gem's engine migrations directory to the host application's migrations search paths. This allows host applications to detect and run gem database migrations without needing to manually copy them into the application's workspace.
|
|
227
313
|
|
|
228
314
|
### 6.3 `lib/active_record/undo/model_extension.rb` (Model Extension Module)
|
|
229
315
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
316
|
+
- **`acts_as_undoable(column: :deleted_at)`**
|
|
317
|
+
- *Function*: Class-level DSL macro injected into models to enable soft-deletion.
|
|
318
|
+
- *Details*: Defines class-level configurations:
|
|
319
|
+
- `undoable_column`: Caches the name of the column (defaults to `:deleted_at`).
|
|
320
|
+
- `kept` scope: Returns records that are not soft-deleted (`where(column => nil)`).
|
|
321
|
+
- `soft_deleted` scope: Returns records that are soft-deleted (`where.not(column => nil)`).
|
|
322
|
+
- `expired` scope: Returns records soft-deleted before the global retention period threshold.
|
|
323
|
+
- **`soft_deleted?`**
|
|
324
|
+
- *Function*: Checks if the current record instance has been soft-deleted. Returns `true` if the configured deletion column is populated with a timestamp.
|
|
325
|
+
- **`undoable?`**
|
|
326
|
+
- *Function*: Checks if the soft-deleted record has a valid undo log entry available for restoration.
|
|
327
|
+
- *Steps*:
|
|
242
328
|
1. Returns `false` immediately if `soft_deleted?` is `false`.
|
|
243
329
|
2. Performs a lightweight SQL `EXISTS` query (`ActiveRecord::Undo::UndoLogItem.joins(:undo_log).exists?(item_type: self.class.name, item_id: id)`).
|
|
244
330
|
3. Returns `true` if both the log item and its parent `UndoLog` exist in the database.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
331
|
+
- **`undo_log` / `latest_undo_log`**
|
|
332
|
+
- *Function*: Resolves the latest `UndoLog` audit record recording the soft deletion of this model instance via `find_latest_undo_log_item`. Returns `nil` if not soft-deleted or already purged/restored.
|
|
333
|
+
- **`signed_token(expires_in: ...)` / `to_signed_token`**
|
|
334
|
+
- *Function*: Generates a cryptographic signed token using Rails' message verifier directly from the model instance for secure direct-link restorations.
|
|
335
|
+
- **`soft_delete!(whodunnit: nil, tenant: nil)`**
|
|
336
|
+
- *Function*: Starts the cascade soft-deletion sequence for the record.
|
|
337
|
+
- *Parameters*:
|
|
338
|
+
- `whodunnit`: User/Actor executing the delete (falls back to `current_user_method` or Thread/Fiber context).
|
|
339
|
+
- `tenant`: Tenant context (falls back to `current_tenant_method`, Thread/Fiber context, or record's tenant association).
|
|
340
|
+
- *Steps*:
|
|
255
341
|
1. Calls `ensure_undoable_column_exists!` to verify the database column is present.
|
|
256
342
|
2. Aborts and returns `false` if the record is already soft-deleted.
|
|
257
343
|
3. Invokes `ActiveRecord::Undo.verify_configured_context!` to enforce that configured context methods do not evaluate to `nil`.
|
|
258
344
|
4. Resolves user and tenant attributes via `AttributionHelper`.
|
|
259
345
|
5. Opens an ActiveRecord database transaction block, creates a parent `UndoLog`, and cascades deletion via `CascadeHandler`.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
346
|
+
- **`restore!(whodunnit: nil)`**
|
|
347
|
+
- *Function*: Restores the record from its soft-deleted state.
|
|
348
|
+
- *Parameters*:
|
|
349
|
+
- `whodunnit`: User/Actor executing the restore.
|
|
350
|
+
- *Steps*:
|
|
265
351
|
1. Verifies column presence via `ensure_undoable_column_exists!`.
|
|
266
352
|
2. Aborts and returns `false` if the record is not soft-deleted.
|
|
267
353
|
3. Invokes `ActiveRecord::Undo.verify_configured_context!` to enforce that configured context methods do not evaluate to `nil`.
|
|
@@ -271,64 +357,64 @@ sequenceDiagram
|
|
|
271
357
|
7. Calls `restore!` on the parent `UndoLog` (which restores the entire deleted tree).
|
|
272
358
|
8. If no log item is found, it falls back to a simple, direct restore by setting the deletion column back to `nil`.
|
|
273
359
|
9. Automatically reloads the instance attributes in-memory (using `reload`) to refresh the model state before returning `true`.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
360
|
+
- **`ensure_undoable_column_exists!` (Private)**
|
|
361
|
+
- *Function*: Asserts that the configured soft-delete column exists in the database schema table. Raises `ActiveRecord::Undo::Error` if missing.
|
|
362
|
+
- **`find_latest_undo_log_item` (Private)**
|
|
363
|
+
- *Function*: Queries `UndoLogItem` records pointing to this record, ordering by `created_at DESC` to find the most recent deletion event.
|
|
364
|
+
- **`soft_delete_cascade_internal!(timestamp, undo_log)` (Private)**
|
|
365
|
+
- *Function*: Wraps instantiation and invocation of `CascadeHandler` to encapsulate cascade traversal.
|
|
280
366
|
|
|
281
367
|
### 6.4 `lib/active_record/undo/model_extension/attribution_helper.rb` (Attribution Helper)
|
|
282
368
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
369
|
+
- **`resolve_whodunnit(whodunnit)` (Private)**
|
|
370
|
+
- *Function*: Resolves the actor executing the action in priority order: explicit parameter -> `config.current_user_method.call` -> `ActiveRecord::Undo.whodunnit` -> `nil`.
|
|
371
|
+
- **`resolve_tenant(tenant)` (Private)**
|
|
372
|
+
- *Function*: Resolves the tenant context in priority order: explicit parameter -> `config.current_tenant_method.call` -> `ActiveRecord::Undo.current_tenant` -> model instance `tenant` association -> `nil`.
|
|
373
|
+
- **`build_undo_log_attributes(whodunnit, tenant)` (Private)**
|
|
374
|
+
- *Function*: Builds attributes hash supporting polymorphic model instances and scalar IDs for both actor and tenant.
|
|
289
375
|
|
|
290
376
|
### 6.5 `lib/active_record/undo/model_extension/tenant_verification.rb` (Tenant Verification)
|
|
291
377
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
378
|
+
- **`verify_tenant_match!(undo_log)` (Private)**
|
|
379
|
+
- *Function*: Asserts that the initiating context's tenant matches the log's tenant before restoration. Raises `ActiveRecord::Undo::SecurityError` if the context tenant is missing or mismatched.
|
|
380
|
+
- **`tenant_matches?(undo_log, ctx)` (Private)**
|
|
381
|
+
- *Function*: Compares `undo_log` tenant type/id against active context, supporting polymorphic model instances and raw scalar identifiers.
|
|
296
382
|
|
|
297
383
|
### 6.6 `lib/active_record/undo/cascade_handler.rb` (Cascade Execution)
|
|
298
384
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
385
|
+
- **`initialize(record)`**
|
|
386
|
+
- *Function*: Caches the record instance to be cascade deleted.
|
|
387
|
+
- **`soft_delete_with_cascade!(timestamp, undo_log)`**
|
|
388
|
+
- *Function*: Coordinates the cascade deletion of the current record.
|
|
389
|
+
- *Steps*:
|
|
304
390
|
1. Invokes `#cascade_to_associations!` to recurse into child tables.
|
|
305
391
|
2. Invokes `#update_record_timestamps!` to mark the current record as soft-deleted.
|
|
306
392
|
3. Appends an `UndoLogItem` pointing to this record to the `UndoLog` transaction.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
393
|
+
- **`cascade_to_associations!(timestamp, undo_log)` (Private)**
|
|
394
|
+
- *Function*: Iterates over reflections retrieved by `AssociationFinder`, fetches their records, and calls `#cascade_to_record!` on each associated record.
|
|
395
|
+
- **`cascade_to_record!(associated, reflection, timestamp, undo_log)` (Private)**
|
|
396
|
+
- *Function*: Handles deletion of a single associated child record:
|
|
397
|
+
- Excludes it if it is already soft-deleted.
|
|
398
|
+
- If the child model is also configured with `acts_as_undoable`, calls its private `#soft_delete_cascade_internal!` recursively.
|
|
399
|
+
- If it is not undoable, but configured with `dependent: :destroy`, it invokes `#destroy!` to perform a hard-deletion.
|
|
314
400
|
|
|
315
401
|
### 6.7 `lib/active_record/undo/cascade_handler/association_finder.rb` (Reflections Finder)
|
|
316
402
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
403
|
+
- **`associations_to_cascade` (Private)**
|
|
404
|
+
- *Function*: Reflects on the model's association metadata and filters list of associations to select only those configured with `dependent: :destroy`, `dependent: :soft_delete`, or `dependent: :delete_all`.
|
|
405
|
+
- **`associated_records_for(reflection)` (Private)**
|
|
406
|
+
- *Function*: Fetches associated target records. Normalizes single relations and collection associations (like `has_many`) into a flat array structure.
|
|
321
407
|
|
|
322
408
|
### 6.8 `lib/active_record/undo/cascade_handler/record_updater.rb` (Timestamps Updater)
|
|
323
409
|
|
|
324
|
-
|
|
325
|
-
|
|
410
|
+
- **`update_record_timestamps!(timestamp)` (Private)**
|
|
411
|
+
- *Function*: Bypasses ActiveRecord validations, callbacks, and dirty checking to directly write updates for the soft-delete column and `updated_at` timestamps using database-level `update_columns`.
|
|
326
412
|
|
|
327
413
|
### 6.9 `lib/active_record/undo/undo_log.rb` (Batch Restoration)
|
|
328
414
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
415
|
+
- **`restore!(whodunnit: nil)`**
|
|
416
|
+
- *Function*: Triggers database restoration of the entire tree recorded under this log.
|
|
417
|
+
- *Steps*:
|
|
332
418
|
1. Invokes `ActiveRecord::Undo.verify_configured_context!` to enforce that configured context methods do not evaluate to `nil`.
|
|
333
419
|
2. Enforces tenant verification if tenant association is present on the log.
|
|
334
420
|
3. Resolves `whodunnit` actor context.
|
|
@@ -336,124 +422,171 @@ sequenceDiagram
|
|
|
336
422
|
5. Iterates over associated `undo_log_items` in *reverse order* (`reverse_each`), guaranteeing parent records are restored before child records.
|
|
337
423
|
6. Invokes `#restore_item!` on each item.
|
|
338
424
|
7. Automatically calls `#destroy!` on completion to purge the audit records (`UndoLog` and nested `UndoLogItem` rows) from the database.
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
425
|
+
- **`expired?`**
|
|
426
|
+
- *Function*: Checks whether `created_at` timestamp is older than `ActiveRecord::Undo.config.retention_period`.
|
|
427
|
+
- **`signed_token(expires_in: ActiveRecord::Undo.config.token_expires_in, purpose: :restore)` / `to_signed_token`**
|
|
428
|
+
- *Function*: Generates a cryptographically signed, URL-safe token representing this `UndoLog` for direct link restorations.
|
|
429
|
+
- *Details*: Employs `self.class.token_verifier.generate(id, purpose: purpose, expires_in: expires_in)`.
|
|
430
|
+
- **`.find_by_signed_token(token, purpose: :restore)`**
|
|
431
|
+
- *Function*: Verifies the cryptographic token signature and fetches the matching `UndoLog` database record.
|
|
432
|
+
- *Details*: Returns `nil` if the token is blank, expired, tampered with, has a mismatched purpose, or if the underlying `UndoLog` record no longer exists in the database.
|
|
433
|
+
- **`.verify_signed_token(token, purpose: :restore)`**
|
|
434
|
+
- *Function*: Decodes and verifies the token payload via `token_verifier.verified(token, purpose: purpose)`. Catches `ActiveSupport::MessageVerifier::InvalidSignature` and returns `nil` on tampering or expiration.
|
|
435
|
+
- **`.token_verifier`**
|
|
436
|
+
- *Function*: Provides the `ActiveSupport::MessageVerifier` instance used for token generation and verification. In Rails environments, utilizes `Rails.application.message_verifier(:active_record_undo)`. In standalone environments, falls back to an SHA256 verifier keyed to `ActiveRecord::Undo.config.token_secret_key`.
|
|
437
|
+
- **`.for_whodunnit(user)`**
|
|
438
|
+
- *Function*: Scopes logs to those deleted by a specific user/actor. Supports both model instances and raw IDs.
|
|
439
|
+
- **`.for_tenant(tenant)`**
|
|
440
|
+
- *Function*: Scopes logs to those belonging to a specific tenant. Supports both model instances and raw IDs.
|
|
355
441
|
|
|
356
442
|
### 6.10 `lib/active_record/undo/undo_log_item.rb` (Item Restoration)
|
|
357
443
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
444
|
+
- **`restore_item!`**
|
|
445
|
+
- *Function*: Performs restoration of the individual record referenced by the audit log item.
|
|
446
|
+
- *Steps*:
|
|
361
447
|
1. Resolves model class via `#resolve_model_class`.
|
|
362
448
|
2. Resolves target record using `unscoped.find_by(id: item_id)` (unscoping ignores default scopes filtering soft-deleted records).
|
|
363
449
|
3. Ensures the soft-delete column exists on the model table.
|
|
364
450
|
4. Resets the soft-delete column to `nil` using `#reset_soft_delete_column!`.
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
451
|
+
- **`resolve_model_class` (Private)**
|
|
452
|
+
- *Function*: Constantizes the stored `item_type` string.
|
|
453
|
+
- *Security*: Asserts that the constant is a valid class that inherits from `ActiveRecord::Base`. Raises `ActiveRecord::Undo::Error` if constantization fails or targets non-model classes.
|
|
454
|
+
- **`ensure_column_exists!(klass, column_name)` (Private)**
|
|
455
|
+
- *Function*: Confirms that the target soft-delete column exists in the class's table schema. Throws `ActiveRecord::Undo::Error` if missing.
|
|
456
|
+
- **`reset_soft_delete_column!(target, column_name)` (Private)**
|
|
457
|
+
- *Function*: Bypasses standard callbacks and validations to write a `nil` value to the soft-delete column directly in the database.
|
|
372
458
|
|
|
373
459
|
### 6.11 `lib/active_record/undo/configuration.rb` (Global Configuration)
|
|
374
460
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
461
|
+
- **`Configuration#initialize`**
|
|
462
|
+
- *Function*: Instantiates default configurations for the gem.
|
|
463
|
+
- *Details*:
|
|
464
|
+
- `@retention_period`: Defaults to `30.days`.
|
|
465
|
+
- `@current_user_method`: Defaults to `nil`. Callable proc to resolve the current user/actor.
|
|
466
|
+
- `@current_tenant_method`: Defaults to `nil`. Callable proc to resolve the current tenant.
|
|
467
|
+
- `@base_controller`: Defaults to `"::ApplicationController"`. Base controller for engine authentication and authorization hooks.
|
|
468
|
+
- `@base_job`: Defaults to `"::ActiveJob::Base"`. Base class for ActiveJob workers (`SoftDeleteJob`, `RestoreJob`, `PurgeJob`).
|
|
469
|
+
- `@default_redirect_path`: Defaults to lambda resolving `main_app.root_path` (falling back to `'/'`).
|
|
470
|
+
- `@token_expires_in`: Defaults to `24.hours`. Lifespan for signed direct-link restore tokens.
|
|
471
|
+
- `@token_secret_key`: Defaults to `nil` (uses Rails application verifier).
|
|
472
|
+
- `@error_handling`: Defaults to `:auto`. Controls whether HTML requests redirect on error or render status codes.
|
|
386
473
|
|
|
387
474
|
### 6.12 `lib/active_record/undo/purger.rb` (Hard Purging Engine)
|
|
388
475
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
476
|
+
- **`Purger.purge_expired!(batch_size: 1000)`**
|
|
477
|
+
- *Function*: Cleans up all database records and logs that are past their retention limits.
|
|
478
|
+
- *Steps*:
|
|
392
479
|
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.
|
|
393
480
|
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.
|
|
394
|
-
|
|
481
|
+
- *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.
|
|
395
482
|
|
|
396
483
|
### 6.13 `lib/active_record/undo/purger/reflection_helper.rb` (Purger Reflection Helper)
|
|
397
484
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
485
|
+
- **`nullify_reflections(model)` (Private)**
|
|
486
|
+
- *Function*: Filters model reflections for `:has_many` or `:has_one` associations configured with `dependent: :nullify` where foreign key is nullable.
|
|
487
|
+
- **`cascade_reflections(model)` (Private)**
|
|
488
|
+
- *Function*: Filters model reflections for associations requiring cascade deletion (`:destroy`, `:delete_all`, `:soft_delete`, or non-nullable `:nullify`).
|
|
489
|
+
- **`foreign_key_nullable?(ref)` (Private)**
|
|
490
|
+
- *Function*: Checks table column schema to determine if the foreign key column allows null values.
|
|
404
491
|
|
|
405
492
|
### 6.14 `lib/active_record/undo/purge_job.rb` (Background Task Worker)
|
|
406
493
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
494
|
+
- **`PurgeJob#perform(batch_size: 1000)`**
|
|
495
|
+
- *Function*: Runs inside ActiveJob (when available) to execute purging asynchronously.
|
|
496
|
+
- *Details*: Invokes `ActiveRecord::Undo::Purger.purge_expired!(batch_size: batch_size)`.
|
|
410
497
|
|
|
411
498
|
### 6.15 `lib/active_record/undo/tasks/purge.rake` (Command Line Utility)
|
|
412
499
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
500
|
+
- **`active_record_undo:purge_expired`**
|
|
501
|
+
- *Function*: Exposes CLI Rake task for the purger engine.
|
|
502
|
+
- *Details*: Checks `ENV['BATCH_SIZE']` for custom batch sizes, falling back to 1000, and triggers `ActiveRecord::Undo::Purger.purge_expired!`.
|
|
416
503
|
|
|
417
504
|
### 6.16 `lib/active_record/undo/engine.rb` (Mountable Rails Engine)
|
|
418
505
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
506
|
+
- **`ActiveRecord::Undo::Engine`**
|
|
507
|
+
- *Function*: Rails engine that defines isolated namespace `ActiveRecord::Undo` and mounts routes.
|
|
508
|
+
- *Details*:
|
|
509
|
+
- Automatically hooks view helpers into ActionView via `ActiveSupport.on_load(:action_view)`.
|
|
510
|
+
- Registers `:turbo_stream` MIME type (`text/vnd.turbo-stream.html`) when not already defined.
|
|
511
|
+
- Appends gem migrations to host app's migration paths.
|
|
425
512
|
|
|
426
513
|
### 6.17 `lib/active_record/undo/safe_redirect.rb` (Open-Redirect Prevention)
|
|
427
514
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
515
|
+
- **`determine_redirect_path` (Private)**
|
|
516
|
+
- *Function*: Determines safe target path in priority: validated `params[:redirect_to]`, validated `request.referer`, or `resolve_fallback_path`.
|
|
517
|
+
- **`safe_redirect_path(path)` (Private)**
|
|
518
|
+
- *Function*: Validates URL to strictly allow relative paths or same-host/port absolute URLs, preventing open redirect vulnerabilities. Automatically rejects CRLF characters (`\r`, `\n`) and non-HTTP/HTTPS schemes (e.g. `javascript:`, `data:`).
|
|
432
519
|
|
|
433
520
|
### 6.18 `lib/active_record/undo/view_helpers.rb` (ERB View Helpers)
|
|
434
521
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
522
|
+
- **`undo_button_to(target, text = nil, signed: false, **html_options)`**
|
|
523
|
+
- *Function*: Renders a `button_to` form targeting `active_record_undo.restore_log_path` or `signed_restore_path`.
|
|
524
|
+
- **`undo_link_to(target, text = nil, signed: false, **html_options)`**
|
|
525
|
+
- *Function*: Renders an anchor tag configured with `data: { turbo_method: :post }` for Turbo and UJS compatibility.
|
|
526
|
+
- **`undo_status_badge(target, action: :auto, **html_options)`**
|
|
527
|
+
- *Function*: Renders an HTML `<span>` badge displaying execution state (`Pending`, `Processing`, `Completed`, `Failed`). Auto-detects whether the target model or undo log is in `restore` or `soft_delete` progress.
|
|
528
|
+
- **`soft_delete_status_badge(target, **html_options)`**
|
|
529
|
+
- *Function*: Explicitly renders an execution status badge for an asynchronous soft delete operation.
|
|
530
|
+
- **`restore_status_badge(target, **html_options)`**
|
|
531
|
+
- *Function*: Explicitly renders an execution status badge for an asynchronous restore operation.
|
|
532
|
+
- **`extract_log_target(target)` (Private)**
|
|
533
|
+
- *Function*: Safely extracts the target `UndoLog`. When passed an ActiveRecord model, verifies that the model is soft-deleted and has an associated undo log, raising `ArgumentError` if active or unpersisted to prevent erroneous routing with the model's primary key.
|
|
441
534
|
|
|
442
535
|
### 6.19 `app/controllers/active_record/undo/application_controller.rb` (Engine Base Controller)
|
|
443
536
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
537
|
+
- **`ApplicationController`**
|
|
538
|
+
- *Function*: Inherits from `ActiveRecord::Undo.base_controller_class` with CSRF protection enabled.
|
|
539
|
+
- *Details*: Handles tenant resolution, whodunnit resolution, open redirect verification, and graceful error responses (HTML, Turbo Stream, JSON).
|
|
447
540
|
|
|
448
541
|
### 6.20 `app/controllers/active_record/undo/logs_controller.rb` & `restores_controller.rb` (HTTP Endpoints)
|
|
449
542
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
543
|
+
- **`LogsController#restore`**
|
|
544
|
+
- *Function*: Finds undo log by ID or signed token, checks expiration and permissions, executes restoration (synchronously or asynchronously if `params[:async]` is truthy), and returns format-specific response (emitting `303 See Other` on HTML redirect for Turbo Drive compatibility, or `202 Accepted` on async dispatch).
|
|
545
|
+
- **`LogsController#status`**
|
|
546
|
+
- *Function*: Returns JSON payload displaying current asynchronous execution status (`pending`, `processing`, `completed`, `failed`), action (`soft_delete`, `restore`), completion booleans, and any persistent `error_message`.
|
|
547
|
+
- **`LogsController#find_undo_log` (Private)**
|
|
548
|
+
- *Function*: Resolves the target `UndoLog` from request parameters. If `params[:token]` is present, calls `UndoLog.find_by_signed_token(params[:token])`. If `params[:id]` is present, delegates to `find_by_id_or_token`.
|
|
549
|
+
- **`LogsController#find_by_id_or_token(identifier)` (Private)**
|
|
550
|
+
- *Function*: Dual-lookup resolver. If `identifier` consists purely of numeric digits or matches a UUID format, fetches via `UndoLog.find_by(id: identifier)`. For all other string formats, safely routes to `UndoLog.find_by_signed_token(identifier)` without triggering database type exceptions.
|
|
551
|
+
- **`LogsController#execute_restore_action` (Private)**
|
|
552
|
+
- *Function*: Wraps `@undo_log.restore!(whodunnit: resolve_whodunnit_user)` inside tenant context (`with_tenant_context`) to enforce multi-tenant security during restoration.
|
|
553
|
+
- **`RestoresController#create`**
|
|
554
|
+
- *Function*: Dedicated HTTP endpoint for signed token restorations (`POST /undo/restore/:token`). Inherits from `LogsController` and executes `#restore`.
|
|
555
|
+
- **`RestoresController#status`**
|
|
556
|
+
- *Function*: Dedicated HTTP endpoint for signed token status polling (`GET /undo/status/:token`). Inherits from `LogsController` and executes `#status`.
|
|
557
|
+
|
|
558
|
+
### 6.21 `app/jobs/active_record/undo/soft_delete_job.rb` (Asynchronous Soft Deletion Worker)
|
|
559
|
+
|
|
560
|
+
- **`SoftDeleteJob#perform(record_class_name, record_id, undo_log_id, whodunnit: nil, tenant: nil)`**
|
|
561
|
+
- *Function*: Executes cascading soft deletion across deep association trees in the background.
|
|
562
|
+
- *Details*:
|
|
563
|
+
- Transitions `undo_log` to `status: :processing`.
|
|
564
|
+
- Invokes `CascadeHandler#soft_delete_associations_batched!` using `in_batches` to avoid memory bloat.
|
|
565
|
+
- On completion, transitions `undo_log` to `status: :completed`.
|
|
566
|
+
- On `StandardError`, calls `handle_failure` to update `status: :failed` with `error_message`, then re-raises the exception to let ActiveJob retries and error monitoring tools trigger normally.
|
|
567
|
+
|
|
568
|
+
### 6.22 `app/jobs/active_record/undo/restore_job.rb` (Asynchronous Restoration Worker)
|
|
569
|
+
|
|
570
|
+
- **`RestoreJob#perform(undo_log_id, whodunnit: nil)`**
|
|
571
|
+
- *Function*: Restores cascaded records in reverse order (bottom-up) in batches.
|
|
572
|
+
- *Details*:
|
|
573
|
+
- Acquires database row lock via `undo_log.with_lock` (`SELECT FOR UPDATE`) to prevent concurrent worker races.
|
|
574
|
+
- Transitions `undo_log` to `status: :processing`.
|
|
575
|
+
- Restores items via `undo_log_items.find_each(batch_size: 1000, order: :desc, &:restore_item!)`.
|
|
576
|
+
- Cleans up log items in batches (`delete_all`) and destroys the `UndoLog` upon complete cascade success.
|
|
577
|
+
- On `StandardError`, updates `status: :failed` with `error_message`, and re-raises the exception while preserving all remaining un-restored log items for safe inspection or retrying.
|
|
578
|
+
|
|
579
|
+
### 6.23 `lib/active_record/undo/status_predicates.rb` (Execution State Predicates)
|
|
580
|
+
|
|
581
|
+
- **`StatusPredicates` Concern**
|
|
582
|
+
- *Function*: Provides shared predicate helper methods across models and undo logs:
|
|
583
|
+
- `soft_delete_pending?`, `soft_delete_processing?`, `soft_delete_completed?`, `soft_delete_failed?`
|
|
584
|
+
- `restore_pending?`, `restore_processing?`, `restore_completed?`, `restore_failed?`
|
|
585
|
+
|
|
586
|
+
### 6.24 `lib/active_record/undo/model_extension/async_methods.rb` (Model Async Interface)
|
|
587
|
+
|
|
588
|
+
- **`ModelExtension::AsyncMethods` Concern**
|
|
589
|
+
- *Function*: Provides asynchronous entrypoints and status query methods on undoable ActiveRecord models:
|
|
590
|
+
- `soft_delete_later!(queue: nil, whodunnit: nil, tenant: nil)`: Updates root record immediately to hide it from `.kept` scopes, creates `UndoLog(action: :soft_delete, status: :pending)`, and dispatches `SoftDeleteJob`.
|
|
591
|
+
- `restore_later!(queue: nil, whodunnit: nil)`: Validates record state, transitions log to `status: :pending, action: :restore`, and dispatches `RestoreJob`.
|
|
592
|
+
- `soft_delete_status` & `restore_status`: Inspects the latest associated `UndoLog` to report execution state strings.
|