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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 51efd566f32da4e9349cde6c853d91b25db7db35be7f046e5cc8ae445470c25e
4
- data.tar.gz: b244ae8d8d496baeb51c3e43834bc2f330fe04cbb943ad2e652efdfacbe0a6aa
3
+ metadata.gz: 726191de10d2f5974ac8294f2c4f93a75146631dd2c86200d2de53f9c72cd76f
4
+ data.tar.gz: 2ec95c6e87c4541fb1b2b7050c2bd84d5bd3a46f948b0b711b8f042f58ddf6e5
5
5
  SHA512:
6
- metadata.gz: 16a20ff6ec80f84d828f773ff8c3b1eeeda67dd0472674134aa975a822b37d0d9f86c65ef3ccb03d26c800ffa3a001386d973a77c03ab8d1ef9aeda7dfd312f4
7
- data.tar.gz: e34c0b895a3bfea9174a6836c0a5af28afd1de88743430ba3b2d75494ebe42675e8e1b7a9015823e1e065ba28ed6dbbea864111cf5dd05fc3f5f7b8daf3938b7
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.7.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.0)
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.3)
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.18.1)
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 UnauthorizedError, "There was a problem fetching the access token: #{response.body} "
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class FolioClient
4
- VERSION = "0.7.0"
4
+ VERSION = "0.8.0"
5
5
  end
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 422 Unauthorized
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.7.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-07 00:00:00.000000000 Z
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.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.