FujiForty - GlideScopedEvaluator

Protecting your proprietary ServiceNow scripts is great option in Fuji. The problem is the customer loses the feature they love the most, the ability to customize anything they want. The customer could create their own script include that extends a protected vendor class to override methods but this solution has limited usability. It's not always possible to change the script that is instantiating the class to point it at your new class. Let's take a look at how the new GlideScopedEvaluator API can help us. GlideScopedEvaluator allows us to evaluate any script stored in a table. It also allows for passing variables in and out of the evaluated script environment.

Let's say we're developing a vendor app for the store. It will include a protected script include class to ensure application reliability and upgrade support. However, we also want to give the customer an option to apply custom logic.

Here are a couple samples to show you some possible uses.

The VendorStuff class contains two methods, both will allow customers to override the logic we are providing. The 'getSomethingInteresting' method will check for the existence of an override script stored in a new table shipped with our application. This table and usage would have to be clearly documented for the customer to understand their options and impact. The query for an override method is handled by '_getOverride'. If we find a match, we evaluate the script by calling GlideScopedEvaluator().evaluateScript(), passing it the override table GlideRecord and the field name that contains the script. The return from the script evaluation will be returned to us and set in the 'result' variable so we can finish any vendor processing.

var VendorStuff = Class.create();
VendorStuff.prototype = {
    initialize: function() {
        this.overrideTable = "x_cavu_evaluator_script_override";
        this.overrideScriptField = "script";
    },
    
    getSomethingInteresting: function() {
        var result;
        
        //check for override
        var override = this._getOverride("VendorStuff.getSomethingInteresting");
        
        if (override) {
            gs.debug("Running override for getSomethingInteresting");
            var ScopeEval = new GlideScopedEvaluator();
            var params = {};
            result = ScopeEval.evaluateScript(override, this.overrideScriptField, params);
            gs.debug("getSomethingInteresting: Override returned: {0}", result);
        }
        else {
            //use default method if we don't have override
            gs.debug("getSomethingInteresting: No override found, running vendor logic");
            result = "We think you'll find this interesting www.cavucode.com/fujiforty/";
        }
        return result;
    },
    
    imFeelingLucky: function() {
        //run vendor logic first then check for override
        //get a random user
        var userGr = new GlideRecord("sys_user");
        userGr.query();
        var userCount = userGr.getRowCount();
        var counter = 0;
        var target = Math.floor(Math.random() * userCount); //seconds in a day
        var randomUser;
        while (userGr.next()) {
            counter++;
            if (counter == target) {
                randomUser = userGr.getDisplayValue();
                break;
            }
        }
        gs.debug("imFeelingLucky: Vendor imFeelingLucky picked user {0}", randomUser);
        
        //allow our value to be overriden but provide answer
        var override = this._getOverride("VendorStuff.imFeelingLucky");
        if (override) {
            gs.debug("imFeelingLucky: Running override for {0}, passing parameter user:{1} to override", "imFeelingLucky", randomUser);
            
            var ScopeEval = new GlideScopedEvaluator();
            var params = {user: randomUser};
            //override script will have access to read/write 'user' variable
            
            ScopeEval.evaluateScript(override, this.overrideScriptField, params);
            //get the user value back from the override
            user = ScopeEval.getVariable("user");
            gs.debug("imFeelingLucky: Override returned {0}", user);
        }
        gs.debug("imFeelingLucky: No override found, sticking with vendor value");

        return randomUser;
    },
    
    _getOverride: function(method) {
        var gr = new GlideRecord(this.overrideTable);
        //assumes there is protection to only have one record per method
        gr.addQuery("active", true);
        gr.addQuery("method", method);
        gr.query();
        if (gr.next())
            return gr;
    },
    
    type: 'VendorStuff'
};

 

Running this now would produce the following results. This is the default behavior on a new install of our app.

var vendorUtil = new x_cavu_evaluator.VendorStuff();
gs.debug("getSomethingInteresting = " + vendorUtil.getSomethingInteresting());
gs.debug("imFeelingLucky = " + vendorUtil.imFeelingLucky());
(VendorStuff): getSomethingInteresting: No override found, running vendor logic
getSomethingInteresting = We think you'll find this interesting www.cavucode.com/fujiforty/
(VendorStuff): imFeelingLucky: Vendor imFeelingLucky picked user Jasmin Gum
(VendorStuff): imFeelingLucky: No override found, sticking with vendor value
imFeelingLucky = Jasmin Gum

 

Now let's add some customer scripts to override our logic.

To override the getSomethingInteresting method the customer would create a record in the override table (x_cavu_evaluator_script_override) with method='VendorStuff.getSomethingInteresting' and the following script. This is just how I layout the override table, but all that matters is a way to query for a record with a script field.

//Method: VendorStuff.getSomethingInteresting
getMyOwnInterestingThis();
function getMyOwnInterestingThis() {
    var myInterest = "http://wiki.servicenow.com/index.php?title=Fuji_Release_Notes";
    gs.debug(">>>> Override script returning {0}", myInterest);
    
    return myInterest;
}

The override for 'imFeelingLucky' is slightly more advanced. The vendor script performs it's logic first, then looks for an override script and passes the vendor's value of 'user' to the override so the customer can use it in their logic. Then the vendor class will retrieve the user value from the override.

//Method: VendorStuff.imFeelingLucky

//vendor API says I have access to 'user' variable
gs.debug(">>>> Override script: vendor passed me user:{0}", user);
//get my own user record to return instead
var gr = new GlideRecord("sys_user");
if (gr.get("user_name", "abel.tuter")) {
    gs.debug(">>>> Override decided on user {0} instead", gr.getDisplayValue());
    //return new user value to vendor
    user = gr.getDisplayValue();
}

Here's the updated results when we call our vendor class methods.

var vendorUtil = new x_cavu_evaluator.VendorStuff();
gs.debug("getSomethingInteresting = " + vendorUtil.getSomethingInteresting());
gs.debug("imFeelingLucky = " + vendorUtil.imFeelingLucky());
(VendorStuff): Running override for getSomethingInteresting >>>> Override script returning http://wiki.servicenow.com/index.php?title=Fuji_Release_Notes (VendorStuff): getSomethingInteresting: Override returned: http://wiki.servicenow.com/index.php?title=Fuji_Release_Notes getSomethingInteresting = http://wiki.servicenow.com/index.php?title=Fuji_Release_Notes (VendorStuff): imFeelingLucky: Vendor imFeelingLucky picked user Eli Bettner (VendorStuff): imFeelingLucky: Running override for imFeelingLucky, passing parameter user:Eli Bettner to override >>>> Override script: vendor passed me user:Eli Bettner >>>> Override decided on user Abel Tuter instead (VendorStuff): imFeelingLucky: Override returned Abel Tuter (VendorStuff): imFeelingLucky: No override found, sticking with vendor value imFeelingLucky = Eli Bettner

As you can see, this provides a very flexible, yet stable option for protected vendor classes. Since we love flying with CAVU conditions, here's what we'd like to see before it's considered ideal conditions.

Querying for the script records is pretty easy but it would be nice to have an API to quickly perform something like a 'hasCustomScript' check without building my own logic everything. I'm also concerned with these records not being permanently cached like business rules and script includes are. If our vendor class was called a lot there could be some performance impact. 

I also found that error events are not bubbled up through the evaluator. Ideally, the vendor code should wrap the customer script evaluation in a try/catch block to handle any script errors introduced. Doing so won't currently trap the error since the script fails during the eval and execution doesn't get back to the calling vendor script. I would like to see the GlideScopedEvaluator API add it's own error handler that the vendor code could check for and handle any errors (GlideScopedEvaluator.hasError/getError). For now, I would highly recommend that the vendor documentation explaining the use of the override table clearly explains error handling and the use of try/catch in the customer script block.

One more thing to note. In this case we are querying the override table for what we assume would be a single record. If we wanted to ensure there was only ever one record per method, we should add a business rule on the override table to prevent duplicate method name records.