folio_client 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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