Skip to content

Commit

Permalink
Merge pull request apex-enterprise-patterns#5 from OrtooApps/feature/…
Browse files Browse the repository at this point in the history
…dynamic-selector

Feature/dynamic selector
  • Loading branch information
rob-baillie-ortoo authored Dec 17, 2021
2 parents 6343a26 + c155680 commit 7151a74
Show file tree
Hide file tree
Showing 5 changed files with 296 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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){
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> fieldList = new List<String>();
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<Sobject> The result of the Selection
*/
public List<Sobject> 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<Schema.SObjectField> The configured fields
*/
public List<Schema.SObjectField> getSObjectFieldList()
{
return new List<Schema.SObjectField>();
}

/**
* 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexClass>
Original file line number Diff line number Diff line change
@@ -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<Sobject> returnedSobjects = selector.selectByCriteria( new ortoo_Criteria() );

System.assertEquals( new List<Sobject>(), 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<Sobject> returnedSobjects = selector.selectByCriteria( new ortoo_Criteria() );

System.assertEquals( new List<Sobject>(), 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' );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>52.0</apiVersion>
<status>Active</status>
</ApexClass>

0 comments on commit 7151a74

Please sign in to comment.