folio_client 0.14.0 → 0.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +349 -13
- data/Gemfile +2 -2
- data/Gemfile.lock +34 -40
- data/README.md +3 -2
- data/Rakefile +3 -3
- data/api_test.rb +13 -12
- data/folio_client.gemspec +28 -26
- data/lib/folio_client/authenticator.rb +21 -11
- data/lib/folio_client/data_import.rb +18 -17
- data/lib/folio_client/inventory.rb +19 -20
- data/lib/folio_client/job_status.rb +28 -18
- data/lib/folio_client/organizations.rb +15 -16
- data/lib/folio_client/records_editor.rb +15 -13
- data/lib/folio_client/source_storage.rb +28 -15
- data/lib/folio_client/unexpected_response.rb +4 -0
- data/lib/folio_client/users.rb +12 -13
- data/lib/folio_client/version.rb +1 -1
- data/lib/folio_client.rb +70 -47
- metadata +37 -12
- data/.autoupdate/postupdate +0 -19
- data/.rubocop/custom.yml +0 -84
- data/.standard.yml +0 -1
data/folio_client.gemspec
CHANGED
@@ -1,24 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
lib = File.expand_path(
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
-
require
|
5
|
+
require 'folio_client/version'
|
6
6
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
|
-
spec.name =
|
8
|
+
spec.name = 'folio_client'
|
9
9
|
spec.version = FolioClient::VERSION
|
10
|
-
spec.authors = [
|
11
|
-
spec.email = [
|
10
|
+
spec.authors = ['Peter Mangiafico']
|
11
|
+
spec.email = ['pmangiafico@stanford.edu']
|
12
12
|
|
13
|
-
spec.summary =
|
14
|
-
spec.description =
|
15
|
-
spec.homepage =
|
16
|
-
spec.required_ruby_version =
|
13
|
+
spec.summary = 'Interface for interacting with the Folio ILS API.'
|
14
|
+
spec.description = 'This provides API interaction with the Folio ILS API'
|
15
|
+
spec.homepage = 'https://github.com/sul-dlss/folio_client'
|
16
|
+
spec.required_ruby_version = '>= 3.0.0'
|
17
17
|
|
18
|
-
spec.metadata[
|
19
|
-
spec.metadata[
|
20
|
-
spec.metadata[
|
21
|
-
spec.metadata[
|
18
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
19
|
+
spec.metadata['source_code_uri'] = 'https://github.com/sul-dlss/folio_client'
|
20
|
+
spec.metadata['changelog_uri'] = 'https://github.com/sul-dlss/folio_client/releases'
|
21
|
+
spec.metadata['rubygems_mfa_required'] = 'true'
|
22
22
|
|
23
23
|
# Specify which files should be added to the gem when it is released.
|
24
24
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
@@ -27,20 +27,22 @@ Gem::Specification.new do |spec|
|
|
27
27
|
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
|
28
28
|
end
|
29
29
|
end
|
30
|
-
spec.bindir =
|
30
|
+
spec.bindir = 'exe'
|
31
31
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
32
|
-
spec.require_paths = [
|
32
|
+
spec.require_paths = ['lib']
|
33
33
|
|
34
|
-
spec.add_dependency
|
35
|
-
spec.add_dependency
|
36
|
-
spec.add_dependency
|
37
|
-
spec.add_dependency
|
38
|
-
spec.add_dependency
|
34
|
+
spec.add_dependency 'activesupport', '>= 4.2', '< 8'
|
35
|
+
spec.add_dependency 'dry-monads'
|
36
|
+
spec.add_dependency 'faraday'
|
37
|
+
spec.add_dependency 'faraday-cookie_jar'
|
38
|
+
spec.add_dependency 'marc'
|
39
|
+
spec.add_dependency 'zeitwerk'
|
39
40
|
|
40
|
-
spec.add_development_dependency
|
41
|
-
spec.add_development_dependency
|
42
|
-
spec.add_development_dependency
|
43
|
-
spec.add_development_dependency
|
44
|
-
spec.add_development_dependency
|
45
|
-
spec.add_development_dependency
|
41
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
42
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
43
|
+
spec.add_development_dependency 'rubocop'
|
44
|
+
spec.add_development_dependency 'rubocop-performance'
|
45
|
+
spec.add_development_dependency 'rubocop-rspec'
|
46
|
+
spec.add_development_dependency 'simplecov'
|
47
|
+
spec.add_development_dependency 'webmock'
|
46
48
|
end
|
@@ -3,24 +3,34 @@
|
|
3
3
|
class FolioClient
|
4
4
|
# Fetch a token from the Folio API using login_params
|
5
5
|
class Authenticator
|
6
|
-
def self.token
|
7
|
-
new
|
8
|
-
end
|
9
|
-
|
10
|
-
def initialize(login_params, connection)
|
11
|
-
@login_params = login_params
|
12
|
-
@connection = connection
|
6
|
+
def self.token
|
7
|
+
new.token
|
13
8
|
end
|
14
9
|
|
15
10
|
# Request an access_token
|
16
|
-
def token
|
17
|
-
response = connection.post(
|
11
|
+
def token # rubocop:disable Metrics/AbcSize
|
12
|
+
response = FolioClient.connection.post(login_endpoint, FolioClient.config.login_params.to_json)
|
18
13
|
|
19
14
|
UnexpectedResponse.call(response) unless response.success?
|
20
15
|
|
21
|
-
|
16
|
+
# remove legacy_auth once new tokens enabled on Poppy
|
17
|
+
if FolioClient.config.legacy_auth
|
18
|
+
JSON.parse(response.body)['okapiToken']
|
19
|
+
else
|
20
|
+
access_cookie = FolioClient.cookie_jar.cookies.find { |cookie| cookie.name == 'folioAccessToken' }
|
21
|
+
|
22
|
+
raise StandardError, "Problem with folioAccessToken cookie: #{response.headers}, #{response.body}" unless access_cookie
|
23
|
+
|
24
|
+
access_cookie.value
|
25
|
+
end
|
22
26
|
end
|
23
27
|
|
24
|
-
|
28
|
+
private
|
29
|
+
|
30
|
+
def login_endpoint
|
31
|
+
return '/authn/login-with-expiry' unless FolioClient.config.legacy_auth
|
32
|
+
|
33
|
+
'/authn/login'
|
34
|
+
end
|
25
35
|
end
|
26
36
|
end
|
@@ -1,32 +1,28 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'date'
|
4
|
+
require 'stringio'
|
5
5
|
|
6
6
|
class FolioClient
|
7
7
|
# Imports MARC records into FOLIO
|
8
8
|
class DataImport
|
9
9
|
JOB_PROFILE_ATTRIBUTES = %w[id name description dataType].freeze
|
10
10
|
|
11
|
-
# @param client [FolioClient] the configured client
|
12
|
-
def initialize(client)
|
13
|
-
@client = client
|
14
|
-
end
|
15
|
-
|
16
11
|
# @param records [Array<MARC::Record>] records to be imported
|
17
12
|
# @param job_profile_id [String] job profile id to use for import
|
18
13
|
# @param job_profile_name [String] job profile name to use for import
|
19
14
|
# @return [JobStatus] a job status instance to get information about the data import job
|
15
|
+
# rubocop:disable Metrics/MethodLength
|
20
16
|
def import(records:, job_profile_id:, job_profile_name:)
|
21
|
-
response_hash = client.post(
|
22
|
-
upload_definition_id = response_hash.dig(
|
23
|
-
job_execution_id = response_hash.dig(
|
24
|
-
file_definition_id = response_hash.dig(
|
17
|
+
response_hash = client.post('/data-import/uploadDefinitions', { fileDefinitions: [{ name: marc_filename }] })
|
18
|
+
upload_definition_id = response_hash.dig('fileDefinitions', 0, 'uploadDefinitionId')
|
19
|
+
job_execution_id = response_hash.dig('fileDefinitions', 0, 'jobExecutionId')
|
20
|
+
file_definition_id = response_hash.dig('fileDefinitions', 0, 'id')
|
25
21
|
|
26
22
|
upload_file_response_hash = client.post(
|
27
23
|
"/data-import/uploadDefinitions/#{upload_definition_id}/files/#{file_definition_id}",
|
28
24
|
marc_binary(records),
|
29
|
-
content_type:
|
25
|
+
content_type: 'application/octet-stream'
|
30
26
|
)
|
31
27
|
|
32
28
|
client.post(
|
@@ -36,25 +32,30 @@ class FolioClient
|
|
36
32
|
jobProfileInfo: {
|
37
33
|
id: job_profile_id,
|
38
34
|
name: job_profile_name,
|
39
|
-
dataType:
|
35
|
+
dataType: 'MARC'
|
40
36
|
}
|
41
37
|
}
|
42
38
|
)
|
43
39
|
|
44
|
-
JobStatus.new(
|
40
|
+
JobStatus.new(job_execution_id: job_execution_id)
|
45
41
|
end
|
42
|
+
# rubocop:enable Metrics/MethodLength
|
46
43
|
|
47
44
|
# @return [Array<Hash<String,String>>] a list of job profile hashes
|
48
45
|
def job_profiles
|
49
46
|
client
|
50
|
-
.get(
|
51
|
-
.fetch(
|
47
|
+
.get('/data-import-profiles/jobProfiles')
|
48
|
+
.fetch('jobProfiles', [])
|
52
49
|
.map { |profile| profile.slice(*JOB_PROFILE_ATTRIBUTES) }
|
53
50
|
end
|
54
51
|
|
55
52
|
private
|
56
53
|
|
57
|
-
attr_reader :
|
54
|
+
attr_reader :job_profile_id, :job_profile_name
|
55
|
+
|
56
|
+
def client
|
57
|
+
FolioClient.instance
|
58
|
+
end
|
58
59
|
|
59
60
|
def marc_filename
|
60
61
|
@marc_filename ||= "#{DateTime.now.iso8601}.marc"
|
@@ -3,26 +3,19 @@
|
|
3
3
|
class FolioClient
|
4
4
|
# Lookup items in the Folio inventory
|
5
5
|
class Inventory
|
6
|
-
attr_accessor :client
|
7
|
-
|
8
|
-
# @param client [FolioClient] the configured client
|
9
|
-
def initialize(client)
|
10
|
-
@client = client
|
11
|
-
end
|
12
|
-
|
13
6
|
# get instance HRID from barcode
|
14
7
|
# @param barcode [String] barcode
|
15
8
|
# @return [String,nil] instance HRID if present, otherwise nil.
|
16
9
|
def fetch_hrid(barcode:)
|
17
10
|
# find the instance UUID for this barcode
|
18
|
-
instance = client.get(
|
19
|
-
instance_uuid = instance.dig(
|
11
|
+
instance = client.get('/search/instances', { query: "items.barcode==#{barcode}" })
|
12
|
+
instance_uuid = instance.dig('instances', 0, 'id')
|
20
13
|
|
21
14
|
return nil unless instance_uuid
|
22
15
|
|
23
16
|
# next lookup the instance given the instance_uuid so we can fetch the hrid
|
24
17
|
result = client.get("/inventory/instances/#{instance_uuid}")
|
25
|
-
result
|
18
|
+
result['hrid']
|
26
19
|
end
|
27
20
|
|
28
21
|
# get instance external ID from HRID
|
@@ -30,12 +23,12 @@ class FolioClient
|
|
30
23
|
# @return [String,nil] instance external ID if present, otherwise nil.
|
31
24
|
# @raise [ResourceNotFound, MultipleResourcesFound] if search does not return exactly 1 result
|
32
25
|
def fetch_external_id(hrid:)
|
33
|
-
instance_response = client.get(
|
34
|
-
record_count = instance_response[
|
35
|
-
raise ResourceNotFound, "No matching instance found for #{hrid}" if instance_response[
|
26
|
+
instance_response = client.get('/search/instances', { query: "hrid==#{hrid}" })
|
27
|
+
record_count = instance_response['totalRecords']
|
28
|
+
raise ResourceNotFound, "No matching instance found for #{hrid}" if (instance_response['totalRecords']).zero?
|
36
29
|
raise MultipleResourcesFound, "Expected 1 record for #{hrid}, but found #{record_count}" if record_count > 1
|
37
30
|
|
38
|
-
instance_response.dig(
|
31
|
+
instance_response.dig('instances', 0, 'id')
|
39
32
|
end
|
40
33
|
|
41
34
|
# Retrieve basic information about a instance record. Example usage: get the external ID and _version for update using
|
@@ -46,8 +39,8 @@ class FolioClient
|
|
46
39
|
# @return [Hash] information about the record.
|
47
40
|
# @raise [ArgumentError] if the caller does not provide exactly one of external_id or hrid
|
48
41
|
def fetch_instance_info(external_id: nil, hrid: nil)
|
49
|
-
raise ArgumentError,
|
50
|
-
raise ArgumentError,
|
42
|
+
raise ArgumentError, 'must pass exactly one of external_id or HRID' unless external_id.present? || hrid.present?
|
43
|
+
raise ArgumentError, 'must pass exactly one of external_id or HRID' if external_id.present? && hrid.present?
|
51
44
|
|
52
45
|
external_id ||= fetch_external_id(hrid: hrid)
|
53
46
|
client.get("/inventory/instances/#{external_id}")
|
@@ -57,12 +50,12 @@ class FolioClient
|
|
57
50
|
# @param status_id [String] uuid for an instance status code
|
58
51
|
# @return true if instance status matches the uuid param, false otherwise
|
59
52
|
# @raise [ResourceNotFound] if search by instance HRID returns 0 results
|
60
|
-
def has_instance_status?(hrid:, status_id:)
|
53
|
+
def has_instance_status?(hrid:, status_id:) # rubocop:disable Naming/PredicateName
|
61
54
|
# get the instance record and its statusId
|
62
|
-
instance = client.get(
|
63
|
-
raise ResourceNotFound, "No matching instance found for #{hrid}" if instance[
|
55
|
+
instance = client.get('/inventory/instances', { query: "hrid==#{hrid}" })
|
56
|
+
raise ResourceNotFound, "No matching instance found for #{hrid}" if (instance['totalRecords']).zero?
|
64
57
|
|
65
|
-
instance_status_id = instance.dig(
|
58
|
+
instance_status_id = instance.dig('instances', 0, 'statusId')
|
66
59
|
|
67
60
|
return false unless instance_status_id
|
68
61
|
|
@@ -70,5 +63,11 @@ class FolioClient
|
|
70
63
|
|
71
64
|
false
|
72
65
|
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def client
|
70
|
+
FolioClient.instance
|
71
|
+
end
|
73
72
|
end
|
74
73
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'timeout'
|
4
|
+
require 'dry/monads'
|
5
5
|
|
6
6
|
class FolioClient
|
7
7
|
# Wraps operations waiting for results from jobs
|
@@ -10,10 +10,8 @@ class FolioClient
|
|
10
10
|
|
11
11
|
attr_reader :job_execution_id
|
12
12
|
|
13
|
-
# @param client [FolioClient] the configured client
|
14
13
|
# @param job_execution_id [String] ID of the job to be checked on
|
15
|
-
def initialize(
|
16
|
-
@client = client
|
14
|
+
def initialize(job_execution_id:)
|
17
15
|
@job_execution_id = job_execution_id
|
18
16
|
end
|
19
17
|
|
@@ -27,7 +25,7 @@ class FolioClient
|
|
27
25
|
def status
|
28
26
|
response_hash = client.get("/change-manager/jobExecutions/#{job_execution_id}")
|
29
27
|
|
30
|
-
return Failure(:pending)
|
28
|
+
return Failure(:pending) unless %w[COMMITTED ERROR].include?(response_hash['status'])
|
31
29
|
|
32
30
|
Success()
|
33
31
|
rescue ResourceNotFound
|
@@ -35,28 +33,33 @@ class FolioClient
|
|
35
33
|
Failure(:not_found)
|
36
34
|
end
|
37
35
|
|
38
|
-
def wait_until_complete(wait_secs: default_wait_secs, timeout_secs: default_timeout_secs
|
39
|
-
|
36
|
+
def wait_until_complete(wait_secs: default_wait_secs, timeout_secs: default_timeout_secs,
|
37
|
+
max_checks: default_max_checks)
|
38
|
+
wait_with_timeout(wait_secs: wait_secs, timeout_secs: timeout_secs, max_checks: max_checks) { status }
|
40
39
|
end
|
41
40
|
|
41
|
+
# rubocop:disable Metrics/AbcSize
|
42
42
|
def instance_hrids
|
43
43
|
current_status = status
|
44
44
|
return current_status unless current_status.success?
|
45
45
|
|
46
46
|
@instance_hrids ||= wait_with_timeout do
|
47
47
|
response = client
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
48
|
+
.get("/metadata-provider/journalRecords/#{job_execution_id}")
|
49
|
+
.fetch('journalRecords', [])
|
50
|
+
.select { |journal_record| journal_record['entityType'] == 'INSTANCE' && journal_record['actionStatus'] == 'COMPLETED' }
|
51
|
+
.filter_map { |instance_record| instance_record['entityHrId'] }
|
52
52
|
|
53
53
|
response.empty? ? Failure() : Success(response)
|
54
54
|
end
|
55
55
|
end
|
56
|
+
# rubocop:enable Metrics/AbcSize
|
56
57
|
|
57
58
|
private
|
58
59
|
|
59
|
-
|
60
|
+
def client
|
61
|
+
FolioClient.instance
|
62
|
+
end
|
60
63
|
|
61
64
|
def default_wait_secs
|
62
65
|
1
|
@@ -66,13 +69,19 @@ class FolioClient
|
|
66
69
|
10 * 60
|
67
70
|
end
|
68
71
|
|
69
|
-
def
|
72
|
+
def default_max_checks
|
73
|
+
# arbitrary best guess at number of times to check for job status before erroring
|
74
|
+
10
|
75
|
+
end
|
76
|
+
|
77
|
+
def wait_with_timeout(wait_secs: default_wait_secs, timeout_secs: default_timeout_secs,
|
78
|
+
max_checks: default_max_checks)
|
70
79
|
Timeout.timeout(timeout_secs) do
|
71
80
|
loop.with_index do |_, i|
|
72
81
|
result = yield
|
73
82
|
|
74
83
|
# If a 404, wait a bit longer before raising an error.
|
75
|
-
check_not_found(result, i)
|
84
|
+
check_not_found(result, i, max_checks)
|
76
85
|
return result if done_waiting?(result)
|
77
86
|
|
78
87
|
sleep(wait_secs)
|
@@ -86,10 +95,11 @@ class FolioClient
|
|
86
95
|
result.success? || (result.failure? && result.failure == :error)
|
87
96
|
end
|
88
97
|
|
89
|
-
def check_not_found(result, index)
|
90
|
-
return unless result.failure? && result.failure == :not_found && index >
|
98
|
+
def check_not_found(result, index, max_checks)
|
99
|
+
return unless result.failure? && result.failure == :not_found && index > max_checks
|
91
100
|
|
92
|
-
raise ResourceNotFound,
|
101
|
+
raise ResourceNotFound,
|
102
|
+
"Job #{job_execution_id} not found after #{index} retries. The data import job may still have completed."
|
93
103
|
end
|
94
104
|
end
|
95
105
|
end
|
@@ -5,39 +5,38 @@ class FolioClient
|
|
5
5
|
# https://s3.amazonaws.com/foliodocs/api/mod-organizations/p/organizations.html
|
6
6
|
# https://s3.amazonaws.com/foliodocs/api/mod-organizations-storage/p/interface.html
|
7
7
|
class Organizations
|
8
|
-
attr_accessor :client
|
9
|
-
|
10
|
-
# @param client [FolioClient] the configured client
|
11
|
-
def initialize(client)
|
12
|
-
@client = client
|
13
|
-
end
|
14
|
-
|
15
8
|
# @param query [String] an optional query to limit the number of organizations returned
|
16
9
|
# @param limit [Integer] the number of results to return (defaults to 10,000)
|
17
10
|
# @param offset [Integer] the offset for results returned (defaults to 0)
|
18
11
|
# @param lang [String] language code for returned results (defaults to 'en')
|
19
|
-
def fetch_list(query: nil, limit:
|
20
|
-
params = {limit: limit, offset: offset, lang: lang}
|
12
|
+
def fetch_list(query: nil, limit: 10_000, offset: 0, lang: 'en')
|
13
|
+
params = { limit: limit, offset: offset, lang: lang }
|
21
14
|
params[:query] = query if query
|
22
|
-
client.get(
|
15
|
+
client.get('/organizations/organizations', params)
|
23
16
|
end
|
24
17
|
|
25
18
|
# @param query [String] an optional query to limit the number of organization interfaces returned
|
26
19
|
# @param limit [Integer] the number of results to return (defaults to 10,000)
|
27
20
|
# @param offset [Integer] the offset for results returned (defaults to 0)
|
28
21
|
# @param lang [String] language code for returned results (defaults to 'en')
|
29
|
-
def fetch_interface_list(query: nil, limit:
|
30
|
-
params = {limit: limit, offset: offset, lang: lang}
|
22
|
+
def fetch_interface_list(query: nil, limit: 10_000, offset: 0, lang: 'en')
|
23
|
+
params = { limit: limit, offset: offset, lang: lang }
|
31
24
|
params[:query] = query if query
|
32
|
-
client.get(
|
25
|
+
client.get('/organizations-storage/interfaces', params)
|
33
26
|
end
|
34
27
|
|
35
28
|
# @param id [String] id for requested storage interface
|
36
29
|
# @param lang [String] language code for returned result (defaults to 'en')
|
37
|
-
def fetch_interface_details(id:, lang:
|
30
|
+
def fetch_interface_details(id:, lang: 'en')
|
38
31
|
client.get("/organizations-storage/interfaces/#{id}", {
|
39
|
-
|
40
|
-
|
32
|
+
lang: lang
|
33
|
+
})
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def client
|
39
|
+
FolioClient.instance
|
41
40
|
end
|
42
41
|
end
|
43
42
|
end
|
@@ -1,14 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
class FolioClient
|
4
|
+
# Edit MARC JSON records in Folio
|
4
5
|
class RecordsEditor
|
5
|
-
attr_accessor :client
|
6
|
-
|
7
|
-
# @param client [FolioClient] the configured client
|
8
|
-
def initialize(client)
|
9
|
-
@client = client
|
10
|
-
end
|
11
|
-
|
12
6
|
# Given an HRID, retrieves the associated MARC JSON, yields it to the caller as a hash,
|
13
7
|
# and attempts to re-save it, using optimistic locking to prevent accidental overwrite,
|
14
8
|
# in case another user or process has updated the record in the time between retrieval and
|
@@ -18,24 +12,32 @@ class FolioClient
|
|
18
12
|
# HRID; the updated hash will be saved when control is returned from the block.
|
19
13
|
# @note in limited manual testing, optimistic locking behaved like so when two edit attempts collided:
|
20
14
|
# * One updating client would eventually raise a timeout. This updating client would actually write successfully, and version the record.
|
21
|
-
# * The other updating client would raise a StandardError, with a message like 'duplicate key value violates unique
|
15
|
+
# * The other updating client would raise a StandardError, with a message like 'duplicate key value violates unique
|
16
|
+
# constraint \"idx_records_matched_id_gen\"'.
|
22
17
|
# This client would fail to write.
|
23
18
|
# * As opposed to the expected behavior of the "winner" getting a 200 ok response, and the "loser" getting a 409 conflict response.
|
24
19
|
# @todo If this is a problem in practice, see if it's possible to have Folio respond in a more standard way; or, workaround with error handling.
|
25
20
|
def edit_marc_json(hrid:)
|
26
21
|
instance_info = client.fetch_instance_info(hrid: hrid)
|
27
22
|
|
28
|
-
version = instance_info[
|
29
|
-
external_id = instance_info[
|
23
|
+
version = instance_info['_version']
|
24
|
+
external_id = instance_info['id']
|
30
25
|
|
31
|
-
record_json = client.get(
|
26
|
+
record_json = client.get('/records-editor/records', { externalId: external_id })
|
32
27
|
|
33
|
-
parsed_record_id = record_json[
|
34
|
-
|
28
|
+
parsed_record_id = record_json['parsedRecordId']
|
29
|
+
# setting this field on the JSON we send back is what will allow optimistic locking to catch stale updates
|
30
|
+
record_json['relatedRecordVersion'] = version
|
35
31
|
|
36
32
|
yield record_json
|
37
33
|
|
38
34
|
client.put("/records-editor/records/#{parsed_record_id}", record_json)
|
39
35
|
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def client
|
40
|
+
FolioClient.instance
|
41
|
+
end
|
40
42
|
end
|
41
43
|
end
|
@@ -5,26 +5,23 @@ class FolioClient
|
|
5
5
|
class SourceStorage
|
6
6
|
FIELDS_TO_REMOVE = %w[001 003].freeze
|
7
7
|
|
8
|
-
attr_accessor :client
|
9
|
-
|
10
|
-
# @param client [FolioClient] the configured client
|
11
|
-
def initialize(client)
|
12
|
-
@client = client
|
13
|
-
end
|
14
|
-
|
15
8
|
# get marc bib data from folio given an instance HRID
|
16
9
|
# @param instance_hrid [String] the key to use for MARC lookup
|
17
10
|
# @return [Hash] hash representation of the MARC. should be usable by MARC::Record.new_from_hash (from ruby-marc gem)
|
18
11
|
# @raise [ResourceNotFound]
|
19
12
|
# @raise [MultipleResourcesFound]
|
20
13
|
def fetch_marc_hash(instance_hrid:)
|
21
|
-
response_hash = client.get(
|
14
|
+
response_hash = client.get('/source-storage/source-records', { instanceHrid: instance_hrid })
|
22
15
|
|
23
|
-
record_count = response_hash[
|
16
|
+
record_count = response_hash['totalRecords']
|
24
17
|
raise ResourceNotFound, "No records found for #{instance_hrid}" if record_count.zero?
|
25
|
-
raise MultipleResourcesFound, "Expected 1 record for #{instance_hrid}, but found #{record_count}" if record_count > 1
|
26
18
|
|
27
|
-
|
19
|
+
if record_count > 1
|
20
|
+
raise MultipleResourcesFound,
|
21
|
+
"Expected 1 record for #{instance_hrid}, but found #{record_count}"
|
22
|
+
end
|
23
|
+
|
24
|
+
response_hash['sourceRecords'].first['parsedRecord']['content']
|
28
25
|
end
|
29
26
|
|
30
27
|
# get marc bib data as MARCXML from folio given an instance HRID
|
@@ -33,12 +30,20 @@ class FolioClient
|
|
33
30
|
# @return [String] MARCXML string
|
34
31
|
# @raise [ResourceNotFound]
|
35
32
|
# @raise [MultipleResourcesFound]
|
33
|
+
# rubocop:disable Metrics/MethodLength
|
34
|
+
# rubocop:disable Metrics/AbcSize
|
36
35
|
def fetch_marc_xml(instance_hrid: nil, barcode: nil)
|
37
|
-
|
36
|
+
if barcode.nil? && instance_hrid.nil?
|
37
|
+
raise ArgumentError,
|
38
|
+
'Either a barcode or a Folio instance HRID must be provided'
|
39
|
+
end
|
38
40
|
|
39
41
|
instance_hrid ||= client.fetch_hrid(barcode: barcode)
|
40
42
|
|
41
|
-
|
43
|
+
if instance_hrid.blank?
|
44
|
+
raise ResourceNotFound,
|
45
|
+
"Catalog record not found. HRID: #{instance_hrid} | Barcode: #{barcode}"
|
46
|
+
end
|
42
47
|
|
43
48
|
marc_record = MARC::Record.new_from_hash(
|
44
49
|
fetch_marc_hash(instance_hrid: instance_hrid)
|
@@ -52,10 +57,18 @@ class FolioClient
|
|
52
57
|
updated_marc.fields << field
|
53
58
|
end
|
54
59
|
# explicitly inject the instance_hrid into the 001 field
|
55
|
-
updated_marc.fields << MARC::ControlField.new(
|
60
|
+
updated_marc.fields << MARC::ControlField.new('001', instance_hrid)
|
56
61
|
# explicitly inject FOLIO into the 003 field
|
57
|
-
updated_marc.fields << MARC::ControlField.new(
|
62
|
+
updated_marc.fields << MARC::ControlField.new('003', 'FOLIO')
|
58
63
|
updated_marc.to_xml.to_s
|
59
64
|
end
|
65
|
+
# rubocop:enable Metrics/MethodLength
|
66
|
+
# rubocop:enable Metrics/AbcSize
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def client
|
71
|
+
FolioClient.instance
|
72
|
+
end
|
60
73
|
end
|
61
74
|
end
|
@@ -4,6 +4,8 @@ class FolioClient
|
|
4
4
|
# Handles unexpected responses when communicating with Folio
|
5
5
|
class UnexpectedResponse
|
6
6
|
# @param [Faraday::Response] response
|
7
|
+
# rubocop:disable Metrics/MethodLength
|
8
|
+
# rubocop:disable Metrics/AbcSize
|
7
9
|
def self.call(response)
|
8
10
|
case response.status
|
9
11
|
when 401
|
@@ -23,4 +25,6 @@ class FolioClient
|
|
23
25
|
end
|
24
26
|
end
|
25
27
|
end
|
28
|
+
# rubocop:enable Metrics/MethodLength
|
29
|
+
# rubocop:enable Metrics/AbcSize
|
26
30
|
end
|
data/lib/folio_client/users.rb
CHANGED
@@ -4,29 +4,28 @@ class FolioClient
|
|
4
4
|
# Query user records in Folio; see
|
5
5
|
# https://s3.amazonaws.com/foliodocs/api/mod-users/r/users.html
|
6
6
|
class Users
|
7
|
-
attr_accessor :client
|
8
|
-
|
9
|
-
# @param client [FolioClient] the configured client
|
10
|
-
def initialize(client)
|
11
|
-
@client = client
|
12
|
-
end
|
13
|
-
|
14
7
|
# @param query [String] an optional query to limit the number of users returned
|
15
8
|
# @param limit [Integer] the number of results to return (defaults to 10,000)
|
16
9
|
# @param offset [Integer] the offset for results returned (defaults to 0)
|
17
10
|
# @param lang [String] language code for returned results (defaults to 'en')
|
18
|
-
def fetch_list(query: nil, limit:
|
19
|
-
params = {limit: limit, offset: offset, lang: lang}
|
11
|
+
def fetch_list(query: nil, limit: 10_000, offset: 0, lang: 'en')
|
12
|
+
params = { limit: limit, offset: offset, lang: lang }
|
20
13
|
params[:query] = query if query
|
21
|
-
client.get(
|
14
|
+
client.get('/users', params)
|
22
15
|
end
|
23
16
|
|
24
17
|
# @param id [String] id for requested user
|
25
18
|
# @param lang [String] language code for returned results (defaults to 'en')
|
26
|
-
def fetch_user_details(id:, lang:
|
19
|
+
def fetch_user_details(id:, lang: 'en')
|
27
20
|
client.get("/users/#{id}", {
|
28
|
-
|
29
|
-
|
21
|
+
lang: lang
|
22
|
+
})
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def client
|
28
|
+
FolioClient.instance
|
30
29
|
end
|
31
30
|
end
|
32
31
|
end
|
data/lib/folio_client/version.rb
CHANGED