folio_client 0.7.0 → 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/Gemfile.lock +4 -4
- data/README.md +11 -0
- data/lib/folio_client/holdings.rb +25 -0
- data/lib/folio_client/inventory.rb +27 -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 +50 -3
- metadata +5 -3
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/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
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
6
|
dry-monads
|
7
7
|
faraday
|
@@ -47,7 +47,7 @@ GEM
|
|
47
47
|
unf
|
48
48
|
minitest (5.18.0)
|
49
49
|
parallel (1.22.1)
|
50
|
-
parser (3.2.1.
|
50
|
+
parser (3.2.1.1)
|
51
51
|
ast (~> 2.4.1)
|
52
52
|
public_suffix (5.0.1)
|
53
53
|
rainbow (3.1.1)
|
@@ -63,7 +63,7 @@ GEM
|
|
63
63
|
rspec-expectations (3.12.2)
|
64
64
|
diff-lcs (>= 1.2.0, < 2.0)
|
65
65
|
rspec-support (~> 3.12.0)
|
66
|
-
rspec-mocks (3.12.
|
66
|
+
rspec-mocks (3.12.4)
|
67
67
|
diff-lcs (>= 1.2.0, < 2.0)
|
68
68
|
rspec-support (~> 3.12.0)
|
69
69
|
rspec-support (3.12.0)
|
@@ -84,7 +84,7 @@ GEM
|
|
84
84
|
rubocop-performance (1.15.2)
|
85
85
|
rubocop (>= 1.7.0, < 2.0)
|
86
86
|
rubocop-ast (>= 0.4.0)
|
87
|
-
rubocop-rspec (2.
|
87
|
+
rubocop-rspec (2.19.0)
|
88
88
|
rubocop (~> 1.33)
|
89
89
|
rubocop-capybara (~> 2.17)
|
90
90
|
ruby-progressbar (1.13.0)
|
data/README.md
CHANGED
@@ -80,6 +80,17 @@ data_importer.wait_until_complete
|
|
80
80
|
=> Success()
|
81
81
|
data_importer.instance_hrid
|
82
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
|
+
}
|
83
94
|
```
|
84
95
|
|
85
96
|
## Development
|
@@ -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,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
@@ -17,7 +17,7 @@ class FolioClient
|
|
17
17
|
# Base class for all FolioClient errors
|
18
18
|
class Error < StandardError; end
|
19
19
|
|
20
|
-
# Error raised by the Folio Auth API returns a
|
20
|
+
# Error raised by the Folio Auth API returns a 401 Unauthorized
|
21
21
|
class UnauthorizedError < Error; end
|
22
22
|
|
23
23
|
# Error raised when the Folio API returns a 404 NotFound, or returns 0 results when one was expected
|
@@ -32,6 +32,9 @@ class FolioClient
|
|
32
32
|
# Error raised when the Folio API returns a 500
|
33
33
|
class ServiceUnavailable < Error; end
|
34
34
|
|
35
|
+
# Error raised when the Folio API returns a 422 Unprocessable Entity
|
36
|
+
class ValidationError < Error; end
|
37
|
+
|
35
38
|
DEFAULT_HEADERS = {
|
36
39
|
accept: "application/json, text/plain",
|
37
40
|
content_type: "application/json"
|
@@ -49,8 +52,8 @@ class FolioClient
|
|
49
52
|
self
|
50
53
|
end
|
51
54
|
|
52
|
-
delegate :config, :connection, :get, :post, to: :instance
|
53
|
-
delegate :fetch_hrid, :fetch_marc_hash, :has_instance_status?, :data_import, 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
|
54
57
|
end
|
55
58
|
|
56
59
|
attr_accessor :config
|
@@ -91,6 +94,27 @@ class FolioClient
|
|
91
94
|
JSON.parse(response.body)
|
92
95
|
end
|
93
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
|
+
|
115
|
+
JSON.parse(response.body)
|
116
|
+
end
|
117
|
+
|
94
118
|
# the base connection to the Folio API
|
95
119
|
def connection
|
96
120
|
@connection ||= Faraday.new(
|
@@ -106,6 +130,18 @@ class FolioClient
|
|
106
130
|
.fetch_hrid(...)
|
107
131
|
end
|
108
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(...)
|
143
|
+
end
|
144
|
+
|
109
145
|
def fetch_marc_hash(...)
|
110
146
|
SourceStorage
|
111
147
|
.new(self)
|
@@ -123,4 +159,15 @@ class FolioClient
|
|
123
159
|
.new(self)
|
124
160
|
.import(...)
|
125
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(...)
|
172
|
+
end
|
126
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
|
@@ -191,8 +191,10 @@ files:
|
|
191
191
|
- lib/folio_client.rb
|
192
192
|
- lib/folio_client/authenticator.rb
|
193
193
|
- lib/folio_client/data_import.rb
|
194
|
+
- lib/folio_client/holdings.rb
|
194
195
|
- lib/folio_client/inventory.rb
|
195
196
|
- lib/folio_client/job_status.rb
|
197
|
+
- lib/folio_client/records_editor.rb
|
196
198
|
- lib/folio_client/source_storage.rb
|
197
199
|
- lib/folio_client/token_wrapper.rb
|
198
200
|
- lib/folio_client/unexpected_response.rb
|
@@ -219,7 +221,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
219
221
|
- !ruby/object:Gem::Version
|
220
222
|
version: '0'
|
221
223
|
requirements: []
|
222
|
-
rubygems_version: 3.3.
|
224
|
+
rubygems_version: 3.3.7
|
223
225
|
signing_key:
|
224
226
|
specification_version: 4
|
225
227
|
summary: Interface for interacting with the Folio ILS API.
|