Skip to content
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ client.find_instance_record(instance_record_id: 'some-instance-record-id')
client.find_instance_record(instance_record_hrid: 'some-instance-record-hrid')
client.find_source_record(instance_record_id: 'some-instance-record-id')
client.find_source_record(instance_record_hrid: 'some-instance-record-hrid')
client.find_source_marc_records(modified_since: '2025-01-01T00:00:00Z') { |marc_record_hash| }
client.find_source_marc_records(with_965_value: '965abc') { |marc_record_hash| }
client.find_source_marc_records(modified_since: '2025-01-01T00:00:00Z', with_965_value: '965abc') { |marc_record_hash| }

# Convert a FOLIO MARC source record to a marc gem MARC::Record object:
source_record = client.find_source_record(instance_record_id: 'some-instance-record-id')
Expand Down
45 changes: 44 additions & 1 deletion lib/folio_api_client/finders.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

class FolioApiClient
module Finders
module Finders # rubocop:disable Metrics/ModuleLength
def find_item_record(barcode:)
item_search_results = self.get('/item-storage/items', { query: "barcode==#{barcode}", limit: 2 })['items']
return nil if item_search_results.empty?
Expand Down Expand Up @@ -90,6 +90,28 @@ def find_source_record(instance_record_id: nil, instance_record_hrid: nil)
source_record_search_results['sourceRecords'].first
end

def find_source_marc_records(modified_since: nil, with_965_value: nil, &block)
query = marc_records_query(modified_since: modified_since, with_965_value: with_965_value)

loop do
response = self.get('search/instances', query)
process_marc_for_instance(response['instances'], &block) if block
break if (query[:offset] + query[:limit]) >= response['totalRecords']

query[:offset] += query[:limit]
end
end

def process_marc_for_instance(instances, &block)
instances.each do |instance|
source_record = find_source_record(instance_record_id: instance['id'])
next if source_record.nil? # Occasionally, we find an instance record without a source record. Skip these.

marc_content = source_record.dig('parsedRecord', 'content')
yield(marc_content) if marc_content && block
end
end

def source_record_query(instance_record_id: nil, instance_record_hrid: nil)
return { instanceId: instance_record_id } if instance_record_id
return { instanceHrid: instance_record_hrid } if instance_record_hrid
Expand All @@ -112,5 +134,26 @@ def location_record_query(code: nil)
raise FolioApiClient::Exceptions::MissingQueryFieldError,
'Missing query field. Must supply a code.'
end

def marc_records_query(modified_since: nil, with_965_value: nil) # rubocop:disable Metrics/MethodLength
params = { limit: 100, offset: 0 }

if modified_since.nil? && with_965_value.nil?
raise FolioApiClient::Exceptions::MissingQueryFieldError,
'Missing query field. Must supply either modified_since or with_965_value.'
end

if modified_since && !modified_since.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/)
raise ArgumentError,
%(Invalid format for modified_since argument. Must be a date string like "2025-10-03T16:49:00Z".)
end

query_parts = []
query_parts << "metadata.updatedDate>=\"#{modified_since}\"" if modified_since
query_parts << %(identifiers.value="#{with_965_value}") if with_965_value

params[:query] = query_parts.join(' and ')
params
end
end
end
74 changes: 74 additions & 0 deletions spec/folio_api_client/finders_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,80 @@
end
end

describe '#find_source_marc_records' do
let(:modified_since) { '2025-01-01T00:00:00Z' }
let(:value_965) { '965hyacinth' }
let(:marc_content) { { 'fields' => [{ '001' => '12345' }] } }
let(:instance_record) { { 'id' => 'instance-123' } }
let(:instances_response) { { 'totalRecords' => 1, 'instances' => [instance_record] } }

before do
allow(instance).to receive(:find_source_record).and_return({ 'parsedRecord' => { 'content' => marc_content } })
end

context 'with modified_since parameter' do
before do
allow(instance).to receive(:get).with(
'search/instances', { query: "metadata.updatedDate>=\"#{modified_since}\"", limit: 100, offset: 0 }
).and_return(instances_response)
end

it 'yields MARC content for each source record' do
yielded_records = []
instance.find_source_marc_records(modified_since: modified_since) { |record| yielded_records << record }
expect(yielded_records).to eq([marc_content])
end

it 'raises an exception if the given modified_since parameter is an invalid format' do
expect {
instance.find_source_marc_records(modified_since: 'banana') { |_| }
}.to raise_error(ArgumentError)
end
end

context 'with with_965_value parameter' do
before do
allow(instance).to receive(:get).with(
'search/instances', { query: %(identifiers.value="#{value_965}"), limit: 100, offset: 0 }
).and_return(instances_response)
end

it 'uses the correct query' do
instance.find_source_marc_records(with_965_value: value_965) { |_| }
expect(instance).to have_received(:get).with(
'search/instances', { query: 'identifiers.value="965hyacinth"', limit: 100, offset: 0 }
)
end
end

context 'with modified_since and with_965_value parameters' do
before do
allow(instance).to receive(:get).with(
'search/instances', {
query: %(metadata.updatedDate>="#{modified_since}" and identifiers.value="#{value_965}"),
limit: 100, offset: 0
}
).and_return(instances_response)
end

it 'combines both filters' do
instance.find_source_marc_records(modified_since: modified_since, with_965_value: value_965) { |_| }
expect(instance).to have_received(:get).with(
'search/instances', {
query: %(metadata.updatedDate>="#{modified_since}" and identifiers.value="#{value_965}"),
limit: 100, offset: 0
}
)
end
end

it 'raises an error when no parameters provided' do
expect {
instance.find_source_marc_records { |_| }
}.to raise_error(FolioApiClient::Exceptions::MissingQueryFieldError)
end
end

describe '#source_record_query' do
let(:instance_record_id) { 'instance-record-id' }
let(:instance_record_hrid) { 'instance-record-hrid' }
Expand Down