/** * 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;