From 936fe313461a1e4ed2c1a2473a9e13eb835b5d60 Mon Sep 17 00:00:00 2001 From: Piotr PG Gajek Date: Thu, 18 Jun 2026 21:29:13 +0200 Subject: [PATCH 1/2] API 66.0 Update --- README.md | 2 +- force-app/main/default/classes/DML.cls-meta.xml | 2 +- force-app/main/default/classes/DML_Test.cls-meta.xml | 2 +- package/main/default/classes/DML.cls-meta.xml | 2 +- package/main/default/classes/DML_Full_Test.cls-meta.xml | 2 +- sfdx-project.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bb70507..dd098fe 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Beyond The Cloud logo -API version +API version License GitHub Repo stars GitHub Release diff --git a/force-app/main/default/classes/DML.cls-meta.xml b/force-app/main/default/classes/DML.cls-meta.xml index 82775b9..cad713d 100644 --- a/force-app/main/default/classes/DML.cls-meta.xml +++ b/force-app/main/default/classes/DML.cls-meta.xml @@ -1,5 +1,5 @@ - 65.0 + 66.0 Active diff --git a/force-app/main/default/classes/DML_Test.cls-meta.xml b/force-app/main/default/classes/DML_Test.cls-meta.xml index 82775b9..cad713d 100644 --- a/force-app/main/default/classes/DML_Test.cls-meta.xml +++ b/force-app/main/default/classes/DML_Test.cls-meta.xml @@ -1,5 +1,5 @@ - 65.0 + 66.0 Active diff --git a/package/main/default/classes/DML.cls-meta.xml b/package/main/default/classes/DML.cls-meta.xml index 82775b9..cad713d 100644 --- a/package/main/default/classes/DML.cls-meta.xml +++ b/package/main/default/classes/DML.cls-meta.xml @@ -1,5 +1,5 @@ - 65.0 + 66.0 Active diff --git a/package/main/default/classes/DML_Full_Test.cls-meta.xml b/package/main/default/classes/DML_Full_Test.cls-meta.xml index 82775b9..cad713d 100644 --- a/package/main/default/classes/DML_Full_Test.cls-meta.xml +++ b/package/main/default/classes/DML_Full_Test.cls-meta.xml @@ -1,5 +1,5 @@ - 65.0 + 66.0 Active diff --git a/sfdx-project.json b/sfdx-project.json index 8e774b1..81103af 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -16,7 +16,7 @@ "name": "dml-lib", "namespace": "btcdev", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "65.0", + "sourceApiVersion": "66.0", "packageAliases": { "DML Lib": "0HoP600000001IfKAI", "DML Lib@0.1.0-1": "04tP6000002A7BxIAK", From 0c098aada1b68fb538ebcb93464391f2dc5b97c0 Mon Sep 17 00:00:00 2001 From: Guillem Medina Date: Fri, 19 Jun 2026 10:10:31 +0200 Subject: [PATCH 2/2] Feature/add with mode global method (#52) * Add new withMode method in both public and global classes * Added unit tests * Update the documentation with references to the new method * Rename withMode to accessMode --- force-app/main/default/classes/DML.cls | 9 +- force-app/main/default/classes/DML_Test.cls | 21 +++ package/main/default/classes/DML.cls | 9 +- .../main/default/classes/DML_Full_Test.cls | 131 ++++++++++++++++++ website/configuration/field-level-security.md | 50 ++++++- website/configuration/sharing-mode.md | 4 +- website/dml/delete.md | 2 +- website/dml/insert.md | 2 +- website/dml/undelete.md | 2 +- website/dml/update.md | 2 +- website/dml/upsert.md | 2 +- 11 files changed, 214 insertions(+), 20 deletions(-) diff --git a/force-app/main/default/classes/DML.cls b/force-app/main/default/classes/DML.cls index 3a5b509..558d7aa 100644 --- a/force-app/main/default/classes/DML.cls +++ b/force-app/main/default/classes/DML.cls @@ -126,6 +126,7 @@ public inherited sharing class DML implements Commitable { // Field Level Security Commitable userMode(); Commitable systemMode(); + Commitable accessMode(System.AccessLevel accessMode); // Sharing Mode Commitable withSharing(); Commitable withoutSharing(); @@ -669,14 +670,16 @@ public inherited sharing class DML implements Commitable { // Field Level Security public Commitable userMode() { - return this.setAccessMode(System.AccessLevel.USER_MODE); + this.configuration.accessMode(System.AccessLevel.USER_MODE); + return this; } public Commitable systemMode() { - return this.setAccessMode(System.AccessLevel.SYSTEM_MODE); + this.configuration.accessMode(System.AccessLevel.SYSTEM_MODE); + return this; } - private Commitable setAccessMode(System.AccessLevel accessMode) { + public Commitable accessMode(System.AccessLevel accessMode) { this.configuration.accessMode(accessMode); return this; } diff --git a/force-app/main/default/classes/DML_Test.cls b/force-app/main/default/classes/DML_Test.cls index 52c8e87..15405a1 100644 --- a/force-app/main/default/classes/DML_Test.cls +++ b/force-app/main/default/classes/DML_Test.cls @@ -3532,7 +3532,28 @@ private class DML_Test { Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); } + @IsTest + static void toInsertWithUserModeExplicitlySetAccessMode() { + // Setup + Case newCase = getCase(1); + + Exception expectedException = null; + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + new DML().toInsert(newCase).accessMode(System.AccessLevel.USER_MODE).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 toInsertWithInvalidRelationshipSingleRecord() { diff --git a/package/main/default/classes/DML.cls b/package/main/default/classes/DML.cls index 12acc6b..6d9594d 100644 --- a/package/main/default/classes/DML.cls +++ b/package/main/default/classes/DML.cls @@ -126,6 +126,7 @@ global inherited sharing class DML implements Commitable { // Field Level Security Commitable userMode(); Commitable systemMode(); + Commitable accessMode(System.AccessLevel accessMode); // Sharing Mode Commitable withSharing(); Commitable withoutSharing(); @@ -669,14 +670,16 @@ global inherited sharing class DML implements Commitable { // Field Level Security global Commitable userMode() { - return this.setAccessMode(System.AccessLevel.USER_MODE); + this.configuration.accessMode(System.AccessLevel.USER_MODE); + return this; } global Commitable systemMode() { - return this.setAccessMode(System.AccessLevel.SYSTEM_MODE); + this.configuration.accessMode(System.AccessLevel.SYSTEM_MODE); + return this; } - private Commitable setAccessMode(System.AccessLevel accessMode) { + global Commitable accessMode(System.AccessLevel accessMode) { this.configuration.accessMode(accessMode); return this; } diff --git a/package/main/default/classes/DML_Full_Test.cls b/package/main/default/classes/DML_Full_Test.cls index e2836a0..1f09221 100644 --- a/package/main/default/classes/DML_Full_Test.cls +++ b/package/main/default/classes/DML_Full_Test.cls @@ -6853,6 +6853,29 @@ private class DML_Full_Test { Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); } + @IsTest + static void toInsertWithUserModeExplicitlySetAccessMode() { + // Setup + Case newCase = getCase(1); + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + new DML().toInsert(newCase).accessMode(System.AccessLevel.USER_MODE).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 toUpdateWithUserMode() { // Setup @@ -6903,6 +6926,31 @@ private class DML_Full_Test { Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); } + @IsTest + static void toUpdateWithUserModeExplicitlySetAccessMode() { + // Setup + Case newCase = getCase(1); + insert newCase; + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + newCase.Subject = 'Updated Test Case'; + new DML().toUpdate(newCase).accessMode(System.AccessLevel.USER_MODE).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 toUpsertWithUserMode() { // Setup @@ -6965,6 +7013,37 @@ private class DML_Full_Test { ); } + @IsTest + static void toUpsertWithUserModeExplicitlySetAccessMode() { + // 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).accessMode(System.AccessLevel.USER_MODE).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 toDeleteWithUserMode() { // Setup @@ -7013,6 +7092,30 @@ private class DML_Full_Test { Assert.areEqual('Access to entity \'Case\' denied', expectedException.getMessage(), 'Expected exception message should be thrown.'); } + @IsTest + static void toDeleteWithUserModeExplicitlySetAccessMode() { + // Setup + Case newCase = getCase(1); + insert newCase; + + Exception expectedException = null; + + // Test + Test.startTest(); + System.runAs(minimumAccessUser()) { + try { + new DML().toDelete(newCase).accessMode(System.AccessLevel.USER_MODE).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 toHardDeleteWithUserMode() { // Setup @@ -7111,6 +7214,34 @@ private class DML_Full_Test { Assert.isTrue(expectedException.getMessage().contains('invalid record id'), 'Expected exception message should be thrown.'); } + @IsTest + static void toUndeleteWithUserModeExplicitlySetAccessMode() { + // 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).accessMode(System.AccessLevel.USER_MODE).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 toMergeWithUserMode() { // Setup diff --git a/website/configuration/field-level-security.md b/website/configuration/field-level-security.md index 32095c6..92db837 100644 --- a/website/configuration/field-level-security.md +++ b/website/configuration/field-level-security.md @@ -17,6 +17,39 @@ System.runAs(minimalAccessUser) { } ``` +## accessMode + +Set access mode explicitly using the Salesforce `System.AccessLevel` enum. + +**Signature** + +```apex +Commitable accessMode(System.AccessLevel accessMode); +``` + +Use `accessMode(System.AccessLevel.USER_MODE)` as an alternative to `userMode()`, and `accessMode(System.AccessLevel.SYSTEM_MODE)` as an alternative to `systemMode()`. + +**Standard DML** + +```apex +insert as user new Account(Name = 'My Account'); +insert as system new Case(Subject = 'System Mode Case'); +``` + +**DML Lib** + +```apex +new DML() + .toInsert(new Account(Name = 'My Account')) + .accessMode(System.AccessLevel.USER_MODE) + .commitWork(); + +new DML() + .toInsert(new Case(Subject = 'System Mode Case')) + .accessMode(System.AccessLevel.SYSTEM_MODE) + .commitWork(); +``` + ## userMode Execute DML operations respecting user permissions. This is the **default behavior**. @@ -75,13 +108,16 @@ Use `systemMode()` with caution. It bypasses field-level security, which could e Field-level security and sharing mode are independent settings that work together. -| Configuration | FLS | Sharing Rules | -|---------------|-----|---------------| -| `userMode()` | Enforced | Enforced | -| `userMode().withSharing()` | Enforced | Enforced | -| `userMode().withoutSharing()` | Enforced | Enforced | -| `systemMode().withSharing()` | Bypassed | Enforced | -| `systemMode().withoutSharing()` | Bypassed | Bypassed | +| Configuration | FLS | Sharing Rules | +| ------------------------------------------------------------- | -------- | ------------- | +| `userMode()` | Enforced | Enforced | +| `accessMode(System.AccessLevel.USER_MODE)` | Enforced | Enforced | +| `userMode().withSharing()` | Enforced | Enforced | +| `userMode().withoutSharing()` | Enforced | Enforced | +| `systemMode().withSharing()` | Bypassed | Enforced | +| `accessMode(System.AccessLevel.SYSTEM_MODE).withSharing()` | Bypassed | Enforced | +| `systemMode().withoutSharing()` | Bypassed | Bypassed | +| `accessMode(System.AccessLevel.SYSTEM_MODE).withoutSharing()` | Bypassed | Bypassed | **Example** diff --git a/website/configuration/sharing-mode.md b/website/configuration/sharing-mode.md index fa582aa..174f48f 100644 --- a/website/configuration/sharing-mode.md +++ b/website/configuration/sharing-mode.md @@ -42,7 +42,7 @@ We suggest using `.systemMode().withoutSharing()` in triggers, as trigger logic By default, the DML library uses `with sharing`, meaning it respects the sharing context of the calling class. Sharing mode is enforced by `userMode()`, which is the default mode. -Only when `.systemMode()` is used, `.withSharing()` can control the sharing mode. +Only when `.systemMode()` or `.accessMode(System.AccessLevel.SYSTEM_MODE)` is used, `.withSharing()` can control the sharing mode. Execute DML operations enforcing sharing rules. Records the user doesn't have access to will cause errors. @@ -76,7 +76,7 @@ new DML() ## withoutSharing Execute DML operations bypassing sharing rules. All records are accessible regardless of sharing settings. -To use `.withoutSharing()`, the `.systemMode()` must be enabled. +To use `.withoutSharing()`, `.systemMode()` or `.accessMode(System.AccessLevel.SYSTEM_MODE)` must be enabled. **Signature** diff --git a/website/dml/delete.md b/website/dml/delete.md index 2e01b8d..123762e 100644 --- a/website/dml/delete.md +++ b/website/dml/delete.md @@ -154,7 +154,7 @@ OperationResult deleteImmediately(List records); ``` ::: tip -All DML settings configured on the `DML` instance (such as `userMode()`, `systemMode()`, `withSharing()`, `withoutSharing()`, `allowPartialSuccess()`) are inherited when executing `deleteImmediately`. +All DML settings configured on the `DML` instance (such as `userMode()`, `systemMode()`, `accessMode(System.AccessLevel)`, `withSharing()`, `withoutSharing()`, `allowPartialSuccess()`) are inherited when executing `deleteImmediately`. ::: ### Single Record diff --git a/website/dml/insert.md b/website/dml/insert.md index 6f9a0ea..9f30193 100644 --- a/website/dml/insert.md +++ b/website/dml/insert.md @@ -245,7 +245,7 @@ OperationResult insertImmediately(DML.Records records); ``` ::: tip -All DML settings configured on the `DML` instance (such as `userMode()`, `systemMode()`, `withSharing()`, `withoutSharing()`, `allowPartialSuccess()`) are inherited when executing `insertImmediately`. +All DML settings configured on the `DML` instance (such as `userMode()`, `systemMode()`, `accessMode(System.AccessLevel)`, `withSharing()`, `withoutSharing()`, `allowPartialSuccess()`) are inherited when executing `insertImmediately`. ::: ### Single Record diff --git a/website/dml/undelete.md b/website/dml/undelete.md index 9d33bc8..1cc840a 100644 --- a/website/dml/undelete.md +++ b/website/dml/undelete.md @@ -154,7 +154,7 @@ OperationResult undeleteImmediately(List records); ``` ::: tip -All DML settings configured on the `DML` instance (such as `userMode()`, `systemMode()`, `withSharing()`, `withoutSharing()`, `allowPartialSuccess()`) are inherited when executing `undeleteImmediately`. +All DML settings configured on the `DML` instance (such as `userMode()`, `systemMode()`, `accessMode(System.AccessLevel)`, `withSharing()`, `withoutSharing()`, `allowPartialSuccess()`) are inherited when executing `undeleteImmediately`. ::: ### Single Record diff --git a/website/dml/update.md b/website/dml/update.md index 93f97d6..15d392d 100644 --- a/website/dml/update.md +++ b/website/dml/update.md @@ -232,7 +232,7 @@ OperationResult updateImmediately(DML.Records records); ``` ::: tip -All DML settings configured on the `DML` instance (such as `userMode()`, `systemMode()`, `withSharing()`, `withoutSharing()`, `allowPartialSuccess()`) are inherited when executing `updateImmediately`. +All DML settings configured on the `DML` instance (such as `userMode()`, `systemMode()`, `accessMode(System.AccessLevel)`, `withSharing()`, `withoutSharing()`, `allowPartialSuccess()`) are inherited when executing `updateImmediately`. ::: ### Single Record diff --git a/website/dml/upsert.md b/website/dml/upsert.md index 22e9a94..a1a1ef7 100644 --- a/website/dml/upsert.md +++ b/website/dml/upsert.md @@ -316,7 +316,7 @@ OperationResult upsertImmediately(DML.Records records); ``` ::: tip -All DML settings configured on the `DML` instance (such as `userMode()`, `systemMode()`, `withSharing()`, `withoutSharing()`, `allowPartialSuccess()`) are inherited when executing `upsertImmediately`. +All DML settings configured on the `DML` instance (such as `userMode()`, `systemMode()`, `accessMode(System.AccessLevel)`, `withSharing()`, `withoutSharing()`, `allowPartialSuccess()`) are inherited when executing `upsertImmediately`. ::: ### Single Record