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 +