Skip to content

Commit

Permalink
Fixes #95 by fixing restrictiveness level for RollupEvaluator
Browse files Browse the repository at this point in the history
  • Loading branch information
jamessimone committed May 6, 2021
1 parent e0bc123 commit c99846b
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 37 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Within the `Rollup__mdt` custom metadata type, add a new record with fields:
- `Full Recalculation Default String Value` (optional) - same as `Full Recalculation Default Number Value`, but for String-based fields (including Lookup and Id fields).
- `Calc Item Where Clause` (optional) - add conditions to filter the calculation items that are used. **Note** - the fields, especially parent-level fields, _must_ be present on the calculation items or the filtering will not work correctly. As of [v1.0.9](https://github.com/jamessimone/apex-rollup/releases/tag/v1.0.9), nested conditionals (conditionals contained within parantheses) are supported. However, due to the orthogonal nature of deeply nested conditionals from the original problem area, it's entirely possible that some forms of nested conditionals will not work, or will work in unintended ways. Please [submit an issue](/issues) if you are using Rollup and experience issues with calculation items correctly being flagged / not flagged toward the rollup field. For currency or number fields with multiple decimals, keep in mind that however the number appears in a SOQL query (ie `4.00`) is the format that you should use when performing filtering; `Amount != 4` will not work if the value is stored as `4.00`. The only exception to this is zero; there, you are allowed to omit the decimal places.
- `Is Full Record Set` (optional, defaults to `false`) - by default, if the records you are passing in comprise the full set of child items for a given lookup item but none of them "qualify" to be rolled up (either due to the use of the Calc Item Where Clause, Changed Fields On Calc Item, or a custom Evaluator), Rollup aborts early. If you know you have the exhaustive list of records to be used for a given lookup item **and** you stipulate the Full Recalculation Default Number (or String) Value, you can override the existing rollup item's amount by checking off this field
- `Order By (First/Last)` (optional) - at present, only valid when FIRST/LAST is used as the Rollup Operation. This is the API name of a text/date/number-based field that you would like to order the calculation items by. **Note** unlike DLRS, this field is _not_ optional on a first/last operation; a validation rule will enforce that you supply a value here, even if the value used is the same as the field you are rolling up on.
- `Order By (First/Last)` (optional) - at present, only valid when FIRST/LAST is used as the Rollup Operation. This is the API name of a text/date/number-based field that you would like to order the calculation items by. Like DLRS, this field is optional on a first/last operation, and if a field is not supplied, the `Rollup field On Calc Item` is used.
- `Is Rollup Started From Parent` (optional, defaults to `false`) - if the the records being passed in are the parent records, check this field off. `Rollup` will then go and retrieve the assorted children records before rolling the values up to the parents. If you are using `Is Rollup Started From Parent` and grandparent rollups with Tasks/Events (or anything with a polymorphic relationship field like `Who` or `What` on Task/Event; the `Parent` field on `Contact Point Address` is another example of such a field), you **must** also include a filter for `What.Type` or `Who.Type` in your `Calc Item Where Clause` in order to proceed, e.g. `What.Type = 'Account'`.
- `Grandparent Relationship Field Path` (optional) - if [you are rolling up to a grandparent (or greater) parent object](#grandparent-rollups), use this field to establish the full relationship name of the field, eg from Opportunity Line Items directly to an Account's Annual Revenue: `Opportunity.Account.AnnualRevenue` would be used here. The field name (after the last period) should match up with what is being used in `Rollup Field On Lookup Object`. For caveats and more information on how to setup rollups looking to use this functionality, please refer to the linked section.
- `Rollup To Ultimate Parent` (optional) - Check this box if you are rolling up to an Account, for example, and use the `Parent Account ID` field on accounts, _and_ want the rolled up value to only be used on the top-level account. Can be used with any hierarchy lookup or lookup back to the same object. Must be used in conjunction with `Ultimate Parent Lookup` (below), and _can_ be used in conjunction with `Grandparent Relationship Field Path` (if the hierarchical field you are rolling up to is not on the immediate parent object).
Expand Down Expand Up @@ -189,7 +189,7 @@ Here are the arguments necessary to invoke `Rollup` from a Flow / Process Builde
- `Full Recalculation Default String Value` (optional) - same as `Full Recalculation Default Number Value`, but for String-based fields (including Lookup and Id fields).
- `SOQL Where Clause To Exclude Calc Items` (optional) - add conditions to filter the calculation items that are used. **Note** - the fields, especially parent-level fields, _must_ be present on the calculation items or the filtering will not work correctly. For currency or number fields with multiple decimals, keep in mind that however the number appears in a SOQL query (ie `4.00`) is the format that you should use when performing filtering; `Amount != 4` will not work if the value is stored as `4.00`. The only exception to this is zero; there, you are allowed to omit the decimal places.
- `Is Full Record Set` (optional) - by default, if the records you are passing in comprise the full set of child items for a given lookup item but none of them "qualify" to be rolled up (either due to the use of the Calc Item Where Clause, Changed Fields On Calc Item, or a custom Evaluator), Rollup aborts early. If you know you have the exhaustive list of records to be used for a given lookup item **and** you stipulate the Full Recalculation Default Number (or String) Value, you can override the existing rollup item's amount by toggling this field
- `Order By (First/Last)` (optional) - at present, only valid when FIRST/LAST is used as the Rollup Operation. This is the API name of a text/date/number-based field that you would like to order the calculation items by. **Note** unlike DLRS, this field is _not_ optional on a first/last operation; a validation rule will enforce that you supply a value here, even if the value used is the same as the field you are rolling up on.
- `Order By (First/Last)` (optional) - at present, only valid when FIRST/LAST is used as the Rollup Operation. This is the API name of a text/date/number-based field that you would like to order the calculation items by. Like DLRS, this field is optional on a first/last operation, and if a field is not supplied, the `Rollup field On Calc Item` is used.
- `Defer Processing` (optional, default `false`) - when checked and set to `{!$GlobalConstant.True}`, you have to call the separate invocable method `Process Deferred Rollups` at the end of your flow. Otherwise, each invocable action kicks off a separate queueable/batch job. **Note** - for extremely large flows calling dozens of rollup operations, it behooves the end user / admin to occasionally call the `Process Deferred Rollups` invocable action to separate rollup operations into different jobs. You'll avoid running out of memory by doing so. See the "Process Deferred Rollups" section below for more info.
- `Is Rollup Started From Parent` (optional, defaults to `{!$GlobalConstant.False}`) - set to `{!$GlobalConstant.True}` if collection being passed in is the parent SObject, and you want to recalculate the defined rollup operation for the passed in parent records. Used in conjunction with `Calc Item Type When Rollup Started From Parent`. If you are using `Is Rollup Started From Parent` and grandparent rollups with Tasks/Events (or anything with a polymorphic relationship field like `Who` or `What` on Task/Event; the `Parent` field on `Contact Point Address` is another example of such a field), you **must** also include a filter for `What.Type` or `Who.Type` in your `Calc Item Where Clause` in order to proceed, e.g. `What.Type = 'Account'`.
- `Calc Item Type When Rollup Started From Parent` (optional) - only necessary to provide if `Is Rollup Started From Parent` field is enabled and set to `{!$GlobalConstant.True}`. Normally in this invocable, the calc item type is figured out by examining the passed-in collection - but when the collection is the parent records, we need the SObject API name of the calculation items explicitly defined.
Expand Down
8 changes: 6 additions & 2 deletions rollup/core/classes/Rollup.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1659,7 +1659,7 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
}

private static void enforceValidationRules(FlowInput flowInput) {
if ((flowInput.rollupOperation == 'FIRST' || flowInput.rollupOperation == 'LAST') && String.isBlank(flowInput.orderByFirstLast)) {
if ((flowInput.rollupOperation == 'FIRST' || flowInput.rollupOperation == 'LAST') && String.isBlank(flowInput.orderByFirstLast) && String.isBlank(flowInput.rollupFieldOnCalcItem)) {
throw new IllegalArgumentException('Order By First/Last field required for ' + flowInput.rollupOperation + ' operation');
} else if (
(flowInput.rollupToUltimateParent && String.isBlank(flowInput.ultimateParentLookup)) ||
Expand Down Expand Up @@ -2188,7 +2188,11 @@ global without sharing virtual class Rollup implements Database.Batchable<SObjec
results.eval = RollupEvaluator.getEvaluator(eval, metadata, oldCalcItems, calcItemType);

for (SObject item : calcItems) {
if (results.eval.matches(item)) {
// if the where clause would exclude something, but we're in an update
// and the old value wouldn't have been excluded, pass it on through
// to be handled further downstream
SObject potentialOldItem = oldCalcItems?.isEmpty() == false && oldCalcItems.containsKey(item.Id) ? oldCalcItems.get(item.Id) : item;
if (results.eval.matches(item) || results.eval.matches(potentialOldItem)) {
matchingItems.add(item);
// metadata shouldn't be null, but it's good to check; unfortunately, if(null) throws so we
// have to do this EXTRA explicit check
Expand Down
2 changes: 1 addition & 1 deletion rollup/core/classes/RollupCalculator.cls
Original file line number Diff line number Diff line change
Expand Up @@ -982,7 +982,7 @@ public without sharing abstract class RollupCalculator {
}

public void setMetadata(Rollup__mdt metadata) {
this.orderByField = metadata.OrderByFirstLast__c;
this.orderByField = String.isNotBlank(metadata.OrderByFirstLast__c) ? metadata.OrderByFirstLast__c : metadata.RollupFieldOnCalcItem__c;
this.lookupFieldOnCalcItem = metadata.LookupFieldOnCalcItem__c;
}

Expand Down
17 changes: 1 addition & 16 deletions rollup/core/classes/RollupEvaluator.cls
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public without sharing abstract class RollupEvaluator implements Rollup.Evaluato
List<Rollup.Evaluator> evals = new List<Rollup.Evaluator>{ eval == null ? ALWAYS_TRUE_SINGLETON : eval };

if (String.isNotBlank(metadata.CalcItemWhereClause__c)) {
evals.add(new WhereFieldEvaluator(metadata, sObjectType, oldCalcItems));
evals.add(new WhereFieldEvaluator(metadata.CalcItemWhereClause__c, sObjectType));
}
if (String.isNotBlank(metadata.ChangedFieldsOnCalcItem__c)) {
evals.add(getChangedFieldEval(metadata, oldCalcItems));
Expand Down Expand Up @@ -134,11 +134,6 @@ public without sharing abstract class RollupEvaluator implements Rollup.Evaluato
this.validRelationshipNames = this.getValidRelationshipNames(calcItemSObjectType);
}

public WhereFieldEvaluator(Rollup__mdt metadata, SObjectType calcItemSObjectType, Map<Id, SObject> oldRecords) {
this(metadata.CalcItemWhereClause__c, calcItemSObjectType);
this.oldRecords = oldRecords;
}

public List<String> getWhereClauses() {
return new List<String>(this.splitWheres);
}
Expand Down Expand Up @@ -169,16 +164,6 @@ public without sharing abstract class RollupEvaluator implements Rollup.Evaluato
Boolean matches = calcItem instanceof SObject;
if (matches) {
matches = this.innerMatches(calcItem);

if (matches) {
return matches;
}

SObject item = (SObject) calcItem;
// if the where clause would exclude something, but we're in an update
// and the old value wouldn't have been excluded, pass it on through
SObject potentialOldItem = this.oldRecords?.isEmpty() == false && this.oldRecords.containsKey(item.Id) ? this.oldRecords.get(item.Id) : item;
matches = this.innerMatches(potentialOldItem);
}

return matches;
Expand Down

This file was deleted.

34 changes: 34 additions & 0 deletions rollup/tests/RollupTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -2441,6 +2441,39 @@ private class RollupTests {
System.assertEquals(0, updatedAcc.AnnualRevenue, 'SUM AFTER_UPDATE from flow should match diff for PreferenceRank');
}

@isTest
static void shouldUpdateToActualFirstIfCurrentItemShouldBeExcludedAndOldOneWouldNot() {
Account acc = [SELECT Id, AnnualRevenue FROM Account];
acc.AnnualRevenue = 250;
update acc;

ContactPointAddress cpa = new ContactPointAddress(PreferenceRank = 0, Name = 'First', ParentId = acc.Id);
ContactPointAddress cpaTwo = new ContactPointAddress(PreferenceRank = 1, Name = 'Second', ParentId = acc.Id);
insert new List<ContactPointAddress>{ cpa, cpaTwo };

List<ContactPointAddress> cpas = new List<ContactPointAddress>{ cpa }; // only pass cpa, which would be excluded normally (except for the PreferenceRank being set below to another val)
DMLMock mock = loadMock(cpas);

List<Rollup.FlowInput> flowInputs = prepareFlowTest(cpas, 'UPDATE', 'FIRST');
flowInputs[0].oldRecordsToRollup = new List<ContactPointAddress>{
new ContactPointAddress(ParentId = cpas[0].ParentId, Id = cpas[0].Id, PreferenceRank = acc.AnnualRevenue.intValue())
};
flowInputs[0].calcItemWhereClause = 'PreferenceRank != 0';
flowInputs[0].orderByFirstLast = 'Name';

Test.startTest();
List<Rollup.FlowOutput> flowOutputs = Rollup.performRollup(flowInputs);
Test.stopTest();

System.assertEquals(1, flowOutputs.size(), 'Flow ouputs were not provided');
System.assertEquals('SUCCESS', flowOutputs[0].message);
System.assertEquals(true, flowOutputs[0].isSuccess);

System.assertEquals(1, mock.Records.size(), 'FIRST AFTER_UPDATE from flow did not update accounts');
Account updatedAcc = (Account) mock.Records[0];
System.assertEquals(cpaTwo.PreferenceRank, updatedAcc.AnnualRevenue, 'FIRST AFTER_UPDATE from flow should correctly recalculate and exclude current item');
}

@isTest
static void shouldBeInvokedSuccessfullyBeforeDeleteFromFlow() {
List<ContactPointAddress> cpas = new List<ContactPointAddress>{ new ContactPointAddress(PreferenceRank = 1000) };
Expand Down Expand Up @@ -2495,6 +2528,7 @@ private class RollupTests {
'INSERT',
'FIRST'
);
flowInputs[0].rollupFieldOnCalcItem = null;

Exception ex;
try {
Expand Down
4 changes: 2 additions & 2 deletions sfdx-project.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"default": true,
"package": "apex-rollup",
"path": "rollup",
"versionNumber": "1.2.16.0",
"versionDescription": "Slight performance improvements",
"versionNumber": "1.2.17.0",
"versionDescription": "Fix too-permissiveness for some update operations in RollupEvaluator",
"releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest"
}
],
Expand Down

0 comments on commit c99846b

Please sign in to comment.