API Docs for: 0.0.1
Show:

File: src/io kopie/extra/io-transfer.js

  1. "use strict";
  2.  
  3. /**
  4. * Extends io by adding the methods `get`, `read`, `update`, `insert`, `send` and `delete` to it.
  5. *
  6. * @example
  7. * var IO = require("io/extra/io-transfer.js")(window);
  8. *
  9. * <i>Copyright (c) 2014 ITSA - https://github.com/itsa</i>
  10. * New BSD License - http://choosealicense.com/licenses/bsd-3-clause/
  11. *
  12. * @module io
  13. * @submodule io-transfer
  14. * @class IO
  15. * @since 0.0.1
  16. */
  17.  
  18. require('js-ext/lib/string.js');
  19. require('js-ext/lib/object.js');
  20. require('polyfill/polyfill-base.js');
  21.  
  22. /*jshint proto:true */
  23. var NAME = '[io-transfer]: ',
  24. createHashMap = require('js-ext/extra/hashmap.js').createMap,
  25. PROTO_SUPPORTED = !!Object.__proto__,
  26. REVIVER = function(key, value) {
  27. return ((typeof value==='string') && value.toDate()) || value;
  28. },
  29. REVIVER_PROTOTYPED = function(key, value, proto, parseProtoCheck, reviveDate) {
  30. if (reviveDate && (typeof value==='string')) {
  31. return value.toDate() || value;
  32. }
  33. if (!Object.isObject(value)) {
  34. return value;
  35. }
  36. // only first level of objects can be given the specified prototype
  37. if ((typeof parseProtoCheck === 'function') && !parseProtoCheck(value)) {
  38. return value;
  39. }
  40. if (PROTO_SUPPORTED) {
  41. value.__proto__ = proto;
  42. return value;
  43. }
  44. return value.deepClone(null, proto);
  45. },
  46. MIME_JSON = 'application/json',
  47. CONTENT_TYPE = 'Content-Type',
  48. DELETE = 'delete',
  49. REGEXP_ARRAY = /^( )*\[/,
  50. REGEXP_OBJECT = /^( )*{/,
  51. REGEXP_REMOVE_LAST_COMMA = /^(.*),( )*$/,
  52. SPINNER_ICON = 'spinnercircle-anim',
  53. MIN_SHOWUP = 500;
  54. /*jshint proto:false */
  55.  
  56. module.exports = function (window) {
  57.  
  58. window._ITSAmodules || Object.protectedProp(window, '_ITSAmodules', createHashMap());
  59.  
  60. if (window._ITSAmodules.IO_Transfer) {
  61. return window._ITSAmodules.IO_Transfer; // IO_Transfer was already created
  62. }
  63.  
  64. var IO = require('../io.js')(window),
  65.  
  66. /*
  67. * Adds properties to the xhr-object: in case of streaming,
  68. * xhr._parseStream=function is created to parse streamed data.
  69. *
  70. * @method _progressHandle
  71. * @param xhr {Object} containing the xhr-instance
  72. * @param props {Object} the propertie-object that is added too xhr and can be expanded
  73. * @param options {Object} options of the request
  74. * @private
  75. */
  76. _entendXHR = function(xhr, props, options /*, promise */) {
  77. var isarray, isobject, parialdata, regexpcomma, followingstream;
  78. if ((typeof options.streamback === 'function') && options.headers && (options.headers.Accept==='application/json')) {
  79. console.log(NAME, 'entendXHR');
  80. xhr._parseStream = function(streamData) {
  81. console.log(NAME, 'entendXHR --> _parseStream');
  82. // first step is to determine if the final response would be an array or an object
  83. // partial responses should be expanded to the same type
  84. if (!followingstream) {
  85. isarray = REGEXP_ARRAY.test(streamData);
  86. isarray || (isobject = REGEXP_OBJECT.test(streamData));
  87. }
  88. try {
  89. if (isarray || isobject) {
  90. regexpcomma = streamData.match(REGEXP_REMOVE_LAST_COMMA);
  91. parialdata = regexpcomma ? streamData.match(REGEXP_REMOVE_LAST_COMMA)[1] : streamData;
  92. }
  93. else {
  94. parialdata = streamData;
  95. }
  96. parialdata = (followingstream && isarray ? '[' : '') + (followingstream && isobject ? '{' : '') + parialdata + (regexpcomma && isarray ? ']' : '') + (regexpcomma && isobject ? '}' : '');
  97. // note: parsing will fail for the last streamed part, because there will be a double ] or }
  98. streamData = JSON.parse(parialdata, (options.parseJSONDate) ? REVIVER : null);
  99. }
  100. catch(err) {
  101. console.warn(NAME, err);
  102. }
  103. followingstream = true;
  104. return streamData;
  105. };
  106. }
  107. return xhr;
  108. };
  109.  
  110. IO._xhrList.push(_entendXHR);
  111.  
  112. /**
  113. * Performs an AJAX GET request. Shortcut for a call to [`xhr`](#method_xhr) with `method` set to `'GET'`.
  114. * Additional parameters can be on the url (with questionmark), through `params`, or both.
  115. *
  116. * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
  117. * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
  118. *
  119. * Note: `params` should be a plain object with only primitive types which are transformed into key/value pairs.
  120. *
  121. * @method get
  122. * @param url {String} URL of the resource server
  123. * @param [params] {Object} additional parameters.
  124. * should be a plain object with only primitive types which are transformed into key/value pairs.
  125. * @param [options] {Object}
  126. * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
  127. * @param [options.headers] {Object} HTTP request headers.
  128. * @param [options.responseType] {String} Force the response type.
  129. * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
  130. * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
  131. * @param [options.preventCache=false] {boolean} whether to prevent caching --> a timestamp is added by parameter _ts
  132. * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
  133. * @return {Promise}
  134. * on success:
  135. * xhr {XMLHttpRequest|XDomainRequest} xhr-response
  136. * on failure an Error object
  137. * reason {Error}
  138. */
  139. IO.get = function (url, options) {
  140. console.log(NAME, 'get --> '+url);
  141. var ioPromise, returnPromise;
  142. options || (options={});
  143. options.url = url;
  144. options.method = 'GET';
  145. // delete hidden property `data`: don't want accedentially to be used
  146. delete options.data;
  147. if (options.preventCache) {
  148. url += (url.contains('?') ? '&' : '?') + '_ts=' + Date.now();
  149. }
  150. ioPromise = this.request(options);
  151. returnPromise = ioPromise.then(
  152. function(xhrResponse) {
  153. return xhrResponse.responseText;
  154. }
  155. );
  156. // set `abort` to the thennable-promise:
  157. returnPromise.abort = ioPromise.abort;
  158. return returnPromise;
  159. };
  160.  
  161. /**
  162. * Performs an AJAX request with the GET HTTP method and expects a JSON-object.
  163. * The resolved Promise-callback returns an object (JSON-parsed serverresponse).
  164. *
  165. * Additional request-parameters can be on the url (with questionmark), through `params`, or both.
  166. *
  167. * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
  168. * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
  169. *
  170. * Note1: If you expect the server to response with data that consist of Date-properties, you should set `options.parseJSONDate` true.
  171. * Parsing takes a bit longer, but it will generate trully Date-objects.
  172. * Note2: CORS is supported, as long as the responseserver is set up to:
  173. * a) has a response header which allows the clientdomain:
  174. * header('Access-Control-Allow-Origin: http://www.some-site.com'); or header('Access-Control-Allow-Origin: *');
  175. * b) in cae you have set a custom HEADER (through 'options'), the responseserver MUST listen and respond
  176. * to requests with the OPTION-method
  177. * More info: allows to send to your domain: see http://remysharp.com/2011/04/21/getting-cors-working/
  178. *
  179. * @method read
  180. * @param url {String} URL of the resource server
  181. * @param [params] {Object} additional parameters.
  182. * @param [options] {Object} See also: [`I.io`](#method_xhr)
  183. * can be ignored, even if streams are used --> the returned Promise will always hold all data
  184. * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
  185. * @param [options.headers] {Object} HTTP request headers.
  186. * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
  187. * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
  188. * @param [options.parseJSONDate=false] {boolean} Whether the server returns JSON-stringified data which has Date-objects.
  189. * @param [options.parseProto] {Object} to set the prototype of any object.
  190. * @param [options.parseProtoCheck] {Function} to determine in what case the specified `parseProto` should be set as the prototype.
  191. * The function accepts the `object` as argument and should return a trully value in order to set the prototype.
  192. * When not specified, `parseProto` will always be applied (if `parseProto`is defined)
  193. * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
  194. * @return {Promise}
  195. * on success:
  196. * Object received data
  197. * on failure an Error object
  198. * reason {Error}
  199. */
  200. IO.read = function(url, params, options) {
  201. console.log(NAME, 'read --> '+url+' params: '+JSON.stringify(params));
  202. var ioPromise, returnPromise;
  203. options || (options={});
  204. options.headers || (options.headers={});
  205. options.url = url;
  206. options.method = 'GET';
  207. options.data = params;
  208. options.headers.Accept = 'application/json';
  209. // we don't want the user to re-specify the server's responsetype:
  210. delete options.responseType;
  211. ioPromise = this.request(options);
  212. returnPromise = ioPromise.then(
  213. function(xhrResponse) {
  214. // not 'try' 'catch', because, if parsing fails, we actually WANT the promise to be rejected
  215. // we also need to re-attach the 'abort-handle'
  216. console.log(NAME, 'read returns with: '+JSON.stringify(xhrResponse.responseText));
  217. // xhrResponse.responseText should be 'application/json' --> if it is not,
  218. // JSON.parse throws an error, but that's what we want: the Promise would reject
  219. if (options.parseProto) {
  220. return JSON.parse(xhrResponse.responseText, REVIVER_PROTOTYPED.rbind(null, options.parseProto, options.parseProtoCheck, options.parseJSONDate));
  221. }
  222. return JSON.parse(xhrResponse.responseText, (options.parseJSONDate) ? REVIVER : null);
  223. }
  224. );
  225. // set `abort` to the thennable-promise:
  226. returnPromise.abort = ioPromise.abort;
  227. return returnPromise;
  228. };
  229.  
  230.  
  231. /**
  232. * Sends data (object) which will be JSON-stringified before sending.
  233. * Performs an AJAX request with the PUT HTTP method by default.
  234. * When options.allfields is `false`, it will use the POST-method: see Note2.
  235. *
  236. * The 'content-type' of the header is set to 'application/json', overruling manually options.
  237. *
  238. * 'data' is send as 'body.data' and should be JSON-parsed at the server.
  239. *
  240. * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
  241. * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
  242. *
  243. * Note1: The server needs to inspect the bodyparam: 'action', which always equals 'update'.
  244. * 'body.action' is the way to distinquish 'I.IO.updateObject' from 'I.IO.insertObject'.
  245. * On purpose, we didn't make this distinction through a custom CONTENT-HEADER, because
  246. * that would lead into a more complicated CORS-setup (see Note3)
  247. * Note2: By default this method uses the PUT-request: which is preferable is you send the WHOLE object.
  248. * if you send part of the fields, set `options.allfields`=false.
  249. * This will lead into using the POST-method.
  250. * More about HTTP-methods: https://stormpath.com/blog/put-or-post/
  251. * Note3: CORS is supported, as long as the responseserver is set up to:
  252. * a) has a response header which allows the clientdomain:
  253. * header('Access-Control-Allow-Origin: http://www.some-site.com'); or header('Access-Control-Allow-Origin: *');
  254. * b) in cae you have set a custom HEADER (through 'options'), the responseserver MUST listen and respond
  255. * to requests with the OPTION-method
  256. * More info: allows to send to your domain: see http://remysharp.com/2011/04/21/getting-cors-working/
  257. * Note4: If the server response JSON-stringified data, the Promise resolves with a JS-Object. If you expect this object
  258. * to consist of Date-properties, you should set `options.parseJSONDate` true. Parsing takes a bit longer, but it will
  259. * generate trully Date-objects.
  260. *
  261. *
  262. * @method update
  263. * @param url {String} URL of the resource server
  264. * @param data {Object|Promise} Data to be sent, might be a Promise which resolves with the data-object.
  265. * @param [options] {Object} See also: [`I.io`](#method_xhr)
  266. * @param [options.allfields=true] {boolean} to specify that all the object-fields are sent.
  267. * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
  268. * @param [options.headers] {Object} HTTP request headers.
  269. * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
  270. * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
  271. * @param [options.parseJSONDate=false] {boolean} Whether the server returns JSON-stringified data which has Date-objects.
  272. * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
  273. * @return {Promise}
  274. * on success:
  275. * response {Object} usually, the final object-data, possibly modified
  276. * on failure an Error object
  277. * reason {Error}
  278. */
  279.  
  280. /**
  281. * Performs an AJAX request with the POST HTTP method by default.
  282. * When options.allfields is `true`, it will use the PUT-method: see Note2.
  283. * The send data is an object which will be JSON-stringified before sending.
  284. *
  285. * The 'content-type' of the header is set to 'application/json', overruling manually options.
  286. *
  287. * 'data' is send as 'body.data' and should be JSON-parsed at the server.
  288. * 'body.action' has the value 'insert'
  289. *
  290. * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
  291. * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
  292. *
  293. * Note1: The server needs to inspect the bodyparam: 'action', which always equals 'insert'.
  294. * 'body.action' is the way to distinquish 'I.IO.insertObject' from 'I.IO.updateObject'.
  295. * On purpose, we didn't make this distinction through a custom CONTENT-HEADER, because
  296. * that would lead into a more complicated CORS-setup (see Note3)
  297. * Note2: By default this method uses the POST-request: which is preferable if you don't know all the fields (like its unique id).
  298. * if you send ALL the fields, set `options.allfields`=true.
  299. * This will lead into using the PUT-method.
  300. * More about HTTP-methods: https://stormpath.com/blog/put-or-post/
  301. * Note3: CORS is supported, as long as the responseserver is set up to:
  302. * a) has a response header which allows the clientdomain:
  303. * header('Access-Control-Allow-Origin: http://www.some-site.com'); or header('Access-Control-Allow-Origin: *');
  304. * b) in cae you have set a custom HEADER (through 'options'), the responseserver MUST listen and respond
  305. * to requests with the OPTION-method
  306. * More info: allows to send to your domain: see http://remysharp.com/2011/04/21/getting-cors-working/
  307. * Note4: If the server response JSON-stringified data, the Promise resolves with a JS-Object. If you expect this object
  308. * to consist of Date-properties, you should set `options.parseJSONDate` true. Parsing takes a bit longer, but it will
  309. * generate trully Date-objects.
  310. *
  311. * @method insert
  312. * @param url {String} URL of the resource server
  313. * @param data {Object|Promise} Data to be sent, might be a Promise which resolves with the data-object.
  314. * @param [options] {Object} See also: [`I.io`](#method_xhr)
  315. * @param [options.allfields=false] {boolean} to specify that all the object-fields are sent.
  316. * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
  317. * @param [options.headers] {Object} HTTP request headers.
  318. * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
  319. * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
  320. * @param [options.parseJSONDate=false] {boolean} Whether the server returns JSON-stringified data which has Date-objects.
  321. * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
  322. * @return {Promise}
  323. * on success:
  324. * response {Object} usually, the final object-data, possibly modified, holding the key
  325. * on failure an Error object
  326. * reason {Error}
  327. */
  328.  
  329. /**
  330. * Performs an AJAX request with the PUT HTTP method by default.
  331. * When options.allfields is `false`, it will use the POST-method: see Note2.
  332. * The send data is an object which will be JSON-stringified before sending.
  333. *
  334. * The 'content-type' of the header is set to 'application/json', overruling manually options.
  335. *
  336. * 'data' is send as 'body.data' and should be JSON-parsed at the server.
  337. *
  338. * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
  339. * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
  340. *
  341. * Note1: By default this method uses the PUT-request: which is preferable is you send the WHOLE object.
  342. * if you send part of the fields, set `options.allfields`=false.
  343. * This will lead into using the POST-method.
  344. * More about HTTP-methods: https://stormpath.com/blog/put-or-post/
  345. * Note2: CORS is supported, as long as the responseserver is set up to:
  346. * a) has a response header which allows the clientdomain:
  347. * header('Access-Control-Allow-Origin: http://www.some-site.com'); or header('Access-Control-Allow-Origin: *');
  348. * b) in cae you have set a custom HEADER (through 'options'), the responseserver MUST listen and respond
  349. * to requests with the OPTION-method
  350. * More info: allows to send to your domain: see http://remysharp.com/2011/04/21/getting-cors-working/
  351. * Note3: If the server response JSON-stringified data, the Promise resolves with a JS-Object. If you expect this object
  352. * to consist of Date-properties, you should set `options.parseJSONDate` true. Parsing takes a bit longer, but it will
  353. * generate trully Date-objects.
  354. *
  355. * @method send
  356. * @param url {String} URL of the resource server
  357. * @param data {Object} Data to be sent.
  358. * @param [options] {Object} See also: [`I.io`](#method_xhr)
  359. * @param [options.allfields=true] {boolean} to specify that all the object-fields are sent.
  360. * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
  361. * @param [options.headers] {Object} HTTP request headers.
  362. * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
  363. * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
  364. * @param [options.parseJSONDate=false] {boolean} Whether the server returns JSON-stringified data which has Date-objects.
  365. * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
  366. * @return {Promise}
  367. * on success:
  368. * response {Object|String} any response you want the server to return.
  369. If the server send back a JSON-stringified object,
  370. it will be parsed to return as a full object
  371. You could set `options.parseJSONDate` true, it you want ISO8601-dates to be parsed as trully Date-objects
  372. * on failure an Error object
  373. * reason {Error}
  374. */
  375.  
  376. ['update', 'insert', 'send'].forEach(
  377. function (verb) {
  378. IO[verb] = function (url, data, options) {
  379. console.log(NAME, verb+' --> '+url+' data: '+JSON.stringify(data));
  380. var instance = this,
  381. allfields, useallfields, parseJSONDate, ioPromise, returnPromise;
  382. options || (options={});
  383. allfields = options.allfields,
  384. useallfields = (typeof allfields==='boolean') ? allfields : (verb!=='insert');
  385. parseJSONDate = options.parseJSONDate;
  386. options.url = url;
  387. options.method = useallfields ? 'PUT' : 'POST';
  388. options.data = data;
  389. options.headers || (options.headers={});
  390. options.headers[CONTENT_TYPE] = MIME_JSON;
  391. parseJSONDate && (options.headers['X-JSONDate']="true");
  392. if (verb!=='send') {
  393. options.headers.Accept = 'application/json';
  394. // set options.action
  395. options.headers['X-Action'] = verb;
  396. // we don't want the user to re-specify the server's responsetype:
  397. delete options.responseType;
  398. }
  399. ioPromise = instance.request(options);
  400. returnPromise = ioPromise.then(
  401. function(xhrResponse) {
  402. if (verb==='send') {
  403. return xhrResponse.responseText;
  404. }
  405. // In case of `insert` or `update`
  406. // xhrResponse.responseText should be 'application/json' --> if it is not,
  407. // JSON.parse throws an error, but that's what we want: the Promise would reject
  408. return JSON.parse(xhrResponse.responseText, parseJSONDate ? REVIVER : null);
  409. }
  410. );
  411. // set `abort` to the thennable-promise:
  412. returnPromise.abort = ioPromise.abort;
  413. return returnPromise;
  414. };
  415. }
  416. );
  417.  
  418. /**
  419. * Performs an AJAX DELETE request. Shortcut for a call to [`xhr`](#method_xhr) with `method` set to `'DELETE'`.
  420. *
  421. * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
  422. * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
  423. *
  424. * Note: `data` should be a plain object with only primitive types which are transformed into key/value pairs.
  425. *
  426. * @method delete
  427. * @param url {String} URL of the resource server
  428. * @param deleteKey {Object} Indentification of the id that has to be deleted. Typically an object like: {id: 12}
  429. * This object will be passed as the request params.
  430. * @param [options] {Object}
  431. * @param [options.url] {String} The url to which the request is sent.
  432. * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
  433. * @param [options.params] {Object} Data to be sent to the server.
  434. * @param [options.body] {Object} The content for the request body for POST method.
  435. * @param [options.headers] {Object} HTTP request headers.
  436. * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
  437. * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
  438. * @param [options.parseJSONDate=false] {boolean} Whether the server returns JSON-stringified data which has Date-objects.
  439. * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
  440. * @return {Promise}
  441. * on success:
  442. * response {Object|String} any response you want the server to return.
  443. If the server send back a JSON-stringified object,
  444. it will be parsed to return as a full object
  445. You could set `options.parseJSONDate` true, it you want ISO8601-dates to be parsed as trully Date-objects
  446. * on failure an Error object
  447. * reason {Error}
  448. */
  449.  
  450. IO[DELETE] = function (url, deleteKey, options) {
  451. console.log(NAME, 'delete --> '+url+' deleteKey: '+JSON.stringify(deleteKey));
  452. var ioPromise, returnPromise;
  453. options || (options={});
  454. options.url = url;
  455. // method will be uppercased by IO.xhr
  456. options.method = DELETE;
  457. options.data = deleteKey;
  458. delete options.responseType;
  459. ioPromise = this.request(options);
  460. returnPromise = ioPromise.then(
  461. function(xhrResponse) {
  462. var response = xhrResponse.responseText;
  463. try {
  464. response = JSON.parse(response, (options.parseJSONDate) ? REVIVER : null);
  465. }
  466. catch(err) {}
  467. return response;
  468. }
  469. );
  470. // set `abort` to the thennable-promise:
  471. returnPromise.abort = ioPromise.abort;
  472. return returnPromise;
  473. };
  474.  
  475. window._ITSAmodules.IO_Transfer = IO;
  476.  
  477. return IO;
  478. };