Extension to GlideRecord: addQuery2, Throws an exception if you pass a bad column name or bad operator

treycarroll
Giga Guru

Have you ever been burned by the fact that GlideRecord's addQuery function silently tolerates bad column names?   I have. This query was nearly "career-altering" for me, when, in a sprint review meeting with senior leadership after 8 months of development, something "unexpected" happened.

try {

      var gr = new GlideRecord('change_task');

      gr.addQuery('parent_change', current.sys_id);

      gr.addQuery('short_description', 'CONTAINS', 'Build change');

      gr.query();

      while (gr.next()) {

              gr.state = 3;

              gr.update();

      }

} catch (e) {

      gs.logError('Error closing build change task for ' + current.number + ':' + e, 'Normal Change Workflow');

}

Can you spot the error?   'parent_change' is not a valid column name on the change_task table; it should be 'change_request'.     But the code is wrapped in a try/catch, so it will throw if you make a mistake like this, right?   Wrong!     It just ignores line 3 due to the bad column name.   The result is that ALL change tasks with "Build change" in the short description will be closed, not just the one that this workflow is running against.

In this particular situation, as the senior leaders were reviewing what we had built over a WebEx, a developer was following along on a different change.   When they clicked their 'Close Build Change Task' UI Action form button on their change, it also unexpectedly closed the "Build change" task on the change that leadership was stepping through.

The plug got pulled on the release and we were in hot water.     We found the bug, but the damage was done.   We had carefully done our testing.   What happened?     Since the button technically worked (closed the task upon request), this code had made it through extensive testing by a very competent QA department.

This fiasco led me create a replacement for addQuery that will throw an exception if the query is malformed:

GlideRecord.prototype.addQuery2 = function () {

      //@param colListWithDots is a string containing column names separated by dots.

      //@param table is the name of the table

      function checkDotWalking(colListWithDots, table) {

              //@param table is a table name

              //@param column is expected to be the name of a column on the table param

              //@param referenceTypeColumn boolean that signals that this column is a reference-type col

              // Returns an object with properties, success(bool), error (string), nextTable(string)

              // nextTable property is only populated if the current column is a reference-type column

              function checkCol(table, column, referenceTypeColumn) {

                      try {

                              var grCheckTable = GlideRecord(table);

                              grCheckTable.setLimit(1);

                              grCheckTable.query();

                              //Check for bad column name

                              if (!grCheckTable.isValidField(column)) {

                                      return {

                                              success: false,

                                              error: column + " is not a valid field name for table: " + table

                                      };

                              }

                              //Check for non reference-type column used for dot walking

                              if (referenceTypeColumn) {

                                      if (grCheckTable[column].sys_meta.type_description != 'reference') {

                                              return {

                                                      success: false,

                                                      error: 'Attempted dot walking on a column that is not a reference-type column:' + column

                                              };

                                      } else { //return succesful setting the nextTable, signal for continued dot walking

                                              return {

                                                      success: true,

                                                      nextTable: grCheckTable[column].sys_meta.reference

                                              }

                                      }

                              }

                              // We have already passed every possible error condition.   (Not a reference-type field. Reached the end of the dot walking)

                              return {

                                      success: true

                              }

                      } catch (e3) {

                              throw 'Error in checkCol():' + e3;

                      }

              }//end checkCol

              //@param queryColString is the value of the first param from a call to addQuery (Column name)

              //Returns an array filled with the tokens between non-alpha-chars (here 'dots')

              //Example: if passed 'aaa.bbb.ccc.ddd', would return ['aaa','bbb','ccc','ddd']

              function loadTokens(queryColString) {

                      try {

                              var matches = queryColString.match(/(\w+)/ig);

                              var tokens = [];

                              if (matches) {

                                      for (var i = 0; i < matches.length; i++) {

                                              tokens.push(matches[i]);

                                      }

                              }

                              return tokens;

                      } catch (e4) {

                              throw 'Error in loadTokens():' + e4;

                      }

              }

              try {

                      var tokens = loadTokens(colListWithDots);

                      if (tokens.length > 1) {//We only want to do this if there is more than one token

                              for (var i = 0; i < tokens.length - 1; i++) {//Stop iterating one before the end.   The last value is not a reference field

                                      var columnName = tokens[i];

                                      //Check the value to ensure that it is a valid col on that table (for a reference-type column)

                                      var result = checkCol(table, columnName, true);

                                      if (!result.success) {

                                              return result;

                                      }

                                      table = result.nextTable;

                              }

                              var finalColumnName = tokens[tokens.length - 1];

                              //Check the last value to ensure that it is a valid col on that table (but not that it is a reference-type column)

                              result = checkCol(table, finalColumnName, false);

                              if (!result.success) {

                                      return result;

                              }

                              return {

                                      success: true

                              };

                      }

              } catch (e2) {

                      throw 'Error in body of checkDotWalking():' + e2;

              }

      }

      try {

              //Cast params to strings

              var a = arguments[0] + '';

              var b = arguments[1] + '';

              var c = arguments[2] + '';

              //The first arg (column name) uses dot walking

              if (/\w\.\w/.test(a)) {

                      var result = checkDotWalking(a, this.sys_meta.name);

                      if (!result.success) {

                              throw result.error;

                      }

              } else if (!this.isValidField(a)) { //Throw an exception if the simple field name is bad (non-dot walking)

                      throw a + " is not a valid field name for table: " + this.sys_meta.name;

              }

              //3 parameter version

              if (arguments[2]) {

                      var validOperators = ">,>=,=,!=,<,<=,STARTSWITH,ENDSWITH,IN,CONTAINS,DOES NOT CONTAIN";

                      //Throw an exception if the operator is bad                      

                      if (validOperators.indexOf(b) == -1) {

                              throw b + " is not a valid operation.   Valid operations:" + validOperators;

                      }

                      this.addQuery(a, b, c);

                      return;

              }

              //2 parameter version

              this.addQuery(a, b);

              return;

      } catch (e1) {

              throw "Error:" + e1 + ". Source:GlideRecord.prototype.addQuery2";

      }

};

You can now wrap your GlideRecord queries in a try catch and respond to bad field names.

find_real_file.png

                                    find_real_file.png

It also throws when you foul up the operator.

find_real_file.png

find_real_file.png

Rewriting the initial "horror story" query from the beginning of the post to use addQuery2:

try {

      var gr = new GlideRecord('change_task');

      gr.addQuery2('parent_change', current.sys_id);//'parent_change' should be 'change_request'

      gr.addQuery2('short_description', 'CONTAINS', 'Build change');

      gr.query();

      while (gr.next()) {

              gr.state = 3;

              gr.update();

      }

} catch (e) {

      gs.logError('Error closing build change task for '   + current.number + ':' + e, 'Normal Change Workflow');

}

Now the query will produce this error:

Normal Change Workflow: Error closing build change task for CHG0003345:Error:parent_change is not a valid field name for table: change_task. Source:GlideRecord.prototype.addQuery2: no thrown error

Much better!

Updated to reduce code complexity in checking validOperators based on feedback from sabell2012

Message was edited by: Updated to throw an error based on bad dot walking.

19 REPLIES 19

Hi James,



This is added in a global business rule, which is loaded any time you perform any action on any table - so be advised that this practice is generally frowned upon and should only be undertaken only after careful consideration.



Once we got to Geneva, code in a global business rule had to be wrapped in a function.   So the above code will need to be wrapped.   Also, you should wrap the code inside of an if statement like:



if(!GlideRecord.prototype.addQuery2){


...


}



since we don't want to go to the expense of repeatedly redefining the addQuery2 function.



Regards,



Trey Carroll


Trey,



I'm assuming there is a reason you put this into a global business rule and didn't put this in as a script include?


If so, would you mind providing me some insight?



Thanks,


James


I think I just found my answer, Script Includes - ServiceNow Wiki.



Thanks,


James


Hi James,



This functionality needs to be in scope for all table operations, thus the use of a global BR.   You could easily call a Script Include from the global BR to inject the function; but since it will never be reused anywhere else, I opted to just write it directly in the BR.



Trey Carroll


hopps
Tera Contributor

This post is from Oct 2015 but came up as the 3rd result on a google search I conducted today (Jan 2024). It looks like the functionality provided here (whilst really useful at the time) has been superseded by the official GlideQuery plugin introduced by ServiceNow in the Paris release, so I'll signpost to that in case anyone else stumbles across this in future.


KB article: Alternative to GlideRecord and Glidequery - Support and Troubleshooting (servicenow.com)
Docs API reference: https://docs.servicenow.com/csh?topicname=GlideQueryGlobalAPI.html&version=lates... 

Developer site reference: GlideQuery | ServiceNow Developers