├── README.md ├── LICENSE ├── UI Scripts ├── Minified │ ├── EfficientGlideRecord (Desktop Global).js │ └── EfficientGlideRecordPortal (Portal JS Include).js ├── EfficientGlideRecord (Desktop Global).js └── EfficientGlideRecordPortal (Portal-JS Include).js └── Script Includes └── ClientGlideRecordAJAX.js /README.md: -------------------------------------------------------------------------------- 1 | # ServiceNow EfficientGlideRecord 2 | 3 | Client-side EfficientGlideRecord class with a few modifications, added features, and DRASTICALLY improved performance as compared with ServiceNow's out-of-box client-side GlideRecord API class. 4 | 5 | Full article and detailed API documentation at: https://egr.snc.guru 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Tim Woodruff (https://TimothyWoodruff.com) 2 | & SN Pro Tips (https://snprotips.com). 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | Alternative licensing is available upon request. Please contact tim@snc.guru 15 | for more info. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /UI Scripts/Minified/EfficientGlideRecord (Desktop Global).js: -------------------------------------------------------------------------------- 1 | /* 2 | This is a minified, closure-compiled Global Desktop UI Script containing 3 | the EfficientGlideRecord class. 4 | See https://egr.snc.guru for full usage and API documentation. 5 | --- 6 | Copyright (c) 2022 Tim Woodruff (https://TimothyWoodruff.com) 7 | & SN Pro Tips (https://snprotips.com). 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | Alternative licensing is available upon request. Please contact tim@snc.guru 17 | for more info. 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | @version 1.0.4 26 | */ 27 | 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", 28 | "getPseudoGlideRecord");return this}; 29 | 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=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}; 33 | 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+'".')})}; 34 | 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://egr.snc.guru'); 35 | 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}; 36 | 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}; 37 | 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.'), 38 | !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)}; 39 | 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.'), 40 | ""):(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()?"),"")}; 41 | 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 "+ 42 | 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}; 43 | 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."); 44 | (!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."); 45 | return!0}; 46 | -------------------------------------------------------------------------------- /UI Scripts/Minified/EfficientGlideRecordPortal (Portal JS Include).js: -------------------------------------------------------------------------------- 1 | /* 2 | This is a minified, closure-compiled Mobile / Service Portal UI Script 3 | containing the EfficientGlideRecord class. 4 | See https://egr.snc.guru for full usage and API documentation. 5 | --- 6 | Copyright (c) 2022 Tim Woodruff (https://TimothyWoodruff.com) 7 | & SN Pro Tips (https://snprotips.com). 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | Alternative licensing is available upon request. Please contact tim@snc.guru 17 | for more info. 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | @version 1.0.4 26 | */ 27 | 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", 28 | "getPseudoGlideRecord");return this}; 29 | 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=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}; 33 | 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+'".')})}; 34 | 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://egr.snc.guru'); 35 | 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}; 36 | 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}; 37 | 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.'), 38 | !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)}; 39 | 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.'), 40 | ""):(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()?"),"")}; 41 | 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 "+ 42 | 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}; 43 | 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."); 44 | (!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."); 45 | return!0};var EfficientGlideRecord=EfficientGlideRecordPortal; 46 | -------------------------------------------------------------------------------- /Script Includes/ClientGlideRecordAJAX.js: -------------------------------------------------------------------------------- 1 | /* 2 | Server-side client-callable Script Include. 3 | See related article for full usage instructions and API documentation: 4 | https://egr.snc.guru 5 | @version 1.0.4 6 | */ 7 | var ClientGlideRecordAJAX = Class.create(); 8 | ClientGlideRecordAJAX.prototype = Object.extendsObject(AbstractAjaxProcessor, { 9 | 10 | gr_config : {}, 11 | 12 | getPseudoGlideRecord : function() { 13 | var grQuery; 14 | var responseObj = { 15 | '_records' : [], 16 | '_row_count' : 0, 17 | '_config' : {}, 18 | '_executing_as' : { 19 | 'user_name' : gs.getUserName(), 20 | 'user_id' : gs.getUserID() 21 | } 22 | }; 23 | 24 | this.gr_config = {}; 25 | 26 | this.gr_config.table_to_query = this.getParameter('table_to_query'); 27 | //@type {{get_display_value: boolean, name: string}} 28 | this.gr_config.fields_to_get = this.getParameter('fields_to_get'); 29 | this.gr_config.record_limit = this.getParameter('record_limit'); 30 | this.gr_config.order_by_field = this.getParameter('order_by_field'); 31 | this.gr_config.order_by_desc_field = this.getParameter('order_by_desc_field'); 32 | this.gr_config.encoded_queries = this.getParameter('encoded_queries'); 33 | this.gr_config.queries = this.getParameter('queries'); 34 | 35 | //Parse queries/encoded queries array and fields_to_get object 36 | if (this.gr_config.hasOwnProperty('queries') && this.gr_config.queries) { 37 | this.gr_config.queries = JSON.parse(this.gr_config.queries); 38 | } 39 | if (this.gr_config.hasOwnProperty('fields_to_get') && this.gr_config.fields_to_get) { 40 | this.gr_config.fields_to_get = JSON.parse(this.gr_config.fields_to_get); 41 | } 42 | if (this.gr_config.hasOwnProperty('encoded_queries') && this.gr_config.encoded_queries) { 43 | this.gr_config.encoded_queries = JSON.parse(this.gr_config.encoded_queries); 44 | } 45 | 46 | gs.debug('EfficientGlideRecord config: \n' + JSON.stringify(this.gr_config, null, 2)); 47 | 48 | if (!this._validateMandatoryConfig()) { 49 | throw new Error( 50 | 'Mandatory value not specified. ' + 51 | 'Cannot perform query. Halting.\n' + 52 | 'Config: \n' + 53 | JSON.stringify(this.gr_config) 54 | ); 55 | } 56 | 57 | grQuery = this._constructAndGetGlideRecord(); 58 | grQuery.query(); 59 | 60 | while (grQuery.next()) { 61 | responseObj._records.push( 62 | this._getRequestedRecordData(grQuery, this.gr_config) 63 | ); 64 | } 65 | 66 | responseObj._row_count = responseObj._records.length; 67 | responseObj._config = this.gr_config; 68 | 69 | return JSON.stringify(responseObj); 70 | }, 71 | 72 | _constructAndGetGlideRecord : function() { 73 | var i, queryField, queryOperator, queryValue; 74 | var grQuery = new GlideRecordSecure(this.gr_config.table_to_query); 75 | 76 | //Add limit, if specified 77 | if ( 78 | this.gr_config.hasOwnProperty('record_limit') && 79 | this.gr_config.record_limit >= 1 80 | ) { 81 | grQuery.setLimit(this.gr_config.record_limit); 82 | } 83 | 84 | //add order_by or order_by_desc field, if specified 85 | if ( 86 | this.gr_config.hasOwnProperty('order_by_desc_field') && 87 | this.gr_config.order_by_desc_field 88 | ) { 89 | grQuery.orderByDesc(this.gr_config.order_by_desc_field); 90 | } 91 | if ( 92 | this.gr_config.hasOwnProperty('order_by_field') && 93 | this.gr_config.order_by_field 94 | ) { 95 | grQuery.orderBy(this.gr_config.order_by_field); 96 | } 97 | 98 | //Add encoded query, if specified 99 | if ( 100 | this.gr_config.hasOwnProperty('encoded_queries') && 101 | this.gr_config.encoded_queries && 102 | this.gr_config.encoded_queries.length > 0 103 | ) { 104 | for (i = 0; i < this.gr_config.encoded_queries.length; i++) { 105 | if (this.gr_config.encoded_queries[i]) { 106 | grQuery.addEncodedQuery(this.gr_config.encoded_queries[i]); 107 | } 108 | } 109 | } 110 | 111 | //Add field queries if specified 112 | if ( 113 | this.gr_config.hasOwnProperty('queries') && 114 | this.gr_config.queries && 115 | this.gr_config.queries.length > 0 116 | ) { 117 | for (i = 0; i < this.gr_config.queries.length; i++) { 118 | queryField = this.gr_config.queries[i].field; 119 | queryOperator = this.gr_config.queries[i].operator; 120 | queryValue = this.gr_config.queries[i].value; 121 | 122 | grQuery.addQuery(queryField, queryOperator, queryValue); 123 | } 124 | } 125 | 126 | return grQuery; 127 | }, 128 | 129 | _validateMandatoryConfig : function() { 130 | var i, currentQuery; 131 | //May add more later if necessary 132 | var mandatoryFields = [ 133 | 'table_to_query', 134 | 'fields_to_get' 135 | ]; 136 | 137 | for (i = 0; i < mandatoryFields.length; i++) { 138 | if ( 139 | !this.gr_config.hasOwnProperty(mandatoryFields[i]) || 140 | !this.gr_config[mandatoryFields[i]] 141 | ) { 142 | return false; 143 | } 144 | } 145 | 146 | //If both order_by and order_by_desc are specified, log a warning and ignore order_by_desc. 147 | // if ( 148 | // ( 149 | // this.gr_config.hasOwnProperty('order_by_field') && 150 | // this.gr_config.order_by_field 151 | // ) && 152 | // ( 153 | // this.gr_config.hasOwnProperty('order_by_desc_field') && 154 | // this.gr_config.order_by_desc_field 155 | // ) 156 | // ) { 157 | // gs.warn( 158 | // 'ClientGlideRecordAJAX client-callable Script Include called with ' + 159 | // 'both an "order by" and "orderby descending" field. It is only possible to ' + 160 | // 'specify one field to sort by, either ascending or descending. \n' + 161 | // 'Ignoring the descending field, and ordering by the order_by_field field.' 162 | // ); 163 | // this.gr_config.order_by_desc_field = ''; 164 | // } 165 | 166 | /* 167 | Decided to remove the above code and allow the user to order their results 168 | however they like, I'm not their dad. 169 | */ 170 | 171 | if ( 172 | this.gr_config.hasOwnProperty('queries') && 173 | this.gr_config.queries 174 | ) { 175 | for (i = 0; i < this.gr_config.queries.length; i++) { 176 | currentQuery = this.gr_config.queries[i]; 177 | if ( 178 | (!currentQuery.hasOwnProperty('field') || !currentQuery.field) || 179 | (!currentQuery.hasOwnProperty('operator') || !currentQuery.operator) || 180 | (!currentQuery.hasOwnProperty('value') || !currentQuery.value) 181 | ) { 182 | gs.error( 183 | 'Malformed query provided to ClientGlideRecordAJAX Script Include:\n' + 184 | JSON.stringify(currentQuery) 185 | ); 186 | return false; 187 | } 188 | } 189 | } 190 | 191 | return true; 192 | }, 193 | 194 | /*_getRequestedRecordData : function(grRecord, config) { 195 | var iFieldToGet, iFieldChain, grRefRecord, workingFieldName, fieldType, 196 | splitFieldNames, canReadField, isFieldValid, fieldName, fieldElement, 197 | fieldValue, fieldDisplayValue, getDisplay, invalidRefChain, hasNextDotWalk, 198 | brokenRefChain; 199 | var recordData = { 200 | '_config' : config, 201 | '_table_name' : grRecord.getTableName(), 202 | '_field_values' : {} 203 | }; 204 | 205 | for (iFieldToGet = 0; iFieldToGet < recordData._config.fields_to_get.length; iFieldToGet++) { 206 | //Set initial values to false in order to prevent previous loop 207 | // from impacting this one. 208 | invalidRefChain = false; 209 | brokenRefChain = false; 210 | 211 | fieldName = recordData._config.fields_to_get[iFieldToGet].name; 212 | getDisplay = !!recordData._config.fields_to_get[iFieldToGet].get_display_value; 213 | splitFieldNames = fieldName.split('.'); 214 | 215 | //Set initial value of grRefRecord for use in the for-loop below. 216 | grRefRecord = grRecord; 217 | 218 | //Check if the field is valid and readable. 219 | //For dot-walked fields, do this for each step. 220 | for (iFieldChain = 0; iFieldChain < splitFieldNames.length; iFieldChain++) { 221 | workingFieldName = splitFieldNames[iFieldChain]; 222 | hasNextDotWalk = (iFieldChain + 1) < splitFieldNames.length; 223 | 224 | isFieldValid = grRefRecord.isValidField(workingFieldName); 225 | canReadField = (isFieldValid && grRefRecord[workingFieldName].canRead()); 226 | 227 | if (!isFieldValid || !canReadField) { 228 | break; 229 | } 230 | 231 | fieldType = grRefRecord.getElement().getED().getInternalType(); 232 | //If field type is NOT reference, but we're attempting to dot-walk through it 233 | // as indicated by there being additional elements in the field chain, then 234 | // log an error and set a flag to abort getting this field value. 235 | if (fieldType !== 'reference' && hasNextDotWalk) { 236 | invalidRefChain = true; 237 | gs.error( 238 | 'Attempted to get dot-walked field ' + fieldName + 239 | ' from table ' + grRecord.getTableName() + ', but one of the ' + 240 | 'fields in this dot-walk chain is not valid. Aborting getting ' + 241 | 'this field value.' 242 | ); 243 | break; 244 | } 245 | // 246 | 247 | //If there's more in this dot-walk chain and the field IS a reference field, 248 | // then get the next reference object. 249 | if (hasNextDotWalk && fieldType === 'reference') { 250 | if (grRefRecord[workingFieldName].nil()) { 251 | brokenRefChain = true; 252 | break; 253 | } 254 | 255 | grRefRecord = grRefRecord[workingFieldName].getRefRecord(); 256 | } 257 | } 258 | 259 | if (invalidRefChain) { 260 | //If the requested field is dot-walked through an invalid field, 261 | // continue to next loop. 262 | continue; 263 | } 264 | if (brokenRefChain) { 265 | recordData._field_values[fieldName] = { 266 | 'name' : fieldName, 267 | 'value' : '', 268 | 'display_value' : '', 269 | 'can_read' : canReadField 270 | }; 271 | //Continue to next loop after adding blank field value due to broken ref chain. 272 | continue; 273 | } 274 | //Using .getElement() to handle dot-walked fields. 275 | fieldElement = grRecord.getElement(fieldName); 276 | //todo: update to use fieldElement instead of GR 277 | 278 | //Must get canReadField in this way and use it to see if we can see the field values, 279 | // cause otherwise GlideRecordSecure freaks alll the way out. 280 | //canReadField = (grRecord.isValidField(fieldName.split('.')[0]) && grRecord[fieldName].canRead()); 281 | fieldValue = canReadField ? (grRecord.getElement(fieldName).toString() || '') : ''; 282 | fieldDisplayValue = (getDisplay && canReadField && fieldValue) ? 283 | (grRecord[fieldName].getDisplayValue() || '') : (''); 284 | 285 | //Retrieve value (and display value if requested) 286 | recordData._field_values[fieldName] = { 287 | 'name' : fieldName, 288 | 'value' : fieldValue, 289 | 'display_value' : fieldDisplayValue, 290 | //If false, may be caused by ACL restriction, or by invalid field 291 | 'can_read' : canReadField 292 | }; 293 | } 294 | 295 | return recordData; 296 | },*/ 297 | 298 | _getRequestedRecordData : function(grRecord, config) { 299 | var i, canReadField, fieldName, fieldValue, fieldDisplayValue, getDisplay, fieldElement, isFieldValid; 300 | var recordData = { 301 | '_config' : config, 302 | '_table_name' : grRecord.getTableName(), 303 | '_field_values' : {} 304 | }; 305 | 306 | for (i = 0; i < recordData._config.fields_to_get.length; i++) { 307 | fieldName = recordData._config.fields_to_get[i].name; 308 | getDisplay = !!recordData._config.fields_to_get[i].get_display_value; 309 | //Must get canReadField in this way and use it to see if we can see the field values, 310 | // cause otherwise GlideRecordSecure freaks alll the way out. 311 | isFieldValid = grRecord.isValidField(fieldName.split('.')[0]); 312 | fieldElement = isFieldValid && grRecord.getElement(fieldName); 313 | 314 | canReadField = (isFieldValid && 315 | grRecord[(fieldName.split('.')[0])].canRead()) && 316 | fieldElement.canRead(); 317 | 318 | fieldValue = canReadField ? (fieldElement.toString() || '') : ''; 319 | fieldDisplayValue = (getDisplay && canReadField && fieldValue) ? 320 | (fieldElement.getDisplayValue() || '') : (''); 321 | 322 | //Retrieve value (and display value if requested) 323 | recordData._field_values[fieldName] = { 324 | 'name' : fieldName, 325 | 'value' : fieldValue, 326 | 'display_value' : fieldDisplayValue, 327 | //If false, may be caused by ACL restriction, or by invalid field 328 | 'can_read' : canReadField 329 | }; 330 | } 331 | 332 | return recordData; 333 | }, 334 | 335 | type : 'ClientGlideRecordAJAX' 336 | }); 337 | -------------------------------------------------------------------------------- /UI Scripts/EfficientGlideRecord (Desktop Global).js: -------------------------------------------------------------------------------- 1 | /** 2 | * UI Script (Desktop-Global) 3 | * @description See related article for full usage instructions and API documentation: 4 | * https://snprotips.com/efficientgliderecord 5 | * @classdesc https://snprotips.com/efficientgliderecord 6 | * @author 7 | * Tim Woodruff (https://TimothyWoodruff.com) 8 | * SN Pro Tips (https://snprotips.com) 9 | * @version 1.0.4 10 | * @class 11 | * 12 | * @license 13 | * Copyright (c) 2022 Tim Woodruff (https://TimothyWoodruff.com) 14 | * & SN Pro Tips (https://snprotips.com). 15 | * 16 | * Permission is hereby granted, free of charge, to any person obtaining a copy 17 | * of this software and associated documentation files (the "Software"), to deal 18 | * in the Software without restriction, including without limitation the rights 19 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | * copies of the Software, and to permit persons to whom the Software is 21 | * furnished to do so, subject to the following conditions: 22 | * 23 | * The above copyright notice and this permission notice shall be included in all 24 | * copies or substantial portions of the Software. 25 | * 26 | * Alternative licensing is available upon request. Please contact tim@snc.guru 27 | * for more info. 28 | * 29 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | * SOFTWARE. 36 | */ 37 | class EfficientGlideRecord { 38 | 39 | /** 40 | * Instantiated with the 'new' keyword (as classes typically are when instantiated), this 41 | * will construct a client-side EfficientGlideRecord object. The methods of this class can 42 | * then be called to construct a client-side GlideRecord query. EfficientGlideRecord 43 | * replicates *most* of the functionality of the client-side GlideRecord object, but 44 | * with more and enhanced functionality. 45 | * EfficientGlideRecord is FAR preferable to using the out-of-box (OOB) client-side 46 | * GlideRecord query (even asynchronously), because GlideRecord returns a massive amount 47 | * of unnecessary data, and can be much, much slower. EfficientGlideRecord aims to return 48 | * only that data which is necessary and requested from the server, thus providing an 49 | * efficient interface to query records asynchronously without all the additional overhead 50 | * related to information that you don't need. 51 | * 52 | * Additional documentation can be found on the SN Pro Tips blog, at https://egr.snc.guru 53 | * NOTE: For info on performing async queries in onSubmit Client Scripts, see 54 | * https://onsubmit.snc.guru 55 | * 56 | * @param {String} tableName - The name of the table on which to execute your GlideRecord query 57 | * @returns {EfficientGlideRecord} 58 | * @example 59 | * var egrIncident = new EfficientGlideRecord('incident'); 60 | * egrIncident.addField('number') 61 | * .addField('assignment_group', true) 62 | * .addField('assigned_to', true); 63 | * 64 | * egrIncident.get('some_incident_sys_id', function(egrInc) { 65 | * g_form.addInfoMessage( 66 | * egrInc.getValue('number') + '\'s assignment group is ' + 67 | * egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' + 68 | * egrInc.getValue('assignment_group') + ')\n' + 69 | * 'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' + 70 | * egrInc.getValue('assigned_to') + ')' 71 | * ); 72 | * }); 73 | * @constructor 74 | */ 75 | constructor(tableName) { 76 | if (!tableName) { 77 | throw new Error( 78 | 'EfficientGlideRecord constructor called without a valid tableName ' + 79 | 'argument. Cannot continue.' 80 | ); 81 | } 82 | 83 | this._config = { 84 | 'table_to_query' : tableName, 85 | 'fields_to_get' : [{ 86 | 'name' : 'sys_id', 87 | 'get_display_value' : false 88 | }], 89 | 'record_limit' : 0, 90 | 'order_by_field' : '', 91 | 'order_by_desc_field' : '', 92 | 'encoded_queries' : [], 93 | 'queries' : [] 94 | }; 95 | 96 | this._row_count = -1; 97 | this._query_complete = false; 98 | 99 | this._records = []; 100 | this._current_record_index = -1; 101 | this._current_record = {}; 102 | 103 | this._gaQuery = new GlideAjax('ClientGlideRecordAJAX'); 104 | this._gaQuery.addParam('sysparm_name', 'getPseudoGlideRecord'); 105 | 106 | return this; 107 | } 108 | 109 | /** 110 | * Add a field to retrieve from the target record(s). 111 | * Any fields not specified by calling this method will not be available on the resulting 112 | * EfficientGlideRecord object in the callback function after calling .query(). In this 113 | * case, a warning will be shown in the console, and .getValue('field_name') will return 114 | * a blank string. 115 | * If a second argument (getDisplayValue) is not specified and set to true, then the 116 | * field's display value will not be available on the resulting EfficientGlideRecord 117 | * object in the callback function. In this case, .getDisplayValue('field_name') will 118 | * return a blank string. 119 | * @param {String} fieldName - The name of the field to retrieve from the server for the 120 | * specified record(s). 121 | * @param {Boolean} [getDisplayValue=false] - Set this argument to true in order to 122 | * retrieve the display value for the specified field. If this is not set to true then 123 | * calling .getDisplayValue('field_name') will cause a warning to be logged to the 124 | * console, and a blank string will be returned. 125 | * @returns {EfficientGlideRecord} 126 | * @example 127 | * var egrIncident = new EfficientGlideRecord('incident'); 128 | * egrIncident.addField('number') 129 | * .addField('assignment_group', true) 130 | * .addField('assigned_to', true); 131 | * 132 | * egrIncident.get('some_incident_sys_id', function(egrInc) { 133 | * g_form.addInfoMessage( 134 | * egrInc.getValue('number') + '\'s assignment group is ' + 135 | * egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' + 136 | * egrInc.getValue('assignment_group') + ')\n' + 137 | * 'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' + 138 | * egrInc.getValue('assigned_to') + ')' 139 | * ); 140 | * }); 141 | */ 142 | addField(fieldName, getDisplayValue) { 143 | var i; 144 | if (!fieldName) { 145 | console.error( 146 | 'Attempted to call .addField() without a field name specified. ' + 147 | 'Cannot add a blank field to the query.' 148 | ); 149 | return this; 150 | } 151 | for (i = 0; i < this._config.fields_to_get.length; i++) { 152 | if (this._config.fields_to_get[i].name === fieldName) { 153 | //If the field name already exists, then bail. 154 | console.warn( 155 | 'Attempted to add field with name ' + fieldName + ' to ' + 156 | 'EfficientGlideRecord query, but that field already exists. ' + 157 | 'Cannot add the same field twice.' 158 | ); 159 | return this; 160 | } 161 | } 162 | this._config.fields_to_get.push({ 163 | 'name' : fieldName, 164 | 'get_display_value' : (!!getDisplayValue) 165 | }); 166 | 167 | return this; 168 | } 169 | 170 | /** 171 | * Add a query to the EfficientGlideRecord object. 172 | * By specifying a field name, operator, and value, you can perform all sorts of queries. 173 | * If only two arguments are specified, then it's assumed that the first is the field 174 | * name and the second is the field value. The operator will automatically be set to "=". 175 | * 176 | * @param {String} fieldName - The name of the field to perform the query against. 177 | * @param {String} [operator="="] - The operator to use for the query. 178 | * Valid operators: 179 | * Numbers: =, !=, >, >=, <, <= 180 | * Strings: =, !=, STARTSWITH, ENDSWITH, CONTAINS, DOES NOT CONTAIN, IN, NOT IN, INSTANCEOF 181 | * Note: If only two arguments are specified (fieldValue is not defined), then the second 182 | * argument will be treated as the value, and the operator will automatically be set to "=". 183 | * @param {String} fieldValue - The value to compare, using the specified operator, against 184 | * the specified field. 185 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 186 | * chain-calling (as seen in the example below). 187 | * @example 188 | * new EfficientGlideRecord('incident') 189 | * .setLimit(10) 190 | * .addQuery('assignment_group', '!=', 'some_group_sys_id') 191 | * .addQuery('assigned_to', 'some_assignee_sys_id') 192 | * .addNotNullQuery('assignment_group') 193 | * .addField('number') 194 | * .addField('short_description') 195 | * .addField('assignment_group', true) //Get display value as well 196 | * .orderBy('number') 197 | * .query(function (egrIncident) { 198 | * while (egrIncident.next()) { 199 | * console.log( 200 | * 'Short description value: ' + egrIncident.getValue('short_description') + 201 | * '\n' + 202 | * 'Number: ' + egrIncident.getValue('number') + '\n' + 203 | * 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' + 204 | * egrIncident.getDisplayValue('assignment_group') + ')' 205 | * ); 206 | * } 207 | * }); 208 | */ 209 | addQuery(fieldName, operator, fieldValue) { 210 | if (typeof fieldValue === 'undefined') { 211 | fieldValue = operator; 212 | operator = '='; 213 | } 214 | 215 | this._config.queries.push({ 216 | 'field' : fieldName, 217 | 'operator' : operator, 218 | 'value' : fieldValue 219 | }); 220 | 221 | return this; 222 | } 223 | 224 | /** 225 | * Shorthand for this.addQuery(fieldName, '!=', 'NULL');. 226 | * @param {String} fieldName - The name of the field to ensure is not empty on returned 227 | * records. 228 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 229 | * chain-calling. 230 | * @example 231 | * new EfficientGlideRecord('incident') 232 | * .setLimit(10) 233 | * .addQuery('assignment_group', '!=', 'some_group_sys_id') 234 | * .addQuery('assigned_to', 'some_assignee_sys_id') 235 | * .addNotNullQuery('assignment_group') 236 | * .addField('number') 237 | * .addField('short_description') 238 | * .addField('assignment_group', true) //Get display value as well 239 | * .orderBy('number') 240 | * .query(function (egrIncident) { 241 | * while (egrIncident.next()) { 242 | * console.log( 243 | * 'Short description value: ' + egrIncident.getValue('short_description') + 244 | * '\n' + 245 | * 'Number: ' + egrIncident.getValue('number') + '\n' + 246 | * 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' + 247 | * egrIncident.getDisplayValue('assignment_group') + ')' 248 | * ); 249 | * } 250 | * }); 251 | */ 252 | addNotNullQuery(fieldName) { 253 | this.addQuery(fieldName, '!=', 'NULL'); 254 | 255 | return this; 256 | } 257 | 258 | /** 259 | * Shorthand for .addQuery(fieldName, '=', 'NULL') 260 | * @param {String} fieldName - The name of the field to use in your query, getting only 261 | * records where this field is empty. 262 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 263 | * chain-calling. 264 | */ 265 | addNullQuery(fieldName) { 266 | this.addQuery(fieldName, '=', 'NULL'); 267 | 268 | return this; 269 | } 270 | 271 | /** 272 | * Add an encoded query string to your query. Records matching this encoded query will 273 | * be available in your callback function after calling .query(). 274 | * @param {String} encodedQueryString - The encoded query string to use in your query. 275 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 276 | * chain-calling. 277 | */ 278 | addEncodedQuery(encodedQueryString) { 279 | if (!encodedQueryString || typeof encodedQueryString !== 'string') { 280 | throw new Error( 281 | 'Invalid encoded query string specified. Encoded query must be a valid ' + 282 | 'non-empty string.' 283 | ); 284 | } 285 | 286 | this._config.encoded_queries.push(encodedQueryString); 287 | 288 | return this; 289 | } 290 | 291 | /** 292 | * Very similar to .addEncodedQuery(), except that it REPLACES any existing encoded 293 | * queries on the GlideRecord, rather than adding to them. 294 | * @param {String} encodedQueryString - The exact encoded query, as a string, to use in 295 | * your query. 296 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 297 | * chain-calling. 298 | */ 299 | setEncodedQuery(encodedQueryString) { 300 | //REPLACE existing encoded queries, rather than add to them like .addEncodedQuery(). 301 | this._config.encoded_queries = [encodedQueryString]; 302 | 303 | return this; 304 | } 305 | 306 | /** 307 | * Orders the queried table by the specified column, in ascending order 308 | * (Alternate call for .orderBy(fieldName).) 309 | * @param orderByField 310 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 311 | * chain-calling. 312 | */ 313 | addOrderBy(orderByField) { 314 | this.orderBy(orderByField); 315 | 316 | return this; 317 | } 318 | 319 | /** 320 | * Orders the queried table by the specified column, in ascending order 321 | * @param {String} orderByField - Orders the queried table by the specified column, 322 | * in ascending order 323 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 324 | * chain-calling. 325 | */ 326 | orderBy(orderByField) { 327 | this._config.order_by_field = orderByField; 328 | 329 | return this; 330 | } 331 | 332 | /** 333 | * Orders the queried table by the specified column, in descending order 334 | * @param {String} orderByDescField - Orders the queried table by the specified column, 335 | * in descending order 336 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 337 | * chain-calling. 338 | */ 339 | orderByDesc(orderByDescField) { 340 | this._config.order_by_desc_field = orderByDescField; 341 | 342 | return this; 343 | } 344 | 345 | /** 346 | * Limits the number of records queried from the database and 347 | * returned to the response. 348 | * @param {Number} limit - The limit to use in the database query. No more than this number 349 | * of records will be returned. 350 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 351 | * chain-calling. 352 | */ 353 | setLimit(limit) { 354 | if (typeof limit !== 'number' || limit <= 0) { 355 | throw new Error( 356 | 'EfficientGlideRecord.setLimit() method called with an invalid argument. ' + 357 | 'Limit must be a number greater than zero.' 358 | ); 359 | } 360 | this._config.record_limit = limit; 361 | 362 | return this; 363 | } 364 | 365 | /** 366 | * Gets a single record, efficiently, from the database by sys_id. 367 | * @param {String} sysID - The sys_id of the record to retrieve. Must be the sys_id of 368 | * a valid record which the user has permissions to see, in the table specified in the 369 | * constructor when instantiating this object. 370 | * @param {function} callbackFn - The callback function to be called when the query is 371 | * complete. 372 | * When the query is complete, this callback function will be called with one argument: 373 | * the EfficientGlideRecord object containing the records resultant from your query. 374 | * After querying (in your callback function), you can call methods such as .next() 375 | * and .getValue() to iterate through the returned records and get field values. 376 | */ 377 | get(sysID, callbackFn) { 378 | this.addQuery('sys_id', sysID); 379 | this.setLimit(1); 380 | 381 | this.query(function(egr) { 382 | if (egr.next()) { 383 | callbackFn(egr); 384 | } else { 385 | console.warn( 386 | 'EfficientGlideRecord: No records found in the target table ' + 387 | 'with sys_id "' + sysID + '".' 388 | ); 389 | } 390 | }); 391 | 392 | } 393 | 394 | /** 395 | * Perform the async query constructed by calling methods in this class, and get the 396 | * field(s) from the resultant record that were requested by calling 397 | * .addField(fieldName, getDisplayValue) 398 | * @async 399 | * @param {function} callbackFn - The callback function to be called 400 | * when the query is complete. 401 | * When the query is complete, this callback function will be called with one argument: 402 | * the EfficientGlideRecord object containing the records resultant from your query. 403 | * After querying (in your callback function), you can call methods such as .next() 404 | * and .getValue() to iterate through the returned records and get field values. 405 | */ 406 | query(callbackFn) { 407 | let paramName; 408 | 409 | if (!this._readyToSend()) { 410 | //Meaningful errors are logged by this._readyToSend(). 411 | return false; 412 | } 413 | 414 | for (paramName in this._config) { 415 | //Prevent iteration into non-own properties 416 | if (!this._config.hasOwnProperty(paramName)) { 417 | continue; 418 | } 419 | 420 | let paramVal; 421 | 422 | if (typeof this._config[paramName] === 'object') { 423 | paramVal = JSON.stringify(this._config[paramName]); 424 | } else { 425 | paramVal = this._config[paramName]; 426 | } 427 | 428 | this._gaQuery.addParam( 429 | paramName, 430 | paramVal 431 | ); 432 | } 433 | this._gaQuery.getXMLAnswer(function(answer, eGR) { 434 | //Make this work in Portal because SN is bad at documentation and consistency 435 | if (typeof eGR === 'undefined') { 436 | if (typeof this === 'undefined' || this === null) { 437 | 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://egr.snc.guru'); 438 | } else { 439 | //If Service Portal blocked access to/nullified the "this" object FOR 440 | // SOME FREAKIN REASON, grab it from the binding we did in .query(). 441 | eGR = this; 442 | } 443 | } 444 | //Parse answer into a useful object. 445 | answer = JSON.parse(answer); 446 | 447 | //let answer = response.responseXML.documentElement.getAttribute('answer'); 448 | // answer = JSON.parse(answer); //Throws if unparseable -- good. 449 | if (!answer.hasOwnProperty('_records')) { 450 | throw new Error( 451 | 'Something went wrong when attempting to get records from the server.\n' + 452 | 'Response object: \n' + 453 | JSON.stringify(answer) 454 | ); 455 | } 456 | 457 | eGR._query_complete = true; 458 | eGR._records = answer._records; 459 | eGR._row_count = answer._row_count; 460 | eGR._executing_as = answer._executing_as; 461 | 462 | callbackFn(eGR); 463 | }.bind(this), null, this); 464 | 465 | } 466 | 467 | /* The following methods can only be called after the query is performed */ 468 | 469 | /** 470 | * Check if there is a "next" record to iterate into using .next() (without actually 471 | * positioning the current record to the next one). Can only be called from the callback 472 | * function passed into .query()/.get() after the query has completed. 473 | * @returns {boolean} - True if there is a "next" record to iterate into, or false if not. 474 | */ 475 | hasNext() { 476 | if (!this._query_complete) { 477 | /*throw new Error( 478 | 'The .hasNext() method of EfficientGlideRecord can only be called from the ' + 479 | 'callback function after calling .query()' 480 | );*/ 481 | 482 | return false; 483 | } 484 | 485 | return (this._row_count > (this._current_record_index + 1)); 486 | } 487 | 488 | /** 489 | * Iterate into the next record, if one exists. 490 | * Usage is the same as GlideRecord's .next() method. 491 | * @returns {boolean} - True if there was a "next" record, and we've successfully positioned 492 | * into it. False if not. Can only be run from the callback function after a query using 493 | * .query() or .get(). 494 | */ 495 | next() { 496 | if (!this._query_complete) { 497 | /*throw new Error( 498 | 'The .next() method of EfficientGlideRecord can only be called from the ' + 499 | 'callback function after calling .query()' 500 | );*/ 501 | 502 | return false; 503 | } 504 | if (!this.hasNext()) { 505 | return false; 506 | } 507 | 508 | this._current_record_index++; 509 | this._current_record = this._records[this._current_record_index]; 510 | 511 | return true; 512 | } 513 | 514 | /** 515 | * Returns true if the specified field exists and can be read (even if it's blank). 516 | * Will return false in the following cases: 517 | * -The specified field on the current record cannot be read 518 | * -The specified field does not exist in the response object (which may happen if you don't 519 | * add the field to your request using .addField()). 520 | * -The specified field does not exist in the database 521 | * @param {String} fieldName - The name of the field to check whether the user can read or not. 522 | * @returns {Boolean} - Returns true if the specified field exists and can be read, or 523 | * false otherwise. 524 | */ 525 | canRead(fieldName) { 526 | if (!this._query_complete) { 527 | throw new Error( 528 | 'The .canRead() method of EfficientGlideRecord can only be called from the ' + 529 | 'callback function after calling .query(callbackFn)' 530 | ); 531 | } 532 | 533 | if (!this._current_record._field_values.hasOwnProperty(fieldName)) { 534 | console.warn( 535 | 'There is no field with the name ' + fieldName + ' in the ' + 536 | 'EfficientGlideRecord object. Did you remember to specify that you want to ' + 537 | 'get that field in the query using .addField()?' 538 | ); 539 | return false; 540 | } 541 | 542 | if (!this._current_record._field_values[fieldName].hasOwnProperty('can_read')) { 543 | console.warn( 544 | 'The requested field "' + fieldName + '" has no can_read node. ' + 545 | 'This should not happen. Returning a blank false.' 546 | ); 547 | return false; 548 | } 549 | 550 | return (!!this._current_record._field_values[fieldName].can_read) || false; 551 | } 552 | 553 | /** 554 | * Retrieve the database value for the specified field, if the user has permissions to read 555 | * that field's value. 556 | * @param fieldName 557 | * @returns {string} 558 | */ 559 | getValue(fieldName) { 560 | if (!this._query_complete) { 561 | throw new Error( 562 | 'The .getValue() method of EfficientGlideRecord can only be called from the ' + 563 | 'callback function after calling .query(callbackFn)' 564 | ); 565 | } 566 | 567 | if (!this._current_record._field_values.hasOwnProperty(fieldName)) { 568 | console.warn( 569 | 'There is no field with the name ' + fieldName + ' in the ' + 570 | 'EfficientGlideRecord object. Did you remember to specify that you want to ' + 571 | 'get that field in the query using .addField()?' 572 | ); 573 | return ''; 574 | } 575 | 576 | if (!this._current_record._field_values[fieldName].hasOwnProperty('value')) { 577 | console.warn( 578 | 'The requested field "' + fieldName + '" has no value node. ' + 579 | 'This should not happen. Returning a blank string.' 580 | ); 581 | return ''; 582 | } 583 | 584 | return this._current_record._field_values[fieldName].value || ''; 585 | } 586 | 587 | /** 588 | * Retrieve the display value for the specified field, if the user has permission to view 589 | * that field's value. 590 | * Can only be called from the callback function after the query is complete. 591 | * @param fieldName 592 | * @returns {string|*|string} 593 | */ 594 | getDisplayValue(fieldName) { 595 | if (!this._query_complete) { 596 | throw new Error( 597 | 'The .getDisplayValue() method of EfficientGlideRecord can only be called from the ' + 598 | 'callback function after calling .query(callbackFn)' 599 | ); 600 | } 601 | if (!this._current_record._field_values.hasOwnProperty(fieldName)) { 602 | console.warn( 603 | 'There is no field with the name ' + fieldName + ' in the ' + 604 | 'EfficientGlideRecord object. Did you remember to specify that you want to ' + 605 | 'get that field in the query using .addField()?' 606 | ); 607 | return ''; 608 | } 609 | if ( 610 | !this._current_record._field_values[fieldName].hasOwnProperty('display_value') || 611 | !this._current_record._field_values[fieldName].display_value 612 | ) { 613 | console.warn( 614 | 'There is no display value for the field with the name ' + fieldName + 615 | ' in the EfficientGlideRecord object. Did you remember to specify that you ' + 616 | 'want to get that field\'s display value in the query using ' + 617 | '.addField(fieldName, true)?' 618 | ); 619 | return ''; 620 | } 621 | 622 | return this._current_record._field_values[fieldName].display_value || ''; 623 | } 624 | 625 | /** 626 | * Retrieves the number of records returned from the query. 627 | * If used in conjunction with .setLimit(), then the maximum value returned from this 628 | * method will be the limit number (since no more records than the specified limit can 629 | * be returned from the server). 630 | * 631 | * @returns {number} - The number of records returned from the query. 632 | * @example 633 | * //Show the number of child Incidents missing Short Descriptions. 634 | * new EfficientGlideRecord('incident') 635 | * .addQuery('parent', g_form.getUniqueValue()) 636 | * .addNullQuery('short_description') 637 | * .addField('number') 638 | * .query(function (egrIncident) { 639 | * if (egrIncident.hasNext()) { 640 | * g_form.addErrorMessage( 641 | * egrIncident.getRowCount() + ' child Incidents are missing a short 642 | * description.' 643 | * ); 644 | * } 645 | * }); 646 | * @since 1.0.1 647 | */ 648 | getRowCount() { 649 | return this._row_count; 650 | } 651 | 652 | /* Private helper methods below */ 653 | 654 | _readyToSend() { 655 | if (!this._config.table_to_query) { 656 | console.error( 657 | 'EfficientGlideRecord not ready to query. Table name was not specified in ' + 658 | 'the constructor\'s initialize argument.' 659 | ); 660 | return false; 661 | } 662 | 663 | if (this._config.fields_to_get.length <= 1) { 664 | console.warn( 665 | 'EfficientGlideRecord: No fields other than sys_id were specified ' + 666 | 'to retrieve. \nYou can specify which fields you want to retrieve from ' + 667 | 'the GlideRecord object using .addField(fieldName, getDisplayValue). ' + 668 | 'Afterward, in your callback, you can use .getValue(fieldName). If ' + 669 | 'you set getDisplayValue to true in .addField(), you can also use ' + 670 | '.getDisplayValue(fieldName).\n' + 671 | 'Without fields to retrieve specified using .addField(), each record ' + 672 | 'will be returned with only a sys_id. \n' + 673 | 'This will not prevent you from performing your query, unless ' + 674 | 'something has gone terribly wrong.' 675 | ); 676 | //Not returning false, because this is not a blocking error. 677 | } 678 | 679 | //Warn if queries AND encoded queries are both empty and limit is unspecified 680 | // (but don't return false) 681 | if ( 682 | ( 683 | !this._config.hasOwnProperty('queries') || 684 | this._config.queries.length < 1 685 | ) && 686 | ( 687 | !this._config.hasOwnProperty('encoded_queries') || 688 | this._config.encoded_queries.length < 1 689 | ) && 690 | ( 691 | !this._config.hasOwnProperty('record_limit') || 692 | this._config.record_limit < 1 693 | ) 694 | ) { 695 | console.warn( 696 | 'The EfficientGlideRecord query has no query and no record limit ' + 697 | 'associated with it. This may result in poor performance when querying larger ' + 698 | 'tables. Please make sure that you need all records in the specified table, ' + 699 | 'as all records will be returned by this query.' 700 | ); 701 | } 702 | 703 | //Return true if none of the above validations have failed. 704 | return true; 705 | } 706 | } 707 | -------------------------------------------------------------------------------- /UI Scripts/EfficientGlideRecordPortal (Portal-JS Include).js: -------------------------------------------------------------------------------- 1 | /** 2 | * UI Script (Mobile / Service Portal) 3 | * Please add this (or better yet, the minified version of this file) as a JS Include 4 | * to the Theme that you're using on any Service Portals used by your organization. 5 | * @description See related article for full usage instructions and API 6 | * documentation: 7 | * https://egr.snc.guru 8 | * @classdesc https://egr.snc.guru 9 | * @author 10 | * Tim Woodruff (https://TimothyWoodruff.com) 11 | * SN Pro Tips (https://snprotips.com) 12 | * @version 1.0.4 13 | * @class 14 | * 15 | * @license 16 | * Copyright (c) 2022 Tim Woodruff (https://TimothyWoodruff.com) 17 | * & SN Pro Tips (https://snprotips.com). 18 | * 19 | * Permission is hereby granted, free of charge, to any person obtaining a copy 20 | * of this software and associated documentation files (the "Software"), to deal 21 | * in the Software without restriction, including without limitation the rights 22 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | * copies of the Software, and to permit persons to whom the Software is 24 | * furnished to do so, subject to the following conditions: 25 | * 26 | * The above copyright notice and this permission notice shall be included in all 27 | * copies or substantial portions of the Software. 28 | * 29 | * Alternative licensing is available upon request. Please contact tim@snc.guru 30 | * for more info. 31 | * 32 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 33 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 34 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 35 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 36 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 37 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 38 | * SOFTWARE. 39 | */ 40 | class EfficientGlideRecordPortal { 41 | 42 | /** 43 | * Instantiated with the 'new' keyword (as classes typically are when instantiated), this 44 | * will construct a client-side EfficientGlideRecord object. The methods of this class can 45 | * then be called to construct a client-side GlideRecord query. EfficientGlideRecord 46 | * replicates *most* of the functionality of the client-side GlideRecord object, but 47 | * with more and enhanced functionality. 48 | * EfficientGlideRecord is FAR preferable to using the out-of-box (OOB) client-side 49 | * GlideRecord query (even asynchronously), because GlideRecord returns a massive amount 50 | * of unnecessary data, and can be much, much slower. EfficientGlideRecord aims to return 51 | * only that data which is necessary and requested from the server, thus providing an 52 | * efficient interface to query records asynchronously without all the additional overhead 53 | * related to information that you don't need. 54 | * 55 | * Additional documentation can be found on the SN Pro Tips blog, at https://egr.snc.guru 56 | * NOTE: For info on performing async queries in onSubmit Client Scripts, see 57 | * https://onsubmit.snc.guru 58 | * 59 | * @param {String} tableName - The name of the table on which to execute your GlideRecord query 60 | * @returns {EfficientGlideRecord} 61 | * @example 62 | * var egrIncident = new EfficientGlideRecord('incident'); 63 | * egrIncident.addField('number') 64 | * .addField('assignment_group', true) 65 | * .addField('assigned_to', true); 66 | * 67 | * egrIncident.get('some_incident_sys_id', function(egrInc) { 68 | * g_form.addInfoMessage( 69 | * egrInc.getValue('number') + '\'s assignment group is ' + 70 | * egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' + 71 | * egrInc.getValue('assignment_group') + ')\n' + 72 | * 'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' + 73 | * egrInc.getValue('assigned_to') + ')' 74 | * ); 75 | * }); 76 | * @constructor 77 | */ 78 | constructor(tableName) { 79 | if (!tableName) { 80 | throw new Error( 81 | 'EfficientGlideRecord constructor called without a valid tableName ' + 82 | 'argument. Cannot continue.' 83 | ); 84 | } 85 | 86 | this._config = { 87 | 'table_to_query' : tableName, 88 | 'fields_to_get' : [{ 89 | 'name' : 'sys_id', 90 | 'get_display_value' : false 91 | }], 92 | 'record_limit' : 0, 93 | 'order_by_field' : '', 94 | 'order_by_desc_field' : '', 95 | 'encoded_queries' : [], 96 | 'queries' : [] 97 | }; 98 | 99 | this._row_count = -1; 100 | this._query_complete = false; 101 | 102 | this._records = []; 103 | this._current_record_index = -1; 104 | this._current_record = {}; 105 | 106 | this._gaQuery = new GlideAjax('ClientGlideRecordAJAX'); 107 | this._gaQuery.addParam('sysparm_name', 'getPseudoGlideRecord'); 108 | 109 | return this; 110 | } 111 | 112 | /** 113 | * Add a field to retrieve from the target record(s). 114 | * Any fields not specified by calling this method will not be available on the resulting 115 | * EfficientGlideRecord object in the callback function after calling .query(). In this 116 | * case, a warning will be shown in the console, and .getValue('field_name') will return 117 | * a blank string. 118 | * If a second argument (getDisplayValue) is not specified and set to true, then the 119 | * field's display value will not be available on the resulting EfficientGlideRecord 120 | * object in the callback function. In this case, .getDisplayValue('field_name') will 121 | * return a blank string. 122 | * @param {String} fieldName - The name of the field to retrieve from the server for the 123 | * specified record(s). 124 | * @param {Boolean} [getDisplayValue=false] - Set this argument to true in order to 125 | * retrieve the display value for the specified field. If this is not set to true then 126 | * calling .getDisplayValue('field_name') will cause a warning to be logged to the 127 | * console, and a blank string will be returned. 128 | * @returns {EfficientGlideRecord} 129 | * @example 130 | * var egrIncident = new EfficientGlideRecord('incident'); 131 | * egrIncident.addField('number') 132 | * .addField('assignment_group', true) 133 | * .addField('assigned_to', true); 134 | * 135 | * egrIncident.get('some_incident_sys_id', function(egrInc) { 136 | * g_form.addInfoMessage( 137 | * egrInc.getValue('number') + '\'s assignment group is ' + 138 | * egrInc.getDisplayValue('assignment_group') + ' (sys_id: ' + 139 | * egrInc.getValue('assignment_group') + ')\n' + 140 | * 'The assignee is ' + egrInc.getDisplayValue('assigned_to') + ' (sys_id: ' + 141 | * egrInc.getValue('assigned_to') + ')' 142 | * ); 143 | * }); 144 | */ 145 | addField(fieldName, getDisplayValue) { 146 | var i; 147 | if (!fieldName) { 148 | console.error( 149 | 'Attempted to call .addField() without a field name specified. ' + 150 | 'Cannot add a blank field to the query.' 151 | ); 152 | return this; 153 | } 154 | for (i = 0; i < this._config.fields_to_get.length; i++) { 155 | if (this._config.fields_to_get[i].name === fieldName) { 156 | //If the field name already exists, then bail. 157 | console.warn( 158 | 'Attempted to add field with name ' + fieldName + ' to ' + 159 | 'EfficientGlideRecord query, but that field already exists. ' + 160 | 'Cannot add the same field twice.' 161 | ); 162 | return this; 163 | } 164 | } 165 | this._config.fields_to_get.push({ 166 | 'name' : fieldName, 167 | 'get_display_value' : (!!getDisplayValue) 168 | }); 169 | 170 | return this; 171 | } 172 | 173 | /** 174 | * Add a query to the EfficientGlideRecord object. 175 | * By specifying a field name, operator, and value, you can perform all sorts of queries. 176 | * If only two arguments are specified, then it's assumed that the first is the field 177 | * name and the second is the field value. The operator will automatically be set to "=". 178 | * 179 | * @param {String} fieldName - The name of the field to perform the query against. 180 | * @param {String} [operator="="] - The operator to use for the query. 181 | * Valid operators: 182 | * Numbers: =, !=, >, >=, <, <= 183 | * Strings: =, !=, STARTSWITH, ENDSWITH, CONTAINS, DOES NOT CONTAIN, IN, NOT IN, INSTANCEOF 184 | * Note: If only two arguments are specified (fieldValue is not defined), then the second 185 | * argument will be treated as the value, and the operator will automatically be set to "=". 186 | * @param {String} fieldValue - The value to compare, using the specified operator, against 187 | * the specified field. 188 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 189 | * chain-calling (as seen in the example below). 190 | * @example 191 | * new EfficientGlideRecord('incident') 192 | * .setLimit(10) 193 | * .addQuery('assignment_group', '!=', 'some_group_sys_id') 194 | * .addQuery('assigned_to', 'some_assignee_sys_id') 195 | * .addNotNullQuery('assignment_group') 196 | * .addField('number') 197 | * .addField('short_description') 198 | * .addField('assignment_group', true) //Get display value as well 199 | * .orderBy('number') 200 | * .query(function (egrIncident) { 201 | * while (egrIncident.next()) { 202 | * console.log( 203 | * 'Short description value: ' + egrIncident.getValue('short_description') + 204 | * '\n' + 205 | * 'Number: ' + egrIncident.getValue('number') + '\n' + 206 | * 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' + 207 | * egrIncident.getDisplayValue('assignment_group') + ')' 208 | * ); 209 | * } 210 | * }); 211 | */ 212 | addQuery(fieldName, operator, fieldValue) { 213 | if (typeof fieldValue === 'undefined') { 214 | fieldValue = operator; 215 | operator = '='; 216 | } 217 | 218 | this._config.queries.push({ 219 | 'field' : fieldName, 220 | 'operator' : operator, 221 | 'value' : fieldValue 222 | }); 223 | 224 | return this; 225 | } 226 | 227 | /** 228 | * Shorthand for this.addQuery(fieldName, '!=', 'NULL');. 229 | * @param {String} fieldName - The name of the field to ensure is not empty on returned 230 | * records. 231 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 232 | * chain-calling. 233 | * @example 234 | * new EfficientGlideRecord('incident') 235 | * .setLimit(10) 236 | * .addQuery('assignment_group', '!=', 'some_group_sys_id') 237 | * .addQuery('assigned_to', 'some_assignee_sys_id') 238 | * .addNotNullQuery('assignment_group') 239 | * .addField('number') 240 | * .addField('short_description') 241 | * .addField('assignment_group', true) //Get display value as well 242 | * .orderBy('number') 243 | * .query(function (egrIncident) { 244 | * while (egrIncident.next()) { 245 | * console.log( 246 | * 'Short description value: ' + egrIncident.getValue('short_description') + 247 | * '\n' + 248 | * 'Number: ' + egrIncident.getValue('number') + '\n' + 249 | * 'Assignment group: ' + egrIncident.getValue('assignment_group') + ' (' + 250 | * egrIncident.getDisplayValue('assignment_group') + ')' 251 | * ); 252 | * } 253 | * }); 254 | */ 255 | addNotNullQuery(fieldName) { 256 | this.addQuery(fieldName, '!=', 'NULL'); 257 | 258 | return this; 259 | } 260 | 261 | /** 262 | * Shorthand for .addQuery(fieldName, '=', 'NULL') 263 | * @param {String} fieldName - The name of the field to use in your query, getting only 264 | * records where this field is empty. 265 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 266 | * chain-calling. 267 | */ 268 | addNullQuery(fieldName) { 269 | this.addQuery(fieldName, '=', 'NULL'); 270 | 271 | return this; 272 | } 273 | 274 | /** 275 | * Add an encoded query string to your query. Records matching this encoded query will 276 | * be available in your callback function after calling .query(). 277 | * @param {String} encodedQueryString - The encoded query string to use in your query. 278 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 279 | * chain-calling. 280 | */ 281 | addEncodedQuery(encodedQueryString) { 282 | if (!encodedQueryString || typeof encodedQueryString !== 'string') { 283 | throw new Error( 284 | 'Invalid encoded query string specified. Encoded query must be a valid ' + 285 | 'non-empty string.' 286 | ); 287 | } 288 | 289 | this._config.encoded_queries.push(encodedQueryString); 290 | 291 | return this; 292 | } 293 | 294 | /** 295 | * Very similar to .addEncodedQuery(), except that it REPLACES any existing encoded 296 | * queries on the GlideRecord, rather than adding to them. 297 | * @param {String} encodedQueryString - The exact encoded query, as a string, to use in 298 | * your query. 299 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 300 | * chain-calling. 301 | */ 302 | setEncodedQuery(encodedQueryString) { 303 | //REPLACE existing encoded queries, rather than add to them like .addEncodedQuery(). 304 | this._config.encoded_queries = [encodedQueryString]; 305 | 306 | return this; 307 | } 308 | 309 | /** 310 | * Orders the queried table by the specified column, in ascending order 311 | * (Alternate call for .orderBy(fieldName).) 312 | * @param orderByField 313 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 314 | * chain-calling. 315 | */ 316 | addOrderBy(orderByField) { 317 | this.orderBy(orderByField); 318 | 319 | return this; 320 | } 321 | 322 | /** 323 | * Orders the queried table by the specified column, in ascending order 324 | * @param {String} orderByField - Orders the queried table by the specified column, 325 | * in ascending order 326 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 327 | * chain-calling. 328 | */ 329 | orderBy(orderByField) { 330 | this._config.order_by_field = orderByField; 331 | 332 | return this; 333 | } 334 | 335 | /** 336 | * Orders the queried table by the specified column, in descending order 337 | * @param {String} orderByDescField - Orders the queried table by the specified column, 338 | * in descending order 339 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 340 | * chain-calling. 341 | */ 342 | orderByDesc(orderByDescField) { 343 | this._config.order_by_desc_field = orderByDescField; 344 | 345 | return this; 346 | } 347 | 348 | /** 349 | * Limits the number of records queried from the database and 350 | * returned to the response. 351 | * @param {Number} limit - The limit to use in the database query. No more than this number 352 | * of records will be returned. 353 | * @returns {EfficientGlideRecord} - Returns the instantiated object for optional 354 | * chain-calling. 355 | */ 356 | setLimit(limit) { 357 | if (typeof limit !== 'number' || limit <= 0) { 358 | throw new Error( 359 | 'EfficientGlideRecord.setLimit() method called with an invalid argument. ' + 360 | 'Limit must be a number greater than zero.' 361 | ); 362 | } 363 | this._config.record_limit = limit; 364 | 365 | return this; 366 | } 367 | 368 | /** 369 | * Gets a single record, efficiently, from the database by sys_id. 370 | * @param {String} sysID - The sys_id of the record to retrieve. Must be the sys_id of 371 | * a valid record which the user has permissions to see, in the table specified in the 372 | * constructor when instantiating this object. 373 | * @param {function} callbackFn - The callback function to be called when the query is 374 | * complete. 375 | * When the query is complete, this callback function will be called with one argument: 376 | * the EfficientGlideRecord object containing the records resultant from your query. 377 | * After querying (in your callback function), you can call methods such as .next() 378 | * and .getValue() to iterate through the returned records and get field values. 379 | */ 380 | get(sysID, callbackFn) { 381 | this.addQuery('sys_id', sysID); 382 | this.setLimit(1); 383 | 384 | this.query(function(egr) { 385 | if (egr.next()) { 386 | callbackFn(egr); 387 | } else { 388 | console.warn( 389 | 'EfficientGlideRecord: No records found in the target table ' + 390 | 'with sys_id "' + sysID + '".' 391 | ); 392 | } 393 | }); 394 | 395 | } 396 | 397 | /** 398 | * Perform the async query constructed by calling methods in this class, and get the 399 | * field(s) from the resultant record that were requested by calling 400 | * .addField(fieldName, getDisplayValue) 401 | * @async 402 | * @param {function} callbackFn - The callback function to be called 403 | * when the query is complete. 404 | * When the query is complete, this callback function will be called with one argument: 405 | * the EfficientGlideRecord object containing the records resultant from your query. 406 | * After querying (in your callback function), you can call methods such as .next() 407 | * and .getValue() to iterate through the returned records and get field values. 408 | */ 409 | query(callbackFn) { 410 | let paramName; 411 | 412 | if (!this._readyToSend()) { 413 | //Meaningful errors are logged by this._readyToSend(). 414 | return false; 415 | } 416 | 417 | for (paramName in this._config) { 418 | //Prevent iteration into non-own properties 419 | if (!this._config.hasOwnProperty(paramName)) { 420 | continue; 421 | } 422 | 423 | let paramVal; 424 | 425 | if (typeof this._config[paramName] === 'object') { 426 | paramVal = JSON.stringify(this._config[paramName]); 427 | } else { 428 | paramVal = this._config[paramName]; 429 | } 430 | 431 | this._gaQuery.addParam( 432 | paramName, 433 | paramVal 434 | ); 435 | } 436 | this._gaQuery.getXMLAnswer(function(answer, eGR) { 437 | //Make this work in Portal because SN is bad at documentation and consistency 438 | if (typeof eGR === 'undefined') { 439 | if (typeof this === 'undefined' || this === null) { 440 | 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://egr.snc.guru'); 441 | } else { 442 | //If Service Portal blocked access to/nullified the "this" object FOR 443 | // SOME FREAKIN REASON, grab it from the binding we did in .query(). 444 | eGR = this; 445 | } 446 | } 447 | //Parse answer into a useful object. 448 | answer = JSON.parse(answer); 449 | 450 | //let answer = response.responseXML.documentElement.getAttribute('answer'); 451 | // answer = JSON.parse(answer); //Throws if unparseable -- good. 452 | if (!answer.hasOwnProperty('_records')) { 453 | throw new Error( 454 | 'Something went wrong when attempting to get records from the server.\n' + 455 | 'Response object: \n' + 456 | JSON.stringify(answer) 457 | ); 458 | } 459 | 460 | eGR._query_complete = true; 461 | eGR._records = answer._records; 462 | eGR._row_count = answer._row_count; 463 | eGR._executing_as = answer._executing_as; 464 | 465 | callbackFn(eGR); 466 | }.bind(this), null, this); 467 | 468 | } 469 | 470 | /* The following methods can only be called after the query is performed */ 471 | 472 | /** 473 | * Check if there is a "next" record to iterate into using .next() (without actually 474 | * positioning the current record to the next one). Can only be called from the callback 475 | * function passed into .query()/.get() after the query has completed. 476 | * @returns {boolean} - True if there is a "next" record to iterate into, or false if not. 477 | */ 478 | hasNext() { 479 | if (!this._query_complete) { 480 | /*throw new Error( 481 | 'The .hasNext() method of EfficientGlideRecord can only be called from the ' + 482 | 'callback function after calling .query()' 483 | );*/ 484 | 485 | return false; 486 | } 487 | 488 | return (this._row_count > (this._current_record_index + 1)); 489 | } 490 | 491 | /** 492 | * Iterate into the next record, if one exists. 493 | * Usage is the same as GlideRecord's .next() method. 494 | * @returns {boolean} - True if there was a "next" record, and we've successfully positioned 495 | * into it. False if not. Can only be run from the callback function after a query using 496 | * .query() or .get(). 497 | */ 498 | next() { 499 | if (!this._query_complete) { 500 | /*throw new Error( 501 | 'The .next() method of EfficientGlideRecord can only be called from the ' + 502 | 'callback function after calling .query()' 503 | );*/ 504 | 505 | return false; 506 | } 507 | if (!this.hasNext()) { 508 | return false; 509 | } 510 | 511 | this._current_record_index++; 512 | this._current_record = this._records[this._current_record_index]; 513 | 514 | return true; 515 | } 516 | 517 | /** 518 | * Returns true if the specified field exists and can be read (even if it's blank). 519 | * Will return false in the following cases: 520 | * -The specified field on the current record cannot be read 521 | * -The specified field does not exist in the response object (which may happen if you don't 522 | * add the field to your request using .addField()). 523 | * -The specified field does not exist in the database 524 | * @param {String} fieldName - The name of the field to check whether the user can read or not. 525 | * @returns {Boolean} - Returns true if the specified field exists and can be read, or 526 | * false otherwise. 527 | */ 528 | canRead(fieldName) { 529 | if (!this._query_complete) { 530 | throw new Error( 531 | 'The .canRead() method of EfficientGlideRecord can only be called from the ' + 532 | 'callback function after calling .query(callbackFn)' 533 | ); 534 | } 535 | 536 | if (!this._current_record._field_values.hasOwnProperty(fieldName)) { 537 | console.warn( 538 | 'There is no field with the name ' + fieldName + ' in the ' + 539 | 'EfficientGlideRecord object. Did you remember to specify that you want to ' + 540 | 'get that field in the query using .addField()?' 541 | ); 542 | return false; 543 | } 544 | 545 | if (!this._current_record._field_values[fieldName].hasOwnProperty('can_read')) { 546 | console.warn( 547 | 'The requested field "' + fieldName + '" has no can_read node. ' + 548 | 'This should not happen. Returning a blank false.' 549 | ); 550 | return false; 551 | } 552 | 553 | return (!!this._current_record._field_values[fieldName].can_read) || false; 554 | } 555 | 556 | /** 557 | * Retrieve the database value for the specified field, if the user has permissions to read 558 | * that field's value. 559 | * @param fieldName 560 | * @returns {string} 561 | */ 562 | getValue(fieldName) { 563 | if (!this._query_complete) { 564 | throw new Error( 565 | 'The .getValue() method of EfficientGlideRecord can only be called from the ' + 566 | 'callback function after calling .query(callbackFn)' 567 | ); 568 | } 569 | 570 | if (!this._current_record._field_values.hasOwnProperty(fieldName)) { 571 | console.warn( 572 | 'There is no field with the name ' + fieldName + ' in the ' + 573 | 'EfficientGlideRecord object. Did you remember to specify that you want to ' + 574 | 'get that field in the query using .addField()?' 575 | ); 576 | return ''; 577 | } 578 | 579 | if (!this._current_record._field_values[fieldName].hasOwnProperty('value')) { 580 | console.warn( 581 | 'The requested field "' + fieldName + '" has no value node. ' + 582 | 'This should not happen. Returning a blank string.' 583 | ); 584 | return ''; 585 | } 586 | 587 | return this._current_record._field_values[fieldName].value || ''; 588 | } 589 | 590 | /** 591 | * Retrieve the display value for the specified field, if the user has permission to view 592 | * that field's value. 593 | * Can only be called from the callback function after the query is complete. 594 | * @param fieldName 595 | * @returns {string|*|string} 596 | */ 597 | getDisplayValue(fieldName) { 598 | if (!this._query_complete) { 599 | throw new Error( 600 | 'The .getDisplayValue() method of EfficientGlideRecord can only be called from the ' + 601 | 'callback function after calling .query(callbackFn)' 602 | ); 603 | } 604 | if (!this._current_record._field_values.hasOwnProperty(fieldName)) { 605 | console.warn( 606 | 'There is no field with the name ' + fieldName + ' in the ' + 607 | 'EfficientGlideRecord object. Did you remember to specify that you want to ' + 608 | 'get that field in the query using .addField()?' 609 | ); 610 | return ''; 611 | } 612 | if ( 613 | !this._current_record._field_values[fieldName].hasOwnProperty('display_value') || 614 | !this._current_record._field_values[fieldName].display_value 615 | ) { 616 | console.warn( 617 | 'There is no display value for the field with the name ' + fieldName + 618 | ' in the EfficientGlideRecord object. Did you remember to specify that you ' + 619 | 'want to get that field\'s display value in the query using ' + 620 | '.addField(fieldName, true)?' 621 | ); 622 | return ''; 623 | } 624 | 625 | return this._current_record._field_values[fieldName].display_value || ''; 626 | } 627 | 628 | /** 629 | * Retrieves the number of records returned from the query. 630 | * If used in conjunction with .setLimit(), then the maximum value returned from this 631 | * method will be the limit number (since no more records than the specified limit can 632 | * be returned from the server). 633 | * 634 | * @returns {number} - The number of records returned from the query. 635 | * @example 636 | * //Show the number of child Incidents missing Short Descriptions. 637 | * new EfficientGlideRecord('incident') 638 | * .addQuery('parent', g_form.getUniqueValue()) 639 | * .addNullQuery('short_description') 640 | * .addField('number') 641 | * .query(function (egrIncident) { 642 | * if (egrIncident.hasNext()) { 643 | * g_form.addErrorMessage( 644 | * egrIncident.getRowCount() + ' child Incidents are missing a short 645 | * description.' 646 | * ); 647 | * } 648 | * }); 649 | * @since 1.0.1 650 | */ 651 | getRowCount() { 652 | return this._row_count; 653 | } 654 | 655 | /* Private helper methods below */ 656 | 657 | _readyToSend() { 658 | if (!this._config.table_to_query) { 659 | console.error( 660 | 'EfficientGlideRecord not ready to query. Table name was not specified in ' + 661 | 'the constructor\'s initialize argument.' 662 | ); 663 | return false; 664 | } 665 | 666 | if (this._config.fields_to_get.length <= 1) { 667 | console.warn( 668 | 'EfficientGlideRecord: No fields other than sys_id were specified ' + 669 | 'to retrieve. \nYou can specify which fields you want to retrieve from ' + 670 | 'the GlideRecord object using .addField(fieldName, getDisplayValue). ' + 671 | 'Afterward, in your callback, you can use .getValue(fieldName). If ' + 672 | 'you set getDisplayValue to true in .addField(), you can also use ' + 673 | '.getDisplayValue(fieldName).\n' + 674 | 'Without fields to retrieve specified using .addField(), each record ' + 675 | 'will be returned with only a sys_id. \n' + 676 | 'This will not prevent you from performing your query, unless ' + 677 | 'something has gone terribly wrong.' 678 | ); 679 | //Not returning false, because this is not a blocking error. 680 | } 681 | 682 | //Warn if queries AND encoded queries are both empty and limit is unspecified 683 | // (but don't return false) 684 | if ( 685 | ( 686 | !this._config.hasOwnProperty('queries') || 687 | this._config.queries.length < 1 688 | ) && 689 | ( 690 | !this._config.hasOwnProperty('encoded_queries') || 691 | this._config.encoded_queries.length < 1 692 | ) && 693 | ( 694 | !this._config.hasOwnProperty('record_limit') || 695 | this._config.record_limit < 1 696 | ) 697 | ) { 698 | console.warn( 699 | 'The EfficientGlideRecord query has no query and no record limit ' + 700 | 'associated with it. This may result in poor performance when querying larger ' + 701 | 'tables. Please make sure that you need all records in the specified table, ' + 702 | 'as all records will be returned by this query.' 703 | ); 704 | } 705 | 706 | //Return true if none of the above validations have failed. 707 | return true; 708 | } 709 | } 710 | 711 | const EfficientGlideRecord = EfficientGlideRecordPortal; 712 | --------------------------------------------------------------------------------