generator-addin
Version:
MyGeotab/Geotab Drive add-in generator
527 lines (516 loc) • 23.8 kB
JavaScript
/**
* @version 2014-07-21
* @description The Geotab JS API library.
* Used to login and make any API calls to a Geotab server.
*
* Automatically prompts the user to login if the password is changed or if the
* database is moved to another server.
*
* If credentials are required (for example, the first time you've visited the page, the
* database has moved or the user's password has changed, etc.), the 'getCredentialsCallback' is executed
* with a callback function that can be used on your end to pass credentials into the system. When
* you call this function, any pending calls will also be completed automatically.
* @example
* var api = GeotabApi(function(authenticateCallback) {
* // We need credentials; we either never had them, or lost them (the server was moved, password
* // changed, etc.)
*
* someLoginDialog.style.display = 'block'; // Show a login dialog
* someLoginButton.addEventListener('click', function () {
* // Call to authenticate
* authenticateCallback(serverField.value, databaseField.value, emailField.value, passwordField.value, function(errorString) {
* alert(errorString);
* });
* });
* })
* @function
* @param {Function} getCredentialsCallback This function is called when this class can't login or is
* fetching credentials for the first time
* @param {Object} [newOptions] Can be used to override default values in the 'options' variable
* @param {Object} [customCredentialStore] Override the default localStorage-based credential storage
with a custom storage implementation
*/
var GeotabApi = function (getCredentialsCallback, newOptions, customCredentialStore) {
'use strict';
var JSONP_REQUESTS_PROPERTY_STR = 'geotabJSONP',
credentials,
server,
pendingCalls = [],
options = {
// Use localStorage to store credentials automatically
rememberMe: true,
// Show debugging information (in Chrome dev tools, Firebug, etc.)
debug: false,
// Use JSONP for all calls (for using the API without running a server)
jsonp: false,
// How long to wait for a response from the server (in seconds); 0 (or null) means no timeout.
timeout: 0
},
credentialsStore,
/**
* Logs some debug information to the browser console, if options.debug is true
* @private
*/
debugLog = function () {
if (options.debug) {
var logs = [new Date()];
logs.push.apply(logs, arguments);
console.log.apply(console, logs);
}
},
/**
* Normalizes and handles errors
* @private
* @param {Object} [error] The error object
* @callback {failureCallback} [errorCallback] The function to call once the error has been normalize.
* It passes back a string for a known error, and the raw error
* object if some custom handling is required.
*/
handleError = function (error, errorCallback) {
var errorString;
if (error.errors && error.errors.length > 0) {
error = error.errors[0];
}
if (error && error.name && error.message) {
errorString = error.name + ': ' + error.message;
} else if (error.target || (error instanceof XMLHttpRequest && error.status === 0)) {
errorString = 'Network Error: Couldn\'t connect to the server. Please check your network connection and try again.';
}
if (options.debug) {
console.error(errorString, error);
}
if (errorCallback) {
errorCallback(errorString || 'Error', error);
}
},
/**
* Gets the method call URL
* @private
* @param {String} [method] The method to call on the server
* @return {String} The method call URL string
*/
getCallUrl = function (method) {
var thisServer = server.replace(/\S*:\/\//, '').replace(/\/$/, '');
return location.protocol + '//' + thisServer + '/apiv1' + (method ? '/' + method : '');
},
/**
* Cleans up the call with the given unique ID
* @private
* @param {String} uid The unique ID of the response callback
*/
cleanupCall = function (uid) {
// Remove this function once we're done with it
var script = document.getElementById(uid);
if (script) {
script.parentNode.removeChild(script);
// Manually garbage-collect the script
for (var prop in script) {
if (script.hasOwnProperty(prop)) {
delete script[prop];
}
}
}
delete window[JSONP_REQUESTS_PROPERTY_STR][uid];
},
/**
* Construct a JSONP request for a Geotab API call
* @private
* @param {String} method The name of the API method
* @param {Object} params The method parameters
* @param {successCallback} callbackSuccess The function that is called if the method call was successful
* @param {failureCallback} [callbackError] The function that is called if the method call failed
* @return {Object} An object with operations for the call. Supported operation(s): abort()
*/
callJSONP = function (method, params, callbackSuccess, callbackError) {
var uid = 'json' + (Math.random() * 100).toString().replace(/\./g, ''),
buildParamString = function () {
params = params || {};
var query = [];
for (var key in params) {
if (params.hasOwnProperty(key)) {
query.push.apply(query, ['&', encodeURIComponent(key), '=', encodeURIComponent(JSON.stringify(params[key]))]);
}
}
return query.join('');
},
timeoutTimer;
window[JSONP_REQUESTS_PROPERTY_STR][uid] = function JSONPResponse(data) {
// Clear timeout timer first
if (timeoutTimer) {
clearTimeout(timeoutTimer);
timeoutTimer = null;
}
// Try to handle the response from the server
try {
if (data && data.error) {
debugLog(method, 'ERROR', data.error);
handleError(data.error, callbackError);
} else {
var result = data.result;
debugLog(method, 'SUCCESS', {
result: result
});
if (callbackSuccess) {
callbackSuccess(result);
}
}
} finally {
cleanupCall(uid);
}
};
document.getElementsByTagName('body')[0].appendChild((function () {
var s = document.createElement('script');
s.type = 'text/javascript';
s.id = uid;
s.async = 'async';
s.src = getCallUrl(method) + '?JSONP=' + JSONP_REQUESTS_PROPERTY_STR + '.' + uid + buildParamString();
s.onerror = function JSONPError(error) {
try {
debugLog('CallJSONP', method, 'ERROR', error);
handleError(error, callbackError);
} finally {
cleanupCall(uid);
}
};
return s;
})());
if (timeoutTimer) {
clearTimeout(timeoutTimer);
}
if (options.timeout && method !== 'Authenticate') {
timeoutTimer = setTimeout(function () {
if (window[JSONP_REQUESTS_PROPERTY_STR].hasOwnProperty(uid)) {
window[JSONP_REQUESTS_PROPERTY_STR][uid]({
error: {
name: 'JSONPTimeout',
message: 'Could not complete the JSONP request in a timely manner (' + options.timeout + 's)',
target: document.getElementById(uid)
}
});
window[JSONP_REQUESTS_PROPERTY_STR][uid] = function () {
cleanupCall(uid);
};
}
}, options.timeout * 1000);
}
return {
abort: function () {
cleanupCall(uid);
if (errorCallback) {
errorCallback('Cancelled', {});
}
}
}
},
/**
* Construct an XMLHttpRequest POST request for a Geotab API call
* @private
* @param {String} method The name of the API method
* @param {Object} params The method parameters
* @param {successCallback} callbackSuccess The function that is called if the method call was successful
* @param {failureCallback} [callbackError] The function that is called if the method call failed
* @return {Object} An object with operations for the call. Supported operation(s): abort()
*/
callXHR = function (method, params, callbackSuccess, callbackError) {
var xhr = new XMLHttpRequest();
xhr.open('POST', getCallUrl(), true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.addEventListener('abort', function (e) {
if (callbackError) {
callbackError('Cancelled', e);
}
});
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
var data,
error,
result;
try {
data = JSON.parse(xhr.responseText);
if (data && data.error) {
error = data.error;
debugLog(method, 'ERROR', error);
handleError(error, callbackError);
} else {
result = data.result;
debugLog(method, 'SUCCESS', {
result: result
});
callbackSuccess(result);
}
} catch (e) {
handleError(e, callbackError);
}
} else {
debugLog(method, 'ERROR', xhr);
handleError(xhr, callbackError);
}
}
};
var rpcString;
try {
rpcString = JSON.stringify({
method: method || '',
params: params
});
} catch (e) {
handleError(e, callbackError);
return;
}
if (options.timeout) {
xhr.timeout = options.timeout * 1000;
}
xhr.send('JSON-RPC=' + encodeURIComponent(rpcString));
return {
abort: function () {
xhr.abort();
}
}
},
/**
* Construct a request for a Geotab API call
* @private
* @param {String} method The name of the API method
* @param {Object} params The method parameters
* @param {successCallback} callbackSuccess The function that is called if the method call was successful
* @param {failureCallback} [callbackError] The function that is called if the method call failed
* @return {Object} An object with operations for the call. Supported operation(s): abort()
*/
callBase = function (method, params, callbackSuccess, callbackError) {
if (options.jsonp) {
return callJSONP(method, params, callbackSuccess, callbackError);
}
return callXHR(method, params, callbackSuccess, callbackError);
},
/**
* Authenticates a Geotab user
* @private
* @param {String} newServer The federation server name string
* @param {String} database The database name
* @param {String} username The username/email to authenticate with
* @param {String} password The password to authenticate with
* @param {successCallback} callbackSuccess The function that is called on the successful authentication
* @param {failureCallback} [callbackError] The function that is called when there is an
* authentication failure with the error string and the
* error objects as parameters
* @return {Object} An object with operations for the call. Supported operation(s): abort()
*/
authenticateWithServer = function (newServer, database, username, password, callbackSuccess, callbackError) {
server = newServer;
// Authenticate the user
return callBase('Authenticate', {
database: database,
userName: username,
password: password
}, function (data) {
if (data.path && data.path !== 'ThisServer') {
server = 'https://' + data.path + '/';
}
credentials = data.credentials;
if (options.rememberMe) {
credentialsStore.set(credentials, server);
}
if (callbackSuccess) {
callbackSuccess();
}
}, callbackError);
},
/**
* Populates the authenticate callback with a function
* @private
* @param {successCallback} [callbackSuccess] Called when we have successfully authenticated
*/
populateAuthenticationCallback = function (callbackSuccess) {
getCredentialsCallback(function (newServer, database, username, password, error) {
return authenticateWithServer(newServer, database, username, password, function () {
if (callbackSuccess) {
callbackSuccess();
}
// Try again
pendingCalls.forEach(function (p) {
call.apply(this, p);
});
pendingCalls = [];
}, error);
});
},
/**
* Calls a Geotab method. Handles cases where the credentials have expired or are invalid.
* @public
* @param {String} method The name of the API method
* @param {Object} params The method parameters object.
* @param {successCallback} callbackSuccess The function that is called if the method call was successful
* @param {failureCallback} [callbackError] The function that is called if the method call failed
* @return {Object} An object with operations for the call. Supported operation(s): abort()
*/
call = function (method, params, callbackSuccess, callbackError) {
var
needsLoginAndCall = function () {
pendingCalls.push([method, params, callbackSuccess, callbackError]);
var storedCredentials = credentialsStore.get();
if (storedCredentials && options.rememberMe) {
credentials = storedCredentials.credentials;
server = storedCredentials.server;
// Try again
pendingCalls.forEach(function (p) {
call.apply(this, p);
});
pendingCalls = [];
} else {
populateAuthenticationCallback();
}
};
if (!credentials) {
needsLoginAndCall();
return {
abort: function () { }
};
}
params.credentials = credentials;
return callBase(method, params, callbackSuccess, function (errorString, errorObject) {
var errors = errorObject.errors;
if (errors && errors[0] && errors[0].name === 'InvalidUserException' && method !== 'Authenticate') {
// We do have a credentials token, but it's no longer valid
// Let's clear it
credentialsStore.clear();
needsLoginAndCall();
} else {
if (callbackError) {
callbackError(errorString, errorObject);
}
}
});
},
/**
* Calls multiple Geotab methods at the same time. Returns an array of results corresponding to the order the calls are passed in
* @public
* @param {Array} calls The calls array. For each entity in this array, we expect a 1 or 2 item array: the first
* item is the method name, and (optionally) the second is an object of the method parameters
* eg. [
* ['GetVersion'],
* ['Get', { typeName: 'Device', search: { serialNumber: 'GTA1234556678' } }],
* ['Add', { typeName: 'Device', entity: { name: 'New Device', serialNumber: 'GTA0000000000' }]
* ]
* @param {successCallback} callbackSuccess The function that is called if the method calls were all successful
* @param {failureCallback} [callbackError] The function that is called if any method call failed
* @return {Object} An object with operations for the call. Supported operation(s): abort()
*/
multiCall = function (calls, callbackSuccess, callbackError) {
var formattedCalls = calls.map(function (call) {
var params = call[1];
return {
method: call[0],
params: params
};
});
return call('ExecuteMultiCall', {
calls: formattedCalls
}, callbackSuccess, callbackError);
},
/**
* Retrieves a session. Useful for single sign-on or other cases where you require the credentials
* @param {successCallback} callbackSuccess The function that is called if the retrieval of sessionId was successful
* @param {Boolean} [newSession] If true, always retrieve a new session from the server. Otherwise, return the current session (if active) or
* retrieve a new one from the server if there are no active sessions
*/
getSession = function (callbackSuccess, newSession) {
var storedCredentials = credentialsStore.get();
if (!newSession && storedCredentials && options.rememberMe) {
if (callbackSuccess) {
callbackSuccess(storedCredentials.credentials, storedCredentials.server);
}
return;
}
populateAuthenticationCallback(function () {
if (callbackSuccess) {
// At this point, 'credentials' and 'server' have been set within populateAuthenticationCallback()
callbackSuccess(credentials, server);
}
});
},
/**
* Clears credentials and the credential store.
* @public
*/
forget = function () {
credentials = null;
credentialsStore.clear();
populateAuthenticationCallback();
};
// Overrides default options with passed in ones, if there are any
if (newOptions) {
for (var prop in newOptions) {
if (newOptions.hasOwnProperty(prop)) {
options[prop] = newOptions[prop];
}
}
}
// Creates a property for managing open requests
window[JSONP_REQUESTS_PROPERTY_STR] = {};
/**
* The default credentials store. Uses localStorage to save and retrieve
* credentials to save a user's session.
*/
var defaultCredentialsStore = {
CREDENTIALS_KEY: 'geotabAPI_credentials',
SERVER_KEY: 'geotabAPI_server',
/**
* Gets the credentials from localStorage
* @returns {*} An object with two properties: 'server', a server name
* string, and 'credentials', a credentials object
*/
get: function () {
var storedCredentials = localStorage.getItem(this.CREDENTIALS_KEY),
storedServer = localStorage.getItem(this.SERVER_KEY),
thisCredentials = false;
if (storedCredentials && storedServer) {
try {
thisCredentials = {
credentials: JSON.parse(storedCredentials),
server: storedServer
};
} catch (e) {
// Malformed JSON
return false;
}
}
return thisCredentials;
},
/**
* Saves the credentials into localStorage
* @param {Object} credentials The credentials object
* @param {String} server The server string
*/
set: function (credentials, server) {
localStorage.setItem(this.CREDENTIALS_KEY, JSON.stringify(credentials));
localStorage.setItem(this.SERVER_KEY, server);
},
/**
* Clears the credentials from localStorage
*/
clear: function () {
localStorage.removeItem(this.CREDENTIALS_KEY);
localStorage.removeItem(this.SERVER_KEY);
}
};
// If applicable, overrides default credentials storage implementation with a
// custom one that was passed into the constructor
credentialsStore = customCredentialStore || defaultCredentialsStore;
// Public methods
return {
authenticate: authenticateWithServer,
call: call,
multiCall: multiCall,
forget: forget,
getSession: getSession
};
};
if (typeof define !== 'undefined' && define.amd) {
// AMD. Register as an anonymous module.
define(function () {
'use strict';
return GeotabApi;
});
}
module.export = GeotabApi;