diff --git a/README.md b/README.md index 577761b..27aed56 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Deploy to Salesforce -#### [Unlocked Package Installation (Production)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t3h000004juLaAAI) +#### [Unlocked Package Installation (Production)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t3h000004juLuAAI) -#### [Unlocked Package Installation (Sandbox)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t3h000004juLaAAI) +#### [Unlocked Package Installation (Sandbox)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t3h000004juLuAAI) --- @@ -32,15 +32,15 @@ The related lists on the `SObject_Trigger_Setting__mdt` record provide a consoli The Trigger Actions Framework conforms strongly to the [Open–closed principle](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle) and the [Single-responsibility principle](https://en.wikipedia.org/wiki/Single-responsibility_principle). To add or modify trigger logic in our Salesforce org, we won't need to keep modifying the body of a TriggerHandler class; we can create a class or a flow with responsibility scoped to the automation we are trying to build and configure these actions to run in a specified order within a given trigger context. -The work is performed in the `MetadataTriggerHandler` class which implements the the [Strategy Pattern](https://en.wikipedia.org/wiki/Strategy_pattern) by fetching all Trigger Action metadata that is configured in the org for the given trigger context and uses [reflection](https://en.wikipedia.org/wiki/Reflective_programming) to dynamically instantiate an object which implements a `TriggerAction` interface, then casts the object to the appropriate interface as specified in the metadata and calls the respective context methods in the order specified. +The work is performed in the `MetadataTriggerHandler` class which implements the [Strategy Pattern](https://en.wikipedia.org/wiki/Strategy_pattern) by fetching all Trigger Action metadata that is configured in the org for the given trigger context and uses [reflection](https://en.wikipedia.org/wiki/Reflective_programming) to dynamically instantiate an object that implements a `TriggerAction`` interface, then casts the object to the appropriate interface as specified in the metadata and calls the respective context methods in the order specified. Note that if an Apex class is specified in metadata and it does not exist or does not implement the correct interface, a runtime error will occur. --- -### Enabling on an SObject +### Enabling for an SObject -To get started, call the the `MetadataTriggerHandler` class within the body of the trigger of the sObject: +To get started, call the `MetadataTriggerHandler` class within the body of the trigger of the sObject: ```java trigger OpportunityTrigger on Opportunity ( @@ -100,13 +100,28 @@ To make your flows usable, they must be auto-launched flows and you need to crea | Variable Name | Variable Type | Available for Input | Available for Output | Description | Available Contexts | | ------------- | ------------- | ------------------- | -------------------- | -------------------------------------------------- | ------------------------ | -| record | record | yes | yes | the new version of the record in the DML operation | insert, update, undelete | -| recordPrior | record | yes | no | the old version of the record in the DML operation | update, delete | +| `record` | record | yes | yes | the new version of the record in the DML operation | insert, update, undelete | +| `recordPrior` | record | yes | no | the old version of the record in the DML operation | update, delete | -To enable this flow, simply insert a trigger action record with `Apex_Class_Name__` equal to `TriggerActionFlow` and set the `Flow_Name__c` field with the API name of the flow itself. You can select the `Allow_Flow_Recursion__c` checkbox to allow flows to run recursively (advanced). +To enable this flow, simply insert a trigger action record with `Apex_Class_Name__c` equal to `TriggerActionFlow` and set the `Flow_Name__c` field with the API name of the flow itself. You can select the `Allow_Flow_Recursion__c` checkbox to allow flows to run recursively (advanced). ![Flow Trigger Action](images/flowTriggerAction.png) +### Flow Actions for Change Data Capture Events + +Trigger Action Flows can also be used to process Change Data Capture events, but there are two minor modifications necessary: + +#### Adjust the Flow Variables + +| Variable Name | Variable Type | Available for Input | Available for Output | Description | +| ------------- | -------------------------------------- | ------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `record` | record | yes | no | the changeEvent object | +| `header` | `FlowChangeEventHeader` (Apex Defined) | yes | no | a flow-accessible version of the [`ChangeEventHeader` object](https://developer.salesforce.com/docs/atlas.en-us.change_data_capture.meta/change_data_capture/cdc_event_fields_header.htm) | + +#### Adjust the `Trigger_Action__mdt` Record + +Create a trigger action record with `Apex_Class_Name__c` equal to `TriggerActionFlowChangeEvent` (instead of `TriggerActionFlow`) and set the `Flow_Name__c` field with the API name of the flow itself. + --- ## Compatibility with sObjects from Installed Packages @@ -220,7 +235,7 @@ Developers can enter the API name of a permission in the `Bypass_Permission__c` #### Required Permission -Developers can enter the API name of a permission in the `Required_Permission__c` field. If this field has a value, then the trigger/action will only execute if the running user has the custom permission identified. This can be allow for new functionality to be released to a subset of users. +Developers can enter the API name of a permission in the `Required_Permission__c` field. If this field has a value, then the trigger/action will only execute if the running user has the custom permission identified. This can allow for new functionality to be released to a subset of users. --- @@ -288,7 +303,7 @@ public class TA_Opportunity_StandardizeName implements TriggerAction.BeforeInser ``` **Note:** -In the example above, the top level class is the implementation of the Singleton pattern, but we also define an inner class called `Service` which is the actual Trigger Action itself. When using this pattern for query management, the `Apex_Class_Name__c` value on the `Trigger_Action__mdt` row would be `TA_Opportunity_Queries.Service`. +In the example above, the top-level class is the implementation of the Singleton pattern, but we also define an inner class called `Service` which is the actual Trigger Action itself. When using this pattern for query management, the `Apex_Class_Name__c` value on the `Trigger_Action__mdt` row would be `TA_Opportunity_Queries.Service`. ![Query Setup](images/queriesService.png) @@ -389,11 +404,11 @@ The Apex Trigger Actions Framework now has support for a novel feature not found A DML finalizer is a piece of code that executes **exactly one time** at the very end of a DML operation. -This is notably different than the final action within a given trigger context. The final configured action can be executed multiple times in case of cascading DML operations within trigger logic or when more than 200 records are included in the original DML operation. This can lead to challenges capturing logs or invoking asynchronous logic. +This is notably different than the final action within a given trigger context. The final configured action can be executed multiple times in case of cascading DML operations within trigger logic or when more than 200 records are included in the original DML operation. This can lead to challenges when capturing logs or invoking asynchronous logic. DML finalizers can be very helpful for things such as _enqueuing a queuable operation_ or _inserting a collection of gathered logs_. -Finalizers within the Apex Trigger Actions Framework operate using many of the same mechanisms. First define a class which implements the `TriggerAction.DmlFinalizer` interface. Include public static variables/methods so that the trigger actions executing can register objects to be processed during the finalizer's execution. +Finalizers within the Apex Trigger Actions Framework operate using many of the same mechanisms. First, define a class that implements the `TriggerAction.DmlFinalizer` interface. Include public static variables/methods so that the trigger actions executing can register objects to be processed during the finalizer's execution. ```java public with sharing class OpportunityCategoryCalculator implements Queueable, TriggerAction.DmlFinalizer { @@ -471,13 +486,13 @@ The `FinalizerHandler.Context` object specified in the `TriggerAction.DmlFinaliz #### Universal Adoption -To use a DML Finalizer, the Apex Trigger Actions Framework must be enabled on every SObject which supports triggers which will have a DML operation on it during a transaction, and enabled in all trigger contexts on those sObjects. If DML is performed on an SObject that has a trigger which does not use the framework, the system will not be able to detect when to finalize the DML operation. +To use a DML Finalizer, the Apex Trigger Actions Framework must be enabled on every SObject that supports triggers and will have a DML operation on it during a transaction, and enabled in all trigger contexts on those sObjects. If DML is performed on an SObject that has a trigger that does not use the framework, the system will not be able to detect when to finalize the DML operation. #### Offsetting the Number of DML Rows -Detecting when to finalize the operation requires knowledge of the total number of records passed to the DML operation. Unfortunately, there is no bulletproof way of how to do this currently in Apex; the best thing we can do is to rely on `Limits.getDmlRows()` to infer the number of records passed to the DML operation. +Detecting when to finalize the operation requires knowledge of the total number of records passed to the DML operation. Unfortunately, there is no bulletproof way to do this currently in Apex; the best thing we can do is to rely on `Limits.getDmlRows()` to infer the number of records passed to the DML operation. -This works in most cases, but certain operations such as setting a `System.Savepoint` consume a DML row, and there are certain sObjects where triggers are not supported like `CaseTeamMember` which can throw off the counts and remove our ability to detect when to finalize. In order to avoid this problem, use the `TriggerBase.offsetExistingDmlRows()` method before calling the first DML operation within your Apex. +This works in most cases, but certain operations (such as setting a `System.Savepoint`) consume a DML row, and there are certain sObjects where triggers are not supported like `CaseTeamMember` which can throw off the counts and remove our ability to detect when to finalize. In order to avoid this problem, use the `TriggerBase.offsetExistingDmlRows()` method before calling the first DML operation within your Apex. ```java Savepoint sp = Database.setSavepoint(); // adds to Limits.getDmlRows() diff --git a/sfdx-project.json b/sfdx-project.json index fdf0855..9a005f4 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -1,33 +1,34 @@ { - "packageDirectories": [ - { - "path": "trigger-actions-framework", - "default": true, - "package": "Trigger Actions Framework", - "versionName": "Version 0.2", - "versionNumber": "0.2.1.NEXT" - } - ], - "namespace": "", - "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "56.0", - "packageAliases": { - "Trigger Actions Framework": "0Ho3h0000008Om4CAE", - "Trigger Actions Framework@0.1.0-1": "04t3h000004VaHaAAK", - "Trigger Actions Framework@0.1.2": "04t3h000004VaIdAAK", - "Trigger Actions Framework@0.1.3": "04t3h000004VaInAAK", - "Trigger Actions Framework@0.1.4-0": "04t3h000004VaJWAA0", - "Trigger Actions Framework@0.1.4-1": "04t3h000004VaJbAAK", - "Trigger Actions Framework@0.1.5-0": "04t3h000004VaJqAAK", - "Trigger Actions Framework@0.1.6": "04t3h000004VaLDAA0", - "Trigger Actions Framework@0.1.7": "04t3h000004VaLIAA0", - "Trigger Actions Framework@0.1.8": "04t3h000004VaLNAA0", - "Trigger Actions Framework@0.1.9": "04t3h000004VaLSAA0", - "Trigger Actions Framework@0.2.0": "04t3h000004VaLmAAK", - "Trigger Actions Framework@0.2.1": "04t3h000004VaVFAA0", - "Trigger Actions Framework@0.2.2-1": "04t3h000004OYREAA4", - "Trigger Actions Framework@0.2.3-1": "04t3h000004OYTKAA4", - "Trigger Actions Framework@0.2.5-1": "04t3h000004OYUDAA4", - "Trigger Actions Framework@0.2.6-1": "04t3h000004juLaAAI" - } -} \ No newline at end of file + "packageDirectories": [ + { + "path": "trigger-actions-framework", + "default": true, + "package": "Trigger Actions Framework", + "versionName": "Version 0.2", + "versionNumber": "0.2.8.NEXT" + } + ], + "namespace": "", + "sfdcLoginUrl": "https://login.salesforce.com", + "sourceApiVersion": "59.0", + "packageAliases": { + "Trigger Actions Framework": "0Ho3h0000008Om4CAE", + "Trigger Actions Framework@0.1.0-1": "04t3h000004VaHaAAK", + "Trigger Actions Framework@0.1.2": "04t3h000004VaIdAAK", + "Trigger Actions Framework@0.1.3": "04t3h000004VaInAAK", + "Trigger Actions Framework@0.1.4-0": "04t3h000004VaJWAA0", + "Trigger Actions Framework@0.1.4-1": "04t3h000004VaJbAAK", + "Trigger Actions Framework@0.1.5-0": "04t3h000004VaJqAAK", + "Trigger Actions Framework@0.1.6": "04t3h000004VaLDAA0", + "Trigger Actions Framework@0.1.7": "04t3h000004VaLIAA0", + "Trigger Actions Framework@0.1.8": "04t3h000004VaLNAA0", + "Trigger Actions Framework@0.1.9": "04t3h000004VaLSAA0", + "Trigger Actions Framework@0.2.0": "04t3h000004VaLmAAK", + "Trigger Actions Framework@0.2.1": "04t3h000004VaVFAA0", + "Trigger Actions Framework@0.2.2-1": "04t3h000004OYREAA4", + "Trigger Actions Framework@0.2.3-1": "04t3h000004OYTKAA4", + "Trigger Actions Framework@0.2.5-1": "04t3h000004OYUDAA4", + "Trigger Actions Framework@0.2.6-1": "04t3h000004juLaAAI", + "Trigger Actions Framework@0.2.8": "04t3h000004juLuAAI" + } +} diff --git a/trigger-actions-framework/main/default/classes/FlowChangeEventHeader.cls b/trigger-actions-framework/main/default/classes/FlowChangeEventHeader.cls new file mode 100644 index 0000000..34f48b0 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/FlowChangeEventHeader.cls @@ -0,0 +1,92 @@ +/* + Copyright 2023 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +@SuppressWarnings('PMD.ApexDoc') +public with sharing class FlowChangeEventHeader { + @InvocableVariable + @AuraEnabled + public String entityName; + @InvocableVariable + @AuraEnabled + public List recordIds; + @InvocableVariable + @AuraEnabled + public String changeType; + @InvocableVariable + @AuraEnabled + public String changeOrigin; + @InvocableVariable + @AuraEnabled + public String transactionKey; + @InvocableVariable + @AuraEnabled + public Integer sequenceNumber; + @InvocableVariable + @AuraEnabled + public Long commitTimestamp; + @InvocableVariable + @AuraEnabled + public String commitUser; + @InvocableVariable + @AuraEnabled + public Long commitNumber; + @InvocableVariable + @AuraEnabled + public List nulledFields; + @InvocableVariable + @AuraEnabled + public List diffFields; + @InvocableVariable + @AuraEnabled + public List changedFields; + + public FlowChangeEventHeader(EventBus.ChangeEventHeader header) { + this.entityName = header.entityName; + this.recordIds = header.recordIds; + this.changeType = header.changeType; + this.changeOrigin = header.changeOrigin; + this.transactionKey = header.transactionKey; + this.sequenceNumber = header.sequenceNumber; + this.commitTimestamp = header.commitTimestamp; + this.commitUser = header.commitUser; + this.commitNumber = header.commitNumber; + this.nulledFields = header.nulledFields; + this.diffFields = header.diffFields; + this.changedFields = header.changedFields; + } + + public Boolean equals(Object obj) { + if (obj != null && obj instanceof FlowChangeEventHeader) { + FlowChangeEventHeader other = (FlowChangeEventHeader) obj; + return this.entityName == other.entityName && + this.recordIds == other.recordIds && + this.changeType == other.changeType && + this.changeOrigin == other.changeOrigin && + this.transactionKey == other.transactionKey && + this.sequenceNumber == other.sequenceNumber && + this.commitTimestamp == other.commitTimestamp && + this.commitUser == other.commitUser && + this.commitNumber == other.commitNumber && + this.nulledFields == other.nulledFields && + this.diffFields == other.diffFields && + this.changedFields == other.changedFields; + } + return false; + } + + public Integer hashCode() { + return JSON.serialize(this).hashCode(); + } +} diff --git a/trigger-actions-framework/main/default/classes/FlowChangeEventHeader.cls-meta.xml b/trigger-actions-framework/main/default/classes/FlowChangeEventHeader.cls-meta.xml new file mode 100644 index 0000000..642d054 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/FlowChangeEventHeader.cls-meta.xml @@ -0,0 +1,5 @@ + + + 58.0 + Active + \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/FlowChangeEventHeaderTest.cls b/trigger-actions-framework/main/default/classes/FlowChangeEventHeaderTest.cls new file mode 100644 index 0000000..aca79b9 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/FlowChangeEventHeaderTest.cls @@ -0,0 +1,57 @@ +/* + Copyright 2023 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +@IsTest(isParallel=true) +@SuppressWarnings('PMD.ApexDoc') +private with sharing class FlowChangeEventHeaderTest { + private static FlowChangeEventHeader header = new FlowChangeEventHeader( + new EventBus.ChangeEventHeader() + ); + + @IsTest + private static void shouldBeAbleToConstruct() { + Assert.isNotNull(header, 'Unable to construct a FlowChangeEventHeader'); + } + + @IsTest + private static void shouldBeAbleToGenerateHashCode() { + Assert.isNotNull(header.hashCode(), 'Hash code was not generated'); + } + + @IsTest + private static void shouldBeAbleToCompare() { + FlowChangeEventHeader other = new FlowChangeEventHeader( + new EventBus.ChangeEventHeader() + ); + other.changeType = 'CREATE'; + + Assert.areEqual( + new FlowChangeEventHeader(new EventBus.ChangeEventHeader()), + header, + 'Unable to detect identical FlowChangeEventHeader objects' + ); + Assert.areNotEqual( + header, + other, + 'Unable to detect different FlowChangeEventHeader objects' + ); + Assert.areNotEqual( + header, + null, + 'Unable to detect difference between FlowChangeEventHeader and null' + ); + } +} diff --git a/trigger-actions-framework/main/default/classes/FlowChangeEventHeaderTest.cls-meta.xml b/trigger-actions-framework/main/default/classes/FlowChangeEventHeaderTest.cls-meta.xml new file mode 100644 index 0000000..642d054 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/FlowChangeEventHeaderTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 58.0 + Active + \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls b/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls index 7588092..678ff43 100644 --- a/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlow.cls @@ -15,7 +15,7 @@ */ @SuppressWarnings('PMD.ApexDoc, PMD.CyclomaticComplexity') -public inherited sharing class TriggerActionFlow implements TriggerAction.BeforeInsert, TriggerAction.AfterInsert, TriggerAction.BeforeUpdate, TriggerAction.AfterUpdate, TriggerAction.BeforeDelete, TriggerAction.AfterDelete, TriggerAction.AfterUndelete { +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 @@ -229,7 +229,7 @@ public inherited sharing class TriggerActionFlow implements TriggerAction.Before return result; } - private List> getInterviewInputs( + protected virtual List> getInterviewInputs( List records, String variableName ) { diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEvent.cls b/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEvent.cls new file mode 100644 index 0000000..98fdede --- /dev/null +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEvent.cls @@ -0,0 +1,55 @@ +/* + Copyright 2023 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +@SuppressWarnings('PMD.ApexDoc') +public with sharing class TriggerActionFlowChangeEvent extends TriggerActionFlow { + @TestVisible + private static final String CHANGE_EVENT_HEADER = 'ChangeEventHeader'; + @TestVisible + private static final String HEADER = 'header'; + @TestVisible + private static final String NOT_CHANGE_EVENT_ERROR = 'TriggerActionFlowChangeEvent can only be used on change events.'; + + @TestVisible + protected override List> getInterviewInputs( + List records, + String variableName + ) { + validateInputs(records, variableName); + List> result = new List>(); + for (Integer i = 0; i < records.size(); i++) { + result.add( + new Map{ + variableName => records[i], + HEADER => new FlowChangeEventHeader( + (EventBus.ChangeEventHeader) records[i].get(CHANGE_EVENT_HEADER) + ) + } + ); + } + return result; + } + + private void validateInputs(List records, String variableName) { + if (records == null || records.isEmpty()) { + return; + } + try { + records[0].get(CHANGE_EVENT_HEADER); + } catch (System.SObjectException e) { + throw new IllegalArgumentException(NOT_CHANGE_EVENT_ERROR); + } + } +} diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEvent.cls-meta.xml b/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEvent.cls-meta.xml new file mode 100644 index 0000000..642d054 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEvent.cls-meta.xml @@ -0,0 +1,5 @@ + + + 58.0 + Active + \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEventTest.cls b/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEventTest.cls new file mode 100644 index 0000000..b05ece7 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEventTest.cls @@ -0,0 +1,98 @@ +/* + Copyright 2023 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +@SuppressWarnings('PMD.ApexDoc') +@IsTest(isParallel=true) +private with sharing class TriggerActionFlowChangeEventTest { + private static IllegalArgumentException caught; + + private static AccountChangeEvent changeEvent = new AccountChangeEvent( + ChangeEventHeader = new EventBus.ChangeEventHeader() + ); + + private static TriggerActionFlowChangeEvent action = new TriggerActionFlowChangeEvent(); + private static List> response; + + @IsTest + private static void wrongDataTypeShouldThrowException() { + try { + response = action.getInterviewInputs( + new List{ new Account() }, + TriggerActionConstants.RECORD_VARIABLE + ); + } catch (IllegalArgumentException e) { + caught = e; + } + + Assert.isNotNull( + caught, + 'The system failed to add an exception when we are expecting one' + ); + } + + @IsTest + private static void emptyInputShouldProduceEmptyFlowInputs() { + response = action.getInterviewInputs( + new List(), + TriggerActionConstants.RECORD_VARIABLE + ); + + Assert.isNotNull( + response, + 'The system produced a null input to be passed into the invocable flows' + ); + Assert.areEqual( + 0, + response.size(), + 'There should be exactly zero maps in the produced invocable inputs' + ); + } + + @IsTest + private static void correctInputShouldProduceFlowInputs() { + response = action.getInterviewInputs( + new List{ changeEvent }, + TriggerActionConstants.RECORD_VARIABLE + ); + + Assert.isNotNull( + response, + 'The system produced a null input to be passed into the invocable flows' + ); + Assert.areEqual( + 1, + response.size(), + 'There should be exactly one map in the produced invocable inputs' + ); + Assert.isTrue( + response[0].containsKey(TriggerActionConstants.RECORD_VARIABLE), + 'The map should have a key of "record"' + ); + Assert.areEqual( + changeEvent, + response[0].get(TriggerActionConstants.RECORD_VARIABLE), + 'The value for "record" is constructed improperly' + ); + Assert.isTrue( + response[0].containsKey(TriggerActionFlowChangeEvent.HEADER), + 'The map should have a key of "header"' + ); + Assert.areEqual( + new FlowChangeEventHeader(changeEvent.ChangeEventHeader), + response[0].get(TriggerActionFlowChangeEvent.HEADER), + 'The value for "header" is constructed improperly' + ); + } +} diff --git a/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEventTest.cls-meta.xml b/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEventTest.cls-meta.xml new file mode 100644 index 0000000..642d054 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/TriggerActionFlowChangeEventTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 58.0 + Active + \ No newline at end of file diff --git a/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/validationRules/Flow_Name.validationRule-meta.xml b/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/validationRules/Flow_Name.validationRule-meta.xml index 45b7857..d67b255 100644 --- a/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/validationRules/Flow_Name.validationRule-meta.xml +++ b/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/validationRules/Flow_Name.validationRule-meta.xml @@ -1,9 +1,7 @@ - + Flow_Name true - NOT(ISBLANK( Flow_Name__c )) && Apex_Class_Name__c != "TriggerActionFlow" - The Apex Class Name must equal "TriggerActionFlow" if the Flow Name is populated. + NOT(ISBLANK( Flow_Name__c )) && Apex_Class_Name__c != "TriggerActionFlow" && Apex_Class_Name__c != "TriggerActionFlowChangeEvent" + The Apex Class Name must equal "TriggerActionFlow" or "TriggerActionFlowChangeEvent" if the Flow Name is populated.