Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add find_by_alternate_identifier, for #207 #419

Merged
merged 3 commits into from
Mar 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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_ids
uri_for(:alternate_ids)
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
52 changes: 47 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,17 @@ 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
cleanup_alternate_resources(resource) if alternate_resources
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 +39,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 +71,41 @@ 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 cleanup_alternate_resources(updated_resource)
persisted_resource = adapter.query_service.find_by(id: updated_resource.id)
removed_identifiers = persisted_resource.alternate_ids - updated_resource.alternate_ids

removed_identifiers.each do |removed_id|
adapter.persister.delete(resource: adapter.query_service.find_by(id: removed_id))
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
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
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
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
Loading