EfficientGlideRecord (Client-side)
EfficientGlideRecord is a client-side API class from which you can perform asynchronous client-side GlideRecord-style queries while maximizing performance (eliminating the negative performance impact of using the client-side GlideRecord object) and without having to create a separate GlideAjax Script Include!
Every senior ServiceNow developer knows that client-side GlideRecord queries are slow and inefficient, and that it's far preferable to use a GlideAjax call. However, GlideAjax can be a REAL pain to implement. I've got an entire article about using GlideAjax from both a client and server perspective.
Even I have to look up my own article from time to time to remind myself of the correct patterns when I need to use it, and I groan every time I think about having to create yet another Script Include just to handle this one little use-case in this unique application scope or something.
A couple days ago, I was whingeing on the ServiceNow Developers Discord about the poor performance (and inaccurate documentation) of the client-side GlideRecord API, as well as the fact that it doesn’t support dot-walking.
I was wishing there was something better that didn’t require me to make a whole separate Script Include just to query a single record from the database and get the value of a few fields and one dot-walked field value on that record in my Client Script.
Unfortunately, that’s just the way it is. Client-side GlideRecord is massively inefficient, far too slow, and returns way too much unnecessary data to be used commonly in production code. GlideAjax is simply the best and most efficient method for looking up data from client-side scripts.
At least, it was… until now!
After searching for a better solution for another couple of hours, I finally decided:
So, I did. And now I’m sharing that solution with you!
This consists of only a few files: A client-callable Script Include that does the back-end work for us, and a Global UI Script that acts as the client-side GlideRecord alternative (which I very creatively named EfficientGlideRecord
). There is also a "portal" version of the same UI Script.
Aside from the fact that you'll specify only the fields you want to retrieve from the database for maximum performance (see examples in the API documentation), this is otherwise a near-perfect drop-in replacement for the client-side GlideRecord
class; meaning that in the vast majority of cases, you'll be able to take your existing code, change the word "GlideRecord
" to "EfficientGlideRecord
", call .addField()
for each field you want to retrieve, and that's it - you're done!
You might be wondering: "Okay, that's not too much work. I could do a code search for client-side code calling GlideRecord and get a performance and user-experience boost by replacing it with EfficientGlideRecord and adding any fields referenced in the callback function... but just how much performance improvement are we talking about here? Is it actually worth it?"
Oh my sweet summer child... even I was baffled when I did my performance testing, at just how inefficient the client-side GlideRecord
is, and by just how much performance could be improved with EfficientGlideRecord
.
As you can see in the Performance section (or in the image below), with the fastest internet, performance was improved by 80% (from nearly three full seconds, down to about half a second). For larger queries by users with a slower 1-10Mbps internet connection, performance was improved by as much as 93% - from ~71,700 milliseconds, down to ~5,100ms.
Read on to learn more, see usage examples, and download this free tool as an Update Set!
Index
- EfficientGlideRecord APIs
- addField(fieldName, getDisplayValue)
- addQuery(fieldName, operator, fieldValue)
- addEncodedQuery(encodedQueryString)
- addNullQuery(fieldName)
- addNotNullQuery(fieldName)
- orderBy(fieldName)
- orderByDesc(fieldName)
- setLimit(recordLimit)
- get(recordSysID)
- query(callbackFn)
- hasNext()
- next()
- canRead(fieldName)
- getValue(fieldName)
- getDisplayValue(fieldName)
- getRowCount()
- Performance Comparison
- Download Update Set/Code
- Changelog
- Upcoming features
- View Code
Enjoying this article? Don’t forget to subscribe to SN Pro Tips!
We never spam you, never sell your information to marketing firms, and never send you things that aren’t relevant to ServiceNow.
We typically only send out 1-4 newsletters per year, but trust me - you don't want to miss them!
EfficientGlideRecord
This new client-side API, EfficientGlideRecord, is a nearly-drop-in-replacement for the client-side GlideRecord API, at least when it comes to queries. I may implement insert/update/delete functionality in a later version.
The client-side API documentation is below. The actual code can be found in the Download and Code section of this page.
addField(fieldName, getDisplayValue)
Add a field to retrieve from the target record(s).Any fields not specified by calling this method will not be available on the resulting
EfficientGlideRecord
object in the callback function after calling .query()
(except for sys_id
, which is always returned). In this case, a warning will be shown in the console, and .getValue('field_name')
will return a blank string.
If a second argument (
getDisplayValue
) is not specified and set to true, then the field's display value will not be available on the resulting EfficientGlideRecord
object in the callback function. In this case, .getDisplayValue('field_name')
will return a blank string.
Note: You can retrieve the value of “dot-walked” fields by calling .addField()
and passing in a string with the dot-walked field name.
For example, if you're querying the incident
table and want the assignee's email address, you can use .addField('assigned_to.email')
to add the field. In your callback function, while iterating through the returned records, you can then use .getValue('assigned_to.email')
to retrieve that field's value!
Parameters
Name | Type | Required | Description |
---|---|---|---|
fieldName | String | True | Add a field to retrieve from the target record(s). Any fields not specified by calling this method will not be available on the resulting EfficientGlideRecord object in the callback function after calling .query(). In this case, a warning will be shown in the console, and .getValue('field_name') will return a blank string. If a second argument (getDisplayValue) is not specified and set to true, then the field's display value will not be available on the resulting EfficientGlideRecord object in the callback function. In this case, .getDisplayValue('field_name') will return a blank string. |
getDisplayValue | Boolean | False | Set this to true to retrieve the display value for the specified field as well. In addition to .getValue() , you'll also then be able to call .getDisplayValue() on the EfficientGlideRecord object in your callback function after performing a query.If set to false (or if unspecified), the display value is not retrieved from the server; so make sure that if you need to use the display value in your callback function, you set this argument to true. |
Example
var egrIncident = new EfficientGlideRecord('incident');
egrIncident.addField('number')
.addField('assignment_group', true)
.addField('assigned_to', true);
egrIncident.get('some_incident_sys_id', function(egrInc) {
g_form.addInfoMessage(
egrInc.getValue('number') + '\'s assignment group is ' +
egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' +
egrInc.getValue('assignment_group') + ')\n' +
'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' +
egrInc.getValue('assigned_to') + ')'
);
});
addQuery(fieldName, operator, fieldValue)
Add a query to the EfficientGlideRecord object.By specifying a field name, operator, and value, you can perform all sorts of queries.
If only two arguments are specified, then it's assumed that the first is the field name and the second is the field value. The operator will automatically be set to
"="
.
Parameters
Name | Type | Required | Description |
---|---|---|---|
fieldName | String | True | The name of the field to perform the query against. |
operator | String | False | The operator to use for the query. Valid operators: = , != , > , >= , <, <= , STARTSWITH , ENDSWITH , CONTAINS , DOES NOT CONTAIN , IN , NOT IN , INSTANCEOF Note: If only two arguments are specified (fieldValue is not defined), then the second argument will be treated as the value, and the operator will automatically be set to "=" . |
fieldValue | String | True | The value to compare, using the specified operator, against the specified field. |
Example
new EfficientGlideRecord('incident')
.setLimit(10)
.addQuery('assignment_group', '!=', 'some_group_sys_id')
.addQuery('assigned_to', 'some_assignee_sys_id')
.addNotNullQuery('assignment_group')
.addField('number')
.addField('short_description')
.addField('assignment_group', true) //Get display value as well
.orderBy('number')
.query(function (egrIncident) {
while (egrIncident.next()) {
var logMsg = '';
if (egrIncident.canRead('short_description')) {
logMsg += 'Short description value: ' + egrIncident.getValue('short_description') + '\n';
}
if (egrIncident.canRead('number')) {
logMsg += 'Number: ' + egrIncident.getValue('number') + '\n';
}
if (egrIncident.canRead('assignment_group')) {
logMsg += 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' +
egrIncident.getDisplayValue('assignment_group') + ')';
}
console.log(logMsg);
}
});
addEncodedQuery(encodedQueryString)
Add an encoded query string to your query. Records matching this encoded query will be available in your callback function after calling.query()
.
Parameters
Name | Type | Required | Description |
---|---|---|---|
encodedQueryString | String | True | The encoded query string to use in your query. |
Example
var egrIncident = new EfficientGlideRecord('incident');
egrIncident.addField('number');
egrIncident.addField('assignment_group', true);
egrIncident.addField('assigned_to', true);
egrIncident.addEncodedQuery('some_encoded_query_string');
egrIncident.get('some_incident_sys_id', function(egrInc) {
g_form.addInfoMessage(
egrInc.getValue('number') + '\'s assignment group is ' +
egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' +
egrInc.getValue('assignment_group') + ')\n' +
'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' +
egrInc.getValue('assigned_to') + ')'
);
});
addNullQuery(fieldName)
Shorthand for.addQuery(fieldName, '=', 'NULL')
Parameters
Name | Type | Required | Description |
---|---|---|---|
fieldName | String | True | The name of the field to use in your query, getting only records where this field is empty. |
Example
//Get IDs, numbers, and assignment groups for all child Incidents with missing short descriptions
new EfficientGlideRecord('incident')
.addQuery('parent', g_form.getUniqueValue())
.addNullQuery('short_description')
.addField('number')
.addField('assignment_group', true) //Get display value as well
.query(function (egrIncident) {
var incidentsWithoutShortDesc = [];
while (egrIncident.next()) {
incidentsWithoutShortDesc.push(egrIncident.getValue('number'));
}
if (incidentsWithoutShortDesc.length > 0) {
g_form.addErrorMessage(
'The following child Incidents have no short description:<br />* ' +
incidentsWithoutShortDesc.join('<br />* ')
);
}
});
addNotNullQuery(fieldName)
Shorthand forthis.addQuery(fieldName, '!=', 'NULL');
.
Parameters
Name | Type | Required | Description |
---|---|---|---|
fieldName | String | True | The name of the field to use in your query, getting only records where this field is not empty. |
Example
new EfficientGlideRecord('incident')
.setLimit(10)
.addQuery('assignment_group', '!=', 'some_group_sys_id')
.addQuery('assigned_to', 'some_assignee_sys_id')
.addNotNullQuery('assignment_group')
.addField('number')
.addField('short_description')
.addField('assignment_group', true) //Get display value as well
.orderBy('number')
.query(function (egrIncident) {
while (egrIncident.next()) {
var logMsg = '';
if (egrIncident.canRead('short_description')) {
logMsg += 'Short description value: ' + egrIncident.getValue('short_description') + '\n';
}
if (egrIncident.canRead('number')) {
logMsg += 'Number: ' + egrIncident.getValue('number') + '\n';
}
if (egrIncident.canRead('assignment_group')) {
logMsg += 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' +
egrIncident.getDisplayValue('assignment_group') + ')';
}
console.log(logMsg);
}
});
orderBy(fieldName)
Orders the queried table by the specified column, in ascending order.Parameters
Name | Type | Required | Description |
---|---|---|---|
fieldName | String | True | Orders the queried table by the specified column, in ascending order |
Example
//Get IDs, numbers, and assignment groups for all child Incidents with missing short descriptions
new EfficientGlideRecord('incident')
.addQuery('parent', g_form.getUniqueValue())
.addNullQuery('short_description')
.addField('number')
.addField('assignment_group', true) //Get display value as well
.orderBy('number')
.query(function (egrIncident) {
var incidentsWithoutShortDesc = [];
while (egrIncident.next()) {
incidentsWithoutShortDesc.push(egrIncident.getValue('number'));
}
if (incidentsWithoutShortDesc.length > 0) {
g_form.addErrorMessage(
'The following child Incidents have no short description:<br />* ' +
incidentsWithoutShortDesc.join('<br />* ')
);
}
});
orderByDesc(fieldName)
Orders the queried table by the specified column, in descending order.Parameters
Name | Type | Required | Description |
---|---|---|---|
fieldName | String | True | Orders the queried table by the specified column, in descending order |
Example
//Get IDs, numbers, and assignment groups for all child Incidents with missing short descriptions
new EfficientGlideRecord('incident')
.addQuery('parent', g_form.getUniqueValue())
.addNullQuery('short_description')
.addField('number')
.addField('assignment_group', true) //Get display value as well
.orderByDesc('number')
.query(function (egrIncident) {
var incidentsWithoutShortDesc = [];
while (egrIncident.next()) {
incidentsWithoutShortDesc.push(egrIncident.getValue('number'));
}
if (incidentsWithoutShortDesc.length > 0) {
g_form.addErrorMessage(
'The following child Incidents have no short description:<br />* ' +
incidentsWithoutShortDesc.join('<br />* ')
);
}
});
setLimit(recordLimit)
Limits the number of records queried from the database and returned in the response.Parameters
Name | Type | Required | Description |
---|---|---|---|
limit | Number | True | The limit to use in the database query. No more than this number of records will be returned. |
Example
//Does at least one child Incident exist without a short description?
new EfficientGlideRecord('incident')
.setLimit(1)
.addQuery('parent', g_form.getUniqueValue())
.addNullQuery('short_description')
.addField('number')
.query(function (egrIncident) {
if (egrIncident.hasNext()) {
g_form.addErrorMessage(
'At least one child Incident exists without a short description.'
);
}
});
get(recordSysID, callbackFn)
Gets a single record, efficiently, from the database by sys_id.Parameters
Name | Type | Required | Description |
---|---|---|---|
sysID | String | True | The sys_id of the record to retrieve. Must be the sys_id of a valid record which the user has permissions to see, in the table specified in the constructor when instantiating this object. |
callbackFn | Function | True | The callback function to be called when the query is complete. When the query is complete, this callback function will be called with one argument: the EfficientGlideRecord object containing the records resultant from your query. After querying (in your callback function), you can call methods such as .next() and .getValue() to iterate through the returned records and get field values. |
Example
var egrIncident = new EfficientGlideRecord('incident');
egrIncident.setLimit(10);
egrIncident.addField('number');
egrIncident.addField('assignment_group', true);
egrIncident.addField('assigned_to', true);
egrIncident.get('some_incident_sys_id', function(egrInc) {
g_form.addInfoMessage(
egrInc.getValue('number') + '\'s assignment group is ' +
egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' +
egrInc.getValue('assignment_group') + ')\n' +
'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' +
egrInc.getValue('assigned_to') + ')'
);
});
query(callbackFn)
Perform the async query constructed by calling methods in this class, and get the field(s) from the resultant record(s) that were requested by calling.addField(fieldName, getDisplayValue)
.
Parameters
Name | Type | Required | Description |
---|---|---|---|
callbackFn | Function | True | The callback function to be called when the query is complete. When the query is complete, this callback function will be called with one argument: the EfficientGlideRecord object containing the records resultant from your query. After querying (in your callback function), you can call methods such as .next() and .getValue() to iterate through the returned records, and get field values. |
Example
new EfficientGlideRecord('incident')
.setLimit(10)
.addQuery('assignment_group', '!=', 'some_group_sys_id')
.addQuery('assigned_to', 'some_assignee_sys_id')
.addNotNullQuery('assignment_group')
.addField('number')
.addField('short_description')
.addField('assignment_group', true) //Get display value as well
.orderBy('number')
.query(function (egrIncident) {
while (egrIncident.next()) {
console.log(
'Short description value: ' + egrIncident.getValue('short_description') + '\n' +
'Number: ' + egrIncident.getValue('number') + '\n' +
'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' +
egrIncident.getDisplayValue('assignment_group') + ')'
);
}
});
hasNext()
Check if there is a "next" record to iterate into using .next() (without actually positioning the current record to the next one).Can only be called from the callback function passed into
.query()
/.get()
after the query has completed.
Example
//Does at least one child Incident exist without a short description?
new EfficientGlideRecord('incident')
.setLimit(1)
.addQuery('parent', g_form.getUniqueValue())
.addNullQuery('short_description')
.addField('number')
.query(function (egrIncident) {
if (egrIncident.hasNext()) {
g_form.addErrorMessage(
'At least one child Incident exists without a short description.'
);
}
});
next()
Iterate into the next record, if one exists.Usage is the same as GlideRecord's
.next()
method.Can only be run from the callback function after a query using
.query()
or .get()
.
Example
new EfficientGlideRecord('incident')
.setLimit(10)
.addQuery('assignment_group', '!=', 'some_group_sys_id')
.addQuery('assigned_to', 'some_assignee_sys_id')
.addNotNullQuery('assignment_group')
.addField('number')
.addField('short_description')
.addField('assignment_group', true) //Get display value as well
.orderBy('number')
.query(function (egrIncident) {
while (egrIncident.next()) {
console.log(
'Short description value: ' + egrIncident.getValue('short_description') + '\n' +
'Number: ' + egrIncident.getValue('number') + '\n' +
'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' +
egrIncident.getDisplayValue('assignment_group') + ')'
);
}
});
canRead(fieldName)
Returns true if the specified field exists and can be read (even if it's blank).Will return false in the following cases:
-The specified field on the current record cannot be read
-The specified field does not exist in the response object (which may happen if you don't add the field to your request using .addField()).
-The specified field does not exist in the database
Parameters
Name | Type | Required | Description |
---|---|---|---|
fieldName | String | True | The name of the field to check whether the user can read or not. |
Example
new EfficientGlideRecord('incident')
.setLimit(10)
.addQuery('assignment_group', '!=', 'some_group_sys_id')
.addQuery('assigned_to', 'some_assignee_sys_id')
.addNotNullQuery('assignment_group')
.addField('number')
.addField('short_description')
.addField('assignment_group', true) //Get display value as well
.orderBy('number')
.query(function (egrIncident) {
while (egrIncident.next()) {
var logMsg = '';
if (egrIncident.canRead('short_description')) {
logMsg += 'Short description value: ' + egrIncident.getValue('short_description') + '\n';
}
if (egrIncident.canRead('number')) {
logMsg += 'Number: ' + egrIncident.getValue('number') + '\n';
}
if (egrIncident.canRead('assignment_group')) {
logMsg += 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' +
egrIncident.getDisplayValue('assignment_group') + ')';
}
console.log(logMsg);
}
});
getValue(fieldName)
Retrieve the database value for the specified field, if the user has permissions to read that field's value.Parameters
Name | Type | Required | Description |
---|---|---|---|
fieldName | String | True | The name of the field to retrieve the database value for. |
Example
//Get IDs, numbers, and assignment groups for all child Incidents with missing short descriptions
new EfficientGlideRecord('incident')
.addQuery('parent', g_form.getUniqueValue())
.addNullQuery('short_description')
.addField('number')
.addField('assignment_group', true) //Get display value as well
.query(function (egrIncident) {
var incidentsWithoutShortDesc = [];
while (egrIncident.next()) {
incidentsWithoutShortDesc.push(egrIncident.getValue('number'));
}
if (incidentsWithoutShortDesc.length > 0) {
g_form.addErrorMessage(
'The following child Incidents have no short description:<br />* ' +
incidentsWithoutShortDesc.join('<br />* ')
);
}
});
getDisplayValue(fieldName)
Retrieve the display value for the specified field, if the user has permission to view that field's value.Can only be called from the callback function after the query is complete.
Parameters
Name | Type | Required | Description |
---|---|---|---|
fieldName | String | True | The name of the field to retrieve the display value for. |
Example
//Get assignment groups for child Incidents for some reason
new EfficientGlideRecord('incident')
.addQuery('parent', g_form.getUniqueValue())
.addField('assignment_group', true) //Get display value as well
.query(function (egrIncident) {
var assignmentGroupNames = [];
while (egrIncident.next()) {
assignmentGroupNames.push(egrIncident.getDisplayValue('assignment_group'));
}
//todo: Do something with the assignment group names
});
getRowCount()
Retrieves the number of records returned from the query.If used in conjunction with
.setLimit()
, then the maximum value returned from this method will be the limit number (since no more records than the specified limit can be returned from the server).
Example
//Show the number of child Incidents missing Short Descriptions.
new EfficientGlideRecord('incident')
.addQuery('parent', g_form.getUniqueValue())
.addNullQuery('short_description')
.addField('number')
.query(function (egrIncident) {
if (egrIncident.hasNext()) {
g_form.addErrorMessage(
egrIncident.getRowCount() + ' child Incidents are missing a short description.'
);
}
});
Performance Comparison
In my testing, EfficientGlideRecord was, on average, ~83% faster than ServiceNow’s client-side GlideRecord API. As you can see from the table below, the performance difference is massive; especially for users with internet speeds <10Mbps.
These performance tests did not involve interacting with the data much after the query was complete, but I would expect further performance improvements there as well. This is due to the fact that the client-side EfficientGlideRecord object is much smaller and more efficiently-structured; therefore, retrieving values from it should result in even more performance savings.
Download Update Set (v1.0.4)
You can simply import the above Update Set, or you can copy-and-paste the code from the official Github repo.NOTE: The minified EfficientGlideRecord
class (as EfficientGlideRecordPortal
) has been added as a JS Include to the default “Stock” Service Portal theme by this Update Set. However, if you use a different theme on your Service Portal, please be sure to create a new JS Include, and point it to the UI Script that’s specifically marked as working with the Service Portal, as you can see in the screenshot below:
Changelog
- v1.0.4
- Updated query validation so that queries can now be performed even when no fields are specified.
- In this case (when no fields are specified using
.adddField()
), only the sys_id will be returned as an accessible field.
- In this case (when no fields are specified using
- The
sys_id
field will now always be returned, whether specified using.addField()
or not. - Dot-walking is now supported when calling
.addField()
,.getValue()
, and.getDisplayValue()
!- To use this functionality, simply call
.addField('some_reference_field.some_other_field', getDisplayValue)
. - To get the dot-walked field value after the query, use
.getValue('some_reference_field.some_other_field')
. - To get the display value, use
.getDisplayValue('some_reference_field.some_other_field')
.
- To use this functionality, simply call
- Updated query validation so that queries can now be performed even when no fields are specified.
- v1.0.3
- Split this functionality into two separate UI Scripts to work around ServiceNow's weird and broken handling of JS Includes when two UI Scripts have the same name. Portal behavior should now be much more stable. For real this time.
- v1.0.2
- Fixed an issue with scope binding specifically in Service Portal.
- Added a JS Include to the default "Stock" Service Portal theme to include the minified version of EfficientGlideRecord. If you use a different theme for your Service Portal, please be sure to add a JS Include for the portal version of the EfficientGlideRecord UI Script to your theme.
- v1.0.1
- Added
.getRowCount()
method to get the number of rows returned from the server (limited by.setLimit()
if specified). - Improved documentation and examples (in this article as well as in the JSDoc in the code).
- Updated
get(recordSysID)
so you no longer have to callnext()
in the callback function (it should've been this way from the start; my bad).
- Added
- v1.0.0
- Initial public release
Upcoming features
- Ability to insert/update/delete using EfficientGlideRecord
A highly efficient client-side(Done!) (Thanks to Artur for the suggestion!)..getRowCount()
- Probably some bug-fixes. I've tested this thoroughly and am not aware of any bugs, but you jerks are probably going to identify some tiny little bug in my code that will make the inside of my brain itch until I fix it.
- Just kidding, I absolutely adore people who report any bugs in my code. Please do tell me if you notice any actual or potential bugs.
Performance comparison so you can see exactly how much faster EfficientGlideRecord is versus GlideRecord.(done!)- If you have additional feature-requests, please let me know by filing a feature request in the official Git repo!
Code
Below, you can see the exact code in the Gist, which you can copy and paste into your environment if you prefer that over importing the XML for the Script Include using the above instructions.
You can also see the latest version of the app in the official repository on Github, where you can also file bug reports and feature requests.
Gist code (expand)
//Client-side example usage | |
doThing(); | |
function doThing() { | |
new EfficientGlideRecord('incident') | |
.setLimit(10) | |
.addNotNullQuery('assignment_group') | |
.addField('number') | |
.addField('short_description') | |
.addField('assignment_group', true) //Get display value as well | |
.orderBy('number') | |
.query(function (eGR) { | |
while (eGR.next()) { | |
console.log( | |
'Short description value: ' + eGR.getValue('short_description') + '\n' + | |
'Number: ' + eGR.getValue('number') + '\n' + | |
'Assignment group: ' + eGR.getValue('assignment_group') + ' (' + | |
eGR.getDisplayValue('assignment_group') + ')' | |
); | |
} | |
}); | |
} |
/* | |
Server-side client-callable Script Include. | |
See related article for full usage instructions and API documentation: | |
https://go.snc.guru/egr | |
@version 1.0.4 | |
*/ | |
var ClientGlideRecordAJAX = Class.create(); | |
ClientGlideRecordAJAX.prototype = Object.extendsObject(AbstractAjaxProcessor, { | |
gr_config : {}, | |
getPseudoGlideRecord : function() { | |
var grQuery; | |
var responseObj = { | |
'_records' : [], | |
'_row_count' : 0, | |
'_config' : {}, | |
'_executing_as' : { | |
'user_name' : gs.getUserName(), | |
'user_id' : gs.getUserID() | |
} | |
}; | |
this.gr_config = {}; | |
this.gr_config.table_to_query = this.getParameter('table_to_query'); | |
//@type {{get_display_value: boolean, name: string}} | |
this.gr_config.fields_to_get = this.getParameter('fields_to_get'); | |
this.gr_config.record_limit = this.getParameter('record_limit'); | |
this.gr_config.order_by_field = this.getParameter('order_by_field'); | |
this.gr_config.order_by_desc_field = this.getParameter('order_by_desc_field'); | |
this.gr_config.encoded_queries = this.getParameter('encoded_queries'); | |
this.gr_config.queries = this.getParameter('queries'); | |
//Parse queries/encoded queries array and fields_to_get object | |
if (this.gr_config.hasOwnProperty('queries') && this.gr_config.queries) { | |
this.gr_config.queries = JSON.parse(this.gr_config.queries); | |
} | |
if (this.gr_config.hasOwnProperty('fields_to_get') && this.gr_config.fields_to_get) { | |
this.gr_config.fields_to_get = JSON.parse(this.gr_config.fields_to_get); | |
} | |
if (this.gr_config.hasOwnProperty('encoded_queries') && this.gr_config.encoded_queries) { | |
this.gr_config.encoded_queries = JSON.parse(this.gr_config.encoded_queries); | |
} | |
gs.debug('EfficientGlideRecord config: \n' + JSON.stringify(this.gr_config, null, 2)); | |
if (!this._validateMandatoryConfig()) { | |
throw new Error( | |
'Mandatory value not specified. ' + | |
'Cannot perform query. Halting.\n' + | |
'Config: \n' + | |
JSON.stringify(this.gr_config) | |
); | |
} | |
grQuery = this._constructAndGetGlideRecord(); | |
grQuery.query(); | |
while (grQuery.next()) { | |
responseObj._records.push( | |
this._getRequestedRecordData(grQuery, this.gr_config) | |
); | |
} | |
responseObj._row_count = responseObj._records.length; | |
responseObj._config = this.gr_config; | |
return JSON.stringify(responseObj); | |
}, | |
_constructAndGetGlideRecord : function() { | |
var i, queryField, queryOperator, queryValue; | |
var grQuery = new GlideRecordSecure(this.gr_config.table_to_query); | |
//Add limit, if specified | |
if ( | |
this.gr_config.hasOwnProperty('record_limit') && | |
this.gr_config.record_limit >= 1 | |
) { | |
grQuery.setLimit(this.gr_config.record_limit); | |
} | |
//add order_by or order_by_desc field, if specified | |
if ( | |
this.gr_config.hasOwnProperty('order_by_desc_field') && | |
this.gr_config.order_by_desc_field | |
) { | |
grQuery.orderByDesc(this.gr_config.order_by_desc_field); | |
} | |
if ( | |
this.gr_config.hasOwnProperty('order_by_field') && | |
this.gr_config.order_by_field | |
) { | |
grQuery.orderBy(this.gr_config.order_by_field); | |
} | |
//Add encoded query, if specified | |
if ( | |
this.gr_config.hasOwnProperty('encoded_queries') && | |
this.gr_config.encoded_queries && | |
this.gr_config.encoded_queries.length > 0 | |
) { | |
for (i = 0; i < this.gr_config.encoded_queries.length; i++) { | |
if (this.gr_config.encoded_queries[i]) { | |
grQuery.addEncodedQuery(this.gr_config.encoded_queries[i]); | |
} | |
} | |
} | |
//Add field queries if specified | |
if ( | |
this.gr_config.hasOwnProperty('queries') && | |
this.gr_config.queries && | |
this.gr_config.queries.length > 0 | |
) { | |
for (i = 0; i < this.gr_config.queries.length; i++) { | |
queryField = this.gr_config.queries[i].field; | |
queryOperator = this.gr_config.queries[i].operator; | |
queryValue = this.gr_config.queries[i].value; | |
grQuery.addQuery(queryField, queryOperator, queryValue); | |
} | |
} | |
return grQuery; | |
}, | |
_validateMandatoryConfig : function() { | |
var i, currentQuery; | |
//May add more later if necessary | |
var mandatoryFields = [ | |
'table_to_query', | |
'fields_to_get' | |
]; | |
for (i = 0; i < mandatoryFields.length; i++) { | |
if ( | |
!this.gr_config.hasOwnProperty(mandatoryFields[i]) || | |
!this.gr_config[mandatoryFields[i]] | |
) { | |
return false; | |
} | |
} | |
//If both order_by and order_by_desc are specified, log a warning and ignore order_by_desc. | |
// if ( | |
// ( | |
// this.gr_config.hasOwnProperty('order_by_field') && | |
// this.gr_config.order_by_field | |
// ) && | |
// ( | |
// this.gr_config.hasOwnProperty('order_by_desc_field') && | |
// this.gr_config.order_by_desc_field | |
// ) | |
// ) { | |
// gs.warn( | |
// 'ClientGlideRecordAJAX client-callable Script Include called with ' + | |
// 'both an "order by" and "orderby descending" field. It is only possible to ' + | |
// 'specify one field to sort by, either ascending or descending. \n' + | |
// 'Ignoring the descending field, and ordering by the order_by_field field.' | |
// ); | |
// this.gr_config.order_by_desc_field = ''; | |
// } | |
/* | |
Decided to remove the above code and allow the user to order their results | |
however they like, I'm not their dad. | |
*/ | |
if ( | |
this.gr_config.hasOwnProperty('queries') && | |
this.gr_config.queries | |
) { | |
for (i = 0; i < this.gr_config.queries.length; i++) { | |
currentQuery = this.gr_config.queries[i]; | |
if ( | |
(!currentQuery.hasOwnProperty('field') || !currentQuery.field) || | |
(!currentQuery.hasOwnProperty('operator') || !currentQuery.operator) || | |
(!currentQuery.hasOwnProperty('value') || !currentQuery.value) | |
) { | |
gs.error( | |
'Malformed query provided to ClientGlideRecordAJAX Script Include:\n' + | |
JSON.stringify(currentQuery) | |
); | |
return false; | |
} | |
} | |
} | |
return true; | |
}, | |
/*_getRequestedRecordData : function(grRecord, config) { | |
var iFieldToGet, iFieldChain, grRefRecord, workingFieldName, fieldType, | |
splitFieldNames, canReadField, isFieldValid, fieldName, fieldElement, | |
fieldValue, fieldDisplayValue, getDisplay, invalidRefChain, hasNextDotWalk, | |
brokenRefChain; | |
var recordData = { | |
'_config' : config, | |
'_table_name' : grRecord.getTableName(), | |
'_field_values' : {} | |
}; | |
for (iFieldToGet = 0; iFieldToGet < recordData._config.fields_to_get.length; iFieldToGet++) { | |
//Set initial values to false in order to prevent previous loop | |
// from impacting this one. | |
invalidRefChain = false; | |
brokenRefChain = false; | |
fieldName = recordData._config.fields_to_get[iFieldToGet].name; | |
getDisplay = !!recordData._config.fields_to_get[iFieldToGet].get_display_value; | |
splitFieldNames = fieldName.split('.'); | |
//Set initial value of grRefRecord for use in the for-loop below. | |
grRefRecord = grRecord; | |
//Check if the field is valid and readable. | |
//For dot-walked fields, do this for each step. | |
for (iFieldChain = 0; iFieldChain < splitFieldNames.length; iFieldChain++) { | |
workingFieldName = splitFieldNames[iFieldChain]; | |
hasNextDotWalk = (iFieldChain + 1) < splitFieldNames.length; | |
isFieldValid = grRefRecord.isValidField(workingFieldName); | |
canReadField = (isFieldValid && grRefRecord[workingFieldName].canRead()); | |
if (!isFieldValid || !canReadField) { | |
break; | |
} | |
fieldType = grRefRecord.getElement().getED().getInternalType(); | |
//If field type is NOT reference, but we're attempting to dot-walk through it | |
// as indicated by there being additional elements in the field chain, then | |
// log an error and set a flag to abort getting this field value. | |
if (fieldType !== 'reference' && hasNextDotWalk) { | |
invalidRefChain = true; | |
gs.error( | |
'Attempted to get dot-walked field ' + fieldName + | |
' from table ' + grRecord.getTableName() + ', but one of the ' + | |
'fields in this dot-walk chain is not valid. Aborting getting ' + | |
'this field value.' | |
); | |
break; | |
} | |
// | |
//If there's more in this dot-walk chain and the field IS a reference field, | |
// then get the next reference object. | |
if (hasNextDotWalk && fieldType === 'reference') { | |
if (grRefRecord[workingFieldName].nil()) { | |
brokenRefChain = true; | |
break; | |
} | |
grRefRecord = grRefRecord[workingFieldName].getRefRecord(); | |
} | |
} | |
if (invalidRefChain) { | |
//If the requested field is dot-walked through an invalid field, | |
// continue to next loop. | |
continue; | |
} | |
if (brokenRefChain) { | |
recordData._field_values[fieldName] = { | |
'name' : fieldName, | |
'value' : '', | |
'display_value' : '', | |
'can_read' : canReadField | |
}; | |
//Continue to next loop after adding blank field value due to broken ref chain. | |
continue; | |
} | |
//Using .getElement() to handle dot-walked fields. | |
fieldElement = grRecord.getElement(fieldName); | |
//todo: update to use fieldElement instead of GR | |
//Must get canReadField in this way and use it to see if we can see the field values, | |
// cause otherwise GlideRecordSecure freaks alll the way out. | |
//canReadField = (grRecord.isValidField(fieldName.split('.')[0]) && grRecord[fieldName].canRead()); | |
fieldValue = canReadField ? (grRecord.getElement(fieldName).toString() || '') : ''; | |
fieldDisplayValue = (getDisplay && canReadField && fieldValue) ? | |
(grRecord[fieldName].getDisplayValue() || '') : (''); | |
//Retrieve value (and display value if requested) | |
recordData._field_values[fieldName] = { | |
'name' : fieldName, | |
'value' : fieldValue, | |
'display_value' : fieldDisplayValue, | |
//If false, may be caused by ACL restriction, or by invalid field | |
'can_read' : canReadField | |
}; | |
} | |
return recordData; | |
},*/ | |
_getRequestedRecordData : function(grRecord, config) { | |
var i, canReadField, fieldName, fieldValue, fieldDisplayValue, getDisplay, fieldElement, isFieldValid; | |
var recordData = { | |
'_config' : config, | |
'_table_name' : grRecord.getTableName(), | |
'_field_values' : {} | |
}; | |
for (i = 0; i < recordData._config.fields_to_get.length; i++) { | |
fieldName = recordData._config.fields_to_get[i].name; | |
getDisplay = !!recordData._config.fields_to_get[i].get_display_value; | |
//Must get canReadField in this way and use it to see if we can see the field values, | |
// cause otherwise GlideRecordSecure freaks alll the way out. | |
isFieldValid = grRecord.isValidField(fieldName.split('.')[0]); | |
fieldElement = isFieldValid && grRecord.getElement(fieldName); | |
canReadField = (isFieldValid && | |
grRecord[(fieldName.split('.')[0])].canRead()) && | |
fieldElement.canRead(); | |
fieldValue = canReadField ? (fieldElement.toString() || '') : ''; | |
fieldDisplayValue = (getDisplay && canReadField && fieldValue) ? | |
(fieldElement.getDisplayValue() || '') : (''); | |
//Retrieve value (and display value if requested) | |
recordData._field_values[fieldName] = { | |
'name' : fieldName, | |
'value' : fieldValue, | |
'display_value' : fieldDisplayValue, | |
//If false, may be caused by ACL restriction, or by invalid field | |
'can_read' : canReadField | |
}; | |
} | |
return recordData; | |
}, | |
type : 'ClientGlideRecordAJAX' | |
}); |
/* | |
This is a minified, closure-compiled Global Desktop UI Script containing | |
the EfficientGlideRecord class. | |
See https://go.snc.guru/egr for full usage and API documentation. | |
--- | |
Copyright (c) 2022 Tim Woodruff (https://TimothyWoodruff.com) | |
& SN Pro Tips (https://snprotips.com). | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
Alternative licensing is available upon request. Please contact tim@snc.guru | |
for more info. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
@version 1.0.4 | |
*/ | |
var EfficientGlideRecord=function(a){if(!a)throw Error("EfficientGlideRecord constructor called without a valid tableName argument. Cannot continue.");this._config={table_to_query:a,fields_to_get:[{name:"sys_id",get_display_value:!1}],record_limit:0,order_by_field:"",order_by_desc_field:"",encoded_queries:[],queries:[]};this._row_count=-1;this._query_complete=!1;this._records=[];this._current_record_index=-1;this._current_record={};this._gaQuery=new GlideAjax("ClientGlideRecordAJAX");this._gaQuery.addParam("sysparm_name", | |
"getPseudoGlideRecord");return this}; | |
EfficientGlideRecord.prototype.addField=function(a,b){var c;if(!a)return console.error("Attempted to call .addField() without a field name specified. Cannot add a blank field to the query."),this;for(c=0;c<this._config.fields_to_get.length;c++)if(this._config.fields_to_get[c].name===a)return console.warn("Attempted to add field with name "+a+" to EfficientGlideRecord query, but that field already exists. Cannot add the same field twice."),this;this._config.fields_to_get.push({name:a,get_display_value:!!b}); | |
return this};EfficientGlideRecord.prototype.addQuery=function(a,b,c){"undefined"===typeof c&&(c=b,b="=");this._config.queries.push({field:a,operator:b,value:c});return this};EfficientGlideRecord.prototype.addNotNullQuery=function(a){this.addQuery(a,"!=","NULL");return this};EfficientGlideRecord.prototype.addNullQuery=function(a){this.addQuery(a,"=","NULL");return this}; | |
EfficientGlideRecord.prototype.addEncodedQuery=function(a){if(!a||"string"!==typeof a)throw Error("Invalid encoded query string specified. Encoded query must be a valid non-empty string.");this._config.encoded_queries.push(a);return this};EfficientGlideRecord.prototype.setEncodedQuery=function(a){this._config.encoded_queries=[a];return this};EfficientGlideRecord.prototype.addOrderBy=function(a){this.orderBy(a);return this}; | |
EfficientGlideRecord.prototype.orderBy=function(a){this._config.order_by_field=a;return this};EfficientGlideRecord.prototype.orderByDesc=function(a){this._config.order_by_desc_field=a;return this};EfficientGlideRecord.prototype.setLimit=function(a){if("number"!==typeof a||0>=a)throw Error("EfficientGlideRecord.setLimit() method called with an invalid argument. Limit must be a number greater than zero.");this._config.record_limit=a;return this}; | |
EfficientGlideRecord.prototype.get=function(a,b){this.addQuery("sys_id",a);this.setLimit(1);this.query(function(c){c.next()?b(c):console.warn('EfficientGlideRecord: No records found in the target table with sys_id "'+a+'".')})}; | |
EfficientGlideRecord.prototype.query=function(a){var b;if(!this._readyToSend())return!1;for(b in this._config)if(this._config.hasOwnProperty(b)){var c=void 0;c="object"===typeof this._config[b]?JSON.stringify(this._config[b]):this._config[b];this._gaQuery.addParam(b,c)}this._gaQuery.getXMLAnswer(function(d,e){if("undefined"===typeof e){if("undefined"===typeof this||null===this)throw Error('EfficientGlideRecord ran into a problem. Neither eGR nor the "this" scope are defined. I have no idea how this happened. Better go find Tim and yell at him: https://go.snc.guru/egr'); | |
e=this}d=JSON.parse(d);if(!d.hasOwnProperty("_records"))throw Error("Something went wrong when attempting to get records from the server.\nResponse object: \n"+JSON.stringify(d));e._query_complete=!0;e._records=d._records;e._row_count=d._row_count;e._executing_as=d._executing_as;a(e)}.bind(this),null,this)};EfficientGlideRecord.prototype.hasNext=function(){return this._query_complete?this._row_count>this._current_record_index+1:!1}; | |
EfficientGlideRecord.prototype.next=function(){if(!this._query_complete||!this.hasNext())return!1;this._current_record_index++;this._current_record=this._records[this._current_record_index];return!0}; | |
EfficientGlideRecord.prototype.canRead=function(a){if(!this._query_complete)throw Error("The .canRead() method of EfficientGlideRecord can only be called from the callback function after calling .query(callbackFn)");return this._current_record._field_values.hasOwnProperty(a)?this._current_record._field_values[a].hasOwnProperty("can_read")?!!this._current_record._field_values[a].can_read||!1:(console.warn('The requested field "'+a+'" has no can_read node. This should not happen. Returning a blank false.'), | |
!1):(console.warn("There is no field with the name "+a+" in the EfficientGlideRecord object. Did you remember to specify that you want to get that field in the query using .addField()?"),!1)}; | |
EfficientGlideRecord.prototype.getValue=function(a){if(!this._query_complete)throw Error("The .getValue() method of EfficientGlideRecord can only be called from the callback function after calling .query(callbackFn)");return this._current_record._field_values.hasOwnProperty(a)?this._current_record._field_values[a].hasOwnProperty("value")?this._current_record._field_values[a].value||"":(console.warn('The requested field "'+a+'" has no value node. This should not happen. Returning a blank string.'), | |
""):(console.warn("There is no field with the name "+a+" in the EfficientGlideRecord object. Did you remember to specify that you want to get that field in the query using .addField()?"),"")}; | |
EfficientGlideRecord.prototype.getDisplayValue=function(a){if(!this._query_complete)throw Error("The .getDisplayValue() method of EfficientGlideRecord can only be called from the callback function after calling .query(callbackFn)");return this._current_record._field_values.hasOwnProperty(a)?this._current_record._field_values[a].hasOwnProperty("display_value")&&this._current_record._field_values[a].display_value?this._current_record._field_values[a].display_value||"":(console.warn("There is no display value for the field with the name "+ | |
a+" in the EfficientGlideRecord object. Did you remember to specify that you want to get that field's display value in the query using .addField(fieldName, true)?"),""):(console.warn("There is no field with the name "+a+" in the EfficientGlideRecord object. Did you remember to specify that you want to get that field in the query using .addField()?"),"")};EfficientGlideRecord.prototype.getRowCount=function(){return this._row_count}; | |
EfficientGlideRecord.prototype._readyToSend=function(){if(!this._config.table_to_query)return console.error("EfficientGlideRecord not ready to query. Table name was not specified in the constructor's initialize argument."),!1;1>=this._config.fields_to_get.length&&console.warn("EfficientGlideRecord: No fields other than sys_id were specified to retrieve. \nYou can specify which fields you want to retrieve from the GlideRecord object using .addField(fieldName, getDisplayValue). Afterward, in your callback, you can use .getValue(fieldName). If you set getDisplayValue to true in .addField(), you can also use .getDisplayValue(fieldName).\nWithout fields to retrieve specified using .addField(), each record will be returned with only a sys_id. \nThis will not prevent you from performing your query, unless something has gone terribly wrong."); | |
(!this._config.hasOwnProperty("queries")||1>this._config.queries.length)&&(!this._config.hasOwnProperty("encoded_queries")||1>this._config.encoded_queries.length)&&(!this._config.hasOwnProperty("record_limit")||1>this._config.record_limit)&&console.warn("The EfficientGlideRecord query has no query and no record limit associated with it. This may result in poor performance when querying larger tables. Please make sure that you need all records in the specified table, as all records will be returned by this query."); | |
return!0}; |
/** | |
* UI Script (Desktop-Global) | |
* @description See related article for full usage instructions and API documentation: | |
* https://snprotips.com/efficientgliderecord | |
* @classdesc https://snprotips.com/efficientgliderecord | |
* @author | |
* Tim Woodruff (https://TimothyWoodruff.com) | |
* SN Pro Tips (https://snprotips.com) | |
* @version 1.0.4 | |
* @class | |
* | |
* @license | |
* Copyright (c) 2022 Tim Woodruff (https://TimothyWoodruff.com) | |
* & SN Pro Tips (https://snprotips.com). | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* Alternative licensing is available upon request. Please contact tim@snc.guru | |
* for more info. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ | |
class EfficientGlideRecord { | |
/** | |
* Instantiated with the 'new' keyword (as classes typically are when instantiated), this | |
* will construct a client-side EfficientGlideRecord object. The methods of this class can | |
* then be called to construct a client-side GlideRecord query. EfficientGlideRecord | |
* replicates *most* of the functionality of the client-side GlideRecord object, but | |
* with more and enhanced functionality. | |
* EfficientGlideRecord is FAR preferable to using the out-of-box (OOB) client-side | |
* GlideRecord query (even asynchronously), because GlideRecord returns a massive amount | |
* of unnecessary data, and can be much, much slower. EfficientGlideRecord aims to return | |
* only that data which is necessary and requested from the server, thus providing an | |
* efficient interface to query records asynchronously without all the additional overhead | |
* related to information that you don't need. | |
* | |
* Additional documentation can be found on the SN Pro Tips blog, at https://go.snc.guru/egr | |
* NOTE: For info on performing async queries in onSubmit Client Scripts, see | |
* https://go.snc.guru/onsubmit | |
* | |
* @param {String} tableName - The name of the table on which to execute your GlideRecord query | |
* @returns {EfficientGlideRecord} | |
* @example | |
* var egrIncident = new EfficientGlideRecord('incident'); | |
* egrIncident.addField('number') | |
* .addField('assignment_group', true) | |
* .addField('assigned_to', true); | |
* | |
* egrIncident.get('some_incident_sys_id', function(egrInc) { | |
* g_form.addInfoMessage( | |
* egrInc.getValue('number') + '\'s assignment group is ' + | |
* egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' + | |
* egrInc.getValue('assignment_group') + ')\n' + | |
* 'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' + | |
* egrInc.getValue('assigned_to') + ')' | |
* ); | |
* }); | |
* @constructor | |
*/ | |
constructor(tableName) { | |
if (!tableName) { | |
throw new Error( | |
'EfficientGlideRecord constructor called without a valid tableName ' + | |
'argument. Cannot continue.' | |
); | |
} | |
this._config = { | |
'table_to_query' : tableName, | |
'fields_to_get' : [{ | |
'name' : 'sys_id', | |
'get_display_value' : false | |
}], | |
'record_limit' : 0, | |
'order_by_field' : '', | |
'order_by_desc_field' : '', | |
'encoded_queries' : [], | |
'queries' : [] | |
}; | |
this._row_count = -1; | |
this._query_complete = false; | |
this._records = []; | |
this._current_record_index = -1; | |
this._current_record = {}; | |
this._gaQuery = new GlideAjax('ClientGlideRecordAJAX'); | |
this._gaQuery.addParam('sysparm_name', 'getPseudoGlideRecord'); | |
return this; | |
} | |
/** | |
* Add a field to retrieve from the target record(s). | |
* Any fields not specified by calling this method will not be available on the resulting | |
* EfficientGlideRecord object in the callback function after calling .query(). In this | |
* case, a warning will be shown in the console, and .getValue('field_name') will return | |
* a blank string. | |
* If a second argument (getDisplayValue) is not specified and set to true, then the | |
* field's display value will not be available on the resulting EfficientGlideRecord | |
* object in the callback function. In this case, .getDisplayValue('field_name') will | |
* return a blank string. | |
* @param {String} fieldName - The name of the field to retrieve from the server for the | |
* specified record(s). | |
* @param {Boolean} [getDisplayValue=false] - Set this argument to true in order to | |
* retrieve the display value for the specified field. If this is not set to true then | |
* calling .getDisplayValue('field_name') will cause a warning to be logged to the | |
* console, and a blank string will be returned. | |
* @returns {EfficientGlideRecord} | |
* @example | |
* var egrIncident = new EfficientGlideRecord('incident'); | |
* egrIncident.addField('number') | |
* .addField('assignment_group', true) | |
* .addField('assigned_to', true); | |
* | |
* egrIncident.get('some_incident_sys_id', function(egrInc) { | |
* g_form.addInfoMessage( | |
* egrInc.getValue('number') + '\'s assignment group is ' + | |
* egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' + | |
* egrInc.getValue('assignment_group') + ')\n' + | |
* 'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' + | |
* egrInc.getValue('assigned_to') + ')' | |
* ); | |
* }); | |
*/ | |
addField(fieldName, getDisplayValue) { | |
var i; | |
if (!fieldName) { | |
console.error( | |
'Attempted to call .addField() without a field name specified. ' + | |
'Cannot add a blank field to the query.' | |
); | |
return this; | |
} | |
for (i = 0; i < this._config.fields_to_get.length; i++) { | |
if (this._config.fields_to_get[i].name === fieldName) { | |
//If the field name already exists, then bail. | |
console.warn( | |
'Attempted to add field with name ' + fieldName + ' to ' + | |
'EfficientGlideRecord query, but that field already exists. ' + | |
'Cannot add the same field twice.' | |
); | |
return this; | |
} | |
} | |
this._config.fields_to_get.push({ | |
'name' : fieldName, | |
'get_display_value' : (!!getDisplayValue) | |
}); | |
return this; | |
} | |
/** | |
* Add a query to the EfficientGlideRecord object. | |
* By specifying a field name, operator, and value, you can perform all sorts of queries. | |
* If only two arguments are specified, then it's assumed that the first is the field | |
* name and the second is the field value. The operator will automatically be set to "=". | |
* | |
* @param {String} fieldName - The name of the field to perform the query against. | |
* @param {String} [operator="="] - The operator to use for the query. | |
* Valid operators: | |
* Numbers: =, !=, >, >=, <, <= | |
* Strings: =, !=, STARTSWITH, ENDSWITH, CONTAINS, DOES NOT CONTAIN, IN, NOT IN, INSTANCEOF | |
* Note: If only two arguments are specified (fieldValue is not defined), then the second | |
* argument will be treated as the value, and the operator will automatically be set to "=". | |
* @param {String} fieldValue - The value to compare, using the specified operator, against | |
* the specified field. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling (as seen in the example below). | |
* @example | |
* new EfficientGlideRecord('incident') | |
* .setLimit(10) | |
* .addQuery('assignment_group', '!=', 'some_group_sys_id') | |
* .addQuery('assigned_to', 'some_assignee_sys_id') | |
* .addNotNullQuery('assignment_group') | |
* .addField('number') | |
* .addField('short_description') | |
* .addField('assignment_group', true) //Get display value as well | |
* .orderBy('number') | |
* .query(function (egrIncident) { | |
* while (egrIncident.next()) { | |
* console.log( | |
* 'Short description value: ' + egrIncident.getValue('short_description') + | |
* '\n' + | |
* 'Number: ' + egrIncident.getValue('number') + '\n' + | |
* 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' + | |
* egrIncident.getDisplayValue('assignment_group') + ')' | |
* ); | |
* } | |
* }); | |
*/ | |
addQuery(fieldName, operator, fieldValue) { | |
if (typeof fieldValue === 'undefined') { | |
fieldValue = operator; | |
operator = '='; | |
} | |
this._config.queries.push({ | |
'field' : fieldName, | |
'operator' : operator, | |
'value' : fieldValue | |
}); | |
return this; | |
} | |
/** | |
* Shorthand for this.addQuery(fieldName, '!=', 'NULL');. | |
* @param {String} fieldName - The name of the field to ensure is not empty on returned | |
* records. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
* @example | |
* new EfficientGlideRecord('incident') | |
* .setLimit(10) | |
* .addQuery('assignment_group', '!=', 'some_group_sys_id') | |
* .addQuery('assigned_to', 'some_assignee_sys_id') | |
* .addNotNullQuery('assignment_group') | |
* .addField('number') | |
* .addField('short_description') | |
* .addField('assignment_group', true) //Get display value as well | |
* .orderBy('number') | |
* .query(function (egrIncident) { | |
* while (egrIncident.next()) { | |
* console.log( | |
* 'Short description value: ' + egrIncident.getValue('short_description') + | |
* '\n' + | |
* 'Number: ' + egrIncident.getValue('number') + '\n' + | |
* 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' + | |
* egrIncident.getDisplayValue('assignment_group') + ')' | |
* ); | |
* } | |
* }); | |
*/ | |
addNotNullQuery(fieldName) { | |
this.addQuery(fieldName, '!=', 'NULL'); | |
return this; | |
} | |
/** | |
* Shorthand for .addQuery(fieldName, '=', 'NULL') | |
* @param {String} fieldName - The name of the field to use in your query, getting only | |
* records where this field is empty. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
addNullQuery(fieldName) { | |
this.addQuery(fieldName, '=', 'NULL'); | |
return this; | |
} | |
/** | |
* Add an encoded query string to your query. Records matching this encoded query will | |
* be available in your callback function after calling .query(). | |
* @param {String} encodedQueryString - The encoded query string to use in your query. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
addEncodedQuery(encodedQueryString) { | |
if (!encodedQueryString || typeof encodedQueryString !== 'string') { | |
throw new Error( | |
'Invalid encoded query string specified. Encoded query must be a valid ' + | |
'non-empty string.' | |
); | |
} | |
this._config.encoded_queries.push(encodedQueryString); | |
return this; | |
} | |
/** | |
* Very similar to .addEncodedQuery(), except that it REPLACES any existing encoded | |
* queries on the GlideRecord, rather than adding to them. | |
* @param {String} encodedQueryString - The exact encoded query, as a string, to use in | |
* your query. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
setEncodedQuery(encodedQueryString) { | |
//REPLACE existing encoded queries, rather than add to them like .addEncodedQuery(). | |
this._config.encoded_queries = [encodedQueryString]; | |
return this; | |
} | |
/** | |
* Orders the queried table by the specified column, in ascending order | |
* (Alternate call for .orderBy(fieldName).) | |
* @param orderByField | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
addOrderBy(orderByField) { | |
this.orderBy(orderByField); | |
return this; | |
} | |
/** | |
* Orders the queried table by the specified column, in ascending order | |
* @param {String} orderByField - Orders the queried table by the specified column, | |
* in ascending order | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
orderBy(orderByField) { | |
this._config.order_by_field = orderByField; | |
return this; | |
} | |
/** | |
* Orders the queried table by the specified column, in descending order | |
* @param {String} orderByDescField - Orders the queried table by the specified column, | |
* in descending order | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
orderByDesc(orderByDescField) { | |
this._config.order_by_desc_field = orderByDescField; | |
return this; | |
} | |
/** | |
* Limits the number of records queried from the database and | |
* returned to the response. | |
* @param {Number} limit - The limit to use in the database query. No more than this number | |
* of records will be returned. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
setLimit(limit) { | |
if (typeof limit !== 'number' || limit <= 0) { | |
throw new Error( | |
'EfficientGlideRecord.setLimit() method called with an invalid argument. ' + | |
'Limit must be a number greater than zero.' | |
); | |
} | |
this._config.record_limit = limit; | |
return this; | |
} | |
/** | |
* Gets a single record, efficiently, from the database by sys_id. | |
* @param {String} sysID - The sys_id of the record to retrieve. Must be the sys_id of | |
* a valid record which the user has permissions to see, in the table specified in the | |
* constructor when instantiating this object. | |
* @param {function} callbackFn - The callback function to be called when the query is | |
* complete. | |
* When the query is complete, this callback function will be called with one argument: | |
* the EfficientGlideRecord object containing the records resultant from your query. | |
* After querying (in your callback function), you can call methods such as .next() | |
* and .getValue() to iterate through the returned records and get field values. | |
*/ | |
get(sysID, callbackFn) { | |
this.addQuery('sys_id', sysID); | |
this.setLimit(1); | |
this.query(function(egr) { | |
if (egr.next()) { | |
callbackFn(egr); | |
} else { | |
console.warn( | |
'EfficientGlideRecord: No records found in the target table ' + | |
'with sys_id "' + sysID + '".' | |
); | |
} | |
}); | |
} | |
/** | |
* Perform the async query constructed by calling methods in this class, and get the | |
* field(s) from the resultant record that were requested by calling | |
* .addField(fieldName, getDisplayValue) | |
* @async | |
* @param {function} callbackFn - The callback function to be called | |
* when the query is complete. | |
* When the query is complete, this callback function will be called with one argument: | |
* the EfficientGlideRecord object containing the records resultant from your query. | |
* After querying (in your callback function), you can call methods such as .next() | |
* and .getValue() to iterate through the returned records and get field values. | |
*/ | |
query(callbackFn) { | |
let paramName; | |
if (!this._readyToSend()) { | |
//Meaningful errors are logged by this._readyToSend(). | |
return false; | |
} | |
for (paramName in this._config) { | |
//Prevent iteration into non-own properties | |
if (!this._config.hasOwnProperty(paramName)) { | |
continue; | |
} | |
let paramVal; | |
if (typeof this._config[paramName] === 'object') { | |
paramVal = JSON.stringify(this._config[paramName]); | |
} else { | |
paramVal = this._config[paramName]; | |
} | |
this._gaQuery.addParam( | |
paramName, | |
paramVal | |
); | |
} | |
this._gaQuery.getXMLAnswer(function(answer, eGR) { | |
//Make this work in Portal because SN is bad at documentation and consistency | |
if (typeof eGR === 'undefined') { | |
if (typeof this === 'undefined' || this === null) { | |
throw new Error('EfficientGlideRecord ran into a problem. Neither eGR nor the "this" scope are defined. I have no idea how this happened. Better go find Tim and yell at him: https://go.snc.guru/egr'); | |
} else { | |
//If Service Portal blocked access to/nullified the "this" object FOR | |
// SOME FREAKIN REASON, grab it from the binding we did in .query(). | |
eGR = this; | |
} | |
} | |
//Parse answer into a useful object. | |
answer = JSON.parse(answer); | |
//let answer = response.responseXML.documentElement.getAttribute('answer'); | |
// answer = JSON.parse(answer); //Throws if unparseable -- good. | |
if (!answer.hasOwnProperty('_records')) { | |
throw new Error( | |
'Something went wrong when attempting to get records from the server.\n' + | |
'Response object: \n' + | |
JSON.stringify(answer) | |
); | |
} | |
eGR._query_complete = true; | |
eGR._records = answer._records; | |
eGR._row_count = answer._row_count; | |
eGR._executing_as = answer._executing_as; | |
callbackFn(eGR); | |
}.bind(this), null, this); | |
} | |
/* The following methods can only be called after the query is performed */ | |
/** | |
* Check if there is a "next" record to iterate into using .next() (without actually | |
* positioning the current record to the next one). Can only be called from the callback | |
* function passed into .query()/.get() after the query has completed. | |
* @returns {boolean} - True if there is a "next" record to iterate into, or false if not. | |
*/ | |
hasNext() { | |
if (!this._query_complete) { | |
/*throw new Error( | |
'The .hasNext() method of EfficientGlideRecord can only be called from the ' + | |
'callback function after calling .query()' | |
);*/ | |
return false; | |
} | |
return (this._row_count > (this._current_record_index + 1)); | |
} | |
/** | |
* Iterate into the next record, if one exists. | |
* Usage is the same as GlideRecord's .next() method. | |
* @returns {boolean} - True if there was a "next" record, and we've successfully positioned | |
* into it. False if not. Can only be run from the callback function after a query using | |
* .query() or .get(). | |
*/ | |
next() { | |
if (!this._query_complete) { | |
/*throw new Error( | |
'The .next() method of EfficientGlideRecord can only be called from the ' + | |
'callback function after calling .query()' | |
);*/ | |
return false; | |
} | |
if (!this.hasNext()) { | |
return false; | |
} | |
this._current_record_index++; | |
this._current_record = this._records[this._current_record_index]; | |
return true; | |
} | |
/** | |
* Returns true if the specified field exists and can be read (even if it's blank). | |
* Will return false in the following cases: | |
* -The specified field on the current record cannot be read | |
* -The specified field does not exist in the response object (which may happen if you don't | |
* add the field to your request using .addField()). | |
* -The specified field does not exist in the database | |
* @param {String} fieldName - The name of the field to check whether the user can read or not. | |
* @returns {Boolean} - Returns true if the specified field exists and can be read, or | |
* false otherwise. | |
*/ | |
canRead(fieldName) { | |
if (!this._query_complete) { | |
throw new Error( | |
'The .canRead() method of EfficientGlideRecord can only be called from the ' + | |
'callback function after calling .query(callbackFn)' | |
); | |
} | |
if (!this._current_record._field_values.hasOwnProperty(fieldName)) { | |
console.warn( | |
'There is no field with the name ' + fieldName + ' in the ' + | |
'EfficientGlideRecord object. Did you remember to specify that you want to ' + | |
'get that field in the query using .addField()?' | |
); | |
return false; | |
} | |
if (!this._current_record._field_values[fieldName].hasOwnProperty('can_read')) { | |
console.warn( | |
'The requested field "' + fieldName + '" has no can_read node. ' + | |
'This should not happen. Returning a blank false.' | |
); | |
return false; | |
} | |
return (!!this._current_record._field_values[fieldName].can_read) || false; | |
} | |
/** | |
* Retrieve the database value for the specified field, if the user has permissions to read | |
* that field's value. | |
* @param fieldName | |
* @returns {string} | |
*/ | |
getValue(fieldName) { | |
if (!this._query_complete) { | |
throw new Error( | |
'The .getValue() method of EfficientGlideRecord can only be called from the ' + | |
'callback function after calling .query(callbackFn)' | |
); | |
} | |
if (!this._current_record._field_values.hasOwnProperty(fieldName)) { | |
console.warn( | |
'There is no field with the name ' + fieldName + ' in the ' + | |
'EfficientGlideRecord object. Did you remember to specify that you want to ' + | |
'get that field in the query using .addField()?' | |
); | |
return ''; | |
} | |
if (!this._current_record._field_values[fieldName].hasOwnProperty('value')) { | |
console.warn( | |
'The requested field "' + fieldName + '" has no value node. ' + | |
'This should not happen. Returning a blank string.' | |
); | |
return ''; | |
} | |
return this._current_record._field_values[fieldName].value || ''; | |
} | |
/** | |
* Retrieve the display value for the specified field, if the user has permission to view | |
* that field's value. | |
* Can only be called from the callback function after the query is complete. | |
* @param fieldName | |
* @returns {string|*|string} | |
*/ | |
getDisplayValue(fieldName) { | |
if (!this._query_complete) { | |
throw new Error( | |
'The .getDisplayValue() method of EfficientGlideRecord can only be called from the ' + | |
'callback function after calling .query(callbackFn)' | |
); | |
} | |
if (!this._current_record._field_values.hasOwnProperty(fieldName)) { | |
console.warn( | |
'There is no field with the name ' + fieldName + ' in the ' + | |
'EfficientGlideRecord object. Did you remember to specify that you want to ' + | |
'get that field in the query using .addField()?' | |
); | |
return ''; | |
} | |
if ( | |
!this._current_record._field_values[fieldName].hasOwnProperty('display_value') || | |
!this._current_record._field_values[fieldName].display_value | |
) { | |
console.warn( | |
'There is no display value for the field with the name ' + fieldName + | |
' in the EfficientGlideRecord object. Did you remember to specify that you ' + | |
'want to get that field\'s display value in the query using ' + | |
'.addField(fieldName, true)?' | |
); | |
return ''; | |
} | |
return this._current_record._field_values[fieldName].display_value || ''; | |
} | |
/** | |
* Retrieves the number of records returned from the query. | |
* If used in conjunction with .setLimit(), then the maximum value returned from this | |
* method will be the limit number (since no more records than the specified limit can | |
* be returned from the server). | |
* | |
* @returns {number} - The number of records returned from the query. | |
* @example | |
* //Show the number of child Incidents missing Short Descriptions. | |
* new EfficientGlideRecord('incident') | |
* .addQuery('parent', g_form.getUniqueValue()) | |
* .addNullQuery('short_description') | |
* .addField('number') | |
* .query(function (egrIncident) { | |
* if (egrIncident.hasNext()) { | |
* g_form.addErrorMessage( | |
* egrIncident.getRowCount() + ' child Incidents are missing a short | |
* description.' | |
* ); | |
* } | |
* }); | |
* @since 1.0.1 | |
*/ | |
getRowCount() { | |
return this._row_count; | |
} | |
/* Private helper methods below */ | |
_readyToSend() { | |
if (!this._config.table_to_query) { | |
console.error( | |
'EfficientGlideRecord not ready to query. Table name was not specified in ' + | |
'the constructor\'s initialize argument.' | |
); | |
return false; | |
} | |
if (this._config.fields_to_get.length <= 1) { | |
console.warn( | |
'EfficientGlideRecord: No fields other than sys_id were specified ' + | |
'to retrieve. \nYou can specify which fields you want to retrieve from ' + | |
'the GlideRecord object using .addField(fieldName, getDisplayValue). ' + | |
'Afterward, in your callback, you can use .getValue(fieldName). If ' + | |
'you set getDisplayValue to true in .addField(), you can also use ' + | |
'.getDisplayValue(fieldName).\n' + | |
'Without fields to retrieve specified using .addField(), each record ' + | |
'will be returned with only a sys_id. \n' + | |
'This will not prevent you from performing your query, unless ' + | |
'something has gone terribly wrong.' | |
); | |
//Not returning false, because this is not a blocking error. | |
} | |
//Warn if queries AND encoded queries are both empty and limit is unspecified | |
// (but don't return false) | |
if ( | |
( | |
!this._config.hasOwnProperty('queries') || | |
this._config.queries.length < 1 | |
) && | |
( | |
!this._config.hasOwnProperty('encoded_queries') || | |
this._config.encoded_queries.length < 1 | |
) && | |
( | |
!this._config.hasOwnProperty('record_limit') || | |
this._config.record_limit < 1 | |
) | |
) { | |
console.warn( | |
'The EfficientGlideRecord query has no query and no record limit ' + | |
'associated with it. This may result in poor performance when querying larger ' + | |
'tables. Please make sure that you need all records in the specified table, ' + | |
'as all records will be returned by this query.' | |
); | |
} | |
//Return true if none of the above validations have failed. | |
return true; | |
} | |
} |
/* | |
This is a minified, closure-compiled Mobile / Service Portal UI Script | |
containing the EfficientGlideRecord class. | |
See https://go.snc.guru/egr for full usage and API documentation. | |
--- | |
Copyright (c) 2022 Tim Woodruff (https://TimothyWoodruff.com) | |
& SN Pro Tips (https://snprotips.com). | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
Alternative licensing is available upon request. Please contact tim@snc.guru | |
for more info. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
@version 1.0.4 | |
*/ | |
var EfficientGlideRecordPortal=function(a){if(!a)throw Error("EfficientGlideRecord constructor called without a valid tableName argument. Cannot continue.");this._config={table_to_query:a,fields_to_get:[{name:"sys_id",get_display_value:!1}],record_limit:0,order_by_field:"",order_by_desc_field:"",encoded_queries:[],queries:[]};this._row_count=-1;this._query_complete=!1;this._records=[];this._current_record_index=-1;this._current_record={};this._gaQuery=new GlideAjax("ClientGlideRecordAJAX");this._gaQuery.addParam("sysparm_name", | |
"getPseudoGlideRecord");return this}; | |
EfficientGlideRecordPortal.prototype.addField=function(a,b){var c;if(!a)return console.error("Attempted to call .addField() without a field name specified. Cannot add a blank field to the query."),this;for(c=0;c<this._config.fields_to_get.length;c++)if(this._config.fields_to_get[c].name===a)return console.warn("Attempted to add field with name "+a+" to EfficientGlideRecord query, but that field already exists. Cannot add the same field twice."),this;this._config.fields_to_get.push({name:a,get_display_value:!!b}); | |
return this};EfficientGlideRecordPortal.prototype.addQuery=function(a,b,c){"undefined"===typeof c&&(c=b,b="=");this._config.queries.push({field:a,operator:b,value:c});return this};EfficientGlideRecordPortal.prototype.addNotNullQuery=function(a){this.addQuery(a,"!=","NULL");return this};EfficientGlideRecordPortal.prototype.addNullQuery=function(a){this.addQuery(a,"=","NULL");return this}; | |
EfficientGlideRecordPortal.prototype.addEncodedQuery=function(a){if(!a||"string"!==typeof a)throw Error("Invalid encoded query string specified. Encoded query must be a valid non-empty string.");this._config.encoded_queries.push(a);return this};EfficientGlideRecordPortal.prototype.setEncodedQuery=function(a){this._config.encoded_queries=[a];return this};EfficientGlideRecordPortal.prototype.addOrderBy=function(a){this.orderBy(a);return this}; | |
EfficientGlideRecordPortal.prototype.orderBy=function(a){this._config.order_by_field=a;return this};EfficientGlideRecordPortal.prototype.orderByDesc=function(a){this._config.order_by_desc_field=a;return this};EfficientGlideRecordPortal.prototype.setLimit=function(a){if("number"!==typeof a||0>=a)throw Error("EfficientGlideRecord.setLimit() method called with an invalid argument. Limit must be a number greater than zero.");this._config.record_limit=a;return this}; | |
EfficientGlideRecordPortal.prototype.get=function(a,b){this.addQuery("sys_id",a);this.setLimit(1);this.query(function(c){c.next()?b(c):console.warn('EfficientGlideRecord: No records found in the target table with sys_id "'+a+'".')})}; | |
EfficientGlideRecordPortal.prototype.query=function(a){var b;if(!this._readyToSend())return!1;for(b in this._config)if(this._config.hasOwnProperty(b)){var c=void 0;c="object"===typeof this._config[b]?JSON.stringify(this._config[b]):this._config[b];this._gaQuery.addParam(b,c)}this._gaQuery.getXMLAnswer(function(d,e){if("undefined"===typeof e){if("undefined"===typeof this||null===this)throw Error('EfficientGlideRecord ran into a problem. Neither eGR nor the "this" scope are defined. I have no idea how this happened. Better go find Tim and yell at him: https://go.snc.guru/egr'); | |
e=this}d=JSON.parse(d);if(!d.hasOwnProperty("_records"))throw Error("Something went wrong when attempting to get records from the server.\nResponse object: \n"+JSON.stringify(d));e._query_complete=!0;e._records=d._records;e._row_count=d._row_count;e._executing_as=d._executing_as;a(e)}.bind(this),null,this)};EfficientGlideRecordPortal.prototype.hasNext=function(){return this._query_complete?this._row_count>this._current_record_index+1:!1}; | |
EfficientGlideRecordPortal.prototype.next=function(){if(!this._query_complete||!this.hasNext())return!1;this._current_record_index++;this._current_record=this._records[this._current_record_index];return!0}; | |
EfficientGlideRecordPortal.prototype.canRead=function(a){if(!this._query_complete)throw Error("The .canRead() method of EfficientGlideRecord can only be called from the callback function after calling .query(callbackFn)");return this._current_record._field_values.hasOwnProperty(a)?this._current_record._field_values[a].hasOwnProperty("can_read")?!!this._current_record._field_values[a].can_read||!1:(console.warn('The requested field "'+a+'" has no can_read node. This should not happen. Returning a blank false.'), | |
!1):(console.warn("There is no field with the name "+a+" in the EfficientGlideRecord object. Did you remember to specify that you want to get that field in the query using .addField()?"),!1)}; | |
EfficientGlideRecordPortal.prototype.getValue=function(a){if(!this._query_complete)throw Error("The .getValue() method of EfficientGlideRecord can only be called from the callback function after calling .query(callbackFn)");return this._current_record._field_values.hasOwnProperty(a)?this._current_record._field_values[a].hasOwnProperty("value")?this._current_record._field_values[a].value||"":(console.warn('The requested field "'+a+'" has no value node. This should not happen. Returning a blank string.'), | |
""):(console.warn("There is no field with the name "+a+" in the EfficientGlideRecord object. Did you remember to specify that you want to get that field in the query using .addField()?"),"")}; | |
EfficientGlideRecordPortal.prototype.getDisplayValue=function(a){if(!this._query_complete)throw Error("The .getDisplayValue() method of EfficientGlideRecord can only be called from the callback function after calling .query(callbackFn)");return this._current_record._field_values.hasOwnProperty(a)?this._current_record._field_values[a].hasOwnProperty("display_value")&&this._current_record._field_values[a].display_value?this._current_record._field_values[a].display_value||"":(console.warn("There is no display value for the field with the name "+ | |
a+" in the EfficientGlideRecord object. Did you remember to specify that you want to get that field's display value in the query using .addField(fieldName, true)?"),""):(console.warn("There is no field with the name "+a+" in the EfficientGlideRecord object. Did you remember to specify that you want to get that field in the query using .addField()?"),"")};EfficientGlideRecordPortal.prototype.getRowCount=function(){return this._row_count}; | |
EfficientGlideRecordPortal.prototype._readyToSend=function(){if(!this._config.table_to_query)return console.error("EfficientGlideRecord not ready to query. Table name was not specified in the constructor's initialize argument."),!1;1>=this._config.fields_to_get.length&&console.warn("EfficientGlideRecord: No fields other than sys_id were specified to retrieve. \nYou can specify which fields you want to retrieve from the GlideRecord object using .addField(fieldName, getDisplayValue). Afterward, in your callback, you can use .getValue(fieldName). If you set getDisplayValue to true in .addField(), you can also use .getDisplayValue(fieldName).\nWithout fields to retrieve specified using .addField(), each record will be returned with only a sys_id. \nThis will not prevent you from performing your query, unless something has gone terribly wrong."); | |
(!this._config.hasOwnProperty("queries")||1>this._config.queries.length)&&(!this._config.hasOwnProperty("encoded_queries")||1>this._config.encoded_queries.length)&&(!this._config.hasOwnProperty("record_limit")||1>this._config.record_limit)&&console.warn("The EfficientGlideRecord query has no query and no record limit associated with it. This may result in poor performance when querying larger tables. Please make sure that you need all records in the specified table, as all records will be returned by this query."); | |
return!0};var EfficientGlideRecord=EfficientGlideRecordPortal; |
/** | |
* UI Script (Mobile / Service Portal) | |
* Please add this (or better yet, the minified version of this file) as a JS Include | |
* to the Theme that you're using on any Service Portals used by your organization. | |
* @description See related article for full usage instructions and API | |
* documentation: | |
* https://go.snc.guru/egr | |
* @classdesc https://go.snc.guru/egr | |
* @author | |
* Tim Woodruff (https://TimothyWoodruff.com) | |
* SN Pro Tips (https://snprotips.com) | |
* @version 1.0.4 | |
* @class | |
* | |
* @license | |
* Copyright (c) 2022 Tim Woodruff (https://TimothyWoodruff.com) | |
* & SN Pro Tips (https://snprotips.com). | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* Alternative licensing is available upon request. Please contact tim@snc.guru | |
* for more info. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ | |
class EfficientGlideRecordPortal { | |
/** | |
* Instantiated with the 'new' keyword (as classes typically are when instantiated), this | |
* will construct a client-side EfficientGlideRecord object. The methods of this class can | |
* then be called to construct a client-side GlideRecord query. EfficientGlideRecord | |
* replicates *most* of the functionality of the client-side GlideRecord object, but | |
* with more and enhanced functionality. | |
* EfficientGlideRecord is FAR preferable to using the out-of-box (OOB) client-side | |
* GlideRecord query (even asynchronously), because GlideRecord returns a massive amount | |
* of unnecessary data, and can be much, much slower. EfficientGlideRecord aims to return | |
* only that data which is necessary and requested from the server, thus providing an | |
* efficient interface to query records asynchronously without all the additional overhead | |
* related to information that you don't need. | |
* | |
* Additional documentation can be found on the SN Pro Tips blog, at https://go.snc.guru/egr | |
* NOTE: For info on performing async queries in onSubmit Client Scripts, see | |
* https://go.snc.guru/onsubmit | |
* | |
* @param {String} tableName - The name of the table on which to execute your GlideRecord query | |
* @returns {EfficientGlideRecord} | |
* @example | |
* var egrIncident = new EfficientGlideRecord('incident'); | |
* egrIncident.addField('number') | |
* .addField('assignment_group', true) | |
* .addField('assigned_to', true); | |
* | |
* egrIncident.get('some_incident_sys_id', function(egrInc) { | |
* g_form.addInfoMessage( | |
* egrInc.getValue('number') + '\'s assignment group is ' + | |
* egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' + | |
* egrInc.getValue('assignment_group') + ')\n' + | |
* 'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' + | |
* egrInc.getValue('assigned_to') + ')' | |
* ); | |
* }); | |
* @constructor | |
*/ | |
constructor(tableName) { | |
if (!tableName) { | |
throw new Error( | |
'EfficientGlideRecord constructor called without a valid tableName ' + | |
'argument. Cannot continue.' | |
); | |
} | |
this._config = { | |
'table_to_query' : tableName, | |
'fields_to_get' : [{ | |
'name' : 'sys_id', | |
'get_display_value' : false | |
}], | |
'record_limit' : 0, | |
'order_by_field' : '', | |
'order_by_desc_field' : '', | |
'encoded_queries' : [], | |
'queries' : [] | |
}; | |
this._row_count = -1; | |
this._query_complete = false; | |
this._records = []; | |
this._current_record_index = -1; | |
this._current_record = {}; | |
this._gaQuery = new GlideAjax('ClientGlideRecordAJAX'); | |
this._gaQuery.addParam('sysparm_name', 'getPseudoGlideRecord'); | |
return this; | |
} | |
/** | |
* Add a field to retrieve from the target record(s). | |
* Any fields not specified by calling this method will not be available on the resulting | |
* EfficientGlideRecord object in the callback function after calling .query(). In this | |
* case, a warning will be shown in the console, and .getValue('field_name') will return | |
* a blank string. | |
* If a second argument (getDisplayValue) is not specified and set to true, then the | |
* field's display value will not be available on the resulting EfficientGlideRecord | |
* object in the callback function. In this case, .getDisplayValue('field_name') will | |
* return a blank string. | |
* @param {String} fieldName - The name of the field to retrieve from the server for the | |
* specified record(s). | |
* @param {Boolean} [getDisplayValue=false] - Set this argument to true in order to | |
* retrieve the display value for the specified field. If this is not set to true then | |
* calling .getDisplayValue('field_name') will cause a warning to be logged to the | |
* console, and a blank string will be returned. | |
* @returns {EfficientGlideRecord} | |
* @example | |
* var egrIncident = new EfficientGlideRecord('incident'); | |
* egrIncident.addField('number') | |
* .addField('assignment_group', true) | |
* .addField('assigned_to', true); | |
* | |
* egrIncident.get('some_incident_sys_id', function(egrInc) { | |
* g_form.addInfoMessage( | |
* egrInc.getValue('number') + '\'s assignment group is ' + | |
* egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' + | |
* egrInc.getValue('assignment_group') + ')\n' + | |
* 'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' + | |
* egrInc.getValue('assigned_to') + ')' | |
* ); | |
* }); | |
*/ | |
addField(fieldName, getDisplayValue) { | |
var i; | |
if (!fieldName) { | |
console.error( | |
'Attempted to call .addField() without a field name specified. ' + | |
'Cannot add a blank field to the query.' | |
); | |
return this; | |
} | |
for (i = 0; i < this._config.fields_to_get.length; i++) { | |
if (this._config.fields_to_get[i].name === fieldName) { | |
//If the field name already exists, then bail. | |
console.warn( | |
'Attempted to add field with name ' + fieldName + ' to ' + | |
'EfficientGlideRecord query, but that field already exists. ' + | |
'Cannot add the same field twice.' | |
); | |
return this; | |
} | |
} | |
this._config.fields_to_get.push({ | |
'name' : fieldName, | |
'get_display_value' : (!!getDisplayValue) | |
}); | |
return this; | |
} | |
/** | |
* Add a query to the EfficientGlideRecord object. | |
* By specifying a field name, operator, and value, you can perform all sorts of queries. | |
* If only two arguments are specified, then it's assumed that the first is the field | |
* name and the second is the field value. The operator will automatically be set to "=". | |
* | |
* @param {String} fieldName - The name of the field to perform the query against. | |
* @param {String} [operator="="] - The operator to use for the query. | |
* Valid operators: | |
* Numbers: =, !=, >, >=, <, <= | |
* Strings: =, !=, STARTSWITH, ENDSWITH, CONTAINS, DOES NOT CONTAIN, IN, NOT IN, INSTANCEOF | |
* Note: If only two arguments are specified (fieldValue is not defined), then the second | |
* argument will be treated as the value, and the operator will automatically be set to "=". | |
* @param {String} fieldValue - The value to compare, using the specified operator, against | |
* the specified field. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling (as seen in the example below). | |
* @example | |
* new EfficientGlideRecord('incident') | |
* .setLimit(10) | |
* .addQuery('assignment_group', '!=', 'some_group_sys_id') | |
* .addQuery('assigned_to', 'some_assignee_sys_id') | |
* .addNotNullQuery('assignment_group') | |
* .addField('number') | |
* .addField('short_description') | |
* .addField('assignment_group', true) //Get display value as well | |
* .orderBy('number') | |
* .query(function (egrIncident) { | |
* while (egrIncident.next()) { | |
* console.log( | |
* 'Short description value: ' + egrIncident.getValue('short_description') + | |
* '\n' + | |
* 'Number: ' + egrIncident.getValue('number') + '\n' + | |
* 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' + | |
* egrIncident.getDisplayValue('assignment_group') + ')' | |
* ); | |
* } | |
* }); | |
*/ | |
addQuery(fieldName, operator, fieldValue) { | |
if (typeof fieldValue === 'undefined') { | |
fieldValue = operator; | |
operator = '='; | |
} | |
this._config.queries.push({ | |
'field' : fieldName, | |
'operator' : operator, | |
'value' : fieldValue | |
}); | |
return this; | |
} | |
/** | |
* Shorthand for this.addQuery(fieldName, '!=', 'NULL');. | |
* @param {String} fieldName - The name of the field to ensure is not empty on returned | |
* records. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
* @example | |
* new EfficientGlideRecord('incident') | |
* .setLimit(10) | |
* .addQuery('assignment_group', '!=', 'some_group_sys_id') | |
* .addQuery('assigned_to', 'some_assignee_sys_id') | |
* .addNotNullQuery('assignment_group') | |
* .addField('number') | |
* .addField('short_description') | |
* .addField('assignment_group', true) //Get display value as well | |
* .orderBy('number') | |
* .query(function (egrIncident) { | |
* while (egrIncident.next()) { | |
* console.log( | |
* 'Short description value: ' + egrIncident.getValue('short_description') + | |
* '\n' + | |
* 'Number: ' + egrIncident.getValue('number') + '\n' + | |
* 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' + | |
* egrIncident.getDisplayValue('assignment_group') + ')' | |
* ); | |
* } | |
* }); | |
*/ | |
addNotNullQuery(fieldName) { | |
this.addQuery(fieldName, '!=', 'NULL'); | |
return this; | |
} | |
/** | |
* Shorthand for .addQuery(fieldName, '=', 'NULL') | |
* @param {String} fieldName - The name of the field to use in your query, getting only | |
* records where this field is empty. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
addNullQuery(fieldName) { | |
this.addQuery(fieldName, '=', 'NULL'); | |
return this; | |
} | |
/** | |
* Add an encoded query string to your query. Records matching this encoded query will | |
* be available in your callback function after calling .query(). | |
* @param {String} encodedQueryString - The encoded query string to use in your query. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
addEncodedQuery(encodedQueryString) { | |
if (!encodedQueryString || typeof encodedQueryString !== 'string') { | |
throw new Error( | |
'Invalid encoded query string specified. Encoded query must be a valid ' + | |
'non-empty string.' | |
); | |
} | |
this._config.encoded_queries.push(encodedQueryString); | |
return this; | |
} | |
/** | |
* Very similar to .addEncodedQuery(), except that it REPLACES any existing encoded | |
* queries on the GlideRecord, rather than adding to them. | |
* @param {String} encodedQueryString - The exact encoded query, as a string, to use in | |
* your query. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
setEncodedQuery(encodedQueryString) { | |
//REPLACE existing encoded queries, rather than add to them like .addEncodedQuery(). | |
this._config.encoded_queries = [encodedQueryString]; | |
return this; | |
} | |
/** | |
* Orders the queried table by the specified column, in ascending order | |
* (Alternate call for .orderBy(fieldName).) | |
* @param orderByField | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
addOrderBy(orderByField) { | |
this.orderBy(orderByField); | |
return this; | |
} | |
/** | |
* Orders the queried table by the specified column, in ascending order | |
* @param {String} orderByField - Orders the queried table by the specified column, | |
* in ascending order | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
orderBy(orderByField) { | |
this._config.order_by_field = orderByField; | |
return this; | |
} | |
/** | |
* Orders the queried table by the specified column, in descending order | |
* @param {String} orderByDescField - Orders the queried table by the specified column, | |
* in descending order | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
orderByDesc(orderByDescField) { | |
this._config.order_by_desc_field = orderByDescField; | |
return this; | |
} | |
/** | |
* Limits the number of records queried from the database and | |
* returned to the response. | |
* @param {Number} limit - The limit to use in the database query. No more than this number | |
* of records will be returned. | |
* @returns {EfficientGlideRecord} - Returns the instantiated object for optional | |
* chain-calling. | |
*/ | |
setLimit(limit) { | |
if (typeof limit !== 'number' || limit <= 0) { | |
throw new Error( | |
'EfficientGlideRecord.setLimit() method called with an invalid argument. ' + | |
'Limit must be a number greater than zero.' | |
); | |
} | |
this._config.record_limit = limit; | |
return this; | |
} | |
/** | |
* Gets a single record, efficiently, from the database by sys_id. | |
* @param {String} sysID - The sys_id of the record to retrieve. Must be the sys_id of | |
* a valid record which the user has permissions to see, in the table specified in the | |
* constructor when instantiating this object. | |
* @param {function} callbackFn - The callback function to be called when the query is | |
* complete. | |
* When the query is complete, this callback function will be called with one argument: | |
* the EfficientGlideRecord object containing the records resultant from your query. | |
* After querying (in your callback function), you can call methods such as .next() | |
* and .getValue() to iterate through the returned records and get field values. | |
*/ | |
get(sysID, callbackFn) { | |
this.addQuery('sys_id', sysID); | |
this.setLimit(1); | |
this.query(function(egr) { | |
if (egr.next()) { | |
callbackFn(egr); | |
} else { | |
console.warn( | |
'EfficientGlideRecord: No records found in the target table ' + | |
'with sys_id "' + sysID + '".' | |
); | |
} | |
}); | |
} | |
/** | |
* Perform the async query constructed by calling methods in this class, and get the | |
* field(s) from the resultant record that were requested by calling | |
* .addField(fieldName, getDisplayValue) | |
* @async | |
* @param {function} callbackFn - The callback function to be called | |
* when the query is complete. | |
* When the query is complete, this callback function will be called with one argument: | |
* the EfficientGlideRecord object containing the records resultant from your query. | |
* After querying (in your callback function), you can call methods such as .next() | |
* and .getValue() to iterate through the returned records and get field values. | |
*/ | |
query(callbackFn) { | |
let paramName; | |
if (!this._readyToSend()) { | |
//Meaningful errors are logged by this._readyToSend(). | |
return false; | |
} | |
for (paramName in this._config) { | |
//Prevent iteration into non-own properties | |
if (!this._config.hasOwnProperty(paramName)) { | |
continue; | |
} | |
let paramVal; | |
if (typeof this._config[paramName] === 'object') { | |
paramVal = JSON.stringify(this._config[paramName]); | |
} else { | |
paramVal = this._config[paramName]; | |
} | |
this._gaQuery.addParam( | |
paramName, | |
paramVal | |
); | |
} | |
this._gaQuery.getXMLAnswer(function(answer, eGR) { | |
//Make this work in Portal because SN is bad at documentation and consistency | |
if (typeof eGR === 'undefined') { | |
if (typeof this === 'undefined' || this === null) { | |
throw new Error('EfficientGlideRecord ran into a problem. Neither eGR nor the "this" scope are defined. I have no idea how this happened. Better go find Tim and yell at him: https://go.snc.guru/egr'); | |
} else { | |
//If Service Portal blocked access to/nullified the "this" object FOR | |
// SOME FREAKIN REASON, grab it from the binding we did in .query(). | |
eGR = this; | |
} | |
} | |
//Parse answer into a useful object. | |
answer = JSON.parse(answer); | |
//let answer = response.responseXML.documentElement.getAttribute('answer'); | |
// answer = JSON.parse(answer); //Throws if unparseable -- good. | |
if (!answer.hasOwnProperty('_records')) { | |
throw new Error( | |
'Something went wrong when attempting to get records from the server.\n' + | |
'Response object: \n' + | |
JSON.stringify(answer) | |
); | |
} | |
eGR._query_complete = true; | |
eGR._records = answer._records; | |
eGR._row_count = answer._row_count; | |
eGR._executing_as = answer._executing_as; | |
callbackFn(eGR); | |
}.bind(this), null, this); | |
} | |
/* The following methods can only be called after the query is performed */ | |
/** | |
* Check if there is a "next" record to iterate into using .next() (without actually | |
* positioning the current record to the next one). Can only be called from the callback | |
* function passed into .query()/.get() after the query has completed. | |
* @returns {boolean} - True if there is a "next" record to iterate into, or false if not. | |
*/ | |
hasNext() { | |
if (!this._query_complete) { | |
/*throw new Error( | |
'The .hasNext() method of EfficientGlideRecord can only be called from the ' + | |
'callback function after calling .query()' | |
);*/ | |
return false; | |
} | |
return (this._row_count > (this._current_record_index + 1)); | |
} | |
/** | |
* Iterate into the next record, if one exists. | |
* Usage is the same as GlideRecord's .next() method. | |
* @returns {boolean} - True if there was a "next" record, and we've successfully positioned | |
* into it. False if not. Can only be run from the callback function after a query using | |
* .query() or .get(). | |
*/ | |
next() { | |
if (!this._query_complete) { | |
/*throw new Error( | |
'The .next() method of EfficientGlideRecord can only be called from the ' + | |
'callback function after calling .query()' | |
);*/ | |
return false; | |
} | |
if (!this.hasNext()) { | |
return false; | |
} | |
this._current_record_index++; | |
this._current_record = this._records[this._current_record_index]; | |
return true; | |
} | |
/** | |
* Returns true if the specified field exists and can be read (even if it's blank). | |
* Will return false in the following cases: | |
* -The specified field on the current record cannot be read | |
* -The specified field does not exist in the response object (which may happen if you don't | |
* add the field to your request using .addField()). | |
* -The specified field does not exist in the database | |
* @param {String} fieldName - The name of the field to check whether the user can read or not. | |
* @returns {Boolean} - Returns true if the specified field exists and can be read, or | |
* false otherwise. | |
*/ | |
canRead(fieldName) { | |
if (!this._query_complete) { | |
throw new Error( | |
'The .canRead() method of EfficientGlideRecord can only be called from the ' + | |
'callback function after calling .query(callbackFn)' | |
); | |
} | |
if (!this._current_record._field_values.hasOwnProperty(fieldName)) { | |
console.warn( | |
'There is no field with the name ' + fieldName + ' in the ' + | |
'EfficientGlideRecord object. Did you remember to specify that you want to ' + | |
'get that field in the query using .addField()?' | |
); | |
return false; | |
} | |
if (!this._current_record._field_values[fieldName].hasOwnProperty('can_read')) { | |
console.warn( | |
'The requested field "' + fieldName + '" has no can_read node. ' + | |
'This should not happen. Returning a blank false.' | |
); | |
return false; | |
} | |
return (!!this._current_record._field_values[fieldName].can_read) || false; | |
} | |
/** | |
* Retrieve the database value for the specified field, if the user has permissions to read | |
* that field's value. | |
* @param fieldName | |
* @returns {string} | |
*/ | |
getValue(fieldName) { | |
if (!this._query_complete) { | |
throw new Error( | |
'The .getValue() method of EfficientGlideRecord can only be called from the ' + | |
'callback function after calling .query(callbackFn)' | |
); | |
} | |
if (!this._current_record._field_values.hasOwnProperty(fieldName)) { | |
console.warn( | |
'There is no field with the name ' + fieldName + ' in the ' + | |
'EfficientGlideRecord object. Did you remember to specify that you want to ' + | |
'get that field in the query using .addField()?' | |
); | |
return ''; | |
} | |
if (!this._current_record._field_values[fieldName].hasOwnProperty('value')) { | |
console.warn( | |
'The requested field "' + fieldName + '" has no value node. ' + | |
'This should not happen. Returning a blank string.' | |
); | |
return ''; | |
} | |
return this._current_record._field_values[fieldName].value || ''; | |
} | |
/** | |
* Retrieve the display value for the specified field, if the user has permission to view | |
* that field's value. | |
* Can only be called from the callback function after the query is complete. | |
* @param fieldName | |
* @returns {string|*|string} | |
*/ | |
getDisplayValue(fieldName) { | |
if (!this._query_complete) { | |
throw new Error( | |
'The .getDisplayValue() method of EfficientGlideRecord can only be called from the ' + | |
'callback function after calling .query(callbackFn)' | |
); | |
} | |
if (!this._current_record._field_values.hasOwnProperty(fieldName)) { | |
console.warn( | |
'There is no field with the name ' + fieldName + ' in the ' + | |
'EfficientGlideRecord object. Did you remember to specify that you want to ' + | |
'get that field in the query using .addField()?' | |
); | |
return ''; | |
} | |
if ( | |
!this._current_record._field_values[fieldName].hasOwnProperty('display_value') || | |
!this._current_record._field_values[fieldName].display_value | |
) { | |
console.warn( | |
'There is no display value for the field with the name ' + fieldName + | |
' in the EfficientGlideRecord object. Did you remember to specify that you ' + | |
'want to get that field\'s display value in the query using ' + | |
'.addField(fieldName, true)?' | |
); | |
return ''; | |
} | |
return this._current_record._field_values[fieldName].display_value || ''; | |
} | |
/** | |
* Retrieves the number of records returned from the query. | |
* If used in conjunction with .setLimit(), then the maximum value returned from this | |
* method will be the limit number (since no more records than the specified limit can | |
* be returned from the server). | |
* | |
* @returns {number} - The number of records returned from the query. | |
* @example | |
* //Show the number of child Incidents missing Short Descriptions. | |
* new EfficientGlideRecord('incident') | |
* .addQuery('parent', g_form.getUniqueValue()) | |
* .addNullQuery('short_description') | |
* .addField('number') | |
* .query(function (egrIncident) { | |
* if (egrIncident.hasNext()) { | |
* g_form.addErrorMessage( | |
* egrIncident.getRowCount() + ' child Incidents are missing a short | |
* description.' | |
* ); | |
* } | |
* }); | |
* @since 1.0.1 | |
*/ | |
getRowCount() { | |
return this._row_count; | |
} | |
/* Private helper methods below */ | |
_readyToSend() { | |
if (!this._config.table_to_query) { | |
console.error( | |
'EfficientGlideRecord not ready to query. Table name was not specified in ' + | |
'the constructor\'s initialize argument.' | |
); | |
return false; | |
} | |
if (this._config.fields_to_get.length <= 1) { | |
console.warn( | |
'EfficientGlideRecord: No fields other than sys_id were specified ' + | |
'to retrieve. \nYou can specify which fields you want to retrieve from ' + | |
'the GlideRecord object using .addField(fieldName, getDisplayValue). ' + | |
'Afterward, in your callback, you can use .getValue(fieldName). If ' + | |
'you set getDisplayValue to true in .addField(), you can also use ' + | |
'.getDisplayValue(fieldName).\n' + | |
'Without fields to retrieve specified using .addField(), each record ' + | |
'will be returned with only a sys_id. \n' + | |
'This will not prevent you from performing your query, unless ' + | |
'something has gone terribly wrong.' | |
); | |
//Not returning false, because this is not a blocking error. | |
} | |
//Warn if queries AND encoded queries are both empty and limit is unspecified | |
// (but don't return false) | |
if ( | |
( | |
!this._config.hasOwnProperty('queries') || | |
this._config.queries.length < 1 | |
) && | |
( | |
!this._config.hasOwnProperty('encoded_queries') || | |
this._config.encoded_queries.length < 1 | |
) && | |
( | |
!this._config.hasOwnProperty('record_limit') || | |
this._config.record_limit < 1 | |
) | |
) { | |
console.warn( | |
'The EfficientGlideRecord query has no query and no record limit ' + | |
'associated with it. This may result in poor performance when querying larger ' + | |
'tables. Please make sure that you need all records in the specified table, ' + | |
'as all records will be returned by this query.' | |
); | |
} | |
//Return true if none of the above validations have failed. | |
return true; | |
} | |
} | |
const EfficientGlideRecord = EfficientGlideRecordPortal; |
- March 2025
- March 2024
-
February 2024
- Feb 12, 2024 5 Lessons About Programming From Richard Feynman
- July 2023
- April 2023
-
December 2022
- Dec 13, 2022 ServiceNow Developers: BE THE GUIDE!
- October 2022
-
August 2022
- Aug 23, 2022 Using .addJoinQuery() & How to Query Records with Attachments in ServiceNow
- Aug 18, 2022 Free, Simple URL Shortener for ServiceNow Nerds (snc.guru)
- Aug 16, 2022 How to Get and Parse ServiceNow Journal Entries as Strings/HTML
- Aug 14, 2022 New tool: Get Latest Version of ServiceNow Docs Page
- March 2022
- February 2022
- May 2021
- April 2021
- February 2021
-
November 2020
- Nov 17, 2020 SN Guys is now part of Jahnel Group!
- September 2020
- July 2020
-
January 2020
- Jan 20, 2020 Getting Help from the ServiceNow Community
- December 2019
- November 2019
-
April 2019
- Apr 21, 2019 Understanding Attachments in ServiceNow
- Apr 10, 2019 Using Custom Search Engines in Chrome to Quickly Navigate ServiceNow
- Apr 4, 2019 Set Catalog Variables from URL Params (Free tool)
- Apr 1, 2019 Outlook for Android Breaks Email Approvals (+Solution)
- March 2019
-
February 2019
- Feb 27, 2019 Making Update Sets Smarter - Free Tool
-
November 2018
- Nov 29, 2018 How to Learn ServiceNow
- Nov 6, 2018 ServiceNow & ITSM as a Career?
- October 2018
- September 2018
-
July 2018
- Jul 23, 2018 Admin Duty Separation with a Single Account
-
June 2018
- Jun 19, 2018 Improving Performance on Older Instances with Table Rotation
- Jun 4, 2018 New Free Tool: Login Link Generator
-
May 2018
- May 29, 2018 Learning ServiceNow: Second Edition!
- April 2018
- March 2018
-
February 2018
- Feb 11, 2018 We have a new book!
- November 2017
-
September 2017
- Sep 12, 2017 Handling TimeZones in ServiceNow (TimeZoneUtil)
- July 2017
-
June 2017
- Jun 25, 2017 What's New in ServiceNow: Jakarta (Pt. 1)
- Jun 4, 2017 Powerful Scripted Text Search in ServiceNow
- May 2017
- April 2017
-
March 2017
- Mar 12, 2017 reCAPTCHA in ServiceNow CMS/Service Portal
-
December 2016
- Dec 20, 2016 Pro Tip: Use updateMultiple() for Maximum Efficiency!
- Dec 2, 2016 We're Writing a Book!
-
November 2016
- Nov 10, 2016 Chrome Extension: Load in ServiceNow Frame
- September 2016
-
July 2016
- Jul 17, 2016 Granting Temporary Roles/Groups in ServiceNow
- Jul 15, 2016 Scripted REST APIs & Retrieving RITM Variables via SRAPI
-
May 2016
- May 17, 2016 What's New in Helsinki?
-
April 2016
- Apr 27, 2016 Customizing UI16 Through CSS and System Properties
- Apr 5, 2016 ServiceNow Versions: Express Vs. Enterprise
-
March 2016
- Mar 28, 2016 Update Set Collision Avoidance Tool: V2
- Mar 18, 2016 ServiceNow: What's New in Geneva & UI16 (Pt. 2)
-
February 2016
- Feb 22, 2016 Reference Field Auto-Complete Attributes
- Feb 6, 2016 GlideRecord & GlideAjax: Client-Side Vs. Server-Side
- Feb 1, 2016 Make Your Log Entries Easier to Find
-
January 2016
- Jan 29, 2016 A Better, One-Click Approval
- Jan 25, 2016 Quickly Move Changes Between Update Sets
- Jan 20, 2016 Customize the Reference Icon Pop-up
- Jan 7, 2016 ServiceNow: Geneva & UI16 - What's new
- Jan 4, 2016 Detect/Prevent Update Set Conflicts Before They Happen
-
December 2015
- Dec 28, 2015 SN101: Boolean logic and ServiceNow's Condition Builder
- Dec 17, 2015 Locate any record in any table, by sys_id in ServiceNow
- Dec 16, 2015 Detecting Duplicate Records with GlideAggregate
- Dec 11, 2015 Array.indexOf() not working in ServiceNow - Solution!
- Dec 2, 2015 Understanding Dynamic Filters & Checking a Record Against a Filter Using GlideFilter
- October 2015
-
August 2015
- Aug 27, 2015 Easily Clone One User's Access to Another User