Merge pull request #1609 from xtuple/4_5_x
[xtuple] / enyo-client / application / source / ext / datasource.js
1 /*jshint indent:2, curly:true, eqeqeq:true, immed:true, latedef:true,
2 newcap:true, noarg:true, regexp:true, undef:true, strict:true, trailing:true,
3 white:true*/
4 /*global XT:true, XM:true, io:true, Backbone:true, _:true, console:true, enyo:true
5   document:true, setTimeout:true, document:true, RJSON:true */
6
7 (function () {
8   "use strict";
9
10   XT.Request = {
11     /** @scope XT.Request.prototype */
12
13     send: function (data) {
14       var details = XT.session.details,
15         sock = XT.dataSource._sock,
16         notify = this._notify,
17         handle = this._handle,
18         errorMessage,
19         payload = {
20           payload: data
21         },
22         callback;
23
24       if (!notify || !(notify instanceof Function)) {
25         callback = function () {};
26       } else {
27         callback = function (response) {
28           notify(response);
29
30           if (response && response.isError) {
31             // notify the user in the case of error.
32             // Wait a second to make sure that whatever the expected callback
33             // function has time to do whatever it has to do. Not pretty,
34             // but works across a broad range of callback errors.
35             errorMessage = response.status ? response.status.message : response.message;
36             if (errorMessage) {
37               XT.log("Error:", errorMessage);
38               setTimeout(function () {
39                 XT.app.$.postbooks.notify(null, {
40                   type: XM.Model.CRITICAL,
41                   message: errorMessage
42                 });
43               }, 1000);
44             }
45           }
46         };
47       }
48
49       // attach the session details to the payload
50       payload = _.extend(payload, details);
51
52       if (XT.session.config.debugging) {
53         XT.log("Socket sending: %@".replace("%@", handle), payload);
54       }
55
56       sock.json.emit(handle, payload, callback);
57
58       return this;
59     },
60
61     handle: function (event) {
62       this._handle = event;
63       return this;
64     },
65
66     notify: function (method) {
67       var args = Array.prototype.slice.call(arguments).slice(1);
68       this._notify = function (response) {
69         args.unshift(response);
70         method.apply(null, args);
71       };
72       return this;
73     }
74   };
75
76   XT.DataSource = {
77     // TODO - Old way.
78     //datasourceUrl: DOCUMENT_HOSTNAME,
79     //datasourcePort: 443,
80     isConnected: false,
81
82     /**
83       Returns the value of the user preference if it exists in the
84       preferences cache.
85     */
86     getUserPreference: function (name) {
87       var pref = _.find(XT.session.preferences.attributes,
88         function (v, k) {
89           return k === name;
90         });
91       return pref;
92     },
93
94     /**
95       Performs a dispatch call to save the given payload
96       string to the specified user preference.
97       @param {String} name
98       @param {String} stringified payload
99       @param {String} operation string
100     */
101     saveUserPreference: function (name, payload, op) {
102       var options = {}, patch = [], param = [],
103         path = "/" + name;
104       options.error = function (resp) {
105         if (resp.isError) { console.log("uh oh"); }
106       };
107       patch = [
108         {"op": op, "path": path, "value": payload}
109       ];
110       // Add the patch array to the parameter array
111       param.push(patch);
112       XM.ModelMixin.dispatch('XT.Session',
113         'commitPreferences', param, options);
114     },
115
116     /**
117      * Decode the server's response to a bona fide Javascript object.
118      * @see node-datasource/routes/data.js#encodeResponse
119      *
120      * @param {Object}  response  the server's response object
121      * @param {Object}  options   the request options
122      *
123      * @return {Object} the server's response as a Javascript object.
124      */
125     decodeResponse: function (response, options) {
126       var encoding = options.encoding || XT.session.config.encoding;
127
128       if (!encoding) {
129         return response;
130       }
131       else if (encoding === "rjson") {
132         return RJSON.unpack(response);
133       }
134       else {
135         return {
136           isError: true,
137           status: "Encoding [" + encoding + "] not recognized."
138         };
139       }
140     },
141
142     /*
143     Server request
144
145     @param {Object} model or collection
146     @param {String} method
147     @param {Object} payload
148     @param {Object} options
149     */
150     request: function (obj, method, payload, options) {
151       var that = this,
152         isDispatch = _.isObject(payload.dispatch),
153         complete = function (response) {
154           var dataHash,
155             params = {},
156             error,
157             attrs;
158
159           // Handle error
160           if (response.isError) {
161             if (options && options.error) {
162               params.error = response.message;
163               error = XT.Error.clone('xt1001', { params: params });
164               options.error.call(that, error);
165             }
166             return;
167           }
168
169           dataHash = that.decodeResponse(response, options).data;
170
171           // Handle no data on a single record retrieve as error
172           if (method === "get" && options.id &&
173             _.isEmpty(dataHash.data)) {
174             if (options && options.error) {
175               error = XT.Error.clone('xt1007');
176               options.error.call(obj, error);
177             }
178             return;
179           }
180
181           // Handle success
182           if (options && options.success) {
183             if (isDispatch) {
184               options.success(dataHash, options);
185               return;
186             }
187
188             // Handle case where an entire collection was saved
189             if (options.collection) {
190               // Destroyed models won't have a response unless they made the whole
191               // request fail. Assume successful destruction.
192               options.collection.each(function (model) {
193                 if (model.getStatus() === XM.Model.DESTROYED_DIRTY) {
194                   model.trigger("destroy", model, model.collection, options);
195                 }
196               });
197
198               if (dataHash[0].patches) {
199                 _.each(dataHash, function (data) {
200                   var cModel;
201
202                   cModel = _.find(options.collection.models, function (model) {
203                     return data.id === model.id;
204                   });
205                   attrs = cModel.toJSON({includeNested: true});
206                   XM.jsonpatch.apply(attrs, data.patches);
207                   cModel.etag = data.etag;
208
209                   // This is a hack to work around Backbone messing with 
210                   // attributes when we don't want it to. Parse function
211                   // on model handles the other side of this
212                   options.fixAttributes = cModel.attributes;
213
214                   options.success.call(that, cModel, attrs, options);
215
216                   options.collection.remove(cModel);
217                 });
218               } else {
219                 // This typically happens when requery option === false
220                 // and no patches were found
221                 options.success.call(that, options.collection.at(0), true, options);
222               }
223               return;
224
225             // Handle normal single model case
226             } else if (dataHash.patches) {
227               if (obj) {
228                 attrs = obj.toJSON({includeNested: true});
229                 XM.jsonpatch.apply(attrs, dataHash.patches);
230               } else {
231                 attrs = dataHash.patches;
232               }
233             } else {
234               attrs = dataHash.data;
235             }
236             if (obj instanceof Backbone.Model) {
237               obj.etag = dataHash.etag;
238             }
239             options.success.call(that, obj, attrs, options);
240           }
241         };
242
243       _(payload).extend({
244         encoding: options.encoding || XT.session.config.encoding
245       });
246
247       return XT.Request
248                .handle(method)
249                .notify(complete)
250                .send(payload);
251     },
252
253     /**
254       Generic implementation of AJAX response handler
255      */
256     ajaxSuccess: function (inSender, inResponse) {
257       var params = {}, error;
258
259       // handle error
260       if (inResponse.isError) {
261         if (inSender.error) {
262           params.error = inResponse.message;
263           error = XT.Error.clone('xt1001', { params: params });
264           inSender.error.call(this, error);
265         }
266         return;
267       }
268
269       // handle success
270       if (inSender.success) {
271         inSender.success.call(this, inResponse.data);
272       }
273     },
274     /**
275       Generic implementation of AJAX request
276      */
277     callRoute: function (path, payload, options) {
278       var ajax = new enyo.Ajax({
279         url: XT.getOrganizationPath() + "/" + path,
280         success: options ? options.success : undefined,
281         error: options ? options.error : undefined
282       });
283
284       ajax.response(this.ajaxSuccess);
285       ajax.go(payload);
286     },
287
288     /*
289       Reset a global user's password.
290
291     @param {String} id
292     @param {Function} options.success callback
293     @param {Function} options.error callback
294     */
295     resetPassword: function (id, options) {
296       var payload = {
297         id: id,
298         newPassword: options.newPassword
299       };
300
301       if (options.newUser) {
302         // we don't want to send false at all, because false turns
303         // into "false" over the wire which is truthy.
304         payload.newUser = options.newUser;
305       }
306       this.callRoute("reset-password", payload, options);
307     },
308
309     /*
310       Change a global password.
311
312     @param {Object} parameters
313     @param {Function} options.success callback
314     @param {Function} options.error callback
315     */
316     changePassword: function (params, options) {
317       var payload = {
318           oldPassword: params.oldPassword,
319           newPassword: params.newPassword
320         };
321
322       this.callRoute("change-password", payload, options);
323     },
324
325     /*
326       Sends a request to node to send out an email
327
328     @param {Object} payload
329     @param {String} payload.from
330     @param {String} payload.to
331     @param {String} payload.cc
332     @param {String} payload.bcc
333     @param {String} payload.subject
334     @param {String} payload.text
335     */
336     sendEmail: function (payload, options) {
337       if (payload.body && !payload.text) {
338         // be flexible with the inputs. Node-emailer prefers the term text, but
339         // body is fine for us as well.
340         payload.text = payload.body;
341       }
342       this.callRoute("email", payload, options);
343     },
344
345     /* @private */
346     connect: function (callback) {
347       if (this.isConnected) {
348         if (callback && callback instanceof Function) {
349           callback();
350         }
351         return;
352       }
353
354       XT.log("Attempting to connect to the datasource");
355
356       var host = document.location.host,
357           path = "clientsock",
358           protocol = document.location.protocol,
359           datasource = "%@//%@/%@".f(protocol, host, path),
360           self = this,
361           didConnect = this.sockDidConnect,
362           didError = this.sockDidError;
363
364       // Attempt to connect and supply the appropriate responders for the connect and error events.
365       this._sock = io.connect(datasource, {secure: true});
366       this._sock.on("connect", function () {
367         //didConnect.call(self, callback);
368       });
369       this._sock.on("ok", function () {
370         didConnect.call(self, callback);
371       });
372       this._sock.on("error", function (err) {
373         // New express conneciton error doesn't send err message back here, but does call this.
374         XT.log("SERVER ERROR.");
375         didError.call(self, err, callback);
376       });
377       this._sock.on("connect_failed", function (err) {
378         // This app has not even started yet. Don't bother with the popup because it won't work.
379         XT.log("AUTHENTICATION INVALID: ", err);
380         XT.logout();
381         return;
382       });
383
384       this._sock.on("debug", function (msg) {
385         XT.log("SERVER DEBUG => ", msg);
386       });
387
388       this._sock.on("timeout", function (msg) {
389         XT.log("SERVER SAID YOU TIMED OUT");
390         var p = XT.app.createComponent({
391           kind: "onyx.Popup",
392           centered: true,
393           modal: true,
394           floating: true,
395           scrim: true,
396           autoDismiss: false,
397           style: "text-align: center;",
398           components: [
399             {content: "_sessionTimedOut".loc()},
400             {kind: "onyx.Button", content: "_ok".loc(), tap: function () { XT.logout(); }}
401           ]
402         });
403         p.show();
404       });
405
406       this._sock.on("disconnect", function () {
407         XT.log("DISCONNECTED FROM SERVER");
408       });
409     },
410
411     /* @private */
412     sockDidError: function (err, callback) {
413       // TODO: need some handling here
414       console.warn(err);
415       if (callback && callback instanceof Function) {
416         callback(err);
417       }
418     },
419
420     /* @private */
421     sockDidConnect: function (callback) {
422
423       XT.log("Successfully connected to the datasource");
424
425       this.isConnected = true;
426
427       // go ahead and create the session object for the
428       // application if it does not already exist
429       if (!XT.session) {
430         XT.session = Object.create(XT.Session);
431         setTimeout(_.bind(XT.session.start, XT.session), 0);
432       }
433
434       if (callback && callback instanceof Function) {
435         callback();
436       }
437     },
438
439     reset: function () {
440       if (!this.isConnected) {
441         return;
442       }
443
444       var sock = this._sock;
445       if (sock) {
446         sock.disconnect();
447         this.isConnected = false;
448       }
449
450       this.connect();
451     }
452
453   };
454
455   XT.dataSource = Object.create(XT.DataSource);
456
457 }());