From 8b1526157127b8727e4931761aa019c0c4f7121a Mon Sep 17 00:00:00 2001 From: Robert Baillie Date: Thu, 16 Dec 2021 16:20:21 +0000 Subject: [PATCH 1/2] Fixed bug with the query factory not recognising an empty criteria when deciding if where clause should be added --- .../default/fflib/default/classes/common/fflib_QueryFactory.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/default/fflib/default/classes/common/fflib_QueryFactory.cls b/framework/default/fflib/default/classes/common/fflib_QueryFactory.cls index 2d2cc425322..0ac00d30090 100644 --- a/framework/default/fflib/default/classes/common/fflib_QueryFactory.cls +++ b/framework/default/fflib/default/classes/common/fflib_QueryFactory.cls @@ -660,7 +660,7 @@ public class fflib_QueryFactory { //No explicit sharing declaration - inherit fr } } result += ' FROM ' + (relationship != null ? relationship.getRelationshipName() : table.getDescribe().getName()); - if(conditionExpression != null) + if( String.isNotBlank( conditionExpression ) ) result += ' WHERE '+conditionExpression; if(order.size() > 0){ From c1556808e3cc6b00aa2d7d6b6ab4716aceccfde7 Mon Sep 17 00:00:00 2001 From: Robert Baillie Date: Thu, 16 Dec 2021 16:20:47 +0000 Subject: [PATCH 2/2] Added the Dynamic Sobject Selector, for building SOQL statements based on configuration --- .../ortoo_DynamicSobjectSelector.cls | 103 ++++++++++ .../ortoo_DynamicSobjectSelector.cls-meta.xml | 5 + .../ortoo_DynamicSobjectSelectorTest.cls | 182 ++++++++++++++++++ ...oo_DynamicSobjectSelectorTest.cls-meta.xml | 5 + 4 files changed, 295 insertions(+) create mode 100644 framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls create mode 100644 framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls-meta.xml create mode 100644 framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls create mode 100644 framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls-meta.xml diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls new file mode 100644 index 00000000000..26e224adddc --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls @@ -0,0 +1,103 @@ +/** + * Provides the ability to dynamically create a selector based on configuration. + * + * Should not be used in order to retrieve configuration of the managed app, but rather is for driving SOQL statements + * from that configuration - for example: + * The system contains configurations for how to match an email to a record in the system. + * That class can be used to turn that configuration into a SOQL statement that will retrieve that record. + * + * @group fflib Extension + */ +public inherited sharing class ortoo_DynamicSobjectSelector extends ortoo_SobjectSelector // NOPMD: specified a mini-namespace to differentiate from fflib versions +{ + List fieldList = new List(); + Schema.SObjectType sobjectType; + + // TODO: add the ability to add sub-queries + + /** + * Define the SObject Type that this selector will retrieve the data for. + * + * @param Schema.SObjectType The SObject Type that this instance will retrieve + * @return ortoo_DynamicSobjectSelector Itself, allowing for a fluent interface + */ + public ortoo_DynamicSobjectSelector setSobjectType( Schema.SObjectType sobjectType ) + { + Contract.requires( sobjectType != null, 'setSobjectType called with a null sobjectType' ); + this.sobjectType = sobjectType; + return this; + } + + /** + * Define the SObject Type that this selector will retrieve the data for, stated by the String representation. + * + * @param String The SObject Type that this instance will retrieve + * @return ortoo_DynamicSobjectSelector Itself, allowing for a fluent interface + */ + public ortoo_DynamicSobjectSelector setSobjectType( String sobjectTypeName ) + { + Contract.requires( sobjectTypeName != null, 'setSobjectType called with a null sobjectTypeName' ); + + Schema.SObjectType sobjectType = SobjectUtils.getSobjectType( sobjectTypeName ); + + Contract.requires( sobjectType != null, 'setSobjectType called with an sobjectTypeName that does not represent a valid SObject Type' ); + + return setSobjectType( sobjectType ); + } + + /** + * Add a field to be returned by the generated SOQL + * + * @param String + * @return ortoo_DynamicSobjectSelector Itself, allowing for a fluent interface + */ + public ortoo_DynamicSobjectSelector addField( String fieldToAdd ) + { + Contract.requires( fieldToAdd != null, 'addField called with a null fieldToAdd' ); + // could we check if the field is valid? May be hard with things like parent relationships + fieldList.add( fieldToAdd ); + return this; + } + + /** + * Retrieve the records that match the passed criteria. + * + * @param ortoo_Criteria The criteria that should be used to derive the records to return + * @return List The result of the Selection + */ + public List selectByCriteria( ortoo_Criteria criteria ) + { + Contract.requires( criteria != null, 'selectByCriteria called with a null criteria' ); + + Contract.assert( sobjectType != null, 'selectByCriteria called when sobjectType has not been set' ); + + return Database.query( generateSoqlByCriteria( criteria ) ); + } + + /** + * Required overload in order to make this a concrete class. + * Never returns any fields as the fields are added from the text representation instead, at the point of query. + * + * @return List The configured fields + */ + public List getSObjectFieldList() + { + return new List(); + } + + /** + * Return the SObject Type that this selector will return. + * + * @return Schema.SObjectType The configured SObject Type + */ + public Schema.SObjectType getSObjectType() + { + return sobjectType; + } + + @testVisible + private String generateSoqlByCriteria( ortoo_Criteria criteria ) + { + return newQueryFactory().selectFields( fieldList ).setCondition( criteria.toSOQL() ).toSOQL(); + } +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls-meta.xml b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_DynamicSobjectSelector.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active + diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls new file mode 100644 index 00000000000..b611ee8c7b4 --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls @@ -0,0 +1,182 @@ +@isTest +private without sharing class ortoo_DynamicSobjectSelectorTest +{ + @isTest + private static void selectByCriteria_whenTheSobjectHasBeenSetByName_willReturnAListOfSobjects() // NOPMD: Test method name format + { + ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector() + .setSobjectType( 'Contact' ); + + List returnedSobjects = selector.selectByCriteria( new ortoo_Criteria() ); + + System.assertEquals( new List(), returnedSobjects, 'selectByCriteria, when the SObject Type has been set by name, will return a list of SObjects' ); + } + + @isTest + private static void selectByCriteria_whenTheSobjectHasBeenSetByType_willReturnAListOfSobjects() // NOPMD: Test method name format + { + ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector() + .setSobjectType( Contact.sobjectType ); + + List returnedSobjects = selector.selectByCriteria( new ortoo_Criteria() ); + + System.assertEquals( new List(), returnedSobjects, 'selectByCriteria, when the SObject Type has been set by type, will return a list of SObjects' ); + } + + @isTest + private static void selectByCriteria_whenTheSobjectTypeHasNotBeenSet_willThrowAnException() // NOPMD: Test method name format + { + ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector(); + + Test.startTest(); + String exceptionMessage; + try + { + selector.selectByCriteria( new ortoo_Criteria() ); + } + catch ( Contract.AssertException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'selectByCriteria called when sobjectType has not been set', exceptionMessage, 'selectByCriteria, when the sobject type has not been set, will throw an exception' ); + } + + @isTest + private static void selectByCriteria_whenGivenANullCriteria_willThrowAnException() // NOPMD: Test method name format + { + ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector() + .setSobjectType( Contact.sobjectType ); + + Test.startTest(); + String exceptionMessage; + try + { + selector.selectByCriteria( null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'selectByCriteria called with a null criteria', exceptionMessage, 'selectByCriteria, when given a null criteria, will throw an exception' ); + } + + @isTest + private static void addField_whenGivenAString_willAddThatFieldToTheSelection() // NOPMD: Test method name format + { + ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector() + .setSobjectType( Contact.sobjectType ); + + Test.startTest(); + selector.addField( 'Name' ); + String generatedSoql = selector.generateSoqlByCriteria( new ortoo_Criteria() ); + Test.stopTest(); + + Amoss_asserts.assertStartsWith( 'SELECT Name FROM Contact', generatedSoql, 'addField, when given a string that represents a valid field, will add that field to the selection' ); + } + + @isTest + private static void addField_whenGivenMultipleStrings_willAddThoseFieldsToTheSelection() // NOPMD: Test method name format + { + ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector() + .setSobjectType( Contact.sobjectType ); + + Test.startTest(); + selector.addField( 'Name' ) + .addField( 'FirstName' ) + .addField( 'LastName' ); + String generatedSoql = selector.generateSoqlByCriteria( new ortoo_Criteria() ); + Test.stopTest(); + + Amoss_asserts.assertStartsWith( 'SELECT FirstName, LastName, Name FROM Contact', generatedSoql, 'addField, when given multiple strings that represent valid fields, will add them to the selection' ); + } + + @isTest + private static void addField_whenPassedANullString_willThrowAnException() // NOPMD: Test method name format + { + ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector() + .setSobjectType( Contact.sobjectType ); + + Test.startTest(); + String exceptionMessage; + try + { + selector.addField( null ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'addField called with a null fieldToAdd', exceptionMessage, 'addField, when passed a null field name, will throw an exception' ); + } + + @isTest + private static void setSobjectType_whenPassedANullString_willThrowAnException() // NOPMD: Test method name format + { + String nullString; + + ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector(); + + Test.startTest(); + String exceptionMessage; + try + { + selector.setSobjectType( nullString ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'setSobjectType called with a null sobjectTypeName', exceptionMessage, 'setSobjectType, when passed a null string, will throw an exception' ); + } + + @isTest + private static void setSobjectType_whenPassedAnInvalidString_willThrowAnException() // NOPMD: Test method name format + { + ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector(); + + Test.startTest(); + String exceptionMessage; + try + { + selector.setSobjectType( 'NotAnSObject' ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'setSobjectType called with an sobjectTypeName that does not represent a valid SObject Type', exceptionMessage, 'setSobjectType, when passed a string that does not represent an sobject, will throw an exception' ); + } + + @isTest + private static void setSobjectType_whenPassedANullSobjectType_willThrowAnException() // NOPMD: Test method name format + { + SobjectType nullSobjectType; + + ortoo_DynamicSobjectSelector selector = new ortoo_DynamicSobjectSelector(); + + Test.startTest(); + String exceptionMessage; + try + { + selector.setSobjectType( nullSobjectType ); + } + catch ( Contract.RequiresException e ) + { + exceptionMessage = e.getMessage(); + } + Test.stopTest(); + + Amoss_Asserts.assertContains( 'setSobjectType called with a null sobjectType', exceptionMessage, 'setSobjectType, when passed a null sobject type, will throw an exception' ); + } + +} \ No newline at end of file diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls-meta.xml new file mode 100644 index 00000000000..dd61d1f917e --- /dev/null +++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_DynamicSobjectSelectorTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 52.0 + Active +