Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a way to do virtual merge through an API
- Loading branch information
Showing
9 changed files
with
263 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# frozen_string_literal: true | ||
|
||
# TODO: this should move to dor-services-app and dor-services-client | ||
# because we don't want Argo to have to know about the XML backing the model. | ||
# | ||
# Adds a constituent relationship between a parent work and child works | ||
# by taking the followin actions: | ||
# 1. altering the contentMD of the parent | ||
# 2. add isConstituentOf assertions to the RELS-EXT of the children | ||
# 3. saving the parent and the child | ||
class ConstituentService | ||
def initialize(parent_druid:) | ||
@parent_druid = parent_druid | ||
end | ||
|
||
def add(child_druids:) | ||
ResetContentMetadataService.new(druid: parent_druid).reset | ||
|
||
child_druids.each do |child_druid| | ||
add_constituent(child_druid: child_druid) | ||
end | ||
parent.save! | ||
end | ||
|
||
private | ||
|
||
def add_constituent(child_druid:) | ||
child = WorkQueryService.find_modifiable_work(child_druid) | ||
child.contentMetadata.ng_xml.search('//resource').each do |resource| | ||
parent.contentMetadata.add_virtual_resource(child.id, resource) | ||
end | ||
child.add_relationship :is_constituent_of, parent | ||
child.save! | ||
end | ||
|
||
def parent | ||
@parent ||= WorkQueryService.find_modifiable_work(parent_druid) | ||
end | ||
|
||
attr_reader :parent_druid | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# frozen_string_literal: true | ||
|
||
# Clears the contentMetadata datastream to the default, wiping out any members. | ||
class ResetContentMetadataService | ||
def initialize(druid:, type: 'image') | ||
@druid = druid | ||
@type = type | ||
end | ||
|
||
def reset | ||
work = WorkQueryService.find_modifiable_work(@druid) | ||
work.contentMetadata.content = "<contentMetadata objectId='#{work.id}' type='#{@type}'/>" | ||
work.save! | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# frozen_string_literal: true | ||
|
||
# Responsible for retrieving information based on the given work (Dor::Item). | ||
class WorkQueryService | ||
# @param [String] id - The id of the work | ||
# @param [#exists?, #find] work_relation - How we will query some of the related information | ||
def initialize(id:, work_relation: default_work_relation) | ||
@id = id | ||
@work_relation = work_relation | ||
end | ||
|
||
delegate :allows_modification?, to: :work | ||
|
||
# @raises [RuntimeError] if the item is not modifiable | ||
def self.find_modifiable_work(druid) | ||
query_service = WorkQueryService.new(id: druid) | ||
query_service.work do |work| | ||
raise "Item #{work.pid} is not open for modification" unless query_service.allows_modification? | ||
end | ||
end | ||
|
||
def work(&block) | ||
@work ||= work_relation.find(id) | ||
return @work unless block_given? | ||
|
||
@work.tap(&block) | ||
end | ||
|
||
private | ||
|
||
attr_reader :id, :work_relation | ||
|
||
def default_work_relation | ||
Dor::Item | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rails_helper' | ||
|
||
RSpec.describe 'Virtual merge of objects' do | ||
let(:payload) { { sub: 'argo' } } | ||
let(:jwt) { JWT.encode(payload, Settings.dor.hmac_secret, 'HS256') } | ||
let(:parent_id) { 'druid:mk420bs7601' } | ||
let(:child1_id) { 'druid:child1' } | ||
let(:child2_id) { 'druid:child2' } | ||
|
||
let(:object) { Dor::Item.new(pid: parent_id) } | ||
let(:service) { instance_double(ConstituentService, add: true) } | ||
|
||
before do | ||
allow(Dor).to receive(:find).and_return(object) | ||
allow(ConstituentService).to receive(:new).with(parent_druid: parent_id).and_return(service) | ||
end | ||
|
||
it 'merges the objects' do | ||
put "/v1/objects/#{parent_id}", | ||
params: { constituent_ids: [child1_id, child2_id] }, | ||
headers: { 'X-Auth' => "Bearer #{jwt}" } | ||
expect(service).to have_received(:add).with(child_druids: [child1_id, child2_id]) | ||
expect(response).to be_successful | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rails_helper' | ||
|
||
RSpec.describe ConstituentService do | ||
let(:parent) do | ||
Dor::Item.new.tap do |item| | ||
item.contentMetadata.content = <<~XML | ||
<contentMetadata></contentMetadata> | ||
XML | ||
end | ||
end | ||
|
||
let(:child1) do | ||
Dor::Item.new.tap do |item| | ||
item.contentMetadata.content = <<~XML | ||
<contentMetadata> | ||
<resource id="bb000kg4251_1" sequence="1" type="image"> | ||
<file id="bb000kg4251.jpg" mimetype="image/jpeg" size="1347965" preserve="yes" publish="no" shelve="no"> | ||
</file> | ||
</resource> | ||
</contentMetadata> | ||
XML | ||
end | ||
end | ||
|
||
let(:child2) do | ||
Dor::Item.new.tap do |item| | ||
item.contentMetadata.content = <<~XML | ||
<contentMetadata> | ||
<resource id="bb000ff1111_1" sequence="1" type="image"> | ||
<file id="bb000ff1111.jpg" mimetype="image/jpeg" size="999" preserve="yes" publish="no" shelve="no"> | ||
</file> | ||
</resource> | ||
</contentMetadata> | ||
XML | ||
end | ||
end | ||
|
||
describe '#add' do | ||
subject(:add) { instance.add(child_druids: [child1.id, child2.id]) } | ||
|
||
let(:instance) do | ||
described_class.new(parent_druid: parent.id) | ||
end | ||
let(:namespaceless) { parent.id.sub('druid:', '') } | ||
let(:client) { instance_double(Dor::Services::Client::Object, version: version_client) } | ||
let(:version_client) { instance_double(Dor::Services::Client::ObjectVersion, close: true) } | ||
|
||
before do | ||
allow(parent).to receive_messages(id: 'druid:parent1', save!: true) | ||
allow(child1).to receive_messages(id: 'druid:child1', save!: true) | ||
allow(child2).to receive_messages(id: 'druid:child2', save!: true) | ||
|
||
# Used in ContentMetadataDS#add_virtual_resource | ||
allow(parent.contentMetadata).to receive(:pid).and_return('druid:parent1') | ||
|
||
allow(WorkQueryService).to receive(:find_modifiable_work).with('druid:parent1').and_return(parent) | ||
allow(WorkQueryService).to receive(:find_modifiable_work).with('druid:child1').and_return(child1) | ||
allow(WorkQueryService).to receive(:find_modifiable_work).with('druid:child2').and_return(child2) | ||
|
||
allow(Dor::Services::Client).to receive(:object).and_return(client) | ||
end | ||
|
||
context 'when the parent is open for modification' do | ||
before do | ||
add | ||
end | ||
|
||
it 'merges objects' do | ||
expect(parent.contentMetadata.content).to be_equivalent_to <<~XML | ||
<contentMetadata objectId="druid:parent1" type="image"> | ||
<resource sequence="1" id="#{namespaceless}_1" type="image"> | ||
<relationship type="alsoAvailableAs" objectId="#{child1.id}"/> | ||
</resource> | ||
<resource sequence="2" id="#{namespaceless}_2" type="image"> | ||
<relationship type="alsoAvailableAs" objectId="#{child2.id}"/> | ||
</resource> | ||
</contentMetadata> | ||
XML | ||
expect(child1.object_relations[:is_constituent_of]).to eq [parent] | ||
expect(child2.object_relations[:is_constituent_of]).to eq [parent] | ||
end | ||
end | ||
|
||
context 'when the parent is closed for modification' do | ||
before do | ||
allow(WorkQueryService).to receive(:find_modifiable_work).with(parent.id).and_raise('nope') | ||
end | ||
|
||
it 'merges nothing' do | ||
expect { add }.to raise_error RuntimeError | ||
expect(parent.contentMetadata.content).to be_equivalent_to <<~XML | ||
<contentMetadata /> | ||
XML | ||
expect(child1.object_relations[:is_constituent_of]).to be_empty | ||
end | ||
end | ||
|
||
context 'when the child is closed for modification' do | ||
before do | ||
allow(WorkQueryService).to receive(:find_modifiable_work).with(child1.id).and_raise('not modifiable') | ||
end | ||
|
||
it 'merges nothing' do | ||
expect { add }.to raise_error RuntimeError | ||
expect(parent.contentMetadata.content).to be_equivalent_to <<~XML | ||
<contentMetadata objectId="druid:parent1" type="image"/> | ||
XML | ||
expect(child1.object_relations[:is_constituent_of]).to be_empty | ||
end | ||
end | ||
end | ||
end |