historiographer 4.4.2 → 4.4.4

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/DEVELOPMENT.md +124 -0
  3. data/Gemfile +2 -0
  4. data/README.md +16 -1
  5. data/Rakefile +68 -0
  6. data/VERSION +1 -1
  7. data/bin/console +10 -0
  8. data/bin/setup +15 -0
  9. data/bin/test +5 -0
  10. data/bin/test-all +10 -0
  11. data/bin/test-rails +5 -0
  12. data/historiographer.gemspec +52 -14
  13. data/lib/historiographer/history.rb +72 -37
  14. data/lib/historiographer.rb +5 -0
  15. data/spec/combustion_helper.rb +34 -0
  16. data/spec/db/migrate/20250826000000_create_test_users.rb +8 -0
  17. data/spec/db/migrate/20250826000001_create_test_user_histories.rb +18 -0
  18. data/spec/db/migrate/20250826000002_create_test_websites.rb +9 -0
  19. data/spec/db/migrate/20250826000003_create_test_website_histories.rb +19 -0
  20. data/spec/db/migrate/20250827000000_create_templates.rb +9 -0
  21. data/spec/db/migrate/20250827000001_create_template_histories.rb +9 -0
  22. data/spec/db/migrate/20250827000002_create_websites.rb +9 -0
  23. data/spec/db/migrate/20250827000003_create_website_histories.rb +9 -0
  24. data/spec/db/migrate/20250827000004_create_template_files.rb +15 -0
  25. data/spec/db/migrate/20250827000005_create_template_file_histories.rb +9 -0
  26. data/spec/db/migrate/20250827000006_create_website_files.rb +15 -0
  27. data/spec/db/migrate/20250827000007_create_website_file_histories.rb +9 -0
  28. data/spec/db/migrate/20250827000008_create_code_files_view.rb +62 -0
  29. data/spec/db/schema.rb +170 -1
  30. data/spec/examples.txt +71 -0
  31. data/spec/historiographer_spec.rb +164 -0
  32. data/spec/integration/historiographer_safe_integration_spec.rb +154 -0
  33. data/spec/internal/app/models/application_record.rb +5 -0
  34. data/spec/internal/app/models/deploy.rb +5 -0
  35. data/spec/internal/app/models/user.rb +4 -0
  36. data/spec/internal/app/models/website.rb +5 -0
  37. data/spec/internal/app/models/website_history.rb +7 -0
  38. data/spec/internal/config/database.yml +9 -0
  39. data/spec/internal/config/routes.rb +2 -0
  40. data/spec/internal/db/schema.rb +48 -0
  41. data/spec/internal/log/development.log +0 -0
  42. data/spec/internal/log/test.log +1479 -0
  43. data/spec/models/code_file.rb +16 -0
  44. data/spec/models/template.rb +6 -0
  45. data/spec/models/template_file.rb +5 -0
  46. data/spec/models/template_file_history.rb +3 -0
  47. data/spec/models/template_history.rb +3 -0
  48. data/spec/models/test_user.rb +4 -0
  49. data/spec/models/test_user_history.rb +3 -0
  50. data/spec/models/test_website.rb +4 -0
  51. data/spec/models/test_website_history.rb +3 -0
  52. data/spec/models/website.rb +7 -0
  53. data/spec/models/website_file.rb +5 -0
  54. data/spec/models/website_file_history.rb +3 -0
  55. data/spec/models/website_history.rb +3 -0
  56. data/spec/rails_integration/historiographer_rails_integration_spec.rb +106 -0
  57. data/spec/view_backed_model_spec.rb +166 -0
  58. metadata +55 -13
  59. data/.document +0 -5
  60. data/.rspec +0 -1
  61. data/.ruby-version +0 -1
  62. data/.standalone_migrations +0 -6
  63. data/Gemfile.lock +0 -341
  64. data/historiographer-4.1.12.gem +0 -0
  65. data/historiographer-4.1.13.gem +0 -0
  66. data/historiographer-4.1.14.gem +0 -0
  67. data/historiographer-4.3.0.gem +0 -0
  68. data/spec/foreign_key_spec.rb +0 -189
@@ -0,0 +1,16 @@
1
+ class CodeFile < ApplicationRecord
2
+ # This is a read-only model backed by a database view
3
+ # The view merges template_files and website_files
4
+
5
+ self.table_name = 'code_files'
6
+ self.primary_key = nil # View doesn't have a primary key
7
+
8
+ belongs_to :website
9
+
10
+ # Default ordering since the view has no primary key
11
+ default_scope { order(created_at: :desc, path: :asc) }
12
+
13
+ def readonly?
14
+ true
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ class Template < ApplicationRecord
2
+ include Historiographer
3
+
4
+ has_many :template_files, dependent: :destroy
5
+ has_many :websites
6
+ end
@@ -0,0 +1,5 @@
1
+ class TemplateFile < ApplicationRecord
2
+ include Historiographer
3
+
4
+ belongs_to :template
5
+ end
@@ -0,0 +1,3 @@
1
+ class TemplateFileHistory < ApplicationRecord
2
+ include Historiographer::History
3
+ end
@@ -0,0 +1,3 @@
1
+ class TemplateHistory < ApplicationRecord
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,7 @@
1
+ class Website < ApplicationRecord
2
+ include Historiographer
3
+
4
+ belongs_to :template, optional: true
5
+ has_many :website_files, dependent: :destroy
6
+ has_many :code_files
7
+ end
@@ -0,0 +1,5 @@
1
+ class WebsiteFile < ApplicationRecord
2
+ include Historiographer
3
+
4
+ belongs_to :website
5
+ end
@@ -0,0 +1,3 @@
1
+ class WebsiteFileHistory < ApplicationRecord
2
+ include Historiographer::History
3
+ end
@@ -0,0 +1,3 @@
1
+ class WebsiteHistory < 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
@@ -0,0 +1,166 @@
1
+ require 'spec_helper'
2
+
3
+ # Load model classes
4
+ require_relative 'models/template'
5
+ require_relative 'models/template_history'
6
+ require_relative 'models/website'
7
+ require_relative 'models/website_history'
8
+ require_relative 'models/template_file'
9
+ require_relative 'models/template_file_history'
10
+ require_relative 'models/website_file'
11
+ require_relative 'models/website_file_history'
12
+ require_relative 'models/code_file'
13
+
14
+ describe 'View-backed model snapshotting' do
15
+ describe 'Website with code_files association (view-backed)' do
16
+ let(:template) {
17
+ Template.create!(
18
+ name: 'Base Template',
19
+ description: 'A base template',
20
+ history_user_id: 1
21
+ )
22
+ }
23
+ let(:website) {
24
+ Website.create!(
25
+ domain: 'example.com',
26
+ template: template,
27
+ history_user_id: 1
28
+ )
29
+ }
30
+
31
+ before do
32
+ # Create template files
33
+ @template_file1 = TemplateFile.create!(
34
+ template: template,
35
+ path: 'index.html',
36
+ content: '<h1>Template Index</h1>',
37
+ shasum: 'abc123',
38
+ history_user_id: 1
39
+ )
40
+
41
+ @template_file2 = TemplateFile.create!(
42
+ template: template,
43
+ path: 'about.html',
44
+ content: '<h1>Template About</h1>',
45
+ shasum: 'def456',
46
+ history_user_id: 1
47
+ )
48
+
49
+ # Create website file that overrides one template file
50
+ @website_file = WebsiteFile.create!(
51
+ website: website,
52
+ path: 'index.html',
53
+ content: '<h1>Custom Index</h1>',
54
+ shasum: 'ghi789',
55
+ history_user_id: 1
56
+ )
57
+ end
58
+
59
+ it 'has code_files that are backed by a view' do
60
+ # Verify that code_files exist
61
+ expect(website.code_files.count).to eq(2)
62
+
63
+ # Verify that CodeFile has no primary key
64
+ expect(CodeFile.primary_key).to be_nil
65
+
66
+ # Verify that CodeFile instances are read-only
67
+ code_file = website.code_files.first
68
+ expect(code_file.readonly?).to be true
69
+ end
70
+
71
+ it 'returns correct merged data from the view' do
72
+ code_files = website.code_files.order(:path)
73
+
74
+ # Should have about.html from template and index.html from website override
75
+ expect(code_files.map(&:path)).to match_array(['about.html', 'index.html'])
76
+
77
+ index_file = code_files.find { |cf| cf.path == 'index.html' }
78
+ about_file = code_files.find { |cf| cf.path == 'about.html' }
79
+
80
+ # index.html should come from website_files (override)
81
+ expect(index_file.content).to eq('<h1>Custom Index</h1>')
82
+ expect(index_file.source_type).to eq('WebsiteFile')
83
+ expect(index_file.source_id).to eq(@website_file.id)
84
+
85
+ # about.html should come from template_files
86
+ expect(about_file.content).to eq('<h1>Template About</h1>')
87
+ expect(about_file.source_type).to eq('TemplateFile')
88
+ expect(about_file.source_id).to eq(@template_file2.id)
89
+ end
90
+
91
+ context 'when snapshotting a model with view-backed associations' do
92
+ it 'creates snapshot for the main model but handles view-backed associations gracefully' do
93
+ # Create a snapshot of the website
94
+ expect { website.snapshot }.not_to raise_error
95
+
96
+ # Verify that website history was created
97
+ expect(WebsiteHistory.where(website_id: website.id)).not_to be_empty
98
+ website_snapshot = WebsiteHistory.where(website_id: website.id).last
99
+ expect(website_snapshot.snapshot_id).not_to be_nil
100
+
101
+ # Verify that associated models with primary keys have histories
102
+ expect(TemplateHistory.where(template_id: template.id)).not_to be_empty
103
+ expect(WebsiteFileHistory.where(website_file_id: @website_file.id)).not_to be_empty
104
+ expect(TemplateFileHistory.where(template_file_id: @template_file1.id)).not_to be_empty
105
+ end
106
+
107
+ it 'does not attempt to create history for view-backed models' do
108
+ # Snapshot should succeed without trying to snapshot code_files
109
+ expect { website.snapshot }.not_to raise_error
110
+
111
+ # Verify snapshot was created
112
+ snapshot = WebsiteHistory.where(website_id: website.id).where.not(snapshot_id: nil).last
113
+ expect(snapshot).not_to be_nil
114
+
115
+ # There should be no CodeFileHistory table/model
116
+ expect { CodeFileHistory }.to raise_error(NameError)
117
+ end
118
+
119
+ it 'correctly identifies models without primary keys' do
120
+ # CodeFile should not have a primary key
121
+ expect(CodeFile.primary_key).to be_nil
122
+
123
+ # Regular models should have primary keys
124
+ expect(Website.primary_key).to eq('id')
125
+ expect(WebsiteFile.primary_key).to eq('id')
126
+ expect(TemplateFile.primary_key).to eq('id')
127
+ end
128
+
129
+ it 'allows snapshot to complete even with view associations present' do
130
+ # Add more associations to make the test more complex
131
+ website2 = Website.create!(domain: 'example2.com', template: template, history_user_id: 1)
132
+ WebsiteFile.create!(
133
+ website: website2,
134
+ path: 'custom.html',
135
+ content: '<h1>Custom Page</h1>',
136
+ shasum: 'xyz999',
137
+ history_user_id: 1
138
+ )
139
+
140
+ # Both websites should snapshot successfully
141
+ expect { website.snapshot }.not_to raise_error
142
+ expect { website2.snapshot }.not_to raise_error
143
+
144
+ # Verify both have history records
145
+ expect(WebsiteHistory.where(website_id: website.id)).not_to be_empty
146
+ expect(WebsiteHistory.where(website_id: website2.id)).not_to be_empty
147
+ end
148
+ end
149
+ end
150
+
151
+ describe 'Error handling for view-backed models' do
152
+ it 'should log a warning when attempting to snapshot a model without a primary key' do
153
+ # We'll need to patch the snapshot method to detect and skip models without primary keys
154
+ # This test verifies the expected behavior once the fix is implemented
155
+
156
+ website = Website.create!(domain: 'test.com', history_user_id: 1)
157
+
158
+ # Expect snapshot to complete successfully
159
+ expect { website.snapshot }.not_to raise_error
160
+
161
+ # The view-backed association should not have created any history
162
+ # (since there's no history table for views)
163
+ expect(WebsiteHistory.where(website_id: website.id).count).to eq(1)
164
+ end
165
+ end
166
+ 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.2
4
+ version: 4.4.4
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-22 00:00:00.000000000 Z
11
+ date: 2025-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -208,27 +208,29 @@ 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
215
220
  - README.md
216
221
  files:
217
- - ".document"
218
- - ".rspec"
219
- - ".ruby-version"
220
- - ".standalone_migrations"
222
+ - DEVELOPMENT.md
221
223
  - Gemfile
222
- - Gemfile.lock
223
224
  - Guardfile
224
225
  - LICENSE.txt
225
226
  - README.md
226
227
  - Rakefile
227
228
  - VERSION
228
- - historiographer-4.1.12.gem
229
- - historiographer-4.1.13.gem
230
- - historiographer-4.1.14.gem
231
- - historiographer-4.3.0.gem
229
+ - bin/console
230
+ - bin/setup
231
+ - bin/test
232
+ - bin/test-all
233
+ - bin/test-rails
232
234
  - historiographer.gemspec
233
235
  - init.rb
234
236
  - instructions/implementation.md
@@ -244,6 +246,7 @@ files:
244
246
  - lib/historiographer/safe.rb
245
247
  - lib/historiographer/silent.rb
246
248
  - lib/historiographer/version.rb
249
+ - spec/combustion_helper.rb
247
250
  - spec/db/database.yml
248
251
  - spec/db/migrate/20161121212228_create_posts.rb
249
252
  - spec/db/migrate/20161121212229_create_post_histories.rb
@@ -266,14 +269,39 @@ files:
266
269
  - spec/db/migrate/20250824000000_create_test_articles.rb
267
270
  - spec/db/migrate/20250824000001_create_test_categories.rb
268
271
  - spec/db/migrate/20250825000000_create_bylines.rb
272
+ - spec/db/migrate/20250826000000_create_test_users.rb
273
+ - spec/db/migrate/20250826000001_create_test_user_histories.rb
274
+ - spec/db/migrate/20250826000002_create_test_websites.rb
275
+ - spec/db/migrate/20250826000003_create_test_website_histories.rb
276
+ - spec/db/migrate/20250827000000_create_templates.rb
277
+ - spec/db/migrate/20250827000001_create_template_histories.rb
278
+ - spec/db/migrate/20250827000002_create_websites.rb
279
+ - spec/db/migrate/20250827000003_create_website_histories.rb
280
+ - spec/db/migrate/20250827000004_create_template_files.rb
281
+ - spec/db/migrate/20250827000005_create_template_file_histories.rb
282
+ - spec/db/migrate/20250827000006_create_website_files.rb
283
+ - spec/db/migrate/20250827000007_create_website_file_histories.rb
284
+ - spec/db/migrate/20250827000008_create_code_files_view.rb
269
285
  - spec/db/schema.rb
286
+ - spec/examples.txt
270
287
  - spec/factories/post.rb
271
- - spec/foreign_key_spec.rb
272
288
  - spec/historiographer_spec.rb
289
+ - spec/integration/historiographer_safe_integration_spec.rb
290
+ - spec/internal/app/models/application_record.rb
291
+ - spec/internal/app/models/deploy.rb
292
+ - spec/internal/app/models/user.rb
293
+ - spec/internal/app/models/website.rb
294
+ - spec/internal/app/models/website_history.rb
295
+ - spec/internal/config/database.yml
296
+ - spec/internal/config/routes.rb
297
+ - spec/internal/db/schema.rb
298
+ - spec/internal/log/development.log
299
+ - spec/internal/log/test.log
273
300
  - spec/models/application_record.rb
274
301
  - spec/models/author.rb
275
302
  - spec/models/author_history.rb
276
303
  - spec/models/byline.rb
304
+ - spec/models/code_file.rb
277
305
  - spec/models/comment.rb
278
306
  - spec/models/comment_history.rb
279
307
  - spec/models/easy_ml/column.rb
@@ -288,15 +316,29 @@ files:
288
316
  - spec/models/safe_post_history.rb
289
317
  - spec/models/silent_post.rb
290
318
  - spec/models/silent_post_history.rb
319
+ - spec/models/template.rb
320
+ - spec/models/template_file.rb
321
+ - spec/models/template_file_history.rb
322
+ - spec/models/template_history.rb
291
323
  - spec/models/test_article.rb
292
324
  - spec/models/test_article_history.rb
293
325
  - spec/models/test_category.rb
294
326
  - spec/models/test_category_history.rb
327
+ - spec/models/test_user.rb
328
+ - spec/models/test_user_history.rb
329
+ - spec/models/test_website.rb
330
+ - spec/models/test_website_history.rb
295
331
  - spec/models/thing_with_compound_index.rb
296
332
  - spec/models/thing_with_compound_index_history.rb
297
333
  - spec/models/thing_without_history.rb
298
334
  - spec/models/user.rb
335
+ - spec/models/website.rb
336
+ - spec/models/website_file.rb
337
+ - spec/models/website_file_history.rb
338
+ - spec/models/website_history.rb
339
+ - spec/rails_integration/historiographer_rails_integration_spec.rb
299
340
  - spec/spec_helper.rb
341
+ - spec/view_backed_model_spec.rb
300
342
  homepage: http://github.com/brettshollenberger/historiographer
301
343
  licenses:
302
344
  - MIT
data/.document DELETED
@@ -1,5 +0,0 @@
1
- lib/**/*.rb
2
- bin/*
3
- -
4
- features/**/*.feature
5
- LICENSE.txt
data/.rspec DELETED
@@ -1 +0,0 @@
1
- --color
data/.ruby-version DELETED
@@ -1 +0,0 @@
1
- 3.0.2
@@ -1,6 +0,0 @@
1
- db:
2
- seeds: spec/db/seeds.rb
3
- migrate: spec/db/migrate
4
- schema: spec/db/schema.rb
5
- config:
6
- database: spec/db/database.yml