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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fdfddfba56fcb69bb91c73edcf7c1ddff76adc25f517d4b225bcf87bdd356540
4
- data.tar.gz: 01e398edc375e2bcf17f22a81c4eb06ab2d507483b05db80b8014ea291f2ac82
3
+ metadata.gz: e0a2f100a48aa0554022b2b53d7dd09cb296b298b6c6d92e6dae5b824cccd600
4
+ data.tar.gz: 6472d913b3d16ae9b5adc2414d1bbc0eaab9d62f632d497e475fd35291b90999
5
5
  SHA512:
6
- metadata.gz: 64b288645843cad4862b06724619ec8c98405745b4d6ba572d00baabbc1ce0d4f706cbf95b42c7e1e6214c870ac0c335fb37ea760f599830037f11a94cf5443e
7
- data.tar.gz: '09663a6452efd27ce2f566b3764e7dd9f29499a61cb5eecc7b488e8fe31270d57a6ceb84489de983cbd4851ac8906cf6140127113ef06751fc6cb19839821906'
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
- # Skip if the association is already defined
262
- return if method_defined?(assoc_name)
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
- # We're writing a belongs_to
268
- # The dataset belongs_to the datasource
269
- # dataset#datasource_id => datasource.id
270
- #
271
- # For the history class, we're writing a belongs_to
272
- # the DatasetHistory belongs_to the DatasourceHistory
273
- # dataset_history#datasource_id => datasource_history.easy_ml_datasource_id
274
- #
275
- # The missing piece for us here is whatever DatasourceHistory would call easy_ml_datasource_id (history foreign key?)
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, ->(history_instance) {
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, ->(history_instance) {
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, ->(history_instance) {
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
- instance = original_class.find_or_initialize_by(original_class.primary_key => attrs[original_class.primary_key])
333
- instance.assign_attributes(attrs.except(*cannot_keep_cols))
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
- dummy_instance.send(method_name, *args, &block)
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
@@ -1,3 +1,3 @@
1
1
  module Historiographer
2
- VERSION = "4.1.7"
2
+ VERSION = "4.1.9"
3
3
  end
@@ -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.create!(attrs).tap do |new_history|
375
- current_history.update!(history_ended_at: now) if current_history.present?
376
- end
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.7
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-20 00:00:00.000000000 Z
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: []