folio_client 0.7.0 → 0.9.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: 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: []