From 4ce9495d4b79bd617157a80b1f3452f45d86e9bd Mon Sep 17 00:00:00 2001
From: Robert Baillie
Date: Fri, 21 Jan 2022 12:45:58 +0000
Subject: [PATCH 1/3] Added framework changes from the example-app
implementation of the filter and paging screen
---
.../classes/criteria/fflib_Criteria.cls | 70 ++--
.../classes/ApplicationMockRegistrar.cls | 4 +-
.../default/classes/FrameworkErrorCodes.cls | 3 +
.../fflib-extension/ortoo_Criteria.cls | 15 +-
.../fflib-extension/ortoo_SobjectSelector.cls | 51 ++-
.../classes/search/ISearchConfiguration.cls | 16 +
.../search/ISearchConfiguration.cls-meta.xml | 5 +
.../classes/search/ISearchCriteria.cls | 7 +
.../search/ISearchCriteria.cls-meta.xml | 5 +
.../classes/search/ISearchCriteriaFactory.cls | 9 +
.../ISearchCriteriaFactory.cls-meta.xml | 5 +
.../default/classes/search/ISearchResult.cls | 6 +
.../classes/search/ISearchResult.cls-meta.xml | 5 +
.../classes/search/ISearchResultBuilder.cls | 8 +
.../search/ISearchResultBuilder.cls-meta.xml | 5 +
.../classes/search/ISearchSelector.cls | 12 +
.../search/ISearchSelector.cls-meta.xml | 5 +
.../classes/search/SearchConfiguration.cls | 89 +++++
.../search/SearchConfiguration.cls-meta.xml | 5 +
.../classes/search/SearchController.cls | 69 ++++
.../search/SearchController.cls-meta.xml | 5 +
.../default/classes/search/SearchOrderBy.cls | 60 +++
.../classes/search/SearchOrderBy.cls-meta.xml | 5 +
.../default/classes/search/SearchResults.cls | 32 ++
.../classes/search/SearchResults.cls-meta.xml | 5 +
.../default/classes/search/SearchWindow.cls | 53 +++
.../classes/search/SearchWindow.cls-meta.xml | 5 +
.../search/tests/SearchConfigurationTest.cls | 262 +++++++++++++
.../SearchConfigurationTest.cls-meta.xml | 5 +
.../search/tests/SearchControllerTest.cls | 237 ++++++++++++
.../tests/SearchControllerTest.cls-meta.xml | 5 +
.../search/tests/SearchOrderByTest.cls | 294 ++++++++++++++
.../tests/SearchOrderByTest.cls-meta.xml | 5 +
.../search/tests/SearchResultsTest.cls | 103 +++++
.../tests/SearchResultsTest.cls-meta.xml | 5 +
.../classes/search/tests/SearchWindowTest.cls | 360 ++++++++++++++++++
.../tests/SearchWindowTest.cls-meta.xml | 5 +
.../search-service/ISearchService.cls | 5 +
.../ISearchService.cls-meta.xml | 5 +
.../services/search-service/SearchService.cls | 17 +
.../search-service/SearchService.cls-meta.xml | 5 +
.../search-service/SearchServiceImpl.cls | 55 +++
.../SearchServiceImpl.cls-meta.xml | 5 +
.../tests/SearchServiceImplTest.cls | 182 +++++++++
.../tests/SearchServiceImplTest.cls-meta.xml | 5 +
.../tests/SearchServiceMockDomain.cls | 29 ++
.../SearchServiceMockDomain.cls-meta.xml | 5 +
.../tests/SearchServiceMockSearchSelector.cls | 24 ++
...archServiceMockSearchSelector.cls-meta.xml | 5 +
.../default/classes/utils/Contract.cls | 6 +-
.../default/classes/utils/StringUtils.cls | 17 +
.../classes/utils/tests/StringUtilsTest.cls | 15 +
...n_Configuration.Search_Service.md-meta.xml | 17 +
.../ortoo-core-CustomLabels.labels-meta.xml | 11 +-
.../__tests__/datatableHelper.test.js | 232 +++++++++++
.../lwc/datatableHelper/datatableHelper.js | 59 +++
.../datatableHelper.js-meta.xml | 5 +
.../__tests__/errorRenderer.test.js | 8 +
.../lwc/errorRenderer/errorRenderer.js | 11 +-
.../filterAndResults/filterAndResults.html | 1 -
.../__tests__/paginationControls.test.js | 175 ++++-----
.../paginationControls/paginationControls.css | 1 +
.../paginationControls.html | 2 +-
.../paginationControls/paginationControls.js | 3 +-
64 files changed, 2599 insertions(+), 146 deletions(-)
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchResult.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchResult.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchSelector.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/ISearchSelector.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/SearchController.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/SearchController.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/SearchResults.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/SearchResults.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/SearchWindow.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/SearchWindow.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchConfigurationTest.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchConfigurationTest.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchControllerTest.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchControllerTest.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchResultsTest.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchResultsTest.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchWindowTest.cls
create mode 100644 framework/default/ortoo-core/default/classes/search/tests/SearchWindowTest.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/ISearchService.cls
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/ISearchService.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/SearchService.cls
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/SearchService.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/SearchServiceImpl.cls
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/SearchServiceImpl.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceImplTest.cls
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceImplTest.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockDomain.cls
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockDomain.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockSearchSelector.cls
create mode 100644 framework/default/ortoo-core/default/classes/services/search-service/tests/SearchServiceMockSearchSelector.cls-meta.xml
create mode 100644 framework/default/ortoo-core/default/customMetadata/Application_Configuration.Search_Service.md-meta.xml
create mode 100644 framework/default/ortoo-core/default/lwc/datatableHelper/__tests__/datatableHelper.test.js
create mode 100644 framework/default/ortoo-core/default/lwc/datatableHelper/datatableHelper.js
create mode 100644 framework/default/ortoo-core/default/lwc/datatableHelper/datatableHelper.js-meta.xml
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 fcd07f0e31d..209357e72e0 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
@@ -54,6 +54,12 @@ public virtual with sharing class fflib_Criteria
this.type = 'AND';
}
+ // TODO: document and test
+ protected void addEvaluator( Evaluator evaluator )
+ {
+ evaluators.add( evaluator );
+ }
+
/**
* Changes the default comparator for each criteria to OR
*
@@ -295,7 +301,7 @@ public virtual with sharing class fflib_Criteria
}
}
- private class FormulaNumberEvaluator implements IFormulaEvaluator
+ public class FormulaNumberEvaluator implements IFormulaEvaluator
{
private Integer expressionNumber;
@@ -737,7 +743,7 @@ public virtual with sharing class fflib_Criteria
else if (operator == fflib_Operator.GREATER_THAN_OR_EQUAL_TO)
return '>=';
else if (operator == fflib_Operator.LIKEx)
- return ' like ';
+ return ' LIKE ';
else if (operator == fflib_Operator.INx)
return ' IN ';
else if (operator == fflib_Operator.NOT_IN)
@@ -817,7 +823,7 @@ public virtual with sharing class fflib_Criteria
return result + ')';
}
- private interface Evaluator
+ public interface Evaluator
{
Boolean evaluate(Object obj);
String toSOQL();
@@ -826,7 +832,7 @@ public virtual with sharing class fflib_Criteria
/**
* Generic criteria handler for comparing against sets
*/
- private virtual class FieldSetEvaluator implements Evaluator
+ public virtual class FieldSetEvaluator implements Evaluator
{
private Schema.SObjectField sObjectField;
private fflib_Objects values;
@@ -851,14 +857,14 @@ public virtual with sharing class fflib_Criteria
public String toSOQL()
{
- return String.format(
- '{0} {1} {2}',
- new List
- {
- this.sObjectField.getDescribe().getName(),
- operatorToString(this.operator),
- toLiteral(this.values)
- }
+ return String.join(
+ new List
+ {
+ this.sObjectField.getDescribe().getName(),
+ operatorToString(this.operator),
+ toLiteral(this.values)
+ },
+ ''
);
}
}
@@ -866,7 +872,7 @@ public virtual with sharing class fflib_Criteria
/**
* Generic field Evaluator
*/
- private class FieldEvaluator implements Evaluator
+ public class FieldEvaluator implements Evaluator
{
private Schema.SObjectField sObjectField;
private Object value;
@@ -903,7 +909,7 @@ public virtual with sharing class fflib_Criteria
}
}
- private abstract class AbstractRelatedFieldEvaluator implements Evaluator
+ public abstract class AbstractRelatedFieldEvaluator implements Evaluator
{
protected String fieldName;
protected fflib_Operator operator;
@@ -926,7 +932,7 @@ public virtual with sharing class fflib_Criteria
}
- private class RelatedFieldEvaluator extends AbstractRelatedFieldEvaluator
+ public class RelatedFieldEvaluator extends AbstractRelatedFieldEvaluator
{
private Object value;
@@ -950,18 +956,18 @@ public virtual with sharing class fflib_Criteria
public String toSOQL()
{
return String.join(
- new List
- {
- this.fieldName,
- operatorToString(this.operator),
- toLiteral(this.value)
- },
- ''
+ new List
+ {
+ this.fieldName,
+ operatorToString(this.operator),
+ toLiteral(this.value)
+ },
+ ''
);
}
}
- private class RelatedFieldSetEvaluator extends AbstractRelatedFieldEvaluator
+ public class RelatedFieldSetEvaluator extends AbstractRelatedFieldEvaluator
{
private fflib_Objects values;
@@ -984,19 +990,19 @@ public virtual with sharing class fflib_Criteria
public String toSOQL()
{
- return String.format(
- '{0} {1} {2}',
- new List
- {
- this.fieldName,
- operatorToString(this.operator),
- toLiteral(this.values)
- }
+ return String.join(
+ new List
+ {
+ this.fieldName,
+ operatorToString(this.operator),
+ toLiteral(this.values)
+ },
+ ''
);
}
}
- private class PropertyEvaluator implements Evaluator
+ public class PropertyEvaluator implements Evaluator
{
private Object property;
private Object value;
diff --git a/framework/default/ortoo-core/default/classes/ApplicationMockRegistrar.cls b/framework/default/ortoo-core/default/classes/ApplicationMockRegistrar.cls
index 68456611c12..8e1bf17c6d7 100644
--- a/framework/default/ortoo-core/default/classes/ApplicationMockRegistrar.cls
+++ b/framework/default/ortoo-core/default/classes/ApplicationMockRegistrar.cls
@@ -60,7 +60,7 @@ public inherited sharing class ApplicationMockRegistrar
* @param Type The domain type (class) that the mock should be generated as
* @return Amoss_Instance The controller for the mock Domain
*/
- private static Amoss_Instance registerMockDomain( SobjectType sobjectType, Type domainType )
+ public static Amoss_Instance registerMockDomain( SobjectType sobjectType, Type domainType )
{
Amoss_Instance mockDomainController = new Amoss_Instance( domainType );
mockDomainController
@@ -187,7 +187,7 @@ public inherited sharing class ApplicationMockRegistrar
* @param Type The selector type (class) that the mock should be generated as
* @return Amoss_Instance The controller for the mock selector
*/
- private static Amoss_Instance registerMockSelector( SobjectType sobjectType, Type selectorType )
+ public static Amoss_Instance registerMockSelector( SobjectType sobjectType, Type selectorType )
{
Amoss_Instance mockSelectorController = new Amoss_Instance( selectorType );
mockSelectorController
diff --git a/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls b/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls
index 70fc8349e9e..101110e471f 100644
--- a/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls
+++ b/framework/default/ortoo-core/default/classes/FrameworkErrorCodes.cls
@@ -27,4 +27,7 @@ public inherited sharing class FrameworkErrorCodes {
public final static String DML_UPDATE_NOT_ALLOWED = '00002';
public final static String DML_DELETE_NOT_ALLOWED = '00003';
public final static String DML_PUBLISH_NOT_ALLOWED = '00004';
+
+ public final static String SELECTOR_UNBOUND_COUNT_QUERY = '0000000';
+
}
\ No newline at end of file
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 c46a116c452..d32ce4860a7 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
@@ -3,6 +3,19 @@
*
* @group fflib Extension
*/
-public inherited sharing virtual class ortoo_Criteria extends fflib_Criteria // NOPMD: specified a mini-namespace to differentiate from fflib versions
+public inherited sharing virtual class ortoo_Criteria extends fflib_Criteria implements ISearchCriteria // NOPMD: specified a mini-namespace to differentiate from fflib versions
{
+
+ public virtual ortoo_Criteria likeString( Schema.SObjectField field, Object value )
+ {
+ addEvaluator( new FieldEvaluator( field, fflib_Operator.LIKEx, value ) );
+ return this;
+ }
+
+ public virtual ortoo_Criteria likeString( String relatedField, Object value )
+ {
+ addEvaluator( new RelatedFieldEvaluator( relatedField, fflib_Operator.LIKEx, value ) );
+ return this;
+ }
+
}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls
index d482ddca9d5..986d8063591 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls
@@ -5,8 +5,10 @@
*
* @group fflib Extension
*/
-public abstract inherited sharing class ortoo_SobjectSelector extends fflib_SobjectSelector // NOPMD: specified a mini-namespace to differentiate from fflib versions
+public abstract inherited sharing class ortoo_SobjectSelector extends fflib_SobjectSelector implements ISearchSelector // NOPMD: specified a mini-namespace to differentiate from fflib versions
{
+ class UnboundCountQueryException extends Exceptions.SelectorException {}
+
public ortoo_SobjectSelector()
{
super();
@@ -23,4 +25,51 @@ public abstract inherited sharing class ortoo_SobjectSelector extends fflib_Sobj
m_enforceFLS = false;
return this;
}
+
+ // TODO: document
+ // TODO: test
+ // TODO: security
+ public SearchResults selectBySearchCriteria( ISearchConfiguration searchConfiguration, ISearchCriteria criteria, SearchWindow window, SearchOrderBy orderBy )
+ {
+ Contract.requires( criteria != null, 'selectByCriteria called with a null criteria' );
+
+ Integer countOfRecords = getCountOfRecords( criteria );
+
+ List resultsRecords = new List();
+
+ if ( countOfRecords > 0 )
+ {
+ fflib_QueryFactory queryFactory = newQueryFactory().setCondition( criteria.toSOQL() );
+
+ queryFactory.selectFields( searchConfiguration.getRequiredFields() );
+ queryFactory.setOffset( window.offset );
+ queryFactory.setLimit( window.length );
+ queryFactory.setOrdering( orderBy.fieldName , orderBy.direction == 'desc' ? fflib_QueryFactory.SortOrder.DESCENDING : fflib_QueryFactory.SortOrder.ASCENDING );
+
+ resultsRecords = Database.query( queryFactory.toSOQL() );
+ }
+
+ return new SearchResults( countOfRecords, resultsRecords );
+ }
+
+ // TODO: document
+ // TODO: test
+ protected Integer getCountOfRecords( ISearchCriteria soqlCriteria )
+ {
+ // TODO: security
+ String whereClause = soqlCriteria.toSOQL();
+
+ if ( String.isBlank( whereClause ) )
+ {
+ throw new UnboundCountQueryException( 'Attempted to perform a count on an unbound query against ' + getSObjectName() )
+ .setErrorCode( FrameworkErrorCodes.SELECTOR_UNBOUND_COUNT_QUERY )
+ .addContext( 'SObjectType', getSObjectName() );
+ }
+
+ String query = 'SELECT COUNT(Id) recordCount FROM ' + getSObjectName() + ' WHERE ' + whereClause;
+
+ AggregateResult result = (AggregateResult)Database.query( query ); // NOPMD: variables come from a trusted source
+
+ return (Integer)result.get( 'recordCount' );
+ }
}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls b/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls
new file mode 100644
index 00000000000..d165c78f20b
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls
@@ -0,0 +1,16 @@
+/**
+ * Interface that defines classes that describe the configuration of searches.
+ *
+ * This includes the ability to define:
+ * * The fields that should be included in the result set
+ * * Which fields are sortable
+ * * How to map from the result set fields to internal SObject fields
+ * * Which Base SObject is used to derive a record in the result set
+ */
+public interface ISearchConfiguration
+{
+ List getRequiredFields();
+ List getSortableFields();
+ String getMappedSobjectField( String resultField );
+ SobjectType getBaseSobjectType();
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchConfiguration.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls b/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls
new file mode 100644
index 00000000000..6978dd4d88a
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls
@@ -0,0 +1,7 @@
+/**
+ * Interface that defines the ability to provide a WHERE clause for a search to be performed with.
+ */
+public interface ISearchCriteria
+{
+ String toSOQL();
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchCriteria.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls b/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls
new file mode 100644
index 00000000000..5c0e69e2c9f
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls
@@ -0,0 +1,9 @@
+/**
+ * Interface that defines the ability to create an instance of ISearchCriteria
+ * based on a Map of properties
+ */
+public interface ISearchCriteriaFactory
+{
+ ISearchCriteriaFactory setProperties( Map properties );
+ ISearchCriteria build();
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchCriteriaFactory.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchResult.cls b/framework/default/ortoo-core/default/classes/search/ISearchResult.cls
new file mode 100644
index 00000000000..9cf12cc90c5
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchResult.cls
@@ -0,0 +1,6 @@
+/**
+ * Interface that defines the ability to represent search results
+ */
+public interface ISearchResult
+{
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchResult.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchResult.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchResult.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls b/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls
new file mode 100644
index 00000000000..3e0978ec32a
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls
@@ -0,0 +1,8 @@
+/**
+ * Interface that defines the ability to build a list of search result objects
+ * based on the current state of that instance (e.g. by a Domain object)
+ */
+public interface ISearchResultBuilder
+{
+ List buildSearchResults( ISearchConfiguration searchConfiguration );
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchResultBuilder.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls b/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls
new file mode 100644
index 00000000000..9cdb5cb1872
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls
@@ -0,0 +1,12 @@
+/**
+ * Interface that defines the ability to retrieve a list of Search Results with a total number of records
+ * based on:
+ * A Search Configuration that defines the minimum fields to return
+ * Search Criteria that define filters for the records to be returned
+ * Search Window defining which subset of the matching records should be returned
+ * Search Order By defining the order of the records prior to the result set being trimmed to the required window
+ */
+public interface ISearchSelector
+{
+ SearchResults selectBySearchCriteria( ISearchConfiguration searchConfiguration, ISearchCriteria criteria, SearchWindow window, SearchOrderBy orderBy );
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/ISearchSelector.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls b/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls
new file mode 100644
index 00000000000..af46cb1943b
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls
@@ -0,0 +1,89 @@
+/**
+ * Parent class for the definition of a Search, being a mechanism for calling 'selectBySearchCriteria' against a selector.
+ * This allows for a windowed and ordered result set to be returned based on an arbitrary set of search criteria.
+ */
+public abstract inherited sharing class SearchConfiguration implements ISearchConfiguration
+{
+ protected Map fieldMapping = new Map();
+ private SobjectType baseSobjectType;
+
+ /**
+ * Constructor, defining the Base SObject type that each resulting record represents
+ *
+ * @param SobjectType The SObject Type that each record represents
+ */
+ public SearchConfiguration( SobjectType baseSobjectType )
+ {
+ Contract.requires( baseSobjectType != null, 'constructor called with a null baseSobjectType' );
+
+ this.baseSobjectType = baseSobjectType;
+ }
+
+ /**
+ * Returns a list of the result object's fields that are regarded as 'sortable' in the result set.
+ * By default, the implementation states that all mapped fields are sortable.
+ * This can be overridden at the concrete class level.
+ *
+ * @return List The result object's fields that can be sorted
+ */
+ public virtual List getSortableFields()
+ {
+ Contract.assert( fieldMapping != null, 'getSortableFields when fieldMapping was null' );
+
+ return new List( fieldMapping.keySet() );
+ }
+
+ /**
+ * Returns a list of the fields that are required on the SObject in order for the results to
+ * be rendered correctly.
+ *
+ * @return List The source SObject's fields that are needed to build the results
+ */
+ public List getRequiredFields()
+ {
+ Contract.assert( fieldMapping != null, 'getRequiredFields when fieldMapping was null' );
+
+ return fieldMapping.values();
+ }
+
+ /**
+ * Given a field on the result object, returns the name of the SObject field on the base object
+ * that the it represents
+ *
+ * @param String The name of the field on the result object
+ * @return String The name of the field on the SObject
+ */
+ public String getMappedSobjectField( String resultField )
+ {
+ Contract.requires( String.isNotBlank( resultField ), 'getMappedSobjectField called with a blank resultField' );
+ Contract.assert( fieldMapping != null, 'getMappedSobjectField when fieldMapping was null' );
+
+ return fieldMapping.get( resultField );
+ }
+
+ /**
+ * The SObject Type that is the basis of the result objects for this search type.
+ *
+ * @return SObjectType The SObject Type that is the basis of the result records
+ */
+ public SObjectType getBaseSobjectType()
+ {
+ return this.baseSobjectType;
+ }
+
+ /**
+ * Add a mapping between a field on the result object and the SObject.
+ *
+ * @return SearchConfiguration Itself, allowing for a fluent implementation
+ */
+ @testVisible
+ protected SearchConfiguration addFieldMapping( String resultField, String sobjectField )
+ {
+ Contract.requires( String.isNotBlank( resultField ), 'addFieldMapping called with a blank resultField' );
+ Contract.requires( String.isNotBlank( sobjectField ), 'addFieldMapping called with a blank sobjectField' );
+ Contract.assert( fieldMapping != null, 'addFieldMapping when fieldMapping was null' );
+
+ this.fieldMapping.put( resultField, sobjectField );
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/SearchConfiguration.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/search/SearchController.cls b/framework/default/ortoo-core/default/classes/search/SearchController.cls
new file mode 100644
index 00000000000..63e834293e6
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/SearchController.cls
@@ -0,0 +1,69 @@
+/**
+ * A base controller that can be used for a given implementation for a given LWC (for example)
+ *
+ * A consumer implementation should instantiate an instance passing a 'searchConfigurationType' that defines the search's behaviour.
+ *
+ * In addition, before calling 'search', it should instantiate an instance of a ISearchCriteriaFactory that is used to convert the raw
+ * criteria into an ISearchCriteria instance. This class is likely to be defined as an inner class.
+ */
+public inherited sharing class SearchController
+{
+ private Type searchConfigurationType;
+
+ /**
+ * Instructs the SearchController to be configured via the given searchConfigurationType.
+ *
+ * @param Type The type of the ISearchConfigration implementation that this search is configured by
+ * @return SearchController Itself, allowing for a fluent interface
+ */
+ public SearchController setSearchConfigurationType( Type searchConfigurationType )
+ {
+ Contract.requires( searchConfigurationType != null, 'setSearchConfigurationType called with a null searchConfigurationType' );
+
+ this.searchConfigurationType = searchConfigurationType;
+ return this;
+ }
+
+ /**
+ * Performs a search using the SearchService, convering raw property based parameters into the appropriate objects.
+ *
+ * @param ISearchCriteriaFactory The factory that should be used to build the ISearchCriteria instance that will define
+ * the result set to return.
+ * @param Map The raw criteria that is used to define the search. This is used by the ISearchCriteriaFactory
+ * to configure the resulting ISearchCriteria
+ * @param Map The raw result set's window representation
+ * @param Map The raw result set's orderBy representation
+ * @return SearchResults The results of the given search, as returned by the service
+ */
+ public SearchResults search( ISearchCriteriaFactory searchCriteriaFactory, Map criteria, Map window, Map orderBy )
+ {
+ Contract.requires( searchCriteriaFactory != null, 'search called with a null searchCriteriaFactory' );
+ Contract.requires( criteria != null, 'search called with a null criteria' );
+ Contract.requires( window != null, 'search called with a null window' );
+ Contract.requires( orderBy != null, 'search called with a null orderBy' );
+
+ Contract.assert( searchConfigurationType != null, 'search called when searchConfigurationType was null' );
+
+ ISearchCriteria criteriaObject = searchCriteriaFactory.setProperties( criteria ).build();
+
+ SearchWindow windowObject = ((SearchWindow)Application.APP_LOGIC.newInstance( SearchWindow.class ) )
+ .configure( window );
+
+ SearchOrderBy orderByObject = ((SearchOrderBy)Application.APP_LOGIC.newInstance( SearchOrderBy.class ) )
+ .configure( orderBy, searchConfigurationType );
+
+ return SearchService.search( searchConfigurationType, criteriaObject, windowObject, orderByObject );
+ }
+
+ /**
+ * Returns the list of sortable fields in the result set. Does so by asking the SearchService to discover it via the configuration
+ *
+ * @return List The list of fields that are searchable
+ */
+ public List getSortableFields()
+ {
+ Contract.assert( searchConfigurationType != null, 'getSortableFields called when searchConfigurationType was null' );
+
+ return SearchService.getSortableFields( searchConfigurationType );
+ }
+}
diff --git a/framework/default/ortoo-core/default/classes/search/SearchController.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/SearchController.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/SearchController.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls
new file mode 100644
index 00000000000..3adb699e66d
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls
@@ -0,0 +1,60 @@
+/**
+ * Defines the Order By for a given search request.
+ * The request is qualified by the given search configuration type.
+ *
+ * The fieldName is mapped from the given result field name into the appropriate sobject
+ * field name, if one is mapped in the configuration.
+ */
+public inherited sharing class SearchOrderBy
+{
+ private static final String DIRECTION_ASCENDING = 'asc';
+ private static final String DIRECTION_DESCENDING = 'desc';
+
+ public String fieldName = '';
+ public String direction = '';
+
+ /**
+ * Configures the order by, defining its properties.
+ *
+ * @param Map The properties of the window. Should contain 'fieldName' and 'offset' properties as Strings
+ * @param Type The type of the SearchConfiguration object that should be used to determine the field mappings
+ * @return SearchOrderBy Itself, allowing for a fluent interface
+ */
+ public SearchOrderBy configure( Map properties, Type searchConfigurationType )
+ {
+ Contract.requires( properties != null, 'configure called with a null properties' );
+ Contract.requires( searchConfigurationType != null, 'configure called with a null searchConfigurationType' );
+
+ ISearchConfiguration searchConfiguration;
+
+ try
+ {
+ searchConfiguration = (ISearchConfiguration)Application.APP_LOGIC.newInstance( searchConfigurationType );
+ } catch ( Exception e ) {
+ Contract.assert( false, 'configure called with a searchConfigurationType ('+searchConfigurationType+') that does not implement ISearchConfiguration or does not have a parameterless constructor' );
+ }
+
+ String resultsFieldName = (String)properties.get( 'fieldName' );
+ String direction = (String)properties.get( 'direction' );
+
+ if ( String.isBlank( direction ) )
+ {
+ direction = DIRECTION_ASCENDING;
+ }
+
+ Contract.assert( direction == DIRECTION_ASCENDING || direction == DIRECTION_DESCENDING,
+ 'configure called with an invalid direction. Was "'+direction+'", but should be one of ['+DIRECTION_ASCENDING+','+DIRECTION_DESCENDING+']' );
+
+ if ( String.isNotBlank( resultsFieldName ) )
+ {
+ String mappedFieldName = searchConfiguration.getMappedSobjectField( resultsFieldName );
+
+ if ( String.isNotBlank( mappedFieldName ) )
+ {
+ this.fieldName = mappedFieldName;
+ this.direction = direction;
+ }
+ }
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls-meta.xml b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/search/SearchResults.cls b/framework/default/ortoo-core/default/classes/search/SearchResults.cls
new file mode 100644
index 00000000000..915747ba527
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/search/SearchResults.cls
@@ -0,0 +1,32 @@
+/**
+ * Represents a window of data, being the results of a search. Is defined by the properties:
+ *
+ * * totalNumberOfRecords - The total number of records for which these results are a subset.
+ * Is always at least as large as the list of records
+ * * records - The subset of the total result record set that are being returned.
+ *
+ * The properties are public and AuraEnabled, allowing them to be viewed in Aura Components / LWCs
+ */
+public inherited sharing class SearchResults
+{
+ @AuraEnabled public Integer totalNumberOfRecords;
+ @AuraEnabled public List
diff --git a/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.js b/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.js
index f66bbb3348d..de83812f9c8 100644
--- a/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.js
+++ b/framework/default/ortoo-core/default/lwc/paginationControls/paginationControls.js
@@ -19,7 +19,8 @@ export default class PaginationControls extends LightningElement {
}
set recordsPerPage( value ) {
const previousOffset = this.offset;
- this._recordsPerPage = value;
+
+ this._recordsPerPage = isNaN( value ) ? this._recordsPerPage : parseInt( value );
this.offset = previousOffset;
}
From 0745ef42b96038ab42bd7ac7027a2821db050e46 Mon Sep 17 00:00:00 2001
From: Robert Baillie
Date: Mon, 24 Jan 2022 11:42:13 +0000
Subject: [PATCH 2/3] Brought in more changes from the example-app
implementation of filter + results
* Improvements to Contract to remove stack trace entry for the Contract methods
* Add ability to define like Strings to the criteria
* Added testing for Sobject Selector
* Added protection to public AuraEnabled properties (private setters)
* Added more capabilities to SObjectUtils for examining types
---
.../classes/criteria/fflib_Criteria.cls | 4 +-
.../classes/common/fflib_SObjectSelector.cls | 2 +-
.../exceptions/utilities/StackTrace.cls | 1 +
.../fflib-extension/ortoo_Criteria.cls | 1 -
.../fflib-extension/ortoo_SobjectSelector.cls | 159 ++++++-----
.../tests/ortoo_CriteriaTest.cls | 33 +++
.../tests/ortoo_CriteriaTest.cls-meta.xml | 5 +
.../tests/ortoo_SobjectSelectorTest.cls | 252 ++++++++++++++++++
.../default/classes/search/SearchOrderBy.cls | 14 +-
.../default/classes/search/SearchWindow.cls | 4 +-
.../search/tests/SearchOrderByTest.cls | 30 ++-
.../default/classes/utils/SobjectUtils.cls | 78 +++++-
.../classes/utils/tests/ContractTest.cls | 27 +-
.../classes/utils/tests/SobjectUtilsTest.cls | 128 ++++++++-
14 files changed, 636 insertions(+), 102 deletions(-)
create mode 100644 framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls
create mode 100644 framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls-meta.xml
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 209357e72e0..151beba5855 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
@@ -54,7 +54,9 @@ public virtual with sharing class fflib_Criteria
this.type = 'AND';
}
- // TODO: document and test
+ /**
+ * Adds the given evalutor onto the formula stack
+ */
protected void addEvaluator( Evaluator evaluator )
{
evaluators.add( evaluator );
diff --git a/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls b/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls
index 00bc12d448d..be8c0ef8a18 100644
--- a/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls
+++ b/framework/default/fflib/default/classes/common/fflib_SObjectSelector.cls
@@ -91,7 +91,7 @@ public abstract with sharing class fflib_SObjectSelector
* 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
**/
diff --git a/framework/default/ortoo-core/default/classes/exceptions/utilities/StackTrace.cls b/framework/default/ortoo-core/default/classes/exceptions/utilities/StackTrace.cls
index 95dee22cf53..5e7f37e6119 100644
--- a/framework/default/ortoo-core/default/classes/exceptions/utilities/StackTrace.cls
+++ b/framework/default/ortoo-core/default/classes/exceptions/utilities/StackTrace.cls
@@ -176,6 +176,7 @@ public class StackTrace {
return stackTraceEntries[0];
}
+ @testVisible
private StackTraceEntry getEntryStackTraceEntry()
{
if ( stackTraceEntries.isEmpty() )
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 d32ce4860a7..bbdf47d27cc 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
@@ -5,7 +5,6 @@
*/
public inherited sharing virtual class ortoo_Criteria extends fflib_Criteria implements ISearchCriteria // NOPMD: specified a mini-namespace to differentiate from fflib versions
{
-
public virtual ortoo_Criteria likeString( Schema.SObjectField field, Object value )
{
addEvaluator( new FieldEvaluator( field, fflib_Operator.LIKEx, value ) );
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls
index 986d8063591..e7047275ab4 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/ortoo_SobjectSelector.cls
@@ -7,69 +7,98 @@
*/
public abstract inherited sharing class ortoo_SobjectSelector extends fflib_SobjectSelector implements ISearchSelector // NOPMD: specified a mini-namespace to differentiate from fflib versions
{
- class UnboundCountQueryException extends Exceptions.SelectorException {}
-
- public ortoo_SobjectSelector()
- {
- super();
- enforceFLS();
- }
-
- /**
- * Configure this instance to ignore FLS when selecting data.
- *
- * @return fflib_SObjectSelector Itself, allowing for a fluent interface.
- */
- public fflib_SObjectSelector ignoreFls()
- {
- m_enforceFLS = false;
- return this;
- }
-
- // TODO: document
- // TODO: test
- // TODO: security
- public SearchResults selectBySearchCriteria( ISearchConfiguration searchConfiguration, ISearchCriteria criteria, SearchWindow window, SearchOrderBy orderBy )
- {
- Contract.requires( criteria != null, 'selectByCriteria called with a null criteria' );
-
- Integer countOfRecords = getCountOfRecords( criteria );
-
- List resultsRecords = new List();
-
- if ( countOfRecords > 0 )
- {
- fflib_QueryFactory queryFactory = newQueryFactory().setCondition( criteria.toSOQL() );
-
- queryFactory.selectFields( searchConfiguration.getRequiredFields() );
- queryFactory.setOffset( window.offset );
- queryFactory.setLimit( window.length );
- queryFactory.setOrdering( orderBy.fieldName , orderBy.direction == 'desc' ? fflib_QueryFactory.SortOrder.DESCENDING : fflib_QueryFactory.SortOrder.ASCENDING );
-
- resultsRecords = Database.query( queryFactory.toSOQL() );
- }
-
- return new SearchResults( countOfRecords, resultsRecords );
- }
-
- // TODO: document
- // TODO: test
- protected Integer getCountOfRecords( ISearchCriteria soqlCriteria )
- {
- // TODO: security
- String whereClause = soqlCriteria.toSOQL();
-
- if ( String.isBlank( whereClause ) )
- {
- throw new UnboundCountQueryException( 'Attempted to perform a count on an unbound query against ' + getSObjectName() )
- .setErrorCode( FrameworkErrorCodes.SELECTOR_UNBOUND_COUNT_QUERY )
- .addContext( 'SObjectType', getSObjectName() );
- }
-
- String query = 'SELECT COUNT(Id) recordCount FROM ' + getSObjectName() + ' WHERE ' + whereClause;
-
- AggregateResult result = (AggregateResult)Database.query( query ); // NOPMD: variables come from a trusted source
-
- return (Integer)result.get( 'recordCount' );
- }
+ public class UnboundCountQueryException extends Exceptions.SelectorException {}
+
+ public ortoo_SobjectSelector()
+ {
+ super();
+ enforceFLS();
+ }
+
+ /**
+ * Configure this instance to ignore FLS when selecting data.
+ *
+ * @return fflib_SObjectSelector Itself, allowing for a fluent interface.
+ */
+ public fflib_SObjectSelector ignoreFls()
+ {
+ m_enforceFLS = false;
+ return this;
+ }
+
+ /**
+ * Given a configuration, search criteria, a window and an order by, will perform the defined search.
+ *
+ * The results includes a total record count as well as the windowed search results.
+ *
+ * WIll perform a maximum of 2 SOQL queries.
+ *
+ * @param ISearchConfiguration The configuration defining the additional fields that should be returned.
+ * @param ISearchCriteria The criteria defining the records that should be returned.
+ * @param SearchWindow Defines the subset of the full result set that should be returned.
+ * @param SearchOrderBy The order in which the records should be returned (is applied prior to the window).
+ * @return SearchResults The resulting total record count and result set.
+ */
+ public SearchResults selectBySearchCriteria( ISearchConfiguration searchConfiguration, ISearchCriteria criteria, SearchWindow window, SearchOrderBy orderBy )
+ {
+ Contract.requires( searchConfiguration != null, 'selectBySearchCriteria called with a null searchConfiguration' );
+ Contract.requires( criteria != null, 'selectBySearchCriteria called with a null criteria' );
+ Contract.requires( window != null, 'selectBySearchCriteria called with a null window' );
+ Contract.requires( orderBy != null, 'selectBySearchCriteria called with a null orderBy' );
+
+ Integer countOfRecords = getCountOfRecords( criteria );
+
+ List resultsRecords = new List();
+
+ if ( countOfRecords > 0 )
+ {
+ fflib_QueryFactory queryFactory = newQueryFactory().setCondition( criteria.toSOQL() );
+
+ queryFactory.selectFields( searchConfiguration.getRequiredFields() );
+ queryFactory.setOffset( window.offset );
+ queryFactory.setLimit( window.length );
+
+ if ( orderBy.isConfigured() )
+ {
+ queryFactory.setOrdering( orderBy.fieldName , orderBy.direction == 'desc' ? fflib_QueryFactory.SortOrder.DESCENDING : fflib_QueryFactory.SortOrder.ASCENDING );
+ }
+
+ resultsRecords = Database.query( queryFactory.toSOQL() );
+ }
+
+ return new SearchResults( countOfRecords, resultsRecords );
+ }
+
+ /**
+ * Given a set of search criteria, will return the number of records that meet that criteria
+ *
+ * Will perform a maximum of 1 SOQL query
+ *
+ * @param ISearchCriteria The criteria defining the records that should be returned.
+ * @return Integer The total number of records that match the criteria
+ */
+ protected Integer getCountOfRecords( ISearchCriteria criteria )
+ {
+ Contract.requires( criteria != null, 'getCountOfRecords called with a null criteria' );
+
+ String whereClause = criteria.toSOQL();
+
+ if ( String.isBlank( whereClause ) )
+ {
+ throw new UnboundCountQueryException( 'Attempted to perform the count of an unbound query against ' + getSObjectName() )
+ .setErrorCode( FrameworkErrorCodes.SELECTOR_UNBOUND_COUNT_QUERY )
+ .addContext( 'SObjectType', getSObjectName() );
+ }
+
+ if ( isEnforcingCRUD() && ! SobjectUtils.isAccessible( sObjectType() ) )
+ {
+ throw new fflib_SObjectDomain.DomainException( 'Permission to access ' + getSObjectName() + ' denied.' ); // to match the exception thrown by fflib
+ }
+
+ String query = 'SELECT COUNT(Id) recordCount FROM ' + getSObjectName() + ' WHERE ' + whereClause;
+
+ AggregateResult result = (AggregateResult)Database.query( query ); // NOPMD: variables come from a trusted source
+
+ return (Integer)result.get( 'recordCount' );
+ }
}
\ No newline at end of file
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
new file mode 100644
index 00000000000..b7e4cff2d93
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls
@@ -0,0 +1,33 @@
+@isTest
+private without sharing class ortoo_CriteriaTest
+{
+ @isTest
+ private static void likeString_field_whenCalled_addsALikeToTheGeneratedSoql() // NOPMD: Test method name format
+ {
+ ortoo_Criteria criteria = new ortoo_Criteria();
+
+ Test.startTest();
+ criteria.likeString( Contact.Name, 'thing%' );
+ Test.stopTest();
+
+ String expected = 'Name LIKE \'thing%\'';
+ String got = criteria.toSOQL();
+
+ System.assertEquals( expected, got, 'likeString, when called with a field, will add a LIKE to the Generated SOQL' );
+ }
+
+ @isTest
+ private static void likeString_stringName_whenCalled_addsALikeToTheGeneratedSoql() // NOPMD: Test method name format
+ {
+ ortoo_Criteria criteria = new ortoo_Criteria();
+
+ Test.startTest();
+ criteria.likeString( 'Account.Name', 'thing%' );
+ Test.stopTest();
+
+ String expected = 'Account.Name LIKE \'thing%\'';
+ String got = criteria.toSOQL();
+
+ System.assertEquals( expected, got, 'likeString, when called with a string name for a field will add a LIKE to the Generated SOQL' );
+ }
+}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls-meta.xml b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls-meta.xml
new file mode 100644
index 00000000000..dd61d1f917e
--- /dev/null
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_CriteriaTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 52.0
+ Active
+
diff --git a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectSelectorTest.cls b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectSelectorTest.cls
index 1eafb7f047c..b2978c5adc6 100644
--- a/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectSelectorTest.cls
+++ b/framework/default/ortoo-core/default/classes/fflib-extension/tests/ortoo_SobjectSelectorTest.cls
@@ -19,6 +19,258 @@ private without sharing class ortoo_SobjectSelectorTest
System.assertEquals( false, selector.isEnforcingFls(), 'ignoreFls, when called, will disable FLS checking' );
}
+ @isTest
+ private static void selectBySearchCriteria_whenGivenCriteria_returnsRecordsThatMatchWithACount() // NOPMD: Test method name format
+ {
+ List accountList = new List
+ {
+ new Account( Name = 'Acc1', AnnualRevenue = 123 ),
+ new Account( Name = 'Acc2', AnnualRevenue = 234 ),
+ new Account( Name = 'Acc3', AnnualRevenue = 345 ),
+ new Account( Name = 'Acc4', AnnualRevenue = 456 ),
+ new Account( Name = 'Acc5', AnnualRevenue = 567 )
+ };
+ insert accountList;
+
+ Amoss_Instance configController = ApplicationMockRegistrar.registerMockAppLogic( ISearchConfiguration.class );
+ Amoss_Instance criteriaController = new Amoss_Instance( ISearchCriteria.class );
+
+ ISearchConfiguration config = (ISearchConfiguration)configController.generateDouble();
+ configController
+ .expects( 'getRequiredFields' )
+ .returns( new List{ 'AnnualRevenue' } )
+ .also()
+ .when( 'getMappedSobjectField' )
+ .returns( 'Name' );
+
+ ISearchCriteria criteria = (ISearchCriteria)criteriaController.generateDouble();
+ criteriaController
+ .when( 'toSOQL' )
+ .returns( 'AnnualRevenue > 200' );
+
+ SearchWindow window = new SearchWindow().configure( new Map{ 'offset' => 1, 'length' => 2 } );
+ SearchOrderBy orderBy = new SearchOrderBy().configure( new Map{ 'fieldName' => 'Name', 'direction' => 'desc' }, ISearchConfiguration.class );
+
+ Test.startTest();
+ SearchResults got = new TestableSelector().selectBySearchCriteria( config, criteria, window, orderBy );
+ Test.stopTest();
+
+ configController.verify();
+
+ System.assertEquals( 4, got.totalNumberOfRecords, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return the total number of records that match' );
+
+ List returnedAccounts = (List)got.records;
+ System.assertEquals( 2, returnedAccounts.size(), 'selectBySearchCriteria, when given config, criteria, window and orderby, will return records that match, limited by the window size' );
+
+ System.assertEquals( 'Acc4', returnedAccounts[0].Name, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return records that match, ordered by the orderBy, with the base selector fields included (1)' );
+ System.assertEquals( 'Acc3', returnedAccounts[1].Name, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return records that match, ordered by the orderBy, with the base selector fields included (2)' );
+
+ System.assertEquals( 456, returnedAccounts[0].AnnualRevenue, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return records that match, ordered by the orderBy, with the passed config fields included (2)' );
+ System.assertEquals( 345, returnedAccounts[1].AnnualRevenue, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return records that match, ordered by the orderBy, with the passed config fields included (2)' );
+ }
+
+ @isTest
+ private static void selectBySearchCriteria_whenNoWindowIsSet_willReturnAllTheResults() // NOPMD: Test method name format
+ {
+ List accountList = new List
+ {
+ new Account( Name = 'Acc1', AnnualRevenue = 123 ),
+ new Account( Name = 'Acc2', AnnualRevenue = 234 ),
+ new Account( Name = 'Acc3', AnnualRevenue = 345 ),
+ new Account( Name = 'Acc4', AnnualRevenue = 456 ),
+ new Account( Name = 'Acc5', AnnualRevenue = 567 )
+ };
+ insert accountList;
+
+ Amoss_Instance configController = ApplicationMockRegistrar.registerMockAppLogic( ISearchConfiguration.class );
+ Amoss_Instance criteriaController = new Amoss_Instance( ISearchCriteria.class );
+
+ ISearchConfiguration config = (ISearchConfiguration)configController.generateDouble();
+ configController
+ .when( 'getRequiredFields' )
+ .returns( new List() )
+ .also()
+ .when( 'getMappedSobjectField' )
+ .returns( 'Name' );
+
+ ISearchCriteria criteria = (ISearchCriteria)criteriaController.generateDouble();
+ criteriaController
+ .when( 'toSOQL' )
+ .returns( 'AnnualRevenue > 200' );
+
+ SearchWindow emptyWindow = new SearchWindow();
+ SearchOrderBy orderBy = new SearchOrderBy().configure( new Map{ 'fieldName' => 'Name', 'direction' => 'desc' }, ISearchConfiguration.class );
+
+ Test.startTest();
+ SearchResults got = new TestableSelector().selectBySearchCriteria( config, criteria, emptyWindow, orderBy );
+ Test.stopTest();
+
+ configController.verify();
+
+ System.assertEquals( 4, got.totalNumberOfRecords, 'selectBySearchCriteria, when given config, criteria, window and orderby, will return the total number of records that match' );
+
+ List returnedAccounts = (List)got.records;
+ System.assertEquals( 4, returnedAccounts.size(), 'selectBySearchCriteria, when given an empty window, will return all the records that match' );
+ }
+
+ @isTest
+ private static void selectBySearchCriteria_whenGivenEmptyOrderBy_stillReturnsResults() // NOPMD: Test method name format
+ {
+ List accountList = new List
+ {
+ new Account( Name = 'Acc1', AnnualRevenue = 123 ),
+ new Account( Name = 'Acc2', AnnualRevenue = 234 ),
+ new Account( Name = 'Acc3', AnnualRevenue = 345 ),
+ new Account( Name = 'Acc4', AnnualRevenue = 456 ),
+ new Account( Name = 'Acc5', AnnualRevenue = 567 )
+ };
+ insert accountList;
+
+ Amoss_Instance configController = ApplicationMockRegistrar.registerMockAppLogic( ISearchConfiguration.class );
+ Amoss_Instance criteriaController = new Amoss_Instance( ISearchCriteria.class );
+
+ ISearchConfiguration config = (ISearchConfiguration)configController.generateDouble();
+ configController
+ .when( 'getRequiredFields' )
+ .returns( new List() );
+
+ ISearchCriteria criteria = (ISearchCriteria)criteriaController.generateDouble();
+ criteriaController
+ .when( 'toSOQL' )
+ .returns( 'AnnualRevenue > 200' );
+
+ SearchWindow window = new SearchWindow().configure( new Map{ 'offset' => 1, 'length' => 2 } );
+ SearchOrderBy emptyOrderBy = new SearchOrderBy();
+
+ Test.startTest();
+ SearchResults got = new TestableSelector().selectBySearchCriteria( config, criteria, window, emptyOrderBy );
+ Test.stopTest();
+
+ System.assertEquals( 4, got.totalNumberOfRecords, 'selectBySearchCriteria, when given an empty order by, will return the total number of records that match' );
+
+ List returnedAccounts = (List)got.records;
+ System.assertEquals( 2, returnedAccounts.size(), 'selectBySearchCriteria, when given an empty order by, will return records that match, limited by the window size' );
+ }
+
+ @isTest
+ private static void selectBySearchCriteria_whenGivenEmptyCriteria_throwsAnException() // NOPMD: Test method name format
+ {
+ Amoss_Instance configController = ApplicationMockRegistrar.registerMockAppLogic( ISearchConfiguration.class );
+ Amoss_Instance criteriaController = new Amoss_Instance( ISearchCriteria.class );
+
+ ISearchConfiguration config = (ISearchConfiguration)configController.generateDouble();
+
+ SearchWindow window = new SearchWindow();
+ SearchOrderBy orderBy = new SearchOrderBy();
+
+ ISearchCriteria criteria = (ISearchCriteria)criteriaController.generateDouble();
+ criteriaController
+ .when( 'toSOQL' )
+ .returns( '' );
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ new TestableSelector().selectBySearchCriteria( config, criteria, window, orderBy );
+ }
+ catch ( ortoo_SobjectSelector.UnboundCountQueryException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ Amoss_Asserts.assertContains( 'Attempted to perform the count of an unbound query against ' + Account.sobjectType.getDescribe().getName(), exceptionMessage, 'selectBySearchCriteria, when given empty criteria, will throw an exception' );
+ }
+
+ @isTest
+ private static void selectBySearchCriteria_whenPassedANullConfig_throwsAnException() // NOPMD: Test method name format
+ {
+ ISearchCriteria criteria = (ISearchCriteria)new Amoss_Instance( ISearchCriteria.class ).generateDouble();
+ SearchWindow window = new SearchWindow();
+ SearchOrderBy orderBy = new SearchOrderBy();
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ new TestableSelector().selectBySearchCriteria( null, criteria, window, orderBy );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ Amoss_Asserts.assertContains( 'selectBySearchCriteria called with a null searchConfiguration', exceptionMessage, 'selectBySearchCriteria, when passed a null searchConfiguration, will throw an exception' );
+ }
+
+ @isTest
+ private static void selectBySearchCriteria_whenPassedANullCriteria_throwsAnException() // NOPMD: Test method name format
+ {
+ ISearchConfiguration config = (ISearchConfiguration)new Amoss_Instance( ISearchConfiguration.class ).generateDouble();
+ SearchWindow window = new SearchWindow();
+ SearchOrderBy orderBy = new SearchOrderBy();
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ new TestableSelector().selectBySearchCriteria( config, null, window, orderBy );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ Amoss_Asserts.assertContains( 'selectBySearchCriteria called with a null criteria', exceptionMessage, 'selectBySearchCriteria, when passed a null criteria, will throw an exception' );
+ }
+
+ @isTest
+ private static void selectBySearchCriteria_whenPassedANullWindow_throwsAnException() // NOPMD: Test method name format
+ {
+ ISearchConfiguration config = (ISearchConfiguration)new Amoss_Instance( ISearchConfiguration.class ).generateDouble();
+ ISearchCriteria criteria = (ISearchCriteria)new Amoss_Instance( ISearchCriteria.class ).generateDouble();
+ SearchOrderBy orderBy = new SearchOrderBy();
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ new TestableSelector().selectBySearchCriteria( config, criteria, null, orderBy );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ Amoss_Asserts.assertContains( 'selectBySearchCriteria called with a null window', exceptionMessage, 'selectBySearchCriteria, when passed a null window, will throw an exception' );
+ }
+
+ @isTest
+ private static void selectBySearchCriteria_whenPassedANullOrderBy_throwsAnException() // NOPMD: Test method name format
+ {
+ ISearchConfiguration config = (ISearchConfiguration)new Amoss_Instance( ISearchConfiguration.class ).generateDouble();
+ ISearchCriteria criteria = (ISearchCriteria)new Amoss_Instance( ISearchCriteria.class ).generateDouble();
+ SearchWindow window = new SearchWindow();
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ new TestableSelector().selectBySearchCriteria( config, criteria, window, null );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ Amoss_Asserts.assertContains( 'selectBySearchCriteria called with a null orderBy', exceptionMessage, 'selectBySearchCriteria, when passed a null orderBy, will throw an exception' );
+ }
+
class TestableSelector extends ortoo_SobjectSelector
{
public List getSObjectFieldList() {
diff --git a/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls
index 3adb699e66d..8863b71c719 100644
--- a/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls
+++ b/framework/default/ortoo-core/default/classes/search/SearchOrderBy.cls
@@ -10,8 +10,8 @@ public inherited sharing class SearchOrderBy
private static final String DIRECTION_ASCENDING = 'asc';
private static final String DIRECTION_DESCENDING = 'desc';
- public String fieldName = '';
- public String direction = '';
+ public String fieldName {get; private set;}
+ public String direction {get; private set;}
/**
* Configures the order by, defining its properties.
@@ -57,4 +57,14 @@ public inherited sharing class SearchOrderBy
}
return this;
}
+
+ /**
+ * States if the OrderBy object has been configured with a fieldName that makes sense.
+ *
+ * @return Boolean Is this object configured
+ */
+ public Boolean isConfigured()
+ {
+ return String.isNotBlank( fieldName );
+ }
}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/search/SearchWindow.cls b/framework/default/ortoo-core/default/classes/search/SearchWindow.cls
index 6c4177f1737..f6266f34f5f 100644
--- a/framework/default/ortoo-core/default/classes/search/SearchWindow.cls
+++ b/framework/default/ortoo-core/default/classes/search/SearchWindow.cls
@@ -7,8 +7,8 @@
*/
public inherited sharing class SearchWindow
{
- public Integer offset;
- public Integer length;
+ public Integer offset {get; private set;}
+ public Integer length {get; private set;}
/**
* Configures the current instance with the given properties, defining the window
diff --git a/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls b/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls
index 56c351cc4df..3c9ed4faa44 100644
--- a/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls
+++ b/framework/default/ortoo-core/default/classes/search/tests/SearchOrderByTest.cls
@@ -24,10 +24,12 @@ public inherited sharing class SearchOrderByTest
System.assertEquals( 'targetFieldName', orderBy.fieldName, 'configure, when given properties with a fieldName and direction, will map the fieldName using the configuration and set the member variable' );
System.assertEquals( 'asc', orderBy.direction, 'configure, when given properties with a fieldName and direction, will set the direction member variable' );
+
+ System.assert( orderBy.isConfigured(), 'configure, when given properties with a fieldName and direction, will set the orderBy to configured' );
}
@isTest
- private static void configure_whenNoFieldName_setsFieldNameAndDirectionToEmpty() // NOPMD: Test method name format
+ private static void configure_whenNoFieldName_setsFieldNameAndDirectionToNull() // NOPMD: Test method name format
{
Map properties = new Map{
'direction' => 'asc'
@@ -44,12 +46,14 @@ public inherited sharing class SearchOrderByTest
configurationController.verify();
- System.assertEquals( '', orderBy.fieldName, 'configure, when given properties with no fieldName and direction, will set the fieldName to an empty string' );
- System.assertEquals( '', orderBy.direction, 'configure, when given properties with no fieldName and direction, will set the direction to an empty string' );
+ System.assertEquals( null, orderBy.fieldName, 'configure, when given properties with no fieldName and direction, will set the fieldName to null' );
+ System.assertEquals( null, orderBy.direction, 'configure, when given properties with no fieldName and direction, will set the direction to null' );
+
+ System.assert( ! orderBy.isConfigured(), 'configure, when given properties with no fieldName and direction, will set the orderBy to not configured' );
}
@isTest
- private static void configure_whenFieldNameEmpty_setsFieldNameAndDirectionToEmpty() // NOPMD: Test method name format
+ private static void configure_whenFieldNameEmpty_setsFieldNameAndDirectionToNull() // NOPMD: Test method name format
{
Map properties = new Map{
'fieldName' => '',
@@ -67,12 +71,14 @@ public inherited sharing class SearchOrderByTest
configurationController.verify();
- System.assertEquals( '', orderBy.fieldName, 'configure, when given properties with empty fieldName and direction, will set the fieldName to an empty string' );
- System.assertEquals( '', orderBy.direction, 'configure, when given properties with empty fieldName and direction, will set the fieldName to an empty string' );
+ System.assertEquals( null, orderBy.fieldName, 'configure, when given properties with empty fieldName and direction, will set the fieldName to null' );
+ System.assertEquals( null, orderBy.direction, 'configure, when given properties with empty fieldName and direction, will set the fieldName to null' );
+
+ System.assert( ! orderBy.isConfigured(), 'configure, when given properties with empty fieldName and direction, will set the orderBy to not configured' );
}
@isTest
- private static void configure_whenFieldNameIsUnmapped_setsFieldNameAndDirectionToEmpty() // NOPMD: Test method name format
+ private static void configure_whenFieldNameIsUnmapped_setsFieldNameAndDirectionToNull() // NOPMD: Test method name format
{
Map properties = new Map{
'fieldName' => 'unmapped',
@@ -91,8 +97,10 @@ public inherited sharing class SearchOrderByTest
configurationController.verify();
- System.assertEquals( '', orderBy.fieldName, 'configure, when given properties with unmapped fieldName, will set the fieldName to an empty string' );
- System.assertEquals( '', orderBy.direction, 'configure, when given properties with unmapped fieldName, will set the direction to an empty string' );
+ System.assertEquals( null, orderBy.fieldName, 'configure, when given properties with unmapped fieldName, will set the fieldName to null' );
+ System.assertEquals( null, orderBy.direction, 'configure, when given properties with unmapped fieldName, will set the direction to null' );
+
+ System.assert( ! orderBy.isConfigured(), 'configure, when given properties with unmapped fieldName, will set the orderBy to not configured' );
}
@isTest
@@ -116,6 +124,8 @@ public inherited sharing class SearchOrderByTest
System.assertEquals( 'asc', orderBy.direction, 'configure, when given properties with no direction, will set the direction to asc' );
System.assertEquals( 'target', orderBy.fieldName, 'configure, when given properties with no direction, will set the fieldName' );
+
+ System.assert( orderBy.isConfigured(), 'configure, when given properties with no direction, will set the orderBy to configured' );
}
@isTest
@@ -169,7 +179,6 @@ public inherited sharing class SearchOrderByTest
@isTest
private static void configure_whenPassedInvalidDirection_throwsAnException() // NOPMD: Test method name format
{
-
Map properties = new Map{
'fieldName' => 'source',
'direction' => 'invalid'
@@ -290,5 +299,4 @@ public inherited sharing class SearchOrderByTest
return null;
}
}
-
}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls b/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls
index 04ef72522d1..4667e1b899c 100644
--- a/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls
+++ b/framework/default/ortoo-core/default/classes/utils/SobjectUtils.cls
@@ -98,7 +98,7 @@ public inherited sharing class SobjectUtils
/**
* States if the given SObject is of a type that the current user is allowed to insert
*
- * @param SObject The SObject to check the type of
+ * @param SObject The SObject to check 'isCreatable' of
* @return Boolean States if the user can insert records of this type
*/
public static Boolean isCreateable( Sobject record )
@@ -111,7 +111,7 @@ public inherited sharing class SobjectUtils
/**
* States if the given SObject is of a type that the current user is allowed to update
*
- * @param SObject The SObject to check the type of
+ * @param SObject The SObject to check 'isUpdateable' of
* @return Boolean States if the user can update records of this type
*/
public static Boolean isUpdateable( Sobject record )
@@ -124,7 +124,7 @@ public inherited sharing class SobjectUtils
/**
* States if the given SObject is of a type that the current user is allowed to delete
*
- * @param SObject The SObject to check the type of
+ * @param SObject The SObject to check 'isDeletable' of
* @return Boolean States if the user can delete records of this type
*/
public static Boolean isDeletable( Sobject record )
@@ -134,6 +134,58 @@ public inherited sharing class SobjectUtils
return getSObjectDescribeResult( record ).isDeletable();
}
+ /**
+ * States if the given SObjectType is one that the current user is allowed to insert
+ *
+ * @param SobjectType The SObject to check 'isCreateable' of
+ * @return Boolean States if the user can insert records of this type
+ */
+ public static Boolean isCreateable( SobjectType type )
+ {
+ Contract.requires( type != null, 'isCreateable called with a null type' );
+
+ return getSObjectDescribeResult( type ).isCreateable();
+ }
+
+ /**
+ * States if the given SObjectType is one that the current user is allowed to update
+ *
+ * @param SobjectType The SObject to check 'isUpdateable' of
+ * @return Boolean States if the user can update records of this type
+ */
+ public static Boolean isUpdateable( SobjectType type )
+ {
+ Contract.requires( type != null, 'isUpdateable called with a null type' );
+
+ return getSObjectDescribeResult( type ).isUpdateable();
+ }
+
+ /**
+ * States if the given SObjectType is one that the current user is allowed to delete
+ *
+ * @param SobjectType The SObject to check 'isDeletable' of
+ * @return Boolean States if the user can delete records of this type
+ */
+ public static Boolean isDeletable( SobjectType type )
+ {
+ Contract.requires( type != null, 'isDeletable called with a null type' );
+
+ return getSObjectDescribeResult( type ).isDeletable();
+ }
+
+ /**
+ * States if the given SObjectType is one that the current user is allowed to access
+ *
+ * @param SobjectType The SObject to check 'isAccessible' of
+ * @return Boolean States if the user can delete records of this type
+ */
+ public static Boolean isAccessible( SobjectType type )
+ {
+ Contract.requires( type != null, 'isAccessible called with a null type' );
+
+ return getSObjectDescribeResult( type ).isAccessible();
+ }
+
/**
* Given an SObject record, will return the DescrideSObjectResult for it.
*
@@ -149,6 +201,24 @@ public inherited sharing class SobjectUtils
{
Contract.requires( record != null, 'getSobjectDescribeResult called with a null record' );
- return record.getSObjectType().getDescribe();
+ return getSobjectDescribeResult( record.getSObjectType() );
+ }
+
+ /**
+ * Given an SObject Type record, will return the DescrideSObjectResult for it.
+ *
+ * Generally shouldn't be used by external methods. Instead the question of the
+ * describe result should be asked of SobjectUtils.
+ *
+ * For example, see 'isCreateable'.
+ *
+ * @param SObjectType The SObjectType to get the describe for
+ * @return DescribeSObjectResult The passed SObject's describe
+ */
+ private static DescribeSObjectResult getSobjectDescribeResult( SobjectType type )
+ {
+ Contract.requires( type != null, 'getSobjectDescribeResult called with a null type' );
+
+ return type.getDescribe();
}
}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls b/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls
index ad18c22b50c..997bdda706b 100644
--- a/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls
+++ b/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls
@@ -12,18 +12,19 @@ private without sharing class ContractTest
private static void requires_whenGivenAFalseCondition_willThrowAnException() // NOPMD: Test method name format
{
Test.startTest();
- String exceptionMessage;
+ ortoo_Exception exceptionThrown;
try
{
Contract.requires( false, 'will throw' );
}
- catch ( Exception e )
+ catch ( ortoo_Exception e )
{
- exceptionMessage = e.getMessage();
+ exceptionThrown = e;
}
Test.stopTest();
- Amoss_Asserts.assertContains( 'Contract.requires failed: will throw', exceptionMessage, 'requires, when given a false condition, will throw an exception' );
+ Amoss_Asserts.assertContains( 'Contract.requires failed: will throw', exceptionThrown.getMessage(), 'requires, when given a false condition, will throw an exception' );
+ System.assertEquals( 'requires_whenGivenAFalseCondition_willThrowAnException', exceptionThrown.getStackTrace().getInnermostMethodName(), 'requires, when given a false condition, will throw an exception with the stack trace set to the caller' );
}
@isTest
@@ -37,18 +38,19 @@ private without sharing class ContractTest
private static void ensures_whenGivenAFalseCondition_willThrowAnException() // NOPMD: Test method name format
{
Test.startTest();
- String exceptionMessage;
+ ortoo_Exception exceptionThrown;
try
{
Contract.ensures( false, 'will throw' );
}
- catch ( Exception e )
+ catch ( ortoo_Exception e )
{
- exceptionMessage = e.getMessage();
+ exceptionThrown = e;
}
Test.stopTest();
- Amoss_Asserts.assertContains( 'Contract.ensures failed: will throw', exceptionMessage, 'ensures, when given a false condition, will throw an exception' );
+ Amoss_Asserts.assertContains( 'Contract.ensures failed: will throw', exceptionThrown.getMessage(), 'ensures, when given a false condition, will throw an exception' );
+ System.assertEquals( 'ensures_whenGivenAFalseCondition_willThrowAnException', exceptionThrown.getStackTrace().getInnermostMethodName(), 'ensures, when given a false condition, will throw an exception with the stack trace set to the caller' );
}
@isTest
@@ -62,17 +64,18 @@ private without sharing class ContractTest
private static void assert_whenGivenAFalseCondition_willThrowAnException() // NOPMD: Test method name format
{
Test.startTest();
- String exceptionMessage;
+ ortoo_Exception exceptionThrown;
try
{
Contract.assert( false, 'will throw' );
}
- catch ( Exception e )
+ catch ( ortoo_Exception e )
{
- exceptionMessage = e.getMessage();
+ exceptionThrown = e;
}
Test.stopTest();
- Amoss_Asserts.assertContains( 'Contract.assert failed: will throw', exceptionMessage, 'assert, when given a false condition, will throw an exception' );
+ Amoss_Asserts.assertContains( 'Contract.assert failed: will throw', exceptionThrown.getMessage(), 'assert, when given a false condition, will throw an exception' );
+ System.assertEquals( 'ensures_whenGivenAFalseCondition_willThrowAnException', exceptionThrown.getStackTrace().getInnermostMethodName(), 'assert, when given a false condition, will throw an exception with the stack trace set to the caller' );
}
}
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/classes/utils/tests/SobjectUtilsTest.cls b/framework/default/ortoo-core/default/classes/utils/tests/SobjectUtilsTest.cls
index cafe13595ac..d3d958118a7 100644
--- a/framework/default/ortoo-core/default/classes/utils/tests/SobjectUtilsTest.cls
+++ b/framework/default/ortoo-core/default/classes/utils/tests/SobjectUtilsTest.cls
@@ -150,11 +150,13 @@ private without sharing class SobjectUtilsTest
@isTest
private static void isCreateable_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format
{
+ SObject nullRecord = null;
+
Test.startTest();
String exceptionMessage;
try
{
- SobjectUtils.isCreateable( null );
+ SobjectUtils.isCreateable( nullRecord );
}
catch ( Contract.RequiresException e )
{
@@ -179,11 +181,13 @@ private without sharing class SobjectUtilsTest
@isTest
private static void isUpdateable_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format
{
+ SObject nullRecord = null;
+
Test.startTest();
String exceptionMessage;
try
{
- SobjectUtils.isUpdateable( null );
+ SobjectUtils.isUpdateable( nullRecord );
}
catch ( Contract.RequiresException e )
{
@@ -208,11 +212,13 @@ private without sharing class SobjectUtilsTest
@isTest
private static void isDeletable_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format
{
+ SObject nullRecord = null;
+
Test.startTest();
String exceptionMessage;
try
{
- SobjectUtils.isDeletable( null );
+ SobjectUtils.isDeletable( nullRecord );
}
catch ( Contract.RequiresException e )
{
@@ -222,4 +228,120 @@ private without sharing class SobjectUtilsTest
Amoss_Asserts.assertContains( 'isDeletable called with a null record', exceptionMessage, 'isDeletable, when given a null record, will throw an exception' );
}
+
+ @isTest
+ private static void isCreateable_sobjectType_whenCalled_willReturnIsCreatableOfThatSobjectType() // NOPMD: Test method name format
+ {
+ Boolean expectedIsCreateable = Contact.sObjectType.getDescribe().isCreateable();
+ Boolean actualIsCreateable = SobjectUtils.isCreateable( Contact.sObjectType );
+
+ System.assertEquals( expectedIsCreateable, actualIsCreateable, 'isCreatable, when called with an SObjectType, will return if that SObject Type is createable by the current user' );
+ }
+
+ @isTest
+ private static void isCreateable_sobjectType_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format
+ {
+ SobjectType nullType = null;
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ SobjectUtils.isCreateable( nullType );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ Amoss_Asserts.assertContains( 'isCreateable called with a null type', exceptionMessage, 'isCreateable, when given a null SObjectType, will throw an exception' );
+ }
+
+ @isTest
+ private static void isUpdateable_sobjectType_whenCalled_willReturnIsUpdateableOfThatSobject() // NOPMD: Test method name format
+ {
+ Boolean expectedIsUpdateable = Contact.sObjectType.getDescribe().isUpdateable();
+ Boolean actualIsUpdateable = SobjectUtils.isUpdateable( Contact.sObjectType );
+
+ System.assertEquals( expectedIsUpdateable, actualIsUpdateable, 'isUpdateable, when called with an SObjectType, will return if that SObject Type is updateable by the current user' );
+ }
+
+ @isTest
+ private static void isUpdateable_sobjectType_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format
+ {
+ SobjectType nullType = null;
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ SobjectUtils.isUpdateable( nullType );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ Amoss_Asserts.assertContains( 'isUpdateable called with a null type', exceptionMessage, 'isUpdateable, when given a null SObjectType, will throw an exception' );
+ }
+
+ @isTest
+ private static void isDeletable_sobjectType_whenCalled_willReturnIsUpdateableOfThatSobject() // NOPMD: Test method name format
+ {
+ Boolean expectedIsDeletable = Contact.sObjectType.getDescribe().isDeletable();
+ Boolean actualIsDeletable = SobjectUtils.isDeletable( Contact.sobjectType );
+
+ System.assertEquals( expectedIsDeletable, actualIsDeletable, 'isDeletable, when called with an SObjectType, will return if that SObject Type is deletable by the current user' );
+ }
+
+ @isTest
+ private static void isDeletable_sobjectType_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format
+ {
+ SobjectType nullType = null;
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ SobjectUtils.isDeletable( nullType );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ Amoss_Asserts.assertContains( 'isDeletable called with a null type', exceptionMessage, 'isDeletable, when given a null SObjectType, will throw an exception' );
+ }
+
+ @isTest
+ private static void isAccessible_sobjectType_whenCalled_willReturnIsUpdateableOfThatSobject() // NOPMD: Test method name format
+ {
+ Boolean expectedIsAccessible = Contact.sObjectType.getDescribe().isAccessible();
+ Boolean actualIsAccessible = SobjectUtils.isAccessible( Contact.sobjectType );
+
+ System.assertEquals( expectedIsAccessible, actualIsAccessible, 'isAccessible, when called with an SObjectType, will return if that SObject Type is accessible by the current user' );
+ }
+
+ @isTest
+ private static void isAccessible_sobjectType_whenGivenANullSobject_willThrowAnException() // NOPMD: Test method name format
+ {
+ SobjectType nullType = null;
+
+ Test.startTest();
+ String exceptionMessage;
+ try
+ {
+ SobjectUtils.isAccessible( nullType );
+ }
+ catch ( Contract.RequiresException e )
+ {
+ exceptionMessage = e.getMessage();
+ }
+ Test.stopTest();
+
+ Amoss_Asserts.assertContains( 'isAccessible called with a null type', exceptionMessage, 'isAccessible, when given a null SObjectType, will throw an exception' );
+ }
}
\ No newline at end of file
From 3448786d023c0760bee839e57c279057d1eea319 Mon Sep 17 00:00:00 2001
From: Robert Baillie
Date: Mon, 24 Jan 2022 11:47:50 +0000
Subject: [PATCH 3/3] Fixed check on message
---
.../ortoo-core/default/classes/utils/tests/ContractTest.cls | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls b/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls
index 997bdda706b..5c8e9f90c24 100644
--- a/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls
+++ b/framework/default/ortoo-core/default/classes/utils/tests/ContractTest.cls
@@ -76,6 +76,6 @@ private without sharing class ContractTest
Test.stopTest();
Amoss_Asserts.assertContains( 'Contract.assert failed: will throw', exceptionThrown.getMessage(), 'assert, when given a false condition, will throw an exception' );
- System.assertEquals( 'ensures_whenGivenAFalseCondition_willThrowAnException', exceptionThrown.getStackTrace().getInnermostMethodName(), 'assert, when given a false condition, will throw an exception with the stack trace set to the caller' );
+ System.assertEquals( 'assert_whenGivenAFalseCondition_willThrowAnException', exceptionThrown.getStackTrace().getInnermostMethodName(), 'assert, when given a false condition, will throw an exception with the stack trace set to the caller' );
}
}
\ No newline at end of file