Third Light Developer Exchange

Code and templating community forum for developers and software integrators

You are not logged in.

Announcement

If you wish to join the Developer Exchange, please contact your account manager - this is to avoid unnecessary spam in the forums. Many thanks for your understanding.

#1 2013-06-10 15:29:53

sam
Third Light Staff
Registered: 2013-06-06
Posts: 6

Javascript API from jQuery (and/or NodeJS)

The promise of JQuery.ajax & JSONP

I've been experimenting with calling the IMS API from jQuery.ajax() (and Ember.js but that's a story for another day).

I wanted to not only get to grips with how our API works but also: what promises are, what JSONP is, how jQuery ajax calls work, and how all of this would work with our API.

It turned out to be not as daunting as it first sounded. It also turned out to be simple to use from NodeJS (which saves all the hassle of making a placeholder HTML page just to see it work. For more details see the bottom of this post below).

Small jQuery Library

To help me (and to make me understand everything) I wrote a small jQuery wrapper to the IMS API. It's fully functional despite being less than 80 lines long:

if((typeof jQuery === "undefined") || (typeof $ === "undefined")  ){ $ = jQuery = require('jquery'); }
if(typeof IMS === "undefined"){ IMS = {}; }

IMS.DEBUG = true;

IMS.API = (function($){

	var my = {},

		baseUrl = "",
		tlx = "/api.json.tlx",
		apiVersion = "1.0",
		jsonpCallback = "handleResponse",
		sessionId = "",
		username = "",
		password = "",
		callbackNo = 0;

	url = function(){
		return baseUrl + tlx;
	}

	my.init = function(settings){
		baseUrl      	= settings.host         	|| baseUrl;
		tlx          	= settings.tlx          	|| tlx;
		jsonpCallback	= settings.jsonpCallback	|| jsonpCallback;
		sessionId    	= settings.sessionId    	|| sessionId;
		username     	= settings.username     	|| username;
		password     	= settings.password     	|| password;

		if((baseUrl.indexOf("http://") !== 0) && (baseUrl.indexOf("https://") !== 0)) {
			baseUrl = 'http://' + baseUrl;
		}
	}
	my.login = function(u,p){
		username	= u || username;
		password	= p || password;
		return my.call("Core.Login",{'username':username,'password':password}).then(function(response){
			sessionId = response.outParams.sessionId;
			return response;
		});
	}
	my.logout = function(){
		return my.call("Core.Logout",{});
	}
	my.call = function(action,inParams){
		
		var params = {};

		params.apiVersion = apiVersion;
		params.sessionId = sessionId;
		params.callback = jsonpCallback + callbackNo;
		params.action = action;
		params.inParams = inParams;

		var data = 'json='+JSON.stringify(params);

		if(IMS.DEBUG) {
			console.log("url:", url());
			console.log("calling:", action, data);
		}

		var settings = {
		  'dataType': "jsonp",
		  'url': url(),
		  'jsonp': false, 
		  'jsonpCallback': jsonpCallback + callbackNo,
		  'crossDomain': true,
		  'data': data,
		};

		callbackNo += 1; // unique call id
		return $.ajax(settings);
	}

	return my;

}(jQuery||$));

The only tricky bit here was having to add the unique callback number to make sure we didn't get into an asyncronous muddle.

It's very easy to use. It only has a minimal interface:

  • init(settings)

  • login()

  • call(action,inParams)

  • logout()

The only tricky bit here, conceptually, is that you need to wait for the login to return before you are ok to make other requests. Promises make this very simple and really help prevent sliding-to-the-right as each callback becomes nested in the previous one. (This is also where using an async framework like AngularJS or EmberJS provides huge benefits - but that's another story for another day).

Using promises avoids a lot of standard boilerplate error handling within the library. To handle errors you just add a .fail() to the promise.

It's used something like this:

IMS.API.init({host: hostname,'username':username,'password':password});

IMS.API.login().then(function(response){
	// logged-in
}).fail(function(err){
	// deal with error
}).then(function(){
	// logged-in
	// make API call
	IMS.API.call("Files.GetAssetDetails",{"assetId":id,"fields":{"metadata":true}}).then(function(response) {
		// do something with response
	});
});
Example

To try it out, paste the code I have below but, obviously, change the hostname, username, password and also find a valid IMS File Reference. I suggest running it before you change these things (and again after each individual change) so you can see the variety of API errors you get.

// quick example of use:-

var hostname = 'samims.thirdlight.local'; // put your hostname in here
var username = 'client'; // put your username in here
var password = 'client'; // put your password in here

IMS.API.init({host: hostname,'username':username,'password':password});

var logged_in = IMS.API.login().then(function(response){
	if(IMS.DEBUG){
		console.log('Login: OK');
		console.log('User: ' + response.outParams.userDetails.username + ' (' + response.outParams.userDetails.email + ')' );
	} 
}).fail(function(err){
	if(IMS.DEBUG){
		console.log('Login: FAILED ')
		console.log('Status: ' + err.status + ', ' + err.statusText );
	} 
}).then(function(){
	// pick an IMS File Reference of a file you know exists in your IMS (called 'Reference' in File Info panel, or see pictureid in URL) ...
	var id = 17069945738; //IMS File Reference

	// these calls are asynchronous so this will fail unless you wait for the login to complete. Hence it's here in the 'then' not below it.
	IMS.API.call("Files.GetAssetDetails",{"assetId":id,"fields":{"metadata":true}}).then(function(response) {
		// should really retun a promise here
		if(response.result.api === "OK" && response.result.action === "OK"){
			// do something with response.outParams
			console.log("filename:", response.outParams.filename);
		} else {
			// oops! something went wrong
			console.log("API:",response.result.api);
			console.log("action:",response.result.action);
			console.log("response:",response.result);
		}
	});
});

This should write the filename of the referenced file out to the console. In my case I got this:

filename: boats.jpg

To see a more detailed view of what is going on you can turn debugging on…

IMS.DEBUG = true;

…and re-run. In my case I get this:

url: http://samims.thirdlight.local/api.json.tlx
calling: Core.Login json={"apiVersion":"1.0","sessionId":"","callback":"handleResponse0","action":"Core.Login","inParams":{"username":"client","password":"client"}}
Login: OK
User: client (client@thirdlight.local)
url: http://samims.thirdlight.local/api.json.tlx
calling: Files.GetAssetDetails json={"apiVersion":"1.0","sessionId":"RgZcWRdYajYGrliVpeGfKQJP2QDDA2lc","callback":"handleResponse1","action":"Files.GetAssetDetails","inParams":{"assetId":17069945738,"fields":{"metadata":true}}}
filename: boats.jpg

I pulled this code out of a protoype I've been working on in-house so I had to write this example specially for this post. Originally I stupidly put the API call after the login (as opposed to inside the '.then' function). My excuse being that it was over a moth since I'd looked at this. I then went through the frustrating process of having all my calls rejected with 'ACTION_NOT_PERMITTED' followed by the humiliation at my own stupidity. This is the brave new world of asyncronicity: calls don't block and wait for a response, they return a promise and move on.

So I could have done this:

var logged_in = IMS.API.login(username,password).then(function(response){
	. . .
});

logged_in.then(function(){

	IMS.API.call("Files.GetAssetDetails",{"assetId":id,"fields":{"metadata":true}}).then(function(response) {
		. . .
	});

});

or, old school, ...

IMS.API.login(username,password).then(function(response){
	. . .
	IMS.API.call("Files.GetAssetDetails",{"assetId":id,"fields":{"metadata":true}}).then(function(response) {
		. . .
	});
}).fail(function(){
	. . .
})

but not this:

IMS.API.login(username,password).then(function(response){
	. . .
}).fail(function(){
	. . .
})

IMS.API.call("Files.GetAssetDetails",{"assetId":id,"fields":{"metadata":true}}).then(function(response) {
	. . .
});

Looking at the output from this last example you can clearly see the Files.GetAssetDetails API call being made just before the login returns OK.

url: http://samims.thirdlight.local/api.json.tlx
calling: Core.Login json={"apiVersion":"1.0","sessionId":"","callback":"handleResponse0","action":"Core.Login","inParams":{"username":"client","password":"client"}}
url: http://samims.thirdlight.local/api.json.tlx
calling: Files.GetAssetDetails json={"apiVersion":"1.0","sessionId":"","callback":"handleResponse1","action":"Files.GetAssetDetails","inParams":{"assetId":17069945738,"fields":{"metadata":true}}}
Login: OK
User: client (client@thirdlight.local)
API: OK
action: ACTION_NOT_PERMITTED
response: { api: 'OK', action: 'ACTION_NOT_PERMITTED', debug: '' }
From NodeJS

First install jQuery for NodeJS using NPM into the folder you will run the script from (or globally if you prefer using '-g' ):

$ npm install jquery

once this is complete you should just be able to run it using node:

$ node IMS_From_jQuery.js
filename: boats.jpg

Offline

Board footer