Improvements to the trigger handler

Ability to disable trigger action in meta-data, and adds a stateful option

Signed-off-by: WimVelzeboer <wimvelzeboer@protonmail.com>
This commit is contained in:
WimVelzeboer
2021-10-28 15:35:28 +01:00
parent ed87378fba
commit ef77fad3ed
8 changed files with 131 additions and 6 deletions
@@ -66,7 +66,6 @@ public virtual inherited sharing class fflib_TriggerActionConfig
else if (triggerAction.ExecutionContext__c == 'Dedicated Queued') this.dedicatedQueued = true;
// Get SObjectType
System.debug('triggerAction: '+triggerAction);
String ObjectTypeApiName =
String.isNotBlank(triggerAction.ObjectType__c)
? triggerAction.ObjectType__r?.QualifiedApiName?.toLowerCase().trim()
@@ -74,6 +73,14 @@ public virtual inherited sharing class fflib_TriggerActionConfig
this.sObjectType = Schema.describeSObjects(new List<String> {ObjectTypeApiName}).get(0).getSObjectType();
}
/**
* @return Returns the Implementation Type of the Trigger Action
*/
public Type getImplementationType()
{
return this.implementationType;
}
/**
* @return Returns the sequence number of the trigger action
*/
@@ -90,6 +97,14 @@ public virtual inherited sharing class fflib_TriggerActionConfig
return (this.dedicatedQueued != null && this.dedicatedQueued);
}
/**
* @return Returns TRUE if the trigger action needs to be kept in memory and reused by the next TriggerHandler run
*/
public Boolean isStateful()
{
return this.stateful;
}
/**
* @return Returns TRUE if the trigger action can be executed in an Queueable context
*/
@@ -107,4 +122,15 @@ public virtual inherited sharing class fflib_TriggerActionConfig
{
return (fflib_ITriggerAction) implementationType.newInstance();
}
public Boolean hasTriggerContext(TriggerOperation triggerOperation)
{
return (triggerOperation == System.TriggerOperation.BEFORE_INSERT && beforeInsert)
|| (triggerOperation == System.TriggerOperation.BEFORE_UPDATE && beforeUpdate)
|| (triggerOperation == System.TriggerOperation.BEFORE_DELETE && beforeDelete)
|| (triggerOperation == System.TriggerOperation.AFTER_DELETE && afterDelete)
|| (triggerOperation == System.TriggerOperation.AFTER_INSERT && afterInsert)
|| (triggerOperation == System.TriggerOperation.AFTER_UNDELETE && afterUndelete)
|| (triggerOperation == System.TriggerOperation.AFTER_UPDATE && afterUpdate);
}
}
@@ -65,12 +65,12 @@ public virtual inherited sharing class fflib_TriggerActionConfigSelector
/**
* @return Query all fflib_TriggerAction__mdt records
*/
public virtual List<fflib_TriggerAction__mdt> selectAll()
public virtual List<fflib_TriggerAction__mdt> selectAllActive()
{
return (List <fflib_TriggerAction__mdt>) Database.query(
String.format(
'SELECT {0} FROM {1}',
'SELECT {0} FROM {1} WHERE IsActive__c=TRUE',
new List<String>
{
getFieldListString() + ', ObjectType__r.QualifiedApiName',
@@ -48,6 +48,12 @@ public virtual with sharing class fflib_TriggerHandler
protected fflib_ITriggerContext triggerContext;
/**
* Retain instance of Trigger Action classes implementing stateful
*/
@TestVisible
private static Map<Type, fflib_ITriggerAction> triggerActionStateByClass;
/**
* Provides access to the trigger action configurations and allowing it to be mocked in unit-tests
*/
@@ -101,7 +107,9 @@ public virtual with sharing class fflib_TriggerHandler
fflib_UnitOfWork unitOfWork = new fflib_UnitOfWork();
for (fflib_ITriggerActionConfig triggerActionConfig : configs)
{
fflib_ITriggerAction triggerAction = triggerActionConfig.newInstance();
if (isValidExecutionContext(ctx, triggerActionConfig) == false) continue;
fflib_ITriggerAction triggerAction = getTriggerActionInstance(triggerActionConfig);
triggerAction.setContext(ctx);
triggerAction.setSequence(triggerActionConfig.getSequence());
@@ -117,12 +125,29 @@ public virtual with sharing class fflib_TriggerHandler
unitOfWork.doWork();
}
private Boolean isValidExecutionContext(fflib_ITriggerContext ctx, fflib_ITriggerActionConfig config)
{
return config.hasTriggerContext(ctx.getTriggerOperation());
}
private fflib_ITriggerAction getTriggerActionInstance(fflib_ITriggerActionConfig triggerActionConfig)
{
if (triggerActionConfig.isStateful()
&& triggerActionStateByClass != null
&& triggerActionStateByClass.containsKey(triggerActionConfig.getImplementationType()))
{
return triggerActionStateByClass.get(triggerActionConfig.getImplementationType());
}
return triggerActionConfig.newInstance();
}
@TestVisible
protected void loadConfiguration()
{
if (configs.isEmpty() == false) return;
for (fflib_TriggerAction__mdt action : new fflib_TriggerActionConfigSelector().selectAll())
for (fflib_TriggerAction__mdt action : new fflib_TriggerActionConfigSelector().selectAllActive())
{
configs.add( new fflib_TriggerActionConfig(action));
}
@@ -29,16 +29,33 @@
*/
public interface fflib_ITriggerActionConfig
{
/**
* @return Returns the Implementation Type of the Trigger Action
*/
System.Type getImplementationType();
/**
* @return Returns the sequence number of the trigger action
*/
Integer getSequence();
/**
* @param triggerOperation The Trigger operation to validate the configuration against.
*
* @return Returns TRUE is the given operation type matches the one of the configuration
*/
Boolean hasTriggerContext(TriggerOperation triggerOperation);
/**
* @return Returns TRUE if the trigger action should be executed in a dedicated Queueable context
*/
Boolean isDedicatedQueued();
/**
* @return Returns TRUE if the trigger action needs to be kept in memory and reused by the next TriggerHandler run
*/
Boolean isStateful();
/**
* @return Returns TRUE if the trigger action can be executed in an Queueable context
*/
@@ -50,4 +67,6 @@ public interface fflib_ITriggerActionConfig
* @return Instance of the Trigger Action class
*/
fflib_ITriggerAction newInstance();
}
@@ -32,5 +32,5 @@ public interface fflib_ITriggerActionConfigSelector
/**
* @return Query all fflib_TriggerAction__mdt records
*/
List<fflib_TriggerAction__mdt> selectAll();
List<fflib_TriggerAction__mdt> selectAllActive();
}
@@ -24,6 +24,10 @@
<behavior>Required</behavior>
<field>NamespacePrefix</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>IsActive__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>IsActive__c</fullName>
<defaultValue>true</defaultValue>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<label>Is Active</label>
<type>Checkbox</type>
</CustomField>
@@ -252,7 +252,49 @@ private class fflib_TriggerHandlerTest
System.assert(ctx.isUpdate());
}
@IsTest
private static void itShouldUseTheStatefulTriggerActionClass()
{
fflib_LogEntry__c record = new fflib_LogEntry__c(LogLevel__c = 'INFO', Message__c = 'Unit Test');
List<fflib_LogEntry__c> records = new List<fflib_LogEntry__c> {record};
fflib_ApexMocks mocks = new fflib_ApexMocks();
fflib_ITriggerAction triggerActionMock = (fflib_ITriggerAction) mocks.mock(fflib_ITriggerAction.class);
mocks.startStubbing();
mocks.when(triggerActionMock.isQueueable()).thenReturn(false);
mocks.when(triggerActionMock.isNotQueueable()).thenReturn(true);
mocks.stopStubbing();
fflib_TriggerHandler.triggerActionStateByClass =
new Map<Type, fflib_ITriggerAction>{ MyTriggerAction.class => triggerActionMock };
fflib_TriggerAction__mdt triggerAction = new fflib_TriggerAction__mdt(
ObjectType__c = null,
ObjectTypeAlternate__c = Schema.fflib_LogEntry__c.SObjectType.getDescribe().getName(),
BeforeInsert__c = true,
ImplementationType__c = 'fflib_TriggerHandlerTest.MyTriggerAction',
ExecutionContext__c = 'Realtime',
Stateful__c = true
);
fflib_TriggerHandler triggerHandler = new fflib_TriggerHandler();
String ctxClassName = triggerHandler.triggerContextType.getName() + '.Constructor';
fflib_TriggerContext ctx =
(fflib_TriggerContext)
((fflib_IDomainConstructor) Type.forName(ctxClassName).newInstance())
.construct(new List<fflib_LogEntry__c> {record});
ctx.triggerOperation = System.TriggerOperation.BEFORE_INSERT;
// WHEN a record is inserted
System.Test.startTest();
triggerHandler.addTriggerActionConfig(triggerAction);
triggerHandler.setTriggerContext(ctx).run();
System.Test.stopTest();
// THEN the stateful trigger action should be re-used
((fflib_ITriggerAction) mocks.verify(triggerActionMock)).doWork();
}
public class MyTriggerAction extends fflib_TriggerAction // Todo - rename handler to action
{