Apex - SoqlTableParser - SOQL Subqueries to a single flat table

PHOTO EMBED

Mon Aug 12 2024 21:42:20 GMT+0000 (Coordinated Universal Time)

Saved by @Justus

/**
 * @author      Justus van den Berg (jfwberg@gmail.com)
 * @date        August 2024
 * @copyright   (c) 2024 Justus van den Berg
 * @license     MIT (See LICENSE file in the project root)
 * @description Class that converts JSON with nested objects to flat values and
 *              puts all values in a single, flat, combined tabled
 * @use case    The main use case is to transform SOQL query results that can be used
 *              for exporting CSV or XLSX data for external systems like analytics,
 *              security or Data Cloud Data Streams
 * - https://medium.com/@justusvandenberg/dynamically-handle-salesforce-soql-subquery-response-data-using-apex-8130bd0622aa
 */
@SuppressWarnings('PMD.OneDeclarationPerLine, PMD.AvoidGlobalModifier, PMD.CognitiveComplexity, PMD.ExcessiveParameterList')
public with sharing class SoqlTableParser {

    /** **************************************************************************************************** **
     **                                          PRIVATE CONSTANTS                                           **
     ** **************************************************************************************************** **/
    // Parameter names to be ignored when creating the data table
    private final static Set<String> ATTRIBUTE_FILTER = new Set<String>{'attributes','done','totalSize','nextRecordsUrl'};

    // List parameter names of this type will not be appended, this allows for a more flat table
    private final static Set<String> LIST_NAME_FILTER  = new Set<String>{'records'};


    /** **************************************************************************************************** **
     **                                       PUBLIC SUPPORT METHODS                                         **
     ** **************************************************************************************************** **/
    /**
     * @description Create table from an untyped Object list
     * @param input A list of Objects to traverse
     * @return      A list of flattened JSON records
     */
    public static List<Map<String,Object>> create(Object[] input){

        // Create a variable for the data output
        List<Map<String,Object>> output = new List<Map<String,Object>>();

        // Input validation, if the input is null, return an empty list
        if(input == null){return output;}

        // Populate the data by traversing the input and add the data to the output
        // Set the path to an empty string, as it is the top level we begin with
        traverseList(input, output, '');

        // Return the flat table
        return output;
    }


    /** **************************************************************************************************** **
     **                                       PRIVATE TRAVERSE METHODS                                       **
     ** **************************************************************************************************** **/
    /**
     * @description Traverse the parent list
     * @param input  The input to traverse
     * @param output The final output map
     * @param path   The location in the traverse path
     */
    private static void traverseList(Object[] input, List<Map<String,Object>> output, String path){

        // Having this type casting again seems redundant, but doing the check in here
        // saves having to do it twice, what on 10k+ statements gives a small performance improvement
        for(Integer i=0, max=input.size();i<max;i++){

            // Create a new row to combine the values in the list
            Map<String,Object> row = new Map<String,Object>();

            // Handle each child object according to it's type
            if(input[i] instanceof Map<String,Object>){
                traverseMap(
                    (Map<String,Object>) input[i],
                    output,
                    row,
                    path
                );

            // If the child is a list, traverse the child list
            }else if(input[i] instanceof Object[]){
                traverseList(
                    (Object[]) input[i],
                    output,
                    (LIST_NAME_FILTER.contains(path?.substringAfterLast('.'))) ?
                        path?.substringBeforeLast('.') :
                        path
                );

            // Anything other than objects and maps, primitive data types are not supported
            // i.e. String Lists ['a','b','c'] or [1,2,3,] etc. So simply ignore these values
            }else{
                continue;
            }

            // After the traversal is complete, Add the full row to the table
            if(!row.isEmpty()){output.add(row);}
        }
    }


    /**
     * @description Method to traverse a map
     * @param input  The input to traverse
     * @param output The final output map
     * @param row    The current row in the traverse path
     * @param path   The location in the traverse path
     */
    private static void traverseMap(Map<String,Object> input, List<Map<String,Object>> output, Map<String,Object> row, String path){

        // Iterate all the values in the input
        for(String key : input.keySet() ){

            // Continue if an attribute needs to be ignored
            if(ATTRIBUTE_FILTER.contains(key)){continue;}

            // Create the path for the specfic child node
            String childPath = (String.isNotBlank(path) ? path + '.' + key : key);

            // If the child node is an object list, traverse as list
            if(input.get(key) instanceof Object[]){
                traverseList(
                    (Object[]) input.get(key),
                    output,
                    (LIST_NAME_FILTER.contains(childPath?.substringAfterLast('.'))) ?
                        childPath?.substringBeforeLast('.') :
                        childPath
                );

            // If the child node is an object, (i.e. owner.name), traverse as map
            }else if(input.get(key) instanceof Map<String,Object>){
                traverseMap(
                    (Map<String,Object>) input.get(key),
                    output,
                    row,
                    childPath
                );

            // If it's not a map nor a list, it must be a value, so add the value to row
            }else{
                populateRow(
                    input.get(key),
                    childPath,
                    row
                );
            }
        }
    }


    /**
     * @description Method to add the value to a row at the end of a traversel path
     * @param input    The input to traverse
     * @param path     The location in the traverse path
     * @param row      The current row in the traverse path
     */
    private static void populateRow(Object input, String path, Map<String,Object> row){

        // Add the value to the row
        row.put(path,input);
    }
}
content_copyCOPY