Collaboration Store, Part XVI

“It’s hard enough to find an error in your code when you’re looking for it; it’s even harder when you’ve assumed your code is error-free.”
Steve McConnell

Now that we have completed all of the parts for the initial set-up process of our new Scoped Application, it’s time to take a step back and see where things stand. On the one hand, after 16 captivating installments, you would think that we would be much further along in this process beyond just the initial set-up. On the other hand, this is a fairly complex endeavor, and it’s good to get this necessary administrative function out of the way so that we can focus on the actual purpose of the application. But before we jump right into that, we should first have quick look at what we have and what we don’t have at this point.

What we have is an initial version of the set-up process for both the Host instance and the Client instances. Now all of this needs to be fully tested in multiple scenarios, but even if we manage to kill all of the bugs that are undoubtedly baked in there at this stage of the game, as it is written, it assumes for the most part that all will go well every time. What I mean by that is that there isn’t a whole lot of error recovery built into the process right now. Everything seems to work if all of the instances are up and running when contacted. That’s not really good enough for prime time, though, as it is always possible that one or more instances might be unavailable or off-line for some reason. At some point, we will have to build in some processes to monitor for that and to deal with it in some way. Right now, if you fail to get some kind of update from the Host, you just don’t get it. That’s not really good enough in the long run, but my approach is always to get things working first, and then add such features later in a future version. Maybe we will even handle that using Event Management, although not everyone has that feature activated, so maybe that’s not a good plan after all.

There are other features that I would like to add as well. For example, it would be nice if each participating instance had some form of logo or image that would visually identify them and all of the items that they have shared with the community. Things like that are nice-to-haves, though, so again, we’ll deal with that later. At this point, I just want to make sure that what we have put together so far actually works the way that it was intended before we go any further.

I also want all of the menu options hidden until set-up is complete, and then once set-up has been completed, I would like the set-up option to be hidden. I haven’t thrown that in there just yet, either, but that’s something that I don’t want to forget to do once I am sure that everything is working as it should be.

Not too long ago, I had an offer to assist with the testing of this particular project. Normally, I like to do all of my own testing, but they say that programmers are the worst testers of their own code, so I’m going to break with tradition and go ahead and put out an Update Set for this app that is clearly not finished and basically not good for anything of value at this point. If anyone want to participate in this effort, all that I ask is that you post any defects that you uncover to the comments section so that I can see if I can’t get them resolved and put out a new version with the corrections.

So, here’s the deal: gather up your friends and neighbors and come up with some strategy to see who draws the short straw and serves as the Host instance, set up the Host first, and then everyone else jump in and set up their Client instances by referencing the Host. This can work with just two instances, but to see the existing instance updates for any new instance, you will need at least three (one for the Host, one for the new Client, and at least one for an existing Client). Four our more would be even better, but three will at least test all of the current features. When all is said and done, everyone’s list of member instances should match, unless something went terribly wrong along the way. And if you really want to put yourself out there, you can set up a Host instance and put your instance ID in the comments so that other people that you don’t even know can attempt to connect to your instance. Your call.

To install this version of the Collaboration Store (we’ll call it version 0.1), you will need this Update Set, which contains the Scoped Application, and you will also need the latest version of snh-form-fields, which you can find here. Install the form fields Update Set first, and then install the Scoped Application. At that point, you should be good to go and should be able to click on the set-up menu option at any time. I’ll let this sit out here for a while and see if anything comes of it. Thanks in advance for helping a guy out. It’s very much appreciated.

Collaboration Store, Part VI

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

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

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

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

Scripted REST Service record for the Collaboration Store app

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

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

/api/x_11556_col_store/v1/info

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

Scripted REST Resource record for the Host info API

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

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

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

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

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

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

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

	return result;
}

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

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

Collaboration Store, Part IV

“Thinking is easy, acting is difficult, and to put one’s thoughts into action is the most difficult thing in the world.”
Johann Wolfgang von Goethe

Now that we have the visual portion of our widget all laid out the way that we want it, it’s time to crawl under the hood and start laying down some code that will make it all work. The easiest pace to start on that would be client side code. There isn’t that much of it, since the bulk of the action will take place on the server side, and it is all pretty basic standard stuff. For the most part, the client side code is just there to handle the various button clicks and to kick things over to the server side for processing.

The first button click to handle will be the save buttons on the initial data entry screen. There are actually two of them, but only one appears on the screen at any given time, and they invoke the same client side function. Here it is:

$scope.save = function() {
	c.data.phase = 0;
	c.data.action = 'save';
	c.server.update().then(function(response) {
		if (response.oneTimeCode) {
			c.data.oneTimeCode = response.oneTimeCode;
			c.data.phase = 2;
		} else {
			c.data.phase = 1;
		}
	});
};

Before throwing things to the server side for processing, we first set the phase to 0 and the action to save. This will control the logic flow on the server side, where we do all of the actual work. Once the server side code has completed, we check for the presence of a one-time code in the response, which basically tells us that all went well and now we are in the process of verifying the email address. If we don’t get a one-time code, then something went off of the rails, and we kick the phase back to 1 to start the process all over again.

The next button click to handle is the email verification, which we can do right here on the client side, as we are now in possession of the one-time code and we can compare that to the code entered on the screen. If the codes match, the we advance things to the set-up process, and if not, we just leave things as they are until the correct code is entered or the process is cancelled.

$scope.verify = function() {
	if (c.data.security_code == c.data.oneTimeCode) {
		c.data.phase = 0;
		c.data.action = 'setup';
		c.server.update().then(function(response) {
			if (response.validationError) {
				c.data.phase = 1;
			} else {
				c.data.phase = 3;
			}
		});
	}
};

It is still possible at this point for things to go south, so we have to check for that as well, and once again, cycle things back to phase 1 if there is a problem. Otherwise, we advance to phase 3, which simply hides the email verification screen and reveals the set-up completion confirmation screen.

The last thing that we need to handle on the client side is the Cancel button. I just stole this code from an earlier project, and it just returns you to the home page of the main UI if you are in the main UI, or returns you to the home page of the portal, if you are running in the Service Portal.

$scope.cancel = function() {
	var destination = '?';
	if (window.parent.location.href != window.location.href) {
		destination = '/home.do';
	}
	window.location.href = destination;
};

The only other code on the client side is a simple check to see if the server set an initial phase, which can happen if you try to run the set-up process more than once. Here is the entire client side script, with everything included:

api.controller=function($scope) {
	var c = this;

	c.data.phase = c.data.phase || 1;

	$scope.save = function() {
		c.data.phase = 0;
		c.data.action = 'save';
		c.server.update().then(function(response) {
			if (response.oneTimeCode) {
				c.data.oneTimeCode = response.oneTimeCode;
				c.data.phase = 2;
			} else {
				c.data.phase = 1;
			}
		});
	};

	$scope.verify = function() {
		if (c.data.security_code == c.data.oneTimeCode) {
			c.data.phase = 0;
			c.data.action = 'setup';
			c.server.update().then(function(response) {
				if (response.validationError) {
					c.data.phase = 1;
				} else {
					c.data.phase = 3;
				}
			});
		}
	};

	$scope.cancel = function() {
		var destination = '?';
		if (window.parent.location.href != window.location.href) {
			destination = '/home.do';
		}
		window.location.href = destination;
	};
};

That takes care of the easy part. All of the real work is over on the server side, and there is actually a lot that needs to go on over there, and much of it outside of the scope of the widget itself. That is undoubtedly a multi-installment activity, but we will start taking that on piece by piece next time out.

Collaboration Store, Part III

“It is not enough to do your best: you must know what to do, and then do your best.”
W. Edwards Deming

Today we will build the widget for the initial set-up process for the Collaboration Store app. I always like to start with the visual portion and lay things out on the screen the way that I want to see them, but before we get into that, I should explain a little bit about my basic concept for the set-up process. There are actually three independent screens involved: 1) the initial screen where you enter all of the data about your installation, 2) an email verification screen where you enter a code that was sent to your email address to verify your access to the email account, and 3) a final completion screen that lets you know that you are all set up. The HTML for the widget will include all three screens, and I will use ng-show attributes to control which section is visible at any given stage of the process. Within the widget, I will refer to that as the phase, and set up a variable called c.data.phase to track the progress through the screens.

Here is what the initial data entry screen looks like:

Initial set-up data entry screen

… and here is the HTML for that initial (phase 1) screen:

<div class="row" ng-show="c.data.phase == 1">
  <div style="text-align: center;">
    <h3>${Collaboration Store Set-up}</h3>
  </div>
  <div>
    <p>
      Welcome to the Collaboration Store set-up process.
      There are two ways that you can set up the Collaboration Store on your instance:
      1) you can be the Host Instance to which all other instances connect, or
      2) you can connect to an existing Collaboration Store with their permission.
      To become the Host Instance of your own Collaboration Store, select <em>This instance will be
      the Host of the store</em> from the Installation Type choices below.
      If you are not the Host Instance, then you will need to provide the Instance ID of the
      Collaboration Store to which you would like to connect.
    </p>
  </div>
  <form id="form1" name="form1" novalidate>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.instance_type"
          snh-name="instance_type"
          snh-label="Installation Type"
          snh-type="select"
          snh-required="true"
          snh-choices='[{"value":"host", "label":"This instance will be the Host of the store"},
                        {"value":"client", "label":"This instance will connect to an existing store"}]'/>
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.host_instance_id"
          snh-name="host_instance_id"
          snh-label="Host Instance ID"
          snh-required="c.data.instance_type == 'client'"
          ng-show="c.data.instance_type == 'client'"/>
        <snh-form-field
          snh-model="c.data.store_name"
          snh-name="store_name"
          snh-label="Store Name"
          snh-required="c.data.instance_type == 'host'"
          ng-show="c.data.instance_type == 'host'"/>
      </div>
    </div>
    <div class="row">
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.instance_name"
          snh-name="instance_name"
          snh-label="Instance Display Name"
          snh-required="true"/>
      </div>
      <div class="col-xs-12 col-sm-6">
        <snh-form-field
          snh-model="c.data.email"
          snh-name="email"
          snh-label="Email"
          snh-type="email"
          snh-required="true"/>
      </div>
    </div>
    <div class="row">
      <div class="col-sm-12">
        <snh-form-field
          snh-model="c.data.description"
          snh-name="description"
          snh-label="Instance Description"
          snh-type="textarea"
          snh-required="true"/>
      </div>
    </div>
  </form>
  <div class="row">
    <div class="col-sm-12" style="text-align: center;">
      <button class="btn btn-primary" ng-disabled="!(form1.$valid)" ng-show="c.data.instance_type == 'host'" ng-click="save();">${Create New Collaboration Store}</button>
      <button class="btn btn-primary" ng-disabled="!(form1.$valid)" ng-show="c.data.instance_type == 'client'" ng-click="save();">${Complete Set-up and Request Access}</button>
    </div>
  </div>
</div>

Basically, this is just a standard HTML form full of snh-form-fields organized into rows and columns. There are a couple of fields and a couple of buttons that are controlled by the value of that first SELECT, but other than that, it is pretty standard stuff, and there is no reason to get into any of that here.

The screen for the 2nd phase is much simpler, with only a single data entry field used to collect the code that was sent out in a Notification (more on that later) to verify the email address.

Email verification data entry screen

… and here is the HTML for the phase 2 screen:

<div class="row" ng-show="c.data.phase == 2">
  <div style="text-align: center;">
    <h3>${Email Verification}</h3>
  </div>
  <div>
    <p>
      A verification email has been sent to {{c.data.email}} with a one-time security code.
      Please enter the code below to continue.
    </p>
    <p>
      Cancelling this process will terminate the set-up process.
    </p>
  </div>
  <form id="form2" name="form2" novalidate>
    <div class="row">
      <div class="col-sm-12">
        <snh-form-field
          snh-model="c.data.security_code"
          snh-name="security_code"
          snh-label="Security Code"
          snh-required="true"
          placeholder="Enter the security code sent to you via email"/>
      </div>
    </div>
  </form>
  <div class="row">
    <div class="col-sm-12" style="text-align: center;">
      <button class="btn btn-default" ng-disabled="!(form2.$valid)" ng-click="cancel();">${Cancel}</button>
      <button class="btn btn-primary" ng-disabled="!(form2.$valid)" ng-click="verify();">${Submit Verification Code}</button>
    </div>
  </div>
</div>

The screen for the 3rd phase is even simpler, with no data entry fields at all. It is just a message indicating that set-up is complete and was successful.

Set-up completion screen

… and here is the HTML for the phase 3 screen:

<div class="row" ng-show="c.data.phase == 3">
  <div style="text-align: center;">
    <h3>${Set Up Complete!}</h3>
  </div>
  <div>
    <p>${Congratulations!}</p>
    <p>
      The Collaboration Store set-up is now complete. Your instance has been successfully registered with the
      <b class="text-primary">{{c.data.registeredHostName}}</b> ({{c.data.registeredHost}})
      Host and is now ready to utilize the Collaboration Store features.
    </p>
  </div>
</div>

That pretty much takes care of the visual portion of the widget, which is usually the easiest part. Now we have to put all of the code underneath to make everything do all of the things that we want to do during the set-up process. One of the first things that we will want to do is to make sure that the set-up process has not already been completed. We should be able to tell that right off the bat by looking to see if there is a record in the table for the instance. If there is, then things have already been set up, and so we can artificially advance things to phase 3 and put up that final screen in the event that someone tries to go through the process a second time.

Assuming that this is the first time through, though, we will want to validate the data, and assuming that all goes well, we will want to send out the notification with the random code and advance the phase so that we can verify the email address. Once that’s done, we will need to update the database, and in the case of a client instance, we will need to register the instance with the host. To make all of that work, we will need some REST services, and probably a Script Include to contain some code to handle both sides of those inter-instance communications. We are definitely not going to get through all of that in a single installment, but we can take them all on, one issue at a time, as we work our way through all of the parts and pieces that will need to be built. Probably the easiest thing to tackle next would be the client-side code of the widget, so let’s start with that 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.

Collaboration Store

“Don’t worry about people stealing your ideas. If your ideas are any good, you’ll have to ram them down people’s throats.”
Howard Aiken

So I have had this idea percolating in the back of my brain for a while, but other than a few false starts, I haven’t ever done much with it. I have considered a number of different approaches, and have gone back and forth on the best way to tackle one thing or another, but other than mulling things over inside of my head, I have never really attempted to build anything out. I still don’t have a full understanding of how I am going to accomplish certain things, but that’s never really stopped me before from just plowing ahead with what I know and figuring the rest out along the way. Like most things, the most important thing of all is to just get started, and the rest will all work itself out over time. As they used to say in the old Nike commercial: Just Do It!

It’s a pretty simple idea, really. I want to build something like the ServiceNow Store or the developer’s Share site, only for a limited consortium of instances. It might be a collection of PDIs or an industry group or maybe just a couple of independent instances in the same organization, but the idea is to have a way to share stuff amongst a private group that are not otherwise connected in the way that one customer’s multiple instances are connected via their own internal store. I’ve gone back and forth on whether it would be better to do this peer to peer, with no central host, or to designate one of the instances as the master and have all of the others communicating through that one and not directly with each other. There are issues and benefits with each approach, and I really like the idea of having no single instance in charge, but after mulling over all of the various challenges, I think having a host instance would be the easiest approach, so I am going to make my first attempt using that strategy.

At this point, I don’t have all of the details worked out, but I have a rough idea of what I would like to accomplish. The first order of business would seem to be to get the instances talking to one another, which I plan to do using the built-in REST capabilities of the Now Platform. Some of the transactions will have to be built using the Scripted REST API, but hopefully most of them will just use the standard, out-of-the-box capability. My plan is to create a Scoped Application with a set-up process where you will either elect to set up a Host Instance, or provide the instance name of the Host Instance to which you would like to connect. The I am the Host option would be the simplest to code; the other would require communication with the specified host and getting your instance registered with the Host Instance. Also, any time a new instance was registered with the group, all of the other instances should be notified of the new member of group. Things now start to get a little complicated here, and this is just the initial set-up! As I said, I don’t have all of the details worked out just yet, but I do have the basic idea, so I just need to get started and see how things start to come out.

To keep track of the instances in the collective, I will need to set up a table that will contain all of the details for each instance. It shouldn’t be too much data; maybe just the instance name, a display name, a description, and some control data like an active flag or an activation date of some kind. Also, if I want keep track of activities related to each member instance, I might also want an activity log table as well. I like the idea of having that information, but I may save that feature for a later time once I get all of this other stuff worked out the way that it needs to be.

There are a bunch of other considerations such as Roles, ACLs, and web-only service accounts to make all of this work, but again, that’s all in the details. Things should definitely be secured, and I will definitely want to do that, but my usual experience with Security is trying to get around it. It will be interesting to spend a little time on the other side of the fence. But that is not my first priority. Initially, I just want to get something to work. Once we cross that bridge, then I will circle back and make sure that everything is locked down in the way that it should be. I can only deal with one thing at a time, so to kick this thing off, I am just going to try to build out the initial set-up process. That’s complex enough all by itself. And it will probably take a little time, just to get that tuned up the way that I want it to work.

So, that’s the idea, anyway, and today was just about throwing the idea out there for all to see. Next time out, I will start putting the pieces together to see if we can’t turn the idea into a real, functioning scoped application.

Content Selector Configuration Editor, Corrected

“Beware of little expenses; a small leak will sink a great ship.”
Benjamin Franklin

It just seems to be the natural order of things that just when I think I finally wrapped something up and tied a big bow on it, I stumble across yet another fly in the ointment. The other day I was building yet another configuration script using my Content Selector Configuration Editor, but when I tried to use it, I ran into a problem. Since I happened to be working in a Scoped Application, the generated script failed to locate the base class, since that class in the global scope. The problem was this generated line of script:

ScopedAppConfig.prototype = Object.extendsObject(ContentSelectorConfig, {

For a Scoped Application, that line should really be this:

ScopedAppConfig.prototype = Object.extendsObject(global.ContentSelectorConfig, {

It seemed simple enough to fix, but first I had to figure out how to tell if I was in a Scoped App or not. After searching around for a bit, I finally came across this little useful tidbit:

var currentScope = gs.getCurrentApplicationScope();

Once I had a way of figuring out what scope I was in, it was easy enough to change this line:

script += ".prototype = Object.extendsObject(ContentSelectorConfig, {\n";

… to this:

script += ".prototype = Object.extendsObject(";
if (gs.getCurrentApplicationScope() != 'global') {
	script += "global.";			
}
script += "ContentSelectorConfig, {\n";

Once I made the change, I pulled my Scoped Application script up in the Content Selector Configuration Editor, saved it, and tried it again. This time, it worked, which was great. Unfortunately, later on when I went to pull it up in the editor again to make some additional changes, it wasn’t on the drop-down list. It didn’t take long to figure out that the problem was the database table search filter specified in the pick list form field definition:

<snh-form-field
  snh-label="Content Selector Configuration"
  snh-model="c.data.script"
  snh-name="script"
  snh-type="reference"
  snh-help="Select the Content Selector Configuration that you would like to edit."
  snh-change="scriptSelected();"
  placeholder="Choose a Content Selector Configuration"
  table="'sys_script_include'"
  default-query="'active=true^scriptCONTAINSObject.extendsObject(ContentSelectorConfig'"
  display-field="'name'"
  search-fields="'name'"
  value-field="'api_name'"/>

The current filter picks up all of the scripts tht contain “Object.extendsObject(ContentSelectorConfig”, but not those that contained “Object.extendsObject(global.ContentSelectorConfig”. I needed to tweak the filter just a bit to pick up both. I ended up with the following filter value:

active=true^scriptCONTAINSObject.extendsObject(ContentSelectorConfig^ORscriptCONTAINSObject.extendsObject(global.ContentSelectorConfig

… and that took care of that little issue. I’m sure that this will not be the last time that I run into a little issue with this particular product, but for now, here is yet another version stuffed into yet another Update Set. Hopefully, that will be it for the needed corrections for at least a little while!

Fun with Webhooks, Part II

“Well done is better than well said.”
Benjamin Franklin

Now that we have the idea fairly well formulated, it’s time to get to work. The first thing that we need to do is create our Scoped Application, which you can do by navigating to My Company Applications and then clicking on the Create new button in the upper right-hand corner. I still use the classic UI rather than the Studio, so you may approach this task a little differently, but the end result should be the same.

New Simple Webhook Application

At this point, I have just created the most basic of empty shells. There are no modules or tables or roles or any other artifacts … it’s basically just the bare application record itself. Creating the app does create a scope, and also puts you into that scope, so as long as you don’t change that, everything that you do from this point on will be built in that scope. The first thing that we will want to build is the database table for our registry, which we can do by navigating to System Definition -> Tables and clicking on the New button.

New Webhook Registry table

Once you give it a Label it will generate the appropriate name, and the next thing that you are going to want to do is to uncheck the Create module option, as we only want the table at this point and we don’t need all of those other artifacts generated. We want to give our registrations an ID using the platform’s built-in tools, so we will want to create a field labeled Number of type String and put the following in the Default value:

javascript:getNextObjNumberPadded();

We have a number of other columns to define, but let’s save this for now and go set up our auto-numbering before we forget. To do that, navigate to System Definition -> Number Maintenance and click on the New button. Select our new table from the list and then let’s set the Prefix to WHR for Webhook Registry,

Setting up auto-numbering for our new Webhook Registry table

With that out of the way, we can go back to our table and add the rest of the fields. Here are the ones that I added to get things started:

  • Active – True/False, with default of True
  • Table – Table Name, with default of incident
  • Owner – Reference to the sys_user table
  • Type – String, with four initial choices (Single Item, Caller / Requester, Assignment Group, and Assignee)
  • URL – URL
  • Authentication – String, with two initial choices (None and Basic)
  • User Name – String
  • Password – Password
  • Document ID – Document ID
  • Person – Reference to the sys_user table
  • Group – Reference to the sys_user_group table

We may add a few more later on as we add new features, but this will get us by for now. Event though my intent is to focus solely on the Incident table for now, I went ahead and added the Table column and just defaulted it to Incident. That was mainly so that I could use as the Dependent field on the Document ID field. If you have never worked with Document ID fields before, you can just think of them as a special form of Reference field where you don’t have to specify the table that is being referenced. Instead, you point to another field on the record that contains the name of the table. This way, one of your records can reference one table and another record in that same table can reference a different table, all based on the table specified in the Dependent field. To set up the Dependent field, use the Dependent Field tab on the Dictionary Entry for the Document ID field.

Document ID Dependent field specification

After entering all of the fields, I used the Show form link at the bottom of the page to bring up the form for the new table. Using the Configure -> Form Layout option, I rearranged the fields on the screen to my liking, and then removed the Table field completely, as that will default to Incident for now and we don’t want anyone changing it to anything else at this point.

After getting everything laid out just right, the next thing that I did was to add a few UI Policies to control certain fields based on the value in other fields. For example, I hide the User Name and Password fields unless you set the Authentication to Basic. Similarly, the presence of the Document ID, Group, and Person fields are all dependent on the value of the Type field. Basically, this just hides fields that are not needed and reveals them when they are.

The other things that I wanted to do on this form was to give the user the ability to test their URL. That seemed like a good use for a new UI Action, but rather than putting all of the code for that process in the Action itself, I decided to start a Script Include to house these types of utilitarian functions. Putting all of that together seems like a good exercise for our next installment in this series, so this looks like a good stopping point for now.

Fun with Webhooks

“Good ideas are common – what’s uncommon are people who’ll work hard enough to bring them about.”
Ashleigh Brilliant

There is quite a bit of Webhook stuff in various IntegrationHub spokes, but it all seems to be oriented towards consuming incoming events from different external event publishers. I want to actually be the publisher, and send out information based on some preferences selected by the consumer. That may be hidden somewhere in the Now Platform already, but I can’t seem to find it, so I have decided that I would try to develop a Scoped Application to do just that. This may very well be recreating something that already exists in the platform today, but it sounds like a fun exercise, so I am going to give it the old college try.

As always, I will attempt to start out with the most basic of offerings, and then incrementally expand to add more and better features. My approach is to treat this feature as somewhat analogous to a Watch List, in that you sign up to follow certain events, but instead of sending a notification to a User when the event occurs, the result will be that the information is posted to a specified URL. This can apply to any number of things, but to start off, I am going to focus on some very specific changes to one particular table (Incident), and then later expand from there.

To make this work, there will need to be some kind of Webhook Registry where a consumer would sign up to receive these posts. When registering your webhook, you would enter the URL to which you want the data posted along with the specifics of what type or types of events you would like to have included. I’m thinking about linking them directly to an owner, and having some kind of My Webhooks Portal Page where you could manage your existing registrations and add new ones. When adding a new one, you should be able to enter and test your URL, and for our first iteration, that may be the only choice that you get. Later on, we will want to add the ability to choose what you want to follow, which specific updates should trigger a new post, and even what you would like to have included in the payload. But we will also want to start out as simple as possible, so the initial registry may turn out to be quite barren as far as input fields go.

Once registered, there will need to be some process to actually send out the posts as requested in the registration. This could be a Business Rule on the source table, or maybe something created in the Flow Designer. Either way, the process should scan the registry for any condition matches and then send out a post for each match. Each post and response should be logged in some kind of Webhook Activity Log, and any bad HTTP Response Codes should be reported to Event Management. A robust service would attempt to repost any failures up to a certain limit before giving up completely, but all of that can be delegated to some Alert Management Rule at some later time. Again, we will want to start out simple, so our initial focus will just be on making that initial post attempt. Everything else can be pushed off until later on in the process.

Those would seem to be the two major functions: registering the webhook and sending out the posts. We may want some other things at some point, such as the ability to review the logs or to manually repost or to clone an existing registration, but for now, just those two things should get the ball rolling. We may also want to set up a sample receiver for testing purposes, but in practice, the receivers would be other products and outside the scope of this development exercise. There is actually an existing service out on the Internet called Webhook.site that might turn out to be just what I need in order to do a little testing. We should check that out when we get to that point.

For our parts list, then, I can see the need for the following artifacts:

  • A table to hold the webhook registrations,
  • A my_webhooks portal widget to list all webhooks owned by the user,
  • A webhook portal widget for editing a single webhook registration,
  • A Business Rule or Flow to send out the posts,
  • A log table to record the posts and response, and possibly
  • A Script Include to contain some common functions.

Of course, before we create any of that, we will have to create the Scoped Application itself, so that should be where we start next time when we initiate the actual construction phase of this effort.