/**
* Provides the classes needed to communicate with the CHECKROOM API
* @module api
* @namespace api
* @copyright CHECKROOM NV 2015
*/
define([
'jquery',
'moment'], function ($, moment) {
var MAX_QUERYSTRING_LENGTH = 2048;
//TODO change this
//system.log fallback
var system = {
log: function(){
// do something
}
};
// Disable caching AJAX requests in IE
// http://stackoverflow.com/questions/5502002/jquery-ajax-producing-304-responses-when-it-shouldnt
$.ajaxSetup({cache: false});
var api = {};
//*************
// ApiErrors
//*************
// Network
api.NetworkNotConnected = function (msg, opt) { this.code = 999; this.message = msg || "Connection interrupted"; this.opt = opt; };
api.NetworkNotConnected.prototype = new Error();
api.NetworkTimeout = function (msg, opt) { this.code = 408; this.message = msg || "Could not reach the server in time"; this.opt = opt; };
api.NetworkTimeout.prototype = new Error();
// Api
api.ApiError = function (msg, opt) { this.code = 500; this.message = msg || "Something went wrong on the server"; this.opt = opt; };
api.ApiError.prototype = new Error();
api.ApiNotFound = function (msg, opt) { this.code = 404; this.message = msg || "Could not find what you're looking for"; this.opt = opt; };
api.ApiNotFound.prototype = new Error();
api.ApiBadRequest = function (msg, opt) { this.code = 400; this.message = msg || "The server did not understand your request"; this.opt = opt; };
api.ApiBadRequest.prototype = new Error();
api.ApiUnauthorized = function (msg, opt) { this.code = 401; this.message = msg || "Your session has expired"; this.opt = opt; };
api.ApiUnauthorized.prototype = new Error();
api.ApiForbidden = function (msg, opt) { this.code = 403; this.message = msg || "You don't have sufficient rights"; this.opt = opt; };
api.ApiForbidden.prototype = new Error();
api.ApiUnprocessableEntity = function (msg, opt) { this.code = 422; this.message = msg || "Some data is invalid"; this.opt = opt; };
api.ApiUnprocessableEntity.prototype = new Error();
api.ApiSubscriptionLimit = function (msg, opt) { this.code = 422; this.message = msg || "You have reached your subscription limit"; this.opt = opt; };
api.ApiSubscriptionLimit.prototype = new Error();
api.ApiPaymentRequired = function (msg, opt) { this.code = 402; this.message = msg || "Your subscription has expired"; this.opt = opt; };
api.ApiPaymentRequired.prototype = new Error();
api.ApiServerCapicity = function(msg, opt){ this.code = 503; this.message = msg || "Back-end server is at capacity"; this.opt = opt; };
api.ApiServerCapicity.prototype = new Error();
//*************
// ApiAjax
//*************
/**
* The ajax communication object which makes the request to the API
* @name ApiAjax
* @param {object} spec
* @constructor
* @memberof api
*/
api.ApiAjax = function(spec) {
spec = spec || {};
this.timeOut = spec.timeOut || 10000;
this.responseInTz = true;
};
api.ApiAjax.prototype.get = function(url, timeOut) {
system.log('ApiAjax: get '+url);
return this._getAjax(url, timeOut);
};
api.ApiAjax.prototype.post = function(url, data, timeOut) {
system.log('ApiAjax: post '+url);
return this._postAjax(url, data, timeOut);
};
// Implementation
// ----
api.ApiAjax.prototype._handleAjaxSuccess = function(dfd, data, opt) {
if (this.responseInTz) {
data = this._fixDates(data);
}
return dfd.resolve(data);
};
api.ApiAjax.prototype._handleAjaxError = function(dfd, x, t, m, opt) {
// ajax call was aborted
if(t == "abort") return;
var msg = "";
if (m==="timeout") {
dfd.reject(new api.NetworkTimeout(msg, opt));
} else {
if (x){
if((x.statusText) &&
(x.statusText.indexOf("Notify user:") > -1)) {
msg = x.statusText.slice(x.statusText.indexOf("Notify user:") + 13);
}
if( (x.status == 422) &&
(x.responseText) &&
(x.responseText.match(/HTTPError: \(.+\)/g).length > 0)){
opt = {
detail: x.responseText.match(/HTTPError: \(.+\)/g)[0]
}
}
}
switch(x.status) {
case 400: dfd.reject(new api.ApiBadRequest(msg, opt)); break;
case 401: dfd.reject(new api.ApiUnauthorized(msg, opt)); break;
case 402: dfd.reject(new api.ApiPaymentRequired(msg, opt)); break;
case 403: dfd.reject(new api.ApiForbidden(msg, opt)); break;
case 404: dfd.reject(new api.ApiNotFound(msg, opt)); break;
case 408: dfd.reject(new api.NetworkTimeout(msg, opt)); break;
case 422:
// 422 Notify user: Cannot create item, max limit 50 items reached
if( (msg) &&
(msg.indexOf('limit') >= 0) &&
(msg.indexOf('reach') >= 0)) {
dfd.reject(new api.ApiSubscriptionLimit(msg, opt));
} else {
dfd.reject(new api.ApiUnprocessableEntity(msg, opt));
}
break;
case 503: dfd.reject(new api.ApiServerCapicity(msg, opt)); break;
case 500:
default: dfd.reject(new api.ApiError(msg, opt)); break;
}
}
};
api.ApiAjax.prototype._postAjax = function(url, data, timeOut, opt) {
var dfd = $.Deferred();
var that = this;
var xhr = $.ajax({
type: "POST",
url: url,
data: JSON.stringify(this._prepareDict(data)),
contentType: "application/json; charset=utf-8",
timeout: timeOut || this.timeOut,
success: function(data) {return that._handleAjaxSuccess(dfd, data, opt);},
error: function(x, t, m) {return that._handleAjaxError(dfd, x, t, m, opt);}
});
// Extend promise with abort method
// to abort xhr request if needed
// http://stackoverflow.com/questions/21766428/chained-jquery-promises-with-abort
var promise = dfd.promise();
promise.abort = function(){
xhr.abort();
};
return promise;
};
api.ApiAjax.prototype._getAjax = function(url, timeOut, opt) {
var dfd = $.Deferred();
var that = this;
var xhr = $.ajax({
url: url,
timeout: timeOut || this.timeOut,
success: function(data) {return that._handleAjaxSuccess(dfd, data, opt);},
error: function(x, t, m) {return that._handleAjaxError(dfd, x, t, m, opt);}
});
// Extend promise with abort method
// to abort xhr request if needed
// http://stackoverflow.com/questions/21766428/chained-jquery-promises-with-abort
var promise = dfd.promise();
promise.abort = function(){
xhr.abort();
};
return promise;
};
api.ApiAjax.prototype._prepareDict = function(data) {
// Makes sure all values from the dict are serializable and understandable for json
if (!data) {
return {};
}
$.each(data, function(key, value) {
if(moment.isMoment(value)) {
data[key] = value.toJSONDate();
}
});
return data;
};
/**
* Turns all strings that look like datetimes into moment objects recursively
*
* @name DateHelper#fixDates
* @method
* @private
*
* @param data
* @returns {*}
*/
api.ApiAjax.prototype._fixDates = function(data){
if (typeof data == 'string' || data instanceof String) {
// "2014-04-03T12:15:00+00:00" (length 25)
// "2014-04-03T09:32:43.841000+00:00" (length 32)
if (data.endsWith('+00:00')) {
var len = data.length;
if (len==25) {
return moment(data.substring(0, len-6));
} else if (len==32) {
return moment(data.substring(0, len-6).split('.')[0]);
}
}
} else if (
(data instanceof Object) ||
($.isArray(data))) {
var that = this;
$.each(data, function(k, v) {
data[k] = that._fixDates(v);
});
}
return data;
};
//*************
// ApiUser
//*************
/**
* @name ApiUser
* @param {object} spec
* @param {string} spec.userId - the users primary key
* @param {string} spec.userToken - the users token
* @param {string} spec.tokenType - the token type (empty for now)
* @constructor
* @memberof api
*/
api.ApiUser = function(spec) {
spec = spec || {};
this.userId = spec.userId || '';
this.userToken = spec.userToken || '';
this.tokenType = spec.tokenType || '';
};
api.ApiUser.prototype.fromStorage = function() {
this.userId = window.localStorage.getItem("userId") || '';
this.userToken = window.localStorage.getItem("userToken") || '';
this.tokenType = window.localStorage.getItem("tokenType") || '';
};
api.ApiUser.prototype.toStorage = function() {
window.localStorage.setItem("userId", this.userId);
window.localStorage.setItem("userToken", this.userToken);
window.localStorage.setItem("tokenType", this.tokenType);
};
api.ApiUser.prototype.removeFromStorage = function() {
window.localStorage.removeItem("userId");
window.localStorage.removeItem("userToken");
window.localStorage.removeItem("tokenType");
};
api.ApiUser.prototype.clearToken = function() {
window.localStorage.setItem("userToken", null);
window.localStorage.setItem("tokenType", null);
};
api.ApiUser.prototype.isValid = function() {
system.log('ApiUser: isValid');
return (this.userId != null && this.userId.length>0) && (this.userToken != null && this.userToken.length>0);
};
api.ApiUser.prototype._reset = function() {
this.userId = '';
this.userToken = '';
this.tokenType = '';
};
//*************
// ApiAuth
//*************
api.ApiAuth = function(spec) {
spec = spec || {};
this.urlAuth = spec.urlAuth || '';
this.ajax = spec.ajax;
this.version = spec.version;
this.platform = spec.platform;
this.device = spec.device;
this.allowAccountOwner = spec.allowAccountOwner !== undefined ? spec.allowAccountOwner:true;
};
api.ApiAuth.prototype.authenticate = function(userId, password) {
system.log('ApiAuth: authenticate '+userId);
var that = this;
var params = {
user: userId,
password: password,
_v: this.version
};
if(this.platform){
params.platform = this.platform;
}
if(this.device){
params.device = this.device;
}
var dfd = $.Deferred();
this.ajax.post(this.urlAuth, params, 30000)
.done(function(resp) {
// Check if login is ok AND if login is ok but account is expired, check if we allow login or not (allowAccountOwner)
//
// REMARK
// - web app allows owners to still login on expired/cancelled account
// - mobile doesn't allow expired logins also not for owners
if ((resp.status=="OK") &&
(['expired', 'cancelled_expired', 'archived'].indexOf(resp.subscription) != -1?that.allowAccountOwner:true)) {
dfd.resolve(resp.data);
} else {
dfd.reject(resp);
}
}).fail(function(err) {
dfd.reject(err);
});
return dfd.promise();
};
// Deprecated ApiAuthV2, use ApiAuth
api.ApiAuthV2 = api.ApiAuth;
//*************
// ApiAnonymous
// Communicates with the API without having token authentication
//*************
/**
* @name ApiAnonymous
* @param {object} spec
* @param {ApiAjax} spec.ajax
* @param {string} spec.urlApi
* @constructor
* @memberof api
*/
api.ApiAnonymous = function(spec) {
spec = spec || {};
this.ajax = spec.ajax;
this.urlApi = spec.urlApi || '';
this.version = spec.version;
};
/**
* Makes a call to the API which doesn't require a token
* @method
* @name ApiAnonymous#call
* @param method
* @param params
* @param timeOut
* @param opt
* @returns {*}
*/
api.ApiAnonymous.prototype.call = function(method, params, timeOut, opt) {
system.log('ApiAnonymous: call ' + method);
if (this.version) {
params = params || {};
params["_v"] = this.version;
}
var url =
this.urlApi +
'/' +
method +
'?' +
$.param(this.ajax._prepareDict(params));
return this.ajax.get(url, timeOut, opt);
};
/**
* Makes a long call (timeout 60s) to the API which doesn't require a token
* @method
* @name ApiAnonymous#longCall
* @param method
* @param params
* @param opt
* @returns {*}
*/
api.ApiAnonymous.prototype.longCall = function(method, params, opt) {
system.log('ApiAnonymous: longCall ' + method);
return this.call(method, params, 60000, opt);
};
//*************
// ApiDataSource
// Communicates with the API using an ApiUser
//*************
/**
* @name ApiDataSource
* @param {object} spec
* @param {string} spec.collection - the collection this datasource uses, e.g. "items"
* @param {string} spec.urlApi - the api url to use
* @param {ApiUser} spec.user - the user auth object
* @param {ApiAjax} spec.ajax - the ajax api object to use
* @constructor
* @memberof api
*/
api.ApiDataSource = function(spec) {
spec = spec || {};
this.collection = spec.collection || '';
this.urlApi = spec.urlApi || '';
this.user = spec.user;
this.ajax = spec.ajax;
this.version = spec.version;
};
/**
* Checks if a certain document exists
* @method
* @name ApiDataSource#exists
* @param pk
* @param fields
* @returns {*}
*/
api.ApiDataSource.prototype.exists = function(pk, fields) {
system.log('ApiDataSource: ' + this.collection + ': exists ' + pk);
var cmd = "exists";
var dfd = $.Deferred();
var that = this;
// We're actually doing a API get
// and resolve to an object,
// so we also pass the fields
var url = this.getBaseUrl() + pk;
var p = this.getParams(fields);
if (!$.isEmptyObject(p)) {
url += '?' + this.getParams(p);
}
this._ajaxGet(cmd, url)
.done(function(data) {
dfd.resolve(data);
}).fail(function(error) {
if (error instanceof api.ApiNotFound) {
dfd.resolve(null);
} else {
dfd.reject(error);
}
});
return dfd.promise();
};
/**
* Gets a certain document by its primary key
* @method
* @name ApiDataSource#get
* @param pk
* @param fields
* @returns {promise}
*/
api.ApiDataSource.prototype.get = function(pk, fields) {
system.log('ApiDataSource: ' + this.collection + ': get ' + pk);
var cmd = "get";
var url = this.getBaseUrl() + pk;
var p = this.getParamsDict(fields);
if (!$.isEmptyObject(p)) {
url += '?' + this.getParams(p);
}
return this._ajaxGet(cmd, url);
};
/**
* Gets a certain document by its primary key, but returns null if not found
* instead of a rejected promise
* @method
* @name ApiDataSource#getIgnore404
* @param pk
* @param fields
* @returns {promise}
*/
api.ApiDataSource.prototype.getIgnore404 = function(pk, fields) {
system.log('ApiDataSource: ' + this.collection + ': getIgnore404 ' + pk);
var that = this;
var dfd = $.Deferred();
this.get(pk, fields)
.done(function(data) {
dfd.resolve(data);
})
.fail(function(err) {
if (err instanceof api.ApiNotFound) {
dfd.resolve(null);
} else {
dfd.reject(err);
}
});
return dfd.promise();
};
/**
* Get multiple document by primary keys in a single query
* @method
* @name ApiDataSource#getMultiple
* @param {array} pks
* @param fields
* @returns {promise}
*/
api.ApiDataSource.prototype.getMultiple = function(pks, fields) {
system.log('ApiDataSource: ' + this.collection + ': getMultiple ' + pks);
var cmd = "getMultiple";
var url = this.getBaseUrl() + pks.join(',') + ',';
var p = this.getParamsDict(fields);
if (!$.isEmptyObject(p)) {
url += '?' + this.getParams(p);
}
return this._ajaxGet(cmd, url);
};
/**
* Deletes a document by its primary key
* @method
* @name ApiDataSource#delete
* @param pk
* @returns {promise}
*/
api.ApiDataSource.prototype.delete = function(pk) {
system.log('ApiDataSource: ' + this.collection + ': delete ' + pk);
var cmd = "delete";
var url = this.getBaseUrl() + pk + '/delete';
return this._ajaxGet(cmd, url);
};
/**
* Deletes documents by their primary key
* @method
* @name ApiDataSource#deleteMultiple
* @param pks
* @returns {promise}
*/
api.ApiDataSource.prototype.deleteMultiple = function(pks, usePost) {
system.log('ApiDataSource: ' + this.collection + ': deleteMultiple ' + pks);
var cmd = "deleteMultiple";
var url = this.getBaseUrl() + 'delete';
var p = { pk: pks };
var geturl = url + '?' + this.getParams(p);
if( (usePost) ||
(geturl.length >= MAX_QUERYSTRING_LENGTH)) {
return this._ajaxPost(cmd, url, p);
} else {
return this._ajaxGet(cmd, geturl);
}
};
/**
* Updates a document by its primary key and a params objects
* @method
* @name ApiDataSource#update
* @param pk
* @param params
* @param fields
* @param timeOut
* @param usePost
* @returns {promise}
*/
api.ApiDataSource.prototype.update = function(pk, params, fields, timeOut, usePost) {
system.log('ApiDataSource: ' + this.collection + ': update ' + pk);
var cmd = "update";
var url = this.getBaseUrl() + pk + '/update';
var p = $.extend({}, params);
if( (fields!=null) &&
(fields.length>0)) {
p['_fields'] = $.isArray(fields) ? fields.join(',') : fields;
}
var geturl = url + '?' + this.getParams(p);
if( (usePost) ||
(geturl.length >= MAX_QUERYSTRING_LENGTH)) {
return this._ajaxPost(cmd, url, p, timeOut);
} else {
return this._ajaxGet(cmd, geturl, timeOut);
}
};
/**
* Creates a document with some data in an object
* @method
* @name ApiDataSource#create
* @param params
* @param fields
* @param timeOut
* @param usePost
* @returns {promise}
*/
api.ApiDataSource.prototype.create = function(params, fields, timeOut, usePost) {
system.log('ApiDataSource: ' + this.collection + ': create');
var cmd = "create";
var url = this.getBaseUrl() + 'create';
var p = $.extend({}, params);
if( (fields!=null) &&
(fields.length>0)) {
p['_fields'] = $.isArray(fields) ? fields.join(',') : fields;
}
var geturl = url + '?' + this.getParams(p);
if( (usePost) ||
(geturl.length >= MAX_QUERYSTRING_LENGTH)) {
return this._ajaxPost(cmd, url, p, timeOut);
} else {
return this._ajaxGet(cmd, geturl, timeOut);
}
};
/**
* Creates multiple objects in one go
* @method
* @name ApiDataSource#createMultiple
* @param objects
* @param fields
* @returns {promise}
*/
api.ApiDataSource.prototype.createMultiple = function(objects, fields) {
system.log('ApiDataSource: ' + this.collection + ': createMultiple (' + objects.length + ')');
var dfd = $.Deferred();
var that = this;
var todoObjs = objects.slice(0);
var doneIds = [];
// Trigger the creates sequentially
var createRecurse = function(todoObjs) {
if (todoObjs.length>0) {
var obj = todoObjs.pop();
that.create(obj, fields)
.done(function(resp) {
doneIds.push(resp._id);
return createRecurse(todoObjs);
}).fail(function(error) {
dfd.reject(error);
});
} else {
dfd.resolve(doneIds);
}
};
createRecurse(todoObjs);
return dfd.promise();
};
/**
* Get a list of objects from the collection
* @method
* @name ApiDataSource#list
* @param name
* @param fields
* @param limit
* @param skip
* @param sort
* @returns {promise}
*/
api.ApiDataSource.prototype.list = function(name, fields, limit, skip, sort) {
name = name || "";
system.log('ApiDataSource: ' + this.collection + ': list ' + name);
var cmd = "list." + name;
var url = this.getBaseUrl();
if( (name!=null) && (name.length>0)) {
url += 'list/' + name + '/';
}
var p = this.getParamsDict(fields, limit, skip, sort);
if (!$.isEmptyObject(p)) {
url += '?' + this.getParams(p);
}
return this._ajaxGet(cmd, url);
};
/**
* Searches for objects in the collection
* @method
* @name ApiDataSource#search
* @param params
* @param fields
* @param limit
* @param skip
* @param sort
* @param mimeType
* @returns {promise}
*/
api.ApiDataSource.prototype.search = function(params, fields, limit, skip, sort, mimeType) {
system.log('ApiDataSource: ' + this.collection + ': search ' + params);
var cmd = "search";
var url = this.searchUrl(params, fields, limit, skip, sort, mimeType);
return this._ajaxGet(cmd, url);
};
api.ApiDataSource.prototype.searchUrl = function(params, fields, limit, skip, sort, mimeType) {
var url = this.getBaseUrl() + 'search';
var p = $.extend(this.getParamsDict(fields, limit, skip, sort), params);
if( (mimeType!=null) &&
(mimeType.length>0)) {
p['mimeType'] = mimeType;
}
url += '?' + this.getParams(p);
return url;
};
/**
* Calls a certain method on an object or on the entire collection
* @method
* @name ApiDataSource#call
* @param pk
* @param method
* @param params
* @param fields
* @param timeOut
* @param usePost
* @returns {promise}
*/
api.ApiDataSource.prototype.call = function(pk, method, params, fields, timeOut, usePost) {
system.log('ApiDataSource: ' + this.collection + ': call ' + method);
var cmd = "call." + method;
var url = ((pk!=null) && (pk.length>0)) ?
this.getBaseUrl() + pk + '/call/' + method :
this.getBaseUrl() + 'call/' + method;
var p = $.extend({}, this.getParamsDict(fields, null, null, null), params);
var getUrl = url + '?' + this.getParams(p);
if( (usePost) ||
(getUrl.length >= MAX_QUERYSTRING_LENGTH)) {
return this._ajaxPost(cmd, url, p, timeOut);
} else {
return this._ajaxGet(cmd, getUrl, timeOut);
}
};
/**
* Calls a certain method on one or more objects in a collection
* @method
* @name ApiDataSource#callMultiple
* @param pks
* @param method
* @param params
* @param fields
* @param timeOut
* @param usePost
* @returns {promise}
*/
api.ApiDataSource.prototype.callMultiple = function(pks, method, params, fields, timeOut, usePost) {
system.log('ApiDataSource: ' + this.collection + ': call ' + method);
var cmd = "call." + method;
var url = this.getBaseUrl() + pks.join(',') + '/call/' + method;
var p = $.extend({}, this.getParamsDict(fields, null, null, null), params);
var getUrl = url + '?' + this.getParams(p);
if (usePost || getUrl.length >= MAX_QUERYSTRING_LENGTH) {
return this._ajaxPost(cmd, url, p, timeOut);
} else {
return this._ajaxGet(cmd, getUrl, timeOut);
}
};
/**
* Makes a long call (timeout 60s) to a certain method on an object or on the entire collection
* @method
* @name ApiDataSource#longCall
* @param pk
* @param method
* @param params
* @param fields
* @param usePost
* @returns {promise}
*/
api.ApiDataSource.prototype.longCall = function(pk, method, params, fields, usePost) {
return this.call(pk, method, params, fields, 60000, usePost);
};
/**
* Gets the base url for all calls to this collection
* @method
* @name ApiDataSource#getBaseUrl
* @returns {string}
*/
api.ApiDataSource.prototype.getBaseUrl = function() {
var tokenType = ((this.user.tokenType != null) && (this.user.tokenType.length>0)) ? this.user.tokenType : 'null';
//Don't use cached version of this because when user session gets expired
//a new token is generated
return this.urlApi + '/' +
this.user.userId + '/' +
this.user.userToken + '/' +
tokenType + '/' +
this.collection + '/';
};
/**
* Prepare some parameters so we can use them during a request
* @method
* @name ApiDataSource#getParams
* @param data
* @returns {object}
*/
api.ApiDataSource.prototype.getParams = function(data) {
return $.param(this.ajax._prepareDict(data));
};
/**
* Gets a dictionary of parameters
* @method
* @name ApiDataSource#getParamsDict
* @param fields
* @param limit
* @param skip
* @param sort
* @returns {{}}
*/
api.ApiDataSource.prototype.getParamsDict = function(fields, limit, skip, sort) {
var p = {};
if (fields) { p['_fields'] = $.isArray(fields) ? fields.join(',') : fields.replace(/\s/g, ""); }
if (limit) { p['_limit'] = limit; }
if (skip) { p['_skip'] = skip; }
if (sort) { p['_sort'] = sort; }
if (this.version) { p['_v'] = this.version; }
return p;
};
/**
* Does an ajax GET call using the api.ApiAjax object
* @param cmd
* @param url
* @param timeout
* @returns {promise}
* @private
*/
api.ApiDataSource.prototype._ajaxGet = function(cmd, url, timeout) {
return this.ajax.get(url, timeout, {coll: this.collection, cmd: cmd});
};
/**
* Does an ajax POST call using the api.ApiAjax object
* @param cmd
* @param url
* @param data
* @param timeout
* @returns {promise}
* @private
*/
api.ApiDataSource.prototype._ajaxPost = function(cmd, url, data, timeout) {
return this.ajax.post(url, data, timeout, {coll: this.collection, cmd: cmd});
};
return api;
});