“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.
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.
“As a rule, software systems do not work well until they have been used, and have failed repeatedly, in real applications.” — David Lorge Parnas
Now that we have completed the initial version of the application publishing process, it would be nice to release a new Update Set so that the folks who are inclined to help out with the testing can try it all out. However, we have already received feedback from the earlier testing of the set-up process, and we should really address all of those issues before publishing a new version for further testing. Here are the items discovered during the testing of the first version of the software released earlier:
Installation error: Table ‘sys_hub_action_status_metadata’ does not exist
Not allowing update of property: x_11556_col_store.store_name
Not allowing update of property: x_11556_col_store.host_instance
In the setup, the instance name field doesn’t inform you that you only need the instance prefix, not the full url
You can only collaborate with one host
I was able to reproduce them all, and here is what I ended up doing for each:
Installation error: Table ‘sys_hub_action_status_metadata’ does not exist
I tried to find out some information on the purpose and use for this table, but I couldn’t really find anything that told me anything of value. I still believe that the ‘sys_hub_action_status_metadata’ table is related to a version or plugin that I have in my instance, but was not present in the instance on which the test installation was being performed. Since it didn’t seem as if it was anything that had anything to do with the operation of the application, I decided to just delete all references to it in the Update Set, just to avoid this issue. I’m not sure if that will cause any issues with any instances that actually do have this table, but it seemed like something worth a try and we’ll see what happens. If it causes a problem, I will not do that in the future and just throw in some release notes that say just ignore the error if it comes up. But let’s see if this works, first.
Not allowing update of property: x_11556_col_store.store_name Not allowing update of property: x_11556_col_store.host_instance
This one took a little bit of research and a little bit of trial and error (mostly error!), but I eventually solved the problem by moving all updates to these properties into my global utilities so that the command was being executed by a global component instead of a scoped component. I did this once before with the gs.sleep() function, and this worked out just as well. Here is the function that I added to the global utilities:
Once I created the function in the global utilities, it was just a matter of changing all gs.setProperty() function calls to csgu.setProperty() and that seemed to have done the trick.
In the setup, the instance name field doesn’t inform you that you only need the instance prefix, not the full url
Since the snh-form-field tag provides for a “help” attribute, which appears underneath the label, I simply updated the definition for that field to include a little help:
<snh-form-field
snh-model="c.data.host_instance_id"
snh-name="host_instance_id"
snh-label="Host Instance ID"
snh-help="Enter the instance ID only, not the full URL of the instance (https://{instance_id}.servicenow.com))"
snh-required="c.data.instance_type == 'client'"
ng-show="c.data.instance_type == 'client'"/>
This renders a little help text underneath the label, and looks like this when the page comes up:
That should resolve this issue.
You can only collaborate with one host
As I mentioned earlier when this was first reported, this is by design. Allowing multiple Host instances introduces a level of complexity with which I’m not quite ready to deal just yet. At this point, we will just file this one under Will not fix or Future release for now.
Other than these issues, I have not personally encountered or heard of any other issues with the set-up process, but that doesn’t mean that the testing is complete by any means. If anyone wants to join the testing process and you missed out on testing the earlier version, you will still have to work your way through the set-up process for any instances involved, so please report any issues with the set-up process as well as any issues with the application publication process.
Speaking of the application publication process, there is still yet another aspect of this process that remains to be developed. Once a new application has been published to the Host instance, the Host instance will need to push that version out to all of the other Client instances. This version does not include that functionality, but we will need to throw that in at some point. I just did not want to hold up the testing for that particular feature, since it really is a completely independent operation.
For those of you who are interested in participating in the testing, you will need this new Update Set, plus this additional global component that is not included in the scoped application. Also, if this is your first time installing the application, you will also need the latest version of snh-form-fields, which you can find here. As always, if you have any feedback, positive or negative, please leave the details in the comments. All information is welcome and much appreciated. Thanks in advance for your assistance.
“The best way out is always through.” — Robert Frost
Now that we have completed the functions that send the application record and version record over to the Host instance, the last thing that we need to do is to send over the Update Set XML file attached to the version record. For sending over an attachment, we will need to use the attachment REST API instead of the standard table REST API. Since the contents of the file that we are sending over is plain text, we can use a little hackery to bypass the need to send over an actual file and just place the text content in the body of the request. But first, as usual, we need to grab the GlideRecord that we want to send before we do anything else.
var gsa = new GlideSysAttachment();
var sysAttGR = new GlideRecord('sys_attachment');
if (sysAttGR.get(answer.attachmentId)) {
...
} else {
answer = this.processError(answer, 'Invalid attachment record sys_id: ' + answer.attachmentId);
}
Once we have obtained the record, we will need to grab the usual suspects from the System Properties and then construct the appropriate URL for the request.
var host = gs.getProperty('x_11556_col_store.host_instance');
var token = gs.getProperty('x_11556_col_store.active_token');
var url = 'https://';
url += host;
url += '.service-now.com/api/now/attachment/file?table_name=x_11556_col_store_member_application_version&table_sys_id=';
url += answer.hostVerId;
url += '&file_name=';
url += sysAttGR.getDisplayValue('file_name');
Next, we will need to construct and configure our sn_ws.RESTMessageV2 object.
var request = new sn_ws.RESTMessageV2();
request.setBasicAuth(this.WORKER_ROOT + host, 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));
Two things to note on this one: 1) the Content-Type header sets the value of the content_type property of the target instance sys_attachment record, and 2) we use our old friend, the GlideSysAttachment utility to obtain the actual XML of the attachment in lieu of a real file (the file contents are not actually a part of the GlideRecord for the sys_attachment table, hence the need to utilize GlideSysAttachment).
Now all we need to do is to execute the request and check the response.
var response = request.execute();
if (response.haveError()) {
answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() != 201) {
answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
}
Unlike the application and version records, we have no need for any information from the returned JSON string, so there is no need to attempt to parse it and pull out any data. As long as there are no errors, we are good to go.
Putting it all together, the entire function looks like this:
processPhase7: function(answer) {
var gsa = new GlideSysAttachment();
var sysAttGR = new GlideRecord('sys_attachment');
if (sysAttGR.get(answer.attachmentId)) {
var host = gs.getProperty('x_11556_col_store.host_instance');
var token = gs.getProperty('x_11556_col_store.active_token');
var url = 'https://';
url += host;
url += '.service-now.com/api/now/attachment/file?table_name=x_11556_col_store_member_application_version&table_sys_id=';
url += answer.hostVerId;
url += '&file_name=';
url += sysAttGR.getDisplayValue('file_name');
var request = new sn_ws.RESTMessageV2();
request.setBasicAuth(this.WORKER_ROOT + host, 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));
var response = request.execute();
if (response.haveError()) {
answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() != 201) {
answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
}
} else {
answer = this.processError(answer, 'Invalid attachment record sys_id: ' + answer.attachmentId);
}
return answer;
},
Unfortunately, when I took it out for a spin, it didn’t work. When I was nosing around to see what the source of the problem was, I figured out that the sys_id value that I was using for the attachment GlideRecord was not a string, but an array of comma-separated sys_id pairs, one for the original attachment and one for the copied attachment. This value came out of the fourth step, where we copied the attachment from the original scoped application record to the version record. Once I realized the actual format of the data returned from the GlideSysAttachment copy function, I did a little rewriting of the processPhase4 function to accommodate the actual structure of the returned data.
processPhase4: function(answer) {
var gsa = new GlideSysAttachment();
var values = gsa.copy('sys_app', answer.appSysId, 'x_11556_col_store_member_application_version', answer.versionId);
gsa.deleteAttachment(answer.attachmentId);
if (values[0]) {
var ids = values[0].split(',');
if (ids[1]) {
answer.attachmentId = ids[1];
} else {
answer = this.processError(answer, 'Unrecognizable response from attachment copy: ' + values);
}
} else {
answer = this.processError(answer, 'Unrecognizable response from attachment copy: ' + values);
}
return answer;
},
While I was at it, I went ahead and did a little work on the processError function to add a few diagnostic breadcrumbs to the system log whenever there is an error. That function now looks like this:
Once I straightened all of that out, everything finally worked as intended. This is the last step in the process, so this essentially completes the code for publishing an application to the Collaboration Store. At this point, I should probably cut another Update Set so that the folks who would like to participate in the testing can take things out for a little test drive. I still need to address the issues with the set-up process uncovered by the last round of testing, so I think I will take that on next time out and then release a new Update Set for those of you who are willing to put things through their paces and report your results. As always, all feedback is very much appreciated.
“It does not matter how slowly you go as long as you do not stop.” — Confucius
Last time we pushed the application record to the Host instance and now we have to do basically the same thing with the version record. The only difference really, other than the table and fields, is that a version record will always be a new record, so there is no need to determine if the record exists or not on the Host instance. This simplifies the code quite a bit. We still need to fetch the GlideRecord that will be sent over, so as we did with the application record, this will be the first order of business.
var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
if (versionGR.get(answer.versionId)) {
...
} else {
answer = this.processError(answer, 'Invalid version record sys_id: ' + answer.versionId);
}
Once we have the record, we can gather up our System Properties and build the payload for the REST API call.
var host = gs.getProperty('x_11556_col_store.host_instance');
var token = gs.getProperty('x_11556_col_store.active_token');
var payload = {};
payload.member_application = answer.hostAppId;
payload.version = versionGR.getDisplayValue('version');
With our payload in hand, we can now create and configure our sn_ws.RESTMessageV2 object.
var request = new sn_ws.RESTMessageV2();
request.setBasicAuth(this.WORKER_ROOT + host, token);
request.setRequestHeader("Accept", "application/json");
request.setHttpMethod('post');
request.setEndpoint('https://' + host + '.service-now.com/api/now/table/x_11556_col_store_member_application_version');
request.setRequestBody(JSON.stringify(payload, null, '\t'));
Now all that is left to do is to execute the request and check the response.
response = request.execute();
if (response.haveError()) {
answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() != 201) {
answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
} else {
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) {
answer.hostVerId = jsonObject.result.sys_id;
}
}
Well, that was easy! Here’s the whole thing all put together.
processPhase6: function(answer) {
var versionGR = new GlideRecord('x_11556_col_store_member_application_version');
if (versionGR.get(answer.versionId)) {
var host = gs.getProperty('x_11556_col_store.host_instance');
var token = gs.getProperty('x_11556_col_store.active_token');
var payload = {};
payload.member_application = answer.hostAppId;
payload.version = versionGR.getDisplayValue('version');
var request = new sn_ws.RESTMessageV2();
request.setBasicAuth(this.WORKER_ROOT + host, token);
request.setRequestHeader("Accept", "application/json");
request.setHttpMethod('post');
request.setEndpoint('https://' + host + '.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()) {
answer = this.processError(answer, 'Error returned from Host instance: ' + response.getErrorCode() + ' - ' + response.getErrorMessage());
} else if (response.getStatusCode() != 201) {
answer = this.processError(answer, 'Invalid HTTP Response Code returned from Host instance: ' + response.getStatusCode());
} else {
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) {
answer.hostVerId = jsonObject.result.sys_id;
}
}
} else {
answer = this.processError(answer, 'Invalid version record sys_id: ' + answer.versionId);
}
return answer;
},
That takes care of 6 of the 7 steps. The last one that we will need to do will be to push the Update SetXMLattachment over to the Host. That one may be a little more involved, so we will save that for next time out.
“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:
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 Setsys_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:
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:
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.
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 SetGlideRecord 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.
“Doubt is an uncomfortable condition, but certainty is a ridiculous one.” — Voltaire
Now that we have the face of our new pop-up window laid out, it’s time to build out the code underneath that will do all of the work. Since this is all server-side activity, we will need some kind of client accessible Script Include that we can call from the client side for each step of the process. Just to do a little testing on the structure and the process, I created a stubbed-out version of a Script Include that I can call from the client-side code to do a little testing before we get too far into the weeds.
var ApplicationPublisher = Class.create();
ApplicationPublisher.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
processPhaseClient: function() {
var phase = this.getParameter('sysparm_phase');
var mbrAppId = this.getParameter('sysparm_mbr_app_id');
var appSysId = this.getParameter('sysparm_app_sys_id');
var updSetId = this.getParameter('sysparm_upd_set_id');
var origAppId = this.getParameter('sysparm_orig_app_id');
return this.processPhase(phase, mbrAppId, appSysId, updSetId, origAppId);
},
processPhase: function(phase, mbrAppId, appSysId, updSetId, origAppId) {
return 'success';
},
type: 'ApplicationPublisher'
});
At this point, all it does is return the value ‘success’ each time that it is called. This will be enough to prove out all of the client-side code before we dig into the actual work on the server side. For now, we just want to prove that the design is functional before we invest too much time in the actual tasks ahead of us.
On the client side, we will want to create a generic function that will work essentially the same for every step of the process. To keep track of where we are, I created a variable called phase, which is basically just the number of the current step. I called this function processPhase.
This is a recursive function that calls itself for every step until all six steps have been completed or there was some kind of an error. If the response from the Ajax call is ‘success’, then the loading icon for that step is hidden and replaced with the success icon, and then the phase is incremented. If we haven’t reached the end of the steps, then the HTML block for the next step is revealed and the process is repeated. If we reach the end of the steps, or if the Ajax call returns anything other than success, then the process comes to an end and the Done button is revealed. There is no effort on the client side to communicate any error or completion message to the operator. The assumption is that all messaging will be handled on the server side.
For the hideElement and showElement functions, I just stole some old code from some other component where I needed to do the same thing. There’s not much exciting here, but just for the sake of including everything, here it is:
function showElement(elementName) {
var elem = gel(elementName);
elem.style.visibility = 'visible';
elem.style.display = '';
}
function hideElement(elementName) {
var elem = gel(elementName);
elem.style.visibility = 'hidden';
elem.style.display = 'none';
}
Even though the processPhase function calls itself once it gets going, something has to initially get the ball rolling to kicks things off. For that, we can use an onload function to initialize all of the values and make that first call.
function onLoad() {
var phase = 1;
var mbrAppId = gel('mbr_app_id').value;
var appSysId = gel('app_sys_id').value;
var updSetId = gel('upd_set_id').value;
var origAppId = gel('mbr_app_id').value;
processPhase(phase, mbrAppId, appSysId, updSetId, origAppId);
}
Although that seemed like a good plan, when I ran my first test run, nothing happened. After doing a little online research, I discovered that the onLoad function is not a natural thing for a UI Page, and if I wanted something to run on load, I needed to add just a bit more code.
addLoadEvent(function() {
onLoad();
});
That was much better! Now after calling the server side six times in succession, the final rendition of the pop-up screen looked like this:
That proves that the client-side process functions as we intended and the stubbed-out Script Include is getting called and is returning the hard-coded ‘success’ value. This should now complete the work on the UI Page itself, so all that is left is to finish out the server-side Script Include to perform all of the tasks itemized in our list of steps. Next time, we can get started on the fist step, which will involve calling the global function that we created earlier to convert the Update Set to XML.
“I believe in a world where all these things can happen, even if I have to do them myself.” — Adrian Lamo
While we wait for the hordes of fellow developers currently running their extensive tests on the initial version of the Collaboration Store app to report their findings, I thought it might be a good time to give a little bit of thought as to where things should go from here. As I mentioned earlier, there are a number of other improvements that I would like to make to the set-up process once we are sure that everything is working as it should. That would definitely be one specific area towards which we could direct our attentions next. On the other hand, I am quite anxious to see if I can actually do some real sharing of artifacts between instances, and I am not quite sure how I am going to do that, so I would like to start focusing on that little project as well. I have managed to cobble together enough parts to get multiple instances aware of one another, so even though the initial set-up process is not quite ready for prime time, maybe it is good enough for now and I can start trying to work towards accomplishing the actual purpose of this application. Besides, we have not received any results from any of those selfless volunteers who have been doing the outside testing as yet, so we really don’t want to disturb that code at this point.
I would like to share all kinds of different artifacts at some point, but to start out, I am going to focus on sharing Scoped Applications in much the same way that applications are shared to internal app stores or the public ServiceNow App Store. There are two sides to the process of sharing: 1) publishing your app to the desired target, and 2) retrieving an app from the desired source and installing it in your instance. Since I can only do one thing at a time, and one obviously has to take place before you can do the other, my initial focus will be to see if I can publish an app to the Host instance. Once we clear that hurdle, then maybe we can see if we can do the flip side and pull something down from the Host and get it installed. But that’s nothing that we need to worry about today, particularly since we don’t even know yet if we can pull off putting something out there!
The first thing that we will need is a couple of tables, one to store the published applications and another to store each version of the application. These are just meta data tables containing information about the app and the versions. I envision that the application itself will be an XML Update Set file, which I plan to attach to the application version table record for each version published. The question is, How do we do that?
There are already built-in capabilities to turn an app into an Update Set and to turn an Update Set into an XML file. Both of those are UI Actions, so taking a peek under the hood of those artifacts would seem like a good place to start. Before you can turn an Update Set into an XML file, you first have to have an Update Set, so let’s first take a peek at the code under the UI Action that does that. The easiest way to do that would be to pull up a Scoped Application and then use the hamburger menu to pull up the list of UI Actions for that form.
The one that we are looking for is called Publish to Update Set… and what we are looking for is the code that is executed when the action is selected.
function publishIt() {
var sysId = gel('sys_uniqueValue').value;
var dialogClass = window.GlideModal ? GlideModal : GlideDialogWindow;
var dd = new dialogClass("publish_app_dialog");
dd.setTitle(new GwtMessage().getMessage('Publish to Update Set'));
dd.setPreference('sysparm_sys_id', sysId);
dd.setWidth(500);
dd.render();
}
Well, that didn’t tell us much. All this code does is launch a dialog box. We will need to look at the UI Page that will appear in the pop-up window, which we can see from the code is called publish_app_dialog. The key element there is the publishApp() function in the Client script of that UI Page.
Basically, this code replaces the original pop-up with a progress bar driven off of the server side publishToUpdateSet function of the com.snc.apps.AppsAjaxProcessor. This is probably something that we still want to do. We just don’t want to be sent to the Update Set form when it all over. Instead, we want to go ahead and turn it into an XML file, and then we want to attach that file to a new version record for the application. Also, if the application has never been published before, we will need to create the master application record as well. But it looks like we can steal most of this code so far. We will need to clone both the UI Action and the UI Page, and then point our cloned action to our cloned page, which is where we will make the majority of the changes. So far, so good.
Now we need to turn the Update Set into XML. For that, we take a peek at the other UI Action for that purpose, found on the Update Set form. To hunt that guy down, we pull up any local Update Set and use the same hamburger menu options to pull up the UI Actions for that form and look for the one called Export to XML. Here is the relevant script:
var updateSetExport = new UpdateSetExport();
var sysid = updateSetExport.exportUpdateSet(current);
action.setRedirectURL("export_update_set.do?sysparm_sys_id=" + sysid + "&sysparm_delete_when_done=true");
The first couple of lines look important, but that last one looks like it pops up the XML file for downloading, which we definitely do not want to do in our adaptation. It turns out that export_update_set is a Processor, which also has code of its own, so we better dig into that guy and see if does anything important that we might need to retain. We can find that guy by entering the word processors in the left-hand navigation menu and then select the lone menu item that appears to bring up the list. Once we have the list, we just need to search for the one where the Path is export_update_set. Here is the relevant code:
(function process(g_request, g_response, g_processor) {
var sysid = g_request.getParameter('sysparm_sys_id');
var remoteUpdateGR = new GlideRecord('sys_remote_update_set');
if (!remoteUpdateGR.get(sysid)) {
gs.addErrorMessage(gs.getMessage('Update Set is invalid'));
return;
}
if (!gs.hasRole('admin') && !sn_app_api.AppStoreAPI.canPublishToUpdateSet(remoteUpdateGR.application)) {
gs.addErrorMessage(gs.getMessage('You do not have permission to perform this operation'));
g_response.setStatus(403);
return;
}
var exporter = new ExportWithRelatedLists('sys_remote_update_set', sysid);
exporter.addRelatedList('sys_update_xml', 'remote_update_set');
exporter.exportRecords(g_response);
var del = g_request.getParameter('sysparm_delete_when_done');
if (del == "true" && remoteUpdateGR.canDelete())
remoteUpdateGR.deleteRecord();
})(g_request, g_response, g_processor);
So it looks like code in which we are interested is the stuff going on with the variable exporter. Unfortunately, there is no output from the exportRecords function of that object and it takes as input the global g_response object. That will send the output to the browser, which we definitely do not want to do. Maybe we can hack it by sending it a fake g_response object, and then pull our output from there. If you right click on the class name (ExportWithRelatedLists), you can use the resulting context menu to pull up the definition and see exactly what it does. There is quite a bit of code in there, but here is the relevant function:
exportRecords: function(response){
this.setHeaders(response);
var outputStream = response.getOutputStream();
this.hd = this.beginExport(outputStream);
var gr = new GlideRecord(this.parent_table);
gr.get(this.sys_id);
this.exportRecord(gr);
this.exportChildren();
this._exportQuerySets();
this.endExport(outputStream);
}
It basically sends everything to the outputStream that it obtains from the response object that you send it. So, we could build our own response object with our own outputStream, use this component right out of the box, and then grab the outputStream from our phony g_response object. Or, we just steal the code from this guy and use it to build our own exporter and have our version of the exportRecords function simply return the XML. Either way, we should be able to leverage all of this code to do what it is that we want to do. That should give us our XML file, anyway.
Of course, generating the XML from the Scoped Application is just the start of the process. Once we have it in hand, we have to create a version record, attach the XML to the version record as a file, and then ship the thing over to the Host instance. Once it arrives at the Host, we will also have to send the version record and the attached XML file out to all of the other instances. That’s all quite a bit of work, but so was the set-up process, and we just trudged through that effort one piece at a time until we got to the end of the to-do list. Hopefully, we can do the same here. We may jump right into that next time, if we don’t yet have any feedback from the set-up testing.
“Don’t worry about people stealing your ideas. If your ideas are any good, you’ll have to ram them down people’s throats.” — Howard Aiken
So I have had this idea percolating in the back of my brain for a while, but other than a few false starts, I haven’t ever done much with it. I have considered a number of different approaches, and have gone back and forth on the best way to tackle one thing or another, but other than mulling things over inside of my head, I have never really attempted to build anything out. I still don’t have a full understanding of how I am going to accomplish certain things, but that’s never really stopped me before from just plowing ahead with what I know and figuring the rest out along the way. Like most things, the most important thing of all is to just get started, and the rest will all work itself out over time. As they used to say in the old Nike commercial: Just Do It!
It’sa pretty simple idea, really. I want to build something like the ServiceNow Store or the developer’s Share site, only for a limited consortium of instances. It might be a collection of PDIs or an industry group or maybe just a couple of independent instances in the same organization, but the idea is to have a way to share stuff amongst a private group that are not otherwise connected in the way that one customer’s multiple instances are connected via their own internal store. I’ve gone back and forth on whether it would be better to do this peer to peer, with no central host, or to designate one of the instances as the master and have all of the others communicating through that one and not directly with each other. There are issues and benefits with each approach, and I really like the idea of having no single instance in charge, but after mulling over all of the various challenges, I think having a host instance would be the easiest approach, so I am going to make my first attempt using that strategy.
At this point, I don’t have all of the details worked out, but I have a rough idea of what I would like to accomplish. The first order of business would seem to be to get the instances talking to one another, which I plan to do using the built-in REST capabilities of the Now Platform. Some of the transactions will have to be built using the Scripted REST API, but hopefully most of them will just use the standard, out-of-the-box capability. My plan is to create a Scoped Application with a set-up process where you will either elect to set up a Host Instance, or provide the instance name of the Host Instance to which you would like to connect. The I am the Host option would be the simplest to code; the other would require communication with the specified host and getting your instance registered with the Host Instance. Also, any time a new instance was registered with the group, all of the other instances should be notified of the new member of group. Things now start to get a little complicated here, and this is just the initial set-up! As I said, I don’t have all of the details worked out just yet, but I do have the basic idea, so I just need to get started and see how things start to come out.
To keep track of the instances in the collective, I will need to set up a table that will contain all of the details for each instance. It shouldn’t be too much data; maybe just the instance name, a display name, a description, and some control data like an active flag or an activation date of some kind. Also, if I want keep track of activities related to each member instance, I might also want an activity log table as well. I like the idea of having that information, but I may save that feature for a later time once I get all of this other stuff worked out the way that it needs to be.
There are a bunch of other considerations such as Roles, ACLs, and web-only service accounts to make all of this work, but again, that’s all in the details. Things should definitely be secured, and I will definitely want to do that, but my usual experience with Security is trying to get around it. It will be interesting to spend a little time on the other side of the fence. But that is not my first priority. Initially, I just want to get something to work. Once we cross that bridge, then I will circle back and make sure that everything is locked down in the way that it should be. I can only deal with one thing at a time, so to kick this thing off, I am just going to try to build out the initial set-up process. That’s complex enough all by itself. And it will probably take a little time, just to get that tuned up the way that I want it to work.
So, that’s the idea, anyway, and today was just about throwing the idea out there for all to see. Next time out, I will start putting the pieces together to see if we can’t turn the idea into a real, functioning scoped application.
“Good order is the foundation of all things.” — Edmund Burke
I’m starting to get quite the collection of various Update Sets from all of the little parts and pieces that I have been playing around with lately, so I decided to try to get a little organized and create a space for them all in the hopes of making it a little easier to hunt down what you might be looking for. You can see the result here. I also added a link to the header menu bar to make it easier to find.
I collected all of the different versions of each little project under a single title, which also serves as a link to the introductory article on the related subject. It’s not in any particular order, so you still might have to do a little nosing around to find anything specific, but at least everything is in one place now, and you can see all of the multiple versions of things together in one spot. It’s not much for style, but it’s functional, so that’s enough effort invested into that little project for now.