Skip navigation

Developer Community

5 Posts authored by: Cory Seering Employee

In my previous blog post Scoped Applications and Client Scripts: A Primer  I point out some of the differences between the APIs available to a scoped client script and a global one. One of the APIs that is explicitly removed is client-side GlideRecord. There are a few reasons that access to this particular API has been removed. A few of the top reasons are:

  • It results in large data transfers
  • Its almost exclusively used to get the value of a single field
  • Almost everyone uses it synchronously
  • It can hold open a database connection

 

I wrote another post about Using utilities in Scoped Applications to replace inaccessible APIs . The example in that case was replacing an auto-generated encoded query string which used an inaccessible API directly within the query. We can expand on that concept, this time on the client rather than the server.

 

UI Script as a utility class

Keeping within the spirit laid out in Background and Philosophy of Scoped Applications  we want a lightweight, asynchronous robust method for making REST calls. Also, we don't want to be limited to just the table APIs. We will create a single UI Script in this post, but this would work with multiple scripts as well.

 

Our client-side scripts (such as onChange and onLoad client scripts) can access the ScriptLoader class. This is an asynchronous JavaScript library that is used for just-in-time loading UI scripts. Thanks to the default structure of our UI Scripts, we can load a UI Script once and access it from multiple places. Additionally, the ScriptLoader knows what it has loaded already, so we can safely ask it to load our utilities multiple times and be sure that it will do so only once.

 

Understanding the UI Script template

To start with, we need a UI Script. When we create a UI Script in our application, we get a template that we can build on. A new UI Script would look like this:

Screen Shot 2016-05-25 at 1.13.08 PM.PNG

 

There are a lot of comments in the code, but it looks a lot more complicated than it is. Before we actually write our UI Script, let's break down what this template is telling us.

 

var sn_ui_script_util = sn_ui_script_util || {};

 

This line creates a variable named sn_ui_script_util, and sets its value to itself, or to an empty object. This is code that runs on the browser, so this is saying If we already have an object in the page named after our scope, we're going to use that. Otherwise, let's create an empty object.

 

sn_ui_script_util.Utilities = (function() {
  "use strict";

 

These two lines add a "Utilities" property to our scope object, and set its value to the result of a function execution. The function itself must run in strict mode. This prevents us from accidentally creating global variables and gives us errors when we make mistakes.

 

/* set your private variables and functions here. For example: 
  var privateVar = 0; 
  function private_function() {
  return ++privateVar;
  }
*/


/* Share variables between multiple UI scripts by adding them to your scope object. For example: 
  sn_ui_script_util.sharedVar = 0; 


 Then access them in your scripts the same way. For example: 
  function get_shared() {
  return sn_ui_script_util.sharedVar;
  }
*/

 

There are three different but connected things going on in the lines above. Firstly, we create a private variable that can only be accessed within this UI Script, and function that changes that variable. Nothing outside of the code written in this UI Script can read the value of that private variable, and nothing can change it.

 

Second, we create another variable, but this one is public. It exists on our scoped object, but it can be accessed by outside scripts. They can read and write to this shared variable, but it's not polluting the global namespace.

 

Lastly, we define a function that will return a reference to our shared variable. This is nothing new or groundbreaking, and since other scripts can access out variable directly, it's not strictly necessary. However, it is impolite to trample someone else's variables and properties, so if they give you a function to access them, you should use that function.

 

    return {
        /* set your public API here. For example:
        incrementAndReturnPrivateVar: function() {
            return private_function();
        },
        */
        type:  "Utilities"
    };

 

 

Here, we are defining the public API. In this example, we are saying that our API is one function named "incrementAndReturnProvateVar". Scripts can call this method and they will increment the value of the private variable we declared earlier, and see the result. We could also add our get_shared function if we wanted to.

 

})();

Finally, we just close out our function definition, and execute it.

 

Creating the utility

So, with that framework, we can create our REST utility. I wrote this today and haven't put a lot of testing into it, but it should work as an example. Note that, while I'm happy to accept fixes and comments about things which might not work with it, I am not offering to support it. This is a demonstration, not a full-fledged solution.

 

var sn_ui_script_util = sn_ui_script_util || {};

sn_ui_script_util.Utilities = (function () {
    "use strict";

    function xhrSupportsJSON() {

        if (typeof XMLHttpRequest == 'undefined') {
            return false;
        }

        var xhr = new XMLHttpRequest();
        xhr.open('get', '/', true);

        try {
            // some browsers throw when setting `responseType` to an unsupported value
            xhr.responseType = 'json';
        } catch (error) {
            return false;
        }

        return 'response' in xhr && xhr.responseType == 'json';
    }

    function getXHR() {
        var xhr = typeof XMLHttpRequest != 'undefined' ?
            new XMLHttpRequest() :
            new ActiveXObject('Microsoft.XMLHTTP');
        return xhr;
    }

    function getParamString(params, q) {
        if (!q)
            q = '';
        else
            q = '?';

        var parts = [];
        for (var key in params) {
            if (params.hasOwnProperty(key))
                parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
        }
        return parts.length ? q + parts.join('&') : '';
    }

    function urlIsRelative(url) {
        return !url.startsWith('http');
    }

    return {

        rest: function (type) {
            var params = {};
            var headers = {};
            var parts = [];
            var successHandler = function (data) {
                try {
                    console.log("Success!");
                    console.log(data);
                } catch (e) {
                    //boo
                }
            };

            var errorHandler = function (data) {
                try {
                    console.log("Error!");
                    console.log(data);
                } catch (e) {
                    //boo
                }
            };

            var obj = {
                get: function (url) {
                    if (!url)
                        errorHandler();

                    url = url + getParamString(params, true);

                    if (urlIsRelative(url))
                        headers['X-UserToken'] = g_ck;

                    var xhr = getXHR();
                    var useJSON = type == 'json' && xhrSupportsJSON();
                    if (useJSON)
                        xhr.responseType = 'json';

                    xhr.open('get', url, true);
                    for (var hdr in headers)
                        if (headers.hasOwnProperty(hdr))
                            xhr.setRequestHeader(hdr, headers[hdr]);

                    xhr.onreadystatechange = function () {
                        var status;
                        var data;

                        if (xhr.readyState == 4) { // `DONE`
                            status = xhr.status;

                            if (status == 200) {
                                data = useJSON ? xhr.response : type == 'json' ? JSON.parse(xhr.responseText) : xhr.responseXML;
                                successHandler && successHandler(data);
                            } else {
                                errorHandler && errorHandler(status);
                            }
                        }
                    };

                    xhr.send();
                },

                post: function (url) {
                    if (!url)
                        errorHandler();

                    if (urlIsRelative(url))
                        headers['X-UserToken'] = g_ck;

                    var xhr = getXHR();
                    var useJSON = type == 'json' && xhrSupportsJSON();
                    if (useJSON)
                        xhr.responseType = 'json';

                    xhr.open('POST', url, true);
                    for (var hdr in headers)
                        if (headers.hasOwnProperty(hdr))
                            xhr.setRequestHeader(hdr, headers[hdr]);

                    xhr.onreadystatechange = function () {
                        var status;
                        var data;
                        if (xhr.readyState == 4) { // `DONE`
                            status = xhr.status;

                            if (status == 200) {
                                data = useJSON ? xhr.response : type == 'json' ? JSON.parse(xhr.responseText) : xhr.responseXML;
                                successHandler && successHandler(data);
                            } else {
                                errorHandler && errorHandler(status);
                            }
                        }
                    };

                    xhr.send(getParamString(params));
                },

                addParam: function (name, value) {
                    params[name] = value;
                },

                addHeader: function (name, value) {
                    headers[name] = value;
                },

                success: function (successCallback) {
                    successHandler = successCallback;
                },

                error: function (errorCallback) {
                    errorHandler = errorCallback;
                }
            }

            return obj;
        },
        type: "Utilities"
    };
})();

 

What?

OK, so that is a lot of code. But you should be able to break it down, using the explanation provided earlier. This is a UI Script with some private functions and variables, which returns a public API that has one method named 'rest'. When we call that, we get an object that allows us to make GET and POST requests, and to accept either XML or JSON. All the rest is just "the stuff that makes that work".

 

Using our Utility

Now that we have a utility defined, we want to make use of it. We have a fairly generic way to make REST requests, and we want to replace at least some of the functionality of client-side GlideRecord. Let's use a common example: get the value of a single field off a referenced record. Let's get the Department of an incident caller, and add that to the work notes of an incident. We'll do that onChange of the Caller field.

 

Screen Shot 2016-05-25 at 3.29.33 PM.PNG

Here is the code we are using:

function onChange(control, oldValue, newValue, isLoading, isTemplate) {
    if (isLoading || newValue === '') {
        return;
    }
    ScriptLoader.getScripts('sn_ui_script_util.Utilities.jsdbx', getDepartment);

    function getDepartment() {
        var req = sn_ui_script_util.Utilities.rest('json');
        req.addParam("sysparm_query", "sys_id=" + newValue);
        req.addParam("sysparm_fields", "department");
        req.addParam("sysparm_display_value", true);
        req.success(updateNotes);

        req.get("/api/now/table/sys_user");
    }

    function updateNotes(data) {
        g_form.setValue("work_notes", data.result[0].department.display_value);
    }
}

 

We have the ScriptLoader fetching our utility script for us, and then calling our function to get the value of the caller's department. When that comes back, we'll update the worknotes field.

You can combine multiple UI Scripts under one namespace (your scope object on the client-side), and multiple scripts can call the ScriptLoader to load them. Think of it like saying "I require the REST utility, so if it isn't already loaded, go get it."

 

This is just one example of a way you can write your own utilities which are scope-safe, asynchronous, and take advantage of the power of ServiceNow's built-in REST API's. There is nothing stopping you from calling your own script REST endpoints, too. And with scoped apps and dependencies, you can keep all these nifty scripts and tools in one application, and simply depend on them for your other apps. It's a very handy way to write-once, use-everywhere.

I have strange and mystical powers.

In November, I wrote a blog post called Background and Philosophy of Scoped Applications. It recently generated an interesting discussion about the limits of the available APIs for scoped apps, with focus on one specific function: gs.dateGenerate. This post will hopefully help you figure out how to work within the available APIs to achieve the same goals in a scoped application that you could achieve in a global one.

 

The scoped API

The scoped API is mostly a subset of the existing Glide API that we all know and love. Both the legacy and the scoped API set are documented on our ServiceNow Developers  site. Since the scoped API is essentially a subset of the legacy API, there will necessarily be some classes and functions which are not available. One such method is "gs.dateGenerate". The scoped API includes GlideDateTime and GlideDuration, and is capable of doing all of the same calculations that gs.dateGenerate can do, but we have to do a little bit of work to make that happen.

 

Utility classes

I'm a fan of writing utility classes. I like making tools, probably more than I like making things with those tools. A utility class is typically the first thing I start with when I make a new scoped application. It starts out empty, and I add to it as I find myself needed the same generic functions multiple times during the course of development. When I find myself using the same things across multiple applications, I'll pull those out into a new application, and make my app depend on that. I don't want to reinvent the wheel every time I need to make a wagon.

 

Creating the Utility

The most basic utility class is really just an object. This is a good pattern to use, because it prevents you from having to new it every time you need to use it. For our example, we're going to create an object with a single function. We'll give it a short name so it's easy to use, and we won't be using the default template it auto-generates for us.

Screen Shot 2016-04-15 at 11.38.49 AM.PNG

I named my include "Tools" and marked it "Client callable" and "Accessible from: All application scopes". Client-callable enables us to use this include in URLs, which is important for our use-case. Making it available to other application scopes means that I am committing to making this an API point for my application. I won't introduce breaking changes without prior notification to my potential users, and the code I am providing should be side-effect-free. You don't need to make your tools available to other scopes unless you are writing an application specifically to provide a toolset to other applications.

 

I've also changed the Script. By default the Script Include form auto-generates a template for our include based on the anme we provided it. The template looked like this:

var Tools = Class.create();
Tools.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
    type: 'Tools'
});

 

I've decided that this particular set of tools doesn't need to maintain state internally, so there is no reason to use Class.create(). If multiple people want to work with the methods on this object, they can do so without colliding. That's why we're starting with an empty object:

var Tools = {};

 

The first function we want to add is a replacement for gs.dateGenerate. It helps to know that the original dateGenerate function is really just a convenience method that works on GlideDateTime objects. If we were to convert the function from Java to JavaScript, it would look like this:

gs.dateGenerate = function(date, range) {
   var gdt = new GlideDateTime();
   if (range.equals("start"))
      gdt.setDisplayValueInternal(date + " 00:00:00");
   else if (range.equals("end"))
      gdt.setDisplayValueInternal(date + " 23:59:59");
   else
      gdt.setDisplayValueInternal(date + " " + range);

   return gdt.getValue();
}

 

That is pretty simple. We'll copy that nearly verbatim into our Tools include, since every object type it access and every option it does is supported within the scoped API. When we copy that into our include, it looks like this:

var Tools = {};


Tools.dateGenerate = function(date, range) {
    if (!range)
        range = '12:00:00';
    
    var gdt = new GlideDateTime();
    if (range.equals("start"))
        gdt.setDisplayValueInternal(date + " 00:00:00");
    else if (range.equals("end"))
        gdt.setDisplayValueInternal(date + " 23:59:59");
    else
        gdt.setDisplayValueInternal(date + " " + range);
    
    return gdt.getValue();
}

 

We added a default value for the range. The original Java function doesn't supply a default value and returns an empty result if you call it that way:

var generatedDate = gs.dateGenerate('2016-04-14');
gs.print(generatedDate);

 

[0:00:00.001] Script completed in scope global: script


*** Script: 

If we left out the default, we would have the same result. That's up to you- it's your utility.

 

Using the utility

Now that we have our replacement for dateGenerate, we want to make use of it. There is one part of the system that relies on dateGenerate pretty heavily: encoded queries. If you open the Incident list and filter between two dates you can see this happen:

Screen Shot 2016-04-15 at 2.02.46 PM.PNG

The encoded query being run on this list is:

opened_atBETWEENjavascript:gs.dateGenerate('2016-02-01','12:00:00')@javascript:gs.dateGenerate('2016-03-31','12:00:00')

 

If we attempted to run that same query in a script in our application, we would get an error:

var enq = "opened_atBETWEENjavascript:gs.dateGenerate('2016-02-01','12:00:00')@javascript:gs.dateGenerate('2016-03-31','12:00:00')";
var inc = new GlideRecord('incident');
inc.addEncodedQuery(enq);
inc.query();

gs.info(inc.getRowCount());

[0:00:00.248] Script completed in scope sn_custom_app: script

Security restricted: Read operation against 'incident' from scope 'sn_custom_app' was granted and added to 'sn_custom_app' cross-scope privileges

Evaluator: com.glide.script.fencing.MethodNotAllowedException: Function dateGenerate is not allowed in scope sn_custom_app

   Caused by error in script at line 1

 

 

==>   1: var enq = "opened_atBETWEENjavascript:gs.dateGenerate('2016-02-01','12:00:00')@javascript:gs.dateGenerate('2016-03-31','12:00:00')";

      2: var inc = new GlideRecord('incident');

      3: inc.addEncodedQuery(enq);

      4: inc.query();

 

 

Evaluator: com.glide.script.fencing.MethodNotAllowedException: Function dateGenerate is not allowed in scope sn_custom_app

   Caused by error in script at line 4

 

 

      1: var enq = "opened_atBETWEENjavascript:gs.dateGenerate('2016-02-01','12:00:00')@javascript:gs.dateGenerate('2016-03-31','12:00:00')";

      2: var inc = new GlideRecord('incident');

      3: inc.addEncodedQuery(enq);

==>   4: inc.query();

      5:

      6: gs.info(inc.getRowCount());

 

 

Background message, type:error, message: Function dateGenerate is not allowed in scope sn_custom_app

Evaluator: com.glide.script.fencing.MethodNotAllowedException: Function dateGenerate is not allowed in scope sn_custom_app

   Caused by error in script at line 1

 

 

==>   1: var enq = "opened_atBETWEENjavascript:gs.dateGenerate('2016-02-01','12:00:00')@javascript:gs.dateGenerate('2016-03-31','12:00:00')";

      2: var inc = new GlideRecord('incident');

      3: inc.addEncodedQuery(enq);

      4: inc.query();

 

 

Evaluator: com.glide.script.fencing.MethodNotAllowedException: Function dateGenerate is not allowed in scope sn_custom_app

   Caused by error in script at line 4

 

 

      1: var enq = "opened_atBETWEENjavascript:gs.dateGenerate('2016-02-01','12:00:00')@javascript:gs.dateGenerate('2016-03-31','12:00:00')";

      2: var inc = new GlideRecord('incident');

      3: inc.addEncodedQuery(enq);

==>   4: inc.query();

      5:

      6: gs.info(inc.getRowCount());

 

 

Background message, type:error, message: Function dateGenerate is not allowed in scope sn_custom_app

sn_custom_app: 0

 

But when we substitute in our dateGenerate function, the query returns the right value:

var enq = "opened_atBETWEENjavascript:Tools.dateGenerate('2016-02-01','12:00:00')@javascript:Tools.dateGenerate('2016-03-31','12:00:00')";
var inc = new GlideRecord('incident');
inc.addEncodedQuery(enq);
inc.query();

gs.info(inc.getRowCount());

 

[0:00:00.005] Script completed in scope sn_custom_app: script


*** Script: 8

 

Manually substituting "Tools" for "gs" in encoded queries is fine, but we can make this a bit more user-friendly. We can write a second utility function to do it for us:

var Tools = {};

Tools.dateGenerate = function(date, range) {
    if (!range)
        range = '12:00:00';

    var gdt = new GlideDateTime();
    if (range.equals("start"))
        gdt.setDisplayValueInternal(date + " 00:00:00");
    else if (range.equals("end"))
        gdt.setDisplayValueInternal(date + " 23:59:59");
    else
        gdt.setDisplayValueInternal(date + " " + range);

    return gdt;
}


Tools.fixFilter = function(filterString) {
    if (!filterString)
        return filterString;

    //lets ensure we are working on a string:
    filterString = filterString + '';

    //fix dateGenerate
    var dateGenerateRegex = /gs\.dateGenerate/gm;
    var ourDateFunction = "Tools.dateGenerate";

    return filterString.replace(dateGenerateRegex, ourDateFunction);
}

 

Then we can use our fixFilter utility on our encoded query:

var enq = "opened_atBETWEENjavascript:gs.dateGenerate('2016-02-01','12:00:00')@javascript:gs.dateGenerate('2016-03-31','12:00:00')";
var inc = new GlideRecord('incident');
inc.addEncodedQuery(Tools.fixFilter(enq));
inc.query();

gs.info(inc.getRowCount());

[0:00:00.004] Script completed in scope sn_custom_app: script


sn_custom_app: 8

 

And because we made this open to all application scopes, other applications can take advantage of them too:

var enq = "opened_atBETWEENjavascript:gs.dateGenerate('2016-02-01','12:00:00')@javascript:gs.dateGenerate('2016-03-31','12:00:00')";
var inc = new GlideRecord('incident');
var Tools = sn_custom_app.Tools;

inc.addEncodedQuery(Tools.fixFilter(enq));
inc.query();

gs.info(inc.getRowCount());

[0:00:00.004] Script completed in scope sn_other_app: script


sn_other_app: 8

 

We are expanding the scoped API with each release, and gs.dateGenerate should be natively available in the feature release that comes after Helsinki. Until then, a utility script like the one above can provide the same functionality.

 

I hope this has been a useful primer on Utility scripts. They are a great way to provide reusable, encapsulated code for all of your applications.

I have strange and mystical powers.

Scoped Applications are a fun topic for me. I was able to contribute during the development of the Scoped App model, and since then I've written a lot of small Scoped Apps personally, as well as contributed to the Studio- a Scoped Application that's like an IDE for building Scoped Applications (read more about that here).

 

What is a scope?

Generally speaking, a scope is like a namespace for your application. Everything in your application falls under this namespace. It separates your classes, tables, and UI components from everyone else's; it gives you control over their names, how they can be accessed or extended by others, and keeps your code from accidentally polluting the global namespace. A good discussion of these benefits can be found here.

 

Technically speaking, scopes are a bit more complex than that. A scope isn't just a namespace, it's also a record in a table (sys_scope). All of the files that are part of your application have a reference to this record- or to one of the tables which extend it (sys_app and sys_store_app). That's not the true root (sys_package is) but it's as top-level as you ever need to go.

 

All files in your Scoped Application will have a reference to your sys_scope record. That's because every record in your application will be in a table that extends sys_metadata, or will be a table definition (a sys_db_object record and/or sys_dictionary record) which itself extends sys_metadata. In the sys_metadata table, there is a reference field to sys_scope, and this ties your whole app together.

 

The global scope is also a scope- you can check that out too. It's a sys_scope record, where the scope name is 'global'.

Screen Shot 2015-11-18 at 10.12.49 AM.PNG

 

Everything created pre-Fuji is pretty much a part of the same legacy globally-scoped application. Most of the legacy stuff defaults to being open to all app scopes to use. It's also possible to create new Scoped Applications where the scope is 'global'. I don't really see much of a reason to do that, though.

 

When checking things like cross-scope access protections, global is "just another scope". If you create a Script Include in your Scoped App that is "Accessible from: This application scope only", then a script running in global cannot access it. Only scripts in your same scope will be able to make use of it, which means you can make changes in each release without worrying that you might be breaking something else that depends on it. Of course- it cuts both ways. If you make the include accessible to other scopes- like you would do for an API, or some common utility script that you think will be useful, then you should be just as careful as ever about making breaking changes.

 

Screen Shot 2015-11-18 at 10.16.24 AM.PNG

 

 

Differences between Scoped Apps and Global Apps

Update Sets

Global Apps are exclusively tracked in Update Sets. When you want to start work on a new release, you have to create a new, clean Update Set, switch into it, and be meticulous about switching out of it when making non-relevant updates, and switching back into it again when you're done. You have to close the set, transfer it, preview it, fix any problems, and apply it.

 

Scoped Apps can be tracked in Update Sets- they can even be moved around via Update Set, but that's a really legacy way of doing it. We automatically create an Update Set for each scoped app, and switch into it whenever you start editing that app. It's generally something you can ignore. Global Apps use Update Sets as a system of record for changes to an instance, and for moving a series of changes between instances. Scoped Apps are always complete- they represent an "end state" instead of "a series of changes to make". It's not impossible to do the same thing with Update Sets, but anyone who's had to go through every single record they believe constitutes their application and "touch" them to get one complete Update Set out of it knows how annoying that can be. I know it- I've done a few times, and I consider it the nuclear option for fixing "my change isn't coming across".

 

Scoped Apps are packaged up complete, with all relevant records to install on a new instance that has never seen the app before, or update an existing instance where an old version is installed. Even if you choose to Publish to an Update Set, the entire app will be copied in, not just the changes you've made since the last time you published. When you publish to the repo (either via "Make App available on other instances" or "Publish to Store"), the app is packaged up, stored on the repo, and (potentially) made available to all of your organizations instances to download and install or upgrade. Published apps always represent an end-state, not a series of changes to be made.

 

Install and Uninstall

Installing a scoped app is generally as easy as going to the Applications page, switching to the Downloads tab, and clicking the Install button. It's an actual installation, versus applying an Update Set, which is how a legacy application is installed.

 

Screen Shot 2015-11-18 at 10.30.01 AM.PNG

 

Global applications can't really be "uninstalled". Since nothing ties your application together into one cohesive app, it's difficult to tell where your "loaner approval" ends and CMDB and Approvals begin. You can back out Update Sets in reverse order, until you've removed the one the one that brought in your changes, but that's neither fun nor easy. You could also comb through all of the references in your Update Sets to find the individual files, or trace code paths and refer to documentation, but that's harder than it should be. Scoped Apps are uninstallable. You can do it right from the sys_store_app record for that app. I think it's two clicks and a confirmation (you have to manually type "uninstall"). The "everything is packaged together" aspect makes it *vastly* easier to install and uninstall.

 

Screen Shot 2015-11-18 at 10.35.22 AM.PNG

 

Public and Private

Global Apps live together, but not always harmoniously. When everything is a public API, nothing is reliable, or everything must be static. If you write a Script Include with 10 useful methods that get used in a hundred different places, changes to those methods can have far-reaching and unintended consequences. You had to do extensive searching to see if any of the server-side script fields that could potentially access that include actually do, and if your change will adversely affect them (not to mention Filters, default dictionary values, forgotten scheduled jobs, etc).

 

Scoped Apps allow you to set a Script Include private- truly private, accessible only to your application. You can make a public, static API (or one that that at least changes infrequently) while updating the behind-the-scenes private stuff with weekly releases. You can also name it anything you like- a scoped Script Include is automatically namespaced to your scope. Your JSONUtil include can't conflict with some else's, nor with a global one. You can split your utilities from the rest of your code and update them separately.

Screen Shot 2015-11-18 at 10.32.17 AM.PNG

 

It's possible to get most of the benefits of Scoped Apps with the legacy global model- you just have to try hard. It's not easy, and you have to be meticulous. The legacy system promotes interdependence; Scoped Apps promote interfacing. In general, I think we'll all find that route leads to cleaner code, easier updates, and less frequent behavioral anomalies (my new favorite term for 'bugs').

 

Philosophy of Scoped Apps

This is my understanding of the philosophy of Scoped Apps. I had a small part in the platform side, and I don't speak for everyone, but here's how I see it.

 

  • Do not break system functionality.
  • Do not break other apps.
  • Do not hog resources.
  • Be self-contained, with clear dependencies where necessary.
  • It's easier to start locked-down and gradually open up than the reverse.

   

Almost everything we've done with Scoped Apps is to address or encourage those points. For example, you cannot make a field on someone else's table disabled or read-only. If they have marked the table extendable, then you can extend it and make all the inherited fields read-only if you wish. That is one way we try to ensure that apps don't interfere with each other. Another example: GlideAjax calls from scoped Client Scripts must be asynchronous. A popular table like Incident is likely to attract many Scoped Applications, adding Client Scripts and validations and nice conveniences. Since you (the app developer) aren't necessarily aware of the exact environment your Client Script is running in- especially with regard to other Scoped Apps installed alongside yours- it's one way we can make sure that the user isn't stuck waiting for multiple round-trip GlideAjax calls to complete when they attempt to submit a form. In the legacy system, the administrator would need to keep tabs on that.

 

By adhering to the above principles, multiple Scoped Apps, from different vendors who may never have even known about each other, can coexist on the same system. You can release as frequently as you like, and so long as you follow best practices around you public-facing interfaces, you can be much more sure that your release can't break someone else's app, nor their releases break yours.

 

That last point is important too- it's easier to open something that used to be closed than it is to close something that used to be open. We spent a lot of time and effort on determining which APIs are exposed to Scoped Apps, and which ones aren't. We fully expect to continue adding to the APIs that are available over time. If we left out something near and dear to you, let us know.

 

Choosing the right model

Almost everything that you want to build can be done with Scoped Apps. However, sometimes it is more convenient or appropriate to use the legacy model. If you require access to an API that isn't available to Scoped Applications, then you may need to use the legacy model. If your processes are dependent on moving Update Sets around, the legacy model might be right for you. If you need to change the core functionality of a legacy application, then you need to do it in the legacy model.

 

When you are creating new functionality, or extending an existing application, then a Scoped App is probably the best choice.

 

Upcoming features

In Geneva, we're releasing the Studio. There's already been a couple of posts about it, and it's honestly the thing I am most excited about. I contributed heavily there, and I I hope we get a lot of feedback about to make it better in Helsinki and beyond. It also has an awesome Code Search tool which has made my life much easier these past several months. In Helsinki, we hope to have some more advanced collaborative development tools, as well as some advancements in the design-time and run-time permission model. Also, Code Search will be broken out into a separate, extendable Scoped App, accessible via a REST API. I wrote this, and I look forward to seeing what you do with it.

 

We aren't done iterating on the Scoped App model. As it gets wider adoption and the feedback comes pouring in, expect the experience to continually improve.

I have strange and mystical powers.

Let's talk about my favorite part of the Community: questions. I love reading the myriad questions posted here, chiming in when I think I know the answer, and seeing the care and attention so many people are putting into helping others.

 

Sometimes though... sometimes I see a question and my initial response is:

 

Huh?

 

huh.jpg

 

Some questions are framed poorly; others lack the context required to solve the problem. And some questions are asked in such a way that all the words individually make sense, but when put in that specific order end up being meaningless. And that sucks, because every question is a request for help, and helping each other is one of the core foundations of any community. If you structure your questions in the best possible way, you'll get the best possible answer.

 

But why listen to me?

 

I know a thing or two about a thing or two.

 

Before I was a developer at ServiceNow, I was on the Support team. Before that, I held several technical support and web development roles at other companies. Altogether I've been fixing problems and helping others fix problems for about 15 years. I know what I need to see in a problem description to quickly understand the context of the question and the likely places to look for a solution- which sometimes means addressing it in a different way than the question was framed. This month is my 4-year anniversary at ServiceNow, and in that time I've answered hundreds of Incidents, pushed thousands of lines of code, and generally tried my best to make sure that every question that comes across my desk has been answered fully.

 

 

What makes a question easier to answer?

 

When I can look at new questions on the Community, I look for questions that have purpose, context, and clarity. If your question displays those three qualities, I know that you've already done the basic research and it's unlikely the answer can be found with a simple Wiki search. It's interesting and therefore likely to draw attention. It also has everything I need to get started on a solution.

 

What is Purpose?

 

Purpose is the problem you are trying to solve. It's the use-case or business case for the question.  Purpose tells people what the end result is going to be, not necessarily the specific difficulty you are having. You should be able to define why you are doing the project- why are you updating this Business Rule, why you're writing this Script Include, or why it's important that this field on your form have a Red highlight for Itil users but a Green highlight for their managers. In some cases knowing the purpose can help us determine if the solution you are struggling with is the best way to accomplish your goal. perhaps you've spent the better part of a day wrestling with the login in an after Business Rule when the best way to solve your issue is with an event. For some examples, look at just about any question posed by rfedoruk :

 

Content created by rfedoruk

Screen Shot 2015-10-05 at 2.13.57 PM.PNG

 

Every question includes purpose. There is a problem to be solved, an attempted approach and finally a specific issue with this approach that is yet to be overcome.

 

 

What is context?

 

When you describe the issue you are having, it's almost always necessary to know where this is occurring. Let's say you're having an issue with a specific script. You must, at the very least, post the problem you have and the content of the script in question:

 

The answer from my Script Include is always null when my script runs.

 

var ga = new GlideAjax("x_emp.DataGenericUtil");
ga.addParam("sysparm_name","doStuff");
ga.addParam("sysparm_input", g_form.getValue("u_foo"));

ga.getXML(myCallback);



 

With this example, we may be able to determine that the "foo" field on the table is probably not supposed to be prefixed with "u_", since this is likely running on a custom table in the app scope "x_emp". Likewise, we could probably say that the "x_emp" prefix on the script include name is not needed here. However, those would be guesses- and pretty poor ones if this is actually a Client Script running on the global table "Foobars" which really does have a field named "u_foo" on it, and it's calling a scoped Script Include called "DataGenericUtil" in the scope "x_emp", even though the script itself is in the global scope.

 

To get help with this script, we'd need to also include the content of the DataGenericUtil Script Include's doStuff function:

 

    doStuff : function() {
        var input = this.getParameter("sysparm_input");
        gs.info("Parameter sent was {0}", input);

        return "plonk";
    }



 

We also need to know if the script was marked Client Callable, if it's accessible from All application scopes or This application scope only, and if it's currently marked Active.

 

We need to know about the table this runs on:

  • Does it have a u_foo field?
  • What kind of data does it have?
    • Is it a String field or a Date field?
    • Does it have instance-specific formatting?
  • What scope is the table in?
  • Does the field have data at the time the script runs?

Speaking of our script, we need more information about that as well:

  • What kind of script is it?
    • Client Script?
    • UI Script?
    • Does it run onLoad, onChange, or onSubmit?
      • If it's onChange, what field does it react to?
  • What scope is the script in?
  • What is the content of the myCallback function?

 

That's not to say that we want to see everything. Include enough code so that someone can reproduce the problem- or set up a minimal test-case of your own on one of our demo instances, and point people to it. We want to be able to examine the relevant code and run the program- in our heads if it's small enough, or on a live system exhibiting the problem if it's really complicated.

 

Finally we want to know if any errors are thrown. This script runs client-side, but it also invokes a server-side script to do some processing. Errors on either side can cause unexpected results. We want to know if you are seeing errors in your browsers console, if there are any errors in your System Logs, and if there is anything that stands out around the transaction in your node log. Errors thrown by your code are designed to be descriptive. A good error message tells you what went wrong, where it went wrong, and (usually by context) how to fix it.

 

That seems like a lot of work for one measly script question, but that's only because you don't need to be brought up to speed on the whole problem- you're already in the middle of it. For the rest of us, the more relevant information you provide, the less time we spend trying to catch up from the back, the quicker we can join you in the middle, and the quicker we can push forward to the solution.

 

 

What is clarity?

 

Some problems in technology are truly hard. One of them is being able to describe a problem with a system that you are intimately familiar with, to a person who isn't familiar with it. There are at least 2 potential language barriers: are you both able to express yourselves in the same language, and do you both understand the same jargon? And there are differing levels of ability- some people have been troubleshooting issues for years, and other people just started learning the platform.

 

When you describe your problem, you want to make sure that the person reading it comes away with a good idea of exactly where this issue occurs. Write in complete sentences. Separate multiple questions into a list, or into multiple paragraphs. Add screenshots to the text near where you reference them. Don't abbreviate unnecessarily. If you don't know the jargon for something, that's OK; if you can describe it well enough, name it, and then refer to it that way consistently then can still communicate about it.

 

Writing in complete sentences, capitalization and punctuation are important. Humanity has been communicating ideas via the written word for millennia. These conventions exist because they are useful. They improve the presentation of your ideas, and they make consumption of them faster. You don't have to be an English major with a Ph.D. in the classics to get your point across- but if you are, think Around the World in 80 Days not Finnegans Wake.

 

Your question title is the first thing people when see when they look at the posts on the Community. Do you think it accurately describes the question you have? Does it sound like an interesting problem to solve? Here are some examples of good and bad problem titles:

 

Bad: script value null

Good: Answer from Script Include is null, only when called from scoped Client Script

 

Bad: Javascript error

Good: What is causing "ScopedGlideAjaxGenerator is undefined" error when loading Incident on tablet interface?

 

Bad: Business Rule doesn't work

Good: gs.eventQueue does not insert events when called from Before business rule on Task table

 

 

Bringing it all together

 

So with the script problem I invented earlier, a post like the one below is the most likely to get accurate help quickly. As a bonus, just by writing the problem out and thinking through it logically, you might even come to a resolution before you even submit!

 

 

Answer attribute is null in GlideAjax response to scoped Script Include

 

I am creating a scoped custom application on my instance, which adds a Client Script to a table outside of my scope. This table is part of a legacy system I have inherited, and I haven't been able to recreate it and merge it into my scoped application yet. The client script in question is an onChange script, that fires on changes to the u_bar field, and sends the value of u_foo to the server. I expect to get the result "plonk" back in my answer attribute on the GlideAjax response, but it is just a null value. The u_foo field contains the number of people who have agreed to attend an event (it's an integer field) and u_bar is the location of the pub we are going to (a reference field to cmn_location). Every time we change pubs, the number of people in u_foo changes, and we want to track that in real time so we know the pub most likely to boost attendance.

 

This is my onChange script:

onChange(newValue,oldValue,isLoading) {
    if (isLoading)
        return;

    console.log("New bar value: " + newValue);
    console.log("Foo value: " + g_form.getValue("u_foo"));

    var ga = new GlideAjax("DataGenericUtil");
    ga.addParam("sysparm_name","doStuff");
    ga.addParam("sysparm_input", g_form.getValue("u_foo"));
    ga.getXML(myCallback);

    function myCallback(response) {
        var answer = response.responseXML.documentElement.getAttribute("answer");
        console.log("Response from server is: " + answer);
    }
}




 

My Script Include is pretty basic. I took an existing include which was doing the work server-side and made it client-callable:

var DataGenericUtil = Class.create();
DataGenericUtil.prototype = Object.extendsObject(global.AbstractAjaxProcessor, {
    initialize : function() {
        //nothing to do here
    },

    doStuff : function() {
        var input = this.getParameter("sysparm_input");
        gs.info("Parameter sent was {0}", input);
    
        return "plonk";
    },

    type: 'DataGenericUtil'
});



 

When I converted it, I made sure to mark it Client callable. It was already Active, obviously, but I did have to extend AbstractAjaxProcessor. I already tried extending global.AbstractAjaxProcessor (as you can see) but that hasn't fixed it.

 

I have verified that the fields on this table exist- the console.log calls output a valid sys_id and an integer between 1 and 25 each time it runs. I don't get any errors in my JavaScript console, and the instance logs are similarly barren.

  1. Is there additional logging I could do to help track this down?
  2. Does the table this script runs on need to be in the same scope as the client script?
  3. Would it help to capture the request headers or the response directly, and post them here?

 

 

 

 

I hope this post is useful. I really do love coming to the Community and trying to solve problems. If you have a question to ask, spend a little extra time doing the write-up, and I promise that you'll get it all back plus more when the answers start rolling in.

I have strange and mystical powers.

I spent some time this morning looking for a link or a document about the unique environment Client Scripts which are part of a Scoped Application execute in. I wasn't able to find an official document outlining the details, not in the Wiki, nor in the Developer API Reference, nor in the KB on Hi. Since I implemented most of this functionality, I thought I'd write up a blog post explaining it. We'll also work on official documentation in the correct places, of course.

 

What are the differences between a scoped Client Script and a global one?

 

Scoped Client Scripts- Client Scripts which are created as part of a Scoped Application- are executed slightly differently than their non-scoped counterparts. They are wrapped in a closure, and certain APIs that would normally be accessible are either modified, or unavailable altogether. We'll get into the reasons later on, but here is a quick rundown of the differences.

 

Inaccessible APIs

Modified APIs

  • GlideRecord
  • window
  • document
  • $ (Prototype library)
  • $$ (Prototype library selector shortcut)
  • jQuery
  • $j (jQuery shortcut)
  • $F (Sizzle form element value shortcut)
  • GlideAjax
  • g_form
  • GlideDialogWindow

 

 

Most of the inaccessible APIs can be re-enabled on a per-application basis. To do so, you need to ship a True/False system property in your application named glide.script.block.client.globals with the value false. As with all system properties in a Scoped Application, the name of the property will automatically have your application's scope name prefixed to it- you don't need to do that yourself. I see access to these global objects as a failure on our part to provide appropriate APIs. Direct DOM manipulation is sometimes the only way to achieve a certain goal, but it's also prone to breaking between releases. Something that worked fine in UI11 may not work in UI15, for example. By tracking which scoped applications needs these globals, we can better target our development efforts on creating or updating APIs to address those needs without resorting to the DOM.

 

I said most of the inaccessible APIs can be turned back on. There is only one that is not available even with that system property set to false- client-side GlideRecord. This should be considered deprecated. I would like to have removed it completely, but there is a lot of legacy code out there in the wild using it. Client-side GlideRecord has some convenience, but it comes at the cost of large payloads, and the fact that almost everybody using it does so synchronously. Everything client-side GlideRecord does can be done with GlideAjax and an appropriately-written Script Include or processor.

 

So what about the modified APIs?

 

The version of GlideAjax that is available to scoped Client Scripts can only make asynchronous requests. Requests made from scoped GlideAjax requests will also take place within the application's scope. You don't need to prefix your scope name on calls to your Script Includes, and other scoped Client Scripts have to respect your access policies.

 

MethodRestrictionAlternative
getXMLWait

Not Supported

getXML(callback)

getXMLAnswer(callback)

 

The scoped version of g_form is slightly more involved. Any calls to getReference must provide a callback function. This makes it an asynchronous action, and doesn't hang the interface while the client is communicating with the server. Scoped applications cannot change the Mandatory, ReadOnly,  Display, or Disabled status of fields outside of their scope- with one interesting exception. If your Client Script runs on a table in your own application, and that table extends a table from another scope, the fields you inherit from that base table are considered in-scope when they are displayed on your form.

 

What?

Consider the difference between adding a field to the Incident table and extending Incident with a new table that has only one field. In the first case, your field is being displayed on Incident. Client Scripts you add to the Incident table can do all the regular things with your field but are limited in what they can do to other fields. You cannot make the Caller mandatory and disable the Category field, because that could negatively affect the functionality of other Client Scripts and UI Policies on the Incident table.

In the second case, where you extend Incident, you are inheriting the fields that are Incident (and in its parent chain), but nothing you do on this table can actually break the functionality of Incident itself. You can set fields to be Mandatory or Disabled or ReadOnly as your requirements dictate, without worrying that you might be breaking some other application. Similarly, fields added onto your table by other scopes (if your Design-time Table Access is set up to allow that) cannot break your application's functionality. If they add Client Scripts to your application, they can't break your carefully-crafted rules about which fields must be filled in by the user before submission, and they can't hold up your user's interaction because they want to make a lot of Ajax calls as part of their functionality.

 

MethodRestrictionAlternative
setReadOnly/setReadonlyCan only apply to fields in the same scope as the calling script.Extend the table instead of adding a field to it
setMandatoryCan only apply to fields in the same scope as the calling script.

Extend the table instead of adding a field to it

setDisabledCan only apply to fields in the same scope as the calling script.Extend the table instead of adding a field to it
setDisplayCan only apply to fields in the same scope as the calling script.Extend the table instead of adding a field to it
getReferenceMust use a callbackg_form.getReference("ref_field",callback);

 

The scoped version of GlideDialogWindow is only marginally interesting. When making calls to GlideDialogWindow, HTML is generated server-side, and then passed down to the client to be displayed. The change to this API is invisible to your application- it merely ensures that the HTML that gets generated is appropriate for your scope. For instance, if you are rendering a form that contains Client Scripts, those Client Scripts are appropriately scoped as well.

 

You mentioned a closure?

 

Technically it's an IIFE- an immediately-invoked function expression. We wrap your scoped Client Scripts in a strict-mode IIFE to provide the appropriate environment, and to prevent leaking variables into the global scope. This prevents two Client Scripts from accidentally overwriting one another's variables and functions. This doesn't remove access to everything in the global lexical scope, it just makes sure that variables and functions you declare don't change underneath you because someone else also likes those names. The downside to this approach is with sharing functions between Client Scripts. In the old model, you could close your onChange or onLoad script, define a function, and it would be available to your other Client Scripts because it was part of the global window object. With the new approach, shared functions need to be defined in UI Scripts. The new model for Scoped UI Scripts creates an object in the global window with the same name as your scope, and allows you to add properties to this object which are then available to all your other UI Scripts and Client Scripts, just by accessing the object.

 

What?

Ok, I realize I'm suddenly talking about UI Scripts, and we seem to have taken a left turn. Trust me, you want to know this.

Scoped UI Scripts have a specific format that we'd prefer applications to adhere to. It's not a requirement because we recognize that UI Scripts serve many purposes, and that "acting as a library of shared functions" is only one of them. But if you adhere to the format that is pre-filled for you when you create your UI Script, you'll be able to add public methods to an object that you control, and can access those methods just by calling scope_name.methodName() in your Client Scripts (assuming the UI Script has been included on the form your script runs on). You can even define new properties on this object from within your Client Scripts, and therefore make them available to your other Client Scripts similar to how you did in the past- without worrying that you are clobbering some other global variable or function, and without worrying someone else is clobbering yours.

 

That should probably be covered in some Best Practices documents, right?

I agree (not surprising- I wrote that question). In addition to the documentation on Client Scripts, we'll work on some examples that show how we envision all of this being put into practice.

 

OK, but why (the short version)?

 

There were 3 things we wanted to address:

  1. Reduce the likelihood that a misbehaving Client Script or a random client-side JavaScript error could break the interface for users.
  2. Keep the interface responsive at all times.
  3. Reduce the likelihood for conflicts between two Scoped Applications running at the same time.

 

Every change that we made was done to address one of those 3 concerns. Asynchronous Ajax calls ensure that the interface remains snappy. IIFE's  make sure that someone's Floops app can't break your Spork form because they happen to also like the variable name "spoon", or they forgot a semicolon in a crucial place and JavaScript execution was halted. We also wanted to make sure that you don't have to constantly be referring to your own scope name. Client Scripts in your scope should not need to tell GlideAjax that they want to access the "alligatorDefenseProtocol" method on your Script Include named "HippopotamusFactoryGenerator" and not some other application's "HippopotamusFactoryGenerator" (which doesn't even include the "alligatorDefenseProtocol" method- how naive). GlideAjax should just know.

 

By pushing developers away from direct DOM manipulation and into supported APIs, we are trying to ensure that the application you build for Fuji keeps working on Geneva. If you build an application in UI14, it should work when the user switches to UI15. And when a new interface comes along, the app shouldn't need to worry about it- if it's using the supported APIs.

 

Most importantly (to me, anyway) is that users aren't negatively affected by applications. Something that they used yesterday still works today- even if an enhancement was added to the Incident form by way of a sweet new application that makes the entire form turn chartreuse when the Incident caller is a VIP, or which automatically searches previous incidents by the same Company for relevant issues so they can get up-to-speed quicker and deliver an answer that is more on-point than ever before. Enhancements and applications should make the experience better, they shouldn't be able to make it worse.

 

That's a lot of information- what's the short, short version?

 

Yes, it is. And that's not everything that I could say on this topic- I could probably talk for a couple of hours about it. But that's annoying because I tend to wander and get stuck in esoteric side-channels and bore the heck out of anyone unlucky enough to be within hearing range. So here is the short, short version:

 

  • ASYNC ALL THE THINGS
  • DON'T MESS WITH STUFF THAT ISN'T YOURS
  • DON'T ACCESS THE DOM DIRECTLY IF YOU CAN HELP IT
  • BE EXPLICIT WITH SHARED VARIABLES BY PUTTING THEM IN A UI SCRIPT
  • glide.script.block.client.globals IS A WORKAROUND (SOMETIMES)


I hope that was informative.

I have strange and mystical powers.

Filter Blog

By date: By tag: