Collaboration Store, Part XV

“The beginning is the most important part of the work.”
Plato

With the completion of the last piece of the registration service, the only remaining component of the set-up process is the Client instance function that utilizes the registration service provided by the Host instance. This function will actually be quite similar to the function that we just created to inform one instance about another. This time, we will be invoking the Scripted REST API instead of a stock REST API, but the process is virtually the same.

Before we make the call, however, we need to take care of few little items. First, we need to create the Service Account needed to access the instance, and then we need to grab a couple of our System Properties. We already created a function to establish the Service Account, so all we need to do is to call that function and then grab the two property values.

this.createUpdateWorker(mbrGR.getUniqueValue());
var host = gs.getProperty('x_11556_col_store.host_instance');
var token = gs.getProperty('x_11556_col_store.active_token');

Now we can build the payload from the instance GlideRecord that was passed to the function.

var payload = {};
payload.sys_id = mbrGR.getUniqueValue();
payload.name = mbrGR.getDisplayValue('name');
payload.instance = mbrGR.getDisplayValue('instance');
payload.email = mbrGR.getDisplayValue('email');
payload.description = mbrGR.getDisplayValue('description');

At this point, we can create a new instance of our old friend, the sn_ws.RESTMessageV2 object, and then populate it with all of the relevant information.

var request = new sn_ws.RESTMessageV2();
request.setHttpMethod('post');
request.setBasicAuth(this.WORKER_ROOT + host, token);
request.setRequestHeader("Accept", "application/json");
request.setEndpoint('https://' + host + '.service-now.com/api/x_11556_col_store/v1/register');
request.setRequestBody(JSON.stringify(payload));

… and as we did before, we obtain the response object by invoking the execute method.

var response = request.execute();

Now all we have to do is to populate the result object with the information contained in the response.

result.responseCode = response.getStatusCode();
result.bodyText = response.getBody();
try {
	result.body = JSON.parse(response.getBody());
} catch(e) {
	//
}
if (response.getErrorCode()) {
	result.error = response.getErrorMessage();
	result.errorCode = response.getErrorCode();
} else if (result.responseCode != '202') {
	result.error = 'Invalid HTTP Response Code: ' + result.responseCode;
} else {
	mbrGR.accepted = new GlideDateTime();
	mbrGR.update();
}

The complete function, including the return of the result, looks like this:

registerWithHost: function(mbrGR) {
	var result = {};

	this.createUpdateWorker(mbrGR.getUniqueValue());
	var host = gs.getProperty('x_11556_col_store.host_instance');
	var token = gs.getProperty('x_11556_col_store.active_token');
	var payload = {};
	payload.sys_id = mbrGR.getUniqueValue();
	payload.name = mbrGR.getDisplayValue('name');
	payload.instance = mbrGR.getDisplayValue('instance');
	payload.email = mbrGR.getDisplayValue('email');
	payload.description = mbrGR.getDisplayValue('description');
	var request = new sn_ws.RESTMessageV2();
	request.setHttpMethod('post');
	request.setBasicAuth(this.WORKER_ROOT + host, token);
	request.setRequestHeader("Accept", "application/json");
	request.setEndpoint('https://' + host + '.service-now.com/api/x_11556_col_store/v1/register');
	request.setRequestBody(JSON.stringify(payload));
	var response = request.execute();
	result.responseCode = response.getStatusCode();
	result.bodyText = response.getBody();
	try {
		result.body = JSON.parse(response.getBody());
	} catch(e) {
		//
	}
	if (response.getErrorCode()) {
		result.error = response.getErrorMessage();
		result.errorCode = response.getErrorCode();
	} else if (result.responseCode != '202') {
		result.error = 'Invalid HTTP Response Code: ' + result.responseCode;
	} else {
		mbrGR.accepted = new GlideDateTime();
		mbrGR.update();
	}

	return result;
}

This final function completes the initial set-up process for our new Scoped Application. The application still doesn’t do anything in the way of sharing components between instances, but it’s a start. Next time, we will figure out where we go from here.

Collaboration Store, Part XI

Never give up on a dream just because of the time it will take to accomplish it. The time will pass anyway.”
Earl Nightingale

We have one more function left in our Script Include that was referenced in the set-process function of our widget that still needs to be created. Last time, we completed the function that creates the Service Account, and now we need to build the one that calls the new instance registration process on the Host instance, which also does not exist as yet, either. In fact, before we build the function that uses the service, we should probably create the service first. This will be another Scripted REST API similar to the one that we already created for the Host info service, but this one will be much more complicated.

When we register a new Client instance with the Host, the following things should occur:

  • The new Client instance should be added to the Host database,
  • The new Client instance data should be pushed to the databases of all of the existing instances, and
  • The new Client instance should be updated with a full list of all of the existing instances.

When this process is complete, all instances, including the new instance, should have a full list of every other instance registered with the Host. However, it seems to me that the registration process itself should not have to wait until all of the instances have been made aware of each other. Once we insert the new instance into the Host database, we should be able to respond to the caller that the registration has been completed, and then pushing the details out to all of the instances could be handled off-line in some other async process. But before we worry too much about all of that, let’s get to creating the service.

We need to create another Scripted REST API Resource, but we can tuck it underneath same the Scripted REST API Service that we already created for our earlier info resource. This one we will call register, which will give it the following URI:

/api/x_11556_col_store/v1/register

This time, we will set the HTTP Method to POST, since we will be accepting input from the client instance in JSON format in the body of the request.

/register Scripted REST API Resource

For our Script, we will push the majority of the code over into yet another new function in our Script Include, and then use the results to populate the response.

(function process(request, response) {
	var data = {};
	if (request.body && request.body.data) {
		data = request.body.data;
	}
	var result = new CollaborationStoreUtils().processRegistrationRequest(data);
	response.setBody(result.body);
	response.setStatus(result.status);
})(request, response);

Unlike our earlier /info service, we will want to require authentication for this service (which is why we created the Service Accounts), so under the Security tab, we will check the Requires authentication checkbox.

/register Scripted REST API Resource Security Tab

Under the Content Negotiation tab, we will set both the Supported request formats and the Supported response formats to application/json, as we will expect the caller to send us a JSON string containing the details of their instance, and we will be responding with a JSON string containing the outcome of the registration process.

/register Scripted REST API Resource Content Negotiation Tab

Once we Save the new resource, it should appear in the Related List at the bottom of the service form.

Our first two Scripted REST API Resources

With that out of the way, now we need to turn our attention to building out the non-existent Script Include function that we referenced in the resource’s script. That’s where all of magic will happen, and quite a lot will go on there, so that seems like a good subject for our next installment.

Collaboration Store, Part VI

“To take the ‘easy way’ is to walk a path that doesn’t exist to the edge of a cliff that does.”
Craig D. Lounsbrough

Today we will set the widget aside for a bit and focus on one of the Scripted REST API services, the one needed to verify the Host instance entered by the user. We could use the stock REST API for the new instance table, but that would require authentication and would not give us the opportunity to inject some additional logic beyond just the value of the table fields. The scripted approach is a little more work, but in this case, it is work that needs to be done. Basically, we want to allow unauthenticated users to obtain information about the Host instance directly from the Host instance to verify that it really is a Collaboration Store Host. The format of the JSON string returned would look something like this:

{
    "result": {
        "status": "success",
        "info": {
            "instance": "dev00001",
            "accepted": "2021-07-22 21:59:48",
            "description": "Test Collaboration Store",
            "sys_id": "2be69501076130103457f2218c1ed02b",
            "name": "Test Collaboration Store",
            "email": "storekeeper@example.com"
        }
    }
}

To produce that kind of an output, you need to create a Scripted REST Resource, but before you can build a resource you have to first create a Scripted REST Service, which is the umbrella record under which you can then define multiple Scripted REST Resources. Here is the record for the service that I will be using for all of the resources needed for this app:

Scripted REST Service record for the Collaboration Store app

Normally, the API ID would be something a little descriptive to identify the purpose of the API, but since this is a Scoped Application, the application scope is already part of the URI, so it seemed rather redundant to put something like that in again. Instead, I just set the API ID to V1, indicating that this is Version #1 of this API.

With that out of the way, we can now go down to the Related Records list and use the New button to create our Scripted REST Resource. I called this one info, which also becomes part of the URI for the service, and with the application scope prefix and the APP ID from the parent service, the full URI for this resource becomes:

/api/x_11556_col_store/v1/info

I also set the HTTP Method to GET, since all we are doing is retrieving the information. Here is the basic information for the resource:

Scripted REST Resource record for the Host info API

For the script itself, I decided to place all of the relevant code in the Script Include, and just code a simple function call in the resource script definition itself:

(function process(request, response) {
	var result = new CollaborationStoreUtils().processInfoRequest();
	response.setBody(result.body);
	response.setStatus(result.status);
})(request, response);

Of course, that just means that we have a lot of work to do in the Script Include, but that’s a better place for all of that code, anyway. We already created the empty shell for the Script Include previously, so now we just need to add a new function called processInfoRequest that returns an object that contains a body and a status.

Underneath the script, there is series of tabs, and under the Security tab, you want to also make sure that the Requires authentication checkbox is unchecked. At this point in the set-up process the prospective client does not have any credentials to use for authentication, so we want this info-only service to be open to everyone. There isn’t any sensitive information contained here, so that shouldn’t present any kind of security risk.

As for the Script Include function itself, we will want to verify that this is, in fact, a Host instance, and then go get the details for the instance from the database table. We can easily tell if it is a Host instance by comparing the stock System Property instance_name with the application’s System Property x_11556_col_store.host_instance. If they match, then we can go fetch the record from the database, and if that operation is successful, we can build the response. If we fail to obtain the record for whatever reason, then something has gone horribly wrong with the installation, and we will respond with a generic 500 Internal Server Error. If the two properties do not match, then this is not a Host instance, and in that case, we respond with a 400 Bad Request, which we will call an Invalid instance error. Here is how the whole thing looks in code:

processInfoRequest: function() {
	var result = {body: {error: {}, status: 'failure'}};

	if (gs.getProperty('instance_name') == gs.getProperty('x_11556_col_store.host_instance')) {
		var mbrGR = new GlideRecord('x_11556_col_store_member_organization');
		if (mbrGR.get('instance', gs.getProperty('instance_name'))) {
			result.status = 200;
			delete result.body.error;
			result.body.status = 'success';
			result.body.info = {};
			result.body.info.instance = mbrGR.instance;
			result.body.info.accepted = mbrGR.accepted;
			result.body.info.description = mbrGR.description;
			result.body.info.sys_id = mbrGR.sys_id;
			result.body.info.name = mbrGR.name;
			result.body.info.email = mbrGR.email;
		} else {
			result.status = 500;
			result.body.error.message = 'Internal server error';
			result.body.error.detail = 'There was an error obtaining the requested information.';
		}
	} else {
		result.status = 400;
		result.body.error.message = 'Invalid instance error';
		result.body.error.detail = 'This instance is not a ServiceNow Collaboration Store host.';
	}

	return result;
}

I start out by building the result object with the expectation of failure, and then override that if everything works out. The main reason that I do that is because there are more failure conditions than success conditions, and so that simplifies the code in more places that if I had done it the other way around. That may not be the most efficient way to approach that, but it works.

That wraps up all of parts for providing the service, and since it is just a simple unauthenticated GET, you can even try it out by simply entering the full URL in a browser. Of course, it will come out formatted in XML instead of JSON, but at least you can see the result. This completes the Host instance side of the interface, but to complete our widget, we still need to build the Script Include function that will run on the instance being set up before all of this will work. That may end up being a little bit of work, so that sounds like a good subject for our next installment in this series.

Collaboration Store, Part V

“Do not be embarrassed by your failures; learn from them and start again.”
Richard Branson

With the completion of the client side code, it is now time to turn our attention to a much bigger effort, all of the things that will need to go on over on the server side. This will involve a number of items beyond just the widget itself, but we can start with the widget and branch out from there. One thing that I know I will need for sure is a Script Include to house all of the various common routines, so I built out an empty shell of that, just to get things started.

var CollaborationStoreUtils = Class.create();
CollaborationStoreUtils.prototype = {
    initialize: function() {
    },

	type: 'CollaborationStoreUtils'
};

That’s enough to reference the script in the widget, which we should do right out of the gate, along with gathering up a couple of our application’s properties and checking to make sure that the set-up process hasn’t already been completed:

var csu = new CollaborationStoreUtils();
data.registeredHost = gs.getProperty('x_11556_col_store.host_instance');
data.registeredHostName = gs.getProperty('x_11556_col_store.store_name');
var thisInstance = gs.getProperty('instance_name');
var mbrGR = new GlideRecord('x_11556_col_store_member_organization');
if (mbrGR.get('instance', thisInstance)) {
	data.phase = 3;
}

We get the instance name from a stock system property (instance_name) and then see if we can fetch a record from the database for that instance. If we can, then the set-up process has already been completed, and we advance the phase to 3 to bring up the set-up completion screen. The next thing that we do is check for input, and if there is input, then we grab the data that we need coming in from the client side and check the input.action variable (c.data.action on the client side) to see what it is that we have been asked to do.

if (input) {
	data.registeredHost = gs.getProperty('x_11556_col_store.host_instance');
	data.registeredHostName = gs.getProperty('x_11556_col_store.store_name');
	data.phase = input.phase;
	data.instance_type = input.instance_type;
	data.host_instance_id = input.host_instance_id;
	data.store_name = input.store_name;
	data.instance_name = input.instance_name;
	data.email = input.email;
	data.description = input.description;
	data.validationError = false;
	if (input.action == 'save') {
		// save logic goes here ...
	} else if (input.action == 'setup') {
		// set-up logic goes here ...
	}
}

That is the basic structure of the widget, but of course, the devil is in the details. Since the save process comes before the set-up process, we’ll take that one on first.

If you elected to set up a Host instance, then there is nothing more to do at this point other than to send out the email verification notice and advance the phase to 2 so that we can collect the value of the code that was sent out and entered by the user. However, if you elected to set up a Client instance, then we have a little bit of further work to do before we proceed. For one thing, we need to make sure that you did not specify your own instance name as the host instance, as you cannot be a client of your own host. Assuming that we passed that little check, the next thing that we need to do is to check to see if the host that you specified is, in fact, an actual Collaboration Store host. That will take a bit of REST API work, but for today, we will assume that there is a function in our Script Include that can make that call. To complete the save action, we can also assume that there is another Script Include function that handles the sending out of the Notification, which will allow us to wrap up the save action logic as far as the widget is concerned.

if (data.instance_type == 'client') {
	if (data.host_instance_id == thisInstance) {
		gs.addErrorMessage('You cannot specify your own instance as the host instance');
		data.validationError = true;
	} else {
		var resp = csu.getStoreInfo(data.host_instance_id);
		if (resp.responseCode == '200' && resp.name > '') {
			data.store_name = resp.name;
			data.store_info = resp.storeInfo.result.info;
		} else {
			gs.addErrorMessage(data.host_instance_id + ' is not a valid Collaboration Store instance');
			data.validationError = true;
		}
	}
}
if (!data.validationError) {
	data.oneTimeCode = csu.verifyInstanceEmail(data.email);
}

So now we have referenced two nonexistent Script Include functions to get through this section of code. We should build those out next, just to complete things, but neither one is an independent function. The getStoreInfo function needs to call a REST service, which also doesn’t exist, and the verifyInstanceEmail function needs to trigger a notification, which does not exist at this point, either. We should create those underlying services first, and make sure that they work, and then we can build the Script Include functions that invoke them to finish things up.

That seems like quite a bit of work in and of itself, so this looks like a good place to wrap things up for now. We can jump on that initial web service first thing next time out.