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 all 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ FFLib Apex Common
Updates
=======

- **December 2022**, **IMPORTANT CHANGE** - Support for native Apex User Mode was added to the library (see [discussion](https://github.com/apex-enterprise-patterns/fflib-apex-common/discussions/419)). For new projects, the old `enforceCRUD` and `enforceFLS` flags on `fflib_SObjectSelector` should be considered deprecated and the constructors that take `dataAccess` arguments should be used instead. Additionally, the introduction of `fflib_SObjectUnitOfWork.UserModeDML` provides an `IDML` implementation that supports `USER_MODE` or `SYSTEM_MODE`. `fflib_SObjectUnitOfWork.SimpleDML` (the default `IDML` implementation) should be considered deprecated. There are measurable performance benefits to using `SYSTEM_MODE` and `USER_MODE` (Apex CPU usage reduction). Additionally, the use of explicit `USER_MODE` and `SYSTEM_MODE` overrides the `with sharing` and `without sharing` class declaration and makes the expected behavior of DML and SOQL easier to understand.
- **April 2020**, **IMPORTANT CHANGE**, the directory format of this project repo was converted to [Salesforce DX Source Format](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_file_format.htm). While the GIT commit history was maintained, it is not visible on GitHub. If you need to see the history, either clone the repo and execute `git log --follow` from the command line or refer to this [tag](https://github.com/apex-enterprise-patterns/fflib-apex-common/tree/metadata-format-prior-to-dx-source-format-conversion) of the codebase prior to conversion.
- **September 2014**, **IMPORTANT CHANGE**, changes applied to support Dreamforce 2014 advanced presentation, library now provides Application factories for major layers and support for ApexMocks. More details to follow! As a result [ApexMocks](https://github.com/apex-enterprise-patterns/fflib-apex-mocks) must be deployed to the org before deploying this library. The sample application [here](https://github.com/apex-enterprise-patterns/fflib-apex-common-samplecode) has also been updated to demonstrate the new features!
- **July 2014**, **IMPORTANT CHANGE**, prior **23rd July 2014**, both the ``fflib_SObjectDomain.onValidate()`` and ``fflib_SObjectDomain.onValidate(Map<Id, SObject> existingRecords)`` methods where called during an on **after update** trigger event. From this point on the ``onValidate()`` method will only be called during on **after insert**. If you still require the orignal behaviour add the line ``Configuration.enableOldOnUpdateValidateBehaviour();`` into your constructor.
- **June 2014**, New classes providing utilities to support security and dynamic queries, in addition to improvements to existing Apex Enterprise Pattern base classes. Read more [here](http://andyinthecloud.com/2014/06/28/financialforce-apex-common-updates/).
- **June 2014**, Experimental [branch](https://github.com/apex-enterprise-patterns/fflib-apex-common/tree/fls-support-experiment) supporting automated FLS checking, see [README](https://github.com/apex-enterprise-patterns/fflib-apex-common/tree/fls-support-experiment#expirimental-crud-and-fls-support) for more details.

This Library
============
Expand Down
144 changes: 96 additions & 48 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.
* @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 selectSObjectFields(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.
* @param fields the List 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 selectSObjectFields(fields.iterator());
}

private fflib_QueryFactory selectSObjectFields(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