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()