separate_history 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/Appraisals +19 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +350 -0
- data/Rakefile +18 -0
- data/gemfiles/rails_7.0.gemfile +8 -0
- data/gemfiles/rails_7.1.gemfile +7 -0
- data/gemfiles/rails_7.2.gemfile +7 -0
- data/gemfiles/rails_8.0.gemfile +8 -0
- data/lib/generators/separate_history/install/install_generator.rb +15 -0
- data/lib/generators/separate_history/install/templates/separate_history.rb +7 -0
- data/lib/generators/separate_history/migration_generator.rb +54 -0
- data/lib/generators/separate_history/model/model_generator.rb +39 -0
- data/lib/generators/separate_history/model/templates/migration.rb.erb +13 -0
- data/lib/generators/separate_history/model/templates/model.rb.erb +13 -0
- data/lib/generators/separate_history/scan/scan_generator.rb +32 -0
- data/lib/generators/separate_history/sync/sync_generator.rb +57 -0
- data/lib/generators/separate_history/sync/templates/migration.rb.erb +18 -0
- data/lib/generators/separate_history/templates/migration.rb.erb +37 -0
- data/lib/separate_history/core.rb +127 -0
- data/lib/separate_history/history.rb +15 -0
- data/lib/separate_history/model.rb +124 -0
- data/lib/separate_history/railtie.rb +13 -0
- data/lib/separate_history/version.rb +5 -0
- data/lib/separate_history.rb +20 -0
- data/sig/separate_history.rbs +4 -0
- metadata +203 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: dc395963f666596f7efacf30e2a23a43430c8dd9a05b8c3610f236afa09d6847
|
4
|
+
data.tar.gz: 67c0767c09a4748791590a02f86d4eddcb001c0df2ceef4209ce231ea1a26ddd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1b6dd2568dac8ecb8e0e86ef8a148225c5b66926f433081236b6d59a94ee8758901c63f1869b27fc8d40233ba077036c4e030a9198d62e2624fc5ba08a78dfd8
|
7
|
+
data.tar.gz: 50a6e61ae7a304cf3803ed82f78d914eec7a4093b59b1a881f12dfba30b315e30389f2c092757cdfe76e4ec73256485868c0a2d5c1d9ba9dc61322e724775976
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/Appraisals
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
appraise "rails-7.0" do
|
4
|
+
gem "rails", "~> 7.0.0"
|
5
|
+
gem "sqlite3", "~> 1.4"
|
6
|
+
end
|
7
|
+
|
8
|
+
appraise "rails-7.1" do
|
9
|
+
gem "rails", "~> 7.1.0"
|
10
|
+
end
|
11
|
+
|
12
|
+
appraise "rails-7.2" do
|
13
|
+
gem "rails", "~> 7.2.0"
|
14
|
+
end
|
15
|
+
|
16
|
+
appraise "rails-8.0" do
|
17
|
+
gem "rails", "~> 8.0.0.rc1"
|
18
|
+
gem "sqlite3", "~> 2.1"
|
19
|
+
end
|
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -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/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Sarvesh Kumar Dwivedi
|
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,350 @@
|
|
1
|
+
# SeparateHistory
|
2
|
+
|
3
|
+
[](https://github.com/sarvesh4396/separate_history/actions/workflows/test.yml)
|
4
|
+
[](https://badge.fury.io/rb/separate_history)
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
6
|
+
|
7
|
+
`SeparateHistory` provides a simple and flexible way to keep a complete history of your ActiveRecord model changes in a separate, dedicated history table. It automatically records every `create`, `update`, and `destroy` event, ensuring you have a full audit trail of your data.
|
8
|
+
|
9
|
+
## Features
|
10
|
+
|
11
|
+
- **Automatic History Tracking:** Automatically creates a history record for every create, update, and destroy action on your models.
|
12
|
+
- **Dedicated History Tables:** Keeps your history data separate from your primary tables, ensuring your main application's performance is not impacted.
|
13
|
+
- **Point-in-Time Recovery:** Easily retrieve the state of a record at any point in the past.
|
14
|
+
- **Easy Setup:** Get started with a single line in your model and a simple migration generator.
|
15
|
+
- **Flexible Configuration:** Select which attributes to track, customize history table names, and more.
|
16
|
+
- **Data Integrity:** Includes a `manipulated?` method to easily check if a history record has been altered after its creation.
|
17
|
+
|
18
|
+
## Installation
|
19
|
+
|
20
|
+
Add this line to your application's Gemfile:
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
gem 'separate_history'
|
24
|
+
```
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
$ bundle install
|
30
|
+
```
|
31
|
+
|
32
|
+
## Quick Start
|
33
|
+
|
34
|
+
Getting started with `SeparateHistory` is a three-step process:
|
35
|
+
|
36
|
+
### 1. Generate the History Table Migration
|
37
|
+
|
38
|
+
Use the provided generator to create a migration for the history table. For a model named `User`, run:
|
39
|
+
|
40
|
+
```bash
|
41
|
+
$ rails g separate_history:sync User
|
42
|
+
```
|
43
|
+
|
44
|
+
This creates a migration file that defines the schema for your history table.
|
45
|
+
|
46
|
+
### 2. Generate the History Model
|
47
|
+
|
48
|
+
Next, generate the history model file. This model will include the necessary `SeparateHistory::History` module.
|
49
|
+
|
50
|
+
```bash
|
51
|
+
$ rails g separate_history:model User
|
52
|
+
```
|
53
|
+
|
54
|
+
This creates the `app/models/user_history.rb` file.
|
55
|
+
|
56
|
+
### 3. Run the Migration and Add to Your Model
|
57
|
+
|
58
|
+
Run the migration to create the table in your database:
|
59
|
+
|
60
|
+
```bash
|
61
|
+
$ rails db:migrate
|
62
|
+
```
|
63
|
+
|
64
|
+
Finally, add the `has_separate_history` macro to your original model:
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# app/models/user.rb
|
68
|
+
class User < ApplicationRecord
|
69
|
+
has_separate_history
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
That's it! Now, every change to a `User` instance will be recorded in the `user_histories` table.
|
74
|
+
## Usage
|
75
|
+
|
76
|
+
### Basic Setup
|
77
|
+
|
78
|
+
1. Generate and run the migration for your model:
|
79
|
+
|
80
|
+
```bash
|
81
|
+
rails generate separate_history:migration User
|
82
|
+
rails db:migrate
|
83
|
+
```
|
84
|
+
|
85
|
+
2. Add to your model:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class User < ApplicationRecord
|
89
|
+
has_separate_history
|
90
|
+
end
|
91
|
+
```
|
92
|
+
|
93
|
+
### Tracking Options
|
94
|
+
|
95
|
+
#### Track Specific Attributes
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
class Article < ApplicationRecord
|
99
|
+
has_separate_history only: [:title, :content]
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
#### Exclude Specific Attributes
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
class User < ApplicationRecord
|
107
|
+
has_separate_history except: [:last_sign_in_ip, :encrypted_password]
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
#### Track Only Changed Attributes
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
class User < ApplicationRecord
|
115
|
+
has_separate_history track_changes: true
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
### Advanced Usage
|
120
|
+
|
121
|
+
#### Custom History Class Name
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class AdminUser < ApplicationRecord
|
125
|
+
has_separate_history history_class_name: 'AdminActionLog'
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
#### Track Specific Events
|
130
|
+
|
131
|
+
Track only certain events (create/update/destroy):
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
class Document < ApplicationRecord
|
135
|
+
has_separate_history events: [:create, :update] # Only track creation and updates
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
#### Accessing History
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
# Get all history records for a user
|
143
|
+
user = User.find(1)
|
144
|
+
user.user_histories.each do |history|
|
145
|
+
puts "Event: #{history.event} at #{history.history_created_at}"
|
146
|
+
end
|
147
|
+
|
148
|
+
# Or use the alias
|
149
|
+
user.separate_histories.each { |h| puts h.inspect }
|
150
|
+
```
|
151
|
+
|
152
|
+
#### Class-Level History Queries
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
# Get historical state of a record at a specific time
|
156
|
+
old_user = User.history_as_of(user_id, 1.month.ago)
|
157
|
+
|
158
|
+
# Check if history exists for a record
|
159
|
+
if User.history_exists?(user_id)
|
160
|
+
# Do something with history
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
### Point-in-Time History
|
165
|
+
|
166
|
+
You can retrieve the state of a record at any given point in time using the `history_for` class method. It returns the last history record created before or at the specified timestamp, giving you a precise snapshot of the record's state.
|
167
|
+
|
168
|
+
This query uses the `history_updated_at` timestamp to ensure accuracy, even if records were created out of order or their timestamps were manually altered.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
# Get the user record as it was 2 days ago
|
172
|
+
user_snapshot = User.history_for(user.id, 2.days.ago)
|
173
|
+
puts user_snapshot.name # => "Old Name"
|
174
|
+
|
175
|
+
# Get what a user looked like 1 week ago
|
176
|
+
user_week_ago = user.history_as_of(1.week.ago)
|
177
|
+
|
178
|
+
# Get the state of a record that might have been deleted
|
179
|
+
old_user = User.history_as_of(deleted_user_id, 1.month.ago)
|
180
|
+
```
|
181
|
+
|
182
|
+
### Error Handling
|
183
|
+
|
184
|
+
When the history table is missing, you'll get a helpful error message:
|
185
|
+
|
186
|
+
```
|
187
|
+
History table `user_histories` is missing.
|
188
|
+
Run `rails g separate_history:model User` to create it.
|
189
|
+
```
|
190
|
+
|
191
|
+
### Validation and Options
|
192
|
+
|
193
|
+
SeparateHistory includes built-in validation for options:
|
194
|
+
|
195
|
+
```ruby
|
196
|
+
# These will raise ArgumentError:
|
197
|
+
has_separate_history only: [:name], except: [:email] # Can't use both only and except
|
198
|
+
has_separate_history invalid_option: true # Invalid option
|
199
|
+
has_separate_history events: [:invalid_event] # Invalid event type
|
200
|
+
has_separate_history track_changes: 'yes' # Must be boolean
|
201
|
+
```
|
202
|
+
|
203
|
+
## Instance Methods
|
204
|
+
|
205
|
+
When you include `has_separate_history` in your model, the following instance methods become available:
|
206
|
+
|
207
|
+
- **`#snapshot_history`**
|
208
|
+
Manually create a snapshot history record for the current state.
|
209
|
+
|
210
|
+
- **`#history?`**
|
211
|
+
Returns `true` if any history exists for this record, otherwise `false`.
|
212
|
+
|
213
|
+
- **`#history_as_of(timestamp)`**
|
214
|
+
Returns the state of the record at or before the given timestamp.
|
215
|
+
|
216
|
+
- **`#all_history`**
|
217
|
+
Returns all history records for this instance.
|
218
|
+
|
219
|
+
- **`#latest_history`**
|
220
|
+
Returns the most recent history record for this instance.
|
221
|
+
|
222
|
+
- **`#clear_history(force: true)`**
|
223
|
+
Deletes all history records for this instance.
|
224
|
+
**Warning:** You must pass `force: true` to confirm deletion.
|
225
|
+
|
226
|
+
**Example:**
|
227
|
+
```ruby
|
228
|
+
user = User.create!(name: "Alice")
|
229
|
+
user.update!(name: "Bob")
|
230
|
+
user.snapshot_history
|
231
|
+
user.history? # => true
|
232
|
+
user.all_history # => [<UserHistory ...>, ...]
|
233
|
+
user.latest_history # => <UserHistory ...>
|
234
|
+
user.history_as_of(1.day.ago) # => <UserHistory ...>
|
235
|
+
user.clear_history(force: true)
|
236
|
+
```
|
237
|
+
|
238
|
+
## Advanced Usage
|
239
|
+
|
240
|
+
### Tracking Only Changes
|
241
|
+
|
242
|
+
By default, `SeparateHistory` saves a complete snapshot of the record on every change. For high-traffic tables, this can lead to a lot of data storage. You can optimize this by enabling the `track_changes` option. When set to `true`, only the attributes that actually changed during an `update` event will be saved.
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
# in app/models/user.rb
|
246
|
+
class User < ApplicationRecord
|
247
|
+
has_separate_history track_changes: true
|
248
|
+
end
|
249
|
+
```
|
250
|
+
|
251
|
+
With this enabled, if you only update a user's name, the history record will store the new name, but all other attributes will be `nil`.
|
252
|
+
|
253
|
+
### Excluding Attributes
|
254
|
+
|
255
|
+
You can prevent certain attributes from being saved to the history table by using the `except` option. This is useful for ignoring fields that change frequently but aren't important for auditing, like `sign_in_count` or `last_login_at`.
|
256
|
+
|
257
|
+
```ruby
|
258
|
+
# Only track changes to name and email
|
259
|
+
class User < ApplicationRecord
|
260
|
+
has_separate_history only: [:name, :email]
|
261
|
+
end
|
262
|
+
|
263
|
+
# Track all attributes except for sign_in_count
|
264
|
+
class User < ApplicationRecord
|
265
|
+
has_separate_history except: [:sign_in_count]
|
266
|
+
end
|
267
|
+
```
|
268
|
+
|
269
|
+
### Custom History Class Name
|
270
|
+
|
271
|
+
If you want to use a different name for your history model, you can specify it with the `history_class_name` option.
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
# in app/models/user.rb
|
275
|
+
class User < ApplicationRecord
|
276
|
+
has_separate_history history_class_name: 'UserAuditTrail'
|
277
|
+
end
|
278
|
+
|
279
|
+
# in app/models/user_audit_trail.rb
|
280
|
+
class UserAuditTrail < ApplicationRecord
|
281
|
+
# ...
|
282
|
+
end
|
283
|
+
```
|
284
|
+
|
285
|
+
### Checking for Manipulation
|
286
|
+
|
287
|
+
To verify that a history record has not been altered since it was first created, you can use the `manipulated?` method.
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
last_history = user.histories.last
|
291
|
+
last_history.manipulated? # => false
|
292
|
+
|
293
|
+
# If someone changes the record later...
|
294
|
+
last_history.update(name: "A new name")
|
295
|
+
last_history.manipulated? # => true
|
296
|
+
```
|
297
|
+
|
298
|
+
## Creating Snapshots
|
299
|
+
|
300
|
+
If you add `SeparateHistory` to a model with existing records, you may want to create an initial history entry for them. You can do this by creating a `snapshot` event. This is also useful for creating periodic backups of your records.
|
301
|
+
|
302
|
+
Here is an example of a Rake task to create an initial snapshot for all records in your `User` model:
|
303
|
+
|
304
|
+
```ruby
|
305
|
+
# lib/tasks/history.rake
|
306
|
+
namespace :history do
|
307
|
+
desc "Create initial history records for existing users"
|
308
|
+
task sync_users: :environment do
|
309
|
+
User.find_each do |user|
|
310
|
+
history_class = User.history_class
|
311
|
+
unless history_class.exists?(original_id: user.id)
|
312
|
+
history_class.create!(user.attributes.merge(original_id: user.id, event: 'snapshot'))
|
313
|
+
puts "Created snapshot for User ##{user.id}"
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
Run it with `bundle exec rake history:sync_users`.
|
321
|
+
|
322
|
+
## Development
|
323
|
+
|
324
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the tests (RSpec, Minitest, and RuboCop).
|
325
|
+
|
326
|
+
This project uses `appraisal` to test against multiple versions of Rails. The test suites can be run with:
|
327
|
+
|
328
|
+
```bash
|
329
|
+
$ bundle exec appraisal rake
|
330
|
+
```
|
331
|
+
|
332
|
+
Before running test suites install dependencies.
|
333
|
+
|
334
|
+
```bash
|
335
|
+
$ bundle exec appraisal install
|
336
|
+
```
|
337
|
+
|
338
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
339
|
+
|
340
|
+
## Development
|
341
|
+
|
342
|
+
For a detailed log of the debugging and development process for the Rails 7 compatibility fixes, please see [DEV.md](DEV.md).
|
343
|
+
|
344
|
+
## Contributing
|
345
|
+
|
346
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/sarvesh4396/separate_history](https://github.com/sarvesh4396/separate_history).
|
347
|
+
|
348
|
+
## License
|
349
|
+
|
350
|
+
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,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
require "rake/testtask"
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << "test"
|
11
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
12
|
+
end
|
13
|
+
|
14
|
+
# require "rubocop/rake_task"
|
15
|
+
|
16
|
+
# RuboCop::RakeTask.new
|
17
|
+
|
18
|
+
task default: %i[spec test]
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
|
5
|
+
module SeparateHistory
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def copy_initializer
|
11
|
+
template "separate_history.rb", "config/initializers/separate_history.rb"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
require "rails/generators/active_record"
|
5
|
+
|
6
|
+
module SeparateHistory
|
7
|
+
module Generators
|
8
|
+
class MigrationGenerator < ActiveRecord::Generators::Base
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
10
|
+
argument :name, type: :string, desc: "The name of the model to create history for"
|
11
|
+
|
12
|
+
def create_migration_file
|
13
|
+
migration_template "migration.rb.erb", "db/migrate/create_#{history_table_name}.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def history_table_name
|
19
|
+
"#{name.underscore}_histories"
|
20
|
+
end
|
21
|
+
|
22
|
+
def history_class_name
|
23
|
+
"#{name.camelize}History"
|
24
|
+
end
|
25
|
+
|
26
|
+
def original_class
|
27
|
+
@original_class ||= name.camelize.constantize
|
28
|
+
rescue NameError
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def original_table_name
|
33
|
+
if original_class
|
34
|
+
original_class.table_name
|
35
|
+
else
|
36
|
+
name.underscore.pluralize
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def original_columns
|
41
|
+
if original_class
|
42
|
+
# Use actual model columns if class exists
|
43
|
+
original_class.columns.reject do |c|
|
44
|
+
[original_class.primary_key, "created_at", "updated_at"].include?(c.name)
|
45
|
+
end
|
46
|
+
else
|
47
|
+
# Use common columns structure if model doesn't exist
|
48
|
+
# This will be a basic structure for the migration
|
49
|
+
[]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|