Lightweight Custom Metadata driven Trigger Framework that scales to your needs. Extended from TriggerX by Seb Wagner, provided with <3 by appero.com
- added
SObjectApiName__c
toMyTrigger Settings
to accomodate for sObjects not available in thesObject
picklist - added
IsByPassAllowed__c
toMyTrigger Settings
and a custom permissionbypassMyTriggers
. This allows for excluding certain users from trigger execution (e.g. Integration User).
- initial release
- Production Instances: https://login.salesforce.com/packaging/installPackage.apexp?p0=04t1i000000gZ4HAAU
- Sandbox Instances: https://test.salesforce.com/packaging/installPackage.apexp?p0=04t1i000000gZ4HAAU
Clone this repo
git clone https://github.com/appero-com/MyTriggers
Create a scratch org and push source
sfdx force:org:create -a MyTriggers -s -f config/project-scratch-def.json && sfdx force:source:push -r MyTriggers/framework
or deploy to your org
sfdx force:source:convert -r MyTriggers/framework -d src && sfdx force:mdapi:deploy -u <username> -d src
Found something? Use the issues page
PRs are welcome
MyTriggers is a lightweight Custom Metadata driven Trigger Framework that scales to your needs. It is based upon the foundation of TriggerX that Sebastian Wagner wrote in 2013, which was a perfect starting point since it covered all one could wish for in a trigger handler except Custom Metadata Types.
The general approach behind MyTriggers is
- run all triggers through one central handler, even across namespaces.
- design the orchestration of logic in a way that allows you to declarative wire things differently
- think about Triggers in new ways: configurable, closer to business needs/processes than database changes
MyTriggers was released by appero GmbH and publicly presented by Christian Szandor Knapp and Daniel Stange.
If you want to recap the session, a recording will be available a few weeks after Dreamforce. The session discussion and assets will be stored at the session's tralblazer commuinty page.
You can follow Szandor on Twitter at @ch_sz_knapp, Daniel is @stangomat.
When you reflect upon what your triggers actually do, you may hardly ever say that they are pieces of code that react to changes in data whenever they happen. You'd rather describe them as entry points for your business processes that, for example, create an onboarding case for new customers whenever an opportunity closes, but only for accounts that never had a closed opportunity before.
Now, with that description in mind, we should build our trigger handlers in a way that they can react to change in business requirements, and that they handle a lot more than just the records that initially started the process.
This is why MyTriggers has a records property that contains any sObject type (but all the records that are handled currently), and this is why there are Custom Metadata Type records that can be activated, grouped by names, put in a sequential orders (and re-ordered if need be).
You decide what happens when a trigger fires - the constant is that it will always open one central instance of MyTriggers that orchestrates your business processes according to your Metadata config.
- Trigger execution will be started by instantion of MyTriggers and calling the run() method from a Trigger
- records contains all objects currently handled by the trigger context
- recordsNotYetProcessed can be inspected through their getter method
- Ids of updated records can be accessed through their getter method
- handled trigger contexts can be accessed through their setter method
- you can enable() or disable() specific handler steps or trigger contexts at runtime
- for each handler step, MyTriggers has to be extended
- each handler step orchestrates its logic through overriding methods specific for the trigger contexts. _ onBeforeInsert() _ onAfterInsert() _ onBeforeUpdate() _ onAfterUpdate() _ onBeforeDelete() _ onAfterDelete() * onAfterUndelete()
For MyTriggers to handle your triggers, create a Trigger for all contexts. Create a new instance of MyTriggers and call run()
.
Trigger AccountTrigger on Account (
before insert,
before update,
before delete,
after insert,
after update,
after delete,
after undelete) {
MyTriggers.run();
}
Each handler step should extend the MyTriggers class and can override any of the methods.
public class newAccounts_ValidateType extends MyTriggers {
public override void onBeforeInsert() {
List<Account> newAccounts = (List<Account>)records;
}
private void stepLogic() {
// some method to handle the logic
}
}
MyTriggers exposes a public instance variable records that contains all records that are handled by MyTriggers. When working with the records property, you should cast it to a specific type.
(List<Account>)records
MyTriggers has a helper property for you to control your the process flow: recordsNotYetProcessed. You can access it through its getter method, Same goes for updated records - you can access their Ids through a getter
The execution flow is controlled by custom metadata records of the MyTriggerSetting type. You have to specify
- an sObject Type of a standard or custom object or a platform event
- a class that contains your logic for this step
- a trigger context
Additionally, you should set
- the activation flag
- a sequence number
Optionally, set
- a namespace prefix for the class that you are going to call if you want to call (or build) namespaced trigger handler steps.
It doesn't really matter which sequence numbering you choose as long as it can be sorted. To avoid renumbering a whole set of steps, choose a numbering method that allows for gaps.
Classic ERP numbering styles and sequence might make you smile - but if your initial numbering sequence was 100, 200, 300, 400, 500 ..., you can add 190 and 210 later, and 195 and 215... without renumbering the whole list if you add one step inbetween.
MyTriggers allows you to deactivate or re-wire Trigger steps in productive environments. But just because it is possible does not mean that it is a good idea, necessarily.
Be extra careful when you deactivate or modify productive Trigger handler steps and keep a reminder that works for you (sticky notes, an alarm clock) so that you don't forget to activate your triggers again.
MyTriggers allows total control over all trigger operation at runtime.
public override void onAfterInsert() {
// records property provided by myTriggers
List<Account> newAccounts = new Map<Id,Map>(records);
// disable after insert trigger on Opp so it doesn't interfere
myTriggers.disable(myCaseTrigger.class,
new List<System.TriggerOperation>{
System.TriggerOperation.AFTER_UPDATE});
CaseService.updateCustomerCareCases(newAccounts);
}
// use string field names
List<String> fieldNamesToCheck = new String[]{'Multiplier__c','Revenue__c','OwnerId','Type__c'};
if (MyTriggers.hasChangedFields(fieldNamesToCheck,record,recordOld)){
// logic executed when condition is true
}
// or sObjectFields
sObjectField[] fieldsToCheck = new sObjectField[]{Share__c.Multiplier__c, Share__c.Revenue__c, Share__c.OwnerId, Share__c.Type__c};
for (sObjectField field : MyTriggers.getChangedFields(fieldsToCheck,record,recordOld)){
// process field
}
// add all records in the current update context
MyTriggers.addUpdatedIds(triggerOldMap.keySet());
// and use this to return only records which havent been processed before
List<Sobject> untouchedRecords = MyTriggers.getRecordsNotYetProcessed();
global virtual class MyTriggers
Leightweight Custom Metadata driven Trigger Framework that scales to your needs
records
cast records to appropriate sObjectType in implementations
-
add set of ids to updatedIds
-
disable disables all events for System.Type MyClass
-
disable disables all events for the trigger handler with given namespace and classname Method also works in subscriber org with hidden (public) trigger handlers from managed package
-
disable disable all specificed events for the System.Type MyClass
-
disable all specificed events for given ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
-
disable a single event for System.Type MyClass
-
disable a single event for ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
-
used instead of constructor since handlers are instanciated with an empty contructor
-
removes all disabled events for the System.Type MyClass
-
removes all disabled events for given ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
-
enable all specificed events for the System.Type MyClass
-
enable all specificed events for given ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
-
enable a single event for System.Type MyClass
-
enable a single event for ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
-
getAfterEvents list of all AFTER System.TriggerOperation enums
-
getBeforeEvents list of all BEFORE System.TriggerOperation enums
-
returns a list of changed fields based on provided fieldList list
-
returns a list of changed fields based on provided fieldList list
-
getDeleteEvents all delete events
-
returns set of disabled events
-
returns set of disabled events Method also works in subscriber org with hidden (public) trigger handlers from managed package
-
getInsertEvents all insert events
-
returns a list of objects that have not been processed yet
-
return all updated ids
-
getUpdateEvents all update events
-
returns true if a value of one of the specified fields has changed
-
returns true if a value of one of the specified fields has changed
-
isDisabled returns true if the specified event is disabled
-
isDisabled returns true if the specified event is disabled Method also works in subscriber org with hidden (public) trigger handlers from managed package
-
Global Constructor reserved for future use
-
executed to perform AFTER_DELETE operations
-
executed to perform AFTER_INSERT operations
-
executed to perform AFTER_UNDELETE operations
-
executed to perform AFTER_UPDATE operations
-
executed to perform BEFORE_DELETE operations
-
executed to perform BEFORE_INSERT operations
-
executed to perform BEFORE_UPDATE operations
-
Entry point of myTriggers framework - called from implementations
-
loads trigger event settings MyTriggerSetting__mdt
-
loads trigger event settings MyTriggerSetting__mdt Method also works in subscriber org with hidden (public) trigger handlers from managed package
-
converts a Set of Event enums into Strings
global static void addUpdatedIds(Set idSet)
add set of ids to updatedIds
- idSet - Set, usally Trigger.newMap.keyset()
global static void disable(Type MyClass)
disable disables all events for System.Type MyClass
global static void disable(String namespacePrefix, String className)
disable disables all events for the trigger handler with given namespace and classname Method also works in subscriber org with hidden (public) trigger handlers from managed package
global static void disable(Type MyClass, System.TriggerOperation[] events)
disable all specificed events for the System.Type MyClass
disable all specificed events for given ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
global static void disable(Type MyClass, System.TriggerOperation event)
disable a single event for System.Type MyClass
global static void disable(String namespacePrefix, String className, System.TriggerOperation event)
disable a single event for ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
global virtual myTriggers doConstruct(sObject[] records)
used instead of constructor since handlers are instanciated with an empty contructor
- records - Array of current sObjects. For INSERT & UPDATE Trigger.new otherwise Trigger.old
global static void enable(Type MyClass)
removes all disabled events for the System.Type MyClass
global static void enable(String namespacePrefix, String className)
removes all disabled events for given ClassName and Namespace Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
global static void enable(Type MyClass, System.TriggerOperation[] events)
enable all specificed events for the System.Type MyClass
enable all specificed events for given ClassName and Namespace. Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
global static void enable(Type MyClass, System.TriggerOperation event)
enable a single event for System.Type MyClass
global static void enable(String namespacePrefix, String className, System.TriggerOperation event)
enable a single event for ClassName and Namespace. Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
global static System.TriggerOperation[] getAfterEvents()
getAfterEvents list of all AFTER System.TriggerOperation enums
global static System.TriggerOperation[] getBeforeEvents()
getBeforeEvents list of all BEFORE System.TriggerOperation enums
global static String[] getChangedFields(String[] fieldList, sObject record, sObject recordOld)
returns a list of changed fields based on provided fieldList list
returns a list of changed fields based on provided fieldList list
global static System.TriggerOperation[] getDeleteEvents()
getDeleteEvents all delete events
global static Set getDisabledEvents(Type MyClass)
returns set of disabled events
- Set of disabled Event Namens (e.g. 'AFTER_UPDATE')
global static Set getDisabledEvents(String namespacePrefix, String className)
returns set of disabled events. Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
global static System.TriggerOperation[] getInsertEvents()
get all insert events
global protected sObject[] getRecordsNotYetProcessed()
returns a list of objects that have not been processed yet
global static Set getUpdatedIds()
return all updated ids
- set of updated/already touched Ids
global static System.TriggerOperation[] getUpdateEvents()
getUpdateEvents all update events
System.TriggerOperation[]
global static Boolean hasChangedFields(String[] fieldList, sObject record, sObject recordOld)
returns true if a value of one of the specified fields has changed
global static Boolean hasChangedFields(sObjectField[] fieldList, sObject record, sObject recordOld)
returns true if a value of one of the specified fields has changed
global static Boolean isDisabled(Type MyClass, System.TriggerOperation event)
returns true if the specified event is disabled
returns true if the specified event is disabled Method. Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
Global Constructor reserved for future use
global virtual void onAfterDelete()
executed to perform AFTER_DELETE operations
global virtual void onAfterInsert()
executed to perform AFTER_INSERT operations
global virtual void onAfterUndelete()
executed to perform AFTER_UNDELETE operations
global virtual void onAfterUpdate(Map<Id,sObject> triggerOldMap)
executed to perform AFTER_UPDATE operations
global virtual void onBeforeDelete()
executed to perform BEFORE_DELETE operations
global virtual void onBeforeInsert()
executed to perform BEFORE_INSERT operations
global virtual void onBeforeUpdate(Map<Id,sObject> triggerOldMap)
executed to perform BEFORE_UPDATE operations
Entry point of myTriggers framework - called from implementations
global static void setAllowedTriggerEvents(Type triggerHandlerType, Boolean forceInit)
loads trigger event settings MyTriggerSetting__mdt
- triggerHandlerType
- forceInit - force reload of event settings
loads trigger event settings MyTriggerSetting__mdt records. Also works in subscriber org with packaged public trigger handlers implementing MyTriggers
- namespacePrefix
- className
- forceInit - force reload of event settings
global static Set toStringEvents(System.TriggerOperation[] events)
converts a Set of Event enums into Strings