Skip to content

Commit

Permalink
V1.2.5 - Custom Concat Delimiter & Calc Item Where Clause Improvements (
Browse files Browse the repository at this point in the history
#67)

* Started working on allowing grandparent rollups to do intermediate object filtering

* Passing test for somewhat complex calc item where clause with great-grandparent rollup - need to add additional coverage for nested conditionals still

* Ensuring polymorphic fields are supported with Evaluator

* All tests passing, still TODO: actually supporting re-querying when polymorphic lookup fields are part of calc item where clause

* Wrap up work to support calc item where clause with grandparent rollups, and added support for filtering on polymorphic fields within Rollup

* Fixes #2 by adding custom concat delimiter option to Rollup__mdt

* Scope creep - add code coverage for different polymorphic field types, including the fix of a subtle bug in RollupEvaluator where Who.Type/What.Type wouldn't always get selected correctly from a parent-level object

* Fixes #70 by instituting sorting for the final output of rollup concat operations
  • Loading branch information
jamessimone authored Apr 9, 2021
1 parent 2e1fa4e commit 15323f4
Show file tree
Hide file tree
Showing 19 changed files with 655 additions and 76 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ That's it! Now you're ready to configure your rollups using Custom Metadata. `Ro

#### Special Considerations For Use Of Custom Fields As Rollup/Lookup Fields

One **special** thing to note on the subject of Field Definitions — custom fields/objects referenced in CMDT Field Definition fields are stored in an atypical way, and require the usage of an additional SOQL query as part of `Rollup`'s upfront cost. A typical `Rollup` operation will use `2` SOQL queries per rollup — the query that determines whether or not a job should be queued or batched, and a query to get the `Rollup__mdt` custom metadata. Though it's true that since Spring 21, it's possible to retrieve CMDT straight from the cache without consuming a SOQL query, Custom Metadata Types retrieved from the cache don't have their parent-level fields initialized, which makes it impossible to massage the data into a usable shape within the rest of `Rollup`. If the SOQL queries used by `Rollup` becomes cause for concern, please [submit an issue](/issues) and we can work to address it!
One **special** thing to note on the subject of Field Definitions — custom fields/objects referenced in CMDT Field Definition fields are stored in an atypical way, and require the usage of an additional SOQL query as part of `Rollup`'s upfront cost. A typical `Rollup` operation will use `2` SOQL queries per rollup — the query that determines whether or not a job should be queued or batched, and a query to get the `Rollup__mdt` custom metadata. Though it's true that since Spring 21, it's possible to retrieve CMDT straight from the cache without consuming a SOQL query, Custom Metadata Types retrieved from the cache don't have their parent-level fields initialized, which makes it impossible to massage the data into a usable shape within the rest of `Rollup`. Additionally, if your `Calc Item Where Clause` contains a reference to a polymorphic field (like `What.Type` or `Owner.Name` or `What.Type` etc ...), an extra query will be consumed prior to `Rollup` going async to ensure that only matching items are passed along to be rolled up.

If the SOQL queries used by `Rollup` becomes cause for concern, please [submit an issue](/issues) and we can work to address it!

#### Rollup Custom Metadata Field Breakdown

Expand Down Expand Up @@ -246,7 +248,7 @@ In this example, there are four objects in scope:

- **super important** all intermediate objects in the chain (so, in this example, `Application__c`, and `ParentApplication__c`) must _also_ have the `Rollup.runFromTrigger()` snippet in those object's triggers (or the appropriate invocable built). This special caveat handles cases where the intermediate objects' lookup fields are updated; no big deal if the ultimate parent lookup hasn't changed, but _big_ deal if the ultimate parent lookup _has_ changed
- if your CMDT/invocable is set up with a relationship that is not the immediate parent and you don't fill out the `Grandparent Relationship Field Path`, it simply won't work. The field path is required because it's common for objects to have more than one lookup field to the same object
- if you are using `Grandparent Relationship Field Path` with a polymorphic standard field like `Task.WhatId` or `Task.WhoId`, you should also supply a `SOQL Where Clause` to ensure you are filtering the calculation items to only be related to one type of parent at a time. **Note** - at this time, the `SOQL Where Clause` only works on the fields present on the initial calculation items, and does not support cross-object filtering
- if you are using `Grandparent Relationship Field Path` with a polymorphic standard field like `Task.WhatId` or `Task.WhoId`, you should also supply a `Calc Item Where Clause` to ensure you are filtering the calculation items to only be related to one type of parent at a time (eg: your `Calc Item Where Clause` would look like `What.Type = 'Account'`)
- grandparent rollups respect [SOQL's map relationship-field hopping of 5 levels](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_relationships_query_limits.htm):

> In each specified relationship, no more than five levels can be specified in a child-to-parent relationship. For example, Contact.Account.Owner.FirstName (three levels)
Expand Down
210 changes: 209 additions & 1 deletion extra-tests/classes/RollupIntegrationTests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ private class RollupIntegrationTests {
Test.stopTest();

app = [SELECT Objects__c FROM Application__c WHERE Id = :app.Id];
System.assertEquals('Lead, Account', app.Objects__c);
System.assertEquals('Account, Lead', app.Objects__c);
}

@isTest
Expand Down Expand Up @@ -373,4 +373,212 @@ private class RollupIntegrationTests {
System.assertEquals(secondChild.Name, updatedGreatGrandparent.Name, 'Grandparent record should have retriggered greatgrandparent rollup!');
System.assertEquals(child.Name, updatedGreatGrandparentTwo.Name, 'Grandparent record should have retriggered greatgrandparent rollup again!');
}

@isTest
static void shouldAllowIntermediateCustomObjectCalcItemWhereClauseFiltering() {
Account greatGrandparent = new Account(Name = 'Great-grandparent NonFiltered');
insert greatGrandparent;

ParentApplication__c grandParent = new ParentApplication__c(Name = 'Grandparent NonFiltered', Account__c = greatGrandparent.Id);
ParentApplication__c secondGrandparent = new ParentApplication__c(Name = 'Should be filtered grandparent', Account__c = greatGrandparent.Id);
insert new List<ParentApplication__c>{ grandParent, secondGrandparent };

Application__c parent = new Application__c(Name = 'Parent-level NonFiltered', ParentApplication__c = grandParent.Id);
Application__c secondParent = new Application__c(Name = 'Second parent-level filtered', ParentApplication__c = secondGrandparent.Id);
insert new List<Application__c>{ parent, secondParent };

ApplicationLog__c child = new ApplicationLog__c(Application__c = secondParent.Id, Name = 'Should not be appended since application should be filtered');
ApplicationLog__c secondChild = new ApplicationLog__c(Name = 'Should correctly be appended', Application__c = parent.Id);
ApplicationLog__c nonMatchChild = new ApplicationLog__c(Name = 'nonmatch', Application__c = parent.Id);
List<ApplicationLog__c> appLogs = new List<ApplicationLog__c>{ child, secondChild, nonMatchChild };
insert appLogs;

Rollup.records = appLogs;
Rollup.rollupMetadata = new List<Rollup__mdt>{
new Rollup__mdt(
CalcItem__c = 'ApplicationLog__c',
RollupFieldOnCalcItem__c = 'Name',
LookupFieldOnCalcItem__c = 'Application__c',
LookupObject__c = 'Account',
LookupFieldOnLookupObject__c = 'Id',
RollupFieldOnLookupObject__c = 'Name',
RollupOperation__c = 'CONCAT_DISTINCT',
GrandparentRelationshipFieldPath__c = 'Application__r.ParentApplication__r.Account__r.Name',
CalcItemWhereClause__c = 'Application__r.Name != \'' +
secondParent.Name +
'\' AND Application__r.ParentApplication__r.Name != \'' +
grandParent.Name +
'\' AND Name != \'' +
nonMatchChild.Name +
'\''
)
};
Rollup.shouldRun = true;
Rollup.apexContext = TriggerOperation.AFTER_INSERT;

Test.startTest();
Rollup.runFromTrigger();
Test.stopTest();

Account updatedGreatGrandparent = [SELECT Name FROM Account WHERE Id = :greatGrandparent.Id];
System.assertEquals(greatGrandparent.Name, updatedGreatGrandparent.Name, 'Great-grandparent name should not have been appended based on exclusions');
}

@isTest
static void shouldProperlyFilterPolymorphicWhatFields() {
Account acc = new Account(Name = 'Matching type');
Case cas = new Case();
insert new List<SObject>{ acc, cas };

Opportunity opp = new Opportunity(CloseDate = System.today(), StageName = 'Prospecting', Name = 'parent opp', AccountId = acc.Id);
insert opp;

Task matchingTask = new Task(ActivityDate = System.today(), WhatId = opp.Id, Subject = 'Match');
Task nonMatchingTask = new Task(ActivityDate = System.today(), WhatId = cas.Id, Subject = 'Non match');
List<Task> tasks = new List<Task>{ matchingTask, nonMatchingTask };
insert tasks;

// things like What.Type aren't included by default in updates made within Triggers/Flows
// specifically NOT requerying here validates that Rollup can handle a polymorphic where clause
Rollup.records = tasks;
Rollup.rollupMetadata = new List<Rollup__mdt>{
new Rollup__mdt(
CalcItem__c = 'Task',
RollupFieldOnCalcItem__c = 'Subject',
LookupFieldOnCalcItem__c = 'WhatId',
LookupObject__c = 'Account',
LookupFieldOnLookupObject__c = 'Id',
RollupFieldOnLookupObject__c = 'Name',
RollupOperation__c = 'CONCAT_DISTINCT',
GrandparentRelationshipFieldPath__c = 'What.Account.Name',
CalcItemWhereClause__c = 'What.Type = \'Opportunity\''
)
};
Rollup.shouldRun = true;
Rollup.apexContext = TriggerOperation.AFTER_INSERT;

Test.startTest();
Rollup.runFromTrigger();
Test.stopTest();

Account updatedAcc = [SELECT Name FROM Account WHERE Id = :acc.Id];
System.assertEquals(matchingTask.Subject + ', ' + acc.Name, updatedAcc.Name, 'Only matching task subject should have been appended via What.Type');
}

@isTest
static void shouldAvoidRequeryingIfPolymorphicFieldsAreIncluded() {
Account acc = new Account(Name = 'Matching type');
insert acc;

Opportunity opp = new Opportunity(CloseDate = System.today(), StageName = 'Prospecting', Name = 'parent opp', AccountId = acc.Id);
insert opp;

Task matchingTask = new Task(ActivityDate = System.today(), WhatId = opp.Id, Subject = 'Match');
insert matchingTask;

Rollup.records = [SELECT Id, Subject, What.Type, WhatId FROM Task];
Rollup.rollupMetadata = new List<Rollup__mdt>{
new Rollup__mdt(
CalcItem__c = 'Task',
RollupFieldOnCalcItem__c = 'Subject',
LookupFieldOnCalcItem__c = 'WhatId',
LookupObject__c = 'Account',
LookupFieldOnLookupObject__c = 'Id',
RollupFieldOnLookupObject__c = 'Name',
RollupOperation__c = 'CONCAT_DISTINCT',
GrandparentRelationshipFieldPath__c = 'What.Account.Name',
CalcItemWhereClause__c = 'What.Type = \'Opportunity\''
)
};
Rollup.shouldRun = true;
Rollup.apexContext = TriggerOperation.AFTER_INSERT;

Test.startTest();
Rollup.runFromTrigger();
Test.stopTest();

Account updatedAcc = [SELECT Name FROM Account WHERE Id = :acc.Id];
System.assertEquals(
matchingTask.Subject +
', ' +
acc.Name,
updatedAcc.Name,
'Only matching task subject should have been appended via Who.Type requerying'
);
}

@isTest
static void shouldProperlyFilterPolymorphicWhoFields() {
Contact con = new Contact(LastName = 'Polly', Email = 'polly@morhpism.com');
Lead lead = new Lead(LastName = 'Morphism', Email = 'morphism@polly.com', Company = 'PollyMorphism');
insert new List<SObject>{ con, lead };

Task matchingTask = new Task(ActivityDate = System.today(), WhoId = lead.Id, Subject = 'Match');
Task nonMatchingTask = new Task(ActivityDate = System.today(), WhoId = con.Id, Subject = 'Not a Match');
List<Task> tasks = new List<Task>{ matchingTask, nonMatchingTask };
insert tasks;

Rollup.records = tasks;
Rollup.rollupMetadata = new List<Rollup__mdt>{
new Rollup__mdt(
CalcItem__c = 'Task',
RollupFieldOnCalcItem__c = 'Subject',
LookupFieldOnCalcItem__c = 'WhoId',
LookupObject__c = 'Lead',
LookupFieldOnLookupObject__c = 'Id',
RollupFieldOnLookupObject__c = 'LastName',
RollupOperation__c = 'CONCAT_DISTINCT',
CalcItemWhereClause__c = 'Who.Type = \'Lead\''
)
};
Rollup.shouldRun = true;
Rollup.apexContext = TriggerOperation.AFTER_INSERT;

Test.startTest();
Rollup.runFromTrigger();
Test.stopTest();

Lead updatedLead = [SELECT LastName FROM Lead WHERE Id = :lead.Id];
System.assertEquals(matchingTask.Subject + ', ' + lead.LastName, updatedLead.LastName, 'Only matching task should have been appended via Who.Type');
}

@isTest
static void shouldProperlyFilterPolymorphicOwnerFields() {
User currentUser = [SELECT Id, Name FROM User WHERE Id = :UserInfo.getUserId()];
Lead matchingLead = new Lead(Company = 'Matching polymorphic', LastName = 'Polly', OwnerId = currentUser.Id);
insert matchingLead;

Event nonMatchingEvent = new Event(
ActivityDateTime = System.now(),
WhoId = matchingLead.Id,
OwnerId = currentUser.Id,
Subject = 'Not a Match',
DurationInMinutes = 30
);
List<Event> events = new List<Event>{ nonMatchingEvent };
insert events;

Rollup.records = events;
Rollup.rollupMetadata = new List<Rollup__mdt>{
new Rollup__mdt(
CalcItem__c = 'Event',
RollupFieldOnCalcItem__c = 'Subject',
LookupFieldOnCalcItem__c = 'WhoId',
LookupObject__c = 'Lead',
LookupFieldOnLookupObject__c = 'Id',
RollupFieldOnLookupObject__c = 'LastName',
RollupOperation__c = 'CONCAT_DISTINCT',
CalcItemWhereClause__c = 'Owner.Name != \'' + currentUser.Name + '\''
)
};
Rollup.shouldRun = true;
Rollup.apexContext = TriggerOperation.AFTER_INSERT;

Test.startTest();
Rollup.runFromTrigger();
Test.stopTest();

Lead updatedLead = [SELECT LastName FROM Lead WHERE Id = :matchingLead.Id];
System.assertEquals(matchingLead.LastName, updatedLead.LastName, 'Lead last name should not have been updated');
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "apex-rollup",
"version": "1.2.4",
"version": "1.2.5",
"description": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.",
"repository": {
"type": "git",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,8 @@ describe('Rollup force recalc tests', () => {
lookupSObjectName: 'Account',
calcItemSObjectName: 'Contact',
operationName: 'CONCAT',
potentialWhereClause: ''
potentialWhereClause: '',
potentialConcatDelimiter: ''
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@
required
>
</lightning-input>
<lightning-input
data-id="potentialConcatDelimiter"
class="slds-col slds-form-element slds-form-element_horizontal"
type="text"
label="Concat Delimiter (Optional)"
name="potentialConcatDelimiter"
onchange={handleChange}
>
</lightning-input>
<lightning-textarea
class="slds-col slds-form-element slds-form-element_horizontal"
onchange={handleChange}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export default class RollupForceRecalculation extends LightningElement {
lookupSObjectName: '',
calcItemSObjectName: '',
operationName: '',
potentialWhereClause: ''
potentialWhereClause: '',
potentialConcatDelimiter: ''
};
@api isCMDTRecalc = false;
@api rollupMetadataOptions = [];
Expand Down Expand Up @@ -89,7 +90,8 @@ export default class RollupForceRecalculation extends LightningElement {
}
await this._getBatchJobStatus(jobId);
} catch (e) {
this._displayErrorToast('An error occurred while rolling up', e.body?.message ?? e.message);
const errorMessage = !!e.body && e.body.message ? e.body.message : e.message;
this._displayErrorToast('An error occurred while rolling up', errorMessage);
console.error(e); // in the event you dismiss the toast but still want to see the error
}
}
Expand Down
Loading

0 comments on commit 15323f4

Please sign in to comment.