diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0efd31..2f16998 100755 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,50 +5,18 @@ on: branches: [ main ] pull_request: branches: [ main ] - workflow_dispatch: jobs: - deploy-to-scratch-and-run-tests: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Install Salesforce CLI with NPM - run: | - npm install @salesforce/cli --global - - - name: Authorize with the dev hub - run: | - echo "${{ secrets.SFDX_AUTH_URL_DEVHUB }}" | sf org login sfdx-url --alias DevHub --set-default-dev-hub --sfdx-url-stdin > /dev/null 2>&1 - echo "✓ Successfully authenticated with Dev Hub" - - - name: Create scratch org - run: | - sf org create scratch --definition-file config/project-scratch-def.json --alias ScratchOrg --wait 30 --duration-days 1 --set-default > /dev/null 2>&1 - echo "✓ Successfully created scratch org" - - - name: Push source - run: sf project deploy start --target-org ScratchOrg --wait 30 - - - name: Run tests - run: sf apex run test --target-org ScratchOrg --code-coverage --result-format human --output-dir ./tests/apex --test-level RunLocalTests --wait 30 - - - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - slug: beyond-the-cloud-dev/dml-lib - flags: Apex - - - name: Delete scratch org - if: always() - run: | - sf org delete scratch --target-org ScratchOrg --no-prompt > /dev/null 2>&1 - echo "✓ Scratch org cleanup completed" + salesforce-ci: + uses: beyond-the-cloud-dev/cicd-template/.github/workflows/salesforce-ci.yml@main + with: + node-version: '20' + sf-cli-version: 'latest' + scratch-org-duration: 1 + test-level: 'RunLocalTests' + upload-to-codecov: true + codecov-slug: ${{ github.repository }} # Automatically uses current repository + secrets: + SFDX_AUTH_URL_DEVHUB: ${{ secrets.SFDX_AUTH_URL_DEVHUB }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index c5fca3d..ec900b8 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The DML Lib provides functional constructs for DML statements in Apex. -DML Lib is part of [Apex Fluently](https://apexfluently.beyondthecloud.dev/), a suite of production-ready Salesforce libraries by Beyond the Cloud. +DML Lib is part of [Apex Fluently](https://apexfluently.beyondthecloud.dev/), a suite of production-ready Salesforce libraries by [Beyond the Cloud](https://blog.beyondthecloud.dev/blog). For more details, please refer to the [documentation](https://dml.beyondthecloud.dev). diff --git a/force-app/main/default/classes/DML.cls b/force-app/main/default/classes/DML.cls index 81b3f57..ec1488e 100644 --- a/force-app/main/default/classes/DML.cls +++ b/force-app/main/default/classes/DML.cls @@ -2,10 +2,10 @@ * Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/dml-lib/blob/main/LICENSE) * - * v1.9.0 + * v2.0.0 * * PMD False Positives: - * - MethodNamingConventions - Some methods are uppercase to indicate that they are "constructors" of othere internal classes + * - MethodNamingConventions - Some methods are uppercase to indicate that they are "constructors" of other internal classes * - CyclomaticComplexity: It is a library and we tried to put everything into ONE class * - CognitiveComplexity: It is a library and we tried to put everything into ONE class * - ExcessivePublicCount: It is a library and we tried to put everything into ONE class @@ -60,19 +60,31 @@ global inherited sharing class DML implements Commitable { Commitable toUpdate(DML.Records records); // Upsert Commitable toUpsert(SObject record); + Commitable toUpsert(SObject record, SObjectField externalIdField); Commitable toUpsert(DML.Record record); Commitable toUpsert(List records); + Commitable toUpsert(List records, SObjectField externalIdField); Commitable toUpsert(DML.Records records); // Delete Commitable toDelete(Id recordId); Commitable toDelete(SObject record); Commitable toDelete(Iterable recordIds); Commitable toDelete(List records); + // Hard Delete + Commitable toHardDelete(Id recordId); + Commitable toHardDelete(SObject record); + Commitable toHardDelete(Iterable recordIds); + Commitable toHardDelete(List records); // Undelete Commitable toUndelete(Id recordId); Commitable toUndelete(SObject record); Commitable toUndelete(Iterable recordIds); Commitable toUndelete(List records); + // Merge + Commitable toMerge(SObject mergeToRecord, SObject duplicatedRecord); + Commitable toMerge(SObject mergeToRecord, List duplicateRecords); + Commitable toMerge(SObject mergeToRecord, Id duplicatedRecordId); + Commitable toMerge(SObject mergeToRecord, Iterable duplicatedRecordIds); // Platform Event Commitable toPublish(SObject record); Commitable toPublish(List records); @@ -120,6 +132,7 @@ global inherited sharing class DML implements Commitable { Commitable options(Database.DmlOptions options); Commitable discardWork(); Commitable commitHook(DML.Hook callback); + Commitable combineOnDuplicate(); // Save Result dryRun(); Result commitWork(); @@ -152,6 +165,7 @@ global inherited sharing class DML implements Commitable { List upserts(); List deletes(); List undeletes(); + List merges(); List events(); // Per Object OperationType OperationResult insertsOf(Schema.SObjectType objectType); @@ -159,6 +173,7 @@ global inherited sharing class DML implements Commitable { OperationResult upsertsOf(Schema.SObjectType objectType); OperationResult deletesOf(Schema.SObjectType objectType); OperationResult undeletesOf(Schema.SObjectType objectType); + OperationResult mergesOf(Schema.SObjectType objectType); OperationResult eventsOf(Schema.SObjectType objectType); } @@ -214,6 +229,7 @@ global inherited sharing class DML implements Commitable { Mockable allUpserts(); Mockable allDeletes(); Mockable allUndeletes(); + Mockable allMerges(); Mockable allPublishes(); // Per Object OperationType Mockable insertsFor(SObjectType objectType); @@ -221,7 +237,24 @@ global inherited sharing class DML implements Commitable { Mockable upsertsFor(SObjectType objectType); Mockable deletesFor(SObjectType objectType); Mockable undeletesFor(SObjectType objectType); + Mockable mergesFor(SObjectType objectType); Mockable publishesFor(SObjectType objectType); + // Errors + Mockable exceptionOnInserts(); + Mockable exceptionOnUpdates(); + Mockable exceptionOnUpserts(); + Mockable exceptionOnDeletes(); + Mockable exceptionOnUndeletes(); + Mockable exceptionOnMerges(); + Mockable exceptionOnPublishes(); + // Per Operation Type Per Object Type + Mockable exceptionOnInsertsFor(SObjectType objectType); + Mockable exceptionOnUpdatesFor(SObjectType objectType); + Mockable exceptionOnUpsertsFor(SObjectType objectType); + Mockable exceptionOnDeletesFor(SObjectType objectType); + Mockable exceptionOnUndeletesFor(SObjectType objectType); + Mockable exceptionOnMergesFor(SObjectType objectType); + Mockable exceptionOnPublishesFor(SObjectType objectType); } // Hooks @@ -233,11 +266,10 @@ global inherited sharing class DML implements Commitable { // Implementation - private Configuration configuration = new Configuration(); - - global enum OperationType { INSERT_DML, UPDATE_DML, UPSERT_DML, DELETE_DML, UNDELETE_DML, PUBLISH_DML } + global enum OperationType { INSERT_DML, UPSERT_DML, UPDATE_DML, MERGE_DML, DELETE_DML, UNDELETE_DML, PUBLISH_DML } - private Map operationsByType = new Map(); + private Orchestrator orchestrator; + private Configuration configuration; private Hook hook = null; @@ -247,23 +279,15 @@ global inherited sharing class DML implements Commitable { private static final Map dmlIdentifierToMock = new Map(); global DML() { - this.initializeOperations(); + this.configuration = new Configuration(); + this.orchestrator = new Orchestrator(this.configuration); } global DML(List customExecutionOrder) { this(); this.configuration.executionOrder(customExecutionOrder); } - - private void initializeOperations() { - this.operationsByType.put(OperationType.INSERT_DML, new InsertOperation(this.configuration)); - this.operationsByType.put(OperationType.UPDATE_DML, new UpdateOperation(this.configuration)); - this.operationsByType.put(OperationType.UPSERT_DML, new UpsertOperation(this.configuration)); - this.operationsByType.put(OperationType.DELETE_DML, new DeleteOperation(this.configuration)); - this.operationsByType.put(OperationType.UNDELETE_DML, new UndeleteOperation(this.configuration)); - this.operationsByType.put(OperationType.PUBLISH_DML, new PlatformEventOperation(this.configuration)); - } - + // Insert public Commitable toInsert(SObject record) { @@ -271,8 +295,7 @@ global inherited sharing class DML implements Commitable { } public Commitable toInsert(Record record) { - this.operationsByType.get(OperationType.INSERT_DML).register(record); - return this; + return this.registerOperation(new InsertCommand(record)); } public Commitable toInsert(List records) { @@ -280,8 +303,7 @@ global inherited sharing class DML implements Commitable { } public Commitable toInsert(Records records) { - this.operationsByType.get(OperationType.INSERT_DML).register(records); - return this; + return this.registerOperation(new InsertCommand(records)); } public OperationResult insertImmediately(SObject record) { @@ -289,7 +311,7 @@ global inherited sharing class DML implements Commitable { } public OperationResult insertImmediately(Record record) { - return new InsertOperation(this.configuration).register(record).commitWork().get(0); + return this.executeImmediately(new InsertCommand(record)); } public OperationResult insertImmediately(List records) { @@ -297,9 +319,10 @@ global inherited sharing class DML implements Commitable { } public OperationResult insertImmediately(Records records) { - return new InsertOperation(this.configuration).register(records).commitWork().get(0); + return this.executeImmediately(new InsertCommand(records)); } + // Update public Commitable toUpdate(SObject record) { @@ -307,8 +330,7 @@ global inherited sharing class DML implements Commitable { } public Commitable toUpdate(Record record) { - this.operationsByType.get(OperationType.UPDATE_DML).register(record); - return this; + return this.registerOperation(new UpdateCommand(record)); } public Commitable toUpdate(List records) { @@ -316,8 +338,7 @@ global inherited sharing class DML implements Commitable { } public Commitable toUpdate(Records records) { - this.operationsByType.get(OperationType.UPDATE_DML).register(records); - return this; + return this.registerOperation(new UpdateCommand(records)); } public OperationResult updateImmediately(SObject record) { @@ -325,7 +346,7 @@ global inherited sharing class DML implements Commitable { } public OperationResult updateImmediately(Record record) { - return new UpdateOperation(this.configuration).register(record).commitWork().get(0); + return this.executeImmediately(new UpdateCommand(record)); } public OperationResult updateImmediately(List records) { @@ -333,7 +354,7 @@ global inherited sharing class DML implements Commitable { } public OperationResult updateImmediately(Records records) { - return new UpdateOperation(this.configuration).register(records).commitWork().get(0); + return this.executeImmediately(new UpdateCommand(records)); } // Upsert @@ -342,18 +363,24 @@ global inherited sharing class DML implements Commitable { return this.toUpsert(Record(record)); } + public Commitable toUpsert(SObject record, SObjectField externalIdField) { + return this.registerOperation(new UpsertCommand(Record(record)).withExternalIdField(externalIdField)); + } + public Commitable toUpsert(Record record) { - this.operationsByType.get(OperationType.UPSERT_DML).register(record); - return this; + return this.registerOperation(new UpsertCommand(record)); } public Commitable toUpsert(List records) { return this.toUpsert(Records(records)); } + public Commitable toUpsert(List records, SObjectField externalIdField) { + return this.registerOperation(new UpsertCommand(Records(records)).withExternalIdField(externalIdField)); + } + public Commitable toUpsert(Records records) { - this.operationsByType.get(OperationType.UPSERT_DML).register(records); - return this; + return this.registerOperation(new UpsertCommand(records)); } public OperationResult upsertImmediately(SObject record) { @@ -361,7 +388,7 @@ global inherited sharing class DML implements Commitable { } public OperationResult upsertImmediately(Record record) { - return new UpsertOperation(this.configuration).register(record).commitWork().get(0); + return this.executeImmediately(new UpsertCommand(record)); } public OperationResult upsertImmediately(List records) { @@ -369,14 +396,13 @@ global inherited sharing class DML implements Commitable { } public OperationResult upsertImmediately(Records records) { - return new UpsertOperation(this.configuration).register(records).commitWork().get(0); + return this.executeImmediately(new UpsertCommand(records)); } // Delete public Commitable toDelete(Id recordId) { - this.operationsByType.get(OperationType.DELETE_DML).register(Record(recordId)); - return this; + return this.registerOperation(new DeleteCommand(Record(recordId))); } public Commitable toDelete(SObject record) { @@ -384,36 +410,51 @@ global inherited sharing class DML implements Commitable { } public Commitable toDelete(Iterable recordIds) { - this.operationsByType.get(OperationType.DELETE_DML).register(Records(recordIds)); - return this; + return this.registerOperation(new DeleteCommand(Records(recordIds))); } public Commitable toDelete(List records) { - this.operationsByType.get(OperationType.DELETE_DML).register(Records(records)); - return this; + return this.registerOperation(new DeleteCommand(Records(records))); } public OperationResult deleteImmediately(Id recordId) { - return new DeleteOperation(this.configuration).register(Record(recordId)).commitWork().get(0); + return this.executeImmediately(new DeleteCommand(Record(recordId))); } public OperationResult deleteImmediately(SObject record) { - return new DeleteOperation(this.configuration).register(Record(record)).commitWork().get(0); + return this.executeImmediately(new DeleteCommand(Record(record))); } public OperationResult deleteImmediately(Iterable recordIds) { - return new DeleteOperation(this.configuration).register(Records(recordIds)).commitWork().get(0); + return this.executeImmediately(new DeleteCommand(Records(recordIds))); } public OperationResult deleteImmediately(List records) { - return new DeleteOperation(this.configuration).register(Records(records)).commitWork().get(0); + return this.executeImmediately(new DeleteCommand(Records(records))); + } + + // Hard Delete + + public Commitable toHardDelete(Id recordId) { + return this.registerOperation(new DeleteCommand(Record(recordId)).withHardDelete()); + } + + public Commitable toHardDelete(SObject record) { + return this.toHardDelete(record.Id); + } + + public Commitable toHardDelete(Iterable recordIds) { + return this.registerOperation(new DeleteCommand(Records(recordIds)).withHardDelete()); + } + + public Commitable toHardDelete(List records) { + return this.registerOperation(new DeleteCommand(Records(records)).withHardDelete()); } // Undelete public Commitable toUndelete(Id recordId) { - this.operationsByType.get(OperationType.UNDELETE_DML).register(Record(recordId)); - return this; + return this.registerOperation(new UndeleteCommand(Record(recordId))); } public Commitable toUndelete(SObject record) { @@ -421,49 +462,74 @@ global inherited sharing class DML implements Commitable { } public Commitable toUndelete(Iterable recordIds) { - this.operationsByType.get(OperationType.UNDELETE_DML).register(Records(recordIds)); - return this; + return this.registerOperation(new UndeleteCommand(Records(recordIds))); } public Commitable toUndelete(List records) { - this.operationsByType.get(OperationType.UNDELETE_DML).register(Records(records)); - return this; + return this.registerOperation(new UndeleteCommand(Records(records))); } public OperationResult undeleteImmediately(Id recordId) { - return new UndeleteOperation(this.configuration).register(Record(recordId)).commitWork().get(0); + return this.executeImmediately(new UndeleteCommand(Record(recordId))); } public OperationResult undeleteImmediately(SObject record) { - return new UndeleteOperation(this.configuration).register(Record(record)).commitWork().get(0); + return this.executeImmediately(new UndeleteCommand(Record(record))); } public OperationResult undeleteImmediately(Iterable recordIds) { - return new UndeleteOperation(this.configuration).register(Records(recordIds)).commitWork().get(0); + return this.executeImmediately(new UndeleteCommand(Records(recordIds))); } public OperationResult undeleteImmediately(List records) { - return new UndeleteOperation(this.configuration).register(Records(records)).commitWork().get(0); + return this.executeImmediately(new UndeleteCommand(Records(records))); + } + + // Merge + + public Commitable toMerge(SObject mergeToRecord, List duplicateRecords) { + return this.registerOperation(new MergeCommand(Record(mergeToRecord), Records(duplicateRecords))); + } + + public Commitable toMerge(SObject mergeToRecord, SObject duplicatedRecord) { + return this.registerOperation(new MergeCommand(Record(mergeToRecord), Record(duplicatedRecord))); + } + + public Commitable toMerge(SObject mergeToRecord, Id duplicatedRecordId) { + return this.registerOperation(new MergeCommand(Record(mergeToRecord), Record(duplicatedRecordId))); + } + + public Commitable toMerge(SObject mergeToRecord, Iterable duplicatedRecordIds) { + return this.registerOperation(new MergeCommand(Record(mergeToRecord), Records(duplicatedRecordIds))); } // Platform Event public Commitable toPublish(SObject record) { - this.operationsByType.get(OperationType.PUBLISH_DML).register(Record(record)); - return this; + return this.registerOperation(new PlatformEventCommand(Record(record))); } public Commitable toPublish(List records) { - this.operationsByType.get(OperationType.PUBLISH_DML).register(Records(records)); - return this; + return this.registerOperation(new PlatformEventCommand(Records(records))); } public OperationResult publishImmediately(SObject record) { - return new PlatformEventOperation(this.configuration).register(Record(record)).commitWork().get(0); + return this.executeImmediately(new PlatformEventCommand(Record(record))); } public OperationResult publishImmediately(List records) { - return new PlatformEventOperation(this.configuration).register(Records(records)).commitWork().get(0); + return this.executeImmediately(new PlatformEventCommand(Records(records))); + } + + // Helpers + + private Commitable registerOperation(DmlCommand command) { + this.orchestrator.register(command); + return this; + } + + private OperationResult executeImmediately(DmlCommand command) { + return command.setGlobalConfiguration(this.configuration).commitWork(); } // Identifier @@ -477,13 +543,7 @@ global inherited sharing class DML implements Commitable { public void preview() { this.configuration.preview(); - - this.operationsByType.get(OperationType.INSERT_DML).preview(); - this.operationsByType.get(OperationType.UPDATE_DML).preview(); - this.operationsByType.get(OperationType.UPSERT_DML).preview(); - this.operationsByType.get(OperationType.DELETE_DML).preview(); - this.operationsByType.get(OperationType.UNDELETE_DML).preview(); - this.operationsByType.get(OperationType.PUBLISH_DML).preview(); + this.orchestrator.preview(); } // Field Level Security @@ -525,6 +585,11 @@ global inherited sharing class DML implements Commitable { return this; } + public Commitable combineOnDuplicate() { + this.configuration.combineOnDuplicate(); + return this; + } + public Commitable options(Database.DmlOptions options) { this.configuration.options(options); return this; @@ -548,13 +613,11 @@ global inherited sharing class DML implements Commitable { try { this.hook?.before(); - DMLResult result = this.executeOperations(); + DMLResult result = this.orchestrator.execute(); this.hook?.after(result); return result; - } catch (Exception e) { - throw e; } finally { Database.rollback(savePoint); Database.releaseSavepoint(savePoint); @@ -567,9 +630,9 @@ global inherited sharing class DML implements Commitable { try { this.hook?.before(); - result = this.executeOperations(); + result = this.orchestrator.execute(); - dmlIdentifierToResult.put(this.configuration.dmlIdentifier, result); + this.storeResult(result); this.hook?.after(result); } finally { @@ -579,6 +642,14 @@ global inherited sharing class DML implements Commitable { return result; } + private void storeResult(DMLResult result) { + if (String.isBlank(this.configuration.dmlIdentifier)) { + return; + } + + dmlIdentifierToResult.put(this.configuration.dmlIdentifier, result); + } + public Result commitTransaction() { if (!this.configuration.allOrNone) { throw new DmlException('commitTransaction() is not supported when allOrNone=false'); @@ -596,39 +667,95 @@ global inherited sharing class DML implements Commitable { } } - private DMLResult executeOperations() { - DMLResult result = new DmlResult(); + private void reset() { + this.orchestrator = new Orchestrator(this.configuration); + } - result.add(OperationType.INSERT_DML, this.operationsByType.get(OperationType.INSERT_DML).commitWork()); - result.add(OperationType.UPSERT_DML, this.operationsByType.get(OperationType.UPSERT_DML).commitWork()); - result.add(OperationType.UPDATE_DML, this.operationsByType.get(OperationType.UPDATE_DML).commitWork()); - result.add(OperationType.DELETE_DML, this.operationsByType.get(OperationType.DELETE_DML).commitWork()); - result.add(OperationType.UNDELETE_DML, this.operationsByType.get(OperationType.UNDELETE_DML).commitWork()); - result.add(OperationType.PUBLISH_DML, this.operationsByType.get(OperationType.PUBLISH_DML).commitWork()); + private class Orchestrator { + private Map commandsByHashCode = new Map(); + private Map> commandsByOperationType = new Map>(); + private Map dependencyGraphsByOperationType = new Map(); - return result; + private Configuration configuration; + + private Orchestrator(Configuration configuration) { + this.configuration = configuration; + + for (OperationType operationType : OperationType.values()) { + this.commandsByOperationType.put(operationType, new List()); + this.dependencyGraphsByOperationType.put(operationType, new OrderDependencyGraph()); + } + } + + private void register(DmlCommand command) { + DmlCommand existingCommand = this.commandsByHashCode.get(command.hashCode()); + + if (existingCommand == null) { + OrderDependencyGraph operationDependencyGraph = this.dependencyGraphsByOperationType.get(command.getOperationType()); + + command + .setGlobalConfiguration(this.configuration) + .setDependencyGraph(operationDependencyGraph) + .recalculate(); + + this.commandsByHashCode.put(command.hashCode(), command); + this.commandsByOperationType.get(command.getOperationType()).add(command); + + return; + } + + this.configuration.duplicateCombineStrategy.combine(existingCommand, command); + + existingCommand.recalculate(); + } + + private void preview() { + for (OperationType operationType : OperationType.values()) { + for (DmlCommand command : this.getSortedCommands(operationType)) { + command.preview(); + } + } + } + + private DmlResult execute() { + DmlResult result = new DmlResult(); + + for (OperationType operationType : OperationType.values()) { + for (DmlCommand command : this.getSortedCommands(operationType)) { + result.add(operationType, command.commitWork()); + } + } + + return result; + } + + private List getSortedCommands(OperationType operationType) { + List operationCommands = this.commandsByOperationType.get(operationType); + operationCommands.sort(new DmlCommandComparator(this.getSObjectTypesExecutionOrder(operationType))); + + return operationCommands; + } + + private List getSObjectTypesExecutionOrder(OperationType operationType) { + if (!this.configuration.customExecutionOrder.isEmpty()) { + return this.configuration.customExecutionOrder; + } + + return this.dependencyGraphsByOperationType.get(operationType).getTopologicalOrder(); + } } - private void reset() { - this.operationsByType.clear(); - this.initializeOperations(); - } - - public class DmlResult implements Result { - private Map> operationResultsByObjectType = new Map>{ - OperationType.INSERT_DML => new Map(), - OperationType.UPDATE_DML => new Map(), - OperationType.UPSERT_DML => new Map(), - OperationType.DELETE_DML => new Map(), - OperationType.UNDELETE_DML => new Map(), - OperationType.PUBLISH_DML => new Map() - }; - - public DmlResult add(OperationType operationType, List results) { - for (OperationResult result : results) { - this.operationResultsByObjectType.get(operationType).put(result.objectType(), result); + private class DmlResult implements Result { + private Map> operationResultsByObjectType = new Map>(); + + public DmlResult() { + for (OperationType operationType : OperationType.values()) { + this.operationResultsByObjectType.put(operationType, new Map()); } + } + public DmlResult add(OperationType operationType, OperationResult result) { + this.operationResultsByObjectType.get(operationType).put(result.objectType(), result); return this; } @@ -652,6 +779,10 @@ global inherited sharing class DML implements Commitable { return this.getOperationResults(OperationType.UNDELETE_DML); } + public List merges() { + return this.getOperationResults(OperationType.MERGE_DML); + } + public List events() { return this.getOperationResults(OperationType.PUBLISH_DML); } @@ -676,18 +807,16 @@ global inherited sharing class DML implements Commitable { return this.getOperationResult(OperationType.UNDELETE_DML, objectType); } + public OperationResult mergesOf(SObjectType objectType) { + return this.getOperationResult(OperationType.MERGE_DML, objectType); + } + public OperationResult eventsOf(SObjectType objectType) { return this.getOperationResult(OperationType.PUBLISH_DML, objectType); } private List getOperationResults(OperationType operationType) { - List operationResults = new List(); - - for (SObjectType objectType : this.operationResultsByObjectType.get(operationType).keySet()) { - operationResults.add(this.operationResultsByObjectType.get(operationType).get(objectType)); - } - - return operationResults; + return this.operationResultsByObjectType.get(operationType).values(); } private OperationResult getOperationResult(OperationType operationType, SObjectType objectType) { @@ -710,6 +839,7 @@ global inherited sharing class DML implements Commitable { private System.AccessLevel accessMode = System.AccessLevel.USER_MODE; private Database.DmlOptions options = new Database.DMLOptions(); private DmlSharing sharingExecutor = new InheritedSharing(); + private DuplicateCombineStrategy duplicateCombineStrategy = new ThrownExceptionDuplicateStrategy(); private Boolean allOrNone = true; private String dmlIdentifier = null; @@ -742,6 +872,10 @@ global inherited sharing class DML implements Commitable { this.options.optAllOrNone = false; } + public void combineOnDuplicate() { + this.duplicateCombineStrategy = new MergeDuplicateStrategy(); + } + public void options(Database.DmlOptions options) { this.options = options; this.options.optAllOrNone = this.options.optAllOrNone ?? this.allOrNone ?? true; @@ -764,324 +898,428 @@ global inherited sharing class DML implements Commitable { } } - public abstract class DmlOperation { - protected Map recordsProcessContainerByType = new Map(); - protected Configuration globalConfiguration; - protected OrderDependencyGraph orderDependencyGraph = new OrderDependencyGraph(); + private class DmlCommandComparator implements System.Comparator { + private List sobjectExecutionOrder; - public DmlOperation(Configuration configuration) { - this.globalConfiguration = configuration; + public DmlCommandComparator(List sobjectExecutionOrder) { + this.sobjectExecutionOrder = sobjectExecutionOrder; + } + + public Integer compare(DmlCommand a, DmlCommand b) { + return this.sobjectExecutionOrder.indexOf(a.getObjectType()) - this.sobjectExecutionOrder.indexOf(b.getObjectType()); + } + } + + private abstract class DmlCommand { + private SObjectType objectType; + private Configuration globalConfiguration; + private OrderDependencyGraph orderDependencyGraph; + private List enhancedRecords = new List(); + + public DmlCommand(Record record) { + this.register(record); } - public DmlOperation register(Records records) { + public DmlCommand(Records records) { for (Record record : records.get()) { this.register(record); } - - return this; } - public DmlOperation register(Record record) { + private void register(Record record) { EnhancedRecord enhancedRecord = record.get(); - SObjectType typeName = enhancedRecord.getSObjectType(); - - this.validate(record); - this.validateCustomExecutionOrder(typeName); + this.setObjectType(enhancedRecord); - this.addNewRecordToProcess(typeName, enhancedRecord); - - enhancedRecord.addToDependencyGraph(this.orderDependencyGraph); - - return this; + this.enhancedRecords.add(enhancedRecord); } - private void validateCustomExecutionOrder(SObjectType typeName) { - if (!this.globalConfiguration.customExecutionOrder.isEmpty() && !this.globalConfiguration.customExecutionOrder.contains(typeName)) { - throw new DmlException('Only the following types can be registered: ' + this.globalConfiguration.customExecutionOrder); + private void setObjectType(EnhancedRecord enhancedRecord) { + if (this.objectType == null) { + this.objectType = enhancedRecord.getSObjectType(); } } - private void addNewRecordToProcess(SObjectType typeName, EnhancedRecord enhancedRecord) { - if (!this.recordsProcessContainerByType.containsKey(typeName)) { - this.recordsProcessContainerByType.put(typeName, new RecordsContainer()); - } + private DmlCommand setGlobalConfiguration(Configuration globalConfiguration) { + this.globalConfiguration = globalConfiguration; + return this; + } - this.recordsProcessContainerByType.get(typeName).addNewRecordToProcess(enhancedRecord); + private DmlCommand setDependencyGraph(OrderDependencyGraph orderDependencyGraph) { + this.orderDependencyGraph = orderDependencyGraph; + return this; } - public void preview() { + private void preview() { System.debug(LoggingLevel.ERROR, - '\n\n============ ' + this.getType() + ' Preview ============' - + '\nRecords Process Container: ' + JSON.serializePretty(this.recordsProcessContainerByType) + '\n\n============ ' + this.getOperationType() + ' Preview ============' + + '\nObject Type: ' + this.objectType + + '\nRecords: ' + JSON.serializePretty(this.enhancedRecords) + '\n=======================================\n' ); } - public List commitWork() { - return this.globalConfiguration.sharingExecutor.execute(this); - } + private DmlCommand recalculate() { + this.validateCustomExecutionOrder(); - public List execute() { - if (!this.globalConfiguration.customExecutionOrder.isEmpty()) { - return this.process(this.globalConfiguration.customExecutionOrder); - } + for (EnhancedRecord enhancedRecord : this.enhancedRecords) { + this.validate(enhancedRecord); + enhancedRecord.addToDependencyGraph(this.orderDependencyGraph); + } - return this.process(this.orderDependencyGraph.getTopologicalOrder()); + return this; } - private List process(Iterable objectTypesOrder) { - List operationResults = new List(); + private void validateCustomExecutionOrder() { + if (this.globalConfiguration.customExecutionOrder.isEmpty()) { + return; + } - for (SObjectType objectTypeName : objectTypesOrder) { - if (!this.recordsProcessContainerByType.containsKey(objectTypeName)) { - continue; - } + if (!this.globalConfiguration.customExecutionOrder.contains(this.getObjectType())) { + throw new DmlException('Only the following types can be registered: ' + this.globalConfiguration.customExecutionOrder); + } + } - List recordsToProcess = this.recordsProcessContainerByType.get(objectTypeName).getRecordsToProcess(); + private SObjectType getObjectType() { + return this.objectType; + } - DmlMock dmlMock = dmlIdentifierToMock.get(this.globalConfiguration.dmlIdentifier); + private OperationResult commitWork() { + return this.globalConfiguration.sharingExecutor.execute(this); + } + + private virtual OperationResult execute() { + List recordsToProcess = this.getRecordsToProcess(); - List recordResults; + OperationSummary operationSummary = new OperationSummary(this.objectType, this.getOperationType()).setRecords(recordsToProcess); - if (dmlMock != null && dmlMock.shouldBeMocked(this.getType(), objectTypeName)) { - recordResults = this.getMockedRecordResults(recordsToProcess); - } else { - recordResults = getAdapter().getStandardizedResults(this.executeDml(recordsToProcess), recordsToProcess); - } + DmlMock dmlMock = dmlIdentifierToMock.get(this.globalConfiguration.dmlIdentifier); - operationResults.add( - new OperationSummary(objectTypeName, this.getType()) - .setRecords(recordsToProcess) - .setRecordResults(recordResults) - ); + if (dmlMock != null && dmlMock.shouldBeMocked(this.getOperationType(), this.objectType)) { + return operationSummary.setRecordResults(dmlMock.getMockedRecordResults(this, recordsToProcess)); } - return operationResults; + return operationSummary.setRecordResults(this.getAdapter().get(this.executeDml(recordsToProcess), recordsToProcess)); } - private List getMockedRecordResults(List recordsToProcess) { - List recordResults = new List(); + private List getRecordsToProcess() { + List recordsToProcess = new List(); - for (SObject record : recordsToProcess) { - recordResults.add(this.prepareMockedDml(record)); + for (EnhancedRecord enhancedRecord : this.enhancedRecords) { + enhancedRecord.resolveRecordRelationships(); + recordsToProcess.add(enhancedRecord.getRecord()); } - return recordResults; + return recordsToProcess; } - public abstract OperationType getType(); - public abstract List executeDml(List recordsToProcess); - public abstract RecordResult prepareMockedDml(SObject record); - public abstract DmlResultAdapter getAdapter(); + protected abstract OperationType getOperationType(); + protected abstract DmlResultAdapter getAdapter(); + protected abstract List executeDml(List recordsToProcess); + protected abstract RecordSummary executeMockedDml(SObject record); - public virtual void validate(Record record) { + protected virtual void validate(EnhancedRecord enhancedRecord) { return; } - } - private class RecordsContainer { - private List enhancedRecords = new List(); - - public void addNewRecordToProcess(EnhancedRecord enhancedRecord) { - this.enhancedRecords.add(enhancedRecord); + protected virtual String hashCode() { + return this.getOperationType() + '_' + this.objectType; } + } - public List getRecordsToProcess() { - List recordsToProcess = new List(); + // Commands - for (EnhancedRecord enhancedRecord : this.enhancedRecords) { - enhancedRecord.resolveRecordRelationships(); - recordsToProcess.add(enhancedRecord.getRecord()); - } - - return recordsToProcess; + private inherited sharing class InsertCommand extends DmlCommand { + public InsertCommand(Record record) { + super(record); } - } - public inherited sharing class InsertOperation extends DmlOperation { - public InsertOperation(Configuration configuration) { - super(configuration); + public InsertCommand(Records records) { + super(records); } - public override OperationType getType() { + protected override OperationType getOperationType() { return OperationType.INSERT_DML; } - public override void validate(Record record) { - if (record.get().doesRecordHaveIdSpecified()) { + public override DmlResultAdapter getAdapter() { + return new SaveResultAdapter(); + } + + protected override void validate(EnhancedRecord enhancedRecord) { + if (enhancedRecord.hasId()) { throw new DmlException('Only new records can be registered as new.'); } } - public override List executeDml(List recordsToProcess) { + protected override List executeDml(List recordsToProcess) { return Database.insert(recordsToProcess, this.globalConfiguration.options, this.globalConfiguration.accessMode); } - public override RecordResult prepareMockedDml(SObject record) { + protected override RecordSummary executeMockedDml(SObject record) { record.put('Id', randomIdGenerator.get(record.getSObjectType())); return new RecordSummary().isSuccess(true).recordId(record.Id); } + } - public override DmlResultAdapter getAdapter() { - return new SaveResultAdapter(); + private inherited sharing class UpsertCommand extends DmlCommand { + private SObjectField externalIdField; + + public UpsertCommand(Record record) { + super(record); + } + + public UpsertCommand(Records records) { + super(records); + } + + private UpsertCommand withExternalIdField(SObjectField externalIdField) { + this.externalIdField = externalIdField; + return this; + } + + protected override String hashCode() { + String externalId = this.externalIdField == null ? 'Id' : this.externalIdField.toString(); + return super.hashCode() + '_' + externalId; + } + + protected override OperationType getOperationType() { + return OperationType.UPSERT_DML; + } + + protected override DmlResultAdapter getAdapter() { + return new UpsertResultAdapter(); + } + + protected override List executeDml(List recordsToProcess) { + if (this.externalIdField == null) { + return Database.upsert(recordsToProcess, this.globalConfiguration.allOrNone, this.globalConfiguration.accessMode); + } + + return Database.upsert(recordsToProcess, this.externalIdField, this.globalConfiguration.allOrNone, this.globalConfiguration.accessMode); + } + + protected override RecordSummary executeMockedDml(SObject record) { + if (record.Id == null) { + record.put('Id', randomIdGenerator.get(record.getSObjectType())); + } + return new RecordSummary().isSuccess(true).recordId(record.Id); } } - public inherited sharing class UpdateOperation extends DmlOperation { - public UpdateOperation(Configuration configuration) { - super(configuration); + private inherited sharing class UpdateCommand extends DmlCommand { + public UpdateCommand(Record record) { + super(record); + } + + public UpdateCommand(Records records) { + super(records); } - public override OperationType getType() { + protected override OperationType getOperationType() { return OperationType.UPDATE_DML; } - public override void validate(Record record) { - if (!record.get().doesRecordHaveIdSpecified()) { + protected override DmlResultAdapter getAdapter() { + return new SaveResultAdapter(); + } + + protected override void validate(EnhancedRecord enhancedRecord) { + if (!enhancedRecord.hasId()) { throw new DmlException('Only existing records can be updated.'); } } - public override List executeDml(List recordsToProcess) { + protected override List executeDml(List recordsToProcess) { return Database.update(recordsToProcess, this.globalConfiguration.options, this.globalConfiguration.accessMode); } - public override RecordResult prepareMockedDml(SObject record) { + protected override RecordSummary executeMockedDml(SObject record) { return new RecordSummary().isSuccess(true).recordId(record.Id); } + } - public override DmlResultAdapter getAdapter() { - return new SaveResultAdapter(); + private inherited sharing class MergeCommand extends DmlCommand { + private List duplicateRecords = new List(); + + public MergeCommand(Record mergeToRecord, Record duplicateRecord) { + super(mergeToRecord); + + this.duplicateRecords.add(duplicateRecord.get().getRecord()); } - } - public inherited sharing class UpsertOperation extends DmlOperation { - public UpsertOperation(Configuration configuration) { - super(configuration); + public MergeCommand(Record mergeToRecord, Records duplicateRecords) { + super(mergeToRecord); + + for (Record duplicateRecord : duplicateRecords.get()) { + this.duplicateRecords.add(duplicateRecord.get().getRecord()); + } } - public override OperationType getType() { - return OperationType.UPSERT_DML; + protected override String hashCode() { + return super.hashCode() + '_' + this.enhancedRecords[0].getRecordId(); } - public override List executeDml(List recordsToProcess) { - return Database.upsert(recordsToProcess, this.globalConfiguration.allOrNone, this.globalConfiguration.accessMode); + protected override OperationType getOperationType() { + return OperationType.MERGE_DML; } - public override RecordResult prepareMockedDml(SObject record) { - if (record.Id == null) { - record.put('Id', randomIdGenerator.get(record.getSObjectType())); + protected override DmlResultAdapter getAdapter() { + return new MergeResultAdapter(); + } + + protected override void validate(EnhancedRecord enhancedRecord) { + if (!enhancedRecord.hasId()) { + throw new DmlException('Only existing records can be merged.'); } - return new RecordSummary().isSuccess(true).recordId(record.Id); } - public override DmlResultAdapter getAdapter() { - return new UpsertResultAdapter(); + protected override List executeDml(List recordsToProcess) { + return Database.merge(recordsToProcess[0], this.duplicateRecords, this.globalConfiguration.allOrNone, this.globalConfiguration.accessMode); + } + + protected override RecordSummary executeMockedDml(SObject record) { + if (this.duplicateRecords.isEmpty()) { + throw new DmlException('A single merge request must contain at least one record to merge, got none.'); + } + return new RecordSummary().isSuccess(true).recordId(record.Id); } } - public inherited sharing class DeleteOperation extends DmlOperation { - public DeleteOperation(Configuration configuration) { - super(configuration); + private inherited sharing class DeleteCommand extends DmlCommand { + private Boolean makeHardDelete = false; + + public DeleteCommand(Record record) { + super(record); } - public override OperationType getType() { + public DeleteCommand(Records records) { + super(records); + } + + private DeleteCommand withHardDelete() { + this.makeHardDelete = true; + return this; + } + + protected override String hashCode() { + return super.hashCode() + '_' + this.makeHardDelete; + } + + protected override OperationType getOperationType() { return OperationType.DELETE_DML; } - public override void validate(Record record) { - if (!record.get().doesRecordHaveIdSpecified()) { + protected override DmlResultAdapter getAdapter() { + return new DeleteResultAdapter(); + } + + protected override void validate(EnhancedRecord enhancedRecord) { + if (!enhancedRecord.hasId()) { throw new DmlException('Only existing records can be registered as deleted.'); } } - public override List executeDml(List recordsToProcess) { - return Database.delete(recordsToProcess, this.globalConfiguration.allOrNone, this.globalConfiguration.accessMode); + protected override List executeDml(List recordsToProcess) { + List dmlResults = Database.delete(recordsToProcess, this.globalConfiguration.allOrNone, this.globalConfiguration.accessMode); + + if (this.makeHardDelete) { + System.Database.emptyRecycleBin(recordsToProcess); + } + + return dmlResults; } - public override RecordResult prepareMockedDml(SObject record) { + protected override RecordSummary executeMockedDml(SObject record) { return new RecordSummary().isSuccess(true).recordId(record.Id); } + } - public override DmlResultAdapter getAdapter() { - return new DeleteResultAdapter(); + private inherited sharing class UndeleteCommand extends DmlCommand { + public UndeleteCommand(Record record) { + super(record); } - } - public inherited sharing class UndeleteOperation extends DmlOperation { - public UndeleteOperation(Configuration configuration) { - super(configuration); + public UndeleteCommand(Records records) { + super(records); } - public override OperationType getType() { + protected override OperationType getOperationType() { return OperationType.UNDELETE_DML; } - public override void validate(Record record) { - if (!record.get().doesRecordHaveIdSpecified()) { + protected override DmlResultAdapter getAdapter() { + return new UndeleteResultAdapter(); + } + + protected override void validate(EnhancedRecord enhancedRecord) { + if (!enhancedRecord.hasId()) { throw new DmlException('Only deleted records can be undeleted.'); } } - public override List executeDml(List recordsToProcess) { + protected override List executeDml(List recordsToProcess) { return Database.undelete(recordsToProcess, this.globalConfiguration.allOrNone, this.globalConfiguration.accessMode); } - public override RecordResult prepareMockedDml(SObject record) { + protected override RecordSummary executeMockedDml(SObject record) { return new RecordSummary().isSuccess(true).recordId(record.Id); } + } - public override DmlResultAdapter getAdapter() { - return new UndeleteResultAdapter(); + private inherited sharing class PlatformEventCommand extends DmlCommand { + public PlatformEventCommand(Record record) { + super(record); } - } - public inherited sharing class PlatformEventOperation extends DmlOperation { - public PlatformEventOperation(Configuration configuration) { - super(configuration); + public PlatformEventCommand(Records records) { + super(records); } - public override OperationType getType() { + protected override OperationType getOperationType() { return OperationType.PUBLISH_DML; } - public override List executeDml(List recordsToProcess) { - return EventBus.publish(recordsToProcess); + protected override DmlResultAdapter getAdapter() { + return new SaveResultAdapter(); } - public override RecordResult prepareMockedDml(SObject record) { - return new RecordSummary().isSuccess(true).recordId(randomIdGenerator.get(record.getSObjectType())); + protected override List executeDml(List recordsToProcess) { + return EventBus.publish(recordsToProcess); } - public override DmlResultAdapter getAdapter() { - return new SaveResultAdapter(); + protected override RecordSummary executeMockedDml(SObject record) { + return new RecordSummary().isSuccess(true).recordId(randomIdGenerator.get(record.getSObjectType())); } } - + + // Issuers + private interface DmlSharing { - List execute(DmlOperation executor); + OperationResult execute(DmlCommand command); } private inherited sharing class InheritedSharing implements DmlSharing { - public List execute(DmlOperation executor) { - return executor.execute(); + public OperationResult execute(DmlCommand command) { + return command.execute(); } } private without sharing class WithoutSharing implements DmlSharing { - public List execute(DmlOperation executor) { - return executor.execute(); + public OperationResult execute(DmlCommand command) { + return command.execute(); } } private with sharing class WithSharing implements DmlSharing { - public List execute(DmlOperation executor) { - return executor.execute(); + public OperationResult execute(DmlCommand command) { + return command.execute(); } } + // Result Adapters + private abstract class DmlResultAdapter { - public List getStandardizedResults(List dmlResults, List processedRecords) { + public List get(List dmlResults, List processedRecords) { List recordResults = new List(); for (Integer i = 0; i < dmlResults.size(); i++) { @@ -1116,6 +1354,17 @@ global inherited sharing class DML implements Commitable { } } + private class MergeResultAdapter extends DmlResultAdapter { + public override RecordSummary transform(Object result) { + Database.MergeResult mergeResult = (Database.MergeResult) result; + + return new RecordSummary() + .isSuccess(mergeResult.isSuccess()) + .recordId(mergeResult.getId()) + .errors(mergeResult.getErrors()); + } + } + private class DeleteResultAdapter extends DmlResultAdapter { public override RecordSummary transform(Object result) { Database.DeleteResult deleteResult = (Database.DeleteResult) result; @@ -1130,13 +1379,48 @@ global inherited sharing class DML implements Commitable { private class UndeleteResultAdapter extends DmlResultAdapter { public override RecordSummary transform(Object result) { Database.UndeleteResult undeleteResult = (Database.UndeleteResult) result; - + return new RecordSummary() .isSuccess(undeleteResult.isSuccess()) .recordId(undeleteResult.getId()) .errors(undeleteResult.getErrors()); } } + + private interface DuplicateCombineStrategy { + void combine(DmlCommand mergeToCommand, DmlCommand duplicateCommand); + } + + private class ThrownExceptionDuplicateStrategy implements DuplicateCombineStrategy { + public void combine(DmlCommand mergeToCommand, DmlCommand duplicateCommand) { + for (EnhancedRecord newEnhancedRecord : duplicateCommand.enhancedRecords) { + if (mergeToCommand.enhancedRecords.contains(newEnhancedRecord)) { + throw new DmlException('Duplicate records found during registration. Fix the code or use the combineOnDuplicate() method.'); + } + } + + mergeToCommand.enhancedRecords.addAll(duplicateCommand.enhancedRecords); + } + } + + private class MergeDuplicateStrategy implements DuplicateCombineStrategy { + public void combine(DmlCommand mergeToCommand, DmlCommand duplicateCommand) { + for (EnhancedRecord newEnhancedRecord : duplicateCommand.enhancedRecords) { + if (mergeToCommand.enhancedRecords.contains(newEnhancedRecord)) { + Integer index = mergeToCommand.enhancedRecords.indexOf(newEnhancedRecord); + + SObject existingRecord = mergeToCommand.enhancedRecords[index].getRecord(); + SObject newRecord = newEnhancedRecord.getRecord(); + + for (String field : newRecord.getPopulatedFieldsAsMap().keySet()) { + existingRecord.put(field, newRecord.get(field)); + } + } else { + mergeToCommand.enhancedRecords.add(newEnhancedRecord); + } + } + } + } private class OperationSummary implements OperationResult { private OperationType type; @@ -1144,8 +1428,9 @@ global inherited sharing class DML implements Commitable { private List recordResults = new List(); private List records = new List(); - private List cachedSuccesses; - private List cachedFailures; + private List cachedSuccesses = new List(); + private List cachedFailures = new List(); + private List cachedErrors = new List(); private OperationSummary(SObjectType objectType, OperationType type) { this.objectType = objectType; @@ -1159,9 +1444,21 @@ global inherited sharing class DML implements Commitable { private OperationSummary setRecordResults(List recordResults) { this.recordResults = recordResults; + this.flatResults(); return this; } + private void flatResults() { + for (RecordResult recordResult : this.recordResults) { + if (recordResult.isSuccess()) { + this.cachedSuccesses.add(recordResult.record()); + } else { + this.cachedErrors.addAll(recordResult.errors()); + this.cachedFailures.add(recordResult.record()); + } + } + } + public SObjectType objectType() { return this.objectType; } @@ -1175,13 +1472,7 @@ global inherited sharing class DML implements Commitable { } public List errors() { - List errors = new List(); - - for (RecordResult recordResult : this.recordResults) { - errors.addAll(recordResult.errors()); - } - - return errors; + return this.cachedErrors; } public List records() { @@ -1189,34 +1480,10 @@ global inherited sharing class DML implements Commitable { } public List successes() { - if (this.cachedSuccesses != null) { - return this.cachedSuccesses; - } - - this.cachedSuccesses = new List(); - - for (RecordResult recordResult : this.recordResults) { - if (recordResult.isSuccess()) { - this.cachedSuccesses.add(recordResult.record()); - } - } - return this.cachedSuccesses; } public List failures() { - if (this.cachedFailures != null) { - return this.cachedFailures; - } - - this.cachedFailures = new List(); - - for (RecordResult recordResult : this.recordResults) { - if (!recordResult.isSuccess()) { - this.cachedFailures.add(recordResult.record()); - } - } - return this.cachedFailures; } @@ -1247,23 +1514,28 @@ global inherited sharing class DML implements Commitable { return this.errors; } - public RecordSummary recordId(Id recordId) { + private RecordSummary recordId(Id recordId) { this.recordId = recordId; return this; } - public RecordSummary record(SObject record) { + private RecordSummary record(SObject record) { this.record = record; return this; } @SuppressWarnings('PMD.AvoidBooleanMethodParameters') - public RecordSummary isSuccess(Boolean isSuccess) { + private RecordSummary isSuccess(Boolean isSuccess) { this.isSuccess = isSuccess; return this; } - public RecordSummary errors(List errors) { + private RecordSummary error(Error error) { + this.errors.add(error); + return this; + } + + private RecordSummary errors(List errors) { for (Database.Error error : errors ?? new List()) { this.errors.add(new RecordProcessingError(error)); } @@ -1276,12 +1548,29 @@ global inherited sharing class DML implements Commitable { private System.StatusCode statusCode; private List fields; + private RecordProcessingError() {} + private RecordProcessingError(Database.Error error) { this.message = error.getMessage(); this.statusCode = error.getStatusCode(); this.fields = error.getFields(); } + private RecordProcessingError setMessage(String message) { + this.message = message; + return this; + } + + private RecordProcessingError setStatusCode(System.StatusCode statusCode) { + this.statusCode = statusCode; + return this; + } + + private RecordProcessingError setFields(List fields) { + this.fields = fields; + return this; + } + public String message() { return this.message; } @@ -1297,24 +1586,15 @@ global inherited sharing class DML implements Commitable { private class DmlMock implements Mockable { private Set mockedDmlTypes = new Set(); - private Map> mockedObjectTypesByDmlType = new Map>{ - OperationType.INSERT_DML => new Set(), - OperationType.UPDATE_DML => new Set(), - OperationType.UPSERT_DML => new Set(), - OperationType.DELETE_DML => new Set(), - OperationType.UNDELETE_DML => new Set(), - OperationType.PUBLISH_DML => new Set() - }; + private Set thrownExceptionDmlTypes = new Set(); + + private Map> mockedObjectTypesByDmlType = new Map>(); + private Map> thrownExceptionDmlTypesByObjectTypes = new Map>(); public Mockable allDmls() { - this.allInserts(); - this.allUpdates(); - this.allUpserts(); - this.allDeletes(); - this.allUndeletes(); - this.allPublishes(); + this.mockedDmlTypes.addAll(OperationType.values()); return this; - } + } public Mockable allInserts() { return this.thenMockDml(OperationType.INSERT_DML); @@ -1336,6 +1616,10 @@ global inherited sharing class DML implements Commitable { return this.thenMockDml(OperationType.UNDELETE_DML); } + public Mockable allMerges() { + return this.thenMockDml(OperationType.MERGE_DML); + } + public Mockable allPublishes() { return this.thenMockDml(OperationType.PUBLISH_DML); } @@ -1365,17 +1649,140 @@ global inherited sharing class DML implements Commitable { return this.thenMockDmlFor(OperationType.UNDELETE_DML, objectType); } + public Mockable mergesFor(SObjectType objectType) { + return this.thenMockDmlFor(OperationType.MERGE_DML, objectType); + } + public Mockable publishesFor(SObjectType objectType) { return this.thenMockDmlFor(OperationType.PUBLISH_DML, objectType); } private Mockable thenMockDmlFor(OperationType dmlType, SObjectType objectType) { + if (!this.mockedObjectTypesByDmlType.containsKey(dmlType)) { + this.mockedObjectTypesByDmlType.put(dmlType, new Set()); + } this.mockedObjectTypesByDmlType.get(dmlType).add(objectType); return this; } + public Mockable exceptionOnInserts() { + return this.thenExceptionOn(OperationType.INSERT_DML); + } + + public Mockable exceptionOnUpdates() { + return this.thenExceptionOn(OperationType.UPDATE_DML); + } + + public Mockable exceptionOnUpserts() { + return this.thenExceptionOn(OperationType.UPSERT_DML); + } + + public Mockable exceptionOnDeletes() { + return this.thenExceptionOn(OperationType.DELETE_DML); + } + + public Mockable exceptionOnUndeletes() { + return this.thenExceptionOn(OperationType.UNDELETE_DML); + } + + public Mockable exceptionOnMerges() { + return this.thenExceptionOn(OperationType.MERGE_DML); + } + + public Mockable exceptionOnPublishes() { + return this.thenExceptionOn(OperationType.PUBLISH_DML); + } + + private Mockable thenExceptionOn(OperationType dmlType) { + this.thrownExceptionDmlTypes.add(dmlType); + return this; + } + + public Mockable exceptionOnInsertsFor(SObjectType objectType) { + return this.thenExceptionOnFor(OperationType.INSERT_DML, objectType); + } + + public Mockable exceptionOnUpdatesFor(SObjectType objectType) { + return this.thenExceptionOnFor(OperationType.UPDATE_DML, objectType); + } + + public Mockable exceptionOnUpsertsFor(SObjectType objectType) { + return this.thenExceptionOnFor(OperationType.UPSERT_DML, objectType); + } + + public Mockable exceptionOnDeletesFor(SObjectType objectType) { + return this.thenExceptionOnFor(OperationType.DELETE_DML, objectType); + } + + public Mockable exceptionOnUndeletesFor(SObjectType objectType) { + return this.thenExceptionOnFor(OperationType.UNDELETE_DML, objectType); + } + + public Mockable exceptionOnMergesFor(SObjectType objectType) { + return this.thenExceptionOnFor(OperationType.MERGE_DML, objectType); + } + + public Mockable exceptionOnPublishesFor(SObjectType objectType) { + return this.thenExceptionOnFor(OperationType.PUBLISH_DML, objectType); + } + + private Mockable thenExceptionOnFor(OperationType dmlType, SObjectType objectType) { + if (!this.thrownExceptionDmlTypesByObjectTypes.containsKey(dmlType)) { + this.thrownExceptionDmlTypesByObjectTypes.put(dmlType, new Set()); + } + this.thrownExceptionDmlTypesByObjectTypes.get(dmlType).add(objectType); + return this; + } + private Boolean shouldBeMocked(OperationType dmlType, SObjectType objectType) { - return this.mockedDmlTypes.contains(dmlType) || this.mockedObjectTypesByDmlType.get(dmlType).contains(objectType); + return this.mockedDmlTypes.contains(dmlType) || (this.mockedObjectTypesByDmlType.get(dmlType) ?? new Set()).contains(objectType) || this.shouldThrowException(dmlType, objectType); + } + + private List getMockedRecordResults(DmlCommand command, List recordsToProcess) { + if (this.shouldThrowException(command.getOperationType(), command.getObjectType())) { + if (command.globalConfiguration.allOrNone) { + throw new DmlException('Exception thrown for ' + command.getOperationType() + ' operation.'); + } + + // all or none is false, so we need to return a list of record results with errors + return this.getMockedRecordErrors(command, recordsToProcess); + } + + return this.getMockedRecordSuccesses(command, recordsToProcess); + } + + private List getMockedRecordErrors(DmlCommand command, List recordsToProcess) { + List recordResults = new List(); + String errorMessage = 'Exception thrown for ' + command.getOperationType() + ' operation.'; + + for (SObject record : recordsToProcess) { + RecordSummary recordSummary = new RecordSummary() + .isSuccess(false) + .error( + new RecordProcessingError() + .setMessage(errorMessage) + .setStatusCode(System.StatusCode.ALREADY_IN_PROCESS) + .setFields(new List { 'Id' }) + ); + + recordResults.add(recordSummary); + } + + return recordResults; + } + + private List getMockedRecordSuccesses(DmlCommand command, List recordsToProcess) { + List recordResults = new List(); + + for (SObject record : recordsToProcess) { + recordResults.add(command.executeMockedDml(record)); + } + + return recordResults; + } + + private Boolean shouldThrowException(OperationType dmlType, SObjectType objectType) { + return this.thrownExceptionDmlTypes.contains(dmlType) || (this.thrownExceptionDmlTypesByObjectTypes.get(dmlType) ?? new Set()).contains(objectType); } } @@ -1391,10 +1798,8 @@ global inherited sharing class DML implements Commitable { } private DmlRecords(Iterable recordIds) { - SObjectType objectType = recordIds.iterator().next()?.getSObjectType(); - for (Id recordId : recordIds) { - this.recordsToProcess.add(objectType.newSObject(recordId)); + this.recordsToProcess.add(recordId.getSObjectType().newSObject(recordId)); } } @@ -1491,11 +1896,7 @@ global inherited sharing class DML implements Commitable { } private EnhancedRecord(Id currentRecordId) { - if (currentRecordId == null) { - return; - } - - this.currentRecord = currentRecordId.getSObjectType().newSObject(currentRecordId); + this.currentRecord = currentRecordId?.getSObjectType()?.newSObject(currentRecordId); } private void with(SObjectField field, Object value) { @@ -1511,6 +1912,10 @@ global inherited sharing class DML implements Commitable { } private void addToDependencyGraph(OrderDependencyGraph orderDependencyGraph) { + if (orderDependencyGraph == null) { + return; + } + orderDependencyGraph.registerType(this.getSObjectType()); for (ParentRelationship parentRelationship : this.parentRelationships) { @@ -1519,17 +1924,9 @@ global inherited sharing class DML implements Commitable { } private void resolveRecordRelationships() { - this.resolveRelationships(); - this.resolveExternalRelationships(); - } - - private void resolveRelationships() { for (ParentRelationship parentRelationship : this.parentRelationships) { parentRelationship.resolve(this.currentRecord); } - } - - private void resolveExternalRelationships() { for (ExternalRelationship externalRelationship : this.externalRelationships) { externalRelationship.resolve(this.currentRecord); } @@ -1539,17 +1936,25 @@ global inherited sharing class DML implements Commitable { return this.currentRecord?.getSObjectType(); } - private Boolean doesRecordHaveIdSpecified() { - return String.isNotBlank(this.getRecordId()); - } - private SObject getRecord() { return this.currentRecord; } + private Boolean hasId() { + return this.currentRecord?.Id != null; + } + private Id getRecordId() { return this.currentRecord?.Id; } + + public Boolean equals(Object other) { + if (this.getRecordId() == null) { + return false; + } + + return this.getRecordId() == ((EnhancedRecord) other)?.getRecordId(); + } } private class ParentRelationship { @@ -1608,10 +2013,8 @@ global inherited sharing class DML implements Commitable { } private void validateExternalIdField(SObjectField relationshipField, SObjectField externalIdField) { - Boolean externalIdFieldIsValid = externalIdField.getDescribe().isExternalId(); - - if (!externalIdFieldIsValid) { - throw new DmlException('Invalid argument: externalIdField. Field supplied is not a marked as an External Identifier.'); + if (!externalIdField.getDescribe().isExternalId()) { + throw new DmlException('Invalid argument: externalIdField. Field supplied is not marked as an External Identifier.'); } SObjectType relatedObjectType = relationshipField.getDescribe().getReferenceTo()[0]; @@ -1629,11 +2032,11 @@ global inherited sharing class DML implements Commitable { private Map> childrenByType = new Map>(); private Map parentsRemainingByType = new Map(); - public void registerType(SObjectType type) { + private void registerType(SObjectType type) { this.ensureTypeRegistered(type); } - public void addDependency(SObjectType parentType, SObjectType childType) { + private void addDependency(SObjectType parentType, SObjectType childType) { this.ensureTypeRegistered(parentType); this.ensureTypeRegistered(childType); @@ -1652,15 +2055,9 @@ global inherited sharing class DML implements Commitable { } // Kahn’s sort algorithm - public List getTopologicalOrder() { + private List getTopologicalOrder() { + List parentsTypesWithoutDependencies = this.getTypesWithNoDependencies(); List typesExecutionOrder = new List(); - List parentsTypesWithoutDependencies = new List(); - - for (SObjectType type : this.parentsRemainingByType.keySet()) { - if (this.parentsRemainingByType.get(type) == 0) { - parentsTypesWithoutDependencies.add(type); - } - } while (!parentsTypesWithoutDependencies.isEmpty()) { SObjectType currentType = parentsTypesWithoutDependencies.remove(0); @@ -1678,14 +2075,27 @@ global inherited sharing class DML implements Commitable { } if (typesExecutionOrder.size() < this.parentsRemainingByType.size()) { - throw new DmlException('Cyclic type dependencies detected during rgistration graph.'); + throw new DmlException('Cyclic type dependencies detected during registration graph.'); } return typesExecutionOrder; } + + private List getTypesWithNoDependencies() { + List typesWithNoDependencies = new List(); + + for (SObjectType type : this.parentsRemainingByType.keySet()) { + if (this.parentsRemainingByType.get(type) == 0) { + typesWithNoDependencies.add(type); + } + } + + return typesWithNoDependencies; + } } - public class RandomIdGenerator { + @TestVisible + private class RandomIdGenerator { public Id get(SObjectType objectType) { return get(objectType.getDescribe().getKeyPrefix()); } diff --git a/force-app/main/default/classes/DML_Test.cls b/force-app/main/default/classes/DML_Test.cls index e4165e5..83b30b9 100644 --- a/force-app/main/default/classes/DML_Test.cls +++ b/force-app/main/default/classes/DML_Test.cls @@ -2,34 +2,34 @@ * Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev) * Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/dml-lib/blob/main/LICENSE) * - * v1.9.0 + * v2.0.0 * * PMD False Positives: * - CyclomaticComplexity: It is a library and we tried to put everything into ONE class * - CognitiveComplexity: It is a library and we tried to put everything into ONE class * - ApexUnitTestClassShouldHaveRunAs: System.runAs is used to test fls and sharing modes - * - NcssMethodCount: Some methods are longer becasue of amount of assertions + * - NcssMethodCount: Some methods are longer because of amount of assertions * - NcssTypeCount: It is a library and we tried to put everything into ONE class **/ @SuppressWarnings('PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.ApexUnitTestClassShouldHaveRunAs,PMD.NcssMethodCount,PMD.NcssTypeCount') @IsTest private class DML_Test { - // INSERT + // INSERT - @IsTest - static void toInsertSingleRecord() { - // Setup + @IsTest + static void toInsertSingleRecord() { + // Setup Account account1 = getAccount(1); - // Test - Test.startTest(); + // Test + Test.startTest(); DML.Result result = new DML() .toInsert(account1) - .commitWork(); - Test.stopTest(); + .commitWork(); + Test.stopTest(); - // Verify + // Verify Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Single record should be inserted.'); Assert.isNotNull(account1.Id, 'Account should be inserted and have an Id.'); @@ -41,7 +41,6 @@ private class DML_Test { Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); - DML.OperationResult operationResult = result.insertsOf(Account.SObjectType); Assert.areEqual(1, operationResult.records().size(), 'Inserted operation result should contain the inserted record.'); @@ -49,7 +48,7 @@ private class DML_Test { Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Inserted operation result should contain Account object type.'); Assert.areEqual(DML.OperationType.INSERT_DML, operationResult.operationType(), 'Inserted operation result should contain insert type.'); Assert.isFalse(operationResult.hasFailures(), 'Inserted operation result should not have failures.'); - } + } @IsTest static void insertImmediatelySingleRecord() { @@ -69,41 +68,41 @@ private class DML_Test { Assert.areEqual(Account.SObjectType, result.objectType(), 'Inserted operation result should contain Account object type.'); Assert.areEqual(DML.OperationType.INSERT_DML, result.operationType(), 'Inserted operation result should contain insert type.'); Assert.isFalse(result.hasFailures(), 'Inserted operation result should not have failures.'); - } + } - @IsTest - static void toInsertWithRelationshipSingleRecord() { - // Setup + @IsTest + static void toInsertWithRelationshipSingleRecord() { + // Setup Account newAccount = getAccount(1); Contact newContact = getContact(1); - // Test - Test.startTest(); - new DML() - .toInsert(newAccount) - .toInsert(DML.Record(newContact).withRelationship(Contact.AccountId, newAccount)) - .commitWork(); - Test.stopTest(); + // Test + Test.startTest(); + new DML() + .toInsert(newAccount) + .toInsert(DML.Record(newContact).withRelationship(Contact.AccountId, newAccount)) + .commitWork(); + Test.stopTest(); - // Verify + // Verify Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be inserted.'); Assert.areEqual(1, [SELECT COUNT() FROM Contact], 'Contact should be inserted.'); - Assert.areEqual(newAccount.Id, newContact.AccountId, 'Contact should be related to Account.'); - } + Assert.areEqual(newAccount.Id, newContact.AccountId, 'Contact should be related to Account.'); + } - @IsTest + @IsTest static void toInsertWithInvalidRelationshipSingleRecord() { - // Setup + // Setup Account newAccount = getAccount(1); Contact newContact = getContact(1); - // Test - Test.startTest(); + // Test + Test.startTest(); Exception expectedException = null; try { - new DML() + new DML() .toInsert(newAccount) .toInsert(DML.Record(newContact).withRelationship(Contact.LastName, newAccount)) .commitWork(); @@ -164,7 +163,7 @@ private class DML_Test { // Verify Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.areEqual('Invalid argument: externalIdField. Field supplied is not a marked as an External Identifier.', expectedException.getMessage(), 'Expected exception message should be thrown.'); + Assert.areEqual('Invalid argument: externalIdField. Field supplied is not marked as an External Identifier.', expectedException.getMessage(), 'Expected exception message should be thrown.'); } @IsTest @@ -176,16 +175,16 @@ private class DML_Test { // Test Test.startTest(); DML.Result result = new DML() - .toInsert(account1) - .toInsert(account2) - .commitWork(); - Test.stopTest(); + .toInsert(account1) + .toInsert(account2) + .commitWork(); + Test.stopTest(); - // Verify + // Verify Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be inserted.'); - - Assert.isNotNull(account1.Id, 'Account 1 should be inserted and have an Id.'); - Assert.isNotNull(account2.Id, 'Account 2 should be inserted and have an Id.'); + + Assert.isNotNull(account1.Id, 'Account 1 should be inserted and have an Id.'); + Assert.isNotNull(account2.Id, 'Account 2 should be inserted and have an Id.'); Assert.areEqual(1, result.inserts().size(), 'Inserted operation result should contain 1 result, because 2 Account records are grouped.'); Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); @@ -202,11 +201,11 @@ private class DML_Test { Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Inserted operation result should contain Account object type.'); Assert.areEqual(DML.OperationType.INSERT_DML, operationResult.operationType(), 'Inserted operation result should contain insert type.'); Assert.isFalse(operationResult.hasFailures(), 'Inserted operation result should not have failures.'); - } + } - @IsTest + @IsTest static void insertImmediatelyMultipleRecords() { - // Setup + // Setup Account account1 = getAccount(1); Account account2 = getAccount(2); @@ -228,55 +227,55 @@ private class DML_Test { Assert.isFalse(result.hasFailures(), 'Inserted operation result should not have failures.'); } - @IsTest - static void toInsertWithRelationshipMultipleRecords() { - // Setup + @IsTest + static void toInsertWithRelationshipMultipleRecords() { + // Setup Account newAccount = getAccount(1); Contact newContact1 = getContact(1); Contact newContact2 = getContact(2); List contacts = new List{ newContact1, newContact2 }; - // Test - Test.startTest(); - new DML() - .toInsert(newAccount) - .toInsert(DML.Records(contacts).withRelationship(Contact.AccountId, newAccount)) - .commitWork(); - Test.stopTest(); + // Test + Test.startTest(); + new DML() + .toInsert(newAccount) + .toInsert(DML.Records(contacts).withRelationship(Contact.AccountId, newAccount)) + .commitWork(); + Test.stopTest(); - // Verify + // Verify Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be inserted.'); Assert.areEqual(2, [SELECT COUNT() FROM Contact], 'Contacts should be inserted.'); Assert.areEqual(newAccount.Id, newContact1.AccountId, 'Contact should be related to Account.'); - Assert.areEqual(newAccount.Id, newContact2.AccountId, 'Contact 2 should be related to Account.'); - } + Assert.areEqual(newAccount.Id, newContact2.AccountId, 'Contact 2 should be related to Account.'); + } - @IsTest + @IsTest static void toInsertWithInvalidTargetRelationshipFieldMultipleRecords() { - // Setup + // Setup Account newAccount = getAccount(1); Contact newContact1 = getContact(1); Contact newContact2 = getContact(2); List contacts = new List{ newContact1, newContact2 }; - // Test - Test.startTest(); + // Test + Test.startTest(); Exception expectedException = null; try { - new DML() + new DML() .toInsert(newAccount) .toInsert(DML.Records(contacts).withRelationship(Contact.LastName, newAccount)) - .commitWork(); + .commitWork(); } catch (Exception e) { expectedException = e; } - Test.stopTest(); + Test.stopTest(); - // Verify + // Verify Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); Assert.areEqual('Invalid argument: LastName. Field supplied is not a relationship field.', expectedException.getMessage(), 'Expected exception message should be thrown.'); } @@ -334,7 +333,7 @@ private class DML_Test { // Verify Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.areEqual('Invalid argument: externalIdField. Field supplied is not a marked as an External Identifier.', expectedException.getMessage(), 'Expected exception message should be thrown.'); + Assert.areEqual('Invalid argument: externalIdField. Field supplied is not marked as an External Identifier.', expectedException.getMessage(), 'Expected exception message should be thrown.'); } @IsTest @@ -393,160 +392,160 @@ private class DML_Test { Assert.areEqual(Lead.SObjectType, leadOperationResult.objectType(), 'Inserted operation result should contain Lead object type.'); Assert.areEqual(DML.OperationType.INSERT_DML, leadOperationResult.operationType(), 'Inserted operation result should contain insert type.'); Assert.isFalse(leadOperationResult.hasFailures(), 'Inserted operation result should not have failures.'); - } + } - @IsTest - static void toInsertListOfRecords() { - // Setup - List accounts = new List{ + @IsTest + static void toInsertListOfRecords() { + // Setup + List accounts = new List{ getAccount(1), getAccount(2), getAccount(3) - }; - - // Test - Test.startTest(); - new DML() - .toInsert(accounts) - .commitWork(); - Test.stopTest(); - - // Verify + }; + + // Test + Test.startTest(); + new DML() + .toInsert(accounts) + .commitWork(); + Test.stopTest(); + + // Verify Assert.areEqual(3, [SELECT COUNT() FROM Account], 'Accounts should be inserted.'); - Assert.isNotNull(accounts[0].Id, 'Account 1 should be inserted and have an Id.'); - Assert.isNotNull(accounts[1].Id, 'Account 2 should be inserted and have an Id.'); - Assert.isNotNull(accounts[2].Id, 'Account 3 should be inserted and have an Id.'); - } + Assert.isNotNull(accounts[0].Id, 'Account 1 should be inserted and have an Id.'); + Assert.isNotNull(accounts[1].Id, 'Account 2 should be inserted and have an Id.'); + Assert.isNotNull(accounts[2].Id, 'Account 3 should be inserted and have an Id.'); + } - @IsTest - static void toInsertWithExistingIds() { - // Setup + @IsTest + static void toInsertWithExistingIds() { + // Setup Account account = getAccount(1); - insert account; - - DmlException expectedException = null; - - // Test - Test.startTest(); - try { - new DML() - .toInsert(account) - .commitWork(); - } catch (DmlException e) { - expectedException = e; - } - Test.stopTest(); - - // Verify - Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.areEqual('Only new records can be registered as new.', expectedException.getMessage(), 'Expected exception message should be thrown.'); - } - - @IsTest - static void toInsertWithPartialSuccess() { - // Setup - List accounts = new List{ + insert account; + + DmlException expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toInsert(account) + .commitWork(); + } catch (DmlException e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.areEqual('Only new records can be registered as new.', expectedException.getMessage(), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toInsertWithPartialSuccess() { + // Setup + List accounts = new List{ getAccount(1), - new Account() // Name is required - }; - - // Test - Test.startTest(); - new DML() - .toInsert(accounts) - .allowPartialSuccess() - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Only one account should be inserted, because one has missing required field.'); - } - - @IsTest + new Account() // Name is required + }; + + // Test + Test.startTest(); + new DML() + .toInsert(accounts) + .allowPartialSuccess() + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Only one account should be inserted, because one has missing required field.'); + } + + @IsTest static void toInsertUserMode() { - // Setup + // Setup Case newCase = getCase(1); Exception expectedException = null; - // Test - Test.startTest(); + // Test + Test.startTest(); System.runAs(minimumAccessUser()) { try { - new DML() + new DML() .toInsert(newCase) .commitWork(); // user mode by default } catch (Exception e) { expectedException = e; } } - Test.stopTest(); + Test.stopTest(); - // Verify + // Verify Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); - } + } - @IsTest + @IsTest static void toInsertWithUserModeExplicitlySet() { - // Setup + // Setup Case newCase = getCase(1); Exception expectedException = null; - // Test - Test.startTest(); - System.runAs(minimumAccessUser()) { - try { - new DML() - .toInsert(newCase) + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + new DML() + .toInsert(newCase) .userMode() .commitWork(); } catch (Exception e) { - expectedException = e; - } - } - Test.stopTest(); - - // Verify - Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); - } - - @IsTest - static void toInsertSystemMode() { - // Setup + expectedException = e; + } + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toInsertSystemMode() { + // Setup Case newCase = getCase(1); - // Test - Test.startTest(); - System.runAs(minimumAccessUser()) { - new DML() - .toInsert(newCase) - .systemMode() - .commitWork(); - } - Test.stopTest(); - - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Case], 'Case should be inserted.'); - Assert.isNotNull(newCase.Id, 'Case should be inserted and have an Id.'); - } - - @IsTest + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + new DML() + .toInsert(newCase) + .systemMode() + .commitWork(); + } + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Case], 'Case should be inserted.'); + Assert.isNotNull(newCase.Id, 'Case should be inserted and have an Id.'); + } + + @IsTest static void toInsertSingleRecordWithMocking() { - // Setup + // Setup Account account1 = getAccount(1); DML.mock('dmlMockId').allInserts(); - // Test - Test.startTest(); - new DML() + // Test + Test.startTest(); + new DML() .toInsert(account1) .identifier('dmlMockId') - .commitWork(); + .commitWork(); Test.stopTest(); DML.Result result = DML.retrieveResultFor('dmlMockId'); @@ -578,11 +577,11 @@ private class DML_Test { .toInsert(account2) .identifier('dmlMockId') .commitWork(); - Test.stopTest(); + Test.stopTest(); DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify + // Verify Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); Assert.areEqual(1, result.inserts().size(), 'Inserted operation result should contain 1 result.'); Assert.areEqual(Account.SObjectType, result.insertsOf(Account.SObjectType).objectType(), 'Inserted operation result should contain Account object type.'); @@ -612,11 +611,11 @@ private class DML_Test { .toInsert(contact1) .identifier('dmlMockId') .commitWork(); - Test.stopTest(); + Test.stopTest(); DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify + // Verify Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should not be inserted, because it was mocked.'); Assert.areEqual(1, [SELECT COUNT() FROM Contact], 'Contact should be inserted, because only Account was mocked.'); Assert.areEqual(2, result.inserts().size(), 'Inserted operation result should contain 2 results.'); @@ -673,872 +672,629 @@ private class DML_Test { Assert.isNotNull(result.insertsOf(Contact.SObjectType).recordResults()[0].id(), 'Inserted operation result should contain a mocked record Id.'); Assert.isNotNull(account1.Id, 'Account should have mocked Id.'); Assert.isNotNull(contact1.Id, 'Contact should have mocked Id.'); - } - - // UPDATE + } - @IsTest - static void toUpdateSingleRecord() { - // Setup + @IsTest + static void toInsertWithMockingException() { + // Setup Account account1 = getAccount(1); - insert account1; - - // Test - Test.startTest(); - account1.Name = 'Updated Test Account'; - DML.Result result = new DML() - .toUpdate(account1) - .commitWork(); - Test.stopTest(); + DML.mock('dmlMockId').exceptionOnInserts(); - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be updated.'); + Exception expectedException = null; - Assert.areEqual('Updated Test Account', account1.Name, 'Account should be updated.'); - - Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); - Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); - Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); - Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); - Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); - Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + // Test + Test.startTest(); + try { + new DML() + .toInsert(account1) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } - DML.OperationResult operationResult = result.updatesOf(Account.SObjectType); + @IsTest + static void toInsertWithMockingExceptionWhenAllOrNoneIsSet() { + // Setup + Account account1 = getAccount(1); - Assert.areEqual(1, operationResult.records().size(), 'Updated operation result should contain the updated record.'); - Assert.areEqual(1, operationResult.recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Updated operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, operationResult.operationType(), 'Updated operation result should contain update type.'); - Assert.isFalse(operationResult.hasFailures(), 'Updated operation result should not have failures.'); - } + DML.mock('dmlMockId').exceptionOnInserts(); - @IsTest - static void updateImmediatelySingleRecord() { - // Setup - Account account1 = getAccount(1); - insert account1; - account1.Name = 'Updated Test Account'; + Exception expectedException = null; + DML.Result result = null; // Test Test.startTest(); - DML.OperationResult result = new DML().updateImmediately(account1); + try { + result = new DML() + .toInsert(account1) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } Test.stopTest(); // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account WHERE Name = 'Updated Test Account'], 'Single record should be updated.'); - Assert.areEqual('Updated Test Account', account1.Name, 'Account should be updated.'); - Assert.areEqual(1, result.records().size(), 'Updated operation result should contain the updated record.'); - Assert.areEqual(1, result.recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Updated operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, result.operationType(), 'Updated operation result should contain update type.'); - Assert.isFalse(result.hasFailures(), 'Updated operation result should not have failures.'); + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.insertsOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.INSERT_DML, operationResult.operationType(), 'Result should contain insert type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); } - @IsTest - static void toUpdateWithRelationshipSingleRecord() { - // Setup - Account newAccount = getAccount(1); - Contact newContact = getContact(1); - insert newContact; + @IsTest + static void toInsertWithMockingExceptionForSpecificSObjectType() { + // Setup + Account account1 = getAccount(1); - // Test - Test.startTest(); - new DML() - .toInsert(newAccount) - .toUpdate(DML.Record(newContact).withRelationship(Contact.AccountId, newAccount)) - .commitWork(); - Test.stopTest(); + DML.mock('dmlMockId').exceptionOnInsertsFor(Account.SObjectType); - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be inserted.'); - Assert.areEqual(1, [SELECT COUNT() FROM Contact], 'Contact should be updated.'); + Exception expectedException = null; - List contacts = [SELECT Id, AccountId FROM Contact WHERE Id = :newContact.Id]; + // Test + Test.startTest(); + try { + new DML() + .toInsert(account1) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); - Assert.areEqual(newAccount.Id, contacts[0].AccountId, 'Contact should be related to Account.'); - } + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } - @IsTest - static void toUpdateMultipleRecords() { - // Setup + @IsTest + static void toInsertWithMockingExceptionForSpecificSObjectTypeWhenAllOrNoneIsSet() { + // Setup Account account1 = getAccount(1); - Account account2 = getAccount(2); - insert new List{ account1, account2 }; + DML.mock('dmlMockId').exceptionOnInsertsFor(Account.SObjectType); + + Exception expectedException = null; + DML.Result result = null; // Test Test.startTest(); - account1.Name = 'Updated Test Account 1'; - account2.Name = 'Updated Test Account 2'; - - DML.Result result = new DML() - .toUpdate(account1) - .toUpdate(account2) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be updated.'); + try { + result = new DML() + .toInsert(account1) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); - Assert.areEqual('Updated Test Account 1', account1.Name, 'Account 1 should be updated.'); - Assert.areEqual('Updated Test Account 2', account2.Name, 'Account 2 should be updated.'); + // Verify + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); - Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + DML.OperationResult operationResult = result.insertsOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.INSERT_DML, operationResult.operationType(), 'Result should contain insert type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); + } + + @IsTest + static void toInsertWithEmptyRecords() { + // Test + Test.startTest(); + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + DML.Result result = new DML() + .toInsert(new List()) + .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); + Assert.areEqual(1, result.inserts().size(), 'Inserted operation result should contain 1 results.'); Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); - Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + DML.OperationResult operationResult = result.insertsOf(Account.SObjectType); - DML.OperationResult operationResult = result.updatesOf(Account.SObjectType); - - Assert.areEqual(2, operationResult.records().size(), 'Updated operation result should contain the updated records.'); - Assert.areEqual(2, operationResult.recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Updated operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, operationResult.operationType(), 'Updated operation result should contain update type.'); - Assert.isFalse(operationResult.hasFailures(), 'Updated operation result should not have failures.'); - } + Assert.areEqual(0, operationResult.records().size(), 'Inserted operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Inserted operation result should contain 0 record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Inserted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.INSERT_DML, operationResult.operationType(), 'Inserted operation result should contain insert type.'); + Assert.isFalse(operationResult.hasFailures(), 'Inserted operation result should not have failures.'); + } - @IsTest - static void updateImmediatelyMultipleRecords() { - // Setup - Account account1 = getAccount(1); - Account account2 = getAccount(2); - insert new List{ account1, account2 }; - - account1.Name = 'Updated Test Account 1'; - account2.Name = 'Updated Test Account 2'; + @IsTest + static void toInsertWithEmptyRecordWhenMocking() { + // Setup + DML.mock('dmlMockId').allInserts(); // Test Test.startTest(); - DML.OperationResult result = new DML().updateImmediately(new List{ account1, account2 }); - Test.stopTest(); - - // Verify - Assert.areEqual(2, [SELECT COUNT() FROM Account WHERE Name IN ('Updated Test Account 1', 'Updated Test Account 2')], 'Accounts should be updated.'); - - Assert.areEqual('Updated Test Account 1', account1.Name, 'Account 1 should be updated.'); - Assert.areEqual('Updated Test Account 2', account2.Name, 'Account 2 should be updated.'); + Integer dmlStatementsBefore = Limits.getDMLStatements(); - Assert.areEqual(2, result.records().size(), 'Updated operation result should contain the updated records.'); - Assert.areEqual(2, result.recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Updated operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, result.operationType(), 'Updated operation result should contain update type.'); - Assert.isFalse(result.hasFailures(), 'Updated operation result should not have failures.'); - } + new DML() + .toInsert(new List()) + .identifier('dmlMockId') + .commitWork(); - @IsTest - static void toUpdateWithRelationshipMultipleRecords() { - // Setup - Account newAccount = getAccount(1); - Contact newContact = getContact(1); - Contact newContact2 = getContact(2); + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); - List contactsToCreate = new List{ newContact, newContact2 }; - insert contactsToCreate; + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); - // Test - Test.startTest(); - new DML() - .toInsert(newAccount) - .toUpdate(DML.Records(contactsToCreate).withRelationship(Contact.AccountId, newAccount)) - .commitWork(); - Test.stopTest(); + DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify - Assert.areEqual(2, [SELECT COUNT() FROM Contact], 'Contacts should be updated.'); - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be inserted.'); + Assert.areEqual(1, result.inserts().size(), 'Inserted operation result should contain 1 result.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); - List contacts = [SELECT Id, AccountId FROM Contact WHERE Id IN :contactsToCreate]; + DML.OperationResult operationResult = result.insertsOf(Account.SObjectType); + + Assert.areEqual(0, operationResult.records().size(), 'Inserted operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Inserted operation result should contain 0 record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Inserted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.INSERT_DML, operationResult.operationType(), 'Inserted operation result should contain insert type.'); + Assert.isFalse(operationResult.hasFailures(), 'Inserted operation result should not have failures.'); + } - Assert.areEqual(newAccount.Id, contacts[0].AccountId, 'Contact should be related to Account.'); - Assert.areEqual(newAccount.Id, contacts[1].AccountId, 'Contact 2 should be related to Account.'); - } + // UPDATE - @IsTest - static void toUpdateMultipleRecordsTypes() { - // Setup + @IsTest + static void toUpdateSingleRecord() { + // Setup Account account1 = getAccount(1); - Opportunity opportunity1 = getOpportunity(1); - Lead lead1 = getLead(1); - - insert new List{ account1, opportunity1, lead1 }; + insert account1; - // Test - Test.startTest(); + // Test + Test.startTest(); account1.Name = 'Updated Test Account'; - opportunity1.Name = 'Updated Test Opportunity'; - lead1.FirstName = 'Updated Test'; DML.Result result = new DML() .toUpdate(account1) - .toUpdate(opportunity1) - .toUpdate(lead1) - .commitWork(); - Test.stopTest(); + .commitWork(); + Test.stopTest(); - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be updated.'); - Assert.areEqual(1, [SELECT COUNT() FROM Opportunity], 'Opportunity should be updated.'); - Assert.areEqual(1, [SELECT COUNT() FROM Lead], 'Lead should be updated.'); + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be updated.'); Assert.areEqual('Updated Test Account', account1.Name, 'Account should be updated.'); - Assert.areEqual('Updated Test Opportunity', opportunity1.Name, 'Opportunity should be updated.'); - Assert.areEqual('Updated Test', lead1.FirstName, 'Lead should be updated.'); - + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); - Assert.areEqual(3, result.updates().size(), 'Updated operation result should contain 3 results.'); + Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); - DML.OperationResult accountOperationResult = result.updatesOf(Account.SObjectType); + DML.OperationResult operationResult = result.updatesOf(Account.SObjectType); - Assert.areEqual(1, accountOperationResult.records().size(), 'Updated operation result should contain the updated record.'); - Assert.areEqual(1, accountOperationResult.recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.areEqual(Account.SObjectType, accountOperationResult.objectType(), 'Updated operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, accountOperationResult.operationType(), 'Updated operation result should contain update type.'); - Assert.isFalse(accountOperationResult.hasFailures(), 'Updated operation result should not have failures.'); + Assert.areEqual(1, operationResult.records().size(), 'Updated operation result should contain the updated record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, operationResult.operationType(), 'Updated operation result should contain update type.'); + Assert.isFalse(operationResult.hasFailures(), 'Updated operation result should not have failures.'); + } - DML.OperationResult opportunityOperationResult = result.updatesOf(Opportunity.SObjectType); + @IsTest + static void toUpdateSingleRecordTwice() { + // Setup + Account account1 = getAccount(1); + insert account1; - Assert.areEqual(1, opportunityOperationResult.records().size(), 'Updated operation result should contain the updated record.'); - Assert.areEqual(1, opportunityOperationResult.recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.areEqual(Opportunity.SObjectType, opportunityOperationResult.objectType(), 'Updated operation result should contain Opportunity object type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, opportunityOperationResult.operationType(), 'Updated operation result should contain update type.'); - Assert.isFalse(opportunityOperationResult.hasFailures(), 'Updated operation result should not have failures.'); + // Test + Test.startTest(); + Exception expectedException = null; - DML.OperationResult leadOperationResult = result.updatesOf(Lead.SObjectType); + try { + account1.Name = 'Updated Test Account'; - Assert.areEqual(1, leadOperationResult.records().size(), 'Updated operation result should contain the updated record.'); - Assert.areEqual(1, leadOperationResult.recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.areEqual(Lead.SObjectType, leadOperationResult.objectType(), 'Updated operation result should contain Lead object type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, leadOperationResult.operationType(), 'Updated operation result should contain update type.'); - Assert.isFalse(leadOperationResult.hasFailures(), 'Updated operation result should not have failures.'); - } + DML db = new DML(); + + db.toUpdate(account1); + + Account duplicatedAccount = new Account(Id = account1.Id, Name = 'Updated Test Account 2'); + + db.toUpdate(duplicatedAccount); + + db.commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.areEqual('Duplicate records found during registration. Fix the code or use the combineOnDuplicate() method.', expectedException.getMessage(), 'Expected exception message should be thrown.'); + } - @IsTest - static void toUpdateListOfRecords() { - // Setup - List accounts = new List{ - getAccount(1), - getAccount(2), - getAccount(3) - }; - insert accounts; + @IsTest + static void toUpdateSingleRecordTwiceWithMergeOnDuplicate() { + // Setup + Account account1 = getAccount(1); + insert account1; // Test Test.startTest(); - accounts[0].Name = 'Updated Test Account 1'; - accounts[1].Name = 'Updated Test Account 2'; - accounts[2].Name = 'Updated Test Account 3'; - - new DML() - .toUpdate(accounts) - .commitWork(); - Test.stopTest(); + account1.Name = 'Updated Test Account'; + account1.Website = 'https://www.updatedtestaccount.com'; - // Verify - Assert.areEqual(3, [SELECT COUNT() FROM Account], 'Accounts should be updated.'); + DML db = new DML(); + + db.combineOnDuplicate(); + + db.toUpdate(account1); + + Account duplicatedAccount = new Account(Id = account1.Id, Name = 'Updated Test Account 2', Description = 'Updated Test Description'); + + db.toUpdate(duplicatedAccount); + + DML.Result result = db.commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be updated.'); - Assert.areEqual('Updated Test Account 1', accounts[0].Name, 'Account 1 should be updated.'); - Assert.areEqual('Updated Test Account 2', accounts[1].Name, 'Account 2 should be updated.'); - Assert.areEqual('Updated Test Account 3', accounts[2].Name, 'Account 3 should be updated.'); - } + Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); - @IsTest - static void toUpdateWithoutExistingIds() { - // Setup - Account account = getAccount(1); - - DmlException expectedException = null; - - // Test - Test.startTest(); - try { - new DML() - .toUpdate(account) - .commitWork(); - } catch (DmlException e) { - expectedException = e; - } - Test.stopTest(); - - // Verify - Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.areEqual('Only existing records can be updated.', expectedException.getMessage(), 'Expected exception message should be thrown.'); - } - - @IsTest - static void toUpdateWithPartialSuccess() { - // Setup - List accounts = new List{ getAccount(1), getAccount(2) }; - insert accounts; - - // Test - Test.startTest(); - accounts[0].Name = null; - accounts[1].Name = 'Test Account 1 New Name'; - - new DML() - .toUpdate(accounts) - .allowPartialSuccess() - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(2, [SELECT COUNT() FROM Account WHERE Name IN ('Test Account 1', 'Test Account 1 New Name')], 'Accounts should be present with expected names after partial success update.'); + DML.OperationResult operationResult = result.updatesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Updated operation result should contain the updated record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, operationResult.operationType(), 'Updated operation result should contain update type.'); + Assert.isFalse(operationResult.hasFailures(), 'Updated operation result should not have failures.'); + Assert.areEqual('Updated Test Account 2', account1.Name, 'Account should be updated.'); + Assert.areEqual('https://www.updatedtestaccount.com', account1.Website, 'Account should be updated.'); + Assert.areEqual('Updated Test Description', account1.Description, 'Account should be updated.'); } @IsTest - static void toUpdateWithUserMode() { + static void toUpdateWithEmptyRecordIds() { // Setup - Case newCase = getCase(1); - insert newCase; - - Exception expectedException = null; + List recordIds = new List(); // Test Test.startTest(); - System.runAs(minimumAccessUser()) { - try { - newCase.Subject = 'Updated Test Case'; - new DML() - .toUpdate(newCase) - .commitWork(); // user mode by default - } catch (Exception e) { - expectedException = e; - } - } - Test.stopTest(); - - // Verify - Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); - } - - @IsTest - static void toUpdateWithUserModeExplicitlySet() { - // Setup - Case newCase = getCase(1); - insert newCase; + Integer dmlStatementsBefore = Limits.getDMLStatements(); - Exception expectedException = null; + DML.Result result = new DML() + .toUpdate(DML.Records(recordIds)) + .commitWork(); - // Test - Test.startTest(); - System.runAs(minimumAccessUser()) { - try { - newCase.Subject = 'Updated Test Case'; - new DML() - .toUpdate(newCase) - .userMode() - .commitWork(); - } catch (Exception e) { - expectedException = e; - } - } + Integer dmlStatementsAfter = Limits.getDMLStatements(); Test.stopTest(); - + // Verify - Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); } @IsTest - static void toUpdateWithSystemMode() { + static void updateImmediatelySingleRecord() { // Setup - Case newCase = getCase(1); - insert newCase; + Account account1 = getAccount(1); + insert account1; + account1.Name = 'Updated Test Account'; // Test Test.startTest(); - System.runAs(minimumAccessUser()) { - newCase.Subject = 'Updated Test Case'; - - new DML() - .toUpdate(newCase) - .systemMode() - .withoutSharing() - .commitWork(); - } + DML.OperationResult result = new DML().updateImmediately(account1); Test.stopTest(); // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Case], 'Case should be updated.'); - Assert.isNotNull(newCase.Id, 'Case should be updated and have an Id.'); + Assert.areEqual(1, [SELECT COUNT() FROM Account WHERE Name = 'Updated Test Account'], 'Single record should be updated.'); + Assert.areEqual('Updated Test Account', account1.Name, 'Account should be updated.'); + Assert.areEqual(1, result.records().size(), 'Updated operation result should contain the updated record.'); + Assert.areEqual(1, result.recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, result.operationType(), 'Updated operation result should contain update type.'); + Assert.isFalse(result.hasFailures(), 'Updated operation result should not have failures.'); } - @IsTest - static void toUpdateWithSystemModeAndWithSharing() { + static void toUpdateWithRelationshipSingleRecord() { // Setup + Account newAccount = getAccount(1); Contact newContact = getContact(1); insert newContact; - Exception expectedException = null; - - // Test - Test.startTest(); - System.runAs(minimumAccessUser()) { - try { - newContact.FirstName = 'Updated Test Contact'; - new DML() - .toUpdate(newContact) - .systemMode() - .withSharing() - .commitWork(); - } catch (Exception e) { - expectedException = e; - } - } - Test.stopTest(); - - // Verify - Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.isTrue(expectedException.getMessage().contains('insufficient access rights on cross-reference id'), 'Expected exception message should be thrown.'); - } - - @IsTest - static void toUpdateSingleRecordWithMocking() { - // Setup - Account account1 = getAccount(1); - account1.Id = DML.randomIdGenerator.get(Account.SObjectType); - - DML.mock('dmlMockId').allUpdates(); - // Test Test.startTest(); - account1.Name = 'Updated Test Account'; - new DML() - .toUpdate(account1) - .identifier('dmlMockId') + .toInsert(newAccount) + .toUpdate(DML.Record(newContact).withRelationship(Contact.AccountId, newAccount)) .commitWork(); Test.stopTest(); - DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); - Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); - Assert.areEqual(Account.SObjectType, result.updatesOf(Account.SObjectType).objectType(), 'Updated operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Account.SObjectType).operationType(), 'Updated operation result should contain update type.'); - Assert.areEqual(1, result.updatesOf(Account.SObjectType).records().size(), 'Updated operation result should contain the updated record.'); - Assert.areEqual(1, result.updatesOf(Account.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.areEqual('Updated Test Account', account1.Name, 'Account should be updated.'); - Assert.isTrue(result.updatesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); - Assert.isNotNull(result.updatesOf(Account.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be inserted.'); + Assert.areEqual(1, [SELECT COUNT() FROM Contact], 'Contact should be updated.'); + + List contacts = [SELECT Id, AccountId FROM Contact WHERE Id = :newContact.Id]; + + Assert.areEqual(newAccount.Id, contacts[0].AccountId, 'Contact should be related to Account.'); } @IsTest - static void toUpdateMultipleRecordsWithMocking() { + static void toUpdateMultipleRecords() { // Setup Account account1 = getAccount(1); Account account2 = getAccount(2); - account1.Id = DML.randomIdGenerator.get(Account.SObjectType); - account2.Id = DML.randomIdGenerator.get(Account.SObjectType); - - DML.mock('dmlMockId').allUpdates(); + insert new List{ account1, account2 }; // Test Test.startTest(); - account1.Name = 'Updated Test Account 1'; - account2.Name = 'Updated Test Account 2'; + account1.Name = 'Updated Test Account 1'; + account2.Name = 'Updated Test Account 2'; - new DML() + DML.Result result = new DML() .toUpdate(account1) .toUpdate(account2) - .identifier('dmlMockId') .commitWork(); Test.stopTest(); - DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); - Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); - Assert.areEqual(Account.SObjectType, result.updatesOf(Account.SObjectType).objectType(), 'Updated operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Account.SObjectType).operationType(), 'Updated operation result should contain update type.'); - Assert.areEqual(2, result.updatesOf(Account.SObjectType).records().size(), 'Updated operation result should contain the updated records.'); - Assert.areEqual(2, result.updatesOf(Account.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.isTrue(result.updatesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); - Assert.isTrue(result.updatesOf(Account.SObjectType).recordResults()[1].isSuccess(), 'Updated operation result should contain a successful record result.'); - Assert.isNotNull(result.updatesOf(Account.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); - Assert.isNotNull(result.updatesOf(Account.SObjectType).recordResults()[1].id(), 'Updated operation result should contain a mocked record Id.'); + Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be updated.'); + Assert.areEqual('Updated Test Account 1', account1.Name, 'Account 1 should be updated.'); Assert.areEqual('Updated Test Account 2', account2.Name, 'Account 2 should be updated.'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + + DML.OperationResult operationResult = result.updatesOf(Account.SObjectType); + + Assert.areEqual(2, operationResult.records().size(), 'Updated operation result should contain the updated records.'); + Assert.areEqual(2, operationResult.recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, operationResult.operationType(), 'Updated operation result should contain update type.'); + Assert.isFalse(operationResult.hasFailures(), 'Updated operation result should not have failures.'); } @IsTest - static void toUpdateMultipleRecordTypesWithMocking() { + static void updateImmediatelyMultipleRecords() { // Setup Account account1 = getAccount(1); - Contact contact1 = getContact(1); - - account1.Id = DML.randomIdGenerator.get(Account.SObjectType); - contact1.Id = DML.randomIdGenerator.get(Contact.SObjectType); - - DML.mock('dmlMockId').allUpdates(); + Account account2 = getAccount(2); + insert new List{ account1, account2 }; + + account1.Name = 'Updated Test Account 1'; + account2.Name = 'Updated Test Account 2'; // Test Test.startTest(); - account1.Name = 'Updated Test Account 1'; - contact1.FirstName = 'Updated Test Contact 1'; - - new DML() - .toUpdate(account1) - .toUpdate(contact1) - .identifier('dmlMockId') - .commitWork(); + DML.OperationResult result = new DML().updateImmediately(new List{ account1, account2 }); Test.stopTest(); - DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); - Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'No records should be inserted to the database.'); - Assert.areEqual(2, result.updates().size(), 'Updated operation result should contain 2 results.'); - Assert.areEqual(Account.SObjectType, result.updatesOf(Account.SObjectType).objectType(), 'Updated operation result should contain Account object type.'); - Assert.areEqual(Contact.SObjectType, result.updatesOf(Contact.SObjectType).objectType(), 'Updated operation result should contain Contact object type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Account.SObjectType).operationType(), 'Updated operation result should contain update type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Contact.SObjectType).operationType(), 'Updated operation result should contain update type.'); - Assert.areEqual(1, result.updatesOf(Account.SObjectType).records().size(), 'Updated operation result should contain the updated record.'); - Assert.areEqual(1, result.updatesOf(Account.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.areEqual(1, result.updatesOf(Contact.SObjectType).records().size(), 'Updated operation result should contain the updated record.'); - Assert.areEqual(1, result.updatesOf(Contact.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.isTrue(result.updatesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); - Assert.isTrue(result.updatesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); - Assert.isNotNull(result.updatesOf(Account.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); - Assert.isNotNull(result.updatesOf(Contact.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); + Assert.areEqual(2, [SELECT COUNT() FROM Account WHERE Name IN ('Updated Test Account 1', 'Updated Test Account 2')], 'Accounts should be updated.'); + Assert.areEqual('Updated Test Account 1', account1.Name, 'Account 1 should be updated.'); - Assert.areEqual('Updated Test Contact 1', contact1.FirstName, 'Contact 1 should be updated.'); + Assert.areEqual('Updated Test Account 2', account2.Name, 'Account 2 should be updated.'); + + Assert.areEqual(2, result.records().size(), 'Updated operation result should contain the updated records.'); + Assert.areEqual(2, result.recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, result.operationType(), 'Updated operation result should contain update type.'); + Assert.isFalse(result.hasFailures(), 'Updated operation result should not have failures.'); } @IsTest - static void toUpdateMultipleRecordsWithMockingSpecificSObjectType() { + static void toUpdateWithRelationshipMultipleRecords() { // Setup - Account account1 = getAccount(1); - insert account1; - - Contact contact1 = getContact(1); - insert contact1; + Account newAccount = getAccount(1); + Contact newContact = getContact(1); + Contact newContact2 = getContact(2); - DML.mock('dmlMockId').updatesFor(Account.SObjectType); + List contactsToCreate = new List{ newContact, newContact2 }; + insert contactsToCreate; // Test Test.startTest(); - Integer dmlStatementsBefore = Limits.getDMLStatements(); - - account1.Name = 'Updated Test Account 1'; - contact1.FirstName = 'Updated Test Contact 1'; - new DML() - .toUpdate(account1) - .toUpdate(contact1) - .identifier('dmlMockId') + .toInsert(newAccount) + .toUpdate(DML.Records(contactsToCreate).withRelationship(Contact.AccountId, newAccount)) .commitWork(); - - Integer dmlStatementsAfter = Limits.getDMLStatements(); Test.stopTest(); - DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should not be updated in the database.'); - Assert.areEqual(1, [SELECT COUNT() FROM Contact], 'Contact should not be updated in the database.'); - Assert.areEqual(1, dmlStatementsAfter - dmlStatementsBefore, 'DML statements should be 1, because second commitWork() should not do anything.'); - Assert.areEqual(2, result.updates().size(), 'Updated operation result should contain 2 results.'); - Assert.areEqual(Account.SObjectType, result.updatesOf(Account.SObjectType).objectType(), 'Updated operation result should contain Account object type.'); - Assert.areEqual(Contact.SObjectType, result.updatesOf(Contact.SObjectType).objectType(), 'Updated operation result should contain Contact object type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Account.SObjectType).operationType(), 'Updated operation result should contain update type.'); - Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Contact.SObjectType).operationType(), 'Updated operation result should contain update type.'); - Assert.areEqual(1, result.updatesOf(Account.SObjectType).records().size(), 'Updated operation result should contain the updated record.'); - Assert.areEqual(1, result.updatesOf(Account.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.areEqual(1, result.updatesOf(Contact.SObjectType).records().size(), 'Updated operation result should contain the updated record.'); - Assert.areEqual(1, result.updatesOf(Contact.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); - Assert.isTrue(result.updatesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); - Assert.isTrue(result.updatesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); - Assert.isNotNull(result.updatesOf(Account.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); - Assert.isNotNull(result.updatesOf(Contact.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); - } + Assert.areEqual(2, [SELECT COUNT() FROM Contact], 'Contacts should be updated.'); + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be inserted.'); - // UPSERT + List contacts = [SELECT Id, AccountId FROM Contact WHERE Id IN :contactsToCreate]; + + Assert.areEqual(newAccount.Id, contacts[0].AccountId, 'Contact should be related to Account.'); + Assert.areEqual(newAccount.Id, contacts[1].AccountId, 'Contact 2 should be related to Account.'); + } @IsTest - static void toUpsertSingleExistingRecord() { + static void toUpdateMultipleRecordsTypes() { // Setup Account account1 = getAccount(1); - insert account1; + Opportunity opportunity1 = getOpportunity(1); + Lead lead1 = getLead(1); + + insert new List{ account1, opportunity1, lead1 }; // Test Test.startTest(); account1.Name = 'Updated Test Account'; + opportunity1.Name = 'Updated Test Opportunity'; + lead1.FirstName = 'Updated Test'; DML.Result result = new DML() - .toUpsert(account1) - .commitWork(); - Test.stopTest(); + .toUpdate(account1) + .toUpdate(opportunity1) + .toUpdate(lead1) + .commitWork(); + Test.stopTest(); - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be updated.'); + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be updated.'); + Assert.areEqual(1, [SELECT COUNT() FROM Opportunity], 'Opportunity should be updated.'); + Assert.areEqual(1, [SELECT COUNT() FROM Lead], 'Lead should be updated.'); Assert.areEqual('Updated Test Account', account1.Name, 'Account should be updated.'); + Assert.areEqual('Updated Test Opportunity', opportunity1.Name, 'Opportunity should be updated.'); + Assert.areEqual('Updated Test', lead1.FirstName, 'Lead should be updated.'); Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); - Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); - Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(3, result.updates().size(), 'Updated operation result should contain 3 results.'); Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); - DML.OperationResult operationResult = result.upsertsOf(Account.SObjectType); + DML.OperationResult accountOperationResult = result.updatesOf(Account.SObjectType); - Assert.areEqual(1, operationResult.records().size(), 'Upserted operation result should contain the upserted records.'); - Assert.areEqual(1, operationResult.recordResults().size(), 'Upserted operation result should contain the upserted record results.'); - Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Upserted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPSERT_DML, operationResult.operationType(), 'Upserted operation result should contain upsert type.'); - Assert.isFalse(operationResult.hasFailures(), 'Upserted operation result should not have failures.'); - } + Assert.areEqual(1, accountOperationResult.records().size(), 'Updated operation result should contain the updated record.'); + Assert.areEqual(1, accountOperationResult.recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.areEqual(Account.SObjectType, accountOperationResult.objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, accountOperationResult.operationType(), 'Updated operation result should contain update type.'); + Assert.isFalse(accountOperationResult.hasFailures(), 'Updated operation result should not have failures.'); + + DML.OperationResult opportunityOperationResult = result.updatesOf(Opportunity.SObjectType); + + Assert.areEqual(1, opportunityOperationResult.records().size(), 'Updated operation result should contain the updated record.'); + Assert.areEqual(1, opportunityOperationResult.recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.areEqual(Opportunity.SObjectType, opportunityOperationResult.objectType(), 'Updated operation result should contain Opportunity object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, opportunityOperationResult.operationType(), 'Updated operation result should contain update type.'); + Assert.isFalse(opportunityOperationResult.hasFailures(), 'Updated operation result should not have failures.'); + + DML.OperationResult leadOperationResult = result.updatesOf(Lead.SObjectType); + Assert.areEqual(1, leadOperationResult.records().size(), 'Updated operation result should contain the updated record.'); + Assert.areEqual(1, leadOperationResult.recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.areEqual(Lead.SObjectType, leadOperationResult.objectType(), 'Updated operation result should contain Lead object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, leadOperationResult.operationType(), 'Updated operation result should contain update type.'); + Assert.isFalse(leadOperationResult.hasFailures(), 'Updated operation result should not have failures.'); + } @IsTest - static void upsertImmediatelySingleRecord() { + static void toUpdateListOfRecords() { // Setup - Account account1 = getAccount(1); - insert account1; - account1.Name = 'Upserted Test Account'; + List accounts = new List{ + getAccount(1), + getAccount(2), + getAccount(3) + }; + insert accounts; // Test Test.startTest(); - DML.OperationResult result = new DML().upsertImmediately(account1); + accounts[0].Name = 'Updated Test Account 1'; + accounts[1].Name = 'Updated Test Account 2'; + accounts[2].Name = 'Updated Test Account 3'; + + new DML() + .toUpdate(accounts) + .commitWork(); Test.stopTest(); // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account WHERE Name = 'Upserted Test Account'], 'Single record should be upserted.'); - Assert.areEqual('Upserted Test Account', account1.Name, 'Account should be upserted.'); - Assert.areEqual(1, result.records().size(), 'Upserted operation result should contain the upserted record.'); - Assert.areEqual(1, result.recordResults().size(), 'Upserted operation result should contain the upserted record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Upserted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPSERT_DML, result.operationType(), 'Upserted operation result should contain upsert type.'); - Assert.isFalse(result.hasFailures(), 'Upserted operation result should not have failures.'); - } - - - @IsTest - static void toUpsertSingleNewRecord() { - // Setup - Account account1 = getAccount(1); - - // Test - Test.startTest(); - DML.Result result = new DML() - .toUpsert(account1) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be inserted.'); - - Assert.isNotNull(account1.Id, 'Account should be inserted and have an Id.'); - - Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); - Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); - Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); - Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); - Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); - Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); - - - DML.OperationResult operationResult = result.upsertsOf(Account.SObjectType); - - Assert.areEqual(1, operationResult.records().size(), 'Upserted operation result should contain the upserted records.'); - Assert.areEqual(1, operationResult.recordResults().size(), 'Upserted operation result should contain the upserted record results.'); - Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Upserted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPSERT_DML, operationResult.operationType(), 'Upserted operation result should contain upsert type.'); - Assert.isFalse(operationResult.hasFailures(), 'Upserted operation result should not have failures.'); - } - - @IsTest - static void toUpsertMultipleExistingRecords() { - // Setup - Account account1 = getAccount(1); - Account account2 = getAccount(2); - - insert new List{ account1, account2 }; - - // Test - Test.startTest(); - account1.Name = 'Updated Test Account 1'; - account2.Name = 'Updated Test Account 2'; - - new DML() - .toUpsert(account1) - .toUpsert(account2) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be updated.'); - - Assert.areEqual('Updated Test Account 1', account1.Name, 'Account 1 should be updated.'); - Assert.areEqual('Updated Test Account 2', account2.Name, 'Account 2 should be updated.'); - } - - @IsTest - static void toUpsertMultipleNewRecords() { - // Setup - Account account1 = getAccount(1); - Account account2 = getAccount(2); + Assert.areEqual(3, [SELECT COUNT() FROM Account], 'Accounts should be updated.'); - // Test - Test.startTest(); - new DML() - .toUpsert(account1) - .toUpsert(account2) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be inserted.'); - - Assert.isNotNull(account1.Id, 'Account 1 should be inserted and have an Id.'); - Assert.isNotNull(account2.Id, 'Account 2 should be inserted and have an Id.'); - } + Assert.areEqual('Updated Test Account 1', accounts[0].Name, 'Account 1 should be updated.'); + Assert.areEqual('Updated Test Account 2', accounts[1].Name, 'Account 2 should be updated.'); + Assert.areEqual('Updated Test Account 3', accounts[2].Name, 'Account 3 should be updated.'); + } @IsTest - static void upsertImmediatelyMultipleRecords() { + static void toUpdateWithoutExistingIds() { // Setup - Account account1 = getAccount(1); - Account account2 = getAccount(2); - insert account1; // Make account1 existing for upsert + Account account = getAccount(1); + + DmlException expectedException = null; - account1.Name = 'Upserted Test Account 1'; - // account2 is new for upsert - // Test Test.startTest(); - DML.OperationResult result = new DML().upsertImmediately(new List{ account1, account2 }); + try { + new DML() + .toUpdate(account) + .commitWork(); + } catch (DmlException e) { + expectedException = e; + } Test.stopTest(); - + // Verify - Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be upserted.'); - - Assert.areEqual('Upserted Test Account 1', account1.Name, 'Account 1 should be updated.'); - Assert.isNotNull(account2.Id, 'Account 2 should be inserted and have an Id.'); - - Assert.areEqual(2, result.records().size(), 'Upserted operation result should contain the upserted records.'); - Assert.areEqual(2, result.recordResults().size(), 'Upserted operation result should contain the upserted record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Upserted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPSERT_DML, result.operationType(), 'Upserted operation result should contain upsert type.'); - Assert.isFalse(result.hasFailures(), 'Upserted operation result should not have failures.'); - } - - @IsTest - static void toUpsertExistingAndNewRecords() { - // Setup - Account account1 = getAccount(1); - Account account2 = getAccount(2); - - insert account1; - - // Test - Test.startTest(); - account1.Name = 'Updated Test Account 1'; - - new DML() - .toUpsert(account1) - .toUpsert(account2) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Both accounts should be upserted.'); - - Assert.areEqual('Updated Test Account 1', account1.Name, 'Account 1 should be updated.'); - Assert.isNotNull(account2.Id, 'Account 2 should be inserted and have an Id.'); - } + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.areEqual('Only existing records can be updated.', expectedException.getMessage(), 'Expected exception message should be thrown.'); + } - @IsTest - static void toUpsertListOfRecords() { - // Setup - List existingAccounts = new List{ - getAccount(1), - getAccount(2), - getAccount(3) - }; - insert existingAccounts; + @IsTest + static void toUpdateWithPartialSuccess() { + // Setup + List accounts = new List{ getAccount(1), getAccount(2) }; + insert accounts; // Test Test.startTest(); - existingAccounts[0].Name = 'Updated Test Account 1'; - existingAccounts[1].Name = 'Updated Test Account 2'; - existingAccounts[2].Name = 'Updated Test Account 3'; - - List newAccounts = new List{ - getAccount(1), - getAccount(2), - getAccount(3) - }; - - new DML() - .toUpsert(existingAccounts) - .toUpsert(newAccounts) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(6, [SELECT COUNT() FROM Account], 'Accounts should be updated and inserted.'); - - Assert.areEqual('Updated Test Account 1', existingAccounts[0].Name, 'Account 1 should be updated.'); - Assert.areEqual('Updated Test Account 2', existingAccounts[1].Name, 'Account 2 should be updated.'); - Assert.areEqual('Updated Test Account 3', existingAccounts[2].Name, 'Account 3 should be updated.'); - - Assert.isNotNull(newAccounts[0].Id, 'New Account 1 should be inserted and have an Id.'); - Assert.isNotNull(newAccounts[1].Id, 'New Account 2 should be inserted and have an Id.'); - Assert.isNotNull(newAccounts[2].Id, 'New Account 3 should be inserted and have an Id.'); - } - - @IsTest - static void toUpsertWithPartialSuccess() { - // Setup - List accounts = new List{ - getAccount(1), - getAccount(2) - }; - insert accounts; + accounts[0].Name = null; + accounts[1].Name = 'Test Account 1 New Name'; - // Test - Test.startTest(); - accounts[0].Name = null; // Name will not change, because it's set to null - accounts[1].Name = 'Test Account 1 New Name'; + new DML() + .toUpdate(accounts) + .allowPartialSuccess() + .commitWork(); + Test.stopTest(); - accounts.add(getAccount(3)); // New account - - new DML() - .toUpsert(accounts) - .allowPartialSuccess() - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(3, [SELECT COUNT() FROM Account WHERE Name IN ('Test Account 1', 'Test Account 1 New Name', 'Test Account 3')], 'Accounts should be upserted with expected names.'); + // Verify + Assert.areEqual(2, [SELECT COUNT() FROM Account WHERE Name IN ('Test Account 1', 'Test Account 1 New Name')], 'Accounts should be present with expected names after partial success update.'); } @IsTest - static void toUpsertWithUserMode() { + static void toUpdateWithUserMode() { // Setup - Case newCase1 = getCase(1); - Case newCase2 = getCase(2); - - insert newCase1; + Case newCase = getCase(1); + insert newCase; Exception expectedException = null; @@ -1546,11 +1302,9 @@ private class DML_Test { Test.startTest(); System.runAs(minimumAccessUser()) { try { - newCase1.Subject = 'Updated Test Case'; - + newCase.Subject = 'Updated Test Case'; new DML() - .toUpsert(newCase1) - .toUpsert(newCase2) + .toUpdate(newCase) .commitWork(); // user mode by default } catch (Exception e) { expectedException = e; @@ -1560,16 +1314,14 @@ private class DML_Test { // Verify Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.isTrue(expectedException.getMessage().contains('Operation failed due to fields being inaccessible on Sobject Case, check errors on Exception or Result'), 'Expected exception message should be thrown.'); + Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); } @IsTest - static void toUpsertWithUserModeExplicitlySet() { + static void toUpdateWithUserModeExplicitlySet() { // Setup - Case newCase1 = getCase(1); - Case newCase2 = getCase(2); - - insert newCase1; + Case newCase = getCase(1); + insert newCase; Exception expectedException = null; @@ -1577,11 +1329,9 @@ private class DML_Test { Test.startTest(); System.runAs(minimumAccessUser()) { try { - newCase1.Subject = 'Updated Test Case'; - - new DML() - .toUpsert(newCase1) - .toUpsert(newCase2) + newCase.Subject = 'Updated Test Case'; + new DML() + .toUpdate(newCase) .userMode() .commitWork(); } catch (Exception e) { @@ -1592,25 +1342,22 @@ private class DML_Test { // Verify Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.isTrue(expectedException.getMessage().contains('Operation failed due to fields being inaccessible on Sobject Case, check errors on Exception or Result'), 'Expected exception message should be thrown.'); + Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); } @IsTest - static void toUpsertWithSystemMode() { + static void toUpdateWithSystemMode() { // Setup - Case newCase1 = getCase(1); - Case newCase2 = getCase(2); - - insert newCase1; + Case newCase = getCase(1); + insert newCase; // Test Test.startTest(); System.runAs(minimumAccessUser()) { - newCase1.Subject = 'Updated Test Case'; + newCase.Subject = 'Updated Test Case'; new DML() - .toUpsert(newCase1) - .toUpsert(newCase2) + .toUpdate(newCase) .systemMode() .withoutSharing() .commitWork(); @@ -1618,19 +1365,16 @@ private class DML_Test { Test.stopTest(); // Verify - Assert.areEqual(2, [SELECT COUNT() FROM Case], 'Cases should be upserted.'); - - Assert.isNotNull(newCase1.Id, 'Case 1 should be upserted and have an Id.'); - Assert.isNotNull(newCase2.Id, 'Case 2 should be upserted and have an Id.'); + Assert.areEqual(1, [SELECT COUNT() FROM Case], 'Case should be updated.'); + Assert.isNotNull(newCase.Id, 'Case should be updated and have an Id.'); } + @IsTest - static void toUpsertWithSystemModeAndWithSharing() { + static void toUpdateWithSystemModeAndWithSharing() { // Setup - Contact newContact1 = getContact(1); - Contact newContact2 = getContact(2); - - insert newContact1; + Contact newContact = getContact(1); + insert newContact; Exception expectedException = null; @@ -1638,12 +1382,11 @@ private class DML_Test { Test.startTest(); System.runAs(minimumAccessUser()) { try { - newContact1.FirstName = 'Updated Test Contact'; + newContact.FirstName = 'Updated Test Contact'; new DML() - .toUpsert(newContact1) - .toUpsert(newContact2) - .withSharing() + .toUpdate(newContact) .systemMode() + .withSharing() .commitWork(); } catch (Exception e) { expectedException = e; @@ -1657,18 +1400,19 @@ private class DML_Test { } @IsTest - static void toUpsertSingleRecordWhenIdIsNotSpecifiedWithMocking() { + static void toUpdateSingleRecordWithMocking() { // Setup Account account1 = getAccount(1); + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); - DML.mock('dmlMockId').allUpserts(); + DML.mock('dmlMockId').allUpdates(); // Test Test.startTest(); - account1.Name = 'Upserted Test Account'; + account1.Name = 'Updated Test Account'; new DML() - .toUpsert(account1) + .toUpdate(account1) .identifier('dmlMockId') .commitWork(); Test.stopTest(); @@ -1677,32 +1421,35 @@ private class DML_Test { // Verify Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); - Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); - Assert.areEqual(Account.SObjectType, result.upsertsOf(Account.SObjectType).objectType(), 'Upserted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Account.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); - Assert.areEqual(1, result.upsertsOf(Account.SObjectType).records().size(), 'Upserted operation result should contain the upserted records.'); - Assert.areEqual(1, result.upsertsOf(Account.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); - Assert.areEqual('Upserted Test Account', account1.Name, 'Account should be upserted.'); - Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); - Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); - Assert.isNotNull(account1.Id, 'Account should have mocked Id.'); + Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); + Assert.areEqual(Account.SObjectType, result.updatesOf(Account.SObjectType).objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Account.SObjectType).operationType(), 'Updated operation result should contain update type.'); + Assert.areEqual(1, result.updatesOf(Account.SObjectType).records().size(), 'Updated operation result should contain the updated record.'); + Assert.areEqual(1, result.updatesOf(Account.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.areEqual('Updated Test Account', account1.Name, 'Account should be updated.'); + Assert.isTrue(result.updatesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); + Assert.isNotNull(result.updatesOf(Account.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); } @IsTest - static void toUpsertSingleRecordWhenIdIsSpecifiedWithMocking() { + static void toUpdateMultipleRecordsWithMocking() { // Setup Account account1 = getAccount(1); + Account account2 = getAccount(2); account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + account2.Id = DML.randomIdGenerator.get(Account.SObjectType); - DML.mock('dmlMockId').allUpserts(); + DML.mock('dmlMockId').allUpdates(); // Test Test.startTest(); - account1.Name = 'Upserted Test Account'; + account1.Name = 'Updated Test Account 1'; + account2.Name = 'Updated Test Account 2'; new DML() - .toUpsert(account1) + .toUpdate(account1) + .toUpdate(account2) .identifier('dmlMockId') .commitWork(); Test.stopTest(); @@ -1711,33 +1458,38 @@ private class DML_Test { // Verify Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); - Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); - Assert.areEqual(Account.SObjectType, result.upsertsOf(Account.SObjectType).objectType(), 'Upserted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Account.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); - Assert.areEqual(1, result.upsertsOf(Account.SObjectType).records().size(), 'Upserted operation result should contain the upserted records.'); - Assert.areEqual(1, result.upsertsOf(Account.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); - Assert.areEqual('Upserted Test Account', account1.Name, 'Account should be upserted.'); - Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); - Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); - Assert.isNotNull(account1.Id, 'Account should have mocked Id.'); + Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); + Assert.areEqual(Account.SObjectType, result.updatesOf(Account.SObjectType).objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Account.SObjectType).operationType(), 'Updated operation result should contain update type.'); + Assert.areEqual(2, result.updatesOf(Account.SObjectType).records().size(), 'Updated operation result should contain the updated records.'); + Assert.areEqual(2, result.updatesOf(Account.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.isTrue(result.updatesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); + Assert.isTrue(result.updatesOf(Account.SObjectType).recordResults()[1].isSuccess(), 'Updated operation result should contain a successful record result.'); + Assert.isNotNull(result.updatesOf(Account.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); + Assert.isNotNull(result.updatesOf(Account.SObjectType).recordResults()[1].id(), 'Updated operation result should contain a mocked record Id.'); + Assert.areEqual('Updated Test Account 1', account1.Name, 'Account 1 should be updated.'); + Assert.areEqual('Updated Test Account 2', account2.Name, 'Account 2 should be updated.'); } @IsTest - static void toUpsertMultipleRecordsWithMocking() { + static void toUpdateMultipleRecordTypesWithMocking() { // Setup Account account1 = getAccount(1); - Account account2 = getAccount(2); + Contact contact1 = getContact(1); - DML.mock('dmlMockId').allUpserts(); + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + contact1.Id = DML.randomIdGenerator.get(Contact.SObjectType); + + DML.mock('dmlMockId').allUpdates(); // Test Test.startTest(); - account1.Name = 'Upserted Test Account 1'; - account2.Name = 'Upserted Test Account 2'; - + account1.Name = 'Updated Test Account 1'; + contact1.FirstName = 'Updated Test Contact 1'; + new DML() - .toUpsert(account1) - .toUpsert(account2) + .toUpdate(account1) + .toUpdate(contact1) .identifier('dmlMockId') .commitWork(); Test.stopTest(); @@ -1746,993 +1498,3043 @@ private class DML_Test { // Verify Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); - Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); - Assert.areEqual(Account.SObjectType, result.upsertsOf(Account.SObjectType).objectType(), 'Upserted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Account.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); - Assert.areEqual(2, result.upsertsOf(Account.SObjectType).records().size(), 'Upserted operation result should contain the upserted records.'); - Assert.areEqual(2, result.upsertsOf(Account.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); - Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); - Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[1].isSuccess(), 'Upserted operation result should contain a successful record result.'); - Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); - Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[1].id(), 'Upserted operation result should contain a mocked record Id.'); - Assert.areEqual('Upserted Test Account 1', account1.Name, 'Account 1 should be upserted.'); - Assert.areEqual('Upserted Test Account 2', account2.Name, 'Account 2 should be upserted.'); + Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'No records should be inserted to the database.'); + Assert.areEqual(2, result.updates().size(), 'Updated operation result should contain 2 results.'); + Assert.areEqual(Account.SObjectType, result.updatesOf(Account.SObjectType).objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(Contact.SObjectType, result.updatesOf(Contact.SObjectType).objectType(), 'Updated operation result should contain Contact object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Account.SObjectType).operationType(), 'Updated operation result should contain update type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Contact.SObjectType).operationType(), 'Updated operation result should contain update type.'); + Assert.areEqual(1, result.updatesOf(Account.SObjectType).records().size(), 'Updated operation result should contain the updated record.'); + Assert.areEqual(1, result.updatesOf(Account.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.areEqual(1, result.updatesOf(Contact.SObjectType).records().size(), 'Updated operation result should contain the updated record.'); + Assert.areEqual(1, result.updatesOf(Contact.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.isTrue(result.updatesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); + Assert.isTrue(result.updatesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); + Assert.isNotNull(result.updatesOf(Account.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); + Assert.isNotNull(result.updatesOf(Contact.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); + Assert.areEqual('Updated Test Account 1', account1.Name, 'Account 1 should be updated.'); + Assert.areEqual('Updated Test Contact 1', contact1.FirstName, 'Contact 1 should be updated.'); } @IsTest - static void toUpsertMultipleRecordTypesWithMocking() { + static void toUpdateMultipleRecordsWithMockingSpecificSObjectType() { // Setup Account account1 = getAccount(1); + insert account1; + Contact contact1 = getContact(1); + insert contact1; - DML.mock('dmlMockId').allUpserts(); + DML.mock('dmlMockId').updatesFor(Account.SObjectType); // Test Test.startTest(); - account1.Name = 'Upserted Test Account 1'; - contact1.FirstName = 'Upserted Test Contact 1'; - + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + account1.Name = 'Updated Test Account 1'; + contact1.FirstName = 'Updated Test Contact 1'; + new DML() - .toUpsert(account1) - .toUpsert(contact1) + .toUpdate(account1) + .toUpdate(contact1) .identifier('dmlMockId') .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); Test.stopTest(); DML.Result result = DML.retrieveResultFor('dmlMockId'); // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); - Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'No records should be inserted to the database.'); - Assert.areEqual(2, result.upserts().size(), 'Upserted operation result should contain 2 results.'); - Assert.areEqual(Account.SObjectType, result.upsertsOf(Account.SObjectType).objectType(), 'Upserted operation result should contain Account object type.'); - Assert.areEqual(Contact.SObjectType, result.upsertsOf(Contact.SObjectType).objectType(), 'Upserted operation result should contain Contact object type.'); - Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Account.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); - Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Contact.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); - Assert.areEqual(1, result.upsertsOf(Account.SObjectType).records().size(), 'Upserted operation result should contain the upserted record.'); - Assert.areEqual(1, result.upsertsOf(Account.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); - Assert.areEqual(1, result.upsertsOf(Contact.SObjectType).records().size(), 'Upserted operation result should contain the upserted record.'); - Assert.areEqual(1, result.upsertsOf(Contact.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); - Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); - Assert.isTrue(result.upsertsOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); - Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); - Assert.isNotNull(result.upsertsOf(Contact.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); - Assert.areEqual('Upserted Test Account 1', account1.Name, 'Account 1 should be updated.'); - Assert.areEqual('Upserted Test Contact 1', contact1.FirstName, 'Contact 1 should be updated.'); - } + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should not be updated in the database.'); + Assert.areEqual(1, [SELECT COUNT() FROM Contact], 'Contact should not be updated in the database.'); + Assert.areEqual(1, dmlStatementsAfter - dmlStatementsBefore, 'DML statements should be 1, because second commitWork() should not do anything.'); + Assert.areEqual(2, result.updates().size(), 'Updated operation result should contain 2 results.'); + Assert.areEqual(Account.SObjectType, result.updatesOf(Account.SObjectType).objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(Contact.SObjectType, result.updatesOf(Contact.SObjectType).objectType(), 'Updated operation result should contain Contact object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Account.SObjectType).operationType(), 'Updated operation result should contain update type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, result.updatesOf(Contact.SObjectType).operationType(), 'Updated operation result should contain update type.'); + Assert.areEqual(1, result.updatesOf(Account.SObjectType).records().size(), 'Updated operation result should contain the updated record.'); + Assert.areEqual(1, result.updatesOf(Account.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.areEqual(1, result.updatesOf(Contact.SObjectType).records().size(), 'Updated operation result should contain the updated record.'); + Assert.areEqual(1, result.updatesOf(Contact.SObjectType).recordResults().size(), 'Updated operation result should contain the updated record results.'); + Assert.isTrue(result.updatesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); + Assert.isTrue(result.updatesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Updated operation result should contain a successful record result.'); + Assert.isNotNull(result.updatesOf(Account.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); + Assert.isNotNull(result.updatesOf(Contact.SObjectType).recordResults()[0].id(), 'Updated operation result should contain a mocked record Id.'); + } @IsTest - static void toUpsertMultipleRecordsWithMockingSpecificSObjectType() { + static void toUpdateWithMockingException() { // Setup Account account1 = getAccount(1); - insert account1; + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); - Contact contact1 = getContact(1); + DML.mock('dmlMockId').exceptionOnUpdates(); - DML.mock('dmlMockId').upsertsFor(Contact.SObjectType); + Exception expectedException = null; // Test Test.startTest(); - Integer dmlStatementsBefore = Limits.getDMLStatements(); - - account1.Name = 'Upserted Test Account 1'; - contact1.FirstName = 'Upserted Test Contact 1'; - - new DML() - .toUpsert(account1) - .toUpsert(contact1) - .identifier('dmlMockId') - .commitWork(); - - Integer dmlStatementsAfter = Limits.getDMLStatements(); + try { + new DML() + .toUpdate(account1) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } Test.stopTest(); - DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should not be updated in the database.'); - Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'Contact should be upserted in the database.'); - Assert.areEqual(1, dmlStatementsAfter - dmlStatementsBefore, 'DML statements should be 1, because second commitWork() should not do anything.'); - Assert.areEqual(2, result.upserts().size(), 'Upserted operation result should contain 2 results.'); - Assert.areEqual(Account.SObjectType, result.upsertsOf(Account.SObjectType).objectType(), 'Upserted operation result should contain Account object type.'); - Assert.areEqual(Contact.SObjectType, result.upsertsOf(Contact.SObjectType).objectType(), 'Upserted operation result should contain Contact object type.'); - Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Account.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); - Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Contact.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); - Assert.areEqual(1, result.upsertsOf(Account.SObjectType).records().size(), 'Upserted operation result should contain the upserted record.'); - Assert.areEqual(1, result.upsertsOf(Account.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); - Assert.areEqual(1, result.upsertsOf(Contact.SObjectType).records().size(), 'Upserted operation result should contain the upserted record.'); - Assert.areEqual(1, result.upsertsOf(Contact.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); - Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); - Assert.isTrue(result.upsertsOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); - Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); - Assert.isNotNull(result.upsertsOf(Contact.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); - } - - // DELETE - - @IsTest - static void toDeleteSingleRecordById() { - // Setup - Account account = insertAccount(); - - // Test - Test.startTest(); - new DML() - .toDelete(account.Id) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); - } - - @IsTest - static void toDeleteSingleRecord() { - // Setup - Account account1 = insertAccount(); - - // Test - Test.startTest(); - DML.Result result = new DML() - .toDelete(account1) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } - Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); - Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); - Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); - Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); - Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); - Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + @IsTest + static void toUpdateWithMockingExceptionWhenAllOrNoneIsSet() { + // Setup + Account account1 = getAccount(1); + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + DML.mock('dmlMockId').exceptionOnUpdates(); - DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); + Exception expectedException = null; + DML.Result result = null; - Assert.areEqual(1, operationResult.records().size(), 'Deleted operation result should contain the deleted record.'); - Assert.areEqual(1, operationResult.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, operationResult.operationType(), 'Deleted operation result should contain delete type.'); - Assert.isFalse(operationResult.hasFailures(), 'Deleted operation result should not have failures.'); - } + // Test + Test.startTest(); + try { + result = new DML() + .toUpdate(account1) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.updatesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, operationResult.operationType(), 'Result should contain update type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); + } @IsTest - static void deleteImmediatelySingleRecord() { + static void toUpdateWithMockingExceptionForSpecificSObjectType() { // Setup Account account1 = getAccount(1); - insert account1; + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').exceptionOnUpdatesFor(Account.SObjectType); + + Exception expectedException = null; // Test Test.startTest(); - DML.OperationResult result = new DML().deleteImmediately(account1); + try { + new DML() + .toUpdate(account1) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } Test.stopTest(); // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Single record should be deleted.'); - Assert.areEqual(1, result.records().size(), 'Deleted operation result should contain the deleted record.'); - Assert.areEqual(1, result.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, result.operationType(), 'Deleted operation result should contain delete type.'); - Assert.isFalse(result.hasFailures(), 'Deleted operation result should not have failures.'); - } + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } @IsTest - static void deleteImmediatelySingleRecordById() { + static void toUpdateWithMockingExceptionForSpecificSObjectTypeWhenAllOrNoneIsSet() { // Setup Account account1 = getAccount(1); - insert account1; + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').exceptionOnUpdatesFor(Account.SObjectType); + + Exception expectedException = null; + DML.Result result = null; // Test Test.startTest(); - DML.OperationResult result = new DML().deleteImmediately(account1.Id); + try { + result = new DML() + .toUpdate(account1) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } Test.stopTest(); // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Single record should be deleted.'); - Assert.areEqual(1, result.records().size(), 'Deleted operation result should contain the deleted record.'); - Assert.areEqual(1, result.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, result.operationType(), 'Deleted operation result should contain delete type.'); - Assert.isFalse(result.hasFailures(), 'Deleted operation result should not have failures.'); - } + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); - @IsTest - static void toDeleteMultipleRecords() { - // Setup - Account account1 = getAccount(1); - Account account2 = getAccount(2); + DML.OperationResult operationResult = result.updatesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, operationResult.operationType(), 'Result should contain update type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); + } + + @IsTest + static void toUpdateWithEmptyRecords() { + // Test + Test.startTest(); + Integer dmlStatementsBefore = Limits.getDMLStatements(); - insert new List{ account1, account2 }; - - // Test - Test.startTest(); DML.Result result = new DML() - .toDelete(account1) - .toDelete(account2) - .commitWork(); - Test.stopTest(); + .toUpdate(new List()) + .commitWork(); - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be updated in the database.'); + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); - Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); - Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); + Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + DML.OperationResult operationResult = result.updatesOf(Account.SObjectType); - DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); - - Assert.areEqual(2, operationResult.records().size(), 'Deleted operation result should contain the deleted records.'); - Assert.areEqual(2, operationResult.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, operationResult.operationType(), 'Deleted operation result should contain delete type.'); - Assert.isFalse(operationResult.hasFailures(), 'Deleted operation result should not have failures.'); - } + Assert.areEqual(0, operationResult.records().size(), 'Updated operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Updated operation result should contain 0 record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, operationResult.operationType(), 'Updated operation result should contain update type.'); + Assert.isFalse(operationResult.hasFailures(), 'Updated operation result should not have failures.'); + } @IsTest - static void deleteImmediatelyMultipleRecords() { + static void toUpdateWithEmptyRecordsWhenMocking() { // Setup - Account account1 = getAccount(1); - Account account2 = getAccount(2); - insert new List{ account1, account2 }; + DML.mock('dmlMockId').allUpdates(); // Test Test.startTest(); - DML.OperationResult result = new DML().deleteImmediately(new List{ account1, account2 }); + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + new DML() + .toUpdate(new List()) + .identifier('dmlMockId') + .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); Test.stopTest(); // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be updated in the database.'); + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); - Assert.areEqual(2, result.records().size(), 'Deleted operation result should contain the deleted records.'); - Assert.areEqual(2, result.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, result.operationType(), 'Deleted operation result should contain delete type.'); - Assert.isFalse(result.hasFailures(), 'Deleted operation result should not have failures.'); - } + DML.Result result = DML.retrieveResultFor('dmlMockId'); - @IsTest - static void deleteImmediatelyMultipleRecordsByIds() { - // Setup - Account account1 = getAccount(1); - Account account2 = getAccount(2); + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(1, result.updates().size(), 'Updated operation result should contain 1 result.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); - insert new List{ account1, account2 }; - - // Test - Test.startTest(); - DML.OperationResult result = new DML() - .deleteImmediately(new List{ account1.Id, account2.Id }); - Test.stopTest(); + DML.OperationResult operationResult = result.updatesOf(Account.SObjectType); + + Assert.areEqual(0, operationResult.records().size(), 'Updated operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Updated operation result should contain 0 record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Updated operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPDATE_DML, operationResult.operationType(), 'Updated operation result should contain update type.'); + Assert.isFalse(operationResult.hasFailures(), 'Updated operation result should not have failures.'); + } - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + // UPSERT - Assert.areEqual(2, result.records().size(), 'Deleted operation result should contain the deleted records.'); - Assert.areEqual(2, result.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, result.operationType(), 'Deleted operation result should contain delete type.'); - Assert.isFalse(result.hasFailures(), 'Deleted operation result should not have failures.'); - } + @IsTest + static void toUpsertSingleExistingRecord() { + // Setup + Account account1 = getAccount(1); + insert account1; + + // Test + Test.startTest(); + account1.Name = 'Updated Test Account'; - @IsTest - static void toDeleteListOfRecords() { - // Setup - List accounts = insertAccounts(); + DML.Result result = new DML() + .toUpsert(account1) + .commitWork(); + Test.stopTest(); - // Test - Test.startTest(); - new DML() - .toDelete(accounts) - .commitWork(); - Test.stopTest(); + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be updated.'); - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); - } + Assert.areEqual('Updated Test Account', account1.Name, 'Account should be updated.'); - @IsTest - static void toDeleteMultipleRecordsById() { - // Setup - List accounts = insertAccounts(); + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); - // Test - Test.startTest(); - Set accountIds = new Map(accounts).keySet(); - new DML() - .toDelete(accountIds) - .commitWork(); - Test.stopTest(); + DML.OperationResult operationResult = result.upsertsOf(Account.SObjectType); - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); - } + Assert.areEqual(1, operationResult.records().size(), 'Upserted operation result should contain the upserted records.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Upserted operation result should contain the upserted record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Upserted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, operationResult.operationType(), 'Upserted operation result should contain upsert type.'); + Assert.isFalse(operationResult.hasFailures(), 'Upserted operation result should not have failures.'); + } @IsTest - static void toDeleteMultipleRecordsTypes() { + static void upsertImmediatelySingleRecord() { // Setup Account account1 = getAccount(1); - Opportunity opportunity1 = getOpportunity(1); - Lead lead1 = getLead(1); + insert account1; + account1.Name = 'Upserted Test Account'; - insert new List{ account1, opportunity1, lead1 }; + // Test + Test.startTest(); + DML.OperationResult result = new DML().upsertImmediately(account1); + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account WHERE Name = 'Upserted Test Account'], 'Single record should be upserted.'); + Assert.areEqual('Upserted Test Account', account1.Name, 'Account should be upserted.'); + Assert.areEqual(1, result.records().size(), 'Upserted operation result should contain the upserted record.'); + Assert.areEqual(1, result.recordResults().size(), 'Upserted operation result should contain the upserted record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Upserted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, result.operationType(), 'Upserted operation result should contain upsert type.'); + Assert.isFalse(result.hasFailures(), 'Upserted operation result should not have failures.'); + } + + + @IsTest + static void toUpsertSingleNewRecord() { + // Setup + Account account1 = getAccount(1); // Test Test.startTest(); DML.Result result = new DML() - .toDelete(account1) - .toDelete(opportunity1) - .toDelete(lead1) + .toUpsert(account1) .commitWork(); - Test.stopTest(); + Test.stopTest(); // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); - Assert.areEqual(0, [SELECT COUNT() FROM Opportunity], 'Opportunity should be deleted.'); - Assert.areEqual(0, [SELECT COUNT() FROM Lead], 'Lead should be deleted.'); + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be inserted.'); + + Assert.isNotNull(account1.Id, 'Account should be inserted and have an Id.'); Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); - Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); - Assert.areEqual(3, result.deletes().size(), 'Deleted operation result should contain 3 results.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); - DML.OperationResult accountOperationResult = result.deletesOf(Account.SObjectType); + DML.OperationResult operationResult = result.upsertsOf(Account.SObjectType); - Assert.areEqual(1, accountOperationResult.records().size(), 'Deleted operation result should contain the deleted record.'); - Assert.areEqual(1, accountOperationResult.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.areEqual(Account.SObjectType, accountOperationResult.objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, accountOperationResult.operationType(), 'Deleted operation result should contain delete type.'); - Assert.isFalse(accountOperationResult.hasFailures(), 'Deleted operation result should not have failures.'); + Assert.areEqual(1, operationResult.records().size(), 'Upserted operation result should contain the upserted records.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Upserted operation result should contain the upserted record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Upserted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, operationResult.operationType(), 'Upserted operation result should contain upsert type.'); + Assert.isFalse(operationResult.hasFailures(), 'Upserted operation result should not have failures.'); + } - DML.OperationResult opportunityOperationResult = result.deletesOf(Opportunity.SObjectType); + @IsTest + static void toUpsertMultipleExistingRecords() { + // Setup + Account account1 = getAccount(1); + Account account2 = getAccount(2); - Assert.areEqual(1, opportunityOperationResult.records().size(), 'Deleted operation result should contain the deleted record.'); - Assert.areEqual(1, opportunityOperationResult.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.areEqual(Opportunity.SObjectType, opportunityOperationResult.objectType(), 'Deleted operation result should contain Opportunity object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, opportunityOperationResult.operationType(), 'Deleted operation result should contain delete type.'); - Assert.isFalse(opportunityOperationResult.hasFailures(), 'Deleted operation result should not have failures.'); + insert new List{ account1, account2 }; - DML.OperationResult leadOperationResult = result.deletesOf(Lead.SObjectType); + // Test + Test.startTest(); + account1.Name = 'Updated Test Account 1'; + account2.Name = 'Updated Test Account 2'; - Assert.areEqual(1, leadOperationResult.records().size(), 'Deleted operation result should contain the deleted record.'); - Assert.areEqual(1, leadOperationResult.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.areEqual(Lead.SObjectType, leadOperationResult.objectType(), 'Deleted operation result should contain Lead object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, leadOperationResult.operationType(), 'Deleted operation result should contain delete type.'); - Assert.isFalse(leadOperationResult.hasFailures(), 'Deleted operation result should not have failures.'); - } + new DML() + .toUpsert(account1) + .toUpsert(account2) + .commitWork(); + Test.stopTest(); - @IsTest - static void toDeleteWithoutExistingIds() { - // Setup - Account account = getAccount(1); - - DmlException expectedException = null; - - // Test - Test.startTest(); - try { - new DML() - .toDelete(account) - .commitWork(); - } catch (DmlException e) { - expectedException = e; - } - Test.stopTest(); - - // Verify - Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.areEqual('Only existing records can be registered as deleted.', expectedException.getMessage(), 'Expected exception message should be thrown.'); - } - - @IsTest - static void toDeleteWithPartialSuccess() { - // Setup - List accounts = new List{ - getAccount(1), - getAccount(2) - }; + // Verify + Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be updated.'); + + Assert.areEqual('Updated Test Account 1', account1.Name, 'Account 1 should be updated.'); + Assert.areEqual('Updated Test Account 2', account2.Name, 'Account 2 should be updated.'); + } + + @IsTest + static void toUpsertMultipleNewRecords() { + // Setup + Account account1 = getAccount(1); + Account account2 = getAccount(2); - insert accounts; + // Test + Test.startTest(); + new DML() + .toUpsert(account1) + .toUpsert(account2) + .commitWork(); + Test.stopTest(); - // Test - Test.startTest(); - new DML() - .toDelete(accounts) - .allowPartialSuccess() - .commitWork(); - Test.stopTest(); + // Verify + Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be inserted.'); - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); - } + Assert.isNotNull(account1.Id, 'Account 1 should be inserted and have an Id.'); + Assert.isNotNull(account2.Id, 'Account 2 should be inserted and have an Id.'); + } @IsTest - static void toDeleteWithUserMode() { + static void upsertImmediatelyMultipleRecords() { // Setup - Case newCase = getCase(1); - insert newCase; - - Exception expectedException = null; + Account account1 = getAccount(1); + Account account2 = getAccount(2); + insert account1; // Make account1 existing for upsert + + account1.Name = 'Upserted Test Account 1'; + // account2 is new for upsert // Test Test.startTest(); - System.runAs(minimumAccessUser()) { - try { - new DML() - .toDelete(newCase) - .commitWork(); // user mode by default - } catch (Exception e) { - expectedException = e; - } - } + DML.OperationResult result = new DML().upsertImmediately(new List{ account1, account2 }); Test.stopTest(); // Verify - Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); + Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be upserted.'); + + Assert.areEqual('Upserted Test Account 1', account1.Name, 'Account 1 should be updated.'); + Assert.isNotNull(account2.Id, 'Account 2 should be inserted and have an Id.'); + + Assert.areEqual(2, result.records().size(), 'Upserted operation result should contain the upserted records.'); + Assert.areEqual(2, result.recordResults().size(), 'Upserted operation result should contain the upserted record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Upserted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, result.operationType(), 'Upserted operation result should contain upsert type.'); + Assert.isFalse(result.hasFailures(), 'Upserted operation result should not have failures.'); } @IsTest - static void toDeleteWithUserModeExplicitlySet() { + static void toUpsertExistingAndNewRecords() { // Setup - Case newCase = getCase(1); - insert newCase; - - Exception expectedException = null; + Account account1 = getAccount(1); + Account account2 = getAccount(2); + insert account1; + // Test Test.startTest(); - System.runAs(minimumAccessUser()) { - try { - new DML() - .toDelete(newCase) - .userMode() - .commitWork(); - } catch (Exception e) { - expectedException = e; - } - } + account1.Name = 'Updated Test Account 1'; + + new DML() + .toUpsert(account1) + .toUpsert(account2) + .commitWork(); Test.stopTest(); + + // Verify + Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Both accounts should be upserted.'); + + Assert.areEqual('Updated Test Account 1', account1.Name, 'Account 1 should be updated.'); + Assert.isNotNull(account2.Id, 'Account 2 should be inserted and have an Id.'); + } + + @IsTest + static void toUpsertListOfRecords() { + // Setup + List existingAccounts = new List{ + getAccount(1), + getAccount(2), + getAccount(3) + }; + insert existingAccounts; + + // Test + Test.startTest(); + existingAccounts[0].Name = 'Updated Test Account 1'; + existingAccounts[1].Name = 'Updated Test Account 2'; + existingAccounts[2].Name = 'Updated Test Account 3'; + + List newAccounts = new List{ + getAccount(1), + getAccount(2), + getAccount(3) + }; + new DML() + .toUpsert(existingAccounts) + .toUpsert(newAccounts) + .commitWork(); + Test.stopTest(); + // Verify - Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); + Assert.areEqual(6, [SELECT COUNT() FROM Account], 'Accounts should be updated and inserted.'); + + Assert.areEqual('Updated Test Account 1', existingAccounts[0].Name, 'Account 1 should be updated.'); + Assert.areEqual('Updated Test Account 2', existingAccounts[1].Name, 'Account 2 should be updated.'); + Assert.areEqual('Updated Test Account 3', existingAccounts[2].Name, 'Account 3 should be updated.'); + + Assert.isNotNull(newAccounts[0].Id, 'New Account 1 should be inserted and have an Id.'); + Assert.isNotNull(newAccounts[1].Id, 'New Account 2 should be inserted and have an Id.'); + Assert.isNotNull(newAccounts[2].Id, 'New Account 3 should be inserted and have an Id.'); } @IsTest - static void toDeleteWithSystemMode() { + static void toUpsertWithPartialSuccess() { // Setup - Case newCase = getCase(1); - insert newCase; + List accounts = new List{ + getAccount(1), + getAccount(2) + }; + insert accounts; + + // Test + Test.startTest(); + accounts[0].Name = null; // Name will not change, because it's set to null + accounts[1].Name = 'Test Account 1 New Name'; + + accounts.add(getAccount(3)); // New account + + new DML() + .toUpsert(accounts) + .allowPartialSuccess() + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(3, [SELECT COUNT() FROM Account WHERE Name IN ('Test Account 1', 'Test Account 1 New Name', 'Test Account 3')], 'Accounts should be upserted with expected names.'); + } + + @IsTest + static void toUpsertWithUserMode() { + // Setup + Case newCase1 = getCase(1); + Case newCase2 = getCase(2); + + insert newCase1; + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + newCase1.Subject = 'Updated Test Case'; + + new DML() + .toUpsert(newCase1) + .toUpsert(newCase2) + .commitWork(); // user mode by default + } catch (Exception e) { + expectedException = e; + } + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.isTrue(expectedException.getMessage().contains('Operation failed due to fields being inaccessible on Sobject Case, check errors on Exception or Result'), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toUpsertWithUserModeExplicitlySet() { + // Setup + Case newCase1 = getCase(1); + Case newCase2 = getCase(2); + + insert newCase1; + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + newCase1.Subject = 'Updated Test Case'; + + new DML() + .toUpsert(newCase1) + .toUpsert(newCase2) + .userMode() + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.isTrue(expectedException.getMessage().contains('Operation failed due to fields being inaccessible on Sobject Case, check errors on Exception or Result'), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toUpsertWithSystemMode() { + // Setup + Case newCase1 = getCase(1); + Case newCase2 = getCase(2); + + insert newCase1; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + newCase1.Subject = 'Updated Test Case'; + + new DML() + .toUpsert(newCase1) + .toUpsert(newCase2) + .systemMode() + .withoutSharing() + .commitWork(); + } + Test.stopTest(); + + // Verify + Assert.areEqual(2, [SELECT COUNT() FROM Case], 'Cases should be upserted.'); + + Assert.isNotNull(newCase1.Id, 'Case 1 should be upserted and have an Id.'); + Assert.isNotNull(newCase2.Id, 'Case 2 should be upserted and have an Id.'); + } + + @IsTest + static void toUpsertWithSystemModeAndWithSharing() { + // Setup + Contact newContact1 = getContact(1); + Contact newContact2 = getContact(2); + + insert newContact1; + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + newContact1.FirstName = 'Updated Test Contact'; + new DML() + .toUpsert(newContact1) + .toUpsert(newContact2) + .withSharing() + .systemMode() + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.isTrue(expectedException.getMessage().contains('insufficient access rights on cross-reference id'), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toUpsertSingleRecordWhenIdIsNotSpecifiedWithMocking() { + // Setup + Account account1 = getAccount(1); + + DML.mock('dmlMockId').allUpserts(); + + // Test + Test.startTest(); + account1.Name = 'Upserted Test Account'; + + new DML() + .toUpsert(account1) + .identifier('dmlMockId') + .commitWork(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); + Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); + Assert.areEqual(Account.SObjectType, result.upsertsOf(Account.SObjectType).objectType(), 'Upserted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Account.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); + Assert.areEqual(1, result.upsertsOf(Account.SObjectType).records().size(), 'Upserted operation result should contain the upserted records.'); + Assert.areEqual(1, result.upsertsOf(Account.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); + Assert.areEqual('Upserted Test Account', account1.Name, 'Account should be upserted.'); + Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); + Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); + Assert.isNotNull(account1.Id, 'Account should have mocked Id.'); + } + + @IsTest + static void toUpsertSingleRecordWhenIdIsSpecifiedWithMocking() { + // Setup + Account account1 = getAccount(1); + + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').allUpserts(); + + // Test + Test.startTest(); + account1.Name = 'Upserted Test Account'; + + new DML() + .toUpsert(account1) + .identifier('dmlMockId') + .commitWork(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); + Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); + Assert.areEqual(Account.SObjectType, result.upsertsOf(Account.SObjectType).objectType(), 'Upserted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Account.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); + Assert.areEqual(1, result.upsertsOf(Account.SObjectType).records().size(), 'Upserted operation result should contain the upserted records.'); + Assert.areEqual(1, result.upsertsOf(Account.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); + Assert.areEqual('Upserted Test Account', account1.Name, 'Account should be upserted.'); + Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); + Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); + Assert.isNotNull(account1.Id, 'Account should have mocked Id.'); + } + + @IsTest + static void toUpsertMultipleRecordsWithMocking() { + // Setup + Account account1 = getAccount(1); + Account account2 = getAccount(2); + + DML.mock('dmlMockId').allUpserts(); + + // Test + Test.startTest(); + account1.Name = 'Upserted Test Account 1'; + account2.Name = 'Upserted Test Account 2'; + + new DML() + .toUpsert(account1) + .toUpsert(account2) + .identifier('dmlMockId') + .commitWork(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); + Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); + Assert.areEqual(Account.SObjectType, result.upsertsOf(Account.SObjectType).objectType(), 'Upserted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Account.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); + Assert.areEqual(2, result.upsertsOf(Account.SObjectType).records().size(), 'Upserted operation result should contain the upserted records.'); + Assert.areEqual(2, result.upsertsOf(Account.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); + Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); + Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[1].isSuccess(), 'Upserted operation result should contain a successful record result.'); + Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); + Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[1].id(), 'Upserted operation result should contain a mocked record Id.'); + Assert.areEqual('Upserted Test Account 1', account1.Name, 'Account 1 should be upserted.'); + Assert.areEqual('Upserted Test Account 2', account2.Name, 'Account 2 should be upserted.'); + } + + @IsTest + static void toUpsertMultipleRecordTypesWithMocking() { + // Setup + Account account1 = getAccount(1); + Contact contact1 = getContact(1); + + DML.mock('dmlMockId').allUpserts(); + + // Test + Test.startTest(); + account1.Name = 'Upserted Test Account 1'; + contact1.FirstName = 'Upserted Test Contact 1'; + + new DML() + .toUpsert(account1) + .toUpsert(contact1) + .identifier('dmlMockId') + .commitWork(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); + Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'No records should be inserted to the database.'); + Assert.areEqual(2, result.upserts().size(), 'Upserted operation result should contain 2 results.'); + Assert.areEqual(Account.SObjectType, result.upsertsOf(Account.SObjectType).objectType(), 'Upserted operation result should contain Account object type.'); + Assert.areEqual(Contact.SObjectType, result.upsertsOf(Contact.SObjectType).objectType(), 'Upserted operation result should contain Contact object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Account.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Contact.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); + Assert.areEqual(1, result.upsertsOf(Account.SObjectType).records().size(), 'Upserted operation result should contain the upserted record.'); + Assert.areEqual(1, result.upsertsOf(Account.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); + Assert.areEqual(1, result.upsertsOf(Contact.SObjectType).records().size(), 'Upserted operation result should contain the upserted record.'); + Assert.areEqual(1, result.upsertsOf(Contact.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); + Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); + Assert.isTrue(result.upsertsOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); + Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); + Assert.isNotNull(result.upsertsOf(Contact.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); + Assert.areEqual('Upserted Test Account 1', account1.Name, 'Account 1 should be updated.'); + Assert.areEqual('Upserted Test Contact 1', contact1.FirstName, 'Contact 1 should be updated.'); + } + + @IsTest + static void toUpsertMultipleRecordsWithMockingSpecificSObjectType() { + // Setup + Account account1 = getAccount(1); + insert account1; + + Contact contact1 = getContact(1); + + DML.mock('dmlMockId').upsertsFor(Contact.SObjectType); + + // Test + Test.startTest(); + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + account1.Name = 'Upserted Test Account 1'; + contact1.FirstName = 'Upserted Test Contact 1'; + + new DML() + .toUpsert(account1) + .toUpsert(contact1) + .identifier('dmlMockId') + .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should not be updated in the database.'); + Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'Contact should be upserted in the database.'); + Assert.areEqual(1, dmlStatementsAfter - dmlStatementsBefore, 'DML statements should be 1, because second commitWork() should not do anything.'); + Assert.areEqual(2, result.upserts().size(), 'Upserted operation result should contain 2 results.'); + Assert.areEqual(Account.SObjectType, result.upsertsOf(Account.SObjectType).objectType(), 'Upserted operation result should contain Account object type.'); + Assert.areEqual(Contact.SObjectType, result.upsertsOf(Contact.SObjectType).objectType(), 'Upserted operation result should contain Contact object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Account.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, result.upsertsOf(Contact.SObjectType).operationType(), 'Upserted operation result should contain upsert type.'); + Assert.areEqual(1, result.upsertsOf(Account.SObjectType).records().size(), 'Upserted operation result should contain the upserted record.'); + Assert.areEqual(1, result.upsertsOf(Account.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); + Assert.areEqual(1, result.upsertsOf(Contact.SObjectType).records().size(), 'Upserted operation result should contain the upserted record.'); + Assert.areEqual(1, result.upsertsOf(Contact.SObjectType).recordResults().size(), 'Upserted operation result should contain the upserted record results.'); + Assert.isTrue(result.upsertsOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); + Assert.isTrue(result.upsertsOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Upserted operation result should contain a successful record result.'); + Assert.isNotNull(result.upsertsOf(Account.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); + Assert.isNotNull(result.upsertsOf(Contact.SObjectType).recordResults()[0].id(), 'Upserted operation result should contain a mocked record Id.'); + } + + @IsTest + static void toUpsertWithMockingException() { + // Setup + Account account1 = getAccount(1); + + DML.mock('dmlMockId').exceptionOnUpserts(); + + Exception expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toUpsert(account1) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } + + @IsTest + static void toUpsertWithMockingExceptionWhenAllOrNoneIsSet() { + // Setup + Account account1 = getAccount(1); + + DML.mock('dmlMockId').exceptionOnUpserts(); + + Exception expectedException = null; + DML.Result result = null; + + // Test + Test.startTest(); + try { + result = new DML() + .toUpsert(account1) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.upsertsOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, operationResult.operationType(), 'Result should contain upsert type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); + } + + @IsTest + static void toUpsertWithMockingExceptionForSpecificSObjectType() { + // Setup + Account account1 = getAccount(1); + + DML.mock('dmlMockId').exceptionOnUpsertsFor(Account.SObjectType); + + Exception expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toUpsert(account1) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } + + @IsTest + static void toUpsertWithMockingExceptionForSpecificSObjectTypeWhenAllOrNoneIsSet() { + // Setup + Account account1 = getAccount(1); + + DML.mock('dmlMockId').exceptionOnUpsertsFor(Account.SObjectType); + + Exception expectedException = null; + DML.Result result = null; + + // Test + Test.startTest(); + try { + result = new DML() + .toUpsert(account1) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.upsertsOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, operationResult.operationType(), 'Result should contain upsert type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); + } + + @IsTest + static void toUpsertWithEmptyRecords() { + // Test + Test.startTest(); + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + DML.Result result = new DML() + .toUpsert(new List()) + .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be upserted in the database.'); + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + DML.OperationResult operationResult = result.upsertsOf(Account.SObjectType); + + Assert.areEqual(0, operationResult.records().size(), 'Upserted operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Upserted operation result should contain 0 record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Upserted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, operationResult.operationType(), 'Upserted operation result should contain upsert type.'); + Assert.isFalse(operationResult.hasFailures(), 'Upserted operation result should not have failures.'); + } + + @IsTest + static void toUpsertWithEmptyRecordsWhenMocking() { + // Setup + DML.mock('dmlMockId').allUpserts(); + + // Test + Test.startTest(); + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + new DML() + .toUpsert(new List()) + .identifier('dmlMockId') + .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be upserted in the database.'); + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(1, result.upserts().size(), 'Upserted operation result should contain 1 result.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + DML.OperationResult operationResult = result.upsertsOf(Account.SObjectType); + + Assert.areEqual(0, operationResult.records().size(), 'Upserted operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Upserted operation result should contain 0 record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Upserted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UPSERT_DML, operationResult.operationType(), 'Upserted operation result should contain upsert type.'); + Assert.isFalse(operationResult.hasFailures(), 'Upserted operation result should not have failures.'); + } + + // DELETE + + @IsTest + static void toDeleteSingleRecordById() { + // Setup + Account account = insertAccount(); + + // Test + Test.startTest(); + new DML() + .toDelete(account.Id) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); + } + + @IsTest + static void toDeleteSingleRecord() { + // Setup + Account account1 = insertAccount(); + + // Test + Test.startTest(); + DML.Result result = new DML() + .toDelete(account1) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + + DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Deleted operation result should contain the deleted record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, operationResult.operationType(), 'Deleted operation result should contain delete type.'); + Assert.isFalse(operationResult.hasFailures(), 'Deleted operation result should not have failures.'); + } + + @IsTest + static void deleteImmediatelySingleRecord() { + // Setup + Account account1 = getAccount(1); + insert account1; + + // Test + Test.startTest(); + DML.OperationResult result = new DML().deleteImmediately(account1); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Single record should be deleted.'); + Assert.areEqual(1, result.records().size(), 'Deleted operation result should contain the deleted record.'); + Assert.areEqual(1, result.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, result.operationType(), 'Deleted operation result should contain delete type.'); + Assert.isFalse(result.hasFailures(), 'Deleted operation result should not have failures.'); + } + + @IsTest + static void deleteImmediatelySingleRecordById() { + // Setup + Account account1 = getAccount(1); + insert account1; + + // Test + Test.startTest(); + DML.OperationResult result = new DML().deleteImmediately(account1.Id); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Single record should be deleted.'); + Assert.areEqual(1, result.records().size(), 'Deleted operation result should contain the deleted record.'); + Assert.areEqual(1, result.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, result.operationType(), 'Deleted operation result should contain delete type.'); + Assert.isFalse(result.hasFailures(), 'Deleted operation result should not have failures.'); + } + + @IsTest + static void toDeleteMultipleRecords() { + // Setup + Account account1 = getAccount(1); + Account account2 = getAccount(2); + + insert new List{ account1, account2 }; + + // Test + Test.startTest(); + DML.Result result = new DML() + .toDelete(account1) + .toDelete(account2) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + + DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); + + Assert.areEqual(2, operationResult.records().size(), 'Deleted operation result should contain the deleted records.'); + Assert.areEqual(2, operationResult.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, operationResult.operationType(), 'Deleted operation result should contain delete type.'); + Assert.isFalse(operationResult.hasFailures(), 'Deleted operation result should not have failures.'); + } + + @IsTest + static void deleteImmediatelyMultipleRecords() { + // Setup + Account account1 = getAccount(1); + Account account2 = getAccount(2); + insert new List{ account1, account2 }; + + // Test + Test.startTest(); + DML.OperationResult result = new DML().deleteImmediately(new List{ account1, account2 }); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + + Assert.areEqual(2, result.records().size(), 'Deleted operation result should contain the deleted records.'); + Assert.areEqual(2, result.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, result.operationType(), 'Deleted operation result should contain delete type.'); + Assert.isFalse(result.hasFailures(), 'Deleted operation result should not have failures.'); + } + + @IsTest + static void deleteImmediatelyMultipleRecordsByIds() { + // Setup + Account account1 = getAccount(1); + Account account2 = getAccount(2); + + insert new List{ account1, account2 }; + + // Test + Test.startTest(); + DML.OperationResult result = new DML() + .deleteImmediately(new List{ account1.Id, account2.Id }); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + + Assert.areEqual(2, result.records().size(), 'Deleted operation result should contain the deleted records.'); + Assert.areEqual(2, result.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, result.operationType(), 'Deleted operation result should contain delete type.'); + Assert.isFalse(result.hasFailures(), 'Deleted operation result should not have failures.'); + } + + @IsTest + static void toDeleteListOfRecords() { + // Setup + List accounts = insertAccounts(); + + // Test + Test.startTest(); + new DML() + .toDelete(accounts) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + } + + @IsTest + static void toDeleteMultipleRecordsById() { + // Setup + List accounts = insertAccounts(); + + // Test + Test.startTest(); + Set accountIds = new Map(accounts).keySet(); + + new DML() + .toDelete(accountIds) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + } + + @IsTest + static void toDeleteMultipleRecordsTypes() { + // Setup + Account account1 = getAccount(1); + Opportunity opportunity1 = getOpportunity(1); + Lead lead1 = getLead(1); + + insert new List{ account1, opportunity1, lead1 }; + + // Test + Test.startTest(); + DML.Result result = new DML() + .toDelete(account1) + .toDelete(opportunity1) + .toDelete(lead1) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); + Assert.areEqual(0, [SELECT COUNT() FROM Opportunity], 'Opportunity should be deleted.'); + Assert.areEqual(0, [SELECT COUNT() FROM Lead], 'Lead should be deleted.'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(3, result.deletes().size(), 'Deleted operation result should contain 3 results.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + + DML.OperationResult accountOperationResult = result.deletesOf(Account.SObjectType); + + Assert.areEqual(1, accountOperationResult.records().size(), 'Deleted operation result should contain the deleted record.'); + Assert.areEqual(1, accountOperationResult.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.areEqual(Account.SObjectType, accountOperationResult.objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, accountOperationResult.operationType(), 'Deleted operation result should contain delete type.'); + Assert.isFalse(accountOperationResult.hasFailures(), 'Deleted operation result should not have failures.'); + + DML.OperationResult opportunityOperationResult = result.deletesOf(Opportunity.SObjectType); + + Assert.areEqual(1, opportunityOperationResult.records().size(), 'Deleted operation result should contain the deleted record.'); + Assert.areEqual(1, opportunityOperationResult.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.areEqual(Opportunity.SObjectType, opportunityOperationResult.objectType(), 'Deleted operation result should contain Opportunity object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, opportunityOperationResult.operationType(), 'Deleted operation result should contain delete type.'); + Assert.isFalse(opportunityOperationResult.hasFailures(), 'Deleted operation result should not have failures.'); + + DML.OperationResult leadOperationResult = result.deletesOf(Lead.SObjectType); + + Assert.areEqual(1, leadOperationResult.records().size(), 'Deleted operation result should contain the deleted record.'); + Assert.areEqual(1, leadOperationResult.recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.areEqual(Lead.SObjectType, leadOperationResult.objectType(), 'Deleted operation result should contain Lead object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, leadOperationResult.operationType(), 'Deleted operation result should contain delete type.'); + Assert.isFalse(leadOperationResult.hasFailures(), 'Deleted operation result should not have failures.'); + } + + @IsTest + static void toDeleteWithoutExistingIds() { + // Setup + Account account = getAccount(1); + + DmlException expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toDelete(account) + .commitWork(); + } catch (DmlException e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.areEqual('Only existing records can be registered as deleted.', expectedException.getMessage(), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toDeleteWithPartialSuccess() { + // Setup + List accounts = new List{ + getAccount(1), + getAccount(2) + }; + + insert accounts; + + // Test + Test.startTest(); + new DML() + .toDelete(accounts) + .allowPartialSuccess() + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + } + + @IsTest + static void toDeleteWithUserMode() { + // Setup + Case newCase = getCase(1); + insert newCase; + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + new DML() + .toDelete(newCase) + .commitWork(); // user mode by default + } catch (Exception e) { + expectedException = e; + } + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toDeleteWithUserModeExplicitlySet() { + // Setup + Case newCase = getCase(1); + insert newCase; + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + new DML() + .toDelete(newCase) + .userMode() + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toDeleteWithSystemMode() { + // Setup + Case newCase = getCase(1); + insert newCase; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + new DML() + .toDelete(newCase) + .systemMode() + .withoutSharing() + .commitWork(); + } + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Cases should be deleted.'); + } + + @IsTest + static void toDeleteWithSystemModeAndWithSharing() { + // Setup + Case newCase = getCase(1); + insert newCase; + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + new DML() + .toDelete(newCase) + .systemMode() + .withoutSharing() + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + } + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Case should be deleted with system mode and without sharing.'); + Assert.isNull(expectedException, 'No exception should be thrown.'); + } + + @IsTest + static void toDeleteSingleRecordWithMocking() { + // Setup + Account account1 = getAccount(1); + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').allDeletes(); + + // Test + Test.startTest(); + new DML() + .toDelete(account1) + .identifier('dmlMockId') + .commitWork(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); + Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); + Assert.areEqual(Account.SObjectType, result.deletesOf(Account.SObjectType).objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Account.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); + Assert.areEqual(1, result.deletesOf(Account.SObjectType).records().size(), 'Deleted operation result should contain the deleted record.'); + Assert.areEqual(1, result.deletesOf(Account.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.isTrue(result.deletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); + Assert.isNotNull(result.deletesOf(Account.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); + } + + @IsTest + static void toDeleteMultipleRecordsWithMocking() { + // Setup + Account account1 = getAccount(1); + Account account2 = getAccount(2); + + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + account2.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').allDeletes(); + + // Test + Test.startTest(); + new DML() + .toDelete(account1) + .toDelete(account2) + .identifier('dmlMockId') + .commitWork(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); + Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); + Assert.areEqual(Account.SObjectType, result.deletesOf(Account.SObjectType).objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Account.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); + Assert.areEqual(2, result.deletesOf(Account.SObjectType).records().size(), 'Deleted operation result should contain the deleted records.'); + Assert.areEqual(2, result.deletesOf(Account.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.isTrue(result.deletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); + Assert.isTrue(result.deletesOf(Account.SObjectType).recordResults()[1].isSuccess(), 'Deleted operation result should contain a successful record result.'); + Assert.isNotNull(result.deletesOf(Account.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); + Assert.isNotNull(result.deletesOf(Account.SObjectType).recordResults()[1].id(), 'Deleted operation result should contain a mocked record Id.'); + } + + @IsTest + static void toDeleteMultipleRecordTypesWithMocking() { + // Setup + Account account1 = getAccount(1); + Contact contact1 = getContact(1); + + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + contact1.Id = DML.randomIdGenerator.get(Contact.SObjectType); + + DML.mock('dmlMockId').allDeletes(); + + // Test + Test.startTest(); + new DML() + .toDelete(account1) + .toDelete(contact1) + .identifier('dmlMockId') + .commitWork(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No Account records should exist, because delete was mocked.'); + Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'No Contact records should exist, because delete was mocked.'); + Assert.areEqual(2, result.deletes().size(), 'Deleted operation result should contain 2 results.'); + Assert.areEqual(Account.SObjectType, result.deletesOf(Account.SObjectType).objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(Contact.SObjectType, result.deletesOf(Contact.SObjectType).objectType(), 'Deleted operation result should contain Contact object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Account.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Contact.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); + Assert.areEqual(1, result.deletesOf(Account.SObjectType).records().size(), 'Deleted operation result should contain the deleted record.'); + Assert.areEqual(1, result.deletesOf(Account.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.areEqual(1, result.deletesOf(Contact.SObjectType).records().size(), 'Deleted operation result should contain the deleted record.'); + Assert.areEqual(1, result.deletesOf(Contact.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.isTrue(result.deletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); + Assert.isTrue(result.deletesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); + Assert.isNotNull(result.deletesOf(Account.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); + Assert.isNotNull(result.deletesOf(Contact.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); + } + + @IsTest + static void toDeleteMultipleRecordsWithMockingSpecificSObjectType() { + // Setup + Account account1 = getAccount(1); + insert account1; + + Contact contact1 = getContact(1); + insert contact1; + + DML.mock('dmlMockId').deletesFor(Account.SObjectType); + + // Test + Test.startTest(); + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + new DML() + .toDelete(account1) + .toDelete(contact1) + .identifier('dmlMockId') + .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should not be deleted from the database.'); + Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'Contact should be deleted from the database.'); + Assert.areEqual(1, dmlStatementsAfter - dmlStatementsBefore, 'DML statements should be 1, because second commitWork() should not do anything.'); + Assert.areEqual(2, result.deletes().size(), 'Deleted operation result should contain 2 results.'); + Assert.areEqual(Account.SObjectType, result.deletesOf(Account.SObjectType).objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(Contact.SObjectType, result.deletesOf(Contact.SObjectType).objectType(), 'Deleted operation result should contain Contact object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Account.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Contact.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); + Assert.areEqual(1, result.deletesOf(Account.SObjectType).records().size(), 'Deleted operation result should contain the deleted record.'); + Assert.areEqual(1, result.deletesOf(Account.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.areEqual(1, result.deletesOf(Contact.SObjectType).records().size(), 'Deleted operation result should contain the deleted record.'); + Assert.areEqual(1, result.deletesOf(Contact.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); + Assert.isTrue(result.deletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); + Assert.isTrue(result.deletesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); + Assert.isNotNull(result.deletesOf(Account.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); + Assert.isNotNull(result.deletesOf(Contact.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); + } + + @IsTest + static void toDeleteWithMockingException() { + // Setup + Account account1 = getAccount(1); + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').exceptionOnDeletes(); + + Exception expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toDelete(account1) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } + + @IsTest + static void toDeleteWithMockingExceptionWhenAllOrNoneIsSet() { + // Setup + Account account1 = getAccount(1); + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').exceptionOnDeletes(); + + Exception expectedException = null; + DML.Result result = null; + + // Test + Test.startTest(); + try { + result = new DML() + .toDelete(account1) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, operationResult.operationType(), 'Result should contain delete type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); + } + + @IsTest + static void toDeleteWithMockingExceptionForSpecificSObjectType() { + // Setup + Account account1 = getAccount(1); + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').exceptionOnDeletesFor(Account.SObjectType); + + Exception expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toDelete(account1) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } + + @IsTest + static void toDeleteWithMockingExceptionForSpecificSObjectTypeWhenAllOrNoneIsSet() { + // Setup + Account account1 = getAccount(1); + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').exceptionOnDeletesFor(Account.SObjectType); + + Exception expectedException = null; + DML.Result result = null; + + // Test + Test.startTest(); + try { + result = new DML() + .toDelete(account1) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, operationResult.operationType(), 'Result should contain delete type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); + } + + // HARD DELETE + + @IsTest + static void toHardDeleteSingleRecordById() { + // Setup + Account account = insertAccount(); + + // Test + Test.startTest(); + new DML() + .toHardDelete(account.Id) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be hard deleted.'); + // No assertion with ALL ROWS, because there is Salesforce error + } + + @IsTest + static void toHardDeleteSingleRecord() { + // Setup + Account account1 = insertAccount(); + + // Test + Test.startTest(); + DML.Result result = new DML() + .toHardDelete(account1) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be hard deleted.'); + // No assertion with ALL ROWS, because there is Salesforce error + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Hard deleted operation result should contain the deleted record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Hard deleted operation result should contain the deleted record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Hard deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, operationResult.operationType(), 'Hard deleted operation result should contain delete type.'); + Assert.isFalse(operationResult.hasFailures(), 'Hard deleted operation result should not have failures.'); + } + + @IsTest + static void toHardDeleteMultipleRecords() { + // Setup + Account account1 = getAccount(1); + Account account2 = getAccount(2); + + insert new List{ account1, account2 }; + + // Test + Test.startTest(); + DML.Result result = new DML() + .toHardDelete(account1) + .toHardDelete(account2) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be hard deleted.'); + // No assertion with ALL ROWS, because there is Salesforce error + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); + + Assert.areEqual(2, operationResult.records().size(), 'Hard deleted operation result should contain the deleted records.'); + Assert.areEqual(2, operationResult.recordResults().size(), 'Hard deleted operation result should contain the deleted record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Hard deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, operationResult.operationType(), 'Hard deleted operation result should contain delete type.'); + Assert.isFalse(operationResult.hasFailures(), 'Hard deleted operation result should not have failures.'); + } + + @IsTest + static void toHardDeleteListOfRecords() { + // Setup + List accounts = insertAccounts(); + + // Test + Test.startTest(); + new DML() + .toHardDelete(accounts) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be hard deleted.'); + // No assertion with ALL ROWS, because there is Salesforce error + } + + @IsTest + static void toHardDeleteMultipleRecordsById() { + // Setup + List accounts = insertAccounts(); + + // Test + Test.startTest(); + Set accountIds = new Map(accounts).keySet(); + + new DML() + .toHardDelete(accountIds) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be hard deleted.'); + // No assertion with ALL ROWS, because there is Salesforce error + } + + @IsTest + static void toHardDeleteMultipleRecordsTypes() { + // Setup + Account account1 = getAccount(1); + Opportunity opportunity1 = getOpportunity(1); + Lead lead1 = getLead(1); + + insert new List{ account1, opportunity1, lead1 }; + + // Test + Test.startTest(); + DML.Result result = new DML() + .toHardDelete(account1) + .toHardDelete(opportunity1) + .toHardDelete(lead1) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be hard deleted.'); + Assert.areEqual(0, [SELECT COUNT() FROM Opportunity], 'Opportunity should be hard deleted.'); + Assert.areEqual(0, [SELECT COUNT() FROM Lead], 'Lead should be hard deleted.'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(3, result.deletes().size(), 'Deleted operation result should contain 3 results.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + DML.OperationResult accountOperationResult = result.deletesOf(Account.SObjectType); + + Assert.areEqual(1, accountOperationResult.records().size(), 'Hard deleted operation result should contain the deleted record.'); + Assert.areEqual(1, accountOperationResult.recordResults().size(), 'Hard deleted operation result should contain the deleted record results.'); + Assert.areEqual(Account.SObjectType, accountOperationResult.objectType(), 'Hard deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, accountOperationResult.operationType(), 'Hard deleted operation result should contain delete type.'); + Assert.isFalse(accountOperationResult.hasFailures(), 'Hard deleted operation result should not have failures.'); + + DML.OperationResult opportunityOperationResult = result.deletesOf(Opportunity.SObjectType); + + Assert.areEqual(1, opportunityOperationResult.records().size(), 'Hard deleted operation result should contain the deleted record.'); + Assert.areEqual(1, opportunityOperationResult.recordResults().size(), 'Hard deleted operation result should contain the deleted record results.'); + Assert.areEqual(Opportunity.SObjectType, opportunityOperationResult.objectType(), 'Hard deleted operation result should contain Opportunity object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, opportunityOperationResult.operationType(), 'Hard deleted operation result should contain delete type.'); + Assert.isFalse(opportunityOperationResult.hasFailures(), 'Hard deleted operation result should not have failures.'); + + DML.OperationResult leadOperationResult = result.deletesOf(Lead.SObjectType); + + Assert.areEqual(1, leadOperationResult.records().size(), 'Hard deleted operation result should contain the deleted record.'); + Assert.areEqual(1, leadOperationResult.recordResults().size(), 'Hard deleted operation result should contain the deleted record results.'); + Assert.areEqual(Lead.SObjectType, leadOperationResult.objectType(), 'Hard deleted operation result should contain Lead object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, leadOperationResult.operationType(), 'Hard deleted operation result should contain delete type.'); + Assert.isFalse(leadOperationResult.hasFailures(), 'Hard deleted operation result should not have failures.'); + } + + @IsTest + static void toHardDeleteWithoutExistingIds() { + // Setup + Account account = getAccount(1); + + DmlException expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toHardDelete(account) + .commitWork(); + } catch (DmlException e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.areEqual('Only existing records can be registered as deleted.', expectedException.getMessage(), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toHardDeleteWithPartialSuccess() { + // Setup + List accounts = new List{ + getAccount(1), + getAccount(2) + }; + + insert accounts; + + // Test + Test.startTest(); + new DML() + .toHardDelete(accounts) + .allowPartialSuccess() + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be hard deleted.'); + // No assertion with ALL ROWS, because there is Salesforce error + } + + @IsTest + static void toHardDeleteWithUserMode() { + // Setup + Case newCase = getCase(1); + insert newCase; + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + new DML() + .toHardDelete(newCase) + .commitWork(); // user mode by default + } catch (Exception e) { + expectedException = e; + } + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toHardDeleteWithSystemMode() { + // Setup + Case newCase = getCase(1); + insert newCase; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + new DML() + .toHardDelete(newCase) + .systemMode() + .withoutSharing() + .commitWork(); + } + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Cases should be hard deleted.'); + // No assertion with ALL ROWS, because there is Salesforce error + } + + @IsTest + static void toDeleteWithEmptyRecords() { + // Test + Test.startTest(); + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + DML.Result result = new DML() + .toDelete(new List()) + .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be deleted from the database.'); + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); + + Assert.areEqual(0, operationResult.records().size(), 'Deleted operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Deleted operation result should contain 0 record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, operationResult.operationType(), 'Deleted operation result should contain delete type.'); + Assert.isFalse(operationResult.hasFailures(), 'Deleted operation result should not have failures.'); + } + + @IsTest + static void toDeleteWithEmptyRecordsWhenMocking() { + // Setup + DML.mock('dmlMockId').allDeletes(); + + // Test + Test.startTest(); + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + new DML() + .toDelete(new List()) + .identifier('dmlMockId') + .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be deleted from the database.'); + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); + + Assert.areEqual(0, operationResult.records().size(), 'Deleted operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Deleted operation result should contain 0 record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.DELETE_DML, operationResult.operationType(), 'Deleted operation result should contain delete type.'); + Assert.isFalse(operationResult.hasFailures(), 'Deleted operation result should not have failures.'); + } + + // UNDELETE + + @IsTest + static void toUndeleteSingleRecordById() { + // Setup + Account account = insertAccount(); + delete account; + + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); + + // Test + Test.startTest(); + new DML() + .toUndelete(account.Id) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be undeleted.'); + } + + @IsTest + static void toUndeleteSingleRecord() { + // Setup + Account account1 = insertAccount(); + delete account1; + + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); + + // Test + Test.startTest(); + DML.Result result = new DML() + .toUndelete(account1) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be undeleted.'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(1, result.undeletes().size(), 'Undeleted operation result should contain 1 result.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + + DML.OperationResult operationResult = result.undeletesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Undeleted operation result should contain the undeleted record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Undeleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, operationResult.operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.isFalse(operationResult.hasFailures(), 'Undeleted operation result should not have failures.'); + } + + @IsTest + static void undeleteImmediatelySingleRecord() { + // Setup + Account account1 = getAccount(1); + insert account1; + delete account1; + + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); + + // Test + Test.startTest(); + DML.OperationResult result = new DML().undeleteImmediately(account1); + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Single record should be undeleted.'); + Assert.areEqual(1, result.records().size(), 'Undeleted operation result should contain the undeleted record.'); + Assert.areEqual(1, result.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Undeleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, result.operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.isFalse(result.hasFailures(), 'Undeleted operation result should not have failures.'); + } + + @IsTest + static void undeleteImmediatelySingleRecordById() { + // Setup + Account account1 = getAccount(1); + insert account1; + delete account1; + + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); + + // Test + Test.startTest(); + DML.OperationResult result = new DML().undeleteImmediately(account1.Id); + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Single record should be undeleted.'); + Assert.areEqual(1, result.records().size(), 'Undeleted operation result should contain the undeleted record.'); + Assert.areEqual(1, result.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Undeleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, result.operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.isFalse(result.hasFailures(), 'Undeleted operation result should not have failures.'); + } + + @IsTest + static void toUndeleteMultipleRecordsById() { + // Setup + List accounts = insertAccounts(); + delete accounts; + + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + + // Test + Test.startTest(); + Set accountIds = new Map(accounts).keySet(); + + new DML() + .toUndelete(accountIds) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(accounts.size(), [SELECT COUNT() FROM Account], 'Accounts should be undeleted.'); + } + + @IsTest + static void toUndeleteMultipleRecords() { + // Setup + List accounts = insertAccounts(); + delete accounts; + + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + + // Test + Test.startTest(); + DML.Result result = new DML() + .toUndelete(accounts) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(accounts.size(), [SELECT COUNT() FROM Account], 'Accounts should be undeleted.'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(1, result.undeletes().size(), 'Undeleted operation result should contain 1 result.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + + DML.OperationResult operationResult = result.undeletesOf(Account.SObjectType); + + Assert.areEqual(3, operationResult.records().size(), 'Undeleted operation result should contain the undeleted records.'); + Assert.areEqual(3, operationResult.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Undeleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, operationResult.operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.isFalse(operationResult.hasFailures(), 'Undeleted operation result should not have failures.'); + } + + @IsTest + static void undeleteImmediatelyMultipleRecords() { + // Setup + Account account1 = getAccount(1); + Account account2 = getAccount(2); + insert new List{ account1, account2 }; + delete new List{ account1, account2 }; + + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + + // Test + Test.startTest(); + DML.OperationResult result = new DML().undeleteImmediately(new List{ account1, account2 }); + Test.stopTest(); + + // Verify + Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be undeleted.'); + + Assert.areEqual(2, result.records().size(), 'Undeleted operation result should contain the undeleted records.'); + Assert.areEqual(2, result.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Undeleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, result.operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.isFalse(result.hasFailures(), 'Undeleted operation result should not have failures.'); + } + + @IsTest + static void undeleteImmediatelyMultipleRecordsByIds() { + // Setup + Account account1 = getAccount(1); + Account account2 = getAccount(2); + insert new List{ account1, account2 }; + delete new List{ account1, account2 }; + + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + + // Test + Test.startTest(); + DML.OperationResult result = new DML().undeleteImmediately(new List{ account1.Id, account2.Id }); + Test.stopTest(); + + // Verify + Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be undeleted.'); + + Assert.areEqual(2, result.records().size(), 'Undeleted operation result should contain the undeleted records.'); + Assert.areEqual(2, result.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.areEqual(Account.SObjectType, result.objectType(), 'Undeleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, result.operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.isFalse(result.hasFailures(), 'Undeleted operation result should not have failures.'); + } + + @IsTest + static void toUndeleteMultipleRecordsTypes() { + // Setup + Account account1 = getAccount(1); + Opportunity opportunity1 = getOpportunity(1); + Lead lead1 = getLead(1); + + insert new List{ account1, opportunity1, lead1 }; + delete new List{ account1, opportunity1, lead1 }; + + // Test + Test.startTest(); + DML.Result result = new DML() + .toUndelete(account1) + .toUndelete(opportunity1) + .toUndelete(lead1) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be undeleted.'); + Assert.areEqual(1, [SELECT COUNT() FROM Opportunity], 'Opportunity should be undeleted.'); + Assert.areEqual(1, [SELECT COUNT() FROM Lead], 'Lead should be undeleted.'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(3, result.undeletes().size(), 'Undeleted operation result should contain 3 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + + DML.OperationResult accountOperationResult = result.undeletesOf(Account.SObjectType); + + Assert.areEqual(1, accountOperationResult.records().size(), 'Undeleted operation result should contain the undeleted record.'); + Assert.areEqual(1, accountOperationResult.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.areEqual(Account.SObjectType, accountOperationResult.objectType(), 'Undeleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, accountOperationResult.operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.isFalse(accountOperationResult.hasFailures(), 'Undeleted operation result should not have failures.'); + + DML.OperationResult opportunityOperationResult = result.undeletesOf(Opportunity.SObjectType); + + Assert.areEqual(1, opportunityOperationResult.records().size(), 'Undeleted operation result should contain the undeleted record.'); + Assert.areEqual(1, opportunityOperationResult.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.areEqual(Opportunity.SObjectType, opportunityOperationResult.objectType(), 'Undeleted operation result should contain Opportunity object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, opportunityOperationResult.operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.isFalse(opportunityOperationResult.hasFailures(), 'Undeleted operation result should not have failures.'); + + DML.OperationResult leadOperationResult = result.undeletesOf(Lead.SObjectType); + + Assert.areEqual(1, leadOperationResult.records().size(), 'Undeleted operation result should contain the undeleted record.'); + Assert.areEqual(1, leadOperationResult.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.areEqual(Lead.SObjectType, leadOperationResult.objectType(), 'Undeleted operation result should contain Lead object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, leadOperationResult.operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.isFalse(leadOperationResult.hasFailures(), 'Undeleted operation result should not have failures.'); + } + + @IsTest + static void toUndeleteWithoutExistingIds() { + // Setup + Account account = getAccount(1); + + DmlException expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toUndelete(account) + .commitWork(); + } catch (DmlException e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.areEqual('Only deleted records can be undeleted.', expectedException.getMessage(), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toUndeleteWithUserMode() { + // Setup + Case newCase = getCase(1); + insert newCase; + + delete newCase; + + Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Cases should be deleted.'); + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + new DML() + .toUndelete(newCase) + .commitWork(); // user mode by default + } catch (Exception e) { + expectedException = e; + } + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.isTrue(expectedException.getMessage().contains('invalid record id'), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toUndeleteWithUserModeExplicitlySet() { + // Setup + Case newCase = getCase(1); + insert newCase; + + delete newCase; + + Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Cases should be deleted.'); + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + new DML() + .toUndelete(newCase) + .userMode() + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.isTrue(expectedException.getMessage().contains('invalid record id'), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toUndeleteWithSystemMode() { + // Setup + Case newCase = getCase(1); + insert newCase; + + delete newCase; + + Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Cases should be deleted.'); + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + new DML() + .toUndelete(newCase) + .systemMode() + .withoutSharing() + .commitWork(); + } + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Case], 'Cases should be undeleted.'); + } + + + @IsTest + static void toUndeleteWithSystemModeAndWithSharing() { + // Setup + Case newCase = getCase(1); + insert newCase; + + delete newCase; + + Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Cases should be deleted.'); + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + new DML() + .toUndelete(newCase) + .systemMode() + .withSharing() + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + } + Test.stopTest(); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Case should not be undeleted, because user has no access with sharing mode.'); + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.isTrue(expectedException.getMessage().contains('insufficient access rights on object id'), 'Expected exception message should be thrown.'); + } + + @IsTest + static void toUndeleteSingleRecordWithMocking() { + // Setup + Account account1 = getAccount(1); + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').allUndeletes(); + + // Test + Test.startTest(); + new DML() + .toUndelete(account1) + .identifier('dmlMockId') + .commitWork(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); + Assert.areEqual(1, result.undeletes().size(), 'Undeleted operation result should contain 1 result.'); + Assert.areEqual(Account.SObjectType, result.deletesOf(Account.SObjectType).objectType(), 'Deleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Account.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.areEqual(1, result.undeletesOf(Account.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted record.'); + Assert.areEqual(1, result.undeletesOf(Account.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.isTrue(result.undeletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); + Assert.isNotNull(result.undeletesOf(Account.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); + } + + @IsTest + static void toUndeleteMultipleRecordsWithMocking() { + // Setup + Account account1 = getAccount(1); + Account account2 = getAccount(2); + + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + account2.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').allUndeletes(); + + // Test + Test.startTest(); + new DML() + .toUndelete(account1) + .toUndelete(account2) + .identifier('dmlMockId') + .commitWork(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); + Assert.areEqual(1, result.undeletes().size(), 'Undeleted operation result should contain 1 result.'); + Assert.areEqual(Account.SObjectType, result.undeletesOf(Account.SObjectType).objectType(), 'Undeleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Account.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.areEqual(2, result.undeletesOf(Account.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted records.'); + Assert.areEqual(2, result.undeletesOf(Account.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.isTrue(result.undeletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); + Assert.isTrue(result.undeletesOf(Account.SObjectType).recordResults()[1].isSuccess(), 'Undeleted operation result should contain a successful record result.'); + Assert.isNotNull(result.undeletesOf(Account.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); + Assert.isNotNull(result.undeletesOf(Account.SObjectType).recordResults()[1].id(), 'Undeleted operation result should contain a mocked record Id.'); + } + + @IsTest + static void toUndeleteMultipleRecordTypesWithMocking() { + // Setup + Account account1 = getAccount(1); + Contact contact1 = getContact(1); + + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + contact1.Id = DML.randomIdGenerator.get(Contact.SObjectType); + + DML.mock('dmlMockId').allUndeletes(); + + // Test + Test.startTest(); + new DML() + .toUndelete(account1) + .toUndelete(contact1) + .identifier('dmlMockId') + .commitWork(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No Account records should exist, because undelete was mocked.'); + Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'No Contact records should exist, because undelete was mocked.'); + Assert.areEqual(2, result.undeletes().size(), 'Undeleted operation result should contain 2 results.'); + Assert.areEqual(Account.SObjectType, result.undeletesOf(Account.SObjectType).objectType(), 'Undeleted operation result should contain Account object type.'); + Assert.areEqual(Contact.SObjectType, result.undeletesOf(Contact.SObjectType).objectType(), 'Undeleted operation result should contain Contact object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Account.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Contact.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.areEqual(1, result.undeletesOf(Account.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted record.'); + Assert.areEqual(1, result.undeletesOf(Account.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.areEqual(1, result.undeletesOf(Contact.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted record.'); + Assert.areEqual(1, result.undeletesOf(Contact.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.isTrue(result.undeletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); + Assert.isTrue(result.undeletesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); + Assert.isNotNull(result.undeletesOf(Account.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); + Assert.isNotNull(result.undeletesOf(Contact.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); + } + + @IsTest + static void toUndeleteMultipleRecordsWithMockingSpecificSObjectType() { + // Setup + Account account1 = getAccount(1); + insert account1; + delete account1; + + Contact contact1 = getContact(1); + insert contact1; + delete contact1; + + DML.mock('dmlMockId').undeletesFor(Account.SObjectType); + + // Test + Test.startTest(); + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + new DML() + .toUndelete(account1) + .toUndelete(contact1) + .identifier('dmlMockId') + .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + // Verify + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should not be undeleted in the database.'); + Assert.areEqual(1, [SELECT COUNT() FROM Contact], 'Contact should be undeleted in the database.'); + Assert.areEqual(1, dmlStatementsAfter - dmlStatementsBefore, 'DML statements should be 1, because second commitWork() should not do anything.'); + Assert.areEqual(2, result.undeletes().size(), 'Undeleted operation result should contain 2 results.'); + Assert.areEqual(Account.SObjectType, result.undeletesOf(Account.SObjectType).objectType(), 'Undeleted operation result should contain Account object type.'); + Assert.areEqual(Contact.SObjectType, result.undeletesOf(Contact.SObjectType).objectType(), 'Undeleted operation result should contain Contact object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Account.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Contact.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.areEqual(1, result.undeletesOf(Account.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted record.'); + Assert.areEqual(1, result.undeletesOf(Account.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.areEqual(1, result.undeletesOf(Contact.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted record.'); + Assert.areEqual(1, result.undeletesOf(Contact.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + Assert.isTrue(result.undeletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); + Assert.isTrue(result.undeletesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); + Assert.isNotNull(result.undeletesOf(Account.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); + Assert.isNotNull(result.undeletesOf(Contact.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); + } + + @IsTest + static void toUndeleteWithMockingException() { + // Setup + Account account1 = getAccount(1); + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').exceptionOnUndeletes(); + + Exception expectedException = null; // Test Test.startTest(); - System.runAs(minimumAccessUser()) { + try { new DML() - .toDelete(newCase) - .systemMode() - .withoutSharing() + .toUndelete(account1) + .identifier('dmlMockId') .commitWork(); + } catch (Exception e) { + expectedException = e; } Test.stopTest(); // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Cases should be deleted.'); + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); } @IsTest - static void toDeleteWithSystemModeAndWithSharing() { + static void toUndeleteWithMockingExceptionWhenAllOrNoneIsSet() { // Setup - Case newCase = getCase(1); - insert newCase; + Account account1 = getAccount(1); + account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').exceptionOnUndeletes(); Exception expectedException = null; + DML.Result result = null; // Test Test.startTest(); - System.runAs(minimumAccessUser()) { - try { - new DML() - .toDelete(newCase) - .systemMode() - .withoutSharing() - .commitWork(); - } catch (Exception e) { - expectedException = e; - } + try { + result = new DML() + .toUndelete(account1) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; } Test.stopTest(); // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Case should be deleted with system mode and without sharing.'); - Assert.isNull(expectedException, 'No exception should be thrown.'); + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.undeletesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, operationResult.operationType(), 'Result should contain undelete type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); } @IsTest - static void toDeleteSingleRecordWithMocking() { + static void toUndeleteWithMockingExceptionForSpecificSObjectType() { // Setup Account account1 = getAccount(1); account1.Id = DML.randomIdGenerator.get(Account.SObjectType); - DML.mock('dmlMockId').allDeletes(); + DML.mock('dmlMockId').exceptionOnUndeletesFor(Account.SObjectType); + + Exception expectedException = null; // Test Test.startTest(); - new DML() - .toDelete(account1) - .identifier('dmlMockId') - .commitWork(); + try { + new DML() + .toUndelete(account1) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } Test.stopTest(); - DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); - Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); - Assert.areEqual(Account.SObjectType, result.deletesOf(Account.SObjectType).objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Account.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); - Assert.areEqual(1, result.deletesOf(Account.SObjectType).records().size(), 'Deleted operation result should contain the deleted record.'); - Assert.areEqual(1, result.deletesOf(Account.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.isTrue(result.deletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); - Assert.isNotNull(result.deletesOf(Account.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); } @IsTest - static void toDeleteMultipleRecordsWithMocking() { + static void toUndeleteWithMockingExceptionForSpecificSObjectTypeWhenAllOrNoneIsSet() { // Setup Account account1 = getAccount(1); - Account account2 = getAccount(2); - account1.Id = DML.randomIdGenerator.get(Account.SObjectType); - account2.Id = DML.randomIdGenerator.get(Account.SObjectType); - DML.mock('dmlMockId').allDeletes(); + DML.mock('dmlMockId').exceptionOnUndeletesFor(Account.SObjectType); + + Exception expectedException = null; + DML.Result result = null; // Test Test.startTest(); - new DML() - .toDelete(account1) - .toDelete(account2) - .identifier('dmlMockId') - .commitWork(); + try { + result = new DML() + .toUndelete(account1) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } Test.stopTest(); - DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); - Assert.areEqual(1, result.deletes().size(), 'Deleted operation result should contain 1 result.'); - Assert.areEqual(Account.SObjectType, result.deletesOf(Account.SObjectType).objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Account.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); - Assert.areEqual(2, result.deletesOf(Account.SObjectType).records().size(), 'Deleted operation result should contain the deleted records.'); - Assert.areEqual(2, result.deletesOf(Account.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.isTrue(result.deletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); - Assert.isTrue(result.deletesOf(Account.SObjectType).recordResults()[1].isSuccess(), 'Deleted operation result should contain a successful record result.'); - Assert.isNotNull(result.deletesOf(Account.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); - Assert.isNotNull(result.deletesOf(Account.SObjectType).recordResults()[1].id(), 'Deleted operation result should contain a mocked record Id.'); + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.undeletesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, operationResult.operationType(), 'Result should contain undelete type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); } @IsTest - static void toDeleteMultipleRecordTypesWithMocking() { - // Setup - Account account1 = getAccount(1); - Contact contact1 = getContact(1); - - account1.Id = DML.randomIdGenerator.get(Account.SObjectType); - contact1.Id = DML.randomIdGenerator.get(Contact.SObjectType); - - DML.mock('dmlMockId').allDeletes(); - + static void toUndeleteWithEmptyRecords() { // Test Test.startTest(); - new DML() - .toDelete(account1) - .toDelete(contact1) - .identifier('dmlMockId') + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + DML.Result result = new DML() + .toUndelete(new List()) .commitWork(); - Test.stopTest(); - DML.Result result = DML.retrieveResultFor('dmlMockId'); + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No Account records should exist, because delete was mocked.'); - Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'No Contact records should exist, because delete was mocked.'); - Assert.areEqual(2, result.deletes().size(), 'Deleted operation result should contain 2 results.'); - Assert.areEqual(Account.SObjectType, result.deletesOf(Account.SObjectType).objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(Contact.SObjectType, result.deletesOf(Contact.SObjectType).objectType(), 'Deleted operation result should contain Contact object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Account.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Contact.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); - Assert.areEqual(1, result.deletesOf(Account.SObjectType).records().size(), 'Deleted operation result should contain the deleted record.'); - Assert.areEqual(1, result.deletesOf(Account.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.areEqual(1, result.deletesOf(Contact.SObjectType).records().size(), 'Deleted operation result should contain the deleted record.'); - Assert.areEqual(1, result.deletesOf(Contact.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.isTrue(result.deletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); - Assert.isTrue(result.deletesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); - Assert.isNotNull(result.deletesOf(Account.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); - Assert.isNotNull(result.deletesOf(Contact.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be undeleted in the database.'); + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(1, result.undeletes().size(), 'Undeleted operation result should contain 1 result.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); + Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + + DML.OperationResult operationResult = result.undeletesOf(Account.SObjectType); + + Assert.areEqual(0, operationResult.records().size(), 'Undeleted operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Undeleted operation result should contain 0 record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Undeleted operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, operationResult.operationType(), 'Undeleted operation result should contain undelete type.'); + Assert.isFalse(operationResult.hasFailures(), 'Undeleted operation result should not have failures.'); } @IsTest - static void toDeleteMultipleRecordsWithMockingSpecificSObjectType() { + static void toUndeleteWithEmptyRecordsWhenMocking() { // Setup - Account account1 = getAccount(1); - insert account1; - - Contact contact1 = getContact(1); - insert contact1; - - DML.mock('dmlMockId').deletesFor(Account.SObjectType); + DML.mock('dmlMockId').allUndeletes(); // Test Test.startTest(); Integer dmlStatementsBefore = Limits.getDMLStatements(); new DML() - .toDelete(account1) - .toDelete(contact1) + .toUndelete(new List()) .identifier('dmlMockId') .commitWork(); Integer dmlStatementsAfter = Limits.getDMLStatements(); Test.stopTest(); - DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should not be deleted from the database.'); - Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'Contact should be deleted from the database.'); - Assert.areEqual(1, dmlStatementsAfter - dmlStatementsBefore, 'DML statements should be 1, because second commitWork() should not do anything.'); - Assert.areEqual(2, result.deletes().size(), 'Deleted operation result should contain 2 results.'); - Assert.areEqual(Account.SObjectType, result.deletesOf(Account.SObjectType).objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(Contact.SObjectType, result.deletesOf(Contact.SObjectType).objectType(), 'Deleted operation result should contain Contact object type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Account.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); - Assert.areEqual(DML.OperationType.DELETE_DML, result.deletesOf(Contact.SObjectType).operationType(), 'Deleted operation result should contain delete type.'); - Assert.areEqual(1, result.deletesOf(Account.SObjectType).records().size(), 'Deleted operation result should contain the deleted record.'); - Assert.areEqual(1, result.deletesOf(Account.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.areEqual(1, result.deletesOf(Contact.SObjectType).records().size(), 'Deleted operation result should contain the deleted record.'); - Assert.areEqual(1, result.deletesOf(Contact.SObjectType).recordResults().size(), 'Deleted operation result should contain the deleted record results.'); - Assert.isTrue(result.deletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); - Assert.isTrue(result.deletesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Deleted operation result should contain a successful record result.'); - Assert.isNotNull(result.deletesOf(Account.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); - Assert.isNotNull(result.deletesOf(Contact.SObjectType).recordResults()[0].id(), 'Deleted operation result should contain a mocked record Id.'); - } - - // UNDELETE - - @IsTest - static void toUndeleteSingleRecordById() { - // Setup - Account account = insertAccount(); - delete account; - - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); - - // Test - Test.startTest(); - new DML() - .toUndelete(account.Id) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be undeleted.'); - } - - @IsTest - static void toUndeleteSingleRecord() { - // Setup - Account account1 = insertAccount(); - delete account1; + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be undeleted in the database.'); + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); - - // Test - Test.startTest(); - DML.Result result = new DML() - .toUndelete(account1) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be undeleted.'); + DML.Result result = DML.retrieveResultFor('dmlMockId'); Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); Assert.areEqual(1, result.undeletes().size(), 'Undeleted operation result should contain 1 result.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); - DML.OperationResult operationResult = result.undeletesOf(Account.SObjectType); - - Assert.areEqual(1, operationResult.records().size(), 'Undeleted operation result should contain the undeleted record.'); - Assert.areEqual(1, operationResult.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); + + Assert.areEqual(0, operationResult.records().size(), 'Undeleted operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Undeleted operation result should contain 0 record results.'); Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Undeleted operation result should contain Account object type.'); Assert.areEqual(DML.OperationType.UNDELETE_DML, operationResult.operationType(), 'Undeleted operation result should contain undelete type.'); Assert.isFalse(operationResult.hasFailures(), 'Undeleted operation result should not have failures.'); - } - - @IsTest - static void undeleteImmediatelySingleRecord() { - // Setup - Account account1 = getAccount(1); - insert account1; - delete account1; - - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); - - // Test - Test.startTest(); - DML.OperationResult result = new DML().undeleteImmediately(account1); - Test.stopTest(); + } - // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Single record should be undeleted.'); - Assert.areEqual(1, result.records().size(), 'Undeleted operation result should contain the undeleted record.'); - Assert.areEqual(1, result.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Undeleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, result.operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.isFalse(result.hasFailures(), 'Undeleted operation result should not have failures.'); - } + // MERGE @IsTest - static void undeleteImmediatelySingleRecordById() { + static void toMergeSingleDuplicateRecord() { // Setup - Account account1 = getAccount(1); - insert account1; - delete account1; + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should be deleted.'); + insert new List{ masterAccount, duplicateAccount }; // Test Test.startTest(); - DML.OperationResult result = new DML().undeleteImmediately(account1.Id); + DML.Result result = new DML() + .toMerge(masterAccount, duplicateAccount) + .commitWork(); Test.stopTest(); // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Single record should be undeleted.'); - Assert.areEqual(1, result.records().size(), 'Undeleted operation result should contain the undeleted record.'); - Assert.areEqual(1, result.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Undeleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, result.operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.isFalse(result.hasFailures(), 'Undeleted operation result should not have failures.'); - } - - @IsTest - static void toUndeleteMultipleRecordsById() { - // Setup - List accounts = insertAccounts(); - delete accounts; - - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); - - // Test - Test.startTest(); - Set accountIds = new Map(accounts).keySet(); - - new DML() - .toUndelete(accountIds) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(accounts.size(), [SELECT COUNT() FROM Account], 'Accounts should be undeleted.'); - } - - @IsTest - static void toUndeleteMultipleRecords() { - // Setup - List accounts = insertAccounts(); - delete accounts; - - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); - - // Test - Test.startTest(); - DML.Result result = new DML() - .toUndelete(accounts) - .commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(accounts.size(), [SELECT COUNT() FROM Account], 'Accounts should be undeleted.'); + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Only master account should remain after merge.'); + Assert.areEqual(masterAccount.Id, [SELECT Id FROM Account LIMIT 1].Id, 'Master account should survive the merge.'); Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); - Assert.areEqual(1, result.undeletes().size(), 'Undeleted operation result should contain 1 result.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(1, result.merges().size(), 'Merged operation result should contain 1 result.'); Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + DML.OperationResult operationResult = result.mergesOf(Account.SObjectType); - DML.OperationResult operationResult = result.undeletesOf(Account.SObjectType); - - Assert.areEqual(3, operationResult.records().size(), 'Undeleted operation result should contain the undeleted records.'); - Assert.areEqual(3, operationResult.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Undeleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, operationResult.operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.isFalse(operationResult.hasFailures(), 'Undeleted operation result should not have failures.'); + Assert.areEqual(1, operationResult.records().size(), 'Merged operation result should contain the merged record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Merged operation result should contain the merged record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Merged operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.MERGE_DML, operationResult.operationType(), 'Merged operation result should contain merge type.'); + Assert.isFalse(operationResult.hasFailures(), 'Merged operation result should not have failures.'); } @IsTest - static void undeleteImmediatelyMultipleRecords() { + static void toMergeSingleDuplicateRecordById() { // Setup - Account account1 = getAccount(1); - Account account2 = getAccount(2); - insert new List{ account1, account2 }; - delete new List{ account1, account2 }; - - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + insert new List{ masterAccount, duplicateAccount }; // Test Test.startTest(); - DML.OperationResult result = new DML().undeleteImmediately(new List{ account1, account2 }); + DML.Result result = new DML() + .toMerge(masterAccount, duplicateAccount.Id) + .commitWork(); Test.stopTest(); // Verify - Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be undeleted.'); + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Only master account should remain after merge.'); + Assert.areEqual(masterAccount.Id, [SELECT Id FROM Account LIMIT 1].Id, 'Master account should survive the merge.'); - Assert.areEqual(2, result.records().size(), 'Undeleted operation result should contain the undeleted records.'); - Assert.areEqual(2, result.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Undeleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, result.operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.isFalse(result.hasFailures(), 'Undeleted operation result should not have failures.'); + Assert.areEqual(1, result.merges().size(), 'Merged operation result should contain 1 result.'); + + DML.OperationResult operationResult = result.mergesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Merged operation result should contain the merged record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Merged operation result should contain the merged record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Merged operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.MERGE_DML, operationResult.operationType(), 'Merged operation result should contain merge type.'); + Assert.isFalse(operationResult.hasFailures(), 'Merged operation result should not have failures.'); } @IsTest - static void undeleteImmediatelyMultipleRecordsByIds() { + static void toMergeMultipleDuplicateRecords() { // Setup - Account account1 = getAccount(1); - Account account2 = getAccount(2); - insert new List{ account1, account2 }; - delete new List{ account1, account2 }; + Account masterAccount = getAccount(1); + Account duplicateAccount1 = getAccount(2); + Account duplicateAccount2 = getAccount(3); + insert new List{ masterAccount, duplicateAccount1, duplicateAccount2 }; - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Accounts should be deleted.'); + List duplicateAccounts = new List{ duplicateAccount1, duplicateAccount2 }; // Test Test.startTest(); - DML.OperationResult result = new DML().undeleteImmediately(new List{ account1.Id, account2.Id }); + DML.Result result = new DML() + .toMerge(masterAccount, duplicateAccounts) + .commitWork(); Test.stopTest(); // Verify - Assert.areEqual(2, [SELECT COUNT() FROM Account], 'Accounts should be undeleted.'); + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Only master account should remain after merge.'); + Assert.areEqual(masterAccount.Id, [SELECT Id FROM Account LIMIT 1].Id, 'Master account should survive the merge.'); - Assert.areEqual(2, result.records().size(), 'Undeleted operation result should contain the undeleted records.'); - Assert.areEqual(2, result.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.areEqual(Account.SObjectType, result.objectType(), 'Undeleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, result.operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.isFalse(result.hasFailures(), 'Undeleted operation result should not have failures.'); + Assert.areEqual(1, result.merges().size(), 'Merged operation result should contain 1 result.'); + + DML.OperationResult operationResult = result.mergesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Merged operation result should contain the merged record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Merged operation result should contain the merged record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Merged operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.MERGE_DML, operationResult.operationType(), 'Merged operation result should contain merge type.'); + Assert.isFalse(operationResult.hasFailures(), 'Merged operation result should not have failures.'); } @IsTest - static void toUndeleteMultipleRecordsTypes() { + static void toMergeMultipleDuplicateRecordsById() { // Setup - Account account1 = getAccount(1); - Opportunity opportunity1 = getOpportunity(1); - Lead lead1 = getLead(1); + Account masterAccount = getAccount(1); + Account duplicateAccount1 = getAccount(2); + Account duplicateAccount2 = getAccount(3); + insert new List{ masterAccount, duplicateAccount1, duplicateAccount2 }; - insert new List{ account1, opportunity1, lead1 }; - delete new List{ account1, opportunity1, lead1 }; + Set duplicateAccountIds = new Set{ duplicateAccount1.Id, duplicateAccount2.Id }; // Test Test.startTest(); DML.Result result = new DML() - .toUndelete(account1) - .toUndelete(opportunity1) - .toUndelete(lead1) + .toMerge(masterAccount, duplicateAccountIds) .commitWork(); - Test.stopTest(); + Test.stopTest(); // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be undeleted.'); - Assert.areEqual(1, [SELECT COUNT() FROM Opportunity], 'Opportunity should be undeleted.'); - Assert.areEqual(1, [SELECT COUNT() FROM Lead], 'Lead should be undeleted.'); + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Only master account should remain after merge.'); + Assert.areEqual(masterAccount.Id, [SELECT Id FROM Account LIMIT 1].Id, 'Master account should survive the merge.'); - Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); - Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); - Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); - Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); - Assert.areEqual(3, result.undeletes().size(), 'Undeleted operation result should contain 3 results.'); - Assert.areEqual(0, result.events().size(), 'Published operation result should contain 0 results.'); + Assert.areEqual(1, result.merges().size(), 'Merged operation result should contain 1 result.'); + DML.OperationResult operationResult = result.mergesOf(Account.SObjectType); - DML.OperationResult accountOperationResult = result.undeletesOf(Account.SObjectType); + Assert.areEqual(1, operationResult.records().size(), 'Merged operation result should contain the merged record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Merged operation result should contain the merged record results.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Merged operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.MERGE_DML, operationResult.operationType(), 'Merged operation result should contain merge type.'); + Assert.isFalse(operationResult.hasFailures(), 'Merged operation result should not have failures.'); + } - Assert.areEqual(1, accountOperationResult.records().size(), 'Undeleted operation result should contain the undeleted record.'); - Assert.areEqual(1, accountOperationResult.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.areEqual(Account.SObjectType, accountOperationResult.objectType(), 'Undeleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, accountOperationResult.operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.isFalse(accountOperationResult.hasFailures(), 'Undeleted operation result should not have failures.'); + @IsTest + static void toMergeWithRelatedRecords() { + // Setup + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + insert new List{ masterAccount, duplicateAccount }; - DML.OperationResult opportunityOperationResult = result.undeletesOf(Opportunity.SObjectType); + Contact contactOnDuplicate = getContact(1); + contactOnDuplicate.AccountId = duplicateAccount.Id; + insert contactOnDuplicate; - Assert.areEqual(1, opportunityOperationResult.records().size(), 'Undeleted operation result should contain the undeleted record.'); - Assert.areEqual(1, opportunityOperationResult.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.areEqual(Opportunity.SObjectType, opportunityOperationResult.objectType(), 'Undeleted operation result should contain Opportunity object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, opportunityOperationResult.operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.isFalse(opportunityOperationResult.hasFailures(), 'Undeleted operation result should not have failures.'); + // Test + Test.startTest(); + new DML() + .toMerge(masterAccount, duplicateAccount) + .commitWork(); + Test.stopTest(); - DML.OperationResult leadOperationResult = result.undeletesOf(Lead.SObjectType); + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Only master account should remain after merge.'); + Assert.areEqual(1, [SELECT COUNT() FROM Contact], 'Contact should still exist.'); - Assert.areEqual(1, leadOperationResult.records().size(), 'Undeleted operation result should contain the undeleted record.'); - Assert.areEqual(1, leadOperationResult.recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.areEqual(Lead.SObjectType, leadOperationResult.objectType(), 'Undeleted operation result should contain Lead object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, leadOperationResult.operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.isFalse(leadOperationResult.hasFailures(), 'Undeleted operation result should not have failures.'); + Contact reparentedContact = [SELECT Id, AccountId FROM Contact WHERE Id = :contactOnDuplicate.Id]; + Assert.areEqual(masterAccount.Id, reparentedContact.AccountId, 'Contact should be reparented to master account.'); } @IsTest - static void toUndeleteWithoutExistingIds() { + static void toMergeWithoutExistingMasterRecord() { // Setup - Account account = getAccount(1); + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + insert duplicateAccount; DmlException expectedException = null; - + // Test Test.startTest(); try { new DML() - .toUndelete(account) + .toMerge(masterAccount, duplicateAccount) .commitWork(); } catch (DmlException e) { expectedException = e; } Test.stopTest(); - + // Verify Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.areEqual('Only deleted records can be undeleted.', expectedException.getMessage(), 'Expected exception message should be thrown.'); + Assert.areEqual('Only existing records can be merged.', expectedException.getMessage(), 'Expected exception message should be thrown.'); } @IsTest - static void toUndeleteWithUserMode() { + static void toMergeWithoutExistingDuplicateRecord() { // Setup - Case newCase = getCase(1); - insert newCase; - - delete newCase; - - Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Cases should be deleted.'); + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + insert masterAccount; Exception expectedException = null; // Test Test.startTest(); - System.runAs(minimumAccessUser()) { try { new DML() - .toUndelete(newCase) - .commitWork(); // user mode by default + .toMerge(masterAccount, duplicateAccount) + .commitWork(); } catch (Exception e) { expectedException = e; } - } Test.stopTest(); // Verify Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.isTrue(expectedException.getMessage().contains('invalid record id'), 'Expected exception message should be thrown.'); } @IsTest - static void toUndeleteWithUserModeExplicitlySet() { + static void toMergeWithUserMode() { // Setup - Case newCase = getCase(1); - insert newCase; - - delete newCase; - - Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Cases should be deleted.'); + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + insert new List{ masterAccount, duplicateAccount }; Exception expectedException = null; @@ -2741,9 +4543,8 @@ private class DML_Test { System.runAs(minimumAccessUser()) { try { new DML() - .toUndelete(newCase) - .userMode() - .commitWork(); + .toMerge(masterAccount, duplicateAccount) + .commitWork(); // user mode by default } catch (Exception e) { expectedException = e; } @@ -2752,24 +4553,20 @@ private class DML_Test { // Verify Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.isTrue(expectedException.getMessage().contains('invalid record id'), 'Expected exception message should be thrown.'); } @IsTest - static void toUndeleteWithSystemMode() { + static void toMergeWithSystemMode() { // Setup - Case newCase = getCase(1); - insert newCase; - - delete newCase; - - Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Cases should be deleted.'); + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + insert new List{ masterAccount, duplicateAccount }; // Test Test.startTest(); System.runAs(minimumAccessUser()) { new DML() - .toUndelete(newCase) + .toMerge(masterAccount, duplicateAccount) .systemMode() .withoutSharing() .commitWork(); @@ -2777,55 +4574,119 @@ private class DML_Test { Test.stopTest(); // Verify - Assert.areEqual(1, [SELECT COUNT() FROM Case], 'Cases should be undeleted.'); + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Only master account should remain after merge.'); } + @IsTest + static void toMergeLeads() { + // Setup + Lead masterLead = getLead(1); + Lead duplicateLead = getLead(2); + insert new List{ masterLead, duplicateLead }; + + // Test + Test.startTest(); + DML.Result result = new DML() + .toMerge(masterLead, duplicateLead) + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Lead], 'Only master lead should remain after merge.'); + Assert.areEqual(masterLead.Id, [SELECT Id FROM Lead LIMIT 1].Id, 'Master lead should survive the merge.'); + + Assert.areEqual(1, result.merges().size(), 'Merged operation result should contain 1 result.'); + + DML.OperationResult operationResult = result.mergesOf(Lead.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Merged operation result should contain the merged record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Merged operation result should contain the merged record results.'); + Assert.areEqual(Lead.SObjectType, operationResult.objectType(), 'Merged operation result should contain Lead object type.'); + Assert.areEqual(DML.OperationType.MERGE_DML, operationResult.operationType(), 'Merged operation result should contain merge type.'); + Assert.isFalse(operationResult.hasFailures(), 'Merged operation result should not have failures.'); + } @IsTest - static void toUndeleteWithSystemModeAndWithSharing() { + static void toMergeContacts() { // Setup - Case newCase = getCase(1); - insert newCase; + Contact masterContact = getContact(1); + Contact duplicateContact = getContact(2); + insert new List{ masterContact, duplicateContact }; - delete newCase; + // Test + Test.startTest(); + DML.Result result = new DML() + .toMerge(masterContact, duplicateContact) + .commitWork(); + Test.stopTest(); - Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Cases should be deleted.'); + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Contact], 'Only master contact should remain after merge.'); + Assert.areEqual(masterContact.Id, [SELECT Id FROM Contact LIMIT 1].Id, 'Master contact should survive the merge.'); - Exception expectedException = null; + Assert.areEqual(1, result.merges().size(), 'Merged operation result should contain 1 result.'); + + DML.OperationResult operationResult = result.mergesOf(Contact.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Merged operation result should contain the merged record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Merged operation result should contain the merged record results.'); + Assert.areEqual(Contact.SObjectType, operationResult.objectType(), 'Merged operation result should contain Contact object type.'); + Assert.areEqual(DML.OperationType.MERGE_DML, operationResult.operationType(), 'Merged operation result should contain merge type.'); + Assert.isFalse(operationResult.hasFailures(), 'Merged operation result should not have failures.'); + } + + @IsTest + static void toMergeSingleRecordWithMocking() { + // Setup + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + + masterAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); + duplicateAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').allMerges(); // Test Test.startTest(); - System.runAs(minimumAccessUser()) { - try { - new DML() - .toUndelete(newCase) - .systemMode() - .withSharing() - .commitWork(); - } catch (Exception e) { - expectedException = e; - } - } + new DML() + .toMerge(masterAccount, duplicateAccount) + .identifier('dmlMockId') + .commitWork(); Test.stopTest(); + DML.Result result = DML.retrieveResultFor('dmlMockId'); + // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Case], 'Case should not be undeleted, because user has no access with sharing mode.'); - Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); - Assert.isTrue(expectedException.getMessage().contains('insufficient access rights on object id'), 'Expected exception message should be thrown.'); + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be in the database.'); + Assert.areEqual(1, result.merges().size(), 'Merged operation result should contain 1 result.'); + Assert.areEqual(Account.SObjectType, result.mergesOf(Account.SObjectType).objectType(), 'Merged operation result should contain Account object type.'); + Assert.areEqual(DML.OperationType.MERGE_DML, result.mergesOf(Account.SObjectType).operationType(), 'Merged operation result should contain merge type.'); + Assert.areEqual(1, result.mergesOf(Account.SObjectType).records().size(), 'Merged operation result should contain the merged record.'); + Assert.areEqual(1, result.mergesOf(Account.SObjectType).recordResults().size(), 'Merged operation result should contain the merged record results.'); + Assert.isTrue(result.mergesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Merged operation result should contain a successful record result.'); + Assert.isNotNull(result.mergesOf(Account.SObjectType).recordResults()[0].id(), 'Merged operation result should contain a mocked record Id.'); } @IsTest - static void toUndeleteSingleRecordWithMocking() { + static void toMergeMultipleRecordTypesWithMocking() { // Setup - Account account1 = getAccount(1); - account1.Id = DML.randomIdGenerator.get(Account.SObjectType); + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + Lead masterLead = getLead(1); + Lead duplicateLead = getLead(2); - DML.mock('dmlMockId').allUndeletes(); + masterAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); + duplicateAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); + masterLead.Id = DML.randomIdGenerator.get(Lead.SObjectType); + duplicateLead.Id = DML.randomIdGenerator.get(Lead.SObjectType); + + DML.mock('dmlMockId').allMerges(); // Test Test.startTest(); new DML() - .toUndelete(account1) + .toMerge(masterAccount, duplicateAccount) + .toMerge(masterLead, duplicateLead) .identifier('dmlMockId') .commitWork(); Test.stopTest(); @@ -2833,32 +4694,41 @@ private class DML_Test { DML.Result result = DML.retrieveResultFor('dmlMockId'); // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); - Assert.areEqual(1, result.undeletes().size(), 'Undeleted operation result should contain 1 result.'); - Assert.areEqual(Account.SObjectType, result.deletesOf(Account.SObjectType).objectType(), 'Deleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Account.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.areEqual(1, result.undeletesOf(Account.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted record.'); - Assert.areEqual(1, result.undeletesOf(Account.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.isTrue(result.undeletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); - Assert.isNotNull(result.undeletesOf(Account.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); + Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No Account records should be in the database.'); + Assert.areEqual(0, [SELECT COUNT() FROM Lead], 'No Lead records should be in the database.'); + Assert.areEqual(2, result.merges().size(), 'Merged operation result should contain 2 results.'); + Assert.areEqual(Account.SObjectType, result.mergesOf(Account.SObjectType).objectType(), 'Merged operation result should contain Account object type.'); + Assert.areEqual(Lead.SObjectType, result.mergesOf(Lead.SObjectType).objectType(), 'Merged operation result should contain Lead object type.'); + Assert.areEqual(DML.OperationType.MERGE_DML, result.mergesOf(Account.SObjectType).operationType(), 'Merged operation result should contain merge type.'); + Assert.areEqual(DML.OperationType.MERGE_DML, result.mergesOf(Lead.SObjectType).operationType(), 'Merged operation result should contain merge type.'); + Assert.areEqual(1, result.mergesOf(Account.SObjectType).records().size(), 'Merged operation result should contain the merged record.'); + Assert.areEqual(1, result.mergesOf(Account.SObjectType).recordResults().size(), 'Merged operation result should contain the merged record results.'); + Assert.areEqual(1, result.mergesOf(Lead.SObjectType).records().size(), 'Merged operation result should contain the merged record.'); + Assert.areEqual(1, result.mergesOf(Lead.SObjectType).recordResults().size(), 'Merged operation result should contain the merged record results.'); + Assert.isTrue(result.mergesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Merged operation result should contain a successful record result.'); + Assert.isTrue(result.mergesOf(Lead.SObjectType).recordResults()[0].isSuccess(), 'Merged operation result should contain a successful record result.'); } @IsTest - static void toUndeleteMultipleRecordsWithMocking() { + static void toMergeWithMockingSpecificSObjectType() { // Setup - Account account1 = getAccount(1); - Account account2 = getAccount(2); + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + insert new List{ masterAccount, duplicateAccount }; - account1.Id = DML.randomIdGenerator.get(Account.SObjectType); - account2.Id = DML.randomIdGenerator.get(Account.SObjectType); + Lead masterLead = getLead(1); + Lead duplicateLead = getLead(2); - DML.mock('dmlMockId').allUndeletes(); + masterLead.Id = DML.randomIdGenerator.get(Lead.SObjectType); + duplicateLead.Id = DML.randomIdGenerator.get(Lead.SObjectType); + + DML.mock('dmlMockId').mergesFor(Lead.SObjectType); // Test Test.startTest(); new DML() - .toUndelete(account1) - .toUndelete(account2) + .toMerge(masterAccount, duplicateAccount) + .toMerge(masterLead, duplicateLead) .identifier('dmlMockId') .commitWork(); Test.stopTest(); @@ -2866,106 +4736,179 @@ private class DML_Test { DML.Result result = DML.retrieveResultFor('dmlMockId'); // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No records should be inserted to the database.'); - Assert.areEqual(1, result.undeletes().size(), 'Undeleted operation result should contain 1 result.'); - Assert.areEqual(Account.SObjectType, result.undeletesOf(Account.SObjectType).objectType(), 'Undeleted operation result should contain Account object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Account.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.areEqual(2, result.undeletesOf(Account.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted records.'); - Assert.areEqual(2, result.undeletesOf(Account.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.isTrue(result.undeletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); - Assert.isTrue(result.undeletesOf(Account.SObjectType).recordResults()[1].isSuccess(), 'Undeleted operation result should contain a successful record result.'); - Assert.isNotNull(result.undeletesOf(Account.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); - Assert.isNotNull(result.undeletesOf(Account.SObjectType).recordResults()[1].id(), 'Undeleted operation result should contain a mocked record Id.'); + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account merge should not be mocked.'); + Assert.areEqual(0, [SELECT COUNT() FROM Lead], 'Lead merge should be mocked.'); + Assert.areEqual(2, result.merges().size(), 'Merged operation result should contain 2 results.'); + Assert.areEqual(Account.SObjectType, result.mergesOf(Account.SObjectType).objectType(), 'Merged operation result should contain Account object type.'); + Assert.areEqual(Lead.SObjectType, result.mergesOf(Lead.SObjectType).objectType(), 'Merged operation result should contain Lead object type.'); } @IsTest - static void toUndeleteMultipleRecordTypesWithMocking() { + static void toMergeWithMockingException() { // Setup - Account account1 = getAccount(1); - Contact contact1 = getContact(1); + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + + masterAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); + duplicateAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').exceptionOnMerges(); + + Exception expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toMerge(masterAccount, duplicateAccount) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } + + @IsTest + static void toMergeWithMockingExceptionWhenAllOrNoneIsSet() { + // Setup + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + + masterAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); + duplicateAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').exceptionOnMerges(); + + Exception expectedException = null; + DML.Result result = null; + + // Test + Test.startTest(); + try { + result = new DML() + .toMerge(masterAccount, duplicateAccount) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.mergesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.MERGE_DML, operationResult.operationType(), 'Result should contain merge type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); + } + + @IsTest + static void toMergeWithMockingExceptionForSpecificSObjectType() { + // Setup + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + + masterAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); + duplicateAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); + + DML.mock('dmlMockId').exceptionOnMergesFor(Account.SObjectType); + + Exception expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toMerge(masterAccount, duplicateAccount) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } + + @IsTest + static void toMergeWithMockingExceptionForSpecificSObjectTypeWhenAllOrNoneIsSet() { + // Setup + Account masterAccount = getAccount(1); + Account duplicateAccount = getAccount(2); + + masterAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); + duplicateAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); - account1.Id = DML.randomIdGenerator.get(Account.SObjectType); - contact1.Id = DML.randomIdGenerator.get(Contact.SObjectType); + DML.mock('dmlMockId').exceptionOnMergesFor(Account.SObjectType); - DML.mock('dmlMockId').allUndeletes(); + Exception expectedException = null; + DML.Result result = null; // Test Test.startTest(); - new DML() - .toUndelete(account1) - .toUndelete(contact1) - .identifier('dmlMockId') - .commitWork(); + try { + result = new DML() + .toMerge(masterAccount, duplicateAccount) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } Test.stopTest(); - DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'No Account records should exist, because undelete was mocked.'); - Assert.areEqual(0, [SELECT COUNT() FROM Contact], 'No Contact records should exist, because undelete was mocked.'); - Assert.areEqual(2, result.undeletes().size(), 'Undeleted operation result should contain 2 results.'); - Assert.areEqual(Account.SObjectType, result.undeletesOf(Account.SObjectType).objectType(), 'Undeleted operation result should contain Account object type.'); - Assert.areEqual(Contact.SObjectType, result.undeletesOf(Contact.SObjectType).objectType(), 'Undeleted operation result should contain Contact object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Account.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Contact.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.areEqual(1, result.undeletesOf(Account.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted record.'); - Assert.areEqual(1, result.undeletesOf(Account.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.areEqual(1, result.undeletesOf(Contact.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted record.'); - Assert.areEqual(1, result.undeletesOf(Contact.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.isTrue(result.undeletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); - Assert.isTrue(result.undeletesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); - Assert.isNotNull(result.undeletesOf(Account.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); - Assert.isNotNull(result.undeletesOf(Contact.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); - } + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.mergesOf(Account.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Result should contain Account object type.'); + Assert.areEqual(DML.OperationType.MERGE_DML, operationResult.operationType(), 'Result should contain merge type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); + } @IsTest - static void toUndeleteMultipleRecordsWithMockingSpecificSObjectType() { + static void toMergeWithEmptyRecordsWhenMocking() { // Setup - Account account1 = getAccount(1); - insert account1; - delete account1; + Account masterAccount = getAccount(1); + masterAccount.Id = DML.randomIdGenerator.get(Account.SObjectType); - Contact contact1 = getContact(1); - insert contact1; - delete contact1; - - DML.mock('dmlMockId').undeletesFor(Account.SObjectType); + DML.mock('dmlMockId').allMerges(); // Test Test.startTest(); - Integer dmlStatementsBefore = Limits.getDMLStatements(); - - new DML() - .toUndelete(account1) - .toUndelete(contact1) - .identifier('dmlMockId') - .commitWork(); + Exception expectedException = null; - Integer dmlStatementsAfter = Limits.getDMLStatements(); + try { + new DML() + .toMerge(masterAccount, new List()) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } Test.stopTest(); - DML.Result result = DML.retrieveResultFor('dmlMockId'); - // Verify - Assert.areEqual(0, [SELECT COUNT() FROM Account], 'Account should not be undeleted in the database.'); - Assert.areEqual(1, [SELECT COUNT() FROM Contact], 'Contact should be undeleted in the database.'); - Assert.areEqual(1, dmlStatementsAfter - dmlStatementsBefore, 'DML statements should be 1, because second commitWork() should not do anything.'); - Assert.areEqual(2, result.undeletes().size(), 'Undeleted operation result should contain 2 results.'); - Assert.areEqual(Account.SObjectType, result.undeletesOf(Account.SObjectType).objectType(), 'Undeleted operation result should contain Account object type.'); - Assert.areEqual(Contact.SObjectType, result.undeletesOf(Contact.SObjectType).objectType(), 'Undeleted operation result should contain Contact object type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Account.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, result.undeletesOf(Contact.SObjectType).operationType(), 'Undeleted operation result should contain undelete type.'); - Assert.areEqual(1, result.undeletesOf(Account.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted record.'); - Assert.areEqual(1, result.undeletesOf(Account.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.areEqual(1, result.undeletesOf(Contact.SObjectType).records().size(), 'Undeleted operation result should contain the undeleted record.'); - Assert.areEqual(1, result.undeletesOf(Contact.SObjectType).recordResults().size(), 'Undeleted operation result should contain the undeleted record results.'); - Assert.isTrue(result.undeletesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); - Assert.isTrue(result.undeletesOf(Contact.SObjectType).recordResults()[0].isSuccess(), 'Undeleted operation result should contain a successful record result.'); - Assert.isNotNull(result.undeletesOf(Account.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); - Assert.isNotNull(result.undeletesOf(Contact.SObjectType).recordResults()[0].id(), 'Undeleted operation result should contain a mocked record Id.'); - } + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + Assert.isTrue(expectedException.getMessage().contains('A single merge request must contain at least one record to merge, got none'), 'Expected exception message should be thrown.'); + } - // PLATFORM EVENT + // PLATFORM EVENT @IsTest static void toPublishSingleRecord() { @@ -3163,6 +5106,199 @@ private class DML_Test { Assert.isNotNull(result.eventsOf(FlowOrchestrationEvent.SObjectType).recordResults()[1].id(), 'Published operation result should contain a mocked record Id.'); } + @IsTest + static void toPublishWithMockingException() { + // Setup + FlowOrchestrationEvent event = new FlowOrchestrationEvent(); + + DML.mock('dmlMockId').exceptionOnPublishes(); + + Exception expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toPublish(event) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } + + @IsTest + static void toPublishWithMockingExceptionWhenAllOrNoneIsSet() { + // Setup + FlowOrchestrationEvent event = new FlowOrchestrationEvent(); + + DML.mock('dmlMockId').exceptionOnPublishes(); + + Exception expectedException = null; + DML.Result result = null; + + // Test + Test.startTest(); + try { + result = new DML() + .toPublish(event) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.eventsOf(FlowOrchestrationEvent.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(FlowOrchestrationEvent.SObjectType, operationResult.objectType(), 'Result should contain FlowOrchestrationEvent object type.'); + Assert.areEqual(DML.OperationType.PUBLISH_DML, operationResult.operationType(), 'Result should contain publish type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); + } + + @IsTest + static void toPublishWithMockingExceptionForSpecificSObjectType() { + // Setup + FlowOrchestrationEvent event = new FlowOrchestrationEvent(); + + DML.mock('dmlMockId').exceptionOnPublishesFor(FlowOrchestrationEvent.SObjectType); + + Exception expectedException = null; + + // Test + Test.startTest(); + try { + new DML() + .toPublish(event) + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNotNull(expectedException, 'Expected exception to be thrown.'); + } + + @IsTest + static void toPublishWithMockingExceptionForSpecificSObjectTypeWhenAllOrNoneIsSet() { + // Setup + FlowOrchestrationEvent event = new FlowOrchestrationEvent(); + + DML.mock('dmlMockId').exceptionOnPublishesFor(FlowOrchestrationEvent.SObjectType); + + Exception expectedException = null; + DML.Result result = null; + + // Test + Test.startTest(); + try { + result = new DML() + .toPublish(event) + .allowPartialSuccess() + .identifier('dmlMockId') + .commitWork(); + } catch (Exception e) { + expectedException = e; + } + Test.stopTest(); + + // Verify + Assert.isNull(expectedException, 'Expected exception to not be thrown.'); + + DML.OperationResult operationResult = result.eventsOf(FlowOrchestrationEvent.SObjectType); + + Assert.areEqual(1, operationResult.records().size(), 'Result should contain 1 record.'); + Assert.areEqual(1, operationResult.recordResults().size(), 'Result should contain 1 record result.'); + Assert.areEqual(FlowOrchestrationEvent.SObjectType, operationResult.objectType(), 'Result should contain FlowOrchestrationEvent object type.'); + Assert.areEqual(DML.OperationType.PUBLISH_DML, operationResult.operationType(), 'Result should contain publish type.'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Result should contain a failed record result.'); + Assert.isTrue(operationResult.hasFailures(), 'Result should have failures.'); + } + + @IsTest + static void toPublishWithEmptyRecords() { + // Test + Test.startTest(); + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + DML.Result result = new DML() + .toPublish(new List()) + .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); + Assert.areEqual(1, result.events().size(), 'Published operation result should contain 1 result.'); + + DML.OperationResult operationResult = result.eventsOf(FlowOrchestrationEvent.SObjectType); + + Assert.areEqual(0, operationResult.records().size(), 'Published operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Published operation result should contain 0 record results.'); + Assert.areEqual(FlowOrchestrationEvent.SObjectType, operationResult.objectType(), 'Published operation result should contain FlowOrchestrationEvent object type.'); + Assert.areEqual(DML.OperationType.PUBLISH_DML, operationResult.operationType(), 'Published operation result should contain publish type.'); + Assert.isFalse(operationResult.hasFailures(), 'Published operation result should not have failures.'); + } + + @IsTest + static void toPublishWithEmptyRecordsWhenMocking() { + // Setup + DML.mock('dmlMockId').allPublishes(); + + // Test + Test.startTest(); + Integer dmlStatementsBefore = Limits.getDMLStatements(); + + new DML() + .toPublish(new List()) + .identifier('dmlMockId') + .commitWork(); + + Integer dmlStatementsAfter = Limits.getDMLStatements(); + Test.stopTest(); + + // Verify + Assert.areEqual(0, dmlStatementsAfter - dmlStatementsBefore, 'No DML statements should be executed.'); + + DML.Result result = DML.retrieveResultFor('dmlMockId'); + + Assert.areEqual(0, result.inserts().size(), 'Inserted operation result should contain 0 results.'); + Assert.areEqual(0, result.upserts().size(), 'Upserted operation result should contain 0 results.'); + Assert.areEqual(0, result.updates().size(), 'Updated operation result should contain 0 results.'); + Assert.areEqual(0, result.deletes().size(), 'Deleted operation result should contain 0 results.'); + Assert.areEqual(0, result.undeletes().size(), 'Undeleted operation result should contain 0 results.'); + Assert.areEqual(0, result.merges().size(), 'Merged operation result should contain 0 results.'); + Assert.areEqual(1, result.events().size(), 'Published operation result should contain 1 result.'); + + DML.OperationResult operationResult = result.eventsOf(FlowOrchestrationEvent.SObjectType); + + Assert.areEqual(0, operationResult.records().size(), 'Published operation result should contain 0 records.'); + Assert.areEqual(0, operationResult.recordResults().size(), 'Published operation result should contain 0 record results.'); + Assert.areEqual(FlowOrchestrationEvent.SObjectType, operationResult.objectType(), 'Published operation result should contain FlowOrchestrationEvent object type.'); + Assert.areEqual(DML.OperationType.PUBLISH_DML, operationResult.operationType(), 'Published operation result should contain publish type.'); + Assert.isFalse(operationResult.hasFailures(), 'Published operation result should not have failures.'); + } + // DEBUG @IsTest @@ -3931,8 +6067,15 @@ private class DML_Test { // Verify Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account should be inserted.'); - Assert.areEqual(1, [SELECT COUNT() FROM Contact], 'Contact should be inserted.'); - Assert.areEqual(1, [SELECT COUNT() FROM Opportunity], 'Opportunity should be inserted.'); + Assert.areEqual(1, [SELECT COUNT() FROM Contact WHERE AccountId != null], 'Contact should be inserted.'); + Assert.areEqual(1, [SELECT COUNT() FROM Opportunity WHERE AccountId != null], 'Opportunity should be inserted.'); + + Assert.isNotNull(contact1.Id, 'Contact should be inserted and have an Id.'); + Assert.isNotNull(opportunity1.Id, 'Opportunity should be inserted and have an Id.'); + Assert.isNotNull(account1.Id, 'Account should be inserted and have an Id.'); + + Assert.areEqual(account1.Id, contact1.AccountId, 'Contact should be related to Account.'); + Assert.areEqual(account1.Id, opportunity1.AccountId, 'Opportunity should be related to Account.'); } // SHARED @@ -3954,56 +6097,56 @@ private class DML_Test { Assert.areEqual(1, dmlStatementsAfter - dmlStatementsBefore, 'DML statements should be 1, because second commitWork() should not do anything.'); } - // COMPLEX - - @IsTest - static void registerComplex() { - // Setup - DML uow = new DML(); - - // Test - Test.startTest(); - for(Integer i = 0 ; i < 10 ; i++) { - Opportunity newOpportunity = new Opportunity( - Name = 'UoW Test Name ' + i, - StageName = 'Open', - CloseDate = System.today() - ); - - uow.toInsert(newOpportunity); - - for(Integer j = 0 ; j < i + 1 ; j++) { - Product2 product = new Product2(Name = newOpportunity.Name + ' : Product : ' + i); - - uow.toInsert(product); - - PricebookEntry pbe = new PricebookEntry( - UnitPrice = 10, - IsActive = true, - UseStandardPrice = false, - Pricebook2Id = Test.getStandardPricebookId() - ); - - uow.toInsert( - DML.Record(pbe) - .withRelationship(PricebookEntry.Product2Id, product) - ); - - uow.toInsert( - DML.Record(new OpportunityLineItem(Quantity = 1, TotalPrice = 10)) - .withRelationship(OpportunityLineItem.PricebookEntryId, pbe) - .withRelationship(OpportunityLineItem.OpportunityId, newOpportunity) - ); - } - } - uow.commitWork(); - Test.stopTest(); - - // Verify - Assert.areEqual(10, [SELECT COUNT() FROM Opportunity], 'Opportunities should be inserted.'); - Assert.areEqual(55, [SELECT COUNT() FROM Product2], 'Products should be inserted.'); - Assert.areEqual(55, [SELECT COUNT() FROM PricebookEntry], 'Pricebook entries should be inserted.'); - } + // COMPLEX + + @IsTest + static void registerComplex() { + // Setup + DML uow = new DML(); + + // Test + Test.startTest(); + for(Integer i = 0 ; i < 10 ; i++) { + Opportunity newOpportunity = new Opportunity( + Name = 'UoW Test Name ' + i, + StageName = 'Open', + CloseDate = System.today() + ); + + uow.toInsert(newOpportunity); + + for(Integer j = 0 ; j < i + 1 ; j++) { + Product2 product = new Product2(Name = newOpportunity.Name + ' : Product : ' + i); + + uow.toInsert(product); + + PricebookEntry pbe = new PricebookEntry( + UnitPrice = 10, + IsActive = true, + UseStandardPrice = false, + Pricebook2Id = Test.getStandardPricebookId() + ); + + uow.toInsert( + DML.Record(pbe) + .withRelationship(PricebookEntry.Product2Id, product) + ); + + uow.toInsert( + DML.Record(new OpportunityLineItem(Quantity = 1, TotalPrice = 10)) + .withRelationship(OpportunityLineItem.PricebookEntryId, pbe) + .withRelationship(OpportunityLineItem.OpportunityId, newOpportunity) + ); + } + } + uow.commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(10, [SELECT COUNT() FROM Opportunity], 'Opportunities should be inserted.'); + Assert.areEqual(55, [SELECT COUNT() FROM Product2], 'Products should be inserted.'); + Assert.areEqual(55, [SELECT COUNT() FROM PricebookEntry], 'Pricebook entries should be inserted.'); + } // HOOK @@ -4040,9 +6183,9 @@ private class DML_Test { Assert.isNotNull(result, 'Result should not be null.'); } - } + } - // HELPERS + // HELPERS static Account getAccount(Integer index) { return new Account(Name = 'Test Account ' + index); @@ -4061,47 +6204,43 @@ private class DML_Test { } static Case getCase(Integer index) { - return new Case(Status = 'New', Origin = 'Web'); - } - - static Task getTask(Integer index) { - return new Task(Subject = 'Test ' + index, Type = 'Other'); + return new Case(Status = 'New', Origin = 'Web', Subject = 'Test ' + index); } - static Account insertAccount() { - Account account = new Account(Name = 'Test Account'); - insert account; + static Account insertAccount() { + Account account = new Account(Name = 'Test Account'); + insert account; - return account; - } + return account; + } @SuppressWarnings('PMD.AvoidNonRestrictiveQueries') static List getAccounts() { return [SELECT Id, Name FROM Account]; - } - - static List insertAccounts() { - List accounts = new List{ - new Account(Name = 'Test Account 1'), - new Account(Name = 'Test Account 2'), - new Account(Name = 'Test Account 3') - }; - insert accounts; - - return accounts; - } - - static User minimumAccessUser() { - return new User( - Alias = 'newUser', - Email = 'newuser@testorg.com', - EmailEncodingKey = 'UTF-8', - LastName = 'Testing', - LanguageLocaleKey = 'en_US', - LocaleSidKey = 'en_US', - Profile = new Profile(Name = 'Minimum Access - Salesforce'), - TimeZoneSidKey = 'America/Los_Angeles', + } + + static List insertAccounts() { + List accounts = new List{ + new Account(Name = 'Test Account 1'), + new Account(Name = 'Test Account 2'), + new Account(Name = 'Test Account 3') + }; + insert accounts; + + return accounts; + } + + static User minimumAccessUser() { + return new User( + Alias = 'newUser', + Email = 'newuser@testorg.com', + EmailEncodingKey = 'UTF-8', + LastName = 'Testing', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + Profile = new Profile(Name = 'Minimum Access - Salesforce'), + TimeZoneSidKey = 'America/Los_Angeles', UserName = 'btcdmllibuser@testorg.com' - ); - } + ); + } } \ No newline at end of file diff --git a/sfdx-project.json b/sfdx-project.json index c893893..e8b461d 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -1,8 +1,8 @@ { "packageDirectories": [ { - "versionName": "ver 2.0", - "versionNumber": "2.0.0.NEXT", + "versionName": "ver 2.1", + "versionNumber": "2.1.0.NEXT", "path": "force-app", "default": true, "package": "DML Lib", @@ -17,6 +17,7 @@ "DML Lib": "0HoP600000001IfKAI", "DML Lib@0.1.0-1": "04tP6000002A7BxIAK", "DML Lib@1.8.0-1": "04tP6000002A7OrIAK", - "DML Lib@1.9.0-1": "04tP6000002AvdNIAS" + "DML Lib@1.9.0-1": "04tP6000002AvdNIAS", + "DML Lib@2.0.0-1": "04tP6000002CeRxIAK" } } \ No newline at end of file diff --git a/todo b/todo index 779a4e7..9777ade 100644 --- a/todo +++ b/todo @@ -4,6 +4,7 @@ - [x] Fluent Bulk records update without the loop e.g. - [x] DML.Records(opportunities).with(Opportunity.Status, 'Closed Won') — loop-free builder that batches field patches, applies FLS (per mode), then registers the op. - [ ] Bulk DMLs — group by SObjectType and operation; execute in chunked batches (default 200) to minimize statements and respect limits. +- [ ] when the same record is added with the same id, avoid duplication, apply for update and upser - [ ] Savepoint + rollback — wrap commitWork() in a savepoint; - [x] on error rollback everything; - [ ] clear internal state afterward. @@ -21,7 +22,7 @@ - [x] with sharing - [x] without sharing - [ ] Sharing mode configurable per operation (?) -- [ ] Relationships +- [x] Relationships - [x] Deferred relationship resolution — resolve lookups just-in-time during commit (like fflib), not at registration time. - [x] Automatic dependency ordering — parents insert before children; deletes child-first; derive order from relationships if not explicit. - [ ] Preview mode or debug mode (?) - dry-run uow.preview() shows per-type op counts, fields to mutate, effective chunk size/allOrNone/security mode, and pending relationships; no DML. @@ -96,18 +97,18 @@ class UowResult { } ``` - [x] discardWork() — clear all pending operations without committing. -- [ ] Clear internal state after commitWork() — prevent re-commit of same operations. +- [x] Clear internal state after commitWork() — prevent re-commit of same operations. - [ ] Query operations — query(), countQuery(), getQueryLocator(), getCursor() with same access/sharing mode. - [ ] Connector to SOQL Lib — inject selectors for reads; honor chosen sharing mode. -- [ ] Dedupling registration — when records with the same Id are registered and modify the same field: policy LAST_WINS | THROW; track warnings when deduping occurs. +- [x] Dedupling registration — when records with the same Id are registered and modify the same field: policy LAST_WINS | THROW; track warnings when deduping occurs. - [ ] Observability hooks — uow.withLogger(ILogger) to emit timings, limits, retries, per-op summaries; keep lightweight to avoid CPU spikes. - [ ] DML optimization - [ ] Minimize DML statements — batch same-type operations. - - [ ] Build dependency graph — derive parent/child order from registered relationships. + - [x] Build dependency graph — derive parent/child order from registered relationships. - [x] insertImmediately, updateImmediately, etc. - [ ] Email integration — toSendEmail(Messaging.Email) sent during commit. - [ ] Custom work (IDoWork) — registerWork(DML.Work) for custom logic during commit. -- [ ] Lifecycle hooks — onCommitWorkStarting(), onDMLFinished(), onCommitWorkFinished(success) for subclass overrides. + --- ```java @@ -235,13 +236,35 @@ toHardDelete(List records); Account account1 = new Account(Name = 'Account 1'); Contact contact1 = new Contact(LastName = 'Contact 1'); - Opportunity opportunity1 = new Opportunity(Name = 'Opportunity 1'); + Opportunity opportunity1 = new Opportunity(Name = 'Opportunity 1', StageName = 'Discovery', CloseDate = System.today()); new DML() - .toInsert(account1) - .toInsert(DML.Record(contact1).withRelationship(Contact.AccountId, account1)) .toInsert(DML.Record(opportunity1) .withRelationship(Opportunity.AccountId, account1) .withRelationship(Opportunity.Contact__c, contact1) ) - .commitWork(); \ No newline at end of file + .toInsert(DML.Record(contact1).withRelationship(Contact.AccountId, account1)) + .toInsert(DML.Record(account1).withRelationship(Account.Contact__c, contact1)) + .commitWork(); + + +--- + +Status | Priority | Feature | Effort | Impact | +|------|----------|---------|--------|--------| +| [x] | 🔴 Critical | Deferred Relationship Resolution | High | Very High | +| [x] | 🔴 Critical | Automatic Dependency Ordering | High | Very High | +| [x] | 🔴 Critical | Merge Operations | Medium | High | +| [x] | 🔴 Critical | Hard Delete | Low | Medium | +| [x] | 🔴 Critical | Discard Work | Low | Medium | +| [x] | 🟡 Important | Upsert with External ID | Medium | High | +| [ ] | 🟡 Important | Lead Conversion | Medium | Medium | +| [ ] | 🟡 Important | Per-Operation Overrides | High | High | +| [ ] | 🟡 Important | Email Integration | Medium | Medium | +| [ ] | 🟡 Important | Custom Work (IDoWork) | Low | Medium | +| [x] | 🟢 Nice | Dedupling registration | Medium | Medium | +| [ ] | 🟢 Nice | Better Mock API with Errors | Medium | Medium | +| [ ] | 🟢 Nice | Retry Strategy | High | Medium | +| [ ] | 🟢 Nice | Chunk Size | Medium | Medium | +| [ ] | 🟢 Nice | Query Integration | Medium | Low | +| [x] | 🟢 Nice | Lifecycle Hooks | Medium | Low | diff --git a/website/docs/.vitepress/config.mts b/website/docs/.vitepress/config.mts index 0df245a..fd4fad3 100644 --- a/website/docs/.vitepress/config.mts +++ b/website/docs/.vitepress/config.mts @@ -28,7 +28,9 @@ export default defineConfig({ { text: 'Home', link: '/' }, { text: 'Documentation', link: '/introduction' } ], - + search: { + provider: 'local' + }, sidebar: [ { text: 'Docs', @@ -46,6 +48,7 @@ export default defineConfig({ { text: 'Upsert', link: '/dml/upsert' }, { text: 'Delete', link: '/dml/delete' }, { text: 'Undelete', link: '/dml/undelete' }, + { text: 'Merge', link: '/dml/merge' }, { text: 'Publish', link: '/dml/publish' }, { text: 'Result', link: '/result' } ] @@ -54,11 +57,13 @@ export default defineConfig({ text: 'Mocking', collapsed: true, items: [ + { text: 'Introduction', link: '/mocking/mocking' }, { text: 'Insert', link: '/mocking/insert' }, { text: 'Update', link: '/mocking/update' }, { text: 'Upsert', link: '/mocking/upsert' }, { text: 'Delete', link: '/mocking/delete' }, { text: 'Undelete', link: '/mocking/undelete' }, + { text: 'Merge', link: '/mocking/merge' }, { text: 'Publish', link: '/mocking/publish' } ] }, @@ -75,7 +80,8 @@ export default defineConfig({ text: 'Architecture', collapsed: true, items: [ - { text: 'Rollback', link: '/architecture/rollback' } + { text: 'Rollback', link: '/architecture/rollback' }, + { text: 'Registration', link: '/architecture/registration' } ] } ], diff --git a/website/docs/architecture/registration.md b/website/docs/architecture/registration.md new file mode 100644 index 0000000..d5e1854 --- /dev/null +++ b/website/docs/architecture/registration.md @@ -0,0 +1,105 @@ +--- +outline: deep +--- + +# Registration + +## Deduplication Strategy + +When records with the same ID are added to a list and an update is attempted, Salesforce throws the error `System.ListException: Duplicate id in list`. + +**Standard DML** + +```apex +Account account = [SELECT Id, Name FROM Account LIMIT 1]; + +List accountsToUpdate = new List(); + +Account account1 = new Account(Id = account.Id, Name = 'New Account 1', Website = 'mywebsite.com'); +accountsToUpdate.add(account1); + +Account account2 = new Account(Id = account.Id, Name = 'New Account 2'); +accountsToUpdate.add(account2); + +update accountsToUpdate; // Throws: System.ListException: Duplicate id in list +``` + +The same behavior applies in DML Lib. By default, an error will be thrown when the same record is registered multiple times. + +**DML Lib (Default Behavior)** + +```apex +Account account = [SELECT Id, Name FROM Account LIMIT 1]; + +new DML() + .toUpdate(new Account(Id = account.Id, Name = 'New Account 1', Website = 'mywebsite.com')) + .toUpdate(new Account(Id = account.Id, Name = 'New Account 2')) + .commitWork(); // Throws: Duplicate records found during registration +``` + +### combineOnDuplicate + +Use `combineOnDuplicate()` to automatically merge duplicate registrations into a single record. When the same record ID is registered multiple times, field values from later registrations override earlier ones, while preserving fields that are only set in earlier registrations. + +**Signature** + +```apex +Commitable combineOnDuplicate(); +``` + +**Example** + +```apex +Account account = [SELECT Id, Name FROM Account LIMIT 1]; + +new DML() + .combineOnDuplicate() + .toUpdate(new Account(Id = account.Id, Name = 'New Account 1', Website = 'mywebsite.com')) + .toUpdate(new Account(Id = account.Id, Name = 'New Account 2')) + .commitWork(); +``` + +The two records will be merged into one before the DML operation: + +| Field | First Registration | Second Registration | Final Value | +|-------|-------------------|---------------------|-------------| +| Id | account.Id | account.Id | account.Id | +| Name | 'New Account 1' | 'New Account 2' | 'New Account 2' | +| Website | 'mywebsite.com' | - | 'mywebsite.com' | + +Result: `new Account(Id = account.Id, Name = 'New Account 2', Website = 'mywebsite.com')` will be updated. + +## Registration Order + +DML Lib uses Kahn's algorithm (topological sort) to resolve dependencies between records and commit them in the correct order. You can register records in any order — DML Lib will automatically determine the proper execution sequence. + +**Example** + +```apex +Account account = new Account(Name = 'Acme'); +Contact contact = new Contact(LastName = 'Smith'); +Opportunity opportunity = new Opportunity(Name = 'Deal', StageName = 'New', CloseDate = Date.today()); + +new DML() + .toInsert(DML.Record(contact).withRelationship(Contact.AccountId, account)) + .toInsert(DML.Record(opportunity).withRelationship(Opportunity.AccountId, account)) + .toInsert(account) // Registered last, but inserted first + .commitWork(); +``` + +## Minimal DMLs + +DML Lib groups records by SObject type and operation, reducing the number of DML statements to the minimum required. + +**Example** + +```apex +new DML() + .toInsert(new Account(Name = 'Account 1')) + .toInsert(new Account(Name = 'Account 2')) + .toUpsert(new Contact(LastName = 'Smith'), Contact.MyExternalId__c) + .toUpsert(new Contact(LastName = 'Doe'), Contact.MyExternalId__c) + .commitWork(); +``` + +Only 2 DML statements will be executed: one `INSERT` for Accounts, one `UPSERT` for Contacts. Records are automatically bulkified regardless of whether they are registered together or in separate method calls. \ No newline at end of file diff --git a/website/docs/dml/merge.md b/website/docs/dml/merge.md new file mode 100644 index 0000000..30fbb3c --- /dev/null +++ b/website/docs/dml/merge.md @@ -0,0 +1,151 @@ +--- +outline: deep +--- + +# Merge + +Merge duplicate records into a master record. + +**Example** + +```apex +Account masterAccount = [SELECT Id FROM Account WHERE ...]; +Account duplicate1 = [SELECT Id FROM Account WHERE ...]; +Account duplicate2 = [SELECT Id FROM Account WHERE ...]; + +new DML() + .toMerge(masterAccount, new List{ duplicate1, duplicate2 }) + .systemMode() + .withoutSharing() + .commitWork(); +``` + +## toMerge + +Register records for merging. The actual DML is executed when `commitWork()` is called. + +**Signature** + +```apex +Commitable toMerge(SObject mergeToRecord, SObject duplicatedRecord); +Commitable toMerge(SObject mergeToRecord, List duplicateRecords); +Commitable toMerge(SObject mergeToRecord, Id duplicatedRecordId); +Commitable toMerge(SObject mergeToRecord, Iterable duplicatedRecordIds); +``` + +### Single Duplicate Record + +**Signature** + +```apex +Commitable toMerge(SObject mergeToRecord, SObject duplicatedRecord); +``` + +**Standard DML** + +```apex +Account masterAccount = [SELECT Id FROM Account WHERE ...]; +Account duplicateAccount = [SELECT Id FROM Account WHERE ...]; + +Database.merge(masterAccount, duplicateAccount); +``` + +**DML Lib** + +```apex +Account masterAccount = [SELECT Id FROM Account WHERE ...]; +Account duplicateAccount = [SELECT Id FROM Account WHERE ...]; + +new DML() + .toMerge(masterAccount, duplicateAccount) + .commitWork(); +``` + +#### By Record ID + +Merge using a duplicate record ID directly. + +**Signature** + +```apex +Commitable toMerge(SObject mergeToRecord, Id duplicatedRecordId); +``` + +**Standard DML** + +```apex +Account masterAccount = [SELECT Id FROM Account WHERE ...]; +Id duplicateAccountId = '001xx000003DGbYAAW'; + +Database.merge(masterAccount, duplicateAccountId); +``` + +**DML Lib** + +```apex +Account masterAccount = [SELECT Id FROM Account WHERE ...]; +Id duplicateAccountId = '001xx000003DGbYAAW'; + +new DML() + .toMerge(masterAccount, duplicateAccountId) + .commitWork(); +``` + +### Multiple Duplicate Records + +**Signature** + +```apex +Commitable toMerge(SObject mergeToRecord, List duplicateRecords); +Commitable toMerge(SObject mergeToRecord, Iterable duplicatedRecordIds); +``` + +**Standard DML** + +```apex +Account masterAccount = [SELECT Id FROM Account WHERE ...]; +List duplicateAccounts = [SELECT Id FROM Account WHERE ...]; + +Database.merge(masterAccount, duplicateAccounts); +``` + +**DML Lib** + +```apex +Account masterAccount = [SELECT Id FROM Account WHERE ...]; +List duplicateAccounts = [SELECT Id FROM Account WHERE ...]; + +new DML() + .toMerge(masterAccount, duplicateAccounts) + .commitWork(); +``` + +#### By Record IDs + +Merge using a collection of duplicate record IDs. + +**Signature** + +```apex +Commitable toMerge(SObject mergeToRecord, Iterable duplicatedRecordIds); +``` + +**Standard DML** + +```apex +Account masterAccount = [SELECT Id FROM Account WHERE ...]; +Set duplicateAccountIds = new Set{ duplicateId1, duplicateId2 }; + +Database.merge(masterAccount, new List(duplicateAccountIds)); +``` + +**DML Lib** + +```apex +Account masterAccount = [SELECT Id FROM Account WHERE ...]; +Set duplicateAccountIds = new Set{ duplicateId1, duplicateId2 }; + +new DML() + .toMerge(masterAccount, duplicateAccountIds) + .commitWork(); +``` diff --git a/website/docs/dml/upsert.md b/website/docs/dml/upsert.md index 3365d27..22e9a94 100644 --- a/website/docs/dml/upsert.md +++ b/website/docs/dml/upsert.md @@ -30,8 +30,10 @@ Register records for upsert. The actual DML is executed when `commitWork()` is c ```apex Commitable toUpsert(SObject record); +Commitable toUpsert(SObject record, SObjectField externalIdField); Commitable toUpsert(DML.Record record); -Commitable toUpsert(Iterable records); +Commitable toUpsert(List records); +Commitable toUpsert(List records, SObjectField externalIdField); Commitable toUpsert(DML.Records records); ``` @@ -124,6 +126,39 @@ new DML() .commitWork(); ``` +#### With External Id Field + +Upsert using a custom external ID field instead of the standard Id field. + +**Signature** + +```apex +Commitable toUpsert(SObject record, SObjectField externalIdField); +``` + +**Standard DML** + +```apex +Account account = new Account( + MyExternalId__c = 'EXT-001', + Name = 'Acme Corp' +); +Database.upsert(account, Account.MyExternalId__c); +``` + +**DML Lib** + +```apex +Account account = new Account( + MyExternalId__c = 'EXT-001', + Name = 'Acme Corp' +); + +new DML() + .toUpsert(account, Account.MyExternalId__c) + .commitWork(); +``` + ### Multiple Records **Signature** @@ -234,6 +269,39 @@ new DML() .commitWork(); ``` +#### With External Id Field + +Upsert multiple records using a custom external ID field instead of the standard Id field. + +**Signature** + +```apex +Commitable toUpsert(List records, SObjectField externalIdField); +``` + +**Standard DML** + +```apex +List accounts = new List{ + new Account(MyExternalId__c = 'EXT-001', Name = 'Acme Corp'), + new Account(MyExternalId__c = 'EXT-002', Name = 'Global Inc') +}; +Database.upsert(accounts, Account.MyExternalId__c); +``` + +**DML Lib** + +```apex +List accounts = new List{ + new Account(MyExternalId__c = 'EXT-001', Name = 'Acme Corp'), + new Account(MyExternalId__c = 'EXT-002', Name = 'Global Inc') +}; + +new DML() + .toUpsert(accounts, Account.MyExternalId__c) + .commitWork(); +``` + ## upsertImmediately Upsert records immediately and return the operation result without calling `commitWork()`. diff --git a/website/docs/installation.md b/website/docs/installation.md index c4d9153..1115aa8 100644 --- a/website/docs/installation.md +++ b/website/docs/installation.md @@ -10,21 +10,21 @@ Install the SOQL Lib unlocked package with `btcdev` namespace to your Salesforce environment: -`/packaging/installPackage.apexp?p0=04tP6000002AvdNIAS` +`/packaging/installPackage.apexp?p0=04tP6000002CeRxIAK` -[Install on Sandbox](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tP6000002AvdNIAS) +[Install on Sandbox](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tP6000002CeRxIAK) -[Install on Production](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tP6000002AvdNIAS) +[Install on Production](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tP6000002CeRxIAK) ## Install via Unmanaged Package Install the DML Lib unmanaged package without namespace to your Salesforce environment: -`/packaging/installPackage.apexp?p0=04tP60000029Hmr` +`/packaging/installPackage.apexp?p0=04tP60000029HoT` -[Install on Sandbox](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tP60000029Hmr) +[Install on Sandbox](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tP60000029HoT) -[Install on Production](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tP60000029Hmr) +[Install on Production](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tP60000029HoT) ## Deploy via Button diff --git a/website/docs/mocking/delete.md b/website/docs/mocking/delete.md index ea8f11b..561942b 100644 --- a/website/docs/mocking/delete.md +++ b/website/docs/mocking/delete.md @@ -183,10 +183,115 @@ static void shouldAccessRecordResults() { // Verify DML.Result result = DML.retrieveResultFor('AccountService.deleteAccount'); - DML.OperationResult opResult = result.deletesOf(Account.SObjectType); + DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); - Assert.areEqual(Account.SObjectType, opResult.objectType(), 'Should be Account type'); - Assert.areEqual(DML.OperationType.DELETE_DML, opResult.operationType(), 'Should be DELETE operation'); - Assert.isFalse(opResult.hasFailures(), 'Should have no failures'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Should be Account type'); + Assert.areEqual(DML.OperationType.DELETE_DML, operationResult.operationType(), 'Should be DELETE operation'); + Assert.isFalse(operationResult.hasFailures(), 'Should have no failures'); +} +``` + +## Exception + +Simulate DML exceptions for delete operations without touching the database. + +::: tip allowPartialSuccess +When `allowPartialSuccess()` is used, exceptions are **not thrown**. Instead, failures are recorded in the `Result` object. Use `hasFailures()` and `recordResults()` to check for errors. +::: + +### exceptionOnDeletes + +Throw an exception for all delete operations. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnDeletes(); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnDelete() { + // Setup + DML.mock('myDmlId').exceptionOnDeletes(); + + Id accountId = DML.randomIdGenerator.get(Account.SObjectType); + + // Test & Verify + try { + new DML() + .toDelete(accountId) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Delete failed')); + } +} +``` + +### exceptionOnDeletesFor + +Throw an exception only for delete operations on a specific SObject type. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnDeletesFor(SObjectType objectType); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnlyForAccountDeletes() { + // Setup - Exception only for Account deletes + DML.mock('myDmlId').exceptionOnDeletesFor(Account.SObjectType); + + Id accountId = DML.randomIdGenerator.get(Account.SObjectType); + Id contactId = DML.randomIdGenerator.get(Contact.SObjectType); + + // Test & Verify + try { + new DML() + .toDelete(accountId) + .toDelete(contactId) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Delete failed')); + } +} +``` + +### allowPartialSuccess + +When using `allowPartialSuccess()`, failures are captured in the result instead of throwing an exception. + +**Test** + +```apex +@IsTest +static void shouldCaptureFailureInResult() { + // Setup + DML.mock('myDmlId').exceptionOnDeletes(); + + Id accountId = DML.randomIdGenerator.get(Account.SObjectType); + + // Test - no exception thrown + DML.Result result = new DML() + .toDelete(accountId) + .allowPartialSuccess() + .identifier('myDmlId') + .commitWork(); + + // Verify + DML.OperationResult operationResult = result.deletesOf(Account.SObjectType); + + Assert.isTrue(operationResult.hasFailures(), 'Should have failures'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Record should be marked as failed'); } ``` diff --git a/website/docs/mocking/insert.md b/website/docs/mocking/insert.md index 89f69ff..36f53f9 100644 --- a/website/docs/mocking/insert.md +++ b/website/docs/mocking/insert.md @@ -195,17 +195,115 @@ static void shouldAccessRecordResults() { // Verify DML.Result result = DML.retrieveResultFor('AccountService.createAccount'); - DML.OperationResult opResult = result.insertsOf(Account.SObjectType); + DML.OperationResult operationResult = result.insertsOf(Account.SObjectType); // Check operation metadata - Assert.areEqual(Account.SObjectType, opResult.objectType(), 'Should be Account type'); - Assert.areEqual(DML.OperationType.INSERT_DML, opResult.operationType(), 'Should be INSERT operation'); - Assert.isFalse(opResult.hasFailures(), 'Should have no failures'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Should be Account type'); + Assert.areEqual(DML.OperationType.INSERT_DML, operationResult.operationType(), 'Should be INSERT operation'); + Assert.isFalse(operationResult.hasFailures(), 'Should have no failures'); // Check record results - List recordResults = opResult.recordResults(); + List recordResults = operationResult.recordResults(); Assert.areEqual(1, recordResults.size(), 'Should have 1 record result'); Assert.isTrue(recordResults[0].isSuccess(), 'Record should be successful'); Assert.isNotNull(recordResults[0].id(), 'Record should have mocked ID'); } ``` + +## Exception + +Simulate DML exceptions for insert operations without touching the database. + +::: tip allowPartialSuccess +When `allowPartialSuccess()` is used, exceptions are **not thrown**. Instead, failures are recorded in the `Result` object. Use `hasFailures()` and `recordResults()` to check for errors. +::: + +### exceptionOnInserts + +Throw an exception for all insert operations. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnInserts(); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnInsert() { + // Setup + DML.mock('myDmlId').exceptionOnInserts(); + + // Test & Verify + try { + new DML() + .toInsert(new Account(Name = 'Test Account')) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Insert failed')); + } +} +``` + +### exceptionOnInsertsFor + +Throw an exception only for insert operations on a specific SObject type. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnInsertsFor(SObjectType objectType); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnlyForAccountInserts() { + // Setup - Exception only for Account inserts + DML.mock('myDmlId').exceptionOnInsertsFor(Account.SObjectType); + + // Test & Verify + try { + new DML() + .toInsert(new Account(Name = 'Test Account')) + .toInsert(new Contact(LastName = 'Doe')) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Insert failed')); + } +} +``` + +### allowPartialSuccess + +When using `allowPartialSuccess()`, failures are captured in the result instead of throwing an exception. + +**Test** + +```apex +@IsTest +static void shouldCaptureFailureInResult() { + // Setup + DML.mock('myDmlId').exceptionOnInserts(); + + // Test - no exception thrown + DML.Result result = new DML() + .toInsert(new Account(Name = 'Test Account')) + .allowPartialSuccess() + .identifier('myDmlId') + .commitWork(); + + // Verify + DML.OperationResult operationResult = result.insertsOf(Account.SObjectType); + + Assert.isTrue(operationResult.hasFailures(), 'Should have failures'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Record should be marked as failed'); +} +``` diff --git a/website/docs/mocking/merge.md b/website/docs/mocking/merge.md new file mode 100644 index 0000000..aee2618 --- /dev/null +++ b/website/docs/mocking/merge.md @@ -0,0 +1,352 @@ +--- +outline: deep +--- + +# Merge + +Mock merge operations in unit tests to avoid actual database merges. + +::: warning +The `DML.mock()` and `DML.retrieveResultFor()` methods are `@TestVisible` and should only be used in test classes. +::: + +::: tip +- **No database operations**: Mocked merges don't touch the database +- **Records must have IDs**: Both master and duplicate records must have IDs assigned before mocking +- **Results are captured**: All operation details are available via `DML.retrieveResultFor()` +- **Selective mocking**: Use `mergesFor()` to mock specific SObject types while allowing others to execute +::: + +**Example** + +```apex +public class AccountService { + public void mergeAccounts(Id masterId, Id duplicateId) { + Account master = [SELECT Id FROM Account WHERE Id = :masterId]; + Account duplicate = [SELECT Id FROM Account WHERE Id = :duplicateId]; + + new DML() + .toMerge(master, duplicate) + .identifier('AccountService.mergeAccounts') + .commitWork(); + } +} +``` + +```apex +@IsTest +static void shouldMergeAccounts() { + // Setup + Account master = new Account( + Id = DML.randomIdGenerator.get(Account.SObjectType), + Name = 'Master' + ); + Account duplicate = new Account( + Id = DML.randomIdGenerator.get(Account.SObjectType), + Name = 'Duplicate' + ); + + DML.mock('AccountService.mergeAccounts').allMerges(); + + // Test + Test.startTest(); + new AccountService().mergeAccounts(master.Id, duplicate.Id); + Test.stopTest(); + + // Verify + DML.Result result = DML.retrieveResultFor('AccountService.mergeAccounts'); + + DML.OperationResult mergeResult = result.mergesOf(Account.SObjectType); + Assert.areEqual(1, mergeResult.successes().size(), '1 merge should succeed'); +} +``` + +## allMerges + +Mock all merge operations regardless of SObject type. + +**Signature** + +```apex +DML.mock(String identifier).allMerges(); +``` + +**Class** + +```apex +public class MergeService { + public void mergeDuplicates(Account master, Account duplicate) { + new DML() + .toMerge(master, duplicate) + .identifier('MergeService.mergeDuplicates') + .commitWork(); + } +} +``` + +**Test** + +```apex +@IsTest +static void shouldMockMergeOperation() { + // Setup + Account master = new Account( + Id = DML.randomIdGenerator.get(Account.SObjectType) + Name = 'Master' + ); + Account duplicate = new Account( + Id = DML.randomIdGenerator.get(Account.SObjectType) + Name = 'Duplicate' + ); + + DML.mock('MergeService.mergeDuplicates').allMerges(); + + // Test + Test.startTest(); + new MergeService().mergeDuplicates(master, duplicate); + Test.stopTest(); + + // Verify + DML.Result result = DML.retrieveResultFor('MergeService.mergeDuplicates'); + + Assert.areEqual(1, result.merges().size(), '1 merge operation mocked'); + Assert.isTrue(result.mergesOf(Account.SObjectType).recordResults()[0].isSuccess(), 'Merge should succeed'); + Assert.isNotNull(result.mergesOf(Account.SObjectType).recordResults()[0].id(), 'Should have mocked record Id'); +} +``` + +## mergesFor + +Mock merge operations only for a specific SObject type. Other SObject types will be merged in the database. + +**Signature** + +```apex +DML.mock(String identifier).mergesFor(SObjectType objectType); +``` + +**Test** + +```apex +@IsTest +static void shouldMockOnlyLeadMerges() { + // Setup - Real accounts, mocked leads + Account masterAcc = new Account(Name = 'Master Account'); + Account dupAcc = new Account(Name = 'Duplicate Account'); + insert new List{ masterAcc, dupAcc }; + + Lead masterLead = new Lead( + Id = DML.randomIdGenerator.get(Lead.SObjectType), + LastName = 'Master', + Company = 'Test' + ); + Lead dupLead = new Lead( + Id = DML.randomIdGenerator.get(Lead.SObjectType), + LastName = 'Duplicate', + Company = 'Test' + ); + + DML.mock('MergeService.mergeRecords').mergesFor(Lead.SObjectType); + + // Test + Test.startTest(); + new DML() + .toMerge(masterAcc, dupAcc) + .toMerge(masterLead, dupLead) + .identifier('MergeService.mergeRecords') + .commitWork(); + Test.stopTest(); + + // Verify + Assert.areEqual(1, [SELECT COUNT() FROM Account], 'Account merge executed - only master remains'); + Assert.areEqual(1, result.mergesOf(Lead.SObjectType).successes().size(), 'Lead merge mocked'); +} +``` + +## Retrieving Results + +Use `DML.retrieveResultFor()` to access the mocked operation results. + +**Signature** + +```apex +DML.Result result = DML.retrieveResultFor(String identifier); +``` + +**Class** + +```apex +public class MergeService { + public void mergeAccounts(Account master, Account duplicate) { + new DML() + .toMerge(master, duplicate) + .identifier('MergeService.mergeAccounts') + .commitWork(); + } +} +``` + +**Test** + +```apex +@IsTest +static void shouldAccessMergeResults() { + // Setup + Account master = new Account( + Id = DML.randomIdGenerator.get(Account.SObjectType), + Name = 'Master' + ); + Account duplicate = new Account( + Id = DML.randomIdGenerator.get(Account.SObjectType), + Name = 'Duplicate' + ); + + DML.mock('MergeService.mergeAccounts').allMerges(); + + // Test + Test.startTest(); + new MergeService().mergeAccounts(master, duplicate); + Test.stopTest(); + + // Verify + DML.Result result = DML.retrieveResultFor('MergeService.mergeAccounts'); + DML.OperationResult operationResult = result.mergesOf(Account.SObjectType); + + // Check operation metadata + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Should be Account type'); + Assert.areEqual(DML.OperationType.MERGE_DML, operationResult.operationType(), 'Should be MERGE operation'); + Assert.isFalse(operationResult.hasFailures(), 'Should have no failures'); + + // Check record results + List recordResults = operationResult.recordResults(); + Assert.areEqual(1, recordResults.size(), 'Should have 1 record result'); + Assert.isTrue(recordResults[0].isSuccess(), 'Record should be successful'); + Assert.isNotNull(recordResults[0].id(), 'Record should have mocked ID'); +} +``` + +## Exception + +Simulate DML exceptions for merge operations without touching the database. + +::: tip allowPartialSuccess +When `allowPartialSuccess()` is used, exceptions are **not thrown**. Instead, failures are recorded in the `Result` object. Use `hasFailures()` and `recordResults()` to check for errors. +::: + +### exceptionOnMerges + +Throw an exception for all merge operations. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnMerges(); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnMerge() { + // Setup + Account master = new Account( + Id = DML.randomIdGenerator.get(Account.SObjectType), + Name = 'Master' + ); + Account duplicate = new Account( + Id = DML.randomIdGenerator.get(Account.SObjectType), + Name = 'Duplicate' + ); + + DML.mock('myDmlId').exceptionOnMerges(); + + // Test & Verify + try { + new DML() + .toMerge(master, duplicate) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Merge failed')); + } +} +``` + +### exceptionOnMergesFor + +Throw an exception only for merge operations on a specific SObject type. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnMergesFor(SObjectType objectType); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnlyForLeadMerges() { + // Setup - Exception only for Lead merges + Lead masterLead = new Lead( + Id = DML.randomIdGenerator.get(Lead.SObjectType), + LastName = 'Master', + Company = 'Test' + ); + Lead dupLead = new Lead( + Id = DML.randomIdGenerator.get(Lead.SObjectType), + LastName = 'Duplicate', + Company = 'Test' + ); + + DML.mock('myDmlId').exceptionOnMergesFor(Lead.SObjectType); + + // Test & Verify + try { + new DML() + .toMerge(masterLead, dupLead) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Merge failed')); + } +} +``` + +### allowPartialSuccess + +When using `allowPartialSuccess()`, failures are captured in the result instead of throwing an exception. + +**Test** + +```apex +@IsTest +static void shouldCaptureFailureInResult() { + // Setup + Account master = new Account( + Id = DML.randomIdGenerator.get(Account.SObjectType), + Name = 'Master' + ); + Account duplicate = new Account( + Id = DML.randomIdGenerator.get(Account.SObjectType), + Name = 'Duplicate' + ); + + DML.mock('myDmlId').exceptionOnMerges(); + + // Test - no exception thrown + DML.Result result = new DML() + .toMerge(master, duplicate) + .allowPartialSuccess() + .identifier('myDmlId') + .commitWork(); + + // Verify + DML.OperationResult operationResult = result.mergesOf(Account.SObjectType); + + Assert.isTrue(operationResult.hasFailures(), 'Should have failures'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Record should be marked as failed'); +} +``` diff --git a/website/docs/mocking/mocking.md b/website/docs/mocking/mocking.md new file mode 100644 index 0000000..f7a2a06 --- /dev/null +++ b/website/docs/mocking/mocking.md @@ -0,0 +1,98 @@ +--- +outline: deep +--- + +# Mocking + +Mock DML operations in unit tests to avoid actual database changes while still verifying your code's behavior. + +## How It Works + +1. **Add an identifier** to your DML operation in the production code +2. **Register a mock** for that identifier in your test +3. **Execute the code** - DML operations are intercepted, no database changes occur +4. **Verify results** using `DML.retrieveResultFor()` + +## Quick Example + +**Class** + +```apex +public class AccountService { + public void createAccount() { + new DML() + .toInsert(new Account(Name = 'Acme')) + .identifier('AccountService.createAccount') + .commitWork(); + } +} +``` + +**Test** + +```apex +@IsTest +static void shouldCreateAccount() { + // 1. Register mock + DML.mock('AccountService.createAccount').allInserts(); + + // 2. Execute + Test.startTest(); + new AccountService().createAccount(); + Test.stopTest(); + + // 3. Verify - no records in database, but results captured + DML.Result result = DML.retrieveResultFor('AccountService.createAccount'); + Assert.areEqual(1, result.insertsOf(Account.SObjectType).successes().size()); +} +``` + +## Key Benefits + +- **Fast tests** - No database operations means faster execution +- **IDs assigned** - Records receive valid mock IDs for relationship testing +- **Selective mocking** - Mock specific SObject types while allowing others to execute +- **Exception simulation** - Test error handling without real failures + +## Available Methods + +```apex +// Mock all operations +Mockable allDmls(); + +// Mock by operation type +Mockable allInserts(); +Mockable allUpdates(); +Mockable allUpserts(); +Mockable allDeletes(); +Mockable allUndeletes(); +Mockable allMerges(); +Mockable allPublishes(); + +// Mock by SObject type +Mockable insertsFor(SObjectType objectType); +Mockable updatesFor(SObjectType objectType); +Mockable upsertsFor(SObjectType objectType); +Mockable deletesFor(SObjectType objectType); +Mockable undeletesFor(SObjectType objectType); +Mockable mergesFor(SObjectType objectType); +Mockable publishesFor(SObjectType objectType); + +// Simulate exceptions +Mockable exceptionOnInserts(); +Mockable exceptionOnUpdates(); +Mockable exceptionOnUpserts(); +Mockable exceptionOnDeletes(); +Mockable exceptionOnUndeletes(); +Mockable exceptionOnMerges(); +Mockable exceptionOnPublishes(); + +// Simulate exceptions for specific SObject type +Mockable exceptionOnInsertsFor(SObjectType objectType); +Mockable exceptionOnUpdatesFor(SObjectType objectType); +Mockable exceptionOnUpsertsFor(SObjectType objectType); +Mockable exceptionOnDeletesFor(SObjectType objectType); +Mockable exceptionOnUndeletesFor(SObjectType objectType); +Mockable exceptionOnMergesFor(SObjectType objectType); +Mockable exceptionOnPublishesFor(SObjectType objectType); +``` diff --git a/website/docs/mocking/publish.md b/website/docs/mocking/publish.md index aee5e72..2bae124 100644 --- a/website/docs/mocking/publish.md +++ b/website/docs/mocking/publish.md @@ -186,9 +186,107 @@ static void shouldAccessRecordResults() { // Verify DML.Result result = DML.retrieveResultFor('NotificationService.sendAlert'); - DML.OperationResult opResult = result.eventsOf(AlertEvent__e.SObjectType); + DML.OperationResult operationResult = result.eventsOf(AlertEvent__e.SObjectType); - Assert.areEqual(AlertEvent__e.SObjectType, opResult.objectType(), 'Should be AlertEvent__e type'); - Assert.areEqual(DML.OperationType.PUBLISH_DML, opResult.operationType(), 'Should be PUBLISH operation'); + Assert.areEqual(AlertEvent__e.SObjectType, operationResult.objectType(), 'Should be AlertEvent__e type'); + Assert.areEqual(DML.OperationType.PUBLISH_DML, operationResult.operationType(), 'Should be PUBLISH operation'); +} +``` + +## Exception + +Simulate exceptions for publish operations without actually publishing events. + +::: tip allowPartialSuccess +When `allowPartialSuccess()` is used, exceptions are **not thrown**. Instead, failures are recorded in the `Result` object. Use `hasFailures()` and `recordResults()` to check for errors. +::: + +### exceptionOnPublishes + +Throw an exception for all publish operations. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnPublishes(); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnPublish() { + // Setup + DML.mock('myDmlId').exceptionOnPublishes(); + + // Test & Verify + try { + new DML() + .toPublish(new MyEvent__e(Message__c = 'Test')) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Publish failed')); + } +} +``` + +### exceptionOnPublishesFor + +Throw an exception only for publish operations on a specific event type. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnPublishesFor(SObjectType objectType); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnlyForOrderEvents() { + // Setup - Exception only for OrderEvent__e publishes + DML.mock('myDmlId').exceptionOnPublishesFor(OrderEvent__e.SObjectType); + + // Test & Verify + try { + new DML() + .toPublish(new OrderEvent__e(OrderId__c = '12345')) + .toPublish(new ShipmentEvent__e(TrackingNumber__c = 'ABC')) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Publish failed')); + } +} +``` + +### allowPartialSuccess + +When using `allowPartialSuccess()`, failures are captured in the result instead of throwing an exception. + +**Test** + +```apex +@IsTest +static void shouldCaptureFailureInResult() { + // Setup + DML.mock('myDmlId').exceptionOnPublishes(); + + // Test - no exception thrown + DML.Result result = new DML() + .toPublish(new MyEvent__e(Message__c = 'Test')) + .allowPartialSuccess() + .identifier('myDmlId') + .commitWork(); + + // Verify + DML.OperationResult operationResult = result.eventsOf(MyEvent__e.SObjectType); + + Assert.isTrue(operationResult.hasFailures(), 'Should have failures'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Record should be marked as failed'); } ``` diff --git a/website/docs/mocking/undelete.md b/website/docs/mocking/undelete.md index aed5f09..a510b19 100644 --- a/website/docs/mocking/undelete.md +++ b/website/docs/mocking/undelete.md @@ -183,10 +183,115 @@ static void shouldAccessRecordResults() { // Verify DML.Result result = DML.retrieveResultFor('AccountService.restoreAccount'); - DML.OperationResult opResult = result.undeletesOf(Account.SObjectType); + DML.OperationResult operationResult = result.undeletesOf(Account.SObjectType); - Assert.areEqual(Account.SObjectType, opResult.objectType(), 'Should be Account type'); - Assert.areEqual(DML.OperationType.UNDELETE_DML, opResult.operationType(), 'Should be UNDELETE operation'); - Assert.isFalse(opResult.hasFailures(), 'Should have no failures'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Should be Account type'); + Assert.areEqual(DML.OperationType.UNDELETE_DML, operationResult.operationType(), 'Should be UNDELETE operation'); + Assert.isFalse(operationResult.hasFailures(), 'Should have no failures'); +} +``` + +## Exception + +Simulate DML exceptions for undelete operations without touching the database. + +::: tip allowPartialSuccess +When `allowPartialSuccess()` is used, exceptions are **not thrown**. Instead, failures are recorded in the `Result` object. Use `hasFailures()` and `recordResults()` to check for errors. +::: + +### exceptionOnUndeletes + +Throw an exception for all undelete operations. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnUndeletes(); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnUndelete() { + // Setup + DML.mock('myDmlId').exceptionOnUndeletes(); + + Id accountId = DML.randomIdGenerator.get(Account.SObjectType); + + // Test & Verify + try { + new DML() + .toUndelete(accountId) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Undelete failed')); + } +} +``` + +### exceptionOnUndeletesFor + +Throw an exception only for undelete operations on a specific SObject type. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnUndeletesFor(SObjectType objectType); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnlyForAccountUndeletes() { + // Setup - Exception only for Account undeletes + DML.mock('myDmlId').exceptionOnUndeletesFor(Account.SObjectType); + + Id accountId = DML.randomIdGenerator.get(Account.SObjectType); + Id contactId = DML.randomIdGenerator.get(Contact.SObjectType); + + // Test & Verify + try { + new DML() + .toUndelete(accountId) + .toUndelete(contactId) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Undelete failed')); + } +} +``` + +### allowPartialSuccess + +When using `allowPartialSuccess()`, failures are captured in the result instead of throwing an exception. + +**Test** + +```apex +@IsTest +static void shouldCaptureFailureInResult() { + // Setup + DML.mock('myDmlId').exceptionOnUndeletes(); + + Id accountId = DML.randomIdGenerator.get(Account.SObjectType); + + // Test - no exception thrown + DML.Result result = new DML() + .toUndelete(accountId) + .allowPartialSuccess() + .identifier('myDmlId') + .commitWork(); + + // Verify + DML.OperationResult operationResult = result.undeletesOf(Account.SObjectType); + + Assert.isTrue(operationResult.hasFailures(), 'Should have failures'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Record should be marked as failed'); } ``` diff --git a/website/docs/mocking/update.md b/website/docs/mocking/update.md index 2d78796..2828a84 100644 --- a/website/docs/mocking/update.md +++ b/website/docs/mocking/update.md @@ -187,10 +187,113 @@ static void shouldAccessRecordResults() { // Verify DML.Result result = DML.retrieveResultFor('AccountService.archiveAccount'); - DML.OperationResult opResult = result.updatesOf(Account.SObjectType); + DML.OperationResult operationResult = result.updatesOf(Account.SObjectType); - Assert.areEqual(Account.SObjectType, opResult.objectType(), 'Should be Account type'); - Assert.areEqual(DML.OperationType.UPDATE_DML, opResult.operationType(), 'Should be UPDATE operation'); - Assert.isFalse(opResult.hasFailures(), 'Should have no failures'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Should be Account type'); + Assert.areEqual(DML.OperationType.UPDATE_DML, operationResult.operationType(), 'Should be UPDATE operation'); + Assert.isFalse(operationResult.hasFailures(), 'Should have no failures'); +} +``` + +## Exception + +Simulate DML exceptions for update operations without touching the database. + +::: tip allowPartialSuccess +When `allowPartialSuccess()` is used, exceptions are **not thrown**. Instead, failures are recorded in the `Result` object. Use `hasFailures()` and `recordResults()` to check for errors. +::: + +### exceptionOnUpdates + +Throw an exception for all update operations. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnUpdates(); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnUpdate() { + // Setup + DML.mock('myDmlId').exceptionOnUpdates(); + + Id accountId = DML.randomIdGenerator.get(Account.SObjectType); + + // Test & Verify + try { + new DML() + .toUpdate(new Account(Id = accountId, Name = 'Updated Name')) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Update failed')); + } +} +``` + +### exceptionOnUpdatesFor + +Throw an exception only for update operations on a specific SObject type. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnUpdatesFor(SObjectType objectType); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnlyForAccountUpdates() { + // Setup - Exception only for Account updates + DML.mock('myDmlId').exceptionOnUpdatesFor(Account.SObjectType); + + Id accountId = DML.randomIdGenerator.get(Account.SObjectType); + + // Test & Verify + try { + new DML() + .toUpdate(new Account(Id = accountId, Name = 'Updated Name')) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Update failed')); + } +} +``` + +### allowPartialSuccess + +When using `allowPartialSuccess()`, failures are captured in the result instead of throwing an exception. + +**Test** + +```apex +@IsTest +static void shouldCaptureFailureInResult() { + // Setup + DML.mock('myDmlId').exceptionOnUpdates(); + + Id accountId = DML.randomIdGenerator.get(Account.SObjectType); + + // Test - no exception thrown + DML.Result result = new DML() + .toUpdate(new Account(Id = accountId, Name = 'Updated Name')) + .allowPartialSuccess() + .identifier('myDmlId') + .commitWork(); + + // Verify + DML.OperationResult operationResult = result.updatesOf(Account.SObjectType); + + Assert.isTrue(operationResult.hasFailures(), 'Should have failures'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Record should be marked as failed'); } ``` diff --git a/website/docs/mocking/upsert.md b/website/docs/mocking/upsert.md index f51a1fc..39a6975 100644 --- a/website/docs/mocking/upsert.md +++ b/website/docs/mocking/upsert.md @@ -193,10 +193,108 @@ static void shouldAccessRecordResults() { // Verify DML.Result result = DML.retrieveResultFor('AccountService.upsertAccount'); - DML.OperationResult opResult = result.upsertsOf(Account.SObjectType); + DML.OperationResult operationResult = result.upsertsOf(Account.SObjectType); - Assert.areEqual(Account.SObjectType, opResult.objectType(), 'Should be Account type'); - Assert.areEqual(DML.OperationType.UPSERT_DML, opResult.operationType(), 'Should be UPSERT operation'); - Assert.isFalse(opResult.hasFailures(), 'Should have no failures'); + Assert.areEqual(Account.SObjectType, operationResult.objectType(), 'Should be Account type'); + Assert.areEqual(DML.OperationType.UPSERT_DML, operationResult.operationType(), 'Should be UPSERT operation'); + Assert.isFalse(operationResult.hasFailures(), 'Should have no failures'); +} +``` + +## Exception + +Simulate DML exceptions for upsert operations without touching the database. + +::: tip allowPartialSuccess +When `allowPartialSuccess()` is used, exceptions are **not thrown**. Instead, failures are recorded in the `Result` object. Use `hasFailures()` and `recordResults()` to check for errors. +::: + +### exceptionOnUpserts + +Throw an exception for all upsert operations. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnUpserts(); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnUpsert() { + // Setup + DML.mock('myDmlId').exceptionOnUpserts(); + + // Test & Verify + try { + new DML() + .toUpsert(new Account(Name = 'Test Account')) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Upsert failed')); + } +} +``` + +### exceptionOnUpsertsFor + +Throw an exception only for upsert operations on a specific SObject type. + +**Signature** + +```apex +DML.mock(String identifier).exceptionOnUpsertsFor(SObjectType objectType); +``` + +**Test** + +```apex +@IsTest +static void shouldThrowExceptionOnlyForContactUpserts() { + // Setup - Exception only for Contact upserts + DML.mock('myDmlId').exceptionOnUpsertsFor(Contact.SObjectType); + + // Test & Verify + try { + new DML() + .toUpsert(new Account(Name = 'Test Account')) + .toUpsert(new Contact(LastName = 'Doe')) + .identifier('myDmlId') + .commitWork(); + Assert.fail('Expected exception'); + } catch (DmlException e) { + Assert.isTrue(e.getMessage().contains('Upsert failed')); + } +} +``` + +### allowPartialSuccess + +When using `allowPartialSuccess()`, failures are captured in the result instead of throwing an exception. + +**Test** + +```apex +@IsTest +static void shouldCaptureFailureInResult() { + // Setup + DML.mock('myDmlId').exceptionOnUpserts(); + + // Test - no exception thrown + DML.Result result = new DML() + .toUpsert(new Account(Name = 'Test Account')) + .allowPartialSuccess() + .identifier('myDmlId') + .commitWork(); + + // Verify + DML.OperationResult operationResult = result.upsertsOf(Account.SObjectType); + + Assert.isTrue(operationResult.hasFailures(), 'Should have failures'); + Assert.isFalse(operationResult.recordResults()[0].isSuccess(), 'Record should be marked as failed'); } ```