diff --git a/subfolders/fflib/.github/ISSUE_TEMPLATE/bug_report.md b/subfolders/fflib/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 00000000000..92c0956aeba
--- /dev/null
+++ b/subfolders/fflib/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,29 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+(A clear and concise description of what the bug is.)
+
+**To Reproduce**
+
+(Please provide a public github repo with a full SFDX project that demonstrates the problem. If the repro case can be followed with a single example Apex class against a scratch org with just the fflib-apex-common and fflib-apex-mocks project deployed into it, you don't need to provide a github repo)
+
+Steps to reproduce the behavior:
+1. Create a scratch org as follows....
+2. Run the following Anonymous Apex....
+3. See error
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Screenshots and text of error observed**
+If applicable, add screenshots to help explain your problem. Also, paste the text of the raw error into the issue as well so that it can be found by others via search.
+
+**Version**
+Did you try to reproduce the problem against the latest fflib-apex-common code?
diff --git a/subfolders/fflib/.github/ISSUE_TEMPLATE/feature_request.md b/subfolders/fflib/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 00000000000..bbcbbe7d615
--- /dev/null
+++ b/subfolders/fflib/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/subfolders/fflib/.github/workflows/deploy.and.test.yml b/subfolders/fflib/.github/workflows/deploy.and.test.yml
new file mode 100644
index 00000000000..394bf826646
--- /dev/null
+++ b/subfolders/fflib/.github/workflows/deploy.and.test.yml
@@ -0,0 +1,31 @@
+name: Create a Scratch Org, Push Source and Run Apex Tests
+
+on:
+ push:
+ pull_request_target:
+ workflow_dispatch:
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ ref: ${{github.event.pull_request.head.ref}}
+ repository: ${{github.event.pull_request.head.repo.full_name}}
+ - name: Install SFDX CLI and authorize DevHub
+ uses: apex-enterprise-patterns/setup-sfdx@v1 #We're using a fork of https://github.com/sfdx-actions/setup-sfdx for safety
+ with:
+ sfdx-auth-url: ${{ secrets.DEVHUB_SFDXURL }}
+ - run: sfdx force:config:set defaultdevhubusername=SFDX-ENV -g #Even though the setup-sfdx action uses --setdefaultdevhubusername, it doesn't seem to stick since it uses --setdefaultusername so we brute force it here
+ - run: echo y | sfdx plugins:install shane-sfdx-plugins
+ - run: sfdx force:org:create -f config/project-scratch-def.json --setdefaultusername -d 1
+ - run: sfdx shane:github:src:install -c -g apex-enterprise-patterns -r fflib-apex-mocks -p sfdx-source/apex-mocks
+ - run: sfdx force:source:push
+ - run: sfdx force:apex:test:run -w 5
+ - name: Destroy scratch org
+ run: sfdx force:org:delete -p
+ if: always()
+
diff --git a/subfolders/fflib/.gitignore b/subfolders/fflib/.gitignore
new file mode 100644
index 00000000000..e403467fae7
--- /dev/null
+++ b/subfolders/fflib/.gitignore
@@ -0,0 +1,52 @@
+# General Project related
+.DS_Store
+target/
+temp/
+/deploy/*
+/debug/
+**/dep-dir.txt
+*.prefs
+build.properties
+/sfdx-source/core/main/default
+
+# MavensMate IDE related
+*mm.log
+*.sublime-build
+*.sublime-project
+*.sublime-settings
+*.sublime-workspace
+.sublime-project
+.tm_properties
+
+# Eclipse IDE Related
+.project
+.settings/
+salesforce.schema
+Referenced Packages/
+
+# VS Code IDE Related
+.vscode/
+.history/
+
+# IntelliJ IDE Related
+.idea/
+*.iml
+*.iws
+IlluminatedCloud/
+.project.license
+
+# SFDX Related
+.sfdx/
+sfdx-source/common-base/main/default/
+sfdx-source/untracked/
+.execanon
+
+# NPM Related
+package.json
+/node_modules
+package-lock.json
+
+
+sfdx-source/group*
+research/
+.pmdCache
diff --git a/subfolders/fflib/LICENSE b/subfolders/fflib/LICENSE
new file mode 100644
index 00000000000..03f67b613b2
--- /dev/null
+++ b/subfolders/fflib/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c), FinancialForce.com, inc
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+ are permitted provided that the following conditions are met:
+
+- Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+- Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+- Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ may be used to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/subfolders/fflib/README.md b/subfolders/fflib/README.md
new file mode 100644
index 00000000000..2c06119f3f1
--- /dev/null
+++ b/subfolders/fflib/README.md
@@ -0,0 +1,62 @@
+FFLib Apex Common
+=================
+
+![Push Source and Run Apex Tests](https://github.com/apex-enterprise-patterns/fflib-apex-common/workflows/Create%20a%20Scratch%20Org,%20Push%20Source%20and%20Run%20Apex%20Tests/badge.svg)
+
+
+**Dependencies:** Must deploy [ApexMocks](https://github.com/apex-enterprise-patterns/fflib-apex-mocks) before deploying this library
+
+
+
+
+
+Updates
+=======
+
+- **April 2020**, **IMPORTANT CHANGE**, the directory format of this project repo was converted to [Salesforce DX Source Format](https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_source_file_format.htm). While the GIT commit history was maintained, it is not visible on GitHub. If you need to see the history, either clone the repo and execute `git log --follow` from the command line or refer to this [tag](https://github.com/apex-enterprise-patterns/fflib-apex-common/tree/metadata-format-prior-to-dx-source-format-conversion) of the codebase prior to conversion.
+- **September 2014**, **IMPORTANT CHANGE**, changes applied to support Dreamforce 2014 advanced presentation, library now provides Application factories for major layers and support for ApexMocks. More details to follow! As a result [ApexMocks](https://github.com/apex-enterprise-patterns/fflib-apex-mocks) must be deployed to the org before deploying this library. The sample application [here](https://github.com/apex-enterprise-patterns/fflib-apex-common-samplecode) has also been updated to demonstrate the new features!
+- **July 2014**, **IMPORTANT CHANGE**, prior **23rd July 2014**, both the ``fflib_SObjectDomain.onValidate()`` and ``fflib_SObjectDomain.onValidate(Map existingRecords)`` methods where called during an on **after update** trigger event. From this point on the ``onValidate()`` method will only be called during on **after insert**. If you still require the orignal behaviour add the line ``Configuration.enableOldOnUpdateValidateBehaviour();`` into your constructor.
+- **June 2014**, New classes providing utilities to support security and dynamic queries, in addition to improvements to existing Apex Enterprise Pattern base classes. Read more [here](http://andyinthecloud.com/2014/06/28/financialforce-apex-common-updates/).
+- **June 2014**, Experimental [branch](https://github.com/apex-enterprise-patterns/fflib-apex-common/tree/fls-support-experiment) supporting automated FLS checking, see [README](https://github.com/apex-enterprise-patterns/fflib-apex-common/tree/fls-support-experiment#expirimental-crud-and-fls-support) for more details.
+
+This Library
+============
+
+Is derived from the **Dreamforce 2012** presentation on [Apex Enterprise Patterns](https://github.com/financialforcedev/df12-apex-enterprise-patterns) and progresses the patterns further with a more general ongoing home for them. It also adds some form of namespace qualification from the previous version. So that classes are grouped together more easily in the IDE's and packages. Below you can find comprehensive articles and videos on the use of these patterns. There is also a **working sample application** illustrating the patterns [here](https://github.com/apex-enterprise-patterns/fflib-apex-common-samplecode).
+
+![Alt text](/images/patternsturning.png "Optional title")
+
+Application Enterprise Patterns on Force.com
+============================================
+
+Design patterns are an invaluable tool for developers and architects looking to build enterprise solutions. Here are presented some tried and tested enterprise application engineering patterns that have been used in other platforms and languages. We will discuss and illustrate how patterns such as Data Mapper, Service Layer, Unit of Work and of course Model View Controller can be applied to Force.com. Applying these patterns can help manage governed resources (such as DML) better, encourage better separation-of-concerns in your logic and enforce Force.com coding best practices.
+
+Dreamforce Session and Slides
+-----------------------------
+
+- View slides for the **Dreamforce 2013** session [here](https://docs.google.com/file/d/0B6brfGow3cD8RVVYc1dCX2s0S1E/edit)
+- Video recording of the **Dreamforce 2013** session [here](http://www.youtube.com/watch?v=qlq46AEAlLI).
+- Video recording of the **Advanced Apex Enterprise Dreamforce 2014** session [here](http://dreamforce.vidyard.com/watch/7QtP2628KmtXfmiwI-7B1w%20).
+- View slides for the **Dreamforce 2015** session [here](http://www.slideshare.net/andyinthecloud/building-strong-foundations-apex-enterprise-patterns)
+
+Documentation
+-------------
+
+- [Apex Sharing and applying to Apex Enterprise Patterns](http://andyinthecloud.com/2016/01/10/apex-sharing-and-applying-to-apex-enterprise-patterns/)
+- [Tips for Migrating to Apex Enterprise Patterns](http://andyinthecloud.com/2015/09/30/tips-for-migrating-to-apex-enterprise-patterns/)
+- [Great Contributions to Apex Enterprise Patterns](http://andyinthecloud.com/2015/07/25/great-contributions-to-apex-enterprise-patterns/)
+- [Unit Testing, Apex Enterprise Patterns and ApexMocks – Part 1](http://andyinthecloud.com/2015/03/22/unit-testing-with-apex-enterprise-patterns-and-apexmocks-part-1/)
+- [Unit Testing, Apex Enterprise Patterns and ApexMocks – Part 2](http://andyinthecloud.com/2015/03/29/unit-testing-apex-enterprise-patterns-and-apexmocks-part-2/)
+- [Apex Enterprise Patterns - Separation of Concerns](http://wiki.developerforce.com/page/Apex_Enterprise_Patterns_-_Separation_of_Concerns)
+- [Apex Enterprise Patterns - Service Layer](http://wiki.developerforce.com/page/Apex_Enterprise_Patterns_-_Service_Layer)
+- [Apex Enterprise Patterns - Domain Layer](http://wiki.developerforce.com/page/Apex_Enterprise_Patterns_-_Domain_Layer)
+- [Apex Enterprise Patterns - Selector Layer](https://github.com/financialforcedev/df12-apex-enterprise-patterns#data-mapper-selector)
+
+**Other Related Blogs**
+
+- [Preview of Advanced Apex Patterns Session (Application Factory and ApexMocks Features)](http://andyinthecloud.com/2014/08/26/preview-of-advanced-apex-enterprise-patterns-session/)
+- [Unit Testing with the Domain Layer](http://andyinthecloud.com/2014/03/23/unit-testing-with-the-domain-layer/)
+- [MavensMate Templates](http://andyinthecloud.com/2014/05/23/mavensmate-templates-and-apex-enterprise-patterns/)
+- [FinancialForce Apex Common Updates](http://andyinthecloud.com/2014/06/28/financialforce-apex-common-updates/)
+
diff --git a/subfolders/fflib/config/project-scratch-def.json b/subfolders/fflib/config/project-scratch-def.json
new file mode 100644
index 00000000000..020b6b8e0a4
--- /dev/null
+++ b/subfolders/fflib/config/project-scratch-def.json
@@ -0,0 +1,9 @@
+{
+ "orgName": "apex-common",
+ "edition": "Developer",
+ "settings": {
+ "lightningExperienceSettings": {
+ "enableS1DesktopEnabled": true
+ }
+ }
+}
diff --git a/subfolders/fflib/images/patternsturning.png b/subfolders/fflib/images/patternsturning.png
new file mode 100644
index 00000000000..4981c0572f1
Binary files /dev/null and b/subfolders/fflib/images/patternsturning.png differ
diff --git a/subfolders/fflib/sfdx-project.json b/subfolders/fflib/sfdx-project.json
new file mode 100644
index 00000000000..6cfb92e19b7
--- /dev/null
+++ b/subfolders/fflib/sfdx-project.json
@@ -0,0 +1,11 @@
+{
+ "packageDirectories": [
+ {
+ "path": "sfdx-source/apex-common",
+ "default": true
+ }
+ ],
+ "namespace": "",
+ "sfdcLoginUrl": "https://login.salesforce.com",
+ "sourceApiVersion": "53.0"
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Application.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Application.cls
new file mode 100644
index 00000000000..427de5514e7
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Application.cls
@@ -0,0 +1,500 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+/**
+ * Class provides inner classes implementing factories for the main components
+ * of the Apex Enterprise Patterns, Service, Unit Of Work, Selector and Domain.
+ * See the sample applications Application.cls file for an example
+ **/
+public virtual class fflib_Application
+{
+ /**
+ * Class implements a Unit of Work factory
+ **/
+ public virtual class UnitOfWorkFactory implements fflib_IUnitOfWorkFactory
+ {
+ protected List m_objectTypes;
+ protected fflib_ISObjectUnitOfWork m_mockUow;
+
+ /**
+ * Constructs a Unit Of Work factory
+ **/
+ public UnitOfWorkFactory() { }
+
+ /**
+ * Constructs a Unit Of Work factory
+ *
+ * @param objectTypes List of SObjectTypes in dependency order
+ **/
+ public UnitOfWorkFactory(List objectTypes)
+ {
+ m_objectTypes = objectTypes.clone();
+ }
+
+ /**
+ * Returns a new fflib_SObjectUnitOfWork configured with the
+ * SObjectType list provided in the constructor, returns a Mock implementation
+ * if set via the setMock method
+ **/
+ public virtual fflib_ISObjectUnitOfWork newInstance()
+ {
+ // Mock?
+ if(m_mockUow!=null)
+ return m_mockUow;
+ return new fflib_SObjectUnitOfWork(m_objectTypes);
+ }
+
+ /**
+ * Returns a new fflib_SObjectUnitOfWork configured with the
+ * SObjectType list provided in the constructor, returns a Mock implementation
+ * if set via the setMock method
+ **/
+ public virtual fflib_ISObjectUnitOfWork newInstance(fflib_SObjectUnitOfWork.IDML dml)
+ {
+ // Mock?
+ if(m_mockUow!=null)
+ return m_mockUow;
+ return new fflib_SObjectUnitOfWork(m_objectTypes, dml);
+ }
+
+ /**
+ * Returns a new fflib_SObjectUnitOfWork configured with the
+ * SObjectType list specified, returns a Mock implementation
+ * if set via the setMock method
+ *
+ * @remark If mock is set, the list of SObjectType in the mock could be different
+ * then the list of SObjectType specified in this method call
+ **/
+ public virtual fflib_ISObjectUnitOfWork newInstance(List objectTypes)
+ {
+ // Mock?
+ if(m_mockUow!=null)
+ return m_mockUow;
+ return new fflib_SObjectUnitOfWork(objectTypes);
+ }
+
+ /**
+ * Returns a new fflib_SObjectUnitOfWork configured with the
+ * SObjectType list specified, returns a Mock implementation
+ * if set via the setMock method
+ *
+ * @remark If mock is set, the list of SObjectType in the mock could be different
+ * then the list of SObjectType specified in this method call
+ **/
+ public virtual fflib_ISObjectUnitOfWork newInstance(List objectTypes, fflib_SObjectUnitOfWork.IDML dml)
+ {
+ // Mock?
+ if(m_mockUow!=null)
+ return m_mockUow;
+ return new fflib_SObjectUnitOfWork(objectTypes, dml);
+ }
+
+ @TestVisible
+ protected virtual void setMock(fflib_ISObjectUnitOfWork mockUow)
+ {
+ m_mockUow = mockUow;
+ }
+ }
+
+ /**
+ * Simple Service Factory implementation
+ **/
+ public virtual class ServiceFactory implements fflib_IServiceFactory
+ {
+ protected Map m_serviceInterfaceTypeByServiceImplType;
+
+ protected Map m_serviceInterfaceTypeByMockService;
+
+ /**
+ * Constructs a simple Service Factory
+ **/
+ public ServiceFactory() { }
+
+ /**
+ * Constructs a simple Service Factory,
+ * using a Map of Apex Interfaces to Apex Classes implementing the interface
+ * Note that this will not check the Apex Classes given actually implement the interfaces
+ * as this information is not presently available via the Apex runtime
+ *
+ * @param serviceInterfaceTypeByServiceImplType Map ofi interfaces to classes
+ **/
+ public ServiceFactory(Map serviceInterfaceTypeByServiceImplType)
+ {
+ m_serviceInterfaceTypeByServiceImplType = serviceInterfaceTypeByServiceImplType;
+ m_serviceInterfaceTypeByMockService = new Map();
+ }
+
+ /**
+ * Returns a new instance of the Apex class associated with the given Apex interface
+ * Will return any mock implementation of the interface provided via setMock
+ * Note that this method will not check the configured Apex class actually implements the interface
+ *
+ * @param serviceInterfaceType Apex interface type
+ * @exception Is thrown if there is no registered Apex class for the interface type
+ **/
+ public virtual Object newInstance(Type serviceInterfaceType)
+ {
+ // Mock implementation?
+ if( m_serviceInterfaceTypeByMockService.containsKey( serviceInterfaceType ) )
+ {
+ return m_serviceInterfaceTypeByMockService.get( serviceInterfaceType );
+ }
+
+ String serviceInterfaceName = serviceInterfaceType.getName();
+
+ // Check if a configured implementation exists in the Custom Metadata
+ Type serviceImpl = m_serviceInterfaceTypeByServiceImplType.get(serviceInterfaceType);
+ if ( serviceImpl != null )
+ {
+ try {
+ return serviceImpl.newInstance();
+ }
+ catch ( Exception e ) {
+ throw new DeveloperException( 'Implementation for service interface ' + serviceInterfaceName + ' (' + serviceImpl.getName() + ') could not be constructed', e );
+ }
+ }
+
+ // Check if we can use a default service instead, based on the name IServiceInterface => ServiceInterfaceImpl
+ if ( ! isAServiceInterfaceName( serviceInterfaceName ) )
+ {
+ throw new DeveloperException( 'No implementation registered for service interface ' + serviceInterfaceName + ' and default implementation cannot be determined from the name (not an interface in the standard naming convention (Ixxxx)' );
+ }
+
+ String defaultServiceName = buildDefaultServiceName( serviceInterfaceName );
+
+ Type defaultServiceType = Type.forName( defaultServiceName );
+
+ if ( defaultServiceType == null ) {
+ throw new DeveloperException( 'No implementation registered for service interface ' + serviceInterfaceName + ' and no default implementation found with the name ' + defaultServiceName );
+ }
+
+ try {
+ if ( !serviceInterfaceName.endsWith( 'ILoggerService' ) )
+ {
+ LoggerService.log( LoggerService.LEVEL.INFO, 'Using default implementation ' + defaultServiceName + ' for ' + serviceInterfaceName );
+ }
+
+ return defaultServiceType.newInstance();
+ }
+ catch ( Exception e ) {
+ throw new DeveloperException( 'Default implementation for service interface ' + serviceInterfaceName + ' (' + defaultServiceName + ') could not be constructed', e );
+ }
+ }
+
+ @testVisible
+ private Boolean isAServiceInterfaceName( String serviceName )
+ {
+ return ( ( ! serviceName.contains( '.' ) && serviceName.startsWith( 'I' ) ) || serviceName.substringAfterLast( '.' ).startsWith( 'I' ) );
+ }
+
+ @testVisible
+ private String buildDefaultServiceName( String serviceInterfaceName )
+ {
+ String[] serviceInterfaceNameParts = serviceInterfaceName.split( '\\.' );
+ String serviceInterfaceNameLastPart = serviceInterfaceNameParts[ serviceInterfaceNameParts.size() - 1 ];
+ String defaultServiceImpl = serviceInterfaceNameLastPart.substringAfter( 'I' ) + 'Impl';
+ serviceInterfaceNameParts[ serviceInterfaceNameParts.size() - 1 ] = defaultServiceImpl;
+
+ return String.join( serviceInterfaceNameParts, '.' );
+ }
+
+ @TestVisible
+ protected virtual void setMock(Type serviceInterfaceType, Object serviceImpl)
+ {
+ m_serviceInterfaceTypeByMockService.put(serviceInterfaceType, serviceImpl);
+ }
+ }
+
+ /**
+ * Class implements a Selector class factory
+ **/
+ public virtual class SelectorFactory implements fflib_ISelectorFactory
+ {
+ protected Map m_sObjectBySelectorType;
+ protected Map m_sObjectByMockSelector;
+
+ /**
+ * Constructs a simple Selector Factory
+ **/
+ public SelectorFactory() { }
+
+ /**
+ * Consturcts a Selector Factory linking SObjectType's with Apex Classes implement the fflib_ISObjectSelector interface
+ * Note that the factory does not check the given Apex Classes implement the interface
+ * currently this is not possible in Apex.
+ *
+ * @Param sObjectBySelectorType Map of SObjectType's to Selector Apex Classes
+ **/
+ public SelectorFactory(Map sObjectBySelectorType)
+ {
+ m_sObjectBySelectorType = sObjectBySelectorType;
+ m_sObjectByMockSelector = new Map();
+ }
+
+ /**
+ * Creates a new instance of the associated Apex Class implementing fflib_ISObjectSelector
+ * for the given SObjectType, or if provided via setMock returns the Mock implementation
+ *
+ * @param sObjectType An SObjectType token, e.g. Account.SObjectType
+ **/
+ public virtual fflib_ISObjectSelector newInstance(SObjectType sObjectType)
+ {
+ // Mock implementation?
+ if(m_sObjectByMockSelector.containsKey(sObjectType))
+ return m_sObjectByMockSelector.get(sObjectType);
+
+ // Determine Apex class for Selector class
+ Type selectorClass = m_sObjectBySelectorType.get(sObjectType);
+ if(selectorClass==null)
+ throw new DeveloperException('Selector class not found for SObjectType ' + sObjectType);
+
+ // Construct Selector class and query by Id for the records
+ return (fflib_ISObjectSelector) selectorClass.newInstance();
+ }
+
+ /**
+ * Helper method to query the given SObject records
+ * Internally creates an instance of the registered Selector and calls its
+ * selectSObjectById method
+ *
+ * @param recordIds The SObject record Ids, must be all the same SObjectType
+ * @exception Is thrown if the record Ids are not all the same or the SObjectType is not registered
+ **/
+ public virtual List selectById(Set recordIds)
+ {
+ // No point creating an empty Domain class, nor can we determine the SObjectType anyway
+ if(recordIds==null || recordIds.size()==0)
+ throw new DeveloperException('Invalid record Id\'s set');
+
+ // Determine SObjectType
+ SObjectType domainSObjectType = new List(recordIds)[0].getSObjectType();
+ for(Id recordId : recordIds)
+ if(recordId.getSobjectType()!=domainSObjectType)
+ throw new DeveloperException('Unable to determine SObjectType, Set contains Id\'s from different SObject types');
+
+ // Construct Selector class and query by Id for the records
+ return newInstance(domainSObjectType).selectSObjectsById(recordIds);
+ }
+
+ /**
+ * Helper method to query related records to those provided, for example
+ * if passed a list of Opportunity records and the Account Id field will
+ * construct internally a list of Account Ids and call the registered
+ * Account selector to query the related Account records, e.g.
+ *
+ * List accounts =
+ * (List) Application.Selector.selectByRelationship(myOpps, Opportunity.AccountId);
+ *
+ * @param relatedRecords used to extract the related record Ids, e.g. Opportunity records
+ * @param relationshipField field in the passed records that contains the relationship records to query, e.g. Opportunity.AccountId
+ **/
+ public virtual List selectByRelationship(List relatedRecords, SObjectField relationshipField)
+ {
+ Set relatedIds = new Set();
+ for(SObject relatedRecord : relatedRecords)
+ {
+ Id relatedId = (Id) relatedRecord.get(relationshipField);
+ if(relatedId!=null)
+ relatedIds.add(relatedId);
+ }
+ return selectById(relatedIds);
+ }
+
+ @TestVisible
+ protected virtual void setMock(fflib_ISObjectSelector selectorInstance)
+ {
+ m_sObjectByMockSelector.put(selectorInstance.sObjectType(), selectorInstance);
+ }
+ }
+
+ /**
+ * Class implements a Domain class factory
+ **/
+ public virtual class DomainFactory implements fflib_IDomainFactory
+ {
+ protected fflib_Application.SelectorFactory m_selectorFactory;
+
+ protected Map constructorTypeByObject;
+
+ protected Map mockDomainByObject;
+
+ /**
+ * Constructs a Domain factory
+ **/
+ public DomainFactory() { }
+
+ /**
+ * Constructs a Domain factory, using an instance of the Selector Factory
+ * and a map of Apex classes implementing fflib_ISObjectDomain by SObjectType
+ * Note this will not check the Apex classes provided actually implement the interfaces
+ * since this is not possible in the Apex runtime at present
+ *
+ * @param selectorFactory , e.g. Application.Selector
+ * @param constructorTypeByObject Map of Domain classes by ObjectType
+ **/
+ public DomainFactory(fflib_Application.SelectorFactory selectorFactory,
+ Map constructorTypeByObject)
+ {
+ m_selectorFactory = selectorFactory;
+ this.constructorTypeByObject = constructorTypeByObject;
+ this.mockDomainByObject = new Map();
+ }
+
+ /**
+ * Constructs a Domain factory, using an instance of the Selector Factory
+ * and a map of Apex classes implementing fflib_ISObjectDomain by SObjectType
+ * Note this will not check the Apex classes provided actually implement the interfaces
+ * since this is not possible in the Apex runtime at present
+ *
+ * @param selectorFactory, e.g. Application.Selector
+ * @param sObjectByDomainConstructorType Map of Apex classes by SObjectType
+ **/
+ public DomainFactory(fflib_Application.SelectorFactory selectorFactory,
+ Map sObjectByDomainConstructorType)
+ {
+ m_selectorFactory = selectorFactory;
+ this.constructorTypeByObject = getConstructorTypeByObject(sObjectByDomainConstructorType);
+ this.mockDomainByObject = new Map();
+ }
+
+ /**
+ * Dynamically constructs an instance of a Domain class for the given record Ids
+ * Internally uses the Selector Factory to query the records before passing to a
+ * dynamically constructed instance of the application Apex Domain class
+ *
+ * @param recordIds A list of Id's of the same type
+ * @exception Throws an exception via the Selector Factory if the Ids are not all of the same SObjectType
+ **/
+ public virtual fflib_IDomain newInstance(Set recordIds)
+ {
+ return newInstance(m_selectorFactory.selectById(recordIds));
+
+ }
+
+ /**
+ * Dynamically constructs an instance of the Domain class for the given records
+ * Will return a Mock implementation if one has been provided via setMock
+ *
+ * @param records A concrete list (e.g. List vs List) of records
+ * @exception Throws an exception if the SObjectType cannot be determined from the list
+ * or the constructor for Domain class was not registered for the SObjectType
+ **/
+ public virtual fflib_IDomain newInstance(List records)
+ {
+ SObjectType domainSObjectType = records.getSObjectType();
+ if(domainSObjectType==null)
+ throw new DeveloperException('Unable to determine SObjectType');
+
+ return newInstance((List) records, (Object) domainSObjectType);
+ }
+
+ public virtual fflib_IDomain newInstance(List objects, Object objectType)
+ {
+ // Mock implementation?
+ if (mockDomainByObject.containsKey(objectType))
+ return mockDomainByObject.get(objectType);
+
+ // Determine SObjectType and Apex classes for Domain class
+ Type domainConstructorClass = constructorTypeByObject.get(objectType);
+ if(domainConstructorClass==null)
+ throw new DeveloperException('Domain constructor class not found for SObjectType ' + objectType);
+
+ // Construct Domain class passing in the queried records
+ Object domainConstructor = domainConstructorClass.newInstance();
+
+ // for backwards compatibility
+ if (domainConstructor instanceof fflib_SObjectDomain.IConstructable2)
+ {
+ return (fflib_IDomain)
+ ((fflib_SObjectDomain.IConstructable2) domainConstructor)
+ .construct((List) objects, (SObjectType) objectType);
+ }
+ else if (domainConstructor instanceof fflib_SObjectDomain.IConstructable)
+ {
+ return (fflib_IDomain)
+ ((fflib_SObjectDomain.IConstructable) domainConstructor)
+ .construct((List) objects);
+ }
+
+ return ((fflib_IDomainConstructor) domainConstructor)
+ .construct(objects);
+ }
+
+ /**
+ * Dynamically constructs an instance of the Domain class for the given records and SObjectType
+ * Will return a Mock implementation if one has been provided via setMock
+ *
+ * @param records A list records
+ * @param domainSObjectType SObjectType for list of records
+ * @exception Throws an exception if the SObjectType is not specified or if constructor for Domain class was not registered for the SObjectType
+ *
+ * @remark Will support List but all records in the list will be assumed to be of
+ * the type specified in sObjectType
+ **/
+ public virtual fflib_IDomain newInstance(List records, SObjectType domainSObjectType)
+ {
+ if(domainSObjectType==null)
+ throw new DeveloperException('Must specify sObjectType');
+
+ return newInstance(
+ (List) records,
+ (Object) domainSObjectType
+ );
+ }
+
+ @TestVisible
+ protected virtual void setMock(fflib_ISObjectDomain mockDomain)
+ {
+ mockDomainByObject.put((Object) mockDomain.sObjectType(), (fflib_IDomain) mockDomain);
+ }
+
+ @TestVisible
+ protected virtual void setMock(fflib_IDomain mockDomain)
+ {
+ mockDomainByObject.put(mockDomain.getType(), mockDomain);
+ }
+
+ protected virtual Map getConstructorTypeByObject(Map constructorTypeBySObjectType)
+ {
+ Map result = new Map();
+ for (SObjectType sObjectType : constructorTypeBySObjectType.keySet())
+ {
+ result.put(
+ (Object) sObjectType,
+ constructorTypeBySObjectType.get(sObjectType)
+ );
+ }
+ return result;
+ }
+ }
+
+ public class ApplicationException extends Exception { }
+
+ /**
+ * Exception representing a developer coding error, not intended for end user eyes
+ **/
+ public class DeveloperException extends Exception { }
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Application.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Application.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Application.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomain.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomain.cls
new file mode 100644
index 00000000000..298f5b8ed42
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomain.cls
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+public interface fflib_IDomain
+{
+ Object getType();
+ List getObjects();
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomain.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomain.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomain.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainConstructor.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainConstructor.cls
new file mode 100644
index 00000000000..c4096d1d00a
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainConstructor.cls
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+public interface fflib_IDomainConstructor
+{
+ fflib_IDomain construct(List objects);
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainConstructor.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainConstructor.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainConstructor.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls
new file mode 100644
index 00000000000..1e0f982d5ae
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+public interface fflib_IDomainFactory
+{
+ fflib_IDomain newInstance(Set recordIds);
+ fflib_IDomain newInstance(List records);
+ fflib_IDomain newInstance(List objects, Object objectType);
+ fflib_IDomain newInstance(List records, SObjectType domainSObjectType);
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls-meta.xml
new file mode 100644
index 00000000000..d75b0582fba
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IDomainFactory.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 51.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IObjects.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IObjects.cls
new file mode 100644
index 00000000000..80358b22dd7
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IObjects.cls
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+public interface fflib_IObjects extends fflib_IDomain
+{
+ /**
+ * @param value Values to check if they are part of the domain
+ *
+ * @return True if the provided value is part of the domain
+ */
+ Boolean contains(Object value);
+
+ /**
+ * @param values Values to check if they are part of the domain
+ *
+ * @return True if all the provided values are part of the domain
+ */
+ Boolean containsAll(List values);
+
+ /**
+ * @param values Values to check if they are part of the domain
+ *
+ * @return True if all the provided values are part of the domain
+ */
+ Boolean containsAll(Set values);
+
+ /**
+ * @param value Value to check if it is part of the domain
+ *
+ * @return True if the provided value is not part of the domain
+ */
+ Boolean containsNot(Object value);
+
+ /**
+ * @param values Values to check if they are part of the domain
+ *
+ * @return True if all the provided values are not part of the domain
+ */
+ Boolean containsNot(List values);
+
+ /**
+ * @param values Values to check if they are part of the domain
+ *
+ * @return True if all the provided values are not part of the domain
+ */
+ Boolean containsNot(Set values);
+
+ /**
+ * @return Returns True is the domain is empty
+ */
+ Boolean isEmpty();
+
+ /**
+ * @return Returns True is the domain has objects
+ */
+ Boolean isNotEmpty();
+
+ /**
+ * @return Returns the amount of records contained in the domain
+ */
+ Integer size();
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IObjects.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IObjects.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IObjects.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectDomain.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectDomain.cls
new file mode 100644
index 00000000000..3dfadce34d4
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectDomain.cls
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+public interface fflib_ISObjectDomain extends fflib_IDomain
+{
+ /**
+ * Returns the SObjectType this Domain class represents
+ **/
+ Schema.SObjectType sObjectType();
+
+ /**
+ * Alternative to the Records property, provided to support mocking of Domain classes
+ **/
+ List getRecords();
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectDomain.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectDomain.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectDomain.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectSelector.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectSelector.cls
new file mode 100644
index 00000000000..6cc8a3baddc
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectSelector.cls
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+public interface fflib_ISObjectSelector
+{
+ /**
+ * Provides the SObjectType for the object the given Selector is providing query logic for
+ **/
+ Schema.SObjectType sObjectType();
+
+ /**
+ * Selects by Id records using the fields defined by the Selector configuration
+ **/
+ List selectSObjectsById(Set idSet);
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectSelector.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectSelector.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectSelector.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls
new file mode 100644
index 00000000000..a18e1256f40
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls
@@ -0,0 +1,229 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+/**
+ * @see fflib_SObjectUnitOfWork
+ **/
+public interface fflib_ISObjectUnitOfWork
+{
+ /**
+ * Register a newly created SObject instance to be inserted when commitWork is called
+ *
+ * @param record A newly created SObject instance to be inserted during commitWork
+ **/
+ void registerNew(SObject record);
+ /**
+ * Register a list of newly created SObject instances to be inserted when commitWork is called
+ *
+ * @param records A list of newly created SObject instances to be inserted during commitWork
+ **/
+ void registerNew(List records);
+ /**
+ * Register a newly created SObject instance to be inserted when commitWork is called,
+ * you may also provide a reference to the parent record instance (should also be registered as new separately)
+ *
+ * @param record A newly created SObject instance to be inserted during commitWork
+ * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent
+ * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately)
+ **/
+ void registerNew(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord);
+ /**
+ * Register a relationship between two records that have yet to be inserted to the database. This information will be
+ * used during the commitWork phase to make the references only when related records have been inserted to the database.
+ *
+ * @param record An existing or newly created record
+ * @param relatedToField A SObjectField reference to the lookup field that relates the two records together
+ * @param relatedTo A SObject instance (yet to be committed to the database)
+ */
+ void registerRelationship(SObject record, Schema.SObjectField relatedToField, SObject relatedTo);
+ /**
+ * Registers a relationship between a record and a Messaging.Email where the record has yet to be inserted
+ * to the database. This information will be
+ * used during the commitWork phase to make the references only when related records have been inserted to the database.
+ *
+ * @param email a single email message instance
+ * @param relatedTo A SObject instance (yet to be committed to the database)
+ */
+ void registerRelationship(Messaging.SingleEmailMessage email, SObject relatedTo);
+ /**
+ * Registers a relationship between a record and a lookup value using an external ID field and a provided value. This
+ * information will be used during the commitWork phase to make the lookup reference requested when inserted to the database.
+ *
+ * @param record An existing or newly created record
+ * @param relatedToField A SObjectField reference to the lookup field that relates the two records together
+ * @param externalIdField A SObjectField reference to a field on the target SObject that is marked as isExternalId
+ * @param externalId A Object representing the targeted value of the externalIdField in said lookup
+ *
+ * Usage Example: uow.registerRelationship(recordSObject, record_sobject__c.relationship_field__c, lookup_sobject__c.external_id__c, 'abc123');
+ *
+ * Wraps putSObject, creating a new instance of the lookup sobject using the external id field and value.
+ */
+ void registerRelationship(SObject record, Schema.SObjectField relatedToField, Schema.SObjectField externalIdField, Object externalId);
+ /**
+ * Register an existing record to be updated during the commitWork method
+ *
+ * @param record An existing record
+ **/
+ void registerDirty(SObject record);
+ /**
+ * Register specific fields on records to be updated when work is committed
+ *
+ * If the records are previously registered as dirty, the dirty fields on the records in this call will overwrite
+ * the values of the previously registered dirty records
+ *
+ * @param records A list of existing records
+ * @param dirtyFields The fields to update if record is already registered
+ **/
+ void registerDirty(List records, List dirtyFields);
+ /**
+ * Register specific fields on record to be updated when work is committed
+ *
+ * If the record has previously been registered as dirty, the dirty fields on the record in this call will overwrite
+ * the values of the previously registered dirty record
+ *
+ * @param record An existing record
+ * @param dirtyFields The fields to update if record is already registered
+ **/
+ void registerDirty(SObject record, List dirtyFields);
+ /**
+ * Register an existing record to be updated when commitWork is called,
+ * you may also provide a reference to the parent record instance (should also be registered as new separately)
+ *
+ * @param record A newly created SObject instance to be inserted during commitWork
+ * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent
+ * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately)
+ **/
+ void registerDirty(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord);
+ /**
+ * Register a list of existing records to be updated during the commitWork method
+ *
+ * @param records A list of existing records
+ **/
+ void registerDirty(List records);
+ /**
+ * Register an deleted record to be removed from the recycle bin during the commitWork method
+ *
+ * @param record An deleted record
+ **/
+ void registerEmptyRecycleBin(SObject record);
+ /**
+ * Register deleted records to be removed from the recycle bin during the commitWork method
+ *
+ * @param records Deleted records
+ **/
+ void registerEmptyRecycleBin(List records);
+ /**
+ * Register a new or existing record to be inserted or updated during the commitWork method
+ *
+ * @param record An new or existing record
+ **/
+ void registerUpsert(SObject record);
+ /**
+ * Register a list of mix of new and existing records to be upserted during the commitWork method
+ *
+ * @param records A list of mix of existing and new records
+ **/
+ void registerUpsert(List records);
+ /**
+ * Register an existing record to be deleted during the commitWork method
+ *
+ * @param record An existing record
+ **/
+ void registerDeleted(SObject record);
+ /**
+ * Register a list of existing records to be deleted during the commitWork method
+ *
+ * @param records A list of existing records
+ **/
+ void registerDeleted(List records);
+ /**
+ * Register a list of existing records to be deleted and removed from the recycle bin during the commitWork method
+ *
+ * @param records A list of existing records
+ **/
+ void registerPermanentlyDeleted(List records);
+ /**
+ * Register a list of existing records to be deleted and removed from the recycle bin during the commitWork method
+ *
+ * @param record A list of existing records
+ **/
+ void registerPermanentlyDeleted(SObject record);
+ /**
+ * Register a newly created SObject (Platform Event) instance to be published when commitWork is called
+ *
+ * @param record A newly created SObject (Platform Event) instance to be inserted during commitWork
+ **/
+ void registerPublishBeforeTransaction(SObject record);
+ /**
+ * Register a list of newly created SObject (Platform Event) instance to be published when commitWork is called
+ *
+ * @param records A list of existing records
+ **/
+ void registerPublishBeforeTransaction(List records);
+ /**
+ * Register a newly created SObject (Platform Event) instance to be published when commitWork has successfully
+ * completed
+ *
+ * @param record A newly created SObject (Platform Event) instance to be inserted during commitWork
+ **/
+ void registerPublishAfterSuccessTransaction(SObject record);
+ /**
+ * Register a list of newly created SObject (Platform Event) instance to be published when commitWork has successfully
+ * completed
+ *
+ * @param records A list of existing records
+ **/
+ void registerPublishAfterSuccessTransaction(List records);
+ /**
+ * Register a newly created SObject (Platform Event) instance to be published when commitWork has caused an error
+ *
+ * @param record A newly created SObject (Platform Event) instance to be inserted during commitWork
+ **/
+ void registerPublishAfterFailureTransaction(SObject record);
+ /**
+ * Register a list of newly created SObject (Platform Event) instance to be published when commitWork has caused an
+ * error
+ *
+ * @param records A list of existing records
+ **/
+ void registerPublishAfterFailureTransaction(List records);
+ /**
+ * Takes all the work that has been registered with the UnitOfWork and commits it to the database
+ **/
+ void commitWork();
+ /**
+ * Register a generic peace of work to be invoked during the commitWork phase
+ *
+ * @param work Work to be registered
+ **/
+ void registerWork(fflib_SObjectUnitOfWork.IDoWork work);
+ /**
+ * Registers the given email to be sent during the commitWork
+ *
+ * @param email Email to be sent
+ **/
+ void registerEmail(Messaging.Email email);
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjectUnitOfWork.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjects.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjects.cls
new file mode 100644
index 00000000000..bf30d9a276e
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjects.cls
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+public interface fflib_ISObjects extends fflib_IObjects
+{
+ /**
+ * @return Return the SObject records contained in the domain
+ */
+ List getRecords();
+
+ /**
+ * @return Return the SObject records ids contained in the domain
+ */
+ Set getRecordIds();
+
+ /**
+ * @return Return the SObjectType of the SObjects contained in the domain
+ */
+ SObjectType getSObjectType();
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjects.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjects.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISObjects.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISelectorFactory.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISelectorFactory.cls
new file mode 100644
index 00000000000..a39095b289b
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISelectorFactory.cls
@@ -0,0 +1,31 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+public interface fflib_ISelectorFactory
+{
+ fflib_ISObjectSelector newInstance(SObjectType sObjectType);
+ List selectById(Set recordIds);
+ List selectByRelationship(List relatedRecords, SObjectField relationshipField);
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISelectorFactory.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISelectorFactory.cls-meta.xml
new file mode 100644
index 00000000000..d75b0582fba
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_ISelectorFactory.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 51.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IServiceFactory.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IServiceFactory.cls
new file mode 100644
index 00000000000..93fa4124cc7
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IServiceFactory.cls
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+public interface fflib_IServiceFactory
+{
+ Object newInstance(Type serviceInterfaceType);
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IServiceFactory.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IServiceFactory.cls-meta.xml
new file mode 100644
index 00000000000..d75b0582fba
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IServiceFactory.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 51.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IUnitOfWorkFactory.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IUnitOfWorkFactory.cls
new file mode 100644
index 00000000000..371ab1ccde9
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IUnitOfWorkFactory.cls
@@ -0,0 +1,32 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+public interface fflib_IUnitOfWorkFactory
+{
+ fflib_ISObjectUnitOfWork newInstance();
+ fflib_ISObjectUnitOfWork newInstance(fflib_SObjectUnitOfWork.IDML dml);
+ fflib_ISObjectUnitOfWork newInstance(List objectTypes);
+ fflib_ISObjectUnitOfWork newInstance(List objectTypes, fflib_SObjectUnitOfWork.IDML dml);
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IUnitOfWorkFactory.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IUnitOfWorkFactory.cls-meta.xml
new file mode 100644
index 00000000000..d75b0582fba
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_IUnitOfWorkFactory.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 51.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Objects.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Objects.cls
new file mode 100644
index 00000000000..44c4f069eaf
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Objects.cls
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+public virtual class fflib_Objects implements fflib_IObjects
+{
+ protected List objects { get; private set;} { objects = new List(); }
+
+ /**
+ * Class constructor
+ */
+ public fflib_Objects(List objects)
+ {
+ this.objects = objects.clone();
+ }
+
+ public virtual Object getType()
+ {
+ return Object.class;
+ }
+
+ public List getObjects()
+ {
+ return this.objects;
+ }
+
+ public Boolean contains(Object value)
+ {
+ return getObjects()?.contains(value);
+ }
+
+ public Boolean containsAll(List values)
+ {
+ if (values == null) return false;
+
+ return containsAll(new Set(values));
+ }
+
+ public Boolean containsAll(Set values)
+ {
+ if (values == null) return false;
+
+ for (Object value : values)
+ {
+ if (!getObjects()?.contains(value)) return false;
+ }
+ return true;
+ }
+
+ public Boolean containsNot(Object value)
+ {
+ return !contains(value);
+ }
+
+ public Boolean containsNot(List values)
+ {
+ if (values == null) return true;
+
+ return containsNot(new Set(values));
+ }
+
+ public Boolean containsNot(Set values)
+ {
+ if (values == null) return true;
+
+ for (Object value : values)
+ {
+ if (getObjects()?.contains(value)) return false;
+ }
+ return true;
+ }
+
+ public Boolean isEmpty()
+ {
+ return (getObjects() == null || getObjects().isEmpty());
+ }
+
+ public Boolean isNotEmpty()
+ {
+ return !isEmpty();
+ }
+
+ public Integer size()
+ {
+ return getObjects().size();
+ }
+
+ protected void setObjects(List objects)
+ {
+ this.objects = objects;
+ }
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Objects.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Objects.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_Objects.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_QueryFactory.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_QueryFactory.cls
new file mode 100644
index 00000000000..9a5958e10a3
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_QueryFactory.cls
@@ -0,0 +1,821 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+/**
+ * QueryFactory provides an object-oriented way of building SOQL queries without resorting to string manipulation.
+ * This class is not meant to be used as a replacement for all SOQL queries, and due to the relatively high overhead in both CPU and describe calls
+ * should be used in places where highly dynamic queries, such as those that include field sets or are mutated heavily
+ * in multiple locations are a good fit for use with fflib_QueryFactory.
+ *
+ * To use call construct a new instance for each query you intend to make.
+ * To add additional fields to the query make use of the selectField(s) methods.
+ *
+ * Currently the WHERE clause of the query is manipulated as a single string, and is decidedly less OO-styled than other methods.
+ * This is expected to be expanded upon in the future.
+ *
+ * To include one or more ORDER BY clause(s), use one of the addOrdering methods. If not specified, the "NULLS FIRST" keywords
+ * will be included by default. Constructing Ordering instances manually is discouraged.
+ *
+ * Subselect Queries are supported with the subselectQuery methods.
+ * More than one sub-query can be added to a single query, but sub-queries can only be 1 level deep.
+ * An exception will thrown from the subselectQuery method when there is an attempt to add a subquery to a sub-query
+ * or to add a subquery to a query with an invalid relationship.
+ *
+ * Current limitations:
+ * - Aggregate functions are not currently supported.
+ * - Cross-object references currently require using String argument(s) to selectField(s).
+ * - The behavior of serializing and deserializing an fflib_QueryFactory instance is currently untested and undefined.
+ *
+ * There is a google doc providing additional guidance on the use of this class with field sets at
+ * https://docs.google.com/a/financialforce.com/document/d/1I4cxN4xHT4UJj_3Oi0YBL_MJ5chm-KG8kMN1D1un8-g/edit?usp=sharing
+**/
+public class fflib_QueryFactory { //No explicit sharing declaration - inherit from caller
+ public enum SortOrder {ASCENDING, DESCENDING}
+
+ /**
+ * This property is read-only and may not be set after instantiation.
+ * The {@link Schema.SObjectType} token of the SObject that will be used in the FROM clause of the resultant query.
+ **/
+ public Schema.SObjectType table {get; private set;}
+ @TestVisible
+ private Set fields;
+ private String conditionExpression {
+ get {
+ if ( conditionCriteria != null && String.isNotBlank( conditionExpression ) ) {
+ // currently an impossible scenario, assuming that setCondition is always used to set the conditions up.
+ throw new fflib_Application.DeveloperException( 'Both criteria and expression were set on the query factory : ' + conditionExpression + ' + ' + conditionCriteria );
+ }
+ else {
+ if ( conditionCriteria != null ) {
+ return conditionCriteria.toSOQL();
+ }
+ }
+ return conditionExpression;
+ }
+ set;
+ }
+ private ISearchCriteria conditionCriteria;
+ private Integer limitCount;
+ private Integer offsetCount;
+ private List order;
+ /**
+ * Integrate checking for READ Field Level Security within the selectField(s) methods
+ * This can optionally be enforced (or not) by calling the setEnforceFLS method prior to calling
+ * one of the selectField or selectFieldset methods.
+ **/
+ private Boolean enforceFLS;
+
+ private Boolean sortSelectFields = true;
+
+ /**
+ * The relationship and subselectQueryMap variables are used to support subselect queries. Subselects can be added to
+ * a query, as long as it isn't a subselect query itself. You may have many subselects inside
+ * a query, but they may only be 1 level deep (no subselect inside a subselect)
+ * to add a subselect, call the subselectQuery method, passing in the ChildRelationship.
+ **/
+ private Schema.ChildRelationship relationship;
+ private Map subselectQueryMap;
+
+ private String getFieldPath(String fieldName, Schema.sObjectType relatedSObjectType){
+ if(!fieldName.contains('.')){ //single field
+ Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(table).getField(fieldName.toLowerCase());
+ if(token == null)
+ throw new InvalidFieldException(fieldName,this.table);
+ if (enforceFLS)
+ fflib_SecurityUtils.checkFieldIsReadable(this.table, token);
+ return token.getDescribe().getName();
+ }
+
+ //traversing FK relationship(s)
+ List fieldPath = new List();
+ Schema.SObjectType lastSObjectType = table;
+ Iterator i = fieldName.split('\\.').iterator();
+ while(i.hasNext()){
+ String field = i.next();
+ Schema.SObjectField token = fflib_SObjectDescribe.getDescribe(lastSObjectType).getField(field.toLowerCase());
+ DescribeFieldResult tokenDescribe = token != null ? token.getDescribe() : null;
+
+ if (token != null && enforceFLS) {
+ fflib_SecurityUtils.checkFieldIsReadable(lastSObjectType, token);
+ }
+
+ if (token != null && i.hasNext() && tokenDescribe.getSoapType() == Schema.SoapType.ID) {
+ List relatedObjs = tokenDescribe.getReferenceTo(); //if it's polymorphic, it matters which one we use - i.e. Lead.Owner is GROUP|USER and each has different fields.
+
+ if (relatedObjs.size() == 1 || relatedSObjectType == null) {
+ lastSObjectType = relatedObjs[0]; //caller did not specify the one to use or there's only one so use the first one
+ }
+ else{
+ for (Schema.sObjectType sot : relatedObjs) {
+ if (fflib_SObjectDescribe.getDescribe(sot).getDescribe().getSObjectType() == relatedSObjectType) {
+ lastSObjectType = sot;
+ break;
+ }
+ }
+ }
+
+ fieldPath.add(tokenDescribe.getRelationshipName());
+ }else if(token != null && !i.hasNext()){
+ fieldPath.add(tokenDescribe.getName());
+ }else{
+ if(token == null)
+ throw new InvalidFieldException(field,lastSObjectType);
+ else
+ throw new NonReferenceFieldException(lastSObjectType+'.'+field+' is not a lookup or master-detail field but is used in a cross-object query field.');
+ }
+ }
+
+ return String.join(fieldPath,'.');
+ }
+
+ private String getFieldPath(String fieldName) {
+ return this.getFieldPath(fieldName, null);
+ }
+
+ @TestVisible
+ private static String getFieldTokenPath(Schema.SObjectField field){
+ if(field == null){
+ throw new InvalidFieldException('Invalid field: null');
+ }
+ return field.getDescribe().getName();
+ }
+
+ /**
+ * fflib_QueryFactory instances will be considered equal if they produce the same SOQL query.
+ * A faster comparison will first be attempted to check if they apply to the same table, and contain the same number of fields selected.
+ * This method will never return true if the provided object is not an instance of fflib_QueryFactory.
+ * @param obj the object to check equality of.
+ **/
+ public Boolean equals(Object obj){
+ if( !(obj instanceof fflib_QueryFactory) || ((fflib_QueryFactory)obj).table != this.table || ((fflib_QueryFactory)obj).fields.size() != this.fields.size() )
+ return false;
+ return ((fflib_QueryFactory)obj).toSOQL() == this.toSOQL();
+ }
+
+ /**
+ * Construct a new fflib_QueryFactory instance with no options other than the FROM clause.
+ * You *must* call selectField(s) before {@link #toSOQL} will return a valid, runnable query.
+ * @param table the SObject to be used in the FROM clause of the resultant query. This sets the value of {@link #table}.
+ **/
+ public fflib_QueryFactory(Schema.SObjectType table){
+ this.table = table;
+ fields = new Set();
+ order = new List();
+ enforceFLS = false;
+ }
+
+ /**
+ * Construct a new fflib_QueryFactory instance with no options other than the FROM clause and the relationship.
+ * This should be used when constructing a subquery query for addition to a parent query.
+ * Objects created with this constructor cannot be added to another object using the subselectQuery method.
+ * You *must* call selectField(s) before {@link #toSOQL} will return a valid, runnable query.
+ * @param relationship the ChildRelationship to be used in the FROM Clause of the resultant Query (when set overrides value of table). This sets the value of {@link #relationship} and {@link #table}.
+ **/
+ private fflib_QueryFactory(Schema.ChildRelationship relationship){
+ this(relationship.getChildSObject());
+ this.relationship = relationship;
+ }
+
+ /**
+ * This method checks to see if the User has Read Access on {@link #table}.
+ * Asserts true if User has access.
+ **/
+ public fflib_QueryFactory assertIsAccessible(){
+ fflib_SecurityUtils.checkObjectIsReadable(table);
+ return this;
+ }
+
+ /**
+ * This method sets a flag to indicate that this query should have FLS Read
+ * permission enforced. If this method is not called, the default behavior
+ * is that FLS read permission will not be checked.
+ * @param enforce whether to enforce field level security (read)
+ **/
+ public fflib_QueryFactory setEnforceFLS(Boolean enforce){
+ this.enforceFLS = enforce;
+ return this;
+ }
+
+ /**
+ * Sets a flag to indicate that this query should have ordered
+ * query fields in the select statement (this at a small cost to performance).
+ * If you are processing large query sets, you should switch this off.
+ * @param doSort whether or not select fields should be sorted in the soql statement.
+ **/
+ public fflib_QueryFactory setSortSelectFields(Boolean doSort){
+ this.sortSelectFields = doSort;
+ return this;
+ }
+ /**
+ * Selects a single field from the SObject specified in {@link #table}.
+ * Selecting fields is idempotent, if this field is already selected calling this method will have no additional impact.
+ * @param fieldName the API name of the field to add to the query's SELECT clause.
+ **/
+ public fflib_QueryFactory selectField(String fieldName){
+ fields.add( getFieldPath(fieldName, null) );
+ return this;
+ }
+
+ /**
+ * Selects a single field from the SObject specified in {@link #table}.
+ * Selecting fields is idempotent, if this field is already selected calling this method will have no additional impact.
+ * @param fieldName the API name of the field to add to the query's SELECT clause.
+ * @param relatedSObjectType the related sObjectType to resolve polymorphic object fields.
+ **/
+ public fflib_QueryFactory selectField(String fieldName, Schema.sOBjectType relatedObjectType) {
+ fields.add(getFieldPath(fieldName, relatedObjectType));
+ return this;
+ }
+
+ /**
+ * Selects a field, avoiding the possible ambiguity of String API names.
+ * @see #selectField(String)
+ * @param field the {@link Schema.SObjectField} to select with this query.
+ * @exception InvalidFieldException If the field is null {@code field}.
+ **/
+ public fflib_QueryFactory selectField(Schema.SObjectField field){
+ if(field == null)
+ throw new InvalidFieldException(null,this.table);
+ if (enforceFLS)
+ fflib_SecurityUtils.checkFieldIsReadable(table, field);
+ fields.add( getFieldTokenPath(field) );
+ return this;
+ }
+ /**
+ * Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
+ * @param fieldNames the Set of field API names to select.
+ **/
+ public fflib_QueryFactory selectFields(Set fieldNames){
+ for(String fieldName:fieldNames){
+ fields.add( getFieldPath(fieldName) );
+ }
+ return this;
+ }
+ /**
+ * Selects multiple fields. This acts the same as calling {@link #selectField(String)} multiple times.
+ * @param fieldNames the List of field API names to select.
+ **/
+ public fflib_QueryFactory selectFields(List fieldNames){
+ for(String fieldName:fieldNames)
+ fields.add( getFieldPath(fieldName) );
+ return this;
+ }
+ /**
+ * Selects multiple fields. This acts the same as calling {@link #selectField(Schema.SObjectField)} multiple times.
+ * @param fields the set of {@link Schema.SObjectField}s to select.
+ * @exception InvalidFieldException if the fields are null {@code fields}.
+ **/
+ public fflib_QueryFactory selectFields(Set fields){
+ for(Schema.SObjectField token:fields){
+ if(token == null)
+ throw new InvalidFieldException();
+ if (enforceFLS)
+ fflib_SecurityUtils.checkFieldIsReadable(table, token);
+ this.fields.add( getFieldTokenPath(token) );
+ }
+ return this;
+ }
+ /**
+ * Selects multiple fields. This acts the same as calling {@link #selectField(Schema.SObjectField)} multiple times.
+ * @param fields the set of {@link Schema.SObjectField}s to select.
+ * @exception InvalidFieldException if the fields are null {@code fields}.
+ **/
+ public fflib_QueryFactory selectFields(List fields){
+ for(Schema.SObjectField token:fields){
+ if(token == null)
+ throw new InvalidFieldException();
+ if (enforceFLS)
+ fflib_SecurityUtils.checkFieldIsReadable(table, token);
+ this.fields.add( getFieldTokenPath(token) );
+ }
+ return this;
+ }
+ /**
+ * @see #selectFieldSet(Schema.FieldSet,Boolean)
+ **/
+ public fflib_QueryFactory selectFieldSet(Schema.FieldSet fieldSet){
+ return selectFieldSet(fieldSet,true);
+ }
+ /**
+ * This is equivalent to iterating the fields in the field set and calling {@link #selectField(String)} on each.
+ * @param fieldSet Select all fields included in the field set.
+ * @param allowCrossObject if false this method will throw an exception if any fields in the field set reference fields on a related record.
+ * @exception InvalidFieldSetException if the fieldset is invalid for table {@code fields}.
+ **/
+ public fflib_QueryFactory selectFieldSet(Schema.FieldSet fieldSet, Boolean allowCrossObject){
+ if(fieldSet.getSObjectType() != table)
+ throw new InvalidFieldSetException('Field set "'+fieldSet.getName()+'" is not for SObject type "'+table+'"');
+ for(Schema.FieldSetMember field: fieldSet.getFields()){
+ if(!allowCrossObject && field.getFieldPath().contains('.'))
+ throw new InvalidFieldSetException('Cross-object fields not allowed and field "'+field.getFieldPath()+'"" is a cross-object field.');
+ fields.add( getFieldPath(field.getFieldPath()) );
+ }
+ return this;
+ }
+ /**
+ * @param conditionExpression Sets the WHERE clause to the string provided. Do not include the "WHERE".
+ **/
+ public fflib_QueryFactory setCondition(String conditionExpression){
+ this.conditionExpression = conditionExpression;
+ this.conditionCriteria = null;
+ return this;
+ }
+
+ /**
+ * @param conditionExpression Sets the WHERE clause to the string provided by the given ISearchCriteria
+ **/
+ public fflib_QueryFactory setCondition(ISearchCriteria criteria){
+ this.conditionCriteria = criteria;
+ this.conditionExpression = null;
+ return this;
+ }
+
+ /**
+ * @returns the current value of the WHERE clause, if any, as set by {@link #setCondition}
+ **/
+ public String getCondition(){
+ return this.conditionExpression;
+ }
+ /**
+ * @param limitCount if not null causes a LIMIT clause to be added to the resulting query.
+ **/
+ public fflib_QueryFactory setLimit(Integer limitCount){
+ this.limitCount = limitCount;
+ return this;
+ }
+ /**
+ * @returns the current value of the LIMIT clause, if any.
+ **/
+ public Integer getLimit(){
+ return this.limitCount;
+ }
+ /**
+ * @param offsetCount if not null causes a OFFSET clause to be added to the resulting query.
+ **/
+ public fflib_QueryFactory setOffset(Integer offsetCount){
+ this.offsetCount = offsetCount;
+ return this;
+ }
+ /**
+ * @returns the current value of the OFFSET clause, if any.
+ **/
+ public Integer getOffset(){
+ return this.offsetCount;
+ }
+ /**
+ * @param o an instance of {@link fflib_QueryFactory.Ordering} to be added to the query's ORDER BY clause.
+ **/
+ public fflib_QueryFactory addOrdering(Ordering o){
+ this.order.add(o);
+ return this;
+ }
+
+ /**
+ * @param o an instance of {@link fflib_QueryFactory.Ordering} to remove all existing (for instance defaults) and be added to the query's ORDER BY clause.
+ **/
+ public fflib_QueryFactory setOrdering(Ordering o){
+ this.order = new List{ o };
+ return this;
+ }
+ /**
+ * @returns the list of orderings that will be used as the query's ORDER BY clause. You may remove elements from the returned list, or otherwise mutate it, to remove previously added orderings.
+ **/
+ public List getOrderings(){
+ return this.order;
+ }
+
+ /**
+ * @returns the selected fields
+ **/
+ public Set getSelectedFields() {
+ return this.fields;
+ }
+
+ /**
+ * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned.
+ * If not, a new one will be created and returned.
+ * @deprecated Replaced by {@link #subselectQuery(String relationshipName)} and {@link #subselectQuery(ChildRelationship relationship)}
+ * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship
+ * @param related The related object type
+ **/
+ public fflib_QueryFactory subselectQuery(SObjectType related){
+ System.debug(LoggingLevel.WARN, 'fflib_QueryFactory.subselectQuery(Schema.SObjectType) is deprecated and will be removed in a future release. Use fflib_QueryFactory.subselectQuery(String) or fflib_QueryFactory.subselectQuery(ChildRelationship) instead.');
+ return setSubselectQuery(getChildRelationship(related), false);
+ }
+
+ /**
+ * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned.
+ * If not, a new one will be created and returned.
+ * @deprecated Replaced by {@link #subselectQuery(String relationshipName, Boolean assertIsAccessible)} and {@link #subselectQuery(ChildRelationship relationship, Boolean assertIsAccessible)}
+ * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship
+ * @param related The related object type
+ * @param assertIsAccessible indicates whether to check if the user has access to the subquery object
+ **/
+ public fflib_QueryFactory subselectQuery(SObjectType related, Boolean assertIsAccessible){
+ System.debug(LoggingLevel.WARN, 'fflib_QueryFactory.subselectQuery(Schema.SObjectType, Boolean) is deprecated and will be removed in a future release. Use fflib_QueryFactory.subselectQuery(String, Boolean) or fflib_QueryFactory.subselectQuery(ChildRelationship, Boolean) instead.');
+ return setSubselectQuery(getChildRelationship(related), assertIsAccessible);
+ }
+
+ /**
+ * Add a subquery query to this query. If a subquery for this relationshipName already exists, it will be returned.
+ * If not, a new one will be created and returned.
+ * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship
+ * @param relationshipName The relationshipName to be added as a subquery
+ **/
+ public fflib_QueryFactory subselectQuery(String relationshipName){
+ return subselectQuery(relationshipName, false);
+ }
+
+ /**
+ * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned.
+ * If not, a new one will be created and returned.
+ * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship
+ * @param relationshipName The relationshipName to be added as a subquery
+ * @param assertIsAccessible indicates whether to check if the user has access to the subquery object
+ **/
+ public fflib_QueryFactory subselectQuery(String relationshipName, Boolean assertIsAccessible){
+ Schema.ChildRelationship relationship = getChildRelationship(relationshipName);
+ if (relationship != null) {
+ return setSubselectQuery(relationship, assertIsAccessible);
+ }
+ throw new InvalidSubqueryRelationshipException('Invalid call to subselectQuery with relationshipName = '+relationshipName +'. Relationship does not exist for ' + table.getDescribe().getName());
+ }
+
+ /**
+ * Add a subquery query to this query. If a subquery for this relationshipName already exists, it will be returned.
+ * If not, a new one will be created and returned.
+ * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship
+ * @param relationship The ChildRelationship to be added as a subquery
+ **/
+ public fflib_QueryFactory subselectQuery(Schema.ChildRelationship relationship){
+ return subselectQuery(relationship, false);
+ }
+
+ /**
+ * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned.
+ * If not, a new one will be created and returned.
+ * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship
+ * @param relationship The ChildRelationship to be added as a subquery
+ * @param assertIsAccessible indicates whether to check if the user has access to the subquery object
+ **/
+ public fflib_QueryFactory subselectQuery(Schema.ChildRelationship relationship, Boolean assertIsAccessible){
+ return setSubselectQuery(relationship, assertIsAccessible);
+ }
+
+ /**
+ * Add a subquery query to this query. If a subquery for this relationship already exists, it will be returned.
+ * If not, a new one will be created and returned.
+ * @exception InvalidSubqueryRelationshipException If this method is called on a subselectQuery or with an invalid relationship
+ * @param relationship The ChildRelationship to be added as a subquery
+ **/
+ private fflib_QueryFactory setSubselectQuery(Schema.ChildRelationship relationship, Boolean assertIsAccessible){
+ if (this.relationship != null){
+ throw new InvalidSubqueryRelationshipException('Invalid call to subselectQuery. You may not add a subselect query to a subselect query.');
+ }
+ if (this.subselectQueryMap == null){
+ this.subselectQueryMap = new Map();
+ }
+ if (this.subselectQueryMap.containsKey(relationship)){
+ return subselectQueryMap.get(relationship);
+ }
+
+ fflib_QueryFactory subSelectQuery = new fflib_QueryFactory(relationship);
+
+ //The child queryFactory should be configured in the same way as the parent by default - can override after if required
+ subSelectQuery.setSortSelectFields(sortSelectFields);
+
+ if(assertIsAccessible){
+ subSelectQuery.assertIsAccessible();
+ }
+ subselectQueryMap.put(relationship, subSelectQuery);
+ return subSelectQuery;
+ }
+
+ /**
+ * @returns the list of subquery instances of fflib_QueryFactory which will be added to the SOQL as relationship/child/sub-queries.
+ **/
+ public List getSubselectQueries(){
+ if (subselectQueryMap != null) {
+ return subselectQueryMap.values();
+ }
+ return null;
+ }
+
+ /**
+ * Get the ChildRelationship from the Table for the object type passed in.
+ * @param objType The object type of the child relationship to get
+ **/
+ private Schema.ChildRelationship getChildRelationship(SObjectType objType){
+ for (Schema.ChildRelationship childRow : table.getDescribe().getChildRelationships()){
+ //occasionally on some standard objects (Like Contact child of Contact) do not have a relationship name.
+ //if there is no relationship name, we cannot query on it, so throw an exception.
+ if (childRow.getChildSObject() == objType && childRow.getRelationshipName() != null){
+ return childRow;
+ }
+ }
+ throw new InvalidSubqueryRelationshipException('Invalid call to subselectQuery. Invalid relationship for table '+table + ' and objtype='+objType);
+ }
+
+ /**
+ * Get the ChildRelationship from the Table for the relationship name passed in.
+ * @param relationshipName The name of the object's ChildRelationship on get
+ **/
+ private Schema.ChildRelationship getChildRelationship(String relationshipName){
+ for (Schema.ChildRelationship childRow : table.getDescribe().getChildRelationships()){
+ if (childRow.getRelationshipName() == relationshipName){
+ return childRow;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Add a field to be sorted on. This may be a direct field or a field
+ * related through an object lookup or master-detail relationship.
+ * Use the set to store unique field names, since we only want to sort
+ * by the same field one time. The sort expressions are stored in a list
+ * so that they are applied to the SOQL in the same order that they
+ * were added in.
+ * @param fieldName The string value of the field to be sorted on
+ * @param direction the direction to be sorted on (ASCENDING or DESCENDING)
+ * @param nullsLast whether to sort null values last (NULLS LAST keyword included).
+ **/
+ public fflib_QueryFactory addOrdering(String fieldName, SortOrder direction, Boolean nullsLast){
+ order.add(
+ new Ordering(getFieldPath(fieldName), direction, nullsLast)
+ );
+ return this;
+ }
+
+ /**
+ * Add a field to be sorted on. This may be a direct field or a field
+ * related through an object lookup or master-detail relationship.
+ * Use the set to store unique field names, since we only want to sort
+ * by the same field one time. The sort expressions are stored in a list
+ * so that they are applied to the SOQL in the same order that they
+ * were added in.
+ * @param field The SObjectField to sort. This can only be a direct reference.
+ * @param direction the direction to be sorted on (ASCENDING or DESCENDING)
+ * @param nullsLast whether to sort null values last (NULLS LAST keyword included).
+ **/
+ public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction, Boolean nullsLast){
+ order.add(
+ new Ordering(getFieldTokenPath(field), direction, nullsLast)
+ );
+ return this;
+ }
+
+ /**
+ * Add a field to be sorted on. This may be a direct field or a field
+ * related through an object lookup or master-detail relationship.
+ * Use the set to store unique field names, since we only want to sort
+ * by the same field one time. The sort expressions are stored in a list
+ * so that they are applied to the SOQL in the same order that they
+ * were added in.
+ * The "NULLS FIRST" keywords will be included by default. If "NULLS LAST"
+ * is required, use one of the overloaded addOrdering methods which include this parameter.
+ * @param fieldName The string value of the field to be sorted on
+ * @param direction the direction to be sorted on (ASCENDING or DESCENDING)
+ **/
+ public fflib_QueryFactory addOrdering(String fieldName, SortOrder direction){
+ order.add(
+ new Ordering(getFieldPath(fieldName), direction)
+ );
+ return this;
+ }
+
+ /**
+ * Add a field to be sorted on. This may be a direct field or a field
+ * related through an object lookup or master-detail relationship.
+ * Use the set to store unique field names, since we only want to sort
+ * by the same field one time. The sort expressions are stored in a list
+ * so that they are applied to the SOQL in the same order that they
+ * were added in.
+ * The "NULLS FIRST" keywords will be included by default. If "NULLS LAST"
+ * is required, use one of the overloaded addOrdering methods which include this parameter.
+ * @param field The SObjectField to sort. This can only be a direct reference.
+ * @param direction the direction to be sorted on (ASCENDING or DESCENDING)
+ **/
+ public fflib_QueryFactory addOrdering(SObjectField field, SortOrder direction){
+ order.add(
+ new Ordering(getFieldTokenPath(field), direction)
+ );
+ return this;
+ }
+
+ /**
+ * Remove existing ordering and set a field to be sorted on. This may be a direct field or a field
+ * related through an object lookup or master-detail relationship.
+ * Use the set to store unique field names, since we only want to sort
+ * by the same field one time. The sort expressions are stored in a list
+ * so that they are applied to the SOQL in the same order that they
+ * were added in.
+ * @param fieldName The string value of the field to be sorted on
+ * @param direction the direction to be sorted on (ASCENDING or DESCENDING)
+ * @param nullsLast whether to sort null values last (NULLS LAST keyword included).
+ **/
+ public fflib_QueryFactory setOrdering(String fieldName, SortOrder direction, Boolean nullsLast){
+ Ordering ordr = new Ordering(getFieldPath(fieldName), direction, nullsLast);
+ return setOrdering(ordr);
+ }
+
+ /**
+ * Remove existing ordering and set a field to be sorted on. This may be a direct field or a field
+ * related through an object lookup or master-detail relationship.
+ * Use the set to store unique field names, since we only want to sort
+ * by the same field one time. The sort expressions are stored in a list
+ * so that they are applied to the SOQL in the same order that they
+ * were added in.
+ * @param field The SObjectField to sort. This can only be a direct reference.
+ * @param direction the direction to be sorted on (ASCENDING or DESCENDING)
+ * @param nullsLast whether to sort null values last (NULLS LAST keyword included).
+ **/
+ public fflib_QueryFactory setOrdering(SObjectField field, SortOrder direction, Boolean nullsLast){
+ Ordering ordr = new Ordering(getFieldTokenPath(field), direction, nullsLast);
+ return setOrdering(ordr);
+ }
+
+ /**
+ * Remove existing ordering and set a field to be sorted on. This may be a direct field or a field
+ * related through an object lookup or master-detail relationship.
+ * Use the set to store unique field names, since we only want to sort
+ * by the same field one time. The sort expressions are stored in a list
+ * so that they are applied to the SOQL in the same order that they
+ * were added in.
+ * @param fieldName The string value of the field to be sorted on
+ * @param direction the direction to be sorted on (ASCENDING or DESCENDING)
+ **/
+ public fflib_QueryFactory setOrdering(String fieldName, SortOrder direction){
+ Ordering ordr = new Ordering(getFieldPath(fieldName), direction);
+ return setOrdering(ordr);
+ }
+
+ /**
+ * Remove existing ordering and set a field to be sorted on. This may be a direct field or a field
+ * related through an object lookup or master-detail relationship.
+ * Use the set to store unique field names, since we only want to sort
+ * by the same field one time. The sort expressions are stored in a list
+ * so that they are applied to the SOQL in the same order that they
+ * were added in.
+ * @param field The SObjectField to sort. This can only be a direct reference.
+ * @param direction the direction to be sorted on (ASCENDING or DESCENDING)
+ **/
+ public fflib_QueryFactory setOrdering(SObjectField field, SortOrder direction){
+ Ordering ordr = new Ordering(getFieldTokenPath(field), direction);
+ return setOrdering(ordr);
+ }
+
+ /**
+ * Convert the values provided to this instance into a full SOQL string for use with Database.query
+ * Check to see if subqueries queries need to be added after the field list.
+ **/
+ public String toSOQL(){
+ String result = 'SELECT ';
+ //if no fields have been added, just add the Id field so that the query or subquery will not just fail
+ if (fields.size() == 0){
+ if (enforceFLS) fflib_SecurityUtils.checkFieldIsReadable(table, 'Id');
+ result += 'Id';
+ }else {
+ List fieldsToQuery = new List(fields);
+
+ if(sortSelectFields){
+ fieldsToQuery.sort();
+ }
+
+ result += String.join(fieldsToQuery,', ');
+ }
+
+ if(subselectQueryMap != null && !subselectQueryMap.isEmpty()){
+ for (fflib_QueryFactory childRow : subselectQueryMap.values()){
+ result += ', (' + childRow.toSOQL() + ') ';
+ }
+ }
+ result += ' FROM ' + (relationship != null ? relationship.getRelationshipName() : table.getDescribe().getName());
+ if( String.isNotBlank( conditionExpression ) )
+ result += ' WHERE '+conditionExpression;
+
+ if(order.size() > 0){
+ result += ' ORDER BY ';
+ for(Ordering o:order)
+ result += o.toSOQL() +', ';
+ result = result.substring(0,result.length()-2);
+ }
+
+ if(limitCount != null)
+ result += ' LIMIT '+limitCount;
+
+ if(offsetCount != null && offsetCount > 0)
+ result += ' OFFSET '+offsetCount;
+
+ return result;
+ }
+
+ /**
+ * Create a "deep" clone of this object that can be safely mutated without affecting the cloned instance
+ * @return a deep clone of this fflib_QueryFactory
+ **/
+ public fflib_QueryFactory deepClone(){
+
+ fflib_QueryFactory clone = new fflib_QueryFactory(this.table)
+ .setLimit(this.limitCount)
+ .setOffset(this.offsetCount)
+ .setCondition(this.conditionExpression)
+ .setEnforceFLS(this.enforceFLS);
+
+ Map subqueries = this.subselectQueryMap;
+ if(subqueries != null) {
+ Map clonedSubqueries = new Map();
+ for(Schema.ChildRelationship key : subqueries.keySet()) {
+ clonedSubqueries.put(key, subqueries.get(key).deepClone());
+ }
+ clone.subselectQueryMap = clonedSubqueries;
+ }
+
+ clone.relationship = this.relationship;
+ clone.order = this.order.clone();
+ clone.fields = this.fields.clone();
+
+ return clone;
+ }
+
+ public class Ordering{
+ private SortOrder direction;
+ private boolean nullsLast;
+ private String field;
+
+ public Ordering(String sobjType, String fieldName, SortOrder direction){
+ this(
+ fflib_SObjectDescribe.getDescribe(sobjType).getField(fieldName),
+ direction
+ );
+ }
+ /**
+ * Construct a new ordering instance for use with {@link fflib_QueryFactory#addOrdering}
+ * Once constructed it's properties may not be modified.
+ **/
+ public Ordering(Schema.SObjectField field, SortOrder direction){
+ this(fflib_QueryFactory.getFieldTokenPath(field), direction, false); //SOQL docs state NULLS FIRST is default behavior
+ }
+ public Ordering(Schema.SObjectField field, SortOrder direction, Boolean nullsLast){
+ this(fflib_QueryFactory.getFieldTokenPath(field), direction, nullsLast);
+ }
+ @TestVisible
+ private Ordering(String field, SortOrder direction){
+ this(field, direction, false);
+ }
+ @TestVisible
+ private Ordering(String field, SortOrder direction, Boolean nullsLast){
+ this.direction = direction;
+ this.field = field;
+ this.nullsLast = nullsLast;
+ }
+ public String getField(){
+ return this.field;
+ }
+ public SortOrder getDirection(){
+ return direction;
+ }
+ public String toSOQL(){
+ return field + ' ' + (direction == SortOrder.ASCENDING ? 'ASC' : 'DESC') + (nullsLast ? ' NULLS LAST ' : ' NULLS FIRST ');
+ }
+ }
+
+
+
+ public class InvalidFieldException extends Exception{
+ private String fieldName;
+ private Schema.SObjectType objectType;
+ public InvalidFieldException(String fieldName, Schema.SObjectType objectType){
+ this.objectType = objectType;
+ this.fieldName = fieldName;
+ this.setMessage( 'Invalid field \''+fieldName+'\' for object \''+objectType+'\'' );
+ }
+ }
+
+ public class InvalidFieldSetException extends Exception{}
+ public class NonReferenceFieldException extends Exception{}
+ public class InvalidSubqueryRelationshipException extends Exception{}
+}
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_QueryFactory.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_QueryFactory.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_QueryFactory.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDescribe.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDescribe.cls
new file mode 100644
index 00000000000..ac1575a5e57
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDescribe.cls
@@ -0,0 +1,357 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+/**
+ * fflib_SObjectDescribe is a semi-intelligent wrapper for standard apex Schema methods.
+ * It provides an internal caching layer, to avoid hitting describe limits from repeated use,
+ * as well as wrapper classes and methods to make common tasks like working with relationship field name oddities
+ * as well namespace handling.
+ *
+ * Of particular note for use in contexts that may be released as managed packages are the #getFields and get #getGlobalDescribe methods
+ * These return special immutable wrapper objects that automatically imply the current namespace (detected as the one this class is contained in)
+ * and allow an older API style of omitting the namespace when working with fields or global describe maps.
+ * This allows both upgrading old code to APIv29 by making use of these as a nearly drop in replacement, as well as keeping
+ * namespace detection logic encapsulated.
+**/
+public class fflib_SObjectDescribe {
+ //internal implementation details
+ private Schema.SObjectType token;
+ private Schema.SObjectField nameField;
+ private Schema.DescribeSObjectResult describe { //lazy load - keep this lightweight until we need more data
+ get{
+ if(describe == null)
+ describe = token.getDescribe();
+ return describe;
+ }
+ set;
+ }
+ private Map fields {
+ get{
+ if(fields == null)
+ fields = describe.fields.getMap();
+ return fields;
+ }
+ set;
+ }
+ private Map fieldSets {
+ get{
+ if(fieldSets == null)
+ fieldSets = describe.fieldSets.getMap();
+ return fieldSets;
+ }
+ set;
+ }
+ private FieldsMap wrappedFields {
+ get{
+ if(wrappedFields == null){
+ wrappedFields = new FieldsMap(this.fields);
+ }
+ return wrappedFields;
+ }
+ set;
+ }
+
+ private fflib_SObjectDescribe(Schema.SObjectType token){
+ if(token == null)
+ throw new InvalidDescribeException('Invalid SObject type: null');
+ this.token = token;
+ instanceCache.put( String.valueOf(token).toLowerCase() , this);
+ }
+
+ //public instace methods
+ /**
+ * Returns the Schema.SObjectType this fflib_SObjectDescribe instance is based on.
+ **/
+ public Schema.SObjectType getSObjectType(){
+ return token;
+ }
+ /**
+ * This method is a convenient shorthand for calling getField(name, true)
+ **/
+ public Schema.SObjectField getField(String name){
+ return this.getField(name, true);
+ }
+ /**
+ * This method provides a simplified shorthand for calling #getFields and getting the provided field.
+ * Additionally it handles finding the correct SObjectField for relationship notation,
+ * e.g. getting the Account field on Contact would fail without being referenced as AccountId - both work here.
+ **/
+ public Schema.SObjectField getField(String fieldName, Boolean implyNamespace){
+ Schema.SObjectField result = wrappedFields.get(
+ (fieldName.endsWithIgnoreCase('__r') ? //resolve custom field cross-object (__r) syntax
+ (fieldName.removeEndIgnoreCase('__r')+'__c') :
+ fieldName),
+ implyNamespace
+ );
+ if(result == null){
+ result = wrappedFields.get(fieldName+'Id', implyNamespace); //in case it's a standard lookup in cross-object format
+ }
+ return result;
+ }
+
+ /**
+ * Returns the field where isNameField() is true (if any); otherwise returns null
+ **/
+ public Schema.SObjectField getNameField()
+ {
+ if(nameField == null) {
+ for(Schema.SObjectField field : wrappedFields.values()) {
+ if(field.getDescribe().isNameField()) {
+ nameField = field;
+ break;
+ }
+ }
+ }
+ return nameField;
+ }
+
+ /**
+ * Returns the raw Schema.DescribeSObjectResult an fflib_SObjectDescribe instance wraps.
+ **/
+ public Schema.DescribeSObjectResult getDescribe(){
+ return describe;
+ }
+ /**
+ * This method returns the raw data and provides no namespace handling.
+ * Due to this, __use of this method is discouraged__ in favor of getFields().
+ **/
+ public Map getFieldsMap(){
+ return fields;
+ }
+ public FieldsMap getFields(){
+ return wrappedFields;
+ }
+ public Map getFieldSetsMap(){
+ return fieldSets;
+ }
+
+
+
+ private static Map rawGlobalDescribe {
+ get{
+ if(rawGlobalDescribe == null)
+ rawGlobalDescribe = Schema.getGlobalDescribe();
+ return rawGlobalDescribe;
+ }
+ set;
+ }
+ private static GlobalDescribeMap wrappedGlobalDescribe{
+ get{
+ if(wrappedGlobalDescribe == null){
+ wrappedGlobalDescribe = new GlobalDescribeMap(rawGlobalDescribe);
+ }
+ return wrappedGlobalDescribe;
+ }
+ set;
+ }
+ /**
+ * This is used to cache fflib_SObjectDescribe instances as they're constructed
+ * to prevent repeatedly re-constructing the same type.
+ * These instances are not guaranteed to be, but typically will be, unique per sObject type due to the presence of flushCache.
+ **/
+ private static Map instanceCache {get{
+ if(instanceCache == null)
+ instanceCache = new Map();
+ return instanceCache;
+ }
+ set;
+ }
+ public static fflib_SObjectDescribe getDescribe(String sObjectName){
+ if(String.isBlank(sObjectName))
+ return null;
+ fflib_SObjectDescribe result = instanceCache.get(sObjectName.toLowerCase());
+ if(result == null){
+ Schema.SObjectType token = wrappedGlobalDescribe.get(sObjectName.toLowerCase());
+ if(token == null)
+ result = null;
+ else
+ result = new fflib_SObjectDescribe(token);
+ }
+ return result;
+ }
+ public static fflib_SObjectDescribe getDescribe(Schema.SObjectType token){
+ if(token == null)
+ return null;
+ fflib_SObjectDescribe result = instanceCache.get(String.valueOf(token).toLowerCase());
+ if(result == null)
+ result = new fflib_SObjectDescribe(token);
+ return result;
+ }
+ public static fflib_SObjectDescribe getDescribe(Schema.DescribeSObjectResult nativeDescribe){
+ if(nativeDescribe == null)
+ return null;
+ fflib_SObjectDescribe result = instanceCache.get(nativeDescribe.getName().toLowerCase());
+ if(result == null)
+ result = new fflib_SObjectDescribe(nativeDescribe.getSObjectType());
+ return result;
+ }
+ public static fflib_SObjectDescribe getDescribe(SObject instance){
+ if(instance == null)
+ return null;
+ return getDescribe(instance.getSObjectType());
+ }
+
+ //returns the same results as the native method, just with caching built in to avoid limits
+ public static Map getRawGlobalDescribe(){
+ return rawGlobalDescribe;
+ }
+ public static GlobalDescribeMap getGlobalDescribe(){
+ return wrappedGlobalDescribe;
+ }
+ //Useful when working in heap space constrained environments.
+ //Existing references to SObjectDescribe instances will continue to work.
+ public static void flushCache(){
+ rawGlobalDescribe = null;
+ instanceCache = null;
+ }
+
+
+ /**
+ * This class handles emulating a Map's non-mutating instance methods and helps navigate the complex topic of
+ * handling implicit namespace behavior like pre-APIv29 did, while also allowing fully qualified references.
+ * Note that this requires the API version of fflib_SObjectDescribe to be 29 or higher to function properly.
+ *
+ * Due to the lack of language support for covariant return types subclasses are responsible for implementing the get methods.
+ * A minimal implementation of these would be a cast and returning getObject's result.
+ **/
+ private abstract class NamespacedAttributeMap{
+ @TestVisible
+ protected String currentNamespace;
+ protected Map values;
+
+ protected NamespacedAttributeMap(Map values){
+ //namespace detection courtesy http://salesforce.stackexchange.com/a/28977/60
+ currentNamespace = fflib_SObjectDescribe.class.getName().substringBefore('fflib_SObjectDescribe').removeEnd('.').toLowerCase();
+ this.values = values;
+ }
+ //A no-args constructor to allow subclasses with different constructor signatures
+ protected NamespacedAttributeMap(){
+ this(new Map());
+ }
+ /**
+ * A convenient shortcut for invoking #getObject(name, true)
+ **/
+ protected virtual Object getObject(String name){
+ return this.getObject(name, true);
+ }
+ /**
+ *
+ **/
+ protected virtual Object getObject(String name, Boolean implyNamespace){
+ if(name == null) //short-circuit lookup logic since null can't possibly be a valid field name, and it saves us null checking
+ return null;
+ String preferredValue = ((implyNamespace ? currentNamespace+'__' : '') + name).toLowerCase();
+ if(values.containsKey(preferredValue)){
+ return values.get(preferredValue);
+ }else if(implyNamespace){
+ return values.get(name.toLowerCase());
+ }else{
+ return null;
+ }
+ }
+ public virtual Boolean containsKey(String name){
+ return this.containsKey(name, true);
+ }
+ public virtual Boolean containsKey(String name, Boolean implyNamespace){
+ if(name == null) //short-circuit lookup logic since null can't possibly be a valid field name, and it saves us null checking
+ return null;
+ String preferredValue = ((implyNamespace ? currentNamespace+'__' : '') + name).toLowerCase();
+ return (
+ values.containsKey(preferredValue) ||
+ implyNamespace && values.containsKey(name.toLowerCase())
+ );
+ }
+ public virtual Integer size(){
+ return values.size();
+ }
+ /**
+ * Returns the key set of the map.
+ * Note: unlike other NamespacedAttributeMap methods keySet defaults implyNamespace to false if not specified.
+ **/
+ public virtual Set keySet(){
+ return this.keySet(false);
+ }
+ public virtual Set keySet(Boolean implyNamespace){
+ if(implyNamespace){
+ Set result = new Set();
+ for(String key:values.keySet()){
+ result.add(
+ key.removeStartIgnoreCase(currentNamespace+'__')
+ );
+ }
+ return result;
+ }else{
+ return values.keySet();
+ }
+ }
+ }
+
+ /**
+ * A subclass of NamespacedAttributeMap for handling the data returned by #Schema.DescribeSObjectResult.fields.getMap
+ **/
+ public class FieldsMap extends NamespacedAttributeMap{
+
+ @TestVisible
+ private FieldsMap(Map values){
+ super(values);
+ }
+
+ public Schema.SObjectField get(String name){
+ return this.get(name, true);
+ }
+ public Schema.SObjectField get(String name, Boolean implyNamespace){
+ return (Schema.SObjectField) this.getObject(name, implyNamespace);
+ }
+ public List values(){
+ return (List) values.values();
+ }
+
+ }
+ /**
+ * A subclass of NamespacedAttributeMap for handling the data returned by #Schema.getGlobalDescribe
+ **/
+ public class GlobalDescribeMap extends NamespacedAttributeMap{
+ @TestVisible
+ private GlobalDescribeMap(Map values){
+ super(values);
+ }
+
+ public Schema.SObjectType get(String name){
+ return this.get(name, true);
+ }
+ public Schema.SObjectType get(String name, Boolean implyNamespace){
+ return (Schema.SObjectType) this.getObject(name, implyNamespace);
+ }
+ public List values(){
+ return (List) values.values();
+ }
+ }
+
+
+ public abstract class DescribeException extends Exception{}
+ public class DuplicateDescribeException extends DescribeException{} //Test coverage for this requires APIv28's @testVisible annotation to force exception cases.
+ public class InvalidDescribeException extends DescribeException{}
+}
\ No newline at end of file
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDescribe.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDescribe.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDescribe.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDomain.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDomain.cls
new file mode 100644
index 00000000000..80a1a72f504
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDomain.cls
@@ -0,0 +1,1125 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+/**
+ * Base class aiding in the implementation of a Domain Model around SObject collections
+ *
+ * Domain (software engineering). “a set of common requirements, terminology, and functionality
+ * for any software program constructed to solve a problem in that field”,
+ * http://en.wikipedia.org/wiki/Domain_(software_engineering)
+ *
+ * Domain Model, “An object model of the domain that incorporates both behavior and data.”,
+ * “At its worst business logic can be very complex. Rules and logic describe many different "
+ * "cases and slants of behavior, and it's this complexity that objects were designed to work with...”
+ * Martin Fowler, EAA Patterns
+ * http://martinfowler.com/eaaCatalog/domainModel.html
+ *
+ **/
+public virtual with sharing class fflib_SObjectDomain
+ extends fflib_SObjects2
+ implements fflib_ISObjectDomain
+{
+ /**
+ * Provides access to the data represented by this domain class
+ **/
+ public List Records {
+ get
+ {
+ return getRecords();
+ }
+ }
+
+
+ /**
+ * Provides access to Trigger.oldMap and allowing it to be mocked in unit-tests
+ **/
+ @TestVisible
+ protected Map ExistingRecords
+ {
+ get
+ {
+ if (ExistingRecords == null)
+ {
+ if (System.Test.isRunningTest() & Test.Database.hasRecords())
+ {
+ // If in test context and records are in the mock database use those instead of Trigger.oldMap
+ ExistingRecords = Test.Database.oldRecords;
+ }
+ else
+ {
+ ExistingRecords = Trigger.oldMap;
+ }
+ }
+ return ExistingRecords;
+
+ }
+ private set;
+ }
+
+ /**
+ * Exposes the configuration for this domain class instance
+ **/
+ public Configuration Configuration {get; private set;}
+
+ /**
+ * DEPRECATED, This property has been moved to fflib_SObjects
+ **/
+ public static fflib_SObjectDomain.ErrorFactory Errors {get; private set;}
+
+ /**
+ * Useful during unit testing to access mock support for database inserts and updates (testing without DML)
+ **/
+ public static TestFactory Test {get; private set;}
+
+ /**
+ * Retains instances of domain classes implementing trigger stateful
+ **/
+ private static Map> TriggerStateByClass;
+
+ /**
+ * Retains the trigger tracking configuration used for each domain
+ **/
+ private static Map TriggerEventByClass;
+
+ static
+ {
+ Errors = new fflib_SObjectDomain.ErrorFactory();
+
+ Test = new TestFactory();
+
+ TriggerStateByClass = new Map>();
+
+ TriggerEventByClass = new Map();
+ }
+
+ /**
+ * Constructs the domain class with the data on which to apply the behaviour implemented within
+ *
+ * @param sObjectList A concrete list (e.g. List vs List) of records
+
+ **/
+ public fflib_SObjectDomain(List sObjectList)
+ {
+ this(sObjectList, sObjectList.getSObjectType());
+ }
+
+ /**
+ * Constructs the domain class with the data and type on which to apply the behaviour implemented within
+ *
+ * @param sObjectList A list (e.g. List, List, etc.) of records
+ * @param sObjectType The Schema.SObjectType of the records contained in the list
+ *
+ * @remark Will support List but all records in the list will be assumed to be of
+ * the type specified in sObjectType
+ **/
+ public fflib_SObjectDomain(List sObjectList, SObjectType sObjectType)
+ {
+ // Ensure the domain class has its own copy of the data
+ super(sObjectList, sObjectType);
+
+ // Configure the Domain object instance
+ Configuration = new Configuration();
+ }
+
+ /**
+ * Override this to apply defaults to the records, this is called by the handleBeforeInsert method
+ **/
+ public virtual void onApplyDefaults() { }
+
+ /**
+ * Override this to apply general validation to be performed during insert or update, called by the handleAfterInsert and handleAfterUpdate methods
+ **/
+ public virtual void onValidate() { }
+
+ /**
+ * Override this to apply validation to be performed during insert, called by the handleAfterUpdate method
+ **/
+ public virtual void onValidate(Map existingRecords) { }
+
+ /**
+ * Override this to perform processing during the before insert phase, this is called by the handleBeforeInsert method
+ **/
+ public virtual void onBeforeInsert() { }
+
+ /**
+ * Override this to perform processing during the before update phase, this is called by the handleBeforeUpdate method
+ **/
+ public virtual void onBeforeUpdate(Map existingRecords) { }
+
+ /**
+ * Override this to perform processing during the before delete phase, this is called by the handleBeforeDelete method
+ **/
+ public virtual void onBeforeDelete() { }
+
+ /**
+ * Override this to perform processing during the after insert phase, this is called by the handleAfterInsert method
+ **/
+ public virtual void onAfterInsert() { }
+
+ /**
+ * Override this to perform processing during the after update phase, this is called by the handleAfterUpdate method
+ **/
+ public virtual void onAfterUpdate(Map existingRecords) { }
+
+ /**
+ * Override this to perform processing during the after delete phase, this is called by the handleAfterDelete method
+ **/
+ public virtual void onAfterDelete() { }
+
+ /**
+ * Override this to perform processing during the after undelete phase, this is called by the handleAfterDelete method
+ **/
+ public virtual void onAfterUndelete() { }
+
+ /**
+ * Base handler for the Apex Trigger event Before Insert, calls the onApplyDefaults method, followed by onBeforeInsert
+ **/
+ public virtual void handleBeforeInsert()
+ {
+ onApplyDefaults();
+ onBeforeInsert();
+ }
+
+ /**
+ * Base handler for the Apex Trigger event Before Update, calls the onBeforeUpdate method
+ **/
+ public virtual void handleBeforeUpdate(Map existingRecords)
+ {
+ onBeforeUpdate(existingRecords);
+ }
+
+ /**
+ * Base handler for the Apex Trigger event Before Delete, calls the onBeforeDelete method
+ **/
+ public virtual void handleBeforeDelete()
+ {
+ onBeforeDelete();
+ }
+
+ /**
+ * Base handler for the Apex Trigger event After Insert, checks object security and calls the onValidate and onAfterInsert methods
+ *
+ * @throws DomainException if the current user context is not able to create records
+ **/
+ public virtual void handleAfterInsert()
+ {
+ if(Configuration.EnforcingTriggerCRUDSecurity && !SObjectDescribe.isCreateable())
+ throw new DomainException('Permission to create an ' + SObjectDescribe.getName() + ' denied.');
+
+ onValidate();
+ onAfterInsert();
+ }
+
+ /**
+ * Base handler for the Apex Trigger event After Update, checks object security and calls the onValidate, onValidate(Map) and onAfterUpdate methods
+ *
+ * @throws DomainException if the current user context is not able to update records
+ **/
+ public virtual void handleAfterUpdate(Map existingRecords)
+ {
+ if(Configuration.EnforcingTriggerCRUDSecurity && !SObjectDescribe.isUpdateable())
+ throw new DomainException('Permission to update an ' + SObjectDescribe.getName() + ' denied.');
+
+ if(Configuration.OldOnUpdateValidateBehaviour)
+ onValidate();
+ onValidate(existingRecords);
+ onAfterUpdate(existingRecords);
+ }
+
+ /**
+ * Base handler for the Apex Trigger event After Delete, checks object security and calls the onAfterDelete method
+ *
+ * @throws DomainException if the current user context is not able to delete records
+ **/
+ public virtual void handleAfterDelete()
+ {
+ if(Configuration.EnforcingTriggerCRUDSecurity && !SObjectDescribe.isDeletable())
+ throw new DomainException('Permission to delete an ' + SObjectDescribe.getName() + ' denied.');
+
+ onAfterDelete();
+ }
+
+ /**
+ * Base handler for the Apex Trigger event After Undelete, checks object security and calls the onAfterUndelete method
+ *
+ * @throws DomainException if the current user context is not able to delete records
+ **/
+ public virtual void handleAfterUndelete()
+ {
+ if(Configuration.EnforcingTriggerCRUDSecurity && !SObjectDescribe.isUndeletable())
+ throw new DomainException('Permission to undelete an ' + SObjectDescribe.getName() + ' denied.');
+
+ onAfterUndelete();
+ }
+
+ /**
+ * Returns the SObjectType this Domain class represents
+ **/
+ public SObjectType sObjectType()
+ {
+ return getSObjectType();
+ }
+
+ /**
+ * Detects whether any values in context records have changed for given fields as strings
+ * Returns list of SObject records that have changes in the specified fields
+ **/
+ public List getChangedRecords(Set fieldNames)
+ {
+ List changedRecords = new List();
+ for (SObject newRecord : Records)
+ {
+ Id recordId = (Id) newRecord.get('Id');
+ if (this.ExistingRecords == null || !this.ExistingRecords.containsKey(recordId))
+ {
+ continue;
+ }
+
+ SObject oldRecord = this.ExistingRecords.get(recordId);
+ for (String fieldName : fieldNames)
+ {
+ if (oldRecord.get(fieldName) != newRecord.get(fieldName))
+ {
+ changedRecords.add(newRecord);
+ break; // prevents the records from being added multiple times
+ }
+ }
+ }
+ return changedRecords;
+ }
+
+ /**
+ * Detects whether any values in context records have changed for given fields as tokens
+ * Returns list of SObject records that have changes in the specified fields
+ **/
+ public List getChangedRecords(Set fieldTokens)
+ {
+ List changedRecords = new List();
+ for (SObject newRecord : Records)
+ {
+ Id recordId = (Id) newRecord.get('Id');
+ if (this.ExistingRecords == null || !this.ExistingRecords.containsKey(recordId))
+ {
+ continue;
+ }
+ SObject oldRecord = this.ExistingRecords.get(recordId);
+ for (Schema.SObjectField fieldToken : fieldTokens)
+ {
+ if (oldRecord.get(fieldToken) != newRecord.get(fieldToken))
+ {
+ changedRecords.add(newRecord);
+ break; // prevents the records from being added multiple times
+ }
+ }
+ }
+ return changedRecords;
+ }
+
+ /**
+ * Interface used to aid the triggerHandler in constructing instances of Domain classes
+ **/
+ public interface IConstructable
+ {
+ fflib_SObjectDomain construct(List sObjectList);
+ }
+
+ /**
+ * Interface used to aid the triggerHandler in constructing instances of Domain classes
+ **/
+ public interface IConstructable2 extends IConstructable
+ {
+ fflib_SObjectDomain construct(List sObjectList, SObjectType sObjectType);
+ }
+
+ /**
+ * For Domain classes implementing the ITriggerStateful interface returns the instance
+ * of the domain class being shared between trigger invocations, returns null if
+ * the Domain class trigger has not yet fired or the given domain class does not implement
+ * the ITriggerStateful interface. Note this method is sensitive to recursion, meaning
+ * it will return the applicable domain instance for the level of recursion
+ **/
+ public static fflib_SObjectDomain getTriggerInstance(Type domainClass)
+ {
+ List domains = TriggerStateByClass.get(domainClass);
+ if(domains==null || domains.size()==0)
+ return null;
+ return domains[domains.size()-1];
+ }
+
+ /**
+ * Method constructs the given Domain class with the current Trigger context
+ * before calling the applicable override methods such as beforeInsert, beforeUpdate etc.
+ **/
+ public static void triggerHandler(Type domainClass)
+ {
+ // Process the trigger context
+ if(System.Test.isRunningTest() & Test.Database.hasRecords())
+ {
+ // If in test context and records in the mock database delegate initially to the mock database trigger handler
+ Test.Database.testTriggerHandler(domainClass);
+ }
+ else
+ {
+ // Process the runtime Apex Trigger context
+ triggerHandler(domainClass,
+ Trigger.isBefore,
+ Trigger.isAfter,
+ Trigger.isInsert,
+ Trigger.isUpdate,
+ Trigger.isDelete,
+ Trigger.isUnDelete,
+ Trigger.new,
+ Trigger.oldMap);
+ }
+ }
+
+ /**
+ * Calls the applicable override methods such as beforeInsert, beforeUpdate etc. based on a Trigger context
+ **/
+ private static void triggerHandler(Type domainClass, Boolean isBefore, Boolean isAfter, Boolean isInsert, Boolean isUpdate, Boolean isDelete, Boolean isUndelete, List newRecords, Map oldRecordsMap)
+ {
+ // After phase of trigger will reuse prior instance of domain class if ITriggerStateful implemented
+ fflib_SObjectDomain domainObject = isBefore ? null : popTriggerInstance(domainClass, isDelete ? oldRecordsMap.values() : newRecords);
+ if(domainObject==null)
+ {
+ // Construct the domain class constructor class
+ String domainClassName = domainClass.getName();
+ Type constructableClass = domainClassName.endsWith('Constructor') ? Type.forName(domainClassName) : Type.forName(domainClassName+'.Constructor');
+ IConstructable domainConstructor = (IConstructable) constructableClass.newInstance();
+
+ // Construct the domain class with the approprite record set
+ if(isInsert) domainObject = domainConstructor.construct(newRecords);
+ else if(isUpdate) domainObject = domainConstructor.construct(newRecords);
+ else if(isDelete) domainObject = domainConstructor.construct(oldRecordsMap.values());
+ else if(isUndelete) domainObject = domainConstructor.construct(newRecords);
+
+ // Should this instance be reused on the next trigger invocation?
+ if(domainObject.Configuration.TriggerStateEnabled)
+ // Push this instance onto the stack to be popped during the after phase
+ pushTriggerInstance(domainClass, domainObject);
+ }
+
+ // has this event been disabled?
+ if(!getTriggerEvent(domainClass).isEnabled(isBefore, isAfter, isInsert, isUpdate, isDelete, isUndelete))
+ {
+ return;
+ }
+
+ // Invoke the applicable handler
+ if(isBefore)
+ {
+ if(isInsert) domainObject.handleBeforeInsert();
+ else if(isUpdate) domainObject.handleBeforeUpdate(oldRecordsMap);
+ else if(isDelete) domainObject.handleBeforeDelete();
+ }
+ else
+ {
+ if(isInsert) domainObject.handleAfterInsert();
+ else if(isUpdate) domainObject.handleAfterUpdate(oldRecordsMap);
+ else if(isDelete) domainObject.handleAfterDelete();
+ else if(isUndelete) domainObject.handleAfterUndelete();
+ }
+ }
+
+ /**
+ * Pushes to the stack of domain classes per type a domain object instance
+ **/
+ private static void pushTriggerInstance(Type domainClass, fflib_SObjectDomain domain)
+ {
+ List domains = TriggerStateByClass.get(domainClass);
+ if(domains==null)
+ TriggerStateByClass.put(domainClass, domains = new List());
+ domains.add(domain);
+ }
+
+ /**
+ * Pops from the stack of domain classes per type a domain object instance and updates the record set
+ **/
+ private static fflib_SObjectDomain popTriggerInstance(Type domainClass, List records)
+ {
+ List domains = TriggerStateByClass.get(domainClass);
+ if(domains==null || domains.size()==0)
+ return null;
+ fflib_SObjectDomain domain = domains.remove(domains.size()-1);
+
+ domain.setObjects(records);
+ return domain;
+ }
+
+ public static TriggerEvent getTriggerEvent(Type domainClass)
+ {
+ if(!TriggerEventByClass.containsKey(domainClass))
+ {
+ TriggerEventByClass.put(domainClass, new TriggerEvent());
+ }
+
+ return TriggerEventByClass.get(domainClass);
+ }
+
+ public class TriggerEvent
+ {
+ public boolean BeforeInsertEnabled {get; private set;}
+ public boolean BeforeUpdateEnabled {get; private set;}
+ public boolean BeforeDeleteEnabled {get; private set;}
+
+ public boolean AfterInsertEnabled {get; private set;}
+ public boolean AfterUpdateEnabled {get; private set;}
+ public boolean AfterDeleteEnabled {get; private set;}
+ public boolean AfterUndeleteEnabled {get; private set;}
+
+ public TriggerEvent()
+ {
+ this.enableAll();
+ }
+
+ // befores
+ public TriggerEvent enableBeforeInsert() {BeforeInsertEnabled = true; return this;}
+ public TriggerEvent enableBeforeUpdate() {BeforeUpdateEnabled = true; return this;}
+ public TriggerEvent enableBeforeDelete() {BeforeDeleteEnabled = true; return this;}
+
+ public TriggerEvent disableBeforeInsert() {BeforeInsertEnabled = false; return this;}
+ public TriggerEvent disableBeforeUpdate() {BeforeUpdateEnabled = false; return this;}
+ public TriggerEvent disableBeforeDelete() {BeforeDeleteEnabled = false; return this;}
+
+ // afters
+ public TriggerEvent enableAfterInsert() {AfterInsertEnabled = true; return this;}
+ public TriggerEvent enableAfterUpdate() {AfterUpdateEnabled = true; return this;}
+ public TriggerEvent enableAfterDelete() {AfterDeleteEnabled = true; return this;}
+ public TriggerEvent enableAfterUndelete() {AfterUndeleteEnabled = true; return this;}
+
+
+ public TriggerEvent disableAfterInsert() {AfterInsertEnabled = false; return this;}
+ public TriggerEvent disableAfterUpdate() {AfterUpdateEnabled = false; return this;}
+ public TriggerEvent disableAfterDelete() {AfterDeleteEnabled = false; return this;}
+ public TriggerEvent disableAfterUndelete(){AfterUndeleteEnabled = false; return this;}
+
+ public TriggerEvent enableAll()
+ {
+ return this.enableAllBefore().enableAllAfter();
+ }
+
+ public TriggerEvent disableAll()
+ {
+ return this.disableAllBefore().disableAllAfter();
+ }
+
+ public TriggerEvent enableAllBefore()
+ {
+ return this.enableBeforeInsert().enableBeforeUpdate().enableBeforeDelete();
+ }
+
+ public TriggerEvent disableAllBefore()
+ {
+ return this.disableBeforeInsert().disableBeforeUpdate().disableBeforeDelete();
+ }
+
+ public TriggerEvent enableAllAfter()
+ {
+ return this.enableAfterInsert().enableAfterUpdate().enableAfterDelete().enableAfterUndelete();
+ }
+
+ public TriggerEvent disableAllAfter()
+ {
+ return this.disableAfterInsert().disableAfterUpdate().disableAfterDelete().disableAfterUndelete();
+ }
+
+ public boolean isEnabled(Boolean isBefore, Boolean isAfter, Boolean isInsert, Boolean isUpdate, Boolean isDelete, Boolean isUndelete)
+ {
+ if(isBefore)
+ {
+ if(isInsert) return BeforeInsertEnabled;
+ else if(isUpdate) return BeforeUpdateEnabled;
+ else if(isDelete) return BeforeDeleteEnabled;
+ }
+ else if(isAfter)
+ {
+ if(isInsert) return AfterInsertEnabled;
+ else if(isUpdate) return AfterUpdateEnabled;
+ else if(isDelete) return AfterDeleteEnabled;
+ else if(isUndelete) return AfterUndeleteEnabled;
+ }
+ return true; // shouldnt ever get here!
+ }
+ }
+
+ /**
+ * Fluent style Configuration system for Domain class creation
+ **/
+ public class Configuration
+ {
+ /**
+ * Backwards compatibility mode for handleAfterUpdate routing to onValidate()
+ **/
+ public Boolean OldOnUpdateValidateBehaviour {get; private set;}
+ /**
+ * True if the base class is checking the users CRUD requirements before invoking trigger methods
+ **/
+ public Boolean EnforcingTriggerCRUDSecurity {get; private set;}
+
+ /**
+ * Enables reuse of the same Domain instance between before and after trigger phases (subject to recursive scenarios)
+ **/
+ public Boolean TriggerStateEnabled {get; private set;}
+
+ /**
+ * Default configuration
+ **/
+ public Configuration()
+ {
+ EnforcingTriggerCRUDSecurity = true; // Default is true for backwards compatability
+ TriggerStateEnabled = false;
+ OldOnUpdateValidateBehaviour = false; // Breaking change, but felt to better practice
+ }
+
+ /**
+ * See associated property
+ **/
+ public Configuration enableTriggerState()
+ {
+ TriggerStateEnabled = true;
+ return this;
+ }
+
+ /**
+ * See associated property
+ **/
+ public Configuration disableTriggerState()
+ {
+ TriggerStateEnabled = false;
+ return this;
+ }
+
+ /**
+ * See associated property
+ **/
+ public Configuration enforceTriggerCRUDSecurity()
+ {
+ EnforcingTriggerCRUDSecurity = true;
+ return this;
+ }
+
+ /**
+ * See associated property
+ **/
+ public Configuration disableTriggerCRUDSecurity()
+ {
+ EnforcingTriggerCRUDSecurity = false;
+ return this;
+ }
+
+ /**
+ * See associated property
+ **/
+ public Configuration enableOldOnUpdateValidateBehaviour()
+ {
+ OldOnUpdateValidateBehaviour = true;
+ return this;
+ }
+
+ /**
+ * See associated property
+ **/
+ public Configuration disableOldOnUpdateValidateBehaviour()
+ {
+ OldOnUpdateValidateBehaviour = false;
+ return this;
+ }
+ }
+
+ /**
+ * General exception class for the domain layer
+ **/
+ public class DomainException extends Exception
+ {
+ }
+
+ /**
+ * Ensures logging of errors in the Domain context for later assertions in tests
+ **/
+ public override String error(String message, SObject record)
+ {
+ return fflib_SObjectDomain.Errors.error(this, message, record);
+ }
+
+ /**
+ * Ensures logging of errors in the Domain context for later assertions in tests
+ **/
+ public override String error(String message, SObject record, SObjectField field)
+ {
+ return fflib_SObjectDomain.Errors.error(this, message, record, field);
+ }
+
+ /**
+ * DEPRECATED, This class has been moved to fflib_SObjects
+ **/
+ public class ErrorFactory
+ {
+ private List errorList = new List();
+
+ private ErrorFactory()
+ {
+
+ }
+
+ public String error(String message, SObject record)
+ {
+ return error(null, message, record);
+ }
+
+ private String error(fflib_SObjectDomain domain, String message, SObject record)
+ {
+ ObjectError objectError = new ObjectError();
+ objectError.domain = domain;
+ objectError.message = message;
+ objectError.record = record;
+ errorList.add(objectError);
+ return message;
+ }
+
+ public String error(String message, SObject record, SObjectField field)
+ {
+ return error(null, message, record, field);
+ }
+
+ private String error(fflib_SObjectDomain domain, String message, SObject record, SObjectField field)
+ {
+ FieldError fieldError = new FieldError();
+ fieldError.domain = domain;
+ fieldError.message = message;
+ fieldError.record = record;
+ fieldError.field = field;
+ errorList.add(fieldError);
+ return message;
+ }
+
+ public List getAll()
+ {
+ return errorList.clone();
+ }
+
+ public void clearAll()
+ {
+ errorList.clear();
+ }
+ }
+
+ /**
+ * DEPRECATED, This class has been moved to fflib_SObjects
+ **/
+ public virtual class FieldError extends ObjectError
+ {
+ public SObjectField field;
+
+ public FieldError()
+ {
+
+ }
+ }
+
+ /**
+ * DEPRECATED, This class has been moved to fflib_SObjects
+ **/
+ public virtual class ObjectError extends Error
+ {
+ public SObject record;
+
+ public ObjectError()
+ {
+
+ }
+ }
+
+ /**
+ * DEPRECATED, This class has been moved to fflib_SObjects
+ **/
+ public abstract class Error
+ {
+ public String message;
+ public fflib_SObjectDomain domain;
+ }
+
+ /**
+ * Provides test context mocking facilities to unit tests testing domain classes
+ **/
+ public class TestFactory
+ {
+ public MockDatabase Database = new MockDatabase();
+
+ private TestFactory()
+ {
+
+ }
+ }
+
+ /**
+ * Class used during Unit testing of Domain classes, can be used (not exclusively) to speed up test execution and focus testing
+ **/
+ public class MockDatabase
+ {
+ private Boolean isInsert = false;
+ private Boolean isUpdate = false;
+ private Boolean isDelete = false;
+ private Boolean isUndelete = false;
+ private List records = new List();
+ private Map oldRecords = new Map();
+
+ private MockDatabase()
+ {
+
+ }
+
+ private void testTriggerHandler(Type domainClass)
+ {
+ // Mock Before
+ triggerHandler(domainClass, true, false, isInsert, isUpdate, isDelete, isUndelete, records, oldRecords);
+
+ // Mock After
+ triggerHandler(domainClass, false, true, isInsert, isUpdate, isDelete, isUndelete, records, oldRecords);
+ }
+
+ public void onInsert(List records)
+ {
+ this.isInsert = true;
+ this.isUpdate = false;
+ this.isDelete = false;
+ this.isUndelete = false;
+ this.records = records;
+ }
+
+ public void onUpdate(List records, Map oldRecords)
+ {
+ this.isInsert = false;
+ this.isUpdate = true;
+ this.isDelete = false;
+ this.records = records;
+ this.isUndelete = false;
+ this.oldRecords = oldRecords;
+ }
+
+ public void onDelete(Map records)
+ {
+ this.isInsert = false;
+ this.isUpdate = false;
+ this.isDelete = true;
+ this.isUndelete = false;
+ this.oldRecords = records;
+ }
+
+ public void onUndelete(List records)
+ {
+ this.isInsert = false;
+ this.isUpdate = false;
+ this.isDelete = false;
+ this.isUndelete = true;
+ this.records = records;
+ }
+
+ public Boolean hasRecords()
+ {
+ return records!=null && records.size()>0 || oldRecords!=null && oldRecords.size()>0;
+ }
+ }
+
+ /**
+ * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes)
+ **/
+ public with sharing class TestSObjectDomain extends fflib_SObjectDomain
+ {
+ private String someState;
+
+ public TestSObjectDomain(List sObjectList)
+ {
+ // Domain classes are initialised with lists to enforce bulkification throughout
+ super(sObjectList);
+ }
+
+ public TestSObjectDomain(List sObjectList, SObjectType sObjectType)
+ {
+ // Domain classes are initialised with lists to enforce bulkification throughout
+ super(sObjectList, sObjectType);
+ }
+
+ public override void onApplyDefaults()
+ {
+ // Not required in production code
+ super.onApplyDefaults();
+
+ // Apply defaults to Testfflib_SObjectDomain
+ for(Opportunity opportunity : (List) Records)
+ {
+ opportunity.CloseDate = System.today().addDays(30);
+ }
+ }
+
+ public override void onValidate()
+ {
+ // Not required in production code
+ super.onValidate();
+
+ // Validate Testfflib_SObjectDomain
+ for(Opportunity opp : (List) Records)
+ {
+ if(opp.Type!=null && opp.Type.startsWith('Existing') && opp.AccountId == null)
+ {
+ opp.AccountId.addError( error('You must provide an Account for Opportunities for existing Customers.', opp, Opportunity.AccountId) );
+ }
+ }
+ }
+
+ public override void onValidate(Map existingRecords)
+ {
+ // Not required in production code
+ super.onValidate(existingRecords);
+
+ // Validate changes to Testfflib_SObjectDomain
+ for(Opportunity opp : (List) Records)
+ {
+ Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id);
+ if(opp.Type != existingOpp.Type)
+ {
+ opp.Type.addError( error('You cannot change the Opportunity type once it has been created.', opp, Opportunity.Type) );
+ }
+ }
+ }
+
+ public override void onBeforeDelete()
+ {
+ // Not required in production code
+ super.onBeforeDelete();
+
+ // Validate changes to Testfflib_SObjectDomain
+ for(Opportunity opp : (List) Records)
+ {
+ opp.addError( error('You cannot delete this Opportunity.', opp) );
+ }
+ }
+
+ public override void onAfterUndelete()
+ {
+ // Not required in production code
+ super.onAfterUndelete();
+ }
+
+ public override void onBeforeInsert()
+ {
+ // Assert this variable is null in the after insert (since this domain class is stateless)
+ someState = 'This should not survice the trigger after phase';
+ }
+
+ public override void onAfterInsert()
+ {
+ // This is a stateless domain class, so should not retain anything betweet before and after
+ System.assertEquals(null, someState);
+ }
+ }
+
+ /**
+ * Typically an inner class to the domain class, supported here for test purposes
+ **/
+ public class TestSObjectDomainConstructor implements fflib_SObjectDomain.IConstructable
+ {
+ public fflib_SObjectDomain construct(List sObjectList)
+ {
+ return new TestSObjectDomain(sObjectList);
+ }
+ }
+
+ /**
+ * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes)
+ **/
+ public with sharing class TestSObjectStatefulDomain
+ extends fflib_SObjectDomain
+ {
+ public String someState;
+
+ public TestSObjectStatefulDomain(List sObjectList)
+ {
+ super(sObjectList);
+
+ // Ensure this instance is re-used in the after trigger phase (subject to recursive scenarios)
+ Configuration.enableTriggerState();
+ }
+
+ public override void onBeforeInsert()
+ {
+ // This must always be null, as we do not reuse domain instances within recursive scenarios (different record sets)
+ System.assertEquals(null, someState);
+
+ // Process records
+ List newOpps = new List();
+ for(Opportunity opp : (List) Records)
+ {
+ // Set some state sensitive to the incoming records
+ someState = 'Error on Record ' + opp.Name;
+
+ // Create a new Opportunity record to trigger recursive code path?
+ if(opp.Name.equals('Test Recursive 1'))
+ newOpps.add(new Opportunity ( Name = 'Test Recursive 2', Type = 'Existing Account' ));
+ }
+
+ // If testing recursiving emulate an insert
+ if(newOpps.size()>0)
+ {
+ // This will force recursion and thus validate via the above assert results in a new domain instance
+ fflib_SObjectDomain.Test.Database.onInsert(newOpps);
+ fflib_SObjectDomain.triggerHandler(fflib_SObjectDomain.TestSObjectStatefulDomainConstructor.class);
+ }
+ }
+
+ public override void onAfterInsert()
+ {
+ // Use the state set in the before insert (since this is a stateful domain class)
+ if(someState!=null)
+ for(Opportunity opp : (List) Records)
+ opp.addError(error(someState, opp));
+ }
+ }
+
+ /**
+ * Typically an inner class to the domain class, supported here for test purposes
+ **/
+ public class TestSObjectStatefulDomainConstructor implements fflib_SObjectDomain.IConstructable
+ {
+ public fflib_SObjectDomain construct(List sObjectList)
+ {
+ return new TestSObjectStatefulDomain(sObjectList);
+ }
+ }
+
+ /**
+ * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes)
+ **/
+ public with sharing class TestSObjectOnValidateBehaviour
+ extends fflib_SObjectDomain
+ {
+ public TestSObjectOnValidateBehaviour(List sObjectList)
+ {
+ super(sObjectList);
+
+ // Enable old behaviour based on the test Opportunity name passed in
+ if(sObjectList[0].Name == 'Test Enable Old Behaviour')
+ Configuration.enableOldOnUpdateValidateBehaviour();
+ }
+
+ public override void onValidate()
+ {
+ // Throw exception to give the test somethign to assert on
+ throw new DomainException('onValidate called');
+ }
+ }
+
+ /**
+ * Typically an inner class to the domain class, supported here for test purposes
+ **/
+ public class TestSObjectOnValidateBehaviourConstructor implements fflib_SObjectDomain.IConstructable
+ {
+ public fflib_SObjectDomain construct(List sObjectList)
+ {
+ return new TestSObjectOnValidateBehaviour(sObjectList);
+ }
+ }
+
+ /**
+ * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes)
+ **/
+ public with sharing class TestSObjectChangedRecords
+ extends fflib_SObjectDomain
+ {
+ public TestSObjectChangedRecords(List sObjectList)
+ {
+ super(sObjectList);
+ }
+ }
+
+ /**
+ * Typically an inner class to the domain class, supported here for test purposes
+ **/
+ public class TestSObjectChangedRecordsConstructor implements fflib_SObjectDomain.IConstructable
+ {
+ public fflib_SObjectDomain construct(List sObjectList)
+ {
+ return new TestSObjectChangedRecords(sObjectList);
+ }
+ }
+
+ /**
+ * Test domain class (ideally this would be in the test class, however Type.newInstance does not see such classes)
+ **/
+ public with sharing class TestSObjectDisableBehaviour
+ extends fflib_SObjectDomain
+ {
+ public TestSObjectDisableBehaviour(List sObjectList)
+ {
+ super(sObjectList);
+ }
+
+ public override void onAfterInsert() {
+ // Throw exception to give the test somethign to assert on
+ throw new DomainException('onAfterInsert called');
+ }
+
+ public override void onBeforeInsert() {
+ // Throw exception to give the test somethign to assert on
+ throw new DomainException('onBeforeInsert called');
+ }
+
+ public override void onAfterUpdate(map existing) {
+ // Throw exception to give the test somethign to assert on
+ throw new DomainException('onAfterUpdate called');
+ }
+
+ public override void onBeforeUpdate(map existing) {
+ // Throw exception to give the test somethign to assert on
+ throw new DomainException('onBeforeUpdate called');
+ }
+
+ public override void onAfterDelete() {
+ // Throw exception to give the test somethign to assert on
+ throw new DomainException('onAfterDelete called');
+ }
+
+ public override void onBeforeDelete() {
+ // Throw exception to give the test somethign to assert on
+ throw new DomainException('onBeforeDelete called');
+ }
+
+ public override void onAfterUndelete() {
+ // Throw exception to give the test somethign to assert on
+ throw new DomainException('onAfterUndelete called');
+ }
+ }
+
+ /**
+ * Typically an inner class to the domain class, supported here for test purposes
+ **/
+ public class TestSObjectDisableBehaviourConstructor implements fflib_SObjectDomain.IConstructable
+ {
+ public fflib_SObjectDomain construct(List sObjectList)
+ {
+ return new TestSObjectDisableBehaviour(sObjectList);
+ }
+ }
+}
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDomain.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDomain.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectDomain.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectSelector.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectSelector.cls
new file mode 100644
index 00000000000..f9b1c5ea1e7
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectSelector.cls
@@ -0,0 +1,494 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+/**
+ * Class providing common database query support for abstracting and encapsulating query logic
+ **/
+public abstract with sharing class fflib_SObjectSelector
+ implements fflib_ISObjectSelector
+{
+ /**
+ * Indicates whether the sObject has the currency ISO code field for organisations which have multi-currency
+ * enabled.
+ **/
+ private Boolean CURRENCY_ISO_CODE_ENABLED {
+ get {
+ if(CURRENCY_ISO_CODE_ENABLED == null){
+ CURRENCY_ISO_CODE_ENABLED = describeWrapper.getFieldsMap().keySet().contains('currencyisocode');
+ }
+ return CURRENCY_ISO_CODE_ENABLED;
+ }
+ set;
+ }
+
+ /**
+ * Should this selector automatically include the FieldSet fields when building queries?
+ **/
+ private Boolean m_includeFieldSetFields = false;
+
+ /**
+ * Enforce FLS Security
+ **/
+ protected Boolean m_enforceFLS = false;
+
+ /**
+ * Enforce CRUD Security
+ **/
+ private Boolean m_enforceCRUD = true;
+
+ /**
+ * Order by field
+ **/
+ private String m_orderBy;
+
+ /**
+ * Sort the query fields in the select statement (defaults to true, at the expense of performance).
+ * Switch this off if you need more performant queries.
+ **/
+ private Boolean m_sortSelectFields = true;
+
+ /**
+ * Describe helper
+ **/
+ private fflib_SObjectDescribe describeWrapper {
+ get {
+ if(describeWrapper == null)
+ describeWrapper = fflib_SObjectDescribe.getDescribe(getSObjectType());
+ return describeWrapper;
+ }
+ set;
+ }
+ /**
+ * static variables
+ **/
+ private static String DEFAULT_SORT_FIELD = 'CreatedDate';
+ private static String SF_ID_FIELD = 'Id';
+
+ /**
+ * Implement this method to inform the base class of the SObject (custom or standard) to be queried
+ **/
+ abstract Schema.SObjectType getSObjectType();
+
+ /**
+ * Implement this method to inform the base class of the common fields to be queried or listed by the base class methods
+ **/
+ abstract List getSObjectFieldList();
+
+ /**
+ * Constructs the Selector with the default settings
+ **/
+ public fflib_SObjectSelector() { }
+
+ /**
+ * Constructs the Selector
+ *
+ * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well
+ **/
+ public fflib_SObjectSelector(Boolean includeFieldSetFields)
+ {
+ this(includeFieldSetFields, true, false);
+ }
+
+ /**
+ * Constructs the Selector
+ *
+ * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well
+ **/
+ public fflib_SObjectSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS)
+ {
+ this(includeFieldSetFields, enforceCRUD, enforceFLS, true);
+ }
+
+ /**
+ * Constructs the Selector
+ *
+ * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well
+ * @param enforceCRUD Enforce CRUD security
+ * @param enforceFLS Enforce Field Level Security
+ * @param sortSelectFields Set to false if selecting many columns to skip sorting select fields and improve performance
+ **/
+ public fflib_SObjectSelector(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS, Boolean sortSelectFields)
+ {
+ m_includeFieldSetFields = includeFieldSetFields;
+ m_enforceCRUD = enforceCRUD;
+ m_enforceFLS = enforceFLS;
+ m_sortSelectFields = sortSelectFields;
+ }
+
+ /**
+ * Override this method to provide a list of Fieldsets that can optionally drive inclusion of additional fields in the base queries
+ **/
+ public virtual List getSObjectFieldSetList()
+ {
+ return null;
+ }
+
+ /**
+ * Override this method to control the default ordering of records returned by the base queries,
+ * defaults to the name field of the object if it is not encrypted or CreatedDate if there the object has createdDated or Id
+ **/
+ public virtual String getOrderBy()
+ {
+ if (m_orderBy == null)
+ {
+ Schema.SObjectField nameField = describeWrapper.getNameField();
+ if (nameField != null && !nameField.getDescribe().isEncrypted())
+ {
+ m_orderBy = nameField.getDescribe().getName();
+ }
+ else
+ {
+ m_orderBy = DEFAULT_SORT_FIELD;
+ try {
+ if (describeWrapper.getField(m_orderBy) == null)
+ {
+ m_orderBy = SF_ID_FIELD;
+ }
+ }
+ catch(fflib_QueryFactory.InvalidFieldException ex) {
+ m_orderBy = SF_ID_FIELD;
+ }
+ }
+ }
+ return m_orderBy;
+ }
+
+
+ /**
+ * @description Set the selector to enforce FLS Security
+ **/
+ public fflib_SObjectSelector enforceFLS()
+ {
+ m_enforceFLS = true;
+ return this;
+ }
+
+ /**
+ * @description Set the selector to automatically include the FieldSet fields when building queries
+ **/
+ public fflib_SObjectSelector includeFieldSetFields()
+ {
+ this.m_includeFieldSetFields = true;
+ return this;
+ }
+
+ /**
+ * @description Set the selector to ignore CRUD security
+ * @return
+ */
+ public fflib_SObjectSelector ignoreCRUD()
+ {
+ this.m_enforceCRUD = false;
+ return this;
+ }
+
+ public fflib_SObjectSelector unsortedSelectFields()
+ {
+ this.m_sortSelectFields = false;
+ return this;
+ }
+
+ /**
+ * Returns True if this Selector instance has been instructed by the caller to include Field Set fields
+ **/
+ public Boolean isIncludeFieldSetFields()
+ {
+ return m_includeFieldSetFields;
+ }
+
+ /**
+ * Returns True if this Selector is enforcing FLS
+ **/
+ public Boolean isEnforcingFLS()
+ {
+ return m_enforceFLS;
+ }
+
+ /**
+ * Returns True if this Selector is enforcing CRUD Security
+ **/
+ public Boolean isEnforcingCRUD()
+ {
+ return m_enforceCRUD;
+ }
+
+ /**
+ * Provides access to the builder containing the list of fields base queries are using, this is demand
+ * created if one has not already been defined via setFieldListBuilder
+ *
+ * @depricated See newQueryFactory
+ **/
+ public fflib_StringBuilder.CommaDelimitedListBuilder getFieldListBuilder()
+ {
+ return
+ new fflib_StringBuilder.CommaDelimitedListBuilder(
+ new List(newQueryFactory().getSelectedFields()));
+ }
+
+ /**
+ * Use this method to override the default FieldListBuilder (created on demand via getFieldListBuilder) with a custom one,
+ * warning, this will bypass anything getSObjectFieldList or getSObjectFieldSetList returns
+ *
+ * @depricated See newQueryFactory
+ **/
+ public void setFieldListBuilder(fflib_StringBuilder.FieldListBuilder fieldListBuilder)
+ {
+ // TODO: Consider if given the known use cases for this (dynamic selector optimisation) if it's OK to leave this as a null operation
+ }
+
+ /**
+ * Returns in string form a comma delimited list of fields as defined via getSObjectFieldList and optionally getSObjectFieldSetList
+ *
+ * @deprecated See newQueryFactory
+ **/
+ public String getFieldListString()
+ {
+ return getFieldListBuilder().getStringValue();
+ }
+
+ /**
+ * Returns in string form a comma delimited list of fields as defined via getSObjectFieldList and optionally getSObjectFieldSetList
+ * @param relation Will prefix fields with the given relation, e.g. MyLookupField__r
+ *
+ * @depricated See newQueryFactory
+ **/
+ public String getRelatedFieldListString(String relation)
+ {
+ return getFieldListBuilder().getStringValue(relation + '.');
+ }
+
+ /**
+ * Returns the string representation of the SObject this selector represents
+ **/
+ public String getSObjectName()
+ {
+ return describeWrapper.getDescribe().getName();
+ }
+
+ /**
+ * Performs a SOQL query,
+ * - Selecting the fields described via getSObjectFieldsList and getSObjectFieldSetList (if included)
+ * - From the SObject described by getSObjectType
+ * - Where the Id's match those provided in the set
+ * - Ordered by the fields returned via getOrderBy
+ * @returns A list of SObject's
+ **/
+ public virtual List selectSObjectsById(Set idSet)
+ {
+ return Database.query(buildQuerySObjectById());
+ }
+
+ /**
+ * Performs a SOQL query,
+ * - Selecting the fields described via getSObjectFieldsList and getSObjectFieldSetList (if included)
+ * - From the SObject described by getSObjectType
+ * - Where the Id's match those provided in the set
+ * - Ordered by the fields returned via getOrderBy
+ * @returns A QueryLocator (typically for use in a Batch Apex job)
+ **/
+ public virtual Database.QueryLocator queryLocatorById(Set idSet)
+ {
+ return Database.getQueryLocator(buildQuerySObjectById());
+ }
+
+ /**
+ * Throws an exception if the SObject indicated by getSObjectType is not accessible to the current user (read access)
+ *
+ * @deprecated If you utilise the newQueryFactory method this is automatically done for you (unless disabled by the selector)
+ **/
+ public void assertIsAccessible()
+ {
+ if(!getSObjectType().getDescribe().isAccessible())
+ throw new fflib_SObjectDomain.DomainException(
+ 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.');
+ }
+
+ /**
+ * Public access for the getSObjectType during Mock registration
+ * (adding public to the existing method broken base class API backwards compatibility)
+ **/
+ public SObjectType getSObjectType2()
+ {
+ return getSObjectType();
+ }
+
+ /**
+ * Public access for the getSObjectType during Mock registration
+ * (adding public to the existing method broken base class API backwards compatibility1)
+ **/
+ public SObjectType sObjectType()
+ {
+ return getSObjectType();
+ }
+
+ /**
+ * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by
+ **/
+ public fflib_QueryFactory newQueryFactory()
+ {
+ return newQueryFactory(m_enforceCRUD, m_enforceFLS, true);
+ }
+
+ /**
+ * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by
+ **/
+ public fflib_QueryFactory newQueryFactory(Boolean includeSelectorFields)
+ {
+ return newQueryFactory(m_enforceCRUD, m_enforceFLS, includeSelectorFields);
+ }
+
+ /**
+ * Returns a QueryFactory configured with the Selectors object, fields, fieldsets and default order by
+ * CRUD and FLS read security will be checked if the corresponding inputs are true (overrides that defined in the selector).
+ **/
+ public fflib_QueryFactory newQueryFactory(Boolean assertCRUD, Boolean enforceFLS, Boolean includeSelectorFields)
+ {
+ // Construct QueryFactory around the given SObject
+ return configureQueryFactory(
+ new fflib_QueryFactory(getSObjectType2()),
+ assertCRUD, enforceFLS, includeSelectorFields);
+ }
+
+ /**
+ * Adds the selectors fields to the given QueryFactory using the given relationship path as a prefix
+ *
+ * // TODO: This should be consistent (ideally) with configureQueryFactory below
+ **/
+ public void configureQueryFactoryFields(fflib_QueryFactory queryFactory, String relationshipFieldPath)
+ {
+ // Add fields from selector prefixing the relationship path
+ for(SObjectField field : getSObjectFieldList()) {
+ queryFactory.selectField(relationshipFieldPath + '.' + field.getDescribe().getName(), this.getSObjectType());
+ }
+ // Automatically select the CurrencyIsoCode for MC orgs (unless the object is a known exception to the rule)
+ if(UserInfo.isMultiCurrencyOrganization() && CURRENCY_ISO_CODE_ENABLED){
+ queryFactory.selectField(relationshipFieldPath+'.CurrencyIsoCode');
+ }
+ }
+
+ /**
+ * Adds a subselect QueryFactory based on this selector to the given QueryFactor, returns the parentQueryFactory
+ **/
+ public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory)
+ {
+ return addQueryFactorySubselect(parentQueryFactory, true);
+ }
+
+ /**
+ * Adds a subselect QueryFactory based on this selector to the given QueryFactor
+ **/
+ public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, Boolean includeSelectorFields)
+ {
+ fflib_QueryFactory subSelectQueryFactory =
+ parentQueryFactory.subselectQuery(getSObjectType2());
+ return configureQueryFactory(
+ subSelectQueryFactory,
+ m_enforceCRUD,
+ m_enforceFLS,
+ includeSelectorFields);
+ }
+
+ /**
+ * Adds a subselect QueryFactory based on this selector to the given QueryFactor, returns the parentQueryFactory
+ **/
+ public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, String relationshipName)
+ {
+ return addQueryFactorySubselect(parentQueryFactory, relationshipName, TRUE);
+ }
+
+ /**
+ * Adds a subselect QueryFactory based on this selector to the given QueryFactor
+ **/
+ public fflib_QueryFactory addQueryFactorySubselect(fflib_QueryFactory parentQueryFactory, String relationshipName, Boolean includeSelectorFields)
+ {
+ fflib_QueryFactory subSelectQueryFactory = parentQueryFactory.subselectQuery(relationshipName);
+ return configureQueryFactory(subSelectQueryFactory, m_enforceCRUD, m_enforceFLS, includeSelectorFields);
+ }
+
+ /**
+ * Constructs the default SOQL query for this selector, see selectSObjectsById and queryLocatorById
+ **/
+ protected String buildQuerySObjectById()
+ {
+ return newQueryFactory().setCondition('id in :idSet').toSOQL();
+ }
+
+ /**
+ * Configures a QueryFactory instance according to the configuration of this selector
+ **/
+ private fflib_QueryFactory configureQueryFactory(fflib_QueryFactory queryFactory, Boolean assertCRUD, Boolean enforceFLS, Boolean includeSelectorFields)
+ {
+ // CRUD and FLS security required?
+ if (assertCRUD)
+ {
+ try {
+ // Leverage QueryFactory for CRUD checking
+ queryFactory.assertIsAccessible();
+ } catch (fflib_SecurityUtils.CrudException e) {
+ // Marshal exception into DomainException for backwards compatibility
+ throw new fflib_SObjectDomain.DomainException(
+ 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.');
+ }
+ }
+ queryFactory.setEnforceFLS(enforceFLS);
+
+ // Configure the QueryFactory with the Selector fields?
+ if(includeSelectorFields)
+ {
+ // select the Selector fields and Fieldsets and set order
+ queryFactory.selectFields(getSObjectFieldList());
+
+ List fieldSetList = getSObjectFieldSetList();
+ if(m_includeFieldSetFields && fieldSetList != null)
+ for(Schema.FieldSet fieldSet : fieldSetList)
+ queryFactory.selectFieldSet(fieldSet);
+
+ // Automatically select the CurrencyIsoCode for MC orgs (unless the object is a known exception to the rule)
+ if(UserInfo.isMultiCurrencyOrganization() && CURRENCY_ISO_CODE_ENABLED)
+ queryFactory.selectField('CurrencyIsoCode');
+ }
+
+ // Parse the getOrderBy()
+ for(String orderBy : getOrderBy().split(','))
+ {
+ List orderByParts = orderBy.trim().split(' ');
+ String fieldNamePart = orderByParts[0];
+ String fieldSortOrderPart = orderByParts.size() > 1 ? orderByParts[1] : null;
+ fflib_QueryFactory.SortOrder fieldSortOrder = fflib_QueryFactory.SortOrder.ASCENDING;
+ if(fieldSortOrderPart==null)
+ fieldSortOrder = fflib_QueryFactory.SortOrder.ASCENDING;
+ else if(fieldSortOrderPart.equalsIgnoreCase('DESC'))
+ fieldSortOrder = fflib_QueryFactory.SortOrder.DESCENDING;
+ else if(fieldSortOrderPart.equalsIgnoreCase('ASC'))
+ fieldSortOrder = fflib_QueryFactory.SortOrder.ASCENDING;
+ queryFactory.addOrdering(fieldNamePart, fieldSortOrder, orderBy.containsIgnoreCase('NULLS LAST'));
+ }
+
+ queryFactory.setSortSelectFields(m_sortSelectFields);
+
+ return queryFactory;
+ }
+}
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectSelector.cls-meta.xml b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectSelector.cls-meta.xml
new file mode 100644
index 00000000000..40d67933d00
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectSelector.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls
new file mode 100644
index 00000000000..e5b85b32dd0
--- /dev/null
+++ b/subfolders/fflib/sfdx-source/apex-common/main/classes/fflib_SObjectUnitOfWork.cls
@@ -0,0 +1,933 @@
+/**
+ * Copyright (c), FinancialForce.com, inc
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * - Neither the name of the FinancialForce.com, inc nor the names of its contributors
+ * may be used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+**/
+
+/**
+ * Provides an implementation of the Enterprise Application Architecture Unit Of Work, as defined by Martin Fowler
+ * http://martinfowler.com/eaaCatalog/unitOfWork.html
+ *
+ * "When you're pulling data in and out of a database, it's important to keep track of what you've changed; otherwise,
+ * that data won't be written back into the database. Similarly you have to insert new objects you create and
+ * remove any objects you delete."
+ *
+ * "You can change the database with each change to your object model, but this can lead to lots of very small database calls,
+ * which ends up being very slow. Furthermore it requires you to have a transaction open for the whole interaction, which is
+ * impractical if you have a business transaction that spans multiple requests. The situation is even worse if you need to
+ * keep track of the objects you've read so you can avoid inconsistent reads."
+ *
+ * "A Unit of Work keeps track of everything you do during a business transaction that can affect the database. When you're done,
+ * it figures out everything that needs to be done to alter the database as a result of your work."
+ *
+ * In an Apex context this pattern provides the following specific benefits
+ * - Applies bulkfication to DML operations, insert, update and delete
+ * - Manages a business transaction around the work and ensures a rollback occurs (even when exceptions are later handled by the caller)
+ * - Honours dependency rules between records and updates dependent relationships automatically during the commit
+ *
+ * Please refer to the testMethod's in this class for example usage
+ *
+ * TODO: Need to complete the 100% coverage by covering parameter exceptions in tests
+ * TODO: Need to add some more test methods for more complex use cases and some unexpected (e.g. registerDirty and then registerDeleted)
+ *
+ **/
+public virtual class fflib_SObjectUnitOfWork
+ implements fflib_ISObjectUnitOfWork
+{
+ // NOTE:
+ // If registration methods are added to this class, then they should probably also be added
+ // to ortoo_DynamicSobjectUnitOfWork
+ //
+ @testVisible
+ protected List m_sObjectTypes = new List();
+
+ protected Map> m_newListByType = new Map>();
+
+ protected Map> m_dirtyMapByType = new Map>();
+
+ protected Map> m_deletedMapByType = new Map>();
+ protected Map> m_emptyRecycleBinMapByType = new Map>();
+
+ protected Map m_relationships = new Map();
+
+ protected Map> m_publishBeforeListByType = new Map>();
+ protected Map> m_publishAfterSuccessListByType = new Map>();
+ protected Map> m_publishAfterFailureListByType = new Map>();
+
+ protected List m_workList = new List();
+
+ @TestVisible
+ protected IEmailWork m_emailWork = new SendEmailWork();
+
+ protected IDML m_dml;
+
+ /**
+ * Interface describes work to be performed during the commitWork method
+ **/
+ public interface IDoWork
+ {
+ void doWork();
+ }
+
+ public interface IDML
+ {
+ void dmlInsert(List objList);
+ void dmlUpdate(List objList);
+ void dmlDelete(List objList);
+ void eventPublish(List objList);
+ void emptyRecycleBin(List objList);
+ }
+
+ public virtual class SimpleDML implements IDML
+ {
+ public virtual void dmlInsert(List objList)
+ {
+ insert objList;
+ }
+ public virtual void dmlUpdate(List objList)
+ {
+ update objList;
+ }
+ public virtual void dmlDelete(List objList)
+ {
+ delete objList;
+ }
+ public virtual void eventPublish(List objList)
+ {
+ EventBus.publish(objList);
+ }
+ public virtual void emptyRecycleBin(List objList)
+ {
+ if (objList.isEmpty())
+ {
+ return;
+ }
+
+ Database.emptyRecycleBin(objList);
+ }
+ }
+ /**
+ * Constructs a new UnitOfWork to support work against the given object list
+ *
+ * @param sObjectTypes A list of objects given in dependency order (least dependent first)
+ */
+ public fflib_SObjectUnitOfWork(List sObjectTypes)
+ {
+ this(sObjectTypes,new SimpleDML());
+ }
+
+ public fflib_SObjectUnitOfWork(List sObjectTypes, IDML dml)
+ {
+ m_sObjectTypes = sObjectTypes.clone();
+
+ for (Schema.SObjectType sObjectType : m_sObjectTypes)
+ {
+ // register the type
+ handleRegisterType(sObjectType);
+ }
+
+ m_relationships.put(Messaging.SingleEmailMessage.class.getName(), new Relationships());
+
+ m_dml = dml;
+ }
+
+ // default implementations for commitWork events
+ public virtual void onRegisterType(Schema.SObjectType sObjectType) {}
+ public virtual void onCommitWorkStarting() {}
+
+ public virtual void onPublishBeforeEventsStarting() {}
+ public virtual void onPublishBeforeEventsFinished() {}
+
+ public virtual void onDMLStarting() {}
+ public virtual void onDMLFinished() {}
+
+ public virtual void onDoWorkStarting() {}
+ public virtual void onDoWorkFinished() {}
+
+ public virtual void onPublishAfterSuccessEventsStarting() {}
+ public virtual void onPublishAfterSuccessEventsFinished() {}
+
+ public virtual void onPublishAfterFailureEventsStarting() {}
+ public virtual void onPublishAfterFailureEventsFinished() {}
+
+ public virtual void onCommitWorkFinishing() {}
+ public virtual void onCommitWorkFinished(Boolean wasSuccessful) {}
+
+ /**
+ * Registers the type to be used for DML operations
+ *
+ * @param sObjectType - The type to register
+ *
+ */
+ protected void handleRegisterType(Schema.SObjectType sObjectType)
+ {
+ String sObjectName = sObjectType.getDescribe().getName();
+
+ // add type to dml operation tracking
+ m_newListByType.put(sObjectName, new List());
+ m_dirtyMapByType.put(sObjectName, new Map());
+ m_deletedMapByType.put(sObjectName, new Map());
+ m_emptyRecycleBinMapByType.put(sObjectName, new Map());
+ m_relationships.put(sObjectName, new Relationships());
+
+ m_publishBeforeListByType.put(sObjectName, new List());
+ m_publishAfterSuccessListByType.put(sObjectName, new List());
+ m_publishAfterFailureListByType.put(sObjectName, new List());
+
+ // give derived class opportunity to register the type
+ onRegisterType(sObjectType);
+ }
+
+ /**
+ * Register a generic piece of work to be invoked during the commitWork phase
+ **/
+ public void registerWork(IDoWork work)
+ {
+ m_workList.add(work);
+ }
+
+ /**
+ * Registers the given email to be sent during the commitWork
+ **/
+ public void registerEmail(Messaging.Email email)
+ {
+ m_emailWork.registerEmail(email);
+ }
+
+ /**
+ * Register an deleted record to be removed from the recycle bin during the commitWork method
+ *
+ * @param record An deleted record
+ **/
+ public virtual void registerEmptyRecycleBin(SObject record)
+ {
+ String sObjectType = record.getSObjectType().getDescribe().getName();
+ assertForSupportedSObjectType(m_emptyRecycleBinMapByType, sObjectType);
+
+ m_emptyRecycleBinMapByType.get(sObjectType).put(record.Id, record);
+ }
+
+ /**
+ * Register deleted records to be removed from the recycle bin during the commitWork method
+ *
+ * @param records Deleted records
+ **/
+ public virtual void registerEmptyRecycleBin(List records)
+ {
+ for (SObject record : records)
+ {
+ registerEmptyRecycleBin(record);
+ }
+ }
+
+ /**
+ * Register a newly created SObject instance to be inserted when commitWork is called
+ *
+ * @param record A newly created SObject instance to be inserted during commitWork
+ **/
+ public virtual void registerNew(SObject record)
+ {
+ registerNew(record, null, null);
+ }
+
+ /**
+ * Register a list of newly created SObject instances to be inserted when commitWork is called
+ *
+ * @param records A list of newly created SObject instances to be inserted during commitWork
+ **/
+ public virtual void registerNew(List records)
+ {
+ for (SObject record : records)
+ {
+ registerNew(record, null, null);
+ }
+ }
+
+ /**
+ * Register a newly created SObject instance to be inserted when commitWork is called,
+ * you may also provide a reference to the parent record instance (should also be registered as new separately)
+ *
+ * @param record A newly created SObject instance to be inserted during commitWork
+ * @param relatedToParentField A SObjectField reference to the child field that associates the child record with its parent
+ * @param relatedToParentRecord A SObject instance of the parent record (should also be registered as new separately)
+ **/
+ public virtual void registerNew(SObject record, Schema.SObjectField relatedToParentField, SObject relatedToParentRecord)
+ {
+ if (record.Id != null)
+ throw new UnitOfWorkException('Only new records can be registered as new');
+ String sObjectType = record.getSObjectType().getDescribe().getName();
+
+ assertForNonEventSObjectType(sObjectType);
+ assertForSupportedSObjectType(m_newListByType, sObjectType);
+
+ m_newListByType.get(sObjectType).add(record);
+ if (relatedToParentRecord!=null && relatedToParentField!=null)
+ registerRelationship(record, relatedToParentField, relatedToParentRecord);
+ }
+
+ /**
+ * Register a relationship between two records that have yet to be inserted to the database. This information will be
+ * used during the commitWork phase to make the references only when related records have been inserted to the database.
+ *
+ * @param record An existing or newly created record
+ * @param relatedToField A SObjectField reference to the lookup field that relates the two records together
+ * @param relatedTo A SObject instance (yet to be committed to the database)
+ */
+ public virtual void registerRelationship(SObject record, Schema.SObjectField relatedToField, SObject relatedTo)
+ {
+ String sObjectType = record.getSObjectType().getDescribe().getName();
+
+ assertForNonEventSObjectType(sObjectType);
+ assertForSupportedSObjectType(m_newListByType, sObjectType);
+
+ m_relationships.get(sObjectType).add(record, relatedToField, relatedTo);
+ }
+
+ /**
+ * Registers a relationship between a record and a Messaging.Email where the record has yet to be inserted
+ * to the database. This information will be
+ * used during the commitWork phase to make the references only when related records have been inserted to the database.
+ *
+ * @param email a single email message instance
+ * @param relatedTo A SObject instance (yet to be committed to the database)
+ */
+ public virtual void registerRelationship( Messaging.SingleEmailMessage email, SObject relatedTo )
+ {
+ m_relationships.get( Messaging.SingleEmailMessage.class.getName() ).add(email, relatedTo);
+ }
+
+ /**
+ * Registers a relationship between a record and a lookup value using an external ID field and a provided value. This
+ * information will be used during the commitWork phase to make the lookup reference requested when inserted to the database.
+ *
+ * @param record An existing or newly created record
+ * @param relatedToField A SObjectField reference to the lookup field that relates the two records together
+ * @param externalIdField A SObjectField reference to a field on the target SObject that is marked as isExternalId
+ * @param externalId A Object representing the targeted value of the externalIdField in said lookup
+ *
+ * Usage Example: uow.registerRelationship(recordSObject, record_sobject__c.relationship_field__c, lookup_sobject__c.external_id__c, 'abc123');
+ *
+ * Wraps putSObject, creating a new instance of the lookup sobject using the external id field and value.
+ */
+ public virtual void registerRelationship(SObject record, Schema.SObjectField relatedToField, Schema.SObjectField externalIdField, Object externalId)
+ {
+ // NOTE: Due to the lack of ExternalID references on Standard Objects, this method can not be provided a standardized Unit Test. - Rick Parker
+ String sObjectType = record.getSObjectType().getDescribe().getName();
+ if(!m_newListByType.containsKey(sObjectType))
+ throw new UnitOfWorkException(String.format('SObject type {0} is not supported by this unit of work', new String[] { sObjectType }));
+ m_relationships.get(sObjectType).add(record, relatedToField, externalIdField, externalId);
+ }
+
+ /**
+ * Register an existing record to be updated during the commitWork method
+ *
+ * @param record An existing record
+ **/
+ public virtual void registerDirty(SObject record)
+ {
+ registerDirty(record, new List());
+ }
+
+ /**
+ * Registers the entire records as dirty or just only the dirty fields if the record was already registered
+ *
+ * @param records SObjects to register as dirty
+ * @param dirtyFields A list of modified fields
+ */
+ public virtual void registerDirty(List records, List dirtyFields)
+ {
+ for (SObject record : records)
+ {
+ registerDirty(record, dirtyFields);
+ }
+ }
+
+ /**
+ * Registers the entire record as dirty or just only the dirty fields if the record was already registered
+ *
+ * @param record SObject to register as dirty
+ * @param dirtyFields A list of modified fields
+ */
+ public virtual void registerDirty(SObject record, List