|
- class VersionRecorder
- EVENT_TYPES = ['create', 'update', 'discard', 'restore'].freeze
-
- def initialize record:, event_type:, created_by_user:
- @record = record
- @event_type = event_type.to_s
- @created_by_user = created_by_user
-
- validate_event_type!
- end
-
- def record!
- raise "#{ record_class.name } must be persisted" unless @record.persisted?
-
- ApplicationRecord.transaction do
- @record = record_class.unscoped.lock.find(@record.id)
- latest = latest_version
-
- validate_version_sequence! latest
-
- attrs = snapshot_attributes
-
- if @event_type == 'update' && latest && same_snapshot?(latest, attrs)
- return latest
- end
-
- version = version_class.create!(
- base_attributes(latest).merge(record_key => @record).merge(attrs))
-
- update_record_version_no! version.version_no
-
- version
- end
- end
-
- private
-
- def latest_version = versions.order(version_no: :desc).first
-
- def versions = @record.public_send(version_association)
-
- def base_attributes latest
- { version_no: (latest&.version_no || 0) + 1,
- event_type: @event_type,
- created_at: Time.current,
- created_by_user: @created_by_user }
- end
-
- def update_record_version_no! version_no
- @record.update_columns version_no: version_no
- @record.version_no = version_no
- end
-
- def validate_version_sequence! latest
- if !(latest) && @event_type != 'create'
- raise "#{ version_class.name } first event must be create"
- end
-
- if @event_type == 'create' && latest
- raise "#{ version_class.name } create event already exists"
- end
-
- return unless latest
-
- if @record.version_no != latest.version_no
- raise ("#{ record_class.name }##{ @record.id } version_no is #{ @record.version_no }, " +
- "but latest #{ version_class.name } version_no is #{ latest.version_no }")
- end
- end
-
- def same_snapshot? version, attrs
- attrs.all? { |k, v| version.public_send(k) == v }
- end
-
- def validate_event_type!
- return if EVENT_TYPES.include?(@event_type)
-
- raise ArgumentError, "Invalid event_type: #{ @event_type }"
- end
-
- def version_class = raise NotImplementedError
- def version_association = raise NotImplementedError
- def record_key = raise NotImplementedError
- def snapshot_attributes = raise NotImplementedError
-
- def record_class = @record.class
- end
|