diff --git a/.gitignore b/.gitignore index 1660d65c..35f52ce6 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ sfge-*.log.gz .DS_Store .config .vscode -config/data/act-pr-event.json \ No newline at end of file +config/data/act-pr-event.json +.sflogsub \ No newline at end of file diff --git a/README.md b/README.md index e8f7e050..581ad92f 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,12 @@ As well, don't miss [the Wiki](../../wiki), which includes even more info for co ## Deployment & Setup - + Deploy to Salesforce - + Deploy to Salesforce Sandbox diff --git a/extra-tests/classes/InvocableDrivenTests.cls b/extra-tests/classes/InvocableDrivenTests.cls index 122490e9..ddd853f6 100644 --- a/extra-tests/classes/InvocableDrivenTests.cls +++ b/extra-tests/classes/InvocableDrivenTests.cls @@ -3,8 +3,7 @@ private class InvocableDrivenTests { @TestSetup static void setup() { upsert new RollupSettings__c(IsEnabled__c = true); - Account acc = new Account(Name = InvocableDrivenTests.class.getName()); - insert acc; + insert new Account(Name = InvocableDrivenTests.class.getName()); } @IsTest @@ -182,4 +181,22 @@ private class InvocableDrivenTests { parent = [SELECT AnnualRevenue FROM Account WHERE Id = :parent.Id]; System.assertEquals(null, parent.AnnualRevenue); } + + @IsTest + static void refreshWorksWithEmptyCollectionsWhenParentIdIsProvided() { + Account acc = [SELECT Id FROM Account]; + ContactPointAddress child = new ContactPointAddress(Name = 'RollupIntegrationRefresh', PreferenceRank = 99, ParentId = acc.Id); + insert child; + + Flow.Interview flowInterview = new Flow.Interview.Rollup_Integration_Refresh_With_Empty_Collections( + new Map{ 'parentRecordIdForEmptyChildrenCollections' => acc.Id } + ); + + Test.startTest(); + flowInterview.start(); + Test.stopTest(); + + acc = [SELECT AnnualRevenue FROM Account WHERE Id = :acc.Id]; + System.assertEquals(child.PreferenceRank, acc.AnnualRevenue); + } } diff --git a/extra-tests/classes/RollupCurrencyInfoTests.cls b/extra-tests/classes/RollupCurrencyInfoTests.cls index 383e5b5a..d6256a94 100644 --- a/extra-tests/classes/RollupCurrencyInfoTests.cls +++ b/extra-tests/classes/RollupCurrencyInfoTests.cls @@ -83,6 +83,14 @@ private class RollupCurrencyInfoTests { RollupCurrencyInfo.transform(new List{ camp }, Campaign.ActualCost, mockEurInfo.IsoCode, new List()); updatedCamp = (Campaign) RollupCurrencyInfo.getCalcItem(camp, mockEurInfo.IsoCode); System.assertEquals((mockEurInfo.ConversionRate / (mockUsdInfo.ConversionRate / camp.ActualCost)).doubleValue(), updatedCamp.ActualCost); + // sanity check that updates on previously transformed fields still calculate correctly + camp.BudgetedCost = 6; + RollupCurrencyInfo.transform(new List{ camp }, Campaign.BudgetedCost, mockEurInfo.IsoCode, new List()); + Campaign again = (Campaign) RollupCurrencyInfo.getCalcItem(camp, mockEurInfo.IsoCode); + System.assertEquals( + (mockEurInfo.ConversionRate / (mockUsdInfo.ConversionRate / camp.BudgetedCost)).setScale(mockEurInfo.DecimalPlaces), + again.BudgetedCost + ); } @IsTest diff --git a/extra-tests/classes/RollupEvaluatorTests.cls b/extra-tests/classes/RollupEvaluatorTests.cls index 21471857..18516c0c 100644 --- a/extra-tests/classes/RollupEvaluatorTests.cls +++ b/extra-tests/classes/RollupEvaluatorTests.cls @@ -1258,6 +1258,15 @@ private class RollupEvaluatorTests { System.assertEquals(false, eval.matches(nonMatch3)); } + @IsTest + static void stripsExtraParantheticalStructures() { + String whereClause = '(Name IN (\'0-Current\', \'1-30 Days\', \'31-60 Days\')) AND Name IN (\'0-Current\', \'1-30 Days\', \'31-60 Days\')'; + + RollupEvaluator eval = new RollupEvaluator.WhereFieldEvaluator(whereClause, ContactPointConsent.SObjectType); + + System.assertEquals(true, eval.matches(new ContactPointConsent(Name = '0-Current'))); + } + private static String getSoqlCompliantDatetime(Datetime dt) { return dt.format('yyyy-MM-dd\'T\'HH:mm:ssZ'); } diff --git a/extra-tests/classes/RollupLimitsTest.cls b/extra-tests/classes/RollupLimitsTest.cls index bd2cfcb3..dc15d598 100644 --- a/extra-tests/classes/RollupLimitsTest.cls +++ b/extra-tests/classes/RollupLimitsTest.cls @@ -2,6 +2,15 @@ private class RollupLimitsTest { @IsTest static void correctlyReferencesOrgLimits() { - System.assertEquals(false, new RollupLimits.Tester(RollupControl__mdt.getInstance('Org_Default'), false).hasExceededOrgAsyncLimit()); + Assert.areEqual(false, new RollupLimits.Tester(RollupControl__mdt.getInstance('Org_Default'), false).hasExceededOrgAsyncLimit()); + } + + @IsTest + static void doesNotThrowForNullControl() { + RollupLimits.stubbedQueryRows = 50001; + + Boolean hasExceededLimits = Rollup.hasExceededCurrentRollupLimits(null); + + Assert.isTrue(hasExceededLimits); } } diff --git a/extra-tests/classes/RollupRepositoryTests.cls b/extra-tests/classes/RollupRepositoryTests.cls index 5da9796d..56ae8c84 100644 --- a/extra-tests/classes/RollupRepositoryTests.cls +++ b/extra-tests/classes/RollupRepositoryTests.cls @@ -23,6 +23,40 @@ private class RollupRepositoryTests { Assert.isNull(ex); } + @IsTest + static void orderBysEndUpCorrectlySorter() { + List orderBys = new List{ + new RollupOrderBy__mdt(Ranking__c = 3, DeveloperName = 'd'), + new RollupOrderBy__mdt(Ranking__c = 0, DeveloperName = 'a'), + new RollupOrderBy__mdt(Ranking__c = 1, DeveloperName = 'b'), + new RollupOrderBy__mdt(Ranking__c = 2, DeveloperName = 'c') + }; + + orderBys.sort(new RollupRepository.OrderBySorter()); + + Assert.areEqual(0, orderBys.get(0).Ranking__c); + Assert.areEqual(1, orderBys.get(1).Ranking__c); + Assert.areEqual(2, orderBys.get(2).Ranking__c); + Assert.areEqual(3, orderBys.get(3).Ranking__c); + } + + @IsTest + static void orderBysUseDeveloperNameForTieBreaker() { + List orderBys = new List{ + new RollupOrderBy__mdt(Ranking__c = 0, DeveloperName = 'd'), + new RollupOrderBy__mdt(Ranking__c = 0, DeveloperName = 'a'), + new RollupOrderBy__mdt(Ranking__c = 0, DeveloperName = 'b'), + new RollupOrderBy__mdt(Ranking__c = 0, DeveloperName = 'c') + }; + + orderBys.sort(new RollupRepository.OrderBySorter()); + + Assert.areEqual('a', orderBys.get(0).DeveloperName); + Assert.areEqual('b', orderBys.get(1).DeveloperName); + Assert.areEqual('c', orderBys.get(2).DeveloperName); + Assert.areEqual('d', orderBys.get(3).DeveloperName); + } + /** * Serialization proves that we don't get: `System.JSONException: Type unsupported in JSON: common.apex.methods.AccessLevelEnum` */ diff --git a/extra-tests/classes/RollupSObjectUpdaterTests.cls b/extra-tests/classes/RollupSObjectUpdaterTests.cls index 89cc011b..a4b5dcec 100644 --- a/extra-tests/classes/RollupSObjectUpdaterTests.cls +++ b/extra-tests/classes/RollupSObjectUpdaterTests.cls @@ -157,6 +157,25 @@ public class RollupSObjectUpdaterTests { Assert.areEqual('Update failed. First exception on row 0; first error: MISSING_ARGUMENT, Id not specified in an update call: []', message); } + @IsTest + static void nullRollupControlDoesNotPreventExecution() { + RollupLimits.stubbedQueryRows = 50001; + RollupControl__mdt control = new RollupControl__mdt(); + RollupSObjectUpdater updater = new RollupSObjectUpdater(); + + updater.forceSyncUpdate(); + updater.addRollupControl(control); + control = null; + + Exception ex; + try { + updater.doUpdate(new List{ new Account(Id = RollupTestUtils.createId(Account.SObjectType)) }); + } catch (Exception e) { + ex = e; + } + Assert.isNull(ex); + } + public class MockUpdater implements RollupSObjectUpdater.IUpdater { public void performUpdate(List recordsToUpdate, Database.DMLOptions options) { mockUpdatedRecords = recordsToUpdate; diff --git a/extra-tests/classes/RollupTestUtils.cls b/extra-tests/classes/RollupTestUtils.cls index 371ba1d1..d5e72649 100644 --- a/extra-tests/classes/RollupTestUtils.cls +++ b/extra-tests/classes/RollupTestUtils.cls @@ -35,7 +35,7 @@ public class RollupTestUtils { } public static DMLMock loadAccountIdMock(List records) { - Account acc = [SELECT Id FROM Account]; + Account acc = [SELECT Id FROM Account LIMIT 1]; for (SObject record : records) { record.put('ParentId', acc.Id); } diff --git a/extra-tests/classes/RollupTests.cls b/extra-tests/classes/RollupTests.cls index cfc1c918..4769f840 100644 --- a/extra-tests/classes/RollupTests.cls +++ b/extra-tests/classes/RollupTests.cls @@ -1362,6 +1362,7 @@ private class RollupTests { RollupTestUtils.DMLMock mock = RollupTestUtils.loadMock(new List{ somethingElseName }); Rollup.oldRecordsMap = new Map{ somethingElseName.Id => somethingElseName }; Rollup.apexContext = TriggerOperation.BEFORE_DELETE; + Rollup.onlyUseMockMetadata = true; Rollup.rollupMetadata = new List{ new Rollup__mdt( CalcItem__c = 'ContactPointAddress', diff --git a/extra-tests/customMetadata/Rollup.RollupIntegrationParentRecordId.md-meta.xml b/extra-tests/customMetadata/Rollup.RollupIntegrationParentRecordId.md-meta.xml new file mode 100644 index 00000000..3feb6af2 --- /dev/null +++ b/extra-tests/customMetadata/Rollup.RollupIntegrationParentRecordId.md-meta.xml @@ -0,0 +1,161 @@ + + + + false + + CalcItemText__c + ContactPointAddress + + + CalcItemWhereClause__c + Name = 'RollupIntegrationRefresh' + + + CalcItem__c + + + + ChangedFieldsOnCalcItem__c + + + + ConcatDelimiter__c + + + + CurrencyFieldMapping__c + + + + Description__c + + + + FullRecalculationDefaultNumberValue__c + + + + FullRecalculationDefaultStringValue__c + + + + GrandparentRelationshipFieldPath__c + + + + GroupByFields__c + + + + GroupByRowEndDelimiter__c + + + + GroupByRowStartDelimiter__c + + + + IsDistinct__c + false + + + IsFullRecordSet__c + false + + + IsRollupStartedFromParent__c + false + + + IsTableFormatted__c + false + + + LimitAmount__c + + + + LookupFieldOnCalcItemText__c + ParentId + + + LookupFieldOnCalcItem__c + + + + LookupFieldOnLookupObjectText__c + Id + + + LookupFieldOnLookupObject__c + + + + LookupObjectText__c + Account + + + LookupObject__c + + + + OneToManyGrandparentFields__c + + + + OrderByFirstLast__c + + + + RollupControl__c + Org_Defaults + + + RollupFieldOnCalcItemText__c + PreferenceRank + + + RollupFieldOnCalcItem__c + + + + RollupFieldOnLookupObjectText__c + AnnualRevenue + + + RollupFieldOnLookupObject__c + + + + RollupGrouping__c + + + + RollupOperation__c + SUM + + + RollupToUltimateParent__c + false + + + SharingMode__c + + + + ShouldRunWithoutCustomSettingEnabled__c + false + + + SplitConcatDelimiterOnCalcItem__c + false + + + UltimateParentLookup__c + + + diff --git a/extra-tests/flows/Rollup_Integration_Refresh_With_Empty_Collections.flow-meta.xml b/extra-tests/flows/Rollup_Integration_Refresh_With_Empty_Collections.flow-meta.xml new file mode 100644 index 00000000..dbdffe8c --- /dev/null +++ b/extra-tests/flows/Rollup_Integration_Refresh_With_Empty_Collections.flow-meta.xml @@ -0,0 +1,85 @@ + + + + Rollup_ContactPointConsent_Record + + 176 + 134 + RollupFlowBulkProcessor + apex + + T__oldRecordsToRollup + ContactPointAddress + + + T__recordsToRollup + ContactPointAddress + + CurrentTransaction + + calcItemTypeWhenRollupStartedFromParent + + ContactPointAddress + + + + deferProcessing + + false + + + + parentRecordIdForEmptyChildrenCollections + + parentRecordIdForEmptyChildrenCollections + + + + rollupContext + + REFRESH + + + RollupFlowBulkProcessor + true + 1 + + 61.0 + Default + Rollup Integration - Refresh With Empty Collections {!$Flow.CurrentDateTime} + + + BuilderType + + LightningFlowBuilder + + + + CanvasMode + + AUTO_LAYOUT_CANVAS + + + + OriginBuilderType + + LightningFlowBuilder + + + AutoLaunchedFlow + + 50 + 0 + + Rollup_ContactPointConsent_Record + + + Obsolete + + parentRecordIdForEmptyChildrenCollections + String + false + true + false + + diff --git a/package.json b/package.json index 1d9486c6..b22ffbca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "apex-rollup", - "version": "1.6.33", + "version": "1.6.34", "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", diff --git a/plugins/ExtraCodeCoverage/README.md b/plugins/ExtraCodeCoverage/README.md index 64b06f5c..12667eb2 100644 --- a/plugins/ExtraCodeCoverage/README.md +++ b/plugins/ExtraCodeCoverage/README.md @@ -1,11 +1,11 @@ # Extra Code Coverage - + Deploy to Salesforce - + Deploy to Salesforce Sandbox diff --git a/rollup-namespaced/README.md b/rollup-namespaced/README.md index b9a62b9d..c3e0468a 100644 --- a/rollup-namespaced/README.md +++ b/rollup-namespaced/README.md @@ -18,12 +18,12 @@ For more info, see the base `README`. ## Deployment & Setup - + Deploy to Salesforce - + Deploy to Salesforce Sandbox diff --git a/rollup-namespaced/sfdx-project.json b/rollup-namespaced/sfdx-project.json index eb59f208..227100ef 100644 --- a/rollup-namespaced/sfdx-project.json +++ b/rollup-namespaced/sfdx-project.json @@ -4,8 +4,8 @@ "default": true, "package": "apex-rollup-namespaced", "path": "rollup-namespaced/source/rollup", - "versionName": "THIS_MONTH date literal bugfix, grandparent rollup updates", - "versionNumber": "1.1.26.0", + "versionName": "Fixes parentRecordIdForEmptyChildrenCollections flow case", + "versionNumber": "1.1.27.0", "versionDescription": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.", "releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest", "unpackagedMetadata": { @@ -29,6 +29,7 @@ "apex-rollup-namespaced@1.1.23": "04t6g000008ObNSAA0", "apex-rollup-namespaced@1.1.24": "04t6g000008ObbqAAC", "apex-rollup-namespaced@1.1.25": "04t6g000008Obc0AAC", - "apex-rollup-namespaced@1.1.26": "04t6g000008ObeVAAS" + "apex-rollup-namespaced@1.1.26": "04t6g000008ObeVAAS", + "apex-rollup-namespaced@1.1.27": "04t6g000008OfJkAAK" } } diff --git a/rollup/app/lwc/jsconfig.json b/rollup/app/lwc/jsconfig.json index fd2127c2..5328c365 100644 --- a/rollup/app/lwc/jsconfig.json +++ b/rollup/app/lwc/jsconfig.json @@ -1,7 +1,10 @@ { "compilerOptions": { "experimentalDecorators": true, - "baseUrl": "." + "baseUrl": ".", + "paths": { + "c/*": ["*"] + } }, "include": ["**/*", "../../.sfdx/typings/lwc/**/*.d.ts", "../../../.sfdx/typings/lwc/**/*.d.ts"], "typeAcquisition": { diff --git a/rollup/core/classes/Rollup.cls b/rollup/core/classes/Rollup.cls index 4cbfd779..6efa2a06 100644 --- a/rollup/core/classes/Rollup.cls +++ b/rollup/core/classes/Rollup.cls @@ -740,7 +740,7 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje public RollupGrouping__mdt rollupGrouping; public Boolean shouldOverrideNoOp() { - return this.parentRecordIdForEmptyChildrenCollections != null && calcItemTypeWhenRollupStartedFromParent != null; + return this.parentRecordIdForEmptyChildrenCollections != null && this.calcItemTypeWhenRollupStartedFromParent != null; } } @@ -1648,10 +1648,10 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje // it would have been nice if this was an enum! switch on header.changeType { - when 'CREATE', 'GAP_CREATE', 'UNDELETE' { + when 'CREATE', 'UNDELETE' { apexContext = TriggerOperation.AFTER_INSERT; } - when 'UPDATE', 'GAP_UPDATE' { + when 'UPDATE' { apexContext = TriggerOperation.AFTER_UPDATE; } when 'DELETE' { @@ -1850,7 +1850,7 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje RollupLimits.Tester limitTester = new RollupLimits.Tester(control, isContextAsync()); Boolean hasExceededLimits = limitTester.hasExceededLimits() && isDeferralAllowed; if (limitTester.hasExceededLimits() && isDeferralAllowed) { - Boolean isLoggingCurrentlyDisabled = control.IsRollupLoggingEnabled__c == false; + Boolean isLoggingCurrentlyDisabled = control?.IsRollupLoggingEnabled__c == false; if (isLoggingCurrentlyDisabled) { control.IsRollupLoggingEnabled__c = true; RollupLogger.Instance.updateRollupControl(control); @@ -1871,7 +1871,7 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje } private static Rollup__mdt transformFlowInputToRollupMetadata(FlowInput flowInput, String rollupContext, SObjectType sObjectType) { - return getFirstLastMetadata( + Rollup__mdt transformedMeta = getMetadataWithOrderBys( new Rollup__mdt( CalcItem__c = flowInput.isRollupStartedFromParent || String.isNotBlank(flowInput.calcItemTypeWhenRollupStartedFromParent) ? flowInput.calcItemTypeWhenRollupStartedFromParent @@ -1908,6 +1908,8 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje ), flowInput.rollupOperation ); + transformedMeta.RollupControl__c = transformedMeta.RollupControl__r.Id; + return transformedMeta; } private static Boolean isContextAsync() { @@ -2727,7 +2729,7 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje if (isFullRecalcOp(getBaseOperationName(rollupMetadata.RollupOperation__c)) || rollupMetadata.GroupByFields__c != null) { rollupMetadata.IsFullRecordSet__c = true; } - rollupMetadata = getFirstLastMetadata(rollupMetadata, rollupOp.name()); + rollupMetadata = getMetadataWithOrderBys(rollupMetadata, rollupOp.name()); RollupControl__mdt localControl; if (rollupMetadata.RollupControl__c != null) { @@ -2764,7 +2766,7 @@ global without sharing virtual class Rollup implements RollupLogger.ToStringObje return rollupConductor; } - private static Rollup__mdt getFirstLastMetadata(Rollup__mdt meta, String rollupOpName) { + private static Rollup__mdt getMetadataWithOrderBys(Rollup__mdt meta, String rollupOpName) { List replacementOrderBys; if (meta.RollupOrderBys__r.isEmpty() && String.isNotBlank(meta.OrderByFirstLast__c)) { List unparsedOrderBys = meta.OrderByFirstLast__c.split(','); diff --git a/rollup/core/classes/RollupAsyncProcessor.cls b/rollup/core/classes/RollupAsyncProcessor.cls index 04a0309c..f601183b 100644 --- a/rollup/core/classes/RollupAsyncProcessor.cls +++ b/rollup/core/classes/RollupAsyncProcessor.cls @@ -616,7 +616,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme Boolean hasAlreadyBeenQueried = this.cachedQueryToAdditionalCalcItems.containsKey(query); Boolean shouldLimitCalcItemQuery = false; - Integer remainingQueryRowsLeft = 0; + Decimal remainingQueryRowsLeft = 0; Integer outstandingItemCount = 0; List additionalCalcItems = new List(); diff --git a/rollup/core/classes/RollupCurrencyInfo.cls b/rollup/core/classes/RollupCurrencyInfo.cls index e21ffd7c..285696fb 100644 --- a/rollup/core/classes/RollupCurrencyInfo.cls +++ b/rollup/core/classes/RollupCurrencyInfo.cls @@ -234,12 +234,11 @@ public without sharing virtual class RollupCurrencyInfo { } private static String getHashKey(SObject calcItem, Schema.SObjectField opFieldOnCalcItem) { - return '' + calcItem.Id + opFieldOnCalcItem.getDescribe().getName(); + return '' + calcItem.Id + opFieldOnCalcItem.getDescribe().getName() + calcItem.hashCode(); } private static Map getCurrencyMap() { List currencyInfos = new List(); - Map currencyInfoMap = new Map(); if (mockCurrencyData != null) { currencyInfos.addAll(mockCurrencyData); } else { @@ -248,6 +247,7 @@ public without sharing virtual class RollupCurrencyInfo { currencyInfos.addAll((List) JSON.deserialize(JSON.serialize(REPOSITORY.setQuery(query).get()), List.class)); } } + Map currencyInfoMap = new Map(); mapCurrencies(currencyInfoMap, currencyInfos); return currencyInfoMap; } diff --git a/rollup/core/classes/RollupEvaluator.cls b/rollup/core/classes/RollupEvaluator.cls index a0ea0e21..4fe6d602 100644 --- a/rollup/core/classes/RollupEvaluator.cls +++ b/rollup/core/classes/RollupEvaluator.cls @@ -393,7 +393,14 @@ public without sharing abstract class RollupEvaluator implements Rollup.Evaluato String value = words[++index]; if (criteria.endsWithIgnoreCase('in')) { while (value.endsWith(')') == false) { - value += ' ' + words[++index]; + String nextWord = words[++index]; + value += ' ' + nextWord; + if (index + 1 == words.size()) { + break; + } + } + if (value.startsWith('(') && value.endsWith(')') == false) { + value += ')'; } } else if (value.startsWith('\'') && value.endsWith('\'') == false) { String tempVal = value; diff --git a/rollup/core/classes/RollupFlowBulkProcessor.cls b/rollup/core/classes/RollupFlowBulkProcessor.cls index 79ea0bbf..3341bcc3 100644 --- a/rollup/core/classes/RollupFlowBulkProcessor.cls +++ b/rollup/core/classes/RollupFlowBulkProcessor.cls @@ -77,27 +77,23 @@ global without sharing class RollupFlowBulkProcessor { List validInputs = new List(); for (FlowInput flowInput : flowInputs) { Rollup.FlowOutput output = new Rollup.FlowOutput(); - if (flowInput.recordsToRollup?.isEmpty() != false) { + if (flowInput.recordsToRollup?.isEmpty() != false && flowInput.parentRecordIdForEmptyChildrenCollections == null) { output.message = 'No records'; outputs.add(output); } else { List rollupMetadata = Rollup.getMetadataFromCache(Rollup__mdt.SObjectType); // for some reason, lists passed from Flow to Apex report their SObjectType as null. womp. Schema.SObjectType sObjectType = flowInput.recordsToRollup?.get(0).getSObjectType(); + String childName = sObjectType?.getDescribe(SObjectDescribeOptions.DEFERRED).getName(); for (Rollup__mdt meta : rollupMetadata) { if ( meta.IsRollupStartedFromParent__c || sObjectType != null && String.isNotBlank(meta.GrandparentRelationshipFieldPath__c) && Rollup.getPartOfGrandparentChain(meta, sObjectType) != null ) { - flowInput.calcItemTypeWhenRollupStartedFromParent = flowInput.calcItemTypeWhenRollupStartedFromParent != null - ? flowInput.calcItemTypeWhenRollupStartedFromParent - : meta.CalcItem__c; + flowInput.calcItemTypeWhenRollupStartedFromParent = flowInput.calcItemTypeWhenRollupStartedFromParent ?? meta.CalcItem__c; } - if ( - flowInput.recordsToRollup == null || - meta.CalcItem__c == sObjectType?.getDescribe(SObjectDescribeOptions.DEFERRED).getName() || - flowInput.calcItemTypeWhenRollupStartedFromParent == meta.CalcItem__c - ) { + Boolean isMatchingParentSide = flowInput.calcItemTypeWhenRollupStartedFromParent == meta.CalcItem__c; + if ((flowInput.recordsToRollup?.isEmpty() != false && isMatchingParentSide) || meta.CalcItem__c == childName || isMatchingParentSide) { Rollup.FlowInput input = new Rollup.FlowInput(); validInputs.add(input); // pertinent fields from CMDT (can be overridden by optional flow properties) diff --git a/rollup/core/classes/RollupLimits.cls b/rollup/core/classes/RollupLimits.cls index d833b2bc..2c6832e7 100644 --- a/rollup/core/classes/RollupLimits.cls +++ b/rollup/core/classes/RollupLimits.cls @@ -7,7 +7,6 @@ public without sharing class RollupLimits { private static Integer stubAsyncTimeoutInterval; private static final Integer SYNC_TIMEOUT_INTERVAL_MS = 1500; - private static final Integer LIMIT_HEAP_SIZE = Limits.getLimitHeapSize(); private static final Integer LIMIT_QUERY_ROWS = 50000; private static final Integer ASYNC_TIMEOUT_INTERVAL_MS { @@ -39,7 +38,6 @@ public without sharing class RollupLimits { public final Boolean hasExceededQueryNumberLimit; public final Boolean hasExceededQueryRowLimit; - public final Boolean hasExceededHeapSizeLimit; public final Boolean hasExceededDMLRowLimit; public final Boolean hasExceededCPUTimeLimit; @@ -48,7 +46,6 @@ public without sharing class RollupLimits { this.isRunningAsync = isRunningAsync; this.hasExceededQueryNumberLimit = this.control?.MaxNumberOfQueries__c < Limits.getQueries(); this.hasExceededQueryRowLimit = this.getRemainingQueryRows() < 0; - this.hasExceededHeapSizeLimit = (LIMIT_HEAP_SIZE - 2000000) < Limits.getHeapSize(); this.hasExceededDMLRowLimit = this.control?.MaxParentRowsUpdatedAtOnce__c < Limits.getDmlRows(); Integer intervalTillTimeout = this.isRunningAsync ? ASYNC_TIMEOUT_INTERVAL_MS : SYNC_TIMEOUT_INTERVAL_MS; @@ -56,7 +53,6 @@ public without sharing class RollupLimits { this.hasExceededOverallLimits = this.hasExceededQueryNumberLimit || this.hasExceededQueryRowLimit || - this.hasExceededHeapSizeLimit || this.hasExceededDMLRowLimit || this.hasExceededCPUTimeLimit; } @@ -65,10 +61,10 @@ public without sharing class RollupLimits { return this.hasExceededOverallLimits; } - public Integer getRemainingQueryRows() { + public Decimal getRemainingQueryRows() { Integer queryRowsUsed = stubbedQueryRows ?? Limits.getQueryRows(); - Integer remainingQueryRows = (this.control.MaxQueryRows__c?.intValue() ?? LIMIT_QUERY_ROWS) - queryRowsUsed; - return remainingQueryRows > 0 ? remainingQueryRows : 0; + Decimal maxQueryRows = this.control.MaxQueryRows__c != null && this.control.MaxQueryRows__c <= LIMIT_QUERY_ROWS ? this.control.MaxQueryRows__c : LIMIT_QUERY_ROWS; + return maxQueryRows - queryRowsUsed; } public Boolean hasExceededOrgAsyncLimit() { diff --git a/rollup/core/classes/RollupLogger.cls b/rollup/core/classes/RollupLogger.cls index 7c054c18..d5e43bdd 100644 --- a/rollup/core/classes/RollupLogger.cls +++ b/rollup/core/classes/RollupLogger.cls @@ -1,7 +1,7 @@ global without sharing virtual class RollupLogger implements ILogger { @TestVisible // this gets updated via the pipeline as the version number gets incremented - private static final String CURRENT_VERSION_NUMBER = 'v1.6.33'; + private static final String CURRENT_VERSION_NUMBER = 'v1.6.34'; private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG; private static final RollupPlugin PLUGIN = new RollupPlugin(); diff --git a/rollup/core/classes/RollupRepository.cls b/rollup/core/classes/RollupRepository.cls index aeec4f56..471dbe9a 100644 --- a/rollup/core/classes/RollupRepository.cls +++ b/rollup/core/classes/RollupRepository.cls @@ -82,59 +82,59 @@ public without sharing class RollupRepository implements RollupLogger.ToStringOb @SuppressWarnings('PMD.ApexCRUDViolation') public static List getRollupMetadata() { List matchingMetadata = [ - SELECT - MasterLabel, - DeveloperName, - LookupObject__c, - LookupObjectText__c, - LookupObject__r.QualifiedApiName, - CalcItem__c, - CalcItemText__c, - CalcItem__r.QualifiedApiName, - RollupFieldOnCalcItem__c, - RollupFieldOnCalcItemText__c, - RollupFieldOnCalcItem__r.QualifiedApiName, - LookupFieldOnCalcItem__c, - LookupFieldOnCalcItemText__c, - LookupFieldOnCalcItem__r.QualifiedApiName, - LookupFieldOnLookupObject__c, - LookupFieldOnLookupObjectText__c, - LookupFieldOnLookupObject__r.QualifiedApiName, - RollupFieldOnLookupObject__c, - RollupFieldOnLookupObjectText__c, - RollupFieldOnLookupObject__r.QualifiedApiName, - UltimateParentLookup__c, - UltimateParentLookup__r.QualifiedApiName, - CalcItemWhereClause__c, - ChangedFieldsOnCalcItem__c, - ConcatDelimiter__c, - CurrencyFieldMapping__c, - FullRecalculationDefaultNumberValue__c, - FullRecalculationDefaultStringValue__c, - GrandparentRelationshipFieldPath__c, - GroupByFields__c, - GroupByRowEndDelimiter__c, - GroupByRowStartDelimiter__c, - IsDistinct__c, - IsFullRecordSet__c, - IsRollupStartedFromParent__c, - IsTableFormatted__c, - LimitAmount__c, - OneToManyGrandparentFields__c, - OrderByFirstLast__c, - RollupControl__c, - RollupOperation__c, - RollupToUltimateParent__c, - SharingMode__c, - ShouldRunWithoutCustomSettingEnabled__c, - SplitConcatDelimiterOnCalcItem__c, - (SELECT Id, FieldName__c, NullSortOrder__c, Ranking__c, SortOrder__c FROM RollupOrderBys__r ORDER BY Ranking__c, DeveloperName), - RollupGrouping__r.Id, - RollupGrouping__r.RollupOperation__c - FROM Rollup__mdt - WHERE RollupControl__r.ShouldAbortRun__c = FALSE - ORDER BY LookupObject__c - ]; + SELECT + MasterLabel, + DeveloperName, + LookupObject__c, + LookupObjectText__c, + LookupObject__r.QualifiedApiName, + CalcItem__c, + CalcItemText__c, + CalcItem__r.QualifiedApiName, + RollupFieldOnCalcItem__c, + RollupFieldOnCalcItemText__c, + RollupFieldOnCalcItem__r.QualifiedApiName, + LookupFieldOnCalcItem__c, + LookupFieldOnCalcItemText__c, + LookupFieldOnCalcItem__r.QualifiedApiName, + LookupFieldOnLookupObject__c, + LookupFieldOnLookupObjectText__c, + LookupFieldOnLookupObject__r.QualifiedApiName, + RollupFieldOnLookupObject__c, + RollupFieldOnLookupObjectText__c, + RollupFieldOnLookupObject__r.QualifiedApiName, + UltimateParentLookup__c, + UltimateParentLookup__r.QualifiedApiName, + CalcItemWhereClause__c, + ChangedFieldsOnCalcItem__c, + ConcatDelimiter__c, + CurrencyFieldMapping__c, + FullRecalculationDefaultNumberValue__c, + FullRecalculationDefaultStringValue__c, + GrandparentRelationshipFieldPath__c, + GroupByFields__c, + GroupByRowEndDelimiter__c, + GroupByRowStartDelimiter__c, + IsDistinct__c, + IsFullRecordSet__c, + IsRollupStartedFromParent__c, + IsTableFormatted__c, + LimitAmount__c, + OneToManyGrandparentFields__c, + OrderByFirstLast__c, + RollupControl__c, + RollupOperation__c, + RollupToUltimateParent__c, + SharingMode__c, + ShouldRunWithoutCustomSettingEnabled__c, + SplitConcatDelimiterOnCalcItem__c, + (SELECT Id, FieldName__c, NullSortOrder__c, Ranking__c, SortOrder__c FROM RollupOrderBys__r), + RollupGrouping__r.Id, + RollupGrouping__r.RollupOperation__c + FROM Rollup__mdt + WHERE RollupControl__r.ShouldAbortRun__c = FALSE + ORDER BY LookupObject__c + ]; // we have to do transforms on the Entity Definition fields because custom objects/custom fields // have references that otherwise won't work with the rest of the code for (Rollup__mdt meta : matchingMetadata) { @@ -149,6 +149,7 @@ public without sharing class RollupRepository implements RollupLogger.ToStringOb meta.GroupByRowStartDelimiter__c = meta.GroupByRowStartDelimiter__c?.unescapeJava(); meta.SharingMode__c = meta.SharingMode__c ?? RollupMetaPicklists.SharingMode.SystemLevel; meta.UltimateParentLookup__c = meta.UltimateParentLookup__r.QualifiedApiName; + meta.RollupOrderBys__r.sort(new OrderBySorter()); } return matchingMetadata; @@ -161,4 +162,34 @@ public without sharing class RollupRepository implements RollupLogger.ToStringOb private System.AccessLevel transformPermissionLevel(RunAsMode currentRunAs) { return currentRunAs == RunAsMode.USER ? System.AccessLevel.USER_MODE : System.AccessLevel.SYSTEM_MODE; } + + public class OrderBySorter implements System.Comparator { + private final List sortFields = new List{ RollupOrderBy__mdt.Ranking__c, RollupOrderBy__mdt.DeveloperName }; + public Integer compare(RollupOrderBy__mdt first, RollupOrderBy__mdt second) { + Integer returnValue = 0; + List localSortFields = new List(this.sortFields); + while (returnValue == 0 && localSortFields.isEmpty() == false) { + Schema.SObjectField field = localSortFields.remove(0); + Object firstSortValue = first.get(field); + Object secondSortValue = second.get(field); + + if (firstSortValue instanceof Decimal) { + returnValue = this.getDecimalSortedValue((Decimal) firstSortValue, (Decimal) secondSortValue); + } else if (firstSortValue instanceof String) { + returnValue = ((String) firstSortValue).compareTo((String) secondSortValue); + } + } + return returnValue; + } + + private Integer getDecimalSortedValue(Decimal first, Decimal second) { + Integer returnValue = 0; + if (first > second) { + returnValue = 1; + } else if (first < second) { + returnValue = -1; + } + return returnValue; + } + } } diff --git a/rollup/core/objects/Rollup__mdt/validationRules/Rollup_Field_Parent_Required.validationRule-meta.xml b/rollup/core/objects/Rollup__mdt/validationRules/Rollup_Field_Parent_Required.validationRule-meta.xml index ab1dc811..0669e3fb 100644 --- a/rollup/core/objects/Rollup__mdt/validationRules/Rollup_Field_Parent_Required.validationRule-meta.xml +++ b/rollup/core/objects/Rollup__mdt/validationRules/Rollup_Field_Parent_Required.validationRule-meta.xml @@ -1,4 +1,4 @@ - + Rollup_Field_Parent_Required true diff --git a/sfdx-project.json b/sfdx-project.json index 1acb6d31..2ae33484 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -5,8 +5,8 @@ "package": "apex-rollup", "path": "rollup", "scopeProfiles": true, - "versionName": "THIS_MONTH date literal bugfix, grandparent rollup updates", - "versionNumber": "1.6.33.0", + "versionName": "Fixes parentRecordIdForEmptyChildrenCollections flow case", + "versionNumber": "1.6.34.0", "versionDescription": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.", "releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest", "unpackagedMetadata": { @@ -69,12 +69,12 @@ "package": "Apex Rollup - Extra Code Coverage", "versionDescription": "This plugin adds code coverage for Apex Rollup if you need additional code coverage for the base package", "versionName": "Updating code coverage", - "versionNumber": "0.0.23.0", + "versionNumber": "0.0.24.0", "default": false, "scopeProfiles": true, "dependencies": [ { - "package": "apex-rollup@1.6.30" + "package": "apex-rollup@1.6.34" } ] }, @@ -91,21 +91,22 @@ "Apex Rollup - Custom Logger@0.0.11-0": "04t6g000008SirvAAC", "Apex Rollup - Custom Logger@0.0.12-0": "04t6g000008SjqIAAS", "Apex Rollup - Extra Code Coverage": "0Ho6g000000GnCWCA0", - "Apex Rollup - Extra Code Coverage@0.0.22": "04t6g000008ObCiAAK", "Apex Rollup - Extra Code Coverage@0.0.23": "04t6g000008ObVmAAK", + "Apex Rollup - Extra Code Coverage@0.0.24": "04t6g000008ObgCAAS", "Apex Rollup - Nebula Logger": "0Ho6g000000Gn8PCAS", "Apex Rollup - Nebula Logger@0.0.7-0": "04t6g000008b0O7AAI", "Apex Rollup - Nebula Logger@0.0.8-0": "04t6g000007zM6tAAE", "Apex Rollup - Rollup Callback": "0Ho6g000000GnA1CAK", "Apex Rollup - Rollup Callback@0.0.2-0": "04t6g000008ShztAAC", "Apex Rollup - Rollup Callback@0.0.3-0": "04t6g000008Sis0AAC", - "Nebula Logger - Core@4.8.0-NEXT-ignore-origin-method": "04t5Y0000015lslQAA", + "Nebula Logger - Core@4.14.4-optionally-auto-call-lightning-logger-lwc": "04t5Y0000015oRNQAY", "apex-rollup": "0Ho6g000000TNcOCAW", "apex-rollup@1.6.28": "04t6g000008ObN8AAK", "apex-rollup@1.6.29": "04t6g000008ObNNAA0", "apex-rollup@1.6.30": "04t6g000008ObVhAAK", "apex-rollup@1.6.31": "04t6g000008ObblAAC", "apex-rollup@1.6.32": "04t6g000008ObbvAAC", - "apex-rollup@1.6.33": "04t6g000008ObeQAAS" + "apex-rollup@1.6.33": "04t6g000008ObeQAAS", + "apex-rollup@1.6.34": "04t6g000008OfJfAAK" } }