# frozen_string_literal: true module Valkyrie::Persistence::Memory # Persister for the memory metadata adapter. # @see Valkyrie::Persistence::Memory # @note Documentation for persisters in general is maintained here. class Persister attr_reader :adapter delegate :cache, :query_service, to: :adapter # @param adapter [Valkyrie::Persistence::Memory::MetadataAdapter] The memory adapter which # holds the cache for this persister. # @note Many persister methods are part of Valkyrie's public API, but instantiation itself is not def initialize(adapter) @adapter = adapter end # Save a single resource. # @param resource [Valkyrie::Resource] The resource to save. # @param external_resource [Boolean] Whether the resource to be saved comes # from a different metadata store. Allows a resource to be saved even if # it's not already in the store. For example, if you're indexing a # resource into Solr - it's saved in your primary metadata store, but not # in Solr, so it's okay if it doesn't exist in Solr but is marked as # persisted. # @return [Valkyrie::Resource] The resource with an `#id` value generated by the # persistence backend. # @raise [Valkyrie::Persistence::StaleObjectError] def save(resource:, external_resource: false) raise Valkyrie::Persistence::StaleObjectError, "The object #{resource.id} has been updated by another process." unless valid_lock?(resource) raise Valkyrie::Persistence::ObjectNotFoundError, "The object #{resource.id} is previously persisted but not found at save time." unless external_resource || valid_for_save?(resource) # duplicate the resource so we are not creating side effects on the caller's resource internal_resource = resource.dup internal_resource = generate_id(internal_resource) if internal_resource.id.blank? internal_resource.created_at ||= Time.current internal_resource.updated_at = Time.current internal_resource.new_record = false generate_lock_token(internal_resource) normalize_dates!(internal_resource) cache[internal_resource.id] = internal_resource end # return true if resource is # persisted and found # or # not persisted def valid_for_save?(resource) return true unless resource.persisted? # a new resource query_service.find_by(id: resource.id).present? # a persisted resource must be found end # Save a batch of resources. # @param resources [Array] List of resources to save. # @return [Array] List of resources with an `#id` value # generated by the persistence backend. # @raise [Valkyrie::Persistence::StaleObjectError] def save_all(resources:) resources.map do |resource| save(resource: resource) end rescue Valkyrie::Persistence::StaleObjectError # Re-raising with no error message to prevent confusion raise Valkyrie::Persistence::StaleObjectError, "One or more resources have been updated by another process." end # Delete a resource. # @param resource [Valkyrie::Resource] The resource to delete from the persistence # backend. def delete(resource:) cache.delete(resource.id) end # Removes all data from the persistence backend. def wipe! cache.clear end private def generate_id(resource) resource.new(id: SecureRandom.uuid) end # Convert all dates to DateTime in the UTC time zone for consistency. def normalize_dates!(resource) resource.attributes.each { |k, v| resource.send("#{k}=", normalize_date_values(v)) } end def normalize_date_values(v) return v.map { |val| normalize_date_value(val) } if v.is_a?(Array) normalize_date_value(v) end def normalize_date_value(value) return value.new_offset(0) if value.is_a?(DateTime) return value.to_datetime.new_offset(0) if value.is_a?(Time) value end # Create a new lock token based on the current timestamp. def generate_lock_token(resource) return unless resource.optimistic_locking_enabled? token = Valkyrie::Persistence::OptimisticLockToken.new(adapter_id: adapter.id, token: Time.now.to_r) resource.set_value(Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK, token) end # Check whether a resource is current. def valid_lock?(resource) return true unless resource.optimistic_locking_enabled? cached_resource = cache[resource.id] return true if cached_resource.blank? resource_lock_tokens = resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK] resource_value = resource_lock_tokens.find { |lock_token| lock_token.adapter_id == adapter.id } return true if resource_value.blank? cached_value = cached_resource[Valkyrie::Persistence::Attributes::OPTIMISTIC_LOCK].first cached_value == resource_value end end end