Collaboration Store, Part XL

“If one does not know to which port one is sailing, no wind is favorable.”
Lucius Annaeus Seneca

Last time, we started building out the Script Include function that will do all of the heavy lifting for the process of periodically syncing up all of the Client instances with the Host. The next piece of code that we were going to tackle was the process of sending over a missing instance record to the Client. At the time, I was thinking that I had a couple of different versions of the code to do that already built out, one being cloned off of the other, but as it turns out, that is true for the application records, the version records, and the version attachments, but not the instance records. Currently, there is only one function that pushes over an instance record, and that function is called publishInstance and is found in the CollaborationStoreUtils Script Include.

publishInstance: function(instanceGR, targetGR) {
	var result = {};

	var payload = {};
	payload.instance = instanceGR.getDisplayValue('instance');
	payload.name = instanceGR.getDisplayValue('name');
	payload.description = instanceGR.getDisplayValue('description');
	payload.email = instanceGR.getDisplayValue('email');
	payload.accepted = instanceGR.getDisplayValue('accepted');
	payload.active = instanceGR.getDisplayValue('active');
	payload.host = instanceGR.getDisplayValue('host');
	var url = 'https://' + targetGR.getDisplayValue('instance') + '.service-now.com/api/now/table/x_11556_col_store_member_organization';
	var request = new sn_ws.RESTMessageV2();
	request.setEndpoint(url);
	request.setHttpMethod('POST');
	request.setBasicAuth(this.WORKER_ROOT + targetGR.getDisplayValue('instance'), targetGR.getDisplayValue('token'));
	request.setRequestHeader('Content-Type', 'application/json');
	request.setRequestHeader('Accept', 'application/json');
	request.setRequestBody(JSON.stringify(payload, null, '\t'));
	var response = request.execute();
	result.status = response.getStatusCode();
	result.body = response.getBody();
	if (result.body) {
		try {
			result.obj = JSON.parse(result.body);
		} catch (e) {
			result.parse_error = e.toString();
		}
	}
	result.error = response.haveError();
	if (result.error) {
		result.error_code = response.getErrorCode();
		result.error_message = response.getErrorMessage();
	}

	return result;
}

It takes two arguments, the instanceGR and the targetGR. In the current version of our new code, we do not have a GlideRecord for either the instance to be sent over or the instance to which the record is to be sent, but with a little refactoring, we can make that happen. To start with, I changed this code:

var instance = '';
var token = '';
var instanceList = [];
instanceGR = new GlideRecord('x_11556_col_store_member_organization');
instanceGR.addQuery('active', true);
instanceGR.query();
while (instanceGR.next()) {
    instanceList.push(instanceGR.getDisplayValue('name'));
    if (instanceGR.getUniqueValue() == instanceId) {
        instance = instanceGR.getDisplayValue('name');
        token = instanceGR.getDisplayValue('token');
    }
}

… to this:

var targetGR = new GlideRecord('x_11556_col_store_member_organization');
targetGR.get(instanceId);
var instance = targetGR.getDisplayValue('name');
var token = targetGR.getDisplayValue('token');
var instanceList = [];
var instanceGR = new GlideRecord('x_11556_col_store_member_organization');
instanceGR.addQuery('active', true);
instanceGR.query();
while (instanceGR.next()) {
	instanceList.push(instanceGR.getDisplayValue('name'));
}

This still gets me the target instance name and token as it did earlier, but it also gets me a GlideRecord for the target instance as well.

Then I changed this line:

remoteSysId = this.sendInstance(instance, token, thisInstance);

… to this:

remoteSysId = this.sendInstance(targetGR, thisInstance);

That took care of the refactoring. Now all I needed to do was to fetch a GlideRecord for the instance to be sent over, and then I could call the existing function in CollaborationStoreUtils and check the response.

sendInstance: function(targetGR, thisInstance) {
	var sysId = '';
	var instanceGR = new GlideRecord('x_11556_col_store_member_organization');
	if (instanceGR.get('name', thisInstance)) {
		var result = this.CSU.publishInstance(instanceGR, targetGR);
		if (result.error) {
			gs.error('InstanceSyncUtils.sendInstance - Error occurred attempting to push instance ' + thisInstance + ' : ' + result.errorCode + '; ' + result.errorMessage);
		} else if (result.status != 200) {
			gs.error('InstanceSyncUtils.sendInstance - Invalid HTTP response code returned from attempt to push instance ' + thisInstance + ' : ' + result.status);
		} else if (!result.obj) {
			gs.error('InstanceSyncUtils.sendInstance - Invalid response body returned from attempt to push instance ' + thisInstance + ' : ' + result.body);
		} else {
			sysId = result.obj.sys_id;
		}
	}
	return sysId;
}

Of course, I haven’t actually tested any of this as yet, but it looks like it ought to work out OK. If not, well then we will make a few adjustments.

The next thing on the list, then, is to build out the other referenced function that does not yet exist, the syncApplications function. Here is where we will eventually run into the need for that code that was cloned earlier, so we will want to clean all of that up finally, and the publishInstance function actually looks like a good model to use for the way in which it works, sending back all of the good and bad results in an object that can be evaluated by the caller where actions can be taken that are appropriate for the specific context.

Here is the original version of the code that sends over an application:

processPhase5: function(answer) {
	var mbrAppGR = new GlideRecord('x_11556_col_store_member_application');
	if (mbrAppGR.get(answer.mbrAppId)) {
		var host = gs.getProperty('x_11556_col_store.host_instance');
		var token = gs.getProperty('x_11556_col_store.active_token');
		var thisInstance = gs.getProperty('instance_name');
		var request  = new sn_ws.RESTMessageV2();
		request.setHttpMethod('get');
		request.setBasicAuth(this.WORKER_ROOT + host, token);
		request.setRequestHeader("Accept", "application/json");
		request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application?sysparm_fields=sys_id&sysparm_query=provider.instance%3D' + thisInstance + '%5Ename%3D' + encodeURIComponent(mbrAppGR.getDisplayValue('name')));
		var response = request.execute();
		if (response.haveError()) {
			answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
		} else if (response.getStatusCode() == '200') {
			var jsonString = response.getBody();
			var jsonObject = {};
			try {
				jsonObject = JSON.parse(jsonString);
			} catch (e) {
				answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
			}
			if (!answer.error) {
				var payload = {};
				payload.name = mbrAppGR.getDisplayValue('name');
				payload.description = mbrAppGR.getDisplayValue('description');
				payload.current_version = mbrAppGR.getDisplayValue('current_version');
				payload.active = 'true';
				request  = new sn_ws.RESTMessageV2();
				request.setBasicAuth(this.WORKER_ROOT + host, token);
				request.setRequestHeader("Accept", "application/json");
				if (jsonObject.result && jsonObject.result.length > 0) {
					answer.hostAppId = jsonObject.result[0].sys_id;
					request.setHttpMethod('put');
					request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application/' + answer.hostAppId);
				} else {
					request.setHttpMethod('post');
					request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application');
					payload.provider = thisInstance;
				}
				request.setRequestBody(JSON.stringify(payload, null, '\t'));
				response = request.execute();
				if (response.haveError()) {
					answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
				} else if (response.getStatusCode() != 200 && response.getStatusCode() != 201) {
					answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
				} else {
					jsonString = response.getBody();
					jsonObject = {};
					try {
						jsonObject = JSON.parse(jsonString);
					} catch (e) {
						answer = this.processError(answer, 'Unparsable JSON string returned from Host instance: ' + jsonString);
					}
					if (!answer.error) {
						answer.hostAppId = jsonObject.result.sys_id;
					}
				}
			}
		} else {
			answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
		}
	} else {
		answer = this.processError(answer, 'Invalid Member Application sys_id: ' + answer.appSysId);
	}

	return answer;
}

… and here is the other version that was cloned and hacked up based on the original:

publishNewVersion: function(newVersion, targetInstance, attachmentId) {
	var targetGR = new GlideRecord('x_11556_col_store_member_organization');
	if (targetGR.get('instance', targetInstance)) {
		var token = targetGR.getDisplayValue('token');
		var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
		if (versionGR.get(newVersion)) {
			var canContinue = true;
			var targetAppId = '';
			var mbrAppGR = versionGR.member_application.getRefRecord();
			var request  = new sn_ws.RESTMessageV2();
			request.setHttpMethod('get');
			request.setBasicAuth(this.WORKER_ROOT + targetInstance, token);
			request.setRequestHeader("Accept", "application/json");
			request.setEndpoint('https://' + targetInstance + '.service-now.com/api/now/table/x_11556_col_store_member_application?sysparm_fields=sys_id&sysparm_query=provider.instance%3D' + mbrAppGR.getDisplayValue('provider.instance') + '%5Ename%3D' + encodeURIComponent(mbrAppGR.getDisplayValue('name')));
			var response = request.execute();
			if (response.haveError()) {
				gs.error('CollaborationStoreUtils.publishNewVersion: Error returned from Target instance ' + targetInstance + ': ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
				canContinue = false;
			} else if (response.getStatusCode() == '200') {
				var jsonString = response.getBody();
				var jsonObject = {};
				try {
					jsonObject = JSON.parse(jsonString);
				} catch (e) {
					gs.error('CollaborationStoreUtils.publishNewVersion: Unparsable JSON string returned from Target instance ' + targetInstance + ': ' + jsonString);
					canContinue = false;
				}
				if (canContinue) {
					var payload = {};
					payload.name = mbrAppGR.getDisplayValue('name');
					payload.description = mbrAppGR.getDisplayValue('description');
					payload.current_version = mbrAppGR.getDisplayValue('current_version');
					payload.active = 'true';
					request  = new sn_ws.RESTMessageV2();
					request.setBasicAuth(this.WORKER_ROOT + targetInstance, token);
					request.setRequestHeader("Accept", "application/json");
					if (jsonObject.result && jsonObject.result.length > 0) {
						targetAppId = jsonObject.result[0].sys_id;
						request.setHttpMethod('put');
						request.setEndpoint('https://' + targetInstance + '.service-now.com/api/now/table/x_11556_col_store_member_application/' + targetAppId);
					} else {
						request.setHttpMethod('post');
						request.setEndpoint('https://' + targetInstance + '.service-now.com/api/now/table/x_11556_col_store_member_application');
						payload.provider = mbrAppGR.getDisplayValue('provider.instance');
					}
					request.setRequestBody(JSON.stringify(payload, null, '\t'));
					response = request.execute();
					if (response.haveError()) {
						gs.error('CollaborationStoreUtils.publishNewVersion: Error returned from Target instance ' + targetInstance + ': ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
						canContinue = false;
					} else if (response.getStatusCode() != 200 && response.getStatusCode() != 201) {
						gs.error('CollaborationStoreUtils.publishNewVersion: Invalid HTTP Response Code returned from Target instance ' + targetInstance + ': ' + response.getStatusCode());
						canContinue = false;
					} else {
						jsonString = response.getBody();
						jsonObject = {};
						try {
							jsonObject = JSON.parse(jsonString);
						} catch (e) {
							gs.error('CollaborationStoreUtils.publishNewVersion: Unparsable JSON string returned from Target instance ' + targetInstance + ': ' + jsonString);
							canContinue = false;
						}
						if (canContinue) {
							targetAppId = jsonObject.result.sys_id;
						}
					}
				}
			} else {
				gs.error('CollaborationStoreUtils.publishNewVersion: Invalid HTTP Response Code returned from Target instance ' + targetInstance + ': ' + response.getStatusCode());
			}
			if (canContinue) {
				this.publishVersionRecord(targetInstance, token, versionGR, targetAppId, attachmentId);
			}
		} else {
			gs.error('CollaborationStoreUtils.publishNewVersion: Version record not found: ' + newVersion);
		}
	} else {
		gs.error('CollaborationStoreUtils.publishNewVersion: Target instance record not found: ' + targetInstance);
	}
}

Both functions accomplish the same thing, first checking to see if the application is already present on the remote instance, and then either adding it or updating it depending on whether or not it was already there. This is what we would want to do as well, so it seems as if we could collapse these two into a single, reusable function that would server our current purpose and these other two use cases as well. We could adopt the result object approach from the above publishInstance function and return the same form of object from our new function and then each user of the function could make their own choices as to what to do with the information returned. This is something that I have wanted to do for a while now, but it looks like it might be a little bit of work and some more refactoring of existing code, so let’s save all of that for our next installment.