Skip to content

Commit 5422faa

Browse files
Merge pull request #420 from apex-enterprise-patterns/419-user-mode-proposal
#419 - implements a proposal for supporting native User Mode Database Operations introduced in Summer '22 (#420) Co-Authored-By: John M. Daniel <[email protected]>
2 parents fbe6df4 + d04b6d7 commit 5422faa

File tree

7 files changed

+430
-79
lines changed

7 files changed

+430
-79
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ FFLib Apex Common
1414
Updates
1515
=======
1616

17+
- **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.
1718
- **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.
1819
- **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!
1920
- **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.
2021
- **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/).
21-
- **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.
2222

2323
This Library
2424
============

sfdx-source/apex-common/main/classes/fflib_QueryFactory.cls

Lines changed: 96 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
**/
5555
public class fflib_QueryFactory { //No explicit sharing declaration - inherit from caller
5656
public enum SortOrder {ASCENDING, DESCENDING}
57+
public enum FLSEnforcement{NONE, LEGACY, USER_MODE, SYSTEM_MODE}
5758

5859
/**
5960
* This property is read-only and may not be set after instantiation.
@@ -71,8 +72,8 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
7172
* This can optionally be enforced (or not) by calling the setEnforceFLS method prior to calling
7273
* one of the selectField or selectFieldset methods.
7374
**/
74-
private Boolean enforceFLS;
75-
75+
private FLSEnforcement mFlsEnforcement;
76+
7677
private Boolean sortSelectFields = true;
7778
private Boolean allRows = false;
7879

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

8889
private String getFieldPath(String fieldName, Schema.sObjectType relatedSObjectType){
90+
91+
//Enforcing FLS using the legacy heuristic requires resolving the full field path to its respective
92+
//Describe result to test for isAccessible on the DescribeFieldResult
93+
//This is computationally expensive and should be bypassed if the QueryFactory instance is not
94+
//enforcing FLS
95+
//Starting in Summer '22, Apex can natively enforce CRUD and FLS with User Mode Operations
96+
//Someday, the LEGACY FLSEnforcement heuristic will be removed
97+
if(mFlsEnforcement == FLSEnforcement.USER_MODE || mFlsEnforcement == FLSEnforcement.SYSTEM_MODE){
98+
return fieldName;
99+
}
100+
89101
if(!fieldName.contains('.')){ //single field
90102
Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(table).getField(fieldName.toLowerCase());
91-
if(token == null)
92-
throw new InvalidFieldException(fieldName,this.table);
93-
if (enforceFLS)
94-
fflib_SecurityUtils.checkFieldIsReadable(this.table, token);
103+
if(token == null) {
104+
throw new InvalidFieldException(fieldName, this.table);
105+
}
106+
if(mFlsEnforcement == FLSEnforcement.LEGACY) {
107+
fflib_SecurityUtils.checkFieldIsReadable(this.table, token);
108+
}
109+
95110
return token.getDescribe().getName();
96111
}
97112

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

107-
if (token != null && enforceFLS) {
122+
if (token != null && mFlsEnforcement == FLSEnforcement.LEGACY) {
108123
fflib_SecurityUtils.checkFieldIsReadable(lastSObjectType, token);
109124
}
110125

@@ -146,7 +161,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
146161
if(field == null){
147162
throw new InvalidFieldException('Invalid field: null');
148163
}
149-
return field.getDescribe().getName();
164+
return field.getDescribe().getLocalName();
150165
}
151166

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

176191
/**
@@ -199,12 +214,18 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
199214
* permission enforced. If this method is not called, the default behavior
200215
* is that FLS read permission will not be checked.
201216
* @param enforce whether to enforce field level security (read)
217+
* @deprecated - use the setEnforceFLS overload that specifies Legacy or Native FLS enforcement
202218
**/
203219
public fflib_QueryFactory setEnforceFLS(Boolean enforce){
204-
this.enforceFLS = enforce;
220+
return setEnforceFLS(enforce ? FLSEnforcement.LEGACY : FLSEnforcement.NONE);
221+
}
222+
223+
public fflib_QueryFactory setEnforceFLS(FLSEnforcement enforcement){
224+
this.mFlsEnforcement = enforcement;
205225
return this;
206226
}
207227

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

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

@@ -243,59 +263,59 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
243263
* @exception InvalidFieldException If the field is null {@code field}.
244264
**/
245265
public fflib_QueryFactory selectField(Schema.SObjectField field){
246-
if(field == null)
247-
throw new InvalidFieldException(null,this.table);
248-
if (enforceFLS)
249-
fflib_SecurityUtils.checkFieldIsReadable(table, field);
250-
fields.add( getFieldTokenPath(field) );
251-
return this;
266+
return selectFields(new Set<Schema.SObjectField>{field});
252267
}
253268
/**
254269
* Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
255270
* @param fieldNames the Set of field API names to select.
256271
**/
257272
public fflib_QueryFactory selectFields(Set<String> fieldNames){
258-
for(String fieldName:fieldNames){
259-
fields.add( getFieldPath(fieldName) );
260-
}
261-
return this;
273+
return selectStringField(fieldNames.iterator());
262274
}
263275
/**
264276
* Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
265277
* @param fieldNames the List of field API names to select.
266278
**/
267279
public fflib_QueryFactory selectFields(List<String> fieldNames){
268-
for(String fieldName:fieldNames)
269-
fields.add( getFieldPath(fieldName) );
280+
return selectStringField(fieldNames.iterator());
281+
}
282+
283+
private fflib_QueryFactory selectStringField(Iterator<String> iter){
284+
while( iter.hasNext() ) {
285+
addField(getFieldPath(iter.next()));
286+
}
270287
return this;
271288
}
289+
272290
/**
273291
* Selects multiple fields. This acts the same as calling {@link #selectField(Schema.SObjectField)} multiple times.
274-
* @param fields the set of {@link Schema.SObjectField}s to select.
292+
* @param fields the Set of {@link Schema.SObjectField}s to select.
275293
* @exception InvalidFieldException if the fields are null {@code fields}.
276294
**/
277295
public fflib_QueryFactory selectFields(Set<Schema.SObjectField> fields){
278-
for(Schema.SObjectField token:fields){
279-
if(token == null)
280-
throw new InvalidFieldException();
281-
if (enforceFLS)
282-
fflib_SecurityUtils.checkFieldIsReadable(table, token);
283-
this.fields.add( getFieldTokenPath(token) );
284-
}
285-
return this;
296+
return selectSObjectFields(fields.iterator());
286297
}
298+
287299
/**
288300
* Selects multiple fields. This acts the same as calling {@link #selectField(Schema.SObjectField)} multiple times.
289-
* @param fields the set of {@link Schema.SObjectField}s to select.
301+
* @param fields the List of {@link Schema.SObjectField}s to select.
290302
* @exception InvalidFieldException if the fields are null {@code fields}.
291303
**/
292-
public fflib_QueryFactory selectFields(List<Schema.SObjectField> fields){
293-
for(Schema.SObjectField token:fields){
294-
if(token == null)
304+
public fflib_QueryFactory selectFields(List<Schema.SObjectField> fields) {
305+
return selectSObjectFields(fields.iterator());
306+
}
307+
308+
private fflib_QueryFactory selectSObjectFields(Iterator<Schema.SObjectField> iter){
309+
310+
while( iter.hasNext() ){
311+
Schema.SObjectField token = iter.next();
312+
if(token == null) {
295313
throw new InvalidFieldException();
296-
if (enforceFLS)
297-
fflib_SecurityUtils.checkFieldIsReadable(table, token);
298-
this.fields.add( getFieldTokenPath(token) );
314+
}
315+
if (mFlsEnforcement == FLSEnforcement.LEGACY) {
316+
fflib_SecurityUtils.checkFieldIsReadable(table, token);
317+
}
318+
addField( getFieldTokenPath(token) );
299319
}
300320
return this;
301321
}
@@ -317,10 +337,26 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
317337
for(Schema.FieldSetMember field: fieldSet.getFields()){
318338
if(!allowCrossObject && field.getFieldPath().contains('.'))
319339
throw new InvalidFieldSetException('Cross-object fields not allowed and field "'+field.getFieldPath()+'"" is a cross-object field.');
320-
fields.add( getFieldPath(field.getFieldPath()) );
340+
addField( getFieldPath(field.getFieldPath()) );
321341
}
322342
return this;
323343
}
344+
345+
private void addField(String fieldPath){
346+
/** With the introduction of SYSTEM_MODE and USER_MODE, it no longer became necessary to
347+
* use DescribeFieldResult methods to resolve a selected field back to its canonical case-preserving
348+
* field definition. The consequence is that duplicate fields could be introduced into the SELECT
349+
* clause if, for instance, the Apex code called "selectField('annualrevenue')" but that same AnnualRevenue
350+
* field were included via a Field Set and the FieldSetMember.getFieldPath() returns "AnnualRevenue"
351+
* So, in the cases where we're using USER_MODE or SYSTEM_MODE, we need to downcase all of the fields in the Set
352+
*/
353+
if(mFlsEnforcement == FLSEnforcement.SYSTEM_MODE || mFlsEnforcement == FLSEnforcement.USER_MODE){
354+
fieldPath = fieldPath.toLowerCase();
355+
}
356+
357+
this.fields.add(fieldPath);
358+
}
359+
324360
/**
325361
* @param conditionExpression Sets the WHERE clause to the string provided. Do not include the "WHERE".
326362
**/
@@ -679,7 +715,9 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
679715
String result = 'SELECT ';
680716
//if no fields have been added, just add the Id field so that the query or subquery will not just fail
681717
if (fields.size() == 0){
682-
if (enforceFLS) fflib_SecurityUtils.checkFieldIsReadable(table, 'Id');
718+
if (mFlsEnforcement == FLSEnforcement.LEGACY){
719+
fflib_SecurityUtils.checkFieldIsReadable(table, 'Id');
720+
}
683721
result += 'Id';
684722
}else {
685723
List<String> fieldsToQuery = new List<String>(fields);
@@ -697,8 +735,18 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
697735
}
698736
}
699737
result += ' FROM ' + (relationship != null ? relationship.getRelationshipName() : table.getDescribe().getName());
700-
if(conditionExpression != null)
701-
result += ' WHERE '+conditionExpression;
738+
739+
if(conditionExpression != null) {
740+
result += ' WHERE ' + conditionExpression;
741+
}
742+
743+
//Subselects can't specify USER_MODE or SYSTEM_MODE -- only the top-level query can do so
744+
if(relationship == null && mFlsEnforcement == FLSEnforcement.USER_MODE){
745+
result += ' WITH USER_MODE';
746+
}
747+
else if(relationship == null && mFlsEnforcement == FLSEnforcement.SYSTEM_MODE){
748+
result += ' WITH SYSTEM_MODE';
749+
}
702750

703751
if(order.size() > 0){
704752
result += ' ORDER BY ';
@@ -730,7 +778,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr
730778
.setLimit(this.limitCount)
731779
.setOffset(this.offsetCount)
732780
.setCondition(this.conditionExpression)
733-
.setEnforceFLS(this.enforceFLS);
781+
.setEnforceFLS(this.mFlsEnforcement);
734782

735783
Map<Schema.ChildRelationship, fflib_QueryFactory> subqueries = this.subselectQueryMap;
736784
if(subqueries != null) {

0 commit comments

Comments
 (0)