active_record-undo 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c421e5d58b60163d2abd5d6acc9033b9a96dbcdc0cd628b49d30bc0e0631d88c
4
+ data.tar.gz: f283ee35d356473309b5fbbcff68793765a4e307e31cbdcc82b5bd0c91b312d5
5
+ SHA512:
6
+ metadata.gz: 0dfc203a6c9a27028cdddcdbec738a680ea177c3ada73c8e46f3ad836aa2c95402b985f78003ce74f1b421bf3ee84ee7e821ee2350d6eeb5fa5f03b15998fa13
7
+ data.tar.gz: d2d46f0384fbe4a0fe0ff8fd654732ca52405c90cd511eff2b9148a7b9aad83be94462927bfa46152ed3b07ecb1cb9015a8dc4804f96add8468d394ca8a03ff7
data/.rubocop.yml ADDED
@@ -0,0 +1,44 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ SuggestExtensions: false
4
+ Exclude:
5
+ - 'bin/**/*'
6
+ - 'db/schema.rb'
7
+ - 'spec/internal/**/*'
8
+ - 'vendor/**/*'
9
+
10
+ Style/Documentation:
11
+ Enabled: false
12
+
13
+ Style/FrozenStringLiteralComment:
14
+ Enabled: true
15
+
16
+ Layout/LineLength:
17
+ Max: 120
18
+
19
+ Gemspec/DevelopmentDependencies:
20
+ Enabled: false
21
+
22
+ Metrics/BlockLength:
23
+ Exclude:
24
+ - 'spec/**/*'
25
+ - 'active_record-undo.gemspec'
26
+
27
+ Naming/VariableNumber:
28
+ Enabled: false
29
+
30
+ Metrics/MethodLength:
31
+ Max: 10
32
+
33
+ Metrics/AbcSize:
34
+ Max: 15
35
+
36
+ Metrics/CyclomaticComplexity:
37
+ Max: 7
38
+ Exclude:
39
+ - 'db/migrate/**/*'
40
+
41
+ Metrics/PerceivedComplexity:
42
+ Max: 7
43
+ Exclude:
44
+ - 'db/migrate/**/*'
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-08-08
4
+
5
+ - Initial release
@@ -0,0 +1,132 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, caste, color, religion, or sexual
10
+ identity and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the overall
26
+ community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or advances of
31
+ any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email address,
35
+ without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official email address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ [INSERT CONTACT METHOD].
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series of
86
+ actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or permanent
93
+ ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within the
113
+ community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.1, available at
119
+ [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120
+
121
+ Community Impact Guidelines were inspired by
122
+ [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123
+
124
+ For answers to common questions about this code of conduct, see the FAQ at
125
+ [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126
+ [https://www.contributor-covenant.org/translations][translations].
127
+
128
+ [homepage]: https://www.contributor-covenant.org
129
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130
+ [Mozilla CoC]: https://github.com/mozilla/diversity
131
+ [FAQ]: https://www.contributor-covenant.org/faq
132
+ [translations]: https://www.contributor-covenant.org/translations
data/DOCUMENTATION.md ADDED
@@ -0,0 +1,261 @@
1
+ # ActiveRecord::Undo Architecture & Technical Documentation
2
+
3
+ `ActiveRecord::Undo` is a Rails Engine gem providing transactional, cascade-aware soft deletion and restoration capabilities for ActiveRecord models. It records state transitions into a dedicated polymorphic audit structure within single database transactions.
4
+
5
+ ---
6
+
7
+ ## 1. System Architecture Overview
8
+
9
+ ```mermaid
10
+ graph TD
11
+ SubGraphHostApp[Host Rails Application Models] -->|acts_as_undoable| ModelExt[ActiveRecord::Undo::ModelExtension]
12
+
13
+ subgraph GemCore[ActiveRecord::Undo Core Engine]
14
+ ModelExt -->|Calls soft_delete!| TxBoundary[ActiveRecord::Base.transaction]
15
+
16
+ subgraph TxBoundary
17
+ CreateLog[Create UndoLog Parent]
18
+ CreateLog --> Cascade[CascadeHandler Engine]
19
+ Cascade -->|Reflects Associations| AssociationLoop{Has Dependent Relations?}
20
+ AssociationLoop -->|Yes| RecursiveCall[Recurse soft_delete_cascade_internal!]
21
+ AssociationLoop -->|No / Completed| UpdateCol[update_columns timestamp]
22
+ UpdateCol --> AppendItem[Build UndoLogItem Record]
23
+ end
24
+
25
+ TxBoundary --> LogResult[Return UndoLog Instance]
26
+ end
27
+
28
+ subgraph DatabaseStorage[Persistence Layer]
29
+ AppendItem --> UndoLogTable[(undo_logs)]
30
+ AppendItem --> UndoItemTable[(undo_log_items)]
31
+ end
32
+ ```
33
+
34
+ ---
35
+
36
+ ## 2. Core Components & Responsibilities
37
+
38
+ ### Component Summary
39
+
40
+ | Component | File Path | Class / Module | Core Responsibility |
41
+ | :--- | :--- | :--- | :--- |
42
+ | **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!`) |
44
+ | **Cascade Engine** | `lib/active_record/undo/cascade_handler.rb` | `CascadeHandler` | Inspects ActiveRecord reflections (`reflections`) and executes DFS traversal |
45
+ | **Cascade Association Finder** | `lib/active_record/undo/cascade_handler/association_finder.rb` | `AssociationFinder` | Resolves which records should cascade based on dependency configuration |
46
+ | **Cascade Record Updater** | `lib/active_record/undo/cascade_handler/record_updater.rb` | `RecordUpdater` | Updates the database timestamps directly bypassing callbacks |
47
+ | **Audit Log Parent** | `lib/active_record/undo/undo_log.rb` | `UndoLog` | Represents the top-level deletion event and manages atomic batch restoration |
48
+ | **Audit Log Child** | `lib/active_record/undo/undo_log_item.rb` | `UndoLogItem` | Maps polymorphic targets (`item_type`, `item_id`) to original deleted entities |
49
+ | **Engine Link** | `lib/active_record/undo/engine.rb` | `Engine` | Appends `db/migrate/` directly to host app migration paths |
50
+
51
+ ---
52
+
53
+ ## 3. Data Model & Database Design
54
+
55
+ ```mermaid
56
+ erDiagram
57
+ UNDO_LOGS ||--|{ UNDO_LOG_ITEMS : "has_many"
58
+ UNDO_LOG_ITEMS }|--|| TARGET_MODEL : "belongs_to (polymorphic)"
59
+
60
+ UNDO_LOGS {
61
+ bigint id PK
62
+ datetime created_at
63
+ datetime updated_at
64
+ }
65
+
66
+ UNDO_LOG_ITEMS {
67
+ bigint id PK
68
+ bigint undo_log_id FK
69
+ string item_type
70
+ bigint item_id
71
+ datetime created_at
72
+ datetime updated_at
73
+ }
74
+
75
+ TARGET_MODEL {
76
+ bigint id PK
77
+ datetime deleted_at "or custom column"
78
+ }
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 4. Sequence Diagrams
84
+
85
+ ### Soft Deletion Flow (`#soft_delete!`)
86
+
87
+ ```mermaid
88
+ sequenceDiagram
89
+ autonumber
90
+ actor User
91
+ participant Model as Post Model
92
+ participant Ext as ModelExtension
93
+ participant Tx as DB Transaction
94
+ participant Log as UndoLog
95
+ participant Cascade as CascadeHandler
96
+ participant Child as Comment Model
97
+
98
+ User->>Model: post.soft_delete!
99
+ Model->>Ext: Check soft_deleted?
100
+ Ext-->>Model: false
101
+ Model->>Tx: Open ActiveRecord::Base.transaction
102
+ Tx->>Log: UndoLog.create!
103
+ Tx->>Cascade: CascadeHandler.new(post).soft_delete_with_cascade!
104
+
105
+ rect rgb(240, 240, 240)
106
+ note over Cascade, Child: Dynamic Association Reflection
107
+ Cascade->>Cascade: Inspect Post.reflections (:comments)
108
+ Cascade->>Child: Recurse soft_delete_cascade_internal!
109
+ Child->>Child: update_columns(deleted_at: timestamp)
110
+ Child->>Log: undo_log_items.build(item: comment_101)
111
+ end
112
+
113
+ Cascade->>Model: update_columns(deleted_at: timestamp)
114
+ Cascade->>Log: undo_log_items.build(item: post)
115
+ Log->>Tx: undo_log.save!
116
+ Tx-->>Model: Commit Transaction
117
+ Model-->>User: Returns UndoLog Instance
118
+ ```
119
+
120
+ ### Restoration Flow (`#restore!`)
121
+
122
+ ```mermaid
123
+ sequenceDiagram
124
+ autonumber
125
+ actor User
126
+ participant Log as UndoLog
127
+ participant Item as UndoLogItem
128
+ participant Model as Target Models
129
+
130
+ User->>Log: undo_log.restore!
131
+ Log->>Log: Open ActiveRecord::Base.transaction
132
+
133
+ rect rgb(240, 240, 240)
134
+ note over Log, Model: Reverse Order Processing (Bottom-Up)
135
+ Log->>Item: undo_log_items.reverse_each
136
+ Item->>Model: TargetClass.unscoped.find_by(id)
137
+ Item->>Model: target.update_columns(column_name => nil)
138
+ end
139
+
140
+ Log->>Log: destroy! (Deletes UndoLog & UndoLogItems)
141
+ Log-->>User: Restoration Complete
142
+ ```
143
+
144
+ ---
145
+
146
+ ## 5. Key Technical Considerations
147
+
148
+ 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
+ 2. **Reverse Restoration Order:** `#restore!` calls `undo_log_items.reverse_each`. This ensures the parent node is restored first before restoring its dependent records, maintaining database relational integrity.
150
+ 3. **Bypassing Callbacks:** Soft-deletion updates use `update_columns`. This executes a direct SQL `UPDATE` query without firing standard ActiveRecord persistence callbacks (`save`, `validate`), preventing unintended side effects during soft deletes.
151
+ 4. **Unscoped Model Resolution:** `#restore_item!` uses `klass.unscoped.find_by(id: item_id)` to locate records. This guarantees records are retrieved even when models define default scopes that filter out soft-deleted records.
152
+ 5. **Class Inheritance Security Check:** When constantizing stored class strings, the gem validates that target models inherit from `ActiveRecord::Base` to prevent arbitrary non-model constant manipulation.
153
+
154
+ ---
155
+
156
+ ## 6. Method-by-Method Implementation Reference
157
+
158
+ ### 6.1 `lib/active_record/undo.rb` (Entrypoint)
159
+
160
+ * **`require 'active_record'` Loader Check**
161
+ - *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`.
162
+ * **Loader Hook Block**
163
+ - *Function*: Detects if `ActiveSupport` is loaded:
164
+ - If present, registers `ActiveSupport.on_load(:active_record)` to inject the extension module when ActiveRecord boots.
165
+ - If absent (e.g. running in simple Ruby scripts), directly includes `ModelExtension` into `ActiveRecord::Base` as a fallback.
166
+
167
+ ### 6.2 `lib/active_record/undo/engine.rb` (Rails Integration)
168
+
169
+ * **`initializer 'active_record_undo.migrations'`**
170
+ - *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.
171
+
172
+ ### 6.3 `lib/active_record/undo/model_extension.rb` (Model Extension Module)
173
+
174
+ * **`acts_as_undoable(column: :deleted_at)`**
175
+ - *Function*: Class-level DSL macro injected into models to enable soft-deletion.
176
+ - *Details*: Defines class-level configurations:
177
+ - `undoable_column`: Caches the name of the column (defaults to `:deleted_at`).
178
+ - `kept` scope: Returns records that are not soft-deleted (`where(column => nil)`).
179
+ - `soft_deleted` scope: Returns records that are soft-deleted (`where.not(column => nil)`).
180
+ * **`soft_deleted?`**
181
+ - *Function*: Checks if the current record instance has been soft-deleted. Returns `true` if the configured deletion column is populated with a timestamp.
182
+ * **`soft_delete!`**
183
+ - *Function*: Starts the cascade soft-deletion sequence for the record.
184
+ - *Steps*:
185
+ 1. Calls `ensure_undoable_column_exists!` to verify the database column is present.
186
+ 2. Aborts and returns `false` if the record is already soft-deleted.
187
+ 3. Opens an ActiveRecord database transaction block.
188
+ 4. Creates a new parent `UndoLog` object.
189
+ 5. Recursively invokes cascading soft-deletes on associations and marks the record itself as soft-deleted via `CascadeHandler`.
190
+ 6. Saves the transaction and returns the constructed `UndoLog`.
191
+ * **`restore!`**
192
+ - *Function*: Restores the record from its soft-deleted state.
193
+ - *Steps*:
194
+ 1. Verifies column presence via `ensure_undoable_column_exists!`.
195
+ 2. Aborts and returns `false` if the record is not soft-deleted.
196
+ 3. Resolves the latest `UndoLogItem` that records the soft-deletion of this instance.
197
+ 4. If a log item is found, it calls `restore!` on the parent `UndoLog` (which restores the entire deleted tree).
198
+ 5. If no log item is found, it falls back to a simple, direct restore by setting the deletion column back to `nil`.
199
+ * **`ensure_undoable_column_exists!` (Private)**
200
+ - *Function*: Asserts that the configured soft-delete column exists in the database schema table. Raises `ActiveRecord::Undo::Error` if missing.
201
+ * **`find_latest_undo_log_item` (Private)**
202
+ - *Function*: Queries `UndoLogItem` records pointing to this record, ordering by `created_at DESC` to find the most recent deletion event.
203
+ * **`soft_delete_cascade_internal!(timestamp, undo_log)` (Private)**
204
+ - *Function*: Wraps instantiation and invocation of `CascadeHandler` to encapsulate cascade traversal.
205
+
206
+ ### 6.4 `lib/active_record/undo/cascade_handler.rb` (Cascade Execution)
207
+
208
+ * **`initialize(record)`**
209
+ - *Function*: Caches the record instance to be cascade deleted.
210
+ * **`soft_delete_with_cascade!(timestamp, undo_log)`**
211
+ - *Function*: Coordinates the cascade deletion of the current record.
212
+ - *Steps*:
213
+ 1. Invokes `#cascade_to_associations!` to recurse into child tables.
214
+ 2. Invokes `#update_record_timestamps!` to mark the current record as soft-deleted.
215
+ 3. Appends an `UndoLogItem` pointing to this record to the `UndoLog` transaction.
216
+ * **`cascade_to_associations!(timestamp, undo_log)` (Private)**
217
+ - *Function*: Iterates over reflections retrieved by `AssociationFinder`, fetches their records, and calls `#cascade_to_record!` on each associated record.
218
+ * **`cascade_to_record!(associated, reflection, timestamp, undo_log)` (Private)**
219
+ - *Function*: Handles deletion of a single associated child record:
220
+ - Excludes it if it is already soft-deleted.
221
+ - If the child model is also configured with `acts_as_undoable`, calls its private `#soft_delete_cascade_internal!` recursively.
222
+ - If it is not undoable, but configured with `dependent: :destroy`, it invokes `#destroy!` to perform a hard-deletion.
223
+
224
+ ### 6.5 `lib/active_record/undo/cascade_handler/association_finder.rb` (Reflections Finder)
225
+
226
+ * **`associations_to_cascade` (Private)**
227
+ - *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`.
228
+ * **`associated_records_for(reflection)` (Private)**
229
+ - *Function*: Fetches associated target records. Normalizes single relations and collection associations (like `has_many`) into a flat array structure.
230
+
231
+ ### 6.6 `lib/active_record/undo/cascade_handler/record_updater.rb` (Timestamps Updater)
232
+
233
+ * **`update_record_timestamps!(timestamp)` (Private)**
234
+ - *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`.
235
+
236
+ ### 6.7 `lib/active_record/undo/undo_log.rb` (Batch Restoration)
237
+
238
+ * **`restore!`**
239
+ - *Function*: Triggers database restoration of the entire tree recorded under this log.
240
+ - *Steps*:
241
+ 1. Opens a database transaction block.
242
+ 2. Iterates over associated `undo_log_items` in *reverse order* (`reverse_each`), guaranteeing parent records are restored before child records.
243
+ 3. Invokes `#restore_item!` on each item.
244
+ 4. Automatically calls `#destroy!` on completion to purge the audit records (`UndoLog` and nested `UndoLogItem` rows) from the database.
245
+
246
+ ### 6.8 `lib/active_record/undo/undo_log_item.rb` (Item Restoration)
247
+
248
+ * **`restore_item!`**
249
+ - *Function*: Performs restoration of the individual record referenced by the audit log item.
250
+ - *Steps*:
251
+ 1. Resolves model class via `#resolve_model_class`.
252
+ 2. Resolves target record using `unscoped.find_by(id: item_id)` (unscoping ignores default scopes filtering soft-deleted records).
253
+ 3. Ensures the soft-delete column exists on the model table.
254
+ 4. Resets the soft-delete column to `nil` using `#reset_soft_delete_column!`.
255
+ * **`resolve_model_class` (Private)**
256
+ - *Function*: Constantizes the stored `item_type` string.
257
+ - *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.
258
+ * **`ensure_column_exists!(klass, column_name)` (Private)**
259
+ - *Function*: Confirms that the target soft-delete column exists in the class's table schema. Throws `ActiveRecord::Undo::Error` if missing.
260
+ * **`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.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Saurabh Sharma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # ActiveRecord::Undo
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/active_record-undo.svg)](https://badge.fury.io/rb/active_record-undo)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ **ActiveRecord::Undo** brings transactional, cascade-aware soft deletes and automatic restores to Rails applications.
7
+
8
+ Unlike conventional soft-deletion gems, `active_record-undo` automatically captures an audit snapshot of all records affected across associations (e.g., dependent `has_many` or `has_one` relations) and tracks them in a dedicated polymorphic undo log. Restoring a deleted record cleanly restores its entire deleted child tree in a single atomic database transaction.
9
+
10
+ ---
11
+
12
+ ## Features
13
+
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.
16
+ - ⚙️ **Configurable Columns:** Supports custom soft-delete columns (e.g., `:archived_at`, `:discarded_at`) per model while defaulting to `:deleted_at`.
17
+ - 📦 **Polymorphic Tracking:** Records deletion events via native `UndoLog` and `UndoLogItem` models—no messy JSON payload parsing required.
18
+ - 🚂 **Zero Generator Setup:** Built on top of `Rails::Engine`. Migrations automatically hook into `rails db:migrate`.
19
+ - 🔍 **Default Scopes & Helpers:** Provides `.kept`, `.soft_deleted`, and `#soft_deleted?` query methods out of the box.
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's `Gemfile`:
26
+
27
+ ```ruby
28
+ gem "active_record-undo"
29
+ ```
30
+
31
+ Then execute:
32
+
33
+ ```bash
34
+ $ bundle install
35
+ ```
36
+
37
+ Run database migrations. The gem automatically appends its tables (`undo_logs` and `undo_log_items`) to your app's migration path:
38
+
39
+ ```bash
40
+ $ rails db:migrate
41
+ ```
42
+
43
+ *(Optional)* If you need to customize the migration, copy it to your host application's `db/migrate` folder:
44
+
45
+ ```bash
46
+ $ rails active_record_undo:install:migrations
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Usage
52
+
53
+ ### 1. Database Setup
54
+
55
+ Ensure models using soft deletion have a timestamp column in their underlying database table:
56
+
57
+ ```ruby
58
+ class AddSoftDeleteColumnsToModels < ActiveRecord::Migration[7.0]
59
+ def change
60
+ add_column :posts, :deleted_at, :datetime
61
+ add_column :comments, :deleted_at, :datetime
62
+ add_column :archive_items, :archived_at, :datetime
63
+
64
+ add_index :posts, :deleted_at
65
+ add_index :comments, :deleted_at
66
+ add_index :archive_items, :archived_at
67
+ end
68
+ end
69
+ ```
70
+
71
+ ### 2. Configure Models
72
+
73
+ Add `acts_as_undoable` to models where you want soft deletion enabled. By default, it uses `:deleted_at`, but you can pass a custom column name:
74
+
75
+ ```ruby
76
+ class Post < ApplicationRecord
77
+ # Uses default :deleted_at column
78
+ acts_as_undoable
79
+
80
+ has_many :comments, dependent: :destroy
81
+ end
82
+
83
+ class Comment < ApplicationRecord
84
+ # Uses default :deleted_at column
85
+ acts_as_undoable
86
+
87
+ belongs_to :post
88
+ end
89
+
90
+ class ArchiveItem < ApplicationRecord
91
+ # Configured with a custom column
92
+ acts_as_undoable column: :archived_at
93
+ end
94
+ ```
95
+
96
+ ---
97
+
98
+ ## Quick Start Guide
99
+
100
+ ### Soft Delete & Cascade
101
+
102
+ Call `soft_delete!` on a record. It sets the configured soft-delete column across the record and its dependent relations inside a single transaction, returning an `ActiveRecord::Undo::UndoLog` instance:
103
+
104
+ ```ruby
105
+ post = Post.find(1)
106
+
107
+ # Soft deletes post and all associated comments
108
+ undo_log = post.soft_delete!
109
+
110
+ post.soft_deleted? # => true
111
+ post.comments.kept.count # => 0
112
+ ```
113
+
114
+ ### Custom Column Usage
115
+
116
+ ```ruby
117
+ item = ArchiveItem.find(5)
118
+ item.soft_delete!
119
+
120
+ item.soft_deleted? # => true
121
+ item.archived_at # => 2026-08-08 22:20:16 UTC
122
+ ```
123
+
124
+ ### Inspect Deletion Logs
125
+
126
+ Inspect affected records through standard Rails associations on the returned `UndoLog`:
127
+
128
+ ```ruby
129
+ # List all items affected by this deletion event
130
+ undo_log.undo_log_items.map(&:item)
131
+ # => [#<Comment 101... id:>, #<Comment 102... id:>, #<Post 1... id:>]
132
+ ```
133
+
134
+ ### Restoring Records
135
+
136
+ To restore a deleted object tree, you can invoke `restore!` either on the corresponding `UndoLog` or directly on the model instance itself:
137
+
138
+ #### Option A: Restore from the model instance (Recommended)
139
+ Calling `restore!` directly on the soft-deleted model automatically resolves its latest deletion event log, performs the cascading restore, and cleans up the log database rows:
140
+
141
+ ```ruby
142
+ # Restores the post and all comments deleted in the same batch
143
+ post.restore!
144
+
145
+ post.reload.soft_deleted? # => false
146
+ post.comments.count # => 2
147
+ ```
148
+
149
+ #### Option B: Restore from the `UndoLog`
150
+ ```ruby
151
+ # Reverses soft deletes for the post and comments
152
+ undo_log.restore!
153
+
154
+ post.reload.soft_deleted? # => false
155
+ post.comments.count # => 2
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Scopes & Querying
161
+
162
+ `ActiveRecord::Undo` provides scopes for filtering records based on the configured column:
163
+
164
+ ```ruby
165
+ # Fetch only active (non-deleted) records
166
+ Post.kept
167
+
168
+ # Fetch soft-deleted records
169
+ Post.soft_deleted
170
+
171
+ # Retrieve records including soft-deleted ones via unscoped
172
+ Post.unscoped.where(id: 1)
173
+ ```
174
+
175
+ ---
176
+
177
+ ## How It Works
178
+
179
+ 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
+ 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
+ 3. **Polymorphic Logging:** An `ActiveRecord::Undo::UndoLog` record is created alongside multiple `ActiveRecord::Undo::UndoLogItem` entries mapping polymorphic references (`item_type`, `item_id`) to every affected record.
182
+ 4. **Atomic Operation:** All updates and log creations take place within an `ActiveRecord::Base.transaction`.
183
+
184
+ ---
185
+
186
+ ## Error Handling
187
+
188
+ To ensure database integrity and provide clear debugging context, the gem raises an `ActiveRecord::Undo::Error` in the following scenarios:
189
+ * **Missing Column at Runtime:** If the configured soft-delete column is missing from the database table when calling `soft_delete!` or `restore!`.
190
+ * **Missing Model Class:** If a model class has been renamed or deleted, preventing the polymorphic log items from finding the target class during restore.
191
+ * **Missing Column on Target Class:** If a model class exists but no longer has the target soft-delete column during restore.
192
+
193
+ ---
194
+
195
+ ## Development
196
+
197
+ After cloning the repository, install dependencies:
198
+
199
+ ```bash
200
+ $ bundle install
201
+ ```
202
+
203
+ Run test suite via RSpec:
204
+
205
+ ```bash
206
+ $ bundle exec rspec
207
+ ```
208
+
209
+ ---
210
+
211
+ ## Contributing
212
+
213
+ Bug reports and pull requests are welcome on GitHub at https://github.com/saurabh-activecode/active_record-undo.
214
+
215
+ ## License
216
+
217
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,16 @@
1
+ # db/migrate/20260808000000_create_active_record_undo_tables.rb
2
+ # frozen_string_literal: true
3
+
4
+ class CreateActiveRecordUndoTables < ActiveRecord::Migration[7.0]
5
+ def change
6
+ create_table :undo_logs, &:timestamps
7
+
8
+ create_table :undo_log_items do |t|
9
+ t.references :undo_log, null: false, foreign_key: { to_table: :undo_logs }
10
+ t.references :item, polymorphic: true, null: false
11
+ t.timestamps
12
+ end
13
+
14
+ add_index :undo_log_items, %i[item_type item_id]
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ # lib/active_record/undo/cascade_handler/association_finder.rb
2
+ # frozen_string_literal: true
3
+
4
+ module ActiveRecord
5
+ module Undo
6
+ class CascadeHandler
7
+ module AssociationFinder
8
+ private
9
+
10
+ def associations_to_cascade
11
+ @record.class.reflections.values.select do |reflection|
12
+ %i[destroy soft_delete delete_all].include?(reflection.options[:dependent])
13
+ end
14
+ end
15
+
16
+ def associated_records_for(reflection)
17
+ target = @record.public_send(reflection.name)
18
+ return [] if target.nil?
19
+
20
+ target.is_a?(Enumerable) ? target : [target]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # lib/active_record/undo/cascade_handler/record_updater.rb
2
+ # frozen_string_literal: true
3
+
4
+ module ActiveRecord
5
+ module Undo
6
+ class CascadeHandler
7
+ module RecordUpdater
8
+ private
9
+
10
+ def update_record_timestamps!(timestamp)
11
+ column_name = @record.class.respond_to?(:undoable_column) ? @record.class.undoable_column : :deleted_at
12
+
13
+ @record.update_columns(
14
+ column_name => timestamp,
15
+ updated_at: timestamp
16
+ )
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,44 @@
1
+ # lib/active_record/undo/cascade_handler.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'cascade_handler/association_finder'
5
+ require_relative 'cascade_handler/record_updater'
6
+
7
+ module ActiveRecord
8
+ module Undo
9
+ class CascadeHandler
10
+ include AssociationFinder
11
+ include RecordUpdater
12
+
13
+ def initialize(record)
14
+ @record = record
15
+ end
16
+
17
+ def soft_delete_with_cascade!(timestamp, undo_log)
18
+ cascade_to_associations!(timestamp, undo_log)
19
+ update_record_timestamps!(timestamp)
20
+ undo_log.undo_log_items.build(item: @record)
21
+ end
22
+
23
+ private
24
+
25
+ def cascade_to_associations!(timestamp, undo_log)
26
+ associations_to_cascade.each do |reflection|
27
+ associated_records_for(reflection).each do |associated|
28
+ cascade_to_record!(associated, reflection, timestamp, undo_log)
29
+ end
30
+ end
31
+ end
32
+
33
+ def cascade_to_record!(associated, reflection, timestamp, undo_log)
34
+ return if associated.respond_to?(:soft_deleted?) && associated.soft_deleted?
35
+
36
+ if associated.respond_to?(:soft_delete_cascade_internal!, true)
37
+ associated.send(:soft_delete_cascade_internal!, timestamp, undo_log)
38
+ elsif reflection.options[:dependent] == :destroy
39
+ associated.destroy!
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ # lib/active_record/undo/engine.rb
2
+ # frozen_string_literal: true
3
+
4
+ module ActiveRecord
5
+ module Undo
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace ActiveRecord::Undo
8
+
9
+ # Automatically appends gem migrations to the host application's migration path
10
+ initializer 'active_record_undo.migrations' do |app|
11
+ unless app.root.to_s.match?(root.to_s)
12
+ config.paths['db/migrate'].expanded.each do |expanded_path|
13
+ app.config.paths['db/migrate'] << expanded_path
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,85 @@
1
+ # lib/active_record/undo/model_extension.rb
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'cascade_handler'
5
+
6
+ module ActiveRecord
7
+ module Undo
8
+ module ModelExtension
9
+ extend ActiveSupport::Concern
10
+
11
+ class_methods do
12
+ # Accept a custom column parameter, defaulting to :deleted_at
13
+ def acts_as_undoable(column: :deleted_at)
14
+ include InstanceMethods
15
+
16
+ class_attribute :undoable_column
17
+ self.undoable_column = column.to_sym
18
+
19
+ # Dynamic scopes using the configured column
20
+ scope :kept, -> { where(undoable_column => nil) }
21
+ scope :soft_deleted, -> { where.not(undoable_column => nil) }
22
+ end
23
+ end
24
+
25
+ module InstanceMethods
26
+ def soft_deleted?
27
+ public_send(self.class.undoable_column).present?
28
+ end
29
+
30
+ def soft_delete!
31
+ ensure_undoable_column_exists!
32
+ return false if soft_deleted?
33
+
34
+ undo_log = nil
35
+ timestamp = Time.current
36
+
37
+ transaction do
38
+ undo_log = ActiveRecord::Undo::UndoLog.create!
39
+ soft_delete_cascade_internal!(timestamp, undo_log)
40
+ undo_log.save!
41
+ end
42
+
43
+ undo_log
44
+ end
45
+
46
+ def restore!
47
+ ensure_undoable_column_exists!
48
+ return false unless soft_deleted?
49
+
50
+ log_item = find_latest_undo_log_item
51
+
52
+ if log_item
53
+ log_item.undo_log.restore!
54
+ else
55
+ column_name = self.class.undoable_column
56
+ update_columns(column_name => nil, updated_at: Time.current)
57
+ true
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def ensure_undoable_column_exists!
64
+ column_name = self.class.undoable_column
65
+ return if self.class.column_names.include?(column_name.to_s)
66
+
67
+ raise ActiveRecord::Undo::Error,
68
+ "The configured soft-delete column '#{column_name}' " \
69
+ "does not exist on table '#{self.class.table_name}'."
70
+ end
71
+
72
+ def find_latest_undo_log_item
73
+ ActiveRecord::Undo::UndoLogItem
74
+ .where(item: self)
75
+ .order(created_at: :desc)
76
+ .first
77
+ end
78
+
79
+ def soft_delete_cascade_internal!(timestamp, undo_log)
80
+ CascadeHandler.new(self).soft_delete_with_cascade!(timestamp, undo_log)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,21 @@
1
+ # lib/active_record/undo/undo_log.rb
2
+ # frozen_string_literal: true
3
+
4
+ module ActiveRecord
5
+ module Undo
6
+ class UndoLog < ActiveRecord::Base
7
+ self.table_name = 'undo_logs'
8
+
9
+ has_many :undo_log_items, class_name: 'ActiveRecord::Undo::UndoLogItem', dependent: :destroy
10
+
11
+ # Restores all records associated with this deletion batch
12
+ def restore!
13
+ transaction do
14
+ # Reverse order ensures child records are restored before or after parents as needed
15
+ undo_log_items.reverse_each(&:restore_item!)
16
+ destroy! # Clean up log after successful restoration
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,56 @@
1
+ # lib/active_record/undo/undo_log_item.rb
2
+ # frozen_string_literal: true
3
+
4
+ module ActiveRecord
5
+ module Undo
6
+ class UndoLogItem < ActiveRecord::Base
7
+ self.table_name = 'undo_log_items'
8
+
9
+ belongs_to :undo_log, class_name: 'ActiveRecord::Undo::UndoLog'
10
+ belongs_to :item, polymorphic: true
11
+
12
+ def restore_item!
13
+ klass = resolve_model_class
14
+ target = klass.unscoped.find_by(id: item_id)
15
+ return unless target
16
+
17
+ column_name = klass.respond_to?(:undoable_column) ? klass.undoable_column : :deleted_at
18
+ ensure_column_exists!(klass, column_name)
19
+
20
+ return unless target.public_send(column_name).present?
21
+
22
+ reset_soft_delete_column!(target, column_name)
23
+ end
24
+
25
+ private
26
+
27
+ def resolve_model_class
28
+ klass = item_type.constantize
29
+ unless klass.is_a?(Class) && klass < ActiveRecord::Base
30
+ raise ActiveRecord::Undo::Error,
31
+ "Cannot restore item: '#{item_type}' is not an ActiveRecord model."
32
+ end
33
+
34
+ klass
35
+ rescue NameError => e
36
+ raise ActiveRecord::Undo::Error,
37
+ "Cannot restore item: model class '#{item_type}' could not be loaded. " \
38
+ "Original error: #{e.message}"
39
+ end
40
+
41
+ def ensure_column_exists!(klass, column_name)
42
+ return if klass.column_names.include?(column_name.to_s)
43
+
44
+ raise ActiveRecord::Undo::Error,
45
+ "Cannot restore item: column '#{column_name}' does not exist on '#{item_type}' model."
46
+ end
47
+
48
+ def reset_soft_delete_column!(target, column_name)
49
+ target.update_columns(
50
+ column_name => nil,
51
+ updated_at: Time.current
52
+ )
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,8 @@
1
+ # lib/active_record/undo/version.rb
2
+ # frozen_string_literal: true
3
+
4
+ module ActiveRecord
5
+ module Undo
6
+ VERSION = '0.1.1'
7
+ end
8
+ end
@@ -0,0 +1,31 @@
1
+ # lib/active_record/undo.rb
2
+ # frozen_string_literal: true
3
+
4
+ begin
5
+ require 'active_record'
6
+ rescue LoadError => e
7
+ raise LoadError,
8
+ 'active_record-undo requires ActiveRecord. ' \
9
+ "Please add 'activerecord' to your Gemfile. (Original error: #{e.message})"
10
+ end
11
+
12
+ require_relative 'undo/version'
13
+ require_relative 'undo/engine' if defined?(Rails::Engine)
14
+ require_relative 'undo/model_extension'
15
+ require_relative 'undo/undo_log'
16
+ require_relative 'undo/undo_log_item'
17
+
18
+ module ActiveRecord
19
+ module Undo
20
+ class Error < StandardError; end
21
+ end
22
+ end
23
+
24
+ if defined?(ActiveSupport)
25
+ ActiveSupport.on_load(:active_record) do
26
+ include ActiveRecord::Undo::ModelExtension
27
+ end
28
+ elsif defined?(ActiveRecord::Base)
29
+ # Fallback for bare Ruby scripts where ActiveSupport hooks aren't initialized yet
30
+ ActiveRecord::Base.include(ActiveRecord::Undo::ModelExtension)
31
+ end
@@ -0,0 +1,6 @@
1
+ module ActiveRecord
2
+ module Undo
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_record-undo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Saurabh Sharma
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activerecord
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 7.0.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 7.0.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: bundler
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: combustion
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.3'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rake
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '13.0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '13.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: rspec-rails
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rubocop
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '1.21'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.21'
110
+ - !ruby/object:Gem::Dependency
111
+ name: sqlite3
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '1.4'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '1.4'
124
+ description: Provides soft deletion and automatic restoration of records and their
125
+ dependent associations.
126
+ email:
127
+ - saurabh@activecode.in
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".rubocop.yml"
133
+ - CHANGELOG.md
134
+ - CODE_OF_CONDUCT.md
135
+ - DOCUMENTATION.md
136
+ - LICENSE.txt
137
+ - README.md
138
+ - Rakefile
139
+ - db/migrate/20260808000000_create_active_record_undo_tables.rb
140
+ - lib/active_record/undo.rb
141
+ - lib/active_record/undo/cascade_handler.rb
142
+ - lib/active_record/undo/cascade_handler/association_finder.rb
143
+ - lib/active_record/undo/cascade_handler/record_updater.rb
144
+ - lib/active_record/undo/engine.rb
145
+ - lib/active_record/undo/model_extension.rb
146
+ - lib/active_record/undo/undo_log.rb
147
+ - lib/active_record/undo/undo_log_item.rb
148
+ - lib/active_record/undo/version.rb
149
+ - sig/active_record/undo.rbs
150
+ homepage: https://github.com/saurabh-activecode/active_record-undo
151
+ licenses:
152
+ - MIT
153
+ metadata:
154
+ homepage_uri: https://github.com/saurabh-activecode/active_record-undo
155
+ rubygems_mfa_required: 'true'
156
+ rdoc_options: []
157
+ require_paths:
158
+ - lib
159
+ required_ruby_version: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ">="
162
+ - !ruby/object:Gem::Version
163
+ version: 3.1.0
164
+ required_rubygems_version: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ requirements: []
170
+ rubygems_version: 3.6.9
171
+ specification_version: 4
172
+ summary: Soft-delete extension for ActiveRecord with transactional cascade undo tracking.
173
+ test_files: []