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 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 34634e9..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) --- @@ -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. 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. 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" } } diff --git a/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls b/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls index a4fa549..446fe17 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..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 { @@ -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( diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls b/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls index f4fdfce..51503d3 100644 --- a/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls @@ -19,14 +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 TriggerActionFlow.NameExtractor nameExtractor = new TriggerActionFlow.NameExtractor(); public String flowName; public Boolean allowRecursion; @@ -59,6 +69,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 +110,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) { + if (flowType == null) { + throw new IllegalArgumentException(TYPE_MUST_BE_PROVIDED_ERROR); + } + String typeName = nameExtractor.extractName(flowType); + if (typeName == null || !typeName.startsWith(FLOW_INTERVIEW_PREFIX)) { + throw new IllegalArgumentException( + 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_ERROR); + } + return typeName.replace(FLOW_INTERVIEW_PREFIX + '.', ''); + } + /** * @description This method validates the specified bypass type. * @@ -380,4 +448,12 @@ public virtual inherited sharing class TriggerActionFlow implements TriggerActio return action.invoke(); } } + + @SuppressWarnings('PMD.ApexDoc') + @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..5d49d80 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'; @@ -34,6 +36,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, @@ -85,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' ); @@ -359,6 +364,169 @@ 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(TriggerActionFlow.TYPE_MUST_REPRESENT_FLOW_ERROR), + 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(TriggerActionFlow.TYPE_MUST_REPRESENT_FLOW_ERROR), + 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(TriggerActionFlow.TYPE_MUST_REPRESENT_FLOW_ERROR), + 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(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 + ); + } + @IsTest private static void invokeFlowShouldReturnUnsuccessfulResponseWithBogusFlowName() { List results = new TriggerActionFlow.InvocableAction() @@ -400,6 +568,7 @@ private class TriggerActionFlowTest { this.outputParameters = outputParameters; return this; } + @SuppressWarnings('PMD.AvoidBooleanMethodParameters') public InvocableActionResultBuilder setSuccess(Boolean success) { this.success = success; return this; @@ -454,4 +623,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; + } + } } diff --git a/trigger-actions-framework/main/default/classes/TriggerBase.cls b/trigger-actions-framework/main/default/classes/TriggerBase.cls index 61389ec..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 @@ -136,6 +136,40 @@ 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(SObjectDescribeOptions.DEFERRED).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(SObjectDescribeOptions.DEFERRED).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(SObjectDescribeOptions.DEFERRED).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..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'; @@ -295,6 +297,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();