Christopher Cro
Giga Expert

Scenario:

You want to store the values of the current record you're accessing via GlideRecord for later reference. Easy, right? 
Wrong!

Problem #1:

Storing a reference to GlideRecord doesn't help, it's all lost as soon as the next() method is called.

Solution

This is because GlideRecord is mutable. The old record is gone and there's nothing we can do to keep it around. That being said, we can make a copy!

Keep in mind that it is impossible to make a direct copy of the GlideRecord itself. We are instead copying the contents of each GlideElement contained within using the GlideRecord and building a Javascript native object with the results. We do this using a for/in loop to iterate over the object properties and the getValue method of GlideRecord to extract copies of each GlideElement value.

The below implementation took about 4ms to convert a single Task record in my benchmark.

var obj = {};
var gr = new GlideRecord('task');
gr.get('1337baddeadbeeffeedcafe1337426');

for(k in gr){
  obj[k] = gr.getValue(k);
}

Output Example:

{
  "sys_meta": null, // NOTE: This doesn't exist
  "parent": "d65c314deadbeef44a8b42e58d4bcb45",
  "caused_by": null,
  "watch_list": null,
  "active_escalation": null,
  "upon_reject": "cancel",
  "sys_updated_on": "2020-03-03 14:33:40",
   // SNIP
  "active": "1", // NOTE: This is a boolean value
   // SNIP
}

Problem #2:

Some of the copied fields have inconsistent or otherwise unexpected values. Everything is a string, booleans are unhelpfully '1'/'0' (often mixed in with integer '1'/'0' values), nulls are everywhere, etc... This is, unfortunately, working as intended, per the documentation.

GlideRecord's getValue method always returns strings (...or null). Null is returned when an invalid field is being accessed... or when that field is currently an empty string. It also converts booleans to the strings '1' and '0'. Dates are handled by the default toString, outputting a formatted date that varies based on the configured server

timezone. Obviously, there's room for improvement here.

Solution A (Converts almost everything!)

It's possible to cross reference the getValue output against methods on the individual GlideElements to achieve more useful output. Leveraging GlideElement's toString, among a few other quirks related to Rhino native objects, we can achieve a nearly flawless conversion for most data types that can be coerced into a Javascript analogue.

Anything we don't have an analogue for, like empty dates and special fields, get converted to null ("special", meaning anything Glide-specific inventions like journals, tags, durations). Unlike the first implementation, only special cases get set as null. There's no ambiguity, because all other cases are depicted using the correct data types (empty strings are '', invalid properties are left undefined).

The below implementation took about 15ms to convert a single Task record in my benchmark.

 // Usage: var outObject = extractRecord(glideRecord);
function extractRecord(gr){
    var obj = {};
    var grK;

    // Helper function, strains out information from the GlideElement
    function extractElement(gr, k){
        // GlideElement toString explicitly shows booleans
        var kString = grK.toString();
        // Convert 'booleans' to booleans
        if(kString === 'true' || kString === 'false')
            return kString === 'true';
        // This returns true for values that can be coaxed into a number
        // This will pass for date strings, since they start with a number
        if(!isNaN(parseFloat(kString))){
            // This will only pass for bona fide numbers
            if(isFinite(kString))
                return parseFloat(kString);
            // Only GlideElements with a date have the dateNumericValue method
            // Reflection is slow, so we put this off as long as possible
            // (try/catch seems to be even slower in Rhino)
            if(grK.dateNumericValue)
                // Extract a unix timestamp and build a JS Date from that
                return new Date(grK.dateNumericValue());
        }
        // Only empty Dates, special fields, strings, and RefIds get this far
        // special fields and empty dates are interpreted as '', we return null for these
        if(kString === '') return null;
        // String fields and refids get returned untouched
        // Reminder: we filtered out the real empty strings several steps ago
        return kString
    }

    for(k in gr){
        // Skips sys_meta, which is always null. This prevents a Rhino warning
        if(k === 'sys_meta') continue;
        grK = gr[k];
        // Filter out bad properties immediately. Complain in the logs
        if(grK === null){
            gs.debug('Attempted to access non-existent GlideRecord property ' + k);
            continue;
        }
        // We can now safely assume all other null values are empty strings!
        if(gr.getValue(k) === null){
            obj[k] = '';
        } else {
            // Punt the element to the helper function for a deeper inspection
            obj[k] = extractElement(gr, k);
        }
    }
    return obj;
}

Output Example

{
  "parent": "d65c31421b17deadbeef42e58d4bcb45",
  "caused_by": "", // Empty lists and strings are always "", not null
  "watch_list": "",
  "active_escalation": "",
  "upon_reject": "cancel",
  "sys_updated_on": "2020-03-03T14:33:40.000Z", //NOTE: This is a real JS Date object
  "support_manager": "",
  "approval_history": null, //NOTE: Non JS-ish values are null, nothing else
  "skills": "",
  "number": "CS0244029",
  "problem": "",
  "state": 18, // Numbers are converted to real JS numbers, not strings
  "assigned_on": null //NOTE: Empty Date fields are also set to null
  // SNIP
}

Solution B (Faster!)

While 15ms is nothing to sneeze at, it's no 4ms turnaround! If you don't particularly care about dates, you can get a fair performance boost using this simplified implementation.

This speedier version does not convert GlideDates, instead stringifying them. This is so much faster because we don't have to reflect to figure out if the GlideElement is a GlideDate or not.

The below implementation took about 7ms in my benchmark:

var obj = {};
var gr = new GlideRecord('task');
gr.get('1337baddeadbeeffeedcafe1337426');

for(k in gr){
  // Skip sys_meta, it's always null
  if(k === 'sys_meta') continue;
  var grK = gr[k];
  // Filter out invalid properties so we can safely assume nulls to be empty strings
  if(grK === null){
    gs.debug('Attempted to access non-existent GlideRecord property ' + k) 
    continue;
  }
  if(gr.getValue(k) === null){ 
    obj[k] = '';
  } else {
    // Use GlideElement toString, since it provides better boolean strings
    var kString = grK.toString();
    // Convert from 'booleans' to booleans
    if(kString === 'true' || kString === 'false'){
      obj[k] = kString === 'true'
    // Check if the string is a valid number and convert if possible
    } else if(!isNaN(parseFloat(kString)) && isFinite(kString)) {
      obj[k] = parseFloat(kString);
    } else {
      // Anything that lacks a toString output is assumed to be non-JSish
      // GlideDates are converted to date strings based on server timezone settings
      // Reminder: we already captured the real empty strings in an earlier step!
      obj[k] = kString || null;
    }
  }
} 

Output Example

{
  "parent": "d65c31421b17deadbeef42e58d4bcb45",
  "caused_by": "",
  "watch_list": "",
  "active_escalation": "",
  "upon_reject": "cancel",
  "sys_updated_on": "2020-03-03 14:33:40",
  "support_manager": "",
  "approval_history": null,
  "skills": "",
  // SNIP
  "state": 18,
  // SNIP
  "knowledge": false,
  // SNIP
}
Comments
DrewW
Mega Sage

Could you give a use case on why you would want/need to use this, or the case you used it in?  All of the cases I have ever needed to store a rec for later use the "later" is minutes to days later so I would not want to store a copy of it I want the latest version so I just store the sys_id and go get the record again.

Christopher Cro
Giga Expert

Hey Drew! In my particular use-case, I wanted to be able to process query results as a collection (via map/reduce) because my coding background is fairly grounded in the FP camp. I suppose this has added memory overhead, but performance has been relatively solid far me so far.

I suppose another use case could be to serialize the data and throw it in a scratchpad... there may be some situations where you'd like to have a full query shipped along with your page. Obviously that's a little excessive, but at least it's convenient to do now?

Version history
Last update:
‎03-03-2020 07:08 PM
Updated by: