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