promise versions of record proxy fns
[tine20] / tine20 / Tinebase / js / data / RecordProxy.js
1 /*
2  * Tine 2.0
3  * 
4  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
5  * @author      Cornelius Weiss <c.weiss@metaways.de>
6  * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
7  *
8  */
9  
10 Ext.ns('Tine.Tinebase.data');
11
12 /**
13  * @namespace   Tine.Tinebase.data
14  * @class       Tine.Tinebase.data.RecordProxy
15  * @extends     Ext.data.DataProxy
16  * @author      Cornelius Weiss <c.weiss@metaways.de>
17  * 
18  * Generic record proxy for an model/datatype of an application
19  * 
20  * @constructor
21  * @param {Object} config Config Object
22  */
23 Tine.Tinebase.data.RecordProxy = function(c) {
24     // we support all actions
25     c.api = {read: true, create: true, update: true, destroy: true};
26     
27     Tine.Tinebase.data.RecordProxy.superclass.constructor.call(this, c);
28     
29     Ext.apply(this, c);
30     this.appName    = this.appName    ? this.appName    : this.recordClass.getMeta('appName');
31     this.modelName  = this.modelName  ? this.modelName  : this.recordClass.getMeta('modelName');
32     this.idProperty = this.idProperty ? this.idProperty : this.recordClass.getMeta('idProperty');
33     
34     /* NOTE: in ExtJS records always are part of a store. The store is
35              the only instance which triggers read/write actions.
36              
37              In our edit dialoges in contrast we work with single (store-less) 
38              records and the handlers itselve trigger the read/write actions.
39              This might change in future, but as long as we do so, we also need
40              the reader/writer here.
41      */
42     this.jsonReader = new Ext.data.JsonReader(Ext.apply({
43         id: this.idProperty,
44         root: 'results',
45         totalProperty: 'totalcount'
46     }, c.readerConfig || {}), this.recordClass);
47 };
48
49 Ext.extend(Tine.Tinebase.data.RecordProxy, Ext.data.DataProxy, {
50     /**
51      * @cfg {Ext.data.Record} recordClass
52      * record definition class  (required)
53      */
54     recordClass: null,
55     
56     /**
57      * @type String 
58      * @property appName
59      * internal/untranslated app name
60      */
61     appName: null,
62     
63     /**
64      * default value for timeout on searches
65      * 
66      * @type Number
67      */
68     searchTimeout: 60000,
69     
70     /**
71      * default value for timeout on saving
72      * 
73      * @type Number
74      */
75     saveTimeout: 300000,
76     
77     /**
78      * default value for timeout on deleting by filter
79      * 
80      * @type Number
81      */
82     deleteByFilterTimeout: 300000,
83     
84     /**
85      * default value for timeout on deleting
86      * 
87      * @type Number
88      */
89     deleteTimeout: 120000,
90     
91     /**
92      * @type String 
93      * @property idProperty
94      * property of the id of the record
95      */
96     idProperty: null,
97     
98     /**
99      * @type String 
100      * @property modelName
101      * name of the model/record 
102      */
103     modelName: null,
104     
105     /**
106      * id of last transaction
107      * @property transId
108      */
109     transId: null,
110
111     /**
112      * TODO is this really needed?
113      */
114     onDestroyRecords: Ext.emptyFn,
115     removeFromBatch: Ext.emptyFn,
116     
117     /**
118      * Aborts any outstanding request.
119      * @param {Number} transactionId (Optional) defaults to the last transaction
120      */
121     abort : function(transactionId) {
122         return Ext.Ajax.abort(transactionId);
123     },
124     
125     /**
126      * Determine whether this object has a request outstanding.
127      * @param {Number} transactionId (Optional) defaults to the last transaction
128      * @return {Boolean} True if there is an outstanding request.
129      */
130     isLoading : function(transId){
131         return Ext.Ajax.isLoading(transId);
132     },
133         
134     /**
135      * loads a single 'full featured' record
136      * 
137      * @param   {Ext.data.Record} record
138      * @param   {Object} options
139      * @return  {Number} Ext.Ajax transaction id
140      * @success {Ext.data.Record}
141      */
142     loadRecord: function(record, options) {
143         options = options || {};
144         options.params = options.params || {};
145         options.beforeSuccess = function(response) {
146             this.postMessage('update', response.responseText);
147             return [this.recordReader(response)];
148         };
149         
150         var p = options.params;
151         p.method = this.appName + '.get' + this.modelName;
152         p.id = Ext.isString(record) ? record : record.get(this.idProperty);
153         
154         return this.doXHTTPRequest(options);
155     },
156
157     promiseLoadRecord: function(record, options) {
158         var me = this;
159         return new Promise(function (fulfill, reject) {
160             try {
161                 me.loadRecord(record, Ext.apply(options || {}, {
162                     success: function (r) {
163                         fulfill(r);
164                     },
165                     failure: function (error) {
166                         reject(new Error(error));
167                     }
168                 }));
169             } catch (error) {
170                 if (Ext.isFunction(reject)) {
171                     reject(new Error(options));
172                 }
173             }
174         });
175     },
176
177     /**
178      * searches all (lightweight) records matching filter
179      * 
180      * @param   {Object} filter
181      * @param   {Object} paging
182      * @param   {Object} options
183      * @return  {Number} Ext.Ajax transaction id
184      * @success {Object} root:[records], totalcount: number
185      */
186     searchRecords: function(filter, paging, options) {
187         options = options || {};
188         options.params = options.params || {};
189         
190         var p = options.params;
191         
192         p.method = this.appName + '.search' + this.modelName + 's';
193         p.filter = (filter) ? filter : [];
194         p.paging = paging;
195
196         options.beforeSuccess = function(response) {
197             return [this.jsonReader.read(response)];
198         };
199         
200         // increase timeout as this can take a longer (1 minute)
201         options.timeout = this.searchTimeout;
202         
203         return this.doXHTTPRequest(options);
204     },
205     
206     /**
207      * saves a single record
208      * 
209      * @param   {Ext.data.Record} record
210      * @param   {Object} options
211      * @param   {Object} additionalArguments
212      * @return  {Number} Ext.Ajax transaction id
213      * @success {Ext.data.Record}
214      */
215     saveRecord: function(record, options, additionalArguments) {
216         options = options || {};
217         options.params = options.params || {};
218         options.beforeSuccess = function(response) {
219             // do we need to distingush create/update?
220             this.postMessage('update', response.responseText);
221             return [this.recordReader(response)];
222         };
223         
224         var p = options.params;
225         p.method = this.appName + '.save' + this.modelName;
226         p.recordData = record.data;
227         if (additionalArguments) {
228             Ext.apply(p, additionalArguments);
229         }
230         
231         // increase timeout as this can take a longer (5 minutes)
232         options.timeout = this.saveTimeout;
233         
234         return this.doXHTTPRequest(options);
235     },
236
237     promiseSaveRecord: function(record, options, additionalArguments) {
238         var me = this;
239         return new Promise(function (fulfill, reject) {
240             try {
241                 me.saveRecord(record, Ext.apply(options || {}, {
242                     success: function (r) {
243                         fulfill(r);
244                     },
245                     failure: function (error) {
246                         reject(new Error(error));
247                     }
248                 }), additionalArguments);
249             } catch (error) {
250                 if (Ext.isFunction(reject)) {
251                     reject(new Error(options));
252                 }
253             }
254         });
255     },
256
257     /**
258      * deletes multiple records identified by their ids
259      * 
260      * @param   {Array} records Array of records or ids
261      * @param   {Object} options
262      * @param   {Object} additionalArguments
263      * @return  {Number} Ext.Ajax transaction id
264      * @success 
265      */
266     deleteRecords: function(records, options, additionalArguments) {
267         options = options || {};
268         options.params = options.params || {};
269         options.params.method = this.appName + '.delete' + this.modelName + 's';
270         options.params.ids = this.getRecordIds(records);
271         if (additionalArguments) {
272             Ext.apply(options.params, additionalArguments);
273         }
274
275         options.beforeSuccess = function(response) {
276             var _ = window.lodash,
277                 me = this;
278
279             _.each(records, function(record) {
280                 me.postMessage('delete', record.data);
281             });
282         };
283
284         // increase timeout as this can take a long time (2 mins)
285         options.timeout = this.deleteTimeout;
286         
287         return this.doXHTTPRequest(options);
288     },
289
290     /**
291      * deletes multiple records identified by a filter
292      * 
293      * @param   {Object} filter
294      * @param   {Object} options
295      * @return  {Number} Ext.Ajax transaction id
296      * @success 
297      */
298     deleteRecordsByFilter: function(filter, options) {
299         options = options || {};
300         options.params = options.params || {};
301         options.params.method = this.appName + '.delete' + this.modelName + 'sByFilter';
302         options.params.filter = filter;
303         
304         // increase timeout as this can take a long time (5 mins)
305         options.timeout = this.deleteByFilterTimeout;
306         
307         return this.doXHTTPRequest(options);
308     },
309     
310     /**
311      * updates multiple records with the same data
312      * 
313      * @param   {Array} filter filter data
314      * @param   {Object} updates
315      * @return  {Number} Ext.Ajax transaction id
316      * @success
317      */
318     updateRecords: function(filter, updates, options) {
319         options = options || {};
320         options.params = options.params || {};
321         options.params.method = this.appName + '.updateMultiple' + this.modelName + 's';
322         options.params.filter = filter;
323         options.params.values = updates;
324         
325         options.beforeSuccess = function(response) {
326             return [Ext.util.JSON.decode(response.responseText)];
327         };
328         
329         return this.doXHTTPRequest(options);
330     },
331     
332     /**
333      * returns an array of ids
334      * 
335      * @private 
336      * @param  {Ext.data.Record|Array}
337      * @return {Array} of ids
338      */
339     getRecordIds : function(records) {
340         var ids = [];
341         
342         if (! Ext.isArray(records)) {
343             records = [records];
344         }
345         
346         for (var i=0; i<records.length; i++) {
347             ids.push(records[i].id ? records[i].id : records.id);
348         }
349         
350         return ids;
351     },
352     
353     /**
354      * required method for Ext.data.Proxy, used by store
355      */
356     load : function(params, reader, callback, scope, arg){
357         if(this.fireEvent("beforeload", this, params) !== false){
358             
359             // move paging to own object
360             var paging = {
361                 sort:  params.sort,
362                 dir:   params.dir,
363                 start: params.start,
364                 limit: params.limit
365             };
366             
367             this.searchRecords(params.filter, paging, {
368                 params: params,
369                 scope: this,
370                 success: function(records) {
371                     callback.call(scope||this, records, arg, true);
372                 },
373                 failure: function(exception) {
374                     //this.fireEvent('exception', this, 'remote', 'read', options, response, arg);
375                     this.fireEvent('loadexception', this, 'remote',  exception, arg);
376                     callback.call(scope||this, exception, arg, false);
377                 }
378             });
379             
380         } else {
381             callback.call(scope||this, null, arg, false);
382         }
383     },
384     
385     /**
386      * do the request
387      * 
388      * @param {} action
389      * @param {} rs
390      * @param {} params
391      * @param {} reader
392      * @param {} callback
393      * @param {} scope
394      * @param {} options
395      */
396     doRequest : function(action, rs, params, reader, callback, scope, options) {
397         var opts = {
398             params: params, 
399             callback: callback,
400             scope: scope
401         };
402         
403         switch (action) {
404             case Ext.data.Api.actions.create:
405                 this.saveRecord(rs, opts);
406                 break;
407             case Ext.data.Api.actions.read:
408                 this.load(params, reader, callback, scope, options);
409                 break;
410             case Ext.data.Api.actions.update:
411                 this.saveRecord(rs, opts);
412                 break;
413             case Ext.data.Api.actions.destroy:
414                 this.deleteRecords(rs, opts);
415                 break;
416         }
417     },
418
419     
420     /**
421      * returns reader
422      * 
423      * @return {Ext.data.DataReader}
424      */
425     getReader: function() {
426         return this.jsonReader;
427     },
428     
429     /**
430      * reads a single 'fully featured' record from json data
431      * 
432      * NOTE: You might want to override this method if you have a more complex record
433      * 
434      * @param  XHR response
435      * @return {Ext.data.Record}
436      */
437     recordReader: function(response) {
438         return Tine.Tinebase.data.Record.setFromJson(response.responseText, this.recordClass);
439     },
440     
441     /**
442      * is request still loading?
443      * 
444      * @param  {Number} Ext.Ajax transaction id
445      * @return {Bool}
446      */
447     isLoading: function(tid) {
448         return Ext.Ajax.isLoading(tid);
449     },
450     
451     /**
452      * performs an Ajax request
453      */
454     doXHTTPRequest: function(options) {
455         var requestOptions = {
456             scope: this,
457             params: options.params,
458             callback: options.callback,
459             success: function(response) {
460                 var args = [];
461                 if (typeof options.beforeSuccess == 'function') {
462                     args = options.beforeSuccess.call(this, response);
463                 }
464
465                 if (typeof options.success == 'function') {
466                     options.success.apply(options.scope, args);
467                 }
468             },
469             // note incoming options are implicitly json-rpc converted
470             failure: function (response, jsonrpcoptions) {
471                 var responseData = Ext.decode(response.responseText),
472                     exception = responseData.data ? responseData.data : responseData;
473                     
474                 exception.request = jsonrpcoptions.jsonData;
475                 exception.response = response.responseText;
476
477                 var args = [exception];
478                 if (typeof options.beforeFailure == 'function') {
479                     args = options.beforeFailure.call(this, response);
480                 }
481                 if (typeof options.failure == 'function') {
482                     Tine.log.debug('Tine.Tinebase.data.RecordProxy::doXHTTPRequest -> call failure fn');
483                     options.failure.apply(options.scope, args);
484                 }
485                 // requests with callback need to define their own exception handling
486                 else if (! options.callback) {
487                     Tine.log.debug('Tine.Tinebase.data.RecordProxy::doXHTTPRequest -> handle exception');
488                     this.handleRequestException(exception);
489                 } else {
490                     Tine.log.debug('Tine.Tinebase.data.RecordProxy::doXHTTPRequest -> call callback fn');
491                 }
492             }
493         };
494         
495         if (options.timeout) {
496             requestOptions.timeout = options.timeout;
497         }
498         
499         this.transId = Ext.Ajax.request(requestOptions);
500         
501         return this.transId;
502     },
503     
504     /**
505      * default exception handler
506      * 
507      * @param {Object} exception
508      */
509     handleRequestException: function(exception) {
510         Tine.Tinebase.ExceptionHandler.handleRequestException(exception);
511     },
512
513     /**
514      * posts message about record crud action on central message bus
515      *
516      * NOTE: we don't use the Ext internal write event as:
517      *       a) to deal with cross window issues we only publish bare data
518      *       b) to be able mix with other libraries
519      *
520      * @param {String} action [create|update|delete]
521      * @param {Record} record
522      */
523     postMessage: function(action, record) {
524         var _ = window.lodash,
525             recordData = _.isFunction(record.beginEdit) ? record.data :
526                          _.isString(record) ? JSON.parse(record) :
527                          record;
528
529         window.postal.publish({
530             channel: "recordchange",
531             topic: [this.appName, this.modelName, action].join('.'),
532             data: recordData
533         });
534     }
535 });