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