- "use strict";
-
- /**
- * Extends io by adding the methods `get`, `read`, `update`, `insert`, `send` and `delete` to it.
- *
- * @example
- * var IO = require("io/extra/io-transfer.js")(window);
- *
- * <i>Copyright (c) 2014 ITSA - https://github.com/itsa</i>
- * New BSD License - http://choosealicense.com/licenses/bsd-3-clause/
- *
- * @module io
- * @submodule io-transfer
- * @class IO
- * @since 0.0.1
- */
-
- require('js-ext/lib/string.js');
- require('js-ext/lib/object.js');
- require('polyfill/polyfill-base.js');
-
- /*jshint proto:true */
- var NAME = '[io-transfer]: ',
- createHashMap = require('js-ext/extra/hashmap.js').createMap,
- PROTO_SUPPORTED = !!Object.__proto__,
- REVIVER = function(key, value) {
- return ((typeof value==='string') && value.toDate()) || value;
- },
- REVIVER_PROTOTYPED = function(key, value, proto, parseProtoCheck, reviveDate) {
- if (reviveDate && (typeof value==='string')) {
- return value.toDate() || value;
- }
- if (!Object.isObject(value)) {
- return value;
- }
- // only first level of objects can be given the specified prototype
- if ((typeof parseProtoCheck === 'function') && !parseProtoCheck(value)) {
- return value;
- }
- if (PROTO_SUPPORTED) {
- value.__proto__ = proto;
- return value;
- }
- return value.deepClone(null, proto);
- },
- MIME_JSON = 'application/json',
- CONTENT_TYPE = 'Content-Type',
- DELETE = 'delete',
- REGEXP_ARRAY = /^( )*\[/,
- REGEXP_OBJECT = /^( )*{/,
- REGEXP_REMOVE_LAST_COMMA = /^(.*),( )*$/,
- SPINNER_ICON = 'spinnercircle-anim',
- MIN_SHOWUP = 500;
- /*jshint proto:false */
-
- module.exports = function (window) {
-
- window._ITSAmodules || Object.protectedProp(window, '_ITSAmodules', createHashMap());
-
- if (window._ITSAmodules.IO_Transfer) {
- return window._ITSAmodules.IO_Transfer; // IO_Transfer was already created
- }
-
- var IO = require('../io.js')(window),
-
- /*
- * Adds properties to the xhr-object: in case of streaming,
- * xhr._parseStream=function is created to parse streamed data.
- *
- * @method _progressHandle
- * @param xhr {Object} containing the xhr-instance
- * @param props {Object} the propertie-object that is added too xhr and can be expanded
- * @param options {Object} options of the request
- * @private
- */
- _entendXHR = function(xhr, props, options /*, promise */) {
- var isarray, isobject, parialdata, regexpcomma, followingstream;
- if ((typeof options.streamback === 'function') && options.headers && (options.headers.Accept==='application/json')) {
- console.log(NAME, 'entendXHR');
- xhr._parseStream = function(streamData) {
- console.log(NAME, 'entendXHR --> _parseStream');
- // first step is to determine if the final response would be an array or an object
- // partial responses should be expanded to the same type
- if (!followingstream) {
- isarray = REGEXP_ARRAY.test(streamData);
- isarray || (isobject = REGEXP_OBJECT.test(streamData));
- }
- try {
- if (isarray || isobject) {
- regexpcomma = streamData.match(REGEXP_REMOVE_LAST_COMMA);
- parialdata = regexpcomma ? streamData.match(REGEXP_REMOVE_LAST_COMMA)[1] : streamData;
- }
- else {
- parialdata = streamData;
- }
- parialdata = (followingstream && isarray ? '[' : '') + (followingstream && isobject ? '{' : '') + parialdata + (regexpcomma && isarray ? ']' : '') + (regexpcomma && isobject ? '}' : '');
- // note: parsing will fail for the last streamed part, because there will be a double ] or }
- streamData = JSON.parse(parialdata, (options.parseJSONDate) ? REVIVER : null);
- }
- catch(err) {
- console.warn(NAME, err);
- }
- followingstream = true;
- return streamData;
- };
- }
- return xhr;
- };
-
- IO._xhrList.push(_entendXHR);
-
- /**
- * Performs an AJAX GET request. Shortcut for a call to [`xhr`](#method_xhr) with `method` set to `'GET'`.
- * Additional parameters can be on the url (with questionmark), through `params`, or both.
- *
- * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
- * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
- *
- * Note: `params` should be a plain object with only primitive types which are transformed into key/value pairs.
- *
- * @method get
- * @param url {String} URL of the resource server
- * @param [params] {Object} additional parameters.
- * should be a plain object with only primitive types which are transformed into key/value pairs.
- * @param [options] {Object}
- * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
- * @param [options.headers] {Object} HTTP request headers.
- * @param [options.responseType] {String} Force the response type.
- * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
- * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
- * @param [options.preventCache=false] {boolean} whether to prevent caching --> a timestamp is added by parameter _ts
- * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
- * @return {Promise}
- * on success:
- * xhr {XMLHttpRequest|XDomainRequest} xhr-response
- * on failure an Error object
- * reason {Error}
- */
- IO.get = function (url, options) {
- console.log(NAME, 'get --> '+url);
- var ioPromise, returnPromise;
- options || (options={});
- options.url = url;
- options.method = 'GET';
- // delete hidden property `data`: don't want accedentially to be used
- delete options.data;
- if (options.preventCache) {
- url += (url.contains('?') ? '&' : '?') + '_ts=' + Date.now();
- }
- ioPromise = this.request(options);
- returnPromise = ioPromise.then(
- function(xhrResponse) {
- return xhrResponse.responseText;
- }
- );
- // set `abort` to the thennable-promise:
- returnPromise.abort = ioPromise.abort;
- return returnPromise;
- };
-
- /**
- * Performs an AJAX request with the GET HTTP method and expects a JSON-object.
- * The resolved Promise-callback returns an object (JSON-parsed serverresponse).
- *
- * Additional request-parameters can be on the url (with questionmark), through `params`, or both.
- *
- * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
- * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
- *
- * Note1: If you expect the server to response with data that consist of Date-properties, you should set `options.parseJSONDate` true.
- * Parsing takes a bit longer, but it will generate trully Date-objects.
- * Note2: CORS is supported, as long as the responseserver is set up to:
- * a) has a response header which allows the clientdomain:
- * header('Access-Control-Allow-Origin: http://www.some-site.com'); or header('Access-Control-Allow-Origin: *');
- * b) in cae you have set a custom HEADER (through 'options'), the responseserver MUST listen and respond
- * to requests with the OPTION-method
- * More info: allows to send to your domain: see http://remysharp.com/2011/04/21/getting-cors-working/
- *
- * @method read
- * @param url {String} URL of the resource server
- * @param [params] {Object} additional parameters.
- * @param [options] {Object} See also: [`I.io`](#method_xhr)
- * can be ignored, even if streams are used --> the returned Promise will always hold all data
- * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
- * @param [options.headers] {Object} HTTP request headers.
- * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
- * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
- * @param [options.parseJSONDate=false] {boolean} Whether the server returns JSON-stringified data which has Date-objects.
- * @param [options.parseProto] {Object} to set the prototype of any object.
- * @param [options.parseProtoCheck] {Function} to determine in what case the specified `parseProto` should be set as the prototype.
- * The function accepts the `object` as argument and should return a trully value in order to set the prototype.
- * When not specified, `parseProto` will always be applied (if `parseProto`is defined)
- * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
- * @return {Promise}
- * on success:
- * Object received data
- * on failure an Error object
- * reason {Error}
- */
- IO.read = function(url, params, options) {
- console.log(NAME, 'read --> '+url+' params: '+JSON.stringify(params));
- var ioPromise, returnPromise;
- options || (options={});
- options.headers || (options.headers={});
- options.url = url;
- options.method = 'GET';
- options.data = params;
- options.headers.Accept = 'application/json';
- // we don't want the user to re-specify the server's responsetype:
- delete options.responseType;
- ioPromise = this.request(options);
- returnPromise = ioPromise.then(
- function(xhrResponse) {
- // not 'try' 'catch', because, if parsing fails, we actually WANT the promise to be rejected
- // we also need to re-attach the 'abort-handle'
- console.log(NAME, 'read returns with: '+JSON.stringify(xhrResponse.responseText));
- // xhrResponse.responseText should be 'application/json' --> if it is not,
- // JSON.parse throws an error, but that's what we want: the Promise would reject
- if (options.parseProto) {
- return JSON.parse(xhrResponse.responseText, REVIVER_PROTOTYPED.rbind(null, options.parseProto, options.parseProtoCheck, options.parseJSONDate));
- }
- return JSON.parse(xhrResponse.responseText, (options.parseJSONDate) ? REVIVER : null);
- }
- );
- // set `abort` to the thennable-promise:
- returnPromise.abort = ioPromise.abort;
- return returnPromise;
- };
-
-
- /**
- * Sends data (object) which will be JSON-stringified before sending.
- * Performs an AJAX request with the PUT HTTP method by default.
- * When options.allfields is `false`, it will use the POST-method: see Note2.
- *
- * The 'content-type' of the header is set to 'application/json', overruling manually options.
- *
- * 'data' is send as 'body.data' and should be JSON-parsed at the server.
- *
- * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
- * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
- *
- * Note1: The server needs to inspect the bodyparam: 'action', which always equals 'update'.
- * 'body.action' is the way to distinquish 'I.IO.updateObject' from 'I.IO.insertObject'.
- * On purpose, we didn't make this distinction through a custom CONTENT-HEADER, because
- * that would lead into a more complicated CORS-setup (see Note3)
- * Note2: By default this method uses the PUT-request: which is preferable is you send the WHOLE object.
- * if you send part of the fields, set `options.allfields`=false.
- * This will lead into using the POST-method.
- * More about HTTP-methods: https://stormpath.com/blog/put-or-post/
- * Note3: CORS is supported, as long as the responseserver is set up to:
- * a) has a response header which allows the clientdomain:
- * header('Access-Control-Allow-Origin: http://www.some-site.com'); or header('Access-Control-Allow-Origin: *');
- * b) in cae you have set a custom HEADER (through 'options'), the responseserver MUST listen and respond
- * to requests with the OPTION-method
- * More info: allows to send to your domain: see http://remysharp.com/2011/04/21/getting-cors-working/
- * Note4: If the server response JSON-stringified data, the Promise resolves with a JS-Object. If you expect this object
- * to consist of Date-properties, you should set `options.parseJSONDate` true. Parsing takes a bit longer, but it will
- * generate trully Date-objects.
- *
- *
- * @method update
- * @param url {String} URL of the resource server
- * @param data {Object|Promise} Data to be sent, might be a Promise which resolves with the data-object.
- * @param [options] {Object} See also: [`I.io`](#method_xhr)
- * @param [options.allfields=true] {boolean} to specify that all the object-fields are sent.
- * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
- * @param [options.headers] {Object} HTTP request headers.
- * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
- * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
- * @param [options.parseJSONDate=false] {boolean} Whether the server returns JSON-stringified data which has Date-objects.
- * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
- * @return {Promise}
- * on success:
- * response {Object} usually, the final object-data, possibly modified
- * on failure an Error object
- * reason {Error}
- */
-
- /**
- * Performs an AJAX request with the POST HTTP method by default.
- * When options.allfields is `true`, it will use the PUT-method: see Note2.
- * The send data is an object which will be JSON-stringified before sending.
- *
- * The 'content-type' of the header is set to 'application/json', overruling manually options.
- *
- * 'data' is send as 'body.data' and should be JSON-parsed at the server.
- * 'body.action' has the value 'insert'
- *
- * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
- * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
- *
- * Note1: The server needs to inspect the bodyparam: 'action', which always equals 'insert'.
- * 'body.action' is the way to distinquish 'I.IO.insertObject' from 'I.IO.updateObject'.
- * On purpose, we didn't make this distinction through a custom CONTENT-HEADER, because
- * that would lead into a more complicated CORS-setup (see Note3)
- * Note2: By default this method uses the POST-request: which is preferable if you don't know all the fields (like its unique id).
- * if you send ALL the fields, set `options.allfields`=true.
- * This will lead into using the PUT-method.
- * More about HTTP-methods: https://stormpath.com/blog/put-or-post/
- * Note3: CORS is supported, as long as the responseserver is set up to:
- * a) has a response header which allows the clientdomain:
- * header('Access-Control-Allow-Origin: http://www.some-site.com'); or header('Access-Control-Allow-Origin: *');
- * b) in cae you have set a custom HEADER (through 'options'), the responseserver MUST listen and respond
- * to requests with the OPTION-method
- * More info: allows to send to your domain: see http://remysharp.com/2011/04/21/getting-cors-working/
- * Note4: If the server response JSON-stringified data, the Promise resolves with a JS-Object. If you expect this object
- * to consist of Date-properties, you should set `options.parseJSONDate` true. Parsing takes a bit longer, but it will
- * generate trully Date-objects.
- *
- * @method insert
- * @param url {String} URL of the resource server
- * @param data {Object|Promise} Data to be sent, might be a Promise which resolves with the data-object.
- * @param [options] {Object} See also: [`I.io`](#method_xhr)
- * @param [options.allfields=false] {boolean} to specify that all the object-fields are sent.
- * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
- * @param [options.headers] {Object} HTTP request headers.
- * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
- * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
- * @param [options.parseJSONDate=false] {boolean} Whether the server returns JSON-stringified data which has Date-objects.
- * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
- * @return {Promise}
- * on success:
- * response {Object} usually, the final object-data, possibly modified, holding the key
- * on failure an Error object
- * reason {Error}
- */
-
- /**
- * Performs an AJAX request with the PUT HTTP method by default.
- * When options.allfields is `false`, it will use the POST-method: see Note2.
- * The send data is an object which will be JSON-stringified before sending.
- *
- * The 'content-type' of the header is set to 'application/json', overruling manually options.
- *
- * 'data' is send as 'body.data' and should be JSON-parsed at the server.
- *
- * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
- * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
- *
- * Note1: By default this method uses the PUT-request: which is preferable is you send the WHOLE object.
- * if you send part of the fields, set `options.allfields`=false.
- * This will lead into using the POST-method.
- * More about HTTP-methods: https://stormpath.com/blog/put-or-post/
- * Note2: CORS is supported, as long as the responseserver is set up to:
- * a) has a response header which allows the clientdomain:
- * header('Access-Control-Allow-Origin: http://www.some-site.com'); or header('Access-Control-Allow-Origin: *');
- * b) in cae you have set a custom HEADER (through 'options'), the responseserver MUST listen and respond
- * to requests with the OPTION-method
- * More info: allows to send to your domain: see http://remysharp.com/2011/04/21/getting-cors-working/
- * Note3: If the server response JSON-stringified data, the Promise resolves with a JS-Object. If you expect this object
- * to consist of Date-properties, you should set `options.parseJSONDate` true. Parsing takes a bit longer, but it will
- * generate trully Date-objects.
- *
- * @method send
- * @param url {String} URL of the resource server
- * @param data {Object} Data to be sent.
- * @param [options] {Object} See also: [`I.io`](#method_xhr)
- * @param [options.allfields=true] {boolean} to specify that all the object-fields are sent.
- * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
- * @param [options.headers] {Object} HTTP request headers.
- * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
- * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
- * @param [options.parseJSONDate=false] {boolean} Whether the server returns JSON-stringified data which has Date-objects.
- * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
- * @return {Promise}
- * on success:
- * response {Object|String} any response you want the server to return.
- If the server send back a JSON-stringified object,
- it will be parsed to return as a full object
- You could set `options.parseJSONDate` true, it you want ISO8601-dates to be parsed as trully Date-objects
- * on failure an Error object
- * reason {Error}
- */
-
- ['update', 'insert', 'send'].forEach(
- function (verb) {
- IO[verb] = function (url, data, options) {
- console.log(NAME, verb+' --> '+url+' data: '+JSON.stringify(data));
- var instance = this,
- allfields, useallfields, parseJSONDate, ioPromise, returnPromise;
- options || (options={});
- allfields = options.allfields,
- useallfields = (typeof allfields==='boolean') ? allfields : (verb!=='insert');
- parseJSONDate = options.parseJSONDate;
- options.url = url;
- options.method = useallfields ? 'PUT' : 'POST';
- options.data = data;
- options.headers || (options.headers={});
- options.headers[CONTENT_TYPE] = MIME_JSON;
- parseJSONDate && (options.headers['X-JSONDate']="true");
- if (verb!=='send') {
- options.headers.Accept = 'application/json';
- // set options.action
- options.headers['X-Action'] = verb;
- // we don't want the user to re-specify the server's responsetype:
- delete options.responseType;
- }
- ioPromise = instance.request(options);
- returnPromise = ioPromise.then(
- function(xhrResponse) {
- if (verb==='send') {
- return xhrResponse.responseText;
- }
- // In case of `insert` or `update`
- // xhrResponse.responseText should be 'application/json' --> if it is not,
- // JSON.parse throws an error, but that's what we want: the Promise would reject
- return JSON.parse(xhrResponse.responseText, parseJSONDate ? REVIVER : null);
- }
- );
- // set `abort` to the thennable-promise:
- returnPromise.abort = ioPromise.abort;
- return returnPromise;
- };
- }
- );
-
- /**
- * Performs an AJAX DELETE request. Shortcut for a call to [`xhr`](#method_xhr) with `method` set to `'DELETE'`.
- *
- * The Promise gets fulfilled if the server responses with `STATUS-CODE` in the 200-range (excluded 204).
- * It will be rejected if a timeout occurs (see `options.timeout`), or if `xhr.abort()` gets invoked.
- *
- * Note: `data` should be a plain object with only primitive types which are transformed into key/value pairs.
- *
- * @method delete
- * @param url {String} URL of the resource server
- * @param deleteKey {Object} Indentification of the id that has to be deleted. Typically an object like: {id: 12}
- * This object will be passed as the request params.
- * @param [options] {Object}
- * @param [options.url] {String} The url to which the request is sent.
- * @param [options.sync=false] {boolean} By default, all requests are sent asynchronously. To send synchronous requests, set to true.
- * @param [options.params] {Object} Data to be sent to the server.
- * @param [options.body] {Object} The content for the request body for POST method.
- * @param [options.headers] {Object} HTTP request headers.
- * @param [options.timeout=3000] {Number} to timeout the request, leading into a rejected Promise.
- * @param [options.withCredentials=false] {boolean} Whether or not to send credentials on the request.
- * @param [options.parseJSONDate=false] {boolean} Whether the server returns JSON-stringified data which has Date-objects.
- * @param [options.stayActive] {Number} minimal time the request should be pending, even if IO has finished
- * @return {Promise}
- * on success:
- * response {Object|String} any response you want the server to return.
- If the server send back a JSON-stringified object,
- it will be parsed to return as a full object
- You could set `options.parseJSONDate` true, it you want ISO8601-dates to be parsed as trully Date-objects
- * on failure an Error object
- * reason {Error}
- */
-
- IO[DELETE] = function (url, deleteKey, options) {
- console.log(NAME, 'delete --> '+url+' deleteKey: '+JSON.stringify(deleteKey));
- var ioPromise, returnPromise;
- options || (options={});
- options.url = url;
- // method will be uppercased by IO.xhr
- options.method = DELETE;
- options.data = deleteKey;
- delete options.responseType;
- ioPromise = this.request(options);
- returnPromise = ioPromise.then(
- function(xhrResponse) {
- var response = xhrResponse.responseText;
- try {
- response = JSON.parse(response, (options.parseJSONDate) ? REVIVER : null);
- }
- catch(err) {}
- return response;
- }
- );
- // set `abort` to the thennable-promise:
- returnPromise.abort = ioPromise.abort;
- return returnPromise;
- };
-
- window._ITSAmodules.IO_Transfer = IO;
-
- return IO;
- };
-