Collaboration Store, Part XVIII

“Every tester has the heart of a developer … in a jar on their desk.”
Unknown

Last time, we discussed jumping into the code that would allow us to share a local Scoped Application with the Host instance, but the results are starting to come in from some of the folks who have been testing the set-up process, so we should probably deal with those first. Here’s what we have so far:

  • 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

Many thanks to all of those who have been participating in the testing process. Your efforts are much appreciated. For those of you who tried things out, but neglected to post anything, please feel free to leave a comment on your experience, even if you have no defects to report. All feedback is welcome … thanks!

Now let’s take a look at the issues that have been reported so far. one issue at a time.

Installation error: Table ‘sys_hub_action_status_metadata’ does not exist

It looks like the table ‘sys_hub_action_status_metadata’ is a table related to a version or plugin that I have in my instance, but is not present in the instance on which the test installation was being performed. The version of my instance is glide-rome-06-23-2021__patch0-07-07-2021 according to stats.do. The table in question looks relatively new, with a create date of 2021-07-31 16:24:07, and its name implies some sort of metadata, so I don’t think it is anything critical to the operation of the application. If there are no other issues with this installation, my opinion would be that this error could be safely ignored. To make it go away, I could probably just remove any references to this table from the Update Set. That sounds to me like the best way to go, just to avoid the potential of this error popping up, even though it seems as if it is fairly benign.

Not allowing update of property: x_11556_col_store.store_name
Not allowing update of property: x_11556_col_store.host_instance

These two are basically the same problem for two different System Properties. This is an annoyance that really should be corrected somehow. The work-around that was used was to switch over to the application scope, but that should not be necessary. When you are installing an app for the first time, that scope has not even been established yet, so I need to do something to allow these properties to be modified from the global scope, or from any scope for that matter. I’m not exactly sure how to do that, so I will have to do a little research and see what I can come up with. But this is definitely an issue that needs to be addressed.

In the setup, the instance name field doesn’t inform you that you only need the instance prefix, not the full url

This is very true, and should probably be addressed as well. The snh-form-field tag does provide for a “help” attribute, which appears underneath the label, so that’s probably a good place to throw that onto the screen. I’ll make sure that gets added in there before I release the next version.

You can only collaborate with one host

This is also very true, but that’s the way this particular version was conceived. Back when I was thinking of doing something peer-to-peer without anyone designated as the Host, I was leaning more towards that kind of environment, but once I settled on the Host/Client approach, I was always thinking one Host and many Clients. I can see the benefit of being able to connect to more than one Host, but that’s a little more complicated that I was thinking of taking on at this point, so I think I will file that one in the maybe-I-will-do-that-one-day pile. Good idea, though.

All in all, the list so far is not bad, but I assume that there is more to come. It seems like the biggest issue at this point is the cross-scope updates of the application’s System Properties, but the missing table is also something that might give people pause for no reason. Hopefully, I can find a way to address those before I push out the next version.

Thanks again to everyone who took the time to pull this down and give it a whirl, particularly those of you who posted your findings. And if you did not run into any difficulties and were able to get to the point where every instance has the same list of member organizations, please post those results as well, including the number of instances involved. Any feedback is welcome, and as always, much appreciated. Looking forward to hearing more … thank you all!

Next time out, we’ll see if we can get back to building out the application publishing process and maybe start finding out if we can make that work.

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 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.

Collaboration Store, Part II

“Tell me and I forget. Teach me and I remember. Involve me and I learn.”
Benjamin Franklin

Now that I have gone and thrown the idea out there for all to see, it’s time to get to work and see if I can actually pull this off. To begin, I need to set up the Scoped Application, which is basically a repeat of what I went through to set up the Scoped Application for my little Webhooks project, so there is no need to repeat all of that here. Here is how it came out:

Initial Collaboration Store Scoped Application

With that out of the way, the next order of business is to create that first table in which to store all of the instances. Again, that is pretty standard stuff and not really worthy of a step by step walk-through of the process, but here is the associated form, which will give you an idea of the columns that I have selected at this point in the process:

Member Organization table input form

Now we have a place to store the information on the participating instances, so it’s time to build the initial set-up process that will populate this table. Before we dive into that, though, I should mention that when I set up the application, I also set up a few System Properties using the UI Action that I created for that purpose a couple of years ago.

UI Action to set up System Properties for a Scoped Application

That’s been a handy little tool that does a number of things under the hood, but we don’t need to get into all of that here. If you are interested in that for any reason, you can grab an Update Set from here. For this phase of the project, I came up with three properties that I think will be needed in order to do what I would like to do. That may change over time as I get more into the weeds, but for now, here is the list:

Initial System Properties for the Collaboration Store application

That should give us all of the artifacts that we should need to start working on the initial set-up process. As you might have noticed in the above screen shot, I have already created a menu item to launch the initial set-up process from the navigation side bar. Right now, you can also see some of the other menu options for the app, but sometime before things are ready for prime time, my plan is to make all other menu items inactive, and then once the set-up process has been completed, a final step in the process would activate all of the others and inactivate the set-up menu item. For now, though, you can see everything, and it will probably be that way for some time until we get much closer to the end of things.

As for the set-up process itself, there are a number of different ways to go here. I could build something the main UI, where the primary technology is Apache Jelly. I could also build a Service Portal widget, where the primary technology is AngularJS. Both of those are considered Old School at this point, though, and all of the cool kids are now using the Now® Experience UI Framework and the ui-component extension for application development. While that seems like the appropriate way to go, my personal skill set does not yet include mastery of that particular technology, and I don’t really feel like this project would be a good place to address that particular shortcoming in my technical expertise. Since the initial set-up is just one small part of this effort, I am going to take the easy way out and just build a simple widget.

Building a brand new widget starting with a blank canvas is a little bit of a project, though, so that seems like something worthy of its own dedicated installment. Rather than start on that here, let’s take that up next time out.

Fun with Outbound REST Events, Part III

“If it’s a good idea, go ahead and do it. It is much easier to apologize than it is to get permission.”
Rear Admiral Grace Murray Hopper (“Amazing Grace”)

In the last installment in this series, we set up an Outbound REST Message and announced the intention of devoting this installment to the creation of a Script Include that would utilize the new REST message. In testing out that new Script Include, however, I ended up making a few tweaks to that REST message, so we should probably go over those first, just to bring everyone up to speed on what the REST message looks like today.

The first thing that I ended up changing was the name of the address variable. The web service uses the word street for that parameter, but for some reason I was thinking that ServiceNow called the property on the sys_user table address, so I wanted to adopt their name and not the one used by the web service. As it turns out, however, ServiceNow uses the word street as well, so I went into the REST message definition and changed it back to street in the end point URL and in the variable test value list.

The other thing that I ended up doing was adding a completely new variable to both the URL and the variable list called authToken. When we tested the GET method, we were using the auth-id from the web site’s testing tool, and we expected to get the 401 HTTP Response that we received. However, when testing the new Script Include with my own auth-id, I got the same thing. After rooting around in the documentation on the service’s web site, I discovered that server-side calls to the service require both an auth-id and an auth-token, both of which you receive when you sign up for their services. Once I figured that out, I added an auth-token parameter to the URL, added an authToken variable, and added a new System Property to store the value of the auth-token called us.address.service.auth.token.

After doing all of that, before getting back to the Script Include, I went ahead and tested the modified GET method using the Test link on the method form. That actually got me valid results, but they were a little hard to read.

Web Service Test Results

Fortunately, there is a handy little trick that you can use in ServiceNow to clean up that big, long string quite nicely. If you right click on the Response field label and select Configure Dictionary, you will be taken to the Dictionary Entry form for the Response field. Down at the bottom of the form, you will find the Related Lists. Open up the Attributes tab and then click on the New button at the top of the list. This will take you to the Dictionary Attribute form.

Dictionary Attribute form

Select JSON view from the drop-down list or pop-up selection and then type the word true in the Value field. Save the form, which will take you back to the Dictionary Attribute form, and from there you can use the back arrow on the form to return to the test results. Now you should see a new little icon next to the field label, and if you click on it, a new pop-up screen will appear with the contents of the Response field formatted for much easier reading.

Formatted JSON response

That’s a much better way to look at that data. The JSON View attribute is just one of many field attributes available on the platform. When you have some time, it’s a worthwhile exercise to go back into that selection list and take a look at all of the various choices. It’s very easy to try one or two out, just to see what they do. Some of them, like this one, can be quite useful.

Well, we never got around to actually working on the Script Include, but at least we are all caught up on the changes that I made to get to this point. Since the Script Include discussion will undoubtedly consume an entire post all on its own, I think this will be a good place to stop for now. We will tackle that Script Include in the next installment in this series.

Fun with Outbound REST Events, Part II

“The Hacker Way is an approach to building that involves continuous improvement and iteration. Hackers believe that something can always be better, and that nothing is ever complete.”
Mark Zuckerberg

In the first installment of this series, I just laid out my intentions, and didn’t really produce anything of value. With that out of the way, it’s now time to roll up our sleeves and actually get to work. The first component on our itemized list of artifacts to construct is the Outbound REST Message, so we might as well start there. To begin, pull up the list of Outbound REST Messages and then click on the New button to bring up the form. There are only a couple of required fields, the name and end point, but we will also add the optional description and expand the availability to all scopes on the platform.

New Outbound REST Message

Submitting the form will also generate a default GET HTTP method, which is really all that we will need for our purpose. There are other ways to invoke the address service, including an HTTP POST, but the GET will work, so that is all that we will really need to define. Click on the method to pull up the form so that we can add all of our details.

The HTTP GET method for our new Outbound REST Message

I changed the name of the method to simply get, mainly because we have to call the method by name, and I don’t like to type any more than I have to. The only other thing that we need is the end point. There are a couple of different ways that you can do this, including leaving out the end point all together and inheriting the end point from the main REST Message record, but then you have to define all of the URL parameters individually under the HTTP Request tab. It seems easier to me to just cut and paste the entire URL, query parameters and all, right in the end point field and leave it at that. But, the other way works just as well; your mileage may vary.

I use REST Message Variables for all of the query parameter values, which we can then substitute at execution time. I only do that for the ones that change; last episode we decided that we would always set the match parameter to invalid, so there is no need for a variable for that, as it will always be the same. But for the rest, we will want to use variables for the values, so here is the URL that I ended up with:

https://us-street.api.smartystreets.com/street-address?auth-id=${authid}&street=${address}&city=${city}&state=${state}&zipcode=${zip}&match=invalid

Once we save everything, we can test it out right here on the form, but before we do that, we need to provide a test value to all of the variables that we put in the URL. HTTP Method variables are a Related List, which you will find down at the bottom of the form. We can use the New button on that list to add values for all of our variables. For testing purposes, we can just use the same values that we used when we were using the test tool provided by the provider.

Test values for our HTTP GET Method

If you are paying close attention, you will have noticed that the auth-id value that I used is the same as the one provided in the provider’s test tool. That only works for requests that come from that tool. If you try that from anywhere else, you will get 401 HTTP Response Code. To actually use the service, you have to register to obtain your own ID. There is no cost for up to 250 requests per month, but you still have to register. However, even a 401 response is a valid indication that we have reached the service and the service responded, so let’s click that Test button and see what happens.

First test run (failure)

Well, that didn’t turn out so good. Instead of getting the expected 401, we received an HTTP status value of 0. That basically means that it didn’t even try, so let’s take a closer look at that error message:

Error executing REST request: Invalid uri 'https://us-street.api.smartystreets.com/street-address?auth-id=21102174564513388&street=3901 SW 154th Ave&city=Davie&state=FL&zipcode=33331&match=invalid': I

The complaint is Invalid URL, which is undoubtedly related to the embedded spaces in the street parameter, which are not allowed. Apparently, the test tool does not encode the test values for the variables. This, in my opinion, is a shortcoming of the tool, but it can be easily remedied; I will just replace all of the embedded spaces with %20 and give it another go.

Second test run (success)

There’s that 401 that we were looking for! OK, we have now created our Outbound REST Message, configured our HTTP GET Method, set up values for all of our Method Variables, and successfully tested everything that we have built. We can run another test with our real authentication credentials, just to get a real, valid response, but we have done enough to demonstrate that this configuration actually does reach out and interact with our intended target. That’s good enough for now.

Speaking of your real authentication ID, that’s basically your password to the service, and not really something that you want to have floating around in test scripts or any other components in the system for that matter. The best way to stuff that into a corner somewhere is to create a System Property that can contain the value. This will keep the value out of everything else except for the System Properties table. The easiest way to do that is to go over to the Filter navigator and type sys_properties.do (or you can do what the documentation suggests and type sys_properties.list and then click on the New button). This will bring up the System Property form, where you can define your new property. We’ll call ours us.address.service.auth.id.

System property to hold the auth-id for the service

With that out of the way, we can now use a built-in GlideSystem function to obtain the auth-id in our scripts without having to have the actual value embedded in the script itself. Here is a simple example:

var authId = gs.getProperty('us.address.service.auth.id');

This should give us everything that we need to start in on our Script Include, but that’s a fairly large undertaking, so this seems like a good place to wind things up for now. We’ll start right off with the Script Include next time out.

Scoped Application Properties, Part IV

“Any intelligent fool can make things bigger and more complex… It takes a touch of genius – and a lot of courage to move in the opposite direction.”
Albert Einstein

In my haste to wrap things up last week, I neglected to actually create an application-specific System Property and then verify that the convenience link and menu item that we created actually worked as intended. I also neglected to post the Update Set containing all of the artifacts related to this adventure, so I have decided that a fourth installment in this series is warranted. To begin, let’s return to where we last off, which was to push the new Setup Properties button to produce all of the application components necessary to manage properties specific to the application. We can scroll down to the bottom of the page now and see the results:

Sample Application page after clicking on the Setup Properties button

The first thing that you will notice is that the Setup Properties button is now gone. Having completed its reason for living, it can now go away permanently, never to be seen or heard from again until we create our next scoped application. Our other UI Action, the link to the System Properties maintenance page, is not visible either, though. That’s just because we have yet to create our first System Property for this application. Let’s fix that by clicking on that New button under the System Properties tab. And just to show off our Reference type System Properties hack, let’s go ahead and make it a Reference property.

Creating a new application-specific property

To create the new property, we give it a Name and a Description, select reference as the Type and then select the sys_user_group table as the Table. Once we have completed the form, we can now click on the Submit button, which will return us to the original Sample App definition page where we can see the effect of adding the first property to the application

Related Links with at least one System Property defined

Now our new UI Action appears, as there is at least one System Property to manage. Clicking on the link will take us to the page where all of the properties for this application can be valued.

Application Property maintenance page

Of course, we also put this same link on the left-hand side bar menu, so we can type Sample App in the navigation filter to bring that one up to verify as well.

Using the left-hand navigation to open up the Application Properties page

That pretty much verifies that all of the parts and pieces are actually doing their respective jobs. Now all that is left is to leave you with that Update Set.

Scoped Application Properties, Part III

“You may delay, but time will not, and lost time is never found again.”
Benjamin Franklin

I got a little sidetracked last time and never got around to building out the code that we need to automate the set-up of application-level properties, so let’s get right to it. Let’s see, at the push of our new button, we wanted to create a Category, a Role, and a Menu Item. We don’t want to create them if they were already created, though, so first we should check and see if they exist, them create them if they don’t. Everything will be named in accordance with the underlying application, so let’s start out by gathering up all of that application-specific data right at the top:

var gr = new GlideRecord('sys_app');
gr.get('name', appName);
var scope = gr.sys_id;
var prefix = gr.scope;
var menu =  appsgrcope.menu;
gs.addInfoMessage('Scope: ' + scope + '; Prefix: ' + prefix + '; Menu: ' + menu);

The scope, prefix, and menu values obtained in this section will be used in setting up the Category, the Role, and the Module (menu). First let’s do the Category:

var category = new GlideRecord('sys_properties_category');
category.addQuery('name', appName);
category.query();
if (category.next()) {
	gs.info('Category ' + appName + ' already exists.');
} else {
	gs.info('Creating category ' + appName);
	category.initialize();
	category.application = scope;
	category.name = appName;
	category.title = "System Properties for Application " + appName;
	category.insert();
}

There’s not much mystery here: we look for a Category on the sys_properties_category table, and if we don’t find it, then we create. We can take the same approach for the Role:

var role = new GlideRecord('sys_user_role');
role.addQuery('name', prefix + '.admin');
role.query();
if (role.next()) {
	gs.info('Admin role already exists: ' + role.name);
} else {
	gs.info('Creating Admin role ' + prefix + '.admin');
	role.initialize();
	role.sys_scope = scope;
	role.name = prefix + '.admin';
	role.suffix = 'admin';
	role.description = appName + ' Administrators';
	role.insert();
}

Last, but not least is the sidebar menu item. Here we have to check to see if the application has its own menu section, put the item there if it does, and put it with the other System Properties if it doesn’t:

var module = new GlideRecord('sys_app_module');
module.active = true;
module.link_type = "DIRECT";
module.query = "/system_properties_ui.do?sysparm_title=" + encodeURIComponent(appName + " Properties") +"&sysparm_category=" + encodeURIComponent(appName);
module.order = 999;
module.roles = role.name;
if (menu.nil()) {
	gs.info("Adding to System Properties Menu ... no orginal menu");
	module.application = 'd546447bc0a8016900046469895b557a';
	module.sys_name = appName + " Properties";
	module.title = appName + " Properties";
} else {
	gs.info("Adding to Module Menu ... has an established menu");
	module.application = menu;
	module.sys_name = "Application Properties";
	module.title = "Application Properties";
}
module.insert();

That’s pretty much it. We can wrap all of that up into a function in a global Script Include, and then update the UI Action to call that function when the button is clicked. Here is the entire script of the new Script Include:

var AppPropertiesUtils = Class.create();
AppPropertiesUtils.prototype = Object.extendsObject(AbstractAjaxProcessor, {
	
	setUpApplicationProperties: function() {
		var appName = current.getDisplayValue('name');
		gs.info("Setting up system properties for the following application: " + appName );
		
		var gr = new GlideRecord('sys_app');
		gr.get('name', appName);
		var scope = gr.sys_id;
		var prefix = gr.scope;
		var menu =  gr.menu;
		gs.info('Scope: ' + scope + '; Prefix: ' + prefix + '; Menu: ' + menu);
		
		var category = new GlideRecord('sys_properties_category');
		category.addQuery('name', appName);
		category.query();
		if (category.next()) {
			gs.info('Category ' + appName + ' already exists.');
		} else {
			gs.info('Creating category ' + appName);
			category.initialize();
			category.application = scope;
			category.name = appName;
			category.title = "System Properties for Application " + appName;
			category.insert();
			
			var role = new GlideRecord('sys_user_role');
			role.addQuery('name', prefix + '.admin');
			role.query();
			if (role.next()) {
				gs.info('Admin role already exists: ' + role.name);
			} else {
				gs.info('Creating Admin role ' + prefix + '.admin');
				role.initialize();
				role.sys_scope = scope;
				role.name = prefix + '.admin';
				role.suffix = 'admin';
				role.description = appName + ' Administrators';
				role.insert();
			}

			var module = new GlideRecord('sys_app_module');
			module.active = true;
			module.link_type = "DIRECT";
			module.query = "/system_properties_ui.do?sysparm_title=" + encodeURIComponent(appName + " Properties") +"&sysparm_category=" + encodeURIComponent(appName);
			module.order = 999;
			module.roles = role.name;
			
			if (menu.nil()) {
				gs.info("Adding to System Properties Menu ... no orginal menu");
				module.application = 'd546447bc0a8016900046469895b557a';
				module.sys_name = appName + " Properties";
				module.title = appName + " Properties";
			} else {
				gs.info("Adding to Module Menu ... has an established menu");
				module.application = menu;
				module.sys_name = "Application Properties";
				module.title = "Application Properties";
			}
			module.insert();
			
			gs.addInfoMessage("System properties for this application have now been successfully initialized. Use the System Properties Tab at the bottom of the page to add, change, and delete System Properties for this application.");
		}
	},

	type: 'AppPropertiesUtils'
});

… and here is the code that we will add to our UI Action to call the script and then refresh the page:

new AppPropertiesUtils().setUpApplicationProperties();
action.setRedirectURL(current);

Now that we have all of the pieces in place, the only thing left to do is to give it all a try and see if everything works out as we intended. The first thing to do is to pull up an app and push the new button. For testing purposes, I created a useless sample app, just to see if we can’t give this thing a go and see what happens.

Before pushing the button

To test things out, all we have to do is pull up the application and click on the Setup Properties button and then see what happens:

After pushing the button

As you can see from the image above, not only did the message come out indicating that the set-up work has been completed, but the Setup Properties button itself is now no longer on the page, as its work has been done and there is no longer any need for the UI Action. All I need to do now it wrap all of these parts and pieces into an Update Set and post it out here one day for those of you that might want to take a closer look …

Scoped Application Properties, Part II

“Any daily work task that takes 5 minutes will cost over 20 hours a year, or over half of a work week. Even if it takes 20 hours to automate that daily 5 minute task, the automation will break even in a year.”
Breaking into Information Security: Crafting a Custom Career Path to Get the Job You Really Want by Josh More, Anthony J Stieber, & Chris Liu

So far, we have created the UI Action to produce our Setup Properties button, configured the conditions under which the button would appear, and built the Business Rule to ensure that all properties created for an application are placed in a Systems Properties Category of the same name. Before we jump into the business of building out all of the things that we want to automate through the push of that button, there are just a couple of more little odds and ends that we will want to take care of first. One thing that will be helpful in maintaining System Properties for the application will be to have the properties listed out as Related Lists. That’s accomplished fairly easily by selecting Configure -> Related Lists from the hamburger menu on the main Application configuration page:

Configuring Related Lists on the main Application page

This will bring up the Available and Selected Related Lists for an Application, and you just need to find the System Property->Application entry in the Available bucket, highlight it, and then use the right arrow button to push it over into the Selected bucket. Oh, and don’t forget to click on that Save button once you’re done.

Activating the System Properties Related List

One other little thing that would be handy would be a link to the property admin page somewhere on the main Application configuration page. Even though our setup automation will be creating a link to that page somewhere on the main navigation menu, it would still be handy to have a link to that same page right here where we are configuring the app. The format of the URL for that link in both instances is the address of the page, plus a couple of parameters, one for the page title and the other for the name of the Category:

/system_properties_ui.do?sysparm_title=<appName> Properties&sysparm_category=<appName>

To build a link to that page on the main Application configuration page can be easily accomplished with another UI Action similar to the first UI Action that we built for our button, but this time we will select the Form link checkbox instead of the Form button checkbox. The code itself is just constructing the URL and then navigating to that location:

function openPage() {
	var appname = document.getElementById('sys_app.name').value;
	var url = '/system_properties_ui.do?sysparm_title=' + encodeURIComponent(appname) + '%20Properties&sysparm_category=' + encodeURIComponent(appname);
	window.location.href = url;
}

There may be more elegant ways to do that, but this works, so that’s good enough for now. This link should show after the Application Properties have been configured, so the display rules are basically opposite of those for our button: the button will show until you use it and the link will only show after you use the button. We can pretty much steal the condition from other UI Action, then, and just flip the last portion that checks for the presence of the Category:

gs.getCurrentApplicationId() == current.sys_id && (new GlideRecord('sys_properties_category').get('name',current.name)) && (new GlideRecord('sys_properties_category_m2m').get('category.name',current.name))

Those of you who are paying close attention will notice that we also add yet one more check to our condition, and that was just to make sure that there was at least one System Property defined for the app. There is no point in bringing up the property value maintenance page if there aren’t any properties. Assuming that you have set up application properties and you have defined at least one property, you should see the new link appear down at the bottom of the main Application configuration page:

New link to the property value maintenance page

That should take care of all of those other little odds and ends … now all that is left is to build out the code that will handle all of those initial setup tasks. Last time, I said that we would take care of that here, but as it turns out, that that was a lie. We’ll have to take care of that next time.

Scoped Application Properties

“I have been impressed with the urgency of doing. Knowing is not enough; we must apply. Being willing is not enough; we must do.”
Leonardo da Vinci

Many times when I build a scoped application in ServiceNow I will end up creating one or more System Properties that are specific to the application. To allow application administrators to maintain the values for those properties, I will usually create a menu item for that purpose, either under the application’s menu section if there is one, or under the the general Systems Properties menu item if there is not. To set all of that up requires a certain amount of work, and being a person who likes to avoid work whenever possible, I decided that it might be worth looking into possible ways to automate all of that work so that I could accomplish all of those tasks with minimal effort. It’s not that I’m afraid of work — I can lay right down next to a huge pile of work and sleep like a babe — it’s just that if there is a way to automate something to save myself some of that time and effort, I’m all in.

So what exactly is it that I seem to keep doing over and over again whenever I start out on a new app? I guess the main thing is the menu option that brings up the System Properties related to the application. If the application happens to have a menu section of its own, then I usually add this down at the bottom of that section and label it Application Properties. For applications that do not have a menu section of their own, I will use the name of the application (plus the word Properties) for the label, and stick it in the existing System Properties section of the menu. Regardless of where the menu item lives, it will point to the stock ServiceNow System Properties value entry page, which is not driven by Scope, but by Category. In order to bring up just the properties for this particular application, I also need to create a Category, usually using the name of the application as the category name, and then put all of application’s properties in that Category. Of course, the easiest way to put all of an application’s properties into a specific category is to create a Business Rule that does just that whenever a new property is created. Now, you don’t want just anyone to be tinkering with these important properties, so yet another thing that I always end up having to create is a Role that can be used to secure access to the properties.

None of these things are complicated, but it all takes time, and the time adds up. If you could do it all with just the push of a button, that would be pretty sweet. So what would we need to do that? Well, right off the bat, I guess the first thing that you would need would be that button. Let’s create a UI Action tied to the application form. It doesn’t have to do anything at this point; we just want to get the button out there to start things off.

UI Action to Set Up Application Property Support

Open up the New UI Action form by clicking on the New button at the top of the UI Action list, and enter Setup Properties in both the Name and Action Name fields. Check the Form button checkbox and the Active checkbox and that should be enough to save the record and create the UI Action. Pull up any Scoped Application at this point, and you should see the new button at the top of the screen.

Setup Properties button on Custom Application page

You really only want to see that button on apps that haven’t already been set up for application-level properties. Once you push that button and everything gets set up, the button should go away and never be seen or heard from again. Pushing the button will initiate a number of different things, so there will be a number of potential things that you could check to see if it’s work has already been accomplished. One of the easiest is probably the presence of a System Properties Category with the same name as the application. To configure the UI Action to only appear if such a category does not yet exist, you can add this line to the Condition property of the UI Action:

gs.getCurrentApplicationId() == current.sys_id && !(new GlideRecord('sys_properties_category').get('name',current.name))

The above line also ensures that you are in the right scope to be editing the app, as you wouldn’t want anyone setting everything up under the wrong scope. So now we have the button and we have it set up so that it only shows up when it is needed. We also have to figure what, exactly, is going to happen when we click on the button, but that’s a little more complicated, so let’s just set that aside for now. A considerably easier task would be to create that Business Rule that makes sure that all System Properties created for this app get placed into the Category of the same name. That one is pretty straightforward.

(function executeRule(current, previous) {
	var sc = new GlideRecord('sys_scope');
	sc.get(current.sys_scope);  
	if (sc.name != "Global" && sc.name != "global") {
		var category = new GlideRecord('sys_properties_category');
		if (category.get('name', sc.name)) {
			var prop = new GlideRecord('sys_properties');
			prop.addQuery('sys_scope', current.sys_scope);
			prop.orderBy('suffix');
			prop.query();
			while (prop.next()) {
			    var propertyCategory = new GlideRecord('sys_properties_category_m2m');	
				propertyCategory.addQuery('property', prop.sys_id);
				propertyCategory.addQuery('category', category.sys_id);
				propertyCategory.query();
				if (!propertyCategory.next()) {
					propertyCategory.initialize();
					propertyCategory.category = category.sys_id;
					propertyCategory.application = current.sys_scope;
					propertyCategory.order = 100;
					propertyCategory.property = prop.sys_id;
					propertyCategory.insert();	
				}
			}
		}
	}
})(current, previous);

This is a fairly simple script that implements fairly simple rules: get the scope of the current property, make sure it isn’t global, see if there is a Category on file with the same name as the Scope, and if so, put the current property into that category. That’s it. Once this is active, whenever you create a non-global System Property, it will automatically be assigned to the Category created for that application.

This is probably a good place to stop for now. Next time out, we will get into the details of just what happens when you push that new button we created.