Proxy URLs soon rather than late
[sitka/overdrive-evergreen-opac.git] / src / od_api.coffee
CommitLineData
8ebc3fff
SC
1define [
2 'jquery'
3 'lodash'
4 'json'
5 'cookies'
6 'moment'
7 'od_config'
bf67304d 8 'od_session'
9669700c
SC
9 'od_data'
10], ($, _, json, C, M, config, Session, D) ->
8ebc3fff
SC
11
12 # Dump the given arguments or log them to console
13 log = ->
14 try
15 dump "#{x}\n" for x in arguments
16 return
17 catch
18 console.log arguments
19 return
20
21 $notify = $ {}
22
23 logError = (jqXHR, textStatus, errorThrown) ->
24 log "#{textStatus} #{jqXHR.status} #{errorThrown}"
25 $notify.trigger 'od.fail', arguments
26
27 # Define custom event names for this module. A custom event is triggered
28 # whenever result data becomes available after making an API request.
29 eventList = [
30 'od.clientaccess'
9669700c 31 'od.libraryinfo'
8ebc3fff
SC
32 'od.metadata'
33 'od.availability'
34
35 'od.patronaccess'
36 'od.patroninfo'
37 'od.holds'
38 'od.checkouts'
39 'od.interests'
40 'od.action'
41
42 'od.hold.update'
43 'od.hold.delete'
44 'od.checkout.update'
45 'od.checkout.delete'
46
47 'od.prefs'
48 'od.login'
49 'od.logout'
50 'od.error'
51 ]
52 eventObject = $({}).on eventList.join(' '), (e, x, y...) -> log e.namespace, x, y
53
bf67304d
SC
54 # On page load, we unserialize the text string found in local storage into
55 # an object, or if there is no string yet, we create the default object.
8ebc3fff
SC
56 # The session object uses a local storage mechanism based on window.name;
57 # see
58 # http://stackoverflow.com/questions/2035075/using-window-name-as-a-local-data-cache-in-web-browsers
59 # for pros and cons and alternatives.
bf67304d 60 session = new Session window.name, Boolean C('eg_loggedin') or window.IAMXUL
8ebc3fff 61
bf67304d
SC
62 # On window unload, we serialize it into local storage so that it survives
63 # page reloads.
64 $(window).on 'unload', -> window.name = session.store()
8ebc3fff
SC
65
66 # Customize the plain jQuery ajax to post a request for an access token
67 _api = (url, data) ->
68
69 $.ajax $.extend {},
70 # The Basic Authorization string is always added to the HTTP header.
71 headers: Authorization: "Basic #{config.credentials}"
508513a1 72 url: url
8ebc3fff
SC
73 type: 'POST'
74 # We expect data to be always given; the ajax method will convert
75 # it to a query string.
76 data: data
77
8ebc3fff
SC
78 # Convert a serialized array into a serialized object
79 serializeObject = (a) ->
80 o = {}
81 $.each a, ->
82 v = @value or ''
83 if (n = o[@name]) isnt undefined
84 o[@name] = [n] unless n.push
85 o[@name].push v
86 else
87 o[@name] = v
88 return o
89
90 # TODO unused
91 $.fn.extend
92
93 # Convert this serialized array to a serialized object
94 _serializeObject: -> serializeObject @serializeArray()
95
96 # Serialize this to a json string, an object, an array, a query string, or just return itself
97 _serializeX: (X) ->
98 switch X
99 when 'j' then json.stringify @_serializeX 'o'
100 when 'k' then json.stringify @_serializeX 'a'
101 when 'p' then $.param @_serializeX 'a'
102 when 's' then @serialize()
103 when 'o' then serializeObject @_serializeX 'a'
104 when 'a' then @serializeArray()
105 else @
106
8ebc3fff
SC
107
108 # We define the public interface of the module
109 # TODO wrap od in jquery so that we can use it to trigger events and bind event handlers
110 od =
111
112 # Povides the anchor object for implementing a publish/subscribe
113 # mechanism for this module.
114 $: eventObject.on
115
116 # Notification that there are possible changes of values from
117 # preferences page that should be updated in the session cache
bf67304d 118 'od.prefs': (ev, x) -> session.prefs.update x
8ebc3fff
SC
119
120 # Expire patron access token if user is no longer logged into EG
121 'od.logout': (ev, x) ->
122 if x is 'eg'
bf67304d 123 session = new Session() if session.token.is_patron_access()
8ebc3fff
SC
124
125 log: log
126
8ebc3fff
SC
127 # Map format id to format name using current session object
128 labels: (id) -> session.labels[id] or id
129
130 # Customize the plain jQuery ajax method to handle a GET or POST method
131 # for the Overdrive api.
8546cdf5 132 api: (url, method, data, beforeSend) ->
8ebc3fff
SC
133
134 # Do some pre-processing of data before it is sent to server
135 if method is 'post'
136
137 # Convert numberOfDays value from an ISO 8601 date string to
138 # number of days relative to now. There are two subtleties
139 # regarding rounding errors: First, we use only use now of
140 # resolution to days to avoid a local round-down from 1 to 0.
141 # Second, we need to add one to avoid a round-down at the OD
142 # server.
143 for v in data.fields when v.name is 'numberOfDays'
144 v.value = 1 + M(v.value).diff M().toArray()[0..2], 'days'
145
146 $.ajax $.extend {},
147 # The current Authorization string is always added to the HTTP header.
bf67304d 148 headers: Authorization: "#{session.token.token_type} #{session.token.access_token}"
508513a1 149 url: url
8ebc3fff
SC
150 # Will default to 'get' if no method string is supplied
151 type: method
152 # A given data object is expected to be in JSON format
153 contentType: 'application/json; charset=utf-8'
154 data: json.stringify data
8546cdf5 155 beforeSend: beforeSend
8ebc3fff
SC
156
157 .done ->
158
159 # For a post method, we get a data object in reply. We publish
160 # the object using an event named after the data type, eg,
161 # 'hold', 'checkout'. We can't easily recognize the data type
162 # by looking at the data, so we have to pattern match on the
163 # API URL.
164 if method is 'post'
165 if /\/holds|\/suspension/.test url
9669700c 166 x = new D.Holds holds: [ arguments[0] ]
8ebc3fff
SC
167 od.$.triggerHandler 'od.hold.update', x
168 if /\/checkouts/.test url
9669700c 169 x = new D.Checkouts checkouts: [ arguments[0] ]
8ebc3fff
SC
170 od.$.triggerHandler 'od.checkout.update', x
171
172 # For a delete method, we do not get a data object in reply,
9669700c 173 # thus we pattern match for the specific ID, and trigger an
8ebc3fff
SC
174 # event with the ID.
175 if method is 'delete'
176 if id = url.match /\/holds\/(.+)\/suspension$/
177 return # no relevant event
178 if id = url.match /\/holds\/(.+)$/
9669700c 179 od.$.triggerHandler 'od.hold.delete', id[1]
8ebc3fff 180 if id = url.match /\/checkouts\/(.+)$/
9669700c 181 od.$.triggerHandler 'od.checkout.delete', id[1]
8ebc3fff
SC
182
183 .fail ->
184 od.$.triggerHandler 'od.error', [url, arguments[0]]
185 #$('<div>')._notify arguments[1].statusText, arguments[0].responseText
186
9669700c
SC
187 # Get a library access token so that we can use the Discovery API.
188 # The token is cached and also published to other modules.
8ebc3fff
SC
189 apiDiscAccess: ->
190
191 ok = (x) ->
bf67304d 192 session.token.update x
8ebc3fff 193 od.$.triggerHandler 'od.clientaccess', x
9669700c 194 return x
8ebc3fff
SC
195
196 _api session.links.token.href, grant_type: 'client_credentials'
197
198 .then ok, logError
199
200 # Use the Library Account API to get library account information,
201 # primarily the product link and the available formats. Since we
202 # schedule this call on every page load, it will also tell us if
203 # our access token has expired or not.
204 #
205 # If a retry is needed, we have to decide whether to get a library
206 # access token or a patron access token. However, getting the latter
207 # will, in the general case, require user credentials, which means we
208 # need to store the password in the browser across sessions. An
209 # alternative is to force a logout, so that the user needs to manually
210 # relogin. In effect, we would only proceed with a retry to get a
211 # library access token, but if the user has logged in, we would not.
212 #
9669700c 213 apiLibraryInfo: ->
8ebc3fff
SC
214
215 get = -> od.api session.links.libraries.href
216
217 ok = (x) ->
bf67304d
SC
218 session.links.update x
219 session.labels.update x
9669700c
SC
220 od.$.triggerHandler 'od.libraryinfo', x
221 return x
8ebc3fff
SC
222
223 retry = (jqXHR) ->
224
225 # Retry if we got a 401 error code
226 if jqXHR.status is 401
227
bf67304d 228 if session.token.is_patron_access()
8ebc3fff
SC
229 # Current OD patron access token may have expired
230 od.$.triggerHandler 'od.logout', 'od'
231
232 else
233 # Renew our access token and retry the get operation
234 od.apiDiscAccess()
235 .then get, logError
236 .then ok
237
238 get().then ok, retry
239
240 # We define a two-phase sequence to get a patron access token, for example,
241 # login(credentials); do_something_involving_page_reload(); login();
242 # where credentials is an object containing username and password
243 # properties from the login form.
244 #
245 # Logging into Evergreen can proceed using either barcode or user name,
246 # but logging into Overdrive is only a natural act using barcode. In
247 # order to ensure that logging into OD with a username can proceed, we
248 # presume that EG has been logged into and, as a prelude, we get the
249 # Preferences page of the user so that we can scrape out the barcode
250 # value for logging into OD.
251 #
252 # The login sequence is associated with a cache that remembers the
253 # login response ('parameters') between login sessions. The epilogue to
254 # the login sequence is to use the Patron Information API to get URL
255 # links and templates that will allow the user to make further use of
256 # the Circulation API.
257 login: (credentials) ->
258
259 # Temporarily store the username and password from the login form
bf67304d 260 # into the session cache, and invalidate the session token so that
8ebc3fff
SC
261 # the final part of login sequence can complete.
262 if credentials
bf67304d
SC
263 session.creds.update credentials
264 session.token.update()
8ebc3fff
SC
265 od.$.triggerHandler 'od.login'
266 return
267
bf67304d 268 # Return a promise to a resolved deferredment if session token is still valid
8ebc3fff 269 # TODO is true if in staff client but shouldn't be
bf67304d 270 if session.token.is_patron_access()
8ebc3fff
SC
271 return $.Deferred().resolve().promise()
272
273 # Request OD service for a patron access token using credentials
274 # pulled from the patron's preferences page
275 login = (prefs) ->
276
277 # Define a function to cut the value corresponding to a label
278 # from prefs
279 x = (label) ->
280 r = new RegExp "#{label}<\\/td>\\s+<td.+>(.*?)<\\/td>", 'i'
281 prefs.match(r)?[1] or ''
282
283 # Retrieve values from preferences page and save them in the
284 # session cache for later reference
bf67304d 285 session.prefs.update
8ebc3fff
SC
286 barcode: x 'barcode'
287 email_address: x 'email address'
288 home_library: x 'home library'
8ebc3fff
SC
289
290 # Use barcode as username or the username that was stored in
291 # session cache (in the hope that is a barcode) or give up with
292 # a null string
bf67304d 293 un = session.prefs.barcode or session.creds.un()
8ebc3fff
SC
294
295 # Use the password that was stored in session cache or a dummy value
bf67304d 296 pw = session.creds.pw config.password_required
8ebc3fff
SC
297
298 # Remove the stored credentials from cache as soon as they are
299 # no longer needed
bf67304d 300 session.creds.update()
8ebc3fff
SC
301
302 # Determine the Open Auth scope by mapping the long name of EG
303 # home library to OD authorization name
304 scope = "websiteid:#{config.websiteID} authorizationname:#{config.authorizationname session.prefs.home_library}"
305
306 # Try to get a patron access token from OD server
307 _api session.links.patrontoken.href,
308 grant_type: 'password'
309 username: un
310 password: pw
311 password_required: config.password_required
312 scope: scope
313
314 # Complete login sequence if the session cache is invalid
315 ok = (x) ->
bf67304d 316 session.token.update x
8ebc3fff 317 od.$.triggerHandler 'od.patronaccess', x
9669700c 318 return x
8ebc3fff
SC
319
320 # Get patron preferences page
321 $.get '/eg/opac/myopac/prefs'
322 # Get the access token using credentials from preferences
323 .then login
324 # Update the session cache with access token
325 .then ok
326 # Update the session cache with session links
327 .then od.apiPatronInfo
328 .fail log
329
330 # TODO not used; EG catalogue is used instead
331 apiSearch: (x) ->
332 return unless x
333 od.api session.links.products.href, get, x
334
8ebc3fff
SC
335 apiMetadata: (x) ->
336 return unless x.id
9669700c 337
8ebc3fff
SC
338 od.api "#{session.links.products.href}/#{x.id}/metadata"
339
340 .then (y) ->
9669700c 341 y = new D.Metadata y
8ebc3fff
SC
342 od.$.triggerHandler 'od.metadata', y
343 y
344
345 .fail -> od.$.triggerHandler 'od.metadata', x
346
347 apiAvailability: (x) ->
348 return unless x.id
349
350 url =
351 if (alink = session.links.availability?.href)
352 alink.replace '{crId}', x.id # Use this link if logged in
353 else
354 "#{session.links.products.href}/#{x.id}/availability"
355
356 od.api url
357
8ebc3fff 358 .then (y) ->
9669700c 359 y = new D.Availability y, session.prefs.email_address
8ebc3fff 360 od.$.triggerHandler 'od.availability', y
9669700c 361 return y
8ebc3fff
SC
362
363 .fail -> od.$.triggerHandler 'od.availability', x
364
365 apiPatronInfo: ->
9669700c 366
8ebc3fff 367 ok = (x) ->
bf67304d 368 session.links.update x
8ebc3fff 369 od.$.triggerHandler 'od.patroninfo', x
9669700c 370 return x
8ebc3fff
SC
371
372 od.api session.links.patrons.href
373 .then ok, logError
374
9669700c 375 # Get a specific hold or all holds
8ebc3fff 376 apiHoldsGet: (x) ->
bf67304d 377 return unless session.token.is_patron_access()
8ebc3fff
SC
378
379 od.api "#{session.links.holds.href}#{if x?.productID then x.productID else ''}"
380
8ebc3fff 381 .then (y) ->
9669700c 382 y = new D.Holds y
8ebc3fff 383 od.$.triggerHandler 'od.holds', y
9669700c 384 return y
8ebc3fff 385
9669700c 386 # Get a specific checkout or all checkouts
8ebc3fff 387 apiCheckoutsGet: (x) ->
bf67304d 388 return unless session.token.is_patron_access()
8ebc3fff
SC
389
390 od.api "#{session.links.checkouts.href}#{if x?.reserveID then x.reserveID else ''}"
391
8ebc3fff 392 .then (y) ->
9669700c 393 y = new D.Checkouts y
8ebc3fff 394 od.$.triggerHandler 'od.checkouts', y
9669700c 395 return y
8ebc3fff 396
9669700c
SC
397 # Consolidate the holds and checkouts lists into an object that
398 # represents the 'interests' of the patron
8ebc3fff
SC
399 apiInterestsGet: ->
400 $.when(
401 od.apiHoldsGet()
402 od.apiCheckoutsGet()
403 )
8ebc3fff 404 .then (h, c) ->
9669700c
SC
405 y = new D.Interests h, c
406 od.$.triggerHandler 'od.interests', y
407 return y
8ebc3fff
SC
408
409 return od