Skip to content

Commit

Permalink
Improve FormulaFilter error handling and refactor `MetadataTriggerH…
Browse files Browse the repository at this point in the history
…andler` (#161)

* Improve FormulaFilter error handling and refactor MetadataTriggerHandler.

Adds detailed error messages for invalid formulas and refactors MetadataTriggerHandler for better readability and maintainability.

* Package version update
  • Loading branch information
mitchspano authored Jan 8, 2025
1 parent 2eeb3fb commit 1deddbb
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 84 deletions.
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ Create a trigger action record with `Apex_Class_Name__c` equal to `TriggerAction
Individual trigger actions can have their own dynamic entry criteria defined in a simple formula.
This is a new feature and is built using the [`FormulaEval` namespace](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_namespace_formulaeval.htm) within Apex.

#### [Entry Criteria Beta Package Installation (Production)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tKY000000Pb8ZYAS)
#### [Entry Criteria Beta Package Installation (Production)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tKY000000Pd90YAC)

#### [Entry Criteria Beta Package Installation (Sandbox)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tKY000000Pb8ZYAS)
#### [Entry Criteria Beta Package Installation (Sandbox)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tKY000000Pd90YAC)

### SObject Setup

Expand Down Expand Up @@ -639,3 +639,16 @@ public void execute(FinalizerHandler.Context context) {
}
}
```

---

### Package Version Creation Steps

```sh
sf package version create \
--package "Trigger Actions Framework" \
--installation-key-bypass \
--version-number "major.minor.patch.build" \
--code-coverage \
--wait 50
```
3 changes: 2 additions & 1 deletion sfdx-project.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"Trigger Actions [email protected]": "04t3h000004juNHAAY",
"Trigger Actions [email protected]": "04t3h000004juNRAAY",
"Trigger Actions [email protected]": "04tKY000000Pb8tYAC",
"Trigger Actions [email protected]": "04tKY000000Pb8ZYAS"
"Trigger Actions [email protected]": "04tKY000000Pb8ZYAS",
"Trigger Actions [email protected]": "04tKY000000Pd90YAC"
}
}
16 changes: 13 additions & 3 deletions trigger-actions-framework/main/default/classes/FormulaFilter.cls
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
*/
@SuppressWarnings('PMD.AvoidGlobalModifier, PMD.FieldsSortedAlphabetically')
global class FormulaFilter {
private static final String ERROR_PREFIX = 'Please check the SObject_Trigger_Setting__mdt metadata for the the {0} sObject.';
private static final String ERROR_PREFIX = 'Please check the `SObject_Trigger_Setting__mdt` metadata for the the {0} sObject.';
@TestVisible
private static final String MISSING_CLASS_NAME =
ERROR_PREFIX +
Expand All @@ -33,6 +33,11 @@ global class FormulaFilter {
private static final String INVALID_SUBTYPE =
ERROR_PREFIX +
' The {1} class is specified in the the `TriggerRecord_Class_Name__c` field must be global and it must have properties called `record` and `recordPrior` and it must extend the `TriggerRecord` class.';
@TestVisible
private static final String INVALID_FILTER =
'Something is wrong with the filter on the `Trigger_Action__mdt` record with DeveloperName = \'{0}\' on the {1} sObject.' +
' It could be the case that the {2} class is specified in the the `TriggerRecord_Class_Name__c` referenced on the `SObject_Trigger_Setting__mdt` is not global,' +
' or it could be that the formula is syntactically invalid : `{3}`';

private final Trigger_Action__mdt triggerActionConfiguration;
private final TriggerOperation context;
Expand Down Expand Up @@ -158,8 +163,13 @@ global class FormulaFilter {
} catch (System.FormulaValidationException e) {
throw new IllegalArgumentException(
String.format(
INVALID_SUBTYPE,
new List<String>{ this.sObjectName, nameOfType }
INVALID_FILTER,
new List<String>{
this.triggerActionConfiguration.DeveloperName,
this.sObjectName,
nameOfType,
entryCriteriaFormula
}
)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ global class FormulaFilterTest {
TriggerRecord_Class_Name__c = 'FormulaFilterTest.AccountTriggerRecord'
);
private static Trigger_Action__mdt configuration = new Trigger_Action__mdt(
DeveloperName = 'TA_Test',
Before_Update__r = sobjectSetting
);
private static IllegalArgumentException caught;
Expand Down Expand Up @@ -208,10 +209,12 @@ global class FormulaFilterTest {
}
Assert.areEqual(
String.format(
FormulaFilter.INVALID_SUBTYPE,
FormulaFilter.INVALID_FILTER,
new List<String>{
configuration.DeveloperName,
ACCOUNT_SOBJECT_NAME,
sobjectSetting.TriggerRecord_Class_Name__c
sobjectSetting.TriggerRecord_Class_Name__c,
configuration.Entry_Criteria__c
}
),
caught.getMessage(),
Expand Down Expand Up @@ -273,6 +276,35 @@ global class FormulaFilterTest {
);
}

@IsTest
private static void invalidFormulaSyntaxShouldThrowException() {
String fx = 'This will not compile!!!';
configuration.Entry_Criteria__c = fx;
FormulaFilter filter = new FormulaFilter(
configuration,
TriggerOperation.BEFORE_UPDATE,
ACCOUNT_SOBJECT_NAME
);
try {
filter.filterByEntryCriteria(newList, oldList);
} catch (IllegalArgumentException e) {
caught = e;
}
Assert.areEqual(
String.format(
FormulaFilter.INVALID_FILTER,
new List<String>{
configuration.DeveloperName,
ACCOUNT_SOBJECT_NAME,
sobjectSetting.TriggerRecord_Class_Name__c,
fx
}
),
caught.getMessage(),
'The exception message should match the expected error'
);
}

@SuppressWarnings('PMD.ApexDoc')
global class AccountTriggerRecord extends TriggerRecord {
global Account record {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,97 +359,146 @@ public inherited sharing class MetadataTriggerHandler extends TriggerBase implem
List<SObject> newList,
List<SObject> oldList
) {
List<Trigger_Action__mdt> actionMetadata;
for (Trigger_Action__mdt triggerMetadata : getActionMetadata(context)) {
Object triggerAction = getTriggerActionObject(context, triggerMetadata);
if (!canExecute(triggerMetadata)) {
continue;
}
this.validateType(
context,
triggerAction,
triggerMetadata.Apex_Class_Name__c
);
FormulaFilter.Result filtered = new FormulaFilter(
triggerMetadata,
context,
this.sObjectName
)
.filterByEntryCriteria(newList, oldList);

if (
Math.max(
(filtered.oldList == null) ? 0 : filtered.oldList.size(),
(filtered.newList == null) ? 0 : filtered.newList.size()
) == 0
) {
continue;
}

switch on context {
when BEFORE_INSERT {
((TriggerAction.BeforeInsert) triggerAction)
.beforeInsert(filtered.newList);
}
when AFTER_INSERT {
((TriggerAction.AfterInsert) triggerAction)
.afterInsert(filtered.newList);
}
when BEFORE_UPDATE {
((TriggerAction.BeforeUpdate) triggerAction)
.beforeUpdate(filtered.newList, filtered.oldList);
}
when AFTER_UPDATE {
((TriggerAction.AfterUpdate) triggerAction)
.afterUpdate(filtered.newList, filtered.oldList);
}
when BEFORE_DELETE {
((TriggerAction.BeforeDelete) triggerAction)
.beforeDelete(filtered.oldList);
}
when AFTER_DELETE {
((TriggerAction.AfterDelete) triggerAction)
.afterDelete(filtered.oldList);
}
when AFTER_UNDELETE {
((TriggerAction.AfterUndelete) triggerAction)
.afterUndelete(filtered.newList);
}
}
}
}

/**
* @description Retrieves the Trigger Action metadata based on the provided TriggerOperation context.
* This method uses a switch statement to determine the appropriate metadata list based on the context.
*
* @param context The TriggerOperation context for which to retrieve metadata.
* @return A List of Trigger_Action__mdt metadata records corresponding to the given context.
*/
private List<Trigger_Action__mdt> getActionMetadata(
TriggerOperation context
) {
List<Trigger_Action__mdt> result;
switch on context {
when BEFORE_INSERT {
actionMetadata = this.beforeInsertActionMetadata;
result = this.beforeInsertActionMetadata;
}
when AFTER_INSERT {
actionMetadata = this.afterInsertActionMetadata;
result = this.afterInsertActionMetadata;
}
when BEFORE_UPDATE {
actionMetadata = this.beforeUpdateActionMetadata;
result = this.beforeUpdateActionMetadata;
}
when AFTER_UPDATE {
actionMetadata = this.afterUpdateActionMetadata;
result = this.afterUpdateActionMetadata;
}
when BEFORE_DELETE {
actionMetadata = this.beforeDeleteActionMetadata;
result = this.beforeDeleteActionMetadata;
}
when AFTER_DELETE {
actionMetadata = this.afterDeleteActionMetadata;
result = this.afterDeleteActionMetadata;
}
when AFTER_UNDELETE {
actionMetadata = this.afterUndeleteActionMetadata;
result = this.afterUndeleteActionMetadata;
}
}
for (Trigger_Action__mdt triggerMetadata : actionMetadata) {
Object triggerAction;
try {
triggerAction = Type.forName(triggerMetadata.Apex_Class_Name__c)
.newInstance();
if (triggerMetadata.Flow_Name__c != null) {
((TriggerActionFlow) triggerAction)
.flowName = triggerMetadata.Flow_Name__c;
((TriggerActionFlow) triggerAction)
.allowRecursion = triggerMetadata.Allow_Flow_Recursion__c;
}
} catch (System.NullPointerException e) {
handleException(
INVALID_CLASS_ERROR,
triggerMetadata.Apex_Class_Name__c,
context
);
}
if (
!MetadataTriggerHandler.isBypassed(
triggerMetadata.Apex_Class_Name__c
) && !TriggerBase.isBypassed(this.sObjectName)
) {
this.validateType(
context,
triggerAction,
triggerMetadata.Apex_Class_Name__c
);
FormulaFilter.Result filtered = new FormulaFilter(
triggerMetadata,
context,
this.sObjectName
)
.filterByEntryCriteria(newList, oldList);
switch on context {
when BEFORE_INSERT {
((TriggerAction.BeforeInsert) triggerAction)
.beforeInsert(filtered.newList);
}
when AFTER_INSERT {
((TriggerAction.AfterInsert) triggerAction)
.afterInsert(filtered.newList);
}
when BEFORE_UPDATE {
((TriggerAction.BeforeUpdate) triggerAction)
.beforeUpdate(filtered.newList, filtered.oldList);
}
when AFTER_UPDATE {
((TriggerAction.AfterUpdate) triggerAction)
.afterUpdate(filtered.newList, filtered.oldList);
}
when BEFORE_DELETE {
((TriggerAction.BeforeDelete) triggerAction)
.beforeDelete(filtered.oldList);
}
when AFTER_DELETE {
((TriggerAction.AfterDelete) triggerAction)
.afterDelete(filtered.oldList);
}
when AFTER_UNDELETE {
((TriggerAction.AfterUndelete) triggerAction)
.afterUndelete(filtered.newList);
}
}
return result;
}

/**
* @description Gets the trigger action object based on the trigger metadata.
* This method attempts to create a new instance of the Apex class specified in the trigger metadata.
* If the metadata specifies a flow name, it also sets the flow name and recursion allowance on the resulting object.
* If the specified Apex class does not exist or cannot be instantiated, a MetadataTriggerHandlerException is thrown.
*
* @param context The TriggerOperation context.
* @param triggerMetadata The metadata for the trigger action.
* @return An instance of the trigger action class, or null if the class does not exist.
*/
private Object getTriggerActionObject(
TriggerOperation context,
Trigger_Action__mdt triggerMetadata
) {
Object result;
try {
result = Type.forName(triggerMetadata.Apex_Class_Name__c).newInstance();
if (triggerMetadata.Flow_Name__c != null) {
((TriggerActionFlow) result).flowName = triggerMetadata.Flow_Name__c;
((TriggerActionFlow) result)
.allowRecursion = triggerMetadata.Allow_Flow_Recursion__c;
}
} catch (System.NullPointerException e) {
handleException(
INVALID_CLASS_ERROR,
triggerMetadata.Apex_Class_Name__c,
context
);
}
return result;
}

/**
* @description Checks if the trigger action can be executed.
* The action can execute if it is not bypassed specifically by its class name
* and the entire trigger handler is not bypassed for the current sObject.
*
* @param triggerMetadata The metadata for the trigger action.
* @return True if the trigger action can be executed, false otherwise.
*/
private Boolean canExecute(Trigger_Action__mdt triggerMetadata) {
return !MetadataTriggerHandler.isBypassed(
triggerMetadata.Apex_Class_Name__c
) && !TriggerBase.isBypassed(this.sObjectName);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@
* - `DmlFinalizer`: This interface defines the logic that should be executed after all DML operations have completed.
* ---
* To implement a Trigger Action, you must create a class that implements one or more of the `TriggerAction` interfaces.
* The class must also be annotated with the `@AuraEnabled` annotation.
*
* Once you have created a Trigger Action class, you can register it with the `TriggerActionRegistry` class.
* The `TriggerActionRegistry` class is responsible for managing the execution of Trigger Actions.
*/
@SuppressWarnings('PMD.CyclomaticComplexity')
public class TriggerAction {
Expand Down

0 comments on commit 1deddbb

Please sign in to comment.