Collaboration Store, Part XXXIII

“The only impossible journey is the one you never begin.”
Tony Robbins

Last time, we completed the construction of the process that will send out a new version of an application out to all of the instances in the community. Now we need to come up with a mechanism to kick this process off whenever a new version of an application is published to the Host instance. The simplest way to do that would be to create a new Business Rule on one of the tables housing the artifacts of the new application. To make sure that all of the application artifacts have made their way to the Host, the safest table to target would be the Attachment table, since that is the last artifact to be saved or sent to the Host. Unfortunately, that table is used for all kinds of things, so we will need to make sure that only records attached to the application version table trigger the rule.

To create a new Business Rule, navigate to System Definition > Business Rules and click the New button to bring up the data entry form.

New Business Rule input form

We’ll call our new rule Distribute New Version, associate it with the sys_attachment table, run it async on insert, and check the Advanced checkbox so that we can include a script to run when the rule is triggered.

New Distribute New Version Business Rule

To limit the impact of the rule to just the inserts that we want, we need to add a couple of Filter Conditions, one for the Table and another for the Content Type.

Business Rule filter conditions

On the Advanced tab, we will want to add a Condition that will ensure that this rule will only run on a Host instance. The easiest way to do that is to simply compare the stock instance_name property with the scoped host_instance property. Those two values will only be the same on a Host instance.

gs.getProperty('instance_name') == gs.getProperty('x_11556_col_store.host_instance')

The other thing that we need to do on the Advanced tab is to enter the script that will launch our Subflow. The original way to do that was to utilize the FlowAPI, which is the way that I had coded it initially.

sn_fd.FlowAPI.startSubflow('Distribute_New_Application_Version', {new_version: current.getDisplayValue('table_sys_id'), attachment_id: current.getUniqueValue()});

However, later I decided that it was high time to start embracing the new, preferred approach, which is to use the ScriptableFlowRunner. The code for that is a little more complicated, but at some point the older way of doing this will undoubtedly go away, so we might as well start transitioning things in that direction. Here is the script for the Business Rule using this newer technique:

(function executeRule(current, previous) {
	try {
		var result = sn_fd.FlowAPI.getRunner()
			.subflow('x_11556_col_store.Distribute_New_Application_Version')
			.inBackground()
			.withInputs({new_version: current.getDisplayValue('table_sys_id'), attachment_id: current.getUniqueValue()})
			.run();
	} catch (e) {
		gs.error('Business Rule Distribute New Version - Error running subflow Distribute_New_Application_Version: ' + e.getMessage());
	}
})(current, previous);

That completes the Business Rule, so all we need to do at this point is to Save it and now the next time that an XML attachment is created for a version record, our new Subflow will kick off, distributing the new version of the application to all instances in the community (except for the Host and the instance that produced the application). This also completes the application distribution process, so now it is time to release yet another Update Set so that those of you who have been nice enough to do a little testing can give it a go.

If you have just jumped into this and have not been following along, here is what you will need in order to install the app and try it out:

  • If you have not done so already, you will need to install the latest version of snh-form-fields, which you can find here.
  • You will also need to install this additional global component, which is not included in the scoped application.
  • Once you have successfully installed the prerequisites, you will then need to install the scoped application from this Update Set.
  • After the scoped application has been installed, using an admin account, navigate to Collaboration Store -> Collaboration Store Set-up and complete the set-up process for either a Host or Client instance. The Host instance will need to be set up first, as the set-up for a Client instance requires successful contact with a functioning Host.

And again, as I mentioned last time, in order to test out the distribution process, you will need three or more instances in your community so that one of the Client instances can publish the application to the Host instance and then the Host instance can distribute the new version out to all of the other Client instances. As always, all feedback is welcome and appreciated, and if you are able to get everything to actually work, please let me know in the comments. Thanks!

Next time, we will take a look at where we go from here. Thanks again to all of you who have taken the time to help me out and have let me know what you have found.

Collaboration Store, Part XXXII

“There are two ways to write error-free programs; only the third works.”
Alan J. Perlis

Now that we have all of the Javascript functions needed to push a new application version out to all of the Client instances, we need to create an Action that will call the root function and then create a Subflow that will utilize the new Action. Since we already created a similar Action for the process that shares new Client instances with existing Client instances, the easiest thing to do will be to bring that guy up in the Flow Designer and make a copy. To make a copy, pull up the Action that you want to copy, and use the ellipses menu to select Copy from the drop-down menu.

Making a copy of the Publish New Instance Action

Once we have our copy, we need to make the necessary changes to adapt it to our purpose. The publishNewVersion function that we just created takes three arguments (newVersion, targetInstance, and attachmentId), so we will need to modify the Action Inputs to bring in those values. The new Action Inputs now look like this:

Modified Action Inputs

Similarly, we will need to modify the Input Variables of the Script Step to pass along these values:

Modified Script Input Variables

The last thing that we need to do is modify the script itself. The existing script in the cloned Action was pretty simple, as it was just a call to one of our Script Include functions:

(function execute(inputs, outputs) {
	var csu = new CollaborationStoreUtils();
	csu.publishNewInstance(inputs.new_instance, inputs.target_instance);
})(inputs, outputs);

We also want to call a function, and it is in the very same Script Include, so our modification is just a matter of changing the name of the function along with the arguments passed.

(function execute(inputs, outputs) {
	var csu = new CollaborationStoreUtils();
	csu.publishNewVersion(inputs.new_version, inputs.target_instance, inputs.attachment_id);
})(inputs, outputs);

That’s it for the changes, so now all we have to do is Save and Publish the new Action and we are ready to utilize it in our new Subflow. Creating the Subflow will be another Copy operation, this time from our existing New_Collaboration_Store_Instance Subflow. We’ll call our copy Distribute_New_Application_Version, and once again, we will need to make some modifications to adapt it to our new purpose. The original Subflow had a single input, the New Instance. Instead of a new instance, we will be distributing a new application version, so we can replace the New Instance input with a New Version input that will contain the sys_id of the new version record. We will also need the sys_id of the Attachment record, so we will add an Attachment ID input as well.

Modified Inputs for the new Distribute_New_Application_Version Subflow

The target instances for this process will be the same as those for the original Subflow: all instances in the community except for the Host instance and the instance from which the data was provided (which could also be the Host in this case, since Host instances can publish applications just like any other Client instance). To filter out the data providing instance, we will need to add a new step to use the passed version record sys_id to fetch the GlideRecord that will lead us to the instance that produced the application.

New step to fetch the version record

This gives us the information that we need to complete the filter on the query of the instance table so that we can gather up all of the instances except for the Host and the instance that produced the application.

Modified query filter using the data available in the fetched version record

Now all we need to do is to replace the original custom Action inside of the loop with the new Action that we just created.

New Action added to push application artifacts to the target instance

This completes the changes needed to the cloned Subflow, so again, all we need to do is to Save and Publish and we are all ready to go. Now all we need to is to come up with a way to launch this Subflow whenever a new application version is sent over to the Host instance. We’ll take a look at that next time out, and hopefully release a new Update Set as well so that those of you that have been kind enough to participate in the testing can give this all a trial run.

Collaboration Store, Part XXXI

“Someone’s sitting in the shade today because someone planted a tree a long time ago.”
Warren Buffett

While we wait for additional test results to come in from the corrected Update Set for our latest iteration, it would be a good time to take a look at what we will need to do to complete the last remaining step in the application publishing process. What we have completed so far pushes all of the new application version artifacts to the Host instance. Now we need to build out the process for the Host instance to send out all of those artifacts to all of the other Client instances. Fortunately, we already have in hand most of the code to do that; we just need to clone a few things and make a number of modifications to fit the new purpose.

We have already built a Subflow to push new Client instance information out to all of the existing Client instances during the new instance registration process. Pushing out a new version of an application to those same instances is essentially the same process, so we should be able to clone that guy, and the associated Action, to create our new process. We have also created functions to push over the application record, the version record, and the attached Update Set XML file, and we should be able to copy over those guys as well to complete the process. In fact, we should probably start with the JavaScript functions, as we will need to call the root function in the Action, which will need to be created before it can be referenced in the Subflow.

The functions as written are hard-coded to send everything over to the Host instance. We will want to do basically the same thing, but this time we will be sending things out to various Client instances. Ideally, I should just refactor the code to have the destination instance and credentials passed in so that one function would work in both circumstances, but at this point it was less complicated to just make copies of each function and hack them up for their new purpose. I don’t really like having two copies of essentially the same thing, though, so one day I am going to have to go back and rework the entire mess.

But for now, this is what I came up with when I copied what was once called processPhase5 to create the new publishNewVersion function:

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);
	}
},

Unlike the original application publication process, we are not bouncing back and forth between the client side and the server side, so with the successful completion of one artifact, we can simply move on to pushing over the next artifact. To create the next step in the process, the publishVersionRecord function, I started out with a copy of the original processPhase6 function. Here is the end result:

publishVersionRecord: function(targetInstance, token, versionGR, targetAppId, attachmentId) {
	var canContinue = true;
	var payload = {};
	payload.member_application = targetAppId;
	payload.version = versionGR.getDisplayValue('version');
	var request  = new sn_ws.RESTMessageV2();
	request.setBasicAuth(this.WORKER_ROOT + targetInstance, token);
	request.setRequestHeader("Accept", "application/json");
	request.setHttpMethod('post');
	request.setEndpoint('https://' + targetInstance + '.service-now.com/api/now/table/x_11556_col_store_member_application_version');
	request.setRequestBody(JSON.stringify(payload, null, '\t'));
	response = request.execute();
	if (response.haveError()) {
		gs.error('CollaborationStoreUtils.publishVersionRecord: Error returned from Target instance ' + targetInstance + ': ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
		canContinue = false;
	} else if (response.getStatusCode() != 201) {
		gs.error('CollaborationStoreUtils.publishVersionRecord: 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.publishVersionRecord: Unparsable JSON string returned from Target instance ' + targetInstance + ': ' + jsonString);
			canContinue = false;
		}
		if (canContinue) {
			targetVerId = jsonObject.result.sys_id;
			this.publishVersionAttachment(targetInstance, token, targetVerId, attachmentId);
		}
	}
},

The last step in the process, then, is to send over the Update Set XML file attachment. For that one, I started out with the processPhase7 function and ended up with this:

publishVersionAttachment: function(targetInstance, token, targetVerId, attachmentId) {
	var gsa = new GlideSysAttachment();
	var sysAttGR = new GlideRecord('sys_attachment');
	if (sysAttGR.get(attachmentId)) {
		var url = 'https://';
		url += targetInstance;
		url += '.service-now.com/api/now/attachment/file?table_name=x_11556_col_store_member_application_version&table_sys_id=';
		url += targetVerId;
		url += '&file_name=';
		url += sysAttGR.getDisplayValue('file_name');
		var request  = new sn_ws.RESTMessageV2();
		request.setBasicAuth(this.WORKER_ROOT + targetInstance, token);
		request.setRequestHeader('Content-Type', sysAttGR.getDisplayValue('content_type'));
		request.setRequestHeader('Accept', 'application/json');
		request.setHttpMethod('post');
		request.setEndpoint(url);
		request.setRequestBody(gsa.getContent(sysAttGR));
		response = request.execute();
		if (response.haveError()) {
			gs.error('CollaborationStoreUtils.publishVersionAttachment: Error returned from Target instance ' + targetInstance + ': ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
		} else if (response.getStatusCode() != 201) {
			gs.error('CollaborationStoreUtils.publishVersionAttachment: Invalid HTTP Response Code returned from Target instance ' + targetInstance + ': ' + response.getStatusCode());
		}
	} else {
		gs.error('CollaborationStoreUtils.publishVersionAttachment: Invalid attachment record sys_id: ' + attachmentId);
	}
},

So that takes care of the functions. Now we have to create an Action in the Flow Designer that will call the function, and then produce a new Subflow that will leverage that action. That sounds like a good subject for our next installment.

Special Note to Testers

If you want to test the set-up process, you can do that with a single instance. You just have to select the Host option during the set-up process. If you want to test the Client set-up process, you will need at least two instances, one to serve as the Host, which you will need to reference when you set up the Client (you cannot be a Client of your own Host). But if you really want to test out the full registration process, including the Subflow that informs all existing instances of the new instance, you will need at least three participating instances: 1) the Host instance, 2) the new Client instance, and 3) an existing Client instance that will be notified of the new member of the community.

The same holds true for the application publishing process. You can test a portion of the publishing process by publishing an app on a single Host instance, but if you want to test out all of the parts and pieces, you will want to publish the app from a Client instance and make sure that it makes its way all the way back to the Host. Once we complete the application distribution process, full testing will require at least three instances to verify that the Host distributes the new version of the application to other Clients.

Collaboration Store, Part XXX

“Writing the first 90 percent of a computer program takes 90 percent of the time. The remaining ten percent also takes 90 percent of the time and the final touches also take 90 percent of the time.”
Neil J. Rubenking

Well, the test results are starting to pour in, and it looks like I screwed things up when I manually edited the Update Set in an effort to eliminate one of the errors reported in the earlier testing cycle. It seems as if I should have left well enough alone and just informed people to ignore the error if it comes up. Here is an unmolested Update Set that should not have the problem that I created by hacking up the earlier version after it was generated. Hopefully, this will resolve that issue.

Two things to note, then, of this new version: 1) if you happen to get the unfortunate Table ‘sys_hub_action_status_metadata’ does not exist error, just ignore it, and 2) if you get any preview errors related to any sys_properties, be sure to skip those updates, as you do not want to overlay the property values that were established during the set-up process.

One of the other things that was reported was that it was not really clear as to what, exactly, needed to be tested. I have a tendency to focus on the construction process exclusively, without a lot of attention to the actual end product itself, so let’s see if we can’t rectify that situation a little bit now.

The initial early release of this effort was focused on the set-up process. All the set-up process does is set you up as the Host instance, or get you registered with a Host instance if you are setting up a Client instance. That’s all that it did, so the testing was limited to attempting to set up a Host, and then attempting to set up one or more Clients. A successful test would have all instances appear in the instance table on every instance involved in the testing. That seemed to be pretty straightforward.

For this next iteration, we introduced the ability to actually publish a Scoped Application to the Host. To test this newest feature, you will first have to have gone through the set-up process successfully, and then you need an app to publish. Any app in development will do, and if you don’t have one, you can always just stub one out for the testing.

To publish an app, you need to bring up the app’s primary form, and if all went well with the installation, there should be a new UI Action down at the bottom of the page called Publish to Collaboration Store.

New Publish to Collaboration Store UI Action

If you click on that guy and follow the ensuing pop-up dialogs through completion, the app should be published. To verify that all went well, you will have to go over to the Host instance and see if the app actually appears in the Related List under the publisher’s instance record. You should verify the presence of the application record, the version record, and the XML Update Set attachment. If all of those things are present, then the app was successfully published to the Host.

The one thing that will not happen just yet is for the newly published app to be distributed to any of the other Client instances in the community. That process is still under development, and is not included in this version of the application. Once that gets completed and all of the issues from this round of testing get resolved, we will put out yet another beta test version and go through the testing process all over again.

Thanks again to all of you who are taking the time to take this out for a spin. Any and all feedback is greatly appreciated. Please feel free to report any issues or successes in the comments below. Next time, if there are no further results to review, we will take a look at building out the distribution of newly published versions to the rest of the instances served by the Host.