diff --git a/framework/default/fflib-apex-extensions/default/classes/criteria/fflib_Criteria.cls b/framework/default/fflib-apex-extensions/default/classes/criteria/fflib_Criteria.cls index 6493afe008a..2a31c56c890 100755 --- a/framework/default/fflib-apex-extensions/default/classes/criteria/fflib_Criteria.cls +++ b/framework/default/fflib-apex-extensions/default/classes/criteria/fflib_Criteria.cls @@ -125,8 +125,7 @@ public virtual with sharing class fflib_Criteria public virtual fflib_Criteria addOrCriteria(fflib_Criteria subCriteria) { subCriteria.orCriteria(); - subCriteria.setEmbraced(true); - evaluators.add(subCriteria); + addCriteria( subCriteria ); return this; } @@ -152,6 +151,31 @@ public virtual with sharing class fflib_Criteria public virtual fflib_Criteria addAndCriteria(fflib_Criteria subCriteria) { subCriteria.andCriteria(); + addCriteria( subCriteria ); + return this; + } + + /** + * Adds a sub criteria, not altering the join (AND / OR) + * + * @param subCriteria The condition of the sub criteria + * + * @return An instance of itself to enable method chaining + * + * @example + * new fflib_Criteria() + * .orCriteria() + * .equalTo(Account.Name, 'Example') + * .addCriteria( + * new fflib_Criteria() + * .equalTo(Account.AccountNumber, '0001') + * .equalTo(Account.ShippingCountry, 'USA')) + * + * Evaluates: + * Name = 'Example' OR (AccountNumber = '0001' AND ShippingCountry = 'USA') + */ + public fflib_Criteria addCriteria(fflib_Criteria subCriteria) + { subCriteria.setEmbraced(true); evaluators.add(subCriteria); return this; diff --git a/framework/default/fflib-apex-extensions/default/classes/tests/fflib_CriteriaTest.cls b/framework/default/fflib-apex-extensions/default/classes/tests/fflib_CriteriaTest.cls index f84b9b0733f..90e0f9be041 100755 --- a/framework/default/fflib-apex-extensions/default/classes/tests/fflib_CriteriaTest.cls +++ b/framework/default/fflib-apex-extensions/default/classes/tests/fflib_CriteriaTest.cls @@ -462,6 +462,23 @@ private with sharing class fflib_CriteriaTest ); } + @IsTest + static void itShouldEvaluateAddCondition_First() + { + System.assert( + new fflib_Criteria() + .orCriteria() + .equalTo(Account.Name, A_STRING) + .equalTo(Account.Name, ANOTHER_STRING) + .addCriteria( + new fflib_Criteria() + .equalTo(Account.Name, HELLO_WORLD) + .equalTo(Account.AccountNumber, '002') + ) + .evaluate(new Account(Name = ANOTHER_STRING)) + ); + } + @IsTest static void itShouldEvaluateAddAndCondition_First() { diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_Criteria.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_Criteria.cls index 77978eaccb1..2a1fbffafca 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_Criteria.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_Criteria.cls @@ -100,6 +100,33 @@ public inherited sharing virtual class ortoo_Criteria implements ISearchCriteria return this; } + /** + * Adds a sub-criteria + * + * @param ortoo_Criteria The condition of the sub criteria + * @return ortoo_Criteria Itself, allowing for a fluent interface + * + * @example + * new ortoo_Criteria() + * .orCriteria() + * .equalTo(Account.Name, 'Example') + * .addCriteria( + * new ortoo_Criteria() + * .equalTo(Account.AccountNumber, '0001') + * .equalTo(Account.ShippingCountry, 'USA')) + * + * Evaluates: + * Name = 'Example' OR (AccountNumber = '0001' AND ShippingCountry = 'USA') + */ + public ortoo_Criteria addCriteria( ortoo_Criteria subCriteria ) + { + Contract.requires( subCriteria != null, 'addCriteria called with a null subCriteria' ); + Contract.requires( subCriteria.criteria != null, 'addCriteria called with a subCriteria that has no criteria defined' ); + + criteria.addCriteria( subCriteria.criteria ); + return this; + } + /** * Adds a sub-criteria with AND comparator * diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls index 15ca978aee8..33df156ef6f 100644 --- a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls @@ -94,6 +94,48 @@ private without sharing class ortoo_CriteriaTest System.assertEquals( expected, got, 'andAndCriteria, when given sub criteria, will add the subcriteria set as AND criteria' ); } + @isTest + private static void andCriteria_whenGivenSubCriteria_addsTheSubCriteriaSetAsOriginalSpec() // NOPMD: Test method name format + { + ortoo_Criteria criteria = new ortoo_Criteria(); + + Test.startTest(); + String got = criteria + .equalTo(Account.Name, 'Example') + .addCriteria( + new ortoo_Criteria() + .andCriteria() + .equalTo(Account.AccountNumber, '0001') + .equalTo(Account.AccountNumber, '0002')) + .toSoql(); + Test.stopTest(); + + String expected = 'Name=\'Example\' AND (AccountNumber=\'0001\' AND AccountNumber=\'0002\')'; + + System.assertEquals( expected, got, 'andCriteria, when given sub criteria, will add the subcriteria set as originally configured (was and)' ); + } + + @isTest + private static void andCriteria_whenGivenSubCriteria_addsTheSubCriteriaSetAsOriginalSpec_2() // NOPMD: Test method name format + { + ortoo_Criteria criteria = new ortoo_Criteria(); + + Test.startTest(); + String got = criteria + .equalTo(Account.Name, 'Example') + .addCriteria( + new ortoo_Criteria() + .orCriteria() + .equalTo(Account.AccountNumber, '0001') + .equalTo(Account.AccountNumber, '0002')) + .toSoql(); + Test.stopTest(); + + String expected = 'Name=\'Example\' AND (AccountNumber=\'0001\' OR AccountNumber=\'0002\')'; + + System.assertEquals( expected, got, 'andCriteria, when given sub criteria, will add the subcriteria set as originally configured (was or)' ); + } + @isTest private static void formulaCriteria_whenGivenCriteria_addsTheCriteriaInLineWithTheFormula() // NOPMD: Test method name format { diff --git a/framework/default/ortoo-core/default/classes/services/dml/DmlChildContext.cls b/framework/default/ortoo-core/default/classes/services/dml/DmlChildContext.cls index f5a6be8ca14..75aa6567021 100644 --- a/framework/default/ortoo-core/default/classes/services/dml/DmlChildContext.cls +++ b/framework/default/ortoo-core/default/classes/services/dml/DmlChildContext.cls @@ -105,4 +105,10 @@ public inherited sharing class DmlChildContext uow.registerRelationship( child, relatedByField, parent ); } } + + @testVisible + private SobjectField getRelatedByField() + { + return relatedByField; + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/services/dml/DmlDefiner.cls b/framework/default/ortoo-core/default/classes/services/dml/DmlDefiner.cls index 4edabfe2c85..e04639f6209 100644 --- a/framework/default/ortoo-core/default/classes/services/dml/DmlDefiner.cls +++ b/framework/default/ortoo-core/default/classes/services/dml/DmlDefiner.cls @@ -188,6 +188,16 @@ public inherited sharing class DmlDefiner return sobjects; } + /** + * States if this definer is configured to ensure that other records are deleted + * + * @return Boolean Will other records be deleted + */ + public Boolean getWillDeleteOtherRecords() { + Contract.assert( options != null, 'getWillDeleteOtherRecords was called when options was null' ); + return options.getWillDeleteOtherRecords(); + } + @testVisible private List getDmlRecords() { return recordsToDml; diff --git a/framework/default/ortoo-core/default/classes/services/dml/DmlDefinerOptions.cls b/framework/default/ortoo-core/default/classes/services/dml/DmlDefinerOptions.cls index 58bb3709b41..8ebd3c091cf 100644 --- a/framework/default/ortoo-core/default/classes/services/dml/DmlDefinerOptions.cls +++ b/framework/default/ortoo-core/default/classes/services/dml/DmlDefinerOptions.cls @@ -43,4 +43,14 @@ public inherited sharing class DmlDefinerOptions return new DmlDefinerOptions() .setOtherRecordsMode( OtherRecordsOption.IGNORE_RECORDS ); } + + /** + * States if this definer will instruct to delete other records + * + * @return Boolean Should other records be deleted? + */ + public Boolean getWillDeleteOtherRecords() + { + return getOtherRecordsMode() == OtherRecordsOption.DELETE_RECORDS; + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/services/dml/DmlRecord.cls b/framework/default/ortoo-core/default/classes/services/dml/DmlRecord.cls index d430b40dec7..d331046d0d4 100644 --- a/framework/default/ortoo-core/default/classes/services/dml/DmlRecord.cls +++ b/framework/default/ortoo-core/default/classes/services/dml/DmlRecord.cls @@ -159,10 +159,27 @@ public virtual inherited sharing class DmlRecord @testVisible protected DmlDefiner getChildDefiner( String childRecordType ) { + Contract.requires( childRecordType != null, 'getChildDefiner called with a null childRecordType' ); + checkTypeIsValid( childRecordType ); return this.childRecordsByType.get( childRecordType ); } + + /** + * Returns the Sobjects that represent the child records for the given relationship + * + * @param String The 'Type' that will identify the child relationship for which to return + * @return List The List representing the child records currently on this DmlRecord + */ + @testVisible + protected List getChildSobjects( String childRecordType ) + { + Contract.requires( childRecordType != null, 'getChildSobjects called with a null childRecordType' ); + + return getChildDefiner( childRecordType ).getSobjects(); + } + /** * Sets the DmlDefinerOptions for the given child relationship. * @@ -266,4 +283,10 @@ public virtual inherited sharing class DmlRecord checkTypeIsValid( childRecordType ); return childContextsByType.get( childRecordType ); } + + @testVisible + private Boolean getWillDeleteOtherRecords( String childRecordType ) + { + return getChildDefiner( childRecordType )?.getWillDeleteOtherRecords(); + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/services/dml/tests/DmlDefinerOptionsTest.cls b/framework/default/ortoo-core/default/classes/services/dml/tests/DmlDefinerOptionsTest.cls index 9bb441ad244..51cb51010ba 100644 --- a/framework/default/ortoo-core/default/classes/services/dml/tests/DmlDefinerOptionsTest.cls +++ b/framework/default/ortoo-core/default/classes/services/dml/tests/DmlDefinerOptionsTest.cls @@ -39,4 +39,30 @@ private without sharing class DmlDefinerOptionsTest ortoo_Asserts.assertContains( 'setOtherRecordsMode called with a null otherRecordsMode', exceptionMessage, 'setOtherRecordsMode, when called with null, will throw an exception' ); } + + @isTest + private static void getWillDeleteOtherRecords_whenRecordModeIsDelete_returnsTrue() // NOPMD: Test method name format + { + DmlDefinerOptions options = new DmlDefinerOptions(); + + Test.startTest(); + options.setOtherRecordsMode( DmlDefinerOptions.OtherRecordsOption.DELETE_RECORDS ); + Boolean got = options.getWillDeleteOtherRecords(); + Test.stopTest(); + + System.assertEquals( true, got, 'getWillDeleteOtherRecords, when the record mode is DELETE_RECORDS, will return true' ); + } + + @isTest + private static void getWillDeleteOtherRecords_whenRecordModeIsIgnore_returnsFalse() // NOPMD: Test method name format + { + DmlDefinerOptions options = new DmlDefinerOptions(); + + Test.startTest(); + options.setOtherRecordsMode( DmlDefinerOptions.OtherRecordsOption.IGNORE_RECORDS ); + Boolean got = options.getWillDeleteOtherRecords(); + Test.stopTest(); + + System.assertEquals( false, got, 'getWillDeleteOtherRecords, when the record mode is IGNORE_RECORDS, will return false' ); + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/services/dml/tests/DmlDefinerTest.cls b/framework/default/ortoo-core/default/classes/services/dml/tests/DmlDefinerTest.cls index 8a38bcfa890..69ee840dab9 100644 --- a/framework/default/ortoo-core/default/classes/services/dml/tests/DmlDefinerTest.cls +++ b/framework/default/ortoo-core/default/classes/services/dml/tests/DmlDefinerTest.cls @@ -311,4 +311,24 @@ private without sharing class DmlDefinerTest ortoo_Asserts.assertContains( 'addPreSaveAction called with a null action', exceptionMessage, 'addPreSaveAction, when given null, will throw an exception' ); } + + @isTest + private static void getWillDeleteOtherRecords_willReturnTheValueFromTheDefiner() // NOPMD: Test method name format + { + DmlDefiner dmlDefiner = new DmlDefiner(); + + Amoss_Instance optionsController = new Amoss_Instance( DmlDefinerOptions.class ); + optionsController + .expects( 'getWillDeleteOtherRecords' ) + .returning( true ); + + Test.startTest(); + dmlDefiner.setOptions( (DmlDefinerOptions)optionsController.generateDouble() ); + Boolean got = dmlDefiner.getWillDeleteOtherRecords(); + Test.stopTest(); + + optionsController.verify(); + + System.assertEquals( true, got, 'getWillDeleteOtherRecords, will call getWillDeleteOtherRecords on the options and return the result' ); + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/services/dml/tests/DmlRecordTest.cls b/framework/default/ortoo-core/default/classes/services/dml/tests/DmlRecordTest.cls index eeec1a08978..fb1151e9174 100644 --- a/framework/default/ortoo-core/default/classes/services/dml/tests/DmlRecordTest.cls +++ b/framework/default/ortoo-core/default/classes/services/dml/tests/DmlRecordTest.cls @@ -520,6 +520,61 @@ private without sharing class DmlRecordTest ortoo_Asserts.assertContains( 'Invalid', exceptionMessage, 'getChildDefiner, when given a child type that does not exist, will throw an exception' ); } + @isTest + private static void getChildSobjects_whenGivenAValidChildType_willReturnTheChildrenForThatType() // NOPMD: Test method name format + { + DmlRecord dmlRecord = new DmlRecord( new Account() ); + dmlRecord.addChildContext( 'Contacts', new DmlChildContext( Contact.AccountId, IChildRecordFinder.class ) ); + + List contacts = new List{ + new Contact( LastName = 'name1' ), + new Contact( LastName = 'name2' ) + }; + + Test.startTest(); + dmlRecord + .addChild( 'Contacts', new DmlRecord( contacts[0] ) ) + .addChild( 'Contacts', new DmlRecord( contacts[1] ) ); + + List got = dmlRecord.getChildSobjects( 'Contacts' ); + Test.stopTest(); + + System.assertEquals( contacts, got, 'getChildSobjects, when given a valid child type, will return the sobjects registered for that type' ); + } + + @isTest + private static void getChildSobjects_whenGivenAValidChildTypeThatHasNoChildren_willReturnAnEmptyList() // NOPMD: Test method name format + { + DmlRecord dmlRecord = new DmlRecord( new Account() ); + dmlRecord.addChildContext( 'Contacts', new DmlChildContext( Contact.AccountId, IChildRecordFinder.class ) ); + + Test.startTest(); + List got = dmlRecord.getChildSobjects( 'Contacts' ); + Test.stopTest(); + + System.assertEquals( new List(), got, 'getChildSobjects, when given a valid child type that has no children, will return an empty list' ); + } + + @isTest + private static void getChildSobjects_whenGivenAChildTypeThatDoesNotExist_willThrowAnException() // NOPMD: Test method name format + { + DmlRecord dmlRecord = new DmlRecord( new Account() ); + + Test.startTest(); + String exceptionMessage; + try + { + dmlRecord.getChildSobjects( 'Invalid' ); + } + catch ( Contract.AssertException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + ortoo_Asserts.assertContains( 'Invalid', exceptionMessage, 'getChildSobjects, when given a child type that does not exist, will throw an exception' ); + } + @isTest private static void setChildDmlOptions_whenGivenADmlDefinerOptions_willSetThatOnTheChildDefiner() // NOPMD: Test method name format { diff --git a/framework/default/ortoo-core/default/classes/utils/StringUtils.cls b/framework/default/ortoo-core/default/classes/utils/StringUtils.cls index 9647fbe3787..7c829dcd090 100644 --- a/framework/default/ortoo-core/default/classes/utils/StringUtils.cls +++ b/framework/default/ortoo-core/default/classes/utils/StringUtils.cls @@ -115,4 +115,24 @@ public inherited sharing class StringUtils return String.join( ListUtils.convertToListOfStrings( listOfObjects ), delimiter ); } + + /** + * Given a String, will safely cut it to the desired length, returning the original string if it is already shorter + * + * @param String The string to cut + * @param Integer The length to cut the string to + * @return String The cut string + */ + public static String cut( String originalString, Integer length ) + { + Contract.requires( originalString != null, 'cut called with a null originalString' ); + Contract.requires( length != null, 'cut called with a null length' ); + Contract.requires( length >= 0, 'cut called with a negative length' ); + + if ( originalString.length() < length ) + { + return originalString; + } + return originalString.substring( 0, length ); + } } \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/utils/tests/StringUtilsTest.cls b/framework/default/ortoo-core/default/classes/utils/tests/StringUtilsTest.cls index 6bb167deb21..31d3c5ad251 100644 --- a/framework/default/ortoo-core/default/classes/utils/tests/StringUtilsTest.cls +++ b/framework/default/ortoo-core/default/classes/utils/tests/StringUtilsTest.cls @@ -377,6 +377,74 @@ private without sharing class StringUtilsTest ortoo_Asserts.assertContains( 'convertListToDelimitedString called with an empty delimiter', exceptionMessage, 'convertListToDelimitedString, when passed an empty delimiter, will throw an exception' ); } + @isTest + private static void cut_whenGivenALongString_cutsItToTheDesiredLength() // NOPMD: Test method name format + { + String got = StringUtils.cut( 'theoriginalstring', 5 ); + System.assertEquals( 'theor', got, 'cut, when given a string longer than the desired length, will cut it to the specified length' ); + } + + @isTest + private static void cut_whenGivenAShortString_returnsTheOriginal() // NOPMD: Test method name format + { + String got = StringUtils.cut( 'short', 10 ); + System.assertEquals( 'short', got, 'cut, when given a string shorter than the desired length, will return the original string' ); + } + + @isTest + private static void cut_whenPassedANullString_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + StringUtils.cut( null, 5 ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + ortoo_Asserts.assertContains( 'cut called with a null originalString', exceptionMessage, 'cut, when passed a null string, will throw an exception' ); + } + + @isTest + private static void cut_whenPassedANullLength_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + StringUtils.cut( 'string', null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + ortoo_Asserts.assertContains( 'cut called with a null length', exceptionMessage, 'cut, when passed a null length, will throw an exception' ); + } + + @isTest + private static void cut_whenPassedANegativeLength_throwsAnException() // NOPMD: Test method name format + { + Test.startTest(); + String exceptionMessage; + try + { + StringUtils.cut( 'string', -3 ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + ortoo_Asserts.assertContains( 'cut called with a negative length', exceptionMessage, 'cut, when passed a negative length, will throw an exception' ); + } + private inherited sharing class StringableThing { String name; public StringableThing( String name ) diff --git a/framework/default/sobject-fabricator/classes/ortoo_FabricatedSObjectRegister.cls b/framework/default/sobject-fabricator/classes/ortoo_FabricatedSObjectRegister.cls index 12761b57f7f..d282b9495d6 100644 --- a/framework/default/sobject-fabricator/classes/ortoo_FabricatedSObjectRegister.cls +++ b/framework/default/sobject-fabricator/classes/ortoo_FabricatedSObjectRegister.cls @@ -75,10 +75,15 @@ public class ortoo_FabricatedSObjectRegister { @testVisible private inherited sharing class RegisterInstance { - private List objectRegister = new List(); - private List relationships = new List(); + private List objectRegister; + private List relationships; private Map sobjectsByFabricated; - private DirectedGraph graph = new DirectedGraph(); + private DirectedGraph graph; + + public RegisterInstance() + { + initialise(); + } public void registerObject( sfab_FabricatedSObject objectToRegister ) { @@ -110,6 +115,15 @@ public class ortoo_FabricatedSObjectRegister { registerInserts( uow ); registerRelationships( uow ); uow.commitWork(); + + initialise(); + } + + private void initialise() + { + objectRegister = new List(); + relationships = new List(); + graph = new DirectedGraph(); } @testVisible diff --git a/framework/default/sobject-fabricator/classes/sfab_FabricatedSObject.cls b/framework/default/sobject-fabricator/classes/sfab_FabricatedSObject.cls index ce1c455d7c6..ec6971f7acf 100644 --- a/framework/default/sobject-fabricator/classes/sfab_FabricatedSObject.cls +++ b/framework/default/sobject-fabricator/classes/sfab_FabricatedSObject.cls @@ -25,6 +25,8 @@ public virtual class sfab_FabricatedSObject { public class NodeNotSetException extends Exception {} + Sobject persistedSobject; + /** * Constructs a FabricatedSObject of the given type. * @@ -35,6 +37,12 @@ public virtual class sfab_FabricatedSObject { ortoo_FabricatedSObjectRegister.registerObject( this ); } + public Id persistedId { + get { + return persistedSobject?.Id; + } + } + /** * Constructs a FabricatedSObject of the given type, and then sets the fields specified in the given map. * @@ -306,9 +314,9 @@ public virtual class sfab_FabricatedSObject { * * @return SObject - The built SObject */ - public SObject toPersistableSobject() - { - return internalToSobject( true ); + public SObject toPersistableSobject() { + persistedSobject = internalToSobject( true ); + return persistedSobject; } private Sobject internalToSobject( Boolean persistable )