historiographer 4.1.7 → 4.1.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/historiographer/history.rb +125 -38
- data/lib/historiographer/version.rb +1 -1
- data/lib/historiographer.rb +8 -3
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0a2f100a48aa0554022b2b53d7dd09cb296b298b6c6d92e6dae5b824cccd600
|
4
|
+
data.tar.gz: 6472d913b3d16ae9b5adc2414d1bbc0eaab9d62f632d497e475fd35291b90999
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4dcc56d605f9c67ff7e00de652a9d180034e7c41c911d44e4c697a8843903a0da8669a7bf02a72877566835c5acc937f3a61c674dc206fc51e826720b9ff689
|
7
|
+
data.tar.gz: c16605025d81e61edd43476e8c8b0e85035aacd496b071ab13cec7eeabc5469297b1342df7b3e352dfc8eee4999ee871361aebf6e16e324b0020733f02bf5cbc
|
@@ -61,7 +61,7 @@ module Historiographer
|
|
61
61
|
extend ActiveSupport::Concern
|
62
62
|
|
63
63
|
included do |base|
|
64
|
-
clear_validators!
|
64
|
+
clear_validators! if respond_to?(:clear_validators!)
|
65
65
|
#
|
66
66
|
# A History class (e.g. RetailerProductHistory) will gain
|
67
67
|
# access to a current scope, returning
|
@@ -69,17 +69,6 @@ module Historiographer
|
|
69
69
|
#
|
70
70
|
scope :current, -> { where(history_ended_at: nil).order(id: :desc) }
|
71
71
|
|
72
|
-
#
|
73
|
-
# A History class will be linked to the user
|
74
|
-
# that made the changes.
|
75
|
-
#
|
76
|
-
# E.g.
|
77
|
-
#
|
78
|
-
# RetailerProductHistory.first.user
|
79
|
-
#
|
80
|
-
# To use histories, a user class must be defined.
|
81
|
-
#
|
82
|
-
belongs_to :user, foreign_key: :history_user_id
|
83
72
|
|
84
73
|
#
|
85
74
|
# Historiographer is opinionated about how History classes
|
@@ -94,6 +83,21 @@ module Historiographer
|
|
94
83
|
|
95
84
|
# Store the original class for method delegation
|
96
85
|
class_variable_set(:@@original_class, foreign_class)
|
86
|
+
class_variable_set(:@@method_map, {})
|
87
|
+
|
88
|
+
#
|
89
|
+
# A History class will be linked to the user
|
90
|
+
# that made the changes.
|
91
|
+
#
|
92
|
+
# E.g.
|
93
|
+
#
|
94
|
+
# RetailerProductHistory.first.user
|
95
|
+
#
|
96
|
+
# To use histories, a user class must be defined.
|
97
|
+
#
|
98
|
+
unless foreign_class.ancestors.include?(Historiographer::Silent)
|
99
|
+
belongs_to :user, foreign_key: :history_user_id
|
100
|
+
end
|
97
101
|
|
98
102
|
# Add method_added hook to the original class
|
99
103
|
foreign_class.singleton_class.class_eval do
|
@@ -102,7 +106,6 @@ module Historiographer
|
|
102
106
|
alias_method :original_method_added, :method_added
|
103
107
|
end
|
104
108
|
|
105
|
-
method_map = Hash.new(0)
|
106
109
|
define_method(:method_added) do |method_name|
|
107
110
|
# Skip if we're already in the process of defining a method
|
108
111
|
return if Thread.current[:defining_historiographer_method]
|
@@ -121,6 +124,7 @@ module Historiographer
|
|
121
124
|
return if foreign_class.history_class.method_defined?(method_name)
|
122
125
|
|
123
126
|
# Define the method in the history class
|
127
|
+
foreign_class.history_class.set_method_map(method_name, false)
|
124
128
|
foreign_class.history_class.class_eval do
|
125
129
|
define_method(method_name) do |*args, &block|
|
126
130
|
forward_method(method_name, *args, &block)
|
@@ -239,9 +243,38 @@ module Historiographer
|
|
239
243
|
define_history_association(association)
|
240
244
|
end
|
241
245
|
|
246
|
+
def snapshot
|
247
|
+
raise "Cannot snapshot a history model!"
|
248
|
+
end
|
249
|
+
|
250
|
+
def is_history_class?
|
251
|
+
true
|
252
|
+
end
|
253
|
+
|
242
254
|
end
|
243
255
|
|
244
256
|
class_methods do
|
257
|
+
def is_history_class?
|
258
|
+
true
|
259
|
+
end
|
260
|
+
|
261
|
+
def method_added(method_name)
|
262
|
+
set_method_map(method_name, true)
|
263
|
+
end
|
264
|
+
|
265
|
+
def set_method_map(method_name, is_overridden)
|
266
|
+
mm = method_map
|
267
|
+
mm[method_name.to_sym] = is_overridden
|
268
|
+
class_variable_set(:@@method_map, mm)
|
269
|
+
end
|
270
|
+
|
271
|
+
def method_map
|
272
|
+
unless class_variable_defined?(:@@method_map)
|
273
|
+
class_variable_set(:@@method_map, {})
|
274
|
+
end
|
275
|
+
class_variable_get(:@@method_map) || {}
|
276
|
+
end
|
277
|
+
|
245
278
|
def original_class
|
246
279
|
unless class_variable_defined?(:@@original_class)
|
247
280
|
class_variable_set(:@@original_class, self.name.gsub(/History$/, '').constantize)
|
@@ -255,40 +288,41 @@ module Historiographer
|
|
255
288
|
association = original_class.reflect_on_association(association)
|
256
289
|
end
|
257
290
|
assoc_name = association.name
|
291
|
+
assoc_module = association.active_record.module_parent
|
258
292
|
assoc_history_class_name = "#{association.class_name}History"
|
259
|
-
assoc_foreign_key = association.foreign_key
|
260
293
|
|
261
|
-
|
262
|
-
|
294
|
+
begin
|
295
|
+
assoc_module.const_get(assoc_history_class_name)
|
296
|
+
assoc_history_class_name = "#{assoc_module}::#{assoc_history_class_name}" unless assoc_history_class_name.match?(Regexp.new("#{assoc_module}::"))
|
297
|
+
rescue
|
298
|
+
end
|
299
|
+
|
300
|
+
assoc_foreign_key = association.foreign_key
|
263
301
|
|
264
302
|
# Skip through associations to history classes to avoid infinite loops
|
265
303
|
return if association.class_name.end_with?('History')
|
266
304
|
|
267
|
-
#
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
#
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
305
|
+
# Always use the history class if it exists
|
306
|
+
assoc_class = assoc_history_class_name.safe_constantize || OpenStruct.new(name: association.class_name)
|
307
|
+
assoc_class_name = assoc_class.name
|
308
|
+
|
309
|
+
# Define the scope to filter by snapshot_id for history associations
|
310
|
+
scope = if assoc_class_name.match?(/History/)
|
311
|
+
->(history_instance) { where(snapshot_id: history_instance.snapshot_id) }
|
312
|
+
else
|
313
|
+
->(history_instance) { all }
|
314
|
+
end
|
276
315
|
|
277
316
|
case association.macro
|
278
317
|
when :belongs_to
|
279
|
-
belongs_to assoc_name,
|
280
|
-
where(snapshot_id: history_instance.snapshot_id)
|
281
|
-
}, class_name: assoc_history_class_name, foreign_key: assoc_foreign_key, primary_key: assoc_foreign_key
|
318
|
+
belongs_to assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: assoc_foreign_key
|
282
319
|
when :has_one
|
283
|
-
has_one assoc_name,
|
284
|
-
where(snapshot_id: history_instance.snapshot_id)
|
285
|
-
}, class_name: assoc_history_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
|
320
|
+
has_one assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
|
286
321
|
when :has_many
|
287
|
-
has_many assoc_name,
|
288
|
-
where(snapshot_id: history_instance.snapshot_id)
|
289
|
-
}, class_name: assoc_history_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
|
322
|
+
has_many assoc_name, scope, class_name: assoc_class_name, foreign_key: assoc_foreign_key, primary_key: history_foreign_key
|
290
323
|
end
|
291
324
|
end
|
325
|
+
|
292
326
|
#
|
293
327
|
# The foreign key to the primary class.
|
294
328
|
#
|
@@ -321,21 +355,74 @@ module Historiographer
|
|
321
355
|
def dummy_instance
|
322
356
|
return @dummy_instance if @dummy_instance
|
323
357
|
|
358
|
+
# Only exclude history-specific columns
|
324
359
|
cannot_keep_cols = %w(history_started_at history_ended_at history_user_id snapshot_id)
|
325
|
-
cannot_keep_cols += [self.class.inheritance_column.to_sym] if self.original_class.sti_enabled?
|
326
360
|
cannot_keep_cols += [self.class.history_foreign_key]
|
327
361
|
cannot_keep_cols.map!(&:to_s)
|
328
362
|
|
329
363
|
attrs = attributes.clone
|
330
364
|
attrs[original_class.primary_key] = attrs[self.class.history_foreign_key]
|
331
365
|
|
332
|
-
|
333
|
-
|
366
|
+
if original_class.sti_enabled?
|
367
|
+
# Remove History suffix from type if present
|
368
|
+
attrs[original_class.inheritance_column] = attrs[original_class.inheritance_column]&.gsub(/History$/, '')
|
369
|
+
end
|
370
|
+
|
371
|
+
# Create instance with all attributes except history-specific ones
|
372
|
+
instance = original_class.instantiate(attrs.except(*cannot_keep_cols))
|
373
|
+
|
374
|
+
if instance.valid?
|
375
|
+
if instance.send(original_class.primary_key).present?
|
376
|
+
instance.run_callbacks(:find)
|
377
|
+
end
|
378
|
+
instance.run_callbacks(:initialize)
|
379
|
+
end
|
380
|
+
|
381
|
+
# Filter out any methods that are not overridden on the history class
|
382
|
+
history_methods = self.class.instance_methods(false)
|
383
|
+
history_class_location = Module.const_source_location(self.class.name).first
|
384
|
+
history_methods.select! do |method|
|
385
|
+
self.class.instance_method(method).source_location.first == history_class_location
|
386
|
+
end
|
387
|
+
|
388
|
+
history_methods.each do |method_name|
|
389
|
+
instance.singleton_class.class_eval do
|
390
|
+
define_method(method_name) do |*args, &block|
|
391
|
+
history_instance = instance.instance_variable_get(:@_history_instance)
|
392
|
+
history_instance.send(method_name, *args, &block)
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# For each association in the history class
|
398
|
+
self.class.reflect_on_all_associations.each do |reflection|
|
399
|
+
# Define a method that forwards to the history association
|
400
|
+
instance.singleton_class.class_eval do
|
401
|
+
define_method(reflection.name) do |*args, &block|
|
402
|
+
history_instance = instance.instance_variable_get(:@_history_instance)
|
403
|
+
history_instance.send(reflection.name, *args, &block)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
# Override class method to return history class
|
409
|
+
instance.singleton_class.class_eval do
|
410
|
+
define_method(:class) do
|
411
|
+
history_instance = instance.instance_variable_get(:@_history_instance)
|
412
|
+
history_instance.class
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
instance.instance_variable_set(:@_history_instance, self)
|
334
417
|
@dummy_instance = instance
|
335
418
|
end
|
336
419
|
|
337
420
|
def forward_method(method_name, *args, &block)
|
338
|
-
|
421
|
+
if method_name == :class || method_name == 'class'
|
422
|
+
self.class
|
423
|
+
else
|
424
|
+
dummy_instance.send(method_name, *args, &block)
|
425
|
+
end
|
339
426
|
end
|
340
427
|
end
|
341
428
|
end
|
data/lib/historiographer.rb
CHANGED
@@ -216,21 +216,25 @@ module Historiographer
|
|
216
216
|
base.singleton_class.prepend(Module.new do
|
217
217
|
def belongs_to(name, scope = nil, **options, &extension)
|
218
218
|
super
|
219
|
+
return if is_history_class?
|
219
220
|
history_class.define_history_association(name)
|
220
221
|
end
|
221
222
|
|
222
223
|
def has_one(name, scope = nil, **options, &extension)
|
223
224
|
super
|
225
|
+
return if is_history_class?
|
224
226
|
history_class.define_history_association(name)
|
225
227
|
end
|
226
228
|
|
227
229
|
def has_many(name, scope = nil, **options, &extension)
|
228
230
|
super
|
231
|
+
return if is_history_class?
|
229
232
|
history_class.define_history_association(name)
|
230
233
|
end
|
231
234
|
|
232
235
|
def has_and_belongs_to_many(name, scope = nil, **options, &extension)
|
233
236
|
super
|
237
|
+
return if is_history_class?
|
234
238
|
history_class.define_history_association(name)
|
235
239
|
end
|
236
240
|
end)
|
@@ -371,9 +375,10 @@ module Historiographer
|
|
371
375
|
current_history = histories.where(history_ended_at: nil).order('id desc').limit(1).last
|
372
376
|
|
373
377
|
if history_class.history_foreign_key.present? && history_class.present?
|
374
|
-
history_class.
|
375
|
-
|
376
|
-
|
378
|
+
instance = history_class.new(attrs)
|
379
|
+
instance.save(validate: false)
|
380
|
+
current_history.update!(history_ended_at: now) if current_history.present?
|
381
|
+
instance
|
377
382
|
else
|
378
383
|
raise 'Need foreign key and history class to save history!'
|
379
384
|
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.1.
|
4
|
+
version: 4.1.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- brettshollenberger
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-12-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -220,6 +220,20 @@ dependencies:
|
|
220
220
|
- - ">="
|
221
221
|
- !ruby/object:Gem::Version
|
222
222
|
version: '0'
|
223
|
+
- !ruby/object:Gem::Dependency
|
224
|
+
name: zeitwerk
|
225
|
+
requirement: !ruby/object:Gem::Requirement
|
226
|
+
requirements:
|
227
|
+
- - ">="
|
228
|
+
- !ruby/object:Gem::Version
|
229
|
+
version: '0'
|
230
|
+
type: :development
|
231
|
+
prerelease: false
|
232
|
+
version_requirements: !ruby/object:Gem::Requirement
|
233
|
+
requirements:
|
234
|
+
- - ">="
|
235
|
+
- !ruby/object:Gem::Version
|
236
|
+
version: '0'
|
223
237
|
description: Append-only histories + chained snapshots of your ActiveRecord tables
|
224
238
|
email: brett.shollenberger@gmail.com
|
225
239
|
executables: []
|