folio_client 0.7.0 → 0.9.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: f400cbad25cc1f7a7a4af985a510c597af5dea7b18cf636d5e6cb8196b4ef059
4
+ data.tar.gz: 26d9485bebd8780fafe498b15827575703316c229ed8c35e361aee3246ee70fd
5
5
  SHA512:
6
- metadata.gz: 16a20ff6ec80f84d828f773ff8c3b1eeeda67dd0472674134aa975a822b37d0d9f86c65ef3ccb03d26c800ffa3a001386d973a77c03ab8d1ef9aeda7dfd312f4
7
- data.tar.gz: e34c0b895a3bfea9174a6836c0a5af28afd1de88743430ba3b2d75494ebe42675e8e1b7a9015823e1e065ba28ed6dbbea864111cf5dd05fc3f5f7b8daf3938b7
6
+ metadata.gz: 472ca9f6c928aa3353b8f928cce63030329e62a7c26655700aaaef6c835e777f58d0771c8b58d8bfd50ee59a7c2c1724063246884ab529556b7cd92ee2065dfd
7
+ data.tar.gz: 2add95c0f3492db0c80f1770c40576d7bbac019a31674a9b0124a47706b065e1b02e67735519dddc2cf4f832216d453ddb3126906a4c89f4d4eac1f59c70fd6d
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.9.0)
5
5
  activesupport (>= 4.2, < 8)
6
6
  dry-monads
7
7
  faraday
@@ -11,7 +11,7 @@ PATH
11
11
  GEM
12
12
  remote: https://rubygems.org/
13
13
  specs:
14
- activesupport (7.0.4.2)
14
+ activesupport (7.0.4.3)
15
15
  concurrent-ruby (~> 1.0, >= 1.0.2)
16
16
  i18n (>= 1.6, < 2)
17
17
  minitest (>= 5.1)
@@ -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,28 +63,28 @@ 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)
70
- rubocop (1.44.1)
70
+ rubocop (1.48.1)
71
71
  json (~> 2.3)
72
72
  parallel (~> 1.10)
73
73
  parser (>= 3.2.0.0)
74
74
  rainbow (>= 2.2.2, < 4.0)
75
75
  regexp_parser (>= 1.8, < 3.0)
76
76
  rexml (>= 3.2.5, < 4.0)
77
- rubocop-ast (>= 1.24.1, < 2.0)
77
+ rubocop-ast (>= 1.26.0, < 2.0)
78
78
  ruby-progressbar (~> 1.7)
79
79
  unicode-display_width (>= 2.4.0, < 3.0)
80
80
  rubocop-ast (1.27.0)
81
81
  parser (>= 3.2.1.0)
82
82
  rubocop-capybara (2.17.1)
83
83
  rubocop (~> 1.41)
84
- rubocop-performance (1.15.2)
84
+ rubocop-performance (1.16.0)
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)
@@ -96,10 +96,10 @@ GEM
96
96
  simplecov_json_formatter (~> 0.1)
97
97
  simplecov-html (0.12.3)
98
98
  simplecov_json_formatter (0.1.4)
99
- standard (1.24.3)
99
+ standard (1.25.2)
100
100
  language_server-protocol (~> 3.17.0.2)
101
- rubocop (= 1.44.1)
102
- rubocop-performance (= 1.15.2)
101
+ rubocop (= 1.48.1)
102
+ rubocop-performance (= 1.16.0)
103
103
  tzinfo (2.0.6)
104
104
  concurrent-ruby (~> 1.0)
105
105
  unf (0.1.4)
data/README.md CHANGED
@@ -65,13 +65,13 @@ client.fetch_hrid(barcode: "12345")
65
65
  => "a7927874"
66
66
 
67
67
  # Request a MARC record given an instance hrid
68
- # returns a hash if found; raises FolioClient::UnexpectedResponse::ResourceNotFound if instance_hrid not found
68
+ # returns a hash if found; raises FolioClient::ResourceNotFound if instance_hrid not found
69
69
  client.fetch_marc_hash(instance_hrid: "a7927874")
70
70
  => {"fields"=>
71
71
  [{"003"=>"FOLIO"}....]
72
72
  }
73
73
 
74
- # Import a MARC record
74
+ # Import a MARC record into FOLIO
75
75
  data_importer = client.data_import(marc: my_marc, job_profile_id: '4ba4f4ab', job_profile_name: 'ETDs')
76
76
  # If called too quickly, might get Failure(:not_found)
77
77
  data_importer.status
@@ -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,26 @@
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 record belongs
10
+ def initialize(client, instance_id:)
11
+ @client = client
12
+ @instance_id = instance_id
13
+ end
14
+
15
+ # create a holdings record for the instance
16
+ # @param permanent_location_id [String] the UUID of the permanent location
17
+ # @param holdings_type_id [String] the UUID of the holdings type
18
+ def create(holdings_type_id:, permanent_location_id:)
19
+ client.post("/holdings-storage/holdings", {
20
+ instanceId: instance_id,
21
+ permanentLocationId: permanent_location_id,
22
+ holdingsTypeId: holdings_type_id
23
+ })
24
+ end
25
+ end
26
+ end
@@ -10,8 +10,9 @@ class FolioClient
10
10
  @client = client
11
11
  end
12
12
 
13
- # @param barcode [String] barcode to search by to fetch the HRID
14
- # @return [String,nil] HRID if present, otherwise nil.
13
+ # get instance HRID from barcode
14
+ # @param barcode [String] barcode
15
+ # @return [String,nil] instance HRID if present, otherwise nil.
15
16
  def fetch_hrid(barcode:)
16
17
  # find the instance UUID for this barcode
17
18
  instance = client.get("/search/instances", {query: "items.barcode==#{barcode}"})
@@ -24,9 +25,38 @@ class FolioClient
24
25
  result.dig("hrid")
25
26
  end
26
27
 
27
- # @param hrid [String] folio instance HRID
28
+ # get instance external ID from HRID
29
+ # @param hrid [String] instance HRID
30
+ # @return [String,nil] instance external ID if present, otherwise nil.
31
+ # @raise [ResourceNotFound, MultipleResourcesFound] if search does not return exactly 1 result
32
+ def fetch_external_id(hrid:)
33
+ instance_response = client.get("/search/instances", {query: "hrid==#{hrid}"})
34
+ record_count = instance_response["totalRecords"]
35
+ raise ResourceNotFound, "No matching instance found for #{hrid}" if instance_response["totalRecords"] == 0
36
+ raise MultipleResourcesFound, "Expected 1 record for #{hrid}, but found #{record_count}" if record_count > 1
37
+
38
+ instance_response.dig("instances", 0, "id")
39
+ end
40
+
41
+ # Retrieve basic information about a instance record. Example usage: get the external ID and _version for update using
42
+ # optimistic locking when the HRID is available: `fetch_instance_info(hrid: 'a1234').slice('id', '_version')`
43
+ # (or vice versa if the external ID is available).
44
+ # @param external_id [String] an external ID for the desired instance record
45
+ # @param hrid [String] an instance HRID for the desired instance record
46
+ # @return [Hash] information about the record.
47
+ # @raise [ArgumentError] if the caller does not provide exactly one of external_id or hrid
48
+ def fetch_instance_info(external_id: nil, hrid: nil)
49
+ raise ArgumentError, "must pass exactly one of external_id or HRID" unless external_id.present? || hrid.present?
50
+ raise ArgumentError, "must pass exactly one of external_id or HRID" if external_id.present? && hrid.present?
51
+
52
+ external_id ||= fetch_external_id(hrid: hrid)
53
+ client.get("/inventory/instances/#{external_id}")
54
+ end
55
+
56
+ # @param hrid [String] instance HRID
28
57
  # @param status_id [String] uuid for an instance status code
29
- # @raise [ResourceNotFound] if search by hrid returns 0 results
58
+ # @return true if instance status matches the uuid param, false otherwise
59
+ # @raise [ResourceNotFound] if search by instance HRID returns 0 results
30
60
  def has_instance_status?(hrid:, status_id:)
31
61
  # get the instance record and its statusId
32
62
  instance = client.get("/inventory/instances", {query: "hrid==#{hrid}"})
@@ -87,7 +87,7 @@ class FolioClient
87
87
  def check_not_found(result, index)
88
88
  return unless result.failure? && result.failure == :not_found && index > 2
89
89
 
90
- raise ResourceNotFound, "Job not found"
90
+ raise ResourceNotFound, "Job #{job_execution_id} not found after #{index} retries"
91
91
  end
92
92
  end
93
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
@@ -10,6 +10,7 @@ class FolioClient
10
10
  @client = client
11
11
  end
12
12
 
13
+ # get marc bib data from folio given an instance HRID
13
14
  # @param instance_hrid [String] the key to use for MARC lookup
14
15
  # @return [Hash] hash representation of the MARC. should be usable by MARC::Record.new_from_hash (from ruby-marc gem)
15
16
  # @raises NotFound, MultipleRecordsForIdentifier
@@ -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.9.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.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Mangiafico
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-03-07 00:00:00.000000000 Z
11
+ date: 2023-03-22 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
@@ -204,7 +206,7 @@ metadata:
204
206
  source_code_uri: https://github.com/sul-dlss/folio_client
205
207
  changelog_uri: https://github.com/sul-dlss/folio_client/releases
206
208
  rubygems_mfa_required: 'true'
207
- post_install_message:
209
+ post_install_message:
208
210
  rdoc_options: []
209
211
  require_paths:
210
212
  - lib
@@ -219,8 +221,8 @@ 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
223
- signing_key:
224
+ rubygems_version: 3.3.7
225
+ signing_key:
224
226
  specification_version: 4
225
227
  summary: Interface for interacting with the Folio ILS API.
226
228
  test_files: []