historiographer 4.3.0 → 4.4.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 +4 -4
- data/VERSION +1 -1
- data/historiographer-4.3.0.gem +0 -0
- data/historiographer.gemspec +4 -2
- data/lib/historiographer/history.rb +68 -15
- data/spec/foreign_key_spec.rb +189 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e095861ef6cd8df461a227897f73a7f0055c1ed1cc337ec6025be60fa3755a91
|
4
|
+
data.tar.gz: dc5272631bc5cf63cc55495842cc3a4407a59729b011499ca34e4248fc75d10d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d165aa2cc4f216c3abb3c30cf0b456fc517d3ea4ce9aed12afb109f0ecc01e95a8ab2affb374602b2d6fec9ed9983d1d28831365f768c64c3e785903c5a455a5
|
7
|
+
data.tar.gz: 8a6a3a0ecae45fe220d99f8644a925103aaf686c2048ca8c75cb26e3d785f939affb72fb732c7997346bd1d4ed7cf1029f293178b61c5ef1c7f677850859a140
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
4.
|
1
|
+
4.4.0
|
Binary file
|
data/historiographer.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: historiographer 4.
|
5
|
+
# stub: historiographer 4.4.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "historiographer".freeze
|
9
|
-
s.version = "4.
|
9
|
+
s.version = "4.4.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib".freeze]
|
@@ -33,6 +33,7 @@ Gem::Specification.new do |s|
|
|
33
33
|
"historiographer-4.1.12.gem",
|
34
34
|
"historiographer-4.1.13.gem",
|
35
35
|
"historiographer-4.1.14.gem",
|
36
|
+
"historiographer-4.3.0.gem",
|
36
37
|
"historiographer.gemspec",
|
37
38
|
"init.rb",
|
38
39
|
"instructions/implementation.md",
|
@@ -68,6 +69,7 @@ Gem::Specification.new do |s|
|
|
68
69
|
"spec/db/migrate/2025082100001_create_project_files.rb",
|
69
70
|
"spec/db/schema.rb",
|
70
71
|
"spec/factories/post.rb",
|
72
|
+
"spec/foreign_key_spec.rb",
|
71
73
|
"spec/historiographer_spec.rb",
|
72
74
|
"spec/models/application_record.rb",
|
73
75
|
"spec/models/author.rb",
|
@@ -230,6 +230,9 @@ module Historiographer
|
|
230
230
|
.order('snapshot_id, history_started_at DESC, id DESC')
|
231
231
|
}
|
232
232
|
|
233
|
+
# Track custom association methods
|
234
|
+
base.class_variable_set(:@@history_association_methods, [])
|
235
|
+
|
233
236
|
# Dynamically define associations on the history class
|
234
237
|
foreign_class.reflect_on_all_associations.each do |association|
|
235
238
|
define_history_association(association)
|
@@ -281,20 +284,64 @@ module Historiographer
|
|
281
284
|
assoc_class = assoc_history_class_name.safe_constantize || OpenStruct.new(name: association.class_name)
|
282
285
|
assoc_class_name = assoc_class.name
|
283
286
|
|
284
|
-
# Define the scope to filter by snapshot_id for history associations
|
285
|
-
scope = if assoc_class_name.match?(/History/)
|
286
|
-
->(history_instance) { where(snapshot_id: history_instance.snapshot_id) }
|
287
|
-
else
|
288
|
-
->(history_instance) { all }
|
289
|
-
end
|
290
|
-
|
291
287
|
case association.macro
|
292
288
|
when :belongs_to
|
293
|
-
belongs_to
|
289
|
+
# For belongs_to associations, if the target is a history class, we need special handling
|
290
|
+
if assoc_class_name.match?(/History/)
|
291
|
+
# Override the association method to filter by snapshot_id
|
292
|
+
# The history class uses <model>_id as the foreign key (e.g., author_id for AuthorHistory)
|
293
|
+
history_fk = association.class_name.gsub(/History$/, '').underscore + '_id'
|
294
|
+
|
295
|
+
# Track this custom method
|
296
|
+
methods_list = class_variable_get(:@@history_association_methods) rescue []
|
297
|
+
methods_list << assoc_name
|
298
|
+
class_variable_set(:@@history_association_methods, methods_list)
|
299
|
+
|
300
|
+
define_method(assoc_name) do
|
301
|
+
return nil unless self[assoc_foreign_key]
|
302
|
+
assoc_class.where(
|
303
|
+
history_fk => self[assoc_foreign_key],
|
304
|
+
snapshot_id: self.snapshot_id
|
305
|
+
).first
|
306
|
+
end
|
307
|
+
else
|
308
|
+
belongs_to assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key
|
309
|
+
end
|
294
310
|
when :has_one
|
295
|
-
|
311
|
+
if assoc_class_name.match?(/History/)
|
312
|
+
hfk = history_foreign_key
|
313
|
+
|
314
|
+
# Track this custom method
|
315
|
+
methods_list = class_variable_get(:@@history_association_methods) rescue []
|
316
|
+
methods_list << assoc_name
|
317
|
+
class_variable_set(:@@history_association_methods, methods_list)
|
318
|
+
|
319
|
+
define_method(assoc_name) do
|
320
|
+
assoc_class.where(
|
321
|
+
assoc_foreign_key => self[hfk],
|
322
|
+
snapshot_id: self.snapshot_id
|
323
|
+
).first
|
324
|
+
end
|
325
|
+
else
|
326
|
+
has_one assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
|
327
|
+
end
|
296
328
|
when :has_many
|
297
|
-
|
329
|
+
if assoc_class_name.match?(/History/)
|
330
|
+
hfk = history_foreign_key
|
331
|
+
# Track this custom method
|
332
|
+
methods_list = class_variable_get(:@@history_association_methods) rescue []
|
333
|
+
methods_list << assoc_name
|
334
|
+
class_variable_set(:@@history_association_methods, methods_list)
|
335
|
+
|
336
|
+
define_method(assoc_name) do
|
337
|
+
assoc_class.where(
|
338
|
+
assoc_foreign_key => self[hfk],
|
339
|
+
snapshot_id: self.snapshot_id
|
340
|
+
)
|
341
|
+
end
|
342
|
+
else
|
343
|
+
has_many assoc_name, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
|
344
|
+
end
|
298
345
|
end
|
299
346
|
end
|
300
347
|
|
@@ -360,13 +407,19 @@ module Historiographer
|
|
360
407
|
end
|
361
408
|
end
|
362
409
|
|
363
|
-
# For each association in the history class
|
364
|
-
self.class.reflect_on_all_associations.
|
410
|
+
# For each association in the history class (including custom methods)
|
411
|
+
associations_to_forward = self.class.reflect_on_all_associations.map(&:name)
|
412
|
+
|
413
|
+
# Add custom association methods
|
414
|
+
custom_methods = self.class.class_variable_get(:@@history_association_methods) rescue []
|
415
|
+
associations_to_forward += custom_methods
|
416
|
+
|
417
|
+
associations_to_forward.uniq.each do |assoc_name|
|
365
418
|
# Define a method that forwards to the history association
|
366
419
|
instance.singleton_class.class_eval do
|
367
|
-
define_method(
|
368
|
-
history_instance =
|
369
|
-
history_instance.send(
|
420
|
+
define_method(assoc_name) do |*args, &block|
|
421
|
+
history_instance = instance_variable_get(:@_history_instance)
|
422
|
+
history_instance.send(assoc_name, *args, &block)
|
370
423
|
end
|
371
424
|
end
|
372
425
|
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe 'Foreign key handling for belongs_to associations' do
|
4
|
+
before(:all) do
|
5
|
+
@original_stdout = $stdout
|
6
|
+
$stdout = StringIO.new
|
7
|
+
|
8
|
+
ActiveRecord::Base.connection.create_table :test_users, force: true do |t|
|
9
|
+
t.string :name
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
|
13
|
+
ActiveRecord::Base.connection.create_table :test_websites, force: true do |t|
|
14
|
+
t.string :name
|
15
|
+
t.integer :user_id
|
16
|
+
t.timestamps
|
17
|
+
end
|
18
|
+
|
19
|
+
ActiveRecord::Base.connection.create_table :test_website_histories, force: true do |t|
|
20
|
+
t.integer :test_website_id, null: false
|
21
|
+
t.string :name
|
22
|
+
t.integer :user_id
|
23
|
+
t.timestamps
|
24
|
+
t.datetime :history_started_at, null: false
|
25
|
+
t.datetime :history_ended_at
|
26
|
+
t.integer :history_user_id
|
27
|
+
t.string :snapshot_id
|
28
|
+
|
29
|
+
t.index :test_website_id
|
30
|
+
t.index :history_started_at
|
31
|
+
t.index :history_ended_at
|
32
|
+
t.index :snapshot_id
|
33
|
+
end
|
34
|
+
|
35
|
+
ActiveRecord::Base.connection.create_table :test_user_histories, force: true do |t|
|
36
|
+
t.integer :test_user_id, null: false
|
37
|
+
t.string :name
|
38
|
+
t.timestamps
|
39
|
+
t.datetime :history_started_at, null: false
|
40
|
+
t.datetime :history_ended_at
|
41
|
+
t.integer :history_user_id
|
42
|
+
t.string :snapshot_id
|
43
|
+
|
44
|
+
t.index :test_user_id
|
45
|
+
t.index :history_started_at
|
46
|
+
t.index :history_ended_at
|
47
|
+
t.index :snapshot_id
|
48
|
+
end
|
49
|
+
|
50
|
+
class TestUser < ActiveRecord::Base
|
51
|
+
include Historiographer
|
52
|
+
has_many :test_websites, foreign_key: 'user_id'
|
53
|
+
end
|
54
|
+
|
55
|
+
class TestWebsite < ActiveRecord::Base
|
56
|
+
include Historiographer
|
57
|
+
belongs_to :user, class_name: 'TestUser', foreign_key: 'user_id', optional: true
|
58
|
+
end
|
59
|
+
|
60
|
+
class TestWebsiteHistory < ActiveRecord::Base
|
61
|
+
include Historiographer::History
|
62
|
+
end
|
63
|
+
|
64
|
+
class TestUserHistory < ActiveRecord::Base
|
65
|
+
include Historiographer::History
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
after(:all) do
|
70
|
+
$stdout = @original_stdout
|
71
|
+
ActiveRecord::Base.connection.drop_table :test_website_histories
|
72
|
+
ActiveRecord::Base.connection.drop_table :test_websites
|
73
|
+
ActiveRecord::Base.connection.drop_table :test_user_histories
|
74
|
+
ActiveRecord::Base.connection.drop_table :test_users
|
75
|
+
Object.send(:remove_const, :TestWebsite) if Object.const_defined?(:TestWebsite)
|
76
|
+
Object.send(:remove_const, :TestWebsiteHistory) if Object.const_defined?(:TestWebsiteHistory)
|
77
|
+
Object.send(:remove_const, :TestUser) if Object.const_defined?(:TestUser)
|
78
|
+
Object.send(:remove_const, :TestUserHistory) if Object.const_defined?(:TestUserHistory)
|
79
|
+
end
|
80
|
+
|
81
|
+
describe 'belongs_to association on history models' do
|
82
|
+
it 'does not raise error about wrong column when accessing belongs_to associations' do
|
83
|
+
# This is the core issue: when a history model has a belongs_to association,
|
84
|
+
# it should not use the foreign key as the primary key for lookups
|
85
|
+
|
86
|
+
# Create a user
|
87
|
+
user = TestUser.create!(name: 'Test User', history_user_id: 1)
|
88
|
+
|
89
|
+
# Create a website belonging to the user
|
90
|
+
website = TestWebsite.create!(
|
91
|
+
name: 'Test Website',
|
92
|
+
user_id: user.id,
|
93
|
+
history_user_id: 1
|
94
|
+
)
|
95
|
+
|
96
|
+
# Get the website history
|
97
|
+
website_history = TestWebsiteHistory.last
|
98
|
+
|
99
|
+
# The history should have the correct user_id
|
100
|
+
expect(website_history.user_id).to eq(user.id)
|
101
|
+
|
102
|
+
# The belongs_to association should work without errors
|
103
|
+
# Previously this would fail with "column users.user_id does not exist"
|
104
|
+
# because it was using primary_key: :user_id instead of the default :id
|
105
|
+
expect { website_history.user }.not_to raise_error
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'allows direct creation of history records with foreign keys' do
|
109
|
+
user = TestUser.create!(name: 'Another User', history_user_id: 1)
|
110
|
+
|
111
|
+
# Create history attributes like in the original error case
|
112
|
+
attrs = {
|
113
|
+
"name" => "test.example",
|
114
|
+
"user_id" => user.id,
|
115
|
+
"created_at" => Time.now,
|
116
|
+
"updated_at" => Time.now,
|
117
|
+
"test_website_id" => 100,
|
118
|
+
"history_started_at" => Time.now,
|
119
|
+
"history_user_id" => 1,
|
120
|
+
"snapshot_id" => SecureRandom.uuid
|
121
|
+
}
|
122
|
+
|
123
|
+
# This should not raise an error about test_users.user_id not existing
|
124
|
+
# The original bug was that it would look for test_users.user_id instead of test_users.id
|
125
|
+
expect { TestWebsiteHistory.create!(attrs) }.not_to raise_error
|
126
|
+
|
127
|
+
history = TestWebsiteHistory.last
|
128
|
+
expect(history.user_id).to eq(user.id)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe 'snapshot associations with history models' do
|
133
|
+
it 'correctly filters associations by snapshot_id when using custom association methods' do
|
134
|
+
# First create regular history records
|
135
|
+
user = TestUser.create!(name: 'User One', history_user_id: 1)
|
136
|
+
website = TestWebsite.create!(
|
137
|
+
name: 'Website One',
|
138
|
+
user_id: user.id,
|
139
|
+
history_user_id: 1
|
140
|
+
)
|
141
|
+
|
142
|
+
# Check that regular histories were created
|
143
|
+
expect(TestUserHistory.count).to eq(1)
|
144
|
+
expect(TestWebsiteHistory.count).to eq(1)
|
145
|
+
|
146
|
+
# Now create snapshot histories directly (simulating what snapshot would do)
|
147
|
+
snapshot_id = SecureRandom.uuid
|
148
|
+
|
149
|
+
# Create user history with snapshot
|
150
|
+
user_snapshot = TestUserHistory.create!(
|
151
|
+
test_user_id: user.id,
|
152
|
+
name: user.name,
|
153
|
+
created_at: user.created_at,
|
154
|
+
updated_at: user.updated_at,
|
155
|
+
history_started_at: Time.now,
|
156
|
+
history_user_id: 1,
|
157
|
+
snapshot_id: snapshot_id
|
158
|
+
)
|
159
|
+
|
160
|
+
# Create website history with snapshot
|
161
|
+
website_snapshot = TestWebsiteHistory.create!(
|
162
|
+
test_website_id: website.id,
|
163
|
+
name: website.name,
|
164
|
+
user_id: user.id,
|
165
|
+
created_at: website.created_at,
|
166
|
+
updated_at: website.updated_at,
|
167
|
+
history_started_at: Time.now,
|
168
|
+
history_user_id: 1,
|
169
|
+
snapshot_id: snapshot_id
|
170
|
+
)
|
171
|
+
|
172
|
+
# Now test that the association filtering works
|
173
|
+
# The website history's user association should find the user history with the same snapshot_id
|
174
|
+
user_from_association = website_snapshot.user
|
175
|
+
|
176
|
+
# Since user association points to history when snapshots are involved,
|
177
|
+
# it should return the TestUserHistory with matching snapshot_id
|
178
|
+
if user_from_association.is_a?(TestUserHistory)
|
179
|
+
expect(user_from_association.snapshot_id).to eq(snapshot_id)
|
180
|
+
expect(user_from_association.name).to eq('User One')
|
181
|
+
else
|
182
|
+
# If it returns the regular TestUser (non-history), that's also acceptable
|
183
|
+
# as long as it doesn't error
|
184
|
+
expect(user_from_association).to be_a(TestUser)
|
185
|
+
expect(user_from_association.name).to eq('User One')
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: historiographer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.
|
4
|
+
version: 4.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- brettshollenberger
|
@@ -228,6 +228,7 @@ files:
|
|
228
228
|
- historiographer-4.1.12.gem
|
229
229
|
- historiographer-4.1.13.gem
|
230
230
|
- historiographer-4.1.14.gem
|
231
|
+
- historiographer-4.3.0.gem
|
231
232
|
- historiographer.gemspec
|
232
233
|
- init.rb
|
233
234
|
- instructions/implementation.md
|
@@ -263,6 +264,7 @@ files:
|
|
263
264
|
- spec/db/migrate/2025082100001_create_project_files.rb
|
264
265
|
- spec/db/schema.rb
|
265
266
|
- spec/factories/post.rb
|
267
|
+
- spec/foreign_key_spec.rb
|
266
268
|
- spec/historiographer_spec.rb
|
267
269
|
- spec/models/application_record.rb
|
268
270
|
- spec/models/author.rb
|