Trigger Handler functionality

A trigger handler based on trigger actions configured via custom metadata. It separates the trigger logic per class, and is utilizing the fflib_UnitOfWork to execute the trigger actions.

Signed-off-by: WimVelzeboer <wimvelzeboer@protonmail.com>
This commit is contained in:
wimvelzeboer
2021-10-27 16:04:27 +01:00
committed by WimVelzeboer
parent 87bfcecf34
commit 5357cd1189
49 changed files with 1755 additions and 20 deletions
@@ -42,9 +42,7 @@ public virtual class fflib_AppBindingModule
public virtual void addBinding(fflib_IAppBinding binding)
{
if (null == this.bindings) this.bindings = new List<fflib_IAppBinding>();
this.bindings.add(binding);
addBindings(new List<fflib_IAppBinding>{ binding });
}
public virtual void addBindings(List<fflib_IAppBinding> bindings)
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>51.0</apiVersion>
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>51.0</apiVersion>
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,97 @@
/**
* File Name: fflib_TriggerAction
*
* @author architect ir. Wilhelmus G.J. Velzeboer
*
* Copyright (c), W.G.J. Velzeboer,
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above author notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public abstract inherited sharing class fflib_TriggerAction
extends fflib_DoWork
implements fflib_ITriggerAction
{
protected fflib_ITriggerContext triggerContext;
/**
* Override this to perform logic during the after delete phase, this is called by the handleAfterDelete method
**/
public virtual void onAfterDelete() { }
/**
* Override this to perform logic during the after insert phase, this is called by the handleAfterInsert method
**/
public virtual void onAfterInsert() { }
/**
* Override this to perform logic during the after undelete phase, this is called by the handleAfterDelete method
**/
public virtual void onAfterUnDelete() { }
/**
* Override this to perform logic during the after update phase, this is called by the handleAfterUpdate method
**/
public virtual void onAfterUpdate() { }
/**
* Override this to perform logic during the before delete phase, this is called by the handleBeforeDelete method
**/
public virtual void onBeforeDelete() { }
/**
* Override this to perform logic during the before insert phase, this is called by the handleBeforeInsert method
**/
public virtual void onBeforeInsert() { }
/**
* Override this to perform logic during the before update phase, this is called by the handleBeforeUpdate method
**/
public virtual void onBeforeUpdate() { }
public void doWork()
{
switch on this.triggerContext.getTriggerOperation()
{
when AFTER_DELETE { onAfterDelete(); }
when AFTER_INSERT { onAfterInsert(); }
when AFTER_UNDELETE { onAfterUndelete(); }
when AFTER_UPDATE { onAfterUpdate(); }
when BEFORE_DELETE { onBeforeDelete(); }
when BEFORE_INSERT { onBeforeInsert(); }
when BEFORE_UPDATE { onBeforeUpdate(); }
}
}
/**
* Method used by the TriggerHandler and us to pass in the trigger context
*
* @param ctx Instance of fflib_ITriggerContext
*
* @return Instance of itself to allow for method chaining
*/
public fflib_ITriggerAction setContext(fflib_ITriggerContext ctx)
{
this.triggerContext = ctx;
return this;
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,110 @@
/**
* File Name: fflib_TriggerActionConfig
*
* @author architect ir. Wilhelmus G.J. Velzeboer
*
* Copyright (c), W.G.J. Velzeboer,
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above author notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public virtual inherited sharing class fflib_TriggerActionConfig
implements fflib_ITriggerActionConfig
{
protected Boolean afterDelete = false;
protected Boolean afterInsert = false;
protected Boolean afterUndelete = false;
protected Boolean afterUpdate = false;
protected Boolean beforeDelete = false;
protected Boolean beforeInsert = false;
protected Boolean beforeUpdate = false;
protected Boolean stateful = false;
protected System.Type implementationType { get; set; }
protected Schema.SObjectType sObjectType { get; set; }
protected Integer sequence = 0;
protected Boolean dedicatedQueued;
/**
* Class Constructor
* Takes a custom metadata records and translates that into an instance of fflib_TriggerActionConfig
*
* @param triggerAction Instance of fflib_TriggerActionConfig
*/
public fflib_TriggerActionConfig(fflib_TriggerAction__mdt triggerAction)
{
this.afterDelete = triggerAction.AfterDelete__c == null ? false : triggerAction.AfterDelete__c;
this.afterInsert = triggerAction.AfterInsert__c == null ? false : triggerAction.AfterInsert__c;
this.afterUndelete = triggerAction.AfterUndelete__c == null ? false : triggerAction.AfterUndelete__c;
this.afterUpdate = triggerAction.AfterUpdate__c == null ? false : triggerAction.AfterUpdate__c;
this.beforeDelete = triggerAction.BeforeDelete__c == null ? false : triggerAction.BeforeDelete__c;
this.beforeInsert = triggerAction.BeforeInsert__c == null ? false : triggerAction.BeforeInsert__c;
this.beforeUpdate = triggerAction.BeforeUpdate__c == null ? false : triggerAction.BeforeUpdate__c;
this.implementationType = System.Type.forName(triggerAction.ImplementationType__c);
this.sequence = triggerAction.Sequence__c == null ? 0 : Integer.valueOf(triggerAction.Sequence__c);
this.stateful = triggerAction.Stateful__c == null ? false : triggerAction.Stateful__c;
if (triggerAction.ExecutionContext__c == 'Queued') this.dedicatedQueued = false;
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()
: triggerAction.ObjectTypeAlternate__c.toLowerCase().trim();
this.sObjectType = Schema.describeSObjects(new List<String> {ObjectTypeApiName}).get(0).getSObjectType();
}
/**
* @return Returns the sequence number of the trigger action
*/
public Integer getSequence()
{
return this.sequence;
}
/**
* @return Returns TRUE if the trigger action should be executed in a dedicated Queueable context
*/
public Boolean isDedicatedQueued()
{
return (this.dedicatedQueued != null && this.dedicatedQueued);
}
/**
* @return Returns TRUE if the trigger action can be executed in an Queueable context
*/
public Boolean isQueued()
{
return (this.dedicatedQueued != null);
}
/**
* Creates a new instance of the trigger action class based on the provided implementation type in the configuration
*
* @return Instance of the Trigger Action class
*/
public fflib_ITriggerAction newInstance()
{
return (fflib_ITriggerAction) implementationType.newInstance();
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,92 @@
/**
* File Name: fflib_TriggerActionConfigSelector
*
* @author architect ir. Wilhelmus G.J. Velzeboer
*
* Copyright (c), W.G.J. Velzeboer,
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above author notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public virtual inherited sharing class fflib_TriggerActionConfigSelector
extends fflib_SObjectSelector
implements fflib_ITriggerActionConfigSelector
{
public fflib_TriggerActionConfigSelector()
{
super();
}
public List<Schema.SObjectField> getSObjectFieldList()
{
return new List<Schema.SObjectField>
{
fflib_TriggerAction__mdt.Id,
fflib_TriggerAction__mdt.AfterDelete__c,
fflib_TriggerAction__mdt.AfterInsert__c,
fflib_TriggerAction__mdt.AfterUndelete__c,
fflib_TriggerAction__mdt.AfterUpdate__c,
fflib_TriggerAction__mdt.BeforeDelete__c,
fflib_TriggerAction__mdt.BeforeInsert__c,
fflib_TriggerAction__mdt.BeforeUpdate__c,
fflib_TriggerAction__mdt.ExecutionContext__c,
fflib_TriggerAction__mdt.ImplementationType__c,
fflib_TriggerAction__mdt.ObjectType__c,
fflib_TriggerAction__mdt.ObjectTypeAlternate__c,
fflib_TriggerAction__mdt.Sequence__c,
fflib_TriggerAction__mdt.Stateful__c
};
}
public Schema.SObjectType getSObjectType()
{
return Schema.fflib_TriggerAction__mdt.SObjectType;
}
/**
* @return Query all fflib_TriggerAction__mdt records
*/
public virtual List<fflib_TriggerAction__mdt> selectAll()
{
return (List <fflib_TriggerAction__mdt>) Database.query(
String.format(
'SELECT {0} FROM {1}',
new List<String>
{
getFieldListString() + ', ObjectType__r.QualifiedApiName',
getSObjectType().getDescribe().getName()
}
)
/* Please replace the above String.format method call for that below,
when the following issue in fflib-apex-commons is resolved:
https://github.com/apex-enterprise-patterns/fflib-apex-common/issues/208
newQueryFactory()
.selectField('ObjectType__r.QualifiedApiName')
.toSOQL()
*/
);
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,240 @@
/**
* File Name: fflib_TriggerContext
*
* @author architect ir. Wilhelmus G.J. Velzeboer
*
* Copyright (c), W.G.J. Velzeboer,
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above author notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public virtual inherited sharing class fflib_TriggerContext
extends fflib_SObjects2
implements fflib_ITriggerContext
{
/**
* Provides access to the Trigger Operation and allowing it to be mocked in unit-tests
*/
@TestVisible
protected System.TriggerOperation triggerOperation
{
get
{
if (triggerOperation == null)
{
triggerOperation = Trigger.operationType;
}
return triggerOperation;
}
set;
}
/**
* Provides access to the Trigger.new and allowing it to be mocked in unit-tests
*/
@TestVisible
protected List<SObject> records {
get
{
if (records == null)
{
records = getRecords();
}
return records;
}
set;
}
/**
* Provides access to the Trigger.oldMap and allowing it to be mocked in unit-tests
*/
@TestVisible
protected Map<Id, SObject> existingRecords
{
get
{
if (existingRecords == null)
{
existingRecords = Trigger.oldMap;
}
return existingRecords;
}
set;
}
public fflib_TriggerContext(List<SObject> records)
{
super(records);
}
/**
* Provides access to the data represented by this Trigger Context
**/
public System.TriggerOperation getTriggerOperation()
{
return triggerOperation;
}
/**
* Detects whether any values in context records have changed for given field as string
* Returns list of SObject records that have changes in the specified fields
**/
public List<SObject> getChangedRecords(String fieldName)
{
return getChangedRecords(new Set<String> {fieldName});
}
/**
* Detects whether any values in context records have changed for given fields as strings
* Returns list of SObject records that have changes in the specified fields
**/
public List<SObject> getChangedRecords(Set<String> fieldNames)
{
List<SObject> changedRecords = new List<SObject>();
for (SObject newRecord : getRecords())
{
Id recordId = (Id) newRecord.get('Id');
if (getExistingRecords() == null || !getExistingRecords().containsKey(recordId)) continue;
SObject oldRecord = getExistingRecords().get(recordId);
for (String fieldName : fieldNames)
{
if (oldRecord.get(fieldName) != newRecord.get(fieldName))
{
changedRecords.add(newRecord);
break; // prevents the records from being added multiple times
}
}
}
return changedRecords;
}
/**
* Detects whether any values in context records have changed for given field as token
* Returns list of SObject records that have changes in the specified fields
**/
public List<SObject> getChangedRecords(Schema.SObjectField fieldToken)
{
return getChangedRecords(new Set<Schema.SObjectField> {fieldToken});
}
/**
* Detects whether any values in context records have changed for given fields as tokens
* Returns list of SObject records that have changes in the specified fields
**/
public List<SObject> getChangedRecords(Set<Schema.SObjectField> fieldTokens)
{
List<SObject> changedRecords = new List<SObject>();
for (SObject newRecord : getRecords())
{
Id recordId = (Id) newRecord.get('Id');
if (getExistingRecords() == null || !getExistingRecords().containsKey(recordId)) continue;
SObject oldRecord = getExistingRecords().get(recordId);
for (Schema.SObjectField fieldToken : fieldTokens)
{
if (oldRecord.get(fieldToken) != newRecord.get(fieldToken))
{
changedRecords.add(newRecord);
break; // prevents the records from being added multiple times
}
}
}
return changedRecords;
}
/**
* Provides access to Trigger.oldMap or the mock when configured in a unit-test
**/
public Map<Id, SObject> getExistingRecords()
{
return this.existingRecords;
}
/**
* @return Returns TRUE if the Trigger Context is an INSERT operation
*/
public Boolean isInsert()
{
return this.triggerOperation == System.TriggerOperation.AFTER_INSERT
|| this.triggerOperation == System.TriggerOperation.BEFORE_INSERT;
}
/**
* @return Returns TRUE if the Trigger Context is an UPDATE operation
*/
public Boolean isUpdate()
{
return this.triggerOperation == System.TriggerOperation.AFTER_UPDATE
|| this.triggerOperation == System.TriggerOperation.BEFORE_UPDATE;
}
/**
* @return Returns TRUE if the Trigger Context is an DELETE operation
*/
public Boolean isDelete()
{
return this.triggerOperation == System.TriggerOperation.AFTER_DELETE
|| this.triggerOperation == System.TriggerOperation.BEFORE_DELETE;
}
/**
* @return Returns TRUE if the Trigger Context is an UNDELETE operation
*/
public Boolean isUndelete()
{
return this.triggerOperation == System.TriggerOperation.AFTER_UNDELETE;
}
/**
* @return Returns TRUE if the Trigger Context is an BEFORE operation
*/
public Boolean isBefore()
{
return this.triggerOperation == System.TriggerOperation.BEFORE_DELETE
|| this.triggerOperation == System.TriggerOperation.BEFORE_INSERT
|| this.triggerOperation == System.TriggerOperation.BEFORE_UPDATE;
}
/**
* @return Returns TRUE if the Trigger Context is an AFTER operation
*/
public Boolean isAfter()
{
return this.triggerOperation == System.TriggerOperation.AFTER_DELETE
|| this.triggerOperation == System.TriggerOperation.AFTER_INSERT
|| this.triggerOperation == System.TriggerOperation.AFTER_UNDELETE
|| this.triggerOperation == System.TriggerOperation.AFTER_UPDATE;
}
/**
* Constructor to construct the Trigger Context class
*/
public class Constructor implements fflib_IDomainConstructor
{
public fflib_IDomain construct(List<Object> objects)
{
return new fflib_TriggerContext((List<SObject>) objects);
}
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,155 @@
/**
* File Name: fflib_TriggerHandler
*
* @author architect ir. Wilhelmus G.J. Velzeboer
*
* Copyright (c), W.G.J. Velzeboer,
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above author notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public virtual with sharing class fflib_TriggerHandler
implements fflib_ITriggerHandler
{
public static final System.Type DEFAUT_TRIGGERCONTEXT_TYPE = fflib_TriggerContext.class;
@TestVisible
protected System.Type triggerContextType
{
get
{
if (triggerContextType == null)
{
triggerContextType = DEFAUT_TRIGGERCONTEXT_TYPE;
}
return triggerContextType;
}
private set;
}
protected fflib_ITriggerContext triggerContext;
/**
* Provides access to the trigger action configurations and allowing it to be mocked in unit-tests
*/
@TestVisible
private static List<fflib_ITriggerActionConfig> configs = new List<fflib_ITriggerActionConfig>();
/**
* Main method to execute the trigger handler to call the trigger actions
*/
public void run()
{
loadConfiguration();
fflib_ITriggerContext ctx = getTriggerContext();
execute(ctx);
}
/**
* Tells the trigger handler in which trigger context it is running
*
* @param ctx A configured Trigger Context instance
*
* @return Instance of itself to allow for method chaining
*/
public fflib_ITriggerHandler setTriggerContext(fflib_ITriggerContext ctx)
{
this.triggerContext = ctx;
return this;
}
/**
* Tells the trigger handler in which trigger context it is running
*
* @param classType A reference to the Trigger Context class to use when executing the trigger handler
*
* @return Instance of itself to allow for method chaining
*/
public fflib_ITriggerHandler setTriggerContextType(Type classType)
{
this.triggerContextType = classType;
return this;
}
@TestVisible
protected void addTriggerActionConfig(fflib_TriggerAction__mdt config)
{
configs.add(new fflib_TriggerActionConfig(config));
}
protected void execute(fflib_ITriggerContext ctx)
{
fflib_UnitOfWork unitOfWork = new fflib_UnitOfWork();
for (fflib_ITriggerActionConfig triggerActionConfig : configs)
{
fflib_ITriggerAction triggerAction = triggerActionConfig.newInstance();
triggerAction.setContext(ctx);
triggerAction.setSequence(triggerActionConfig.getSequence());
// Queueable only makes sense in an onAfter context.
if (triggerActionConfig.isQueued() && ctx.isAfter())
{
triggerAction.enableQueueable(triggerActionConfig.isDedicatedQueued());
}
unitOfWork.addWork(triggerAction);
}
unitOfWork.doWork();
}
@TestVisible
protected void loadConfiguration()
{
if (configs.isEmpty() == false) return;
for (fflib_TriggerAction__mdt action : new fflib_TriggerActionConfigSelector().selectAll())
{
configs.add( new fflib_TriggerActionConfig(action));
}
}
private fflib_ITriggerContext constructTriggerContext()
{
// Construct the trigger context domain class constructor class
String triggerContextClassName = triggerContextType.getName();
Type constructableClass =
triggerContextClassName.endsWith('Constructor')
? Type.forName(triggerContextClassName)
: Type.forName(triggerContextClassName + '.Constructor');
fflib_IDomainConstructor domainConstructor = (fflib_IDomainConstructor) constructableClass.newInstance();
// Construct the trigger context domain class with the appropriate record set
if (Trigger.isInsert) return (fflib_ITriggerContext) domainConstructor.construct(Trigger.new);
else if (Trigger.isUpdate) return (fflib_ITriggerContext) domainConstructor.construct(Trigger.new);
else if (Trigger.isDelete) return (fflib_ITriggerContext) domainConstructor.construct(Trigger.old);
else if (Trigger.isUndelete) return (fflib_ITriggerContext) domainConstructor.construct(Trigger.new);
return null;
}
private fflib_ITriggerContext getTriggerContext()
{
if (triggerContext == null) constructTriggerContext();
return triggerContext;
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,76 @@
/**
* File Name: fflib_ALogAppenderFilter
* Description: Log Appender that includes a filter for logginglevel
*
* @author architect ir. Wilhelmus G.J. Velzeboer
*
* Copyright (c), W.G.J. Velzeboer,
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above author notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public interface fflib_ITriggerAction extends fflib_IDoWork
{
/**
* Perform logic during the after delete phase
**/
void onAfterDelete();
/**
* Perform logic during the after Insert phase
**/
void onAfterInsert();
/**
* Perform logic during the after Undelete phase
**/
void onAfterUnDelete();
/**
* Perform logic during the after update phase
**/
void onAfterUpdate();
/**
* Perform logic during the before delete phase
**/
void onBeforeDelete();
/**
* Perform logic during the before insert phase
**/
void onBeforeInsert();
/**
* Perform logic during the before update phase
**/
void onBeforeUpdate();
/**
* Method used by the TriggerHandler and us to pass in the trigger context
*
* @param ctx Instance of fflib_ITriggerContext
*
* @return Instance of itself to allow for method chaining
*/
fflib_ITriggerAction setContext(fflib_ITriggerContext ctx);
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,53 @@
/**
* File Name: fflib_ITriggerActionConfig
*
* @author architect ir. Wilhelmus G.J. Velzeboer
*
* Copyright (c), W.G.J. Velzeboer,
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above author notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public interface fflib_ITriggerActionConfig
{
/**
* @return Returns the sequence number of the trigger action
*/
Integer getSequence();
/**
* @return Returns TRUE if the trigger action should be executed in a dedicated Queueable context
*/
Boolean isDedicatedQueued();
/**
* @return Returns TRUE if the trigger action can be executed in an Queueable context
*/
Boolean isQueued();
/**
* Creates a new instance of the trigger action class based on the provided implementation type in the configuration
*
* @return Instance of the Trigger Action class
*/
fflib_ITriggerAction newInstance();
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,36 @@
/**
* File Name: fflib_ITriggerActionConfigSelector
*
* @author architect ir. Wilhelmus G.J. Velzeboer
*
* Copyright (c), W.G.J. Velzeboer,
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above author notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public interface fflib_ITriggerActionConfigSelector
{
/**
* @return Query all fflib_TriggerAction__mdt records
*/
List<fflib_TriggerAction__mdt> selectAll();
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,100 @@
/**
* File Name: fflib_ITriggerContext
*
* @author architect ir. Wilhelmus G.J. Velzeboer
*
* Copyright (c), W.G.J. Velzeboer,
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above author notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public interface fflib_ITriggerContext
{
/**
* Detects whether any values in context records have changed for given field as string
* Returns list of SObject records that have changes in the specified fields
**/
List<SObject> getChangedRecords(String fieldName);
/**
* Detects whether any values in context records have changed for given fields as strings
* Returns list of SObject records that have changes in the specified fields
**/
List<SObject> getChangedRecords(Set<String> fieldNames);
/**
* Detects whether any values in context records have changed for given field as token
* Returns list of SObject records that have changes in the specified fields
**/
List<SObject> getChangedRecords(Schema.SObjectField fieldToken);
/**
* Detects whether any values in context records have changed for given fields as tokens
* Returns list of SObject records that have changes in the specified fields
**/
List<SObject> getChangedRecords(Set<Schema.SObjectField> fieldTokens);
/**
* Provides access to Trigger.oldMap or the mock when configured in a unit-test
**/
Map<Id, SObject> getExistingRecords();
/**
* @return Returns the System.TriggerOperation enum value
*/
System.TriggerOperation getTriggerOperation();
/**
* Provides access to the data represented by this Trigger Context
**/
List<SObject> getRecords();
/**
* @return Returns TRUE if the Trigger Context is an INSERT operation
*/
Boolean isInsert();
/**
* @return Returns TRUE if the Trigger Context is an UPDATE operation
*/
Boolean isUpdate();
/**
* @return Returns TRUE if the Trigger Context is an DELETE operation
*/
Boolean isDelete();
/**
* @return Returns TRUE if the Trigger Context is an UNDELETE operation
*/
Boolean isUndelete();
/**
* @return Returns TRUE if the Trigger Context is an BEFORE operation
*/
Boolean isBefore();
/**
* @return Returns TRUE if the Trigger Context is an AFTER operation
*/
Boolean isAfter();
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,55 @@
/**
* File Name: fflib_ALogAppenderFilter
* Description: Log Appender that includes a filter for logginglevel
*
* @author architect ir. Wilhelmus G.J. Velzeboer
*
* Copyright (c), W.G.J. Velzeboer,
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above author notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
public interface fflib_ITriggerHandler
{
/**
* Main method to execute the trigger handler to call the trigger actions
*/
void run();
/**
* Tells the trigger handler in which trigger context it is running
*
* @param ctx A configured Trigger Context instance
*
* @return Instance of itself to allow for method chaining
*/
fflib_ITriggerHandler setTriggerContext(fflib_ITriggerContext ctx);
/**
* Tells the trigger handler in which trigger context it is running
*
* @param classType A reference to the Trigger Context class to use when executing the trigger handler
*
* @return Instance of itself to allow for method chaining
*/
fflib_ITriggerHandler setTriggerContextType(System.Type classType);
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<Layout xmlns="http://soap.sforce.com/2006/04/metadata">
<layoutSections>
<customLabel>false</customLabel>
<detailHeading>false</detailHeading>
<editHeading>true</editHeading>
<label>Information</label>
<layoutColumns>
<layoutItems>
<behavior>Required</behavior>
<field>MasterLabel</field>
</layoutItems>
<layoutItems>
<behavior>Required</behavior>
<field>DeveloperName</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>IsProtected</field>
</layoutItems>
<layoutItems>
<behavior>Required</behavior>
<field>NamespacePrefix</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
<layoutSections>
<customLabel>true</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>Trigger Action Configuration</label>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>ObjectType__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>ObjectTypeAlternate__c</field>
</layoutItems>
<layoutItems>
<behavior>Required</behavior>
<field>ImplementationType__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Required</behavior>
<field>ExecutionContext__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Sequence__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>Stateful__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsLeftToRight</style>
</layoutSections>
<layoutSections>
<customLabel>true</customLabel>
<detailHeading>true</detailHeading>
<editHeading>true</editHeading>
<label>Trigger Operation Type</label>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>BeforeInsert__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>BeforeUpdate__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>BeforeDelete__c</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Edit</behavior>
<field>AfterInsert__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>AfterUpdate__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>AfterDelete__c</field>
</layoutItems>
<layoutItems>
<behavior>Edit</behavior>
<field>AfterUndelete__c</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsLeftToRight</style>
</layoutSections>
<layoutSections>
<customLabel>false</customLabel>
<detailHeading>false</detailHeading>
<editHeading>true</editHeading>
<label>System Information</label>
<layoutColumns>
<layoutItems>
<behavior>Readonly</behavior>
<field>CreatedById</field>
</layoutItems>
</layoutColumns>
<layoutColumns>
<layoutItems>
<behavior>Readonly</behavior>
<field>LastModifiedById</field>
</layoutItems>
</layoutColumns>
<style>TwoColumnsTopToBottom</style>
</layoutSections>
<layoutSections>
<customLabel>true</customLabel>
<detailHeading>true</detailHeading>
<editHeading>false</editHeading>
<label>Custom Links</label>
<layoutColumns/>
<layoutColumns/>
<layoutColumns/>
<style>CustomLinks</style>
</layoutSections>
<showEmailCheckbox>false</showEmailCheckbox>
<showHighlightsPanel>false</showHighlightsPanel>
<showInteractionLogPanel>false</showInteractionLogPanel>
<showRunAssignmentRulesCheckbox>false</showRunAssignmentRulesCheckbox>
<showSubmitAndAttachButton>false</showSubmitAndAttachButton>
<summaryLayout>
<masterLabel>00h1g000003HGOK</masterLabel>
<sizeX>4</sizeX>
<sizeY>0</sizeY>
<summaryLayoutStyle>Default</summaryLayoutStyle>
</summaryLayout>
</Layout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomObject xmlns="http://soap.sforce.com/2006/04/metadata">
<label>Trigger Action</label>
<pluralLabel>Trigger Actions</pluralLabel>
<visibility>Protected</visibility>
</CustomObject>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>AfterDelete__c</fullName>
<defaultValue>false</defaultValue>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<label>After Delete</label>
<type>Checkbox</type>
</CustomField>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>AfterInsert__c</fullName>
<defaultValue>false</defaultValue>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<label>After Insert</label>
<type>Checkbox</type>
</CustomField>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>AfterUndelete__c</fullName>
<defaultValue>false</defaultValue>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<label>After Undelete</label>
<type>Checkbox</type>
</CustomField>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>AfterUpdate__c</fullName>
<defaultValue>false</defaultValue>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<label>After Update</label>
<type>Checkbox</type>
</CustomField>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>BeforeDelete__c</fullName>
<defaultValue>false</defaultValue>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<label>Before Delete</label>
<type>Checkbox</type>
</CustomField>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>BeforeInsert__c</fullName>
<defaultValue>false</defaultValue>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<label>Before Insert</label>
<type>Checkbox</type>
</CustomField>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>BeforeUpdate__c</fullName>
<defaultValue>false</defaultValue>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<label>Before Update</label>
<type>Checkbox</type>
</CustomField>
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>ExecutionContext__c</fullName>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<label>Execution Context</label>
<required>true</required>
<type>Picklist</type>
<valueSet>
<restricted>true</restricted>
<valueSetDefinition>
<sorted>false</sorted>
<value>
<fullName>Realtime</fullName>
<default>true</default>
<label>Realtime</label>
</value>
<value>
<fullName>Queued</fullName>
<default>false</default>
<label>Queued</label>
</value>
<value>
<fullName>Dedicated Queued</fullName>
<default>false</default>
<label>Dedicated Queued</label>
</value>
</valueSetDefinition>
</valueSet>
</CustomField>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>ImplementationType__c</fullName>
<externalId>false</externalId>
<fieldManageability>SubscriberControlled</fieldManageability>
<label>Implementation Type</label>
<length>255</length>
<required>true</required>
<type>Text</type>
<unique>false</unique>
</CustomField>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>ObjectTypeAlternate__c</fullName>
<description>Use this field to specify the SObject API name to bind to when the &quot;Object Type&quot; field does not list that particular SObject. Only specify the &quot;Object Type&quot; field or this one; not both.</description>
<externalId>false</externalId>
<fieldManageability>SubscriberControlled</fieldManageability>
<inlineHelpText>Use this field to specify the SObject API name to bind to when the &quot;Object Type&quot; field does not list that particular SObject. Only specify the &quot;Object Type&quot; field or this one; not both.</inlineHelpText>
<label>ObjectType Alternate</label>
<length>255</length>
<required>false</required>
<type>Text</type>
<unique>false</unique>
</CustomField>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>ObjectType__c</fullName>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<label>Object Type</label>
<referenceTo>EntityDefinition</referenceTo>
<relationshipLabel>TriggerActions</relationshipLabel>
<relationshipName>TriggerActions</relationshipName>
<required>false</required>
<type>MetadataRelationship</type>
<unique>false</unique>
</CustomField>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Sequence__c</fullName>
<externalId>false</externalId>
<fieldManageability>SubscriberControlled</fieldManageability>
<label>Sequence</label>
<precision>18</precision>
<required>false</required>
<scale>0</scale>
<type>Number</type>
<unique>false</unique>
</CustomField>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Stateful__c</fullName>
<defaultValue>false</defaultValue>
<externalId>false</externalId>
<fieldManageability>DeveloperControlled</fieldManageability>
<label>Stateful</label>
<type>Checkbox</type>
</CustomField>
@@ -30,15 +30,24 @@
*/
public abstract class fflib_DoWork implements fflib_IDoWork, Comparable
{
protected Integer priority = 0;
protected OrderType orderingType = OrderType.PRIORITY; // Set the default ordering to using Priority over Sequencing
protected Integer orderNumber = 0; // Either the Priority or Sequence value, based on the orderingType
protected Boolean dedicatedQueueable;
public virtual Integer compareTo(Object compareToObject)
{
fflib_IDoWork compareTo = (fflib_IDoWork) compareToObject;
if (priority == compareTo.getPriority()) return 0;
if (priority > compareTo.getPriority()) return -1;
return 1;
if (orderNumber == compareTo.getOrderNumber()) return 0;
if (orderingType == OrderType.PRIORITY)
{
if (orderNumber > compareTo.getOrderNumber()) return -1; else return 1;
}
else
{
if (orderNumber > compareTo.getOrderNumber()) return 1; else return -1;
}
}
/**
@@ -65,13 +74,13 @@ public abstract class fflib_DoWork implements fflib_IDoWork, Comparable
}
/**
* Gets the priority of this work item. The higher the number the higher the priority
* Gets the priority or sequence number of this work item. The higher the number the higher the priority
*
* @return Returns the priority
*/
public virtual Integer getPriority()
public virtual Integer getOrderNumber()
{
return this.priority;
return this.orderNumber;
}
/**
@@ -107,7 +116,16 @@ public abstract class fflib_DoWork implements fflib_IDoWork, Comparable
*/
public virtual fflib_IDoWork setPriority(Integer priority)
{
this.priority = priority;
this.orderNumber = priority;
this.orderingType = OrderType.PRIORITY;
return this;
}
public virtual fflib_IDoWork setSequence(Integer sequence)
{
this.orderNumber = sequence;
this.orderingType = OrderType.SEQUENCE;
return this;
}
private enum OrderType { PRIORITY, SEQUENCE }
}
@@ -129,10 +129,17 @@ public virtual class fflib_UnitOfWork
*/
public virtual void doWork()
{
sortAndDoWork();
doNextWork();
}
private void sortAndDoWork()
{
if (this.work.isEmpty()) return;
this.work.sort();
doRealTimeWork();
doQueuedWork();
doNextWork();
}
/**
@@ -52,11 +52,11 @@ public interface fflib_IDoWork extends Comparable
fflib_IDoWork enableQueueable(Boolean dedicated);
/**
* Gets the priority of this work item. The higher the number the higher the priority
* Gets the priority or sequence number of this work item. The higher the number the higher the priority
*
* @return Returns the priority
*/
Integer getPriority();
Integer getOrderNumber();
/**
* @return Returns true when the work item is intended to be executed in separate UnitOfWork executed in a Queueable context
@@ -81,4 +81,5 @@ public interface fflib_IDoWork extends Comparable
* @return Instance of itself to allow for chaining
*/
fflib_IDoWork setPriority(Integer priority);
fflib_IDoWork setSequence(Integer sequence);
}
@@ -0,0 +1,46 @@
/**
* File Name: MyTest
* Description:
*
* @author: architect ir. Wilhelmus G.J. Velzeboer | wvelzebo@its.jnj.com
*/
public with sharing class MyTest
{
public class WorkA extends fflib_DoWork
{
public void doWork()
{
System.debug('Work A executing');
insert new fflib_LogEntry__c(
LogLevel__c = 'INFO',
Message__c = 'WorkA'
);
}
}
public class WorkB extends fflib_DoWork
{
public void doWork()
{
System.debug('Work B executing');
insert new fflib_LogEntry__c(
LogLevel__c = 'INFO',
Message__c = 'WorkB'
);
}
}
public class WorkC extends fflib_DoWork
{
public void doWork()
{
System.debug('Work C executing');
insert new fflib_LogEntry__c(
LogLevel__c = 'INFO',
Message__c = 'WorkC'
);
}
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -0,0 +1,266 @@
/**
* File Name: fflib_TriggerHandlerTest
* Description: Unit Test class for fflib_TriggerHandler implementation
*
* @author architect ir. Wilhelmus G.J. Velzeboer
*
* Copyright (c), W.G.J. Velzeboer,
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above author notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* - Neither the name of the author nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
* THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@IsTest(IsParallel=true)
private class fflib_TriggerHandlerTest
{
private static Boolean handlerGotInvoked = false;
@IsTest
static void testBehavior()
{
// GIVEN a set of records with a configured trigger handler
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_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'
);
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.loadConfiguration();
triggerHandler.addTriggerActionConfig(triggerAction);
triggerHandler
.setTriggerContextType(fflib_TriggerContext.class)
.setTriggerContext(ctx)
.run();
System.Test.stopTest();
// THEN the trigger handler should be invoked
System.assert(handlerGotInvoked, 'Whoops, the Trigger Action was not invoked');
System.assertEquals(records, ctx.records);
System.assert(ctx.isInsert());
System.assert(ctx.isBefore());
System.assert(ctx.isDelete() == false);
System.assert(ctx.isUndelete() == false);
}
@IsTest
private static void itShouldReturnTheChangedRecordsBySObjectFields()
{
// GIVEN a domain with old and changed records
Id idLuke = fflib_IDGenerator.generate(Account.SObjectType);
Id idHan = fflib_IDGenerator.generate(Account.SObjectType);
Id idLeia = fflib_IDGenerator.generate(Account.SObjectType);
List<Account> oldRecords = new List<Account>
{
new Account(Id = idLuke, Name = 'Luke', Description = 'Jedi'),
new Account(Id = idHan, Name = 'Han', Description = 'Pilot'),
new Account(Id = idLeia, Name = 'Leia')
};
List<Account> newRecords = oldRecords.deepClone(true, true, true);
newRecords.get(0).Name = 'Luke SkyWalker';
newRecords.get(0).Description = 'Jedi Master';
newRecords.get(1).Name = 'Han Solo';
fflib_TriggerContext ctx = new fflib_TriggerContext(newRecords);
ctx.existingRecords = new Map<Id, SObject>(oldRecords);
// WHEN we create a domain with ExistingRecords and request the changed records
List<SObject> result = ctx.getChangedRecords(Account.Name);
// THEN it should only return the changed records
Map<Id, SObject> resultMap = new Map<Id, SObject>(result);
System.assertEquals(2, result.size());
System.assert(resultMap.containsKey(idLuke));
System.assert(resultMap.containsKey(idHan));
}
@IsTest
private static void itShouldReturnTheChangedRecordsByStringFields()
{
// GIVEN a domain with old and changed records
Id idLuke = fflib_IDGenerator.generate(Account.SObjectType);
Id idHan = fflib_IDGenerator.generate(Account.SObjectType);
Id idLeia = fflib_IDGenerator.generate(Account.SObjectType);
List<Account> oldRecords = new List<Account>
{
new Account(Id = idLuke, Name = 'Luke', Description = 'Jedi'),
new Account(Id = idHan, Name = 'Han', Description = 'Pilot'),
new Account(Id = idLeia, Name = 'Leia')
};
List<Account> newRecords = oldRecords.deepClone(true, true, true);
newRecords.get(0).Name = 'Luke SkyWalker';
newRecords.get(0).Description = 'Jedi Master';
newRecords.get(1).Name = 'Han Solo';
fflib_TriggerContext ctx = new fflib_TriggerContext(newRecords);
ctx.existingRecords = new Map<Id, SObject>(oldRecords);
// WHEN we create a domain with ExistingRecords and request the changed records
List<SObject> result = ctx.getChangedRecords('Description');
// THEN it should only return the changed records
Map<Id, SObject> resultMap = new Map<Id, SObject>(result);
System.assertEquals(1, result.size());
System.assert(resultMap.containsKey(idLuke));
}
@IsTest
private static void itShouldCallTriggerActionOnAfterDelete()
{
List<Account> records = new List<Account>
{
new Account(Id = fflib_IDGenerator.generate(Account.SObjectType))
};
fflib_TriggerContext ctx = new fflib_TriggerContext(records);
ctx.triggerOperation = System.TriggerOperation.AFTER_DELETE;
new MyTriggerAction().setContext(ctx).doWork();
System.assert(ctx.isAfter());
System.assert(ctx.isDelete());
}
@IsTest
private static void itShouldCallTriggerActionOnAfterInsert()
{
List<Account> records = new List<Account>
{
new Account(Id = fflib_IDGenerator.generate(Account.SObjectType))
};
fflib_TriggerContext ctx = new fflib_TriggerContext(records);
ctx.triggerOperation = System.TriggerOperation.AFTER_INSERT;
new MyTriggerAction().setContext(ctx).doWork();
System.assert(ctx.isAfter());
System.assert(ctx.isInsert());
}
@IsTest
private static void itShouldCallTriggerActionOnAfterUndelete()
{
List<Account> records = new List<Account>
{
new Account(Id = fflib_IDGenerator.generate(Account.SObjectType))
};
fflib_TriggerContext ctx = new fflib_TriggerContext(records);
ctx.triggerOperation = System.TriggerOperation.AFTER_UNDELETE;
new MyTriggerAction().setContext(ctx).doWork();
System.assert(ctx.isAfter());
System.assert(ctx.isUndelete());
}
@IsTest
private static void itShouldCallTriggerActionOnAfterUpdate()
{
List<Account> records = new List<Account>
{
new Account(Id = fflib_IDGenerator.generate(Account.SObjectType))
};
fflib_TriggerContext ctx = new fflib_TriggerContext(records);
ctx.triggerOperation = System.TriggerOperation.AFTER_UPDATE;
new MyTriggerAction().setContext(ctx).doWork();
System.assert(ctx.isAfter());
System.assert(ctx.isUpdate());
}
@IsTest
private static void itShouldCallTriggerActionOnBeforeDelete()
{
List<Account> records = new List<Account>
{
new Account(Id = fflib_IDGenerator.generate(Account.SObjectType))
};
fflib_TriggerContext ctx = new fflib_TriggerContext(records);
ctx.triggerOperation = System.TriggerOperation.BEFORE_DELETE;
new MyTriggerAction().setContext(ctx).doWork();
System.assert(ctx.isBefore());
System.assert(ctx.isDelete());
}
@IsTest
private static void itShouldCallTriggerActionOnBeforeInsert()
{
List<Account> records = new List<Account>
{
new Account(Id = fflib_IDGenerator.generate(Account.SObjectType))
};
fflib_TriggerContext ctx = new fflib_TriggerContext(records);
ctx.triggerOperation = System.TriggerOperation.BEFORE_INSERT;
new MyTriggerAction().setContext(ctx).doWork();
System.assert(ctx.isBefore());
System.assert(ctx.isInsert());
}
@IsTest
private static void itShouldCallTriggerActionOnBeforeUpdate()
{
List<Account> records = new List<Account>
{
new Account(Id = fflib_IDGenerator.generate(Account.SObjectType))
};
fflib_TriggerContext ctx = new fflib_TriggerContext(records);
ctx.triggerOperation = System.TriggerOperation.BEFORE_UPDATE;
new MyTriggerAction().setContext(ctx).doWork();
System.assert(ctx.isBefore());
System.assert(ctx.isUpdate());
}
public class MyTriggerAction extends fflib_TriggerAction // Todo - rename handler to action
{
public override void onBeforeInsert()
{
fflib_TriggerHandlerTest.handlerGotInvoked = true;
List<SObject> records = triggerContext.getRecords();
}
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -101,7 +101,7 @@ private class fflib_UnitOfWorkTest
}
@IsTest
static void itShouldExecuteAllItemsInTheCorrectOrder()
static void itShouldExecuteAllItemsInTheCorrectOrderOfPriority()
{
// GIVEN - Three different work items added to the fflib_DoWork
Work workA = (Work) new WorkA().setPriority(1);
@@ -122,6 +122,28 @@ private class fflib_UnitOfWorkTest
System.assert(orderOfExecution.get(2).contains('WorkA'), 'Expected Work B with priority 1 to be executed last');
}
@IsTest
static void itShouldExecuteAllItemsInTheCorrectOrderOfSequence()
{
// GIVEN - Three different work items added to the fflib_DoWork
Work workA = (Work) new WorkA().setSequence(3);
Work workB = (Work) new WorkB().setSequence(2);
Work workC = (Work) new WorkC().setSequence(1);
fflib_UnitOfWork myWork = new fflib_UnitOfWork().addWork(
new List<fflib_IDoWork> {workA, workB, workC}
);
// WHEN - The work is being executed
System.Test.startTest();
myWork.doWork();
System.Test.stopTest();
// THEN - Then all three work items should be executed
System.assert(orderOfExecution.get(0).contains('WorkC'), 'Expected Work C with priority 3 to be executed first');
System.assert(orderOfExecution.get(1).contains('WorkB'), 'Expected Work B with priority 2 to be executed second');
System.assert(orderOfExecution.get(2).contains('WorkA'), 'Expected Work B with priority 1 to be executed last');
}
private virtual class Work extends fflib_DoWork
{
public Boolean didWork = false;
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>51.0</apiVersion>
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>51.0</apiVersion>
<apiVersion>53.0</apiVersion>
<status>Active</status>
</ApexClass>