From 05f2cd839ffb4be8ba4bbe9f8cfb834acc92b43e Mon Sep 17 00:00:00 2001 From: Yury Bondarau Date: Fri, 14 Sep 2018 16:05:24 +0200 Subject: [PATCH 1/4] #199 removed 'with sharing' keyword from base classes to respect caller sharing context --- fflib/src/classes/fflib_SObjectDomain.cls | 2 +- fflib/src/classes/fflib_SObjectSelector.cls | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fflib/src/classes/fflib_SObjectDomain.cls b/fflib/src/classes/fflib_SObjectDomain.cls index 06f39426123..b29e036a683 100644 --- a/fflib/src/classes/fflib_SObjectDomain.cls +++ b/fflib/src/classes/fflib_SObjectDomain.cls @@ -38,7 +38,7 @@ * http://martinfowler.com/eaaCatalog/domainModel.html * **/ -public virtual with sharing class fflib_SObjectDomain +public virtual class fflib_SObjectDomain implements fflib_ISObjectDomain { /** diff --git a/fflib/src/classes/fflib_SObjectSelector.cls b/fflib/src/classes/fflib_SObjectSelector.cls index 8ec7ace33fb..18de11d0c76 100644 --- a/fflib/src/classes/fflib_SObjectSelector.cls +++ b/fflib/src/classes/fflib_SObjectSelector.cls @@ -27,7 +27,7 @@ /** * Class providing common database query support for abstracting and encapsulating query logic **/ -public abstract with sharing class fflib_SObjectSelector +public abstract class fflib_SObjectSelector implements fflib_ISObjectSelector { /** From cc7e7940be220925d01ec233416c1bbce0a70939 Mon Sep 17 00:00:00 2001 From: Yury Bondarau Date: Fri, 19 Oct 2018 15:02:22 +0200 Subject: [PATCH 2/4] #199: created base classes with inherited sharing for domain and selector --- fflib/src/classes/fflib_SObjectDomain.cls | 985 +--------------- fflib/src/classes/fflib_SObjectDomainBase.cls | 1035 +++++++++++++++++ .../fflib_SObjectDomainBase.cls-meta.xml | 5 + fflib/src/classes/fflib_SObjectSelector.cls | 409 +------ .../src/classes/fflib_SObjectSelectorBase.cls | 440 +++++++ .../fflib_SObjectSelectorBase.cls-meta.xml | 5 + .../src/classes/fflib_SObjectSelectorTest.cls | 116 +- 7 files changed, 1581 insertions(+), 1414 deletions(-) create mode 100644 fflib/src/classes/fflib_SObjectDomainBase.cls create mode 100644 fflib/src/classes/fflib_SObjectDomainBase.cls-meta.xml create mode 100644 fflib/src/classes/fflib_SObjectSelectorBase.cls create mode 100644 fflib/src/classes/fflib_SObjectSelectorBase.cls-meta.xml diff --git a/fflib/src/classes/fflib_SObjectDomain.cls b/fflib/src/classes/fflib_SObjectDomain.cls index b29e036a683..cd90898f12b 100644 --- a/fflib/src/classes/fflib_SObjectDomain.cls +++ b/fflib/src/classes/fflib_SObjectDomain.cls @@ -2,22 +2,22 @@ * Copyright (c) 2012, FinancialForce.com, inc * All rights reserved. * - * Redistribution and use in source and binary forms, with or without modification, + * 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, + * - 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 + * - 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 + * - 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, + * 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) @@ -26,67 +26,22 @@ /** * Base class aiding in the implemetnation of a Domain Model around SObject collections - * - * Domain (software engineering). “a set of common requirements, terminology, and functionality + * + * 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.”, + * 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...” + * "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 class fflib_SObjectDomain +public virtual with sharing class fflib_SObjectDomain extends fflib_SObjectDomainBase implements fflib_ISObjectDomain { - /** - * Provides access to the data represented by this domain class - **/ - public List Records { get; private set;} - - /** - * Derived from the records provided during construction, provides the native describe for the standard or custom object - **/ - public Schema.DescribeSObjectResult SObjectDescribe {get; private set;} - - /** - * Exposes the configuration for this domain class instance - **/ - public Configuration Configuration {get; private set;} - - /** - * Useful during unit testign to assert at a more granular and robust level for errors raised during the various trigger events - **/ - public static ErrorFactory Errors {get; private set;} - - /** - * Useful during unit testing to access mock support for database inserts and udpates (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 configuraiton used for each domain - **/ - private static Map TriggerEventByClass; - - static - { - Errors = new 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 * @@ -95,7 +50,7 @@ public virtual class fflib_SObjectDomain **/ public fflib_SObjectDomain(List sObjectList) { - this(sObjectList, sObjectList.getSObjectType()); + super(sObjectList, sObjectList.getSObjectType()); } /** @@ -109,169 +64,8 @@ public virtual class fflib_SObjectDomain **/ public fflib_SObjectDomain(List sObjectList, SObjectType sObjectType) { - // Ensure the domain class has its own copy of the data - Records = sObjectList.clone(); - // Capture SObjectType describe for this domain class - SObjectDescribe = sObjectType.getDescribe(); - // Configure the Domain object instance - Configuration = new Configuration(); + super(sObjectList, sObjectType); } - - /** - * 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 udpate 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.isCreateable()) - throw new DomainException('Permission to create an ' + SObjectDescribe.getName() + ' denied.'); - - onAfterUndelete(); - } - - /** - * Returns the SObjectType this Domain class represents - **/ - public SObjectType getSObjectType() - { - return SObjectDescribe.getSObjectType(); - } - - /** - * Returns the SObjectType this Domain class represents - **/ - public SObjectType sObjectType() - { - return getSObjectType(); - } - - /** - * Alternative to the Records property, provided to support mocking of Domain classes - **/ - public List getRecords() - { - return Records; - } - /** * Interface used to aid the triggerHandler in constructing instances of Domain classes **/ @@ -286,750 +80,5 @@ public virtual class fflib_SObjectDomain 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.Records = 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 compatability 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 String error(String message, SObject record) - { - return Errors.error(this, message, record); - } - - /** - * Ensures logging of errors in the Domain context for later assertions in tests - **/ - public String error(String message, SObject record, SObjectField field) - { - return Errors.error(this, message, record, field); - } - - /** - * Ensures logging of errors in the Domain context for later assertions in tests - **/ - 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(); - } - } - - /** - * Ensures logging of errors in the Domain context for later assertions in tests - **/ - public virtual class FieldError extends ObjectError - { - public SObjectField field; - - public FieldError() - { - - } - } - - /** - * Ensures logging of errors in the Domain context for later assertions in tests - **/ - public virtual class ObjectError extends Error - { - public SObject record; - - public ObjectError() - { - - } - } - - /** - * Ensures logging of errors in the Domain context for later assertions in tests - **/ - 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 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/fflib/src/classes/fflib_SObjectDomainBase.cls b/fflib/src/classes/fflib_SObjectDomainBase.cls new file mode 100644 index 00000000000..30c269f9d3f --- /dev/null +++ b/fflib/src/classes/fflib_SObjectDomainBase.cls @@ -0,0 +1,1035 @@ +/** + * Copyright (c) 2012, 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 implemetnation 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 inherited sharing class fflib_SObjectDomainBase + implements fflib_ISObjectDomain +{ + /** + * Provides access to the data represented by this domain class + **/ + public List Records { get; private set;} + + /** + * Derived from the records provided during construction, provides the native describe for the standard or custom object + **/ + public Schema.DescribeSObjectResult SObjectDescribe {get; private set;} + + /** + * Exposes the configuration for this domain class instance + **/ + public Configuration Configuration {get; private set;} + + /** + * Useful during unit testign to assert at a more granular and robust level for errors raised during the various trigger events + **/ + public static ErrorFactory Errors {get; private set;} + + /** + * Useful during unit testing to access mock support for database inserts and udpates (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 configuraiton used for each domain + **/ + private static Map TriggerEventByClass; + + static + { + Errors = new 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 concreate list (e.g. List vs List) of records + + **/ + public fflib_SObjectDomainBase(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_SObjectDomainBase(List sObjectList, SObjectType sObjectType) + { + // Ensure the domain class has its own copy of the data + Records = sObjectList.clone(); + // Capture SObjectType describe for this domain class + SObjectDescribe = sObjectType.getDescribe(); + // 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 udpate 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.isCreateable()) + throw new DomainException('Permission to create an ' + SObjectDescribe.getName() + ' denied.'); + + onAfterUndelete(); + } + + /** + * Returns the SObjectType this Domain class represents + **/ + public SObjectType getSObjectType() + { + return SObjectDescribe.getSObjectType(); + } + + /** + * Returns the SObjectType this Domain class represents + **/ + public SObjectType sObjectType() + { + return getSObjectType(); + } + + /** + * Alternative to the Records property, provided to support mocking of Domain classes + **/ + public List getRecords() + { + return Records; + } + + /** + * Interface used to aid the triggerHandler in constructing instances of Domain classes + **/ + public interface IConstructable + { + fflib_SObjectDomainBase construct(List sObjectList); + } + + /** + * Interface used to aid the triggerHandler in constructing instances of Domain classes + **/ + public interface IConstructable2 extends IConstructable + { + fflib_SObjectDomainBase 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_SObjectDomainBase 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_SObjectDomainBase 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_SObjectDomainBase 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_SObjectDomainBase popTriggerInstance(Type domainClass, List records) + { + List domains = TriggerStateByClass.get(domainClass); + if(domains==null || domains.size()==0) + return null; + fflib_SObjectDomainBase domain = domains.remove(domains.size()-1); + domain.Records = 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 compatability 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 String error(String message, SObject record) + { + return Errors.error(this, message, record); + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public String error(String message, SObject record, SObjectField field) + { + return Errors.error(this, message, record, field); + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + 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_SObjectDomainBase 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_SObjectDomainBase 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(); + } + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public virtual class FieldError extends ObjectError + { + public SObjectField field; + + public FieldError() + { + + } + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public virtual class ObjectError extends Error + { + public SObject record; + + public ObjectError() + { + + } + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public abstract class Error + { + public String message; + public fflib_SObjectDomainBase 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_SObjectDomainBase + { + 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_SObjectDomainBase.IConstructable + { + public fflib_SObjectDomainBase 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_SObjectDomainBase + { + 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_SObjectDomainBase.Test.Database.onInsert(newOpps); + fflib_SObjectDomainBase.triggerHandler(fflib_SObjectDomainBase.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_SObjectDomainBase.IConstructable + { + public fflib_SObjectDomainBase 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_SObjectDomainBase + { + 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_SObjectDomainBase.IConstructable + { + public fflib_SObjectDomainBase 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 TestSObjectDisableBehaviour + extends fflib_SObjectDomainBase + { + 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_SObjectDomainBase.IConstructable + { + public fflib_SObjectDomainBase construct(List sObjectList) + { + return new TestSObjectDisableBehaviour(sObjectList); + } + } +} diff --git a/fflib/src/classes/fflib_SObjectDomainBase.cls-meta.xml b/fflib/src/classes/fflib_SObjectDomainBase.cls-meta.xml new file mode 100644 index 00000000000..ce1d52b30c1 --- /dev/null +++ b/fflib/src/classes/fflib_SObjectDomainBase.cls-meta.xml @@ -0,0 +1,5 @@ + + + 44.0 + Active + diff --git a/fflib/src/classes/fflib_SObjectSelector.cls b/fflib/src/classes/fflib_SObjectSelector.cls index 18de11d0c76..b935e8fa0a0 100644 --- a/fflib/src/classes/fflib_SObjectSelector.cls +++ b/fflib/src/classes/fflib_SObjectSelector.cls @@ -2,22 +2,22 @@ * Copyright (c), FinancialForce.com, inc * All rights reserved. * - * Redistribution and use in source and binary forms, with or without modification, + * 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, + * - 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 + * - 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, + * 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) @@ -27,414 +27,47 @@ /** * Class providing common database query support for abstracting and encapsulating query logic **/ -public abstract class fflib_SObjectSelector +public abstract with sharing class fflib_SObjectSelector extends fflib_SObjectSelectorBase 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; - - /** - * Enforce FLS Security - **/ - private Boolean m_enforceFLS; - - /** - * Enforce CRUD Security - **/ - private Boolean m_enforceCRUD; - - /** - * 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; - - /** - * Describe helper - **/ - private fflib_SObjectDescribe describeWrapper { - get { - if(describeWrapper == null) - describeWrapper = fflib_SObjectDescribe.getDescribe(getSObjectType()); - return describeWrapper; - } - set; - } - - /** - * 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, defaults to not including any FieldSet fields automatically **/ public fflib_SObjectSelector() { - this(false); + super(false); } - + /** * Constructs the Selector * - * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well + * @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); + super(includeFieldSetFields, true, false); } - + /** * Constructs the Selector * - * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well + * @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); + super(includeFieldSetFields, enforceCRUD, enforceFLS, true); } /** * Constructs the Selector * - * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well + * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well * @param enforceCRUD Enforce CRUD security * @param enforeFLS 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 or CreatedDate if there is none - **/ - public virtual String getOrderBy() - { - if(m_orderBy == null) { - m_orderBy = 'CreatedDate'; - if(describeWrapper.getNameField() != null) { - m_orderBy = describeWrapper.getNameField().getDescribe().getName(); - } - } - return m_orderBy; - } - - /** - * 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 optomisation) if it's OK to leave this as a null operation - } - - /** - * Returns in string form a comma delimted list of fields as defined via getSObjectFieldList and optionally getSObjectFieldSetList - * - * @depricated See newQueryFactory - **/ - public String getFieldListString() - { - return getFieldListBuilder().getStringValue(); + super(includeFieldSetFields, enforceCRUD, enforceFLS, sortSelectFields); } - - /** - * Returns in string form a comma delimted 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 representaiton 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 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 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) - * - * @depricated 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 acccess for the getSObjectType during Mock registration - * (adding public to the existing method broken base class API backwards compatability) - **/ - public SObjectType getSObjectType2() - { - return getSObjectType(); - } - - /** - * Public acccess for the getSObjectType during Mock registration - * (adding public to the existing method broken base class API backwards compatability) - **/ - 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 consistant (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()); - // 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 - **/ - private 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 compatability - 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; - } } \ No newline at end of file diff --git a/fflib/src/classes/fflib_SObjectSelectorBase.cls b/fflib/src/classes/fflib_SObjectSelectorBase.cls new file mode 100644 index 00000000000..7edce97f291 --- /dev/null +++ b/fflib/src/classes/fflib_SObjectSelectorBase.cls @@ -0,0 +1,440 @@ +/** + * 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 inherited sharing class fflib_SObjectSelectorBase + 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; + + /** + * Enforce FLS Security + **/ + private Boolean m_enforceFLS; + + /** + * Enforce CRUD Security + **/ + private Boolean m_enforceCRUD; + + /** + * 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; + + /** + * Describe helper + **/ + private fflib_SObjectDescribe describeWrapper { + get { + if(describeWrapper == null) + describeWrapper = fflib_SObjectDescribe.getDescribe(getSObjectType()); + return describeWrapper; + } + set; + } + + /** + * 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, defaults to not including any FieldSet fields automatically + **/ + public fflib_SObjectSelectorBase() + { + this(false); + } + + /** + * Constructs the Selector + * + * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well + **/ + public fflib_SObjectSelectorBase(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_SObjectSelectorBase(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 enforeFLS Enforce Field Level Security + * @param sortSelectFields Set to false if selecting many columns to skip sorting select fields and improve performance + **/ + public fflib_SObjectSelectorBase(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 or CreatedDate if there is none + **/ + public virtual String getOrderBy() + { + if(m_orderBy == null) { + m_orderBy = 'CreatedDate'; + if(describeWrapper.getNameField() != null) { + m_orderBy = describeWrapper.getNameField().getDescribe().getName(); + } + } + return m_orderBy; + } + + /** + * 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 optomisation) if it's OK to leave this as a null operation + } + + /** + * Returns in string form a comma delimted list of fields as defined via getSObjectFieldList and optionally getSObjectFieldSetList + * + * @depricated See newQueryFactory + **/ + public String getFieldListString() + { + return getFieldListBuilder().getStringValue(); + } + + /** + * Returns in string form a comma delimted 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 representaiton 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 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 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) + * + * @depricated 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_SObjectDomainBase.DomainException( + 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); + } + + /** + * Public acccess for the getSObjectType during Mock registration + * (adding public to the existing method broken base class API backwards compatability) + **/ + public SObjectType getSObjectType2() + { + return getSObjectType(); + } + + /** + * Public acccess for the getSObjectType during Mock registration + * (adding public to the existing method broken base class API backwards compatability) + **/ + 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 consistant (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()); + // 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 + **/ + private 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 compatability + throw new fflib_SObjectDomainBase.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; + } +} \ No newline at end of file diff --git a/fflib/src/classes/fflib_SObjectSelectorBase.cls-meta.xml b/fflib/src/classes/fflib_SObjectSelectorBase.cls-meta.xml new file mode 100644 index 00000000000..87b7c74017a --- /dev/null +++ b/fflib/src/classes/fflib_SObjectSelectorBase.cls-meta.xml @@ -0,0 +1,5 @@ + + + 44.0 + Active + diff --git a/fflib/src/classes/fflib_SObjectSelectorTest.cls b/fflib/src/classes/fflib_SObjectSelectorTest.cls index 0fd4c89b0ee..e97d1f93a73 100644 --- a/fflib/src/classes/fflib_SObjectSelectorTest.cls +++ b/fflib/src/classes/fflib_SObjectSelectorTest.cls @@ -2,22 +2,22 @@ * Copyright (c), FinancialForce.com, inc * All rights reserved. * - * Redistribution and use in source and binary forms, with or without modification, + * 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, + * - 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 + * - 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 + * - 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, + * 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) @@ -25,32 +25,32 @@ **/ @IsTest -private with sharing class fflib_SObjectSelectorTest +private with sharing class fflib_SObjectSelectorTest { - + static testMethod void testGetSObjectName() { Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); system.assertEquals(null, selector.getSObjectFieldSetList()); system.assertEquals('Account',selector.getSObjectName()); } - + static testMethod void testSelectSObjectsById() { - // Inserting in reverse order so that we can test the order by of select + // Inserting in reverse order so that we can test the order by of select List accountList = new List { new Account(Name='TestAccount2',AccountNumber='A2',AnnualRevenue=12345.67), - new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; - insert accountList; + new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; + insert accountList; Set idSet = new Set(); for(Account item : accountList) idSet.add(item.Id); - - Test.startTest(); + + Test.startTest(); Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); - List result = (List) selector.selectSObjectsById(idSet); + List result = (List) selector.selectSObjectsById(idSet); Test.stopTest(); - + system.assertEquals(2,result.size()); system.assertEquals('TestAccount2',result[0].Name); system.assertEquals('A2',result[0].AccountNumber); @@ -62,57 +62,57 @@ private with sharing class fflib_SObjectSelectorTest static testMethod void testQueryLocatorById() { - // Inserting in reverse order so that we can test the order by of select + // Inserting in reverse order so that we can test the order by of select List accountList = new List { new Account(Name='TestAccount2',AccountNumber='A2',AnnualRevenue=12345.67), - new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; - insert accountList; + new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; + insert accountList; Set idSet = new Set(); for(Account item : accountList) idSet.add(item.Id); - - Test.startTest(); + + Test.startTest(); Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); - Database.QueryLocator result = selector.queryLocatorById(idSet); + Database.QueryLocator result = selector.queryLocatorById(idSet); System.Iterator iteratorResult = result.iterator(); - Test.stopTest(); + Test.stopTest(); System.assert(true, iteratorResult.hasNext()); Account account = (Account) iteratorResult.next(); system.assertEquals('TestAccount2',account.Name); system.assertEquals('A2',account.AccountNumber); - system.assertEquals(12345.67,account.AnnualRevenue); + system.assertEquals(12345.67,account.AnnualRevenue); System.assert(true, iteratorResult.hasNext()); account = (Account) iteratorResult.next(); system.assertEquals('TestAccount1',account.Name); system.assertEquals('A1',account.AccountNumber); - system.assertEquals(76543.21,account.AnnualRevenue); + system.assertEquals(76543.21,account.AnnualRevenue); System.assertEquals(false, iteratorResult.hasNext()); } - + static testMethod void testAssertIsAccessible() { List accountList = new List { new Account(Name='TestAccount2',AccountNumber='A2',AnnualRevenue=12345.67), - new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; - insert accountList; + new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; + insert accountList; Set idSet = new Set(); for(Account item : accountList) idSet.add(item.Id); - + // Create a user which will not have access to the test object type User testUser = createChatterExternalUser(); if(testUser==null) return; // Abort the test if unable to create a user with low enough acess System.runAs(testUser) - { + { Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); try { List result = (List) selector.selectSObjectsById(idSet); System.assert(false,'Expected exception was not thrown'); } - catch(fflib_SObjectDomain.DomainException e) + catch(fflib_SObjectDomainBase.DomainException e) { System.assertEquals('Permission to access an Account denied.',e.getMessage()); } @@ -123,30 +123,30 @@ private with sharing class fflib_SObjectSelectorTest { List accountList = new List { new Account(Name='TestAccount2',AccountNumber='A2',AnnualRevenue=12345.67), - new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; - insert accountList; + new Account(Name='TestAccount1',AccountNumber='A1',AnnualRevenue=76543.21) }; + insert accountList; Set idSet = new Set(); for(Account item : accountList) idSet.add(item.Id); - + // Create a user which will not have access to the test object type User testUser = createChatterExternalUser(); if(testUser==null) return; // Abort the test if unable to create a user with low enough acess System.runAs(testUser) - { + { Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(false, false, false, true); try { List result = (List) selector.selectSObjectsById(idSet); } - catch(fflib_SObjectDomain.DomainException e) + catch(fflib_SObjectDomainBase.DomainException e) { System.assert(false,'Did not expect an exception to be thrown'); } } } - + static testMethod void testSOQL() { Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); @@ -158,22 +158,22 @@ private with sharing class fflib_SObjectSelectorTest String fieldListString = m.group(1); assertFieldListString(fieldListString, null); } - + static testMethod void testDefaultConfig() { Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(); System.assertEquals(false, selector.isEnforcingFLS()); System.assertEquals(true, selector.isEnforcingCRUD()); System.assertEquals(false, selector.isIncludeFieldSetFields()); - + System.assertEquals('Account', selector.getSObjectName()); System.assertEquals(Account.SObjectType, selector.getSObjectType2()); } - + private static void assertFieldListString(String fieldListString, String prefix) { String prefixString = (!String.isBlank(prefix)) ? prefix + '.' : ''; List fieldList = fieldListString.split(',{1}\\s?'); - System.assertEquals(UserInfo.isMultiCurrencyOrganization() ? 5 : 4, fieldList.size()); + System.assertEquals(UserInfo.isMultiCurrencyOrganization() ? 5 : 4, fieldList.size()); Set fieldSet = new Set(); fieldSet.addAll(fieldList); String expected = prefixString + 'AccountNumber'; @@ -189,7 +189,7 @@ private with sharing class fflib_SObjectSelectorTest System.assert(fieldSet.contains(expected), expected + ' missing from field list string: ' + fieldListString); } } - + @isTest static void testWithoutSorting() @@ -197,7 +197,7 @@ private with sharing class fflib_SObjectSelectorTest //Given Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(false, false, false, false); fflib_QueryFactory qf = selector.newQueryFactory(); - + Set expectedSelectFields = new Set{ 'Name', 'Id', 'AccountNumber', 'AnnualRevenue' }; if (UserInfo.isMultiCurrencyOrganization()) { @@ -223,7 +223,7 @@ private with sharing class fflib_SObjectSelectorTest // Build the selector to test with Testfflib_SObjectSelector selector = new Testfflib_SObjectSelector(false, false, false, false); fflib_QueryFactory qf = selector.newQueryFactory(); - + // Add in the expected fields Set expectedSelectFields = new Set{ 'Name', 'Id', 'AccountNumber', 'AnnualRevenue' }; if (UserInfo.isMultiCurrencyOrganization()) @@ -239,7 +239,7 @@ private with sharing class fflib_SObjectSelectorTest Matcher soqlMatcher = soqlPattern.matcher(soql); system.assert(soqlMatcher.matches(), 'The SOQL should have that expected.'); } - + private static void assertEqualsSelectFields(String expectedSelectFields, String actualSelectFields) { Set expected = new Set(expectedSelectFields.deleteWhiteSpace().split(',')); @@ -247,7 +247,7 @@ private with sharing class fflib_SObjectSelectorTest System.assertEquals(expected, actual); } - + private class Testfflib_SObjectSelector extends fflib_SObjectSelector { public Testfflib_SObjectSelector() @@ -259,7 +259,7 @@ private with sharing class fflib_SObjectSelectorTest { super(includeFieldSetFields, enforceCRUD, enforceFLS, sortSelectFields); } - + public List getSObjectFieldList() { return new List { @@ -269,18 +269,18 @@ private with sharing class fflib_SObjectSelectorTest Account.AnnualRevenue }; } - + public Schema.SObjectType getSObjectType() { return Account.sObjectType; } - + public override String getOrderBy() { return 'Name DESC, AnnualRevenue ASC NULLS LAST'; } } - + /** * Create test user **/ @@ -289,16 +289,16 @@ private with sharing class fflib_SObjectSelectorTest // Can only proceed with test if we have a suitable profile - Chatter External license has no access to Opportunity List testProfiles = [Select Id From Profile where UserLicense.Name='Chatter External' limit 1]; if(testProfiles.size()!=1) - return null; + return null; - // Can only proceed with test if we can successfully insert a test user + // Can only proceed with test if we can successfully insert a test user String testUsername = System.now().format('yyyyMMddhhmmss') + '@testorg.com'; User testUser = new User(Alias = 'test1', Email='testuser1@testorg.com', EmailEncodingKey='UTF-8', LastName='Testing', LanguageLocaleKey='en_US', LocaleSidKey='en_US', ProfileId = testProfiles[0].Id, TimeZoneSidKey='America/Los_Angeles', UserName=testUsername); try { insert testUser; } catch (Exception e) { return null; - } + } return testUser; - } + } } \ No newline at end of file From a983d5512d3d49ac5ed87658c7bd837d9d5c9cfd Mon Sep 17 00:00:00 2001 From: Yury Bondarau Date: Fri, 19 Oct 2018 17:21:32 +0200 Subject: [PATCH 3/4] #199 updated selector --- fflib/src/classes/fflib_SObjectDomain.cls | 985 +++++++++++++++- fflib/src/classes/fflib_SObjectDomainBase.cls | 1035 ----------------- .../fflib_SObjectDomainBase.cls-meta.xml | 5 - .../src/classes/fflib_SObjectSelectorBase.cls | 4 +- .../src/classes/fflib_SObjectSelectorTest.cls | 4 +- fflib/src/package.xml | 3 +- 6 files changed, 974 insertions(+), 1062 deletions(-) delete mode 100644 fflib/src/classes/fflib_SObjectDomainBase.cls delete mode 100644 fflib/src/classes/fflib_SObjectDomainBase.cls-meta.xml diff --git a/fflib/src/classes/fflib_SObjectDomain.cls b/fflib/src/classes/fflib_SObjectDomain.cls index cd90898f12b..06f39426123 100644 --- a/fflib/src/classes/fflib_SObjectDomain.cls +++ b/fflib/src/classes/fflib_SObjectDomain.cls @@ -2,22 +2,22 @@ * Copyright (c) 2012, FinancialForce.com, inc * All rights reserved. * - * Redistribution and use in source and binary forms, with or without modification, + * 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, + * - 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 + * - 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 + * - 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, + * 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) @@ -26,22 +26,67 @@ /** * Base class aiding in the implemetnation of a Domain Model around SObject collections - * - * Domain (software engineering). “a set of common requirements, terminology, and functionality + * + * 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.”, + * 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...” + * "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_SObjectDomainBase +public virtual with sharing class fflib_SObjectDomain implements fflib_ISObjectDomain { + /** + * Provides access to the data represented by this domain class + **/ + public List Records { get; private set;} + + /** + * Derived from the records provided during construction, provides the native describe for the standard or custom object + **/ + public Schema.DescribeSObjectResult SObjectDescribe {get; private set;} + + /** + * Exposes the configuration for this domain class instance + **/ + public Configuration Configuration {get; private set;} + + /** + * Useful during unit testign to assert at a more granular and robust level for errors raised during the various trigger events + **/ + public static ErrorFactory Errors {get; private set;} + + /** + * Useful during unit testing to access mock support for database inserts and udpates (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 configuraiton used for each domain + **/ + private static Map TriggerEventByClass; + + static + { + Errors = new 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 * @@ -50,7 +95,7 @@ public virtual with sharing class fflib_SObjectDomain extends fflib_SObjectDomai **/ public fflib_SObjectDomain(List sObjectList) { - super(sObjectList, sObjectList.getSObjectType()); + this(sObjectList, sObjectList.getSObjectType()); } /** @@ -64,8 +109,169 @@ public virtual with sharing class fflib_SObjectDomain extends fflib_SObjectDomai **/ public fflib_SObjectDomain(List sObjectList, SObjectType sObjectType) { - super(sObjectList, sObjectType); + // Ensure the domain class has its own copy of the data + Records = sObjectList.clone(); + // Capture SObjectType describe for this domain class + SObjectDescribe = sObjectType.getDescribe(); + // 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 udpate 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.isCreateable()) + throw new DomainException('Permission to create an ' + SObjectDescribe.getName() + ' denied.'); + + onAfterUndelete(); + } + + /** + * Returns the SObjectType this Domain class represents + **/ + public SObjectType getSObjectType() + { + return SObjectDescribe.getSObjectType(); + } + + /** + * Returns the SObjectType this Domain class represents + **/ + public SObjectType sObjectType() + { + return getSObjectType(); + } + + /** + * Alternative to the Records property, provided to support mocking of Domain classes + **/ + public List getRecords() + { + return Records; + } + /** * Interface used to aid the triggerHandler in constructing instances of Domain classes **/ @@ -80,5 +286,750 @@ public virtual with sharing class fflib_SObjectDomain extends fflib_SObjectDomai 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.Records = 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 compatability 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 String error(String message, SObject record) + { + return Errors.error(this, message, record); + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public String error(String message, SObject record, SObjectField field) + { + return Errors.error(this, message, record, field); + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + 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(); + } + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public virtual class FieldError extends ObjectError + { + public SObjectField field; + + public FieldError() + { + + } + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + public virtual class ObjectError extends Error + { + public SObject record; + + public ObjectError() + { + + } + } + + /** + * Ensures logging of errors in the Domain context for later assertions in tests + **/ + 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 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/fflib/src/classes/fflib_SObjectDomainBase.cls b/fflib/src/classes/fflib_SObjectDomainBase.cls deleted file mode 100644 index 30c269f9d3f..00000000000 --- a/fflib/src/classes/fflib_SObjectDomainBase.cls +++ /dev/null @@ -1,1035 +0,0 @@ -/** - * Copyright (c) 2012, 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 implemetnation 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 inherited sharing class fflib_SObjectDomainBase - implements fflib_ISObjectDomain -{ - /** - * Provides access to the data represented by this domain class - **/ - public List Records { get; private set;} - - /** - * Derived from the records provided during construction, provides the native describe for the standard or custom object - **/ - public Schema.DescribeSObjectResult SObjectDescribe {get; private set;} - - /** - * Exposes the configuration for this domain class instance - **/ - public Configuration Configuration {get; private set;} - - /** - * Useful during unit testign to assert at a more granular and robust level for errors raised during the various trigger events - **/ - public static ErrorFactory Errors {get; private set;} - - /** - * Useful during unit testing to access mock support for database inserts and udpates (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 configuraiton used for each domain - **/ - private static Map TriggerEventByClass; - - static - { - Errors = new 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 concreate list (e.g. List vs List) of records - - **/ - public fflib_SObjectDomainBase(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_SObjectDomainBase(List sObjectList, SObjectType sObjectType) - { - // Ensure the domain class has its own copy of the data - Records = sObjectList.clone(); - // Capture SObjectType describe for this domain class - SObjectDescribe = sObjectType.getDescribe(); - // 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 udpate 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.isCreateable()) - throw new DomainException('Permission to create an ' + SObjectDescribe.getName() + ' denied.'); - - onAfterUndelete(); - } - - /** - * Returns the SObjectType this Domain class represents - **/ - public SObjectType getSObjectType() - { - return SObjectDescribe.getSObjectType(); - } - - /** - * Returns the SObjectType this Domain class represents - **/ - public SObjectType sObjectType() - { - return getSObjectType(); - } - - /** - * Alternative to the Records property, provided to support mocking of Domain classes - **/ - public List getRecords() - { - return Records; - } - - /** - * Interface used to aid the triggerHandler in constructing instances of Domain classes - **/ - public interface IConstructable - { - fflib_SObjectDomainBase construct(List sObjectList); - } - - /** - * Interface used to aid the triggerHandler in constructing instances of Domain classes - **/ - public interface IConstructable2 extends IConstructable - { - fflib_SObjectDomainBase 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_SObjectDomainBase 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_SObjectDomainBase 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_SObjectDomainBase 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_SObjectDomainBase popTriggerInstance(Type domainClass, List records) - { - List domains = TriggerStateByClass.get(domainClass); - if(domains==null || domains.size()==0) - return null; - fflib_SObjectDomainBase domain = domains.remove(domains.size()-1); - domain.Records = 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 compatability 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 String error(String message, SObject record) - { - return Errors.error(this, message, record); - } - - /** - * Ensures logging of errors in the Domain context for later assertions in tests - **/ - public String error(String message, SObject record, SObjectField field) - { - return Errors.error(this, message, record, field); - } - - /** - * Ensures logging of errors in the Domain context for later assertions in tests - **/ - 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_SObjectDomainBase 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_SObjectDomainBase 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(); - } - } - - /** - * Ensures logging of errors in the Domain context for later assertions in tests - **/ - public virtual class FieldError extends ObjectError - { - public SObjectField field; - - public FieldError() - { - - } - } - - /** - * Ensures logging of errors in the Domain context for later assertions in tests - **/ - public virtual class ObjectError extends Error - { - public SObject record; - - public ObjectError() - { - - } - } - - /** - * Ensures logging of errors in the Domain context for later assertions in tests - **/ - public abstract class Error - { - public String message; - public fflib_SObjectDomainBase 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_SObjectDomainBase - { - 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_SObjectDomainBase.IConstructable - { - public fflib_SObjectDomainBase 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_SObjectDomainBase - { - 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_SObjectDomainBase.Test.Database.onInsert(newOpps); - fflib_SObjectDomainBase.triggerHandler(fflib_SObjectDomainBase.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_SObjectDomainBase.IConstructable - { - public fflib_SObjectDomainBase 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_SObjectDomainBase - { - 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_SObjectDomainBase.IConstructable - { - public fflib_SObjectDomainBase 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 TestSObjectDisableBehaviour - extends fflib_SObjectDomainBase - { - 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_SObjectDomainBase.IConstructable - { - public fflib_SObjectDomainBase construct(List sObjectList) - { - return new TestSObjectDisableBehaviour(sObjectList); - } - } -} diff --git a/fflib/src/classes/fflib_SObjectDomainBase.cls-meta.xml b/fflib/src/classes/fflib_SObjectDomainBase.cls-meta.xml deleted file mode 100644 index ce1d52b30c1..00000000000 --- a/fflib/src/classes/fflib_SObjectDomainBase.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 44.0 - Active - diff --git a/fflib/src/classes/fflib_SObjectSelectorBase.cls b/fflib/src/classes/fflib_SObjectSelectorBase.cls index 7edce97f291..e8ff302c3b4 100644 --- a/fflib/src/classes/fflib_SObjectSelectorBase.cls +++ b/fflib/src/classes/fflib_SObjectSelectorBase.cls @@ -270,7 +270,7 @@ public abstract inherited sharing class fflib_SObjectSelectorBase public void assertIsAccessible() { if(!getSObjectType().getDescribe().isAccessible()) - throw new fflib_SObjectDomainBase.DomainException( + throw new fflib_SObjectDomain.DomainException( 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); } @@ -395,7 +395,7 @@ public abstract inherited sharing class fflib_SObjectSelectorBase queryFactory.assertIsAccessible(); } catch (fflib_SecurityUtils.CrudException e) { // Marshal exception into DomainException for backwards compatability - throw new fflib_SObjectDomainBase.DomainException( + throw new fflib_SObjectDomain.DomainException( 'Permission to access an ' + getSObjectType().getDescribe().getName() + ' denied.'); } } diff --git a/fflib/src/classes/fflib_SObjectSelectorTest.cls b/fflib/src/classes/fflib_SObjectSelectorTest.cls index e97d1f93a73..0fc937bbf71 100644 --- a/fflib/src/classes/fflib_SObjectSelectorTest.cls +++ b/fflib/src/classes/fflib_SObjectSelectorTest.cls @@ -112,7 +112,7 @@ private with sharing class fflib_SObjectSelectorTest List result = (List) selector.selectSObjectsById(idSet); System.assert(false,'Expected exception was not thrown'); } - catch(fflib_SObjectDomainBase.DomainException e) + catch(fflib_SObjectDomain.DomainException e) { System.assertEquals('Permission to access an Account denied.',e.getMessage()); } @@ -140,7 +140,7 @@ private with sharing class fflib_SObjectSelectorTest { List result = (List) selector.selectSObjectsById(idSet); } - catch(fflib_SObjectDomainBase.DomainException e) + catch(fflib_SObjectDomain.DomainException e) { System.assert(false,'Did not expect an exception to be thrown'); } diff --git a/fflib/src/package.xml b/fflib/src/package.xml index b7e70f68e1f..554788ad66a 100644 --- a/fflib/src/package.xml +++ b/fflib/src/package.xml @@ -14,6 +14,7 @@ fflib_SObjectDomainTest fflib_SObjectMocks fflib_SObjectSelector + fflib_SObjectSelectorBase fflib_SObjectSelectorTest fflib_SObjectUnitOfWork fflib_SObjectUnitOfWorkTest @@ -27,5 +28,5 @@ * CustomLabels - 31.0 + 44.0 From 4f9831eb333823e922df4e00db14dae085a5992b Mon Sep 17 00:00:00 2001 From: Yury Bondarau Date: Fri, 19 Oct 2018 17:30:45 +0200 Subject: [PATCH 4/4] #199: created fflib_SObjectSelector2 class to respect caller sharing settings --- fflib/src/classes/fflib_SObjectSelector2.cls | 73 +++++++++++++++++++ .../fflib_SObjectSelector2.cls-meta.xml | 5 ++ fflib/src/package.xml | 1 + 3 files changed, 79 insertions(+) create mode 100644 fflib/src/classes/fflib_SObjectSelector2.cls create mode 100644 fflib/src/classes/fflib_SObjectSelector2.cls-meta.xml diff --git a/fflib/src/classes/fflib_SObjectSelector2.cls b/fflib/src/classes/fflib_SObjectSelector2.cls new file mode 100644 index 00000000000..f65c161490c --- /dev/null +++ b/fflib/src/classes/fflib_SObjectSelector2.cls @@ -0,0 +1,73 @@ +/** + * 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 class fflib_SObjectSelector2 extends fflib_SObjectSelectorBase + implements fflib_ISObjectSelector +{ + /** + * Constructs the Selector, defaults to not including any FieldSet fields automatically + **/ + public fflib_SObjectSelector2() + { + super(false,false,false); + } + + /** + * Constructs the Selector + * + * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well + **/ + public fflib_SObjectSelector2(Boolean includeFieldSetFields) + { + super(includeFieldSetFields, true, false); + } + + /** + * Constructs the Selector + * + * @param includeFieldSetFields Set to true if the Selector queries are to include Fieldset fields as well + **/ + public fflib_SObjectSelector2(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS) + { + super(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 enforeFLS Enforce Field Level Security + * @param sortSelectFields Set to false if selecting many columns to skip sorting select fields and improve performance + **/ + public fflib_SObjectSelector2(Boolean includeFieldSetFields, Boolean enforceCRUD, Boolean enforceFLS, Boolean sortSelectFields) + { + super(includeFieldSetFields, enforceCRUD, enforceFLS, sortSelectFields); + } +} \ No newline at end of file diff --git a/fflib/src/classes/fflib_SObjectSelector2.cls-meta.xml b/fflib/src/classes/fflib_SObjectSelector2.cls-meta.xml new file mode 100644 index 00000000000..ce53270386b --- /dev/null +++ b/fflib/src/classes/fflib_SObjectSelector2.cls-meta.xml @@ -0,0 +1,5 @@ + + + 43.0 + Active + diff --git a/fflib/src/package.xml b/fflib/src/package.xml index 554788ad66a..286c945d2a6 100644 --- a/fflib/src/package.xml +++ b/fflib/src/package.xml @@ -14,6 +14,7 @@ fflib_SObjectDomainTest fflib_SObjectMocks fflib_SObjectSelector + fflib_SObjectSelector2 fflib_SObjectSelectorBase fflib_SObjectSelectorTest fflib_SObjectUnitOfWork