folio_client 0.6.1 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/.rubocop/custom.yml +20 -0
- data/.rubocop_todo.yml +1 -24
- data/Gemfile.lock +24 -7
- data/README.md +21 -0
- data/Rakefile +2 -4
- data/folio_client.gemspec +2 -0
- data/lib/folio_client/data_import.rb +58 -0
- data/lib/folio_client/holdings.rb +25 -0
- data/lib/folio_client/inventory.rb +27 -0
- data/lib/folio_client/job_status.rb +93 -0
- data/lib/folio_client/records_editor.rb +41 -0
- data/lib/folio_client/unexpected_response.rb +1 -1
- data/lib/folio_client/version.rb +1 -1
- data/lib/folio_client.rb +79 -12
- metadata +34 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 726191de10d2f5974ac8294f2c4f93a75146631dd2c86200d2de53f9c72cd76f
|
4
|
+
data.tar.gz: 2ec95c6e87c4541fb1b2b7050c2bd84d5bd3a46f948b0b711b8f042f58ddf6e5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ce0de774a34bbc7eda7a97d26e9ac12d49297a804994425045fe7a8b85017401f54dd11fd43952e4f75e799e232979475f3881eb6f9a211e6f4e32df48168424
|
7
|
+
data.tar.gz: eb31f66db330ab973060794cfcfb30471c02d21529c5242f62fc11a5d97542f1ce13587af5f8d6b2057d5b709951f9c49690725937e39f3d84236df144da3b73
|
data/.rspec
CHANGED
data/.rubocop/custom.yml
CHANGED
@@ -9,6 +9,8 @@ AllCops:
|
|
9
9
|
# Per team developer playbook
|
10
10
|
RSpec/MultipleMemoizedHelpers:
|
11
11
|
Enabled: false
|
12
|
+
RSpec/MultipleExpectations:
|
13
|
+
Max: 3
|
12
14
|
|
13
15
|
RSpec/BeEq: # new in 2.9.0
|
14
16
|
Enabled: true
|
@@ -40,3 +42,21 @@ RSpec/Rails/HaveHttpStatus: # new in 2.12
|
|
40
42
|
Enabled: true
|
41
43
|
RSpec/Rails/InferredSpecType: # new in 2.14
|
42
44
|
Enabled: true
|
45
|
+
Capybara/MatchStyle: # new in 2.17
|
46
|
+
Enabled: true
|
47
|
+
Capybara/NegationMatcher: # new in 2.14
|
48
|
+
Enabled: true
|
49
|
+
Capybara/SpecificActions: # new in 2.14
|
50
|
+
Enabled: true
|
51
|
+
Capybara/SpecificFinders: # new in 2.13
|
52
|
+
Enabled: true
|
53
|
+
Capybara/SpecificMatcher: # new in 2.12
|
54
|
+
Enabled: true
|
55
|
+
RSpec/DuplicatedMetadata: # new in 2.16
|
56
|
+
Enabled: true
|
57
|
+
RSpec/PendingWithoutReason: # new in 2.16
|
58
|
+
Enabled: true
|
59
|
+
RSpec/FactoryBot/FactoryNameStyle: # new in 2.16
|
60
|
+
Enabled: true
|
61
|
+
RSpec/Rails/MinitestAssertions: # new in 2.17
|
62
|
+
Enabled: true
|
data/.rubocop_todo.yml
CHANGED
@@ -1,30 +1,7 @@
|
|
1
1
|
# This configuration was generated by
|
2
2
|
# `rubocop --auto-gen-config`
|
3
|
-
# on 2023-
|
3
|
+
# on 2023-03-03 17:57:09 UTC using RuboCop version 1.44.1.
|
4
4
|
# The point is for the user to remove these configuration records
|
5
5
|
# one by one as the offenses are removed from the code base.
|
6
6
|
# Note that changes in the inspected code, or installation of new
|
7
7
|
# versions of RuboCop, may require this file to be generated again.
|
8
|
-
|
9
|
-
# Offense count: 20
|
10
|
-
# This cop supports safe autocorrection (--autocorrect).
|
11
|
-
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
|
12
|
-
# SupportedStyles: space, no_space, compact
|
13
|
-
# SupportedStylesForEmptyBraces: space, no_space
|
14
|
-
Layout/SpaceInsideHashLiteralBraces:
|
15
|
-
Exclude:
|
16
|
-
- 'lib/folio_client.rb'
|
17
|
-
- 'spec/folio_client/authenticator_spec.rb'
|
18
|
-
- 'spec/folio_client_spec.rb'
|
19
|
-
|
20
|
-
# Offense count: 1
|
21
|
-
# This cop supports safe autocorrection (--autocorrect).
|
22
|
-
# Configuration parameters: EnforcedStyle.
|
23
|
-
# SupportedStyles: be, be_nil
|
24
|
-
RSpec/BeNil:
|
25
|
-
Exclude:
|
26
|
-
- 'spec/folio_client_spec.rb'
|
27
|
-
|
28
|
-
# Offense count: 1
|
29
|
-
RSpec/MultipleExpectations:
|
30
|
-
Max: 3
|
data/Gemfile.lock
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
folio_client (0.
|
4
|
+
folio_client (0.8.0)
|
5
5
|
activesupport (>= 4.2, < 8)
|
6
|
+
dry-monads
|
6
7
|
faraday
|
8
|
+
marc
|
7
9
|
zeitwerk
|
8
10
|
|
9
11
|
GEM
|
@@ -23,6 +25,13 @@ GEM
|
|
23
25
|
rexml
|
24
26
|
diff-lcs (1.5.0)
|
25
27
|
docile (1.4.0)
|
28
|
+
dry-core (1.0.0)
|
29
|
+
concurrent-ruby (~> 1.0)
|
30
|
+
zeitwerk (~> 2.6)
|
31
|
+
dry-monads (1.6.0)
|
32
|
+
concurrent-ruby (~> 1.0)
|
33
|
+
dry-core (~> 1.0, < 2)
|
34
|
+
zeitwerk (~> 2.6)
|
26
35
|
faraday (2.7.4)
|
27
36
|
faraday-net_http (>= 2.0, < 3.1)
|
28
37
|
ruby2_keywords (>= 0.0.4)
|
@@ -32,9 +41,13 @@ GEM
|
|
32
41
|
concurrent-ruby (~> 1.0)
|
33
42
|
json (2.6.3)
|
34
43
|
language_server-protocol (3.17.0.3)
|
35
|
-
|
44
|
+
marc (1.2.0)
|
45
|
+
rexml
|
46
|
+
scrub_rb (>= 1.0.1, < 2)
|
47
|
+
unf
|
48
|
+
minitest (5.18.0)
|
36
49
|
parallel (1.22.1)
|
37
|
-
parser (3.2.1.
|
50
|
+
parser (3.2.1.1)
|
38
51
|
ast (~> 2.4.1)
|
39
52
|
public_suffix (5.0.1)
|
40
53
|
rainbow (3.1.1)
|
@@ -50,7 +63,7 @@ GEM
|
|
50
63
|
rspec-expectations (3.12.2)
|
51
64
|
diff-lcs (>= 1.2.0, < 2.0)
|
52
65
|
rspec-support (~> 3.12.0)
|
53
|
-
rspec-mocks (3.12.
|
66
|
+
rspec-mocks (3.12.4)
|
54
67
|
diff-lcs (>= 1.2.0, < 2.0)
|
55
68
|
rspec-support (~> 3.12.0)
|
56
69
|
rspec-support (3.12.0)
|
@@ -64,18 +77,19 @@ GEM
|
|
64
77
|
rubocop-ast (>= 1.24.1, < 2.0)
|
65
78
|
ruby-progressbar (~> 1.7)
|
66
79
|
unicode-display_width (>= 2.4.0, < 3.0)
|
67
|
-
rubocop-ast (1.
|
80
|
+
rubocop-ast (1.27.0)
|
68
81
|
parser (>= 3.2.1.0)
|
69
82
|
rubocop-capybara (2.17.1)
|
70
83
|
rubocop (~> 1.41)
|
71
84
|
rubocop-performance (1.15.2)
|
72
85
|
rubocop (>= 1.7.0, < 2.0)
|
73
86
|
rubocop-ast (>= 0.4.0)
|
74
|
-
rubocop-rspec (2.
|
87
|
+
rubocop-rspec (2.19.0)
|
75
88
|
rubocop (~> 1.33)
|
76
89
|
rubocop-capybara (~> 2.17)
|
77
|
-
ruby-progressbar (1.
|
90
|
+
ruby-progressbar (1.13.0)
|
78
91
|
ruby2_keywords (0.0.5)
|
92
|
+
scrub_rb (1.0.1)
|
79
93
|
simplecov (0.22.0)
|
80
94
|
docile (~> 1.1)
|
81
95
|
simplecov-html (~> 0.11)
|
@@ -88,6 +102,9 @@ GEM
|
|
88
102
|
rubocop-performance (= 1.15.2)
|
89
103
|
tzinfo (2.0.6)
|
90
104
|
concurrent-ruby (~> 1.0)
|
105
|
+
unf (0.1.4)
|
106
|
+
unf_ext
|
107
|
+
unf_ext (0.0.8.2)
|
91
108
|
unicode-display_width (2.4.2)
|
92
109
|
webmock (3.18.1)
|
93
110
|
addressable (>= 2.8.0)
|
data/README.md
CHANGED
@@ -70,6 +70,27 @@ client.fetch_marc_hash(instance_hrid: "a7927874")
|
|
70
70
|
=> {"fields"=>
|
71
71
|
[{"003"=>"FOLIO"}....]
|
72
72
|
}
|
73
|
+
|
74
|
+
# Import a MARC record
|
75
|
+
data_importer = client.data_import(marc: my_marc, job_profile_id: '4ba4f4ab', job_profile_name: 'ETDs')
|
76
|
+
# If called too quickly, might get Failure(:not_found)
|
77
|
+
data_importer.status
|
78
|
+
=> Failure(:pending)
|
79
|
+
data_importer.wait_until_complete
|
80
|
+
=> Success()
|
81
|
+
data_importer.instance_hrid
|
82
|
+
=> Success("in00000000010")
|
83
|
+
|
84
|
+
# Create a Holdings record
|
85
|
+
holdings_client = client.holdings(instance_id: "99a6d818-d523-42f3-9844-81cf3187dbad")
|
86
|
+
holdings_client.create(permanent_location_id: "1b14e21c-8d47-45c7-bc49-456a0086422b", holdings_type_id: "996f93e2-5b5e-4cf2-9168-33ced1f95eed")
|
87
|
+
=> {
|
88
|
+
"id" => "581f6289-001f-49d1-bab7-035f4d878cbd",
|
89
|
+
"_version" => 1,
|
90
|
+
"hrid" => "ho00000000065",
|
91
|
+
"holdingsTypeId" => "996f93e2-5b5e-4cf2-9168-33ced1f95eed"
|
92
|
+
...
|
93
|
+
}
|
73
94
|
```
|
74
95
|
|
75
96
|
## Development
|
data/Rakefile
CHANGED
@@ -2,11 +2,9 @@
|
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "rspec/core/rake_task"
|
5
|
-
|
6
|
-
RSpec::Core::RakeTask.new(:spec)
|
7
|
-
|
8
5
|
require "rubocop/rake_task"
|
9
6
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
10
8
|
RuboCop::RakeTask.new
|
11
9
|
|
12
|
-
task default: %i[spec
|
10
|
+
task default: %i[rubocop spec]
|
data/folio_client.gemspec
CHANGED
@@ -34,6 +34,8 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.add_dependency "activesupport", ">= 4.2", "< 8"
|
35
35
|
spec.add_dependency "faraday"
|
36
36
|
spec.add_dependency "zeitwerk"
|
37
|
+
spec.add_dependency "marc"
|
38
|
+
spec.add_dependency "dry-monads"
|
37
39
|
|
38
40
|
spec.add_development_dependency "rake", "~> 13.0"
|
39
41
|
spec.add_development_dependency "rspec", "~> 3.0"
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require "marc"
|
5
|
+
require "stringio"
|
6
|
+
|
7
|
+
class FolioClient
|
8
|
+
# Imports MARC records into FOLIO
|
9
|
+
class DataImport
|
10
|
+
# @param client [FolioClient] the configured client
|
11
|
+
def initialize(client)
|
12
|
+
@client = client
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param record [MARC::Record] record to be imported
|
16
|
+
# @param job_profile_id [String] job profile id to use for import
|
17
|
+
# @param job_profile_name [String] job profile name to use for import
|
18
|
+
def import(marc:, job_profile_id:, job_profile_name:)
|
19
|
+
response_hash = client.post("/data-import/uploadDefinitions", {fileDefinitions: [{name: marc_filename}]})
|
20
|
+
upload_definition_id = response_hash.dig("fileDefinitions", 0, "uploadDefinitionId")
|
21
|
+
job_execution_id = response_hash.dig("fileDefinitions", 0, "jobExecutionId")
|
22
|
+
file_definition_id = response_hash.dig("fileDefinitions", 0, "id")
|
23
|
+
|
24
|
+
upload_file_response_hash = client.post("/data-import/uploadDefinitions/#{upload_definition_id}/files/#{file_definition_id}", marc_binary(marc), content_type: "application/octet-stream")
|
25
|
+
|
26
|
+
client.post(
|
27
|
+
"/data-import/uploadDefinitions/#{upload_definition_id}/processFiles",
|
28
|
+
{
|
29
|
+
uploadDefinition: upload_file_response_hash,
|
30
|
+
jobProfileInfo: {
|
31
|
+
id: job_profile_id,
|
32
|
+
name: job_profile_name,
|
33
|
+
dataType: "MARC"
|
34
|
+
}
|
35
|
+
}
|
36
|
+
)
|
37
|
+
|
38
|
+
JobStatus.new(client, job_execution_id: job_execution_id)
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
attr_reader :client, :marc, :job_profile_id, :job_profile_name
|
44
|
+
|
45
|
+
def marc_filename
|
46
|
+
@marc_filename ||= "#{DateTime.now.iso8601}.marc"
|
47
|
+
end
|
48
|
+
|
49
|
+
def marc_binary(marc)
|
50
|
+
StringIO.open do |io|
|
51
|
+
MARC::Writer.new(io) do |writer|
|
52
|
+
writer.write(marc)
|
53
|
+
end
|
54
|
+
io.string
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class FolioClient
|
4
|
+
# Manage holdings records in the Folio inventory
|
5
|
+
class Holdings
|
6
|
+
attr_accessor :client, :instance_id
|
7
|
+
|
8
|
+
# @param client [FolioClient] the configured client
|
9
|
+
# @param instance_id [String] the UUID of the instance to which the holdings records belongs
|
10
|
+
def initialize(client, instance_id:)
|
11
|
+
@client = client
|
12
|
+
@instance_id = instance_id
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param permanent_location_id [String] the UUID of the permanent location
|
16
|
+
# @param holdings_type_id [String] the UUID of the holdings type
|
17
|
+
def create(holdings_type_id:, permanent_location_id:)
|
18
|
+
client.post("/holdings-storage/holdings", {
|
19
|
+
instanceId: instance_id,
|
20
|
+
permanentLocationId: permanent_location_id,
|
21
|
+
holdingsTypeId: holdings_type_id
|
22
|
+
})
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -24,6 +24,33 @@ class FolioClient
|
|
24
24
|
result.dig("hrid")
|
25
25
|
end
|
26
26
|
|
27
|
+
# @param hrid [String] HRID to search by to fetch the external ID
|
28
|
+
# @return [String,nil] external ID if present, otherwise nil.
|
29
|
+
# @raise [ResourceNotFound, MultipleResourcesFound] if search does not return exactly 1 result
|
30
|
+
def fetch_external_id(hrid:)
|
31
|
+
instance_response = client.get("/search/instances", {query: "hrid==#{hrid}"})
|
32
|
+
record_count = instance_response["totalRecords"]
|
33
|
+
raise ResourceNotFound, "No matching instance found for #{hrid}" if instance_response["totalRecords"] == 0
|
34
|
+
raise MultipleResourcesFound, "Expected 1 record for #{hrid}, but found #{record_count}" if record_count > 1
|
35
|
+
|
36
|
+
instance_response.dig("instances", 0, "id")
|
37
|
+
end
|
38
|
+
|
39
|
+
# Retrieve basic information about a record. Example usage: get the external ID and _version for update using
|
40
|
+
# optimistic locking when the HRID is available: `fetch_instance_info(hrid: 'a1234').slice('id', '_version')`
|
41
|
+
# (or vice versa if the external ID is available).
|
42
|
+
# @param external_id [String] an external ID for looking up info about the record
|
43
|
+
# @param hrid [String] an HRID for looking up info about the record
|
44
|
+
# @return [Hash] information about the record.
|
45
|
+
# @raise [ArgumentError] if the caller does not provide exactly one of external_id or hrid
|
46
|
+
def fetch_instance_info(external_id: nil, hrid: nil)
|
47
|
+
raise ArgumentError, "must pass exactly one of external_id or HRID" unless external_id.present? || hrid.present?
|
48
|
+
raise ArgumentError, "must pass exactly one of external_id or HRID" if external_id.present? && hrid.present?
|
49
|
+
|
50
|
+
external_id ||= fetch_external_id(hrid: hrid)
|
51
|
+
client.get("/inventory/instances/#{external_id}")
|
52
|
+
end
|
53
|
+
|
27
54
|
# @param hrid [String] folio instance HRID
|
28
55
|
# @param status_id [String] uuid for an instance status code
|
29
56
|
# @raise [ResourceNotFound] if search by hrid returns 0 results
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timeout"
|
4
|
+
require "dry/monads"
|
5
|
+
|
6
|
+
class FolioClient
|
7
|
+
# Wraps operations waiting for results from jobs
|
8
|
+
class JobStatus
|
9
|
+
include Dry::Monads[:result]
|
10
|
+
|
11
|
+
attr_reader :job_execution_id
|
12
|
+
|
13
|
+
# @param client [FolioClient] the configured client
|
14
|
+
# @param job_execution_id [String] ID of the job to be checked on
|
15
|
+
def initialize(client, job_execution_id:)
|
16
|
+
@client = client
|
17
|
+
@job_execution_id = job_execution_id
|
18
|
+
end
|
19
|
+
|
20
|
+
# @return [Dry::Monads::Result] Success if job is complete,
|
21
|
+
# Failure(:pending) if job is still running,
|
22
|
+
# Failure(:error) if job has errors
|
23
|
+
# Failure(:not_found) if job is not found
|
24
|
+
def status
|
25
|
+
response_hash = client.get("/metadata-provider/jobSummary/#{job_execution_id}")
|
26
|
+
|
27
|
+
return Failure(:error) if response_hash["totalErrors"].positive?
|
28
|
+
return Failure(:pending) if response_hash.dig("sourceRecordSummary", "totalCreatedEntities").zero? && response_hash.dig("sourceRecordSummary", "totalUpdatedEntities").zero?
|
29
|
+
|
30
|
+
Success()
|
31
|
+
rescue ResourceNotFound
|
32
|
+
# Checking the status immediately after starting the import may result in a 404.
|
33
|
+
Failure(:not_found)
|
34
|
+
end
|
35
|
+
|
36
|
+
def wait_until_complete(wait_secs: default_wait_secs, timeout_secs: default_timeout_secs)
|
37
|
+
wait_with_timeout(wait_secs: wait_secs, timeout_secs: timeout_secs) { status }
|
38
|
+
end
|
39
|
+
|
40
|
+
def instance_hrid
|
41
|
+
current_status = status
|
42
|
+
return current_status unless current_status.success?
|
43
|
+
|
44
|
+
@instance_hrid ||= wait_with_timeout do
|
45
|
+
response = client
|
46
|
+
.get("/metadata-provider/journalRecords/#{job_execution_id}")
|
47
|
+
.fetch("journalRecords", [])
|
48
|
+
.find { |journal_record| journal_record["entityType"] == "INSTANCE" }
|
49
|
+
&.fetch("entityHrId", nil)
|
50
|
+
|
51
|
+
response.nil? ? Failure() : Success(response)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
attr_reader :client
|
58
|
+
|
59
|
+
def default_wait_secs
|
60
|
+
1
|
61
|
+
end
|
62
|
+
|
63
|
+
def default_timeout_secs
|
64
|
+
5 * 60
|
65
|
+
end
|
66
|
+
|
67
|
+
def wait_with_timeout(wait_secs: default_wait_secs, timeout_secs: default_timeout_secs)
|
68
|
+
Timeout.timeout(timeout_secs) do
|
69
|
+
loop.with_index do |_, i|
|
70
|
+
result = yield
|
71
|
+
|
72
|
+
# If a 404, wait a bit longer before raising an error.
|
73
|
+
check_not_found(result, i)
|
74
|
+
return result if done_waiting?(result)
|
75
|
+
|
76
|
+
sleep(wait_secs)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
rescue Timeout::Error
|
80
|
+
Failure(:timeout)
|
81
|
+
end
|
82
|
+
|
83
|
+
def done_waiting?(result)
|
84
|
+
result.success? || (result.failure? && result.failure == :error)
|
85
|
+
end
|
86
|
+
|
87
|
+
def check_not_found(result, index)
|
88
|
+
return unless result.failure? && result.failure == :not_found && index > 2
|
89
|
+
|
90
|
+
raise ResourceNotFound, "Job not found"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class FolioClient
|
4
|
+
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
|
+
# Given an HRID, retrieves the associated MARC JSON, yields it to the caller as a hash,
|
13
|
+
# and attempts to re-save it, using optimistic locking to prevent accidental overwrite,
|
14
|
+
# in case another user or process has updated the record in the time between retrieval and
|
15
|
+
# attempted save.
|
16
|
+
# @param hrid [String] the HRID of the MARC record to be edited and saved
|
17
|
+
# @yieldparam record_json [Hash] a hash representation of the MARC JSON for the
|
18
|
+
# HRID; the updated hash will be saved when control is returned from the block.
|
19
|
+
# @note in limited manual testing, optimistic locking behaved like so when two edit attempts collided:
|
20
|
+
# * 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 constraint \"idx_records_matched_id_gen\"'.
|
22
|
+
# This client would fail to write.
|
23
|
+
# * As opposed to the expected behavior of the "winner" getting a 200 ok response, and the "loser" getting a 409 conflict response.
|
24
|
+
# @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
|
+
def edit_marc_json(hrid:)
|
26
|
+
instance_info = client.fetch_instance_info(hrid: hrid)
|
27
|
+
|
28
|
+
version = instance_info["_version"]
|
29
|
+
external_id = instance_info["id"]
|
30
|
+
|
31
|
+
record_json = client.get("/records-editor/records", {externalId: external_id})
|
32
|
+
|
33
|
+
parsed_record_id = record_json["parsedRecordId"]
|
34
|
+
record_json["relatedRecordVersion"] = version # setting this field on the JSON we send back is what will allow optimistic locking to catch stale updates
|
35
|
+
|
36
|
+
yield record_json
|
37
|
+
|
38
|
+
client.put("/records-editor/records/#{parsed_record_id}", record_json)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -13,7 +13,7 @@ class FolioClient
|
|
13
13
|
when 404
|
14
14
|
raise ResourceNotFound, "Endpoint not found or resource does not exist: #{response.body}"
|
15
15
|
when 422
|
16
|
-
raise
|
16
|
+
raise ValidationError, "There was a validation problem with the request: #{response.body} "
|
17
17
|
when 500
|
18
18
|
raise ServiceUnavailable, "The remote server returned an internal server error."
|
19
19
|
else
|
data/lib/folio_client/version.rb
CHANGED
data/lib/folio_client.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "active_support/core_ext/module/delegation"
|
4
|
+
require "active_support/core_ext/object/blank"
|
4
5
|
require "faraday"
|
5
6
|
require "singleton"
|
6
7
|
require "ostruct"
|
@@ -16,7 +17,7 @@ class FolioClient
|
|
16
17
|
# Base class for all FolioClient errors
|
17
18
|
class Error < StandardError; end
|
18
19
|
|
19
|
-
# Error raised by the Folio Auth API returns a
|
20
|
+
# Error raised by the Folio Auth API returns a 401 Unauthorized
|
20
21
|
class UnauthorizedError < Error; end
|
21
22
|
|
22
23
|
# Error raised when the Folio API returns a 404 NotFound, or returns 0 results when one was expected
|
@@ -31,6 +32,9 @@ class FolioClient
|
|
31
32
|
# Error raised when the Folio API returns a 500
|
32
33
|
class ServiceUnavailable < Error; end
|
33
34
|
|
35
|
+
# Error raised when the Folio API returns a 422 Unprocessable Entity
|
36
|
+
class ValidationError < Error; end
|
37
|
+
|
34
38
|
DEFAULT_HEADERS = {
|
35
39
|
accept: "application/json, text/plain",
|
36
40
|
content_type: "application/json"
|
@@ -48,8 +52,8 @@ class FolioClient
|
|
48
52
|
self
|
49
53
|
end
|
50
54
|
|
51
|
-
delegate :config, :connection, :get, :post, to: :instance
|
52
|
-
delegate :fetch_hrid, :fetch_marc_hash, :has_instance_status?, to: :instance
|
55
|
+
delegate :config, :connection, :get, :post, :put, to: :instance
|
56
|
+
delegate :fetch_hrid, :fetch_external_id, :fetch_instance_info, :fetch_marc_hash, :has_instance_status?, :data_import, :holdings, :edit_marc_json, to: :instance
|
53
57
|
end
|
54
58
|
|
55
59
|
attr_accessor :config
|
@@ -64,19 +68,50 @@ class FolioClient
|
|
64
68
|
|
65
69
|
UnexpectedResponse.call(response) unless response.success?
|
66
70
|
|
71
|
+
return nil if response.body.blank?
|
72
|
+
|
67
73
|
JSON.parse(response.body)
|
68
74
|
end
|
69
75
|
|
70
76
|
# Send an authenticated post request
|
77
|
+
# If the body is JSON, it will be automatically serialized
|
71
78
|
# @param path [String] the path to the Folio API request
|
72
|
-
# @param
|
73
|
-
def post(path,
|
79
|
+
# @param body [Object] body to post to the API as JSON
|
80
|
+
def post(path, body = nil, content_type: "application/json")
|
81
|
+
req_body = (content_type == "application/json") ? body&.to_json : body
|
74
82
|
response = TokenWrapper.refresh(config, connection) do
|
75
|
-
|
83
|
+
req_headers = {
|
84
|
+
"x-okapi-token": config.token,
|
85
|
+
"content-type": content_type
|
86
|
+
}
|
87
|
+
connection.post(path, req_body, req_headers)
|
76
88
|
end
|
77
89
|
|
78
90
|
UnexpectedResponse.call(response) unless response.success?
|
79
91
|
|
92
|
+
return nil if response.body.blank?
|
93
|
+
|
94
|
+
JSON.parse(response.body)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Send an authenticated put request
|
98
|
+
# If the body is JSON, it will be automatically serialized
|
99
|
+
# @param path [String] the path to the Folio API request
|
100
|
+
# @param body [Object] body to put to the API as JSON
|
101
|
+
def put(path, body = nil, content_type: "application/json")
|
102
|
+
req_body = (content_type == "application/json") ? body&.to_json : body
|
103
|
+
response = TokenWrapper.refresh(config, connection) do
|
104
|
+
req_headers = {
|
105
|
+
"x-okapi-token": config.token,
|
106
|
+
"content-type": content_type
|
107
|
+
}
|
108
|
+
connection.put(path, req_body, req_headers)
|
109
|
+
end
|
110
|
+
|
111
|
+
UnexpectedResponse.call(response) unless response.success?
|
112
|
+
|
113
|
+
return nil if response.body.blank?
|
114
|
+
|
80
115
|
JSON.parse(response.body)
|
81
116
|
end
|
82
117
|
|
@@ -90,17 +125,49 @@ class FolioClient
|
|
90
125
|
|
91
126
|
# Public methods available on the FolioClient below
|
92
127
|
def fetch_hrid(...)
|
93
|
-
|
94
|
-
|
128
|
+
Inventory
|
129
|
+
.new(self)
|
130
|
+
.fetch_hrid(...)
|
131
|
+
end
|
132
|
+
|
133
|
+
def fetch_external_id(...)
|
134
|
+
Inventory
|
135
|
+
.new(self)
|
136
|
+
.fetch_external_id(...)
|
137
|
+
end
|
138
|
+
|
139
|
+
def fetch_instance_info(...)
|
140
|
+
Inventory
|
141
|
+
.new(self)
|
142
|
+
.fetch_instance_info(...)
|
95
143
|
end
|
96
144
|
|
97
145
|
def fetch_marc_hash(...)
|
98
|
-
|
99
|
-
|
146
|
+
SourceStorage
|
147
|
+
.new(self)
|
148
|
+
.fetch_marc_hash(...)
|
100
149
|
end
|
101
150
|
|
102
151
|
def has_instance_status?(...)
|
103
|
-
|
104
|
-
|
152
|
+
Inventory
|
153
|
+
.new(self)
|
154
|
+
.has_instance_status?(...)
|
155
|
+
end
|
156
|
+
|
157
|
+
def data_import(...)
|
158
|
+
DataImport
|
159
|
+
.new(self)
|
160
|
+
.import(...)
|
161
|
+
end
|
162
|
+
|
163
|
+
def holdings(...)
|
164
|
+
Holdings
|
165
|
+
.new(self, ...)
|
166
|
+
end
|
167
|
+
|
168
|
+
def edit_marc_json(...)
|
169
|
+
RecordsEditor
|
170
|
+
.new(self)
|
171
|
+
.edit_marc_json(...)
|
105
172
|
end
|
106
173
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: folio_client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Mangiafico
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-03-
|
11
|
+
date: 2023-03-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -58,6 +58,34 @@ dependencies:
|
|
58
58
|
- - ">="
|
59
59
|
- !ruby/object:Gem::Version
|
60
60
|
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: marc
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :runtime
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: dry-monads
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :runtime
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
61
89
|
- !ruby/object:Gem::Dependency
|
62
90
|
name: rake
|
63
91
|
requirement: !ruby/object:Gem::Requirement
|
@@ -162,7 +190,11 @@ files:
|
|
162
190
|
- folio_client.gemspec
|
163
191
|
- lib/folio_client.rb
|
164
192
|
- lib/folio_client/authenticator.rb
|
193
|
+
- lib/folio_client/data_import.rb
|
194
|
+
- lib/folio_client/holdings.rb
|
165
195
|
- lib/folio_client/inventory.rb
|
196
|
+
- lib/folio_client/job_status.rb
|
197
|
+
- lib/folio_client/records_editor.rb
|
166
198
|
- lib/folio_client/source_storage.rb
|
167
199
|
- lib/folio_client/token_wrapper.rb
|
168
200
|
- lib/folio_client/unexpected_response.rb
|