cfd787fc36a84cdb391f602bc540aed470ba7309
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / lovefield.js
1 /**
2  * Core Service - egLovefield
3  *
4  * Lovefield wrapper factory for low level offline stuff
5  *
6  */
7 angular.module('egCoreMod')
8
9 .factory('egLovefield', ['$q','$rootScope','egCore','$timeout', 
10                  function($q , $rootScope , egCore , $timeout) { 
11
12     var service = {
13         autoId: 0, // each request gets a unique id.
14         cannotConnect: false,
15         pendingRequests: [],
16         activeSchemas: ['cache'], // add 'offline' in the offline UI
17         schemasInProgress: {},
18         connectedSchemas: [],
19         // TODO: relative path would be more portable
20         workerUrl: '/js/ui/default/staff/offline-db-worker.js'
21     };
22
23     service.connectToWorker = function() {
24         if (service.worker) return;
25
26         try {
27             // relative path would be better...
28             service.worker = new SharedWorker(service.workerUrl);
29         } catch (E) {
30             console.error('SharedWorker() not supported', E);
31             service.cannotConnect = true;
32             return;
33         }
34
35         service.worker.onerror = function(err) {
36             console.error('Error loading shared worker', err);
37             service.cannotConnect = true;
38         }
39
40         // List for responses and resolve the matching pending request.
41         service.worker.port.addEventListener('message', function(evt) {
42             var response = evt.data;
43             var reqId = response.id;
44             var req = service.pendingRequests.filter(
45                 function(r) { return r.id === reqId})[0];
46
47             if (!req) {
48                 console.error('Recieved response for unknown request ' + reqId);
49                 return;
50             }
51
52             if (response.status === 'OK') {
53                 req.deferred.resolve(response.result);
54             } else {
55                 console.error('worker request failed with ' + response.error);
56                 req.deferred.reject(response.error);
57             }
58         });
59
60         service.worker.port.start();
61     }
62
63     service.connectToSchemas = function() {
64
65         if (service.cannotConnect) { 
66             // This can happen in certain environments
67             return $q.reject();
68         }
69         
70         service.connectToWorker(); // no-op if already connected
71
72         var promises = [];
73
74         service.activeSchemas.forEach(function(schema) {
75             promises.push(service.connectToSchema(schema));
76         });
77
78         return $q.all(promises).then(
79             function() {},
80             function() {service.cannotConnect = true}
81         );
82     }
83
84     // Connects if necessary to the active schemas then relays the request.
85     service.request = function(args) {
86         // useful, but very chatty, leaving commented out.
87         // console.debug('egLovfield sending request: ', args);
88         return service.connectToSchemas().then(
89             function() {
90                 return service.relayRequest(args);
91             }
92         );
93     }
94
95     // Send a request to the web worker and register the request for
96     // future resolution.
97     // Store the request ID in the request arguments, so it's included
98     // in the response, and in the pendingRequests list for linking.
99     service.relayRequest = function(args) {
100         var deferred = $q.defer();
101         var reqId = service.autoId++;
102         args.id = reqId;
103         service.pendingRequests.push({id : reqId, deferred: deferred});
104         service.worker.port.postMessage(args);
105         return deferred.promise;
106     }
107
108     // Create and connect to the give schema
109     service.connectToSchema = function(schema) {
110
111         if (service.connectedSchemas.includes(schema)) {
112             // already connected
113             return $q.when();
114         }
115
116         if (service.schemasInProgress[schema]) {
117             return service.schemasInProgress[schema];
118         }
119
120         var deferred = $q.defer();
121
122         service.relayRequest(
123             {schema: schema, action: 'createSchema'}) 
124         .then(
125             function() {
126                 return service.relayRequest(
127                     {schema: schema, action: 'connect'});
128             },
129             deferred.reject
130         ).then(
131             function() { 
132                 service.connectedSchemas.push(schema); 
133                 delete service.schemasInProgress[schema];
134                 deferred.resolve();
135             },
136             deferred.reject
137         );
138
139         return service.schemasInProgress[schema] = deferred.promise;
140     }
141
142     service.isCacheGood = function (type) {
143         return service.request({
144             schema: 'cache',
145             table: 'CacheDate',
146             action: 'selectWhereEqual',
147             field: 'type',
148             value: type
149         }).then(
150             function(result) {
151                 var row = result[0];
152                 if (!row) { return false; }
153                 // hard-coded 1 day offline cache timeout
154                 return (new Date().getTime() - row.cachedate.getTime()) <= 86400000;
155             }
156         );
157     }
158
159     // Remove all pending offline transactions and delete the cached
160     // offline transactions date to indicate no transactions remain.
161     service.destroyPendingOfflineXacts = function () {
162         return service.request({
163             schema: 'offline',
164             table: 'OfflineXact',
165             action: 'deleteAll'
166         }).then(function() {
167             return service.request({
168                 schema: 'cache',
169                 table: 'CacheDate',
170                 action: 'deleteWhereEqual',
171                 field: 'type',
172                 value: '_offlineXact'
173             });
174         });
175     }
176
177     // Returns the cache date when xacts exit, null otherwise.
178     service.havePendingOfflineXacts = function () {
179         return service.request({
180             schema: 'cache',
181             table: 'CacheDate',
182             action: 'selectWhereEqual',
183             field: 'type',
184             value: '_offlineXact'
185         }).then(function(results) {
186             return results[0] ? results[0].cachedate : null;
187         });
188     }
189
190     service.retrievePendingOfflineXacts = function () {
191         return service.request({
192             schema: 'offline',
193             table: 'OfflineXact',
194             action: 'selectAll'
195         }).then(function(resp) {
196             return resp.map(function(x) { return x.value });
197         });
198     }
199
200     // Add an offline transaction and update the cache indicating
201     // now() as the most recent addition of an offline xact.
202     service.addOfflineXact = function (obj) {
203         return service.request({
204             schema: 'offline',
205             table: 'OfflineXact',
206             action: 'insertOrReplace',
207             rows: [{value: obj}]
208         }).then(function() {
209             return service.request({
210                 schema: 'cache',
211                 table: 'CacheDate',
212                 action: 'insertOrReplace',
213                 rows: [{type: '_offlineXact', cachedate : new Date()}]
214             });
215         });
216     }
217
218     service.populateBlockList = function() {
219         return service.request({
220             action: 'populateBlockList',
221             authtoken: egCore.auth.token()
222         });
223     }
224
225     // Returns a promise with true for blocked, false for not blocked
226     service.testOfflineBlock = function (barcode) {
227         return service.request({
228             schema: 'offline',
229             table: 'OfflineBlocks',
230             action: 'selectWhereEqual',
231             field: 'barcode',
232             value: barcode
233         }).then(function(resp) {
234             if (resp.length === 0) return null;
235             return resp[0].reason;
236         });
237     }
238
239     service.setStatCatsCache = function (statcats) {
240         if (lf.isOffline || !statcats || statcats.length === 0) 
241             return $q.when();
242
243         var rows = statcats.map(function(cat) {
244             return {id: cat.id(), value: egCore.idl.toHash(cat)}
245         });
246
247         return service.request({
248             schema: 'cache',
249             table: 'StatCat',
250             action: 'insertOrReplace',
251             rows: rows
252         });
253     }
254
255     service.getStatCatsCache = function () {
256
257         return service.request({
258             schema: 'cache',
259             table: 'StatCat',
260             action: 'selectAll'
261         }).then(function(list) {
262             var result = [];
263             list.forEach(function(s) {
264                 var sc = egCore.idl.fromHash('actsc', s.value);
265
266                 if (Array.isArray(sc.default_entries())) {
267                     sc.default_entries(
268                         sc.default_entries().map( function (k) {
269                             return egCore.idl.fromHash('actsced', k);
270                         })
271                     );
272                 }
273
274                 if (Array.isArray(sc.entries())) {
275                     sc.entries(
276                         sc.entries().map( function (k) {
277                             return egCore.idl.fromHash('actsce', k);
278                         })
279                     );
280                 }
281
282                 result.push(sc);
283             });
284
285             return result;
286         });
287     }
288
289     service.setSettingsCache = function (settings) {
290         if (lf.isOffline) return $q.when();
291
292         var rows = [];
293         angular.forEach(settings, function (val, key) {
294             rows.push({name  : key, value : JSON.stringify(val)});
295         });
296
297         return service.request({
298             schema: 'cache',
299             table: 'Setting',
300             action: 'insertOrReplace',
301             rows: rows
302         });
303     }
304
305     service.getSettingsCache = function (settings) {
306
307         var promise;
308
309         if (settings && settings.length) {
310             promise = service.request({
311                 schema: 'cache',
312                 table: 'Setting',
313                 action: 'selectWhereIn',
314                 field: 'name',
315                 value: settings
316             });
317         } else {
318             promise = service.request({
319                 schema: 'cache',
320                 table: 'Setting',
321                 action: 'selectAll'
322             });
323         }
324
325         return promise.then(
326             function(resp) {
327                 resp.forEach(function(s) { s.value = JSON.parse(s.value); });
328                 return resp;
329             }
330         );
331     }
332
333     service.setListInOfflineCache = function (type, list) {
334         if (lf.isOffline) return $q.when();
335
336         return service.isCacheGood(type).then(function(good) {
337             if (good) { return };  // already cached
338
339             var pkey = egCore.idl.classes[type].pkey;
340             var rows = Object.values(list).map(function(item) {
341                 return {
342                     type: type, 
343                     id: '' + item[pkey](), 
344                     object: egCore.idl.toHash(item)
345                 };
346             });
347
348             return service.request({
349                 schema: 'cache',
350                 table: 'Object',
351                 action: 'insertOrReplace',
352                 rows: rows
353             }).then(function(resp) {
354                 return service.request({
355                     schema: 'cache',
356                     table: 'CacheDate',
357                     action: 'insertOrReplace',
358                     rows: [{type: type, cachedate : new Date()}]
359                 });
360             });
361         });
362     }
363
364     service.getListFromOfflineCache = function(type) {
365         return service.request({
366             schema: 'cache',
367             table: 'Object',
368             action: 'selectWhereEqual',
369             field: 'type',
370             value: type
371         }).then(function(resp) {
372             return resp.map(function(item) {
373                 return egCore.idl.fromHash(type,item['object']);
374             });
375         });
376     }
377
378     service.reconstituteList = function(type) {
379         if (lf.isOffline) {
380             console.debug('egLovefield reading ' + type + ' list');
381             return service.getListFromOfflineCache(type).then(function (list) {
382                 egCore.env.absorbList(list, type, true)
383                 return $q.when(true);
384             });
385         }
386         return $q.when(false);
387     }
388
389     service.reconstituteTree = function(type) {
390         if (lf.isOffline) {
391             console.debug('egLovefield reading ' + type + ' tree');
392
393             var pkey = egCore.idl.classes[type].pkey;
394             var parent_field = 'parent';
395
396             if (type == 'aou') {
397                 parent_field = 'parent_ou';
398             }
399
400             return service.getListFromOfflineCache(type).then(function (list) {
401                 var hash = {};
402                 var top = null;
403                 angular.forEach(list, function (item) {
404
405                     // Special case for aou, to reconstitue ou_type
406                     if (type == 'aou') {
407                         if (item.ou_type()) {
408                             item.ou_type( egCore.idl.fromHash('aout', item.ou_type()) );
409                         }
410                     }
411
412                     hash[''+item[pkey]()] = item;
413                     if (!item[parent_field]()) {
414                         top = item;
415                     } else if (angular.isObject(item[parent_field]())) {
416                         // un-objectify the parent
417                         item[parent_field](
418                             item[parent_field]()[pkey]()
419                         );
420                     }
421                 });
422
423                 angular.forEach(list, function (item) {
424                     item.children([]); // just clear it out if there's junk in there
425
426                     item.children( list.filter(function (kid) {
427                         return kid[parent_field]() == item[pkey]();
428                     }) );
429                 });
430
431                 angular.forEach(list, function (item) {
432                     if (item[parent_field]()) {
433                         item[parent_field]( hash[''+item[parent_field]()] );
434                     }
435                 });
436
437                 egCore.env.absorbTree(top, type, true)
438                 return $q.when(true)
439             });
440         }
441         return $q.when(false);
442     }
443
444     return service;
445 }]);
446