active_record-undo 0.1.4 → 0.2.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/DOCUMENTATION.md +226 -92
- data/README.md +402 -131
- data/app/controllers/active_record/undo/application_controller.rb +91 -0
- data/app/controllers/active_record/undo/logs_controller.rb +87 -0
- data/app/controllers/active_record/undo/restores_controller.rb +11 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20260821000000_add_tenant_and_user_attribution_to_undo_logs.rb +16 -0
- data/lib/active_record/undo/configuration.rb +10 -1
- data/lib/active_record/undo/engine.rb +14 -1
- data/lib/active_record/undo/model_extension/attribution_helper.rb +54 -0
- data/lib/active_record/undo/model_extension/tenant_verification.rb +39 -0
- data/lib/active_record/undo/model_extension.rb +61 -21
- data/lib/active_record/undo/purger/reflection_helper.rb +38 -0
- data/lib/active_record/undo/purger.rb +50 -30
- data/lib/active_record/undo/safe_redirect.rb +60 -0
- data/lib/active_record/undo/undo_log.rb +81 -2
- data/lib/active_record/undo/version.rb +1 -1
- data/lib/active_record/undo/view_helpers.rb +101 -0
- data/lib/active_record/undo.rb +53 -0
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1fc1fe96cf406b3cdb4034d8c0a20b2920b3ea685eaa67a88c59b850bf2971d8
|
|
4
|
+
data.tar.gz: cc1830bd09b2eeec06f8d27992a2b18255d6db0277751fd9c7614581f8d9d10b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d558abdcbb507970ff723a3453f35a8a37581c4aad79e605112affbddd23be71bc224a6a1f400fee76ffd7b3655eb408752c800216ab70229e02e62b883a5e8
|
|
7
|
+
data.tar.gz: d0be5d9b492aa4a6cf3bc2a46ff5aa2c83da139f069eb377c5799932e158e82ffc0e3122a47e9aca0de9eb2d9c2d4c9b58033dda90047f78f8a2f7eeb7ef209d
|
data/DOCUMENTATION.md
CHANGED
|
@@ -39,20 +39,29 @@ graph TD
|
|
|
39
39
|
|
|
40
40
|
### Component Summary
|
|
41
41
|
|
|
42
|
-
| Component
|
|
43
|
-
|
|
|
44
|
-
| **Main Hook**
|
|
45
|
-
| **Model Extension**
|
|
46
|
-
| **
|
|
47
|
-
| **
|
|
48
|
-
| **Cascade
|
|
49
|
-
| **
|
|
50
|
-
| **
|
|
51
|
-
| **
|
|
52
|
-
| **
|
|
53
|
-
| **
|
|
54
|
-
| **
|
|
55
|
-
| **
|
|
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
|
+
| **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
|
+
| **Purger Reflection Helper** | `lib/active_record/undo/purger/reflection_helper.rb` | `ReflectionHelper` | Determines cascade and nullify reflection behavior for purge routines |
|
|
57
|
+
| **Purge Job** | `lib/active_record/undo/purge_job.rb` | `PurgeJob` | ActiveJob background runner invoking Purger |
|
|
58
|
+
| **Rake Task** | `lib/active_record/undo/tasks/purge.rake` | Rake Task | Exposes `active_record_undo:purge_expired` command |
|
|
59
|
+
| **Mountable Engine** | `lib/active_record/undo/engine.rb` | `Engine` | Defines mountable Rails engine route namespace, view helper hooks, and migrations |
|
|
60
|
+
| **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` and `undo_link_to` helpers with automatic model and signed token resolution |
|
|
62
|
+
| **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 by ID or signed token for HTML, Turbo Stream, and JSON |
|
|
64
|
+
| **Signed Restore Endpoint** | `app/controllers/active_record/undo/restores_controller.rb` | `RestoresController` | Dedicated route endpoint for signed token restorations (`POST /undo/restore/:token`) |
|
|
56
65
|
|
|
57
66
|
---
|
|
58
67
|
|
|
@@ -62,9 +71,15 @@ graph TD
|
|
|
62
71
|
erDiagram
|
|
63
72
|
UNDO_LOGS ||--|{ UNDO_LOG_ITEMS : "has_many"
|
|
64
73
|
UNDO_LOG_ITEMS }|--|| TARGET_MODEL : "belongs_to (polymorphic)"
|
|
74
|
+
UNDO_LOGS }o--o| WHODUNNIT_MODEL : "belongs_to (polymorphic)"
|
|
75
|
+
UNDO_LOGS }o--o| TENANT_MODEL : "belongs_to (polymorphic)"
|
|
65
76
|
|
|
66
77
|
UNDO_LOGS {
|
|
67
78
|
bigint id PK
|
|
79
|
+
string whodunnit_type "optional"
|
|
80
|
+
bigint whodunnit_id "optional"
|
|
81
|
+
string tenant_type "optional"
|
|
82
|
+
bigint tenant_id "optional"
|
|
68
83
|
datetime created_at
|
|
69
84
|
datetime updated_at
|
|
70
85
|
}
|
|
@@ -179,147 +194,266 @@ sequenceDiagram
|
|
|
179
194
|
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.
|
|
180
195
|
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.
|
|
181
196
|
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.
|
|
197
|
+
7. **Tenant Matching Verification:** During `#restore!`, if `UndoLog` is associated with a tenant, the current tenant context is verified against the log's tenant before initiating restoration, raising `ActiveRecord::Undo::SecurityError` on mismatch to prevent cross-tenant data restores.
|
|
198
|
+
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
|
+
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
|
+
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.
|
|
182
201
|
|
|
183
202
|
---
|
|
184
203
|
|
|
185
204
|
## 6. Method-by-Method Implementation Reference
|
|
186
205
|
|
|
187
|
-
### 6.1 `lib/active_record/undo.rb` (Entrypoint)
|
|
206
|
+
### 6.1 `lib/active_record/undo.rb` (Entrypoint & Context)
|
|
188
207
|
|
|
189
208
|
* **`require 'active_record'` Loader Check**
|
|
190
|
-
|
|
209
|
+
* *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`.
|
|
191
210
|
* **Loader Hook Block**
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
* *Function*: Detects if `ActiveSupport` is loaded:
|
|
212
|
+
* If present, registers `ActiveSupport.on_load(:active_record)` to inject the extension module when ActiveRecord boots.
|
|
213
|
+
* If absent (e.g. running in simple Ruby scripts), directly includes `ModelExtension` into `ActiveRecord::Base` as a fallback.
|
|
214
|
+
* **`whodunnit` / `whodunnit=`**
|
|
215
|
+
* *Function*: Thread/Fiber-safe accessor for the active actor performing deletions or restorations.
|
|
216
|
+
* **`current_tenant` / `current_tenant=`**
|
|
217
|
+
* *Function*: Thread/Fiber-safe accessor for the active tenant context.
|
|
218
|
+
* **`verify_configured_context!`**
|
|
219
|
+
* *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`.
|
|
220
|
+
* **`SecurityError` Exception Class**
|
|
221
|
+
* *Function*: Custom error inheriting from `ActiveRecord::Undo::Error`, raised when a cross-tenant restoration attempt, missing tenant context, or configured context violation occurs.
|
|
195
222
|
|
|
196
223
|
### 6.2 `lib/active_record/undo/engine.rb` (Rails Integration)
|
|
197
224
|
|
|
198
225
|
* **`initializer 'active_record_undo.migrations'`**
|
|
199
|
-
|
|
226
|
+
* *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.
|
|
200
227
|
|
|
201
228
|
### 6.3 `lib/active_record/undo/model_extension.rb` (Model Extension Module)
|
|
202
229
|
|
|
203
230
|
* **`acts_as_undoable(column: :deleted_at)`**
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
231
|
+
* *Function*: Class-level DSL macro injected into models to enable soft-deletion.
|
|
232
|
+
* *Details*: Defines class-level configurations:
|
|
233
|
+
* `undoable_column`: Caches the name of the column (defaults to `:deleted_at`).
|
|
234
|
+
* `kept` scope: Returns records that are not soft-deleted (`where(column => nil)`).
|
|
235
|
+
* `soft_deleted` scope: Returns records that are soft-deleted (`where.not(column => nil)`).
|
|
236
|
+
* `expired` scope: Returns records soft-deleted before the global retention period threshold.
|
|
210
237
|
* **`soft_deleted?`**
|
|
211
|
-
|
|
238
|
+
* *Function*: Checks if the current record instance has been soft-deleted. Returns `true` if the configured deletion column is populated with a timestamp.
|
|
212
239
|
* **`undoable?`**
|
|
213
|
-
|
|
214
|
-
|
|
240
|
+
* *Function*: Checks if the soft-deleted record has a valid undo log entry available for restoration.
|
|
241
|
+
* *Steps*:
|
|
215
242
|
1. Returns `false` immediately if `soft_deleted?` is `false`.
|
|
216
243
|
2. Performs a lightweight SQL `EXISTS` query (`ActiveRecord::Undo::UndoLogItem.joins(:undo_log).exists?(item_type: self.class.name, item_id: id)`).
|
|
217
244
|
3. Returns `true` if both the log item and its parent `UndoLog` exist in the database.
|
|
218
|
-
* **`
|
|
219
|
-
|
|
220
|
-
|
|
245
|
+
* **`undo_log` / `latest_undo_log`**
|
|
246
|
+
* *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.
|
|
247
|
+
* **`signed_token(expires_in: ...)` / `to_signed_token`**
|
|
248
|
+
* *Function*: Generates a cryptographic signed token using Rails' message verifier directly from the model instance for secure direct-link restorations.
|
|
249
|
+
* **`soft_delete!(whodunnit: nil, tenant: nil)`**
|
|
250
|
+
* *Function*: Starts the cascade soft-deletion sequence for the record.
|
|
251
|
+
* *Parameters*:
|
|
252
|
+
* `whodunnit`: User/Actor executing the delete (falls back to `current_user_method` or Thread/Fiber context).
|
|
253
|
+
* `tenant`: Tenant context (falls back to `current_tenant_method`, Thread/Fiber context, or record's tenant association).
|
|
254
|
+
* *Steps*:
|
|
221
255
|
1. Calls `ensure_undoable_column_exists!` to verify the database column is present.
|
|
222
256
|
2. Aborts and returns `false` if the record is already soft-deleted.
|
|
223
|
-
3.
|
|
224
|
-
4.
|
|
225
|
-
5.
|
|
226
|
-
|
|
227
|
-
*
|
|
228
|
-
|
|
229
|
-
|
|
257
|
+
3. Invokes `ActiveRecord::Undo.verify_configured_context!` to enforce that configured context methods do not evaluate to `nil`.
|
|
258
|
+
4. Resolves user and tenant attributes via `AttributionHelper`.
|
|
259
|
+
5. Opens an ActiveRecord database transaction block, creates a parent `UndoLog`, and cascades deletion via `CascadeHandler`.
|
|
260
|
+
* **`restore!(whodunnit: nil)`**
|
|
261
|
+
* *Function*: Restores the record from its soft-deleted state.
|
|
262
|
+
* *Parameters*:
|
|
263
|
+
* `whodunnit`: User/Actor executing the restore.
|
|
264
|
+
* *Steps*:
|
|
230
265
|
1. Verifies column presence via `ensure_undoable_column_exists!`.
|
|
231
266
|
2. Aborts and returns `false` if the record is not soft-deleted.
|
|
232
|
-
3.
|
|
233
|
-
4.
|
|
234
|
-
5. If
|
|
235
|
-
6.
|
|
267
|
+
3. Invokes `ActiveRecord::Undo.verify_configured_context!` to enforce that configured context methods do not evaluate to `nil`.
|
|
268
|
+
4. Resolves the latest `UndoLogItem` that records the soft-deletion of this instance.
|
|
269
|
+
5. If a log item is found, verifies that the current tenant context matches the log's tenant via `TenantVerification` (raises `ActiveRecord::Undo::SecurityError` if mismatched).
|
|
270
|
+
6. Attributes the restoration transaction context to `whodunnit`.
|
|
271
|
+
7. Calls `restore!` on the parent `UndoLog` (which restores the entire deleted tree).
|
|
272
|
+
8. If no log item is found, it falls back to a simple, direct restore by setting the deletion column back to `nil`.
|
|
273
|
+
9. Automatically reloads the instance attributes in-memory (using `reload`) to refresh the model state before returning `true`.
|
|
236
274
|
* **`ensure_undoable_column_exists!` (Private)**
|
|
237
|
-
|
|
275
|
+
* *Function*: Asserts that the configured soft-delete column exists in the database schema table. Raises `ActiveRecord::Undo::Error` if missing.
|
|
238
276
|
* **`find_latest_undo_log_item` (Private)**
|
|
239
|
-
|
|
277
|
+
* *Function*: Queries `UndoLogItem` records pointing to this record, ordering by `created_at DESC` to find the most recent deletion event.
|
|
240
278
|
* **`soft_delete_cascade_internal!(timestamp, undo_log)` (Private)**
|
|
241
|
-
|
|
279
|
+
* *Function*: Wraps instantiation and invocation of `CascadeHandler` to encapsulate cascade traversal.
|
|
242
280
|
|
|
243
|
-
### 6.4 `lib/active_record/undo/
|
|
281
|
+
### 6.4 `lib/active_record/undo/model_extension/attribution_helper.rb` (Attribution Helper)
|
|
282
|
+
|
|
283
|
+
* **`resolve_whodunnit(whodunnit)` (Private)**
|
|
284
|
+
* *Function*: Resolves the actor executing the action in priority order: explicit parameter -> `config.current_user_method.call` -> `ActiveRecord::Undo.whodunnit` -> `nil`.
|
|
285
|
+
* **`resolve_tenant(tenant)` (Private)**
|
|
286
|
+
* *Function*: Resolves the tenant context in priority order: explicit parameter -> `config.current_tenant_method.call` -> `ActiveRecord::Undo.current_tenant` -> model instance `tenant` association -> `nil`.
|
|
287
|
+
* **`build_undo_log_attributes(whodunnit, tenant)` (Private)**
|
|
288
|
+
* *Function*: Builds attributes hash supporting polymorphic model instances and scalar IDs for both actor and tenant.
|
|
289
|
+
|
|
290
|
+
### 6.5 `lib/active_record/undo/model_extension/tenant_verification.rb` (Tenant Verification)
|
|
291
|
+
|
|
292
|
+
* **`verify_tenant_match!(undo_log)` (Private)**
|
|
293
|
+
* *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.
|
|
294
|
+
* **`tenant_matches?(undo_log, ctx)` (Private)**
|
|
295
|
+
* *Function*: Compares `undo_log` tenant type/id against active context, supporting polymorphic model instances and raw scalar identifiers.
|
|
296
|
+
|
|
297
|
+
### 6.6 `lib/active_record/undo/cascade_handler.rb` (Cascade Execution)
|
|
244
298
|
|
|
245
299
|
* **`initialize(record)`**
|
|
246
|
-
|
|
300
|
+
* *Function*: Caches the record instance to be cascade deleted.
|
|
247
301
|
* **`soft_delete_with_cascade!(timestamp, undo_log)`**
|
|
248
|
-
|
|
249
|
-
|
|
302
|
+
* *Function*: Coordinates the cascade deletion of the current record.
|
|
303
|
+
* *Steps*:
|
|
250
304
|
1. Invokes `#cascade_to_associations!` to recurse into child tables.
|
|
251
305
|
2. Invokes `#update_record_timestamps!` to mark the current record as soft-deleted.
|
|
252
306
|
3. Appends an `UndoLogItem` pointing to this record to the `UndoLog` transaction.
|
|
253
307
|
* **`cascade_to_associations!(timestamp, undo_log)` (Private)**
|
|
254
|
-
|
|
308
|
+
* *Function*: Iterates over reflections retrieved by `AssociationFinder`, fetches their records, and calls `#cascade_to_record!` on each associated record.
|
|
255
309
|
* **`cascade_to_record!(associated, reflection, timestamp, undo_log)` (Private)**
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
310
|
+
* *Function*: Handles deletion of a single associated child record:
|
|
311
|
+
* Excludes it if it is already soft-deleted.
|
|
312
|
+
* If the child model is also configured with `acts_as_undoable`, calls its private `#soft_delete_cascade_internal!` recursively.
|
|
313
|
+
* If it is not undoable, but configured with `dependent: :destroy`, it invokes `#destroy!` to perform a hard-deletion.
|
|
260
314
|
|
|
261
|
-
### 6.
|
|
315
|
+
### 6.7 `lib/active_record/undo/cascade_handler/association_finder.rb` (Reflections Finder)
|
|
262
316
|
|
|
263
317
|
* **`associations_to_cascade` (Private)**
|
|
264
|
-
|
|
318
|
+
* *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`.
|
|
265
319
|
* **`associated_records_for(reflection)` (Private)**
|
|
266
|
-
|
|
320
|
+
* *Function*: Fetches associated target records. Normalizes single relations and collection associations (like `has_many`) into a flat array structure.
|
|
267
321
|
|
|
268
|
-
### 6.
|
|
322
|
+
### 6.8 `lib/active_record/undo/cascade_handler/record_updater.rb` (Timestamps Updater)
|
|
269
323
|
|
|
270
324
|
* **`update_record_timestamps!(timestamp)` (Private)**
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
### 6.
|
|
274
|
-
|
|
275
|
-
* **`restore
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
1.
|
|
279
|
-
2.
|
|
280
|
-
3.
|
|
281
|
-
4.
|
|
282
|
-
|
|
283
|
-
|
|
325
|
+
* *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
|
+
|
|
327
|
+
### 6.9 `lib/active_record/undo/undo_log.rb` (Batch Restoration)
|
|
328
|
+
|
|
329
|
+
* **`restore!(whodunnit: nil)`**
|
|
330
|
+
* *Function*: Triggers database restoration of the entire tree recorded under this log.
|
|
331
|
+
* *Steps*:
|
|
332
|
+
1. Invokes `ActiveRecord::Undo.verify_configured_context!` to enforce that configured context methods do not evaluate to `nil`.
|
|
333
|
+
2. Enforces tenant verification if tenant association is present on the log.
|
|
334
|
+
3. Resolves `whodunnit` actor context.
|
|
335
|
+
4. Opens a database transaction block.
|
|
336
|
+
5. Iterates over associated `undo_log_items` in *reverse order* (`reverse_each`), guaranteeing parent records are restored before child records.
|
|
337
|
+
6. Invokes `#restore_item!` on each item.
|
|
338
|
+
7. Automatically calls `#destroy!` on completion to purge the audit records (`UndoLog` and nested `UndoLogItem` rows) from the database.
|
|
339
|
+
* **`expired?`**
|
|
340
|
+
* *Function*: Checks whether `created_at` timestamp is older than `ActiveRecord::Undo.config.retention_period`.
|
|
341
|
+
* **`signed_token(expires_in: ActiveRecord::Undo.config.token_expires_in, purpose: :restore)` / `to_signed_token`**
|
|
342
|
+
* *Function*: Generates a cryptographically signed, URL-safe token representing this `UndoLog` for direct link restorations.
|
|
343
|
+
* *Details*: Employs `self.class.token_verifier.generate(id, purpose: purpose, expires_in: expires_in)`.
|
|
344
|
+
* **`.find_by_signed_token(token, purpose: :restore)`**
|
|
345
|
+
* *Function*: Verifies the cryptographic token signature and fetches the matching `UndoLog` database record.
|
|
346
|
+
* *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.
|
|
347
|
+
* **`.verify_signed_token(token, purpose: :restore)`**
|
|
348
|
+
* *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.
|
|
349
|
+
* **`.token_verifier`**
|
|
350
|
+
* *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`.
|
|
351
|
+
* **`.for_whodunnit(user)`**
|
|
352
|
+
* *Function*: Scopes logs to those deleted by a specific user/actor. Supports both model instances and raw IDs.
|
|
353
|
+
* **`.for_tenant(tenant)`**
|
|
354
|
+
* *Function*: Scopes logs to those belonging to a specific tenant. Supports both model instances and raw IDs.
|
|
355
|
+
|
|
356
|
+
### 6.10 `lib/active_record/undo/undo_log_item.rb` (Item Restoration)
|
|
284
357
|
|
|
285
358
|
* **`restore_item!`**
|
|
286
|
-
|
|
287
|
-
|
|
359
|
+
* *Function*: Performs restoration of the individual record referenced by the audit log item.
|
|
360
|
+
* *Steps*:
|
|
288
361
|
1. Resolves model class via `#resolve_model_class`.
|
|
289
362
|
2. Resolves target record using `unscoped.find_by(id: item_id)` (unscoping ignores default scopes filtering soft-deleted records).
|
|
290
363
|
3. Ensures the soft-delete column exists on the model table.
|
|
291
364
|
4. Resets the soft-delete column to `nil` using `#reset_soft_delete_column!`.
|
|
292
365
|
* **`resolve_model_class` (Private)**
|
|
293
|
-
|
|
294
|
-
|
|
366
|
+
* *Function*: Constantizes the stored `item_type` string.
|
|
367
|
+
* *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.
|
|
295
368
|
* **`ensure_column_exists!(klass, column_name)` (Private)**
|
|
296
|
-
|
|
369
|
+
* *Function*: Confirms that the target soft-delete column exists in the class's table schema. Throws `ActiveRecord::Undo::Error` if missing.
|
|
297
370
|
* **`reset_soft_delete_column!(target, column_name)` (Private)**
|
|
298
|
-
|
|
371
|
+
* *Function*: Bypasses standard callbacks and validations to write a `nil` value to the soft-delete column directly in the database.
|
|
299
372
|
|
|
300
|
-
### 6.
|
|
373
|
+
### 6.11 `lib/active_record/undo/configuration.rb` (Global Configuration)
|
|
301
374
|
|
|
302
375
|
* **`Configuration#initialize`**
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
376
|
+
* *Function*: Instantiates default configurations for the gem.
|
|
377
|
+
* *Details*:
|
|
378
|
+
* `@retention_period`: Defaults to `30.days`.
|
|
379
|
+
* `@current_user_method`: Defaults to `nil`. Callable proc to resolve the current user/actor.
|
|
380
|
+
* `@current_tenant_method`: Defaults to `nil`. Callable proc to resolve the current tenant.
|
|
381
|
+
* `@base_controller`: Defaults to `"::ApplicationController"`. Base controller for engine authentication and authorization hooks.
|
|
382
|
+
* `@default_redirect_path`: Defaults to lambda resolving `main_app.root_path` (falling back to `'/'`).
|
|
383
|
+
* `@token_expires_in`: Defaults to `24.hours`. Lifespan for signed direct-link restore tokens.
|
|
384
|
+
* `@token_secret_key`: Defaults to `nil` (uses Rails application verifier).
|
|
385
|
+
* `@error_handling`: Defaults to `:auto`. Controls whether HTML requests redirect on error or render status codes.
|
|
386
|
+
|
|
387
|
+
### 6.12 `lib/active_record/undo/purger.rb` (Hard Purging Engine)
|
|
307
388
|
|
|
308
389
|
* **`Purger.purge_expired!(batch_size: 1000)`**
|
|
309
|
-
|
|
310
|
-
|
|
390
|
+
* *Function*: Cleans up all database records and logs that are past their retention limits.
|
|
391
|
+
* *Steps*:
|
|
311
392
|
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
393
|
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
|
-
|
|
394
|
+
* *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
|
+
|
|
396
|
+
### 6.13 `lib/active_record/undo/purger/reflection_helper.rb` (Purger Reflection Helper)
|
|
397
|
+
|
|
398
|
+
* **`nullify_reflections(model)` (Private)**
|
|
399
|
+
* *Function*: Filters model reflections for `:has_many` or `:has_one` associations configured with `dependent: :nullify` where foreign key is nullable.
|
|
400
|
+
* **`cascade_reflections(model)` (Private)**
|
|
401
|
+
* *Function*: Filters model reflections for associations requiring cascade deletion (`:destroy`, `:delete_all`, `:soft_delete`, or non-nullable `:nullify`).
|
|
402
|
+
* **`foreign_key_nullable?(ref)` (Private)**
|
|
403
|
+
* *Function*: Checks table column schema to determine if the foreign key column allows null values.
|
|
314
404
|
|
|
315
|
-
### 6.
|
|
405
|
+
### 6.14 `lib/active_record/undo/purge_job.rb` (Background Task Worker)
|
|
316
406
|
|
|
317
407
|
* **`PurgeJob#perform(batch_size: 1000)`**
|
|
318
|
-
|
|
319
|
-
|
|
408
|
+
* *Function*: Runs inside ActiveJob (when available) to execute purging asynchronously.
|
|
409
|
+
* *Details*: Invokes `ActiveRecord::Undo::Purger.purge_expired!(batch_size: batch_size)`.
|
|
320
410
|
|
|
321
|
-
### 6.
|
|
411
|
+
### 6.15 `lib/active_record/undo/tasks/purge.rake` (Command Line Utility)
|
|
322
412
|
|
|
323
413
|
* **`active_record_undo:purge_expired`**
|
|
324
|
-
|
|
325
|
-
|
|
414
|
+
* *Function*: Exposes CLI Rake task for the purger engine.
|
|
415
|
+
* *Details*: Checks `ENV['BATCH_SIZE']` for custom batch sizes, falling back to 1000, and triggers `ActiveRecord::Undo::Purger.purge_expired!`.
|
|
416
|
+
|
|
417
|
+
### 6.16 `lib/active_record/undo/engine.rb` (Mountable Rails Engine)
|
|
418
|
+
|
|
419
|
+
* **`ActiveRecord::Undo::Engine`**
|
|
420
|
+
* *Function*: Rails engine that defines isolated namespace `ActiveRecord::Undo` and mounts routes.
|
|
421
|
+
* *Details*:
|
|
422
|
+
* Automatically hooks view helpers into ActionView via `ActiveSupport.on_load(:action_view)`.
|
|
423
|
+
* Registers `:turbo_stream` MIME type (`text/vnd.turbo-stream.html`) when not already defined.
|
|
424
|
+
* Appends gem migrations to host app's migration paths.
|
|
425
|
+
|
|
426
|
+
### 6.17 `lib/active_record/undo/safe_redirect.rb` (Open-Redirect Prevention)
|
|
427
|
+
|
|
428
|
+
* **`determine_redirect_path` (Private)**
|
|
429
|
+
* *Function*: Determines safe target path in priority: validated `params[:redirect_to]`, validated `request.referer`, or `resolve_fallback_path`.
|
|
430
|
+
* **`safe_redirect_path(path)` (Private)**
|
|
431
|
+
* *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
|
+
|
|
433
|
+
### 6.18 `lib/active_record/undo/view_helpers.rb` (ERB View Helpers)
|
|
434
|
+
|
|
435
|
+
* **`undo_button_to(target, text = nil, signed: false, **html_options)`**
|
|
436
|
+
* *Function*: Renders a `button_to` form targeting `active_record_undo.restore_log_path` or `signed_restore_path`.
|
|
437
|
+
* **`undo_link_to(target, text = nil, signed: false, **html_options)`**
|
|
438
|
+
* *Function*: Renders an anchor tag configured with `data: { turbo_method: :post }` for Turbo and UJS compatibility.
|
|
439
|
+
* **`extract_log_target(target)` (Private)**
|
|
440
|
+
* *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
|
+
|
|
442
|
+
### 6.19 `app/controllers/active_record/undo/application_controller.rb` (Engine Base Controller)
|
|
443
|
+
|
|
444
|
+
* **`ApplicationController`**
|
|
445
|
+
* *Function*: Inherits from `ActiveRecord::Undo.base_controller_class` with CSRF protection enabled.
|
|
446
|
+
* *Details*: Handles tenant resolution, whodunnit resolution, open redirect verification, and graceful error responses (HTML, Turbo Stream, JSON).
|
|
447
|
+
|
|
448
|
+
### 6.20 `app/controllers/active_record/undo/logs_controller.rb` & `restores_controller.rb` (HTTP Endpoints)
|
|
449
|
+
|
|
450
|
+
* **`LogsController#restore`**
|
|
451
|
+
* *Function*: Finds undo log by ID or signed token, checks expiration and permissions, executes restoration, and returns format-specific response (emitting `303 See Other` on HTML redirect for Turbo Drive compatibility).
|
|
452
|
+
* **`LogsController#find_undo_log` (Private)**
|
|
453
|
+
* *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`.
|
|
454
|
+
* **`LogsController#find_by_id_or_token(identifier)` (Private)**
|
|
455
|
+
* *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.
|
|
456
|
+
* **`LogsController#execute_restore_action` (Private)**
|
|
457
|
+
* *Function*: Wraps `@undo_log.restore!(whodunnit: resolve_whodunnit_user)` inside tenant context (`with_tenant_context`) to enforce multi-tenant security during restoration.
|
|
458
|
+
* **`RestoresController#create`**
|
|
459
|
+
* *Function*: Dedicated HTTP endpoint for signed token restorations (`POST /undo/restore/:token`). Inherits from `LogsController` and executes `#restore`.
|