Collaboration Store, Part XIV

“I find that the harder I work, the more luck I seem to have.”
Thomas Jefferson

With the completion of the asynchronous Subflow, we now need to turn our attention to the Script Include function that was referenced in the custom Action built for the repeating step in the Subflow. This function needs to tell each existing instance about the new instance and tell the new instance about all of the existing instances. Both tasks can actually be accomplished with the same code, leveraging the out-of-the-box REST API for ServiceNow tables. Basically, what we will be doing will be to insert a new record into the instance table for both operations, which can be easily handled with the stock API.

Assuming that we are passed a GlideRecord for both the instance to contact and the instance to be shared, the first thing that we will want to do is to create an object containing all of the relevant information about the instance to be shared.

var payload = {};
payload.instance = instanceGR.getDisplayValue('instance');
payload.name = instanceGR.getDisplayValue('name');
payload.description = instanceGR.getDisplayValue('description');
payload.email = instanceGR.getDisplayValue('email');
payload.accepted = instanceGR.getDisplayValue('accepted');
payload.active = instanceGR.getDisplayValue('active');
payload.host = instanceGR.getDisplayValue('host');

Once we have constructed the payload object from the shared instance GlideRecord, we will want to construct the end point URL using the instance name from the target instance GlideRecord.

var url = 'https://' + targetGR.getDisplayValue('instance') + '.service-now.com/api/now/table/x_11556_col_store_member_organization';

Now we need to build a new RESTMessageV2 with the information that we have assembled for this action.

var request = new sn_ws.RESTMessageV2();
request.setEndpoint(url);
request.setHttpMethod('POST');
request.setBasicAuth(this.WORKER_ROOT + targetGR.getDisplayValue('instance'), targetGR.getDisplayValue('token'));
request.setRequestHeader('Content-Type', 'application/json');
request.setRequestHeader('Accept', 'application/json');
request.setRequestBody(JSON.stringify(payload, null, '\t'));

To obtain the response object, we just need to invoke the execute method on the newly created RESTMessageV2 object.

var response = request.execute();

… and now we just have to examine the response to populate the result object.

result.status = response.getStatusCode();
result.body = response.getBody();
if (result.body) {
	try {
		result.obj = JSON.parse(result.body);
	} catch (e) {
		result.parse_error = e.toString();
	}
}
result.error = response.haveError();
if (result.error) {
	result.error_code = response.getErrorCode();
	result.error_message = response.getErrorMessage();
}

Once that has been completed, all that is left is to return the result object back to the caller. All together, the function looks like this:

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

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

	return result;
}

We can now utilize this function in the function called by our custom Action. That function is passed the two instance names, so we will need to fetch the GlideRecords for those instances, and then pass those GlideRecords to the publishInstance function, once to tell the existing instance about the new instance, and then again to tell the new instance about the existing instance.

publishNewInstance: function(new_instance, target_instance) {
	instanceGR = new GlideRecord('x_11556_col_store_member_organization');
	instanceGR.get('instance', new_instance);
	targetGR = new GlideRecord('x_11556_col_store_member_organization');
	targetGR.get('instance', target_instance);
	this.publishInstance(instanceGR, targetGR);
	this.publishInstance(targetGR, instanceGR);
}

That completes all of the parts for the registration service. However, we still need to build the function that calls this Host service from the new Client instance. That will be the subject of our next installment.

Collaboration Store, Part XIII

“First, solve the problem. Then, write the code.”
John Johnson

Today we need to build out the Subflow that we referenced in the Script Include function that we built last time out. To create a new Subflow, open up the Flow Designer, click on the New button to bring up the selection list, and select Subflow from the list.

Creating a new Subflow

When the initial form pops up, all you really need to enter is the name of the Subflow, which we already referred to in our Script Include function as New_Collaboration_Store_Instance.

New Subflow properties form

Once you submit the initial properties form, the new Subflow will appear in the list of Subflows, and from there you can bring it up in full edit mode. In edit mode, we can add the one Input to the Subflow, the name of the new instance.

Subflow Inputs and Outputs

Since we will be launching this Subflow to run on its own in the background, there is no need for any Outputs.

Once we configure the Inputs and Outputs, we can move on to the steps of the Subflow. Our first step will be to gather up all of the records in the table of instances except for two: the Host instance, which has already been updated, and the new instance, which already knows about itself.

Database query step

We select the Look Up Records Action, select the Member Organization table, and then define two Conditions to get the records that we want: 1) the host value is false, and 2) the instance is not the instance that we brought in as Input. It really doesn’t matter for our purposes what sequence the records come in, but I went ahead and sorted the records by instance, just so they we will always work through them in the same order.

Our next step, then is to loop through the records. We do that with a For Each Item Flow Control step, setting the items to the records obtained in the first step.

Looping through the retrieved records

Now we have the basic structure of the Subflow; we just need to perform the tasks necessary to notify each host of the new instance, and notify the new instance of each host within the the For Each Item loop. This could all be done with additional Subflow steps, but I took the easy way out here and just built another function in our Script Include to handle the REST API calls to the instances. In fact, I built two functions, one to make the call, and another to call that function twice, once to tell the existing host about the new host, and then again to inform the new host of the existing host. To run that script, I had to create a custom Action, which is just a simple Script Action that calls the function, passing in the names of the two instances (existing, from the current record, and new, from the Subflow input). Once I built the custom Action, I was able to select it from the list and then configure it.

Custom Script Action step

That completes the Subflow, but once again, we have referenced a function in our Script Include that does not exist, so we will have to get into that in our next installment.

Collaboration Store, Part XII

“The slogan ‘press on’ has solved and always will solve the problems of the human race.”
Calvin Coolidge

In the previous installment in this series, we created a new Scripted REST API Resource and referenced another nonexistent function in our Script Include. Now it is time to create that function, which will perform some of the work required to register a new client instance and then hand off the remaining tasks to an asynchronous Subflow so that the function can return the results without waiting for all of the other instances to be notified of the new client instance. The only thing to be done in the function will be to insert the new client instance into the database and kick off the Subflow. But before we do that, we need to first check to make sure that the Client has not already registered with the Host.

var result = {body: {error: {}, status: 'failure'}};

var mbrGR = new GlideRecord('x_11556_col_store_member_organization');
if (mbrGR.get('instance', data.instance)) {
	result.status = 400;
	result.body.error.message = 'Duplicate registration error';
	result.body.error.detail = 'This instance has already been registered with this store.';
} else {
	...

As we did before, we construct our result object with the expectation of failure, since there are more error conditions than the one successful path through the logic. In the case of an instance that has already been registered, we respond with a 400 Bad Request HTTP Response Code and accompanying error details. If the instance is not already in the database, then we attempt to insert it.

mbrGR.initialize();
mbrGR.name = data.name;
mbrGR.instance = data.instance;
mbrGR.email = data.email;
mbrGR.description = data.description;
mbrGR.token = data.sys_id;
mbrGR.active = true;
mbrGR.host = false;
mbrGR.accepted = new GlideDateTime();
if (mbrGR.insert()) {
	result.status = 202;
	delete result.body.error;
	result.body.info = {};
	result.body.info.message = 'Registration complete';
	result.body.info.detail = 'This instance has been successfully registered with this store.';
	result.body.status = 'success';
	...

If the new record was inserted successfully, then we response with a 202 Accepted HTTP Response Code, indicating that the registration was accepted, but the complete registration process (notifying all of the other instances) is not yet complete. At this point, all we have left to do is to initiate the Subflow to handle the rest of the process. We haven’t built the Subflow just yet, but for the purposes of this exercise, we can just assume that it is out there and then we can build it out later. There a couple of different ways to launch an asynchronous Subflow in the background, the original way, and the newer, preferred method. Both methods use the Scripted Flow API. Here is the way that we used to do this:

sn_fd.FlowAPI.startSubflow('New_Collaboration_Store_Instance', {new_instance: data.instance});

… and here is way that ServiceNow would like you to do it now:

sn_fd.FlowAPI.getRunner()
	.subflow('New_Collaboration_Store_Instance')
	.inBackground()
	.withInputs({new_instance: data.instance})
	.run();

Right now, both methods will work, and I’m still using the older, simpler way, but one day I’m going to need to switch over.

There should never be a problem inserting the new record, but just in case, we make that a conditional, and if for some reason it fails, we respond with a 500 Internal Server Error HTTP Response Code.

result.status = 500;
result.body.error.message = 'Internal server error';
result.body.error.detail = 'There was a problem processing this registration request.';

That’s it for all of the little parts and pieces. Here is the entire function, all put together.

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

	var mbrGR = new GlideRecord('x_11556_col_store_member_organization');
	if (mbrGR.get('instance', data.instance)) {
		result.status = 400;
		result.body.error.message = 'Duplicate registration error';
		result.body.error.detail = 'This instance has already been registered with this store.';
	} else {
		mbrGR.initialize();
		mbrGR.name = data.name;
		mbrGR.instance = data.instance;
		mbrGR.email = data.email;
		mbrGR.description = data.description;
		mbrGR.token = data.sys_id;
		mbrGR.active = true;
		mbrGR.host = false;
		mbrGR.accepted = new GlideDateTime();
		if (mbrGR.insert()) {
			result.status = 202;
			delete result.body.error;
			result.body.info = {};
			result.body.info.message = 'Registration complete';
			result.body.info.detail = 'This instance has been successfully registered with this store.';
			result.body.status = 'success';
			sn_fd.FlowAPI.startSubflow('New_Collaboration_Store_Instance', {new_instance: data.instance});
		} else {
			result.status = 500;
			result.body.error.message = 'Internal server error';
			result.body.error.detail = 'There was a problem processing this registration request.';
		}
	}

	return result;
}

Now we have completed the nonexistent function that was referenced in the REST API Resource, but we have also now referenced a new nonexistent Subflow that we will need to build out before things are complete. That sounds like a good subject for our next installment.

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 X

“It’s better to wait for a productive programmer to become available than it is to wait for the first available programmer to become productive.”
Steve McConnell

With the completion of the set-up widget, we can now turn our attention to the missing Script Include functions and the initial registration process that one of those functions will be calling on the Host instance. Since I always like to start with the easy/familiar stuff first (that checks more things off of the “to do” list faster than the other way around!), let’s jump into the createUpdateWorker() function that inserts or updates the Service Account in the sys_user table. But before we do that, we will need to create a special Role for these worker accounts that has limited privileges.

To create a new Role, navigate to System Security -> Roles and click on the New button to bring up the input form. The only field that we need to fill out is the Suffix, which we will set to worker.

Once we have saved our Role, we will want to bring up the form again and grab the sys_id using the Copy sys_id option on the hamburger menu. We will use that to set the value of one of the constants that we will define at the top of our Script Include.

WORKER_ROOT: 'csworker1.',
WORKER_ROLE: 'f1421a6c2fe430104425fcecf699b6a9',

The other constant is the prefix for the user ID for the Service Account, to which we will append the name of the instance. Now that we have defined our Role and set up our constants, we can build the code that will create the account.

var user_name = this.WORKER_ROOT + gs.getProperty('instance_name');
var userGR = new GlideRecord('sys_user');
if (!userGR.get('user_name', user_name)) {
	userGR.initialize();
	userGR.user_name = user_name;
	userGR.insert();
}
userGR.first_name = 'CS';
userGR.last_name = 'Worker';
userGR.title = 'Collaboration Store Worker';
userGR.active = true;
userGR.locked_out = false;
userGR.web_service_access_only  = true;
userGR.user_password.setDisplayValue(passwd);
userGR.update();

Since it is possible that an earlier failed attempt to set up the app already created the account, we first check for that, and if it isn’t already present in the sys_user table, then we create it. Then we set the appropriate fields to their current values and update the record. One thing to note is the way in which we update the user_password field, which is different than all of the others. Because the value of that field is one-way encrypted, we have to set the Display Value instead of the Value. It took me a bit of research to figure that out; it is not very well documented anywhere that I could find.

Once we create the account, we then have to assign it to the Role that we created earlier. Once again, this may have already been done in an earlier failed attempt, so we have to check for that before proceeding.

var userRoleGR = new GlideRecord('sys_user_has_role');
userRoleGR.addEncodedQuery('user=' + userGR.getUniqueValue() + '^role=' + this.WORKER_ROLE);
userRoleGR.query();
if (!userRoleGR.next()) {
	userRoleGR.initialize();
	userRoleGR.user = userGR.getUniqueValue();
	userRoleGR.role = this.WORKER_ROLE;
	userRoleGR.insert();
}

That takes care of creating/updating the account and applying the Role. Putting it all together, the entire function looks like this:

createUpdateWorker: function(passwd) {
	var user_name = this.WORKER_ROOT + gs.getProperty('instance_name');
	var userGR = new GlideRecord('sys_user');
	if (!userGR.get('user_name', user_name)) {
		userGR.initialize();
		userGR.user_name = user_name;
		userGR.insert();
	}
	userGR.first_name = 'CS';
	userGR.last_name = 'Worker';
	userGR.title = 'Collaboration Store Worker';
	userGR.active = true;
	userGR.locked_out - false;
	userGR.web_service_access_only  = true;
	userGR.user_password.setDisplayValue(passwd);
	userGR.update();

	var userRoleGR = new GlideRecord('sys_user_has_role');
	userRoleGR.addEncodedQuery('user=' + userGR.getUniqueValue() + '^role=' + this.WORKER_ROLE);
	userRoleGR.query();
	if (!userRoleGR.next()) {
		userRoleGR.initialize();
		userRoleGR.user = userGR.getUniqueValue();
		userRoleGR.role = this.WORKER_ROLE;
		userRoleGR.insert();
	}
},

Well, that takes care of the easy part. Next time, we will start digging into the more complex elements of the remaining work needed to complete the set-up process.

Collaboration Store, Part IX

“You can’t go back and make a new start, but you can start right now and make a brand new ending.”
James R. Sherman

Now that we have dealt with the two Script Include functions that were referenced in the Save process, we can return to our widget and address the Set-up process. A number of things have to happen in the Set-up process, including saving the user’s input in the database, creating a service account in the User table for authenticated REST API activities, and in the case of a client instance, we need to register the client with the specified Host instance. We will need to build out the other side of that registration process as well, which will include sharing the new instance information with all of the existing instances as well as sharing all of the existing instances with the new client. That’s quite a bit of work, but we’ll take it all on one piece at a time.

One of the first things that we will want to do, regardless of which type of instance we are setting up, is to save the user’s input. That’s pretty basic GlideRecord stuff, but let’s lay it all out here just the same.

mbrGR.initialize();
mbrGR.active = true;
mbrGR.instance = thisInstance;
mbrGR.name = data.instance_name;
mbrGR.email = data.email;
mbrGR.description = data.description;
mbrGR.name = data.instance_name;
if (data.instance_type == 'host') {
	mbrGR.host = true;
	mbrGR.accepted = new GlideDateTime();
	gs.setProperty('x_11556_col_store.host_instance', thisInstance);
} else {
	mbrGR.host = false;
	gs.setProperty('x_11556_col_store.host_instance', data.host_instance_id);
}
mbrGR.insert();

Most data fields are the same for both Host and Client instances, but a Host instance gets the host field set to true and the accepted date valued, while the Client instance gets the host field set to false and the accepted date is not valued until the registration process is successful.

Now that we have entered the user’s input into the database, we will want to perform additional actions depending on the type of instance. For a Client instance, we will want to register the Client with the Host, and for the purpose of this widget, we can just assume for now that there is a function in our Script Include that handles all of the heavy lifting for that operation.

var resp = csu.registerWithHost(mbrGR);

That simple function call represents a lot of work, but it’s code that we really don’t want cluttering up our widget, so we will stuff it all into the Script Include and worry about building it all out later. We will want to check on how it all came out though, because if it was successful, we will want to update the accepted date for the instance and add the Host instance to our database table as well. Again, this is all pretty standard GlideRecord stuff, so nothing really exciting to see here.

mbrGR.accepted = new GlideDateTime();
mbrGR.update();
mbrGR.initialize();
mbrGR.instance = input.store_info.instance;
mbrGR.accepted = input.store_info.accepted;
mbrGR.description = input.store_info.description;
mbrGR.name = input.store_info.name;
mbrGR.email = input.store_info.email;
mbrGR.active = true;
mbrGR.host = true;
mbrGR.insert();

If the registration process was not successful though, we will want delete the record that we just created, inform the user of the error, and cycle the phase back to 1 to start all over again.

mbrGR.deleteRecord();
var errMsg = resp.error;
if (resp.body && resp.body.result && resp.body.result.error) {
	errMsg = resp.body.result.error.message + ': ' + resp.body.result.error.detail;
}
gs.addErrorMessage(errMsg);
data.validationError = true;

One of the things that will happen during the registration process in the Script Include will be to create the Service Account to be used for authenticated REST API calls to the instance. Since the registration process is only called for Client instances, we will need to handle that directly when setting up a Host instance. Once again, we can assume that there is a Script Include function that handles that process, which greatly simplifies the code in the widget.

csu.createUpdateWorker(mbrGR.getUniqueValue());

Putting it all together, the entire server side Javascript code for the set-up process now looks like this:

function setupProcess() {
	gs.setProperty('x_11556_col_store.store_name', data.store_name);
	mbrGR.initialize();
	mbrGR.active = true;
	mbrGR.instance = thisInstance;
	mbrGR.name = data.instance_name;
	mbrGR.email = data.email;
	mbrGR.description = data.description;
	mbrGR.name = data.instance_name;
	if (data.instance_type == 'host') {
		mbrGR.host = true;
		mbrGR.accepted = new GlideDateTime();
		gs.setProperty('x_11556_col_store.host_instance', thisInstance);
	} else {
		mbrGR.host = false;
		gs.setProperty('x_11556_col_store.host_instance', data.host_instance_id);
	}
	mbrGR.insert();
	if (data.instance_type == 'host') {
		csu.createUpdateWorker(mbrGR.getUniqueValue());
	} else {
		var resp = csu.registerWithHost(mbrGR);
		if (resp.responseCode == '202') {
			mbrGR.accepted = new GlideDateTime();
			mbrGR.update();
			mbrGR.initialize();
			mbrGR.instance = input.store_info.instance;
			mbrGR.accepted = input.store_info.accepted;
			mbrGR.description = input.store_info.description;
			mbrGR.name = input.store_info.name;
			mbrGR.email = input.store_info.email;
			mbrGR.active = true;
			mbrGR.host = true;
			mbrGR.insert();
		} else {
			mbrGR.deleteRecord();
			var errMsg = resp.error;
			if (resp.body && resp.body.result && resp.body.result.error) {
				errMsg = resp.body.result.error.message + ': ' + resp.body.result.error.detail;
			}
			gs.addErrorMessage(errMsg);
			data.validationError = true;
		}
	}		
}

That’s it for the work on the widget itself. Of course, we still have a lot of work to do to both complete the referenced Script Include functions that do not yet exist, and to build out the registration process that one of those functions will be calling. There is a lot to choose from there for the subject of our next installment, but when the time comes, we will pick something and dive in.

Collaboration Store, Part VIII

“Fit the parts together, one into the other, and build your figure like a carpenter builds a house. Everything must be constructed, composed of parts that make a whole.”
Henri Matisse

With the first of the two referenced Script Include methods now completed, we can turn our attention to the second one, which is responsible for sending out the Email Notification containing the email verification code. Before we get into that, though, we should first come up with a way to generate a random code. In Javascript, you can use the Math.random() function to generate a random number between 0 (inclusive), and 1 (exclusive). Coupled with the Math.floor() function and a little multiplication, you can generate a single numeric digit (0-9):

var singleDigit = Math.floor(Math.random() * 10);

So, if we want a random 6-digit numeric code, we can just loop through that process multiple times to build up a 6 character string:

var oneTimeCode = '';
for (var i=0; i<6; i++) {
	oneTimeCode += Math.floor(Math.random() * 10);
}

That was pretty easy. Now that we have the code, what should we do with it? Since our goal is to send out a notification, the easiest thing to do would be to trigger a System Event and include the recipient’s email address and our random code. In order to do that, we must first create the Event. To do that, navigate to System Policy > Events > Registry to bring up the list of existing Events and then click on the New button to bring up the form.

The Event Registry form

For our purposes, the only field that we need to fill out is the Suffix, which we will set to email_verification. That will generate the actual Event name, which will include the Application Scope. It is the full Event name that we will have to reference in our script when we trigger the Event. To do that, we will use the GlideSystem eventQueue() method. And that’s all we have to do other than to return the random code that we generated back to the calling widget. Here is the complete Script Include function:

verifyInstanceEmail: function(email) {
	var oneTimeCode = '';

	for (var i=0; i<6; i++) {
		oneTimeCode += Math.floor(Math.random() * 10);
	}
	gs.eventQueue('x_11556_col_store.email_verification', null, email, oneTimeCode);

	return oneTimeCode;
}

Of course, even though we have completed the missing Script Include function, our work is not complete. We still have to build a Notification that will be triggered by the Event. To create a new Notification, navigate to System Notification -> Email -> Notifications to bring up the list of existing Notifications, and then click on the New button to create a new Notification.

Collaboration Store Email Verification Notification

I named this one the Collaboration Store Email Verification, set the Type to EMAIL, and checked the Active checkbox. Under the When to send tab, we set the Send when value to Event is fired, and in the Event name field, we enter the full name of our new System Event.

Notification form When to send tab

In the Who will receive tab, we check the Exclude delegates, Event parm 1 contains recipient, and Send to event creator checkboxes. That last one is especially important, as it will usually be the person doing the set-up that owns the email address, and if that box is not checked, the email will not be sent.

Notification form Who will receive tab

The last tab to fill out is the What will it contain tab. Here is where we will build the subject and body of the email that will be sent out. We don’t really need all that much here, since all we are trying to do is to get them the code that we generated, but here is the text that I came up for the body:

Here is the security code that you will need to complete the set-up of the Collaboration Store application:

${event.parm2}

That last line pulls the code out of the Event that was fired and places it in the body of the message. For the Subject, I cut and pasted the same text that I had used for the name of the Notification, Collaboration Store Email Verification.

Notification form What will it contain tab

Once you Save the Notification, you can test it out using a generated Event or an actual Event, just to see how it all comes out. To really test everything end to end, you can pull up the Set-up widget and enter in the details for a new instance. The email should then go out if all is working as it should. Just don’t fill the code in on the form just yet, as that would trigger the final set-up process, and we haven’t built that just yet. But we should get started on that, next time out.

Collaboration Store, Part VII

“The best way to predict your future is to create it.”
Abraham Lincoln

Now that we have built a REST end point that will run on a Host instance, we need to complete the function referenced in our widget to utilize the API from our potential client instance. Like many things on the Now Platform, there are a number of ways that this could be accomplished. One way would be to create a new Outbound REST Service using the input form for that purpose.

New Outbound REST Service input form

That will definitely work, but for our simple unauthenticated GET info service, that really is more work than is necessary. Using a new RESTMessageV2 object, you can essentially build the same thing from a blank slate, and of the two properties that we have to value, one of them (the URL or Endpoint) would have to be overwritten anyway, since the instance name portion of the URL is provided by the user’s input. The entire set-up is three simple lines of code:

var request  = new sn_ws.RESTMessageV2();
request.setHttpMethod('get');
request.setEndpoint('https://' + host + '.service-now.com/api/x_11556_col_store/v1/info');

Once you have the completed the set-up, the response object is obtained by calling the execute method:

var response = request.execute();

Because we just built the service that we will be calling, we already know what responses could be coming from the service itself; however, there may be other responses as well if the service is not installed on the instance or if the instance name provided is not a valid instance or if it is a PDI that is currently asleep. Our function should be equipped to handle all of that, so there are several things that we will need to check.

One of the methods on the RESTResponseV2 object is haveError(), which we need to check before anything else. If there some kind of error with the process, then there is no need to check the HTTP Response Code, since we never made it that far. At the very top of our new getStoreInfo() method, I created an empty object for holding all of the information that we will return to the caller:

var respObj = {};

In the event that the haveError() method returns true, we then populate that object with the error details:

if (response.haveError()) {
	respObj.error = response.getErrorMessage();
	respObj.errorCode = response.getErrorCode();
	respObj.body = response.getBody();
}

If there was no error, then the next thing that we want to check is the HTTP Response Code using the response object’s getStatusCode() method. If the code is 200, then we have a valid response. If not, then we are not talking to an active Collaboration Store Host, and we need to populate our response object with that information:

respObj.error = 'Invalid HTTP Response Code: ' + respObj.responseCode;
respObj.body = response.getBody();

If we do receive a 200, though, then we need to parse the JSON string and return all of the information about the Host so that it can be processed by the widget:

respObj.storeInfo = JSON.parse(response.getBody());
respObj.name = respObj.storeInfo.result.info.name;

Putting it all together, the entire function looks like this:

getStoreInfo: function(host) {
	var respObj = {};

	var request  = new sn_ws.RESTMessageV2();
	request.setHttpMethod('get');
	request.setEndpoint('https://' + host + '.service-now.com/api/x_11556_col_store/v1/info');
	var response = request.execute();
	respObj.responseCode = response.getStatusCode();
	if (response.haveError()) {
		respObj.error = response.getErrorMessage();
		respObj.errorCode = response.getErrorCode();
		respObj.body = response.getBody();
	} else if (respObj.responseCode == '200') {
		respObj.storeInfo = JSON.parse(response.getBody());
		respObj.name = respObj.storeInfo.result.info.name;
		gs.setProperty('x_11556_col_store.active_token', respObj.storeInfo.result.info.sys_id);
	} else {
		respObj.error = 'Invalid HTTP Response Code: ' + respObj.responseCode;
		respObj.body = response.getBody();
	}

	return respObj;
}

That completes all of the work for the first Script Include function that we referenced in the server side code of the widget. Next, we’ll get started on that second one that sends out the email verification code to the email address entered on the form.

sn-record-picker Helper, Revisited

“The only thing that should surprise us is that there are still some things that can surprise us.”
Francois de La Rochefoucauld

I’m not generally one to obsess over SEO or site rankings or any kind of traffic analysis, but occasionally, I will poke around and see what comes up on searches, just for my own amusement. For the most part, this whole web site is for my own amusement, and I don’t really expend a lot energy trying to obtain an audience or generate any kind of interest. Mainly, I just like to create stuff or see what I can do, and then share it with anyone who might have a similar interest. So it surprises me quite a bit to search the web for something and find this site listed twice in the top 5 search results and also be the source of the definitive answer to the top question in the People also ask section of the results:

Google search results for sn record picker servicenow

There are quite a number of web sites out there that discuss this topic, so that just seems pretty amazing to me, all things considered. Let’s face it, my little tidbits of amateur hackery are pretty insignificant compared to the massive volumes of information out there dedicated to the ServiceNow product. I guess I should pay more attention, but I just did not expect to see that.

The other question that this brought to my mind was, why the sn-record-picker Helper? I mean, that was well over a year ago, and there are quite a few other areas of ServiceNow explored on this site. That was basically just a short two-part series, not counting the later correction. The sn-record-picker (also written and searched on as snRecordPicker, although that returns different results) is definitely a Service Portal thing, but I have written about a number of other Service Portal elements such as customizing the data table, dynamic breadcrumbs, integrated help, my delegates widget, my content selector and associated configurator, Highcharts, @mentions, a user directory, and my personal favorite, the snh-form-fields. Outside of the Service Portal, you can find other ServiceNow elements here such as the REST API, the Excel Parser, the Flow Designer, Webhooks, System Properties, Event Management, and a host of other just-as-interesting(?) topics. So what was so special about the sn-record-picker? I have no idea.

Still, it is interesting stuff. At least, it was interesting to me. One day I may invest a little more time into figuring out what drives such things, but other than finding it rather amazing when I stumble across it, I don’t see myself investing a whole lot of time on trying to manipulate or manufacture such results. It’s much more fun to me to build stuff and throw out Update Sets for people to pull down and play around with. Plus, if you have to manipulate the results artificially, then your place in the results is more of a reflection of your manipulation skills rather than the value of the content. I could build an article like this one that repeats the words sn-record-picker and snRecordPicker over and over again, just to see what happens, but no one wants to see that. At least, I don’t think so. Personally, I think my time would be much better spent getting back to building stuff, which is what I like to do anyway!

But there is a certain amount of satisfaction in seeing your own site show up on such a list. These things have a way of constantly shifting over time, though, so we’ll see how long it lasts.

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.