From 5c14faa02e0603f8431ee14863144395bdd01253 Mon Sep 17 00:00:00 2001
From: William Velzeboer <6429417+wimvelzeboer@users.noreply.github.com>
Date: Tue, 20 Jan 2026 10:42:08 +0000
Subject: [PATCH] Add getParentFieldById methods to fflib_SObjects2 (#73)
This method can fetch deeply related data which is distributed over multiple related domains
---
README.md | 5 +-
.../classes/domains/fflib_SObjects2.cls | 166 +++++++++++++++++-
.../classes/utils/fflib_ArrayUtils.cls | 30 ++++
.../classes/domains/fflib_SObjects2Test.cls | 77 ++++++++
4 files changed, 271 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index 76b57ff..1f13251 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,10 @@ Some of the changes that have major impact are listed here;
_mergeMaps_,
Add method overload to merge two maps of idByBoolean,
- **fflib_SObjects2**
The following methods have been added to the SObjects2 class:
- _getBooleanFieldByIdField_
+ _getBooleanFieldByIdField_
+ _getParentBooleanFieldById_
+ _getParentDecimalFieldById_
+ _getParentStringFieldById_
- **API Upgrades**
Upgraded to API 65.0
diff --git a/sfdx-source/apex-extensions/main/default/classes/domains/fflib_SObjects2.cls b/sfdx-source/apex-extensions/main/default/classes/domains/fflib_SObjects2.cls
index 8680ab9..73dc37c 100644
--- a/sfdx-source/apex-extensions/main/default/classes/domains/fflib_SObjects2.cls
+++ b/sfdx-source/apex-extensions/main/default/classes/domains/fflib_SObjects2.cls
@@ -104,6 +104,129 @@ public virtual class fflib_SObjects2
return (result.size() == 0) ? null : result.get(0);
}
+ /**
+ * Gets the field value from the last given grandparent domain by the record Id
+ *
+ * @param valueField The field use return as value for the map
+ * @param domainsByRelationshipField A Map of domains and the in hierarchical order from first parent to grandparent
+ *
+ * @return Returns a map with KEY: The Ids of the records in this domain,
+ * to the VALUE: The value of the given valueField of the last grandparent record listed in the relatedDomains,
+ * null if no grandparent exists.
+ *
+ * @example
+ * ICases casesDomain = Cases.newInstance(caseRecords);
+ * IContacts contactsDomain = Contacts.newInstance(contactRecords);
+ * IAccount accountDomain = Accounts.newInstance(accountRecords);
+ *
+ * // A map with key - Case.Id to value - Account.Id
+ * Map accountNameByCaseId =
+ * casesDomain.getParentStringFieldById(
+ * Schema.Account.Name,
+ * new Map
+ * {
+ * Schema.Case.ContactId => contactDomain,
+ * Schema.Contact.AccountId => accountDomain
+ * });
+ */
+ public virtual Map getParentStringFieldById(
+ Schema.SObjectField valueField,
+ Map domainsByRelationshipField)
+ {
+ Map parentIdByChildId = getParentIdById(domainsByRelationshipField);
+
+ List domains = domainsByRelationshipField.values();
+ fflib_SObjects2 lastGrandParentDomain = domains.get(domains.size() - 1);
+
+ return fflib_ArrayUtils.replaceValue(
+ parentIdByChildId,
+ lastGrandParentDomain.getStringFieldByIdField(
+ valueField,
+ lastGrandParentDomain.getSObjectType().getDescribe().fields.getMap().get('Id')));
+ }
+
+ /**
+ * Gets the field value from the last given grandparent domain by the record Id
+ *
+ * @param valueField The field use return as value for the map
+ * @param domainsByRelationshipField A Map of domains and the in hierarchical order from first parent to grandparent
+ *
+ * @return Returns a map with KEY: The Ids of the records in this domain,
+ * to the VALUE: The value of the given valueField of the last grandparent record listed in the relatedDomains,
+ * null if no grandparent exists.
+ *
+ * @example
+ * ICases casesDomain = Cases.newInstance(caseRecords);
+ * IContacts contactsDomain = Contacts.newInstance(contactRecords);
+ * IAccount accountDomain = Accounts.newInstance(accountRecords);
+ *
+ * // A map with key - Case.Id to value - Account.Id
+ * Map accountNameByCaseId =
+ * casesDomain.getParentDecimalFieldById(
+ * Schema.Account.Name,
+ * new Map
+ * {
+ * Schema.Case.ContactId => contactDomain,
+ * Schema.Contact.AccountId => accountDomain
+ * });
+ */
+ public virtual Map getParentDecimalFieldById(
+ Schema.SObjectField valueField,
+ Map domainsByRelationshipField)
+ {
+ Map parentIdByChildId = getParentIdById(domainsByRelationshipField);
+
+ List domains = domainsByRelationshipField.values();
+ fflib_SObjects2 lastGrandParentDomain = domains.get(domains.size() - 1);
+
+ return fflib_ArrayUtils.replaceValue(
+ parentIdByChildId,
+ lastGrandParentDomain.getDecimalById(
+ valueField,
+ lastGrandParentDomain.getSObjectType().getDescribe().fields.getMap().get('Id')));
+ }
+
+ /**
+ * Gets the field value from the last given grandparent domain by the record Id
+ *
+ * @param valueField The field use return as value for the map
+ * @param domainsByRelationshipField A Map of domains and the in hierarchical order from first parent to grandparent
+ *
+ * @return Returns a map with KEY: The Ids of the records in this domain,
+ * to the VALUE: The value of the given valueField of the last grandparent record listed in the relatedDomains,
+ * null if no grandparent exists.
+ *
+ * @example
+ * ICases casesDomain = Cases.newInstance(caseRecords);
+ * IContacts contactsDomain = Contacts.newInstance(contactRecords);
+ * IAccount accountDomain = Accounts.newInstance(accountRecords);
+ *
+ * // A map with key - Case.Id to value - Account.Id
+ * Map accountNameByCaseId =
+ * casesDomain.getParentBooleanFieldById(
+ * Schema.Account.Name,
+ * new Map
+ * {
+ * Schema.Case.ContactId => contactDomain,
+ * Schema.Contact.AccountId => accountDomain
+ * });
+ */
+ public virtual Map getParentBooleanFieldById(
+ Schema.SObjectField valueField,
+ Map domainsByRelationshipField)
+ {
+ Map parentIdByChildId = getParentIdById(domainsByRelationshipField);
+
+ List domains = domainsByRelationshipField.values();
+ fflib_SObjects2 lastGrandParentDomain = domains.get(domains.size() - 1);
+
+ return fflib_ArrayUtils.replaceValue(
+ parentIdByChildId,
+ lastGrandParentDomain.getBooleanFieldByIdField(
+ valueField,
+ lastGrandParentDomain.getSObjectType().getDescribe().fields.getMap().get('Id')));
+ }
+
/**
* Creates a map between the record Ids of this domain to the last given grandparent domain.
*
@@ -129,11 +252,14 @@ public virtual class fflib_SObjects2
*/
public virtual Map getParentIdById(Map domainsByRelationshipField)
{
- // Takes the highest related domain from the domainsByRelationshipField
+ // Clone the domain map as we are about to modify it within this method
+ Map domains = domainsByRelationshipField.clone();
+
+ // Takes the highest related domain from the domains
Schema.SObjectField relationshipField =
- new List(domainsByRelationshipField.keySet())
+ new List(domains.keySet())
.get(0);
- fflib_SObjects2 relatedDomain = domainsByRelationshipField.remove(relationshipField);
+ fflib_SObjects2 relatedDomain = domains.remove(relationshipField);
// Extract the Id values of the related SObjectType
Map result = getIdFieldByIdField(
@@ -141,10 +267,10 @@ public virtual class fflib_SObjects2
getSObjectType().getDescribe().fields.getMap().get('Id'));
// Check if we need to resolve another parent relation, if not return the results
- if (domainsByRelationshipField.isEmpty()) return result;
+ if (domains.isEmpty()) return result;
// Fetch the Ids of the related SObjectType
- Map parentIdByChildId = relatedDomain.getParentIdById(domainsByRelationshipField);
+ Map parentIdByChildId = relatedDomain.getParentIdById(domains);
// Replace the maps value (ParentId) with the grand parent Id
return fflib_ArrayUtils.replaceValue(result, parentIdByChildId);
@@ -207,7 +333,7 @@ public virtual class fflib_SObjects2
*/
public virtual Map> getChildIdsById(Map domainsByRelationshipField)
{
- // Takes the highest related domain from the domainsByRelationshipField
+ // Takes the highest related domain from the domains
Schema.SObjectField relationshipField =
new List(domainsByRelationshipField.keySet())
.get(0);
@@ -552,6 +678,34 @@ public virtual class fflib_SObjects2
return result;
}
+ /**
+ * Get a map with the values of two fields.
+ * Key fields containing null values are omitted
+ *
+ * @param valueField The field to use as the Value of the Map
+ * @param keyField The Id field to use as the Key of the map
+ *
+ * @return Returns a map with the values of two fields
+ *
+ * @example
+ * Accounts accounts = Accounts.newInstance(records);
+ * Map accountNameById = accounts.getFieldValueByIdField(Account.Name, Account.Id);
+ */
+ @TestVisible
+ protected virtual Map getFieldValueByIdField(SObjectField valueField, SObjectField keyField) {
+ Map result = new Map();
+ for (SObject record : getRecords())
+ {
+ if (record.get(keyField) == null) continue;
+
+ result.put(
+ (Id) record.get(keyField),
+ record.get(valueField)
+ );
+ }
+ return result;
+ }
+
/**
* @return Returns only the first SObject contained in the domain.
*/
diff --git a/sfdx-source/apex-extensions/main/default/classes/utils/fflib_ArrayUtils.cls b/sfdx-source/apex-extensions/main/default/classes/utils/fflib_ArrayUtils.cls
index 830d025..e37aaaf 100644
--- a/sfdx-source/apex-extensions/main/default/classes/utils/fflib_ArrayUtils.cls
+++ b/sfdx-source/apex-extensions/main/default/classes/utils/fflib_ArrayUtils.cls
@@ -373,6 +373,36 @@ public class fflib_ArrayUtils
return result;
}
+ /**
+ * Takes two maps and replaces the value of the source with the value of the replacement,
+ * where the source value matches the replacement key.
+ * The value will become null if the source value does not exists as key in replacement map
+ *
+ * @param source The map to replace its value
+ * @param replacement The map to use its value for replacement
+ *
+ * @return Returns a Map<{Source} Id, {Replacement} Id>
+ */
+ public static Map replaceValue(Map source, Map replacement)
+ {
+ Map result = new Map();
+ for (Id keyId : source.keySet())
+ {
+ Id sourceValue = source.get(keyId);
+
+ if (replacement.containsKey(sourceValue))
+ {
+ result.put(keyId, replacement.get(sourceValue));
+ }
+ else
+ {
+ // There is no replacement value for this keyId
+ result.put(keyId, null);
+ }
+ }
+ return result;
+ }
+
/**
* Takes two maps and replaces the value of the source with the value of the replacement,
* where the source value matches the replacement key.
diff --git a/sfdx-source/apex-extensions/tests/classes/domains/fflib_SObjects2Test.cls b/sfdx-source/apex-extensions/tests/classes/domains/fflib_SObjects2Test.cls
index c0a1366..f78a311 100644
--- a/sfdx-source/apex-extensions/tests/classes/domains/fflib_SObjects2Test.cls
+++ b/sfdx-source/apex-extensions/tests/classes/domains/fflib_SObjects2Test.cls
@@ -257,6 +257,83 @@ private class fflib_SObjects2Test
System.Assert.isNull(result.get(caseIdC), 'Case Id B should have no related Account Id');
}
+ @IsTest
+ static void itShouldReturnTheRelatedParentStringById()
+ {
+ // GIVEN a case related to contact related to Account ,
+ // a case record related to contact but not to Account,
+ // and a case record related to nothing.
+ // All encapsulated in domains.
+ Id caseIdA = fflib_IDGenerator.generate(Schema.Case.SObjectType);
+ Id caseIdB = fflib_IDGenerator.generate(Schema.Case.SObjectType);
+ Id caseIdC = fflib_IDGenerator.generate(Schema.Case.SObjectType);
+ Id contactIdA = fflib_IDGenerator.generate(Schema.Contact.SObjectType);
+ Id contactIdB = fflib_IDGenerator.generate(Schema.Contact.SObjectType);
+ Id accountId = fflib_IDGenerator.generate(Schema.Account.SObjectType);
+ Account accountRecord = new Account(Id = accountId, Name = 'Account Name', AnnualRevenue = 100.1);
+ Contact contactRecordA = new Contact(Id = contactIdA, AccountId = accountId, DoNotCall = true);
+ Contact contactRecordB = new Contact(Id = contactIdB);
+ Case caseRecordA = new Case(Id = caseIdA, ContactId = contactIdA);
+ Case caseRecordB = new Case(Id = caseIdB, ContactId = contactIdB);
+ Case caseRecordC = new Case(Id = caseIdC);
+
+ DomainAccounts accountDomain = new DomainAccounts(new List {accountRecord});
+ DomainContacts contactDomain = new DomainContacts(new List {contactRecordA, contactRecordB});
+ DomainCases caseDomain = new DomainCases(new List {caseRecordA, caseRecordB, caseRecordC});
+
+ // WHEN we request the relation between Case and Account
+ System.Test.startTest();
+ Map stringResult = caseDomain.getParentStringFieldById(
+ Schema.Account.Name,
+ new Map
+ {
+ Schema.Case.ContactId => contactDomain,
+ Schema.Contact.AccountId => accountDomain
+ }
+ );
+ Map decimalResult = caseDomain.getParentDecimalFieldById(
+ Schema.Account.AnnualRevenue,
+ new Map
+ {
+ Schema.Case.ContactId => contactDomain,
+ Schema.Contact.AccountId => accountDomain
+ }
+ );
+ Map booleanResult = caseDomain.getParentBooleanFieldById(
+ Schema.Contact.DoNotCall,
+ new Map
+ {
+ Schema.Case.ContactId => contactDomain
+ }
+ );
+ System.Test.stopTest();
+
+ // THEN it should return the AccountId By CaseId
+ System.Assert.areEqual(3, stringResult.size(), 'Expected one related account to the case');
+ System.Assert.isTrue(stringResult.containsKey(caseIdA));
+ System.Assert.isTrue(stringResult.containsKey(caseIdB));
+ System.Assert.isTrue(stringResult.containsKey(caseIdC));
+ System.Assert.areEqual('Account Name', stringResult.get(caseIdA), 'Account Name should be related to the Case Id A');
+ System.Assert.isNull(stringResult.get(caseIdB), 'Case Id B should have no related Account');
+ System.Assert.isNull(stringResult.get(caseIdC), 'Case Id B should have no related Account');
+
+ System.Assert.areEqual(3, decimalResult.size(), 'Expected one related account to the case');
+ System.Assert.isTrue(decimalResult.containsKey(caseIdA));
+ System.Assert.isTrue(decimalResult.containsKey(caseIdB));
+ System.Assert.isTrue(decimalResult.containsKey(caseIdC));
+ System.Assert.areEqual(100.1, decimalResult.get(caseIdA), 'Account Name should be related to the Case Id A');
+ System.Assert.isNull(decimalResult.get(caseIdB), 'Case Id B should have no related Account');
+ System.Assert.isNull(decimalResult.get(caseIdC), 'Case Id B should have no related Account');
+
+ System.Assert.areEqual(3, booleanResult.size(), 'Expected one related account to the case');
+ System.Assert.isTrue(booleanResult.containsKey(caseIdA));
+ System.Assert.isTrue(booleanResult.containsKey(caseIdB));
+ System.Assert.isTrue(booleanResult.containsKey(caseIdC));
+ System.Assert.isTrue(booleanResult.get(caseIdA), 'Contact DoNotCall should be related to the Case Id A');
+ System.Assert.isNull(booleanResult.get(caseIdB), 'Case Id B should have no related Contact');
+ System.Assert.isNull(booleanResult.get(caseIdC), 'Case Id B should have no related Contact');
+ }
+
/** Unit test for the deprecated method **/
@IsTest
static void itShouldReturnTheRelatedParentDomainId_deprecated()