From a14efd1e8e318282d4a50d533a5ca2b1a9e7ce2c Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Sun, 19 Oct 2025 20:30:47 -0500 Subject: [PATCH 1/9] Add `Schema.SObjectType` bypass functionality to TriggerBase class and corresponding tests --- .../main/default/classes/TriggerBase.cls | 32 ++++++++++++++++++- .../main/default/classes/TriggerBaseTest.cls | 32 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/trigger-actions-framework/main/default/classes/TriggerBase.cls b/trigger-actions-framework/main/default/classes/TriggerBase.cls index 61389ec..c45a231 100644 --- a/trigger-actions-framework/main/default/classes/TriggerBase.cls +++ b/trigger-actions-framework/main/default/classes/TriggerBase.cls @@ -33,7 +33,7 @@ * The `TriggerAction` interface defines the methods that should be implemented by trigger actions. */ @SuppressWarnings( - 'PMD.StdCyclomaticComplexity, PMD.CyclomaticComplexity, PMD.cognitivecomplexity, PMD.PropertyNamingConventions' + 'PMD.StdCyclomaticComplexity, PMD.CyclomaticComplexity, PMD.CognitiveComplexity, PMD.PropertyNamingConventions' ) public inherited sharing virtual class TriggerBase { @TestVisible @@ -136,6 +136,36 @@ public inherited sharing virtual class TriggerBase { return TriggerBase.bypassedObjects.contains(sObjectName); } + /** + * @description This method bypasses the execution of the specified object. + * + * @param sObjectType The sObjectType of the object to bypass. + */ + public static void bypass(Schema.sObjectType sObjectType) { + TriggerBase.bypassedObjects.add(sObjectType.getDescribe().getName()); + } + + /** + * @description This method clears the bypass for the specified object. + * + * @param sObjectType The sObjectType of the object to clear the bypass for. + */ + public static void clearBypass(Schema.sObjectType sObjectType) { + TriggerBase.bypassedObjects.remove(sObjectType.getDescribe().getName()); + } + + /** + * @description This method checks if the specified object is bypassed. + * + * @param sObjectType The sObjectType of the object to check the bypass for. + * @return True if the object is bypassed, false otherwise. + */ + public static Boolean isBypassed(Schema.sObjectType sObjectType) { + return TriggerBase.bypassedObjects.contains( + sObjectType.getDescribe().getName() + ); + } + /** * @description This method clears all bypasses. */ diff --git a/trigger-actions-framework/main/default/classes/TriggerBaseTest.cls b/trigger-actions-framework/main/default/classes/TriggerBaseTest.cls index 54e65f9..885b351 100644 --- a/trigger-actions-framework/main/default/classes/TriggerBaseTest.cls +++ b/trigger-actions-framework/main/default/classes/TriggerBaseTest.cls @@ -295,6 +295,38 @@ private class TriggerBaseTest { ); } + @IsTest + private static void bypassWithSObjectTypeShouldSucceed() { + TriggerBase.bypass(Schema.Account.SObjectType); + + System.Assert.isTrue( + TriggerBase.bypassedObjects.contains(ACCOUNT), + BYPASSES_SHOULD_BE_CONFIGURED_CORRECTLY + ); + } + + @IsTest + private static void clearBypassWithSObjectTypeShouldSucceed() { + TriggerBase.bypass(Schema.Account.SObjectType); + + TriggerBase.clearBypass(Schema.Account.SObjectType); + + System.Assert.isFalse( + TriggerBase.bypassedObjects.contains(ACCOUNT), + BYPASSES_SHOULD_BE_CONFIGURED_CORRECTLY + ); + } + + @IsTest + private static void isBypassedWithSObjectTypeShouldSucceed() { + TriggerBase.bypass(Schema.Account.SObjectType); + + System.Assert.isTrue( + TriggerBase.isBypassed(Schema.Account.SObjectType), + BYPASSES_SHOULD_BE_CONFIGURED_CORRECTLY + ); + } + @IsTest private static void offsetRowsShouldWork() { TriggerBase.offsetExistingDmlRows(); From 21833c2e8c66675e8b576063401ef64b57a8ccff Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Sun, 19 Oct 2025 20:34:50 -0500 Subject: [PATCH 2/9] Add bypass functionality for Trigger Actions via Type in MetadataTriggerHandler --- .../classes/MetadataTriggerHandler.cls | 34 ++++++++++++++++++- .../classes/MetadataTriggerHandlerTest.cls | 31 +++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls b/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls index a4fa549..10a8ef1 100644 --- a/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls +++ b/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls @@ -75,7 +75,9 @@ * This example will execute all Trigger Actions defined in Custom Metadata for the `Account` sObject. * */ -@SuppressWarnings('PMD.CognitiveComplexity') +@SuppressWarnings( + 'PMD.CognitiveComplexity, PMD.StdCyclomaticComplexity, PMD.CyclomaticComplexity' +) public inherited sharing class MetadataTriggerHandler extends TriggerBase implements TriggerAction.BeforeInsert, TriggerAction.AfterInsert, TriggerAction.BeforeUpdate, TriggerAction.AfterUpdate, TriggerAction.BeforeDelete, TriggerAction.AfterDelete, TriggerAction.AfterUndelete { private static final String DOUBLE_UNDERSCORE = '__'; private static final String EMPTY_STRING = ''; @@ -146,6 +148,36 @@ public inherited sharing class MetadataTriggerHandler extends TriggerBase implem return MetadataTriggerHandler.bypassedActions.contains(actionName); } + /** + * @description Bypass the execution of a Trigger Action. + * + * @param actionType The Type of the Trigger Action to bypass. + */ + public static void bypass(System.Type actionType) { + MetadataTriggerHandler.bypassedActions.add(actionType.getName()); + } + + /** + * @description Clear the bypass for a Trigger Action. + * + * @param actionType The Type of the Trigger Action to clear the bypass for. + */ + public static void clearBypass(System.Type actionType) { + MetadataTriggerHandler.bypassedActions.remove(actionType.getName()); + } + + /** + * @description Check if a Trigger Action is bypassed. + * + * @param actionType The Type of the Trigger Action to check. + * @return True if the Trigger Action is bypassed, false otherwise. + */ + public static Boolean isBypassed(System.Type actionType) { + return MetadataTriggerHandler.bypassedActions.contains( + actionType.getName() + ); + } + /** * @description Clear all bypasses for Trigger Actions. */ diff --git a/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls b/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls index 20e394b..9d0861d 100644 --- a/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls +++ b/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls @@ -631,6 +631,37 @@ private class MetadataTriggerHandlerTest { ); } + @IsTest + private static void bypassWithTypeShouldSucceed() { + MetadataTriggerHandler.bypass(TestBeforeInsert.class); + + System.Assert.isTrue( + MetadataTriggerHandler.bypassedActions.contains(TEST_BEFORE_INSERT), + BYPASSES_SHOULD_BE_CONFIGURED_PROPERLY + ); + } + + @IsTest + private static void clearBypassWithTypeShouldSucceed() { + MetadataTriggerHandler.bypass(TestBeforeInsert.class); + MetadataTriggerHandler.clearBypass(TestBeforeInsert.class); + + System.Assert.isFalse( + MetadataTriggerHandler.bypassedActions.contains(TEST_BEFORE_INSERT), + BYPASSES_SHOULD_BE_CONFIGURED_PROPERLY + ); + } + + @IsTest + private static void isBypassedWithTypeShouldSucceed() { + Boolean isBypassed; + MetadataTriggerHandler.bypass(TestBeforeInsert.class); + + isBypassed = MetadataTriggerHandler.isBypassed(TestBeforeInsert.class); + + System.Assert.isTrue(isBypassed, BYPASSES_SHOULD_BE_CONFIGURED_PROPERLY); + } + @IsTest private static void actionShouldExecuteIfNoRequiredOrBypassPermissionsAreDefinedForSObjectOrAction() { MetadataTriggerHandler.selector = new FakeSelector( From 48f1679761ceeb7798e44fdfcda7873e05128c08 Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Sun, 19 Oct 2025 21:42:58 -0500 Subject: [PATCH 3/9] Add compile-safe bypass and validation methods for Flow types in TriggerActionFlow --- .../default/classes/TriggerActionFlow.cls | 69 ++++++++++ .../default/classes/TriggerActionFlowTest.cls | 124 ++++++++++++++++++ 2 files changed, 193 insertions(+) diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls b/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls index f4fdfce..a366001 100644 --- a/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls @@ -24,9 +24,13 @@ public virtual inherited sharing class TriggerActionFlow implements TriggerActio @TestVisible private static final String RECORD_VARIABLE_NOT_FOUND_ERROR = 'There must be a variable defined in this flow with api name of "record" and type of "record" that is marked as "available for output"'; @TestVisible + private static final String FLOW_INTERVIEW_PREFIX = 'Flow.Interview'; + @TestVisible private static Set bypassedFlows = new Set(); @TestVisible private static InvocableAction invocableAction = new InvocableAction(); + @TestVisible + private static NameExtractor nameExtractor = new NameExtractor(); public String flowName; public Boolean allowRecursion; @@ -59,6 +63,40 @@ public virtual inherited sharing class TriggerActionFlow implements TriggerActio return TriggerActionFlow.bypassedFlows.contains(flowName); } + /** + * @description This method bypasses the execution of the Flow for the specified list of records. + * + * @param flowType The Type of the Flow to bypass (e.g., Flow.Interview.MyFlow.class). + * @throws IllegalArgumentException if the type does not represent a Flow. + */ + public static void bypass(System.Type flowType) { + String flowName = getFlowNameFromType(flowType); + TriggerActionFlow.bypassedFlows.add(flowName); + } + + /** + * @description This method clears the bypass for the specified list of records. + * + * @param flowType The Type of the Flow to clear the bypass for (e.g., Flow.Interview.MyFlow.class). + * @throws IllegalArgumentException if the type does not represent a Flow. + */ + public static void clearBypass(System.Type flowType) { + String flowName = getFlowNameFromType(flowType); + TriggerActionFlow.bypassedFlows.remove(flowName); + } + + /** + * @description This method checks if the Flow is bypassed for the specified list of records. + * + * @param flowType The Type of the Flow to check the bypass for (e.g., Flow.Interview.MyFlow.class). + * @return `true` if the Flow is bypassed for the specified list of records, `false` otherwise. + * @throws IllegalArgumentException if the type does not represent a Flow. + */ + public static Boolean isBypassed(System.Type flowType) { + String flowName = getFlowNameFromType(flowType); + return TriggerActionFlow.bypassedFlows.contains(flowName); + } + /** * @description This method clears all bypasses for all Flows. */ @@ -66,6 +104,30 @@ public virtual inherited sharing class TriggerActionFlow implements TriggerActio TriggerActionFlow.bypassedFlows.clear(); } + /** + * @description This method extracts the flow name from a Type and validates that it represents a Flow. + * + * @param flowType The Type to extract the flow name from. + * @return The flow API name. + * @throws IllegalArgumentException if the type does not represent a Flow. + */ + private static String getFlowNameFromType(System.Type flowType) { + String typeName = nameExtractor.extractName(flowType); + if (!typeName.startsWith(FLOW_INTERVIEW_PREFIX)) { + throw new IllegalArgumentException( + 'Type must represent a Flow (e.g., Flow.Interview.MyFlow.class), but got: ' + + typeName + ); + } + // If the type name is exactly "Flow.Interview", it means the flow doesn't exist + if (typeName.equals(FLOW_INTERVIEW_PREFIX)) { + throw new IllegalArgumentException( + 'Flow does not exist. Please check that the flow API name is correct.' + ); + } + return typeName.replace(FLOW_INTERVIEW_PREFIX + '.', ''); + } + /** * @description This method validates the specified bypass type. * @@ -380,4 +442,11 @@ public virtual inherited sharing class TriggerActionFlow implements TriggerActio return action.invoke(); } } + + @TestVisible + private virtual class NameExtractor { + public virtual String extractName(System.Type flowType) { + return flowType.getName(); + } + } } diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls b/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls index b9d9a0f..d3e051e 100644 --- a/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls @@ -34,6 +34,10 @@ private class TriggerActionFlowTest { private static final String ONE_ERROR = 'There should be exactly one error'; private static final String SAMPLE_FLOW_NAME = 'TriggerActionFlowTest'; private static final String TEST = 'Test!'; + private static final String SAMPLE_FLOW_TYPE_NAME = + TriggerActionFlow.FLOW_INTERVIEW_PREFIX + + '.' + + SAMPLE_FLOW_NAME; private static Account myAccount = new Account( Name = MY_ACCOUNT, @@ -359,6 +363,114 @@ private class TriggerActionFlowTest { ); } + @IsTest + private static void bypassWithTypeShouldSucceed() { + TriggerActionFlow.nameExtractor = new FakeNameExtractor( + SAMPLE_FLOW_TYPE_NAME + ); + TriggerActionFlow.bypass(Flow.Interview.TriggerActionFlowTest.class); + + System.Assert.isTrue( + TriggerActionFlow.bypassedFlows.contains(SAMPLE_FLOW_NAME), + BYPASSES_SHOULD_BE_POPULATED_CORRECTLY + ); + } + + @IsTest + private static void clearBypassWithTypeShouldSucceed() { + TriggerActionFlow.nameExtractor = new FakeNameExtractor( + SAMPLE_FLOW_TYPE_NAME + ); + TriggerActionFlow.bypass(Flow.Interview.TriggerActionFlowTest.class); + TriggerActionFlow.clearBypass(Flow.Interview.TriggerActionFlowTest.class); + + System.Assert.isFalse( + TriggerActionFlow.bypassedFlows.contains(SAMPLE_FLOW_NAME), + BYPASSES_SHOULD_BE_POPULATED_CORRECTLY + ); + } + + @IsTest + private static void isBypassedWithTypeShouldSucceed() { + TriggerActionFlow.nameExtractor = new FakeNameExtractor( + SAMPLE_FLOW_TYPE_NAME + ); + Boolean isBypassed; + TriggerActionFlow.bypass(Flow.Interview.TriggerActionFlowTest.class); + + isBypassed = TriggerActionFlow.isBypassed( + Flow.Interview.TriggerActionFlowTest.class + ); + + System.Assert.isTrue(isBypassed, BYPASSES_SHOULD_BE_POPULATED_CORRECTLY); + } + + @IsTest + private static void bypassWithInvalidTypeShouldThrowException() { + try { + TriggerActionFlow.bypass(String.class); + } catch (Exception e) { + myException = e; + } + + System.Assert.areNotEqual(null, myException, EXCEPTION_SHOULD_BE_THROWN); + System.Assert.areEqual( + true, + myException.getMessage().contains('Type must represent a Flow'), + EXCEPTION_SHOULD_HAVE_THE_CORRECT_MESSAGE + ); + } + + @IsTest + private static void clearBypassWithInvalidTypeShouldThrowException() { + try { + TriggerActionFlow.clearBypass(String.class); + } catch (Exception e) { + myException = e; + } + + System.Assert.areNotEqual(null, myException, EXCEPTION_SHOULD_BE_THROWN); + System.Assert.areEqual( + true, + myException.getMessage().contains('Type must represent a Flow'), + EXCEPTION_SHOULD_HAVE_THE_CORRECT_MESSAGE + ); + } + + @IsTest + private static void isBypassedWithInvalidTypeShouldThrowException() { + try { + TriggerActionFlow.isBypassed(String.class); + } catch (Exception e) { + myException = e; + } + + System.Assert.areNotEqual(null, myException, EXCEPTION_SHOULD_BE_THROWN); + System.Assert.areEqual( + true, + myException.getMessage().contains('Type must represent a Flow'), + EXCEPTION_SHOULD_HAVE_THE_CORRECT_MESSAGE + ); + } + + @IsTest + private static void bypassWithNonExistentFlowShouldThrowException() { + try { + TriggerActionFlow.bypass( + Flow.Interview.SomeBogusClassWhichDoesNotExist.class + ); + } catch (Exception e) { + myException = e; + } + + System.Assert.areNotEqual(null, myException, EXCEPTION_SHOULD_BE_THROWN); + System.Assert.areEqual( + true, + myException.getMessage().contains('Flow does not exist'), + EXCEPTION_SHOULD_HAVE_THE_CORRECT_MESSAGE + ); + } + @IsTest private static void invokeFlowShouldReturnUnsuccessfulResponseWithBogusFlowName() { List results = new TriggerActionFlow.InvocableAction() @@ -454,4 +566,16 @@ private class TriggerActionFlowTest { ); } } + + private class FakeNameExtractor extends TriggerActionFlow.NameExtractor { + private String mockTypeName; + + public FakeNameExtractor(String mockTypeName) { + this.mockTypeName = mockTypeName; + } + + public override String extractName(System.Type flowType) { + return this.mockTypeName; + } + } } From b78e840e1b6d859ecb9d347c57b03fad7e6a92b3 Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Sun, 19 Oct 2025 21:48:40 -0500 Subject: [PATCH 4/9] Add compile-safe bypass methods for Flow and Trigger Action types in documentation --- .../MetadataTriggerHandler.md | 59 +++++++++++++++ .../TriggerActionFlow.md | 68 +++++++++++++++++ .../TriggerActionFlowChangeEvent.md | 74 +++++++++++++++++++ docs/trigger-actions-framework/TriggerBase.md | 59 +++++++++++++++ 4 files changed, 260 insertions(+) diff --git a/docs/trigger-actions-framework/MetadataTriggerHandler.md b/docs/trigger-actions-framework/MetadataTriggerHandler.md index 64b5bb3..6fdefc2 100644 --- a/docs/trigger-actions-framework/MetadataTriggerHandler.md +++ b/docs/trigger-actions-framework/MetadataTriggerHandler.md @@ -169,6 +169,65 @@ True if the Trigger Action is bypassed, false otherwise. --- +### `bypass(actionType)` + +Bypass the execution of a Trigger Action. + +#### Signature +```apex +public static void bypass(System.Type actionType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| actionType | System.Type | The Type of the Trigger Action to bypass. | + +#### Return Type +**void** + +--- + +### `clearBypass(actionType)` + +Clear the bypass for a Trigger Action. + +#### Signature +```apex +public static void clearBypass(System.Type actionType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| actionType | System.Type | The Type of the Trigger Action to clear the bypass for. | + +#### Return Type +**void** + +--- + +### `isBypassed(actionType)` + +Check if a Trigger Action is bypassed. + +#### Signature +```apex +public static Boolean isBypassed(System.Type actionType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| actionType | System.Type | The Type of the Trigger Action to check. | + +#### Return Type +**Boolean** + +True if the Trigger Action is bypassed, false otherwise. + +--- + ### `clearAllBypasses()` Clear all bypasses for Trigger Actions. diff --git a/docs/trigger-actions-framework/TriggerActionFlow.md b/docs/trigger-actions-framework/TriggerActionFlow.md index cc43fbf..f2f8eea 100644 --- a/docs/trigger-actions-framework/TriggerActionFlow.md +++ b/docs/trigger-actions-framework/TriggerActionFlow.md @@ -101,6 +101,74 @@ public static Boolean isBypassed(String flowName) --- +### `bypass(flowType)` + +This method bypasses the execution of the Flow for the specified list of records. + +#### Signature +```apex +public static void bypass(System.Type flowType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| flowType | System.Type | The Type of the Flow to bypass (e.g., Flow.Interview.MyFlow.class). | + +#### Return Type +**void** + +#### Throws +IllegalArgumentException: if the type does not represent a Flow. + +--- + +### `clearBypass(flowType)` + +This method clears the bypass for the specified list of records. + +#### Signature +```apex +public static void clearBypass(System.Type flowType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| flowType | System.Type | The Type of the Flow to clear the bypass for (e.g., Flow.Interview.MyFlow.class). | + +#### Return Type +**void** + +#### Throws +IllegalArgumentException: if the type does not represent a Flow. + +--- + +### `isBypassed(flowType)` + +This method checks if the Flow is bypassed for the specified list of records. + +#### Signature +```apex +public static Boolean isBypassed(System.Type flowType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| flowType | System.Type | The Type of the Flow to check the bypass for (e.g., Flow.Interview.MyFlow.class). | + +#### Return Type +**Boolean** + +,[object Object], if the Flow is bypassed for the specified list of records, ,[object Object], otherwise. + +#### Throws +IllegalArgumentException: if the type does not represent a Flow. + +--- + ### `clearAllBypasses()` This method clears all bypasses for all Flows. diff --git a/docs/trigger-actions-framework/TriggerActionFlowChangeEvent.md b/docs/trigger-actions-framework/TriggerActionFlowChangeEvent.md index ecc0bfa..b380d2a 100644 --- a/docs/trigger-actions-framework/TriggerActionFlowChangeEvent.md +++ b/docs/trigger-actions-framework/TriggerActionFlowChangeEvent.md @@ -111,6 +111,80 @@ public static Boolean isBypassed(String flowName) --- +### `bypass(flowType)` + +*Inherited* + +This method bypasses the execution of the Flow for the specified list of records. + +#### Signature +```apex +public static void bypass(System.Type flowType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| flowType | System.Type | The Type of the Flow to bypass (e.g., Flow.Interview.MyFlow.class). | + +#### Return Type +**void** + +#### Throws +IllegalArgumentException: if the type does not represent a Flow. + +--- + +### `clearBypass(flowType)` + +*Inherited* + +This method clears the bypass for the specified list of records. + +#### Signature +```apex +public static void clearBypass(System.Type flowType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| flowType | System.Type | The Type of the Flow to clear the bypass for (e.g., Flow.Interview.MyFlow.class). | + +#### Return Type +**void** + +#### Throws +IllegalArgumentException: if the type does not represent a Flow. + +--- + +### `isBypassed(flowType)` + +*Inherited* + +This method checks if the Flow is bypassed for the specified list of records. + +#### Signature +```apex +public static Boolean isBypassed(System.Type flowType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| flowType | System.Type | The Type of the Flow to check the bypass for (e.g., Flow.Interview.MyFlow.class). | + +#### Return Type +**Boolean** + +,[object Object], if the Flow is bypassed for the specified list of records, ,[object Object], otherwise. + +#### Throws +IllegalArgumentException: if the type does not represent a Flow. + +--- + ### `clearAllBypasses()` *Inherited* diff --git a/docs/trigger-actions-framework/TriggerBase.md b/docs/trigger-actions-framework/TriggerBase.md index 77efa76..96421ca 100644 --- a/docs/trigger-actions-framework/TriggerBase.md +++ b/docs/trigger-actions-framework/TriggerBase.md @@ -110,6 +110,65 @@ True if the object is bypassed, false otherwise. --- +### `bypass(sObjectType)` + +This method bypasses the execution of the specified object. + +#### Signature +```apex +public static void bypass(Schema.sObjectType sObjectType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| sObjectType | Schema.sObjectType | The sObjectType of the object to bypass. | + +#### Return Type +**void** + +--- + +### `clearBypass(sObjectType)` + +This method clears the bypass for the specified object. + +#### Signature +```apex +public static void clearBypass(Schema.sObjectType sObjectType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| sObjectType | Schema.sObjectType | The sObjectType of the object to clear the bypass for. | + +#### Return Type +**void** + +--- + +### `isBypassed(sObjectType)` + +This method checks if the specified object is bypassed. + +#### Signature +```apex +public static Boolean isBypassed(Schema.sObjectType sObjectType) +``` + +#### Parameters +| Name | Type | Description | +|------|------|-------------| +| sObjectType | Schema.sObjectType | The sObjectType of the object to check the bypass for. | + +#### Return Type +**Boolean** + +True if the object is bypassed, false otherwise. + +--- + ### `clearAllBypasses()` This method clears all bypasses. From f7c8caab67c805970218b55780ae22a3e0c6f775 Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Sun, 19 Oct 2025 21:53:32 -0500 Subject: [PATCH 5/9] Update README with compile-safe bypass methods for sObjects, Apex classes, and Flows, and add documentation generation instructions --- README.md | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 34634e9..074c52a 100644 --- a/README.md +++ b/README.md @@ -242,29 +242,35 @@ You can bypass all actions on an sObject as well as specific Apex or Flow action #### Bypass from Apex -To bypass from Apex, use the static `bypass(String name)` method in the `TriggerBase`, `MetadataTriggerHandler`, or `TriggerActionFlow` classes. +The framework provides compile-safe bypass methods that accept type references. + +**Bypass sObjects using Schema.sObjectType:** ```java public void updateAccountsNoTrigger(List accountsToUpdate) { - TriggerBase.bypass('Account'); + TriggerBase.bypass(Schema.Account.SObjectType); update accountsToUpdate; - TriggerBase.clearBypass('Account'); + TriggerBase.clearBypass(Schema.Account.SObjectType); } ``` +**Bypass Apex classes using System.Type:** + ```java public void insertOpportunitiesNoRules(List opportunitiesToInsert) { - MetadataTriggerHandler.bypass('TA_Opportunity_StageInsertRules'); + MetadataTriggerHandler.bypass(TA_Opportunity_StageInsertRules.class); insert opportunitiesToInsert; - MetadataTriggerHandler.clearBypass('TA_Opportunity_StageInsertRules'); + MetadataTriggerHandler.clearBypass(TA_Opportunity_StageInsertRules.class); } ``` +**Bypass Flows using Flow.Interview:** + ```java public void updateContactsNoFlow(List contactsToUpdate) { - TriggerActionFlow.bypass('Contact_Flow'); + TriggerActionFlow.bypass(Flow.Interview.Contact_Flow.class); update contactsToUpdate; - TriggerActionFlow.clearBypass('Contact_Flow'); + TriggerActionFlow.clearBypass(Flow.Interview.Contact_Flow.class); } ``` @@ -615,3 +621,19 @@ public void execute(FinalizerHandler.Context context) { } } ``` + +--- + +## Documentation Generation + +This project uses [ApexDocs](https://github.com/cparra/apexdocs) to generate documentation from Apex class comments. To generate the documentation: + +```bash +npx @cparra/apexdocs markdown \ + -s trigger-actions-framework \ + -t docs \ + -p public \ + -g docsify +``` + +The generated documentation will be available in the `docs/` directory and can be viewed using any static site generator or served directly. From 41a54d9208e6e4c05ab238f369979ba85e0ebe9f Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Mon, 20 Oct 2025 09:32:20 -0500 Subject: [PATCH 6/9] Update `TriggerActionFlow` with additional error handling and validation messages for flow type checks. --- .../default/classes/TriggerActionFlow.cls | 22 +++--- .../default/classes/TriggerActionFlowTest.cls | 70 +++++++++++++++++-- 2 files changed, 77 insertions(+), 15 deletions(-) diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls b/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls index a366001..42d93d8 100644 --- a/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls @@ -19,18 +19,24 @@ * @description This class implements the TriggerAction interface and provides a framework for * executing Flows before or after the insert, update, delete, or undelete of records. */ -@SuppressWarnings('PMD.CyclomaticComplexity') +@SuppressWarnings('PMD.CyclomaticComplexity, PMD.CognitiveComplexity') public virtual inherited sharing class TriggerActionFlow implements TriggerAction.BeforeInsert, TriggerAction.AfterInsert, TriggerAction.BeforeUpdate, TriggerAction.AfterUpdate, TriggerAction.BeforeDelete, TriggerAction.AfterDelete, TriggerAction.AfterUndelete { @TestVisible private static final String RECORD_VARIABLE_NOT_FOUND_ERROR = 'There must be a variable defined in this flow with api name of "record" and type of "record" that is marked as "available for output"'; @TestVisible private static final String FLOW_INTERVIEW_PREFIX = 'Flow.Interview'; @TestVisible + private static final String TYPE_MUST_BE_PROVIDED_ERROR = 'Type must be provided to get the flow name.'; + @TestVisible + private static final String TYPE_MUST_REPRESENT_FLOW_ERROR = 'Type must represent a Flow (e.g., Flow.Interview.MyFlow.class), but got: '; + @TestVisible + private static final String FLOW_DOES_NOT_EXIST_ERROR = 'Flow does not exist. Please check that the flow API name is correct.'; + @TestVisible private static Set bypassedFlows = new Set(); @TestVisible private static InvocableAction invocableAction = new InvocableAction(); @TestVisible - private static NameExtractor nameExtractor = new NameExtractor(); + private static TriggerActionFlow.NameExtractor nameExtractor = new TriggerActionFlow.NameExtractor(); public String flowName; public Boolean allowRecursion; @@ -112,18 +118,18 @@ public virtual inherited sharing class TriggerActionFlow implements TriggerActio * @throws IllegalArgumentException if the type does not represent a Flow. */ private static String getFlowNameFromType(System.Type flowType) { + if (flowType == null) { + throw new IllegalArgumentException(TYPE_MUST_BE_PROVIDED_ERROR); + } String typeName = nameExtractor.extractName(flowType); - if (!typeName.startsWith(FLOW_INTERVIEW_PREFIX)) { + if (typeName == null || !typeName.startsWith(FLOW_INTERVIEW_PREFIX)) { throw new IllegalArgumentException( - 'Type must represent a Flow (e.g., Flow.Interview.MyFlow.class), but got: ' + - typeName + TYPE_MUST_REPRESENT_FLOW_ERROR + typeName ); } // If the type name is exactly "Flow.Interview", it means the flow doesn't exist if (typeName.equals(FLOW_INTERVIEW_PREFIX)) { - throw new IllegalArgumentException( - 'Flow does not exist. Please check that the flow API name is correct.' - ); + throw new IllegalArgumentException(FLOW_DOES_NOT_EXIST_ERROR); } return typeName.replace(FLOW_INTERVIEW_PREFIX + '.', ''); } diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls b/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls index d3e051e..422bde2 100644 --- a/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls @@ -14,7 +14,9 @@ limitations under the License. */ -@SuppressWarnings('PMD.ApexDoc, PMD.ApexUnitTestClassShouldHaveRunAs') +@SuppressWarnings( + 'PMD.ApexDoc, PMD.ApexUnitTestClassShouldHaveRunAs, PMD.CyclomaticComplexity' +) @IsTest(isParallel=true) private class TriggerActionFlowTest { private static final String BOGUS = 'Bogus'; @@ -89,8 +91,7 @@ private class TriggerActionFlowTest { myException = e; } - System.Assert.areEqual( - null, + System.Assert.isNull( myException, 'There should be no exception thrown and the System should do nothing when the flow is bypassed' ); @@ -416,7 +417,8 @@ private class TriggerActionFlowTest { System.Assert.areNotEqual(null, myException, EXCEPTION_SHOULD_BE_THROWN); System.Assert.areEqual( true, - myException.getMessage().contains('Type must represent a Flow'), + myException.getMessage() + .contains(TriggerActionFlow.TYPE_MUST_REPRESENT_FLOW_ERROR), EXCEPTION_SHOULD_HAVE_THE_CORRECT_MESSAGE ); } @@ -432,7 +434,8 @@ private class TriggerActionFlowTest { System.Assert.areNotEqual(null, myException, EXCEPTION_SHOULD_BE_THROWN); System.Assert.areEqual( true, - myException.getMessage().contains('Type must represent a Flow'), + myException.getMessage() + .contains(TriggerActionFlow.TYPE_MUST_REPRESENT_FLOW_ERROR), EXCEPTION_SHOULD_HAVE_THE_CORRECT_MESSAGE ); } @@ -448,7 +451,8 @@ private class TriggerActionFlowTest { System.Assert.areNotEqual(null, myException, EXCEPTION_SHOULD_BE_THROWN); System.Assert.areEqual( true, - myException.getMessage().contains('Type must represent a Flow'), + myException.getMessage() + .contains(TriggerActionFlow.TYPE_MUST_REPRESENT_FLOW_ERROR), EXCEPTION_SHOULD_HAVE_THE_CORRECT_MESSAGE ); } @@ -466,7 +470,59 @@ private class TriggerActionFlowTest { System.Assert.areNotEqual(null, myException, EXCEPTION_SHOULD_BE_THROWN); System.Assert.areEqual( true, - myException.getMessage().contains('Flow does not exist'), + myException.getMessage() + .contains(TriggerActionFlow.FLOW_DOES_NOT_EXIST_ERROR), + EXCEPTION_SHOULD_HAVE_THE_CORRECT_MESSAGE + ); + } + + @IsTest + private static void bypassWithNullTypeShouldThrowException() { + try { + TriggerActionFlow.bypass((System.Type) null); + } catch (Exception e) { + myException = e; + } + + System.Assert.areNotEqual(null, myException, EXCEPTION_SHOULD_BE_THROWN); + System.Assert.areEqual( + true, + myException.getMessage() + .contains(TriggerActionFlow.TYPE_MUST_BE_PROVIDED_ERROR), + EXCEPTION_SHOULD_HAVE_THE_CORRECT_MESSAGE + ); + } + + @IsTest + private static void clearBypassWithNullTypeShouldThrowException() { + try { + TriggerActionFlow.clearBypass((System.Type) null); + } catch (Exception e) { + myException = e; + } + + System.Assert.areNotEqual(null, myException, EXCEPTION_SHOULD_BE_THROWN); + System.Assert.areEqual( + true, + myException.getMessage() + .contains(TriggerActionFlow.TYPE_MUST_BE_PROVIDED_ERROR), + EXCEPTION_SHOULD_HAVE_THE_CORRECT_MESSAGE + ); + } + + @IsTest + private static void isBypassedWithNullTypeShouldThrowException() { + try { + TriggerActionFlow.isBypassed((System.Type) null); + } catch (Exception e) { + myException = e; + } + + System.Assert.areNotEqual(null, myException, EXCEPTION_SHOULD_BE_THROWN); + System.Assert.areEqual( + true, + myException.getMessage() + .contains(TriggerActionFlow.TYPE_MUST_BE_PROVIDED_ERROR), EXCEPTION_SHOULD_HAVE_THE_CORRECT_MESSAGE ); } From 9f777d62d04b8ef1275e2277d2f541746e471344 Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Mon, 20 Oct 2025 09:46:55 -0500 Subject: [PATCH 7/9] Fix PMD warnings --- .../main/default/classes/MetadataTriggerHandler.cls | 2 +- .../default/classes/MetadataTriggerHandlerTest.cls | 2 +- .../main/default/classes/TriggerActionFlow.cls | 3 ++- .../main/default/classes/TriggerActionFlowTest.cls | 3 ++- .../main/default/classes/TriggerBase.cls | 12 ++++++++---- .../main/default/classes/TriggerBaseTest.cls | 4 +++- 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls b/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls index 10a8ef1..446fe17 100644 --- a/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls +++ b/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls @@ -76,7 +76,7 @@ * */ @SuppressWarnings( - 'PMD.CognitiveComplexity, PMD.StdCyclomaticComplexity, PMD.CyclomaticComplexity' + 'PMD.CognitiveComplexity,PMD.StdCyclomaticComplexity,PMD.CyclomaticComplexity' ) public inherited sharing class MetadataTriggerHandler extends TriggerBase implements TriggerAction.BeforeInsert, TriggerAction.AfterInsert, TriggerAction.BeforeUpdate, TriggerAction.AfterUpdate, TriggerAction.BeforeDelete, TriggerAction.AfterDelete, TriggerAction.AfterUndelete { private static final String DOUBLE_UNDERSCORE = '__'; diff --git a/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls b/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls index 9d0861d..ecbf204 100644 --- a/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls +++ b/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls @@ -15,7 +15,7 @@ */ @SuppressWarnings( - 'PMD.ApexDoc, PMD.CyclomaticComplexity, PMD.ApexUnitTestClassShouldHaveRunAs' + 'PMD.ApexDoc,PMD.CyclomaticComplexity,PMD.ApexUnitTestClassShouldHaveRunAs' ) @IsTest(isParallel=true) private class MetadataTriggerHandlerTest { diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls b/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls index 42d93d8..51503d3 100644 --- a/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls @@ -19,7 +19,7 @@ * @description This class implements the TriggerAction interface and provides a framework for * executing Flows before or after the insert, update, delete, or undelete of records. */ -@SuppressWarnings('PMD.CyclomaticComplexity, PMD.CognitiveComplexity') +@SuppressWarnings('PMD.CyclomaticComplexity,PMD.CognitiveComplexity') public virtual inherited sharing class TriggerActionFlow implements TriggerAction.BeforeInsert, TriggerAction.AfterInsert, TriggerAction.BeforeUpdate, TriggerAction.AfterUpdate, TriggerAction.BeforeDelete, TriggerAction.AfterDelete, TriggerAction.AfterUndelete { @TestVisible private static final String RECORD_VARIABLE_NOT_FOUND_ERROR = 'There must be a variable defined in this flow with api name of "record" and type of "record" that is marked as "available for output"'; @@ -449,6 +449,7 @@ public virtual inherited sharing class TriggerActionFlow implements TriggerActio } } + @SuppressWarnings('PMD.ApexDoc') @TestVisible private virtual class NameExtractor { public virtual String extractName(System.Type flowType) { diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls b/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls index 422bde2..5d49d80 100644 --- a/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlowTest.cls @@ -15,7 +15,7 @@ */ @SuppressWarnings( - 'PMD.ApexDoc, PMD.ApexUnitTestClassShouldHaveRunAs, PMD.CyclomaticComplexity' + 'PMD.ApexDoc,PMD.ApexUnitTestClassShouldHaveRunAs,PMD.CyclomaticComplexity' ) @IsTest(isParallel=true) private class TriggerActionFlowTest { @@ -568,6 +568,7 @@ private class TriggerActionFlowTest { this.outputParameters = outputParameters; return this; } + @SuppressWarnings('PMD.AvoidBooleanMethodParameters') public InvocableActionResultBuilder setSuccess(Boolean success) { this.success = success; return this; diff --git a/trigger-actions-framework/main/default/classes/TriggerBase.cls b/trigger-actions-framework/main/default/classes/TriggerBase.cls index c45a231..d588e09 100644 --- a/trigger-actions-framework/main/default/classes/TriggerBase.cls +++ b/trigger-actions-framework/main/default/classes/TriggerBase.cls @@ -33,7 +33,7 @@ * The `TriggerAction` interface defines the methods that should be implemented by trigger actions. */ @SuppressWarnings( - 'PMD.StdCyclomaticComplexity, PMD.CyclomaticComplexity, PMD.CognitiveComplexity, PMD.PropertyNamingConventions' + 'PMD.StdCyclomaticComplexity,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.PropertyNamingConventions' ) public inherited sharing virtual class TriggerBase { @TestVisible @@ -142,7 +142,9 @@ public inherited sharing virtual class TriggerBase { * @param sObjectType The sObjectType of the object to bypass. */ public static void bypass(Schema.sObjectType sObjectType) { - TriggerBase.bypassedObjects.add(sObjectType.getDescribe().getName()); + TriggerBase.bypassedObjects.add( + sObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName() + ); } /** @@ -151,7 +153,9 @@ public inherited sharing virtual class TriggerBase { * @param sObjectType The sObjectType of the object to clear the bypass for. */ public static void clearBypass(Schema.sObjectType sObjectType) { - TriggerBase.bypassedObjects.remove(sObjectType.getDescribe().getName()); + TriggerBase.bypassedObjects.remove( + sObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName() + ); } /** @@ -162,7 +166,7 @@ public inherited sharing virtual class TriggerBase { */ public static Boolean isBypassed(Schema.sObjectType sObjectType) { return TriggerBase.bypassedObjects.contains( - sObjectType.getDescribe().getName() + sObjectType.getDescribe(SObjectDescribeOptions.DEFERRED).getName() ); } diff --git a/trigger-actions-framework/main/default/classes/TriggerBaseTest.cls b/trigger-actions-framework/main/default/classes/TriggerBaseTest.cls index 885b351..695b582 100644 --- a/trigger-actions-framework/main/default/classes/TriggerBaseTest.cls +++ b/trigger-actions-framework/main/default/classes/TriggerBaseTest.cls @@ -14,7 +14,9 @@ limitations under the License. */ -@SuppressWarnings('PMD.ApexDoc, PMD.ApexUnitTestClassShouldHaveRunAs') +@SuppressWarnings( + 'PMD.ApexDoc,PMD.CyclomaticComplexity,PMD.ApexUnitTestClassShouldHaveRunAs' +) @IsTest(isParallel=true) private class TriggerBaseTest { private static final String ACCOUNT = 'Account'; From e3ad1d43ff3207535f1273b57912d1fe97fd934c Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Mon, 20 Oct 2025 10:15:14 -0500 Subject: [PATCH 8/9] Update CI workflow to use latest versions of checkout and setup-node actions --- .github/workflows/CI.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 66b92a7..11f66ab 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -11,14 +11,13 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v5 with: ref: ${{ github.ref }} fetch-depth: 0 - - uses: actions/setup-node@v1 + - uses: actions/setup-node@v6 with: - node-version: ">=14" check-latest: true - name: Install Salesforce CLI + Scanner From 6a4f9e1380ef2d899e4a39a3e0de84cafb41699e Mon Sep 17 00:00:00 2001 From: Mitch Spano Date: Mon, 20 Oct 2025 10:23:13 -0500 Subject: [PATCH 9/9] Update README with new package installation links and add new package version --- .github/workflows/Package.yml | 4 ++-- README.md | 4 ++-- sfdx-project.json | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/Package.yml b/.github/workflows/Package.yml index 98da286..a158741 100644 --- a/.github/workflows/Package.yml +++ b/.github/workflows/Package.yml @@ -14,9 +14,9 @@ jobs: packageId: ${{ steps.create.outputs.packageId }} steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: ">=20" diff --git a/README.md b/README.md index 074c52a..17887d1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Apex Trigger Actions Framework -#### [Unlocked Package Installation (Production)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tKY000000QQ0RYAW) +#### [Unlocked Package Installation (Production)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tKY000000R0yHYAS) -#### [Unlocked Package Installation (Sandbox)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tKY000000QQ0RYAW) +#### [Unlocked Package Installation (Sandbox)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tKY000000R0yHYAS) --- diff --git a/sfdx-project.json b/sfdx-project.json index 5831e6a..05dfd3f 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -44,6 +44,7 @@ "Trigger Actions Framework@0.3.1-6": "04tKY000000PdZ9YAK", "Trigger Actions Framework@0.3.1-7": "04tKY000000PdZJYA0", "Trigger Actions Framework@0.3.2": "04tKY000000PdZOYA0", - "Trigger Actions Framework@0.3.3": "04tKY000000QQ0RYAW" + "Trigger Actions Framework@0.3.3": "04tKY000000QQ0RYAW", + "Trigger Actions Framework@0.3.4-1": "04tKY000000R0yHYAS" } }