Skip to content

Commit 260cfe1

Browse files
author
tstone
committed
Fields that are polymorphic fail when the chosen sObjectType is not the type specified in the selector.
Add the ability to set the sObjectType when validating the field exists on the object. Use the type set in the selector to determine which polymorphic type to use. Example: Lead.Owner.UserRoleID Owner is GROUP | USER Group does not contain UserRoleID and fails when the getField call sets the sObjectField token to null.
1 parent 0a9d4e8 commit 260cfe1

File tree

3 files changed

+137
-5
lines changed

3 files changed

+137
-5
lines changed

fflib/src/classes/fflib_QueryFactory.cls

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
8484
private Schema.ChildRelationship relationship;
8585
private Map<Schema.ChildRelationship, fflib_QueryFactory> subselectQueryMap;
8686

87-
private QueryField getFieldToken(String fieldName){
87+
private QueryField getFieldToken(String fieldName, Schema.sObjectType relatedSObjectType){
8888
QueryField result;
8989
if(!fieldName.contains('.')){ //single field
9090
Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(table).getField(fieldName.toLowerCase());
@@ -103,7 +103,25 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
103103
if (token != null && enforceFLS)
104104
fflib_SecurityUtils.checkFieldIsReadable(lastSObjectType, token);
105105
if(token != null && i.hasNext() && token.getDescribe().getSOAPType() == Schema.SOAPType.ID){
106-
lastSObjectType = token.getDescribe().getReferenceTo()[0]; //if it's polymorphic doesn't matter which one we get
106+
if(relatedSObjectType == null) {
107+
lastSObjectType = token.getDescribe().getReferenceTo()[0]; //if it's polymorphic get the first one - user did not specify the one to use.
108+
}else{
109+
lastSObjectType = null;
110+
List<Schema.sObjectType> relatedObjs = token.getDescribe().getReferenceTo(); //if it's polymorphic, it matters which one we use - i.e. Lead.Owner is GROUP|USER and each has different fields.
111+
if(relatedObjs.size() > 1) {
112+
String relatedSObjectName = fflib_SObjectDescribe.getDescribe(relatedSObjectType).getDescribe().getName();
113+
for(Schema.sObjectType sot : relatedObjs) {
114+
if(fflib_SObjectDescribe.getDescribe(sot).getDescribe().getName() == relatedSObjectName){
115+
lastSObjectType = sot;
116+
break;
117+
}
118+
}
119+
if(lastSObjectType == null)
120+
throw new InvalidRelationshipException(fieldName, relatedSObjectType);
121+
}else{
122+
lastSObjectType = relatedOjs[0]; //only one type returned, use it.
123+
}
124+
}
107125
fieldPath.add(token);
108126
}else if(token != null && !i.hasNext()){
109127
fieldPath.add(token);
@@ -118,6 +136,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
118136
}
119137
return result;
120138
}
139+
private QueryField getFieldToken(String fieldName) {
140+
return this.getFieldToken(fieldName, null);
141+
}
121142

122143
/**
123144
* fflib_QueryFactory instances will be considered equal if they produce the same SOQL query.
@@ -192,9 +213,19 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
192213
* @param fieldName the API name of the field to add to the query's SELECT clause.
193214
**/
194215
public fflib_QueryFactory selectField(String fieldName){
195-
fields.add( getFieldToken(fieldName) );
216+
fields.add( getFieldToken(fieldName, null) );
196217
return this;
197218
}
219+
/**
220+
* Selects a single field from the SObject specified in {@link #table}.
221+
* Selecting fields is idempotent, if this field is already selected calling this method will have no additional impact.
222+
* @param fieldName the API name of the field to add to the query's SELECT clause.
223+
* @param relatedSObjectType the related sObjectType to resolve polymorphic object fields.
224+
**/
225+
public fflib_QueryFactory selectField(String fieldName, Schema.sOBjectType relatedObjectType){
226+
fields.add( getFieldToken(fieldName, relatedObjectType) );
227+
return this;
228+
}
198229
/**
199230
* Selects a field, avoiding the possible ambiguitiy of String API names.
200231
* @see #selectField(String)
@@ -772,7 +803,16 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
772803
this.setMessage( 'Invalid field \''+fieldName+'\' for object \''+objectType+'\'' );
773804
}
774805
}
806+
public class InvalidRelationshipException extends Exception{
807+
private String fieldName;
808+
private Schema.SObjectType objectType;
809+
public InvalidRelationshipException(String fieldname, Schema.SObjectType objectType) {
810+
this.fieldname = fieldname;
811+
this.objectType = objectType;
812+
this.setMessage( 'Not able to find related sObjectType for object \''+objectType+'\' denoted by field \''+field+'\'' );
813+
}
814+
}
775815
public class InvalidFieldSetException extends Exception{}
776816
public class NonReferenceFieldException extends Exception{}
777-
public class InvalidSubqueryRelationshipException extends Exception{}
817+
public class InvalidSubqueryRelationshipException extends Exception{}
778818
}

fflib/src/classes/fflib_SObjectSelector.cls

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ public abstract with sharing class fflib_SObjectSelector
330330
{
331331
// Add fields from selector prefixing the relationship path
332332
for(SObjectField field : getSObjectFieldList())
333-
queryFactory.selectField(relationshipFieldPath + '.' + field.getDescribe().getName());
333+
queryFactory.selectField(relationshipFieldPath + '.' + field.getDescribe().getName(), this.getSObjectType());
334334
// Automatically select the CurrencyIsoCode for MC orgs (unless the object is a known exception to the rule)
335335
if(Userinfo.isMultiCurrencyOrganization() && CURRENCY_ISO_CODE_ENABLED)
336336
queryFactory.selectField(relationshipFieldPath+'.CurrencyIsoCode');

fflib/src/classes/fflib_SObjectSelectorTest.cls

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,5 +291,97 @@ private with sharing class fflib_SObjectSelectorTest
291291
return null;
292292
}
293293
return testUser;
294+
}
295+
296+
@isTest
297+
static void testPolyMorphicSelectWithRelatedType()
298+
{
299+
//Given
300+
301+
Testfflib_CampaignMemberSelector cmSelector = new Testfflib_CampaignMemberSelector();
302+
fflib_QueryFactory qf = cmSelector.newQueryFactory();
303+
new Testfflib_LeadSelector().configureQueryFactoryFields(qf, 'Lead');
304+
new Testfflib_UserSelector().configureQueryFactoryFields(qf, 'Lead.Owner');
305+
306+
307+
Set<String> expectedSelectFields = new Set<String>{ 'Id', 'Status', 'Lead.Id', 'Lead.OwnerId', 'Lead.Owner.Id', 'Lead.Owner.UserRoleId' };
308+
if (UserInfo.isMultiCurrencyOrganization())
309+
{
310+
expectedSelectFields.add('CurrencyIsoCode');
311+
}
312+
313+
//When
314+
String soql = qf.toSOQL();
315+
316+
//Then
317+
Pattern soqlPattern = Pattern.compile('SELECT (.*) FROM CampaignMember ORDER BY CreatedDate ASC NULLS FIRST ');
318+
Matcher soqlMatcher = soqlPattern.matcher(soql);
319+
soqlMatcher.matches();
320+
321+
List<String> actualSelectFields = soqlMatcher.group(1).deleteWhiteSpace().split(',');
322+
System.assertEquals(expectedSelectFields, new Set<String>(actualSelectFields));
323+
}
324+
325+
private class Testfflib_CampaignMemberSelector extends fflib_SObjectSelector
326+
{
327+
public Testfflib_CampaignMemberSelector()
328+
{
329+
super();
330+
}
331+
332+
public List<Schema.SObjectField> getSObjectFieldList()
333+
{
334+
return new List<Schema.SObjectField> {
335+
CampaignMember.Id,
336+
CampaignMember.Status
337+
};
338+
}
339+
340+
public Schema.SObjectType getSObjectType()
341+
{
342+
return CampaignMember.sObjectType;
343+
}
344+
}
345+
346+
private class Testfflib_UserSelector extends fflib_SObjectSelector
347+
{
348+
public Testfflib_UserSelector()
349+
{
350+
super();
351+
}
352+
353+
public List<Schema.SObjectField> getSObjectFieldList()
354+
{
355+
return new List<Schema.SObjectField> {
356+
User.UserRoleId,
357+
User.Id
358+
};
359+
}
360+
361+
public Schema.SObjectType getSObjectType()
362+
{
363+
return User.sObjectType;
364+
}
365+
}
366+
367+
private class Testfflib_LeadSelector extends fflib_SObjectSelector
368+
{
369+
public Testfflib_LeadSelector()
370+
{
371+
super();
372+
}
373+
374+
public List<Schema.SObjectField> getSObjectFieldList()
375+
{
376+
return new List<Schema.SObjectField> {
377+
Lead.OwnerId,
378+
Lead.Id
379+
};
380+
}
381+
382+
public Schema.SObjectType getSObjectType()
383+
{
384+
return Lead.sObjectType;
385+
}
294386
}
295387
}

0 commit comments

Comments
 (0)