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 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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.1
3
+
4
+ Style/StringLiterals:
5
+ EnforcedStyle: double_quotes
6
+
7
+ Style/StringLiteralsInInterpolation:
8
+ EnforcedStyle: double_quotes
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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-06-26
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/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
+ [![CI](https://github.com/sarvesh4396/separate_history/actions/workflows/test.yml/badge.svg)](https://github.com/sarvesh4396/separate_history/actions/workflows/test.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/separate_history.svg)](https://badge.fury.io/rb/separate_history)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 7.0.0"
6
+ gem "sqlite3", "~> 1.4"
7
+
8
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 7.1.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,7 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 7.2.0"
6
+
7
+ gemspec path: "../"
@@ -0,0 +1,8 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rails", "~> 8.0.0.rc1"
6
+ gem "sqlite3", "~> 2.1"
7
+
8
+ gemspec path: "../"
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SeparateHistory configuration goes here.
4
+ # For example:
5
+ # SeparateHistory.configure do |config|
6
+ # config.default_tracking = :on
7
+ # 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