Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#419 - implements a proposal for supporting native User Mode Database Operations introduced in Summer '22 #420

Merged
merged 13 commits into from
Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 94 additions & 46 deletions sfdx-source/apex-common/main/classes/fflib_QueryFactory.cls
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
**/
public class fflib_QueryFactory { //No explicit sharing declaration - inherit from caller
public enum SortOrder {ASCENDING, DESCENDING}
public enum FLSEnforcement{NONE, LEGACY, USER_MODE, SYSTEM_MODE}

/**
* This property is read-only and may not be set after instantiation.
Expand All @@ -71,8 +72,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
* This can optionally be enforced (or not) by calling the setEnforceFLS method prior to calling
* one of the selectField or selectFieldset methods.
**/
private Boolean enforceFLS;
private FLSEnforcement mFlsEnforcement;

private Boolean sortSelectFields = true;
private Boolean allRows = false;

Expand All @@ -86,12 +87,26 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
private Map<Schema.ChildRelationship, fflib_QueryFactory> subselectQueryMap;

private String getFieldPath(String fieldName, Schema.sObjectType relatedSObjectType){

//Enforcing FLS using the legacy heuristic requires resolving the full field path to its respective
//Describe result to test for isAccessible on the DescribeFieldResult
//This is computationally expensive and should be bypassed if the QueryFactory instance is not
//enforcing FLS
//Starting in Summer '22, Apex can natively enforce CRUD and FLS with User Mode Operations
//Someday, the LEGACY FLSEnforcement heuristic will be removed
if(mFlsEnforcement == FLSEnforcement.USER_MODE || mFlsEnforcement == FLSEnforcement.SYSTEM_MODE){
return fieldName;
}

if(!fieldName.contains('.')){ //single field
Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(table).getField(fieldName.toLowerCase());
if(token == null)
throw new InvalidFieldException(fieldName,this.table);
if (enforceFLS)
fflib_SecurityUtils.checkFieldIsReadable(this.table, token);
if(token == null) {
throw new InvalidFieldException(fieldName, this.table);
}
if(mFlsEnforcement == FLSEnforcement.LEGACY) {
fflib_SecurityUtils.checkFieldIsReadable(this.table, token);
}

return token.getDescribe().getName();
}

Expand All @@ -104,7 +119,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(lastSObjectType).getField(field.toLowerCase());
DescribeFieldResult tokenDescribe = token != null ? token.getDescribe() : null;

if (token != null && enforceFLS) {
if (token != null && mFlsEnforcement == FLSEnforcement.LEGACY) {
fflib_SecurityUtils.checkFieldIsReadable(lastSObjectType, token);
}

Expand Down Expand Up @@ -146,7 +161,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
if(field == null){
throw new InvalidFieldException('Invalid field: null');
}
return field.getDescribe().getName();
return field.getDescribe().getLocalName();
}

/**
Expand All @@ -170,7 +185,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
this.table = table;
fields = new Set<String>();
order = new List<Ordering>();
enforceFLS = false;
mFlsEnforcement = FLSEnforcement.NONE;
}

/**
Expand Down Expand Up @@ -199,12 +214,18 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
* permission enforced. If this method is not called, the default behavior
* is that FLS read permission will not be checked.
* @param enforce whether to enforce field level security (read)
* @deprecated - use the setEnforceFLS overload that specifies Legacy or Native FLS enforcement
**/
public fflib_QueryFactory setEnforceFLS(Boolean enforce){
this.enforceFLS = enforce;
return setEnforceFLS(enforce ? FLSEnforcement.LEGACY : FLSEnforcement.NONE);
}

public fflib_QueryFactory setEnforceFLS(FLSEnforcement enforcement){
this.mFlsEnforcement = enforcement;
return this;
}


/**
* Sets a flag to indicate that this query should have ordered
* query fields in the select statement (this at a small cost to performance).
Expand All @@ -220,9 +241,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
* Selecting fields is idempotent, if this field is already selected calling this method will have no additional impact.
* @param fieldName the API name of the field to add to the query's SELECT clause.
**/
public fflib_QueryFactory selectField(String fieldName){
fields.add( getFieldPath(fieldName, null) );
return this;
public fflib_QueryFactory selectField(String fieldName){
return selectFields(new Set<String>{fieldName});
}

/**
Expand All @@ -231,8 +251,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
* @param fieldName the API name of the field to add to the query's SELECT clause.
* @param relatedSObjectType the related sObjectType to resolve polymorphic object fields.
**/
public fflib_QueryFactory selectField(String fieldName, Schema.sOBjectType relatedObjectType) {
fields.add(getFieldPath(fieldName, relatedObjectType));
public fflib_QueryFactory selectField(String fieldName, Schema.sObjectType relatedObjectType) {
addField(getFieldPath(fieldName, relatedObjectType));
return this;
}

Expand All @@ -243,59 +263,59 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
* @exception InvalidFieldException If the field is null {@code field}.
**/
public fflib_QueryFactory selectField(Schema.SObjectField field){
if(field == null)
throw new InvalidFieldException(null,this.table);
if (enforceFLS)
fflib_SecurityUtils.checkFieldIsReadable(table, field);
fields.add( getFieldTokenPath(field) );
return this;
return selectFields(new Set<Schema.SObjectField>{field});
}
/**
* Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
* @param fieldNames the Set of field API names to select.
**/
public fflib_QueryFactory selectFields(Set<String> fieldNames){
for(String fieldName:fieldNames){
fields.add( getFieldPath(fieldName) );
}
return this;
return selectStringField(fieldNames.iterator());
}
/**
* Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
* @param fieldNames the List of field API names to select.
**/
public fflib_QueryFactory selectFields(List<String> fieldNames){
for(String fieldName:fieldNames)
fields.add( getFieldPath(fieldName) );
return selectStringField(fieldNames.iterator());
}

private fflib_QueryFactory selectStringField(Iterator<String> iter){
while( iter.hasNext() ) {
addField(getFieldPath(iter.next()));
}
return this;
}

/**
* Selects multiple fields. This acts the same as calling {@link #selectField(Schema.SObjectField)} multiple times.
* @param fields the set of {@link Schema.SObjectField}s to select.
* @exception InvalidFieldException if the fields are null {@code fields}.
**/
public fflib_QueryFactory selectFields(Set<Schema.SObjectField> fields){
for(Schema.SObjectField token:fields){
if(token == null)
throw new InvalidFieldException();
if (enforceFLS)
fflib_SecurityUtils.checkFieldIsReadable(table, token);
this.fields.add( getFieldTokenPath(token) );
}
return this;
return selectSObjectField(fields.iterator());
}

/**
* Selects multiple fields. This acts the same as calling {@link #selectField(Schema.SObjectField)} multiple times.
* @param fields the set of {@link Schema.SObjectField}s to select.
* @exception InvalidFieldException if the fields are null {@code fields}.
**/
public fflib_QueryFactory selectFields(List<Schema.SObjectField> fields){
for(Schema.SObjectField token:fields){
if(token == null)
public fflib_QueryFactory selectFields(List<Schema.SObjectField> fields) {
return selectSObjectField(fields.iterator());
}

private fflib_QueryFactory selectSObjectField(Iterator<Schema.SObjectField> iter){

while( iter.hasNext() ){
Schema.SObjectField token = iter.next();
if(token == null) {
throw new InvalidFieldException();
if (enforceFLS)
fflib_SecurityUtils.checkFieldIsReadable(table, token);
this.fields.add( getFieldTokenPath(token) );
}
if (mFlsEnforcement == FLSEnforcement.LEGACY) {
fflib_SecurityUtils.checkFieldIsReadable(table, token);
}
addField( getFieldTokenPath(token) );
}
return this;
}
Expand All @@ -317,10 +337,26 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
for(Schema.FieldSetMember field: fieldSet.getFields()){
if(!allowCrossObject && field.getFieldPath().contains('.'))
throw new InvalidFieldSetException('Cross-object fields not allowed and field "'+field.getFieldPath()+'"" is a cross-object field.');
fields.add( getFieldPath(field.getFieldPath()) );
addField( getFieldPath(field.getFieldPath()) );
}
return this;
}

private void addField(String fieldPath){
/** With the introduction of SYSTEM_MODE and USER_MODE, it no longer became necessary to
* use DescribeFieldResult methods to resolve a selected field back to its canonical case-preserving
* field definition. The consequence is that duplicate fields could be introduced into the SELECT
* clause if, for instance, the Apex code called "selectField('annualrevenue')" but that same AnnualRevenue
* field were included via a Field Set and the FieldSetMember.getFieldPath() returns "AnnualRevenue"
* So, in the cases where we're using USER_MODE or SYSTEM_MODE, we need to downcase all of the fields in the Set
*/
if(mFlsEnforcement == FLSEnforcement.SYSTEM_MODE || mFlsEnforcement == FLSEnforcement.USER_MODE){
fieldPath = fieldPath.toLowerCase();
}

this.fields.add(fieldPath);
}

/**
* @param conditionExpression Sets the WHERE clause to the string provided. Do not include the "WHERE".
**/
Expand Down Expand Up @@ -679,7 +715,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
String result = 'SELECT ';
//if no fields have been added, just add the Id field so that the query or subquery will not just fail
if (fields.size() == 0){
if (enforceFLS) fflib_SecurityUtils.checkFieldIsReadable(table, 'Id');
if (mFlsEnforcement == FLSEnforcement.LEGACY){
fflib_SecurityUtils.checkFieldIsReadable(table, 'Id');
}
result += 'Id';
}else {
List<String> fieldsToQuery = new List<String>(fields);
Expand All @@ -697,8 +735,18 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
}
}
result += ' FROM ' + (relationship != null ? relationship.getRelationshipName() : table.getDescribe().getName());
if(conditionExpression != null)
result += ' WHERE '+conditionExpression;

if(conditionExpression != null) {
result += ' WHERE ' + conditionExpression;
}

//Subselects can't specify USER_MODE or SYSTEM_MODE -- only the top-level query can do so
if(relationship == null && mFlsEnforcement == FLSEnforcement.USER_MODE){
result += ' WITH USER_MODE';
}
else if(relationship == null && mFlsEnforcement == FLSEnforcement.SYSTEM_MODE){
result += ' WITH SYSTEM_MODE';
}

if(order.size() > 0){
result += ' ORDER BY ';
Expand Down Expand Up @@ -730,7 +778,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
.setLimit(this.limitCount)
.setOffset(this.offsetCount)
.setCondition(this.conditionExpression)
.setEnforceFLS(this.enforceFLS);
.setEnforceFLS(this.mFlsEnforcement);

Map<Schema.ChildRelationship, fflib_QueryFactory> subqueries = this.subselectQueryMap;
if(subqueries != null) {
Expand Down
Loading