folio_client 0.7.0 → 0.8.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 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.