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