historiographer 4.4.1 → 4.4.3

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/DEVELOPMENT.md +124 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +14 -0
  5. data/README.md +16 -1
  6. data/Rakefile +54 -0
  7. data/VERSION +1 -1
  8. data/bin/console +10 -0
  9. data/bin/setup +15 -0
  10. data/bin/test +5 -0
  11. data/bin/test-all +10 -0
  12. data/bin/test-rails +5 -0
  13. data/historiographer.gemspec +38 -4
  14. data/lib/historiographer/history.rb +193 -60
  15. data/spec/combustion_helper.rb +34 -0
  16. data/spec/db/migrate/20250823000000_create_easy_ml_columns.rb +26 -0
  17. data/spec/db/migrate/20250824000000_create_test_articles.rb +26 -0
  18. data/spec/db/migrate/20250824000001_create_test_categories.rb +26 -0
  19. data/spec/db/migrate/20250825000000_create_bylines.rb +11 -0
  20. data/spec/db/migrate/20250826000000_create_test_users.rb +8 -0
  21. data/spec/db/migrate/20250826000001_create_test_user_histories.rb +18 -0
  22. data/spec/db/migrate/20250826000002_create_test_websites.rb +9 -0
  23. data/spec/db/migrate/20250826000003_create_test_website_histories.rb +19 -0
  24. data/spec/db/schema.rb +110 -40
  25. data/spec/historiographer_spec.rb +319 -1
  26. data/spec/integration/historiographer_safe_integration_spec.rb +154 -0
  27. data/spec/internal/app/models/application_record.rb +5 -0
  28. data/spec/internal/app/models/deploy.rb +5 -0
  29. data/spec/internal/app/models/user.rb +4 -0
  30. data/spec/internal/app/models/website.rb +5 -0
  31. data/spec/internal/app/models/website_history.rb +7 -0
  32. data/spec/internal/config/database.yml +9 -0
  33. data/spec/internal/config/routes.rb +2 -0
  34. data/spec/internal/db/schema.rb +48 -0
  35. data/spec/models/author.rb +1 -0
  36. data/spec/models/byline.rb +4 -0
  37. data/spec/models/post.rb +2 -0
  38. data/spec/models/test_article.rb +4 -0
  39. data/spec/models/test_article_history.rb +3 -0
  40. data/spec/models/test_category.rb +4 -0
  41. data/spec/models/test_category_history.rb +3 -0
  42. data/spec/models/test_user.rb +4 -0
  43. data/spec/models/test_user_history.rb +3 -0
  44. data/spec/models/test_website.rb +4 -0
  45. data/spec/models/test_website_history.rb +3 -0
  46. data/spec/rails_integration/historiographer_rails_integration_spec.rb +106 -0
  47. data/spec/spec_helper.rb +2 -3
  48. metadata +42 -4
  49. data/spec/foreign_key_spec.rb +0 -189
@@ -0,0 +1,154 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe 'Historiographer::Safe Integration' do
4
+ # This test reproduces the exact error from a real Rails app where:
5
+ # 1. WebsiteHistory is defined first and includes Historiographer::History
6
+ # 2. Website is defined later and includes Historiographer::Safe
7
+ # 3. The error occurs because foreign_class.constantize fails when Website isn't loaded yet
8
+
9
+ context 'when history class is loaded before the main model' do
10
+ before(:each) do
11
+ # Ensure clean state
12
+ Object.send(:remove_const, :RealAppWebsite) if defined?(RealAppWebsite)
13
+ Object.send(:remove_const, :RealAppWebsiteHistory) if defined?(RealAppWebsiteHistory)
14
+
15
+ # Create the tables
16
+ ActiveRecord::Base.connection.create_table :real_app_websites, force: true do |t|
17
+ t.string :name
18
+ t.integer :project_id
19
+ t.integer :user_id
20
+ t.integer :template_id
21
+ t.timestamps
22
+ end
23
+
24
+ ActiveRecord::Base.connection.create_table :real_app_website_histories, force: true do |t|
25
+ t.integer :real_app_website_id, null: false
26
+ t.string :name
27
+ t.integer :project_id
28
+ t.integer :user_id
29
+ t.integer :template_id
30
+ t.datetime :created_at, null: false
31
+ t.datetime :updated_at, null: false
32
+ t.datetime :history_started_at, null: false
33
+ t.datetime :history_ended_at
34
+ t.integer :history_user_id
35
+ t.string :snapshot_id
36
+ t.string :thread_id
37
+ end
38
+
39
+ ActiveRecord::Base.connection.add_index :real_app_website_histories, :real_app_website_id
40
+ ActiveRecord::Base.connection.add_index :real_app_website_histories, :history_started_at
41
+ ActiveRecord::Base.connection.add_index :real_app_website_histories, :history_ended_at
42
+ end
43
+
44
+ after(:each) do
45
+ # Clean up tables
46
+ ActiveRecord::Base.connection.drop_table :real_app_website_histories if ActiveRecord::Base.connection.table_exists?(:real_app_website_histories)
47
+ ActiveRecord::Base.connection.drop_table :real_app_websites if ActiveRecord::Base.connection.table_exists?(:real_app_websites)
48
+
49
+ # Clean up constants
50
+ Object.send(:remove_const, :RealAppWebsiteHistory) if defined?(RealAppWebsiteHistory)
51
+ Object.send(:remove_const, :RealAppWebsite) if defined?(RealAppWebsite)
52
+ end
53
+
54
+ it 'handles history class being defined before the main model exists' do
55
+ # This is the exact scenario from the error report:
56
+ # WebsiteHistory is loaded/required first (common in Rails autoloading)
57
+
58
+ expect {
59
+ class RealAppWebsiteHistory < ApplicationRecord
60
+ self.table_name = 'real_app_website_histories'
61
+ include Historiographer::History
62
+ end
63
+ }.not_to raise_error
64
+
65
+ # At this point, RealAppWebsite doesn't exist yet
66
+ # The history class should handle this gracefully
67
+ expect(RealAppWebsiteHistory.foreign_class).to be_nil
68
+
69
+ # Now define the main model (simulating Rails autoloading it later)
70
+ class RealAppWebsite < ApplicationRecord
71
+ self.table_name = 'real_app_websites'
72
+ include Historiographer::Safe
73
+ end
74
+
75
+ # After the main model is defined, foreign_class should resolve
76
+ expect(RealAppWebsiteHistory.foreign_class).to eq(RealAppWebsite)
77
+
78
+ # And all functionality should work
79
+ website = RealAppWebsite.create!(name: 'Test Site', history_user_id: 1)
80
+ expect(website.histories.count).to eq(1)
81
+
82
+ history = website.histories.first
83
+ expect(history).to be_a(RealAppWebsiteHistory)
84
+ expect(history.real_app_website_id).to eq(website.id)
85
+ expect(history.name).to eq('Test Site')
86
+ end
87
+
88
+ it 'allows history class to define associations even when parent model is not loaded' do
89
+ # Define a Deploy model for association testing
90
+ ActiveRecord::Base.connection.create_table :deploys, force: true do |t|
91
+ t.integer :real_app_website_history_id
92
+ t.string :status
93
+ t.timestamps
94
+ end
95
+
96
+ class Deploy < ApplicationRecord
97
+ self.table_name = 'deploys'
98
+ end
99
+
100
+ # Define history class with associations before main model exists
101
+ expect {
102
+ class RealAppWebsiteHistory < ApplicationRecord
103
+ self.table_name = 'real_app_website_histories'
104
+ include Historiographer::History
105
+
106
+ # This should work even though RealAppWebsite doesn't exist yet
107
+ has_many :deploys, foreign_key: :real_app_website_history_id, dependent: :destroy
108
+ end
109
+ }.not_to raise_error
110
+
111
+ # Now define the main model
112
+ class RealAppWebsite < ApplicationRecord
113
+ self.table_name = 'real_app_websites'
114
+ include Historiographer::Safe
115
+ end
116
+
117
+ # Test that associations work
118
+ website = RealAppWebsite.create!(name: 'Test Site', history_user_id: 1)
119
+ history = website.histories.first
120
+
121
+ deploy = Deploy.create!(real_app_website_history_id: history.id, status: 'pending')
122
+ expect(history.deploys).to include(deploy)
123
+
124
+ # Clean up
125
+ ActiveRecord::Base.connection.drop_table :deploys
126
+ Object.send(:remove_const, :Deploy)
127
+ end
128
+
129
+ it 'supports after_initialize Rails hook when available' do
130
+ # Simulate Rails being available with after_initialize
131
+ rails_app = double('Rails App')
132
+ config = double('Rails Config')
133
+ allow(config).to receive(:after_initialize).and_yield
134
+ allow(rails_app).to receive(:config).and_return(config)
135
+ allow(Rails).to receive(:application).and_return(rails_app)
136
+
137
+ # Define history class
138
+ class RealAppWebsiteHistory < ApplicationRecord
139
+ self.table_name = 'real_app_website_histories'
140
+ include Historiographer::History
141
+ end
142
+
143
+ # Define main model
144
+ class RealAppWebsite < ApplicationRecord
145
+ self.table_name = 'real_app_websites'
146
+ include Historiographer::Safe
147
+ end
148
+
149
+ # The after_initialize should have set up associations
150
+ expect(RealAppWebsiteHistory).to respond_to(:setup_history_associations)
151
+ expect { RealAppWebsiteHistory.setup_history_associations }.not_to raise_error
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationRecord < ActiveRecord::Base
4
+ self.abstract_class = true
5
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Deploy < ApplicationRecord
4
+ belongs_to :website_history
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class User < ApplicationRecord
4
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Website < ApplicationRecord
4
+ include Historiographer::Safe
5
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class WebsiteHistory < ApplicationRecord
4
+ include Historiographer::History
5
+
6
+ has_many :deploys
7
+ end
@@ -0,0 +1,9 @@
1
+ test:
2
+ adapter: postgresql
3
+ database: historiographer_combustion_test
4
+ username: <%= ENV['DB_USERNAME'] || 'postgres' %>
5
+ password: <%= ENV['DB_PASSWORD'] || '' %>
6
+ host: <%= ENV['DB_HOST'] || 'localhost' %>
7
+ port: <%= ENV['DB_PORT'] || 5432 %>
8
+ pool: 5
9
+ timeout: 5000
@@ -0,0 +1,2 @@
1
+ Rails.application.routes.draw do
2
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActiveRecord::Schema.define(version: 1) do
4
+ # Website table - the main model
5
+ create_table :websites, force: true do |t|
6
+ t.string :name
7
+ t.integer :project_id
8
+ t.integer :user_id
9
+ t.integer :template_id
10
+ t.timestamps
11
+ end
12
+
13
+ # Website history table
14
+ create_table :website_histories, force: true do |t|
15
+ t.integer :website_id, null: false
16
+ t.string :name
17
+ t.integer :project_id
18
+ t.integer :user_id
19
+ t.integer :template_id
20
+ t.datetime :created_at, null: false
21
+ t.datetime :updated_at, null: false
22
+ t.datetime :history_started_at, null: false
23
+ t.datetime :history_ended_at
24
+ t.integer :history_user_id
25
+ t.string :snapshot_id
26
+ t.string :thread_id
27
+ end
28
+
29
+ add_index :website_histories, :website_id
30
+ add_index :website_histories, :history_started_at
31
+ add_index :website_histories, :history_ended_at
32
+ add_index :website_histories, :history_user_id
33
+ add_index :website_histories, :snapshot_id
34
+ add_index :website_histories, [:thread_id], unique: true, name: 'index_website_histories_on_thread_id'
35
+
36
+ # Deploy table - to test associations on history models
37
+ create_table :deploys, force: true do |t|
38
+ t.integer :website_history_id
39
+ t.string :status
40
+ t.timestamps
41
+ end
42
+
43
+ # User table
44
+ create_table :users, force: true do |t|
45
+ t.string :name
46
+ t.timestamps
47
+ end
48
+ end
@@ -2,4 +2,5 @@ class Author < ActiveRecord::Base
2
2
  include Historiographer
3
3
  has_many :comments
4
4
  has_many :posts
5
+ has_many :bylines # This model doesn't have history tracking
5
6
  end
@@ -0,0 +1,4 @@
1
+ class Byline < ActiveRecord::Base
2
+ # Note: This model does NOT include Historiographer
3
+ belongs_to :author
4
+ end
data/spec/models/post.rb CHANGED
@@ -5,6 +5,8 @@ class Post < ApplicationRecord
5
5
  acts_as_paranoid
6
6
  has_many :comments
7
7
 
8
+ attr_accessor :type
9
+
8
10
  validates :type, inclusion: { in: ['Post', 'PrivatePost', nil] }
9
11
  before_validation :set_defaults
10
12
  after_find :set_comment_count
@@ -0,0 +1,4 @@
1
+ class TestArticle < ActiveRecord::Base
2
+ include Historiographer
3
+ # Association will be defined later to avoid circular dependency
4
+ end
@@ -0,0 +1,3 @@
1
+ class TestArticleHistory < ActiveRecord::Base
2
+ include Historiographer::History
3
+ end
@@ -0,0 +1,4 @@
1
+ class TestCategory < ActiveRecord::Base
2
+ include Historiographer
3
+ # Association will be defined later to avoid circular dependency
4
+ end
@@ -0,0 +1,3 @@
1
+ class TestCategoryHistory < ActiveRecord::Base
2
+ include Historiographer::History
3
+ end
@@ -0,0 +1,4 @@
1
+ class TestUser < ApplicationRecord
2
+ include Historiographer
3
+ has_many :test_websites, foreign_key: 'user_id'
4
+ end
@@ -0,0 +1,3 @@
1
+ class TestUserHistory < ApplicationRecord
2
+ include Historiographer::History
3
+ end
@@ -0,0 +1,4 @@
1
+ class TestWebsite < ApplicationRecord
2
+ include Historiographer
3
+ belongs_to :user, class_name: 'TestUser', foreign_key: 'user_id', optional: true
4
+ end
@@ -0,0 +1,3 @@
1
+ class TestWebsiteHistory < ApplicationRecord
2
+ include Historiographer::History
3
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'combustion_helper'
4
+
5
+ RSpec.describe 'Historiographer Rails Integration', type: :model do
6
+ describe 'when WebsiteHistory is loaded before Website (Rails autoloading scenario)' do
7
+ it 'handles the load order gracefully' do
8
+ expect(defined?(WebsiteHistory)).to be_truthy
9
+ expect(WebsiteHistory.ancestors).to include(Historiographer::History)
10
+
11
+ expect(defined?(Website)).to be_truthy
12
+ expect(Website.ancestors).to include(Historiographer::Safe)
13
+
14
+ expect(WebsiteHistory.foreign_class).to eq(Website)
15
+
16
+ expect(WebsiteHistory).to respond_to(:setup_history_associations)
17
+ expect(WebsiteHistory).to respond_to(:original_class)
18
+
19
+ expect { WebsiteHistory.setup_history_associations }.not_to raise_error
20
+ end
21
+
22
+ it 'allows creating and querying history records' do
23
+ user = User.create!(name: 'Test User')
24
+
25
+ website = Website.create!(
26
+ name: 'Production Site',
27
+ project_id: 1,
28
+ user_id: user.id,
29
+ history_user_id: user.id
30
+ )
31
+
32
+ expect(website.histories.count).to eq(1)
33
+
34
+ history = website.histories.first
35
+ expect(history).to be_a(WebsiteHistory)
36
+ expect(history.website_id).to eq(website.id)
37
+ expect(history.name).to eq('Production Site')
38
+ expect(history.history_user_id).to eq(user.id)
39
+ expect(history.history_started_at).to be_present
40
+ expect(history.history_ended_at).to be_nil
41
+
42
+ website.update!(name: 'Updated Site', history_user_id: user.id)
43
+
44
+ expect(website.histories.count).to eq(2)
45
+
46
+ old_history = website.histories.where.not(history_ended_at: nil).first
47
+ expect(old_history.name).to eq('Production Site')
48
+ expect(old_history.history_ended_at).to be_present
49
+
50
+ current_history = website.histories.current.first
51
+ expect(current_history.name).to eq('Updated Site')
52
+ expect(current_history.history_ended_at).to be_nil
53
+ end
54
+
55
+ it 'supports associations on history models' do
56
+ website = Website.create!(name: 'Deploy Test Site', history_user_id: 1)
57
+ history = website.histories.first
58
+
59
+ deploy = Deploy.create!(
60
+ website_history_id: history.id,
61
+ status: 'pending'
62
+ )
63
+
64
+ expect(history.deploys).to include(deploy)
65
+ expect(deploy.website_history).to eq(history)
66
+
67
+ expect(history.deploys.where(status: 'pending').count).to eq(1)
68
+ end
69
+
70
+ it 'handles Safe mode without requiring history_user_id after initial creation' do
71
+ website = Website.create!(name: 'Safe Mode Test', history_user_id: 1)
72
+
73
+ expect { website.update!(name: 'Updated Safe Mode Test') }.not_to raise_error
74
+
75
+ expect(website.histories.count).to eq(2)
76
+
77
+ current_history = website.histories.current.first
78
+ expect(current_history.name).to eq('Updated Safe Mode Test')
79
+ expect(current_history.history_user_id).to eq(1)
80
+
81
+ website.update!(name: 'Another Update', history_user_id: nil)
82
+ expect(website.histories.count).to eq(3)
83
+ newest_history = website.histories.current.first
84
+ expect(newest_history.history_user_id).to be_nil
85
+ end
86
+
87
+ it 'properly sets up delegated methods on history instances' do
88
+ website = Website.create!(name: 'Method Delegation Test', history_user_id: 1)
89
+ history = website.histories.first
90
+
91
+ expect(history).to respond_to(:name)
92
+
93
+ expect(history.name).to eq('Method Delegation Test')
94
+ end
95
+ end
96
+
97
+ describe 'Rails after_initialize hook' do
98
+ it 'sets up associations after Rails initialization' do
99
+ expect(WebsiteHistory.reflect_on_association(:website)).to be_present
100
+ expect(WebsiteHistory.reflect_on_association(:deploys)).to be_present
101
+
102
+ website_association = WebsiteHistory.reflect_on_association(:website)
103
+ expect(website_association.class_name).to eq('Website')
104
+ end
105
+ end
106
+ end
data/spec/spec_helper.rb CHANGED
@@ -45,10 +45,9 @@ module Rails
45
45
  end
46
46
 
47
47
  def self.application
48
- OpenStruct.new(
48
+ @application ||= OpenStruct.new(
49
49
  config: OpenStruct.new(
50
- eager_load_namespaces: [],
51
- autoloader: loader
50
+ eager_load_namespaces: []
52
51
  )
53
52
  )
54
53
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: historiographer
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.4.1
4
+ version: 4.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - brettshollenberger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-21 00:00:00.000000000 Z
11
+ date: 2025-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -208,7 +208,12 @@ dependencies:
208
208
  version: '0'
209
209
  description: Creates separate tables for each history table
210
210
  email: brett.shollenberger@gmail.com
211
- executables: []
211
+ executables:
212
+ - console
213
+ - setup
214
+ - test
215
+ - test-all
216
+ - test-rails
212
217
  extensions: []
213
218
  extra_rdoc_files:
214
219
  - LICENSE.txt
@@ -218,6 +223,7 @@ files:
218
223
  - ".rspec"
219
224
  - ".ruby-version"
220
225
  - ".standalone_migrations"
226
+ - DEVELOPMENT.md
221
227
  - Gemfile
222
228
  - Gemfile.lock
223
229
  - Guardfile
@@ -225,6 +231,11 @@ files:
225
231
  - README.md
226
232
  - Rakefile
227
233
  - VERSION
234
+ - bin/console
235
+ - bin/setup
236
+ - bin/test
237
+ - bin/test-all
238
+ - bin/test-rails
228
239
  - historiographer-4.1.12.gem
229
240
  - historiographer-4.1.13.gem
230
241
  - historiographer-4.1.14.gem
@@ -244,6 +255,7 @@ files:
244
255
  - lib/historiographer/safe.rb
245
256
  - lib/historiographer/silent.rb
246
257
  - lib/historiographer/version.rb
258
+ - spec/combustion_helper.rb
247
259
  - spec/db/database.yml
248
260
  - spec/db/migrate/20161121212228_create_posts.rb
249
261
  - spec/db/migrate/20161121212229_create_post_histories.rb
@@ -262,13 +274,30 @@ files:
262
274
  - spec/db/migrate/20241119000000_create_datasets.rb
263
275
  - spec/db/migrate/2025082100000_create_projects.rb
264
276
  - spec/db/migrate/2025082100001_create_project_files.rb
277
+ - spec/db/migrate/20250823000000_create_easy_ml_columns.rb
278
+ - spec/db/migrate/20250824000000_create_test_articles.rb
279
+ - spec/db/migrate/20250824000001_create_test_categories.rb
280
+ - spec/db/migrate/20250825000000_create_bylines.rb
281
+ - spec/db/migrate/20250826000000_create_test_users.rb
282
+ - spec/db/migrate/20250826000001_create_test_user_histories.rb
283
+ - spec/db/migrate/20250826000002_create_test_websites.rb
284
+ - spec/db/migrate/20250826000003_create_test_website_histories.rb
265
285
  - spec/db/schema.rb
266
286
  - spec/factories/post.rb
267
- - spec/foreign_key_spec.rb
268
287
  - spec/historiographer_spec.rb
288
+ - spec/integration/historiographer_safe_integration_spec.rb
289
+ - spec/internal/app/models/application_record.rb
290
+ - spec/internal/app/models/deploy.rb
291
+ - spec/internal/app/models/user.rb
292
+ - spec/internal/app/models/website.rb
293
+ - spec/internal/app/models/website_history.rb
294
+ - spec/internal/config/database.yml
295
+ - spec/internal/config/routes.rb
296
+ - spec/internal/db/schema.rb
269
297
  - spec/models/application_record.rb
270
298
  - spec/models/author.rb
271
299
  - spec/models/author_history.rb
300
+ - spec/models/byline.rb
272
301
  - spec/models/comment.rb
273
302
  - spec/models/comment_history.rb
274
303
  - spec/models/easy_ml/column.rb
@@ -283,10 +312,19 @@ files:
283
312
  - spec/models/safe_post_history.rb
284
313
  - spec/models/silent_post.rb
285
314
  - spec/models/silent_post_history.rb
315
+ - spec/models/test_article.rb
316
+ - spec/models/test_article_history.rb
317
+ - spec/models/test_category.rb
318
+ - spec/models/test_category_history.rb
319
+ - spec/models/test_user.rb
320
+ - spec/models/test_user_history.rb
321
+ - spec/models/test_website.rb
322
+ - spec/models/test_website_history.rb
286
323
  - spec/models/thing_with_compound_index.rb
287
324
  - spec/models/thing_with_compound_index_history.rb
288
325
  - spec/models/thing_without_history.rb
289
326
  - spec/models/user.rb
327
+ - spec/rails_integration/historiographer_rails_integration_spec.rb
290
328
  - spec/spec_helper.rb
291
329
  homepage: http://github.com/brettshollenberger/historiographer
292
330
  licenses: