├── .forceignore
├── .npmrc
├── .prettierignore
├── .gitignore
├── force-app
├── repository
│ ├── SelectFunction.cls
│ ├── SearchGroup.cls
│ ├── Query.cls-meta.xml
│ ├── Cursor.cls-meta.xml
│ ├── CursorTest.cls-meta.xml
│ ├── QueryTest.cls-meta.xml
│ ├── Repository.cls-meta.xml
│ ├── Aggregation.cls-meta.xml
│ ├── AggregationTest.cls-meta.xml
│ ├── DateFunction.cls-meta.xml
│ ├── IRepository.cls-meta.xml
│ ├── QueryField.cls-meta.xml
│ ├── RepositoryTest.cls-meta.xml
│ ├── SearchGroup.cls-meta.xml
│ ├── SelectFunction.cls-meta.xml
│ ├── AdditionalSoslObject.cls-meta.xml
│ ├── AggregateRecord.cls-meta.xml
│ ├── AggregateRepository.cls-meta.xml
│ ├── FieldLevelHistory.cls-meta.xml
│ ├── IAggregateRepository.cls-meta.xml
│ ├── IHistoryRepository.cls-meta.xml
│ ├── QueryFieldTest.cls-meta.xml
│ ├── RepositorySortOrder.cls-meta.xml
│ ├── AggregateRepositoryTest.cls-meta.xml
│ ├── FieldLevelHistoryRepo.cls-meta.xml
│ ├── FieldLevelHistoryRepoTest.cls-meta.xml
│ ├── SObjectRepository.cls-meta.xml
│ ├── SObjectRepositoryTest.cls-meta.xml
│ ├── SObjectRepository.cls
│ ├── DateFunction.cls
│ ├── IHistoryRepository.cls
│ ├── SObjectRepositoryTest.cls
│ ├── AdditionalSoslObject.cls
│ ├── QueryFieldTest.cls
│ ├── AggregationTest.cls
│ ├── AggregateRecord.cls
│ ├── QueryField.cls
│ ├── IAggregateRepository.cls
│ ├── FieldLevelHistory.cls
│ ├── RepositorySortOrder.cls
│ ├── FieldLevelHistoryRepo.cls
│ ├── Aggregation.cls
│ ├── IRepository.cls
│ ├── FieldLevelHistoryRepoTest.cls
│ ├── Cursor.cls
│ ├── CursorTest.cls
│ ├── AggregateRepositoryTest.cls
│ ├── AggregateRepository.cls
│ ├── QueryTest.cls
│ ├── Query.cls
│ ├── Repository.cls
│ └── RepositoryTest.cls
├── dml
│ ├── DML.cls-meta.xml
│ ├── DMLMock.cls-meta.xml
│ ├── DMLTest.cls-meta.xml
│ ├── IDML.cls-meta.xml
│ ├── IDML.cls
│ ├── DML.cls
│ ├── DMLTest.cls
│ └── DMLMock.cls
├── factory
│ ├── Factory.cls-meta.xml
│ ├── RepoFactory.cls-meta.xml
│ ├── LazyFactory.cls-meta.xml
│ ├── RepoFactoryMock.cls-meta.xml
│ ├── LazyFactoryTest.cls-meta.xml
│ ├── RepoFactoryMockTest.cls-meta.xml
│ ├── LazyFactory.cls
│ ├── LazyFactoryTest.cls
│ ├── Factory.cls
│ ├── RepoFactoryMockTest.cls
│ ├── RepoFactory.cls
│ └── RepoFactoryMock.cls
└── utils
│ ├── TestingUtils.cls-meta.xml
│ └── TestingUtils.cls
├── config
└── project-scratch-def.json
├── example-app
├── ExampleFactory.cls-meta.xml
├── ExampleRepoFactory.cls-meta.xml
├── handlers
│ ├── AccountHandler.cls-meta.xml
│ ├── AccountHandlerTests.cls-meta.xml
│ ├── ExampleSObjectTest.cls-meta.xml
│ ├── AccountHandler.cls
│ ├── AccountHandlerTests.cls
│ └── ExampleSObjectTest.cls
├── triggers
│ ├── TriggerHandler.cls-meta.xml
│ ├── TriggerHandler_Tests.cls-meta.xml
│ ├── TriggerHandler.cls
│ └── TriggerHandler_Tests.cls
├── objects
│ └── ExampleSObject__c
│ │ ├── fields
│ │ ├── Account__c.field-meta.xml
│ │ └── SecondAccountLookup__c.field-meta.xml
│ │ └── ExampleSObject__c.object-meta.xml
├── ExampleFactory.cls
├── testSuites
│ └── ApexDmlMockingSuite.testSuite-meta.xml
└── ExampleRepoFactory.cls
├── sfdx-project.json
├── .prettierrc
├── package.json
├── LICENSE
├── scripts
├── validate-history-query.apex
└── test.ps1
├── .github
└── workflows
│ └── deploy.yml
├── CODE_OF_CONDUCT.md
└── Readme.md
/.forceignore:
--------------------------------------------------------------------------------
1 | package.xml
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .sfdx
2 | sfdx-project.json
3 | package-lock.json
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .sfdx/
2 | .sf/
3 | .vscode/
4 | yarn.lock
5 | node_modules/
--------------------------------------------------------------------------------
/force-app/repository/SelectFunction.cls:
--------------------------------------------------------------------------------
1 | public enum SelectFunction {
2 | FORMAT,
3 | TOLABEL
4 | }
5 |
--------------------------------------------------------------------------------
/force-app/repository/SearchGroup.cls:
--------------------------------------------------------------------------------
1 | public enum SearchGroup {
2 | ALL_FIELDS,
3 | EMAIL_FIELDS,
4 | NAME_FIELDS,
5 | PHONE_FIELDS,
6 | SIDEBAR_FIELDS
7 | }
8 |
--------------------------------------------------------------------------------
/config/project-scratch-def.json:
--------------------------------------------------------------------------------
1 | {
2 | "orgName": "Apex DML Mocking",
3 | "edition": "Developer",
4 | "country": "US",
5 | "language": "en_US",
6 | "hasSampleData": true
7 | }
8 |
--------------------------------------------------------------------------------
/force-app/dml/DML.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/dml/DMLMock.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/dml/DMLTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/dml/IDML.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/example-app/ExampleFactory.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 62.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/factory/Factory.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/Query.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/example-app/ExampleRepoFactory.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 62.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/factory/RepoFactory.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/Cursor.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/CursorTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/QueryTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/Repository.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/utils/TestingUtils.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/example-app/handlers/AccountHandler.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 62.0
4 | Active
5 |
--------------------------------------------------------------------------------
/example-app/triggers/TriggerHandler.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 62.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/factory/LazyFactory.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/force-app/factory/RepoFactoryMock.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/Aggregation.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/AggregationTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/DateFunction.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/IRepository.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/QueryField.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/force-app/repository/RepositoryTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/SearchGroup.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/SelectFunction.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/example-app/handlers/AccountHandlerTests.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 62.0
4 | Active
5 |
--------------------------------------------------------------------------------
/example-app/handlers/ExampleSObjectTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 62.0
4 | Active
5 |
--------------------------------------------------------------------------------
/example-app/triggers/TriggerHandler_Tests.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 62.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/factory/LazyFactoryTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/force-app/factory/RepoFactoryMockTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/AdditionalSoslObject.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/AggregateRecord.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/force-app/repository/AggregateRepository.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/FieldLevelHistory.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/IAggregateRepository.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/IHistoryRepository.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/QueryFieldTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/force-app/repository/RepositorySortOrder.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/AggregateRepositoryTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/FieldLevelHistoryRepo.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/FieldLevelHistoryRepoTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
--------------------------------------------------------------------------------
/force-app/repository/SObjectRepository.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/force-app/repository/SObjectRepositoryTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 65.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/sfdx-project.json:
--------------------------------------------------------------------------------
1 | {
2 | "packageDirectories": [
3 | {
4 | "path": "force-app",
5 | "default": true
6 | },
7 | {
8 | "path": "example-app"
9 | }
10 | ],
11 | "namespace": "",
12 | "sfdcLoginUrl": "https://login.salesforce.com",
13 | "sourceApiVersion": "64.0"
14 | }
--------------------------------------------------------------------------------
/force-app/repository/SObjectRepository.cls:
--------------------------------------------------------------------------------
1 | public without sharing class SObjectRepository extends FieldLevelHistoryRepo {
2 | public SObjectRepository(
3 | Schema.SObjectType repoType,
4 | List queryFields,
5 | RepoFactory repoFactory
6 | ) {
7 | super(repoType, queryFields, repoFactory);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/force-app/repository/DateFunction.cls:
--------------------------------------------------------------------------------
1 | // https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_date_functions.htm
2 | public enum DateFunction {
3 | CALENDAR_MONTH,
4 | CALENDAR_QUARTER,
5 | CALENDAR_YEAR,
6 | DAY_IN_MONTH,
7 | DAY_IN_WEEK,
8 | DAY_IN_YEAR,
9 | DAY_ONLY,
10 | FISCAL_MONTH,
11 | FISCAL_QUARTER,
12 | FISCAL_YEAR,
13 | HOUR_IN_DAY,
14 | WEEK_IN_MONTH,
15 | WEEK_IN_YEAR
16 | }
17 |
--------------------------------------------------------------------------------
/force-app/repository/IHistoryRepository.cls:
--------------------------------------------------------------------------------
1 | public interface IHistoryRepository extends IAggregateRepository {
2 | // essentially duplicating the method signatures from IRepository
3 | // but with FieldLevelHistory return types
4 | List getHistory(Query query);
5 | List getHistory(List queries);
6 | List getAllHistory();
7 |
8 | // only needs to be called for standard objects since all
9 | // custom objects use "ParentId"
10 | IHistoryRepository setParentField(Schema.SObjectField parentField);
11 | }
12 |
--------------------------------------------------------------------------------
/force-app/repository/SObjectRepositoryTest.cls:
--------------------------------------------------------------------------------
1 | @IsTest
2 | private class SObjectRepositoryTest {
3 | @IsTest
4 | static void queriesWithGenericRepo() {
5 | Account acc = new Account(Name = SObjectRepositoryTest.class.getName() + System.now().getTime());
6 | insert acc;
7 |
8 | List results = new SObjectRepository(
9 | Schema.Account.SObjectType,
10 | new List(),
11 | new RepoFactory()
12 | )
13 | .getAll();
14 |
15 | Assert.areEqual(1, results.size());
16 | Assert.areEqual(acc.Id, results[0].Id);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/example-app/objects/ExampleSObject__c/fields/Account__c.field-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Account__c
4 | SetNull
5 | false
6 |
7 | Account
8 | Example SObjects
9 | Example_SObjects
10 | false
11 | false
12 | Lookup
13 |
14 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "none",
3 | "singleQuote": true,
4 | "tabWidth": 2,
5 | "printWidth": 120,
6 | "arrowParens": "avoid",
7 | "overrides": [
8 | {
9 | "files": "**/lwc/**/*.html",
10 | "options": {
11 | "parser": "lwc"
12 | }
13 | },
14 | {
15 | "files": "*.{cmp,page,component}",
16 | "options": {
17 | "parser": "html"
18 | }
19 | },
20 | {
21 | "files": "*.{cls, apex}",
22 | "options": {
23 | "parser": "apex"
24 | }
25 | }
26 | ],
27 | "plugins": [
28 | "prettier-plugin-apex"
29 | ],
30 | "$schema": "https://json.schemastore.org/prettierrc"
31 | }
--------------------------------------------------------------------------------
/example-app/objects/ExampleSObject__c/fields/SecondAccountLookup__c.field-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | SecondAccountLookup__c
4 | SetNull
5 | false
6 |
7 | Account
8 | Example SObjects (Second Account Lookup)
9 | Example_SObjects1
10 | false
11 | false
12 | Lookup
13 |
14 |
--------------------------------------------------------------------------------
/force-app/repository/AdditionalSoslObject.cls:
--------------------------------------------------------------------------------
1 | public class AdditionalSoslObject {
2 | public final Schema.SObjectType objectType;
3 | public final Integer queryLimit;
4 | public final List queryFilters;
5 | public final List selectFields;
6 |
7 | @SuppressWarnings('PMD.ExcessiveParameterList')
8 | public AdditionalSoslObject(
9 | Schema.SObjectType objectType,
10 | List selectFields,
11 | List queryFilters,
12 | Integer queryLimit
13 | ) {
14 | this.objectType = objectType;
15 | this.queryFilters = queryFilters;
16 | this.queryLimit = queryLimit;
17 | this.selectFields = selectFields;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/force-app/repository/QueryFieldTest.cls:
--------------------------------------------------------------------------------
1 | @IsTest
2 | private class QueryFieldTest {
3 | @IsTest
4 | static void parentFieldChainsConcatenatedProperly() {
5 | QueryField queryField = new QueryField(
6 | new List{ Contact.AccountId, Account.OwnerId },
7 | new List{ User.Email }
8 | );
9 |
10 | Assert.areEqual('Account.Owner.Email', queryField.toString());
11 | }
12 |
13 | @IsTest
14 | static void concatenatesFieldsProperly() {
15 | QueryField queryField = new QueryField(new List{ Contact.AccountId, Contact.LastName });
16 |
17 | Assert.areEqual('AccountId,LastName', queryField.toString());
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/force-app/repository/AggregationTest.cls:
--------------------------------------------------------------------------------
1 | @IsTest
2 | private class AggregationTest {
3 | @IsTest
4 | static void shouldCorrectlyFormatSum() {
5 | String expectedAlias = 'myAlias';
6 | Aggregation agg = Aggregation.sum(Opportunity.Amount, expectedAlias);
7 | Assert.areEqual('SUM(Amount) myAlias', agg.toString());
8 | Assert.areEqual(expectedAlias, agg.getAlias());
9 | }
10 |
11 | @IsTest
12 | static void shouldCorrectlyFormatCountDistinct() {
13 | String expectedAlias = 'myAlias';
14 | Aggregation agg = Aggregation.countDistinct(Opportunity.StageName, expectedAlias);
15 | Assert.areEqual('COUNT_DISTINCT(StageName) myAlias', agg.toString());
16 | Assert.areEqual(expectedAlias, agg.getAlias());
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/example-app/ExampleFactory.cls:
--------------------------------------------------------------------------------
1 | public virtual class ExampleFactory extends Factory {
2 | public final ExampleRepoFactory repoFactory = new ExampleRepoFactory();
3 |
4 | @TestVisible
5 | private static ExampleFactory factory;
6 |
7 | protected ExampleFactory() {
8 | // enforce getFactory() as the sole way to interact with this class
9 | }
10 |
11 | public static ExampleFactory getFactory() {
12 | if (factory == null) {
13 | factory = new ExampleFactory();
14 | }
15 | return factory;
16 | }
17 |
18 | public virtual TriggerHandler getAccountHandler() {
19 | return new AccountHandler(this);
20 | }
21 |
22 | @TestVisible
23 | private ExampleFactory withMocks {
24 | get {
25 | this.repoFactory.setFacade(new RepoFactoryMock.FacadeMock());
26 | return this;
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/example-app/testSuites/ApexDmlMockingSuite.testSuite-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | AccountHandlerTests
4 | AggregateRepositoryTest
5 | AggregationTest
6 | CursorTest
7 | DMLTest
8 | ExampleSObjectTest
9 | FieldLevelHistoryRepoTest
10 | QueryTest
11 | QueryFieldTest
12 | RepositoryTest
13 | RepoFactoryMockTest
14 | TriggerHandler_Tests
15 |
16 |
--------------------------------------------------------------------------------
/example-app/ExampleRepoFactory.cls:
--------------------------------------------------------------------------------
1 | public without sharing class ExampleRepoFactory extends RepoFactory {
2 | // your app can expand on the repositories provided by the base instance here, like so
3 |
4 | public IAggregateRepository getOppRepo() {
5 | List queryFields = new List{
6 | Opportunity.IsWon,
7 | Opportunity.StageName
8 | // etc ...
9 | };
10 | IAggregateRepository oppRepo = this.facade.getRepo(Opportunity.SObjectType, queryFields, this);
11 | oppRepo.addParentFields(
12 | new List{ Opportunity.AccountId },
13 | new List{ Account.Id }
14 | );
15 | return oppRepo;
16 | }
17 |
18 | public IHistoryRepository getOppFieldHistoryRepo() {
19 | return this.facade.getRepo(OpportunityFieldHistory.SObjectType, new List(), this)
20 | .setParentField(OpportunityFieldHistory.OpportunityId);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/force-app/factory/LazyFactory.cls:
--------------------------------------------------------------------------------
1 | public without sharing virtual class LazyFactory {
2 | private static final Map CACHED_INSTANCES = new Map();
3 |
4 | public interface Instance {
5 | }
6 |
7 | public Instance load(String typeName) {
8 | return this.load(this.getTypeFromName(typeName, ''));
9 | }
10 |
11 | public Instance load(String typeName, String namespace) {
12 | return this.load(this.getTypeFromName(typeName, namespace));
13 | }
14 |
15 | public Instance load(Type type) {
16 | Instance possibleInstance = CACHED_INSTANCES.get(type.getName());
17 | if (possibleInstance == null) {
18 | possibleInstance = (Instance) type.newInstance();
19 | CACHED_INSTANCES.put(type.getName(), possibleInstance);
20 | }
21 | return possibleInstance;
22 | }
23 |
24 | private Type getTypeFromName(String typeName, String possibleNamespace) {
25 | return Type.forName(possibleNamespace, typeName);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/force-app/factory/LazyFactoryTest.cls:
--------------------------------------------------------------------------------
1 | @IsTest
2 | private class LazyFactoryTest {
3 | private static Integer loadCounter = 0;
4 |
5 | @IsTest
6 | static void properlyCachesLazyLoadedInstances() {
7 | ExtendedLazyFactory extendedExampleFactory = new ExtendedLazyFactory();
8 |
9 | Example firstExample = (Example) extendedExampleFactory.example;
10 | Example secondExample = (Example) extendedExampleFactory.example;
11 |
12 | Assert.areEqual(1, loadCounter);
13 | // prove that even without overriding "equals" and a setter for the
14 | // ExtendedLazyFactory that the instances returned are the same
15 | Assert.areEqual(firstExample, secondExample);
16 | }
17 |
18 | private class ExtendedLazyFactory extends LazyFactory {
19 | public Instance example {
20 | get {
21 | return this.load(Example.class);
22 | }
23 | }
24 | }
25 |
26 | private class Example implements LazyFactory.Instance {
27 | public Example() {
28 | loadCounter++;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/example-app/handlers/AccountHandler.cls:
--------------------------------------------------------------------------------
1 | public class AccountHandler extends TriggerHandler {
2 | private final IRepository oppRepo;
3 |
4 | public AccountHandler(ExampleFactory factory) {
5 | this.oppRepo = factory.repoFactory.getOppRepo();
6 | }
7 |
8 | public override void afterInsert(List insertedRecords, Map unused) {
9 | List insertedAccounts = (List) insertedRecords;
10 | this.createOppAutomatically(insertedAccounts);
11 | }
12 |
13 | private void createOppAutomatically(List insertedAccounts) {
14 | List oppsToInsert = new List();
15 | for (Account insertedAccount : insertedAccounts) {
16 | oppsToInsert.add(
17 | new Opportunity(
18 | Name = 'Prospecting Opp for: ' + insertedAccount.Name,
19 | AccountId = insertedAccount.Id,
20 | StageName = 'Open',
21 | CloseDate = Date.today()
22 | )
23 | );
24 | }
25 | this.oppRepo.doInsert(oppsToInsert);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "apex-dml-mocking",
3 | "version": "1.0.2",
4 | "description": "Easy DML / SOQL mocking through the Factory pattern in Apex",
5 | "dependencies": {},
6 | "devDependencies": {
7 | "@salesforce/cli": "2.105.6",
8 | "prettier-plugin-apex": "2.2.6",
9 | "prettier": "3.6.2"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/jamessimone/apex-dml-mocking.git"
14 | },
15 | "scripts": {
16 | "prettier": "prettier --write \"**/*.{cls,cmp,component,css,html,js,json,md,page,trigger,yaml,yml}\"",
17 | "scan": "sf code-analyzer run -r pmd:1 -r pmd:2 -r pmd:3 --workspace force-app"
18 | },
19 | "keywords": [
20 | "apex",
21 | "salesforce",
22 | "factory",
23 | "dml",
24 | "mocks",
25 | "unit",
26 | "test",
27 | "soql"
28 | ],
29 | "author": "James Simone",
30 | "license": "MIT",
31 | "bugs": {
32 | "url": "https://github.com/jamessimone/apex-dml-mocking/issues"
33 | },
34 | "homepage": "https://github.com/jamessimone/apex-dml-mocking#readme"
35 | }
36 |
--------------------------------------------------------------------------------
/force-app/factory/Factory.cls:
--------------------------------------------------------------------------------
1 | public virtual class Factory {
2 | public final RepoFactory repoFactory = new RepoFactory();
3 |
4 | @TestVisible
5 | private static Factory factory;
6 |
7 | @SuppressWarnings('PMD.EmptyStatementBlock')
8 | protected Factory() {
9 | // enforce getFactory() as the sole entry point for usage
10 | }
11 |
12 | public static Factory getFactory() {
13 | // production code can only initialize the factory through this method
14 | // but tests can provide an alternative factory implementation
15 | if (factory == null) {
16 | factory = new Factory();
17 | }
18 |
19 | return factory;
20 | }
21 |
22 | // create methods to initialize your objects here
23 | // (an example is included in the example-app folder)
24 |
25 | @TestVisible
26 | private Factory withMocks {
27 | // you can call "withMocks" after "getFactory" in tests to swap out
28 | // how repositories are created and DML is performed
29 | get {
30 | this.repoFactory.setFacade(new RepoFactoryMock.FacadeMock());
31 | return this;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 James Simone
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/force-app/dml/IDML.cls:
--------------------------------------------------------------------------------
1 | public interface IDML {
2 | Database.SaveResult doInsert(SObject record);
3 | List doInsert(List recordList);
4 | Database.SaveResult doUpdate(SObject record);
5 | List doUpdate(List recordList);
6 | Database.UpsertResult doUpsert(SObject record);
7 | List doUpsert(List recordList);
8 | List doUpsert(List recordList, Schema.SObjectField externalIDField);
9 | Database.UndeleteResult doUndelete(SObject record);
10 | List doUndelete(List recordList);
11 |
12 | Database.DeleteResult doDelete(SObject record);
13 | List doDelete(List recordList);
14 | Database.DeleteResult doHardDelete(SObject record);
15 | List doHardDelete(List recordList);
16 |
17 | Database.SaveResult publish(SObject platformEvent);
18 | List publish(List platformEvent);
19 |
20 | IDML setOptions(Database.DMLOptions options);
21 | IDML setOptions(Database.DMLOptions options, System.AccessLevel accessLevel);
22 | }
23 |
--------------------------------------------------------------------------------
/example-app/handlers/AccountHandlerTests.cls:
--------------------------------------------------------------------------------
1 | @IsTest
2 | private class AccountHandlerTests {
3 | @IsTest
4 | static void itShouldInsertNewOppsWhenAccountsAreInserted() {
5 | Account acc = new Account(Name = 'Test Corp.', Id = TestingUtils.generateId(Account.SObjectType));
6 |
7 | // this is a unit test; an integration test would have a trigger on Account,
8 | // and you would insert/update the Account in order to test end-to-end that your trigger
9 | // correctly called your handler, which correctly did the things you cared about.
10 | // a unit test for a handler does not rely on DML; it does not actually update entities in the database;
11 | // it purely tests inputs and outputs for the expected results
12 | ExampleFactory.getFactory()
13 | .withMocks.getAccountHandler()
14 | .afterInsert(new List{ acc }, new Map(new List{ acc }));
15 |
16 | Opportunity insertedOpp = (Opportunity) DMLMock.Inserted.Opportunities.singleOrDefault;
17 | Assert.areNotEqual(null, insertedOpp, 'Opp should have been inserted!');
18 | Assert.areEqual('Prospecting Opp for: Test Corp.', insertedOpp.Name);
19 | Assert.areEqual(acc.Id, insertedOpp.AccountId);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/scripts/validate-history-query.apex:
--------------------------------------------------------------------------------
1 | if ([SELECT COUNT() FROM OpportunityFieldHistory] == 0) {
2 | List opps = [SELECT Id, Name FROM Opportunity LIMIT 1];
3 | if (opps.isEmpty()) {
4 | Opportunity newOpp = new Opportunity(StageName = 'History', Name = 'History Field Tracking Integration Test', CloseDate = System.today(), Amount = 5);
5 | insert newOpp;
6 | opps.add(newOpp);
7 | }
8 |
9 | Opportunity opp = opps[0];
10 | opp.Name = 'Something New';
11 | update opp;
12 | System.debug('Updated opportunity to create history record');
13 | } else {
14 | System.debug('Histories already exist, continuing ...');
15 | }
16 |
17 | Exception ex;
18 | List histories;
19 | try {
20 | histories = new FieldLevelHistoryRepo(
21 | OpportunityFieldHistory.SObjectType,
22 | new List(),
23 | new RepoFactory()
24 | ).setParentField(OpportunityFieldHistory.OpportunityId).getAllHistory();
25 | System.debug(histories);
26 | } catch (Exception e) {
27 | ex = e;
28 | }
29 |
30 | if (histories.isEmpty()) {
31 | ex = new IllegalArgumentException('History record(s) were not retrieved correctly!');
32 | }
33 | if (ex == null) {
34 | System.debug('Finished successfully!');
35 | } else {
36 | throw ex;
37 | }
38 |
39 |
--------------------------------------------------------------------------------
/force-app/repository/AggregateRecord.cls:
--------------------------------------------------------------------------------
1 | public class AggregateRecord {
2 | private final Map keyToAggregateResult = new Map();
3 | private static final String COUNT_KEY = 'countKey';
4 |
5 | public AggregateRecord putAll(Map values) {
6 | this.keyToAggregateResult.putAll(values);
7 | return this;
8 | }
9 |
10 | public Object get(String key) {
11 | return this.keyToAggregateResult.get(key);
12 | }
13 |
14 | public Integer getCount() {
15 | return (Integer) this.keyToAggregateResult.get(COUNT_KEY);
16 | }
17 |
18 | public AggregateRecord setCount(Integer countAmount) {
19 | this.keyToAggregateResult.put(COUNT_KEY, countAmount);
20 | return this;
21 | }
22 |
23 | public Boolean equals(Object that) {
24 | if (that instanceof AggregateResult) {
25 | Map thatKeyToAggregateResult = ((AggregateResult) that).getPopulatedFieldsAsMap();
26 | return this.keyToAggregateResult.equals(thatKeyToAggregateResult);
27 | } else if (that instanceof AggregateRecord) {
28 | return this.keyToAggregateResult.equals(((AggregateRecord) that).keyToAggregateResult);
29 | }
30 | return false;
31 | }
32 |
33 | public Integer hashCode() {
34 | return this.keyToAggregateResult.hashCode();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/force-app/repository/QueryField.cls:
--------------------------------------------------------------------------------
1 | public without sharing virtual class QueryField {
2 | private final String fieldValue;
3 |
4 | public QueryField(List fieldTokens) {
5 | this(getFieldNames(fieldTokens));
6 | }
7 |
8 | public QueryField(List fieldNames) {
9 | this.fieldValue = String.join(fieldNames, ',');
10 | }
11 |
12 | public QueryField(Schema.SObjectField token) {
13 | this.fieldValue = token.toString();
14 | }
15 |
16 | public QueryField(List parentFieldChain, List parentFields) {
17 | String base = '';
18 | while (parentFieldChain.isEmpty() == false) {
19 | base += parentFieldChain.remove(0).getDescribe().getRelationshipName() + '.';
20 | }
21 | List fields = new List();
22 | for (Schema.SObjectField field : parentFields) {
23 | fields.add(base + field.toString());
24 | }
25 | this.fieldValue = String.join(fields, ',');
26 | }
27 |
28 | public override String toString() {
29 | return this.fieldValue;
30 | }
31 |
32 | private static List getFieldNames(List fieldTokens) {
33 | List fieldNames = new List();
34 | for (Schema.SObjectField token : fieldTokens) {
35 | fieldNames.add(token.toString());
36 | }
37 | return fieldNames;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/force-app/repository/IAggregateRepository.cls:
--------------------------------------------------------------------------------
1 | public interface IAggregateRepository extends IRepository {
2 | IAggregateRepository groupBy(Schema.SObjectField fieldToken);
3 | IAggregateRepository groupBy(DateFunction dateFunction, Schema.SObjectField fieldToken, String alias);
4 | IAggregateRepository groupBy(List parentFieldChain);
5 |
6 | IAggregateRepository addHaving(Aggregation aggregation, Query.Operator operator, Object value);
7 |
8 | IAggregateRepository addSortOrder(Aggregation aggregate, RepositorySortOrder sortOrder);
9 | IAggregateRepository addSortOrder(
10 | DateFunction dateFunction,
11 | Schema.SObjectField fieldToken,
12 | RepositorySortOrder sortOrder
13 | );
14 |
15 | // always a fun one
16 | Integer count();
17 | Integer count(Query query);
18 | Integer count(List queries);
19 | // with support for sum, count, count distinct, average, max, min
20 | List aggregate(Aggregation aggregation);
21 | List aggregate(Aggregation aggregation, Query query);
22 | List aggregate(Aggregation aggregation, List queries);
23 | List aggregate(List aggregations);
24 | List aggregate(List aggregations, Query query);
25 | List aggregate(List aggregations, List queries);
26 | }
27 |
--------------------------------------------------------------------------------
/force-app/repository/FieldLevelHistory.cls:
--------------------------------------------------------------------------------
1 | @SuppressWarnings('PMD.PropertyNamingConventions')
2 | public class FieldLevelHistory {
3 | private String parentLookupName = 'ParentId';
4 |
5 | public Datetime CreatedDate { get; set; }
6 | public Id CreatedById { get; set; }
7 | public Object NewValue { get; set; }
8 | public Object OldValue { get; set; }
9 | public String Field { get; set; }
10 | public Id ParentId { get; private set; }
11 |
12 | public override String toString() {
13 | return JSON.serialize(this);
14 | }
15 |
16 | public FieldLevelHistory setValues(Map values) {
17 | this.CreatedById = (Id) values.get('CreatedById');
18 | Object possibleCreatedDate = values.get('CreatedDate');
19 | this.CreatedDate = Datetime.valueOfGmt(String.valueOf(possibleCreatedDate).replace('T', ' ').remove('"'));
20 | this.Field = (String) values.get('Field');
21 | this.NewValue = values.get('NewValue');
22 | this.OldValue = values.get('OldValue');
23 | this.setParentId(values);
24 | return this;
25 | }
26 |
27 | public FieldLevelHistory setParentLookup(Schema.SObjectField fieldToken) {
28 | if (fieldToken != null) {
29 | this.parentLookupName = fieldToken.getDescribe().getName();
30 | }
31 | return this;
32 | }
33 |
34 | private void setParentId(Map values) {
35 | this.ParentId = (Id) values.get(this.parentLookupName);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/force-app/utils/TestingUtils.cls:
--------------------------------------------------------------------------------
1 | @IsTest
2 | public class TestingUtils {
3 | private static Integer startingNumber = 1;
4 |
5 | @SuppressWarnings('PMD.EagerlyLoadedDescribeSObjectResult')
6 | public static String generateId(Schema.SObjectType sObjectType) {
7 | String result = String.valueOf(startingNumber++);
8 | Schema.DescribeSObjectResult describe = sObjectType.getDescribe(Schema.SObjectDescribeOptions.DEFERRED);
9 | String keyPrefix = describe.getKeyPrefix();
10 | if (keyPrefix == null) {
11 | switch on describe.getAssociateEntityType() {
12 | when 'History' {
13 | keyPrefix = '017';
14 | }
15 | when 'Share' {
16 | keyPrefix = '02c';
17 | }
18 | }
19 | }
20 | return keyPrefix + '0'.repeat(12 - result.length()) + result;
21 | }
22 |
23 | public static SObject generateId(SObject objectInstance) {
24 | objectInstance.Id = objectInstance.Id ?? generateId(objectInstance.getSObjectType());
25 | return objectInstance;
26 | }
27 |
28 | public static void generateIds(List records) {
29 | for (SObject record : records) {
30 | generateId(record);
31 | }
32 | }
33 |
34 | @IsTest
35 | static void generatesFakeHistoryIds() {
36 | Assert.isNotNull(generateId(LeadHistory.SObjectType));
37 | }
38 |
39 | @IsTest
40 | static void generatesFakeShareRecords() {
41 | Assert.isNotNull(generateId(LeadShare.SObjectType));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/force-app/factory/RepoFactoryMockTest.cls:
--------------------------------------------------------------------------------
1 | @IsTest
2 | private class RepoFactoryMockTest {
3 | @IsTest
4 | static void addsChildrenInMemory() {
5 | Account acc = new Account();
6 | String expectedOppName = 'Hello';
7 | List opps = new List{ new Opportunity(Name = expectedOppName) };
8 | // can't simply do acc.Opportunities = opps - this leads to "Field is not writeable: Account.Opportunities" on deploy
9 | Assert.areEqual(0, acc.Opportunities.size(), 'Test has started under the wrong conditions');
10 |
11 | acc = (Account) RepoFactoryMock.addChildrenToRecord(acc, Opportunity.AccountId, opps);
12 |
13 | Assert.areEqual(1, acc.Opportunities.size());
14 | Assert.areEqual(expectedOppName, acc.Opportunities.get(0).Name);
15 | }
16 |
17 | @IsTest
18 | static void addsMockCursor() {
19 | Account acc = new Account(Id = TestingUtils.generateId(Schema.Account.SObjectType));
20 | Account secondAccount = new Account(Id = TestingUtils.generateId(Schema.Account.SObjectType));
21 | RepoFactoryMock.CursorResults.put(
22 | Account.SObjectType,
23 | new List{ new RepoFactoryMock.CursorMock(new List{ acc, secondAccount }) }
24 | );
25 |
26 | RepoFactoryMock.FacadeMock facade = new RepoFactoryMock.FacadeMock();
27 | IHistoryRepository repo = facade.getRepo(
28 | Account.SObjectType,
29 | new List(),
30 | new RepoFactoryMock()
31 | );
32 | Cursor cursor = repo.getCursor(new List());
33 |
34 | Assert.areEqual(2, cursor.getNumRecords());
35 | Assert.areEqual(acc.Id, cursor.fetch(0, 1)[0].Id);
36 | // verify we don't overindex past the actual cursor end
37 | Assert.areEqual(secondAccount.Id, cursor.fetch(1, 3)[0].Id);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/force-app/repository/RepositorySortOrder.cls:
--------------------------------------------------------------------------------
1 | @SuppressWarnings('PMD.PropertyNamingConventions')
2 | public class RepositorySortOrder {
3 | private final SortOrder sortOrder;
4 | private NullSortOrder nullSortOrder;
5 |
6 | public enum SortOrder {
7 | ASCENDING,
8 | DESCENDING
9 | }
10 |
11 | public enum NullSortOrder {
12 | FIRST,
13 | LAST
14 | }
15 |
16 | public static final RepositorySortOrder ASCENDING {
17 | get {
18 | if (ASCENDING == null) {
19 | ASCENDING = new RepositorySortOrder(SortOrder.ASCENDING);
20 | }
21 | return ASCENDING;
22 | }
23 | set;
24 | }
25 |
26 | public static final RepositorySortOrder DESCENDING {
27 | get {
28 | if (DESCENDING == null) {
29 | DESCENDING = new RepositorySortOrder(SortOrder.DESCENDING);
30 | }
31 | return DESCENDING;
32 | }
33 | set;
34 | }
35 |
36 | public RepositorySortOrder(SortOrder sortOrder) {
37 | this(sortOrder, null);
38 | }
39 |
40 | public RepositorySortOrder(SortOrder sortOrder, NullSortOrder nullSortOrder) {
41 | this.sortOrder = sortOrder;
42 | this.nullSortOrder = nullSortOrder;
43 | }
44 |
45 | public override String toString() {
46 | String base = this.sortOrder == RepositorySortOrder.SortOrder.ASCENDING ? 'ASC' : 'DESC';
47 | if (this.nullSortOrder != null) {
48 | base += ' ' + 'NULLS ' + this.nullSortOrder.name();
49 | }
50 | return base;
51 | }
52 |
53 | public Boolean equals(Object thatObj) {
54 | if (thatObj instanceof RepositorySortOrder) {
55 | RepositorySortOrder that = (RepositorySortOrder) thatObj;
56 | return this.nullSortOrder == that.nullSortOrder && this.sortOrder == that.sortOrder;
57 | }
58 | return false;
59 | }
60 |
61 | public Integer hashCode() {
62 | return this.toString().hashCode();
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | # Unique name for this workflow
2 | name: Apex DML Mocking Release
3 |
4 | on:
5 | push:
6 | branches:
7 | - main
8 | paths-ignore:
9 | - 'sfdx-project.json'
10 | - '**/README.md'
11 | - 'package.json'
12 | - 'LICENSE'
13 | - '.gitignore'
14 | - '.prettierignore'
15 | - '.prettierrc'
16 | pull_request:
17 | types: [opened, synchronize]
18 | paths-ignore:
19 | - 'sfdx-project.json'
20 | - '**/README.md'
21 | - 'package.json'
22 | - '.gitignore'
23 | - '.prettierignore'
24 | - '.prettierrc'
25 |
26 | jobs:
27 | scratch-org-test:
28 | runs-on: ubuntu-latest
29 | environment: Test
30 | steps:
31 | # Checkout the code
32 | - name: 'Checkout source code'
33 | uses: actions/checkout@v4
34 | with:
35 | ref: ${{ github.head_ref }}
36 |
37 | - name: 'Setup node'
38 | uses: actions/setup-node@v4
39 | with:
40 | node-version: '22'
41 | cache: 'npm'
42 |
43 | - name: 'Install NPM'
44 | run: npm ci
45 |
46 | - name: 'Static Code Analysis'
47 | run: npm run scan
48 |
49 | # Authenticate using JWT flow
50 | - name: 'Auth to dev hub'
51 | shell: bash
52 | run: |
53 | echo "${{ env.DEVHUB_SERVER_KEY }}" > ./jwt-server.key
54 | npx sf org login jwt --client-id ${{ env.DEVHUB_CONSUMER_KEY }} --username ${{ env.DEVHUB_USERNAME }} --jwt-key-file ./jwt-server.key --set-default-dev-hub
55 | npx sf config set target-org ${{ env.DEVHUB_USERNAME }}
56 | env:
57 | DEVHUB_USERNAME: ${{ secrets.DEVHUB_USERNAME }}
58 | DEVHUB_CONSUMER_KEY: ${{ secrets.DEVHUB_CONSUMER_KEY }}
59 | DEVHUB_SERVER_KEY: ${{ secrets.DEVHUB_SERVER_KEY }}
60 |
61 | - name: 'Deploy & Test'
62 | shell: pwsh
63 | run: '. ./scripts/test.ps1'
64 |
--------------------------------------------------------------------------------
/force-app/repository/FieldLevelHistoryRepo.cls:
--------------------------------------------------------------------------------
1 | public virtual without sharing class FieldLevelHistoryRepo extends AggregateRepository implements IHistoryRepository {
2 | private Schema.SObjectField parentFieldToken;
3 | private final Set fullHistoryFields;
4 |
5 | private Boolean isHistoryQuery = false;
6 |
7 | public FieldLevelHistoryRepo(
8 | Schema.SObjectType repoType,
9 | List queryFields,
10 | RepoFactory repoFactory
11 | ) {
12 | super(repoType, queryFields, repoFactory);
13 | this.fullHistoryFields = this.repoType.getDescribe(SObjectDescribeOptions.DEFERRED).fields.getMap().keySet();
14 | }
15 |
16 | public virtual List getAllHistory() {
17 | return this.getHistory(new List());
18 | }
19 |
20 | public List getHistory(Query query) {
21 | return this.getHistory(new List{ query });
22 | }
23 |
24 | public virtual List getHistory(List queries) {
25 | this.isHistoryQuery = true;
26 | List