Note this is only at the level of a proof of concept but it does work on the sObject Tree Request Body if the numberOfEmployees
is left out.
But I now realise my problem is more complicated - see Joining up data from multiple sfdx force:data:tree:export outputs.
/**
* Insert objects stored in JSON sObject tree format i.e. nested records.
*/
public inherited sharing class TreeJsonLoader {
private static final Map<String, SObjectType> GD_CACHE = Schema.getGlobalDescribe();
private static SObjectType typeFor(String type) {
return GD_CACHE.get(type);
}
// Cache as referenced often
private static final Map<SObjectType, Meta> META_CACHE = new Map<SObjectType, Meta>();
private static Meta metaFor(SObjectType parentType, SObjectType type) {
Meta meta = META_CACHE.get(type);
if (meta == null) {
meta = new Meta(parentType, type);
META_CACHE.put(type, meta);
}
return meta;
}
/**
* Metadata about an SObjectType and its fields.
*/
private class Meta {
SObjectType type;
SObjectField parentField;
Set<String> dataFields = new Set<String>();
String[] childRelationshipNames = new String[] {};
Meta(SObjectType parentType, SObjectType type) {
this.type = type;
// Child relationship names
DescribeSObjectResult dor = type.getDescribe();
for (Schema.ChildRelationship child : dor.getChildRelationships()) {
childRelationshipNames.add(child.getRelationshipName());
}
// Fields
for (SObjectField f : dor.fields.getMap().values()) {
DescribeFieldResult dfr = f.getDescribe();
// Non parent fields
if (dfr.getReferenceTo().size() == 0) {
dataFields.add(String.valueOf(f).toLowerCase());
} else {
if (parentType != null) {
for (SObjectType t : dfr.getReferenceTo()) {
// Parent field; assuming one only here which is not generally the case
if (t == parentType) {
parentField = f;
break;
}
}
}
}
}
}
}
/**
* Represents record parent/child nesting and holds the raw data and processed data.
*/
private class Node {
Node parent;
Node[] children = new Node[] {};
Map<String, Object> record;
SObjectType type;
SObject sob;
Node(Node parent, Map<String, Object> record) {
this.parent = parent;
if (parent != null) parent.children.add(this);
this.record = record;
Map<String, Object> attributes = (Map<String, Object>) record.get('attributes');
if (attributes != null) {
String t = (String) attributes.get('type');
if (t != null) {
this.type = typeFor(t);
}
}
}
/**
* Must be called after parent layer has been inserted so parent Id is available.
*/
SObject createSob() {
Meta meta = metaFor(parent.type, type);
sob = type.newSObject();
for (String key : record.keySet()) {
if (meta.dataFields.contains(key.toLowerCase())) {
Object value = record.get(key);
// TODO type conversion e.g. dates and maybe numbers too
if (value != null) sob.put(key, value);
}
}
if (meta.parentField != null) {
sob.put(meta.parentField, parent.sob.Id);
}
return sob;
}
}
/**
* Entry point that assumes the tree JSON comes from a static resource.
*/
public void loadStaticResource(String ns, String name) {
for (StaticResource sr : [
select Body
from StaticResource
where NamespacePrefix = :ns
and Name = :name
]) {
loadJson(sr.Body.toString());
}
}
private void loadJson(String jsonString) {
Map<String, Object> holder = (Map<String, Object>) JSON.deserializeUntyped(jsonString);
Node root = new Node(null, new Map<String, Object>());
splitIntoNodes(root, holder);
insertLayersOfNodes(root.children);
}
private void splitIntoNodes(Node parent, Map<String, Object> holder) {
Object[] records = (Object[]) holder.get('records');
for (Object r : records) {
Map<String, Object> record = (Map<String, Object>) r;
Node node = new Node(parent, record);
Meta meta = metaFor(parent.type, node.type);
// Add children
for (String childRelationshipName : meta.childRelationshipNames) {
Map<String, Object> childHolder = (Map<String, Object>) record.get(childRelationshipName);
if (childHolder != null) {
// Recursive call
splitIntoNodes(node, childHolder);
}
}
}
}
private void insertLayersOfNodes(Node[] nodes) {
Node[] nextLayer = new Node[] {};
// Allows multiple child types
Map<SObjectType, SObject[]> sobsByType = new Map<SObjectType, SObject[]>();
for (Node node : nodes) {
SObject[] sobs = sobsByType.get(node.type);
if (sobs == null) {
sobs = new SObject[] {};
sobsByType.put(node.type, sobs);
}
sobs.add(node.createSob());
nextLayer.addAll(node.children);
}
for (SObjectType t : sobsByType.keySet()) {
// TODO check insert rights
insert sobsByType.get(t);
}
if (nextLayer.size() > 0) {
// Recursive call
insertLayersOfNodes(nextLayer);
}
}
}