From 45cd658e2098da32579f2bf0c3271e440a068428 Mon Sep 17 00:00:00 2001 From: Heber Date: Sun, 1 Sep 2024 17:16:03 -0600 Subject: [PATCH 1/2] Enable rollup bypass via Custom Setting, Dev API, or per-rollup Cust Perm --- dlrs/main/classes/BypassHandler.cls | 26 +++--- dlrs/main/classes/BypassHandlerTest.cls | 83 +++++++++++++------ dlrs/main/classes/RollupEditorController.cls | 4 + dlrs/main/classes/RollupService.cls | 27 +++++- dlrs/main/classes/RollupServiceTest.cls | 54 ++++++------ dlrs/main/classes/RollupSummariesSelector.cls | 1 + dlrs/main/classes/RollupSummary.cls | 15 ++++ dlrs/main/classes/RollupSummaryTest.cls | 23 +++++ .../classes/RollupSummaryTest.cls-meta.xml | 5 ++ dlrs/main/classes/Utilities.cls | 18 ++++ dlrs/main/classes/UtilitiesTest.cls | 21 +++++ dlrs/main/classes/UtilitiesTest.cls-meta.xml | 5 ++ ...okup Rollup Summary Layout.layout-meta.xml | 4 + dlrs/main/lwc/rollupEditor/rollupEditor.html | 23 ++++- dlrs/main/lwc/rollupEditor/rollupEditor.js | 3 +- .../DisableDLRSGlobally__c.field-meta.xml | 11 +++ .../BypassPermissionApiName__c.field-meta.xml | 14 ++++ 17 files changed, 268 insertions(+), 69 deletions(-) create mode 100644 dlrs/main/classes/RollupSummaryTest.cls create mode 100644 dlrs/main/classes/RollupSummaryTest.cls-meta.xml create mode 100644 dlrs/main/classes/UtilitiesTest.cls create mode 100644 dlrs/main/classes/UtilitiesTest.cls-meta.xml create mode 100644 dlrs/main/objects/DeclarativeLookupRollupSummaries__c/fields/DisableDLRSGlobally__c.field-meta.xml create mode 100644 dlrs/main/objects/LookupRollupSummary2__mdt/fields/BypassPermissionApiName__c.field-meta.xml diff --git a/dlrs/main/classes/BypassHandler.cls b/dlrs/main/classes/BypassHandler.cls index 5accb586..efd0c72e 100644 --- a/dlrs/main/classes/BypassHandler.cls +++ b/dlrs/main/classes/BypassHandler.cls @@ -31,30 +31,31 @@ * The bypass and removebypass method return the result of the default Set object operations. **/ public without sharing class BypassHandler { - private static Set bypassedRollups; + private static Set bypassedRollups = new Set(); + private static Boolean bypassAll = false; /** - * Initialize the set if necessary for adding rollups to the bypass list. + * Checks if the rollup is bypassed or not. Returns true if it is. False otherwise. + * Could be bypassed by custom setting, bypass all, or specific named bypass */ - private static void init() { - if (bypassedRollups == null) { - bypassedRollups = new Set(); - } + public static Boolean isBypassed(String handlerName) { + return DeclarativeLookupRollupSummaries__c.getInstance() + .DisableDLRSGlobally__c == true || + bypassAll || + bypassedRollups.contains(handlerName); } /** - * Checks if the rollup is bypassed or not. Returns true if it is. False otherwise. + * Sets a global bypass value, if true all rollups will be disabled for execution */ - public static Boolean isBypassed(String handlerName) { - return bypassedRollups != null && bypassedRollups.contains(handlerName); + public static void setBypassAll(Boolean val) { + bypassAll = val; } /** * Adds a rollup to the bypassed rollups list. */ public static Boolean bypass(String handlerName) { - init(); - if (handlerName != null) { System.debug( LoggingLevel.INFO, @@ -75,7 +76,7 @@ public without sharing class BypassHandler { * Clears the bypass for a single rollup. */ public static Boolean clearBypass(String handlerName) { - if (bypassedRollups != null && handlerName != null) { + if (handlerName != null) { System.debug( LoggingLevel.INFO, 'DLRS trigger handler is no longer bypassed: ' + handlerName @@ -95,6 +96,7 @@ public without sharing class BypassHandler { * Clears all bypasses, if any. */ public static void clearAllBypasses() { + bypassAll = false; if (bypassedRollups != null) { bypassedRollups.clear(); } diff --git a/dlrs/main/classes/BypassHandlerTest.cls b/dlrs/main/classes/BypassHandlerTest.cls index 52d69340..866e5614 100644 --- a/dlrs/main/classes/BypassHandlerTest.cls +++ b/dlrs/main/classes/BypassHandlerTest.cls @@ -28,54 +28,85 @@ private class BypassHandlerTest { @IsTest static void testApi() { String rollupUniqueName = 'SampleRollup'; - Boolean bypassResult; - - Test.startTest(); - System.assertEquals( - false, + Assert.isFalse( BypassHandler.isBypassed(rollupUniqueName), 'The rollup should not be bypassed yet.' ); - bypassResult = BypassHandler.bypass(rollupUniqueName); - System.assert( - bypassResult, + + Assert.isTrue( + BypassHandler.bypass(rollupUniqueName), 'Should have modified the bypassed rollups set.' ); - System.assertEquals( - true, + Assert.isTrue( BypassHandler.isBypassed(rollupUniqueName), 'The rollup should be bypassed.' ); - bypassResult = BypassHandler.clearBypass(rollupUniqueName); - System.assert( - bypassResult, + + Assert.isTrue( + BypassHandler.clearBypass(rollupUniqueName), 'Should have modified the bypassed rollups set.' ); - System.assertEquals( - false, + Assert.isFalse( BypassHandler.isBypassed(rollupUniqueName), 'The rollup should not be bypassed anymore.' ); BypassHandler.bypass(rollupUniqueName); BypassHandler.clearAllBypasses(); - System.assertEquals( - false, + Assert.isFalse( BypassHandler.isBypassed(rollupUniqueName), 'The rollup should not be bypassed anymore.' ); - bypassResult = BypassHandler.bypass(null); - System.assertEquals( - false, - bypassResult, + Assert.isFalse( + BypassHandler.bypass(null), 'Should return "false" for a null rollup name.' ); - bypassResult = BypassHandler.clearBypass(null); - System.assertEquals( - false, - bypassResult, + + Assert.isFalse( + BypassHandler.clearBypass(null), 'Should return "false" for a null rollup name.' ); - Test.stopTest(); + + BypassHandler.setBypassAll(true); + Assert.isTrue( + BypassHandler.isBypassed(rollupUniqueName), + 'Should return "true" for all rollup names.' + ); + Assert.isTrue( + BypassHandler.isBypassed('new name'), + 'Should return "true" for all rollup names.' + ); + BypassHandler.setBypassAll(false); + + Assert.isFalse( + BypassHandler.isBypassed(rollupUniqueName), + 'Should return "false" for all rollup names.' + ); + Assert.isFalse( + BypassHandler.isBypassed('new name'), + 'Should return "false" for all rollup names.' + ); + BypassHandler.setBypassAll(true); + Assert.isTrue( + BypassHandler.isBypassed('new name'), + 'Should return "true" for all rollup names.' + ); + BypassHandler.clearAllBypasses(); + Assert.isFalse( + BypassHandler.isBypassed('new name'), + 'Should return "false" for all rollup names.' + ); + } + + @IsTest + static void testCustomSettingDisable() { + String rollupUniqueName = 'Rollup1'; + Assert.isFalse(BypassHandler.isBypassed(rollupUniqueName)); + + DeclarativeLookupRollupSummaries__c settings = DeclarativeLookupRollupSummaries__c.getInstance(); + settings.DisableDLRSGlobally__c = true; + insert settings; + + Assert.isTrue(BypassHandler.isBypassed(rollupUniqueName)); } } diff --git a/dlrs/main/classes/RollupEditorController.cls b/dlrs/main/classes/RollupEditorController.cls index cbef278b..e35018e1 100644 --- a/dlrs/main/classes/RollupEditorController.cls +++ b/dlrs/main/classes/RollupEditorController.cls @@ -271,6 +271,8 @@ public with sharing class RollupEditorController { @AuraEnabled public String aggregateResultField; @AuraEnabled + public String bypassPermissionApiName; + @AuraEnabled public String calculationMode; @AuraEnabled public String calculationSharingMode; @@ -309,6 +311,7 @@ public with sharing class RollupEditorController { this.aggregateAllRows = record.AggregateAllRows__c; this.aggregateOperation = record.AggregateOperation__c; this.aggregateResultField = record.AggregateResultField__c; + this.bypassPermissionApiName = record.BypassPermissionApiName__c; this.calculationMode = record.CalculationMode__c; this.calculationSharingMode = record.CalculationSharingMode__c; this.childObject = record.ChildObject__c; @@ -335,6 +338,7 @@ public with sharing class RollupEditorController { record.AggregateAllRows__c = this.aggregateAllRows; record.AggregateOperation__c = this.aggregateOperation; record.AggregateResultField__c = this.aggregateResultField; + record.BypassPermissionApiName__c = this.bypassPermissionApiName; record.CalculationMode__c = this.calculationMode; record.CalculationSharingMode__c = this.calculationSharingMode; record.ChildObject__c = this.childObject; diff --git a/dlrs/main/classes/RollupService.cls b/dlrs/main/classes/RollupService.cls index e2fe5b74..cbd10777 100644 --- a/dlrs/main/classes/RollupService.cls +++ b/dlrs/main/classes/RollupService.cls @@ -381,6 +381,13 @@ global with sharing class RollupService { return BypassHandler.bypass(rollupName); } + /** + * Allow the bypass of all rollups for this transaction, can be cleared with "clearAllBypasses" method + */ + global static void bypassAll() { + BypassHandler.setBypassAll(true); + } + /** * Clears the bypass of a rollup, given its unique name. */ @@ -964,7 +971,10 @@ global with sharing class RollupService { // this avoids having to re-parse RelationshipCriteria & OrderBy fields during field change detection Map> fieldsInvolvedInLookup = new Map>(); for (RollupSummary lookup : lookups) { - if (BypassHandler.isBypassed(lookup.UniqueName)) { + if ( + Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) || + BypassHandler.isBypassed(lookup.UniqueName) + ) { continue; } @@ -1099,7 +1109,10 @@ global with sharing class RollupService { // Build a revised list of lookups to process that includes only where fields used in the rollup have changed List lookupsToProcess = new List(); for (RollupSummary lookup : lookups) { - if (BypassHandler.isBypassed(lookup.UniqueName)) { + if ( + Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) || + BypassHandler.isBypassed(lookup.UniqueName) + ) { continue; } @@ -1134,7 +1147,10 @@ global with sharing class RollupService { : existingRecords; for (SObject childRecord : recordsToProcess.values()) { for (RollupSummary lookup : lookups) { - if (BypassHandler.isBypassed(lookup.UniqueName)) { + if ( + Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) || + BypassHandler.isBypassed(lookup.UniqueName) + ) { continue; } @@ -1179,7 +1195,10 @@ global with sharing class RollupService { List runnowLookups = new List(); List scheduledItems = new List(); for (RollupSummary lookup : lookups) { - if (BypassHandler.isBypassed(lookup.UniqueName)) { + if ( + Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) || + BypassHandler.isBypassed(lookup.UniqueName) + ) { continue; } diff --git a/dlrs/main/classes/RollupServiceTest.cls b/dlrs/main/classes/RollupServiceTest.cls index 23f8ad48..080fd0c7 100644 --- a/dlrs/main/classes/RollupServiceTest.cls +++ b/dlrs/main/classes/RollupServiceTest.cls @@ -2623,54 +2623,58 @@ private with sharing class RollupServiceTest { @IsTest static void testBypassApi() { String rollupUniqueName = 'SampleRollup'; - Boolean bypassResult; - Test.startTest(); - System.assertEquals( - false, + Assert.isFalse( RollupService.isBypassed(rollupUniqueName), 'The rollup should not be bypassed yet.' ); - bypassResult = RollupService.bypass(rollupUniqueName); - System.assert( - bypassResult, + + Assert.isTrue( + RollupService.bypass(rollupUniqueName), 'Should have modified the bypassed rollups set.' ); - System.assertEquals( - true, + Assert.isTrue( RollupService.isBypassed(rollupUniqueName), 'The rollup should be bypassed.' ); - bypassResult = RollupService.clearBypass(rollupUniqueName); - System.assert( - bypassResult, + + Assert.isTrue( + RollupService.clearBypass(rollupUniqueName), 'Should have modified the bypassed rollups set.' ); - System.assertEquals( - false, + Assert.isFalse( RollupService.isBypassed(rollupUniqueName), 'The rollup should not be bypassed anymore.' ); RollupService.bypass(rollupUniqueName); RollupService.clearAllBypasses(); - System.assertEquals( - false, + Assert.isFalse( RollupService.isBypassed(rollupUniqueName), 'The rollup should not be bypassed anymore.' ); - bypassResult = RollupService.bypass(null); - System.assertEquals( - false, - bypassResult, + Assert.isFalse( + RollupService.bypass(null), 'Should return "false" for a null rollup name.' ); - bypassResult = RollupService.clearBypass(null); - System.assertEquals( - false, - bypassResult, + Assert.isFalse( + RollupService.clearBypass(null), 'Should return "false" for a null rollup name.' ); - Test.stopTest(); + + RollupService.bypassAll(); + Assert.isTrue( + RollupService.isBypassed(rollupUniqueName), + 'Should return "true" for all rollup names.' + ); + Assert.isTrue( + RollupService.isBypassed('new name'), + 'Should return "true" for all rollup names.' + ); + RollupService.clearAllBypasses(); + Assert.isFalse( + RollupService.isBypassed(rollupUniqueName), + 'Should return "false" for all rollup names.' + ); } } diff --git a/dlrs/main/classes/RollupSummariesSelector.cls b/dlrs/main/classes/RollupSummariesSelector.cls index ad44b875..d31f44e2 100644 --- a/dlrs/main/classes/RollupSummariesSelector.cls +++ b/dlrs/main/classes/RollupSummariesSelector.cls @@ -279,6 +279,7 @@ public class RollupSummariesSelector { LookupRollupSummary2__mdt.Active__c, LookupRollupSummary2__mdt.AggregateOperation__c, LookupRollupSummary2__mdt.AggregateResultField__c, + LookupRollupSummary2__mdt.BypassPermissionApiName__c, LookupRollupSummary2__mdt.CalculationMode__c, LookupRollupSummary2__mdt.ChildObject__c, LookupRollupSummary2__mdt.ConcatenateDelimiter__c, diff --git a/dlrs/main/classes/RollupSummary.cls b/dlrs/main/classes/RollupSummary.cls index 4aaf8fed..9bbc4141 100644 --- a/dlrs/main/classes/RollupSummary.cls +++ b/dlrs/main/classes/RollupSummary.cls @@ -100,6 +100,21 @@ public class RollupSummary { } } + public String BypassCustPermApiName { + get { + if (Record instanceof LookupRollupSummary2__mdt) { + return (String) Record.get('BypassPermissionApiName__c'); + } else { + return null; + } + } + set { + if (Record instanceof LookupRollupSummary2__mdt) { + Record.put('BypassPermissionApiName__c', value); + } + } + } + public String CalculationMode { get { return (String) Record.get('CalculationMode__c'); diff --git a/dlrs/main/classes/RollupSummaryTest.cls b/dlrs/main/classes/RollupSummaryTest.cls new file mode 100644 index 00000000..b6cbd011 --- /dev/null +++ b/dlrs/main/classes/RollupSummaryTest.cls @@ -0,0 +1,23 @@ +@IsTest +public class RollupSummaryTest { + @IsTest + static void testBypassCustPermApiName() { + LookupRollupSummary2__mdt rollup = new LookupRollupSummary2__mdt(); + rollup.BypassPermissionApiName__c = null; + RollupSummary rs = new RollupSummary(rollup); + Assert.areEqual(null, rs.BypassCustPermApiName); + rollup.BypassPermissionApiName__c = 'Rollup1'; + rs = new RollupSummary(rollup); + Assert.areEqual('Rollup1', rs.BypassCustPermApiName); + + rs.BypassCustPermApiName = 'Rollup2'; + Assert.areEqual('Rollup2', rs.BypassCustPermApiName); + + LookupRollupSummary__c rollupCO = new LookupRollupSummary__c(); + rs = new RollupSummary(rollupCO); + Assert.areEqual(null, rs.BypassCustPermApiName); + rs.BypassCustPermApiName = 'Rollup1'; + // we're not building support in the Custom Object rollup versions, setting the value is ignored + Assert.areEqual(null, rs.BypassCustPermApiName); + } +} diff --git a/dlrs/main/classes/RollupSummaryTest.cls-meta.xml b/dlrs/main/classes/RollupSummaryTest.cls-meta.xml new file mode 100644 index 00000000..7d5f9e8a --- /dev/null +++ b/dlrs/main/classes/RollupSummaryTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + \ No newline at end of file diff --git a/dlrs/main/classes/Utilities.cls b/dlrs/main/classes/Utilities.cls index 9f342e50..16a23cf3 100644 --- a/dlrs/main/classes/Utilities.cls +++ b/dlrs/main/classes/Utilities.cls @@ -85,6 +85,24 @@ public class Utilities { return orderByFields; } + /** + * permissionNames is null or comma-separated list of Custom Permissions + * returns `true` if user has any of those custom permissions + */ + public static Boolean userHasCustomPermission(String permissionNames) { + if (String.isBlank(permissionNames)) { + return false; + } + + for (String permName : permissionNames.split(',')) { + if (FeatureManagement.checkPermission(permName.trim())) { + return true; + } + } + + return false; + } + // Regular expression for Order By Clause // Case-Insensitive pattern // Group 1 - Field Name (required) diff --git a/dlrs/main/classes/UtilitiesTest.cls b/dlrs/main/classes/UtilitiesTest.cls new file mode 100644 index 00000000..9f47be43 --- /dev/null +++ b/dlrs/main/classes/UtilitiesTest.cls @@ -0,0 +1,21 @@ +@IsTest +public class UtilitiesTest { + @IsTest + static void testUserHasCustomPermission() { + Assert.areEqual(false, Utilities.userHasCustomPermission(null)); + Assert.areEqual(false, Utilities.userHasCustomPermission('madeup_name')); + Assert.areEqual( + false, + Utilities.userHasCustomPermission('madeup_name,name2 , name3,name4') + ); + // TODO: add custom perm and perm set assigned to working user for tests but not add to package + // Assert.areEqual( + // true, + // Utilities.userHasCustomPermission('DLRSLimitedDisable') + // ); + // Assert.areEqual( + // true, + // Utilities.userHasCustomPermission('rollup1, DLRSLimitedDisable ,rollup2') + // ); + } +} diff --git a/dlrs/main/classes/UtilitiesTest.cls-meta.xml b/dlrs/main/classes/UtilitiesTest.cls-meta.xml new file mode 100644 index 00000000..7d5f9e8a --- /dev/null +++ b/dlrs/main/classes/UtilitiesTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active + \ No newline at end of file diff --git a/dlrs/main/layouts/LookupRollupSummary2__mdt-Lookup Rollup Summary Layout.layout-meta.xml b/dlrs/main/layouts/LookupRollupSummary2__mdt-Lookup Rollup Summary Layout.layout-meta.xml index 421986aa..bc10cea0 100644 --- a/dlrs/main/layouts/LookupRollupSummary2__mdt-Lookup Rollup Summary Layout.layout-meta.xml +++ b/dlrs/main/layouts/LookupRollupSummary2__mdt-Lookup Rollup Summary Layout.layout-meta.xml @@ -169,6 +169,10 @@ Edit TestCodeSeeAllData__c + + Edit + BypassPermissionApiName__c + diff --git a/dlrs/main/lwc/rollupEditor/rollupEditor.html b/dlrs/main/lwc/rollupEditor/rollupEditor.html index ba05977c..8548eb1a 100644 --- a/dlrs/main/lwc/rollupEditor/rollupEditor.html +++ b/dlrs/main/lwc/rollupEditor/rollupEditor.html @@ -465,7 +465,11 @@

errors={errors.testCodeParent} > - + errors={errors.testCodeSeeAllData} > + + + + diff --git a/dlrs/main/lwc/rollupEditor/rollupEditor.js b/dlrs/main/lwc/rollupEditor/rollupEditor.js index 9909beaf..fe471590 100644 --- a/dlrs/main/lwc/rollupEditor/rollupEditor.js +++ b/dlrs/main/lwc/rollupEditor/rollupEditor.js @@ -439,7 +439,8 @@ export default class RollupEditor extends LightningModal { "concatenateDelimiter", "testCode", "testCodeParent", - "testCodeSeeAllData" + "testCodeSeeAllData", + "bypassPermissionApiName" ]; let isValid = true; diff --git a/dlrs/main/objects/DeclarativeLookupRollupSummaries__c/fields/DisableDLRSGlobally__c.field-meta.xml b/dlrs/main/objects/DeclarativeLookupRollupSummaries__c/fields/DisableDLRSGlobally__c.field-meta.xml new file mode 100644 index 00000000..cdb19433 --- /dev/null +++ b/dlrs/main/objects/DeclarativeLookupRollupSummaries__c/fields/DisableDLRSGlobally__c.field-meta.xml @@ -0,0 +1,11 @@ + + + DisableDLRSGlobally__c + false + Turns off all DLRS calculations + false + Disable DLRS calculations, useful for bulk loading or other large-scale actions + + false + Checkbox + diff --git a/dlrs/main/objects/LookupRollupSummary2__mdt/fields/BypassPermissionApiName__c.field-meta.xml b/dlrs/main/objects/LookupRollupSummary2__mdt/fields/BypassPermissionApiName__c.field-meta.xml new file mode 100644 index 00000000..be4def99 --- /dev/null +++ b/dlrs/main/objects/LookupRollupSummary2__mdt/fields/BypassPermissionApiName__c.field-meta.xml @@ -0,0 +1,14 @@ + + + BypassPermissionApiName__c + false + false + DeveloperControlled + API name of a Custom Permission, if the running user has that permission then this rollup is skipped + + 255 + false + Text + false + From eb0967cc8e4f8b7986b7b4fe974437d9963f6023 Mon Sep 17 00:00:00 2001 From: Heber Date: Sat, 23 Nov 2024 20:15:59 -0700 Subject: [PATCH 2/2] Make background jobs respect new disablement features --- dlrs/main/classes/RollupCalculateJob.cls | 20 ++ dlrs/main/classes/RollupCalculateJobTest.cls | 145 ++++++++++++++ dlrs/main/classes/RollupJob.cls | 15 ++ dlrs/main/classes/RollupJobTest.cls | 185 ++++++++++++++++++ dlrs/main/classes/RollupService.cls | 21 +- .../DisableDLRS.customPermission-meta.xml | 6 + .../DisableDLRS.permissionset-meta.xml | 10 + 7 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 unpackaged/config/test/customPermissions/DisableDLRS.customPermission-meta.xml create mode 100644 unpackaged/config/test/permissionsets/DisableDLRS.permissionset-meta.xml diff --git a/dlrs/main/classes/RollupCalculateJob.cls b/dlrs/main/classes/RollupCalculateJob.cls index b4796b46..41dbe1a3 100644 --- a/dlrs/main/classes/RollupCalculateJob.cls +++ b/dlrs/main/classes/RollupCalculateJob.cls @@ -41,6 +41,26 @@ public with sharing class RollupCalculateJob implements Database.Batchable lookups = new RollupSummariesSelector() + .selectById(new Set{ (String) lookupId }); + + if (lookups.size() == 0) { + throw RollupServiceException.rollupNotFound(lookupId); + } + + RollupSummary lookup = lookups[0]; + + if ( + Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) || + BypassHandler.isBypassed(lookup.UniqueName) + ) { + System.debug('Rollup is disabled, will not execute ' + lookupId); + // return an "empty" iteration so it doesn't run the execute method + return Database.getQueryLocator( + 'SELECT Id FROM ' + lookup.ParentObject + ' LIMIT 0' + ); + } + // Query all the parent records as per the lookup definition return RollupService.masterRecordsAsQueryLocator( lookupId, diff --git a/dlrs/main/classes/RollupCalculateJobTest.cls b/dlrs/main/classes/RollupCalculateJobTest.cls index 2ef8d296..a0840f31 100644 --- a/dlrs/main/classes/RollupCalculateJobTest.cls +++ b/dlrs/main/classes/RollupCalculateJobTest.cls @@ -129,6 +129,151 @@ private class RollupCalculateJobTest { Assert.areEqual(0, logs.size(), 'Found:' + JSON.serializePretty(logs)); } + @IsTest + static void testRunBatchWithGlobalDisable() { + String prefix = LookupRollupSummary2__mdt.sObjectType.getDescribe() + .getKeyPrefix(); + + LookupRollupSummary2__mdt rollupCfg = new LookupRollupSummary2__mdt( + Id = prefix + '00000000000000D', + Label = 'A Summary', + DeveloperName = 'A_Summary', + ParentObject__c = 'Account', + ChildObject__c = 'Contact', + RelationshipField__c = 'AccountId', + AggregateOperation__c = RollupSummaries.AggregateOperation.Count.name(), + AggregateResultField__c = 'Description', + FieldToAggregate__c = 'Id', + CalculationMode__c = 'Realtime', + AggregateAllRows__c = false, + Active__c = true + ); + RollupSummariesSelector.setRollupCache( + false, + false, + RollupSummary.toList(new List{ rollupCfg }) + ); + + // globally disable DLRS + DeclarativeLookupRollupSummaries__c settings = new DeclarativeLookupRollupSummaries__c( + DisableDLRSGlobally__c = true + ); + insert settings; + + Account a = new Account(Name = 'Test'); + insert a; + + RollupCalculateJob job = new RollupCalculateJob(rollupCfg.Id, 'Id != NULL'); + Test.startTest(); + String jobId = Database.executeBatch(job); + Test.stopTest(); + + AsyncApexJob asyncJob = [ + SELECT Id, Status, JobItemsProcessed, TotalJobItems + FROM AsyncApexJob + WHERE Id = :jobId + ]; + + Assert.areEqual('Completed', asyncJob.Status); + Assert.areEqual(0, asyncJob.JobItemsProcessed); + Assert.areEqual(0, asyncJob.TotalJobItems); + + List logs = [ + SELECT Id, ParentId__c, ParentObject__c, ErrorMessage__c + FROM LookupRollupSummaryLog__c + ]; + Assert.areEqual(0, logs.size(), 'Found:' + JSON.serializePretty(logs)); + } + + @IsTest + static void testRunBatchWithCustPermDisable() { + // find the profile that has access to the Custom Permission we want to use to check (if it even exists in the system) + List permSetsWithAccess = [ + SELECT ParentId + FROM SetupEntityAccess + WHERE + SetupEntityId IN ( + SELECT Id + FROM CustomPermission + WHERE DeveloperName = 'DisableDLRS' + ) + AND Parent.IsOwnedByProfile = FALSE + ]; + if (permSetsWithAccess.isEmpty()) { + return; // this org doesn't have the necessary metadata to test this feature + } + // see if the running user already has that permission set + List assignments = [ + SELECT Id + FROM PermissionSetAssignment + WHERE + AssigneeId = :UserInfo.getUserId() + AND PermissionSetId = :permSetsWithAccess[0].ParentId + ]; + if (assignments.isEmpty()) { + // user doesn't have the necessary perm set to grant it to them. + System.runAs(new User(Id = UserInfo.getUserId())) { + insert new PermissionSetAssignment( + AssigneeId = UserInfo.getUserId(), + PermissionSetId = permSetsWithAccess[0].ParentId + ); + } + } + + String prefix = LookupRollupSummary2__mdt.sObjectType.getDescribe() + .getKeyPrefix(); + + LookupRollupSummary2__mdt rollupCfg = new LookupRollupSummary2__mdt( + Id = prefix + '00000000000000D', + Label = 'A Summary', + DeveloperName = 'A_Summary', + ParentObject__c = 'Account', + ChildObject__c = 'Contact', + RelationshipField__c = 'AccountId', + AggregateOperation__c = RollupSummaries.AggregateOperation.Count.name(), + AggregateResultField__c = 'Description', + FieldToAggregate__c = 'Id', + CalculationMode__c = 'Realtime', + AggregateAllRows__c = false, + Active__c = true, + BypassPermissionApiName__c = 'DisableDLRS' + ); + RollupSummariesSelector.setRollupCache( + false, + false, + RollupSummary.toList(new List{ rollupCfg }) + ); + + Account a = new Account(Name = 'Test'); + insert a; + + RollupCalculateJob job = new RollupCalculateJob(rollupCfg.Id, 'Id != NULL'); + String jobId; + System.runAs(new User(Id = UserInfo.getUserId())) { + Test.startTest(); + Assert.isTrue(FeatureManagement.checkPermission('DisableDLRS')); + // go into runAs because we need to get the perms recalculated + jobId = Database.executeBatch(job); + Test.stopTest(); + } + + AsyncApexJob asyncJob = [ + SELECT Id, Status, JobItemsProcessed, TotalJobItems + FROM AsyncApexJob + WHERE Id = :jobId + ]; + + Assert.areEqual('Completed', asyncJob.Status); + Assert.areEqual(0, asyncJob.JobItemsProcessed); + Assert.areEqual(0, asyncJob.TotalJobItems); + + List logs = [ + SELECT Id, ParentId__c, ParentObject__c, ErrorMessage__c + FROM LookupRollupSummaryLog__c + ]; + Assert.areEqual(0, logs.size(), 'Found:' + JSON.serializePretty(logs)); + } + public class MockBatchableContext implements Database.BatchableContext { public Id getJobId() { return '100000000000000'; diff --git a/dlrs/main/classes/RollupJob.cls b/dlrs/main/classes/RollupJob.cls index 6c79a2cc..d66d770e 100644 --- a/dlrs/main/classes/RollupJob.cls +++ b/dlrs/main/classes/RollupJob.cls @@ -34,6 +34,21 @@ global with sharing class RollupJob implements Schedulable, Database.Batchable{ rollupCfg }) + ); + + Account a = new Account(Name = 'Test'); + insert a; + + // globally disable DLRS + DeclarativeLookupRollupSummaries__c settings = new DeclarativeLookupRollupSummaries__c( + DisableDLRSGlobally__c = true + ); + insert settings; + + List items = new List(); + + LookupRollupSummaryScheduleItems__c scheduledItem = new LookupRollupSummaryScheduleItems__c(); + scheduledItem.Name = a.Id; + scheduledItem.LookupRollupSummary2__c = rollupCfg.Id; + scheduledItem.ParentId__c = a.Id; + scheduledItem.QualifiedParentID__c = a.Id + '#' + rollupCfg.Id; + + items.add(scheduledItem); + + insert items; + + RollupJob job = new RollupJob(); + Test.startTest(); + String jobId = Database.executeBatch(job); + Test.stopTest(); + + AsyncApexJob asyncJob = [ + SELECT Id, Status, JobItemsProcessed, TotalJobItems + FROM AsyncApexJob + WHERE Id = :jobId + ]; + + Assert.areEqual('Completed', asyncJob.Status); + Assert.areEqual(0, asyncJob.JobItemsProcessed); + Assert.areEqual(0, asyncJob.TotalJobItems); + + List logs = [ + SELECT Id, ParentId__c, ParentObject__c + FROM LookupRollupSummaryLog__c + ]; + Assert.areEqual(0, logs.size()); + } + + @IsTest + static void testDisabledSpecificRollupRunJob() { + // find the profile that has access to the Custom Permission we want to use to check (if it even exists in the system) + List permSetsWithAccess = [ + SELECT ParentId + FROM SetupEntityAccess + WHERE + SetupEntityId IN ( + SELECT Id + FROM CustomPermission + WHERE DeveloperName = 'DisableDLRS' + ) + AND Parent.IsOwnedByProfile = FALSE + ]; + if (permSetsWithAccess.isEmpty()) { + return; // this org doesn't have the necessary metadata to test this feature + } + // see if the running user already has that permission set + List assignments = [ + SELECT Id + FROM PermissionSetAssignment + WHERE + AssigneeId = :UserInfo.getUserId() + AND PermissionSetId = :permSetsWithAccess[0].ParentId + ]; + if (assignments.isEmpty()) { + // user doesn't have the necessary perm set to grant it to them. + System.runAs(new User(Id = UserInfo.getUserId())) { + insert new PermissionSetAssignment( + AssigneeId = UserInfo.getUserId(), + PermissionSetId = permSetsWithAccess[0].ParentId + ); + } + } + String prefix = LookupRollupSummary2__mdt.sObjectType.getDescribe() + .getKeyPrefix(); + + LookupRollupSummary2__mdt rollupCfg = new LookupRollupSummary2__mdt( + Id = prefix + '00000000000000D', + Label = 'A Summary', + DeveloperName = 'A_Summary', + ParentObject__c = 'Account', + ChildObject__c = 'Contact', + RelationshipField__c = 'AccountId', + AggregateOperation__c = RollupSummaries.AggregateOperation.Count.name(), + AggregateResultField__c = 'NumberOfEmployees', + FieldToAggregate__c = 'Id', + CalculationMode__c = 'Realtime', + AggregateAllRows__c = false, + Active__c = true, + BypassPermissionApiName__c = 'DisableDLRS' + ); + + RollupSummariesSelector.setRollupCache( + false, + false, + RollupSummary.toList(new List{ rollupCfg }) + ); + + Account a = new Account(Name = 'Test'); + insert a; + + Contact c = new Contact(LastName = 'Test', AccountId = a.Id); + insert c; + + List items = new List(); + + LookupRollupSummaryScheduleItems__c scheduledItem = new LookupRollupSummaryScheduleItems__c(); + scheduledItem.Name = a.Id; + scheduledItem.LookupRollupSummary2__c = rollupCfg.Id; + scheduledItem.ParentId__c = a.Id; + scheduledItem.QualifiedParentID__c = a.Id + '#' + rollupCfg.Id; + + items.add(scheduledItem); + + insert items; + + String jobId; + System.runAs(new User(Id = UserInfo.getUserId())) { + Test.startTest(); + Assert.isTrue(FeatureManagement.checkPermission('DisableDLRS')); + // go into runAs because we need to get the perms recalculated + jobId = Database.executeBatch(new RollupJob()); + Test.stopTest(); + } + + AsyncApexJob asyncJob = [ + SELECT Id, Status, JobItemsProcessed, TotalJobItems + FROM AsyncApexJob + WHERE Id = :jobId + ]; + + Assert.areEqual('Completed', asyncJob.Status); + Assert.areEqual(1, asyncJob.JobItemsProcessed); + Assert.areEqual(1, asyncJob.TotalJobItems); + + a = [SELECT Id, Description FROM Account WHERE Id = :a.Id]; + + Assert.isNull(a.Description); + + items = [ + SELECT Id, ParentId__c, LookupRollupSummary2__c + FROM LookupRollupSummaryScheduleItems__c + ]; + Assert.isTrue( + items.isEmpty(), + 'Expected empty but found' + JSON.serialize(items) + ); + + List logs = [ + SELECT Id, ParentId__c, ParentObject__c + FROM LookupRollupSummaryLog__c + ]; + Assert.areEqual(0, logs.size()); + } + @IsTest static void testFailureWithEmail() { String prefix = LookupRollupSummary2__mdt.sObjectType.getDescribe() diff --git a/dlrs/main/classes/RollupService.cls b/dlrs/main/classes/RollupService.cls index cbd10777..85db874d 100644 --- a/dlrs/main/classes/RollupService.cls +++ b/dlrs/main/classes/RollupService.cls @@ -96,6 +96,14 @@ global with sharing class RollupService { // Already running? checkJobAlreadyRunning(lookupId, lookup.Name); + if ( + DeclarativeLookupRollupSummaries__c.getInstance() + .DisableDLRSGlobally__c == true + ) { + throw new RollupServiceException( + 'DLRS is disabled through Custom Settings, unable to run job.' + ); + } // Already active? if ( (lookup.Active == null || lookup.Active == false) && @@ -396,7 +404,7 @@ global with sharing class RollupService { } /** - * Clears the bypass of aall rollups. + * Clears the bypass of all rollups. */ global static void clearAllBypasses() { BypassHandler.clearAllBypasses(); @@ -498,10 +506,17 @@ global with sharing class RollupService { } else { lookup = lookups.get(scheduleItem.LookupRollupSummary2__c); } - if (lookup == null) { + + if ( + // sched item is for a non-existent rollup definition + lookup == null || + // running user has a custom perm that disables this rollup + Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) + ) { + // do not process this item, item will still be deleted continue; } - // The lookup definition could have been changed or due to a historic bug in correctly associated + // The lookup definition could have been changed or due to a historic bug incorrectly associated if (parentId.getSobjectType() != gd.get(lookup.ParentObject)) continue; Set parentIds = parentIdsByParentType.get(lookup.ParentObject); diff --git a/unpackaged/config/test/customPermissions/DisableDLRS.customPermission-meta.xml b/unpackaged/config/test/customPermissions/DisableDLRS.customPermission-meta.xml new file mode 100644 index 00000000..273dd352 --- /dev/null +++ b/unpackaged/config/test/customPermissions/DisableDLRS.customPermission-meta.xml @@ -0,0 +1,6 @@ + + + Used to disable specific rollup jobs for specific users + false + + diff --git a/unpackaged/config/test/permissionsets/DisableDLRS.permissionset-meta.xml b/unpackaged/config/test/permissionsets/DisableDLRS.permissionset-meta.xml new file mode 100644 index 00000000..d1a905cb --- /dev/null +++ b/unpackaged/config/test/permissionsets/DisableDLRS.permissionset-meta.xml @@ -0,0 +1,10 @@ + + + + true + DisableDLRS + + Used in Apex Testing to disable individual DLRS rollups + false + +