GlideFilter is Broken - Free Tool: “BetterGlideFilter”

Once upon a time, there was a global, instantiatable “GlideFilter” object. It was a little ugly and unintuitive, but it technically worked; usually. Nowadays though, the GlideFilter object is a scoped-API object (also available to the global scope) that is not instantiatable, and which has… some problems.
ServiceNow is aware of these problems, but - although they continue to use GlideFilter in their scoped apps (such as for vulnerability grouping rules in the Vulnerability application, which have been broken by the use of GlideFilter as of London - PRB1329737) - they have refused to fix the issues (PRB605673).

“What problems”, you ask? - Let’s say you’ve got an Incident with the number “INC000123”. You might have an encoded query that looks something like this:

active=true^number=inc000123

If you use that encoded query to filter the list view on the Incident table, that’ll work just fine.
It’ll also work just fine if you use that query in a script, such as this:

var grIncident = new GlideRecord('incident');
grIncident.addEncodedQuery('active=true^number=inc000123');
grIncident.setLimit(1);
grIncident.query();
if (grIncident.next()) {
    gs.print('Found INC000123!'); //Works just fine
}

But let’s say you’ve already got a GlideRecord (let’s call it current), and you want to compare it to an encoded query, to see if the record matches the query. You can imagine a scenario in which you’ve got a “condition” field on one record, and you want to compare the condition in that field to another record. What then?
That’s what GlideFilter is (supposed to be) for. The idea is to write a single line of code, and get a boolean indicating whether the GlideRecord matches the encoded query, like so:

var doesRecordMatch = GlideFilter.checkRecord(current, 'active=true^number=inc000123');

However, even if you have an active Incident with the number INC000123, and even if the query works just fine in a scripted query and in the list filter, the above line of code will actually set the doesRecordMatch variable to false.
Why? - Because GlideFilter.checkRecord(), is case-sensitive.
Why again? - I have no idea. It’s not documented that it’s case-sensitive, and I can’t think of a reason why it should be case-sensitive, but it is, and there’s no way to make it not case-sensitive.

Okay, yeah, but what’s the solution

Alright, enough bitching about GlideFilter, here’s the solution. It’s actually fairly simple (which makes it all the more frustrating that they’ve said they won’t fix it, but I digress); simply copy the below script into a Script Include in your instance.

Name: BetterGlideFilter
Accessible from: All application scopes
Application: Global

/*
    No need to instantiate. Just call like so:
    BetterGlideFilter.checkRecord(current, 'active=true^number=INC000123', false);
*/
var BetterGlideFilter = {
    /**
     * Check a GlideRecord (gr) against an encoded query. Returns true if the GlideRecord matches the query, or false if it does not.
     * If the encoded query contains multiple queries (indicated by containing "^NQ"), then you can set matchAll to true to make sure that checkRecord() only returns true if ALL conditions are met.
     * @param {GlideRecord} gr - A GlideRecord object containing the record to check the query against.
     * @param {String} queries - A string containing the encoded query (or queries) to compare the GlideRecord to. For multiple queries, use the "^NQ" operator or build your multi-tier query using the query builder in the list view.
     * @param {Boolean} [matchAll=false] - Whether to require that all queries (separated by "^NQ") should match in order for checkRecord() to return true. This is an optional parameter (default: false) and only applies when the provided query contains multiple queries (indicated by "^NQ" in the encoded query string).
     * @returns {boolean} - Whether the provided GlideRecord matches the provided query.
     */
    checkRecord: function(gr, queries, matchAll) {
        var i, encQuery;
        var newQueryToken = '^NQ';
        var queryMatches = false;
        
        //Set default value for matchAll
        matchAll = (typeof matchAll == 'undefined') ? (false) : (matchAll);
        
        //Get an array of queries
        if (queries.indexOf(newQueryToken) >= 0) { //If the encoded query contains multiple queries
            queries = queries.split(newQueryToken);
        } else {
            queries = [queries];
        }
        
        /*
            Loop through each query.
            If matchAll is true and one of the queries doesn't match, we can stop there and return false.
            If matchAll is false, and one of the queries DOES match, we can stop there and return true.
            Otherwise, keep looping until we run out of queries, and return the value in queryMatches
        */
        for (i = 0; i < queries.length; i++) {
            encQuery = queries[i];
            queryMatches = this._check(gr, encQuery);
            
            //If matchAll is true, but the query did not match, return false.
            if (matchAll && !queryMatches) {
                return false;
            }
            //If matchAll is false, and the query matched, return true.
            if (!matchAll && queryMatches) {
                return true;
            }
            //If matchAll is false and the query does NOT match, then continue checking.
            //If matchall is true and the query DOES match, continue checking.
        }
        
        return queryMatches;
    },
    
    /**
     * Private helper method for checking one query at a time against a GlideRecord. Used by .checkRecord().
     * @param {GlideRecord} gr
     * @param {String} query
     * @returns {Boolean}
     * @private
     */
    _check: function(gr, query) {
        /*
            Here, I'm using GlideRecord with .setLimit(1) because my query is SO specific (being
             by sys_id) that this is the most efficient method in almost every scenario.
            More information in this blog post:
            https://community.servicenow.com/community?id=community_blog&sys_id=a2bc2e25dbd0dbc01dcaf3231f9619df
        */
        var grCheck = new GlideRecord(gr.getTableName());
        grCheck.addQuery('sys_id', gr.getValue('sys_id'));
        grCheck.addEncodedQuery(query);
        grCheck.setLimit(1);
        grCheck.query();
        return grCheck.hasNext();
    }
};

With the above Script Include in your instance, you’ve simply got to update any references to “GlideFilter”, to instead reference “BetterGlideFilter”, and you now have a case-agnostic solution.


Update 6/6/20

A brilliant fellow ServiceNow developer, János Szentpáli, reached out to me today, to let me know that he’s found that, despite what HI support has told us, GlideFilter does in fact have an (undocumented) mechanism by which to force it to be case-insensitive: by using it as a constructor!

Of course, one should be wary of becoming heavily reliant on undocumented functionality; but the functionality that he’s demonstrated does have at least one advantage over the tool I’ve written above. It can be used against records which have not yet been committed to the database. Certainly if your use-case relies on that ability, then you’d probably be best-off using the method that János describes in his article!

Check out his excellent article here, for more info.