Apex - SoqlMultiTableParser - SOQL Subqueries to multiple individual tables
Mon Aug 12 2024 21:41:03 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 splits out * each child list to it's own individual list using the path as map key * The parent map key is called "parent", child map keys are based on the * path like: "Opportunities" or Opportunities.OpportunityContactRoles * @use case The main use case is to transform SOQL query results that can be used * for exporting CSV or XLSX data that require individual tables for child lists. * A secondary use is to store related data for archiving purposes where a * potential reverse loading of related data might be required. * - https://medium.com/@justusvandenberg/dynamically-handle-salesforce-soql-subquery-response-data-using-apex-8130bd0622aa */ public with sharing class SoqlMultiTableParser { /** **************************************************************************************************** ** ** 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 * @param output The final output map * @return A list of flattened JSON records */ public static Map<String,List<Map<String,Object>>> create(Object[] input){ // Create a variable for the data output Map<String,List<Map<String,Object>>> output = new Map<String,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, 'parent'); // 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, Map<String,List<Map<String,Object>>> output, String path){ // Each list goes into it's own flat table, so create the empty set, map and // populate the lists paths if(!output.containsKey(path)){ output.put(path, new List<Map<String,Object>>()); } // 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, 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.get(path).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 * @param listPath The current list path (map key) in the traverse path */ private static void traverseMap(Map<String,Object> input, Map<String,List<Map<String,Object>>> output, Map<String,Object> row, String path, String listPath){ // 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 != 'parent') ? 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, listPath ); // If it's not a map or a list, it must a value, so add the value to row }else{ populateRow( input.get(key), childPath, row, listPath ); } } } /** * @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 * @param listPath The current list path (map key) in the traverse path */ private static void populateRow(Object input, String path, Map<String,Object> row, String listPath){ // Add the value to the row row.put(path.removeStart(listPath).removeStart('.'),input); } }
Comments