diff --git a/README.md b/README.md index e6d62213..dcd02484 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,12 @@ Create fast, scalable custom rollups driven by Custom Metadata in your Salesforc ### Package deployment options - + Deploy to Salesforce - + Deploy to Salesforce Sandbox diff --git a/extra-tests/classes/InvocableDrivenTests.cls b/extra-tests/classes/InvocableDrivenTests.cls index dd0f12d5..0828050c 100644 --- a/extra-tests/classes/InvocableDrivenTests.cls +++ b/extra-tests/classes/InvocableDrivenTests.cls @@ -49,7 +49,7 @@ private class InvocableDrivenTests { System.assertEquals(today.addDays(-2), reparentAccount.DateField__c); System.assertEquals(3, reparentAccount.NumberOfEmployees, 'Second account should properly reflect reparented record for number of employees'); System.assertEquals(one.Description + ', ' + three.Description, reparentAccount.Description, 'Second account should have only reparented case description'); - System.assertEquals(one.Subject, reparentAccount.Name, 'Second account name field should reflect last subject'); System.assertEquals(2, reparentAccount.AnnualRevenue, 'Second account sum field should include updated amount'); + System.assertEquals(one.Subject, reparentAccount.Name, 'Second account name field should reflect last subject'); } } diff --git a/package.json b/package.json index 84f6bca8..8a9499bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "apex-rollup", - "version": "1.2.37.0", + "version": "1.2.38.0", "description": "Fast, configurable, elastically scaling custom rollup solution. Apex Invocable action, one-liner Apex trigger/CMDT-driven logic, and scheduled Apex-ready.", "repository": { "type": "git", diff --git a/plugins/CustomObjectRollupLogger/README.md b/plugins/CustomObjectRollupLogger/README.md index cef5f29f..19e01867 100644 --- a/plugins/CustomObjectRollupLogger/README.md +++ b/plugins/CustomObjectRollupLogger/README.md @@ -1,11 +1,11 @@ # Custom Object Rollup Logger - + Deploy to Salesforce - + Deploy to Salesforce Sandbox diff --git a/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLogger.cls b/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLogger.cls index eec0762a..00ba0ab5 100644 --- a/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLogger.cls +++ b/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLogger.cls @@ -1,13 +1,22 @@ public class RollupCustomObjectLogger extends RollupLogger { private final List rollupLogEvents = new List(); + private final Database.DMLOptions truncatedAllowedOptions; + + public RollupCustomObjectLogger() { + super(); + this.truncatedAllowedOptions = new Database.DMLOptions(); + this.truncatedAllowedOptions.AllowFieldTruncation = true; + } public override void log(String logString, LoggingLevel logLevel) { - this.rollupLogEvents.add(new RollupLogEvent__e( + RollupLogEvent__e logEvent = new RollupLogEvent__e( LoggingLevel__c = logLevel.name(), LoggedBy__c = UserInfo.getUserId(), Message__c = logString, TransactionId__c = Request.getCurrent().getRequestId() - )); + ); + logEvent.setOptions(this.truncatedAllowedOptions); + this.rollupLogEvents.add(logEvent); } public override void log(String logString, Object logObject, LoggingLevel logLevel) { diff --git a/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLoggerTests.cls b/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLoggerTests.cls index 4f79be3f..97f07d65 100644 --- a/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLoggerTests.cls +++ b/plugins/CustomObjectRollupLogger/classes/RollupCustomObjectLoggerTests.cls @@ -11,12 +11,13 @@ private class RollupCustomObjectLoggerTests { Test.stopTest(); List rollupLogs = [ - SELECT Id, NumberOfLogEntries__c, TransactionId__c, (SELECT Message__c, LoggingLevel__c FROM RollupLogEntry__r) + SELECT Id, NumberOfLogEntries__c, TransactionId__c, ErrorWouldHaveBeenThrown__c, (SELECT Message__c, LoggingLevel__c FROM RollupLogEntry__r) FROM RollupLog__c ]; System.assertEquals(1, rollupLogs.size(), 'Parent-level rollup log should have been created'); RollupLog__c firstEntry = rollupLogs[0]; System.assertNotEquals(null, firstEntry.TransactionId__c, 'Transaction Id should have been assigned'); + System.assertEquals(true, firstEntry.ErrorWouldHaveBeenThrown__c, 'ERROR level log message was created, this field should be flagged'); // Rollup Log Entries System.assertEquals(2, firstEntry.RollupLogEntry__r.size()); diff --git a/plugins/CustomObjectRollupLogger/classes/RollupLogEventHandler.cls b/plugins/CustomObjectRollupLogger/classes/RollupLogEventHandler.cls index 31307e00..d07db4e5 100644 --- a/plugins/CustomObjectRollupLogger/classes/RollupLogEventHandler.cls +++ b/plugins/CustomObjectRollupLogger/classes/RollupLogEventHandler.cls @@ -3,7 +3,11 @@ public class RollupLogEventHandler { Map transactionIdToLogs = new Map(); for (RollupLogEvent__e logEvent : logEvents) { - RollupLog__c rollupLog = new RollupLog__c(TransactionId__c = logEvent.TransactionId__c, LoggedBy__c = Id.valueOf(logEvent.LoggedBy__c)); + RollupLog__c rollupLog = new RollupLog__c( + ErrorWouldHaveBeenThrown__c = logEvent.LoggingLevel__c == LoggingLevel.ERROR.name(), + LoggedBy__c = Id.valueOf(logEvent.LoggedBy__c), + TransactionId__c = logEvent.TransactionId__c + ); transactionIdToLogs.put(logEvent.TransactionId__c, rollupLog); } diff --git a/plugins/CustomObjectRollupLogger/layouts/RollupLog__c-Rollup Log Layout.layout-meta.xml b/plugins/CustomObjectRollupLogger/layouts/RollupLog__c-Rollup Log Layout.layout-meta.xml index 5fe5f6c8..fb5a6647 100644 --- a/plugins/CustomObjectRollupLogger/layouts/RollupLog__c-Rollup Log Layout.layout-meta.xml +++ b/plugins/CustomObjectRollupLogger/layouts/RollupLog__c-Rollup Log Layout.layout-meta.xml @@ -21,6 +21,10 @@ Readonly NumberOfLogEntries__c + + Edit + ErrorWouldHaveBeenThrown__c + @@ -44,6 +48,10 @@ Readonly LastModifiedById + + Edit + TransactionId__c + diff --git a/plugins/CustomObjectRollupLogger/objects/RollupLog__c/RollupLog__c.object-meta.xml b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/RollupLog__c.object-meta.xml index 6ea57fd6..b76e02b9 100644 --- a/plugins/CustomObjectRollupLogger/objects/RollupLog__c/RollupLog__c.object-meta.xml +++ b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/RollupLog__c.object-meta.xml @@ -161,7 +161,7 @@ AutoNumber Rollup Logs - + ReadWrite Public diff --git a/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/ErrorWouldHaveBeenThrown__c.field-meta.xml b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/ErrorWouldHaveBeenThrown__c.field-meta.xml new file mode 100644 index 00000000..986f89e0 --- /dev/null +++ b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/ErrorWouldHaveBeenThrown__c.field-meta.xml @@ -0,0 +1,12 @@ + + + ErrorWouldHaveBeenThrown__c + false + If Rollup logs an otherwise fatal exception (with LoggingLevel.ERROR), this field is checked off + false + If Rollup logs an otherwise fatal exception (with LoggingLevel.ERROR), this field is checked off + + false + false + Checkbox + diff --git a/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/TransactionId__c.field-meta.xml b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/TransactionId__c.field-meta.xml index 3333dde2..a009b98a 100644 --- a/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/TransactionId__c.field-meta.xml +++ b/plugins/CustomObjectRollupLogger/objects/RollupLog__c/fields/TransactionId__c.field-meta.xml @@ -1,4 +1,4 @@ - + TransactionId__c false @@ -10,4 +10,4 @@ false Text true - \ No newline at end of file + diff --git a/plugins/CustomObjectRollupLogger/permissionsets/RollupLogViewer.permissionset-meta.xml b/plugins/CustomObjectRollupLogger/permissionsets/RollupLogViewer.permissionset-meta.xml index a22250eb..12829ac7 100644 --- a/plugins/CustomObjectRollupLogger/permissionsets/RollupLogViewer.permissionset-meta.xml +++ b/plugins/CustomObjectRollupLogger/permissionsets/RollupLogViewer.permissionset-meta.xml @@ -1,6 +1,11 @@ Grants permission to Rollup Log information + + false + RollupLog__c.ErrorWouldHaveBeenThrown__c + true + false RollupLogEntry__c.Message__c diff --git a/plugins/CustomObjectRollupLogger/profiles/Admin.profile-meta.xml b/plugins/CustomObjectRollupLogger/profiles/Admin.profile-meta.xml index 670484ff..9f5b75f8 100644 --- a/plugins/CustomObjectRollupLogger/profiles/Admin.profile-meta.xml +++ b/plugins/CustomObjectRollupLogger/profiles/Admin.profile-meta.xml @@ -1,5 +1,10 @@ + + false + RollupLog__c.ErrorWouldHaveBeenThrown__c + true + true RollupLog__c.LoggedBy__c diff --git a/rollup/core/classes/Rollup.cls b/rollup/core/classes/Rollup.cls index a84a05b7..f0339e9e 100644 --- a/rollup/core/classes/Rollup.cls +++ b/rollup/core/classes/Rollup.cls @@ -18,31 +18,29 @@ global without sharing virtual class Rollup { @testVisible private static RollupControl__mdt specificControl; @testVisible - private static Integer maxQueryRowOverride; - @testVisible - private static final List CACHED_ROLLUPS = new List(); + private static final List CACHED_ROLLUPS = new List(); + private static final Set REPARENTED_KEYS = new Set(); private static Map> CACHED_APEX_OPERATIONS = new Map>(); private static Boolean isCDC = false; private static Boolean isDeferralAllowed = true; private static final String CONTROL_ORG_DEFAULTS = 'Org_Defaults'; - private static final Set ALWAYS_FULL_RECALC_OPS = new Set{ - Op.FIRST.name(), - Op.LAST.name(), - Op.AVERAGE.name(), - Op.CONCAT_DISTINCT.name(), - Op.COUNT_DISTINCT.name() - }; + protected final Set matchingCalcItemIds; protected final RollupControl__mdt rollupControl; protected final InvocationPoint invokePoint; protected final Boolean isBatched; protected final List rollups = new List(); // non-final instance variables + protected List calcItems; + protected Map oldCalcItems; + protected Rollup__mdt metadata; protected Boolean isFullRecalc = false; protected Boolean isNoOp; protected Boolean isCDCUpdate = false; + protected RollupAsyncProcessor fullRecalcProcessor; + protected Integer queryCount; /** * receiving an interface/subclass from a property get/set (from the book "The Art Of Unit Testing") is an old technique; @@ -122,7 +120,10 @@ global without sharing virtual class Rollup { } private class FilterResults { - public List matchingItems { get; set; } + public FilterResults() { + this.matchingItemIds = new Set(); + } + public Set matchingItemIds { get; private set; } public Evaluator eval { get; set; } } @@ -164,9 +165,18 @@ global without sharing virtual class Rollup { protected Rollup(InvocationPoint invokePoint) { this.invokePoint = invokePoint; this.rollupControl = getSingleControlOrDefault(RollupControl__mdt.DeveloperName, CONTROL_ORG_DEFAULTS, defaultControl); + this.isBatched = true; + // a batch only becomes valid if other Rollups are added to it + this.isNoOp = true; + } + + protected Rollup(InvocationPoint invokePoint, List calcItems, Map oldCalcItems) { + this(invokePoint); + this.calcItems = calcItems; + this.oldCalcItems = oldCalcItems; } - protected List getCachedRollups() { + protected List getCachedRollups() { return CACHED_ROLLUPS; } @@ -182,7 +192,7 @@ global without sharing virtual class Rollup { isDeferralAllowed = value; } - protected RollupAsyncProcessor getAsyncRollup( + protected Rollup getAsyncRollup( List rollupOperations, SObjectType sObjectType, List calcItems, @@ -201,6 +211,10 @@ global without sharing virtual class Rollup { return 'Not implemented'; } + protected virtual String getHashedContents() { + return this.toString(); + } + /** * global facing RollupAsyncProcessor calculation section * - Trigger operations @@ -228,6 +242,7 @@ global without sharing virtual class Rollup { public Set queryFields = new Set(); public List metadata = new List(); public String whereClause = ''; + public Integer recordCount = RollupQueryBuilder.SENTINEL_COUNT_VALUE; } @AuraEnabled @@ -257,12 +272,13 @@ global without sharing virtual class Rollup { SObjectType childType = getSObjectFromName(matchingMeta.CalcItem__c).getSObjectType(); Set whereFields = getQueryFieldsFromMetadata(matchingMeta, RollupEvaluator.getWhereEval(matchingMeta.CalcItemWhereClause__c, childType)); + RollupMetadata metaWrapper; if (childToMetaWrapper.containsKey(childType)) { - RollupMetadata metaWrapper = childToMetaWrapper.get(childType); + metaWrapper = childToMetaWrapper.get(childType); metaWrapper.queryFields.addAll(whereFields); metaWrapper.metadata.add(matchingMeta); } else { - RollupMetadata metaWrapper = new RollupMetadata(); + metaWrapper = new RollupMetadata(); metaWrapper.queryFields = wherefields; metaWrapper.metadata.add(matchingMeta); metaWrapper.whereClause = potentialWhereClause; @@ -275,16 +291,19 @@ global without sharing virtual class Rollup { if (currentCount == RollupQueryBuilder.SENTINEL_COUNT_VALUE) { amountOfCalcItems = currentCount; } else { + metaWrapper.recordCount = currentCount; amountOfCalcItems += currentCount; } } } - List processors = new List(); + List processors = new List(); for (SObjectType childType : childToMetaWrapper.keySet()) { RollupMetadata metaWrapper = childToMetaWrapper.get(childType); String queryString = RollupQueryBuilder.Current.getQuery(childType, new List(metaWrapper.queryFields), 'Id', '!=', metaWrapper.whereClause); - processors.add(buildFullRecalcRollup(metaWrapper.metadata, amountOfCalcItems, queryString, objIds, recordIds, childType, null, localInvokePoint)); + Rollup fullRecalcRoll = buildFullRecalcRollup(metaWrapper.metadata, amountOfCalcItems, queryString, objIds, recordIds, childType, null, localInvokePoint); + fullRecalcRoll.queryCount = metaWrapper.recordCount; + processors.add(fullRecalcRoll); } return batch(processors); @@ -416,7 +435,7 @@ global without sharing virtual class Rollup { ) global static List performRollup(List flowInputs) { List flowOutputs = new List(); - List localRollups = new List(); + List localRollups = new List(); InvocationPoint fromInvocable = InvocationPoint.FROM_INVOCABLE; for (FlowInput flowInput : flowInputs) { @@ -466,22 +485,16 @@ global without sharing virtual class Rollup { processCustomMetadata(localRollups, metas, flowInput.recordsToRollup, oldFlowRecords, new Set(), rollupContext, fromInvocable); if (metas.isEmpty() == false) { - RollupAsyncProcessor batchedRollup = getRollup( - new List{ rollupMeta }, - sObjectType, - flowInput.recordsToRollup, - oldFlowRecords, - null, - fromInvocable - ); + Rollup batchedRollup = getRollup(new List{ rollupMeta }, sObjectType, flowInput.recordsToRollup, oldFlowRecords, null, fromInvocable); + batchedRollup.metadata = rollupMeta; String logMessage = 'adding invocable rollup to list'; if (flowInput.deferProcessing) { logMessage = 'deferring processing for rollup'; - CACHED_ROLLUPS.addAll(batchedRollup.rollups); + CACHED_ROLLUPS.add(batchedRollup); } else { - localRollups.addAll(batchedRollup.rollups); + localRollups.add(batchedRollup); } - RollupLogger.Instance.log(logMessage, batchedRollup.rollups, LoggingLevel.DEBUG); + RollupLogger.Instance.log(logMessage, batchedRollup, LoggingLevel.DEBUG); } } @@ -497,7 +510,7 @@ global without sharing virtual class Rollup { } } RollupLogger.Instance.save(); - RollupAsyncProcessor.flatten(CACHED_ROLLUPS); + flatten(CACHED_ROLLUPS); return flowOutputs; } @@ -1102,34 +1115,6 @@ global without sharing virtual class Rollup { ); } - private static Rollup operateFromApex( - SObjectField operationFieldOnCalcItem, - SObjectField lookupFieldOnCalcItem, - SObjectField lookupFieldOnOperationObject, - SObjectField operationFieldOnOperationObject, - SObjectType lookupSObjectType, - Op rollupOperation, - Object defaultRecalculationValue, - String orderByFirstLast, - Evaluator eval - ) { - Rollup__mdt meta = new Rollup__mdt( - RollupFieldOnCalcItem__c = operationFieldOnCalcItem.getDescribe().getName(), - LookupObject__c = String.valueOf(lookupSObjectType), - LookupFieldOnCalcItem__c = lookupFieldOnCalcItem.getDescribe().getName(), - LookupFieldOnLookupObject__c = lookupFieldOnOperationObject.getDescribe().getName(), - RollupFieldOnLookupObject__c = operationFieldOnOperationObject.getDescribe().getName(), - RollupOperation__c = rollupOperation.name(), - OrderByFirstLast__c = orderByFirstLast - ); - if (defaultRecalculationValue instanceof Decimal) { - meta.FullRecalculationDefaultNumberValue__c = (Decimal) defaultRecalculationValue; - } else if (defaultRecalculationValue instanceof String) { - meta.FullRecalculationDefaultStringValue__c = (String) defaultRecalculationValue; - } - return runFromApex(new List{ meta }, eval, getTriggerRecords(), getOldTriggerRecordsMap()); - } - global static void runFromCDCTrigger() { isCDC = true; // CDC always uses Trigger.new @@ -1219,9 +1204,9 @@ global without sharing virtual class Rollup { } global static Rollup runFromApex(List rollupMetadata, Evaluator eval, List calcItems, Map oldCalcItems) { - Rollup batchRollup = new RollupAsyncProcessor(InvocationPoint.FROM_APEX); + Rollup rollupConductor = new RollupAsyncProcessor(InvocationPoint.FROM_APEX, calcItems, oldCalcItems); if (shouldRunFromTrigger() == false) { - return batchRollup; + return rollupConductor; } String rollupContext; @@ -1240,7 +1225,7 @@ global without sharing virtual class Rollup { rollupContext = ''; } when AFTER_DELETE { - reparentAndGetMergedRecordIds(calcItems, rollupMetadata, mergedParentIds, batchRollup.rollupControl); + reparentAndGetMergedRecordIds(calcItems, rollupMetadata, mergedParentIds, rollupConductor.rollupControl); } when else { shouldReturn = true; @@ -1248,7 +1233,7 @@ global without sharing virtual class Rollup { } if (shouldReturn) { - return batchRollup; + return rollupConductor; } List localRollups = new List(); @@ -1258,18 +1243,18 @@ global without sharing virtual class Rollup { populateCachedApexOperations(calcItemSObjectType, apexContext); if (rollupMetadata.isEmpty() == false) { - localRollups.addAll(getRollup(rollupMetadata, calcItemSObjectType, calcItems, oldCalcItems, eval, InvocationPoint.FROM_APEX).rollups); + localRollups.add(getRollup(rollupMetadata, calcItemSObjectType, calcItems, oldCalcItems, eval, InvocationPoint.FROM_APEX)); } - flattenBatches(batchRollup, localRollups); - return batchRollup; + flattenBatches(rollupConductor, localRollups); + return rollupConductor; } /** end global-facing section, begin public/private static helpers */ public static void processStoredFlowRollups() { RollupLogger.Instance.log('processing deferred flow rollups', LoggingLevel.DEBUG); - List rollupsToProcess = new List(CACHED_ROLLUPS); + List rollupsToProcess = new List(CACHED_ROLLUPS); CACHED_ROLLUPS.clear(); batch(rollupsToProcess, InvocationPoint.FROM_INVOCABLE); } @@ -1353,6 +1338,103 @@ global without sharing virtual class Rollup { return operationsWithUnderscores.contains(fullOpName) == false && fullOpName.contains('_') ? fullOpName.substringAfter('_') : fullOpName; } + private static Rollup operateFromApex( + SObjectField operationFieldOnCalcItem, + SObjectField lookupFieldOnCalcItem, + SObjectField lookupFieldOnOperationObject, + SObjectField operationFieldOnOperationObject, + SObjectType lookupSObjectType, + Op rollupOperation, + Object defaultRecalculationValue, + String orderByFirstLast, + Evaluator eval + ) { + Rollup__mdt meta = new Rollup__mdt( + RollupFieldOnCalcItem__c = operationFieldOnCalcItem.getDescribe().getName(), + LookupObject__c = String.valueOf(lookupSObjectType), + LookupFieldOnCalcItem__c = lookupFieldOnCalcItem.getDescribe().getName(), + LookupFieldOnLookupObject__c = lookupFieldOnOperationObject.getDescribe().getName(), + RollupFieldOnLookupObject__c = operationFieldOnOperationObject.getDescribe().getName(), + RollupOperation__c = rollupOperation.name(), + OrderByFirstLast__c = orderByFirstLast + ); + if (defaultRecalculationValue instanceof Decimal) { + meta.FullRecalculationDefaultNumberValue__c = (Decimal) defaultRecalculationValue; + } else if (defaultRecalculationValue instanceof String) { + meta.FullRecalculationDefaultStringValue__c = (String) defaultRecalculationValue; + } + return runFromApex(new List{ meta }, eval, getTriggerRecords(), getOldTriggerRecordsMap()); + } + + private static void flatten(List stackedRollups) { + Map rollupOperationToRollup = new Map(); + Integer counter = 0; + Map> operationToProcessedRecords = new Map>(); + for (Rollup stackedRollup : stackedRollups) { + // If the hashed contents aren't the same, we can't collapse + // the two rollup operations, and instead have to juggle the updated values for the parent in memory + String rollupKey = stackedRollup.getHashedContents(); + Boolean shouldAddSameRollupOperation = false; + + if (rollupOperationToRollup.containsKey(rollupKey)) { + Rollup matchingRollup = rollupOperationToRollup.get(rollupKey); + for (Integer index = stackedRollup.calcItems.size() - 1; index >= 0; index--) { + SObject calcItem = stackedRollup.calcItems[index]; + if (matchingRollup.calcItems.contains(calcItem) == false && operationToProcessedRecords.containsKey(rollupKey) == false) { + doBookkeepingOnCachedItems(matchingRollup, stackedRollup, operationToProcessedRecords, calcItem, rollupKey, index); + } else if ( + matchingRollup.calcItems.contains(calcItem) == false && + operationToProcessedRecords.containsKey(rollupKey) && + operationToProcessedRecords.get(rollupKey).contains(calcItem.Id) == false + ) { + doBookkeepingOnCachedItems(matchingRollup, stackedRollup, operationToProcessedRecords, calcItem, rollupKey, index); + } + } + + if (stackedRollup.calcItems.isEmpty() == false) { + shouldAddSameRollupOperation = true; + } + } else { + rollupOperationToRollup.put(rollupKey, stackedRollup); + } + if (shouldAddSameRollupOperation) { + counter++; + rollupOperationToRollup.put(rollupKey + counter, stackedRollup); + } + } + stackedRollups.clear(); + stackedRollups.addAll(rollupOperationToRollup.values()); + } + + private static void doBookkeepingOnCachedItems( + Rollup matchingRollup, + Rollup stackedRollup, + Map> operationToProcessedRecords, + SObject calcItem, + String rollupKey, + Integer index + ) { + List processedRecords = operationToProcessedRecords.containsKey(rollupKey) + ? operationToProcessedRecords.get(rollupKey) + : new List{ calcItem.Id }; + operationToProcessedRecords.put(rollupKey, processedRecords); + Map idToCalcItem = new Map(matchingRollup.calcItems); + idToCalcItem.put(calcItem.Id, calcItem); + matchingRollup.calcItems.clear(); + matchingRollup.calcItems.addAll(idToCalcItem.values()); + stackedRollup.calcItems.remove(index); + + if (stackedRollup.oldCalcItems.isEmpty() == false && stackedRollup.oldCalcItems.containsKey(calcItem.Id) == false) { + matchingRollup.oldCalcItems.put(calcItem.Id, stackedRollup.oldCalcItems.get(calcItem.Id)); + } + for (Rollup innerRollup : matchingRollup.rollups) { + if (innerRollup.matchingCalcItemIds?.isEmpty() == false) { + innerRollup.matchingCalcItemIds.addAll(idToCalcItem.keySet()); + innerRollup.matchingCalcItemIds.addAll(matchingRollup.oldCalcItems.keySet()); + } + } + } + private static void processCustomMetadata( List rollups, List metas, @@ -1456,12 +1538,15 @@ global without sharing virtual class Rollup { ) { List typedCalcItems = calcItems.clone(); typedCalcItems.clear(); + String potentialDeleteOpName = 'DELETE_' + getBaseOperationName(meta.RollupOperation__c); for (SObject calcItem : calcItems) { if (meta.IsRollupStartedFromParent__c == false && String.isBlank(meta.GrandparentRelationshipFieldPath__c) && oldCalcItems.containsKey(calcItem.Id)) { SObject oldCalcItem = oldCalcItems.get(calcItem.Id); - if (calcItem.get(meta.LookupFieldOnCalcItem__c) != oldCalcItem.get(meta.LookupFieldOnCalcItem__c)) { + String key = potentialDeleteOpName + oldCalcItem.Id + JSON.serialize(meta); + if (calcItem.get(meta.LookupFieldOnCalcItem__c) != oldCalcItem.get(meta.LookupFieldOnCalcItem__c) && REPARENTED_KEYS.contains(key) == false) { typedCalcItems.add(oldCalcItem); + REPARENTED_KEYS.add(key); } } } @@ -1469,10 +1554,18 @@ global without sharing virtual class Rollup { // we only need to perform the delete if we've arrived here // via a route where there were oldCalcItems AND one or more of their lookup keys changed if (typedCalcItems.isEmpty() == false) { - String potentialDeleteOpName = 'DELETE_' + getBaseOperationName(meta.RollupOperation__c); Rollup__mdt clonedMeta = meta.clone(); clonedMeta.RollupOperation__c = potentialDeleteOpName; - rollups.add(getRollup(new List{ clonedMeta }, typedCalcItems.getSObjectType(), typedCalcItems, new Map(), null, invokePoint)); + Rollup deleteProcessor = getRollup( + new List{ clonedMeta }, + typedCalcItems.getSObjectType(), + typedCalcItems, + new Map(), + null, + invokePoint + ); + RollupLogger.Instance.log('adding delete operation for:', deleteProcessor, LoggingLevel.DEBUG); + rollups.add(deleteProcessor); } } @@ -1490,7 +1583,7 @@ global without sharing virtual class Rollup { return '\nORDER BY ' + String.join(orderByFields, ','); } - private static RollupAsyncProcessor getFullRecalcRollup(Rollup__mdt meta, QueryWrapper queryWrapper, InvocationPoint invokePoint) { + private static Rollup getFullRecalcRollup(Rollup__mdt meta, QueryWrapper queryWrapper, InvocationPoint invokePoint) { // just how many items are we talking, here? If it's less than the query limit, we can proceed // otherwise, kick off a batch to fetch the calc items and then chain into the regular code path SObjectType childType = getSObjectFromName(meta.CalcItem__c).getSObjectType(); @@ -1511,10 +1604,12 @@ global without sharing virtual class Rollup { queryFields.addAll(RollupEvaluator.getWhereEval(meta.CalcItemWhereClause__c, childType).getQueryFields()); String queryString = RollupQueryBuilder.Current.getQuery(childType, new List(queryFields), 'Id', '!=', queryWrapper.getQuery()); - return buildFullRecalcRollup(new List{ meta }, amountOfCalcItems, queryString, objIds, recordIds, childType, whereEval, invokePoint); + Rollup fullRecalc = buildFullRecalcRollup(new List{ meta }, amountOfCalcItems, queryString, objIds, recordIds, childType, whereEval, invokePoint); + fullRecalc.queryCount = amountOfCalcItems; + return fullRecalc; } - private static RollupAsyncProcessor buildFullRecalcRollup( + private static Rollup buildFullRecalcRollup( List matchingMeta, Integer amountOfCalcItems, String queryString, @@ -1527,13 +1622,13 @@ global without sharing virtual class Rollup { for (Rollup__mdt meta : matchingMeta) { meta.IsFullRecordSet__c = true; } - RollupAsyncProcessor instance = new RollupAsyncProcessor(invokePoint); + Rollup instance = new Rollup(invokePoint); Boolean shouldQueue = amountOfCalcItems != RollupQueryBuilder.SENTINEL_COUNT_VALUE && amountOfCalcItems < instance.rollupControl.MaxLookupRowsBeforeBatching__c; if (shouldQueue) { List calculationItems = Database.query(queryString); - RollupAsyncProcessor thisRollup = getRollup(matchingMeta, calcItemType, calculationItems, new Map(calculationItems), eval, invokePoint); + Rollup thisRollup = getRollup(matchingMeta, calcItemType, calculationItems, new Map(calculationItems), eval, invokePoint); thisRollup.isFullRecalc = true; return thisRollup; } else { @@ -1543,21 +1638,30 @@ global without sharing virtual class Rollup { } private static String batch(List rollups, InvocationPoint invokePoint) { - Rollup batchRollup = new RollupAsyncProcessor(invokePoint); - flattenBatches(batchRollup, rollups); - return batchRollup.runCalc(); + if (rollups.isEmpty()) { + return new Rollup(invokePoint).runCalc(); + } + Rollup batchedRollup = new RollupAsyncProcessor(invokePoint, rollups[0].calcItems, rollups[0].oldCalcItems); + flattenBatches(batchedRollup, rollups); + return batchedRollup.runCalc(); } private static void flattenBatches(Rollup outerRollup, List rollups) { for (Rollup rollup : rollups) { if (rollup.rollups.isEmpty() == false) { for (Rollup innerRoll : rollup.rollups) { + if (rollup.calcItems?.isEmpty() == false) { + innerRoll.calcItems = rollup.calcItems; + } + if (rollup.oldCalcItems?.isEmpty() == false) { + innerRoll.oldCalcItems = rollup.oldCalcItems; + } innerRoll.isFullRecalc = rollup.isFullRecalc; } // recurse through lists until there aren't any more nested rollups flattenBatches(outerRollup, rollup.rollups); } else { - loadRollups(rollup, outerRollup); + loadRollups((RollupAsyncProcessor) rollup, outerRollup); } } } @@ -1584,9 +1688,7 @@ global without sharing virtual class Rollup { if (String.isNotBlank(errorMessage)) { Exception ex = new IllegalArgumentException(errorMessage); - RollupLogger.Instance.log('an error occurred while validating flow inputs', ex, LoggingLevel.ERROR); - RollupLogger.Instance.save(); - throw ex; + logAndThrowFlowException(ex, 'an error occurred while validating flow inputs'); } } @@ -1609,12 +1711,16 @@ global without sharing virtual class Rollup { (flowInput.isRollupStartedFromParent ? firstRecord : lookupItem).get(flowInput.lookupFieldOnOpObject); } } catch (Exception ex) { - RollupLogger.Instance.log('an error occurred while validating flow-specific rules', ex, LoggingLevel.ERROR); - RollupLogger.Instance.save(); - throw ex; + logAndThrowFlowException(ex, 'an error occurred while validating flow-specific rules'); } } + private static void logAndThrowFlowException(Exception ex, String logString) { + RollupLogger.Instance.log(logString, ex, LoggingLevel.ERROR); + RollupLogger.Instance.save(); + throw ex; + } + private static QueryWrapper getIntermediateGrandparentQueryWrapper(String grandparentFieldPath, List calcItems, Map oldCalcItems) { if (isCDC) { return new QueryWrapper(); @@ -1825,7 +1931,7 @@ global without sharing virtual class Rollup { return null; } - private static RollupAsyncProcessor getRollup( + private static Rollup getRollup( List rollupOperations, SObjectType sObjectType, List calcItems, @@ -1833,8 +1939,9 @@ global without sharing virtual class Rollup { Evaluator eval, InvocationPoint rollupInvokePoint ) { + Rollup rollupConductor = new RollupAsyncProcessor(rollupInvokePoint, calcItems, oldCalcItems); if (rollupOperations.isEmpty() || calcItems.isEmpty()) { - return new RollupAsyncProcessor(rollupInvokePoint); + return rollupConductor; } if (sObjectType == null) { sObjectType = calcItems[0].getSObjectType(); @@ -1843,7 +1950,6 @@ global without sharing virtual class Rollup { * We have rollup operations to perform. That's great! * Let's get ready to rollup! */ - RollupAsyncProcessor batchRollup = new RollupAsyncProcessor(rollupInvokePoint); DescribeSObjectResult describeForSObject = sObjectType.getDescribe(); Map fieldNameToField = describeForSObject.fields.getMap(); @@ -1865,7 +1971,7 @@ global without sharing virtual class Rollup { rollupMetadata.LookupFieldOnLookupObject__c = lookupFieldOnOpObject.getDescribe().getName(); rollupMetadata.RollupFieldOnLookupObject__c = rollupFieldOnOpObject.getDescribe().getName(); - if (rollupMetadata.IsFullRecordSet__c != true && ALWAYS_FULL_RECALC_OPS.contains(getBaseOperationName(rollupMetadata.RollupOperation__c))) { + if (rollupMetadata.IsFullRecordSet__c != true && isFullRecalcOp(getBaseOperationName(rollupMetadata.RollupOperation__c))) { rollupMetadata.IsFullRecordSet__c = true; } @@ -1889,16 +1995,15 @@ global without sharing virtual class Rollup { lookupSObjectType, sObjectType, // calc item SObjectType rollupOp, - filterResults.matchingItems, - oldCalcItems, - batchRollup, + filterResults.matchingItemIds, + rollupConductor, filterResults.eval, localControl, rollupInvokePoint, rollupMetadata ); } - return batchRollup; + return rollupConductor; } private static SObjectField getSObjectFieldByName(DescribeSObjectResult objectDescribe, String desiredField) { @@ -1913,6 +2018,14 @@ global without sharing virtual class Rollup { return null; } + private static Boolean isFullRecalcOp(String baseOperationName) { + return baseOperationName == Op.FIRST.name() || + baseOperationName == Op.LAST.name() || + baseOperationName == Op.AVERAGE.name() || + baseOperationName == Op.CONCAT_DISTINCT.name() || + baseOperationName == Op.COUNT_DISTINCT.name(); + } + private static String getRollupControlKey( InvocationPoint invokePoint, SObjectField rollupFieldOnCalcItem, @@ -2052,16 +2165,15 @@ global without sharing virtual class Rollup { SObjectType lookupSObjectType, SObjectType calcItemSObjectType, Op rollupOp, - List calcItems, - Map oldCalcItems, - Rollup batchRollup, + Set matchingCalcItemIds, + Rollup rollupConductor, Evaluator eval, RollupControl__mdt rollupControl, InvocationPoint invokePoint, Rollup__mdt rollupMetadata ) { - Rollup rollup = RollupAsyncProcessor.getProcessor( - calcItems, + RollupAsyncProcessor processor = RollupAsyncProcessor.getProcessor( + matchingCalcItemIds, rollupFieldOnCalcItem, lookupFieldOnCalcItem, lookupFieldOnOpObject, @@ -2069,23 +2181,19 @@ global without sharing virtual class Rollup { lookupSObjectType, calcItemSObjectType, rollupOp, - oldCalcItems, eval, invokePoint, rollupControl, rollupMetadata ); - return loadRollups(rollup, batchRollup); + return loadRollups(processor, rollupConductor); } - private static Rollup loadRollups(Rollup rollup, Rollup batchRollup) { - RollupAsyncProcessor processor = (RollupAsyncProcessor) rollup; - if (batchRollup != null && rollup != null && rollup.isNoOp == false) { - batchRollup.rollups.add(processor); - } else if (rollup != null && rollup.isNoOp == false) { - rollup.rollups.add(processor); + private static Rollup loadRollups(RollupAsyncProcessor processor, Rollup rollupConductor) { + if (rollupConductor != null && processor != null && processor.isNoOp == false) { + rollupConductor.rollups.add(processor); } - return batchRollup != null ? batchRollup : rollup; + return rollupConductor; } private static RollupControl__mdt getSingleControlOrDefault(SObjectField whereField, Object whereValue, RollupControl__mdt testOverrideData) { @@ -2138,14 +2246,7 @@ global without sharing virtual class Rollup { private static FilterResults filter(List calcItems, Map oldCalcItems, Evaluator eval, Rollup__mdt metadata, SObjectType calcItemType) { FilterResults results = new FilterResults(); - List matchingItems = calcItems == null ? new List() : calcItems.clone(); - results.matchingItems = matchingItems; - if (matchingItems.isEmpty()) { - return results; - } - matchingItems.clear(); // retains the strong-typing on the list for downstream calls to List.getSbjectType() calcItems = replaceCalcItemsForPolymorphicWhereClause(calcItems, metadata); - results.eval = RollupEvaluator.getEvaluator(eval, metadata, oldCalcItems, calcItemType); // while we iterate through calcItems, the only possible mutation to that array is through "replaceCalcItemsForPolymorphicWhereClause" @@ -2156,13 +2257,12 @@ global without sharing virtual class Rollup { // if the where clause would exclude something, but we're in an update // and the old value wouldn't have been excluded, pass it on through // to be handled further downstream - SObject potentialOldItem = oldCalcItems?.isEmpty() == false && oldCalcItems.containsKey(item.Id) ? oldCalcItems.get(item.Id) : item; - if (results.eval.matches(item) || results.eval.matches(potentialOldItem)) { - matchingItems.add(item); - // metadata shouldn't be null, but it's good to check; unfortunately, if(null) throws so we - // have to do this EXTRA explicit check + if (results.eval.matches(item)) { + results.matchingItemIds.add(item.Id); + } else if (oldCalcItems != null && oldCalcItems.containsKey(item.Id) && results.eval.matches(oldCalcItems.get(item.Id))) { + results.matchingItemIds.add(item.Id); } else if (metadata?.IsFullRecordSet__c == true) { - matchingItems.add(item); + results.matchingItemIds.add(item.Id); } } return results; diff --git a/rollup/core/classes/RollupAsyncProcessor.cls b/rollup/core/classes/RollupAsyncProcessor.cls index e57f97a9..8a9cb83a 100644 --- a/rollup/core/classes/RollupAsyncProcessor.cls +++ b/rollup/core/classes/RollupAsyncProcessor.cls @@ -10,16 +10,12 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme private final List deferredRollups = new List(); private final List syncRollups = new List(); - protected final List calcItems; - protected final Map oldCalcItems; - protected final Rollup__mdt metadata; protected final SObjectType calcItemType; private RollupRelationshipFieldFinder.Traversal traversal; private Map> lookupObjectToUniqueFieldNames; private Map> calcObjectToUniqueFieldNames; private List lookupItems; - private RollupAsyncProcessor fullRecalcProcessor; private Map> cachedFullRecalcs; private static final RollupSettings__c SETTINGS = RollupSettings__c.getInstance(); @@ -39,48 +35,17 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } } - public static void flatten(List stackedRollups) { - Map rollupOperationToRollup = new Map(); - Integer counter = 0; - Map> operationToProcessedRecords = new Map>(); - for (RollupAsyncProcessor stackedRollup : stackedRollups) { - // If the hashed contents are the same, we can't collapse - // the two rollup operations, and instead have to juggle the updated values for the parent in memory - String rollupKey = stackedRollup.getHashedContents(); - Boolean shouldAddSameRollupOperation = false; - - if (rollupOperationToRollup.containsKey(rollupKey)) { - RollupAsyncProcessor matchingRollup = rollupOperationToRollup.get(rollupKey); - for (Integer index = stackedRollup.calcItems.size() - 1; index >= 0; index--) { - SObject calcItem = stackedRollup.calcItems[index]; - if (matchingRollup.calcItems.contains(calcItem) == false && operationToProcessedRecords.containsKey(rollupKey) == false) { - doBookkeepingOnCachedItems(matchingRollup, stackedRollup, operationToProcessedRecords, calcItem, rollupKey, index); - } else if ( - matchingRollup.calcItems.contains(calcItem) == false && - operationToProcessedRecords.containsKey(rollupKey) && - operationToProcessedRecords.get(rollupKey).contains(calcItem.Id) == false - ) { - doBookkeepingOnCachedItems(matchingRollup, stackedRollup, operationToProcessedRecords, calcItem, rollupKey, index); - } - } - - if (stackedRollup.calcItems.isEmpty() == false) { - shouldAddSameRollupOperation = true; - } - } else { - rollupOperationToRollup.put(rollupKey, stackedRollup); - } - if (shouldAddSameRollupOperation) { - counter++; - rollupOperationToRollup.put(rollupKey + counter, stackedRollup); - } + private class CalcItemData { + public CalcItemData(List items, Map oldItems) { + this.items = items.clone(); + this.oldItems = oldItems.clone(); } - stackedRollups.clear(); - stackedRollups.addAll(rollupOperationToRollup.values()); + public final List items; + public final Map oldItems; } public static RollupAsyncProcessor getProcessor( - List calcItems, + Set matchingCalcItemIds, SObjectField opFieldOnCalcItem, SObjectField lookupFieldOnCalcItem, SObjectField lookupFieldOnLookupObject, @@ -88,14 +53,13 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme SObjectType lookupObj, SObjectType calcItem, Op operation, - Map oldCalcItems, Evaluator eval, InvocationPoint rollupInvokePoint, RollupControl__mdt rollupControl, Rollup__mdt metadata ) { return new QueueableProcessor( - calcItems, + matchingCalcItemIds, opFieldOnCalcItem, lookupFieldOnCalcItem, lookupFieldOnLookupObject, @@ -103,7 +67,6 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme lookupObj, calcItem, operation, - oldCalcItems, eval, rollupInvokePoint, rollupControl, @@ -113,41 +76,23 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme public RollupAsyncProcessor(InvocationPoint invokePoint) { super(invokePoint); - this.isBatched = true; - // a batch only becomes valid if other Rollups are added to it - this.isNoOp = true; } - public RollupAsyncProcessor(RollupAsyncProcessor innerRollup, Op op, List calcItems) { - this( - calcItems, - innerRollup.opFieldOnCalcItem, - innerRollup.lookupFieldOnCalcItem, - innerRollup.lookupFieldOnLookupObject, - innerRollup.opFieldOnLookupObject, - innerRollup.lookupObj, - innerRollup.calcItemType, - op, - innerRollup.oldCalcItems, - null, // eval gets assigned below - innerRollup.invokePoint, - innerRollup.rollupControl, - innerRollup.metadata - ); + public RollupAsyncProcessor(InvocationPoint invokePoint, List calcItems, Map oldCalcItems) { + super(invokePoint, calcItems, oldCalcItems); + } + + public RollupAsyncProcessor(RollupAsyncProcessor innerRollup) { + super(innerRollup.invokePoint, innerRollup.calcItems, innerRollup.oldCalcItems); this.rollups.addAll(innerRollup.rollups); this.isNoOp = this.rollups.isEmpty() && innerRollup.metadata?.IsFullRecordSet__c == false; this.isFullRecalc = innerRollup.isFullRecalc; this.isCDCUpdate = innerRollup.isCDCUpdate; - this.eval = innerRollup.eval; - } - - public RollupAsyncProcessor(RollupAsyncProcessor innerRollup) { - this(innerRollup, innerRollup.op, innerRollup.calcItems); } public RollupAsyncProcessor( - List calcItems, + Set matchingCalcItemIds, SObjectField opFieldOnCalcItem, SObjectField lookupFieldOnCalcItem, SObjectField lookupFieldOnLookupObject, @@ -155,14 +100,13 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme SObjectType lookupObj, SObjectType calcItemType, Op op, - Map oldCalcItems, Evaluator eval, InvocationPoint invokePoint, RollupControl__mdt rollupControl, Rollup__mdt rollupMetadata ) { super(); - this.calcItems = calcItems; + this.matchingCalcItemIds = matchingCalcItemIds; this.opFieldOnCalcItem = opFieldOnCalcItem; this.lookupFieldOnCalcItem = lookupFieldOnCalcItem; this.lookupFieldOnLookupObject = lookupFieldOnLookupObject; @@ -170,7 +114,6 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme this.lookupObj = lookupObj; this.calcItemType = calcItemType; this.op = op; - this.oldCalcItems = oldCalcItems; this.invokePoint = invokePoint; this.rollupControl = rollupControl; this.metadata = rollupMetadata; @@ -180,7 +123,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme this.eval = eval; } - this.isNoOp = this.calcItems?.isEmpty() == true && this.metadata?.IsFullRecordSet__c == false; + this.isNoOp = this.matchingCalcItemIds?.isEmpty() == true && this.metadata?.IsFullRecordSet__c == false; } public Integer compareTo(Object otherRollup) { @@ -211,14 +154,17 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme public override String toString() { Map props = new Map{ + 'Type' => this.getTypeName(), 'Invocation Point' => this.invokePoint.name(), - 'Calc Items' => JSON.serializePretty(this.calcItems), - 'Old Calc Items' => JSON.serializePretty(this.oldCalcItems), - 'Rollup Metadata' => JSON.serializePretty(this.metadata), - 'Rollup Control' => JSON.serializePretty(this.rollupControl), 'Is Full Recalc' => String.valueOf(this.isFullRecalc), 'Is No Op' => String.valueOf(this.isNoOp) }; + + this.addToMap(props, 'Calc Items', this.calcItems); + this.addToMap(props, 'Old Calc Items', this.oldCalcItems); + this.addToMap(props, 'Rollup Metadata', this.metadata); + this.addToMap(props, 'Rollup Control', this.rollupControl); + String baseString = ''; for (String key : props.keySet()) { baseString += key + ': ' + props.get(key) + '\n'; @@ -300,6 +246,10 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme return rollupProcessId; } + protected virtual String getTypeName() { + return 'RollupAsyncProcessor'; + } + protected RollupAsyncProcessor getAsyncRollup() { // swap off on which async process is running to achieve infinite scaling isRunningAsync = true; @@ -323,7 +273,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme roll = this; } else { // the end of the line - this.throwWithRollupData(this.rollups); + this.logFatalRollups(this.rollups); } return roll; @@ -331,7 +281,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme private class QueueableProcessor extends RollupAsyncProcessor implements System.Queueable { private QueueableProcessor( - List calcItems, + Set matchingCalcItemIds, SObjectField opFieldOnCalcItem, SObjectField lookupFieldOnCalcItem, SObjectField lookupFieldOnLookupObject, @@ -339,14 +289,13 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme SObjectType lookupObj, SObjectType calcItem, Op operation, - Map oldCalcItems, Evaluator eval, InvocationPoint rollupInvokePoint, RollupControl__mdt rollupControl, Rollup__mdt metadata ) { super( - calcItems, + matchingCalcItemIds, opFieldOnCalcItem, lookupFieldOnCalcItem, lookupFieldOnLookupObject, @@ -354,7 +303,6 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme lookupObj, calcItem, operation, - oldCalcItems, eval, rollupInvokePoint, rollupControl, @@ -370,6 +318,10 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme super(roll); } + protected override String getTypeName() { + return 'QueueableProcessor'; + } + protected override String beginAsyncRollup() { RollupLogger.Instance.log('about to queue', LoggingLevel.DEBUG); RollupLogger.Instance.save(); @@ -426,7 +378,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme protected void processDelegatedFullRecalcRollup(List rollupInfo, List calcItems, Map oldCalcItems) { isRunningAsync = true; // the first rollup can immediately start rolling up, instead of dispatching to a queueable / another batchable - RollupAsyncProcessor roll = this.getAsyncRollup(rollupInfo, this.calcItemType, calcItems, oldCalcItems, null, this.invokePoint); + Rollup roll = this.getAsyncRollup(rollupInfo, this.calcItemType, calcItems, oldCalcItems, null, this.invokePoint); roll.isFullRecalc = true; roll.fullRecalcProcessor = this; roll.runCalc(); @@ -491,9 +443,12 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme Map updatedLookupRecords = new Map(); Map grandparentRollups = new Map(); for (RollupAsyncProcessor roll : rollups) { + CalcItemData data = new CalcItemData(this.calcItems, this.oldCalcItems); + this.setupCalcItemData(roll); RollupLogger.Instance.log('starting rollup for: ', roll, LoggingLevel.DEBUG); // for each iteration, ensure we're not operating beyond the bounds of our query limits if (hasExceededCurrentRollupLimits(roll.rollupControl) || roll instanceof RollupFullBatchRecalculator) { + RollupLogger.Instance.log('Deferring current rollup, past limits', LoggingLevel.DEBUG); this.deferredRollups.add(roll); continue; } @@ -519,6 +474,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme for (SObject updatedRecord : updatedParentRecords) { updatedLookupRecords.put(updatedRecord.Id, updatedRecord); } + this.resetCalcItemData(data); } this.splitUpdates(updatedLookupRecords); @@ -528,15 +484,21 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme this.processDeferredRollups(); } - private String getHashedContents() { + protected override String getHashedContents() { // the only thing that necessarily makes a rollup unique is the sum total of the metadata behind it // as well as the calc items driving that calculation. - // you could have multiple rollups with different calc item where clauses all rolling up to the same field + // you could have multiple rollups with different Calc Item Where Clauses all rolling up to the same field // even worse - in situations where multiple DML operations are enqueued in the same transaction // the same calc items by Id might differ slightly by field value. return String.valueOf(this.metadata); } + private void addToMap(Map props, String key, Object ref) { + if (ref != null) { + props.put(key, JSON.serializePretty(ref)); + } + } + private void handleMultipleDMLRollupsEnqueuedInTheSameTransaction(List rolls) { // if items are inserted, updated, deleted (etc ...) // all in the same transaction, they can be introduced out of order @@ -612,15 +574,35 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme this.getAsyncRollup().beginAsyncRollup(); } else if (this.deferredRollups.isEmpty() == false) { - this.throwWithRollupData(this.deferredRollups); + this.logFatalRollups(this.deferredRollups); } } } - private void throwWithRollupData(List rolls) { + private void logFatalRollups(List rolls) { String exceptionString = 'rollup failed to re-queue for: '; RollupLogger.Instance.log(exceptionString, rolls, LoggingLevel.ERROR); - throw new AsyncException(exceptionString + rolls); + RollupLogger.Instance.save(); + } + + private void setupCalcItemData(Rollup roll) { + // if a rollup has calc items set, we take their calcItem dependencies as the source of truth + if (roll.calcItems?.isEmpty() == false) { + this.calcItems = roll.calcItems; + this.oldCalcItems = roll.oldCalcItems != null ? roll.oldCalcItems : new Map(); + } else if (this.calcItems?.isEmpty() == false) { + roll.calcItems = this.calcItems; + roll.oldCalcItems = this.oldCalcItems; + } + } + + private void resetCalcItemData(CalcItemData data) { + if (data.items?.isEmpty() == false) { + this.calcItems = data.items; + } + if (data.oldItems?.isEmpty() == false) { + this.oldCalcItems = data.oldItems; + } } private void getFieldNamesForRollups(List rollups) { @@ -660,15 +642,18 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme rollup.metadata, uniqueQueryFieldNames, rollup.lookupObj, - rollup.oldCalcItems + this.oldCalcItems ) - .getParents(rollup.calcItems); + .getParents(this.calcItems); } else if (rollup.traversal?.getIsFinished() == false) { rollup.traversal.recommence(); } return rollup.traversal.getIsFinished() ? rollup.traversal.getParentLookupToRecords() : lookupFieldToCalcItems; } - for (SObject calcItem : rollup.calcItems) { + for (SObject calcItem : this.calcItems) { + if (rollup.matchingCalcItemIds.contains(calcItem.Id) == false) { + continue; + } String key = (String) calcItem.get(rollup.lookupFieldOnCalcItem); if (lookupFieldToCalcItems.containsKey(key) == false) { lookupFieldToCalcItems.put(key, new Rollup.CalcItemBag(new List{ calcItem })); @@ -678,7 +663,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme // if the lookup key differs from what it was on the old calc item, // include that value as well so that we can fix reparented records' rollup values - SObject potentialOldCalcItem = rollup.oldCalcItems?.get(calcItem.Id); + SObject potentialOldCalcItem = this.oldCalcItems?.get(calcItem.Id); if (potentialOldCalcItem != null) { String oldKey = (String) potentialOldCalcItem.get(rollup.lookupFieldOnCalcItem); @@ -771,6 +756,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme SObjectType targetType; Map> queryCountsToLookupIds = new Map>(); + Integer totalCountOfRecords = 0; for (RollupAsyncProcessor roll : this.rollups) { if (targetType == null) { targetType = roll.lookupObj; @@ -778,22 +764,28 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme hasMoreThanOneTarget = true; } + if (roll.queryCount != null) { + totalCountOfRecords += roll.queryCount; + continue; + } + + if (hasMoreThanOneTarget) { + break; + } + if (String.isNotBlank(roll.metadata?.GrandparentRelationshipFieldPath__c)) { // getting the count for grandparent (or greater) relationships will be handled further // downstream; for our purposes, it isn't useful to try to get all of the records while // we're still in a sync context continue; - } else if (roll.calcItems?.isEmpty() != false) { + } else if (roll.calcItems?.isEmpty() == true) { continue; } - if (hasMoreThanOneTarget) { - break; - } - Set uniqueIds = new Set(); - for (SObject calcItem : roll.calcItems) { + List items = roll.calcItems?.isEmpty() == false ? roll.calcItems : this.calcItems; + for (SObject calcItem : items) { String lookupKey = (String) calcItem.get(roll.lookupFieldOnCalcItem); if (String.isNotBlank(lookupKey)) { uniqueIds.add(lookupKey); @@ -813,7 +805,6 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } } - Integer totalCountOfRecords = 0; if (hasMoreThanOneTarget == false) { for (String countQuery : queryCountsToLookupIds.keySet()) { Set objIds = queryCountsToLookupIds.get(countQuery); @@ -836,7 +827,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme ) { Map recordsToUpdate = new Map(); Map> oldLookupItems = new Map>(); - Set unprocessedCalcItems = new Set(); + Set unprocessedCalcItems = new Set(); RollupSObjectUpdater updater = new RollupSObjectUpdater(rollup.opFieldOnLookupObject); if (this.fullRecalcProcessor != null) { rollup.fullRecalcProcessor = this.fullRecalcProcessor; @@ -860,7 +851,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme localCalcItems.addAll(bag.additional); if (hasExceededCurrentRollupLimits(this.rollupControl)) { - unprocessedCalcItems.addAll(localCalcItems); + unprocessedCalcItems = new Map(localCalcItems).keySet(); lookupItems.remove(index); continue; } @@ -885,19 +876,21 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme return recordsToUpdate.values(); } - private void deferCalculationsWhenApproachingLimits(RollupAsyncProcessor roll, Set unprocessedCalcItems) { + private void deferCalculationsWhenApproachingLimits(RollupAsyncProcessor roll, Set unprocessedCalcItems) { // remove the calc items that were successfully processed - // they're the ones that aren't in the unprocessed Set - for (Integer index = roll.calcItems.size() - 1; index >= 0; index--) { - SObject calcItem = roll.calcItems[index]; - if (unprocessedCalcItems.contains(calcItem) == false) { - roll.calcItems.remove(index); + List mutableItems = this.calcItems.clone(); + for (Integer index = mutableItems.size() - 1; index >= 0; index--) { + SObject calcItem = mutableItems[index]; + if (unprocessedCalcItems.contains(calcItem.Id) == false) { + mutableItems.remove(index); } } // if all of the calc items have been processed, we're golden - no need to proceed // otherwise, the newly trimmed-down Rollup will get picked up downstream for // reprocessing! - if (roll.calcItems.isEmpty() == false) { + if (mutableItems.isEmpty() == false) { + roll.calcItems = mutableItems; this.deferredRollups.add(roll); } } @@ -908,8 +901,8 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme if ( roll.metadata?.IsFullRecordSet__c == true && (roll.eval.matches(calcItem) == false && - (roll.oldCalcItems.containsKey(calcItem.Id) == false || - roll.eval.matches(roll.oldCalcItems.get(calcItem.Id)) == false)) + (this.oldCalcItems.containsKey(calcItem.Id) == false || + roll.eval.matches(this.oldCalcItems.get(calcItem.Id)) == false)) ) { // technically it should only be possible for a calc item that doesn't match // to still exist if it is a Full Record Set operation; this gives people the chance @@ -918,7 +911,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme continue; } // Check for reparented records - SObject oldCalcItem = roll.oldCalcItems.get(calcItem.Id); + SObject oldCalcItem = this.oldCalcItems?.get(calcItem.Id); if (oldCalcItem == null) { continue; @@ -959,13 +952,13 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } private SObject reassignOldCalcItemIfValueChanged(String lookupId, SObject oldCalcItem, RollupAsyncProcessor rollup) { - if (String.isBlank(lookupId)) { + if (String.isBlank(lookupId) || this.oldCalcItems == null) { return oldCalcItem; } // truly terrible, but before we pass the old item through the reparenting code path, we need to validate that it's only // the lookup field that has changed; otherwise, if the opFieldOnCalcItem has changed too, substitute the item whose value // previously corresponded to the parent record - for (SObject otherOldCalcItem : rollup.oldCalcItems.values()) { + for (SObject otherOldCalcItem : this.oldCalcItems.values()) { if (otherOldCalcItem.get(rollup.lookupFieldOnCalcItem) == lookupId) { if (otherOldCalcItem.get(rollup.opFieldOnCalcItem) != oldCalcItem.get(rollup.opFieldOnCalcItem)) { return otherOldCalcItem; @@ -988,7 +981,7 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme ); rollupCalc.setEvaluator(roll.eval); rollupCalc.setCDCUpdate(this.isCDCUpdate); - rollupCalc.performRollup(calcItems, roll.oldCalcItems); + rollupCalc.performRollup(calcItems, this.oldCalcItems); return rollupCalc.getReturnValue(); } @@ -1011,7 +1004,23 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme String currentOp = getBaseOperationName(roll.op.name()); String deleteOpName = 'DELETE_' + currentOp; Op deleteOp = opNameToOp.get(deleteOpName); - RollupAsyncProcessor oldLookupsRollup = new RollupAsyncProcessor(roll, deleteOp, reparentedCalcItems); + + // by default this returns a "batched" (set) of rollups; we + // just want the first (and only) inner rollup to perform the pseudo-delete + RollupAsyncProcessor oldLookupsRollup = getProcessor( + new Set(), + roll.opFieldOnCalcItem, + roll.lookupFieldOnCalcItem, + roll.lookupFieldOnLookupObject, + roll.opFieldOnLookupObject, + roll.lookupObj, + roll.calcItemType, + deleteOp, + null, + this.invokePoint, + this.rollupControl, + roll.metadata + ); RollupLogger.Instance.log('reparenting operation: ', oldLookupsRollup, LoggingLevel.DEBUG); RollupLogger.Instance.log('Reparented item prior to reparenting rollup: ', lookupRecord, LoggingLevel.DEBUG); @@ -1027,27 +1036,4 @@ global virtual without sharing class RollupAsyncProcessor extends Rollup impleme } } } - - private static void doBookkeepingOnCachedItems( - RollupAsyncProcessor matchingRollup, - RollupAsyncProcessor stackedRollup, - Map> operationToProcessedRecords, - SObject calcItem, - String rollupKey, - Integer index - ) { - List processedRecords = operationToProcessedRecords.containsKey(rollupKey) - ? operationToProcessedRecords.get(rollupKey) - : new List{ calcItem.Id }; - operationToProcessedRecords.put(rollupKey, processedRecords); - Map idToCalcItem = new Map(matchingRollup.calcItems); - idToCalcItem.put(calcItem.Id, calcItem); - matchingRollup.calcItems.clear(); - matchingRollup.calcItems.addAll(idToCalcItem.values()); - stackedRollup.calcItems.remove(index); - - if (stackedRollup.oldCalcItems.isEmpty() == false && stackedRollup.oldCalcItems.containsKey(calcItem.Id) == false) { - matchingRollup.oldCalcItems.put(calcItem.Id, stackedRollup.oldCalcItems.get(calcItem.Id)); - } - } } diff --git a/rollup/core/classes/RollupFullBatchRecalculator.cls b/rollup/core/classes/RollupFullBatchRecalculator.cls index b7b28c4e..e8c5dc8e 100644 --- a/rollup/core/classes/RollupFullBatchRecalculator.cls +++ b/rollup/core/classes/RollupFullBatchRecalculator.cls @@ -28,6 +28,7 @@ public class RollupFullBatchRecalculator extends RollupAsyncProcessor implements } public override void execute(Database.BatchableContext bc, List calcItems) { + RollupLogger.Instance.log('starting full batch recalc run', this, LoggingLevel.DEBUG); /** * this batch class is a glorified "for loop" for the calc items, dispatching * them to the overall Rollup framework while breaking us out of the query limits @@ -44,6 +45,10 @@ public class RollupFullBatchRecalculator extends RollupAsyncProcessor implements RollupLogger.Instance.save(); } + protected override String getTypeName() { + return 'RollupFullBatchRecalculator'; + } + protected override void retrieveAdditionalCalcItems(Map lookupToCalcItems, RollupAsyncProcessor rollup) { Map local = new Map(); for (String lookupKey : lookupToCalcItems.keySet()) { diff --git a/rollup/core/classes/RollupLogger.cls b/rollup/core/classes/RollupLogger.cls index 51239b93..0a767995 100644 --- a/rollup/core/classes/RollupLogger.cls +++ b/rollup/core/classes/RollupLogger.cls @@ -86,6 +86,7 @@ public virtual class RollupLogger extends Rollup implements ILogger { try { loggerInstance = (ILogger) Type.forName(SELF.rollupControl.RollupLoggerName__c).newInstance(); } catch (Exception ex) { + SELF.log('cast to Rollup.ILogger failed with message: ' + ex.getMessage() + ', falling back to default logger', SELF, LoggingLevel.WARN); loggerInstance = SELF; } } else { diff --git a/rollup/core/classes/RollupQueryBuilder.cls b/rollup/core/classes/RollupQueryBuilder.cls index e5d5c5bb..bcc78f88 100644 --- a/rollup/core/classes/RollupQueryBuilder.cls +++ b/rollup/core/classes/RollupQueryBuilder.cls @@ -113,7 +113,7 @@ public without sharing class RollupQueryBuilder { optionalWhereClause = optionalWhereClause.replace(whereClause, '').trim(); } } catch (Exception ex) { - RollupLogger.Instance.log('exception occurred while building query: ', ex, LoggingLevel.ERROR); + RollupLogger.Instance.log('exception occurred while building query: ', ex, LoggingLevel.WARN); } return optionalWhereClause; } diff --git a/rollup/tests/RollupStandardIntegrationTests.cls b/rollup/tests/RollupStandardIntegrationTests.cls index 9055637d..0d080bc8 100644 --- a/rollup/tests/RollupStandardIntegrationTests.cls +++ b/rollup/tests/RollupStandardIntegrationTests.cls @@ -2,9 +2,10 @@ private class RollupStandardIntegrationTests { @TestSetup static void setup() { + upsert new RollupSettings__c(IsEnabled__c = true); + // gets nulled out at the end of the setup context Rollup.defaultControl = new RollupControl__mdt(ShouldAbortRun__c = true); insert new Account(Name = 'RollupStandardIntegrationTests'); - upsert new RollupSettings__c(IsEnabled__c = true); } @isTest @@ -99,7 +100,8 @@ private class RollupStandardIntegrationTests { @isTest static void shouldNotFailForTruncatedTextFields() { Account acc = [SELECT Id FROM Account]; - Contact con = new Contact(AccountId = acc.Id, Description = '0'.repeat(256), LastName = 'Truncate', Email = 'rollup@gmail.com'); + Integer maxAccountNameLength = Account.Name.getDescribe().getLength(); + Contact con = new Contact(AccountId = acc.Id, Description = '0'.repeat(maxAccountNameLength + 1), LastName = 'Truncate', Email = 'rollup@gmail.com'); insert con; Rollup__mdt meta = new Rollup__mdt( @@ -117,7 +119,7 @@ private class RollupStandardIntegrationTests { Test.stopTest(); acc = [SELECT Name FROM Account]; - System.assertEquals(255, acc.Name.length(), acc.Name); + System.assertEquals(maxAccountNameLength, acc.Name.length(), acc.Name); } @isTest @@ -652,13 +654,9 @@ private class RollupStandardIntegrationTests { update cpas; Test.startTest(); - List flowOutputs = Rollup.performRollup(flowInputs); + Rollup.performRollup(flowInputs); Test.stopTest(); - System.assertEquals(1, flowOutputs.size(), 'Flow outputs were not provided'); - System.assertEquals('SUCCESS', flowOutputs[0].message); - System.assertEquals(true, flowOutputs[0].isSuccess); - acc = [SELECT Id, AnnualRevenue FROM Account WHERE Id = :acc.Id]; System.assertEquals(1500, acc.AnnualRevenue, 'SUM REFRESH from flow should fully recalc'); reparentedAccount = [SELECT Id, AnnualRevenue FROM Account WHERE Id = :reparentedAccount.Id]; diff --git a/rollup/tests/RollupTests.cls b/rollup/tests/RollupTests.cls index 5f76df3e..49d54814 100644 --- a/rollup/tests/RollupTests.cls +++ b/rollup/tests/RollupTests.cls @@ -3386,13 +3386,11 @@ private class RollupTests { /** Re-queueing */ @isTest static void shouldRequeueRollupsWhenQueryLimitsExceeded() { - DMLMock mock = loadAccountIdMock(new List{ new ContactPointAddress(Id = RollupTestUtils.createId(ContactPointAddress.SObjectType), PreferenceRank = 1) }); - Rollup.apexContext = TriggerOperation.AFTER_INSERT; - Rollup.defaultControl = new RollupControl__mdt( - MaxRollupRetries__c = 1, - IsRollupLoggingEnabled__c = true, - MaxNumberOfQueries__c = 1 + DMLMock mock = loadAccountIdMock( + new List{ new ContactPointAddress(Id = RollupTestUtils.createId(ContactPointAddress.SObjectType), PreferenceRank = 1) } ); + Rollup.apexContext = TriggerOperation.AFTER_INSERT; + Rollup.defaultControl = new RollupControl__mdt(MaxRollupRetries__c = 1, IsRollupLoggingEnabled__c = true, MaxNumberOfQueries__c = 1); Rollup.specificControl = new RollupControl__mdt( ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.SYNCHRONOUS, MaxLookupRowsBeforeBatching__c = 1, @@ -3410,29 +3408,6 @@ private class RollupTests { System.assertEquals('Completed', [SELECT Status FROM AsyncApexJob WHERE JobType = 'Queueable' LIMIT 1]?.Status, [SELECT Status, JobType FROM AsyncApexJob]); } - @isTest - static void shouldThrowIfDeferralNotPossible() { - DMLMock mock = loadAccountIdMock(new List{ new ContactPointAddress(PreferenceRank = 1) }); - Rollup.apexContext = TriggerOperation.AFTER_INSERT; - Rollup.defaultControl = new RollupControl__mdt(MaxRollupRetries__c = 0, IsRollupLoggingEnabled__c = true); - Rollup.specificControl = new RollupControl__mdt( - ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.SYNCHRONOUS, - MaxLookupRowsBeforeBatching__c = -1, - BatchChunkSize__c = 0 - ); - - try { - Test.startTest(); - Rollup.countFromApex(ContactPointAddress.PreferenceRank, ContactPointAddress.ParentId, Account.Id, Account.AnnualRevenue, Account.SObjectType).runCalc(); - Test.stopTest(); - // a throw should occur within startTest()/stopTest() - the assertion below is uncatchable - System.assert(false, 'shouldThrowIfDeferralNotPossible should not make it here'); - } catch (Exception ex) { - System.assertEquals(true, ex.getMessage().contains('rollup failed to re-queue for'), ex.getMessage()); - System.assertNotEquals(true, ex.getMessage().contains('shouldThrowIfDeferralNotPossible should not make it here')); - } - } - /** Grandparent rollups */ @isTest static void shouldAllowGrandparentRollups() { @@ -3521,6 +3496,11 @@ private class RollupTests { Rollup.DML = mock; Rollup.shouldRun = true; Rollup.records = [SELECT Id, AboutMe FROM User WHERE Id = :acc.OwnerId]; + Rollup.defaultControl = new RollupControl__mdt( + ShouldRunAs__c = RollupMetaPicklists.ShouldRunAs.QUEUEABLE, + BatchChunkSize__c = 100, + IsRollupLoggingEnabled__c = true + ); Rollup.rollupMetadata = new List{ new Rollup__mdt( CalcItem__c = 'ContactPointAddress', diff --git a/scripts/build-and-promote-package.ps1 b/scripts/build-and-promote-package.ps1 index c34967e0..4ae8e807 100644 --- a/scripts/build-and-promote-package.ps1 +++ b/scripts/build-and-promote-package.ps1 @@ -59,7 +59,7 @@ if(Test-Path ".\PACKAGING_SFDX_URL.txt") { } $sfdxProjectJson = Get-SFDX-Project-JSON -$currentPackageVersion = $sfdxProjectJson.packageDirectories.versionNumber +$currentPackageVersion = $sfdxProjectJson.packageDirectories[0].versionNumber Write-Output "Current package version number: $currentPackageVersion" @@ -98,8 +98,7 @@ if($currentBranch -eq "main") { # main is a push-protected branch; only create new package versions as part of PRs against main Write-Output "Creating new package version" - $packageVersionNotes = $sfdxProjectJson.packageDirectories.versionDescription - sfdx force:package:version:create -d $sfdxProjectJson.packageDirectories.path -x -w 30 -e $packageVersionNotes -c --releasenotesurl $sfdxProjectJson.packageDirectories.releaseNotesUrl + sfdx force:package:version:create -d $sfdxProjectJson.packageDirectories[0].path -x -w 30 git add ./sfdx-project.json # Now that sfdx-project.json has been updated, grab the latest package version @@ -109,7 +108,10 @@ if($currentBranch -eq "main") { if($currentPackageVersionId -ne $priorPackageVersionId) { $readmePath = "./README.md" - ((Get-Content -path $readmePath -Raw) -replace $priorPackageVersionId, $currentPackageVersionId) | Set-Content -Path $readmePath -NoNewline + $loginReplacement = "https://login.salesforce.com/packaging/installPackage.apexp?p0=" + $currentPackageVersionId + $testReplacement = "https://test.salesforce.com/packaging/installPackage.apexp?p0=" + $currentPackageVersionId + ((Get-Content -path $readmePath -Raw) -replace "https:\/\/login.salesforce.com\/packaging\/installPackage.apexp\?p0=.{0,18}", $loginReplacement) | Set-Content -Path $readmePath -NoNewline + ((Get-Content -path $readmePath -Raw) -replace "https:\/\/test.salesforce.com\/packaging\/installPackage.apexp\?p0=.{0,18}", $testReplacement) | Set-Content -Path $readmePath -NoNewline git add $readmePath } diff --git a/scripts/deploy-sfdx-project.json b/scripts/deploy-sfdx-project.json index 4af37e70..165e69de 100644 --- a/scripts/deploy-sfdx-project.json +++ b/scripts/deploy-sfdx-project.json @@ -8,5 +8,5 @@ ], "namespace": "", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "51.0" + "sourceApiVersion": "52.0" } diff --git a/sfdx-project.json b/sfdx-project.json index dcb76ba9..fb666050 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -4,20 +4,23 @@ "default": true, "package": "apex-rollup", "path": "rollup", - "versionNumber": "1.2.37.0", - "versionDescription": "Fixing deferred rollup logic and adding more logging surrounding deferrals", - "releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest" + "versionNumber": "1.2.38.0", + "versionDescription": "Including extra code coverage during package version creation, REFRESH updates", + "releaseNotesUrl": "https://github.com/jamessimone/apex-rollup/releases/latest", + "unpackagedMetadata": { + "path": "extra-tests" + } }, { "package": "Apex Rollup - Custom Logger", "path": "plugins/CustomObjectRollupLogger", "dependencies": [ { - "package": "apex-rollup@1.2.37-0" + "package": "apex-rollup@1.2.38-0" } ], - "versionNumber": "0.0.2.0", - "versionDescription": "Introducing basic logging plugin", + "versionNumber": "0.0.3.0", + "versionDescription": "Prevent string overflow on log messages, added ErrorWouldHaveBeenThrown__c field on RollupLog__c", "default": false }, { @@ -25,7 +28,7 @@ "path": "plugins/NebulaLogger", "dependencies": [ { - "package": "apex-rollup@1.2.37-0" + "package": "apex-rollup@1.2.38-0" }, { "package": "Nebula Logger - Unlocked Package@4.5.2-0-plugin-framework-enhancements" @@ -36,7 +39,8 @@ "default": false }, { - "path": "extra-tests" + "path": "extra-tests", + "default": false } ], "namespace": "", @@ -47,6 +51,7 @@ "Apex Rollup - Custom Logger": "0Ho6g000000Gn8ZCAS", "Apex Rollup - Custom Logger@0.0.1-0": "04t6g000008SgtwAAC", "Apex Rollup - Custom Logger@0.0.2-0": "04t6g000008SgufAAC", + "Apex Rollup - Custom Logger@0.0.3-0": "04t6g000008SgyNAAS", "Apex Rollup - Nebula Logger": "0Ho6g000000Gn8PCAS", "Apex Rollup - Nebula Logger@0.0.1-0": "04t6g000008SgolAAC", "Nebula Logger - Unlocked Package@4.5.2-0-plugin-framework-enhancements": "04t5Y0000027FNaQAM", @@ -81,6 +86,7 @@ "apex-rollup@1.2.34-0": "04t6g000008SglrAAC", "apex-rollup@1.2.35-0": "04t6g000008SguGAAS", "apex-rollup@1.2.36-0": "04t6g000008SguaAAC", - "apex-rollup@1.2.37-0": "04t6g000008SguzAAC" + "apex-rollup@1.2.37-0": "04t6g000008SguzAAC", + "apex-rollup@1.2.38-0": "04t6g000008SgySAAS" } } \ No newline at end of file