diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls b/nebula-query-and-search/main/classes/AggregateQuery.cls index 47b98f9e..cbd619e9 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls @@ -16,6 +16,7 @@ global class AggregateQuery extends SOQL { private SOQL.GroupingDimension groupingDimension; private List aggregateFields; private List havingConditions; + private String countQuery; global AggregateQuery(Schema.SObjectType sobjectType) { super(sobjectType, false); @@ -79,8 +80,29 @@ global class AggregateQuery extends SOQL { return this.havingAggregate(aggregateFunction, new SOQL.QueryField(field), operator, value); } + global AggregateQuery havingAggregate(SOQL.Aggregate aggregateFunction, Schema.SObjectField field, SOQL.Operator operator, Object value, String bindWithKey) { + return this.havingAggregate(aggregateFunction, new SOQL.QueryField(field), operator, value, bindWithKey); + } + global AggregateQuery havingAggregate(SOQL.Aggregate aggregateFunction, SOQL.QueryField queryField, SOQL.Operator operator, Object value) { - this.havingConditions.add(aggregateFunction.name() + '(' + queryField + ') ' + SOQL.getOperatorValue(operator) + ' ' + value); + return this.havingAggregate(aggregateFunction, queryField, operator, value, null); + } + + global AggregateQuery havingAggregate(SOQL.Aggregate aggregateFunction, SOQL.QueryField queryField, SOQL.Operator operator, Object value, String bindWithKey) { + this.havingConditions.add( + String.format( + '{0}({1}) {2} {3}', + new List { + aggregateFunction.name(), + queryField.toString(), + SOQL.getOperatorValue(operator), + (String.isNotBlank(bindWithKey) ? ':' + bindWithKey : new QueryArgument(value).toString()) + } + ) + ); + if (String.isNotBlank(bindWithKey)) { + this.bindsMap.put(bindWithKey, value); + } return this.setHasChanged(); } @@ -88,10 +110,18 @@ global class AggregateQuery extends SOQL { return this.filterWhere(new SOQL.QueryField(field), operator, value); } + global AggregateQuery filterWhere(Schema.SObjectField field, SOQL.Operator operator, Object value, String bindWithKey) { + return this.filterWhere(new SOQL.QueryField(field), operator, value, bindWithKey); + } + global AggregateQuery filterWhere(SOQL.QueryField queryField, SOQL.Operator operator, Object value) { return this.filterWhere(new SOQL.QueryFilter(queryField, operator, value)); } + global AggregateQuery filterWhere(SOQL.QueryField queryField, SOQL.Operator operator, Object value, String bindWithKey) { + return this.filterWhere(new SOQL.QueryFilter(queryField, operator, value, bindWithKey)); + } + global AggregateQuery filterWhere(SOQL.QueryFilter filter) { return this.filterWhere(new List{ filter }); } @@ -106,6 +136,11 @@ global class AggregateQuery extends SOQL { return this.setHasChanged(); } + global AggregateQuery withAccessLevel(System.AccessLevel accessLevel) { + super.doWithAccessLevel(accessLevel); + return this.setHasChanged(); + } + global AggregateQuery orderByField(Schema.SObjectField field) { return this.orderByField(field, null); } @@ -166,6 +201,26 @@ global class AggregateQuery extends SOQL { return this.setHasChanged(); } + global AggregateQuery setBind(String key, Object value) { + super.doSetBind(key, value); + return this.setHasChanged(); + } + + global AggregateQuery setBinds(Map binds) { + super.doSetBinds(binds); + return this.setHasChanged(); + } + + global AggregateQuery removeBind(String key) { + super.doRemoveBind(key); + return this.setHasChanged(); + } + + global AggregateQuery clearBinds() { + super.doClearBinds(); + return this.setHasChanged(); + } + // TODO decide if this should be global public AggregateQuery cacheResults() { super.doCacheResults(); @@ -206,10 +261,12 @@ global class AggregateQuery extends SOQL { return this.query; } - // TODO consider renaming to getCountResult() - @SuppressWarnings('PMD.ApexSOQLInjection') - global Integer getResultCount() { - String countQuery = + global String getCountQuery() { + if (this.countQuery != null && !this.hasChanged) { + return this.countQuery; + } + + this.countQuery = 'SELECT COUNT()' + ' FROM ' + this.sobjectType + @@ -220,7 +277,19 @@ global class AggregateQuery extends SOQL { super.doGetOrderByString() + super.doGetLimitCountString() + super.doGetOffetString(); - return Database.countQuery(countQuery); + + System.debug(System.LoggingLevel.FINEST, this.countQuery); + return this.countQuery; + } + + // TODO consider renaming to getCountResult() + @SuppressWarnings('PMD.ApexSOQLInjection') + global Integer getResultCount() { + return Database.countQueryWithBinds( + this.getCountQuery(), + this.doGetBindsMap(), + this.doGetAccessLevel() + ); } global AggregateResult getFirstResult() { diff --git a/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml b/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml index 133fce1e..c01f6433 100644 --- a/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml +++ b/nebula-query-and-search/main/classes/AggregateQuery.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 61.0 Active diff --git a/nebula-query-and-search/main/classes/Query.cls b/nebula-query-and-search/main/classes/Query.cls index 2025858f..641f3b9f 100644 --- a/nebula-query-and-search/main/classes/Query.cls +++ b/nebula-query-and-search/main/classes/Query.cls @@ -199,10 +199,18 @@ global class Query extends SOQL { return this.filterWhere(new SOQL.QueryField(field), operator, value); } + global Query filterWhere(Schema.SObjectField field, SOQL.Operator operator, Object value, String bindWithKey) { + return this.filterWhere(new SOQL.QueryField(field), operator, value, bindWithKey); + } + global Query filterWhere(SOQL.QueryField queryField, SOQL.Operator operator, Object value) { return this.filterWhere(new SOQL.QueryFilter(queryField, operator, value)); } + global Query filterWhere(SOQL.QueryField queryField, SOQL.Operator operator, Object value, String bindWithKey) { + return this.filterWhere(new SOQL.QueryFilter(queryField, operator, value, bindWithKey)); + } + global Query filterWhere(SOQL.QueryFilter filter) { return this.filterWhere(new List{ filter }); } @@ -239,6 +247,11 @@ global class Query extends SOQL { //return this.setHasChanged(); //} + global Query withAccessLevel(System.AccessLevel accessLevel) { + super.doWithAccessLevel(accessLevel); + return this.setHasChanged(); + } + global Query orderByField(Schema.SObjectField field) { return this.orderByField(new SOQL.QueryField(field)); } @@ -289,6 +302,26 @@ global class Query extends SOQL { return this.setHasChanged(); } + global Query setBind(String key, Object value) { + super.doSetBind(key, value); + return this.setHasChanged(); + } + + global Query setBinds(Map binds) { + super.doSetBinds(binds); + return this.setHasChanged(); + } + + global Query removeBind(String key) { + super.doRemoveBind(key); + return this.setHasChanged(); + } + + global Query clearBinds() { + super.doClearBinds(); + return this.setHasChanged(); + } + // TODO decide if this should be global public Query cacheResults() { super.doCacheResults(); diff --git a/nebula-query-and-search/main/classes/Query.cls-meta.xml b/nebula-query-and-search/main/classes/Query.cls-meta.xml index 133fce1e..c01f6433 100644 --- a/nebula-query-and-search/main/classes/Query.cls-meta.xml +++ b/nebula-query-and-search/main/classes/Query.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 61.0 Active diff --git a/nebula-query-and-search/main/classes/RecordSearch.cls b/nebula-query-and-search/main/classes/RecordSearch.cls index f3fbdb4a..143d0d04 100644 --- a/nebula-query-and-search/main/classes/RecordSearch.cls +++ b/nebula-query-and-search/main/classes/RecordSearch.cls @@ -64,6 +64,11 @@ global class RecordSearch extends SOSL { return this.setHasChanged(); } + global RecordSearch withAccessLevel(System.AccessLevel accessLevel) { + super.doWithAccessLevel(accessLevel); + return this.setHasChanged(); + } + global RecordSearch updateArticleReporting(SOSL.ArticleReporting articleReporting) { this.articleReporting = articleReporting; return this.setHasChanged(); diff --git a/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml b/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml index 133fce1e..c01f6433 100644 --- a/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml +++ b/nebula-query-and-search/main/classes/RecordSearch.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 61.0 Active diff --git a/nebula-query-and-search/main/classes/SOQL.cls b/nebula-query-and-search/main/classes/SOQL.cls index a2b0f802..178c201d 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls +++ b/nebula-query-and-search/main/classes/SOQL.cls @@ -181,12 +181,13 @@ global abstract class SOQL implements Comparable { protected Set excludedQueryFields; protected Scope scope; protected List whereFilters; + protected System.AccessLevel accessLevel; protected List orderByFieldApiNames; protected Integer limitCount; protected Integer offset; protected Boolean hasChanged; protected Boolean sortQueryFields; - + protected Map bindsMap; protected Boolean cacheResults; protected SOQL(Schema.SObjectType sobjectType, Boolean sortQueryFields) { @@ -198,6 +199,8 @@ global abstract class SOQL implements Comparable { this.excludedQueryFields = new Set(); this.whereFilters = new List(); this.orderByFieldApiNames = new List(); + this.accessLevel = System.AccessLevel.SYSTEM_MODE; + this.bindsMap = new Map(); this.cacheResults = false; this.hasChanged = false; } @@ -246,12 +249,15 @@ global abstract class SOQL implements Comparable { } protected void doFilterWhere(List filters) { - if (filters == null || filters.isEmpty()) { + if (filters?.isEmpty() != false) { return; } for (SOQL.QueryFilter filter : filters) { this.whereFilters.add(filter.toString()); + if (String.isNotBlank(filter.bindKey)) { + this.bindsMap.put(filter.bindKey, filter.value); + } } this.doSetHasChanged(); } @@ -271,6 +277,10 @@ global abstract class SOQL implements Comparable { this.doSetHasChanged(); } + protected void doWithAccessLevel(System.AccessLevel accessLevel) { + this.accessLevel = accessLevel; + } + protected void doOrderBy(SOQL.QueryField queryField, SOQL.SortOrder sortOrder, Boolean sortNullsFirst) { this.doOrderBy(queryField.toString(), sortOrder, sortNullsFirst); } @@ -296,6 +306,22 @@ global abstract class SOQL implements Comparable { this.offset = offset; } + protected void doSetBind(String key, Object value) { + this.bindsMap.put(key, value); + } + + protected void doSetBinds(Map binds) { + this.bindsMap.putAll(binds); + } + + protected void doRemoveBind(String key) { + this.bindsMap.remove(key); + } + + protected void doClearBinds() { + this.bindsMap.clear(); + } + protected SObject doGetFirstResult() { List results = this.doGetResults(); return results == null || results.isEmpty() ? null : results[0]; @@ -305,7 +331,11 @@ global abstract class SOQL implements Comparable { if (this.cacheResults) { return this.getCachedResults(); } else { - return Database.query(this.getQuery()); + return Database.queryWithBinds( + this.getQuery(), + this.doGetBindsMap(), + this.doGetAccessLevel() + ); } } @@ -361,6 +391,10 @@ global abstract class SOQL implements Comparable { return this.whereFilters.isEmpty() ? '' : ' WHERE ' + String.join(this.whereFilters, ' AND '); } + protected System.AccessLevel doGetAccessLevel() { + return this.accessLevel ?? System.AccessLevel.SYSTEM_MODE; + } + protected String doGetOrderByString() { return this.orderByFieldApiNames.isEmpty() ? '' : ' ORDER BY ' + String.join(this.orderByFieldApiNames, ', '); } @@ -373,6 +407,10 @@ global abstract class SOQL implements Comparable { return this.offset == null ? '' : ' OFFSET ' + this.offset; } + protected Map doGetBindsMap() { + return this.bindsMap ?? new Map(); + } + private void doSetHasChanged() { this.hasChanged = true; } @@ -383,7 +421,14 @@ global abstract class SOQL implements Comparable { Boolean isCached = cachedResultsByHashCode.containsKey(hashCode); if (!isCached) { - cachedResultsByHashCode.put(hashCode, Database.query(query)); + cachedResultsByHashCode.put( + hashCode, + Database.queryWithBinds( + this.getQuery(), + this.doGetBindsMap(), + this.doGetAccessLevel() + ) + ); } // Always return a deep clone so the original cached version is never modified @@ -542,16 +587,26 @@ global abstract class SOQL implements Comparable { private Object value; private String formattedValue; private String filterString; + private String bindKey; global QueryFilter(Schema.SObjectField field, SOQL.Operator operator, Object value) { this(new QueryField(field), operator, value); } + global QueryFilter(Schema.SObjectField field, SOQL.Operator operator, Object value, String bindKey) { + this(new QueryField(field), operator, value, bindKey); + } + global QueryFilter(QueryField queryField, SOQL.Operator operator, Object value) { + this(queryField, operator, value, null); + } + + global QueryFilter(QueryField queryField, SOQL.Operator operator, Object value, String bindKey) { this.queryField = queryField; this.operator = operator; this.value = value; - this.formattedValue = new QueryArgument(value).toString(); + this.formattedValue = (String.isNotBlank(bindKey) ? ':' + bindKey : new QueryArgument(value).toString()); + this.bindKey = bindKey; this.filterString = queryField + ' ' + SOQL.getOperatorValue(operator) + ' ' + formattedValue; } @@ -605,7 +660,7 @@ global abstract class SOQL implements Comparable { public class SOQLException extends Exception { } - private class QueryArgument { + public class QueryArgument { private String value; public QueryArgument(Object valueToFormat) { diff --git a/nebula-query-and-search/main/classes/SOQL.cls-meta.xml b/nebula-query-and-search/main/classes/SOQL.cls-meta.xml index 133fce1e..c01f6433 100644 --- a/nebula-query-and-search/main/classes/SOQL.cls-meta.xml +++ b/nebula-query-and-search/main/classes/SOQL.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 61.0 Active diff --git a/nebula-query-and-search/main/classes/SOSL.cls b/nebula-query-and-search/main/classes/SOSL.cls index 918c5b1f..32ad7489 100644 --- a/nebula-query-and-search/main/classes/SOSL.cls +++ b/nebula-query-and-search/main/classes/SOSL.cls @@ -42,6 +42,7 @@ global abstract class SOSL { protected SOSL.ArticleReporting articleReporting; protected List withClauses; protected List withDataCategoryClauses; + protected System.AccessLevel accessLevel; protected SOSL.SearchGroup searchGroup; protected SOSL(String searchTerm, Query sobjectQuery) { @@ -57,6 +58,7 @@ global abstract class SOSL { this.searchGroup = SOSL.SearchGroup.ALL_FIELDS; this.withClauses = new List(); this.withDataCategoryClauses = new List(); + this.accessLevel = System.AccessLevel.SYSTEM_MODE; } public Set getSObjectTypes() { @@ -88,7 +90,7 @@ global abstract class SOSL { if (this.cacheResults) { return this.getCachedResults(); } else { - return System.Search.query(this.getSearch()); + return System.Search.query(this.getSearch(), this.doGetAccessLevel()); } } @@ -119,6 +121,14 @@ global abstract class SOSL { return this.withClauses.isEmpty() ? '' : ' WITH ' + String.join(this.withClauses, ' WITH '); } + protected void doWithAccessLevel(System.AccessLevel accessLevel) { + this.accessLevel = accessLevel; + } + + protected System.AccessLevel doGetAccessLevel() { + return this.accessLevel ?? System.AccessLevel.SYSTEM_MODE; + } + protected String doGetUpdateArticleReportingString() { return this.articleReporting == null ? '' : ' UPDATE ' + this.articleReporting.name(); } @@ -129,7 +139,10 @@ global abstract class SOSL { Boolean isCached = cachedSearchResultsByHashCode.containsKey(hashCode); if (!isCached) { - cachedSearchResultsByHashCode.put(hashCode, Search.query(searchQuery)); + cachedSearchResultsByHashCode.put( + hashCode, + System.Search.query(searchQuery, this.doGetAccessLevel()) + ); } // Always return a deep clone so the original cached version is never modified diff --git a/nebula-query-and-search/main/classes/SOSL.cls-meta.xml b/nebula-query-and-search/main/classes/SOSL.cls-meta.xml index 133fce1e..c01f6433 100644 --- a/nebula-query-and-search/main/classes/SOSL.cls-meta.xml +++ b/nebula-query-and-search/main/classes/SOSL.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 61.0 Active diff --git a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls index 8e4a2a86..a8535619 100644 --- a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls +++ b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls @@ -8,6 +8,35 @@ ) @IsTest(IsParallel=true) private class AggregateQuery_Tests { + + @IsTest + static void it_should_construct_a_count_query_without_binds() + { + // SETUP + String expectedQueryString = 'SELECT COUNT() FROM Opportunity WHERE AccountId != null'; + + // TEST + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Opportunity.AccountId, SOQL.Operator.NOT_EQUAL_TO, null)); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getCountQuery()); + } + + @IsTest + static void it_should_construct_a_count_query_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT COUNT() FROM Opportunity WHERE AccountId != :accountIdFilter'; + + // TEST + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Opportunity.AccountId, SOQL.Operator.NOT_EQUAL_TO, null, 'accountIdFilter')); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getCountQuery()); + } + @IsTest static void it_should_be_usable_after_construction() { String expectedQueryString = 'SELECT COUNT(Id) COUNT__Id FROM Opportunity'; @@ -34,6 +63,53 @@ private class AggregateQuery_Tests { System.Assert.areEqual(expectedResults, returnedResults); } + @IsTest + static void it_should_return_count_result_when_filtering_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT COUNT() FROM Opportunity WHERE AccountId != :accountIdFilter'; + Integer expectedResult = Database.countQueryWithBinds( + expectedQueryString, + new Map { + 'accountIdFilter' => null + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Opportunity.AccountId, SOQL.Operator.NOT_EQUAL_TO, null, 'accountIdFilter')); + + // TEST + Integer returnedResult = aggregateQuery.getResultCount(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getCountQuery()); + System.Assert.areEqual(expectedResult, returnedResult); + } + + @IsTest + static void it_should_return_results_when_filtering_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT Type FROM Opportunity WHERE AccountId != :accountIdFilter GROUP BY Type'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'accountIdFilter' => null + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .groupByField(Schema.Opportunity.Type) + .filterWhere(new SOQL.QueryFilter(Schema.Opportunity.AccountId, SOQL.Operator.NOT_EQUAL_TO, null, 'accountIdFilter')); + + // TEST + List returnedResults = aggregateQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + } + @IsTest static void it_should_return_results_when_filtering_with_an_or_statement() { String expectedQueryString = 'SELECT Type, COUNT(Id) COUNT__Id FROM Account WHERE (AnnualRevenue = null OR Type = null) AND ParentId != null GROUP BY Type'; @@ -115,6 +191,31 @@ private class AggregateQuery_Tests { System.Assert.areEqual(expectedResults, returnedResults); } + @IsTest + static void it_should_group_by_having_aggregate_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT Name, COUNT(Id) COUNT__Id FROM Account GROUP BY Name HAVING COUNT(Id) > :minCount'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'minCount' => 2 + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .groupByField(Schema.Account.Name) + .addAggregate(SOQL.Aggregate.COUNT, Schema.Account.Id) + .havingAggregate(SOQL.Aggregate.COUNT, Schema.Account.Id, SOQL.Operator.GREATER_THAN, 2, 'minCount'); + + // TEST + List returnedResults = aggregateQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + } + @IsTest static void it_should_group_by_a_date_function() { String expectedQueryString = 'SELECT CALENDAR_MONTH(CloseDate), COUNT(Id) COUNT__Id FROM Opportunity GROUP BY CALENDAR_MONTH(CloseDate)'; @@ -129,6 +230,52 @@ private class AggregateQuery_Tests { System.Assert.areEqual(expectedResults, returnedResults); } + @IsTest + static void it_should_run_with_system_mode() { + String expectedQueryString = 'SELECT COUNT(Id) COUNT__Id FROM Opportunity'; + + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .addAggregate(SOQL.Aggregate.COUNT, Opportunity.Id) + .withAccessLevel(System.AccessLevel.SYSTEM_MODE); + + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + List expectedResults = Database.query(expectedQueryString); + List returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = aggregateQuery.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isNull(caughtException, 'Query should not throw exception when run in System Mode'); + System.Assert.areEqual(expectedResults, returnedResults); + } + + @IsTest + static void it_should_run_with_user_mode() { + String expectedQueryString = 'SELECT COUNT(Id) COUNT__Id FROM Opportunity'; + + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Opportunity.SObjectType) + .addAggregate(SOQL.Aggregate.COUNT, Opportunity.Id) + .withAccessLevel(System.AccessLevel.USER_MODE); + + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + List expectedResults = Database.query(expectedQueryString); + List returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = aggregateQuery.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isInstanceOfType(caughtException, System.QueryException.class, 'Query should throw exception when run in User Mode'); + System.Assert.isTrue(caughtException.getMessage().contains('sObject type \'Opportunity\' is not supported'), 'Query should throw exception when run in User Mode'); + } + @IsTest static void it_should_build_a_ridiculous_query_string() { String expectedQueryString = @@ -136,7 +283,7 @@ private class AggregateQuery_Tests { ' COUNT_DISTINCT(AccountId) COUNT_DISTINCT__AccountId, COUNT_DISTINCT(OwnerId) COUNT_DISTINCT__OwnerId, COUNT_DISTINCT(Type) COUNT_DISTINCT__Type,' + ' MAX(CreatedDate) MAX__CreatedDate, MIN(CreatedDate) MIN__CreatedDate, SUM(Amount) SUM__Amount' + ' FROM Opportunity' + - ' WHERE AccountId != null' + + ' WHERE AccountId != null AND CreatedDate >= :createdDateFilter' + ' GROUP BY Account.Type, StageName' + ' ORDER BY Account.Type ASC NULLS FIRST, StageName ASC NULLS FIRST, SUM(Amount) ASC NULLS FIRST,' + ' MIN(CloseDate) DESC NULLS FIRST, MAX(Account.LastActivityDate) ASC NULLS FIRST' + @@ -160,12 +307,148 @@ private class AggregateQuery_Tests { .orderByAggregate(SOQL.Aggregate.MIN, Schema.Opportunity.CloseDate, SOQL.SortOrder.DESCENDING) .orderByAggregate(SOQL.Aggregate.MAX, new SOQL.QueryField(new List{ Schema.Opportunity.AccountId, Schema.Account.LastActivityDate })) .filterWhere(Schema.Opportunity.AccountId, SOQL.Operator.NOT_EQUAL_TO, null) + .filterWhere(Schema.Opportunity.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today(), 'createdDateFilter') .limitTo(100) .offsetBy(0); System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); - List expectedResults = Database.query(expectedQueryString); + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'createdDateFilter' => Date.today() + }, + System.AccessLevel.SYSTEM_MODE + ); + List returnedResults = aggregateQuery.getResults(); + System.Assert.areEqual(expectedResults, returnedResults); + } + + @IsTest + static void it_will_set_a_bind_variable() + { + // SETUP + String expectedQueryString = 'SELECT MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate >= :dateFilter'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'dateFilter' => Date.today().addDays(-1) + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')); + + // TEST + aggregateQuery.setBind('dateFilter', Date.today().addDays(-1)); + List returnedResults = aggregateQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse((Date)returnedResults[0].get('MIN__CreatedDate') < Date.today().addDays(-1)); + } + } + + @IsTest + static void it_will_set_multiple_bind_variables() + { + // SETUP + String expectedQueryString = 'SELECT MAX(CreatedDate) MAX__CreatedDate, MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'minDateFilter' => Date.today().addDays(-7), + 'maxDateFilter' => Date.today() + }, + System.AccessLevel.SYSTEM_MODE + ); + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .addAggregate(SOQL.Aggregate.MAX, Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'minDateFilter')) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Date.today().addDays(-1), 'maxDateFilter')); + + // TEST + aggregateQuery.setBind('minDateFilter', Date.today().addDays(-7)); + aggregateQuery.setBind('maxDateFilter', Date.today()); List returnedResults = aggregateQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse((Date)returnedResults[0].get('MIN__CreatedDate') < Date.today().addDays(-7)); + System.Assert.isFalse((Date)returnedResults[0].get('MAX__CreatedDate') >= Date.today()); + } + } + + @IsTest + static void it_will_remove_a_bind_variable() + { + // SETUP + String expectedQueryString = 'SELECT MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate >= :dateFilter'; + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')); + + // TEST + aggregateQuery.removeBind('dateFilter'); + Exception caughtException; + try { + List returnedResults = aggregateQuery.getResults(); + } + catch (Exception e) { + caughtException = e; + } + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.isInstanceOfType(caughtException, QueryException.class); + System.Assert.areEqual('Key \'dateFilter\' does not exist in the bindMap', caughtException.getMessage()); + } + + @IsTest + static void it_will_clear_all_bind_variables() + { + // SETUP + String expectedQueryString = 'SELECT MAX(CreatedDate) MAX__CreatedDate, MIN(CreatedDate) MIN__CreatedDate FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter'; + AggregateQuery aggregateQuery = new AggregateQuery(Schema.Account.SObjectType) + .addAggregate(SOQL.Aggregate.MIN, Schema.Account.CreatedDate) + .addAggregate(SOQL.Aggregate.MAX, Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'minDateFilter')) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Date.today().addDays(-1), 'maxDateFilter')); + + // TEST + aggregateQuery.clearBinds(); + Exception caughtException; + try { + List returnedResults = aggregateQuery.getResults(); + } + catch (Exception e) { + caughtException = e; + } + + // VERIFY + System.Assert.areEqual(expectedQueryString, aggregateQuery.getQuery()); + System.Assert.isInstanceOfType(caughtException, QueryException.class); + System.Assert.areEqual('Key \'maxDateFilter\' does not exist in the bindMap', caughtException.getMessage()); + } + + static User minimumAccessUser() { + return new User( + Alias = 'newUser', + Email = 'newuser@testorg.com', + EmailEncodingKey = 'UTF-8', + LastName = 'Testing', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + ProfileId = [SELECT Id FROM Profile WHERE Name = 'Minimum Access - Salesforce'].Id, + TimeZoneSidKey = 'GMT', + UserName = 'newuser@testorg.com' + ); } } diff --git a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml index 133fce1e..c01f6433 100644 --- a/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml +++ b/nebula-query-and-search/tests/classes/AggregateQuery_Tests.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 61.0 Active diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls b/nebula-query-and-search/tests/classes/Query_Tests.cls index 051ac675..06e795bf 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls @@ -8,6 +8,35 @@ ) @IsTest(IsParallel=true) private class Query_Tests { + + @IsTest + static void it_should_construct_a_query_without_binds() + { + // SETUP + String expectedQueryString = 'SELECT Id, Name FROM Account WHERE CreatedDate >= THIS_MONTH'; + + // TEST + Query accountQuery = new Query(Schema.Account.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, new SOQL.DateLiteral(SOQL.FixedDateLiteral.THIS_MONTH))); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + } + + @IsTest + static void it_should_construct_a_query_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT Id, Name FROM Account WHERE CreatedDate >= :dateFilter'; + + // TEST + Query accountQuery = new Query(Schema.Account.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + } + @IsTest static void it_should_return_results_for_a_simple_query() { String expectedQueryString = 'SELECT Id, Name FROM Account'; @@ -193,6 +222,75 @@ private class Query_Tests { List accounts = accountQuery.getResults(); } + @IsTest + static void it_should_return_results_when_filtering_with_binds() + { + // SETUP + String expectedQueryString = 'SELECT Id, Name FROM Account WHERE CreatedDate >= :createdDateFilter'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'createdDateFilter' => Date.today() + }, + System.AccessLevel.SYSTEM_MODE + ); + Query accountQuery = new Query(Schema.Account.SObjectType) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today(), 'createdDateFilter')); + + // TEST + List returnedResults = accountQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + } + + @IsTest + static void it_should_run_with_system_mode() { + String expectedQueryString = 'SELECT Id, Name FROM Account LIMIT 1'; + + Query accountQuery = new Query(Schema.Account.SObjectType) + .limitTo(1) + .withAccessLevel(System.AccessLevel.SYSTEM_MODE); + + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + List expectedResults = Database.query(expectedQueryString); + List returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = accountQuery.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isNull(caughtException, 'Query should not throw exception when run in System Mode'); + System.Assert.areEqual(expectedResults, returnedResults); + } + + @IsTest + static void it_should_run_with_user_mode() { + String expectedQueryString = 'SELECT Id, Name FROM Account LIMIT 1'; + + Query accountQuery = new Query(Schema.Account.SObjectType) + .limitTo(1) + .withAccessLevel(System.AccessLevel.USER_MODE); + + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + List expectedResults = Database.query(expectedQueryString); + List returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = accountQuery.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isInstanceOfType(caughtException, System.QueryException.class, 'Query should throw exception when run in User Mode'); + System.Assert.isTrue(caughtException.getMessage().contains('sObject type \'Account\' is not supported'), 'Query should throw exception when run in User Mode'); + } + @IsTest static void it_includes_order_by_statement_for_single_field() { String expectedQueryString = 'SELECT Id, Name FROM Lead ORDER BY CreatedDate ASC NULLS FIRST'; @@ -277,4 +375,135 @@ private class Query_Tests { System.Test.stopTest(); } + + @IsTest + static void it_will_set_a_bind_variable() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :dateFilter ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'dateFilter' => Date.today().addDays(-1) + }, + System.AccessLevel.SYSTEM_MODE + ); + Query accountQuery = new Query(Schema.Account.SObjectType) + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')) + .orderByField(Schema.Account.CreatedDate, SOQL.SortOrder.ASCENDING) + .limitTo(1); + + // TEST + accountQuery.setBind('dateFilter', Date.today().addDays(-1)); + List returnedResults = accountQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse(returnedResults[0].CreatedDate < Date.today().addDays(-1)); + } + } + + @IsTest + static void it_will_set_multiple_bind_variables() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter ORDER BY CreatedDate ASC NULLS FIRST LIMIT 1'; + List expectedResults = Database.queryWithBinds( + expectedQueryString, + new Map { + 'minDateFilter' => Date.today().addDays(-7), + 'maxDateFilter' => Date.today() + }, + System.AccessLevel.SYSTEM_MODE + ); + Query accountQuery = new Query(Schema.Account.SObjectType) + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'minDateFilter')) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Date.today().addDays(-1), 'maxDateFilter')) + .orderByField(Schema.Account.CreatedDate, SOQL.SortOrder.ASCENDING) + .limitTo(1); + + // TEST + accountQuery.setBind('minDateFilter', Date.today().addDays(-7)); + accountQuery.setBind('maxDateFilter', Date.today()); + List returnedResults = accountQuery.getResults(); + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.areEqual(expectedResults, returnedResults); + if (!returnedResults.isEmpty()) + { + System.Assert.isFalse(returnedResults[0].CreatedDate < Date.today().addDays(-7)); + System.Assert.isFalse(returnedResults[0].CreatedDate >= Date.today()); + } + } + + @IsTest + static void it_will_remove_a_bind_variable() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate >= :dateFilter'; + Query accountQuery = new Query(Schema.Account.SObjectType) + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'dateFilter')); + + // TEST + accountQuery.removeBind('dateFilter'); + Exception caughtException; + try { + List returnedResults = accountQuery.getResults(); + } + catch (Exception e) { + caughtException = e; + } + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.isInstanceOfType(caughtException, QueryException.class); + System.Assert.areEqual('Key \'dateFilter\' does not exist in the bindMap', caughtException.getMessage()); + } + + @IsTest + static void it_will_clear_all_bind_variables() + { + // SETUP + String expectedQueryString = 'SELECT CreatedDate, Id, Name FROM Account WHERE CreatedDate < :maxDateFilter AND CreatedDate >= :minDateFilter'; + Query accountQuery = new Query(Schema.Account.SObjectType) + .addField(Schema.Account.CreatedDate) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.GREATER_THAN_OR_EQUAL_TO, Date.today().addMonths(-1), 'minDateFilter')) + .filterWhere(new SOQL.QueryFilter(Schema.Account.CreatedDate, SOQL.Operator.LESS_THAN, Date.today().addDays(-1), 'maxDateFilter')); + + // TEST + accountQuery.clearBinds(); + Exception caughtException; + try { + List returnedResults = accountQuery.getResults(); + } + catch (Exception e) { + caughtException = e; + } + + // VERIFY + System.Assert.areEqual(expectedQueryString, accountQuery.getQuery()); + System.Assert.isInstanceOfType(caughtException, QueryException.class); + System.Assert.areEqual('Key \'maxDateFilter\' does not exist in the bindMap', caughtException.getMessage()); + } + + static User minimumAccessUser() { + return new User( + Alias = 'newUser', + Email = 'newuser@testorg.com', + EmailEncodingKey = 'UTF-8', + LastName = 'Testing', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + ProfileId = [SELECT Id FROM Profile WHERE Name = 'Minimum Access - Salesforce'].Id, + TimeZoneSidKey = 'GMT', + UserName = 'newuser@testorg.com' + ); + } } diff --git a/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml b/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml index 133fce1e..c01f6433 100644 --- a/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml +++ b/nebula-query-and-search/tests/classes/Query_Tests.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 61.0 Active diff --git a/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls b/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls index 53805d8a..66a7a428 100644 --- a/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls +++ b/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls @@ -127,6 +127,56 @@ private class RecordSearch_Tests { List userSearchResults = userSearch.getFirstResults(); } + @IsTest + static void it_should_run_with_system_mode() { + String expectedSearchQueryString = 'FIND \'' + System.UserInfo.getUserEmail() + '\' IN ALL FIELDS RETURNING Contact(Id, Name)'; + + Query contactQuery = new Query(Schema.Contact.SObjectType); + RecordSearch contactSearch = new RecordSearch(System.UserInfo.getUserEmail(), contactQuery) + .withAccessLevel(System.AccessLevel.SYSTEM_MODE); + + System.assertEquals(expectedSearchQueryString, contactSearch.getSearch()); + List contactSearchResults = contactSearch.getFirstResults(); + + List> expectedResults = Search.query(expectedSearchQueryString); + List> returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = contactSearch.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isNull(caughtException, 'Search should not throw exception when run in System Mode'); + System.Assert.areEqual(expectedResults, returnedResults); + } + + @IsTest + static void it_should_run_with_user_mode() { + String expectedSearchQueryString = 'FIND \'' + System.UserInfo.getUserEmail() + '\' IN ALL FIELDS RETURNING Contact(Id, Name)'; + + Query contactQuery = new Query(Schema.Contact.SObjectType); + RecordSearch contactSearch = new RecordSearch(System.UserInfo.getUserEmail(), contactQuery) + .withAccessLevel(System.AccessLevel.USER_MODE); + + System.assertEquals(expectedSearchQueryString, contactSearch.getSearch()); + List contactSearchResults = contactSearch.getFirstResults(); + + List> expectedResults = Search.query(expectedSearchQueryString); + List> returnedResults; + Exception caughtException; + System.runAs(minimumAccessUser()) { + try { + returnedResults = contactSearch.getResults(); + } catch (Exception e) { + caughtException = e; + } + } + System.Assert.isInstanceOfType(caughtException, System.QueryException.class, 'Search should throw exception when run in User Mode'); + System.Assert.isTrue(caughtException.getMessage().contains('sObject type \'contact\' is not supported'), 'Search should throw exception when run in User Mode'); + } + @IsTest static void it_should_cache_search_results_when_enabled() { Integer loops = 4; @@ -150,4 +200,18 @@ private class RecordSearch_Tests { System.Test.stopTest(); } + + static User minimumAccessUser() { + return new User( + Alias = 'newUser', + Email = 'newuser@testorg.com', + EmailEncodingKey = 'UTF-8', + LastName = 'Testing', + LanguageLocaleKey = 'en_US', + LocaleSidKey = 'en_US', + ProfileId = [SELECT Id FROM Profile WHERE Name = 'Minimum Access - Salesforce'].Id, + TimeZoneSidKey = 'GMT', + UserName = 'newuser@testorg.com' + ); + } } diff --git a/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml b/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml index 133fce1e..c01f6433 100644 --- a/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml +++ b/nebula-query-and-search/tests/classes/RecordSearch_Tests.cls-meta.xml @@ -1,5 +1,5 @@ - 58.0 + 61.0 Active diff --git a/sfdx-project.json b/sfdx-project.json index ea0452fd..6644f90c 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -2,7 +2,7 @@ "name": "Nebula Query & Search", "namespace": "", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "58.0", + "sourceApiVersion": "61.0", "plugins": { "sfdx-plugin-prettier": { "enabled": true