diff --git a/cumulusci.yml b/cumulusci.yml
index e2c12adc..54d25674 100644
--- a/cumulusci.yml
+++ b/cumulusci.yml
@@ -4,7 +4,7 @@ project:
package:
name: Declarative Lookup Rollup Summaries Tool
namespace: dlrs
- api_version: "60.0"
+ api_version: "63.0"
git:
repo_url: https://github.com/SFDO-Community/declarative-lookup-rollup-summaries
prefix_beta: beta/
@@ -28,6 +28,24 @@ tasks:
options:
path: unpackaged/config/qa
+ snowfakery_highratio_custom:
+ description: Create a sample dataset for main objects in CSOM EDA org
+ class_path: cumulusci.tasks.bulkdata.snowfakery.Snowfakery
+ options:
+ recipe: datasets/qa/high-ratio-tests-custom-object.yml
+
+ snowfakery_highratio_standard:
+ description: Create a sample dataset for main objects in CSOM EDA org
+ class_path: cumulusci.tasks.bulkdata.snowfakery.Snowfakery
+ options:
+ recipe: datasets/qa/high-ratio-tests-stand-object.yml
+
+ snowfakery_lowratio:
+ description: Create a sample dataset for main objects in CSOM EDA org
+ class_path: cumulusci.tasks.bulkdata.snowfakery.Snowfakery
+ options:
+ recipe: datasets/qa/snowfakerylowratio.yml
+
robot:
options:
suites: robot/DLRS/tests
@@ -79,10 +97,11 @@ flows:
options:
apex: QAHelper.updateAppMenuOrdering();
7:
- task: load_dataset
- options:
- mapping: datasets/qa/mapping.yml
- sql_path: datasets/qa/qa_data.sql
+ task: snowfakery_lowratio
+ 8:
+ task: snowfakery_highratio_standard
+ 9:
+ task: snowfakery_highratio_custom
customer_org:
steps:
@@ -109,13 +128,6 @@ flows:
options:
extra: "--sourcepath ./dlrs/libs/fflib-apexmocks,./dlrs/libs/fflib-common,./dlrs/libs/lrengine,./dlrs/libs/metadataservice,./dlrs/main"
- config_dev:
- steps:
- 1.1:
- task: deploy
- options:
- path: unpackaged/config/qa
-
orgs:
scratch:
dev_prerelease:
@@ -128,6 +140,7 @@ orgs:
config_file: orgs/dev.json
namespaced: True
days: 7
+
plans:
install:
slug: install
diff --git a/datasets/qa/high-ratio-tests-custom-object.yml b/datasets/qa/high-ratio-tests-custom-object.yml
new file mode 100644
index 00000000..768d4b6a
--- /dev/null
+++ b/datasets/qa/high-ratio-tests-custom-object.yml
@@ -0,0 +1,43 @@
+# Section uses the QA Lookup Parent object to create high ratio data.
+# There is one parent, with 25,000 sub-parents each with 1 child.
+# There is a second top level parent with all 25,000 children attached.
+#QA Lookup Parent Record to be referenced later for Second Lookup on Child
+- object: QALookupParent__c
+ nickname: TopLevelParent
+ count: 1
+ fields:
+ Name: Top Level Parent High Ratio
+#QA Lookup Parent Record that will have child QA Lookup Parent
+- object: QALookupParent__c
+ nickname: TopHierarchyParent
+ count: 1
+ fields:
+ Name: Top Level of a wide hierarchy
+ friends:
+ - object: QALookupParent__c
+ nickname: CustomParentwithChild
+ count: 25000
+ fields:
+ Name: QALookupParentwithChild
+ Self_Relationship__c:
+ reference: TopHierarchyParent
+ friends:
+ - object: QALookupChild__c
+ nickname: CustomChild
+ count: 1
+ fields:
+ Name: ${{fake.Text(max_nb_chars = 25)}}
+ Amount__c: 1000
+ Color__c:
+ random_choice:
+ - Red
+ - Yellow
+ - Green
+ - Blue
+ Description__c: ${{fake.Text(max_nb_chars = 200)}}
+ Description2__c: ${{fake.Text(max_nb_chars = 200)}}
+ LookupParent__c:
+ reference: CustomParentwithChild
+
+ LookupParent2__c:
+ reference: TopLevelParent
diff --git a/datasets/qa/high-ratio-tests-stand-objects.yml b/datasets/qa/high-ratio-tests-stand-objects.yml
new file mode 100644
index 00000000..be019eff
--- /dev/null
+++ b/datasets/qa/high-ratio-tests-stand-objects.yml
@@ -0,0 +1,110 @@
+# Macros to reduce duplicate field definitions. All non-reference fields for
+# each object type should be listed here. We don't bother with Account since
+# we just set the Name field.
+
+# Fields to use on all Contacts
+- macro: contact_base
+ fields:
+ Firstname:
+ fake: first_name
+ Lastname:
+ fake: last_name
+
+# Fields to use on Cases
+- macro: case_base
+ fields:
+ Subject: ${{fake.Text(max_nb_chars = 20)}}
+ Description: ${{fake.Paragraph(nb_sentences=5)}}
+
+# Fields to use on Opportunities
+- macro: opp_base
+ fields:
+ Name: ${{fake.Word}} Opportunity
+ Stagename:
+ random_choice:
+ Prospecting: 5%
+ Qualification: 5%
+ Closed Won: 80%
+ Closed Lost: 10%
+ CloseDate:
+ date_between:
+ start_date: -1y
+ end_date: today
+ Amount:
+ random_number:
+ min: 1000
+ max: 10000
+ step: 50
+
+- object: Account
+ count: 1
+ nickname: TopLevelAccount
+ fields:
+ Name:
+ fake: Company
+ friends:
+ - object: Account
+ nickname: ChildAccount
+ fields:
+ Name:
+ fake: Company
+ friends:
+ - object: Contact # Contacts for the child account
+ count: 50
+ include: contact_base
+ fields:
+ AccountId:
+ reference: ChildAccount
+ - object: Case # Case for child Account
+ count: 500
+ include: case_base
+ fields:
+ AccountId:
+ reference: ChildAccount
+ ContactId:
+ random_reference:
+ to: Contact
+ unique: False
+ - object: Contact # Contacts for the top level account.
+ count: 50
+ include: contact_base
+ fields:
+ AccountId:
+ reference: TopLevelAccount
+ - object: Case # Case for Parent Account
+ count: 500
+ include: case_base
+ fields:
+ AccountId:
+ reference: TopLevelAccount
+ ContactId:
+ random_reference:
+ to: Contact
+ unique: False
+ - object: Opportunity
+ count: 500
+ include: opp_base
+ fields:
+ AccountId:
+ reference: TopLevelAccount
+ ContactId:
+ random_reference:
+ to: Contact
+ unique: False
+ friends:
+ - object: OpportunityContactRole
+ count: 2
+ fields:
+ OpportunityId:
+ reference: Opportunity
+ ContactId:
+ random_reference:
+ to: Contact
+ unique: False
+ Role:
+ random_choice:
+ Business User: 20%
+ Decision Maker: 20%
+ Economic Buyer: 20%
+ Economic Decision Maker: 20%
+ Evaluator: 20%
diff --git a/datasets/qa/snowfakerylowratio.yml b/datasets/qa/snowfakerylowratio.yml
new file mode 100644
index 00000000..b8568273
--- /dev/null
+++ b/datasets/qa/snowfakerylowratio.yml
@@ -0,0 +1,338 @@
+#Load Leads to Test Lead Conversion
+- object: Lead
+ count: 20
+ fields:
+ FirstName:
+ fake: first_name
+ LastName:
+ fake: last_name
+ company:
+ fake: Company
+ title:
+ fake.text:
+ max_nb_chars: 15
+ email:
+ fake: email
+ phone:
+ fake: phone_number
+ MobilePhone:
+ fake: phone_number
+ Street:
+ fake: StreetAddress
+ City:
+ fake: City
+ State:
+ fake: State
+ PostalCode:
+ fake: PostalCode
+ Country:
+ fake: Country
+ NumberOfEmployees:
+ random_number:
+ min: 0
+ max: 10000
+ Status:
+ random_choice:
+ Open: 40%
+ Working: 20%
+ Nurturing: 20%
+ Qualified: 10%
+ Unqualified: 10%
+#Outlier Campaign with Responses
+- object: Campaign
+ count: 1
+ nickname: CampaignResponded
+ fields:
+ Name: ${{fake.Text(max_nb_chars = 25)}} Responded
+ IsActive: TRUE
+ Status: Completed
+ StartDate:
+ date_between:
+ start_date: -1y
+ end_date: today
+ EndDate:
+ date_between:
+ start_date: ${{StartDate}}
+ end_date: today
+- object: Campaign
+ count: 10
+ nickname: CampaignSent
+ fields:
+ Name: ${{fake.Text(max_nb_chars = 25)}}
+ IsActive: TRUE
+ Status:
+ random_choice:
+ - In Progress
+ - Completed
+ - Aborted
+ - Planned
+ StartDate:
+ date_between:
+ start_date: -1y
+ end_date: +1y
+ EndDate:
+ date_between:
+ start_date: ${{StartDate}}
+ end_date: +1y
+#QA Lookup Parent Record to be referenced later for Second Lookup on Child
+- object: QALookupParent__c
+ count: 1
+ nickname: CustomParent2
+ fields:
+ Name: QALookupParent2
+#QA Lookup Parent Record that will have child QA Lookup Parent
+- object: QALookupParent__c
+ count: 1
+ nickname: CustomParentTop
+ fields:
+ Name: QALookupParentTop
+ friends:
+ - object: QALookupParent__c
+ count: 1
+ nickname: CustomParentwithChild
+ fields:
+ Name: QALookupParentwithChild
+ Self_Relationship__c:
+ reference: CustomParentTop
+ friends:
+ #Outlier QA Lookup Child with Low Amount and No Parent Lookup 2
+ - object: QALookupChild__c
+ count: 6
+ nickname: CustomChild
+ fields:
+ Name: ${{fake.Text(max_nb_chars = 25)}}
+ Amount__c: 7
+ Color__c:
+ random_choice:
+ - Red
+ - Yellow
+ - Green
+ - Blue
+ Description__c: ${{fake.Text(max_nb_chars = 200)}}
+ Description2__c: ${{fake.Text(max_nb_chars = 200)}}
+ LookupParent__c:
+ reference: CustomParentwithChild
+ - object: QALookupChild__c
+ count: 5
+ nickname: CustomChild
+ fields:
+ Name: ${{fake.Text(max_nb_chars = 25)}}
+ Amount__c: 1000
+ Color__c:
+ random_choice:
+ - Red
+ - Yellow
+ - Green
+ - Blue
+ Description__c: ${{fake.Text(max_nb_chars = 200)}}
+ Description2__c: ${{fake.Text(max_nb_chars = 200)}}
+ LookupParent__c:
+ reference: CustomParentwithChild
+ LookupParent2__c:
+ reference: CustomParent2
+- object: Account
+ count: 1
+ nickname: ParentAccount
+ fields:
+ Name:
+ fake: Company
+ NumberOfEmployees: 10
+ BillingStreet:
+ fake: street_address
+ BillingCity:
+ fake: city
+ BillingState:
+ fake: state
+ BillingPostalCode:
+ fake: postalcode
+ BillingCountry: United States
+- object: Account
+ count: 20
+ nickname: BusinessOrganization
+ fields:
+ Name:
+ fake: Company
+ ParentId:
+ reference: ParentAccount
+ NumberOfEmployees: 10
+ BillingStreet:
+ fake: street_address
+ BillingCity:
+ fake: city
+ BillingState:
+ fake: state
+ BillingPostalCode:
+ fake: postalcode
+ BillingCountry: United States
+ friends:
+ - object: Contact
+ count: 2 #2 contacts for every account
+ nickname: Person
+ fields:
+ Firstname:
+ fake: first_name
+ Lastname:
+ fake: last_name
+ AccountId:
+ reference: Account
+ MobilePhone:
+ random_choice:
+ - choice:
+ probability: 50%
+ pick:
+ fake: PhoneNumber
+ - choice:
+ probability: 50%
+ pick: "None"
+ MailingStreet:
+ fake: street_address
+ MailingCity:
+ fake: city
+ MailingState:
+ fake: state
+ MailingPostalCode:
+ fake: postalcode
+ MailingCountry: United States
+ Birthdate:
+ date_between:
+ start_date: -1y
+ end_date: today
+ friends:
+ #Outlier Campaign with Responses
+ - object: CampaignMember
+ count: 1
+ fields:
+ ContactId:
+ reference: Person
+ CampaignId:
+ random_reference:
+ to: CampaignResponded
+ parent: Person
+ unique: True
+ Status: Responded
+ - object: CampaignMember
+ count: 4 #4 campaign members for every contact
+ fields:
+ ContactId:
+ reference: Person
+ CampaignId:
+ random_reference:
+ to: CampaignSent
+ parent: Person
+ unique: True
+ Status: Sent
+ #Outlier Closed Case
+ - object: Case
+ count: 1
+ nickname: ClosedCase
+ fields:
+ AccountId:
+ reference: BusinessOrganization
+ ContactId:
+ reference: Person
+ Origin: Web
+ Reason: Equipment Design
+ Status: Closed
+ Subject: The ${{Contact.Lastname}} Closed Case
+ #Parent Case
+ - object: Case
+ count: 1
+ nickname: ParentCase
+ fields:
+ AccountId:
+ reference: BusinessOrganization
+ ContactId:
+ reference: Person
+ Origin: Web
+ Reason: Equipment Design
+ Status: Closed
+ Subject: The ${{Contact.Lastname}} Parent Case
+ #QA Lookup Parent Record to with lookups to Account and Case
+ friends:
+ - object: QALookupParent__c
+ count: 1
+ nickname: CustomParentAcctCase
+ fields:
+ Name: QALookupParentAcctCase
+ Account__c:
+ reference: ParentAccount
+ Case__c:
+ reference: ParentCase
+ Colours__c: Red;Yellow;Green;Blue
+ QA_Lookup_Amount_Min__c: 1000
+ Total__c: 5000
+ - object: Case
+ count: 3 #3 cases for every contact
+ nickname: CasewithReferences
+ fields:
+ AccountId:
+ reference: BusinessOrganization
+ ContactId:
+ reference: Person
+ ParentId:
+ reference: ParentCase
+ QA_Lookup_Parent__c:
+ reference: CustomParentwithChild
+ Origin:
+ random_choice:
+ - Phone
+ - Email
+ - Web
+ Reason:
+ random_choice:
+ Installation: 20%
+ Equipment Complexity: 20%
+ Performance: 20%
+ Breakdown: 20%
+ Equipment Design: 20%
+ Status:
+ random_choice:
+ New: 30%
+ Working: 35%
+ Escalated: 35%
+ Subject: The ${{Contact.Lastname}} ${{Case.Reason}} Case
+ #Outlier Opportunity with High Amount
+ - object: Opportunity
+ count: 1
+ fields:
+ name: The ${{Contact.Lastname}} Biggest Opportunity
+ AccountId:
+ reference: BusinessOrganization
+ Stagename: Closed Won
+ CloseDate:
+ date_between:
+ start_date: -1y
+ end_date: today
+ Amount: 100000
+ - object: Opportunity
+ count: 3 #3 opportunities for every contact
+ fields:
+ name: The ${{Contact.Lastname}} Opportunity
+ AccountId:
+ reference: BusinessOrganization
+ QA_Lookup_Parent__c:
+ reference: CustomParentwithChild
+ Stagename:
+ random_choice:
+ Prospecting: 40%
+ Qualification: 40%
+ Closed Lost: 20%
+ CloseDate:
+ date_between:
+ start_date: -1y
+ end_date: today
+ Amount: 1000
+ friends:
+ - object: OpportunityContactRole
+ fields:
+ OpportunityId:
+ reference: Opportunity
+ ContactId:
+ reference: Person
+ Role:
+ random_choice:
+ Business User: 20%
+ Decision Maker: 20%
+ Economic Buyer: 20%
+ Economic Decision Maker: 20%
+ Evaluator: 20%
\ No newline at end of file
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_Answer.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_Answer.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_Answer.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_Answer.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_AnswerTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_AnswerTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_AnswerTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_AnswerTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_AnyOrder.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_AnyOrder.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_AnyOrder.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_AnyOrder.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_AnyOrderTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_AnyOrderTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_AnyOrderTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_AnyOrderTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocks.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocks.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocks.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocks.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksConfig.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksConfig.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksConfig.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksConfig.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksUtils.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksUtils.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksUtils.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksUtils.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksUtilsTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksUtilsTest.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksUtilsTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_ApexMocksUtilsTest.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_ArgumentCaptor.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_ArgumentCaptor.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_ArgumentCaptor.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_ArgumentCaptor.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_ArgumentCaptorTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_ArgumentCaptorTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_ArgumentCaptorTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_ArgumentCaptorTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_IDGenerator.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_IDGenerator.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_IDGenerator.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_IDGenerator.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_IDGeneratorTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_IDGeneratorTest.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_IDGeneratorTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_IDGeneratorTest.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_IMatcher.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_IMatcher.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_IMatcher.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_IMatcher.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_InOrder.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_InOrder.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_InOrder.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_InOrder.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_InOrderTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_InOrderTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_InOrderTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_InOrderTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_Inheritor.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_Inheritor.cls-meta.xml
index dd4f37c4..f3c68426 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_Inheritor.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_Inheritor.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_InheritorTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_InheritorTest.cls-meta.xml
index dd4f37c4..f3c68426 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_InheritorTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_InheritorTest.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_InvocationOnMock.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_InvocationOnMock.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_InvocationOnMock.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_InvocationOnMock.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_Match.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_Match.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_Match.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_Match.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_MatchTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_MatchTest.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_MatchTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_MatchTest.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_MatcherDefinitions.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_MatcherDefinitions.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_MatcherDefinitions.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_MatcherDefinitions.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_MatcherDefinitionsTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_MatcherDefinitionsTest.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_MatcherDefinitionsTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_MatcherDefinitionsTest.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_MatchersReturnValue.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_MatchersReturnValue.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_MatchersReturnValue.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_MatchersReturnValue.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodArgValues.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodArgValues.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodArgValues.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodArgValues.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodArgValuesTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodArgValuesTest.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodArgValuesTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodArgValuesTest.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodCountRecorder.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodCountRecorder.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodCountRecorder.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodCountRecorder.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodReturnValue.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodReturnValue.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodReturnValue.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodReturnValue.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodReturnValueRecorder.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodReturnValueRecorder.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodReturnValueRecorder.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodReturnValueRecorder.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodVerifier.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodVerifier.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_MethodVerifier.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_MethodVerifier.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_Mocks.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_Mocks.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_Mocks.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_Mocks.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_MyList.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_MyList.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_MyList.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_MyList.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethod.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethod.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethod.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethod.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethodAndArgValues.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethodAndArgValues.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethodAndArgValues.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethodAndArgValues.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethodTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethodTest.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethodTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_QualifiedMethodTest.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_System.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_System.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_System.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_System.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_SystemTest.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_SystemTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_SystemTest.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_SystemTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-apexmocks/classes/fflib_VerificationMode.cls-meta.xml b/dlrs/libs/fflib-apexmocks/classes/fflib_VerificationMode.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-apexmocks/classes/fflib_VerificationMode.cls-meta.xml
+++ b/dlrs/libs/fflib-apexmocks/classes/fflib_VerificationMode.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_Application.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_Application.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_Application.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_Application.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_ApplicationTest.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_ApplicationTest.cls-meta.xml
index b6afc265..b3b008df 100644
--- a/dlrs/libs/fflib-common/classes/fflib_ApplicationTest.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_ApplicationTest.cls-meta.xml
@@ -1,4 +1,4 @@
- 60.0
+ 63.0
diff --git a/dlrs/libs/fflib-common/classes/fflib_ISObjectDomain.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_ISObjectDomain.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_ISObjectDomain.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_ISObjectDomain.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_ISObjectSelector.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_ISObjectSelector.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_ISObjectSelector.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_ISObjectSelector.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_ISObjectUnitOfWork.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_ISObjectUnitOfWork.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_ISObjectUnitOfWork.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_ISObjectUnitOfWork.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_QueryFactory.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_QueryFactory.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_QueryFactory.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_QueryFactory.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_QueryFactoryTest.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_QueryFactoryTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_QueryFactoryTest.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_QueryFactoryTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_SObjectDescribe.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_SObjectDescribe.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_SObjectDescribe.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_SObjectDescribe.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_SObjectDescribeTest.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_SObjectDescribeTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_SObjectDescribeTest.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_SObjectDescribeTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_SObjectDomain.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_SObjectDomain.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_SObjectDomain.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_SObjectDomain.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_SObjectDomainTest.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_SObjectDomainTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_SObjectDomainTest.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_SObjectDomainTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_SObjectMocks.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_SObjectMocks.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_SObjectMocks.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_SObjectMocks.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_SObjectSelector.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_SObjectSelector.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_SObjectSelector.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_SObjectSelector.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_SObjectSelectorTest.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_SObjectSelectorTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_SObjectSelectorTest.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_SObjectSelectorTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_SObjectUnitOfWork.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_SObjectUnitOfWork.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_SObjectUnitOfWork.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_SObjectUnitOfWork.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_SObjectUnitOfWorkTest.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_SObjectUnitOfWorkTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_SObjectUnitOfWorkTest.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_SObjectUnitOfWorkTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_SecurityUtils.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_SecurityUtils.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_SecurityUtils.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_SecurityUtils.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_SecurityUtilsTest.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_SecurityUtilsTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_SecurityUtilsTest.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_SecurityUtilsTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_StringBuilder.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_StringBuilder.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_StringBuilder.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_StringBuilder.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/fflib-common/classes/fflib_StringBuilderTest.cls-meta.xml b/dlrs/libs/fflib-common/classes/fflib_StringBuilderTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/fflib-common/classes/fflib_StringBuilderTest.cls-meta.xml
+++ b/dlrs/libs/fflib-common/classes/fflib_StringBuilderTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/lrengine/classes/LREngine.cls-meta.xml b/dlrs/libs/lrengine/classes/LREngine.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/lrengine/classes/LREngine.cls-meta.xml
+++ b/dlrs/libs/lrengine/classes/LREngine.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/lrengine/classes/TestLREngine.cls-meta.xml b/dlrs/libs/lrengine/classes/TestLREngine.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/lrengine/classes/TestLREngine.cls-meta.xml
+++ b/dlrs/libs/lrengine/classes/TestLREngine.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/metadataservice/classes/MetadataService.cls-meta.xml b/dlrs/libs/metadataservice/classes/MetadataService.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/metadataservice/classes/MetadataService.cls-meta.xml
+++ b/dlrs/libs/metadataservice/classes/MetadataService.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/metadataservice/classes/MetadataServiceTest.cls-meta.xml b/dlrs/libs/metadataservice/classes/MetadataServiceTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/libs/metadataservice/classes/MetadataServiceTest.cls-meta.xml
+++ b/dlrs/libs/metadataservice/classes/MetadataServiceTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/libs/metadataservice/components/zip.component-meta.xml b/dlrs/libs/metadataservice/components/zip.component-meta.xml
index 7ba687fa..be17774b 100644
--- a/dlrs/libs/metadataservice/components/zip.component-meta.xml
+++ b/dlrs/libs/metadataservice/components/zip.component-meta.xml
@@ -1,6 +1,6 @@
- 60.0
+ 63.0
1
diff --git a/dlrs/libs/metadataservice/components/zipEntry.component-meta.xml b/dlrs/libs/metadataservice/components/zipEntry.component-meta.xml
index 2d9f01c7..5de2026a 100644
--- a/dlrs/libs/metadataservice/components/zipEntry.component-meta.xml
+++ b/dlrs/libs/metadataservice/components/zipEntry.component-meta.xml
@@ -1,6 +1,6 @@
- 60.0
+ 63.0
1
diff --git a/dlrs/main/aura/optimizer/optimizer.cmp-meta.xml b/dlrs/main/aura/optimizer/optimizer.cmp-meta.xml
index 5b2abc2b..4690ec35 100644
--- a/dlrs/main/aura/optimizer/optimizer.cmp-meta.xml
+++ b/dlrs/main/aura/optimizer/optimizer.cmp-meta.xml
@@ -3,6 +3,6 @@
xmlns="urn:metadata.tooling.soap.sforce.com"
fqn="optimizer"
>
- 60.0
+ 63.0
A Lightning Component Bundle
diff --git a/dlrs/main/aura/optimizerNotification/optimizerNotification.cmp-meta.xml b/dlrs/main/aura/optimizerNotification/optimizerNotification.cmp-meta.xml
index 7535af7e..ba6cf02a 100644
--- a/dlrs/main/aura/optimizerNotification/optimizerNotification.cmp-meta.xml
+++ b/dlrs/main/aura/optimizerNotification/optimizerNotification.cmp-meta.xml
@@ -3,6 +3,6 @@
xmlns="urn:metadata.tooling.soap.sforce.com"
fqn="optimizerNotification"
>
- 60.0
+ 63.0
A Lightning Component Bundle
diff --git a/dlrs/main/classes/ApexClassesSelector.cls-meta.xml b/dlrs/main/classes/ApexClassesSelector.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/ApexClassesSelector.cls-meta.xml
+++ b/dlrs/main/classes/ApexClassesSelector.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/ApexTriggersSelector.cls-meta.xml b/dlrs/main/classes/ApexTriggersSelector.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/ApexTriggersSelector.cls-meta.xml
+++ b/dlrs/main/classes/ApexTriggersSelector.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/AsyncApexJobsSelector.cls b/dlrs/main/classes/AsyncApexJobsSelector.cls
index 04734c23..cb7010b4 100644
--- a/dlrs/main/classes/AsyncApexJobsSelector.cls
+++ b/dlrs/main/classes/AsyncApexJobsSelector.cls
@@ -31,6 +31,7 @@ public class AsyncApexJobsSelector extends fflib_SObjectSelector {
AsyncApexJob.Status,
AsyncApexJob.ExtendedStatus,
AsyncApexJob.CreatedDate,
+ AsyncApexJob.CronTriggerId,
AsyncApexJob.CompletedDate,
AsyncApexJob.JobItemsProcessed,
AsyncApexJob.TotalJobItems,
@@ -63,11 +64,59 @@ public class AsyncApexJobsSelector extends fflib_SObjectSelector {
String query = newQueryFactory()
.setCondition(
'JobType = :jobType And ' +
- 'ApexClass.Name in :classNames And ' +
- 'Status in :statuses'
+ 'ApexClass.Name in :classNames And ' +
+ 'Status in :statuses'
)
.toSOQL();
List jobs = (List) Database.query(query);
return jobs.size() > 0;
}
+
+ /**
+ * Get all scheduled instances of a class
+ **/
+ public List getScheduledInstancesOfType(Type classType) {
+ String classFullyQualifiedName = classType.getName();
+ List nameParts = classFullyQualifiedName.split('\\.');
+
+ // Probably doesn't work for inner-classes in this format
+ String namespace;
+ if (nameParts.size() > 1) {
+ namespace = nameParts.remove(0);
+ }
+ String className = nameParts.remove(0);
+
+ String query = newQueryFactory()
+ .selectFields(
+ new List{
+ 'ApexClass.Name',
+ 'CronTrigger.CronJobDetail.Name',
+ 'CronTrigger.CronExpression',
+ 'CronTrigger.NextFireTime'
+ }
+ )
+ .setCondition(
+ 'Status != \'Aborted\' AND ' +
+ 'JobType = \'ScheduledApex\' AND ' +
+ 'ApexClass.Name = :className AND ' +
+ 'ApexClass.NamespacePrefix = :namespace'
+ )
+ .addOrdering(
+ 'CronTrigger.NextFireTime',
+ fflib_QueryFactory.SortOrder.ASCENDING
+ )
+ .toSOQL();
+ return Database.query(query);
+ }
+
+ /**
+ * Get all scheduled instances of a class
+ **/
+ public List getAllScheduledJobs() {
+ String query = newQueryFactory()
+ .selectField('CronTrigger.CronJobDetail.Name')
+ .setCondition('JobType = \'ScheduledApex\' AND Status != \'Aborted\'')
+ .toSOQL();
+ return Database.query(query);
+ }
}
diff --git a/dlrs/main/classes/AsyncApexJobsSelector.cls-meta.xml b/dlrs/main/classes/AsyncApexJobsSelector.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/AsyncApexJobsSelector.cls-meta.xml
+++ b/dlrs/main/classes/AsyncApexJobsSelector.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/AsyncApexJobsSelectorTest.cls b/dlrs/main/classes/AsyncApexJobsSelectorTest.cls
new file mode 100644
index 00000000..132c6a24
--- /dev/null
+++ b/dlrs/main/classes/AsyncApexJobsSelectorTest.cls
@@ -0,0 +1,68 @@
+@isTest
+private class AsyncApexJobsSelectorTest {
+ @isTest
+ static void testGetScheduledInstancesOfType() {
+ List jobs = new AsyncApexJobsSelector().getAllScheduledJobs();
+ Integer jobCountStart = jobs.size();
+ if (jobCountStart > 98) {
+ System.debug('not enough capacity to schedule new, exiting early');
+ return;
+ }
+
+ // we don't get database isolation here
+ // ensure tests here don't break if extra jobs are scheduled
+ String jobId1 = System.schedule(
+ 'TestJob112233',
+ '0 0 * * * ? 2100',
+ new RollupJob()
+ );
+ String jobId2 = System.schedule(
+ 'TestJob998877',
+ '0 0 * * * ? 2101',
+ new RollupJob()
+ );
+
+ jobs = new AsyncApexJobsSelector().getAllScheduledJobs();
+ Assert.areEqual(
+ 2,
+ jobs.size() - jobCountStart,
+ 'Expected total scheduled jobs to have increased by two'
+ );
+
+ jobs = new List(
+ new AsyncApexJobsSelector().getScheduledInstancesOfType(RollupJob.class)
+ );
+
+ Assert.isTrue(
+ jobs.size() >= 2,
+ 'Exepcted at least 2 jobs, found ' + jobs.size()
+ );
+
+ Boolean hasJob1 = false;
+ Boolean hasJob2 = false;
+
+ for (AsyncApexJob job : jobs) {
+ if (job.CronTriggerId == (Id) jobId1) {
+ hasJob1 = true;
+ }
+ if (job.CronTriggerId == (Id) jobId2) {
+ hasJob2 = true;
+ }
+ }
+
+ Assert.isTrue(
+ hasJob1,
+ 'Expected ' + jobId1 + ' to be included in jobs list:' + jobs
+ );
+ Assert.isTrue(
+ hasJob2,
+ 'Expected ' + jobId2 + ' to be included in jobs list:' + jobs
+ );
+
+ // clear jobs
+ System.abortJob(jobId1);
+ System.abortJob(jobId2);
+
+ Assert.isTrue(jobs.size() >= 2, 'Expected at least two jobs scheduled');
+ }
+}
diff --git a/dlrs/main/classes/AsyncApexJobsSelectorTest.cls-meta.xml b/dlrs/main/classes/AsyncApexJobsSelectorTest.cls-meta.xml
new file mode 100644
index 00000000..835ede48
--- /dev/null
+++ b/dlrs/main/classes/AsyncApexJobsSelectorTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
\ No newline at end of file
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/BypassHandler.cls-meta.xml b/dlrs/main/classes/BypassHandler.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/BypassHandler.cls-meta.xml
+++ b/dlrs/main/classes/BypassHandler.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
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/BypassHandlerTest.cls-meta.xml b/dlrs/main/classes/BypassHandlerTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/BypassHandlerTest.cls-meta.xml
+++ b/dlrs/main/classes/BypassHandlerTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/CronJobDetailsSelector.cls-meta.xml b/dlrs/main/classes/CronJobDetailsSelector.cls-meta.xml
index fd91398d..24cb2e3a 100644
--- a/dlrs/main/classes/CronJobDetailsSelector.cls-meta.xml
+++ b/dlrs/main/classes/CronJobDetailsSelector.cls-meta.xml
@@ -3,6 +3,6 @@
xmlns="urn:metadata.tooling.soap.sforce.com"
fqn="CronJobDetailsSelector"
>
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/CronTriggersSelector.cls-meta.xml b/dlrs/main/classes/CronTriggersSelector.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/CronTriggersSelector.cls-meta.xml
+++ b/dlrs/main/classes/CronTriggersSelector.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/CustomMetadataService.cls b/dlrs/main/classes/CustomMetadataService.cls
index 070b5bcf..9fe5eb12 100644
--- a/dlrs/main/classes/CustomMetadataService.cls
+++ b/dlrs/main/classes/CustomMetadataService.cls
@@ -39,6 +39,46 @@ public class CustomMetadataService {
) {
}
+ /**
+ * Starts async metadata save using Apex Metadata API
+ */
+ public static Id initiateMetadataSave(List records) {
+ Metadata.DeployContainer mdContainer = new Metadata.DeployContainer();
+ for (SObject r : records) {
+ // Setup custom metadata to be created in the subscriber org.
+ Metadata.CustomMetadata customMetadata = new Metadata.CustomMetadata();
+ // Developer name and Label are applied here
+ customMetadata.fullName =
+ r.getSObjectType().getDescribe().getName() +
+ '.' +
+ (String) r.get('DeveloperName');
+ customMetadata.label = (String) r.get('Label');
+ Metadata.CustomMetadata mdt = new Metadata.CustomMetadata();
+ Map populatedFields = r.getPopulatedFieldsAsMap();
+ // We don't want these in values
+ List ignoredKeys = new List{
+ 'Id',
+ 'DeveloperName',
+ 'Label'
+ };
+ for (String key : populatedFields.keySet()) {
+ if (ignoredKeys.contains(key)) {
+ continue;
+ }
+ Metadata.CustomMetadataValue customField = new Metadata.CustomMetadataValue();
+ customField.field = key;
+ customField.value = populatedFields.get(key);
+ customMetadata.values.add(customField);
+ }
+
+ mdContainer.addMetadata(customMetadata);
+ }
+
+ ApexMdApiDeployCallback cb = new ApexMdApiDeployCallback();
+ // Enqueue custom metadata deployment
+ return Metadata.Operations.enqueueDeployment(mdContainer, cb);
+ }
+
/**
* Insert the given Custom Metadata records into the orgs config
**/
@@ -75,8 +115,8 @@ public class CustomMetadataService {
for (String customMetadataFullName : customMetadataFullNames)
qualifiedFullNames.add(
qualifiedMetadataType.getDescribe().getName() +
- '.' +
- customMetadataFullName
+ '.' +
+ customMetadataFullName
);
List results = service.deleteMetadata(
'CustomMetadata',
@@ -85,6 +125,21 @@ public class CustomMetadataService {
handleDeleteResults(results[0]);
}
+ /**
+ * Delete the given Custom Metadata records from the org using Async action
+ **/
+ public static Id deleteMetadataAsync(
+ SObjectType qualifiedMetadataType,
+ List customMetadataFullNames
+ ) {
+ return System.enqueueJob(
+ new DeleteMetadataQueueable(
+ qualifiedMetadataType,
+ customMetadataFullNames
+ )
+ );
+ }
+
public class CustomMetadataServiceException extends Exception {
}
@@ -156,20 +211,20 @@ public class CustomMetadataService {
List messages = new List();
messages.add(
(saveResult.errors.size() == 1 ? 'Error ' : 'Errors ') +
- 'occured processing component ' +
- saveResult.fullName +
- '.'
+ 'occured processing component ' +
+ saveResult.fullName +
+ '.'
);
for (MetadataService.Error error : saveResult.errors)
messages.add(
error.message +
- ' (' +
- error.statusCode +
- ').' +
- (error.fields != null &&
- error.fields.size() > 0
- ? ' Fields ' + String.join(error.fields, ',') + '.'
- : '')
+ ' (' +
+ error.statusCode +
+ ').' +
+ (error.fields != null &&
+ error.fields.size() > 0
+ ? ' Fields ' + String.join(error.fields, ',') + '.'
+ : '')
);
if (messages.size() > 0)
throw new CustomMetadataServiceException(String.join(messages, ' '));
@@ -194,20 +249,20 @@ public class CustomMetadataService {
List messages = new List();
messages.add(
(deleteResult.errors.size() == 1 ? 'Error ' : 'Errors ') +
- 'occured processing component ' +
- deleteResult.fullName +
- '.'
+ 'occured processing component ' +
+ deleteResult.fullName +
+ '.'
);
for (MetadataService.Error error : deleteResult.errors)
messages.add(
error.message +
- ' (' +
- error.statusCode +
- ').' +
- (error.fields != null &&
- error.fields.size() > 0
- ? ' Fields ' + String.join(error.fields, ',') + '.'
- : '')
+ ' (' +
+ error.statusCode +
+ ').' +
+ (error.fields != null &&
+ error.fields.size() > 0
+ ? ' Fields ' + String.join(error.fields, ',') + '.'
+ : '')
);
if (messages.size() > 0)
throw new CustomMetadataServiceException(String.join(messages, ' '));
@@ -217,4 +272,62 @@ public class CustomMetadataService {
'Request failed with no specified error.'
);
}
+
+ @TestVisible
+ class ApexMdApiDeployCallback implements Metadata.DeployCallback {
+ public void handleResult(
+ Metadata.DeployResult result,
+ Metadata.DeployCallbackContext context
+ ) {
+ UserNotification__e updateEvent = new UserNotification__e(
+ Type__c = 'DeploymentResult',
+ Payload__c = JSON.serialize(result)
+ );
+ EventBus.publish(updateEvent);
+ }
+ }
+
+ @TestVisible
+ class DeleteMetadataQueueable implements Queueable, Database.AllowsCallouts {
+ SObjectType qualifiedMetadataType;
+ List customMetadataFullNames;
+ public DeleteMetadataQueueable(
+ SObjectType qualifiedMetadataType,
+ List customMetadataFullNames
+ ) {
+ this.qualifiedMetadataType = qualifiedMetadataType;
+ this.customMetadataFullNames = customMetadataFullNames;
+ }
+
+ public void execute(QueueableContext ctx) {
+ try {
+ CustomMetadataService.deleteMetadata(
+ qualifiedMetadataType,
+ customMetadataFullNames
+ );
+ UserNotification__e updateEvent = new UserNotification__e(
+ Type__c = 'DeleteRequestResult',
+ Payload__c = JSON.serialize(
+ new Map{
+ 'success' => true,
+ 'metadataNames' => customMetadataFullNames
+ }
+ )
+ );
+ EventBus.publish(updateEvent);
+ } catch (CustomMetadataService.CustomMetadataServiceException e) {
+ UserNotification__e updateEvent = new UserNotification__e(
+ Type__c = 'DeleteRequestResult',
+ Payload__c = JSON.serialize(
+ new Map{
+ 'success' => false,
+ 'metadataNames' => customMetadataFullNames,
+ 'error' => e.getMessage()
+ }
+ )
+ );
+ EventBus.publish(updateEvent);
+ }
+ }
+ }
}
diff --git a/dlrs/main/classes/CustomMetadataService.cls-meta.xml b/dlrs/main/classes/CustomMetadataService.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/CustomMetadataService.cls-meta.xml
+++ b/dlrs/main/classes/CustomMetadataService.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/CustomMetadataServiceTest.cls b/dlrs/main/classes/CustomMetadataServiceTest.cls
index e865209e..71e4dbdd 100644
--- a/dlrs/main/classes/CustomMetadataServiceTest.cls
+++ b/dlrs/main/classes/CustomMetadataServiceTest.cls
@@ -26,4 +26,131 @@
@IsTest
private class CustomMetadataServiceTest {
+ @IsTest
+ static void testInitiateMetadataSave() {
+ try {
+ CustomMetadataService.initiateMetadataSave(
+ new List{
+ new LookupRollupSummary2__mdt(
+ DeveloperName = 'TestRec',
+ Label = 'Test Rec',
+ Active__c = true,
+ RowLimit__c = 100
+ )
+ }
+ );
+ Assert.fail('Expected to fail starting the deployment');
+ } catch (System.AsyncException e) {
+ Assert.areEqual(
+ 'Metadata cannot be deployed from within a test',
+ e.getMessage()
+ );
+ }
+ }
+
+ @IsTest
+ static void testDeleteMetadataAsync() {
+ Test.startTest();
+ Test.setMock(
+ WebServiceMock.class,
+ new MetadataServiceDeleteSuccessCalloutMock()
+ );
+ CustomMetadataService.deleteMetadataAsync(
+ LookupRollupSummary2__mdt.getSObjectType(),
+ new List{ 'LookupRollupSummary2__mdt.Test123' }
+ );
+ Test.stopTest();
+ }
+
+ @IsTest
+ static void testDeleteMetadataAsyncFailed() {
+ Test.startTest();
+ Test.setMock(
+ WebServiceMock.class,
+ new MetadataServiceDeleteFailureCalloutMock()
+ );
+ CustomMetadataService.deleteMetadataAsync(
+ LookupRollupSummary2__mdt.getSObjectType(),
+ new List{ 'LookupRollupSummary2__mdt.Test123' }
+ );
+ Test.stopTest();
+ }
+
+ @IsTest
+ static void testApexMdApiDeployCallback() {
+ CustomMetadataService.ApexMdApiDeployCallback cb = new CustomMetadataService.ApexMdApiDeployCallback();
+ Metadata.DeployResult result = new Metadata.DeployResult();
+ result.status = Metadata.DeployStatus.Succeeded;
+ Metadata.DeployMessage messageObj = new Metadata.DeployMessage();
+ messageObj.changed = true;
+ messageObj.success = true;
+ messageObj.fullName = 'TestRec';
+ messageObj.componentType = 'CustomMetadata';
+ messageObj.fullName = 'LookupRollupSummary2__mdt.TestRec';
+ Metadata.DeployDetails deployDetailsObj = new Metadata.DeployDetails();
+ deployDetailsObj.componentSuccesses.add(messageObj);
+ result.details = deployDetailsObj;
+ Metadata.DeployCallbackContext context = new Metadata.DeployCallbackContext();
+
+ // Invoke the callback's handleResult method.
+ cb.handleResult(result, context);
+
+ // expected Platform Event publishing DML
+ Assert.areEqual(1, Limits.getDmlStatements());
+ Assert.areEqual(1, Limits.getDmlRows());
+ }
+
+ public class MetadataServiceDeleteSuccessCalloutMock implements WebServiceMock {
+ public void doInvoke(
+ Object stub,
+ Object request,
+ Map response,
+ String endpoint,
+ String soapAction,
+ String requestName,
+ String responseNS,
+ String responseName,
+ String responseType
+ ) {
+ System.debug(request);
+ MetadataService.deleteMetadataResponse_element responseElement = new MetadataService.deleteMetadataResponse_element();
+ // MetadataService.createMetadataResponse_element responseElement = new MetadataService.createMetadataResponse_element();
+ MetadataService.DeleteResult res = new MetadataService.DeleteResult();
+ res.success = true;
+ res.fullName = 'myTestResult';
+ responseElement.result = new List{ res };
+
+ response.put('response_x', responseElement);
+ }
+ }
+
+ public class MetadataServiceDeleteFailureCalloutMock implements WebServiceMock {
+ public void doInvoke(
+ Object stub,
+ Object request,
+ Map response,
+ String endpoint,
+ String soapAction,
+ String requestName,
+ String responseNS,
+ String responseName,
+ String responseType
+ ) {
+ System.debug(request);
+ MetadataService.deleteMetadataResponse_element responseElement = new MetadataService.deleteMetadataResponse_element();
+ // MetadataService.createMetadataResponse_element responseElement = new MetadataService.createMetadataResponse_element();
+ MetadataService.DeleteResult res = new MetadataService.DeleteResult();
+ res.success = false;
+ res.errors = new List();
+ MetadataService.Error err = new MetadataService.Error();
+ err.message = 'Test Error';
+ err.statusCode = 'Error Code';
+ err.fields = new List{ 'Field__1', 'Field_2' };
+ res.errors.add(err);
+ res.fullName = 'myTestResult';
+ responseElement.result = new List{ res };
+
+ response.put('response_x', responseElement);
+ }
+ }
}
diff --git a/dlrs/main/classes/CustomMetadataServiceTest.cls-meta.xml b/dlrs/main/classes/CustomMetadataServiceTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/CustomMetadataServiceTest.cls-meta.xml
+++ b/dlrs/main/classes/CustomMetadataServiceTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/LookupRollupStatusCheckController.cls b/dlrs/main/classes/LookupRollupStatusCheckController.cls
new file mode 100644
index 00000000..5a76bbc4
--- /dev/null
+++ b/dlrs/main/classes/LookupRollupStatusCheckController.cls
@@ -0,0 +1,79 @@
+public with sharing class LookupRollupStatusCheckController {
+ /**
+ * Get count of scheduled items older than yesterday
+ * The assumption is that normal processing should have handled these
+ */
+ @AuraEnabled(Cacheable=true)
+ public static Integer getOutstandingScheduledItemsForLookup(String lookupID) {
+ return [
+ SELECT COUNT()
+ FROM LookupRollupSummaryScheduleItems__c
+ WHERE LookupRollupSummary2__c = :lookupID AND LastModifiedDate < YESTERDAY
+ ];
+ }
+
+ /**
+ * Check if the rollup has a Full Calculate schedule
+ */
+ @AuraEnabled
+ public static Datetime getScheduledFullCalculates(String lookupID) {
+ try {
+ LookupRollupSummary2__mdt LookupRollupSummary = (LookupRollupSummary2__mdt) new RollupSummariesSelector.CustomMetadataSelector(
+ false,
+ true
+ )
+ .selectById(new Set{ lookupID })[0]
+ .Record;
+
+ if (LookupRollupSummary != null) {
+ String id = (LookupRollupSummary.id).to15();
+ List ct = new CronTriggersSelector()
+ .selectScheduledApexById(id);
+
+ if (!ct.isEmpty()) {
+ return ct[0].NextFireTime;
+ }
+ }
+ } catch (Exception e) {
+ }
+ return null;
+ }
+
+ /**
+ * Check if the rollup has a child/parent trigger
+ */
+ @AuraEnabled
+ public static Boolean hasChildTriggerDeployed(String lookupID) {
+ try {
+ LookupRollupSummary2__mdt LookupRollupSummary = (LookupRollupSummary2__mdt) new RollupSummariesSelector.CustomMetadataSelector(
+ false,
+ true
+ )
+ .selectById(new Set{ lookupID })[0]
+ .Record;
+
+ if (LookupRollupSummary != null) {
+ RollupSummary rs = new RollupSummary(LookupRollupSummary);
+ String childTrigger = RollupSummaries.makeTriggerName(rs);
+ ApexTriggersSelector selector = new ApexTriggersSelector();
+ Map loadTriggers = selector.selectByName(
+ new Set{ ChildTrigger }
+ );
+
+ return loadTriggers.containsKey(ChildTrigger);
+ }
+ } catch (Exception e) {
+ }
+ return false;
+ }
+
+ /**
+ * Check if cron job is running for DLRS
+ */
+ @AuraEnabled
+ public static Integer getScheduledJobs() {
+ return new AsyncApexJobsSelector()
+ .getScheduledInstancesOfType(RollupJob.class)
+ .size();
+ }
+}
diff --git a/dlrs/main/classes/LookupRollupStatusCheckController.cls-meta.xml b/dlrs/main/classes/LookupRollupStatusCheckController.cls-meta.xml
new file mode 100644
index 00000000..835ede48
--- /dev/null
+++ b/dlrs/main/classes/LookupRollupStatusCheckController.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
\ No newline at end of file
diff --git a/dlrs/main/classes/LookupRollupStatusCheckControllerTest.cls b/dlrs/main/classes/LookupRollupStatusCheckControllerTest.cls
new file mode 100644
index 00000000..70586cd3
--- /dev/null
+++ b/dlrs/main/classes/LookupRollupStatusCheckControllerTest.cls
@@ -0,0 +1,106 @@
+@IsTest
+public with sharing class LookupRollupStatusCheckControllerTest {
+ @IsTest
+ static void testGetOutstandingScheduledItemsForLookup() {
+ List summaries = [
+ SELECT Id
+ FROM LookupRollupSummary2__mdt
+ LIMIT 1
+ ];
+ if (summaries.isEmpty()) {
+ return;
+ }
+ Integer val = LookupRollupStatusCheckController.getOutstandingScheduledItemsForLookup(
+ summaries[0].Id
+ );
+ Assert.isTrue(val >= 0, 'Expected a value, even zero');
+ }
+
+ @IsTest
+ static void testgetScheduledFullCalculates() {
+ List summaries = [
+ SELECT Id, DeveloperName
+ FROM LookupRollupSummary2__mdt
+ LIMIT 1
+ ];
+ if (summaries.isEmpty()) {
+ return;
+ }
+
+ // Build the CRON string
+ // Kickoff the calculate job for this lookup
+
+ String uniqueNameForJob =
+ 'rollup_' +
+ summaries[0].DeveloperName +
+ '(' +
+ summaries[0].Id.to15() +
+ ')';
+
+ String jobId;
+ try {
+ jobId = System.schedule(
+ uniqueNameForJob,
+ '0 0 * * * ? 2100',
+ new RollupCalculateJobSchedulable(summaries[0].Id, '')
+ );
+ } catch (Exception e) {
+ System.debug(
+ 'Failed to schedule, probably because it is already scheduled:' +
+ e.getMessage()
+ );
+ }
+
+ Datetime nextRunDate = LookupRollupStatusCheckController.getScheduledFullCalculates(
+ summaries[0].Id
+ );
+ Assert.isNotNull(nextRunDate);
+ if (String.isNotBlank(jobId)) {
+ System.abortJob(jobId);
+ }
+ }
+
+ @IsTest
+ static void testHasChildTriggerDeployed() {
+ // limited to records in the org, would have to mock things
+ LookupRollupStatusCheckController.hasChildTriggerDeployed(null);
+
+ List rec = [
+ SELECT Id
+ FROM LookupRollupSummary2__mdt
+ WHERE Active__c = TRUE AND CalculationMode__c IN ('Realtime', 'Scheduled')
+ LIMIT 1
+ ];
+ if (rec.size() == 0) {
+ return;
+ }
+
+ Boolean isDeployed = LookupRollupStatusCheckController.hasChildTriggerDeployed(
+ rec[0].Id
+ );
+ // Assume anything active and needing a trigger should have one
+ // this test could fail for silly reasons
+ Assert.areEqual(
+ true,
+ isDeployed,
+ 'Expected a rollup that requires a trigger to have one, could be false-positive'
+ );
+ }
+
+ @IsTest
+ static void testGetScheduledJobs() {
+ Integer startVal = LookupRollupStatusCheckController.getScheduledJobs();
+ String jobId = System.schedule(
+ 'Test Job 2000',
+ '0 0 * * * ? 2100',
+ new RollupJob()
+ );
+ Integer endVal = LookupRollupStatusCheckController.getScheduledJobs();
+ Assert.areEqual(
+ 1,
+ endVal - startVal,
+ 'Expected class to report an additional scheduled job'
+ );
+ System.abortJob(jobId);
+ }
+}
diff --git a/dlrs/main/classes/LookupRollupStatusCheckControllerTest.cls-meta.xml b/dlrs/main/classes/LookupRollupStatusCheckControllerTest.cls-meta.xml
new file mode 100644
index 00000000..835ede48
--- /dev/null
+++ b/dlrs/main/classes/LookupRollupStatusCheckControllerTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
\ No newline at end of file
diff --git a/dlrs/main/classes/MLRSControllerTest.cls-meta.xml b/dlrs/main/classes/MLRSControllerTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/MLRSControllerTest.cls-meta.xml
+++ b/dlrs/main/classes/MLRSControllerTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/ManageLookupRollupSummariesController.cls b/dlrs/main/classes/ManageLookupRollupSummariesController.cls
index cc174f1b..185bb590 100644
--- a/dlrs/main/classes/ManageLookupRollupSummariesController.cls
+++ b/dlrs/main/classes/ManageLookupRollupSummariesController.cls
@@ -334,7 +334,12 @@ public with sharing class ManageLookupRollupSummariesController {
public PageReference newWizard() {
try {
- PageReference newPage = Page.managelookuprollupsummaries_New;
+ String namespace = Utilities.namespace();
+ PageReference newPage = new PageReference(
+ '/lightning/n/' +
+ (namespace.length() > 0 ? namespace + '__' : '') +
+ 'ManageLookupRollupSummaries2'
+ );
newPage.setRedirect(true);
return newPage;
} catch (Exception e) {
diff --git a/dlrs/main/classes/ManageLookupRollupSummariesController.cls-meta.xml b/dlrs/main/classes/ManageLookupRollupSummariesController.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/ManageLookupRollupSummariesController.cls-meta.xml
+++ b/dlrs/main/classes/ManageLookupRollupSummariesController.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/ManageLookupRollupSummariesNewController.cls-meta.xml b/dlrs/main/classes/ManageLookupRollupSummariesNewController.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/ManageLookupRollupSummariesNewController.cls-meta.xml
+++ b/dlrs/main/classes/ManageLookupRollupSummariesNewController.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/ManageLookupRollupSummariesNewTest.cls-meta.xml b/dlrs/main/classes/ManageLookupRollupSummariesNewTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/ManageLookupRollupSummariesNewTest.cls-meta.xml
+++ b/dlrs/main/classes/ManageLookupRollupSummariesNewTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/MessageService.cls b/dlrs/main/classes/MessageService.cls
new file mode 100644
index 00000000..c80d8f7c
--- /dev/null
+++ b/dlrs/main/classes/MessageService.cls
@@ -0,0 +1,35 @@
+/**
+ * A service class around the platform Messaging class
+ * Allowing us to capture send attempts and record them for test validation
+ */
+public with sharing class MessageService {
+ @TestVisible
+ static List sentEmailList = new List();
+
+ public static Messaging.SendEmailResult[] sendEmail(
+ Messaging.Email[] emails
+ ) {
+ return sendEmail(emails, true);
+ }
+
+ public static Messaging.SendEmailResult[] sendEmail(
+ Messaging.Email[] emails,
+ Boolean allOrNothing
+ ) {
+ if (Test.isRunningTest()) {
+ sentEmailList.add(new SentEmails(emails, allOrNothing));
+ }
+ return Messaging.sendEmail(emails, allOrNothing);
+ }
+
+ @TestVisible
+ private class SentEmails {
+ public Messaging.Email[] emails;
+ public Boolean allOrNothing;
+
+ public SentEmails(Messaging.Email[] emails, Boolean allOrNothing) {
+ this.emails = emails;
+ this.allOrNothing = allOrNothing;
+ }
+ }
+}
diff --git a/dlrs/main/classes/MessageService.cls-meta.xml b/dlrs/main/classes/MessageService.cls-meta.xml
new file mode 100644
index 00000000..835ede48
--- /dev/null
+++ b/dlrs/main/classes/MessageService.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
\ No newline at end of file
diff --git a/dlrs/main/classes/MetadataServiceCalloutMock.cls-meta.xml b/dlrs/main/classes/MetadataServiceCalloutMock.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/MetadataServiceCalloutMock.cls-meta.xml
+++ b/dlrs/main/classes/MetadataServiceCalloutMock.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/ObjectSelectorController.cls b/dlrs/main/classes/ObjectSelectorController.cls
new file mode 100644
index 00000000..aa209a2a
--- /dev/null
+++ b/dlrs/main/classes/ObjectSelectorController.cls
@@ -0,0 +1,26 @@
+public with sharing class ObjectSelectorController {
+ @AuraEnabled(cacheable=true)
+ public static List getParentObjList() {
+ Map objectDescription = Schema.getGlobalDescribe();
+ List objects = new List();
+ for (Schema.SObjectType obj : objectDescription.values()) {
+ objects.add(new SObjectInfo(obj));
+ }
+ return objects;
+ }
+
+ public class SObjectInfo {
+ @AuraEnabled
+ public String fullName;
+ @AuraEnabled
+ public String label;
+
+ public SObjectInfo(Schema.SObjectType obj) {
+ Schema.DescribeSObjectResult description = obj.getDescribe(
+ SObjectDescribeOptions.DEFERRED
+ );
+ this.fullName = description.getName();
+ this.label = description.getLabel();
+ }
+ }
+}
diff --git a/dlrs/main/classes/ObjectSelectorController.cls-meta.xml b/dlrs/main/classes/ObjectSelectorController.cls-meta.xml
new file mode 100644
index 00000000..e13ee4c2
--- /dev/null
+++ b/dlrs/main/classes/ObjectSelectorController.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
diff --git a/dlrs/main/classes/ObjectSelectorControllerTest.cls b/dlrs/main/classes/ObjectSelectorControllerTest.cls
new file mode 100644
index 00000000..ad38215c
--- /dev/null
+++ b/dlrs/main/classes/ObjectSelectorControllerTest.cls
@@ -0,0 +1,20 @@
+@IsTest
+public with sharing class ObjectSelectorControllerTest {
+ @IsTest
+ static void testGetParentObjList() {
+ List objects = ObjectSelectorController.getParentObjList();
+ Assert.isFalse(objects.isEmpty());
+
+ Set expected = new Set{
+ 'Account',
+ 'Opportunity',
+ Schema.LookupRollupSummary2__mdt.getSObjectType().getDescribe().getName()
+ };
+ for (ObjectSelectorController.SObjectInfo sobj : objects) {
+ expected.remove(sobj.fullName);
+ }
+
+ // expected items should have been removed
+ Assert.isTrue(expected.isEmpty());
+ }
+}
diff --git a/dlrs/main/classes/ObjectSelectorControllerTest.cls-meta.xml b/dlrs/main/classes/ObjectSelectorControllerTest.cls-meta.xml
new file mode 100644
index 00000000..835ede48
--- /dev/null
+++ b/dlrs/main/classes/ObjectSelectorControllerTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
\ No newline at end of file
diff --git a/dlrs/main/classes/OptimizerComponentController.cls-meta.xml b/dlrs/main/classes/OptimizerComponentController.cls-meta.xml
index 02c8d395..7bd9b5c9 100644
--- a/dlrs/main/classes/OptimizerComponentController.cls-meta.xml
+++ b/dlrs/main/classes/OptimizerComponentController.cls-meta.xml
@@ -3,6 +3,6 @@
xmlns="urn:metadata.tooling.soap.sforce.com"
fqn="OptimizerComponentController"
>
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/OptimizerService.cls-meta.xml b/dlrs/main/classes/OptimizerService.cls-meta.xml
index 7d10c5a0..0b80192b 100644
--- a/dlrs/main/classes/OptimizerService.cls-meta.xml
+++ b/dlrs/main/classes/OptimizerService.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/OptimizerServiceTest.cls-meta.xml b/dlrs/main/classes/OptimizerServiceTest.cls-meta.xml
index 1cca9b19..268cd7e9 100644
--- a/dlrs/main/classes/OptimizerServiceTest.cls-meta.xml
+++ b/dlrs/main/classes/OptimizerServiceTest.cls-meta.xml
@@ -3,6 +3,6 @@
xmlns="urn:metadata.tooling.soap.sforce.com"
fqn="OptimizerServiceTest"
>
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupActionCalculate.cls-meta.xml b/dlrs/main/classes/RollupActionCalculate.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupActionCalculate.cls-meta.xml
+++ b/dlrs/main/classes/RollupActionCalculate.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupActionCalculateTest.cls-meta.xml b/dlrs/main/classes/RollupActionCalculateTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupActionCalculateTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupActionCalculateTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupCalculateController.cls-meta.xml b/dlrs/main/classes/RollupCalculateController.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupCalculateController.cls-meta.xml
+++ b/dlrs/main/classes/RollupCalculateController.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupCalculateControllerTest.cls-meta.xml b/dlrs/main/classes/RollupCalculateControllerTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupCalculateControllerTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupCalculateControllerTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupCalculateJob.cls b/dlrs/main/classes/RollupCalculateJob.cls
index 3a900275..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,
@@ -59,6 +79,32 @@ public with sharing class RollupCalculateJob implements Database.Batchable(masterRecords).keySet()
);
} catch (Exception e) {
+ LookupRollupSummaryLog__c logEntry = new LookupRollupSummaryLog__c();
+ List rollup = new RollupSummariesSelector()
+ .selectById(new Set{ lookupId });
+ logEntry.ParentId__c = lookupId;
+ if (!rollup.isEmpty()) {
+ // Log the failure updating the master record for review
+ logEntry.ParentObject__c = rollup[0]
+ .Record.getSObjectType()
+ .getDescribe()
+ .getName();
+ }
+
+ logEntry.ErrorMessage__c =
+ e.getMessage() +
+ ' : ' +
+ e.getStackTraceString();
+
+ upsert logEntry ParentId__c;
+
+ if (
+ DeclarativeLookupRollupSummaries__c.getInstance()
+ .DisableProblemEmails__c
+ ) {
+ // if emails are disabled then bail out
+ return;
+ }
// Ids in scope
List ids = new List();
for (Id recordId : new Map(masterRecords).keySet()) {
@@ -82,7 +128,7 @@ public with sharing class RollupCalculateJob implements Database.Batchable{ e.getMessage(), String.join(ids, ',') }
)
);
- Messaging.sendEmail(new List{ mail });
+ MessageService.sendEmail(new List{ mail });
}
}
diff --git a/dlrs/main/classes/RollupCalculateJob.cls-meta.xml b/dlrs/main/classes/RollupCalculateJob.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupCalculateJob.cls-meta.xml
+++ b/dlrs/main/classes/RollupCalculateJob.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupCalculateJobSchedulable.cls b/dlrs/main/classes/RollupCalculateJobSchedulable.cls
index f3e988a3..b58619b1 100644
--- a/dlrs/main/classes/RollupCalculateJobSchedulable.cls
+++ b/dlrs/main/classes/RollupCalculateJobSchedulable.cls
@@ -15,6 +15,33 @@ public with sharing class RollupCalculateJobSchedulable implements Schedulable {
// Enqueue the job to recalcualte the given rollup parent records
RollupService.runJobToCalculate(rollupRecordId, masterWhereClause);
} catch (Exception e) {
+ LookupRollupSummaryLog__c logEntry = new LookupRollupSummaryLog__c();
+ List rollup = new RollupSummariesSelector()
+ .selectById(new Set{ rollupRecordId });
+ logEntry.ParentId__c = rollupRecordId;
+ if (!rollup.isEmpty()) {
+ // Log the failure updating the master record for review
+ logEntry.ParentObject__c = rollup[0]
+ .Record.getSObjectType()
+ .getDescribe()
+ .getName();
+ }
+
+ logEntry.ErrorMessage__c =
+ e.getMessage() +
+ ' : ' +
+ e.getStackTraceString();
+
+ upsert logEntry ParentId__c;
+
+ if (
+ DeclarativeLookupRollupSummaries__c.getInstance()
+ .DisableProblemEmails__c
+ ) {
+ // if emails are disabled then bail out
+ return;
+ }
+
// Resolve the name of this job
Id triggerId = sc.getTriggerId();
Map jobNameByTriggerId = new CronJobDetailsSelector()
@@ -34,12 +61,12 @@ public with sharing class RollupCalculateJobSchedulable implements Schedulable {
mail.setPlainTextBody(
String.format(
'Error: {0} ' +
- 'Review the error, rollup definition and/or delete the Apex Scheduled job under Setup. ' +
- 'Check if the rollup still exists via the Manage Rollup Summaries and/or Lookup Rollup Summaries tabs. ',
+ 'Review the error, rollup definition and/or delete the Apex Scheduled job under Setup. ' +
+ 'Check if the rollup still exists via the Manage Rollup Summaries and/or Lookup Rollup Summaries tabs. ',
new List{ e.getMessage() }
)
);
- Messaging.sendEmail(new List{ mail });
+ MessageService.sendEmail(new List{ mail });
}
}
}
diff --git a/dlrs/main/classes/RollupCalculateJobSchedulable.cls-meta.xml b/dlrs/main/classes/RollupCalculateJobSchedulable.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupCalculateJobSchedulable.cls-meta.xml
+++ b/dlrs/main/classes/RollupCalculateJobSchedulable.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupCalculateJobSchedulableTest.cls b/dlrs/main/classes/RollupCalculateJobSchedulableTest.cls
new file mode 100644
index 00000000..4a38920c
--- /dev/null
+++ b/dlrs/main/classes/RollupCalculateJobSchedulableTest.cls
@@ -0,0 +1,85 @@
+@IsTest
+private class RollupCalculateJobSchedulableTest {
+ @IsTest
+ private static void testScheduleCalculateJobWithFailure() {
+ LookupRollupSummary__c rollupSummary = new LookupRollupSummary__c();
+ rollupSummary.ParentObject__c = 'Account';
+ rollupSummary.ChildObject__c = 'Contact';
+ rollupSummary.RelationShipField__c = 'AccountId';
+ rollupSummary.FieldToAggregate__c = 'Id';
+ rollupSummary.AggregateOperation__c = RollupSummaries.AggregateOperation.Count.name();
+ rollupSummary.AggregateResultField__c = 'Description';
+ rollupSummary.Active__c = true;
+ rollupSummary.CalculationMode__c = 'Developer';
+ insert rollupSummary;
+
+ // mark the job already running so it throws an exception
+ RollupService.checkJobAlreadyRunning(rollupSummary.Id, rollupSummary.Name);
+
+ ApexPages.StandardController standardController = new ApexPages.StandardController(
+ rollupSummary
+ );
+ RollupScheduledCalculateController controller = new RollupScheduledCalculateController(
+ standardController
+ );
+
+ Test.startTest();
+ controller.scheduleCalculateJob();
+ Test.stopTest();
+
+ List logs = [
+ SELECT Id, ParentId__c, ParentObject__c
+ FROM LookupRollupSummaryLog__c
+ ];
+ Assert.areEqual(1, logs.size());
+ Assert.areEqual(
+ Schema.LookupRollupSummary__c.getSObjectType().getDescribe().getName(),
+ logs[0].ParentObject__c
+ );
+ Assert.areEqual(rollupSummary.Id, logs[0].ParentId__c);
+ Assert.areEqual(1, MessageService.sentEmailList.size());
+ }
+
+ private testMethod static void testScheduleCalculateJobPreventEmail() {
+ LookupRollupSummary__c rollupSummary = new LookupRollupSummary__c();
+ rollupSummary.ParentObject__c = 'Account';
+ rollupSummary.ChildObject__c = 'Contact';
+ rollupSummary.RelationShipField__c = 'AccountId';
+ rollupSummary.FieldToAggregate__c = 'Id';
+ rollupSummary.AggregateOperation__c = RollupSummaries.AggregateOperation.Count.name();
+ rollupSummary.AggregateResultField__c = 'Description';
+ rollupSummary.Active__c = true;
+ rollupSummary.CalculationMode__c = 'Developer';
+ insert rollupSummary;
+
+ // mark the job already running so it throws an exception
+ RollupService.checkJobAlreadyRunning(rollupSummary.Id, rollupSummary.Name);
+
+ ApexPages.StandardController standardController = new ApexPages.StandardController(
+ rollupSummary
+ );
+ RollupScheduledCalculateController controller = new RollupScheduledCalculateController(
+ standardController
+ );
+ DeclarativeLookupRollupSummaries__c settings = new DeclarativeLookupRollupSummaries__c(
+ DisableProblemEmails__c = true
+ );
+ insert settings;
+
+ Test.startTest();
+ controller.scheduleCalculateJob();
+ Test.stopTest();
+
+ List logs = [
+ SELECT Id, ParentId__c, ParentObject__c
+ FROM LookupRollupSummaryLog__c
+ ];
+ Assert.areEqual(1, logs.size());
+ Assert.areEqual(
+ Schema.LookupRollupSummary__c.getSObjectType().getDescribe().getName(),
+ logs[0].ParentObject__c
+ );
+ Assert.areEqual(rollupSummary.Id, logs[0].ParentId__c);
+ Assert.areEqual(0, MessageService.sentEmailList.size());
+ }
+}
diff --git a/dlrs/main/classes/RollupCalculateJobSchedulableTest.cls-meta.xml b/dlrs/main/classes/RollupCalculateJobSchedulableTest.cls-meta.xml
new file mode 100644
index 00000000..835ede48
--- /dev/null
+++ b/dlrs/main/classes/RollupCalculateJobSchedulableTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
\ No newline at end of file
diff --git a/dlrs/main/classes/RollupCalculateJobTest.cls b/dlrs/main/classes/RollupCalculateJobTest.cls
new file mode 100644
index 00000000..648a65a4
--- /dev/null
+++ b/dlrs/main/classes/RollupCalculateJobTest.cls
@@ -0,0 +1,299 @@
+@IsTest
+private class RollupCalculateJobTest {
+ @IsTest
+ static void testCrashHandlingWithEmail() {
+ String prefix = LookupRollupSummary2__mdt.sObjectType.getDescribe()
+ .getKeyPrefix();
+
+ LookupRollupSummary2__mdt rollupCfg = new LookupRollupSummary2__mdt(
+ Id = prefix + '00000000000000D',
+ Label = 'A Summary',
+ DeveloperName = 'A_Summary',
+ ParentObject__c = 'A',
+ ChildObject__c = 'X',
+ RelationshipField__c = '1',
+ CalculationMode__c = 'Realtime',
+ Active__c = true
+ );
+ RollupSummariesSelector.setRollupCache(
+ false,
+ false,
+ RollupSummary.toList(new List{ rollupCfg })
+ );
+
+ Account a = new Account(Name = 'Test');
+ insert a;
+
+ RollupCalculateJob job = new RollupCalculateJob(rollupCfg.Id);
+ Test.startTest();
+ job.execute(new MockBatchableContext(), new List{ a });
+ Test.stopTest();
+
+ List logs = [
+ SELECT Id, ParentId__c, ParentObject__c
+ FROM LookupRollupSummaryLog__c
+ ];
+ Assert.areEqual(1, logs.size());
+ Assert.areEqual(
+ Schema.LookupRollupSummary2__mdt.getSObjectType().getDescribe().getName(),
+ logs[0].ParentObject__c
+ );
+ Assert.areEqual(rollupCfg.Id, logs[0].ParentId__c);
+ Assert.areEqual(1, MessageService.sentEmailList.size());
+ }
+
+ @IsTest
+ static void testCrashHandlingWithoutEmail() {
+ String prefix = LookupRollupSummary2__mdt.sObjectType.getDescribe()
+ .getKeyPrefix();
+
+ LookupRollupSummary2__mdt rollupCfg = new LookupRollupSummary2__mdt(
+ Id = prefix + '00000000000000D',
+ Label = 'A Summary',
+ DeveloperName = 'A_Summary',
+ ParentObject__c = 'A',
+ ChildObject__c = 'X',
+ RelationshipField__c = '1',
+ CalculationMode__c = 'Realtime',
+ Active__c = true
+ );
+ RollupSummariesSelector.setRollupCache(
+ false,
+ false,
+ RollupSummary.toList(new List{ rollupCfg })
+ );
+
+ Account a = new Account(Name = 'Test');
+ insert a;
+
+ DeclarativeLookupRollupSummaries__c settings = new DeclarativeLookupRollupSummaries__c(
+ DisableProblemEmails__c = true
+ );
+ insert settings;
+
+ RollupCalculateJob job = new RollupCalculateJob(rollupCfg.Id);
+ Test.startTest();
+ job.execute(new MockBatchableContext(), new List{ a });
+ Test.stopTest();
+
+ List logs = [
+ SELECT Id, ParentId__c, ParentObject__c
+ FROM LookupRollupSummaryLog__c
+ ];
+ Assert.areEqual(1, logs.size());
+ Assert.areEqual(
+ Schema.LookupRollupSummary2__mdt.getSObjectType().getDescribe().getName(),
+ logs[0].ParentObject__c
+ );
+ Assert.areEqual(rollupCfg.Id, logs[0].ParentId__c);
+ Assert.areEqual(0, MessageService.sentEmailList.size());
+ }
+
+ @IsTest
+ static void testRunBatch() {
+ 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 })
+ );
+
+ Account a = new Account(Name = 'Test');
+ insert a;
+
+ RollupCalculateJob job = new RollupCalculateJob(rollupCfg.Id, 'Id != NULL');
+ Test.startTest();
+ Database.executeBatch(job);
+ Test.stopTest();
+
+ 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 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, SetupEntityId
+ 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
+ }
+
+ CustomPermission perm = [
+ SELECT DeveloperName, NamespacePrefix
+ FROM CustomPermission
+ WHERE Id = :permSetsWithAccess[0].SetupEntityId
+ ];
+
+ String permName = perm.DeveloperName;
+ if (String.isNotBlank(perm.NamespacePrefix)) {
+ permName = perm.NamespacePrefix + '__' + perm.DeveloperName;
+ }
+
+ // 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 = permName
+ );
+ 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;
+ Test.startTest();
+ Assert.isTrue(
+ Utilities.userHasCustomPermission(permName),
+ 'Expected user to have ' + permName
+ );
+ // 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';
+ }
+
+ public Id getChildJobId() {
+ return '100000000000000';
+ }
+ }
+}
diff --git a/dlrs/main/classes/RollupCalculateJobTest.cls-meta.xml b/dlrs/main/classes/RollupCalculateJobTest.cls-meta.xml
new file mode 100644
index 00000000..835ede48
--- /dev/null
+++ b/dlrs/main/classes/RollupCalculateJobTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
\ No newline at end of file
diff --git a/dlrs/main/classes/RollupController.cls b/dlrs/main/classes/RollupController.cls
index 0055e27f..4a4d4a72 100644
--- a/dlrs/main/classes/RollupController.cls
+++ b/dlrs/main/classes/RollupController.cls
@@ -29,7 +29,7 @@
**/
public with sharing class RollupController {
@TestVisible
- private static String FALLBACK_COMPONENT_API_VERSION = '60.0';
+ private static String FALLBACK_COMPONENT_API_VERSION = '63.0';
public String ZipData { get; set; }
@@ -542,26 +542,20 @@ public with sharing class RollupController {
ApexPages.addMessage(
new ApexPages.Message(
ApexPages.Severity.Info,
- 'Apex Trigger ' +
- RollupTriggerName +
- ' is installed.'
+ 'Apex Trigger ' + RollupTriggerName + ' is installed.'
)
);
ApexPages.addMessage(
new ApexPages.Message(
ApexPages.Severity.Info,
- 'Apex Class ' +
- RollupTriggerTestName +
- ' is installed.'
+ 'Apex Class ' + RollupTriggerTestName + ' is installed.'
)
);
if (RollupParentTrigger != null) {
ApexPages.addMessage(
new ApexPages.Message(
ApexPages.Severity.Info,
- 'Apex Trigger ' +
- RollupParentTriggerName +
- ' is installed.'
+ 'Apex Trigger ' + RollupParentTriggerName + ' is installed.'
)
);
}
diff --git a/dlrs/main/classes/RollupController.cls-meta.xml b/dlrs/main/classes/RollupController.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupController.cls-meta.xml
+++ b/dlrs/main/classes/RollupController.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupControllerTest.cls-meta.xml b/dlrs/main/classes/RollupControllerTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupControllerTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupControllerTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupDmlGuard.cls-meta.xml b/dlrs/main/classes/RollupDmlGuard.cls-meta.xml
index 3a10d2eb..835ede48 100644
--- a/dlrs/main/classes/RollupDmlGuard.cls-meta.xml
+++ b/dlrs/main/classes/RollupDmlGuard.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
\ No newline at end of file
diff --git a/dlrs/main/classes/RollupDmlGuardTest.cls-meta.xml b/dlrs/main/classes/RollupDmlGuardTest.cls-meta.xml
index 3a10d2eb..835ede48 100644
--- a/dlrs/main/classes/RollupDmlGuardTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupDmlGuardTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
\ No newline at end of file
diff --git a/dlrs/main/classes/RollupEditorController.cls b/dlrs/main/classes/RollupEditorController.cls
new file mode 100644
index 00000000..e35018e1
--- /dev/null
+++ b/dlrs/main/classes/RollupEditorController.cls
@@ -0,0 +1,360 @@
+public with sharing class RollupEditorController {
+ @AuraEnabled
+ public static List getAllRollupConfigs() {
+ List rollups = new List();
+ for (
+ RollupSummary summary : new RollupSummariesSelector.CustomMetadataSelector(
+ true,
+ true // retrieve for Edit
+ )
+ .selectAll()
+ ) {
+ rollups.add(new RollupConfig((LookupRollupSummary2__mdt) summary.Record));
+ }
+ return rollups;
+ }
+
+ @AuraEnabled
+ public static RollupConfig getRollupConfig(String rollupName) {
+ return new RollupConfig(
+ (LookupRollupSummary2__mdt) new RollupSummariesSelector.CustomMetadataSelector(
+ true,
+ true // retrieve for Edit
+ )
+ .selectByDeveloperName(new Set{ rollupName })[0]
+ .Record
+ );
+ }
+
+ public class SelectOption {
+ @AuraEnabled
+ public String value;
+
+ @AuraEnabled
+ public String label;
+
+ @AuraEnabled
+ public String icon;
+
+ @AuraEnabled
+ public String type;
+
+ @AuraEnabled
+ public List referencesTo;
+
+ public SelectOption(String value, String label) {
+ this.value = value;
+ this.label = label;
+ this.referencesTo = new List();
+ }
+ }
+
+ @AuraEnabled
+ public static List getFieldOptions(String objectName) {
+ Map mapOfFields = Schema.getGlobalDescribe()
+ .get(objectName)
+ .getDescribe()
+ .fields.getMap();
+
+ SelectOption emptyOption = new SelectOption('', '-- Select --');
+
+ List options = new List();
+ options.add(emptyOption);
+
+ for (Schema.SObjectField field : mapOfFields.values()) {
+ Schema.DescribeFieldResult fr = field.getDescribe();
+ String fieldName = fr.getName();
+ String label = fr.getLabel();
+
+ SelectOption option = new SelectOption(
+ fieldName,
+ String.format('{0} ({1})', new List{ label, fieldName })
+ );
+ option.type = String.valueof(fr.getType());
+ List types = fr.getReferenceTo();
+ // if this fields points to a single SObject, pass that up
+ for (Schema.sObjectType t : types) {
+ option.referencesTo.add(types[0].getDescribe().getName());
+ }
+ options.add(option);
+ }
+
+ return options;
+ }
+
+ /**
+ * returns a map of fieldname => list to use in displaying error in the UI
+ * general errors are applied to the 'record' field name
+ */
+ @AuraEnabled
+ public static Map> validateRollupConfig(String rollup) {
+ RollupConfig cfg = (RollupConfig) JSON.deserialize(
+ rollup,
+ RollupConfig.class
+ );
+ // do things like validate that the Rollup Criteria can be used in a SOQL query
+ LookupRollupSummary2__mdt lookupConfig = cfg.getRecord();
+
+ // Process only Custom Metadata records here
+ List mdtRecords = new List();
+ mdtRecords.add(lookupConfig);
+
+ // Validate via Domain class and throw appropirte exception
+ RollupSummaries rollupSummaries = new RollupSummaries(mdtRecords);
+ rollupSummaries.onValidate();
+ Map> errorMap;
+
+ for (RollupSummary rollupSummaryRecord : rollupSummaries.Records) {
+ errorMap = collectErrors(rollupSummaryRecord);
+ }
+
+ LookupRollupSummary2__mdt existing = LookupRollupSummary2__mdt.getInstance(
+ lookupConfig.DeveloperName
+ );
+ if (existing != null && existing.Id != lookupConfig.Id) {
+ if (errorMap == null) {
+ errorMap = new Map>();
+ }
+
+ if (!errorMap.containsKey('developerName')) {
+ errorMap.put('developerName', new List());
+ }
+
+ errorMap.get('developerName')
+ .add('API name already in use by ' + existing.Id);
+ }
+
+ return errorMap;
+ }
+
+ @AuraEnabled
+ public static Id saveRollupConfig(String rollup) {
+ RollupConfig cfg = (RollupConfig) JSON.deserialize(
+ rollup,
+ RollupConfig.class
+ );
+ LookupRollupSummary2__mdt lookupConfig = cfg.getRecord();
+ return CustomMetadataService.initiateMetadataSave(
+ new List{ lookupConfig }
+ );
+ }
+
+ @AuraEnabled
+ public static Id deleteRollupConfig(String rollupName) {
+ try {
+ return CustomMetadataService.deleteMetadataAsync(
+ LookupRollupSummary2__mdt.getSObjectType(),
+ new List{ rollupName }
+ );
+ } catch (Exception e) {
+ throw new AuraHandledException(e.getMessage());
+ }
+ }
+
+ @AuraEnabled(cacheable=true)
+ public static String getManageTriggerPageUrl(Id rollupId) {
+ PageReference pageRef = Page.managetriggermdt;
+ pageRef.getParameters().put('id', rollupId);
+ return pageRef.getUrl();
+ }
+
+ @AuraEnabled(cacheable=true)
+ public static String getFullCalculatePageUrl(Id rollupId) {
+ PageReference pageRef = Page.rollupcalculatemdt;
+ pageRef.getParameters().put('id', rollupId);
+ return pageRef.getUrl();
+ }
+
+ @AuraEnabled(cacheable=true)
+ public static String getScheduleCalculatePageUrl(Id rollupId) {
+ PageReference pageRef = Page.rollupscheduledcalculatemdt;
+ pageRef.getParameters().put('id', rollupId);
+ return pageRef.getUrl();
+ }
+
+ private static Map> collectErrors(RollupSummary rollup) {
+ Map> errorMap = new Map>();
+ if (rollup.Error != null) {
+ buildErrorIfNeeded(errorMap, 'record', new List{ rollup.Error });
+ }
+ buildErrorIfNeeded(errorMap, 'active', rollup.Fields.Active.errors);
+ buildErrorIfNeeded(
+ errorMap,
+ 'aggregateOperation',
+ rollup.Fields.AggregateOperation.errors
+ );
+ buildErrorIfNeeded(
+ errorMap,
+ 'aggregateResultField',
+ rollup.Fields.AggregateResultField.errors
+ );
+ buildErrorIfNeeded(
+ errorMap,
+ 'calculationMode',
+ rollup.Fields.CalculationMode.errors
+ );
+ buildErrorIfNeeded(
+ errorMap,
+ 'calculationSharingMode',
+ rollup.Fields.CalculationSharingMode.errors
+ );
+ buildErrorIfNeeded(
+ errorMap,
+ 'childObject',
+ rollup.Fields.ChildObject.errors
+ );
+ buildErrorIfNeeded(
+ errorMap,
+ 'description',
+ rollup.Fields.Description.errors
+ );
+ buildErrorIfNeeded(
+ errorMap,
+ 'fieldToAggregate',
+ rollup.Fields.FieldToAggregate.errors
+ );
+ buildErrorIfNeeded(
+ errorMap,
+ 'developerName',
+ rollup.Fields.UniqueName.errors
+ );
+ buildErrorIfNeeded(
+ errorMap,
+ 'parentObject',
+ rollup.Fields.ParentObject.errors
+ );
+ buildErrorIfNeeded(
+ errorMap,
+ 'relationshipCriteria',
+ rollup.Fields.RelationshipCriteria.errors
+ );
+ buildErrorIfNeeded(
+ errorMap,
+ 'relationshipCriteriaFields',
+ rollup.Fields.RelationshipCriteriaFields.errors
+ );
+ buildErrorIfNeeded(
+ errorMap,
+ 'relationshipField',
+ rollup.Fields.RelationshipField.errors
+ );
+ buildErrorIfNeeded(errorMap, 'testCode', rollup.Fields.TestCode.errors);
+ buildErrorIfNeeded(errorMap, 'rowLimit', rollup.Fields.RowLimit.errors);
+ return errorMap;
+ }
+
+ private static void buildErrorIfNeeded(
+ Map> errorMap,
+ String fieldName,
+ List errors
+ ) {
+ if (errors.isEmpty()) {
+ return;
+ }
+ errorMap.put(fieldName, errors);
+ }
+
+ @SuppressWarnings('PMD.TooManyFields')
+ public class RollupConfig {
+ @AuraEnabled
+ public Id id;
+ @AuraEnabled
+ public String label;
+ @AuraEnabled
+ public String developerName;
+ @AuraEnabled
+ public Boolean active;
+ @AuraEnabled
+ public Boolean aggregateAllRows;
+ @AuraEnabled
+ public String aggregateOperation;
+ @AuraEnabled
+ public String aggregateResultField;
+ @AuraEnabled
+ public String bypassPermissionApiName;
+ @AuraEnabled
+ public String calculationMode;
+ @AuraEnabled
+ public String calculationSharingMode;
+ @AuraEnabled
+ public String childObject;
+ @AuraEnabled
+ public String concatenateDelimiter;
+ @AuraEnabled
+ public String description;
+ @AuraEnabled
+ public String fieldToAggregate;
+ @AuraEnabled
+ public String fieldToOrderBy;
+ @AuraEnabled
+ public String parentObject;
+ @AuraEnabled
+ public String relationshipCriteria;
+ @AuraEnabled
+ public String relationshipCriteriaFields;
+ @AuraEnabled
+ public String relationshipField;
+ @AuraEnabled
+ public Decimal rowLimit;
+ @AuraEnabled
+ public String testCode;
+ @AuraEnabled
+ public String testCodeParent;
+ @AuraEnabled
+ public Boolean testCodeSeeAllData;
+
+ public RollupConfig(LookupRollupSummary2__mdt record) {
+ this.id = record.Id;
+ this.label = record.Label;
+ this.developerName = record.DeveloperName;
+ this.active = record.Active__c;
+ 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;
+ this.concatenateDelimiter = record.ConcatenateDelimiter__c;
+ this.description = record.Description__c;
+ this.fieldToAggregate = record.FieldToAggregate__c;
+ this.fieldToOrderBy = record.FieldToOrderBy__c;
+ this.parentObject = record.ParentObject__c;
+ this.relationshipCriteria = record.RelationshipCriteria__c;
+ this.relationshipCriteriaFields = record.RelationshipCriteriaFields__c;
+ this.relationshipField = record.RelationshipField__c;
+ this.rowLimit = record.RowLimit__c;
+ this.testCode = record.TestCode2__c;
+ this.testCodeParent = record.TestCodeParent__c;
+ this.testCodeSeeAllData = record.TestCodeSeeAllData__c;
+ }
+
+ public LookupRollupSummary2__mdt getRecord() {
+ LookupRollupSummary2__mdt record = new LookupRollupSummary2__mdt();
+ record.Id = this.id;
+ record.Label = this.label;
+ record.DeveloperName = this.developerName;
+ record.Active__c = this.active;
+ 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;
+ record.ConcatenateDelimiter__c = this.concatenateDelimiter;
+ record.Description__c = this.description;
+ record.FieldToAggregate__c = this.fieldToAggregate;
+ record.FieldToOrderBy__c = this.fieldToOrderBy;
+ record.ParentObject__c = this.parentObject;
+ record.RelationshipCriteria__c = this.relationshipCriteria;
+ record.RelationshipCriteriaFields__c = this.relationshipCriteriaFields;
+ record.RelationshipField__c = this.relationshipField;
+ record.RowLimit__c = this.rowLimit;
+ record.TestCode2__c = this.testCode;
+ record.TestCodeParent__c = this.testCodeParent;
+ record.TestCodeSeeAllData__c = this.testCodeSeeAllData;
+ return record;
+ }
+ }
+}
diff --git a/dlrs/main/classes/RollupEditorController.cls-meta.xml b/dlrs/main/classes/RollupEditorController.cls-meta.xml
new file mode 100644
index 00000000..5f399c3c
--- /dev/null
+++ b/dlrs/main/classes/RollupEditorController.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
diff --git a/dlrs/main/classes/RollupEditorControllerTest.cls b/dlrs/main/classes/RollupEditorControllerTest.cls
new file mode 100644
index 00000000..525b8bea
--- /dev/null
+++ b/dlrs/main/classes/RollupEditorControllerTest.cls
@@ -0,0 +1,212 @@
+@IsTest
+public with sharing class RollupEditorControllerTest {
+ static LookupRollupSummary2__mdt lup = new LookupRollupSummary2__mdt(
+ Id = Schema.LookupRollupSummary2__mdt.getSObjectType()
+ .getDescribe()
+ .getKeyPrefix() + '000000000aaa',
+ Label = 'TestLabel',
+ DeveloperName = 'TestDevName',
+ Active__c = true,
+ AggregateAllRows__c = true,
+ AggregateOperation__c = 'Count',
+ AggregateResultField__c = 'ResultField',
+ CalculationMode__c = 'CalcMode',
+ CalculationSharingMode__c = 'SharingMode',
+ ChildObject__c = 'ChildObject',
+ ConcatenateDelimiter__c = 'Delim',
+ Description__c = 'Desc',
+ FieldToAggregate__c = 'FieldToAgg',
+ FieldToOrderBy__c = 'FieldToOrder',
+ ParentObject__c = 'ParentObject',
+ RelationshipCriteria__c = 'RelCriteria',
+ RelationshipCriteriaFields__c = 'Field1\nField2',
+ RelationshipField__c = 'RelField',
+ RowLimit__c = 100,
+ TestCode2__c = 'TestCode',
+ TestCodeParent__c = 'ParentTestCode',
+ TestCodeSeeAllData__c = true
+ );
+
+ @IsTest
+ static void testGetAllRollupConfigs() {
+ List lookups = [
+ SELECT Id
+ FROM LookupRollupSummary2__mdt
+ ];
+ List configs = RollupEditorController.getAllRollupConfigs();
+ Assert.areEqual(lookups.size(), configs.size());
+ }
+
+ @IsTest
+ static void testGetRollupConfig() {
+ List lookups = [
+ SELECT
+ Id,
+ Label,
+ DeveloperName,
+ Active__c,
+ AggregateAllRows__c,
+ AggregateOperation__c,
+ AggregateResultField__c,
+ CalculationMode__c,
+ CalculationSharingMode__c,
+ ChildObject__c,
+ ConcatenateDelimiter__c,
+ Description__c,
+ FieldToAggregate__c,
+ FieldToOrderBy__c,
+ ParentObject__c,
+ RelationshipCriteria__c,
+ RelationshipCriteriaFields__c,
+ RelationshipField__c,
+ RowLimit__c,
+ TestCode__c,
+ TestCodeParent__c,
+ TestCodeSeeAllData__c
+ FROM LookupRollupSummary2__mdt
+ ];
+ if (lookups.isEmpty()) {
+ return;
+ }
+ LookupRollupSummary2__mdt lup = lookups[0];
+ RollupEditorController.RollupConfig cfg = RollupEditorController.getRollupConfig(
+ lup.DeveloperName
+ );
+ Assert.areEqual(lup.Id, cfg.id);
+ Assert.areEqual(lup.Label, cfg.label);
+ Assert.areEqual(lup.DeveloperName, cfg.developerName);
+ }
+
+ @IsTest
+ static void testGetFieldOptions() {
+ List fields = RollupEditorController.getFieldOptions(
+ 'User'
+ );
+ Assert.isFalse(fields.isEmpty());
+ }
+
+ @IsTest
+ static void testValidateRollupConfig() {
+ RollupEditorController.RollupConfig cfg = new RollupEditorController.RollupConfig(
+ lup
+ );
+ Map> errors = RollupEditorController.validateRollupConfig(
+ JSON.serialize(cfg)
+ );
+ Assert.areEqual(
+ '{"rowLimit":["Row Limit is only supported on Last and Concatenate operators."],"parentObject":["Object does not exist."],"childObject":["Object does not exist."]}',
+ JSON.serialize(errors)
+ );
+ }
+
+ @IsTest
+ static void testSaveRollupConfig() {
+ RollupEditorController.RollupConfig cfg = new RollupEditorController.RollupConfig(
+ lup
+ );
+ try {
+ Id depId = RollupEditorController.saveRollupConfig(JSON.serialize(cfg));
+ Assert.fail('Should throw an exception');
+ } catch (System.AsyncException e) {
+ Assert.areEqual(
+ 'Metadata cannot be deployed from within a test',
+ e.getMessage()
+ );
+ }
+ }
+
+ @IsTest
+ static void testDeleteRollupConfig() {
+ Id queueableId = RollupEditorController.deleteRollupConfig('Hello');
+ AsyncApexJob queueJob = [
+ SELECT Id
+ FROM AsyncApexJob
+ WHERE Id = :queueableId
+ ];
+ Assert.areEqual(queueableId, queueJob.Id);
+ System.abortJob(queueJob.Id);
+ }
+
+ @IsTest
+ static void testGetManageTriggerPageUrl() {
+ String url = RollupEditorController.getManageTriggerPageUrl(lup.Id);
+ PageReference pageRef = Page.managetriggermdt;
+ Assert.areEqual(pageRef.getUrl() + '?id=' + lup.Id, url);
+ }
+
+ @IsTest
+ static void testGetFullCalculatePageUrl() {
+ String url = RollupEditorController.getFullCalculatePageUrl(lup.Id);
+ PageReference pageRef = Page.rollupcalculatemdt;
+ Assert.areEqual(pageRef.getUrl() + '?id=' + lup.Id, url);
+ }
+
+ @IsTest
+ static void testGetScheduleCalculatePageUrl() {
+ String url = RollupEditorController.getScheduleCalculatePageUrl(lup.Id);
+ PageReference pageRef = Page.rollupscheduledcalculatemdt;
+ Assert.areEqual(pageRef.getUrl() + '?id=' + lup.Id, url);
+ }
+
+ @IsTest
+ static void testRollupConfig() {
+ RollupEditorController.RollupConfig cfg = new RollupEditorController.RollupConfig(
+ lup
+ );
+ Assert.areEqual(lup.Id, cfg.id);
+ Assert.areEqual(lup.Label, cfg.label);
+ Assert.areEqual(lup.DeveloperName, cfg.developerName);
+ Assert.areEqual(lup.Active__c, cfg.active);
+ Assert.areEqual(lup.AggregateAllRows__c, cfg.aggregateAllRows);
+ Assert.areEqual(lup.AggregateOperation__c, cfg.aggregateOperation);
+ Assert.areEqual(lup.AggregateResultField__c, cfg.aggregateResultField);
+ Assert.areEqual(lup.CalculationMode__c, cfg.calculationMode);
+ Assert.areEqual(lup.CalculationSharingMode__c, cfg.calculationSharingMode);
+ Assert.areEqual(lup.ChildObject__c, cfg.childObject);
+ Assert.areEqual(lup.ConcatenateDelimiter__c, cfg.concatenateDelimiter);
+ Assert.areEqual(lup.Description__c, cfg.description);
+ Assert.areEqual(lup.FieldToAggregate__c, cfg.fieldToAggregate);
+ Assert.areEqual(lup.FieldToOrderBy__c, cfg.fieldToOrderBy);
+ Assert.areEqual(lup.ParentObject__c, cfg.parentObject);
+ Assert.areEqual(lup.RelationshipCriteria__c, cfg.relationshipCriteria);
+ Assert.areEqual(
+ lup.RelationshipCriteriaFields__c,
+ cfg.relationshipCriteriaFields
+ );
+ Assert.areEqual(lup.RelationshipField__c, cfg.relationshipField);
+ Assert.areEqual(lup.RowLimit__c, cfg.rowLimit);
+ Assert.areEqual(lup.TestCode2__c, cfg.testCode);
+ Assert.areEqual(lup.TestCodeParent__c, cfg.testCodeParent);
+ Assert.areEqual(lup.TestCodeSeeAllData__c, cfg.testCodeSeeAllData);
+ LookupRollupSummary2__mdt newLup = cfg.getRecord();
+
+ Assert.areEqual(cfg.id, newLup.Id);
+ Assert.areEqual(cfg.label, newLup.Label);
+ Assert.areEqual(cfg.developerName, newLup.DeveloperName);
+ Assert.areEqual(cfg.active, newLup.Active__c);
+ Assert.areEqual(cfg.aggregateAllRows, newLup.AggregateAllRows__c);
+ Assert.areEqual(cfg.aggregateOperation, newLup.AggregateOperation__c);
+ Assert.areEqual(cfg.aggregateResultField, newLup.AggregateResultField__c);
+ Assert.areEqual(cfg.calculationMode, newLup.CalculationMode__c);
+ Assert.areEqual(
+ cfg.calculationSharingMode,
+ newLup.CalculationSharingMode__c
+ );
+ Assert.areEqual(cfg.childObject, newLup.ChildObject__c);
+ Assert.areEqual(cfg.concatenateDelimiter, newLup.ConcatenateDelimiter__c);
+ Assert.areEqual(cfg.description, newLup.Description__c);
+ Assert.areEqual(cfg.fieldToAggregate, newLup.FieldToAggregate__c);
+ Assert.areEqual(cfg.fieldToOrderBy, newLup.FieldToOrderBy__c);
+ Assert.areEqual(cfg.parentObject, newLup.ParentObject__c);
+ Assert.areEqual(cfg.relationshipCriteria, newLup.RelationshipCriteria__c);
+ Assert.areEqual(
+ cfg.relationshipCriteriaFields,
+ newLup.RelationshipCriteriaFields__c
+ );
+ Assert.areEqual(cfg.relationshipField, newLup.RelationshipField__c);
+ Assert.areEqual(cfg.rowLimit, newLup.RowLimit__c);
+ Assert.areEqual(cfg.testCode, newLup.TestCode2__c);
+ Assert.areEqual(cfg.testCodeParent, newLup.TestCodeParent__c);
+ Assert.areEqual(cfg.testCodeSeeAllData, newLup.TestCodeSeeAllData__c);
+ }
+}
diff --git a/dlrs/main/classes/RollupEditorControllerTest.cls-meta.xml b/dlrs/main/classes/RollupEditorControllerTest.cls-meta.xml
new file mode 100644
index 00000000..835ede48
--- /dev/null
+++ b/dlrs/main/classes/RollupEditorControllerTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
\ No newline at end of file
diff --git a/dlrs/main/classes/RollupJob.cls b/dlrs/main/classes/RollupJob.cls
index 2c494a5e..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) rollupSummaryScheduleItems
);
} catch (Exception e) {
+ LookupRollupSummaryLog__c logEntry = new LookupRollupSummaryLog__c();
+ logEntry.ParentId__c = 'RollupJob';
+ logEntry.ParentObject__c = 'RollupJob';
+
+ logEntry.ErrorMessage__c =
+ e.getMessage() +
+ ' : ' +
+ e.getStackTraceString();
+
+ upsert logEntry ParentId__c;
+
+ if (
+ DeclarativeLookupRollupSummaries__c.getInstance()
+ .DisableProblemEmails__c
+ ) {
+ // if emails are disabled then bail out
+ return;
+ }
+
// Ids in scope
List ids = new List();
for (
@@ -74,7 +108,7 @@ global with sharing class RollupJob implements Schedulable, Database.Batchable{ e.getMessage(), String.join(ids, ',') }
)
);
- Messaging.sendEmail(new List{ mail });
+ MessageService.sendEmail(new List{ mail });
}
}
diff --git a/dlrs/main/classes/RollupJob.cls-meta.xml b/dlrs/main/classes/RollupJob.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupJob.cls-meta.xml
+++ b/dlrs/main/classes/RollupJob.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupJobTest.cls b/dlrs/main/classes/RollupJobTest.cls
index 12ad6f33..7c79ee23 100644
--- a/dlrs/main/classes/RollupJobTest.cls
+++ b/dlrs/main/classes/RollupJobTest.cls
@@ -26,4 +26,384 @@
@IsTest
private class RollupJobTest {
+ @IsTest
+ static void testSchedule() {
+ RollupJob job = new RollupJob();
+ Test.startTest();
+ Id schedId = System.schedule('TestSchedRollupJob8724', '0 0 * * * ?', job);
+ Test.stopTest();
+ }
+
+ @IsTest
+ static void testRunJob() {
+ 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 })
+ );
+
+ Account a = new Account(Name = 'Test');
+ insert a;
+
+ DeclarativeLookupRollupSummaries__c settings = new DeclarativeLookupRollupSummaries__c(
+ DisableProblemEmails__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();
+ Database.executeBatch(job);
+ Test.stopTest();
+
+ List logs = [
+ SELECT Id, ParentId__c, ParentObject__c
+ FROM LookupRollupSummaryLog__c
+ ];
+ Assert.areEqual(0, logs.size());
+ }
+
+ @IsTest
+ static void testDisabledRunJob() {
+ 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 })
+ );
+
+ 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, SetupEntityId
+ 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
+ }
+
+ CustomPermission perm = [
+ SELECT DeveloperName, NamespacePrefix
+ FROM CustomPermission
+ WHERE Id = :permSetsWithAccess[0].SetupEntityId
+ ];
+
+ String permName = perm.DeveloperName;
+ if (String.isNotBlank(perm.NamespacePrefix)) {
+ permName = perm.NamespacePrefix + '__' + perm.DeveloperName;
+ }
+
+ // 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 = permName
+ );
+
+ 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;
+ Test.startTest();
+ Assert.isTrue(
+ Utilities.userHasCustomPermission(permName),
+ 'Expected user to have ' + permName
+ );
+ // 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()
+ .getKeyPrefix();
+
+ LookupRollupSummary2__mdt rollupCfg = new LookupRollupSummary2__mdt(
+ Id = prefix + '00000000000000D',
+ Label = 'A Summary',
+ DeveloperName = 'A_Summary',
+ ParentObject__c = 'A',
+ ChildObject__c = 'X',
+ RelationshipField__c = '1',
+ CalculationMode__c = 'Realtime',
+ Active__c = true
+ );
+ RollupSummariesSelector.setRollupCache(
+ false,
+ false,
+ RollupSummary.toList(new List{ rollupCfg })
+ );
+
+ Account a = new Account(Name = 'Test');
+ insert a;
+
+ 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();
+ job.execute(new MockBatchableContext(), items);
+ Test.stopTest();
+
+ List logs = [
+ SELECT Id, ParentId__c, ParentObject__c
+ FROM LookupRollupSummaryLog__c
+ ];
+ Assert.areEqual(1, logs.size());
+ Assert.areEqual('RollupJob', logs[0].ParentObject__c);
+ Assert.areEqual('RollupJob', logs[0].ParentId__c);
+ Assert.areEqual(1, MessageService.sentEmailList.size());
+ }
+
+ @IsTest
+ static void testFailureWithoutEmail() {
+ String prefix = LookupRollupSummary2__mdt.sObjectType.getDescribe()
+ .getKeyPrefix();
+
+ LookupRollupSummary2__mdt rollupCfg = new LookupRollupSummary2__mdt(
+ Id = prefix + '00000000000000D',
+ Label = 'A Summary',
+ DeveloperName = 'A_Summary',
+ ParentObject__c = 'A',
+ ChildObject__c = 'X',
+ RelationshipField__c = '1',
+ CalculationMode__c = 'Realtime',
+ Active__c = true
+ );
+ RollupSummariesSelector.setRollupCache(
+ false,
+ false,
+ RollupSummary.toList(new List{ rollupCfg })
+ );
+
+ Account a = new Account(Name = 'Test');
+ insert a;
+
+ DeclarativeLookupRollupSummaries__c settings = new DeclarativeLookupRollupSummaries__c(
+ DisableProblemEmails__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();
+ job.execute(new MockBatchableContext(), items);
+ Test.stopTest();
+
+ List logs = [
+ SELECT Id, ParentId__c, ParentObject__c
+ FROM LookupRollupSummaryLog__c
+ ];
+ Assert.areEqual(1, logs.size());
+ Assert.areEqual('RollupJob', logs[0].ParentObject__c);
+ Assert.areEqual('RollupJob', logs[0].ParentId__c);
+ Assert.areEqual(0, MessageService.sentEmailList.size());
+ }
+
+ public class MockBatchableContext implements Database.BatchableContext {
+ public Id getJobId() {
+ return '100000000000000';
+ }
+
+ public Id getChildJobId() {
+ return '100000000000000';
+ }
+ }
}
diff --git a/dlrs/main/classes/RollupJobTest.cls-meta.xml b/dlrs/main/classes/RollupJobTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupJobTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupJobTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupScheduledCalculateController.cls-meta.xml b/dlrs/main/classes/RollupScheduledCalculateController.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupScheduledCalculateController.cls-meta.xml
+++ b/dlrs/main/classes/RollupScheduledCalculateController.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupScheduledCalculateControllerTest.cls-meta.xml b/dlrs/main/classes/RollupScheduledCalculateControllerTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupScheduledCalculateControllerTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupScheduledCalculateControllerTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupService.cls b/dlrs/main/classes/RollupService.cls
index c041b617..5731326a 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) &&
@@ -381,6 +389,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.
*/
@@ -389,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();
@@ -491,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);
@@ -913,251 +935,289 @@ global with sharing class RollupService {
// Its possible for the user to deploy a trigger on parent objects, to monitor for merge operations...
DescribeSObjectResult sObjectDescribe = sObjectType.getDescribe();
+ Boolean isDeleting = newRecords.isEmpty();
+ Boolean sObjectCanMerge = sObjectDescribe.isMergeable();
+
Set masterRecordIdsFromMerge = new Set();
- if (sObjectDescribe.isMergeable()) {
- for (SObject existingRecord : existingRecords.values()) {
- Id masterRecordId = null;
- try {
- masterRecordId = (Id) existingRecord.get('MasterRecordId');
- } catch (Exception e) {
- }
- if (masterRecordId != null) {
- masterRecordIdsFromMerge.add(masterRecordId);
- }
- }
- }
// If this is a parent record merge operation, determine child object rollups to recalculate...
Boolean scheduleAllRollups = false;
Set childObjects = new Set();
childObjects.add(sObjectType);
- if (masterRecordIdsFromMerge.size() > 0) {
- // If a parent record is being merged, include a recalc of any related child rollups
- List childRelationships = sObjectDescribe.getChildRelationships();
- for (Schema.ChildRelationship childRelationship : childRelationships) {
- childObjects.add(childRelationship.getChildSObject());
+
+ // Are there any rollups to process?
+ RollupSummariesSelector selector = new RollupSummariesSelector(false);
+
+ List lookups = new List();
+ for (
+ RollupSummary lookup : selector.selectActiveByChildObject(
+ calculationModes,
+ new Set{ sObjectDescribe.getName() }
+ )
+ ) {
+ if (
+ Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) ||
+ BypassHandler.isBypassed(lookup.UniqueName)
+ ) {
+ continue;
}
- // Any rollups associated with these child objects will need to done in async,
- // as parent records cannot be updated realtime since the platform is also updating them
- scheduleAllRollups = true;
+ lookups.add(lookup);
}
- // Are there any rollups to process?
- List lookups = describeRollups(
- childObjects,
- calculationModes
- );
- if (lookups.isEmpty())
+ List parentLookups = new List();
+ for (
+ RollupSummary lookup : selector.selectActiveByParentObject(
+ calculationModes,
+ new Set{ sObjectDescribe.getName() }
+ )
+ ) {
+ if (
+ Utilities.userHasCustomPermission(lookup.BypassCustPermApiName) ||
+ BypassHandler.isBypassed(lookup.UniqueName)
+ ) {
+ continue;
+ }
+ parentLookups.add(lookup);
+ }
+
+ // if this object isn't involved in any rollups then we can ignore it
+ if (lookups.isEmpty() && parentLookups.isEmpty()) {
return; // Nothing to see here! :)
+ }
- // if records exist in both maps, then we need to go through change detection.
- // Has anything changed on the child records in respect to the fields referenced on the lookup definition?
- // Or does a record exist in one map but not the other
- if (!existingRecords.isEmpty() && !newRecords.isEmpty()) {
- // Master records to update
+ // this is either create or delete
+ // adding as a hot path to reduce overhead even though it duplicates
+ if (newRecords.isEmpty() || existingRecords.isEmpty()) {
+ // Rollup whichever side has records and update master records
+ // only one map should have records at this point
Set masterRecordIds = new Set();
-
- // Set of field names from the child used in the rollup to search for changes on
- Set fieldsToSearchForChanges = new Set();
- Set relationshipFields = new Set();
- // keep track of fields that should trigger a rollup to be processed
- // 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)) {
- continue;
- }
-
- Set lookupFields = new Set();
- lookupFields.add(lookup.FieldToAggregate);
- if (!String.isBlank(lookup.RelationshipCriteriaFields)) {
- for (
- String criteriaField : lookup.RelationshipCriteriaFields.split(
- '[\r\n]+'
- )
- ) {
- lookupFields.add(criteriaField);
+ Map recordsToProcess = existingRecords.isEmpty()
+ ? newRecords
+ : existingRecords;
+ for (SObject childRecord : recordsToProcess.values()) {
+ if (sObjectCanMerge) {
+ Id masterRecordId = null;
+ try {
+ masterRecordId = (Id) childRecord.get('MasterRecordId');
+ } catch (Exception e) {
+ }
+ if (masterRecordId != null) {
+ masterRecordIdsFromMerge.add(masterRecordId);
}
}
- // only include order by fields when query based rollup (concat, first, last, etc.) since changes to them
- // will not impact the outcome of an aggregate based rollup (sum, count, etc.)
- if (
- LREngine.isQueryBasedRollup(
- RollupSummaries.OPERATION_PICKLIST_TO_ENUMS.get(
- lookup.AggregateOperation
- )
- ) && !String.isBlank(lookup.FieldToOrderBy)
- ) {
- List orderByFields = Utilities.parseOrderByClause(
- lookup.FieldToOrderBy
- );
- if (orderByFields != null && !orderByFields.isEmpty()) {
- for (Utilities.Ordering orderByField : orderByFields) {
- lookupFields.add(orderByField.getField());
+ for (RollupSummary lookup : lookups) {
+ if (childRecord.get(lookup.RelationShipField) != null) {
+ // Check for self referencing rollups, https://github.com/afawcett/declarative-lookup-rollup-summaries/issues/39
+ Id masterRecordId = (Id) childRecord.get(lookup.RelationShipField);
+ if (isDeleting && masterRecordId == childRecord.Id) {
+ continue;
}
+ masterRecordIds.add(masterRecordId);
}
}
+ }
- // add all lookup fields to our master list of fields to search for
- fieldsToSearchForChanges.addAll(lookupFields);
+ // Process the rollups and update the master records
+ // TODO: if we have merge IDs then we need to combine lookups and parentLookups and
- // add relationshipfield to fields for this lookup
- // this comes after adding to fieldsToSearchForChanges because we handle
- // change detection separately for non-relationship fields and relationship fields
- lookupFields.add(lookup.RelationShipField);
+ if (!masterRecordIdsFromMerge.isEmpty()) {
+ lookups.addAll(parentLookups);
+ masterRecordIds.addAll(masterRecordIdsFromMerge);
+ scheduleAllRollups = true;
+ }
+ updateRecords(
+ updateMasterRollupsTrigger(
+ lookups,
+ masterRecordIds,
+ scheduleAllRollups
+ ),
+ false,
+ true
+ );
+ // this is our hot path that doesn't need per-record field checks, no need for additional checking and work
+ return;
+ }
- // add to map for later use
- fieldsInvolvedInLookup.put(lookup.Id, lookupFields);
+ // if records exist in both maps, then we need to go through change detection.
+ // Has anything changed on the child records in respect to the fields referenced on the lookup definition?
+ // Or does a record exist in one map but not the other
+ // Master records to update
+ Set masterRecordIds = new Set();
- // add relationship field to master list of relationship fields
- relationshipFields.add(lookup.RelationShipField);
+ // Set of field names from the child used in the rollup to search for changes on
+ Set fieldsToSearchForChanges = new Set();
+ Set relationshipFields = new Set();
+ // keep track of fields that should trigger a rollup to be processed
+ // this avoids having to re-parse RelationshipCriteria & OrderBy fields during field change detection
+ Map> fieldsInvolvedInLookup = new Map>();
+ for (RollupSummary lookup : lookups) {
+ Set lookupFields = new Set();
+ lookupFields.add(lookup.FieldToAggregate);
+ if (!String.isBlank(lookup.RelationshipCriteriaFields)) {
+ for (
+ String criteriaField : lookup.RelationshipCriteriaFields.split(
+ '[\r\n]+'
+ )
+ ) {
+ lookupFields.add(criteriaField);
+ }
}
-
- // merge all record Id's
- Set mergedRecordIds = new Set(existingRecords.keySet());
- mergedRecordIds.addAll(newRecords.keySet());
-
- // Determine if a a field referenced on the lookup has changed and thus if the lookup itself needs recalculating
- Set fieldsChanged = new Set();
- for (Id recordId : mergedRecordIds) {
- // keep track of whether or not this child has changed in any of the fields involved in
- // lookups that are NOT relationship fields themselves. We'll check relationship fields
- // separately to avoid unnecessary rollups firing on master records that don't require updating
- Boolean nonRelationshipFieldsChanged = false;
-
- // Determine if any of the fields referenced on our selected rollups have changed on this record
- for (String fieldToSearch : fieldsToSearchForChanges) {
- // retrieve old and new records and values if they exist
- SObject oldRecord = existingRecords.get(recordId);
- Object oldValue = oldRecord == null
- ? null
- : oldRecord.get(fieldToSearch);
- SObject newRecord = newRecords.get(recordId);
- Object newValue = newRecord == null
- ? null
- : newRecord.get(fieldToSearch);
-
- // Register this field as having changed?
- // if in old but not in new then its a delete and rollup should be processed
- // if in new but not in old then its an insert and rollup should be processed
- // if in both then its an update and field change detection should occur and rollup should be processed if different
- if (
- (oldRecord == null) ||
- (newRecord == null) ||
- (newValue != oldValue)
- ) {
- fieldsChanged.add(fieldToSearch);
- // mark record as having a non-relationship field changed
- nonRelationshipFieldsChanged = true;
+ // only include order by fields when query based rollup (concat, first, last, etc.) since changes to them
+ // will not impact the outcome of an aggregate based rollup (sum, count, etc.)
+ if (
+ LREngine.isQueryBasedRollup(
+ RollupSummaries.OPERATION_PICKLIST_TO_ENUMS.get(
+ lookup.AggregateOperation
+ )
+ ) && !String.isBlank(lookup.FieldToOrderBy)
+ ) {
+ List orderByFields = Utilities.parseOrderByClause(
+ lookup.FieldToOrderBy
+ );
+ if (orderByFields != null && !orderByFields.isEmpty()) {
+ for (Utilities.Ordering orderByField : orderByFields) {
+ lookupFields.add(orderByField.getField());
}
}
+ }
- // iterate relationship fields looking and track old/new master id
- // if there were changes to the relationship field itself or any
- // other fields involved in a lookup
- for (String relationshipField : relationshipFields) {
- // should we add associated master to list?
- // default to whether or not a non-relationship field on record has changed
- Boolean addMasterIds = nonRelationshipFieldsChanged;
-
- // retrieve old and new records and values if they exist
- SObject oldRecord = existingRecords.get(recordId);
- Object oldValue = oldRecord == null
- ? null
- : oldRecord.get(relationshipField);
- SObject newRecord = newRecords.get(recordId);
- Object newValue = newRecord == null
- ? null
- : newRecord.get(relationshipField);
-
- // Register this field as having changed?
- // if in old but not in new then its a delete and rollup should be processed and master ids included
- // if in new but not in old then its an insert and rollup should be processed and master ids included
- // if in both then its an update and field change detection should occur and rollup should be processed and master ids included if different
- if (
- (oldRecord == null) ||
- (newRecord == null) ||
- (newValue != oldValue)
- ) {
- fieldsChanged.add(relationshipField);
- // master field itself changed so we force old/new master ids to be added for processing
- addMasterIds = true;
- }
+ // add all lookup fields to our master list of fields to search for
+ fieldsToSearchForChanges.addAll(lookupFields);
+
+ // add relationshipfield to fields for this lookup
+ // this comes after adding to fieldsToSearchForChanges because we handle
+ // change detection separately for non-relationship fields and relationship fields
+ lookupFields.add(lookup.RelationShipField);
- // if relationship field itself changed or if change in another non-relationship field
- // Add both old and new value to master record Id list for relationship fields
- // to ensure old and new parent master records are updated (re-parenting)
- if (addMasterIds) {
- if (newValue != null)
- masterRecordIds.add((Id) newValue);
- if (oldValue != null)
- masterRecordIds.add((Id) oldValue);
+ // add to map for later use
+ fieldsInvolvedInLookup.put(lookup.Id, lookupFields);
+
+ // add relationship field to master list of relationship fields
+ relationshipFields.add(lookup.RelationShipField);
+ }
+
+ // merge all record Id's
+ Set mergedRecordIds = new Set(existingRecords.keySet());
+ mergedRecordIds.addAll(newRecords.keySet());
+
+ // Determine if a a field referenced on the lookup has changed and thus if the lookup itself needs recalculating
+ Set fieldsChanged = new Set();
+ for (Id recordId : mergedRecordIds) {
+ // keep track of whether or not this child has changed in any of the fields involved in
+ // lookups that are NOT relationship fields themselves. We'll check relationship fields
+ // separately to avoid unnecessary rollups firing on master records that don't require updating
+ Boolean nonRelationshipFieldsChanged = false;
+ // retrieve old and new records and values if they exist
+ SObject oldRecord = existingRecords.get(recordId);
+ SObject newRecord = newRecords.get(recordId);
+
+ // Determine if any of the fields referenced on our selected rollups have changed on this record
+ for (String fieldToSearch : fieldsToSearchForChanges) {
+ Object oldValue = oldRecord?.get(fieldToSearch);
+ Object newValue = newRecord?.get(fieldToSearch);
+
+ // Register this field as having changed?
+ // if in old but not in new then its a delete and rollup should be processed
+ // if in new but not in old then its an insert and rollup should be processed
+ // if in both then its an update and field change detection should occur and rollup should be processed if different
+ if (
+ (oldRecord == null) ||
+ (newRecord == null) ||
+ (newValue != oldValue)
+ ) {
+ fieldsChanged.add(fieldToSearch);
+ // mark record as having a non-relationship field changed
+ nonRelationshipFieldsChanged = true;
+ }
+ }
+
+ // check for merged records that are newly merged in this transaction
+ if (sObjectCanMerge) {
+ for (SObject existingRecord : existingRecords.values()) {
+ Id masterRecordId = null;
+ Id oldValue;
+ Id newValue;
+ try {
+ oldValue = (Id) oldRecord?.get('MasterRecordId');
+ newValue = (Id) newRecord?.get('MasterRecordId');
+ } catch (Exception e) {
+ }
+ if (String.isNotBlank(newValue) && oldValue != newValue) {
+ masterRecordIdsFromMerge.add(newValue);
}
}
}
- // 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)) {
- continue;
+ // iterate relationship fields looking and track old/new master id
+ // if there were changes to the relationship field itself or any
+ // other fields involved in a lookup
+ for (String relationshipField : relationshipFields) {
+ // should we add associated master to list?
+ // default to whether or not a non-relationship field on record has changed
+ Boolean addMasterIds = nonRelationshipFieldsChanged;
+
+ // retrieve old and new records and values if they exist
+ Object oldValue = oldRecord?.get(relationshipField);
+ Object newValue = newRecord?.get(relationshipField);
+
+ // Register this field as having changed?
+ // if in old but not in new then its a delete and rollup should be processed and master ids included
+ // if in new but not in old then its an insert and rollup should be processed and master ids included
+ // if in both then its an update and field change detection should occur and rollup should be processed and master ids included if different
+ if (
+ (oldRecord == null) ||
+ (newRecord == null) ||
+ (newValue != oldValue)
+ ) {
+ fieldsChanged.add(relationshipField);
+ // master field itself changed so we force old/new master ids to be added for processing
+ addMasterIds = true;
}
- // Are any of the changed fields used by this lookup?
- Set lookupFields = fieldsInvolvedInLookup.get(lookup.Id);
- for (String lookupField : lookupFields) {
- if (fieldsChanged.contains(lookupField)) {
- // add lookup to be processed and exit for loop since we have our answer
- lookupsToProcess.add(lookup);
- break;
- }
+ // if relationship field itself changed or if change in another non-relationship field
+ // Add both old and new value to master record Id list for relationship fields
+ // to ensure old and new parent master records are updated (re-parenting)
+ if (addMasterIds) {
+ if (newValue != null)
+ masterRecordIds.add((Id) newValue);
+ if (oldValue != null)
+ masterRecordIds.add((Id) oldValue);
}
}
- lookups = lookupsToProcess;
-
- // Rollup child records and update master records
- if (lookupsToProcess.size() > 0)
- updateRecords(
- updateMasterRollupsTrigger(lookups, masterRecordIds, false),
- false,
- true
- );
- return;
}
- // Rollup whichever side has records and update master records
- // only one map should have records at this point
- Boolean isDeleting = newRecords.isEmpty();
- Set masterRecordIds = new Set(masterRecordIdsFromMerge);
- Map recordsToProcess = existingRecords.isEmpty()
- ? newRecords
- : existingRecords;
- for (SObject childRecord : recordsToProcess.values()) {
- for (RollupSummary lookup : lookups) {
- if (BypassHandler.isBypassed(lookup.UniqueName)) {
- continue;
- }
-
- // Does this rollup apply to this child record?
- if (lookup.ChildObject.equalsIgnoreCase(sObjectDescribe.getName())) {
- if (childRecord.get(lookup.RelationShipField) != null) {
- // Check for self referencing rollups, https://github.com/afawcett/declarative-lookup-rollup-summaries/issues/39
- Id masterRecordId = (Id) childRecord.get(lookup.RelationShipField);
- if (isDeleting && masterRecordId == childRecord.Id) {
- continue;
- }
- masterRecordIds.add(masterRecordId);
- }
+ // 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) {
+ // Are any of the changed fields used by this lookup?
+ Set lookupFields = fieldsInvolvedInLookup.get(lookup.Id);
+ for (String lookupField : lookupFields) {
+ if (fieldsChanged.contains(lookupField)) {
+ // add lookup to be processed and exit for loop since we have our answer
+ lookupsToProcess.add(lookup);
+ break;
}
}
}
+ lookups = lookupsToProcess;
+ // check if we encountered any merged records and enqueue them for recalc as parents
+ if (!masterRecordIdsFromMerge.isEmpty()) {
+ lookups.addAll(parentLookups);
+ masterRecordIds.addAll(masterRecordIdsFromMerge);
+ scheduleAllRollups = true;
+ }
- // Process the rollups and update the master records
- updateRecords(
- updateMasterRollupsTrigger(lookups, masterRecordIds, scheduleAllRollups),
- false,
- true
- );
+ // Rollup child records and update master records
+ if (lookupsToProcess.size() > 0)
+ updateRecords(
+ updateMasterRollupsTrigger(
+ lookups,
+ masterRecordIds,
+ scheduleAllRollups
+ ),
+ false,
+ true
+ );
}
/**
@@ -1179,7 +1239,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;
}
@@ -1270,7 +1333,32 @@ global with sharing class RollupService {
}
}
// These records drive the work done by the RollupJob Scheduled Apex Class
- upsert scheduledItems QualifiedParentID__c;
+ List results = Database.upsert(
+ scheduledItems,
+ LookupRollupSummaryScheduleItems__c.QualifiedParentID__c,
+ false /* allOrNone */,
+ AccessLevel.SYSTEM_MODE
+ );
+ // iterate through errors, only return an exception for something other than DUPLICATE_VALUE
+ // because DUPLICATE_VALUE errors are usually sharing problems and we're happy as long as the record is in the database
+ for (Integer i = 0, j = results.size(); i < j; i++) {
+ if (!results[i].isSuccess()) {
+ for (Database.Error err : results[i].getErrors()) {
+ if (err.getStatusCode() != System.StatusCode.DUPLICATE_VALUE) {
+ throw new DmlException(
+ 'Upsert failed. First exception on row ' +
+ i +
+ '; first error: ' +
+ err.getStatusCode() +
+ ', ' +
+ err.getMessage() +
+ ': ' +
+ err.getFields()
+ );
+ }
+ }
+ }
+ }
}
// Process each context (parent child relationship) and its associated rollups
@@ -1636,8 +1724,19 @@ global with sharing class RollupService {
masterRecords.set(outerIndex, masterRecords.get(indexOfMin));
masterRecords.set(indexOfMin, temp);
}
+ // Create DmlOptions instance
+ Database.DMLOptions dml = new Database.DMLOptions();
+
+ // Allow save even if duplicates are detected
+ dml.DuplicateRuleHeader.allowSave = true;
+
+ // Run as current user to enforce sharing rules
+ dml.DuplicateRuleHeader.runAsCurrentUser = true;
+
+ dml.OptAllOrNone = allOrNothing;
+
try {
- return Database.update(masterRecords, allOrNothing);
+ return Database.update(masterRecords, dml);
} catch (DMLException e) {
// Determine if the exception is due to parent record/s having been deleted
Boolean throwException = true;
@@ -1660,6 +1759,7 @@ global with sharing class RollupService {
return new List();
}
// Throw on as normal
+
throw e;
}
}
diff --git a/dlrs/main/classes/RollupService.cls-meta.xml b/dlrs/main/classes/RollupService.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupService.cls-meta.xml
+++ b/dlrs/main/classes/RollupService.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupServiceException.cls-meta.xml b/dlrs/main/classes/RollupServiceException.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupServiceException.cls-meta.xml
+++ b/dlrs/main/classes/RollupServiceException.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupServiceMergeTest.cls b/dlrs/main/classes/RollupServiceMergeTest.cls
new file mode 100644
index 00000000..8a3c2f25
--- /dev/null
+++ b/dlrs/main/classes/RollupServiceMergeTest.cls
@@ -0,0 +1,317 @@
+@IsTest
+private class RollupServiceMergeTest {
+ // standard merge logic - https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_triggers_merge_statements.htm
+
+ /*
+ used to validate that a standard merge path is functional
+ the expectation is that when DLRS sees a merge it will add a
+ scheduled item record so the surving record can get recalculated
+ */
+ @IsTest
+ static void testMergeWithMergedObjectBothParentAndChild() {
+ // Test supported?
+ if (!TestContext.isSupported())
+ return;
+
+ mockContactRollupCache();
+ // create a few contacts, merge them together
+ Contact c1 = new Contact(LastName = 'Test1');
+ Contact c2 = new Contact(LastName = 'Test2');
+ insert new List{ c1, c2 };
+ Test.startTest();
+ merge c1 c2;
+ Test.stopTest();
+
+ // make sure a scheduled item record was added as a result of the merge
+ List items = [
+ SELECT Id, ParentId__c, QualifiedParentID__c
+ FROM LookupRollupSummaryScheduleItems__c
+ ];
+ Assert.areEqual(1, items.size(), 'Unexpected Rollup Items:' + items);
+ LookupRollupSummaryScheduleItems__c i = items[0];
+ Assert.areEqual(c1.Id + '#m0000000000000000E', i.QualifiedParentID__c);
+ }
+
+ /*
+ this doesn't depend on the triggers, it allows us to prove that we understand
+ the setup for the same scenario as above and helps to concicely test this one area of code
+ it is also validation for proving we can use this pattern for additional scenarios
+ */
+ @IsTest
+ static void testDirectMergeWithDelete() {
+ mockContactRollupCache();
+
+ // simulate a record with a merged record id
+ Contact c1 = (Contact) JSON.deserialize(
+ JSON.serialize(
+ new Map{
+ 'Id' => '00300000000000000B',
+ 'MasterRecordId' => '00300000000000000A'
+ }
+ ),
+ Schema.Contact.class
+ );
+
+ // simulate AFTER_DELETE trigger where record has a 'MasterRecordId' but Trigger.new is null
+ RollupService.handleRollups(
+ new Map{ c1.Id => c1 },
+ null,
+ Schema.Contact.getSObjectType(),
+ new List{
+ RollupSummaries.CalculationMode.Realtime
+ }
+ );
+
+ // make sure a scheduled item record was added as a result of the merge code
+ List items = [
+ SELECT Id, ParentId__c, QualifiedParentID__c
+ FROM LookupRollupSummaryScheduleItems__c
+ ];
+ Assert.areEqual(1, items.size(), 'Unexpected Rollup Items:' + items);
+ LookupRollupSummaryScheduleItems__c i = items[0];
+ Assert.areEqual(
+ c1.MasterRecordId + '#m0000000000000000E',
+ i.QualifiedParentID__c
+ );
+ }
+
+ /*
+ simulate merge scenarios on cases
+ orgs can be configured for two different merge models on cases
+ delete merged case or keep it and set it to a specific status
+ because the test class can't change this setting for the org we will
+ simulate each of these scenarios
+ */
+
+ /**
+ * if the case is setup to match other merge behavior, deleting the merged record
+ */
+ @IsTest
+ static void testMergeCaseWithDelete() {
+ if (!Schema.Case.SObjectType.getDescribe().isMergeable()) {
+ // if case merging is disabled then don't run this test
+ return;
+ }
+
+ mockCaseRollupCache();
+
+ // simulate a record with a merged record id
+ Case c1 = (Case) JSON.deserialize(
+ JSON.serialize(
+ new Map{
+ 'Id' => '50000000000000000B',
+ 'MasterRecordId' => '50000000000000000A'
+ }
+ ),
+ Schema.Case.class
+ );
+
+ // simulate AFTER_DELETE trigger where record has a 'MasterRecordId' but Trigger.new is null
+ RollupService.handleRollups(
+ new Map{ c1.Id => c1 },
+ null,
+ Schema.Case.getSObjectType(),
+ new List{
+ RollupSummaries.CalculationMode.Realtime
+ }
+ );
+
+ // make sure a scheduled item record was added as a result of the merge code
+ List items = [
+ SELECT Id, ParentId__c, QualifiedParentID__c
+ FROM LookupRollupSummaryScheduleItems__c
+ ];
+ Assert.areEqual(1, items.size(), 'Unexpected Rollup Items:' + items);
+ LookupRollupSummaryScheduleItems__c i = items[0];
+ Assert.areEqual(
+ c1.MasterRecordId + '#m0000000000000000E',
+ i.QualifiedParentID__c
+ );
+ }
+
+ // case w/ keep
+ @IsTest
+ static void testMergeCaseWithKeep() {
+ if (!Schema.Case.SObjectType.getDescribe().isMergeable()) {
+ // if case merging is disabled then don't run this test
+ return;
+ }
+
+ mockCaseRollupCache();
+
+ // simulate a record with a merged record id
+ Case cOld = (Case) JSON.deserialize(
+ JSON.serialize(
+ new Map{
+ 'Id' => '50000000000000000B',
+ 'MasterRecordId' => null
+ }
+ ),
+ Schema.Case.class
+ );
+ Case cNew = (Case) JSON.deserialize(
+ JSON.serialize(
+ new Map{
+ 'Id' => '50000000000000000B',
+ 'MasterRecordId' => '50000000000000000A'
+ }
+ ),
+ Schema.Case.class
+ );
+
+ // simulate AFTER_UPDATE trigger where record has a 'MasterRecordId' but Trigger.new is null
+ RollupService.handleRollups(
+ new Map{ cOld.Id => cOld },
+ new Map{ cNew.Id => cNew },
+ Schema.Case.getSObjectType(),
+ new List{
+ RollupSummaries.CalculationMode.Realtime
+ }
+ );
+
+ // make sure a scheduled item record was added as a result of the merge code
+ List items = [
+ SELECT Id, ParentId__c, QualifiedParentID__c
+ FROM LookupRollupSummaryScheduleItems__c
+ ];
+ Assert.areEqual(1, items.size(), 'Unexpected Rollup Items:' + items);
+ LookupRollupSummaryScheduleItems__c i = items[0];
+ Assert.areEqual(
+ cNew.MasterRecordId + '#m0000000000000000E',
+ i.QualifiedParentID__c
+ );
+ }
+
+ // case w/ edit on keep
+ @IsTest
+ static void testEditMergedCase() {
+ if (!Schema.Case.SObjectType.getDescribe().isMergeable()) {
+ // if case merging is disabled then don't run this test
+ return;
+ }
+
+ mockCaseRollupCache();
+
+ // simulate a record with a merged record id
+ Case cOld = (Case) JSON.deserialize(
+ JSON.serialize(
+ new Map{
+ 'Id' => '50000000000000000B',
+ 'MasterRecordId' => '50000000000000000A',
+ 'Subject' => 'Subject 123'
+ }
+ ),
+ Schema.Case.class
+ );
+ Case cNew = (Case) JSON.deserialize(
+ JSON.serialize(
+ new Map{
+ 'Id' => '50000000000000000B',
+ 'MasterRecordId' => '50000000000000000A',
+ 'Subject' => 'Subject 456'
+ }
+ ),
+ Schema.Case.class
+ );
+
+ // simulate AFTER_DELETE trigger where record has a 'MasterRecordId' but Trigger.new is null
+ RollupService.handleRollups(
+ new Map{ cOld.Id => cOld },
+ new Map{ cNew.Id => cNew },
+ Schema.Case.getSObjectType(),
+ new List{
+ RollupSummaries.CalculationMode.Realtime
+ }
+ );
+
+ // make sure a scheduled item record was added as a result of the merge code
+ List items = [
+ SELECT Id, ParentId__c, QualifiedParentID__c
+ FROM LookupRollupSummaryScheduleItems__c
+ ];
+ Assert.areEqual(0, items.size(), 'Unexpected Rollup Items:' + items);
+ }
+
+ static void mockContactRollupCache() {
+ String prefix = LookupRollupSummary2__mdt.sObjectType.getDescribe()
+ .getKeyPrefix();
+ List rollups = new List{
+ new LookupRollupSummary2__mdt(
+ Id = prefix + '00000000000000D',
+ Label = 'Contact to Account',
+ DeveloperName = 'Contact_to_Account',
+ ParentObject__c = 'Account',
+ ChildObject__c = 'Contact',
+ RelationshipField__c = 'AccountId',
+ FieldToAggregate__c = 'Id',
+ AggregateOperation__c = 'Count',
+ AggregateResultField__c = 'NumberOfEmployees',
+ CalculationMode__c = 'Realtime',
+ AggregateAllRows__c = false,
+ Active__c = true
+ ),
+ new LookupRollupSummary2__mdt(
+ Id = prefix + '00000000000000E',
+ Label = 'Asset to Contact',
+ DeveloperName = 'Asset_To_Contact',
+ ParentObject__c = 'Contact',
+ ChildObject__c = 'Asset',
+ RelationshipField__c = 'ContactId',
+ RelationshipCriteriaFields__c = 'ProductCode',
+ FieldToAggregate__c = 'SerialNumber',
+ AggregateOperation__c = 'First',
+ AggregateResultField__c = 'FirstName',
+ CalculationMode__c = 'Realtime',
+ AggregateAllRows__c = false,
+ Active__c = true
+ )
+ };
+ RollupSummariesSelector.setRollupCache(
+ false,
+ false,
+ RollupSummary.toList(rollups)
+ );
+ }
+
+ static void mockCaseRollupCache() {
+ String prefix = LookupRollupSummary2__mdt.sObjectType.getDescribe()
+ .getKeyPrefix();
+ List rollups = new List{
+ new LookupRollupSummary2__mdt(
+ Id = prefix + '00000000000000D',
+ Label = 'Case to Contact',
+ DeveloperName = 'Case_to_Contact',
+ ParentObject__c = 'Contact',
+ ChildObject__c = 'Case',
+ RelationshipField__c = 'ContactId',
+ RelationshipCriteriaFields__c = 'Subject',
+ FieldToAggregate__c = 'Id',
+ AggregateOperation__c = 'First',
+ AggregateResultField__c = 'FirstName',
+ CalculationMode__c = 'Realtime',
+ AggregateAllRows__c = false,
+ Active__c = true
+ ),
+ new LookupRollupSummary2__mdt(
+ Id = prefix + '00000000000000E',
+ Label = 'Comment to Case',
+ DeveloperName = 'Comment_to_Case',
+ ParentObject__c = 'Case',
+ ChildObject__c = 'CaseComment',
+ RelationshipField__c = 'ParentId',
+ RelationshipCriteriaFields__c = 'CommentBody',
+ FieldToAggregate__c = 'CommentBody',
+ AggregateOperation__c = 'First',
+ AggregateResultField__c = 'Description',
+ CalculationMode__c = 'Realtime',
+ AggregateAllRows__c = false,
+ Active__c = true
+ )
+ };
+ RollupSummariesSelector.setRollupCache(
+ false,
+ false,
+ RollupSummary.toList(rollups)
+ );
+ }
+}
diff --git a/dlrs/main/classes/RollupServiceMergeTest.cls-meta.xml b/dlrs/main/classes/RollupServiceMergeTest.cls-meta.xml
new file mode 100644
index 00000000..5f399c3c
--- /dev/null
+++ b/dlrs/main/classes/RollupServiceMergeTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
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/RollupServiceTest.cls-meta.xml b/dlrs/main/classes/RollupServiceTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupServiceTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupServiceTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupServiceTest2.cls-meta.xml b/dlrs/main/classes/RollupServiceTest2.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupServiceTest2.cls-meta.xml
+++ b/dlrs/main/classes/RollupServiceTest2.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupServiceTest3.cls b/dlrs/main/classes/RollupServiceTest3.cls
index e4f2ef04..c09dfdfd 100644
--- a/dlrs/main/classes/RollupServiceTest3.cls
+++ b/dlrs/main/classes/RollupServiceTest3.cls
@@ -430,6 +430,8 @@ private with sharing class RollupServiceTest3 {
rollupSummary.CalculationMode__c = 'Scheduled';
insert rollupSummary;
+ RollupSummariesSelector.clearRollupCache();
+
ApexPages.StandardController standardController = new ApexPages.StandardController(
rollupSummary
);
@@ -492,6 +494,9 @@ private with sharing class RollupServiceTest3 {
WHERE ApexClass.Name = 'RollupCalculateJobSchedulable'
] > 0
);
+
+ Assert.areEqual(0, [SELECT COUNT() FROM LookupRollupSummaryLog__c]);
+ Assert.areEqual(0, MessageService.sentEmailList.size());
}
private testMethod static void testDeveloperAPI() {
diff --git a/dlrs/main/classes/RollupServiceTest3.cls-meta.xml b/dlrs/main/classes/RollupServiceTest3.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupServiceTest3.cls-meta.xml
+++ b/dlrs/main/classes/RollupServiceTest3.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupServiceTest4.cls-meta.xml b/dlrs/main/classes/RollupServiceTest4.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupServiceTest4.cls-meta.xml
+++ b/dlrs/main/classes/RollupServiceTest4.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupServiceTest5.cls-meta.xml b/dlrs/main/classes/RollupServiceTest5.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupServiceTest5.cls-meta.xml
+++ b/dlrs/main/classes/RollupServiceTest5.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupServiceTest6.cls-meta.xml b/dlrs/main/classes/RollupServiceTest6.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupServiceTest6.cls-meta.xml
+++ b/dlrs/main/classes/RollupServiceTest6.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupSummaries.cls b/dlrs/main/classes/RollupSummaries.cls
index cc994d83..2700951b 100644
--- a/dlrs/main/classes/RollupSummaries.cls
+++ b/dlrs/main/classes/RollupSummaries.cls
@@ -399,7 +399,7 @@ public class RollupSummaries extends fflib_SObjectDomain {
if (!operationsSupportingRowLimit.contains(operation)) {
lookupRollupSummary.Fields.RowLimit.addError(
error(
- 'Row Limit is only supported on Last and Concatentate operators.',
+ 'Row Limit is only supported on Last and Concatenate operators.',
lookupRollupSummary.Record,
LookupRollupSummary__c.RowLimit__c
)
diff --git a/dlrs/main/classes/RollupSummaries.cls-meta.xml b/dlrs/main/classes/RollupSummaries.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummaries.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummaries.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupSummariesSelector.cls b/dlrs/main/classes/RollupSummariesSelector.cls
index ad44b875..d1b5e4a2 100644
--- a/dlrs/main/classes/RollupSummariesSelector.cls
+++ b/dlrs/main/classes/RollupSummariesSelector.cls
@@ -132,7 +132,7 @@ public class RollupSummariesSelector {
}
/**
- * Returns active lookup rollup summary definitions for thr given calculation modes and child object
+ * Returns active lookup rollup summary definitions for the given calculation modes and child object
**/
public List selectActiveByChildObject(
List calculationModes,
@@ -155,6 +155,30 @@ public class RollupSummariesSelector {
return sortSummaries(records, 'ParentObject__c', 'RelationshipField__c');
}
+ /**
+ * Returns active lookup rollup summary definitions for the given calculation modes and child or parent object
+ **/
+ public List selectActiveByParentObject(
+ List calculationModes,
+ Set objectNames
+ ) {
+ List rollupSummaryNames = new List();
+ for (RollupSummaries.CalculationMode cm : calculationModes) {
+ rollupSummaryNames.add(cm.name());
+ }
+ List records = new List();
+ for (RollupSummary rs : this.allSummaries) {
+ if (
+ rs.Active &&
+ containsIgnoreCase(objectNames, rs.ParentObject) &&
+ containsIgnoreCase(rollupSummaryNames, rs.CalculationMode)
+ ) {
+ records.add(rs);
+ }
+ }
+ return sortSummaries(records, 'ParentObject__c', 'RelationshipField__c');
+ }
+
/**
* Returns active lookup rollup summary definitions for the given rollup unique names
**/
@@ -279,6 +303,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/RollupSummariesSelector.cls-meta.xml b/dlrs/main/classes/RollupSummariesSelector.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummariesSelector.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummariesSelector.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupSummariesSelectorTest.cls-meta.xml b/dlrs/main/classes/RollupSummariesSelectorTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummariesSelectorTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummariesSelectorTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupSummariesTest.cls b/dlrs/main/classes/RollupSummariesTest.cls
index 352db024..6afa77fa 100644
--- a/dlrs/main/classes/RollupSummariesTest.cls
+++ b/dlrs/main/classes/RollupSummariesTest.cls
@@ -57,10 +57,13 @@ private class RollupSummariesTest {
);
fflib_SObjectDomain.triggerHandler(RollupSummaries.class);
System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size());
- System.assertEquals(
- 'Relationship Criteria \'StageName Equals Won\' is not valid, see SOQL documentation http://www.salesforce.com/us/developer/docs/soql_sosl/Content/sforce_api_calls_soql_select_conditionexpression.htm, error is \'unexpected token: \'Equals\'\'',
- fflib_SObjectDomain.Errors.getAll()[0].message
+ Assert.isTrue(
+ fflib_SObjectDomain.Errors.getAll()[0]
+ .message.startsWith(
+ 'Relationship Criteria \'StageName Equals Won\' is not valid, see SOQL documentation'
+ )
);
+
System.assertEquals(
LookupRollupSummary__c.RelationShipCriteria__c,
((fflib_SObjectDomain.FieldError) fflib_SObjectDomain.Errors.getAll()[0])
@@ -768,7 +771,7 @@ private class RollupSummariesTest {
fflib_SObjectDomain.triggerHandler(RollupSummaries.class);
System.assertEquals(1, fflib_SObjectDomain.Errors.getAll().size());
System.assertEquals(
- 'Row Limit is only supported on Last and Concatentate operators.',
+ 'Row Limit is only supported on Last and Concatenate operators.',
fflib_SObjectDomain.Errors.getAll()[0].message
);
System.assertEquals(
diff --git a/dlrs/main/classes/RollupSummariesTest.cls-meta.xml b/dlrs/main/classes/RollupSummariesTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummariesTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummariesTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupSummariesTriggerTest.cls-meta.xml b/dlrs/main/classes/RollupSummariesTriggerTest.cls-meta.xml
index 3a10d2eb..835ede48 100644
--- a/dlrs/main/classes/RollupSummariesTriggerTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummariesTriggerTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
\ No newline at end of file
diff --git a/dlrs/main/classes/RollupSummary.cls b/dlrs/main/classes/RollupSummary.cls
index 05e47c00..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');
@@ -335,12 +350,15 @@ public class RollupSummary {
**/
public class FieldData {
public RecordMetadata RecordMetadata { get; private set; }
+ public List errors { get; private set; }
public FieldData(RecordMetadata recordMetadata) {
this.RecordMetadata = recordMetadata;
+ this.errors = new List();
}
public void addError(String errorMessage) {
+ this.errors.add(errorMessage);
// Field in error?
String fieldLabelInError;
if (this === RecordMetadata.Active) {
diff --git a/dlrs/main/classes/RollupSummary.cls-meta.xml b/dlrs/main/classes/RollupSummary.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummary.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummary.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupSummaryEnhancedController.cls-meta.xml b/dlrs/main/classes/RollupSummaryEnhancedController.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummaryEnhancedController.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummaryEnhancedController.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupSummaryEnhancedControllerTest.cls-meta.xml b/dlrs/main/classes/RollupSummaryEnhancedControllerTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummaryEnhancedControllerTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummaryEnhancedControllerTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupSummaryLogDeleteController.cls-meta.xml b/dlrs/main/classes/RollupSummaryLogDeleteController.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummaryLogDeleteController.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummaryLogDeleteController.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupSummaryLogDeleteControllerTest.cls-meta.xml b/dlrs/main/classes/RollupSummaryLogDeleteControllerTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummaryLogDeleteControllerTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummaryLogDeleteControllerTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupSummaryScheduleItemsSelector.cls-meta.xml b/dlrs/main/classes/RollupSummaryScheduleItemsSelector.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummaryScheduleItemsSelector.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummaryScheduleItemsSelector.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
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..835ede48
--- /dev/null
+++ b/dlrs/main/classes/RollupSummaryTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 63.0
+ Active
+
\ No newline at end of file
diff --git a/dlrs/main/classes/RollupSummaryViewController.cls-meta.xml b/dlrs/main/classes/RollupSummaryViewController.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummaryViewController.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummaryViewController.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/RollupSummaryViewControllerTest.cls-meta.xml b/dlrs/main/classes/RollupSummaryViewControllerTest.cls-meta.xml
index df13efa8..436a726e 100644
--- a/dlrs/main/classes/RollupSummaryViewControllerTest.cls-meta.xml
+++ b/dlrs/main/classes/RollupSummaryViewControllerTest.cls-meta.xml
@@ -1,5 +1,5 @@
- 60.0
+ 63.0
Active
diff --git a/dlrs/main/classes/SchedulerController.cls b/dlrs/main/classes/SchedulerController.cls
new file mode 100644
index 00000000..53cce962
--- /dev/null
+++ b/dlrs/main/classes/SchedulerController.cls
@@ -0,0 +1,45 @@
+public with sharing class SchedulerController {
+ @AuraEnabled
+ public static List getCurrentJobs(String className) {
+ return new AsyncApexJobsSelector()
+ .getScheduledInstancesOfType(
+ Type.forName(Utilities.namespace(), className)
+ );
+ }
+
+ @AuraEnabled
+ public static List getAllScheduledJobs() {
+ return new AsyncApexJobsSelector().getAllScheduledJobs();
+ }
+
+ @AuraEnabled
+ public static List scheduleJobs(
+ String className,
+ List newSchedules
+ ) {
+ Integer counter = 1;
+ List jobIds = new List();
+ Schedulable clsInstance = (Schedulable) Type.forName(className)
+ .newInstance();
+ List currentJobs = new AsyncApexJobsSelector()
+ .getAllScheduledJobs();
+ List existingNames = new List();
+ for (AsyncApexJob job : currentJobs) {
+ existingNames.add(job.CronTrigger.CronJobDetail.Name);
+ }
+ for (String cron : newSchedules) {
+ String scheduledName = className + ' ' + counter++;
+ while (existingNames.contains(scheduledName)) {
+ // if this name is taken, get the next
+ scheduledName = className + ' ' + counter++;
+ }
+ jobIds.add(System.schedule(scheduledName, cron, clsInstance));
+ }
+ return jobIds;
+ }
+
+ @AuraEnabled
+ public static void cancelScheduledJob(Id jobId) {
+ System.abortJob(jobId);
+ }
+}
diff --git a/dlrs/main/classes/SchedulerController.cls-meta.xml b/dlrs/main/classes/SchedulerController.cls-meta.xml
new file mode 100644
index 00000000..835ede48
--- /dev/null
+++ b/dlrs/main/classes/SchedulerController.cls-meta.xml
@@ -0,0 +1,5 @@
+
+