|
/* |
|
Single-Record GlideRecord Query Violation Detector (with function & line info) |
|
This script scans all scripts in a given table/field for GlideRecord queries that use |
|
`if (gr.next())` WITHOUT a preceding '.setLimit(1)' before '.query()'. |
|
Documentation: https://filthyqueries.snc.guru |
|
Latest version URL: https://filthyqueries-gist.snc.guru |
|
Author: Tim Woodruff (snprotips.com) |
|
Version: 1.05 |
|
License: Open Source (MIT License) |
|
Date (US): 7/23/2025 |
|
*/ |
|
|
|
//Set arg to `true` to print detailed violations with links and line numbers. |
|
debugPrintCommonViolations(true); |
|
|
|
/* |
|
EXAMPLE OUTPUT without details: |
|
Found 5 tables with violations: |
|
* sys_script_include: 840 violations |
|
* sys_script: 508 violations |
|
* sys_ws_operation: 22 violations |
|
* sys_trigger: 1 violations |
|
* sysevent_script_action: 20 violations |
|
|
|
Total violations: 1391 |
|
*/ |
|
|
|
function debugPrintCommonViolations(printDetails) { |
|
var tableName; |
|
var logMsg = ''; |
|
var totalViolationCount = 0; |
|
var objViolations = { |
|
'sys_script_include' : findDirtyFilthyUglyBadQueries('sys_script_include', 'script'), |
|
'sys_script' : findDirtyFilthyUglyBadQueries('sys_script', 'script'), |
|
'sys_ws_operation' : findDirtyFilthyUglyBadQueries('sys_ws_operation', 'operation_script'), |
|
'sys_trigger' : findDirtyFilthyUglyBadQueries('sys_trigger', 'script'), |
|
'sysevent_script_action' : findDirtyFilthyUglyBadQueries('sysevent_script_action', 'script'), |
|
'sys_variable_value': findDirtyFilthyUglyBadQueries('sys_variable_value', 'value', 'variable.labelLIKEscript') |
|
}; |
|
|
|
logMsg += 'Found ' + Object.keys(objViolations).length + ' tables with violations:\n'; |
|
|
|
for (tableName in objViolations) { |
|
if (!objViolations.hasOwnProperty(tableName)) { |
|
continue; // Skip non-own properties |
|
} |
|
totalViolationCount += objViolations[tableName].violation_count; |
|
logMsg += '* ' + tableName + ': ' + objViolations[tableName].violation_count + ' violations\n'; |
|
} |
|
|
|
logMsg += '\nTotal violations: ' + totalViolationCount + '\n'; |
|
gs.debug(logMsg); |
|
|
|
if (printDetails) { |
|
gs.debug( |
|
'Detailed violations: \n' + |
|
JSON.stringify( |
|
objViolations, null, 2 |
|
) |
|
); |
|
} |
|
} |
|
|
|
/** |
|
* Finds all scripts in the specified table and field that contain single-record queries |
|
* that do not use `.setLimit(1)` before `.query()`. |
|
* |
|
* @param {String} tableName - The name of the table to search for violations in scripts. |
|
* |
|
* @param {String} fieldName - The name of the field in the table that contains the script to |
|
* analyze. |
|
* |
|
* @param {String} [filter] - Optional. An encoded query to apply when querying the table. |
|
* |
|
* @returns {{table: string, field: string, checkedCount: number, violation_count: number, |
|
* violations: *[]}} |
|
*/ |
|
function findDirtyFilthyUglyBadQueries(tableName, fieldName, filter) { |
|
//Main function: Find all Script Includes with single-record query violations |
|
|
|
//init: set default values for tableName and fieldName |
|
tableName = tableName || 'sys_script_include'; |
|
fieldName = fieldName || 'script'; |
|
|
|
//init: hoist and initialize variables |
|
var scriptBody, recordName, recordSysId; |
|
var grScript = new GlideRecord(tableName); |
|
var result = { |
|
table : tableName, |
|
field : fieldName, |
|
checkedCount : 0, |
|
violation_count : 0, |
|
violations : [] |
|
}; |
|
|
|
grScript.addActiveQuery(); // Only check active records, if active field exists. |
|
if (filter) { |
|
grScript.addEncodedQuery(filter); |
|
} |
|
grScript.query(); |
|
|
|
while (grScript.next()) { |
|
result.checkedCount++; |
|
scriptBody = grScript.getValue(fieldName); |
|
|
|
if (!scriptBody) { |
|
continue; |
|
} |
|
|
|
recordName = grScript.getValue('name') || grScript.getDisplayValue(); |
|
recordSysId = grScript.getValue('sys_id'); |
|
|
|
analyzeScript( |
|
result.violations, |
|
scriptBody, |
|
recordName, |
|
recordSysId |
|
); |
|
} |
|
|
|
// Populate the results object before returning |
|
result.violation_count = result.violations.length; |
|
|
|
/*gs.info('Checked ' + checked + ' records in ' + tableName + '. Found ' + violations.length + ' single-record query violations (missing .setLimit(1)):'); |
|
gs.info(JSON.stringify(violations, null, 2));*/ |
|
return result; |
|
|
|
// ================== Helpers are below =================== |
|
function analyzeScript(arrViolations, scriptBody, recordName, recordSysId) { |
|
var iLine, iRecord, iRecordForIf, ifNextMatch; |
|
var line, matchNew, functionName, methodMatch, matchingGR, matchingGRForIf; |
|
var lines = scriptBody.split('\n'); |
|
var glideRecords = []; |
|
var regexNewGR = /var\s+([a-zA-Z0-9_]+)\s*=\s*new\s+GlideRecord\s*\(\s*['"][a-zA-Z0-9_]+['"]\s*\)/; |
|
var regexAssignGR = /([a-zA-Z0-9_]+)\s*=\s*new\s+GlideRecord\s*\(\s*['"][a-zA-Z0-9_]+['"]\s*\)/; |
|
var regexMethodCall = /([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\s*\(/; |
|
var regexIfNext = /if\s*\(\s*([a-zA-Z0-9_]+)\.next\s*\(\s*\)\s*\)/; |
|
|
|
for (iLine = 0; iLine < lines.length; iLine++) { |
|
line = lines[iLine]; |
|
matchNew = regexNewGR.exec(line) || regexAssignGR.exec(line); |
|
if (matchNew) { |
|
functionName = getFunctionNameForLine(lines, iLine); |
|
glideRecords.push({ |
|
varName : matchNew[1], |
|
instLine : iLine, |
|
methods : [], |
|
queryLine : null, |
|
setLimitLine : null, |
|
ifNextLines : [], |
|
functionName : functionName |
|
}); |
|
} |
|
|
|
methodMatch = regexMethodCall.exec(line); |
|
|
|
if (methodMatch) { |
|
matchingGR = null; |
|
for (iRecord = glideRecords.length - 1; iRecord >= 0; iRecord--) { |
|
if (glideRecords[iRecord].varName === methodMatch[1] && glideRecords[iRecord].instLine <= iLine) { |
|
matchingGR = glideRecords[iRecord]; |
|
break; |
|
} |
|
} |
|
if (matchingGR) { |
|
matchingGR.methods.push({name : methodMatch[2], line : iLine}); |
|
if (methodMatch[2] === 'query') { |
|
matchingGR.queryLine = iLine; |
|
} else if (methodMatch[2] === 'setLimit') { |
|
matchingGR.setLimitLine = iLine; |
|
} |
|
} |
|
} |
|
|
|
ifNextMatch = regexIfNext.exec(line); |
|
|
|
if (ifNextMatch) { |
|
matchingGRForIf = null; |
|
|
|
for (iRecordForIf = glideRecords.length - 1; iRecordForIf >= 0; iRecordForIf--) { |
|
if (glideRecords[iRecordForIf].varName === ifNextMatch[1] && glideRecords[iRecordForIf].instLine <= iLine) { |
|
matchingGRForIf = glideRecords[iRecordForIf]; |
|
break; |
|
} |
|
} |
|
|
|
if (matchingGRForIf) { |
|
matchingGRForIf.ifNextLines.push(iLine); |
|
} |
|
} |
|
} |
|
|
|
glideRecords.forEach(function(objRecord) { |
|
objRecord.ifNextLines.forEach(function(ifLine) { |
|
var hasSetLimitBeforeQuery; |
|
|
|
if ( |
|
objRecord.queryLine !== null && |
|
objRecord.queryLine < ifLine |
|
) { |
|
hasSetLimitBeforeQuery = ( |
|
objRecord.setLimitLine !== null && |
|
objRecord.setLimitLine < objRecord.queryLine |
|
); |
|
|
|
if (!hasSetLimitBeforeQuery) { |
|
arrViolations.push({ |
|
name : recordName, |
|
sys_id : recordSysId, |
|
url : buildRecordURL(recordSysId), |
|
variable : objRecord.varName, |
|
functionName : objRecord.functionName, |
|
instLine : objRecord.instLine + 1 // 1-based line number |
|
}); |
|
} |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
function getFunctionNameForLine(lines, targetLine) { |
|
var iTargetLine, line, funcMatch, objFuncMatch, functionName; |
|
|
|
for (iTargetLine = targetLine; iTargetLine >= 0; iTargetLine--) { |
|
line = lines[iTargetLine]; |
|
funcMatch = /function\s+([a-zA-Z0-9_]+)\s*\(/.exec(line); |
|
|
|
if (funcMatch) { |
|
functionName = funcMatch[1]; |
|
break; |
|
} |
|
|
|
objFuncMatch = /([a-zA-Z0-9_]+)\s*:\s*function\s*\(/.exec(line); |
|
|
|
if (objFuncMatch) { |
|
functionName = objFuncMatch[1]; |
|
break; |
|
} |
|
} |
|
return functionName || 'global or anonymous'; |
|
} |
|
|
|
function buildRecordURL(sysId) { |
|
return ( |
|
gs.getProperty('glide.servlet.uri') + tableName + |
|
'.do?sys_id=' + sysId |
|
); |
|
} |
|
} |