Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get commits #2

Merged
merged 9 commits into from
Nov 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 104 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Salesforce Trigger Actions Framework
# Apex Trigger Actions Framework

<a href="https://githubsfdeploy.herokuapp.com?owner=mitchspano&amp;repo=apex-trigger-actions-framework">
<img src="https://raw.githubusercontent.com/afawcett/githubsfdeploy/master/src/main/webapp/resources/img/deploy.png" alt="Deploy to Salesforce" />
Expand Down Expand Up @@ -97,23 +97,87 @@ With this multiplicity of Apex classes, it would be wise to follow a naming conv

## Support for Flows

The trigger action framework can also allow you to invoke a flow by name, and determine the order of the flow's execution amongst other trigger actions in a given trigger context.
The trigger actions framework can also allow you to invoke a flow by name, and determine the order of the flow's execution amongst other trigger actions in a given trigger context. Here is an example of a trigger action flow that checks if a record's status has changed and if so it sets the record's description to a default value.

To make your flows usable, they must be auto-launched flows and you need to create the following flow resource variables depending on which context the flow is meant to be called in:
![Sample Flow](images/sampleFlow.png)

| Variable Name | Variable Type | Available for Input | Available for Output | Description |
| ---------------- | ----------------- | ------------------- | -------------------- | --------------------------------------------------------------- |
| newList | Record Collection | yes | no | Used to store the Trigger.new records |
| oldList | Record Collection | yes | no | Used to store the Trigger.old records |
| newListAfterFlow | Record Collection | no | yes | Used to apply record values back during before insert or update |
### Enable Flows for an sObject

You can use the `TriggerActionFlow.getOldRecord` invocable method to get the old version of a record and see which values have changed. In order to modify field values before insert or update, we must assign all records back to the `newListAfterFlow` collection variable.
To enable Trigger Action Flows on a given sObject, you must first author a class which creates an Apex defined data type to be referenced in flows and can generate the required input to launch the flow from a trigger context. This class must extend `FlowTriggerRecord`, provide @AuraEnabled properties for interacting with the old and new versions of the records within flow, and support a zero-argument constructor.

Here is an example of an auto-launched flow that checks if a Case's status has changed and if so it sets the Case's description to a default value.
```java
public with sharing class OpportunityTriggerRecord extends FlowTriggerRecord {

![Sample Flow](images/sampleFlow.png)
public OpportunityTriggerRecord() {
super();
}

public OpportunityTriggerRecord(
Opportunity newRecord,
Opportunity oldRecord,
Integer newRecordIndex,
Integer triggerActionFlowIdentifier
) {
super(newRecord, oldRecord, newRecordIndex, triggerActionFlowIdentifier);
}

@AuraEnabled
public Opportunity newRecord {
get {
return (Opportunity) this.newSObject;
}
set {
this.newSObject = value;
}
}

To enable this flow, simply insert a trigger action record with Apex Class Name equal to "TriggerActionFlow" and set the Flow Name field with the API name of the flow itself. You can select the "Allow Flow Recursion" checkbox to allow flows to run recursively (advanced).
@AuraEnabled
public Opportunity oldRecord {
get {
return (Opportunity) this.oldSObject;
}
}

public override Map<String, Object> getFlowInput(
List<SObject> newList,
List<SObject> oldList,
Integer triggerActionFlowIdentifier
) {
List<SObject> collection = newList != null ? newList : oldList;
List<OpportunityTriggerRecord> triggerRecords = new List<OpportunityTriggerRecord>();
for (Integer i = 0; i < collection.size(); i++) {
Opportunity newRecord = newList != null ? (Opportunity) newList.get(i) : null;
Opportunity oldRecord = oldList != null ? (Opportunity) oldList.get(i) : null;
triggerRecords.add(
new OpportunityTriggerRecord(
newRecord,
oldRecord,
i,
triggerActionFlowIdentifier
)
);
}
return new Map<String, Object>{
TriggerActionFlow.TRIGGER_RECORDS_VARIABLE => triggerRecords
};
}
}
```

Once this class is defined, the name of the class must be specified on the `SObject_Trigger_Setting` custom
metadata type row for the given sObject in the `FlowTriggerRecord_Class_Name__c` field:

![Set FlowTriggerRecord Class Name](images/flowTriggerRecordName.png)

### Define a Flow

To make your flows usable, they must be auto-launched flows and you need to create the following flow resource variable:

| Variable Name | Variable Type | Available for Input | Available for Output | Description |
| -------------- | ------------------------------------------------------------------------ | ------------------- | -------------------- | ----------------------------------------------------- |
| triggerRecords | Variable Collection of Apex Defined Type which extends FlowTriggerRecord | yes | no | Used to store the Trigger.new and Trigger.old records |

To enable this flow, simply insert a trigger action record with Apex Class Name equal to `TriggerActionFlow` and set the Flow Name 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)

Expand Down Expand Up @@ -338,28 +402,47 @@ Take a look at how both of these are used in the `TA_Opportunity_StageChangeRule

```java
@IsTest
private static void beforeUpdate_test() {
private static void beforeUpdateTest() {
List<Opportunity> newList = new List<Opportunity>();
List<Opportunity> oldList = new List<Opportunity>();
//generate fake Id
Id myRecordId = TestUtility.getFakeId(Opportunity.SObjectType);
newList.add(new Opportunity(Id = myRecordId, StageName = Constants.OPPORTUNITY_STAGENAME_CLOSED_WON));
oldList.add(new Opportunity(Id = myRecordId, StageName = Constants.OPPORTUNITY_STAGENAME_QUALIFICATION));
Test.startTest();
newList.add(
new Opportunity(
Id = myRecordId,
StageName = Constants.OPPORTUNITY_STAGENAME_CLOSED_WON
)
);
oldList.add(
new Opportunity(
Id = myRecordId,
StageName = Constants.OPPORTUNITY_STAGENAME_QUALIFICATION
)
);

new TA_Opportunity_StageChangeRules().beforeUpdate(newList, oldList);
Test.stopTest();

//Use getErrors() SObject method to get errors from addError without performing DML
System.assertEquals(true, newList[0].hasErrors());
System.assertEquals(1, newList[0].getErrors().size());
System.assertEquals(
true,
newList[0].hasErrors(),
'The record should have errors'
);
System.assertEquals(
1,
newList[0].getErrors().size(),
'There should be exactly one error'
);
System.assertEquals(
newList[0].getErrors()[0].getMessage(),
String.format(
TA_Opportunity_StageChangeRules.INVALID_STAGE_CHANGE_ERROR,
new String[] {
new List<String>{
Constants.OPPORTUNITY_STAGENAME_QUALIFICATION,
Constants.OPPORTUNITY_STAGENAME_CLOSED_WON
}
)
),
'The error should be the one we are expecting'
);
}
```
Expand Down
Binary file added images/flowTriggerRecordName.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/sampleFlow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
Copyright 2021 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.EmptyStatementBlock')
public abstract class FlowTriggerRecord {
public static Map<String, SObject> triggerActionFlowIdAndIndexToNewRecord = new Map<String, SObject>();
private static final String INVALID_INPUT = 'The index of new list and the Flow identifier are required.';
@TestVisible
private static final String PIPE = '|';
private Integer newListIndex;
private Integer triggerActionFlowId;

public FlowTriggerRecord() {
// no argument constructor necessary to use Type.ForName
}

protected FlowTriggerRecord(
SObject newSobject,
SObject oldSObject,
Integer newListIndex,
Integer triggerActionFlowId
) {
this.newListIndex = newListIndex;
this.triggerActionFlowId = triggerActionFlowId;
this.newSobject = newSobject;
this.oldSobject = oldSobject;
}

public abstract Map<String, Object> getFlowInput(
List<SObject> newList,
List<SObject> oldList,
Integer triggerActionFlowId
);

protected SObject newSobject {
get {
return newSobject;
}
set {
if (this.newListIndex == null || triggerActionFlowId == null) {
throw new IllegalArgumentException(INVALID_INPUT);
}
this.newSobject = value;
FlowTriggerRecord.triggerActionFlowIdAndIndexToNewRecord.put(
this.triggerActionFlowId +
PIPE +
this.newListIndex,
newSobject
);
}
}

protected SObject oldSobject;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexClass>
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
Copyright 2021 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)
private class FlowTriggerRecordTest {
static Account myAccount = new Account(
Name = 'My Account',
Id = TestUtility.getFakeId(Schema.Account.SObjectType)
);

static Integer i = 0;

@IsTest
private static void triggerRecordsShouldBeUpdated() {
TriggerActionFlowTest.AccountTriggerRecord testTriggerRecord = new TriggerActionFlowTest.AccountTriggerRecord(
myAccount,
myAccount,
0,
i
);
System.assertEquals(
true,
FlowTriggerRecord.triggerActionFlowIdAndIndexToNewRecord.containsKey(
i +
FlowTriggerRecord.PIPE +
0
),
'The index of the newRecord should be stored in the triggerActionFlowIdAndIndexToNewRecord map'
);

testTriggerRecord.newRecord.Id = null;

System.assertEquals(
null,
FlowTriggerRecord.triggerActionFlowIdAndIndexToNewRecord.get(
i +
FlowTriggerRecord.PIPE +
0
)
.Id,
'Modifications to the newRecord should persist through the triggerActionFlowIdAndIndexToNewRecord map'
);
}

@IsTest
private static void triggerRecordsShouldThrowExceptionIfTheNewValueIsSetWithoutAnIndex() {
Exception myException;
try {
TriggerActionFlowTest.AccountTriggerRecord testTriggerRecord = new TriggerActionFlowTest.AccountTriggerRecord(
myAccount,
myAccount,
null,
i
);
} catch (Exception e) {
myException = e;
}

System.assertNotEquals(
null,
myException,
'Setting the value of the new sObject should fail without the index within the newList'
);
}

@IsTest
private static void triggerRecordsShouldThrowExceptionIfTheNewValueIsSetWithoutATriggerActionFlowIdentifier() {
Exception myException;
try {
TriggerActionFlowTest.AccountTriggerRecord testTriggerRecord = new TriggerActionFlowTest.AccountTriggerRecord(
myAccount,
myAccount,
0,
null
);
} catch (Exception e) {
myException = e;
}

System.assertNotEquals(
null,
myException,
'Setting the value of the new sObject should fail without the identifer of the trigger action flow'
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading