Collaboration Store, Part XXIII

“Everything you can imagine is real.”
Pablo Picasso

Last time, we completed the work on the UI Page for our modal pop-up and tested it out with a stubbed-out version of our server-side Script Include. Now we need to go back into our Script Include and put in the actual code that will do all of the work of publishing an app to the Host instance. In our stubbed-out version, our processPhase function simply returned the value ‘success’ for every call. We need to add a little structure to that function so that each phase can have an exclusive function of its own to contain all of the logic for that particular phase. We can do that by examining the phase variable, and then we can create separate functions for each phase. At this point, we can even stub those out as we did the original, just to allow us to build and test one function at at time. Here is the modified portion of the script:

processPhase: function(phase, mbrAppId, appSysId, updSetId, origAppId) {
	var answer = '';

	if (phase == 1) {
		answer = this.processPhase1(mbrAppId, appSysId, updSetId, origAppId);
	} else if (phase == 2) {
		answer = this.processPhase2(mbrAppId, appSysId, updSetId, origAppId);
	} else if (phase == 3) {
		answer = this.processPhase3(mbrAppId, appSysId, updSetId, origAppId);
	} else if (phase == 4) {
		answer = this.processPhase4(mbrAppId, appSysId, updSetId, origAppId);
	} else if (phase == 5) {
		answer = this.processPhase5(mbrAppId, appSysId, updSetId, origAppId);
	} else if (phase == 6) {
		answer = this.processPhase6(mbrAppId, appSysId, updSetId, origAppId);
	} else {
		gs.addErrorMessage('Invalid phase error; invalid phase: ' + phase);
		answer = 'Invalid phase error; invalid phase: ' + phase;
	}

	return answer;
},

processPhase1: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

processPhase2: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

processPhase3: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

processPhase4: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

processPhase5: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

processPhase6: function(mbrAppId, appSysId, updSetId, origAppId) {
	return 'success';
},

At this point, we can even run our test again, just to be sure that we did not break anything, and it still should step through all of the tasks and reveal the Done button at the end. So far, so good. Now we can tackle each step one at a time, and work our way through the various processes until we complete them all. We might as well take them all in order, so let’s start out with the processPhase1 function.

The purpose of this first step will be to turn the Update Set into XML. The first thing that we will need to do is to use the passed Update Set sys_id to get the GlideRecord for the Update Set, which we will need to pass to our global function that does all of the heavy lifting. Once we successfully produce the XML, we will have to do something with it temporarily, since we do not yet have the version record to which it will eventually be attached. The simplest thing to do would be to attach it to some record that we do have, and then transfer the attachment once we create the version record. This should work, but it would be nice to send back the sys_id of the new attachment record, just to make things easier in the future step. If we want to do that, though, we will have to change our strategy for the response from a simple string to a JSON string that can have multiple values. That’s not that much of a change, and it sounds like something that will be useful to have in this process, so let’s just go ahead and do that now.

If we create an object that contains all of the values that we have been passing around, that will actually simplify the function calls, as there will only be the one object to pass as an argument. This will change our onload function to this:

function onLoad() {
	var answer = {};
	answer.phase = 1;
	answer.mbrAppId = gel('mbr_app_id').value;
	answer.appSysId = gel('app_sys_id').value;
	answer.updSetId = gel('upd_set_id').value;
	answer.origAppId = gel('mbr_app_id').value;
	processPhase(answer);
}

It will also change our processPhase function to accept the object as the lone argument, and to both send and receive a stringified version of the object with the GlideAjax call.

function processPhase(answer) {
	var ga = new GlideAjax('ApplicationPublisher');
	ga.addParam('sysparm_name', 'processPhaseClient');
	ga.addParam('sysparm_json', JSON.stringify(answer));
	ga.getXMLAnswer(function (jsonString) {
		hideElement('loading_' + answer.phase);
		answer = JSON.parse(jsonString);
		if (answer.error) {
			showElement('error_' + answer.phase);
			showElement('done_button');
		} else {
			showElement('success_' + answer.phase);
			answer.phase++;
			if (answer.phase < 7) {
				showElement('phase_' + answer.phase);
				processPhase(answer);
			} else {
				showElement('done_button');
			}
		}
	});
}

I actually like this much better, but now we are going to have to modify our Script Include to expect the JSON string coming in and also to send a JSON string back. The modified Script Include now looks like this:

var ApplicationPublisher = Class.create();
ApplicationPublisher.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {

	processPhaseClient: function() {
		var jsonString = this.getParameter('sysparm_json');
		var answer = {};
		try {
			answer = JSON.parse(jsonString);
			answer = this.processPhase(answer);
		} catch (e) {
			answer = this.processError({}, 'Unparsable JSON string parameter: ' + jsonString);
		}
		
		return JSON.stringify(answer);
	},

	processPhase: function(answer) {
		if (answer.phase == 1) {
			answer = this.processPhase1(answer);
		} else if (answer.phase == 2) {
			answer = this.processPhase2(answer);
		} else if (answer.phase == 3) {
			answer = this.processPhase3(answer);
		} else if (answer.phase == 4) {
			answer = this.processPhase4(answer);
		} else if (answer.phase == 5) {
			answer = this.processPhase5(answer);
		} else if (answer.phase == 6) {
			answer = this.processPhase6(answer);
		} else {
			answer = this.processError(answer, 'Invalid phase error; invalid phase: ' + phase);
		}

		return answer;
	},

	processPhase1: function(answer) {
		return answer;
	},

	processPhase2: function(answer) {
		return answer;
	},

	processPhase3: function(answer) {
		return answer;
	},

	processPhase4: function(answer) {
		return answer;
	},

	processPhase5: function(answer) {
		return answer;
	},

	processPhase6: function(answer) {
		return answer;
	},

	processError: function(answer, message) {
		gs.addErrorMessage(message);
		answer.error = message;
		return answer;
	},

	type: 'ApplicationPublisher'
});

I also went ahead and added a processError function to script to consolidate all of the code related to reporting an issue with any of the processes. Again, I think this is much better than the original, as it both simplifies the code and opens possibilities that did not exist with the original design. Before we get back to coding out that initial phase, we should run another test, just to make sure things are still working as they should.

Rerunning the concept test after modifying the UI Page and the Script Include

Well, at lease we did not break anything. Now let’s get back to work on that processPhase1 function. First, we need to fetch the GlideRecord for the Update Set.

var updateSetGR = new GlideRecord('sys_update_set');
if (updateSetGR.get(answer.updSetId)) {
	...
} else {
	answer = this.processError(answer, 'Invalid Update Set sys_id: ' + answer.updSetId);
}

There is no reason to expect that we would not retrieve the Update Set GlideRecord at this point, but just in case, we check for that anyway and report an error if something is amiss. With the GlideRecord in hand, we can now call on our global utility to turn the Update Set into XML.

var csgu = new global.CollaborationStoreGlobalUtils();
var xml = csgu.updateSetToXML(updateSetGR);

Our global utility does not report any kind of error, but we should probably examine the XML returned, just to make sure that we have a valid XML file. We should be able to do that by checking the first line for the standard XML header.

if (xml.startsWith('<?xml version="1.0" encoding="UTF-8"?>')) {
	...
} else {
	answer = this.processError(answer, 'Invalid XML file returned from subroutine');
}

Now that we have the XML, we need to stuff it somewhere until we need it in a future step. We should be able to go ahead and create the attachment record at this point, and then we can just move the attachment to its proper place once we reach that point. To do that, we can take advantage of the GlideSysAttachment API. One of the things that we will need in order to do that is a name for our new XML file, which we should be able to generate from some details in the original application record, so we will have to go fetch that guy first, and then build the file name from there.

var sysAppGR = new GlideRecord('sys_app');
if (sysAppGR.get(answer.appSysId)) {
	var fileName = sysAppGR.getDisplayValue('name');
	fileName = fileName.toLowerCase().replace(/ /g, '_');
	fileName += '_v' + sysAppGR.getDisplayValue('version') + '.xml';
	var gsa = new GlideSysAttachment();
	...
} else {
	answer = this.processError(answer, 'Invalid Application sys_id: ' + answer.appSysId);
}

Once again, there is no reason to expect that we would not retrieve the application GlideRecord at this point, but just in case, we check for that as well, and report any errors. Once we have the record, we build the file name from the app name and the app version. Now we have everything that we need to create the attachment.

answer.attachmentId = gsa.write(sysAppGR, fileName, 'application/xml', xml);
if (!answer.attachmentId) {
	answer = this.processError(answer, 'Unable to create XML file attachment');
}

Now we have generated our XML and stuffed it into an attachment record for later use. All together, the entire function now looks like this:

processPhase1: function(answer) {
	var updateSetGR = new GlideRecord('sys_update_set');
	if (updateSetGR.get(answer.updSetId)) {
		var csgu = new global.CollaborationStoreGlobalUtils();
		var xml = csgu.updateSetToXML(updateSetGR);
		if (xml.startsWith('<?xml version="1.0" encoding="UTF-8"?>')) {
			var sysAppGR = new GlideRecord('sys_app');
			if (sysAppGR.get(answer.appSysId)) {
				var fileName = sysAppGR.getDisplayValue('name');
				fileName = fileName.toLowerCase().replace(/ /g, '_');
				fileName += '_v' + sysAppGR.getDisplayValue('version') + '.xml';
				var gsa = new GlideSysAttachment();
				answer.attachmentId = gsa.write(sysAppGR, fileName, 'application/xml', xml);
				if (!answer.attachmentId) {
					answer = this.processError(answer, 'Unable to create XML file attachment');
				}
			} else {
				answer = this.processError(answer, 'Invalid Application sys_id: ' + answer.appSysId);
			}
		} else {
			answer = this.processError(answer, 'Invalid XML file returned from subroutine');
		}
	} else {
		answer = this.processError(answer, 'Invalid Update Set sys_id: ' + answer.updSetId);
	}

	return answer;
},

Well, that was a bit of work, but hopefully the remaining steps will all be a little easier. Next time out, we can start on the second step, which will be to create or update the Collaboration Store application record.