diff --git a/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerActionConfig.cls b/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerActionConfig.cls index 541d97b..118ab85 100644 --- a/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerActionConfig.cls +++ b/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerActionConfig.cls @@ -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 {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); + } } \ No newline at end of file diff --git a/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerActionConfigSelector.cls b/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerActionConfigSelector.cls index 3ae7b3e..b65f8ef 100644 --- a/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerActionConfigSelector.cls +++ b/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerActionConfigSelector.cls @@ -65,12 +65,12 @@ public virtual inherited sharing class fflib_TriggerActionConfigSelector /** * @return Query all fflib_TriggerAction__mdt records */ - public virtual List selectAll() + public virtual List selectAllActive() { return (List ) Database.query( String.format( - 'SELECT {0} FROM {1}', + 'SELECT {0} FROM {1} WHERE IsActive__c=TRUE', new List { getFieldListString() + ', ObjectType__r.QualifiedApiName', diff --git a/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerHandler.cls b/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerHandler.cls index ebef8ea..da552b5 100644 --- a/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerHandler.cls +++ b/sfdx-source/apex-extensions/main/triggerHandler/classes/fflib_TriggerHandler.cls @@ -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 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)); } diff --git a/sfdx-source/apex-extensions/main/triggerHandler/classes/interfaces/fflib_ITriggerActionConfig.cls b/sfdx-source/apex-extensions/main/triggerHandler/classes/interfaces/fflib_ITriggerActionConfig.cls index 34ed98d..ce09301 100644 --- a/sfdx-source/apex-extensions/main/triggerHandler/classes/interfaces/fflib_ITriggerActionConfig.cls +++ b/sfdx-source/apex-extensions/main/triggerHandler/classes/interfaces/fflib_ITriggerActionConfig.cls @@ -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(); + + } \ No newline at end of file diff --git a/sfdx-source/apex-extensions/main/triggerHandler/classes/interfaces/fflib_ITriggerActionConfigSelector.cls b/sfdx-source/apex-extensions/main/triggerHandler/classes/interfaces/fflib_ITriggerActionConfigSelector.cls index fb3b461..36c48c7 100644 --- a/sfdx-source/apex-extensions/main/triggerHandler/classes/interfaces/fflib_ITriggerActionConfigSelector.cls +++ b/sfdx-source/apex-extensions/main/triggerHandler/classes/interfaces/fflib_ITriggerActionConfigSelector.cls @@ -32,5 +32,5 @@ public interface fflib_ITriggerActionConfigSelector /** * @return Query all fflib_TriggerAction__mdt records */ - List selectAll(); + List selectAllActive(); } \ No newline at end of file diff --git a/sfdx-source/apex-extensions/main/triggerHandler/layouts/fflib_TriggerAction__mdt-Trigger Action Layout.layout-meta.xml b/sfdx-source/apex-extensions/main/triggerHandler/layouts/fflib_TriggerAction__mdt-Trigger Action Layout.layout-meta.xml index 1635563..7cfc0e2 100644 --- a/sfdx-source/apex-extensions/main/triggerHandler/layouts/fflib_TriggerAction__mdt-Trigger Action Layout.layout-meta.xml +++ b/sfdx-source/apex-extensions/main/triggerHandler/layouts/fflib_TriggerAction__mdt-Trigger Action Layout.layout-meta.xml @@ -24,6 +24,10 @@ Required NamespacePrefix + + Edit + IsActive__c + diff --git a/sfdx-source/apex-extensions/main/triggerHandler/objects/fflib_TriggerAction__mdt/fields/IsActive__c.field-meta.xml b/sfdx-source/apex-extensions/main/triggerHandler/objects/fflib_TriggerAction__mdt/fields/IsActive__c.field-meta.xml new file mode 100644 index 0000000..53e7599 --- /dev/null +++ b/sfdx-source/apex-extensions/main/triggerHandler/objects/fflib_TriggerAction__mdt/fields/IsActive__c.field-meta.xml @@ -0,0 +1,9 @@ + + + IsActive__c + true + false + DeveloperControlled + + Checkbox + diff --git a/sfdx-source/apex-extensions/tests/classes/triggerHandler/fflib_TriggerHandlerTest.cls b/sfdx-source/apex-extensions/tests/classes/triggerHandler/fflib_TriggerHandlerTest.cls index 8908533..ee5ce4d 100644 --- a/sfdx-source/apex-extensions/tests/classes/triggerHandler/fflib_TriggerHandlerTest.cls +++ b/sfdx-source/apex-extensions/tests/classes/triggerHandler/fflib_TriggerHandlerTest.cls @@ -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 records = new List {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{ 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 {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 {