Apex - SoqlTableParser - SOQL Subqueries to a single flat table
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); } }
Comments