Skip to content

Commit

Permalink
Add find_by_alternate_identifier, for #207
Browse files Browse the repository at this point in the history
  • Loading branch information
stkenny committed Mar 25, 2018
1 parent 53b36b3 commit 9dc37e0
Show file tree
Hide file tree
Showing 15 changed files with 232 additions and 16 deletions.
8 changes: 8 additions & 0 deletions lib/valkyrie/persistence/fedora/permissive_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,19 @@ def self.id
uri_for(:id)
end

def self.alternate_identifier
uri_for(:alternate_identifier)
end

# @return [RDF::URI]
def self.member_ids
uri_for(:member_ids)
end

def self.references
uri_for(:references)
end

# @return [RDF::URI]
def self.valkyrie_bool
uri_for(:valkyrie_bool)
Expand Down
42 changes: 37 additions & 5 deletions lib/valkyrie/persistence/fedora/persister.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Valkyrie::Persistence::Fedora
# Persister for Fedora MetadataAdapter.
class Persister
require 'valkyrie/persistence/fedora/persister/resource_factory'
require 'valkyrie/persistence/fedora/persister/alternate_identifier'
attr_reader :adapter
delegate :connection, :base_path, :resource_factory, to: :adapter
def initialize(adapter:)
Expand All @@ -16,14 +17,16 @@ def save(resource:)
resource.updated_at ||= Time.current
ensure_multiple_values!(resource)
orm = resource_factory.from_resource(resource: resource)
alternate_resources = find_or_create_alternate_ids(resource)

if !orm.new? || resource.id
orm.update do |req|
req.headers["Prefer"] = "handling=lenient; received=\"minimal\""
end
orm.update { |req| req.headers["Prefer"] = "handling=lenient; received=\"minimal\"" }
else
orm.create
end
resource_factory.to_resource(object: orm)
persisted_resource = resource_factory.to_resource(object: orm)

alternate_resources ? save_reference_to_resource(persisted_resource, alternate_resources) : persisted_resource
end

# (see Valkyrie::Persistence::Memory::Persister#save_all)
Expand All @@ -35,8 +38,15 @@ def save_all(resources:)

# (see Valkyrie::Persistence::Memory::Persister#delete)
def delete(resource:)
if resource.try(:alternate_ids)
resource.alternate_ids.each do |alternate_identifier|
adapter.persister.delete(resource: adapter.query_service.find_by(id: alternate_identifier))
end
end

orm = resource_factory.from_resource(resource: resource)
orm.delete

resource
end

Expand All @@ -60,10 +70,32 @@ def initialize_repository
private

def ensure_multiple_values!(resource)
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id).select do |_k, v|
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id, :references).select do |_k, v|
!v.nil? && !v.is_a?(Array)
end
raise ::Valkyrie::Persistence::UnsupportedDatatype, "#{resource}: #{bad_keys.keys} have non-array values, which can not be persisted by Valkyrie. Cast to arrays." unless bad_keys.keys.empty?
end

def find_or_create_alternate_ids(resource)
return nil unless resource.try(:alternate_ids)

resource.alternate_ids.map do |alternate_identifier|
begin
adapter.query_service.find_by(id: alternate_identifier)
rescue ::Valkyrie::Persistence::ObjectNotFoundError
alternate_resource = ::Valkyrie::Persistence::Fedora::AlternateIdentifier.new(id: alternate_identifier)
adapter.persister.save(resource: alternate_resource)
end
end
end

def save_reference_to_resource(resource, alternate_resources)
alternate_resources.each do |alternate_resource|
alternate_resource.references = resource.id
adapter.persister.save(resource: alternate_resource)
end

resource
end
end
end
10 changes: 10 additions & 0 deletions lib/valkyrie/persistence/fedora/persister/alternate_identifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true
require 'valkyrie/resource'
require 'valkyrie/types'

module Valkyrie::Persistence::Fedora
class AlternateIdentifier < ::Valkyrie::Resource
attribute :id, ::Valkyrie::Types::ID.optional
attribute :references, ::Valkyrie::Types::ID.optional
end
end
26 changes: 19 additions & 7 deletions lib/valkyrie/persistence/fedora/query_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@ def initialize(adapter:)

# (see Valkyrie::Persistence::Memory::QueryService#find_by)
def find_by(id:)
id = Valkyrie::ID.new(id.to_s) if id.is_a?(String)
validate_id(id)
uri = adapter.id_to_uri(id)
begin
resource = Ldp::Resource.for(connection, uri, connection.get(uri))
resource_factory.to_resource(object: resource)
rescue ::Ldp::Gone, ::Ldp::NotFound
raise ::Valkyrie::Persistence::ObjectNotFoundError
end

resource_from_uri(uri)
end

# (see Valkyrie::Persistence::Memory::QueryService#find_by_alternate_identifier)
def find_by_alternate_identifier(alternate_identifier:)
validate_id(alternate_identifier)
uri = adapter.id_to_uri(alternate_identifier)
alternate_id = resource_from_uri(uri).references

find_by(id: alternate_id)
end

# (see Valkyrie::Persistence::Memory::QueryService#find_many_by_ids)
Expand Down Expand Up @@ -113,9 +117,17 @@ def custom_queries
private

def validate_id(id)
id = Valkyrie::ID.new(id.to_s) if id.is_a?(String)
raise ArgumentError, 'id must be a Valkyrie::ID' unless id.is_a? Valkyrie::ID
end

def resource_from_uri(uri)
resource = Ldp::Resource.for(connection, uri, connection.get(uri))
resource_factory.to_resource(object: resource)
rescue ::Ldp::Gone, ::Ldp::NotFound
raise ::Valkyrie::Persistence::ObjectNotFoundError
end

def ensure_persisted(resource)
raise ArgumentError, 'resource is not saved' unless resource.persisted?
end
Expand Down
2 changes: 1 addition & 1 deletion lib/valkyrie/persistence/memory/persister.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def generate_id(resource)
end

def ensure_multiple_values!(resource)
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id).select do |_k, v|
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id, :alternate_identifier).select do |_k, v|
!v.nil? && !v.is_a?(Array)
end
raise ::Valkyrie::Persistence::UnsupportedDatatype, "#{resource}: #{bad_keys.keys} have non-array values, which can not be persisted by Valkyrie. Cast to arrays." unless bad_keys.keys.empty?
Expand Down
11 changes: 11 additions & 0 deletions lib/valkyrie/persistence/memory/query_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ def find_by(id:)
cache[id] || raise(::Valkyrie::Persistence::ObjectNotFoundError)
end

# @param alternate_identifier [Valkyrie::ID] The alternate identifier to query for.
# @raise [Valkyrie::Persistence::ObjectNotFoundError] Raised when the alternate identifier
# isn't in the persistence backend.
# @raise [ArgumentError] Raised when alternate identifier is not a String or a Valkyrie::ID
# @return [Valkyrie::Resource] The object being searched for.
def find_by_alternate_identifier(alternate_identifier:)
alternate_identifier = Valkyrie::ID.new(alternate_identifier.to_s) if alternate_identifier.is_a?(String)
validate_id(alternate_identifier)
cache.select { |_key, resource| resource['alternate_ids'].include?(alternate_identifier) }.values.first || raise(::Valkyrie::Persistence::ObjectNotFoundError)
end

# @param ids [Array<Valkyrie::ID, String>] The IDs to query for.
# @raise [ArgumentError] Raised when any ID is not a String or a Valkyrie::ID
# @return [Array<Valkyrie::Resource>] All requested objects that were found
Expand Down
2 changes: 1 addition & 1 deletion lib/valkyrie/persistence/postgres/persister.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def wipe!
private

def ensure_multiple_values!(resource)
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id).select do |_k, v|
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id, :alternate_identifier).select do |_k, v|
!v.nil? && !v.is_a?(Array)
end
raise ::Valkyrie::Persistence::UnsupportedDatatype, "#{resource}: #{bad_keys.keys} have non-array values, which can not be persisted by Valkyrie. Cast to arrays." unless bad_keys.keys.empty?
Expand Down
8 changes: 8 additions & 0 deletions lib/valkyrie/persistence/postgres/query_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ def find_by(id:)
raise Valkyrie::Persistence::ObjectNotFoundError
end

# (see Valkyrie::Persistence::Memory::QueryService#find_by_alternate_identifier)
def find_by_alternate_identifier(alternate_identifier:)
alternate_identifier = Valkyrie::ID.new(alternate_identifier.to_s) if alternate_identifier.is_a?(String)
validate_id(alternate_identifier)
internal_array = "{\"alternate_ids\": [{\"id\": \"#{alternate_identifier}\"}]}"
run_query(find_inverse_references_query, internal_array).first || raise(Valkyrie::Persistence::ObjectNotFoundError)
end

# (see Valkyrie::Persistence::Memory::QueryService#find_many_by_ids)
def find_many_by_ids(ids:)
ids.map! do |id|
Expand Down
3 changes: 2 additions & 1 deletion lib/valkyrie/persistence/postgres/resource_converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ def initialize(resource, resource_factory:)
def convert!
orm_class.find_or_initialize_by(id: resource.id && resource.id.to_s).tap do |orm_object|
orm_object.internal_resource = resource.internal_resource
orm_object.metadata.merge!(resource.attributes.except(:id, :internal_resource, :created_at, :updated_at))
orm_object.alternate_identifier = resource.alternate_identifier if resource.respond_to?(:alternate_identifier)
orm_object.metadata.merge!(resource.attributes.except(:id, :internal_resource, :created_at, :updated_at, :alternate_identifier))
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/valkyrie/persistence/solr/queries.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Queries
require 'valkyrie/persistence/solr/queries/default_paginator'
require 'valkyrie/persistence/solr/queries/find_all_query'
require 'valkyrie/persistence/solr/queries/find_by_id_query'
require 'valkyrie/persistence/solr/queries/find_by_alternate_identifier_query'
require 'valkyrie/persistence/solr/queries/find_many_by_ids_query'
require 'valkyrie/persistence/solr/queries/find_inverse_references_query'
require 'valkyrie/persistence/solr/queries/find_members_query'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true
module Valkyrie::Persistence::Solr::Queries
# Responsible for returning a single resource identified by an ID.
class FindByAlternateIdentifierQuery
attr_reader :connection, :resource_factory
attr_writer :alternate_identifier
def initialize(alternate_identifier, connection:, resource_factory:)
@alternate_identifier = alternate_identifier
@connection = connection
@resource_factory = resource_factory
end

def run
raise ::Valkyrie::Persistence::ObjectNotFoundError unless resource
resource_factory.to_resource(object: resource)
end

def alternate_identifier
@alternate_identifier.to_s
end

def resource
connection.get("select", params: { q: "alternate_ids_ssim:\"id-#{alternate_identifier}\"", fl: "*", rows: 1 })["response"]["docs"].first
end
end
end
7 changes: 7 additions & 0 deletions lib/valkyrie/persistence/solr/query_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ def find_by(id:)
Valkyrie::Persistence::Solr::Queries::FindByIdQuery.new(id, connection: connection, resource_factory: resource_factory).run
end

# (see Valkyrie::Persistence::Memory::QueryService#find_by_alternate_identifier)
def find_by_alternate_identifier(alternate_identifier:)
alternate_identifier = Valkyrie::ID.new(alternate_identifier.to_s) if alternate_identifier.is_a?(String)
validate_id(alternate_identifier)
Valkyrie::Persistence::Solr::Queries::FindByAlternateIdentifierQuery.new(alternate_identifier, connection: connection, resource_factory: resource_factory).run
end

# (see Valkyrie::Persistence::Memory::QueryService#find_many_by_ids)
def find_many_by_ids(ids:)
ids.map! do |id|
Expand Down
2 changes: 1 addition & 1 deletion lib/valkyrie/persistence/solr/repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def generate_id(resource)
end

def ensure_multiple_values!(resource)
bad_keys = resource.attributes.except(:internal_resource, :created_at, :updated_at, :new_record, :id).select do |_k, v|
bad_keys = resource.attributes.except(:internal_resource, :alternate_ids, :created_at, :updated_at, :new_record, :id).select do |_k, v|
!v.nil? && !v.is_a?(Array)
end
raise ::Valkyrie::Persistence::UnsupportedDatatype, "#{resource}: #{bad_keys.keys} have non-array values, which can not be persisted by Valkyrie. Cast to arrays." unless bad_keys.keys.empty?
Expand Down
40 changes: 40 additions & 0 deletions lib/valkyrie/specs/shared_specs/queries.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
defined? adapter
class CustomResource < Valkyrie::Resource
attribute :id, Valkyrie::Types::ID.optional
attribute :alternate_ids, Valkyrie::Types::Array
attribute :title
attribute :member_ids, Valkyrie::Types::Array
attribute :a_member_of
Expand All @@ -25,6 +26,7 @@ class SecondResource < Valkyrie::Resource
it { is_expected.to respond_to(:find_all).with(0).arguments }
it { is_expected.to respond_to(:find_all_of_model).with_keywords(:model) }
it { is_expected.to respond_to(:find_by).with_keywords(:id) }
it { is_expected.to respond_to(:find_by_alternate_identifier).with_keywords(:alternate_identifier) }
it { is_expected.to respond_to(:find_many_by_ids).with_keywords(:ids) }
it { is_expected.to respond_to(:find_members).with_keywords(:resource, :model) }
it { is_expected.to respond_to(:find_references_by).with_keywords(:resource, :property) }
Expand Down Expand Up @@ -74,6 +76,44 @@ class SecondResource < Valkyrie::Resource
end
end

describe ".find_by_alternate_identifier" do
it "returns a resource by alternate identifier or string representation of an alternate identifier" do
resource = resource_class.new
resource.alternate_ids = [Valkyrie::ID.new('p9s0xfj')]
resource = persister.save(resource: resource)

found = query_service.find_by_alternate_identifier(alternate_identifier: resource.alternate_ids.first)
expect(found.id).to eq resource.id
expect(found).to be_persisted

found = query_service.find_by_alternate_identifier(alternate_identifier: resource.alternate_ids.first.to_s)
expect(found.id).to eq resource.id
expect(found).to be_persisted
end

it "returns a Valkyrie::Persistence::ObjectNotFoundError for a non-found alternate identifier" do
expect { query_service.find_by_alternate_identifier(alternate_identifier: Valkyrie::ID.new("123123123")) }.to raise_error ::Valkyrie::Persistence::ObjectNotFoundError
end

it 'raises an error if the alternate identifier is not a Valkyrie::ID or a string' do
expect { query_service.find_by_alternate_identifier(alternate_identifier: 123) }.to raise_error ArgumentError
end

it 'can have multiple alternate identifiers' do
resource = resource_class.new
resource.alternate_ids = [Valkyrie::ID.new('p9s0xfj'), Valkyrie::ID.new('jks0xfj')]
resource = persister.save(resource: resource)

found = query_service.find_by_alternate_identifier(alternate_identifier: resource.alternate_ids.first)
expect(found.id).to eq resource.id
expect(found).to be_persisted

found = query_service.find_by_alternate_identifier(alternate_identifier: resource.alternate_ids.last)
expect(found.id).to eq resource.id
expect(found).to be_persisted
end
end

describe ".find_many_by_ids" do
let!(:resource) { persister.save(resource: resource_class.new) }
let!(:resource2) { persister.save(resource: resource_class.new) }
Expand Down
60 changes: 60 additions & 0 deletions spec/valkyrie/persistence/fedora/persister_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,64 @@ class CustomResource < Valkyrie::Resource
expect(reloaded).to be_persisted
end
end

context "when given an alternate identifier" do
before do
raise 'persister must be set with `let(:persister)`' unless defined? persister
class CustomResource < Valkyrie::Resource
include Valkyrie::Resource::AccessControls
attribute :id, Valkyrie::Types::ID.optional
attribute :alternate_ids, Valkyrie::Types::Array
attribute :title
attribute :author
attribute :member_ids
attribute :nested_resource
end
end
after do
Object.send(:remove_const, :CustomResource)
end
let(:resource_class) { CustomResource }

it "creates an alternate identifier resource" do
alternate_identifier = Valkyrie::ID.new("alternative")
resource = resource_class.new
resource.alternate_ids = [alternate_identifier]
persister.save(resource: resource)

alternate = query_service.find_by(id: alternate_identifier)
expect(alternate.id).to eq alternate_identifier
expect(alternate).to be_persisted
end

it "updates an alternate identifier resource" do
alternate_identifier = Valkyrie::ID.new("alternative")
resource = resource_class.new
resource.alternate_ids = [alternate_identifier]
reloaded = persister.save(resource: resource)

alternate = query_service.find_by(id: alternate_identifier)
expect(alternate.id).to eq alternate_identifier
expect(alternate).to be_persisted

alternate_identifier = Valkyrie::ID.new("alternate")
reloaded.alternate_ids = [alternate_identifier]
persister.save(resource: reloaded)
expect(query_service.find_by_alternate_identifier(alternate_identifier: alternate_identifier).id).to eq reloaded.id
end

it "deletes the alternate identifier with the resource" do
alternate_identifier = Valkyrie::ID.new("alternative")
resource = resource_class.new
resource.alternate_ids = [alternate_identifier]
reloaded = persister.save(resource: resource)

alternate = query_service.find_by(id: alternate_identifier)
expect(alternate.id).to eq alternate_identifier
expect(alternate).to be_persisted

persister.delete(resource: reloaded)
expect { query_service.find_by(id: alternate_identifier) }.to raise_error(Valkyrie::Persistence::ObjectNotFoundError)
end
end
end

0 comments on commit 9dc37e0

Please sign in to comment.