diff --git a/.atom-build.yml b/.atom-build.yml new file mode 100644 index 0000000..acafb1a --- /dev/null +++ b/.atom-build.yml @@ -0,0 +1,2 @@ +cwd: '{PROJECT_PATH}' +cmd: java -jar .\tools\apexdoc\apexdoc.jar -s .\src\classes -t . -p public;protected -g https://github.com/jongpie/NebulaFramework/blob/master/src/classes/ -a header.html \ No newline at end of file diff --git a/src/classes/AggregateResultQueryBuilder.cls b/src/classes/AggregateResultQueryBuilder.cls new file mode 100644 index 0000000..abd8f46 --- /dev/null +++ b/src/classes/AggregateResultQueryBuilder.cls @@ -0,0 +1,188 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description A builder class that generates dynamic queries & returns a list of AggregateResult +* +*/ +public class AggregateResultQueryBuilder extends QueryBuilder implements IAggregateResultQueryBuilder { + + private Schema.SObjectType sobjectType; + private List groupByList; + private List aggregateFunctionList; + + public AggregateResultQueryBuilder(Schema.SObjectType sobjectType) { + this.sobjectType = sobjectType; + + this.aggregateFunctionList = new List(); + this.groupByList = new List(); + } + + public IAggregateResultQueryBuilder groupBy(IQueryField groupByQueryField) { + return this.groupBy(new List{groupByQueryField}); + } + + public IAggregateResultQueryBuilder groupBy(List groupByQueryFields) { + for(IQueryField groupByQueryField : groupByQueryFields) this.groupByList.add(groupByQueryField.getValue()); + return this; + } + + public IAggregateResultQueryBuilder groupBy(Schema.FieldSet fieldSet) { + for(Schema.FieldSetMember field : fieldSet.getFields()) this.groupByList.add(field.getFieldPath()); + return this; + } + + public IAggregateResultQueryBuilder groupBy(QueryDate groupByQueryDate) { + this.groupByList.add(groupByQueryDate.getValue()); + return this; + } + + /** + * @description Adds the average value of the numeric field to the dynamically generated aggregate query + * @param numericQueryField The field to use for calculating the average + * @return The instance of IAggregateResultQueryBuilder, to allow chaining methods + */ + public IAggregateResultQueryBuilder avg(IQueryField numericQueryField) { + return this.avg(numericQueryField, null); + } + + public IAggregateResultQueryBuilder avg(IQueryField numericQueryField, String fieldAlias) { + return buildAggregateFunction('AVG', numericQueryField, fieldAlias); + } + + public IAggregateResultQueryBuilder count(IQueryField queryField) { + return this.count(queryField, null); + } + + public IAggregateResultQueryBuilder count(IQueryField queryField, String fieldAlias) { + return buildAggregateFunction('COUNT', queryField, fieldAlias); + } + + public IAggregateResultQueryBuilder countDistinct(IQueryField queryField) { + return this.countDistinct(queryField, null); + } + + public IAggregateResultQueryBuilder countDistinct(IQueryField queryField, String fieldAlias) { + return buildAggregateFunction('COUNT_DISTINCT', queryField, fieldAlias); + } + + /** + * @description Adds the maximum value of the field to the dynamically generated aggregate query + * @param queryField The field to use for calculating the maximum + * @return The instance of IAggregateResultQueryBuilder, to allow chaining methods + */ + public IAggregateResultQueryBuilder max(IQueryField queryField) { + return this.max(queryField, null); + } + + public IAggregateResultQueryBuilder max(IQueryField queryField, String fieldAlias) { + return buildAggregateFunction('MAX', queryField, fieldAlias); + } + + /** + * @description Adds the minimum value of the field to the dynamically generated aggregate query + * @param queryField The field to use for calculating the minimum + * @return The instance of IAggregateResultQueryBuilder, to allow chaining methods + */ + public IAggregateResultQueryBuilder min(IQueryField queryField) { + return this.min(queryField, null); + } + + public IAggregateResultQueryBuilder min(IQueryField queryField, String fieldAlias) { + return buildAggregateFunction('MIN', queryField, fieldAlias); + } + + /** + * @description Sums the values of the supplied numeric field to the dynamically generated aggregate query + * @param numericQueryField The field to use for calculating the minimum + * @return The instance of IAggregateResultQueryBuilder, to allow chaining methods + */ + public IAggregateResultQueryBuilder sum(IQueryField numericQueryField) { + return this.sum(numericQueryField, null); + } + + public IAggregateResultQueryBuilder sum(IQueryField numericQueryField, String fieldAlias) { + return buildAggregateFunction('SUM', numericQueryField, fieldAlias); + } + + public IAggregateResultQueryBuilder filterBy(IQueryFilter queryFilter) { + super.doFilterBy(queryFilter); + return this; + } + + public IAggregateResultQueryBuilder filterBy(List queryFilters) { + super.doFilterBy(queryFilters); + return this; + } + + public IAggregateResultQueryBuilder orderBy(IQueryField orderByQueryField) { + super.doOrderBy(orderByQueryField); + return this; + } + + public IAggregateResultQueryBuilder orderBy(IQueryField orderByQueryField, QuerySortOrder sortOrder) { + super.doOrderBy(orderByQueryField, sortOrder); + return this; + } + + public IAggregateResultQueryBuilder orderBy(IQueryField orderByQueryField, QuerySortOrder sortOrder, QueryNullSortOrder nullsSortOrder) { + super.doOrderBy(orderByQueryField, sortOrder, nullsSortOrder); + return this; + } + + public IAggregateResultQueryBuilder limitCount(Integer limitCount) { + super.doLimitCount(limitCount); + return this; + } + + public String getQuery() { + String queryString = 'SELECT ' + this.getGroupByFieldString(false) + this.getAggregateFunctionString() + + '\nFROM ' + this.sobjectType.getDescribe().getName() + + super.doGetWhereClauseString() + + this.getGroupByFieldString(true) + + super.doGetOrderByString() + + super.doGetLimitCountString(); + + return queryString; + } + + public AggregateResult getFirstQueryResult() { + return this.getQueryResults()[0]; + } + + public List getQueryResults() { + return super.doGetQueryResults(this.getQuery()); + } + + private String getGroupByFieldString(Boolean appendGroupByString) { + String prefix = appendGroupByString && !this.groupByList.isEmpty() ? '\nGROUP BY ' : ''; + return prefix + String.join(this.groupByList, ', '); + } + + private String getAggregateFunctionString() { + if(this.groupByList.isEmpty() && this.aggregateFunctionList.isEmpty()) return 'COUNT(Id) COUNT__Id'; + + this.aggregateFunctionList.sort(); + // The extra delimiter adds a comma when needed for grouping by fields & aggregate functions + // Example: 'Type, COUNT_DISTINCT(OwnerId)' + String extraDelimiter = getGroupByFieldString(false) == null ? '' : ',\n'; + return extraDelimiter + String.join(this.aggregateFunctionList, ', '); + } + + private IAggregateResultQueryBuilder buildAggregateFunction(String functionName, IQueryField queryField) { + return this.buildAggregateFunction(functionName, queryField, null); + } + + private IAggregateResultQueryBuilder buildAggregateFunction(String functionName, IQueryField queryField, String fieldAlias) { + if(fieldAlias == null) fieldAlias = functionName + '__' + queryField.getValue(); + // Alias: MIN(Schema.Lead.MyField__c) is auto-aliased to MIN_MyField__c + this.aggregateFunctionList.add(functionName + '(' + queryField.getValue() + ') ' + fieldAlias); + return this; + } + +} \ No newline at end of file diff --git a/src/classes/IQueryBuilder.cls-meta.xml b/src/classes/AggregateResultQueryBuilder.cls-meta.xml similarity index 100% rename from src/classes/IQueryBuilder.cls-meta.xml rename to src/classes/AggregateResultQueryBuilder.cls-meta.xml diff --git a/src/classes/AggregateResultQueryBuilder_Tests.cls b/src/classes/AggregateResultQueryBuilder_Tests.cls new file mode 100644 index 0000000..65eb1de --- /dev/null +++ b/src/classes/AggregateResultQueryBuilder_Tests.cls @@ -0,0 +1,31 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ +@isTest +private class AggregateResultQueryBuilder_Tests { + + @isTest + static void it_should_build_a_ridiculous_query_string() { + String expectedString = 'SELECT Type,\nAVG(Amount) AVG__Amount, COUNT(AccountId) COUNT__AccountId, ' + + 'COUNT_DISTINCT(OwnerId) COUNT_DISTINCT__OwnerId, MAX(CreatedDate) MAX__CreatedDate, MIN(CreatedDate) MIN__CreatedDate' + + '\nFROM Opportunity' + + '\nGROUP BY Type'; + + IAggregateResultQueryBuilder aggregateResultQueryBuilder = new AggregateResultQueryBuilder(Schema.Opportunity.SObjectType) + .max(new QueryField(Schema.Opportunity.CreatedDate)) + .avg(new QueryField(Schema.Opportunity.Amount)) + .countDistinct(new QueryField(Schema.Opportunity.OwnerId)) + .min(new QueryField(Schema.Opportunity.CreatedDate)) + .groupBy(new QueryField(Schema.Opportunity.Type)) + .count(new QueryField(Schema.Opportunity.AccountId)); + String returnedQueryString = aggregateResultQueryBuilder.getQuery(); + + System.assertEquals(expectedString, returnedQueryString); + + // Verify that the query can be executed + Database.query(returnedQueryString); + } + + +} \ No newline at end of file diff --git a/src/classes/QueryBuilder_Tests.cls-meta.xml b/src/classes/AggregateResultQueryBuilder_Tests.cls-meta.xml similarity index 100% rename from src/classes/QueryBuilder_Tests.cls-meta.xml rename to src/classes/AggregateResultQueryBuilder_Tests.cls-meta.xml diff --git a/src/classes/CollectionUtils.cls b/src/classes/CollectionUtils.cls index 3ad823b..213be76 100644 --- a/src/classes/CollectionUtils.cls +++ b/src/classes/CollectionUtils.cls @@ -2,8 +2,71 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Utils +* +* @description A utility class to help with dealing with collections (lists, sets & maps) +* +*/ public without sharing class CollectionUtils { + /** + * @description Returns the last item in a list + * @param listOfItems the list to check + * @return The last Object in the provided list + * @example + * List myList = new List{'A', B', 'C'}; + * String lastItem = CollectionUtils.getLastItem(myList); + * System.assertEquals('C', lastItem); + */ + public static Object getLastItem(List listOfItems) { + Integer indexOfItem = listOfItems.size() - 1; + return listOfItems[indexOfItem]; + } + + /** + * @description Removes the last item in the provided list & returns the item + * @param listOfItems the list to check + * @return The last Object in the provided list + * @example + * List myList = new List{'A', B', 'C'}; + * System.assertEquals(3, myList.size()); + * String lastItem = CollectionUtils.getLastItem(myList); + * System.assertEquals('C', lastItem); + * System.assertEquals(2, myList.size()); + */ + public static Object pop(List listToSplice) { + return splice(listToSplice, listToSplice.size() - 1); + } + + /** + * @description Removes the item in the specified index from the provided list & returns the item + * @param listOfItems The list to check + * @param indexOfItem The index of the item to remove + * @return The Object at the specified index + * @example + * List myList = new List{'A', B', 'C'}; + * System.assertEquals(3, myList.size()); + * String itemToRemove = CollectionUtils.splice(myList, 1); + * System.assertEquals('B', itemToRemove); + * System.assertEquals(2, myList.size()); + */ + public static Object splice(List listToSplice, Integer indexOfItem) { + Object itemToRemove = listToSplice[indexOfItem]; + listToSplice.remove(indexOfItem); + return itemToRemove; + } + + /** + * @description Determines if the provided input is a type of collection (list, set or map) + * @param input The Object to check + * @return true if the item is a type of collection, otherwise returns false + * @example + * List myList = new List{'A', 'B', 'C'}; + * System.assert(CollectionUtils.isCollection(myList)); + */ public static Boolean isCollection(Object input) { return isList(input) || isSet(input) || isMap(input); } diff --git a/src/classes/CollectionUtils_Tests.cls b/src/classes/CollectionUtils_Tests.cls index eac4e59..3e654f8 100644 --- a/src/classes/CollectionUtils_Tests.cls +++ b/src/classes/CollectionUtils_Tests.cls @@ -5,6 +5,37 @@ @isTest private class CollectionUtils_Tests { + @isTest + static void it_should_get_the_last_item_in_a_list() { + List collectionToCheck = new List{'A', 'B', 'C'}; + Integer originalCollectionSize = collectionToCheck.size(); + + System.assertEquals('C', CollectionUtils.getLastItem(collectionToCheck)); + System.assertEquals(originalCollectionSize, collectionToCheck.size()); + } + + @isTest + static void it_should_pop_the_last_item_in_a_list() { + List collectionToCheck = new List{'A', 'B', 'C'}; + Integer originalCollectionSize = collectionToCheck.size(); + + System.assertEquals('C', CollectionUtils.pop(collectionToCheck)); + System.assertEquals(originalCollectionSize - 1, collectionToCheck.size()); + // Verify that the last item has been removed + System.assertEquals(false, new Set(collectionToCheck).contains('C')); + } + + @isTest + static void it_should_splice_the_specified_item_in_a_list() { + List collectionToCheck = new List{'A', 'B', 'C'}; + Integer originalCollectionSize = collectionToCheck.size(); + + System.assertEquals('B', CollectionUtils.splice(collectionToCheck, 1)); + System.assertEquals(originalCollectionSize - 1, collectionToCheck.size()); + // Verify that the specified item has been removed + System.assertEquals(false, new Set(collectionToCheck).contains('B')); + } + // Tests for lists @isTest static void it_should_say_that_a_list_of_strings_is_a_list_and_a_collection() { diff --git a/src/classes/DML.cls b/src/classes/DML.cls index 3f88a83..3825ad1 100644 --- a/src/classes/DML.cls +++ b/src/classes/DML.cls @@ -2,71 +2,74 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ -public abstract class DML extends NebulaCore implements IDML { - - private Schema.SObjectType sobjectType; - public DML(Schema.SObjectType sobjectType) { - this.sobjectType = sobjectType; - } +/** +* +* @group Repository +* +* @description Provides methods with default behavior for DML actions (insert, upsert, etc) +* +*/ +public abstract class DML extends NebulaCore implements IDML { - public virtual void insertRecords(SObject record) { - this.insertRecords(new List{record}); + public virtual List insertRecords(SObject record) { + return this.insertRecords(new List{record}); } - public virtual void insertRecords(List records) { - Database.insert(records); + public virtual List insertRecords(List records) { + return Database.insert(records); } - public virtual void updateRecords(SObject record) { - this.updateRecords(new List{record}); + public virtual List updateRecords(SObject record) { + return this.updateRecords(new List{record}); } - public virtual void updateRecords(List records) { - Database.update(records); + public virtual List updateRecords(List records) { + return Database.update(records); } - public virtual void upsertRecords(SObject record) { - this.upsertRecords(this.castRecords(record)); + public virtual List upsertRecords(SObject record) { + return this.upsertRecords(this.castRecords(record)); } - public virtual void upsertRecords(List records) { - Database.upsert(records); + public virtual List upsertRecords(List records) { + return Database.upsert(records); } - public virtual void undeleteRecords(SObject record) { - this.undeleteRecords(new List{record}); + public virtual List undeleteRecords(SObject record) { + return this.undeleteRecords(new List{record}); } - public virtual void undeleteRecords(List records) { - Database.undelete(records); + public virtual List undeleteRecords(List records) { + return Database.undelete(records); } - public virtual void deleteRecords(SObject record) { - this.deleteRecords(new List{record}); + public virtual List deleteRecords(SObject record) { + return this.deleteRecords(new List{record}); } - public virtual void deleteRecords(List records) { - Database.delete(records); + public virtual List deleteRecords(List records) { + return Database.delete(records); } - public virtual void hardDeleteRecords(SObject record) { - this.hardDeleteRecords(new List{record}); + public virtual List hardDeleteRecords(SObject record) { + return this.hardDeleteRecords(new List{record}); } - public virtual void hardDeleteRecords(List records) { - this.deleteRecords(records); + public virtual List hardDeleteRecords(List records) { + List results = this.deleteRecords(records); if(!records.isEmpty()) Database.emptyRecycleBin(records); + return results; } // Not all objects will have external ID fields, so these methods are protected (instead of public) // Any object that needs an upsert by external ID can expose these methods in their repos - protected virtual void upsertRecords(SObject record, Schema.SObjectField externalIdField) { - this.upsertRecords(this.castRecords(record), externalIdField); + protected virtual List upsertRecords(SObject record, Schema.SObjectField externalIdField) { + return this.upsertRecords(this.castRecords(record), externalIdField); } - protected virtual void upsertRecords(List records, Schema.SObjectField externalIdField) { - Database.upsert(records, externalIdField); + protected virtual List upsertRecords(List records, Schema.SObjectField externalIdField) { + return Database.upsert(records, externalIdField); } private List castRecords(SObject record) { @@ -74,7 +77,7 @@ public abstract class DML extends NebulaCore implements IDML { // This is fine for the bulk method, where we can assume the caller is passing in an explicit list, but for a single record, // the only way to successfully perform the upsert is to dynamically spin up a list of the SObject's type - String listType = 'List<' + this.sobjectType + '>'; + String listType = 'List<' + record.getSObjectType() + '>'; List castRecords = (List)Type.forName(listType).newInstance(); castRecords.add(record); diff --git a/src/classes/DMLMock.cls b/src/classes/DMLMock.cls index 677a376..8c53cbb 100644 --- a/src/classes/DMLMock.cls +++ b/src/classes/DMLMock.cls @@ -2,59 +2,90 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Repository +* +* @description TODO +* +*/ @isTest public class DMLMock { public virtual class Base implements IDML { - public void insertRecords(SObject record) { - this.insertRecords(new List{record}); + // TODO consider changing method name to singular, return type to single Database.SaveResult + public List insertRecords(SObject record) { + return this.insertRecords(new List{record}); } - public void insertRecords(List recordList) { + public List insertRecords(List recordList) { TestingUtils.generateIds(recordList); TestingUtils.insertedRecords.addAll(recordList); + // TODO add logic to try to actually return real results, based on the record list provided + return new List(); } - public void updateRecords(SObject record) { + public List updateRecords(SObject record) { this.updateRecords(new List{record}); + // TODO add logic to try to actually return real results, based on the record list provided + return new List(); } - public void updateRecords(List recordList) { + public List updateRecords(List recordList) { TestingUtils.updatedRecords.addAll(recordList); + // TODO add logic to try to actually return real results, based on the record list provided + return new List(); } - public void upsertRecords(SObject record) { + public List upsertRecords(SObject record) { this.upsertRecords(new List{record}); + // TODO add logic to try to actually return real results, based on the record list provided + return new List(); } - public void upsertRecords(List recordList) { + public List upsertRecords(List recordList) { TestingUtils.generateIds(recordList); TestingUtils.upsertedRecords.addAll(recordList); + // TODO add logic to try to actually return real results, based on the record list provided + return new List(); } - public void undeleteRecords(SObject record) { + public List undeleteRecords(SObject record) { this.undeleteRecords(new List{record}); + // TODO add logic to try to actually return real results, based on the record list provided + return new List(); } - public void undeleteRecords(List recordList) { + public List undeleteRecords(List recordList) { TestingUtils.undeletedRecords.addAll(recordList); + // TODO add logic to try to actually return real results, based on the record list provided + return new List(); } - public void deleteRecords(SObject record) { + public List deleteRecords(SObject record) { this.deleteRecords(new List{record}); + // TODO add logic to try to actually return real results, based on the record list provided + return new List(); } - public void deleteRecords(List recordList) { + public List deleteRecords(List recordList) { if(recordList != null) TestingUtils.deletedRecords.addAll(recordList); + // TODO add logic to try to actually return real results, based on the record list provided + return new List(); } - public void hardDeleteRecords(SObject record) { + public List hardDeleteRecords(SObject record) { this.hardDeleteRecords(new List{record}); + // TODO add logic to try to actually return real results, based on the record list provided + return new List(); } - public void hardDeleteRecords(List recordList) { + public List hardDeleteRecords(List recordList) { this.deleteRecords(recordList); + // TODO add logic to try to actually return real results, based on the record list provided + return new List(); } } diff --git a/src/classes/Environment.cls b/src/classes/Environment.cls index 15efef5..b1f1a74 100644 --- a/src/classes/Environment.cls +++ b/src/classes/Environment.cls @@ -2,9 +2,15 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ -public without sharing class Environment { - private Environment() {} +/** +* +* @group Metadata +* +* @description TODO +* +*/ +public without sharing class Environment { public static String BaseUrl { get {return URL.getSalesforceBaseUrl().toExternalForm();} @@ -16,6 +22,10 @@ public without sharing class Environment { private set; } + /** + * @description Specifies if the org is a production environment or sandbox + * returns true if it's a sandbox + */ public static Boolean IsSandbox { get {return organization.IsSandbox;} private set; @@ -26,6 +36,11 @@ public without sharing class Environment { private set; } + public static String NamespacePrefix { + get {return organization.NamespacePrefix;} + private set; + } + public static String Type { get {return organization.OrganizationType;} private set; @@ -33,7 +48,7 @@ public without sharing class Environment { private static Organization organization { get { - if(organization == null) organization = [SELECT Id, InstanceName, IsSandbox, Name, OrganizationType FROM Organization]; + if(organization == null) organization = [SELECT Id, InstanceName, IsSandbox, Name, NamespacePrefix, OrganizationType FROM Organization]; return organization; } private set; diff --git a/src/classes/Environment_Tests.cls b/src/classes/Environment_Tests.cls index 4eac013..110a833 100644 --- a/src/classes/Environment_Tests.cls +++ b/src/classes/Environment_Tests.cls @@ -28,6 +28,12 @@ private class Environment_Tests { System.assertEquals(org.Name, Environment.Name); } + @isTest + static void it_should_return_namespace_prefix() { + Organization org = [SELECT Id, NamespacePrefix FROM Organization]; + System.assertEquals(org.NamespacePrefix, Environment.NamespacePrefix); + } + @isTest static void it_should_return_type() { Organization org = [SELECT Id, OrganizationType FROM Organization]; diff --git a/src/classes/IAggregateResultQueryBuilder.cls b/src/classes/IAggregateResultQueryBuilder.cls new file mode 100644 index 0000000..8c0c37d --- /dev/null +++ b/src/classes/IAggregateResultQueryBuilder.cls @@ -0,0 +1,57 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description TODO +* +*/ +public interface IAggregateResultQueryBuilder { + + // Group By methods + IAggregateResultQueryBuilder groupBy(IQueryField groupByQueryField); + IAggregateResultQueryBuilder groupBy(List groupByQueryFields); + IAggregateResultQueryBuilder groupBy(Schema.FieldSet fieldSet); + IAggregateResultQueryBuilder groupBy(QueryDate queryDate); + // TODO add support for other features, like 'having count(id) > 1', etc support + // https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_groupby.htm + // TODO need to research 'GROUP BY CUBE' & 'GROUPING' more https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_groupby_cube.htm + + // Aggregate functions + IAggregateResultQueryBuilder avg(IQueryField queryField); + IAggregateResultQueryBuilder avg(IQueryField queryField, String fieldAlias); + IAggregateResultQueryBuilder count(IQueryField queryField); + IAggregateResultQueryBuilder count(IQueryField queryField, String fieldAlias); + IAggregateResultQueryBuilder countDistinct(IQueryField queryField); + IAggregateResultQueryBuilder countDistinct(IQueryField queryField, String fieldAlias); + IAggregateResultQueryBuilder max(IQueryField queryField); + IAggregateResultQueryBuilder max(IQueryField queryField, String fieldAlias); + IAggregateResultQueryBuilder min(IQueryField queryField); + IAggregateResultQueryBuilder min(IQueryField queryField, String fieldAlias); + IAggregateResultQueryBuilder sum(IQueryField queryField); + IAggregateResultQueryBuilder sum(IQueryField queryField, String fieldAlias); + + // Filter methods + IAggregateResultQueryBuilder filterBy(IQueryFilter queryFilter); + IAggregateResultQueryBuilder filterBy(List queryFilters); + + // Order By methods + IAggregateResultQueryBuilder orderBy(IQueryField orderByQueryField); + IAggregateResultQueryBuilder orderBy(IQueryField orderByQueryField, QuerySortOrder sortOrder); + IAggregateResultQueryBuilder orderBy(IQueryField orderByQueryField, QuerySortOrder sortOrder, QueryNullSortOrder nullsSortOrder); + + // Additional query option methods + IAggregateResultQueryBuilder limitCount(Integer limitCount); + + // Query string methods + String getQuery(); + + // Query execution methods + AggregateResult getFirstQueryResult(); + List getQueryResults(); + +} \ No newline at end of file diff --git a/src/classes/SObjectTypeDescriber.cls-meta.xml b/src/classes/IAggregateResultQueryBuilder.cls-meta.xml similarity index 100% rename from src/classes/SObjectTypeDescriber.cls-meta.xml rename to src/classes/IAggregateResultQueryBuilder.cls-meta.xml diff --git a/src/classes/IDML.cls b/src/classes/IDML.cls index 4e53798..91722ee 100644 --- a/src/classes/IDML.cls +++ b/src/classes/IDML.cls @@ -2,19 +2,27 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Repository +* +* @description TODO +* +*/ public interface IDML { - void insertRecords(SObject record); - void insertRecords(List recordList); - void updateRecords(SObject record); - void updateRecords(List recordList); - void upsertRecords(SObject record); - void upsertRecords(List recordList); - void undeleteRecords(SObject record); - void undeleteRecords(List recordList); - void deleteRecords(SObject record); - void deleteRecords(List recordList); - void hardDeleteRecords(SObject record); - void hardDeleteRecords(List recordList); + List insertRecords(SObject record); + List insertRecords(List recordList); + List updateRecords(SObject record); + List updateRecords(List recordList); + List upsertRecords(SObject record); + List upsertRecords(List recordList); + List undeleteRecords(SObject record); + List undeleteRecords(List recordList); + List deleteRecords(SObject record); + List deleteRecords(List recordList); + List hardDeleteRecords(SObject record); + List hardDeleteRecords(List recordList); } \ No newline at end of file diff --git a/src/classes/INebulaCore.cls b/src/classes/INebulaCore.cls index 9be97c9..f3f2fd9 100644 --- a/src/classes/INebulaCore.cls +++ b/src/classes/INebulaCore.cls @@ -2,6 +2,14 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Configuration +* +* @description TODO +* +*/ public interface INebulaCore { String getClassName(); diff --git a/src/classes/IQueryArgumentFormatter.cls b/src/classes/IQueryArgumentFormatter.cls index bf321e2..be402c7 100644 --- a/src/classes/IQueryArgumentFormatter.cls +++ b/src/classes/IQueryArgumentFormatter.cls @@ -2,6 +2,14 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description TODO +* +*/ public interface IQueryArgumentFormatter { String getValue(); diff --git a/src/classes/IQueryBuilder.cls b/src/classes/IQueryBuilder.cls deleted file mode 100644 index 5d5ec29..0000000 --- a/src/classes/IQueryBuilder.cls +++ /dev/null @@ -1,31 +0,0 @@ -/************************************************************************************************* -* This file is part of the Nebula Framework project, released under the MIT License. * -* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * -*************************************************************************************************/ -public interface IQueryBuilder { - - // Query builder methods - IQueryBuilder filterBy(IQueryFilter queryFilter); - IQueryBuilder filterBy(List queryFilters); - - IQueryBuilder includeChildrenRecords(Schema.SObjectField childToParentRelationshipField, ISObjectRepository childSObjectRepository); - IQueryBuilder includeChildrenRecords(Map childFieldToChildSObjectRepositoryrMap); - - IQueryBuilder orderBy(Schema.SObjectField orderByField); - IQueryBuilder orderBy(Schema.SObjectField orderByField, QuerySortOrder sortOrder); - IQueryBuilder orderBy(Schema.SObjectField orderByField, QuerySortOrder sortOrder, QueryNullSortOrder nullsSortOrder); - - IQueryBuilder limitCount(Integer limitCount); - IQueryBuilder setAsUpdate(); - IQueryBuilder usingScope(QueryFilterScope filterScope); - - // Query execution methods - SObject getFirstQueryResult(); - List getQueryResults(); - List getSearchResults(String searchTerm, QuerySearchGroup searchGroup); - - // Get the dyanmic query strings - String getQuery(); - String getQuery(Schema.SObjectField sobjectField); - -} \ No newline at end of file diff --git a/src/classes/IQueryField.cls b/src/classes/IQueryField.cls new file mode 100644 index 0000000..71ed982 --- /dev/null +++ b/src/classes/IQueryField.cls @@ -0,0 +1,17 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description TODO +* +*/ +public interface IQueryField { + + String getValue(); + +} \ No newline at end of file diff --git a/src/classes/SObjectTypeDescriber_Tests.cls-meta.xml b/src/classes/IQueryField.cls-meta.xml similarity index 100% rename from src/classes/SObjectTypeDescriber_Tests.cls-meta.xml rename to src/classes/IQueryField.cls-meta.xml diff --git a/src/classes/IQueryFilter.cls b/src/classes/IQueryFilter.cls index 6fd64e1..f195df3 100644 --- a/src/classes/IQueryFilter.cls +++ b/src/classes/IQueryFilter.cls @@ -2,10 +2,26 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description TODO +* +*/ public interface IQueryFilter { - Object getProvidedValue(); - Schema.SObjectField getSObjectField(); + // Setter methods + IQueryFilter filterByField(QueryField queryField, QueryOperator operator, Object providedValue); + IQueryFilter filterByQueryDate(QueryDate queryDateToFilter, QueryOperator operator, Integer providedValue); + IQueryFilter filterBySubquery(QueryOperator inOrNotIn, Schema.SObjectField lookupFieldOnRelatedSObject); + IQueryFilter filterBySubquery(Schema.SObjectField lookupField, QueryOperator inOrNotIn, Schema.SObjectField lookupFieldOnRelatedSObject); + + IQueryFilter andFilterBy(List queryFilters); + IQueryFilter orFilterBy(List queryFilters); + + // Getter methods String getValue(); } \ No newline at end of file diff --git a/src/classes/ISObjectQueryBuilder.cls b/src/classes/ISObjectQueryBuilder.cls new file mode 100644 index 0000000..f3fbb8b --- /dev/null +++ b/src/classes/ISObjectQueryBuilder.cls @@ -0,0 +1,51 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description TODO +* +*/ +public interface ISObjectQueryBuilder { + + // Field methods + ISObjectQueryBuilder addAllFields(); + ISObjectQueryBuilder addAllStandardFields(); + ISObjectQueryBuilder addAllCustomFields(); + ISObjectQueryBuilder addAllReadableFields(); + ISObjectQueryBuilder addAllEditableFields(); + ISObjectQueryBuilder addFields(List queryFields); + ISObjectQueryBuilder addFields(Schema.FieldSet fieldSet); + + // Parent-to-child relationship query methods + ISObjectQueryBuilder includeChildrenRecords(Schema.SObjectField childToParentRelationshipField, ISObjectQueryBuilder sobjectQueryBuilder); + + // Filter methods + ISObjectQueryBuilder filterBy(IQueryFilter queryFilter); + ISObjectQueryBuilder filterBy(List queryFilters); + + // Order By methods + ISObjectQueryBuilder orderBy(IQueryField orderByQueryField); + ISObjectQueryBuilder orderBy(IQueryField orderByQueryField, QuerySortOrder sortOrder); + ISObjectQueryBuilder orderBy(IQueryField orderByQueryField, QuerySortOrder sortOrder, QueryNullSortOrder nullsSortOrder); + + // Additional query option methods + ISObjectQueryBuilder limitCount(Integer limitCount); + ISObjectQueryBuilder setAsUpdate(); + ISObjectQueryBuilder usingScope(QueryFilterScope filterScope); + + // Query string methods + Database.QueryLocator getQueryLocator(); + String getQuery(); + String getSearchQuery(); + String getChildQuery(Schema.SObjectField childToParentRelationshipField); + + // Query execution methods + SObject getFirstQueryResult(); + List getQueryResults(); + +} \ No newline at end of file diff --git a/src/classes/ISObjectQueryBuilder.cls-meta.xml b/src/classes/ISObjectQueryBuilder.cls-meta.xml new file mode 100644 index 0000000..8b061c8 --- /dev/null +++ b/src/classes/ISObjectQueryBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/src/classes/ISObjectRecordTypes.cls b/src/classes/ISObjectRecordTypes.cls index 87e6c9d..d5c9b16 100644 --- a/src/classes/ISObjectRecordTypes.cls +++ b/src/classes/ISObjectRecordTypes.cls @@ -1,5 +1,21 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ + +/** +* +* @group Record Types +* +* @description TODO +* +*/ public interface ISObjectRecordTypes { + // Setup methods + Schema.SObjectType getSObjectType(); + + // Getter methods Map getAllById(); Map getAllByDeveloperName(); diff --git a/src/classes/ISObjectRepository.cls b/src/classes/ISObjectRepository.cls index 95caf88..972048f 100644 --- a/src/classes/ISObjectRepository.cls +++ b/src/classes/ISObjectRepository.cls @@ -2,8 +2,19 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Repository +* +* @description TODO +* +*/ public interface ISObjectRepository { + // Setup methods + Schema.SObjectType getSObjectType(); + // SOQL SObject getById(Id recordId); List getById(List recordIdList); @@ -17,7 +28,4 @@ public interface ISObjectRepository { List getSearchResults(String searchTerm, QuerySearchGroup searchGroup); List getSearchResults(String searchTerm, QuerySearchGroup searchGroup, List queryFilters); - // Additional methods - IQueryBuilder getQueryBuilder(); - } \ No newline at end of file diff --git a/src/classes/ISObjectTriggerHandler.cls b/src/classes/ISObjectTriggerHandler.cls index b37dd65..8427063 100644 --- a/src/classes/ISObjectTriggerHandler.cls +++ b/src/classes/ISObjectTriggerHandler.cls @@ -2,6 +2,14 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Trigger Handler +* +* @description TODO +* +*/ public interface ISObjectTriggerHandler { void execute(); diff --git a/src/classes/ISearchQueryBuilder.cls b/src/classes/ISearchQueryBuilder.cls new file mode 100644 index 0000000..01fe553 --- /dev/null +++ b/src/classes/ISearchQueryBuilder.cls @@ -0,0 +1,10 @@ +public interface ISearchQueryBuilder { + + ISearchQueryBuilder setQuerySearchGroup(QuerySearchGroup searchGroup); + + String getQuery(); + + List getFirstSearchResult(); + List> getSearchResults(); + +} \ No newline at end of file diff --git a/src/classes/ISearchQueryBuilder.cls-meta.xml b/src/classes/ISearchQueryBuilder.cls-meta.xml new file mode 100644 index 0000000..8b061c8 --- /dev/null +++ b/src/classes/ISearchQueryBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/src/classes/Logger.cls b/src/classes/Logger.cls index f09d149..dadd289 100644 --- a/src/classes/Logger.cls +++ b/src/classes/Logger.cls @@ -2,9 +2,15 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ -public without sharing class Logger { - private Logger() {} +/** +* +* @group Logging +* +* @description TODO +* +*/ +public without sharing class Logger { private static Id logId; private static Attachment logAttachment; @@ -25,7 +31,7 @@ public without sharing class Logger { } public static void saveLogs() { - if(!NebulaSettings.loggerSettings.EnableLogging__c) return; + if(!NebulaSettings.LoggerSettings.EnableLogging__c) return; saveTransactionLog(); saveSingleLogFile(); diff --git a/src/classes/NebulaCore.cls b/src/classes/NebulaCore.cls index edb9099..9865e66 100644 --- a/src/classes/NebulaCore.cls +++ b/src/classes/NebulaCore.cls @@ -2,9 +2,17 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Configuration +* +* @description TODO +* +*/ public abstract class NebulaCore implements INebulaCore { - public enum Module { RECORD_TYPES, REPOSITORY, SETTINGS, TRIGGER_HANDLER } + public enum Module { QUERY_BUILDER, RECORD_TYPES, REPOSITORY, SETTINGS, TRIGGER_HANDLER } public static final String TRANSACTION_ID; public static String INITIAL_CLASS {get; private set;} diff --git a/src/classes/NebulaSettings.cls b/src/classes/NebulaSettings.cls index 6fc734a..507919b 100644 --- a/src/classes/NebulaSettings.cls +++ b/src/classes/NebulaSettings.cls @@ -2,11 +2,23 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Configuration +* +* @description TODO +* +*/ public without sharing class NebulaSettings { + /** + * @description TODO + * returns true if it's a sandbox + */ public static NebulaLoggerSettings__c LoggerSettings {get; private set;} public static NebulaRecordTypesSettings__c RecordTypesSettings {get; private set;} - public static NebulaRepositorySettings__c RepositorySettings {get; private set;} + public static NebulaSObjectQueryBuilderSettings__c SObjectQueryBuilderSettings {get; private set;} public static NebulaTriggerHandlerSettings__c TriggerHandlerSettings {get; private set;} static { @@ -21,21 +33,21 @@ public without sharing class NebulaSettings { private static void loadCustomSettings() { loadLoggerSettings(); loadRecordTypesSettings(); - loadRepositorySettings(); + loadSObjectQueryBuilderSettings(); loadTriggerHandlerSettings(); } private static void deleteExistingCustomSettings() { delete [SELECT Id FROM NebulaLoggerSettings__c]; delete [SELECT Id FROM NebulaRecordTypesSettings__c]; - delete [SELECT Id FROM NebulaRepositorySettings__c]; + delete [SELECT Id FROM NebulaSObjectQueryBuilderSettings__c]; delete [SELECT Id FROM NebulaTriggerHandlerSettings__c]; } private static void createCustomSettings() { upsert NebulaLoggerSettings__c.getOrgDefaults(); upsert NebulaRecordTypesSettings__c.getOrgDefaults(); - upsert NebulaRepositorySettings__c.getOrgDefaults(); + upsert NebulaSObjectQueryBuilderSettings__c.getOrgDefaults(); upsert NebulaTriggerHandlerSettings__c.getOrgDefaults(); } @@ -57,12 +69,12 @@ public without sharing class NebulaSettings { } } - private static void loadRepositorySettings() { - repositorySettings = NebulaRepositorySettings__c.getInstance(); + private static void loadSObjectQueryBuilderSettings() { + sobjectQueryBuilderSettings = NebulaSObjectQueryBuilderSettings__c.getInstance(); - if(repositorySettings.Id == null) { - upsert NebulaRepositorySettings__c.getOrgDefaults(); - repositorySettings = NebulaRepositorySettings__c.getInstance(); + if(sobjectQueryBuilderSettings.Id == null) { + upsert NebulaSObjectQueryBuilderSettings__c.getOrgDefaults(); + sobjectQueryBuilderSettings = NebulaSObjectQueryBuilderSettings__c.getInstance(); } } diff --git a/src/classes/NebulaSettings_Tests.cls b/src/classes/NebulaSettings_Tests.cls index 7fb6181..57b8682 100644 --- a/src/classes/NebulaSettings_Tests.cls +++ b/src/classes/NebulaSettings_Tests.cls @@ -26,12 +26,12 @@ private class NebulaSettings_Tests { } @isTest - static void it_should_return_repository_settings() { - List existingSettings = [SELECT Id FROM NebulaRepositorySettings__c]; + static void it_should_return_SObjectQueryBuilder_settings() { + List existingSettings = [SELECT Id FROM NebulaSObjectQueryBuilderSettings__c]; System.assert(existingSettings.isEmpty()); Test.startTest(); - System.assertNotEquals(null, NebulaSettings.RepositorySettings); + System.assertNotEquals(null, NebulaSettings.SObjectQueryBuilderSettings); Test.stopTest(); } @@ -55,9 +55,9 @@ private class NebulaSettings_Tests { upsert nebulaRecordTypesSettings; Id originalRecordTypesSettingsId = NebulaRecordTypesSettings__c.getInstance().Id; - NebulaRepositorySettings__c nebulaRepositorySettings = NebulaRepositorySettings__c.getInstance(); - upsert nebulaRepositorySettings; - Id originalRepositorySettingsId = NebulaRepositorySettings__c.getInstance().Id; + NebulaSObjectQueryBuilderSettings__c nebulaSObjectQueryBuilderSettings = NebulaSObjectQueryBuilderSettings__c.getInstance(); + upsert nebulaSObjectQueryBuilderSettings; + Id originalSObjectQueryBuilderSettingsId = NebulaSObjectQueryBuilderSettings__c.getInstance().Id; NebulaTriggerHandlerSettings__c nebulaTriggerHandlerSettings = NebulaTriggerHandlerSettings__c.getInstance(); upsert nebulaTriggerHandlerSettings; @@ -69,7 +69,7 @@ private class NebulaSettings_Tests { System.assertNotEquals(originalLoggerSettingsId, NebulaLoggerSettings__c.getInstance().Id); System.assertNotEquals(originalRecordTypesSettingsId, NebulaRecordTypesSettings__c.getInstance().Id); - System.assertNotEquals(originalRepositorySettingsId, NebulaRepositorySettings__c.getInstance().Id); + System.assertNotEquals(originalSObjectQueryBuilderSettingsId, NebulaSObjectQueryBuilderSettings__c.getInstance().Id); System.assertNotEquals(originalTriggerHandlerSettingsId, NebulaTriggerHandlerSettings__c.getInstance().Id); } diff --git a/src/classes/QueryArgumentFormatter.cls b/src/classes/QueryArgumentFormatter.cls index 5015e26..ea4dec5 100644 --- a/src/classes/QueryArgumentFormatter.cls +++ b/src/classes/QueryArgumentFormatter.cls @@ -2,18 +2,25 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ -public virtual class QueryArgumentFormatter extends NebulaCore implements IQueryArgumentFormatter { - private Object argumentValue; +/** +* +* @group Query Builder +* +* @description TODO +* +*/ +public virtual class QueryArgumentFormatter extends NebulaCore implements IQueryArgumentFormatter { - public QueryArgumentFormatter(Object argumentValue) { - this.currentModule = NebulaCore.Module.REPOSITORY; + private String value; - this.argumentValue = argumentValue; + public QueryArgumentFormatter(Object valueToFormat) { + this.currentModule = NebulaCore.Module.QUERY_BUILDER; + this.value = this.objectToQueryString(valueToFormat); } public virtual String getValue() { - return this.objectToQueryString(this.argumentValue); + return this.value; } protected virtual String objectToQueryString(Object valueToFormat) { @@ -43,7 +50,11 @@ public virtual class QueryArgumentFormatter extends NebulaCore implements IQuery Schema.SObjectType sobjectType = (Schema.SObjectType)valueToFormat; return wrapInSingleQuotes(sobjectType.getDescribe().getName()); } - else if(valueToFormat instanceof String) return wrapInSingleQuotes((String)valueToFormat); + else if(valueToFormat instanceof String) { + // Escape single quotes to prevent SOQL/SOSL injection + String stringArgument = String.escapeSingleQuotes((String)valueToFormat); + return wrapInSingleQuotes(stringArgument); + } else return String.valueOf(valueToFormat); } diff --git a/src/classes/QueryArgumentFormatter_Tests.cls b/src/classes/QueryArgumentFormatter_Tests.cls index 7471c60..aafd1fa 100644 --- a/src/classes/QueryArgumentFormatter_Tests.cls +++ b/src/classes/QueryArgumentFormatter_Tests.cls @@ -191,6 +191,18 @@ private class QueryArgumentFormatter_Tests { System.assertEquals(expectedString, returnedValue); } + @isTest + static void it_should_return_query_string_for_string_containing_single_quotes() { + String providedValue = 'Jon\'s test'; + String expectedString = '\'' + String.escapeSingleQuotes(providedValue) + '\''; + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + @isTest static void it_should_return_query_string_for_unsupported_type() { UUID providedValue = new UUID(); diff --git a/src/classes/QueryBuilder.cls b/src/classes/QueryBuilder.cls index 335c7b2..0be9d61 100644 --- a/src/classes/QueryBuilder.cls +++ b/src/classes/QueryBuilder.cls @@ -2,232 +2,114 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ -public class QueryBuilder extends NebulaCore implements IQueryBuilder { - - private String query; - private Set queryFields; - private List childRelationshipQueries; - private List whereClauseList; - @testVisible private List orderByList; - private QueryFilterScope filterScope; - @testVisible private Boolean forUpdate; - private Integer limitCount; - - private final Schema.FieldSet fieldSet; - private final List sobjectFieldList; - private final SObjectType sobjectType; - private final Map sobjectTypeFieldMap; - - public QueryBuilder(Schema.SObjectType sobjectType) { - this(sobjectType, null, null); - } - public QueryBuilder(Schema.SObjectType sobjectType, Schema.FieldSet fieldSet, List sobjectFieldList) { - this.currentModule = NebulaCore.Module.REPOSITORY; +/** +* +* @group Query Builder +* +* @description Abstract class that provides some shared properties & methods for SObjectQueryBuilder & AggregateResultQueryBuilder +* +*/ +public abstract class QueryBuilder extends NebulaCore { + + protected List whereClauseList; + protected List orderByList; + protected Integer limitCount; + + protected SObjectType sobjectType; + protected Map sobjectTypeFieldMap; - this.sobjectType = sobjectType; - this.sobjectFieldList = sobjectFieldList; - this.fieldSet = fieldSet; - this.sobjectTypeFieldMap = this.sobjectType.getDescribe().fields.getMap(); + public QueryBuilder() { + this.currentModule = NebulaCore.Module.QUERY_BUILDER; - this.queryFields = new Set(); - this.childRelationshipQueries = new List(); this.whereClauseList = new List(); this.orderByList = new List(); - this.forUpdate = false; - - this.addCommonQueryFields(); - this.addFieldSetMembers(); - this.addSObjectFields(); - this.addSObjectTypeFields(); } - public IQueryBuilder filterBy(IQueryFilter queryFilter) { - return this.filterBy(new List{queryFilter}); + protected void doFilterBy(IQueryFilter queryFilter) { + this.doFilterBy(new List{queryFilter}); } - public IQueryBuilder filterBy(List queryFilters) { - if(queryFilters == null) return this; + protected void doFilterBy(List queryFilters) { + if(queryFilters == null) return; for(IQueryFilter queryFilter : queryFilters) this.whereClauseList.add(queryFilter.getValue()); - return this; - } - - public IQueryBuilder includeChildrenRecords(Schema.SObjectField childToParentRelationshipField, ISObjectRepository childSObjectRepository) { - return this.includeChildrenRecords(new Map{childToParentRelationshipField => childSObjectRepository}); } - public IQueryBuilder includeChildrenRecords(Map childFieldToChildSObjectRepositoryMap) { - for(Schema.SObjectField sobjectField : childFieldToChildSObjectRepositoryMap.keySet()) { - IQueryBuilder childQueryBuilder = childFieldToChildSObjectRepositoryMap.get(sobjectField).getQueryBuilder(); - - this.childRelationshipQueries.add(childQueryBuilder.getQuery(sobjectField)); - } - return this; - } - - public IQueryBuilder orderBy(Schema.SObjectField orderByField) { - return this.orderBy(orderByField, null, null); + protected void doOrderBy(IQueryField orderByQueryField) { + this.doOrderBy(orderByQueryField, null, null); } - public IQueryBuilder orderBy(Schema.SObjectField orderByField, QuerySortOrder sortOrder) { - return this.orderBy(orderByField, sortOrder, null); + protected void doOrderBy(IQueryField orderByQueryField, QuerySortOrder sortOrder) { + this.doOrderBy(orderByQueryField, sortOrder, null); } - public IQueryBuilder orderBy(Schema.SObjectField orderByField, QuerySortOrder sortOrder, QueryNullSortOrder nullsSortOrder) { - String sortOrderSoql = ''; - if(sortOrder == QuerySortOrder.ASCENDING) sortOrderSoql = ' ASC'; - else if(sortOrder == QuerySortOrder.DESCENDING) sortOrderSoql = ' DESC'; + protected void doOrderBy(IQueryField orderByQueryField, QuerySortOrder sortOrder, QueryNullSortOrder nullsSortOrder) { + String sortOrderString = ''; + if(sortOrder == QuerySortOrder.ASCENDING) sortOrderString = ' ASC'; + else if(sortOrder == QuerySortOrder.DESCENDING) sortOrderString = ' DESC'; - if(nullsSortOrder != null) sortOrderSoql += ' NULLS ' + nullsSortOrder; + if(nullsSortOrder != null) sortOrderString += ' NULLS ' + nullsSortOrder; - this.orderByList.add(orderByField.getDescribe().getName() + sortOrderSoql); - - return this; + this.orderByList.add(orderByQueryField.getValue() + sortOrderString); } - public IQueryBuilder limitCount(Integer limitCount) { + protected void doLimitCount(Integer limitCount) { this.limitCount = limitCount; - return this; } - public IQueryBuilder setAsUpdate() { - this.forUpdate = true; - return this; - } + protected String doGetWhereClauseString() { + if(this.whereClauseList.isEmpty()) return ''; - public IQueryBuilder usingScope(QueryFilterScope filterScope) { - this.filterScope = filterScope; - return this; - } + // Dedupe + this.whereClauseList = new List(new Set(this.whereClauseList)); - // Query execution methods - public SObject getFirstQueryResult() { - return this.getQueryResults()[0]; + this.whereClauseList.sort(); + return '\nWHERE ' + String.join(this.whereClauseList, '\nAND '); } - public List getQueryResults() { - List results = Database.query(this.getQuery()); + protected String doGetOrderByString() { + if(this.orderByList.isEmpty()) return ''; - this.logEntry(results); + // Dedupe + this.orderByList = new List(new Set(this.orderByList)); - return results; - } - - public virtual List getSearchResults(String searchTerm, QuerySearchGroup searchGroup) { - List results = Search.query(this.getSearchQuery(searchTerm, searchGroup))[0]; - - this.logEntry(results); - - return results; + this.orderByList.sort(); + return '\nORDER BY ' + String.join(new List(orderByList), ', '); } - // Query string methods - public String getQuery() { - return this.getQuery(String.valueOf(this.sobjectType)); - } - - public String getQuery(Schema.SObjectField childRelationshipSObjectField) { - Schema.SObjectType parentSObjectType = new SObjectFieldDescriber(childRelationshipSObjectField).getParentSObjectType(); - String childRelationshipName = new SObjectTypeDescriber(parentSObjectType).getChildRelationshipName(childRelationshipSObjectField); - return this.getQuery(childRelationshipName); - } - - private String getQuery(String sobjectQueryName) { - this.query = 'SELECT ' + this.getQueryFieldString(); - - // Only 1 level of child relationships is allowed, so don't include them if the SObject name isn't the current SObject Type - if(sobjectQueryName == String.valueOf(this.sobjectType)) this.query += this.getChildRelationshipsQueryString(); - - this.query +='\nFROM ' + sobjectQueryName - + this.getWhereClauseString() - + this.getOrderByString() - + this.getLimitCountString() - + this.getForUpdateString(); - - return this.query; - } - - private String getSearchQuery(String searchTerm, QuerySearchGroup searchGroup) { - this.query = 'FIND ' + new QueryArgumentFormatter(searchTerm.toLowerCase()).getValue() - + '\nIN ' + searchGroup.name().replace('_', ' ') - + '\nRETURNING ' + this.sobjectType + '(' - + this.getQueryFieldString() - + this.getWhereClauseString() - + this.getOrderByString() - + this.getLimitCountString() - + ')'; - - if(this.forUpdate) Logger.addEntry(this, 'SOSL Search Query method flagged as FOR UPDATE. SOSL cannot use FOR UPDATE, ignoring'); - if(this.filterScope != null) Logger.addEntry(this, 'SOSL Search Query method flagged as USING SCOPE ' + this.filterScope + '. SOSL cannot use USING SCOPE, ignoring'); - - return this.query; - } - - private String getQueryFieldString() { - List queryFieldList = new List(this.queryFields); - queryFieldList.sort(); - return String.join(queryFieldList, ', '); - } - - private String getChildRelationshipsQueryString() { - if(this.childRelationshipQueries.isEmpty()) return ''; - - return ',\n(' + String.join(childRelationshipQueries, '), (') + ')'; - } - - private String getWhereClauseString() { - return !this.whereClauseList.isEmpty() ? '\nWHERE ' + String.join(this.whereClauseList, '\nAND ') : ''; + protected String doGetLimitCountString() { + return this.limitCount != null ? '\nLIMIT '+ this.limitCount : ''; } - private String getOrderByString() { - return !this.orderByList.isEmpty() ? '\nORDER BY ' + String.join(new List(orderByList), ', ') : ''; - } + protected List doGetQueryResults(String query) { + List results = Database.query(query); - private String getLimitCountString() { - return this.limitCount != null ? '\nLIMIT '+ this.limitCount : ''; - } + this.logResults(query, results); - private String getForUpdateString() { - return this.orderByList.isEmpty() && this.forUpdate ? '\nFOR UPDATE' : ''; + return results; } - private void addCommonQueryFields() { - if(!NebulaSettings.RepositorySettings.IncludeCommonFields__c) return; - // Auto-add the common fields that are available for the SObject Type - List commonFieldNameList = new List{ - 'Id', 'CaseNumber', 'CreatedById', 'CreatedDate', 'IsClosed', 'LastModifiedById', 'LastModifiedDate', - 'Name', 'OwnerId', 'ParentId', 'Subject', 'RecordTypeId', 'SystemModStamp' - }; - for(String commonFieldName : commonFieldNameList) { - if(!this.sobjectTypeFieldMap.containsKey(commonFieldName)) continue; - - this.queryFields.add(commonFieldName.toLowerCase()); - } - } + protected List> doGetSearchResults(String query) { + List> results = Search.query(query); - private void addFieldSetMembers() { - if(this.fieldSet == null) return; + this.logResults(query, results); - for(Schema.FieldSetMember field : this.fieldSet.getFields()) this.queryFields.add(field.getFieldPath().toLowerCase()); + return results; } - private void addSObjectFields() { - if(this.sobjectFieldList == null) return; - - for(Schema.SObjectField field : this.sobjectFieldList) this.queryFields.add(field.getDescribe().getName().toLowerCase()); - } + private void filterByWithSeparator(List queryFilters, String separator) { + if(queryFilters == null) return; - private void addSObjectTypeFields() { - if(this.fieldSet != null || this.sobjectFieldList != null) return; + List queryFilterValues = new List(); + for(IQueryFilter queryFilter : queryFilters) queryFilterValues.add(queryFilter.getValue()); - // If no field set or list of fields is provided, we enter 'lazy mode' and all fields for the SObject Type are added - // This is effectively 'select * from '. Performance issues can occur with large objects, so use carefully. - for(Schema.SObjectField field : this.sobjectTypeFieldMap.values()) this.queryFields.add(field.getDescribe().getName().toLowerCase()); + String orStatement = '(' + String.join(queryFilterValues, ' ' + separator + ' ') + ')'; + this.whereClauseList.add(orStatement); } - private void logEntry(List results) { - String logEntry = 'Query:\n' + this.query + '\n\nResults:\n' + JSON.serialize(results); + private void logResults(String query, List results) { + String logEntry = 'Query:\n' + query + '\n\nResults:\n' + JSON.serializePretty(results); Logger.addEntry(this, logEntry); } diff --git a/src/classes/QueryBuilder_Tests.cls b/src/classes/QueryBuilder_Tests.cls deleted file mode 100644 index d6d50b6..0000000 --- a/src/classes/QueryBuilder_Tests.cls +++ /dev/null @@ -1,47 +0,0 @@ -/************************************************************************************************* -* This file is part of the Nebula Framework project, released under the MIT License. * -* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * -*************************************************************************************************/ -@isTest -public class QueryBuilder_Tests { - - @isTest - static void it_should_call_sort_order_properly_ascending() { - Schema.SObjectType objType = Contact.SObjectType; - List fields = new List{Contact.CreatedDate}; - - Test.startTest(); - QueryBuilder query = new QueryBuilder(objType, null, fields); - query.orderBy(Contact.CreatedDate,QuerySortOrder.ASCENDING); - Test.stopTest(); - - System.assert(query.orderByList[0].contains(Contact.CreatedDate.getDescribe().getName())); - } - - @isTest - static void it_should_call_sort_order_properly_descending() { - Schema.SObjectType objType = Contact.SObjectType; - List fields = new List{Contact.CreatedDate}; - - Test.startTest(); - QueryBuilder query = new QueryBuilder(objType, null, fields); - query.orderBy(Contact.CreatedDate,QuerySortOrder.DESCENDING); - Test.stopTest(); - - System.assert(query.orderByList[0].contains(Contact.CreatedDate.getDescribe().getName())); - } - - @isTest - static void it_should_set_for_update() { - Schema.SObjectType objType = Contact.SObjectType; - List fields = new List{Contact.CreatedDate}; - - Test.startTest(); - QueryBuilder query = new QueryBuilder(objType, null, fields); - query.setAsUpdate(); - Test.stopTest(); - - System.assert(query.forUpdate); - } - -} \ No newline at end of file diff --git a/src/classes/QueryDate.cls b/src/classes/QueryDate.cls new file mode 100644 index 0000000..6e0fd5e --- /dev/null +++ b/src/classes/QueryDate.cls @@ -0,0 +1,97 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description Used to dynamically generate SOQL & SOSQL date functions. +* "Date functions in SOQL queries allow you to group or filter data by date periods such as day, calendar month, or fiscal year." +* Salesforce docs: developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_date_functions.htm +* +*/ +public without sharing class QueryDate extends NebulaCore { + + private Schema.SObjectField sobjectField; + private Schema.SObjectType sobjectType; + private String value; + + private QueryDate() { + this.currentModule = NebulaCore.Module.QUERY_BUILDER; + } + + public String getValue() { + return this.value; + } + + public Schema.SObjectType getSObjectType() { + return this.sobjectType; + } + + private QueryDate setValue(Schema.SObjectField sobjectField, String value) { + this.sobjectField = sobjectField; + this.sobjectType = new SObjectFieldDescriber(sobjectField).getSObjectType(); + this.value = value; + return this; + } + + public static QueryDate CALENDAR_MONTH(Schema.SObjectField sobjectField) { + return buildQueryDate('CALENDAR_MONTH', sobjectField); + } + + public static QueryDate CALENDAR_QUARTER(Schema.SObjectField sobjectField) { + return buildQueryDate('CALENDAR_QUARTER', sobjectField); + } + + public static QueryDate CALENDAR_YEAR(Schema.SObjectField sobjectField) { + return buildQueryDate('CALENDAR_YEAR', sobjectField); + } + + public static QueryDate DAY_IN_MONTH(Schema.SObjectField sobjectField) { + return buildQueryDate('DAY_IN_MONTH', sobjectField); + } + + public static QueryDate DAY_IN_WEEK(Schema.SObjectField sobjectField) { + return buildQueryDate('DAY_IN_WEEK', sobjectField); + } + + public static QueryDate DAY_IN_YEAR(Schema.SObjectField sobjectField) { + return buildQueryDate('DAY_IN_YEAR', sobjectField); + } + + public static QueryDate DAY_ONLY(Schema.SObjectField sobjectField) { + return buildQueryDate('DAY_ONLY', sobjectField); + } + + public static QueryDate FISCAL_MONTH(Schema.SObjectField sobjectField) { + return buildQueryDate('FISCAL_MONTH', sobjectField); + } + + public static QueryDate FISCAL_QUARTER(Schema.SObjectField sobjectField) { + return buildQueryDate('FISCAL_QUARTER', sobjectField); + } + + public static QueryDate FISCAL_YEAR(Schema.SObjectField sobjectField) { + return buildQueryDate('FISCAL_YEAR', sobjectField); + } + + public static QueryDate HOUR_IN_DAY(Schema.SObjectField sobjectField) { + return buildQueryDate('HOUR_IN_DAY', sobjectField); + } + + public static QueryDate WEEK_IN_MONTH(Schema.SObjectField sobjectField) { + return buildQueryDate('WEEK_IN_MONTH', sobjectField); + } + + public static QueryDate WEEK_IN_YEAR(Schema.SObjectField sobjectField) { + return buildQueryDate('WEEK_IN_YEAR', sobjectField); + } + + private static QueryDate buildQueryDate(String dateOperation, Schema.SObjectField sobjectField) { + String value = dateOperation + '(' + sobjectField.getDescribe().getName() + ')'; + return new QueryDate().setValue(sobjectField, value); + } + +} \ No newline at end of file diff --git a/src/classes/QueryDate.cls-meta.xml b/src/classes/QueryDate.cls-meta.xml new file mode 100644 index 0000000..8b061c8 --- /dev/null +++ b/src/classes/QueryDate.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/src/classes/QueryDateLiteral.cls b/src/classes/QueryDateLiteral.cls index ea0ccc3..da54e1b 100644 --- a/src/classes/QueryDateLiteral.cls +++ b/src/classes/QueryDateLiteral.cls @@ -2,10 +2,22 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ -public without sharing class QueryDateLiteral { + +/** +* +* @group Query Builder +* +* @description TODO +* +*/ +public without sharing class QueryDateLiteral extends NebulaCore { private String value; + private QueryDateLiteral() { + this.currentModule = NebulaCore.Module.QUERY_BUILDER; + } + public String getValue() { return this.value; } diff --git a/src/classes/QueryDateLiteral_Tests.cls b/src/classes/QueryDateLiteral_Tests.cls index a98420a..e06ea6c 100644 --- a/src/classes/QueryDateLiteral_Tests.cls +++ b/src/classes/QueryDateLiteral_Tests.cls @@ -3,7 +3,7 @@ * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ @isTest -public class QueryDateLiteral_Tests { +private class QueryDateLiteral_Tests { private static Integer offsetNumber = 5; diff --git a/src/classes/QueryDate_Tests.cls b/src/classes/QueryDate_Tests.cls new file mode 100644 index 0000000..d6c4fbf --- /dev/null +++ b/src/classes/QueryDate_Tests.cls @@ -0,0 +1,92 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ +@isTest +private class QueryDate_Tests { + + @isTest + static void it_should_return_sobject_type() { + QueryDate dt = QueryDate.CALENDAR_MONTH(Schema.User.CreatedDate); + System.assertEquals(Schema.User.SObjectType, dt.getSObjectType()); + } + + @isTest + static void it_should_return_calendar_month_string() { + QueryDate dt = QueryDate.CALENDAR_MONTH(Schema.User.CreatedDate); + System.assertEquals('CALENDAR_MONTH(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_calendar_quarter_string() { + QueryDate dt = QueryDate.CALENDAR_QUARTER(Schema.User.CreatedDate); + System.assertEquals('CALENDAR_QUARTER(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_calendar_year_string() { + QueryDate dt = QueryDate.CALENDAR_YEAR(Schema.User.CreatedDate); + System.assertEquals('CALENDAR_YEAR(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_day_in_month_string() { + QueryDate dt = QueryDate.DAY_IN_MONTH(Schema.User.CreatedDate); + System.assertEquals('DAY_IN_MONTH(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_day_in_week_string() { + QueryDate dt = QueryDate.DAY_IN_WEEK(Schema.User.CreatedDate); + System.assertEquals('DAY_IN_WEEK(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_day_in_year_string() { + QueryDate dt = QueryDate.DAY_IN_YEAR(Schema.User.CreatedDate); + System.assertEquals('DAY_IN_YEAR(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_day_only_string() { + QueryDate dt = QueryDate.DAY_ONLY(Schema.User.CreatedDate); + System.assertEquals('DAY_ONLY(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_fiscal_month_string() { + QueryDate dt = QueryDate.FISCAL_MONTH(Schema.User.CreatedDate); + System.assertEquals('FISCAL_MONTH(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_fiscal_quarter_string() { + QueryDate dt = QueryDate.FISCAL_QUARTER(Schema.User.CreatedDate); + System.assertEquals('FISCAL_QUARTER(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_fiscal_year_string() { + QueryDate dt = QueryDate.FISCAL_YEAR(Schema.User.CreatedDate); + System.assertEquals('FISCAL_YEAR(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_hour_in_day_string() { + QueryDate dt = QueryDate.HOUR_IN_DAY(Schema.User.CreatedDate); + System.assertEquals('HOUR_IN_DAY(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_week_in_month_string() { + QueryDate dt = QueryDate.WEEK_IN_MONTH(Schema.User.CreatedDate); + System.assertEquals('WEEK_IN_MONTH(CreatedDate)', dt.getValue()); + } + + @isTest + static void it_should_return_week_in_year_string() { + QueryDate dt = QueryDate.WEEK_IN_YEAR(Schema.User.CreatedDate); + System.assertEquals('WEEK_IN_YEAR(CreatedDate)', dt.getValue()); + } + +} \ No newline at end of file diff --git a/src/classes/QueryDate_Tests.cls-meta.xml b/src/classes/QueryDate_Tests.cls-meta.xml new file mode 100644 index 0000000..8b061c8 --- /dev/null +++ b/src/classes/QueryDate_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/src/classes/QueryField.cls b/src/classes/QueryField.cls new file mode 100644 index 0000000..8b3e6cc --- /dev/null +++ b/src/classes/QueryField.cls @@ -0,0 +1,55 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description Used to dynamically generate field string for SObject fields, including parent fields. +* +*/ +public without sharing class QueryField extends NebulaCore implements IQueryField, Comparable { + + private final List fields; + private final String value; + + public QueryField(SObjectField field) { + this(new List{field}); + } + + public QueryField(List fields) { + this.currentModule = NebulaCore.Module.QUERY_BUILDER; + + this.fields = fields; + this.value = this.parseFields(); + } + + public String getValue() { + return this.value; + } + + private String parseFields() { + if(this.fields.size() == 1) return String.valueOf(this.fields[0]); + + //Remove the last field from the list to iterate through so only the parent relationships are hopped + List fieldsToIterate = this.fields.clone(); + SObjectField lastField = (SObjectField) CollectionUtils.pop(fieldsToIterate); + List fieldChain = new List(); + for(SObjectField parentField : fieldsToIterate) { + fieldChain.add(parentField.getDescribe().getRelationshipName()); + } + // Return the fully qualified field name + return String.join(fieldChain, '.') + '.' + lastField.getDescribe().getName(); + } + + + public Integer compareTo(Object compareTo) { + QueryField compareToQueryField = (QueryField)compareTo; + if(this.getValue() == compareToQueryField.getValue()) return 0; + if(this.getValue() > compareToQueryField.getValue()) return 1; + return -1; + } + +} \ No newline at end of file diff --git a/src/classes/QueryField.cls-meta.xml b/src/classes/QueryField.cls-meta.xml new file mode 100644 index 0000000..8b061c8 --- /dev/null +++ b/src/classes/QueryField.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/src/classes/QueryField_Tests.cls b/src/classes/QueryField_Tests.cls new file mode 100644 index 0000000..a5cbe5a --- /dev/null +++ b/src/classes/QueryField_Tests.cls @@ -0,0 +1,29 @@ +@isTest +private class QueryField_Tests { + + @isTest + static void it_should_return_string_for_sobject_field_name() { + System.assertEquals('CreatedDate', new QueryField(Schema.Lead.CreatedDate).getValue()); + } + + @isTest + static void it_should_return_string_for_parent_sobject_field_name() { + List fieldChain = new List{ + Schema.Contact.AccountId, Schema.Account.CreatedById, Schema.User.Name + }; + System.assertEquals('Account.CreatedBy.Name', new QueryField(fieldChain).getValue()); + } + + @isTest + static void it_should_be_callable_multiple_times_without_pop_removing_field_references() { + List fieldChain = new List{ + Schema.Contact.AccountId, Schema.Account.Name + }; + QueryField queryField = new QueryField(fieldChain); + String expected = 'Account.Name'; + for(Integer i = 0; i < 5; i++) { + System.assertEquals(expected, queryField.getValue()); + } + } + +} \ No newline at end of file diff --git a/src/classes/QueryField_Tests.cls-meta.xml b/src/classes/QueryField_Tests.cls-meta.xml new file mode 100644 index 0000000..8b061c8 --- /dev/null +++ b/src/classes/QueryField_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/src/classes/QueryFilter.cls b/src/classes/QueryFilter.cls index d4f4393..6a8c913 100644 --- a/src/classes/QueryFilter.cls +++ b/src/classes/QueryFilter.cls @@ -2,53 +2,137 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ -public class QueryFilter implements IQueryFilter { - private List parentRelationshipFields; - private Schema.SObjectField sobjectField; - private Schema.SObjectType sobjectType; - private QueryOperator operator; - private Object providedValue; - @testVisible private String queryFieldName; +/** +* +* @group Query Builder +* +* @description Handles generating any query conditions (the WHERE statement in a query) +* Each part of a WHERE statement is a separate instance of query filter. +* +*/ +public class QueryFilter extends NebulaCore implements IQueryFilter, Comparable { - public QueryFilter(Schema.SObjectField fieldToFilter, QueryOperator operator, Object providedValue) { - this(new List(), fieldToFilter, operator, providedValue); + private String value; + + public QueryFilter() { + this.currentModule = NebulaCore.Module.QUERY_BUILDER; } - public QueryFilter(Schema.SObjectField parentRelationshipField, Schema.SObjectField fieldToFilter, QueryOperator operator, Object providedValue) { - this(new List{parentRelationshipField}, fieldToFilter, operator, providedValue); + /** + * @description Creates a filter for a field on a parent sobject + * @param queryField An instance of QueryField, containg the field or chain of fields that should be filtered + * @param operator The instance of QueryOperator to use in the filter the list to check + * @param providedValue The value to compare to in the filter + * @return The instance of IQueryFilter, to allow chaining methods + * @example + * List parentFieldChain = new List{Schema.Lead.CreatedById, Schema.User.Email}; + * QueryFilter filter = new QueryFilter().setValue(parentFieldChain, QueryOperator.NOT_EQUAL_TO, null); + * System.assertEquals('CreatedBy.Email != null', filter.getValue()); + */ + public IQueryFilter filterByField(QueryField queryField, QueryOperator operator, Object providedValue) { + this.value = queryField.getValue() + + ' ' + operator.getValue() + + ' ' + new QueryArgumentFormatter(providedValue).getValue(); + + return this; } - public QueryFilter(List sortedParentRelationshipFields, Schema.SObjectField fieldToFilter, QueryOperator operator, Object providedValue) { - this.parentRelationshipFields = sortedParentRelationshipFields; - this.sobjectField = fieldToFilter; - this.sobjectType = new SObjectFieldDescriber(fieldToFilter).SObjectType; - this.operator = operator; - this.providedValue = providedValue; + /** + * @description Creates a filter for a date function + * @param queryDateToFilter An instance of QueryDate, created by supplying a date or datetime field to filter on + * @param operator The instance of QueryOperator to use in the filter the list to check + * @param providedValue The value to compare to in the filter + * @return The instance of IQueryFilter, to allow chaining methods + * @example + * QueryDate qd = QueryDate.CALENDAR_MONTH(Schema.Lead.CreatedDate); + * QueryFilter filter = new QueryFilter().setValue(qd, QueryOperator.EQUALS, 2); + * System.assertEquals('CALENDAR_MONTH(CreatedDate) = 2', filter.getValue()); + */ + public IQueryFilter filterByQueryDate(QueryDate queryDateToFilter, QueryOperator operator, Integer providedValue) { + this.value = queryDateToFilter.getValue() + + ' ' + operator.getValue() + + ' ' + providedValue; + + return this; + } - this.setQueryFieldName(); + /** + * @description Creates a filter for a subquery on the sobject's ID + * @param inOrNotIn An instance of QueryOperator - it must be QueryOperator.IS_IN or QueryOperator.IS_NOT_IN + * @param lookupFieldOnRelatedSObject The lookup field on a related object that contains the ID of the current sobject + * @return The instance of IQueryFilter, to allow chaining methods + * @example + * QueryFilter filter = new QueryFilter().setValue(QueryOperator.IS_IN, Schema.Lead.ConvertedAccountId); + * System.assertEquals('Id IN (SELECT ConvertedAccountId FROM Lead)', filter.getValue()); + */ + // TODO figure out a better solution for inOrNotIn + public IQueryFilter filterBySubquery(QueryOperator inOrNotIn, Schema.SObjectField lookupFieldOnRelatedSObject) { + return this.setValueForSubquery('Id', inOrNotIn, lookupFieldOnRelatedSObject); } - public Object getProvidedValue() { - return this.providedValue; + /** + * @description Creates a filter for a subquery on an ID field for the current sobject + * @param lookupField The lookup field on the current sobject that contains an ID + * @param inOrNotIn An instance of QueryOperator - it must be QueryOperator.IS_IN or QueryOperator.IS_NOT_IN + * @param lookupFieldOnRelatedSObject The lookup field on a related object that contains the value of the lookupField paraemter + * @return The instance of IQueryFilter, to allow chaining methods + * @example + * QueryFilter filter = new QueryFilter().setValue(Schema.Lead.OwnerId, QueryOperator.IS_IN, Schema.User.Id); + * System.assertEquals('OwnerId IN (SELECT Id FROM User)', filter.getValue()); + */ + public IQueryFilter filterBySubquery(Schema.SObjectField lookupField, QueryOperator inOrNotIn, Schema.SObjectField lookupFieldOnRelatedSObject) { + return this.setValueForSubquery(lookupField.getDescribe().getName(), inOrNotIn, lookupFieldOnRelatedSObject); } - public Schema.SObjectField getSObjectField() { - return this.sobjectField; + /** + * @description Adds several filters together as a set of 'AND' filters + * @param queryFilters The filters to group together + * @return The instance of IQueryFilter, to allow chaining methods + */ + public IQueryFilter andFilterBy(List queryFilters) { + return this.filterByWithSeparator(queryFilters, 'AND'); } + /** + * @description Adds several filters together as a set of 'OR' filters + * @param queryFilters The filters to group together + * @return The instance of IQueryFilter, to allow chaining methods + */ + public IQueryFilter orFilterBy(List queryFilters) { + return this.filterByWithSeparator(queryFilters, 'OR'); + } + + /** + * @description Returns the calculated value, based on the method & parameters provided + * @return The string of the query filter, ready to be used in dynamic SOQL/SOSL + */ public String getValue() { - return this.queryFieldName + ' ' + this.operator.getValue() + ' ' + new QueryArgumentFormatter(this.providedValue).getValue(); + return this.value; } - private void setQueryFieldName() { - this.queryFieldName = ''; - SObjectType currentSObjectType = this.SObjectType; - for(Schema.SObjectField parentRelationshipField : this.parentRelationshipFields) { - this.queryFieldName += parentRelationshipField.getDescribe().getRelationshipName() + '.'; - currentSObjectType = new SObjectFieldDescriber(parentRelationshipField).SObjectType; - } - this.queryFieldName += this.sobjectField; + public Integer compareTo(Object compareTo) { + QueryFilter compareToQueryFilter = (QueryFilter)compareTo; + if(this.getValue() == compareToQueryFilter.getValue()) return 0; + if(this.getValue() > compareToQueryFilter.getValue()) return 1; + return -1; + } + + private IQueryFilter setValueForSubquery(String idFieldName, QueryOperator inOrNotIn, Schema.SObjectField lookupFieldOnRelatedSObject) { + String relatedSObjectTypeName = new SObjectFieldDescriber(lookupFieldOnRelatedSObject).getSObjectType().getDescribe().getName(); + String lookupFieldOnRelatedSObjectName = lookupFieldOnRelatedSObject.getDescribe().getName(); + + this.value = idFieldName + ' ' + inOrNotIn.getValue() + ' (SELECT ' + lookupFieldOnRelatedSObjectName + ' FROM ' + relatedSObjectTypeName + ')'; + + return this; + } + + private IQueryFilter filterByWithSeparator(List queryFilters, String separator) { + List queryFilterValues = new List(); + for(IQueryFilter queryFilter : queryFilters) queryFilterValues.add(queryFilter.getValue()); + + this.value = '(' + String.join(queryFilterValues, ' ' + separator + ' ') + ')'; + return this; } } \ No newline at end of file diff --git a/src/classes/QueryFilterScope.cls b/src/classes/QueryFilterScope.cls index 6e0acc1..55dc3da 100644 --- a/src/classes/QueryFilterScope.cls +++ b/src/classes/QueryFilterScope.cls @@ -2,4 +2,13 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ -public enum QueryFilterScope { EVERYTHING, TEAM, MINE } \ No newline at end of file + +/** +* +* @group Query Builder +* +* @description Enum of possible values for SOQL's optional USING SCOPE +* Salesforce docs: developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_using_scope.htm +* +*/ +public enum QueryFilterScope { EVERYTHING, DELEGATED, TEAM, MINE, MY_TERRITORY, MY_TEAM_TERRITORY } \ No newline at end of file diff --git a/src/classes/QueryFilter_Tests.cls b/src/classes/QueryFilter_Tests.cls index 59b5e9d..c87958a 100644 --- a/src/classes/QueryFilter_Tests.cls +++ b/src/classes/QueryFilter_Tests.cls @@ -9,55 +9,86 @@ private class QueryFilter_Tests { static void it_should_return_the_query_filter_for_a_field() { Schema.SObjectField sobjectField = Schema.User.CompanyName; QueryOperator operator = QueryOperator.IS_IN; - List values = new List{'derp'}; + List providedValues = new List{'derp', 'herp'}; Test.startTest(); - QueryFilter queryFilter = new QueryFilter(sobjectField, operator, values); + QueryFilter queryFilter = (QueryFilter)new QueryFilter().filterByField(new QueryField(sobjectField), operator, providedValues); Test.stopTest(); - String expectedQueryFieldName = 'CompanyName'; - System.assertEquals(expectedQueryFieldName, queryFilter.queryFieldName); - - String expectedQueryFilter = 'CompanyName ' + operator.getValue() + ' (\'' + String.join(values, '\',\'') + '\')'; + String expectedQueryFilter = 'CompanyName ' + operator.getValue() + ' (\'' + String.join(providedValues, '\', \'') + '\')'; System.assertEquals(expectedQueryFilter, queryFilter.getValue()); } @isTest static void it_should_return_the_query_filter_for_a_parent_field() { - Schema.SObjectField parentRelationshipSObjectField = Schema.Lead.CreatedById; - Schema.SObjectField sobjectField = Schema.User.Email; - QueryOperator operator = QueryOperator.EQUALS; - String value = 'derp@test.com'; + QueryField parentFieldToFilter = new QueryField(new List{ + Schema.Lead.CreatedById, Schema.User.Email + }); + QueryOperator operator = QueryOperator.EQUALS; + String providedValue = 'derp@test.com'; Test.startTest(); - QueryFilter queryFilter = new QueryFilter(parentRelationshipSObjectField, sobjectField, operator, value); + QueryFilter queryFilter = (QueryFilter)new QueryFilter().filterByField(parentFieldToFilter, operator, providedValue); Test.stopTest(); - String expectedQueryFieldName = 'CreatedBy.Email'; - System.assertEquals(expectedQueryFieldName, queryFilter.queryFieldName); - - String expectedQueryFilter = 'CreatedBy.Email ' + operator.getValue() + ' \'' + value + '\''; + String expectedQueryFilter = 'CreatedBy.Email ' + operator.getValue() + ' \'' + providedValue + '\''; System.assertEquals(expectedQueryFilter, queryFilter.getValue()); } @isTest static void it_should_return_the_query_filter_for_a_grandparent_field() { - List grandparentFields = new List{ - Schema.Lead.OwnerId, Schema.User.ManagerId, Schema.User.ProfileId - }; + QueryField grandparentFieldToFilter = new QueryField(new List{ + Schema.Lead.OwnerId, Schema.User.ManagerId, Schema.User.ProfileId, Schema.Profile.Name + }); + QueryOperator operator = QueryOperator.EQUALS; + String providedValue = 'derp'; + + Test.startTest(); + QueryFilter queryFilter = (QueryFilter)new QueryFilter().filterByField(grandparentFieldToFilter, operator, providedValue); + Test.stopTest(); + + String expectedQueryFilter = 'Owner.Manager.Profile.Name ' + operator.getValue() + ' \'' + providedValue + '\''; + System.assertEquals(expectedQueryFilter, queryFilter.getValue()); + } + + @isTest + static void it_should_return_the_query_filter_for_a_query_date() { + QueryDate qd = QueryDate.CALENDAR_MONTH(Schema.Lead.CreatedDate); + QueryOperator operator = QueryOperator.EQUALS; + Integer providedValue = 3; + + Test.startTest(); + QueryFilter queryFilter = (QueryFilter)new QueryFilter().filterByQueryDate(qd, operator, providedValue); + Test.stopTest(); + + String expectedQueryFilter = 'CALENDAR_MONTH(CreatedDate) ' + operator.getValue() + ' ' + providedValue; + System.assertEquals(expectedQueryFilter, queryFilter.getValue()); + } - Schema.SObjectField sobjectField = Schema.Profile.Name; - QueryOperator operator = QueryOperator.EQUALS; - String value = 'derp'; + @isTest + static void it_should_return_the_query_filter_for_a_subquery() { + QueryOperator operator = QueryOperator.IS_IN; + Schema.SObjectField lookupFieldOnRelatedSObject = Schema.Lead.ConvertedAccountId; Test.startTest(); - QueryFilter queryFilter = new QueryFilter(grandparentFields, sobjectField, operator, value); + QueryFilter queryFilter = (QueryFilter)new QueryFilter().filterBySubquery(operator, lookupFieldOnRelatedSObject); Test.stopTest(); - String expectedQueryFieldName = 'Owner.Manager.Profile.Name'; - System.assertEquals(expectedQueryFieldName, queryFilter.queryFieldName); + String expectedQueryFilter = 'Id ' + operator.getValue() + ' (SELECT ' + lookupFieldOnRelatedSObject.getDescribe().getName() + ' FROM Lead)'; + System.assertEquals(expectedQueryFilter, queryFilter.getValue()); + } + + @isTest + static void it_should_return_the_query_filter_for_a_subquery_with_a_specified_field() { + QueryOperator operator = QueryOperator.IS_IN; + Schema.SObjectField lookupField = Schema.Lead.OwnerId; + Schema.SObjectField lookupFieldOnRelatedSObject = Schema.User.Id; + + Test.startTest(); + QueryFilter queryFilter = (QueryFilter)new QueryFilter().filterBySubquery(lookupField, operator, lookupFieldOnRelatedSObject); + Test.stopTest(); - String expectedQueryFilter = 'Owner.Manager.Profile.Name ' + operator.getValue() + ' \'' + value + '\''; + String expectedQueryFilter = 'OwnerId ' + operator.getValue() + ' (SELECT ' + lookupFieldOnRelatedSObject.getDescribe().getName() + ' FROM User)'; System.assertEquals(expectedQueryFilter, queryFilter.getValue()); } diff --git a/src/classes/QueryNullSortOrder.cls b/src/classes/QueryNullSortOrder.cls index 370fa08..c00fa0c 100644 --- a/src/classes/QueryNullSortOrder.cls +++ b/src/classes/QueryNullSortOrder.cls @@ -2,4 +2,12 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description TODO +* +*/ public enum QueryNullSortOrder { FIRST, LAST } \ No newline at end of file diff --git a/src/classes/QueryOperator.cls b/src/classes/QueryOperator.cls index c55a463..271f1da 100644 --- a/src/classes/QueryOperator.cls +++ b/src/classes/QueryOperator.cls @@ -2,11 +2,22 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ -public without sharing class QueryOperator { + +/** +* +* @group Query Builder +* +* @description Provides all of the operators needed for SOQL/SOSL queries and minimizes the use of strings within the framework +* Salesforce docs: developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_comparisonoperators.htm +* +*/ +public without sharing class QueryOperator extends NebulaCore { private String value; private QueryOperator(String value) { + this.currentModule = NebulaCore.Module.QUERY_BUILDER; + this.value = value; } diff --git a/src/classes/QuerySearchGroup.cls b/src/classes/QuerySearchGroup.cls index 8025b37..47421ad 100644 --- a/src/classes/QuerySearchGroup.cls +++ b/src/classes/QuerySearchGroup.cls @@ -2,4 +2,12 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description TODO +* +*/ public enum QuerySearchGroup { ALL_FIELDS, NAME_FIELDS, EMAIL_FIELDS, PHONE_FIELDS, SIDEBAR_FIELDS } \ No newline at end of file diff --git a/src/classes/QuerySortOrder.cls b/src/classes/QuerySortOrder.cls index b3daef5..0d192b6 100644 --- a/src/classes/QuerySortOrder.cls +++ b/src/classes/QuerySortOrder.cls @@ -2,4 +2,12 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description TODO +* +*/ public enum QuerySortOrder { ASCENDING, DESCENDING } \ No newline at end of file diff --git a/src/classes/SObjectFieldDescriber.cls b/src/classes/SObjectFieldDescriber.cls index d7b6e66..27f975d 100644 --- a/src/classes/SObjectFieldDescriber.cls +++ b/src/classes/SObjectFieldDescriber.cls @@ -2,6 +2,14 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Metadata +* +* @description TODO +* +*/ public without sharing class SObjectFieldDescriber { // We would love for Schema.SObjectField to have a way to return the SObject Type // Sadly, it doesn't, so we have this class to fill the void. @@ -10,22 +18,26 @@ public without sharing class SObjectFieldDescriber { private static Map sobjectFieldHashCodeToSObjectTypeMap; - public Schema.SObjectField SObjectField {get; private set;} - public Schema.SObjectType SObjectType {get; private set;} + private Schema.SObjectField sobjectField; + private Schema.SObjectType sobjectType; public SObjectFieldDescriber(Schema.SObjectField sobjectField) { this.cacheSObjectTypeFieldHashCodes(); - this.SObjectField = sobjectField; + this.sobjectField = sobjectField; this.setSObjectType(); } public String getFieldName() { - return this.SObjectField.getDescribe().getName(); + return this.sobjectField.getDescribe().getName(); } public String getFullFieldName() { - return this.SObjectType + '.' + this.getFieldName(); + return this.sobjectType + '.' + this.getFieldName(); + } + + public Schema.SObjectType getSObjectType() { + return this.sobjectType; } public Schema.SObjectType getParentSObjectType() { diff --git a/src/classes/SObjectFieldDescriber_Tests.cls b/src/classes/SObjectFieldDescriber_Tests.cls index d8f47df..3117e27 100644 --- a/src/classes/SObjectFieldDescriber_Tests.cls +++ b/src/classes/SObjectFieldDescriber_Tests.cls @@ -13,23 +13,7 @@ private class SObjectFieldDescriber_Tests { Test.startTest(); SObjectFieldDescriber sobjectFieldDescriber = new SObjectFieldDescriber(sobjectField); - System.assertEquals(expectedSObjectType, sobjectFieldDescriber.SObjectType); - - Test.stopTest(); - } - - @isTest - static void it_should_return_the_sobject_field() { - Schema.SObjectField expectedSObjectField = Schema.Lead.Id; - Integer expectedSObjectFieldHash = ((Object)expectedSObjectField).hashCode(); - - Test.startTest(); - - SObjectFieldDescriber sobjectFieldDescriber = new SObjectFieldDescriber(expectedSObjectField); - Schema.SObjectField returnedSObjectField = sobjectFieldDescriber.sobjectField; - Integer returnedSObjectFieldHash = ((Object)returnedSObjectField).hashCode(); - - System.assertEquals(expectedSObjectFieldHash, returnedSObjectFieldHash); + System.assertEquals(expectedSObjectType, sobjectFieldDescriber.getSObjectType()); Test.stopTest(); } diff --git a/src/classes/SObjectQueryBuilder.cls b/src/classes/SObjectQueryBuilder.cls new file mode 100644 index 0000000..69eaa59 --- /dev/null +++ b/src/classes/SObjectQueryBuilder.cls @@ -0,0 +1,261 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description A builder class that generates dynamic SOQL & returns an SObject or list of SObjects +* +*/ +public class SObjectQueryBuilder extends QueryBuilder implements ISObjectQueryBuilder { + + private Set queryFields; + private List childRelationshipQueries; + private QueryFilterScope filterScope; + private Boolean forUpdate; + private SObjectType sobjectType; + private Map sobjectTypeFieldMap; + + public SObjectQueryBuilder(Schema.SObjectType sobjectType) { + this.sobjectType = sobjectType; + this.sobjectTypeFieldMap = sobjectType.getDescribe().fields.getMap(); + + this.queryFields = new Set(); + this.childRelationshipQueries = new List(); + this.forUpdate = false; + + this.addCommonQueryFields(); + } + + public ISObjectQueryBuilder addFields(List queryFields) { + for(IQueryField queryField : queryFields) this.addField(queryField); + return this; + } + + public ISObjectQueryBuilder addFields(Schema.FieldSet fieldSet) { + for(Schema.FieldSetMember field : fieldSet.getFields()) { + this.addField(this.sobjectTypeFieldMap.get(field.getFieldPath())); + } + return this; + } + + public ISObjectQueryBuilder addAllFields() { + for(Schema.SObjectField field : this.sobjectTypeFieldMap.values()) this.addField(field); + return this; + } + + public ISObjectQueryBuilder addAllStandardFields() { + for(Schema.SObjectField field : this.sobjectTypeFieldMap.values()) { + if(field.getDescribe().isCustom()) continue; + + this.addField(field); + } + return this; + } + + public ISObjectQueryBuilder addAllCustomFields() { + for(Schema.SObjectField field : this.sobjectTypeFieldMap.values()) { + if(!field.getDescribe().isCustom()) continue; + + this.addField(field); + } + return this; + } + + public ISObjectQueryBuilder addAllReadableFields() { + for(Schema.SObjectField field : this.sobjectTypeFieldMap.values()) { + if(!field.getDescribe().isAccessible()) continue; + + this.addField(field); + } + return this; + } + + public ISObjectQueryBuilder addAllEditableFields() { + for(Schema.SObjectField field : this.sobjectTypeFieldMap.values()) { + if(!field.getDescribe().isUpdateable()) continue; + + this.addField(field); + } + return this; + } + + public ISObjectQueryBuilder includeChildrenRecords(Schema.SObjectField childToParentRelationshipField, ISObjectQueryBuilder sobjectQueryBuilder) { + childRelationshipQueries.add(sobjectQueryBuilder.getChildQuery(childToParentRelationshipField)); + return this; + } + + public ISObjectQueryBuilder filterBy(IQueryFilter queryFilter) { + super.doFilterBy(queryFilter); + return this; + } + + public ISObjectQueryBuilder filterBy(List queryFilters) { + super.doFilterBy(queryFilters); + return this; + } + + public ISObjectQueryBuilder orderBy(IQueryField orderByQueryField) { + super.doOrderBy(orderByQueryField); + return this; + } + + public ISObjectQueryBuilder orderBy(IQueryField orderByQueryField, QuerySortOrder sortOrder) { + super.doOrderBy(orderByQueryField, sortOrder); + return this; + } + + public ISObjectQueryBuilder orderBy(IQueryField orderByQueryField, QuerySortOrder sortOrder, QueryNullSortOrder nullsSortOrder) { + super.doOrderBy(orderByQueryField, sortOrder, nullsSortOrder); + return this; + } + + public ISObjectQueryBuilder limitCount(Integer limitCount) { + super.doLimitCount(limitCount); + return this; + } + + public ISObjectQueryBuilder setAsUpdate() { + this.forUpdate = true; + return this; + } + + public ISObjectQueryBuilder usingScope(QueryFilterScope filterScope) { + this.filterScope = filterScope; + return this; + } + + public Database.QueryLocator getQueryLocator() { + return Database.getQueryLocator(this.getQuery()); + } + + public String getQuery() { + String query = 'SELECT ' + this.getQueryFieldString() + this.getChildRelationshipsQueryString() + + '\nFROM ' + this.sobjectType + + this.getUsingScopeString() + + super.doGetWhereClauseString() + + super.doGetOrderByString() + + super.doGetLimitCountString() + + this.getForUpdateString(); + + return query; + } + + public String getSearchQuery() { + String sobjectTypeOptions = this.getQueryFieldString() + + super.doGetWhereClauseString() + + super.doGetOrderByString() + + super.doGetLimitCountString(); + // If we have any sobject-specific options, then wrap the options in parentheses + sobjectTypeOptions = String.isEmpty(sobjectTypeOptions) ? '' : '(' + sobjectTypeOptions + ')'; + String query = this.sobjectType + sobjectTypeOptions; + + return query; + } + + public String getChildQuery(Schema.SObjectField childToParentRelationshipField) { + Schema.SObjectType parentSObjectType = childToParentRelationshipField.getDescribe().getReferenceTo()[0]; + // Get the relationship name + String childRelationshipName; + for(Schema.ChildRelationship childRelationship : parentSObjectType.getDescribe().getChildRelationships()) { + if(childRelationship.getField() != childToParentRelationshipField) continue; + + childRelationshipName = childRelationship.getRelationshipName(); + } + + String query = 'SELECT ' + this.getQueryFieldString() + + '\nFROM ' + childRelationshipName + + this.getUsingScopeString() + + super.doGetWhereClauseString() + + super.doGetOrderByString() + + super.doGetLimitCountString(); + + return query; + } + + // Query execution methods + public SObject getFirstQueryResult() { + return this.getQueryResults()[0]; + } + + public List getQueryResults() { + return super.doGetQueryResults(this.getQuery()); + } + + private void addField(IQueryField queryField) { + // If it's a parent field, add it immediately + if(queryField.getValue().contains('.')) this.queryFields.add(queryField.getValue()); + // Otherwise, parse it to an SObjectField so we can reused the logic in addField(Schema.SObjectField field) + else { + Schema.SObjectField field = this.sobjectType.getDescribe().fields.getMap().get(queryField.getValue()); + this.addField(field); + } + } + + private void addField(Schema.SObjectField field) { + this.queryFields.add(field.getDescribe().getName()); + + // If the field is a lookup, then we need to get the name field from the parent object + if(field.getDescribe().getType().name() == 'Reference') { + this.queryFields.add(this.getParentSObjectNameField(field)); + } + } + + private String getParentSObjectNameField(Schema.SObjectField field) { + String relationshipName = field.getDescribe().getRelationshipName(); + Schema.SObjectType parentSObjectType = field.getDescribe().getReferenceTo()[0]; + String nameField; + for(Schema.SObjectField parentField : parentSObjectType.getDescribe().fields.getMap().values()) { + if(parentField.getDescribe().isNameField()) { + nameField = parentField.getDescribe().getName(); + break; + } + } + return relationshipName + '.' + nameField; + } + + private String getQueryFieldString() { + Set cleanedQueryField = new Set(); + for(String queryField : new List(this.queryFields)) { + cleanedQueryField.add(queryField.toLowerCase()); + } + this.queryFields = cleanedQueryField; + List queryFieldList = new List(this.queryFields); + queryFieldList.sort(); + return String.join(queryFieldList, ', '); + } + + private String getChildRelationshipsQueryString() { + if(this.childRelationshipQueries.isEmpty()) return ''; + + this.childRelationshipQueries.sort(); + return ',\n(' + String.join(this.childRelationshipQueries, '), (') + ')'; + } + + private String getForUpdateString() { + return this.orderByList.isEmpty() && this.forUpdate ? '\nFOR UPDATE' : ''; + } + + private String getUsingScopeString() { + return this.filterScope == null ? '' : '\nUSING SCOPE ' + this.filterScope.name(); + } + + private void addCommonQueryFields() { + if(!NebulaSettings.SObjectQueryBuilderSettings.IncludeCommonFields__c) return; + // Auto-add the common fields that are available for the SObject Type + List commonFieldNameList = new List{ + 'Id', 'CaseNumber', 'CreatedById', 'CreatedDate', 'IsClosed', 'LastModifiedById', 'LastModifiedDate', + 'Name', 'OwnerId', 'ParentId', 'Subject', 'RecordTypeId', 'SystemModStamp' + }; + for(String commonFieldName : commonFieldNameList) { + this.sobjectTypeFieldMap = this.sobjectType.getDescribe().fields.getMap(); + if(!this.sobjectTypeFieldMap.containsKey(commonFieldName)) continue; + + this.queryFields.add(commonFieldName); + } + } + +} \ No newline at end of file diff --git a/src/classes/SObjectQueryBuilder.cls-meta.xml b/src/classes/SObjectQueryBuilder.cls-meta.xml new file mode 100644 index 0000000..8b061c8 --- /dev/null +++ b/src/classes/SObjectQueryBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/src/classes/SObjectQueryBuilder_Tests.cls b/src/classes/SObjectQueryBuilder_Tests.cls new file mode 100644 index 0000000..4b0fd51 --- /dev/null +++ b/src/classes/SObjectQueryBuilder_Tests.cls @@ -0,0 +1,416 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ +@isTest +private class SObjectQueryBuilder_Tests { + + static String getFirstLineThatStartsWith(String stringToSearch, String stringToCheckFor) { + String matchingLine; + for(String stringPartToSearch : stringToSearch.split('\n')) { + if(!stringPartToSearch.startsWith(stringToCheckFor)) continue; + + matchingLine = stringPartToSearch; + break; + } + + return matchingLine; + } + + static Set getQueryQueryFieldStringSet(String queryString) { + String selectString = getFirstLineThatStartsWith(queryString, 'SELECT'); + Set queryFieldStringSet = new Set(); + for(String unparsedString : selectString.remove('SELECT ').split(',')) { + queryFieldStringSet.add(unparsedString.trim()); + } + return queryFieldStringSet; + } + + static String getParentSObjectNameField(Schema.SObjectField field) { + if(field.getDescribe().getType().name() != 'Reference') return null; + + String relationshipName = field.getDescribe().getRelationshipName(); + Schema.SObjectType parentSObjectType = field.getDescribe().getReferenceTo()[0]; + String nameField; + for(Schema.SObjectField parentField : parentSObjectType.getDescribe().fields.getMap().values()) { + if(parentField.getDescribe().isNameField()) { + nameField = parentField.getDescribe().getName(); + break; + } + } + return relationshipName + '.' + nameField; + } + + static List buildExpectedQueryFieldStrings(List sobjectFields) { + List expectedQueryFieldStrings = new List(); + for(Schema.SObjectField field : sobjectFields) { + expectedQueryFieldStrings.add(field.getDescribe().getName().toLowerCase()); + + String parentNameField = getParentSObjectNameField(field); + if(parentNameField != null) expectedQueryFieldStrings.add(parentNameField.toLowerCase()); + } + expectedQueryFieldStrings.sort(); + return expectedQueryFieldStrings; + } + + static List convertToQueryFields(List fields) { + List queryFields = new List(); + for(Schema.SObjectField field : fields) queryFields.add(new QueryField(field)); + return queryFields; + } + + @isTest + static void it_should_add_a_list_of_fields() { + NebulaSettings.SObjectQueryBuilderSettings.IncludeCommonFields__c = false; + upsert NebulaSettings.SObjectQueryBuilderSettings; + + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List fields = new List{Schema.Contact.Id, Schema.Contact.AccountId, Schema.Contact.CreatedDate}; + List expectedQueryFieldStrings = buildExpectedQueryFieldStrings(fields); + expectedQueryFieldStrings.sort(); + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType); + query.addFields(convertToQueryFields(fields)); + Test.stopTest(); + + Set queryFieldStringSet = getQueryQueryFieldStringSet(query.getQuery()); + + // Verify that only our expected field name strings are included in the query + System.assertEquals(expectedQueryFieldStrings.size(), queryFieldStringSet.size()); + for(String expectedQueryFieldString : expectedQueryFieldStrings) { + System.assert(queryFieldStringSet.contains(expectedQueryFieldString), expectedQueryFieldString + queryFieldStringSet); + } + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_add_all_fields() { + NebulaSettings.SObjectQueryBuilderSettings.IncludeCommonFields__c = false; + upsert NebulaSettings.SObjectQueryBuilderSettings; + + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List expectedQueryFieldStrings = buildExpectedQueryFieldStrings(sobjectType.getDescribe().fields.getMap().values()); + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType); + query.addAllFields(); + Test.stopTest(); + + Set queryFieldStringSet = getQueryQueryFieldStringSet(query.getQuery()); + + // Verify that only our expected field name strings are included in the query + System.assertEquals(expectedQueryFieldStrings.size(), queryFieldStringSet.size()); + for(String expectedQueryFieldString : expectedQueryFieldStrings) { + System.assert(queryFieldStringSet.contains(expectedQueryFieldString), expectedQueryFieldString + queryFieldStringSet); + } + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_add_all_standard_fields() { + NebulaSettings.SObjectQueryBuilderSettings.IncludeCommonFields__c = false; + upsert NebulaSettings.SObjectQueryBuilderSettings; + + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List standardFields = new List(); + for(Schema.SObjectField field : sobjectType.getDescribe().fields.getMap().values()) { + if(field.getDescribe().isCustom()) continue; + + standardFields.add(field); + } + List expectedQueryFieldStrings = buildExpectedQueryFieldStrings(standardFields); + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType); + query.addAllStandardFields(); + Test.stopTest(); + + Set queryFieldStringSet = getQueryQueryFieldStringSet(query.getQuery()); + + // Verify that only our expected field name strings are included in the query + System.assertEquals(expectedQueryFieldStrings.size(), queryFieldStringSet.size()); + for(String expectedQueryFieldString : expectedQueryFieldStrings) { + System.assert(queryFieldStringSet.contains(expectedQueryFieldString), expectedQueryFieldString + queryFieldStringSet); + } + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_add_all_custom_fields() { + NebulaSettings.SObjectQueryBuilderSettings.IncludeCommonFields__c = false; + upsert NebulaSettings.SObjectQueryBuilderSettings; + + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List customFields = new List(); + for(Schema.SObjectField field : sobjectType.getDescribe().fields.getMap().values()) { + if(!field.getDescribe().isCustom()) continue; + + customFields.add(field); + } + List expectedQueryFieldStrings = buildExpectedQueryFieldStrings(customFields); + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType); + query.addAllCustomFields(); + Test.stopTest(); + + Set queryFieldStringSet = getQueryQueryFieldStringSet(query.getQuery()); + + // Verify that only our expected field name strings are included in the query + System.assertEquals(expectedQueryFieldStrings.size(), queryFieldStringSet.size()); + for(String expectedQueryFieldString : expectedQueryFieldStrings) { + System.assert(queryFieldStringSet.contains(expectedQueryFieldString), expectedQueryFieldString + queryFieldStringSet); + } + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_add_all_readable_fields() { + NebulaSettings.SObjectQueryBuilderSettings.IncludeCommonFields__c = false; + upsert NebulaSettings.SObjectQueryBuilderSettings; + + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List standardFields = new List(); + for(Schema.SObjectField field : sobjectType.getDescribe().fields.getMap().values()) { + if(!field.getDescribe().isAccessible()) continue; + + standardFields.add(field); + } + List expectedQueryFieldStrings = buildExpectedQueryFieldStrings(standardFields); + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType); + query.addAllReadableFields(); + Test.stopTest(); + + Set queryFieldStringSet = getQueryQueryFieldStringSet(query.getQuery()); + + // Verify that only our expected field name strings are included in the query + System.assertEquals(expectedQueryFieldStrings.size(), queryFieldStringSet.size()); + for(String expectedQueryFieldString : expectedQueryFieldStrings) { + System.assert(queryFieldStringSet.contains(expectedQueryFieldString), expectedQueryFieldString + queryFieldStringSet); + } + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_add_all_editable_fields() { + NebulaSettings.SObjectQueryBuilderSettings.IncludeCommonFields__c = false; + upsert NebulaSettings.SObjectQueryBuilderSettings; + + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List standardFields = new List(); + for(Schema.SObjectField field : sobjectType.getDescribe().fields.getMap().values()) { + if(!field.getDescribe().isUpdateable()) continue; + + standardFields.add(field); + } + List expectedQueryFieldStrings = buildExpectedQueryFieldStrings(standardFields); + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType); + query.addAllEditableFields(); + Test.stopTest(); + + Set queryFieldStringSet = getQueryQueryFieldStringSet(query.getQuery()); + + // Verify that only our expected field name strings are included in the query + System.assertEquals(expectedQueryFieldStrings.size(), queryFieldStringSet.size()); + for(String expectedQueryFieldString : expectedQueryFieldStrings) { + System.assert(queryFieldStringSet.contains(expectedQueryFieldString), expectedQueryFieldString + queryFieldStringSet); + } + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_order_by_field() { + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List fields = new List{Schema.Contact.CreatedDate}; + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType).addFields(convertToQueryFields(fields)); + query.orderBy(new QueryField(Schema.Contact.CreatedDate)); + Test.stopTest(); + + String orderByString = getFirstLineThatStartsWith(query.getQuery(), 'ORDER BY'); + System.assert(orderByString.contains(Contact.CreatedDate.getDescribe().getName()), orderByString); + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_order_by_field_ascending() { + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List fields = new List{Schema.Contact.CreatedDate}; + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType).addFields(convertToQueryFields(fields)); + query.orderBy(new QueryField(Schema.Contact.CreatedDate), QuerySortOrder.ASCENDING); + Test.stopTest(); + + String orderByString = getFirstLineThatStartsWith(query.getQuery(), 'ORDER BY'); + System.assert(orderByString.contains(Contact.CreatedDate.getDescribe().getName() + ' ASC'), orderByString); + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_order_by_field_descending() { + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List fields = new List{Schema.Contact.CreatedDate}; + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType).addFields(convertToQueryFields(fields)); + query.orderBy(new QueryField(Schema.Contact.CreatedDate), QuerySortOrder.DESCENDING); + Test.stopTest(); + + String orderByString = getFirstLineThatStartsWith(query.getQuery(), 'ORDER BY'); + System.assert(orderByString.contains(Contact.CreatedDate.getDescribe().getName() + ' DESC'), orderByString); + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_order_by_field_ascending_nulls_first() { + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List fields = new List{Schema.Contact.FirstName}; + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType).addFields(convertToQueryFields(fields)); + query.orderBy(new QueryField(Schema.Contact.FirstName), QuerySortOrder.ASCENDING, QueryNullSortOrder.FIRST); + Test.stopTest(); + + String orderByString = getFirstLineThatStartsWith(query.getQuery(), 'ORDER BY'); + System.assert(orderByString.contains(Contact.FirstName.getDescribe().getName() + ' ASC NULLS FIRST'), orderByString); + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_order_by_field_descending_nulls_first() { + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List fields = new List{Schema.Contact.FirstName}; + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType).addFields(convertToQueryFields(fields)); + query.orderBy(new QueryField(Schema.Contact.FirstName), QuerySortOrder.DESCENDING, QueryNullSortOrder.FIRST); + Test.stopTest(); + + String orderByString = getFirstLineThatStartsWith(query.getQuery(), 'ORDER BY'); + System.assert(orderByString.contains(Contact.FirstName.getDescribe().getName() + ' DESC NULLS FIRST'), orderByString); + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_order_by_field_ascending_nulls_last() { + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List fields = new List{Schema.Contact.FirstName}; + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType).addFields(convertToQueryFields(fields)); + query.orderBy(new QueryField(Schema.Contact.FirstName), QuerySortOrder.ASCENDING, QueryNullSortOrder.LAST); + Test.stopTest(); + + String orderByString = getFirstLineThatStartsWith(query.getQuery(), 'ORDER BY'); + System.assert(orderByString.contains(Contact.FirstName.getDescribe().getName() + ' ASC NULLS LAST'), orderByString); + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_order_by_field_descending_nulls_last() { + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List fields = new List{Schema.Contact.FirstName}; + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType).addFields(convertToQueryFields(fields)); + query.orderBy(new QueryField(Schema.Contact.FirstName), QuerySortOrder.DESCENDING, QueryNullSortOrder.LAST); + Test.stopTest(); + + String orderByString = getFirstLineThatStartsWith(query.getQuery(), 'ORDER BY'); + System.assert(orderByString.contains(Contact.FirstName.getDescribe().getName() + ' DESC NULLS LAST'), orderByString); + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_limit_count() { + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List fields = new List{Schema.Contact.CreatedDate}; + Integer limitCount = 99; + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType).addFields(convertToQueryFields(fields)); + query.limitCount(limitCount); + Test.stopTest(); + + System.assert(query.getQuery().contains('LIMIT ' + limitCount)); + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_set_as_update() { + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List fields = new List{Schema.Contact.CreatedDate}; + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType).addFields(convertToQueryFields(fields)); + query.setAsUpdate(); + Test.stopTest(); + + System.assert(query.getQuery().contains('FOR UPDATE')); + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_set_using_scope() { + Schema.SObjectType sobjectType = Schema.Contact.SObjectType; + List fields = new List{Schema.Contact.CreatedDate}; + QueryFilterScope scope = QueryFilterScope.TEAM; + + Test.startTest(); + SObjectQueryBuilder query = (SObjectQueryBuilder)new SObjectQueryBuilder(sobjectType).addFields(convertToQueryFields(fields)); + query.usingScope(scope); + Test.stopTest(); + + System.assert(query.getQuery().contains('USING SCOPE ' + scope.name())); + + // Execute the query to make sure it's executable + query.getQueryResults(); + } + + @isTest + static void it_should_generate_a_query_with_a_subselect() { + List leads = (List)new SObjectQueryBuilder(Schema.Lead.SObjectType) + .filterBy(new QueryFilter().filterBySubquery(Schema.Lead.OwnerId, QueryOperator.IS_IN, Schema.User.Id)) + .getQueryResults(); + + // TODO finish writings tests System.assert(false, 'finish writing tests'); + } + +} \ No newline at end of file diff --git a/src/classes/SObjectQueryBuilder_Tests.cls-meta.xml b/src/classes/SObjectQueryBuilder_Tests.cls-meta.xml new file mode 100644 index 0000000..8b061c8 --- /dev/null +++ b/src/classes/SObjectQueryBuilder_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/src/classes/SObjectRecordTypes.cls b/src/classes/SObjectRecordTypes.cls index 0077583..09331b7 100644 --- a/src/classes/SObjectRecordTypes.cls +++ b/src/classes/SObjectRecordTypes.cls @@ -2,24 +2,32 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Record Types +* +* @description TODO +* +*/ public abstract class SObjectRecordTypes extends NebulaCore implements ISObjectRecordTypes { private static Map> cachedRecordTypesBySObjectMap = new Map>(); - private Schema.SObjectType sobjectType; private String sobjectName; - public SObjectRecordTypes(Schema.SObjectType sobjectType) { + public SObjectRecordTypes() { this.currentModule = NebulaCore.Module.RECORD_TYPES; - this.sobjectType = sobjectType; - this.sobjectName = this.sobjectType.getDescribe().getName(); + this.sobjectName = this.getSObjectType().getDescribe().getName(); Logger.addEntry(this, 'Getting record types for ' + this.sobjectName); this.populateCache(); } + public abstract Schema.SObjectType getSObjectType(); + public Map getAllById() { return new Map(cachedRecordTypesBySObjectMap.get(this.sobjectName)); } @@ -40,24 +48,24 @@ public abstract class SObjectRecordTypes extends NebulaCore implements ISObjectR private void populateCache() { if(cachedRecordTypesBySObjectMap.containsKey(this.sobjectName)) return; - Schema.SObjectType recordTypeSObjectType = Schema.getGlobalDescribe().get('RecordType'); + // Trying to call Schema.RecordType.SObjectType confuses Apex, so we have to get it through an extra describe call + Schema.SObjectType recordTypeSObjectType = Schema.SObjectType.RecordType.getSObjectType(); + ISObjectQueryBuilder query = new SObjectQueryBuilder(recordTypeSObjectType); // If we don't have the SObject cached, then we need to query - QueryBuilder query = new QueryBuilder(recordTypeSObjectType); - if(NebulaSettings.RecordTypesSettings.LazyLoad__c) { - query.filterBy(new QueryFilter(Schema.RecordType.SObjectType, QueryOperator.EQUALS, this.sobjectName)); + query.filterBy(new QueryFilter().filterByField(new QueryField(Schema.RecordType.SObjectType), QueryOperator.EQUALS, this.sobjectName)); Logger.addEntry(this, 'NebulaSettings.RecordTypesSettings.LazyLoad__c=' + NebulaSettings.RecordTypesSettings.LazyLoad__c); Logger.addEntry(this, 'this.sobjectName=' + this.sobjectName); } if(!NebulaSettings.RecordTypesSettings.IncludeManagedRecordTypes__c) { - query.filterBy(new QueryFilter(Schema.RecordType.NamespacePrefix, QueryOperator.EQUALS, null)); + query.filterBy(new QueryFilter().filterByField(new QueryField(Schema.RecordType.NamespacePrefix), QueryOperator.EQUALS, null)); } - query.orderBy(Schema.RecordType.DeveloperName); + query.orderBy(new QueryField(Schema.RecordType.DeveloperName)); - Logger.addEntry(this, 'SObjectRecordTypes query=' + query.getQuery()); + Logger.addEntry(this, 'Loading SObjectRecordTypes for=' + this.getSObjectType()); if(!cachedRecordTypesBySObjectMap.containsKey(this.sobjectName)) cachedRecordTypesBySObjectMap.put(this.sobjectName, new List()); List recordTypeList = (List)query.getQueryResults(); diff --git a/src/classes/SObjectRecordTypes_Tests.cls b/src/classes/SObjectRecordTypes_Tests.cls index 1d76c93..c13ee69 100644 --- a/src/classes/SObjectRecordTypes_Tests.cls +++ b/src/classes/SObjectRecordTypes_Tests.cls @@ -5,22 +5,22 @@ @isTest private class SObjectRecordTypes_Tests { - private class LeadRecordTypes extends SObjectRecordTypes { + private class UserRecordTypes extends SObjectRecordTypes { // Test subclass that extends SObjectRecordTypes - public LeadRecordTypes() { - super(Schema.Lead.SObjectType); + public override Schema.SObjectType getSObjectType() { + return Schema.User.SObjectType; } } @isTest static void it_should_return_a_map_of_all_record_types_by_id() { - List expectedRecordTypeList = [SELECT Id, DeveloperName FROM RecordType WHERE SObjectType = 'Lead']; + List expectedRecordTypeList = [SELECT Id, DeveloperName FROM RecordType WHERE SObjectType = 'User']; Test.startTest(); - System.assertEquals(expectedRecordTypeList.size(), new SObjectRecordTypes_Tests.LeadRecordTypes().getAllById().size()); + System.assertEquals(expectedRecordTypeList.size(), new SObjectRecordTypes_Tests.UserRecordTypes().getAllById().size()); for(RecordType recordType : expectedRecordTypeList) { - System.assert(new SObjectRecordTypes_Tests.LeadRecordTypes().getAllById().containsKey(recordType.Id)); + System.assert(new SObjectRecordTypes_Tests.UserRecordTypes().getAllById().containsKey(recordType.Id)); } Test.stopTest(); @@ -28,13 +28,13 @@ private class SObjectRecordTypes_Tests { @isTest static void it_should_return_a_map_of_all_record_types_by_developer_name() { - List expectedRecordTypeList = [SELECT Id, DeveloperName FROM RecordType WHERE SObjectType = 'Lead']; + List expectedRecordTypeList = [SELECT Id, DeveloperName FROM RecordType WHERE SObjectType = 'User']; Test.startTest(); - System.assertEquals(expectedRecordTypeList.size(), new SObjectRecordTypes_Tests.LeadRecordTypes().getAllByDeveloperName().size()); + System.assertEquals(expectedRecordTypeList.size(), new SObjectRecordTypes_Tests.UserRecordTypes().getAllByDeveloperName().size()); for(RecordType recordType : expectedRecordTypeList) { - System.assert(new SObjectRecordTypes_Tests.LeadRecordTypes().getAllByDeveloperName().containsKey(recordType.DeveloperName)); + System.assert(new SObjectRecordTypes_Tests.UserRecordTypes().getAllByDeveloperName().containsKey(recordType.DeveloperName)); } Test.stopTest(); @@ -43,7 +43,7 @@ private class SObjectRecordTypes_Tests { @isTest static void it_should_return_a_map_of_all_record_types_by_id_excluding_managed_record_types() { - List expectedRecordTypeList = [SELECT Id, DeveloperName, NamespacePrefix FROM RecordType WHERE SObjectType = 'Lead' AND NamespacePrefix = null]; + List expectedRecordTypeList = [SELECT Id, DeveloperName, NamespacePrefix FROM RecordType WHERE SObjectType = 'User' AND NamespacePrefix = null]; NebulaRecordTypesSettings__c recordTypesSettings = NebulaRecordTypesSettings__c.getInstance(); recordTypesSettings.IncludeManagedRecordTypes__c = false; @@ -51,11 +51,11 @@ private class SObjectRecordTypes_Tests { Test.startTest(); - System.assertEquals(expectedRecordTypeList.size(), new SObjectRecordTypes_Tests.LeadRecordTypes().getAllById().size()); + System.assertEquals(expectedRecordTypeList.size(), new SObjectRecordTypes_Tests.UserRecordTypes().getAllById().size()); for(RecordType recordType : expectedRecordTypeList) { - System.assert(new SObjectRecordTypes_Tests.LeadRecordTypes().getAllById().containsKey(recordType.Id)); + System.assert(new SObjectRecordTypes_Tests.UserRecordTypes().getAllById().containsKey(recordType.Id)); } - for(RecordType recordType : new SObjectRecordTypes_Tests.LeadRecordTypes().getAllById().values()) { + for(RecordType recordType : new SObjectRecordTypes_Tests.UserRecordTypes().getAllById().values()) { System.assertEquals(null, recordType.NamespacePrefix); } @@ -72,7 +72,7 @@ private class SObjectRecordTypes_Tests { System.assertEquals(0, Limits.getQueries()); for(Integer i = 0; i < 10; i++) { - System.debug(new SObjectRecordTypes_Tests.LeadRecordTypes().getAllById()); + System.debug(new SObjectRecordTypes_Tests.UserRecordTypes().getAllById()); } System.assertEquals(1, Limits.getQueries()); @@ -90,7 +90,7 @@ private class SObjectRecordTypes_Tests { System.assertEquals(0, Limits.getQueries()); for(Integer i = 0; i < 10; i++) { - System.debug(new SObjectRecordTypes_Tests.LeadRecordTypes().getAllById()); + System.debug(new SObjectRecordTypes_Tests.UserRecordTypes().getAllById()); } System.assertEquals(1, Limits.getQueries()); diff --git a/src/classes/SObjectRepository.cls b/src/classes/SObjectRepository.cls index 16f3dc4..7902d9c 100644 --- a/src/classes/SObjectRepository.cls +++ b/src/classes/SObjectRepository.cls @@ -2,52 +2,40 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ -public abstract class SObjectRepository extends DML implements ISObjectRepository, IDML { - - protected IQueryBuilder Query { - get { return new QueryBuilder(this.sobjectType, this.fieldSet, this.sobjectFieldList); } - } - - private final Schema.SObjectType sobjectType; - private final Schema.FieldSet fieldSet; - private final List sobjectFieldList; - private final Map sobjectFieldMap; - private final Schema.SObjectField idField; - - public SObjectRepository(Schema.SObjectType sobjectType) { - this(sobjectType, null, null); - } - - public SObjectRepository(Schema.SObjectType sobjectType, Schema.FieldSet fieldSet) { - this(sobjectType, fieldSet, null); - } - public SObjectRepository(Schema.SObjectType sobjectType, List sobjectFieldList) { - this(sobjectType, null, sobjectFieldList); - } +/** +* +* @group Repository +* +* @description TODO +* +*/ +public abstract class SObjectRepository extends DML implements ISObjectRepository, IDML { - private SObjectRepository(Schema.SObjectType sobjectType, Schema.FieldSet fieldSet, List sobjectFieldList) { - super(sobjectType); + private List sobjectFieldList; + private Map sobjectFieldMap; + private QueryField idQueryField; + public SObjectRepository() { this.currentModule = NebulaCore.Module.REPOSITORY; - this.sobjectType = sobjectType; - this.fieldSet = fieldSet; - this.sobjectFieldList = sobjectFieldList; - this.sobjectFieldMap = sobjectType.getDescribe().fields.getMap(); - this.idField = this.getField('Id'); + this.sobjectFieldMap = this.getSObjectType().getDescribe().fields.getMap(); + this.idQueryField = new QueryField(this.getField('Id')); } + public abstract Schema.SObjectType getSObjectType(); + // SOQL public virtual SObject getById(Id recordId) { - return this.Query - .filterBy(new QueryFilter(this.idField, QueryOperator.EQUALS, recordId)) + return new SObjectQueryBuilder(this.getSObjectType()) + .filterBy(new QueryFilter().filterByField(this.idQueryField, QueryOperator.EQUALS, recordId)) + .filterBy(new QueryFilter().filterByField(this.idQueryField, QueryOperator.EQUALS, recordId)) .getFirstQueryResult(); } public virtual List getById(List recordIds) { - return this.Query - .filterBy(new QueryFilter(this.idField, QueryOperator.IS_IN, recordIds)) + return new SObjectQueryBuilder(this.getSObjectType()) + .filterBy(new QueryFilter().filterByField(this.idQueryField, QueryOperator.IS_IN, recordIds)) .getQueryResults(); } @@ -56,7 +44,7 @@ public abstract class SObjectRepository extends DML implements ISObjectRepositor } public virtual List get(List queryFilters) { - return this.Query + return new SObjectQueryBuilder(this.getSObjectType()) .filterBy(queryFilters) .getQueryResults(); } @@ -66,8 +54,8 @@ public abstract class SObjectRepository extends DML implements ISObjectRepositor } public virtual List getByIdAndQueryFilters(List idList, List queryFilters) { - return this.Query - .filterBy(new QueryFilter(this.idField, QueryOperator.IS_IN, idList)) + return new SObjectQueryBuilder(this.getSObjectType()) + .filterBy(new QueryFilter().filterByField(this.idQueryField, QueryOperator.IS_IN, idList)) .filterBy(queryFilters) .getQueryResults(); } @@ -82,13 +70,9 @@ public abstract class SObjectRepository extends DML implements ISObjectRepositor } public virtual List getSearchResults(String searchTerm, QuerySearchGroup searchGroup, List queryFilters) { - return this.Query - .filterBy(queryFilters) - .getSearchResults(searchTerm, searchGroup); - } - - public virtual IQueryBuilder getQueryBuilder() { - return this.Query; + return new SearchQueryBuilder(searchTerm, new SObjectQueryBuilder(this.getSObjectType()).filterBy(queryFilters)) + .setQuerySearchGroup(searchGroup) + .getFirstSearchResult(); } private Schema.SObjectField getField(String fieldName) { diff --git a/src/classes/SObjectRepositoryMocks.cls b/src/classes/SObjectRepositoryMocks.cls index 0b48984..65938df 100644 --- a/src/classes/SObjectRepositoryMocks.cls +++ b/src/classes/SObjectRepositoryMocks.cls @@ -2,6 +2,14 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Repository +* +* @description TODO +* +*/ @isTest public class SObjectRepositoryMocks { @@ -19,6 +27,10 @@ public class SObjectRepositoryMocks { this.records = records; } + public Schema.SObjectType getSObjectType() { + return Schema.User.SObjectType; + } + public Base with(List records) { this.records = records; return this; @@ -73,11 +85,14 @@ public class SObjectRepositoryMocks { public virtual List getByIdAndQueryFilters(List idList, List queryFilters) { List records = this.getById(idList); - for(SObject record : records) { - for(IQueryFilter queryFilter : queryFilters) { - record.put(queryFilter.getSObjectField(), queryFilter.getProvidedValue()); - } - } + // TODO need to revisit - QueryFilter no longer has the methods needed below, so need a new solution + // for(SObject record : records) { + // for(IQueryFilter queryFilter : queryFilters) { + // if(queryFilter.getSObjectField() == null) continue; + // + // record.put(queryFilter.getSObjectField(), queryFilter.getProvidedValue()); + // } + // } return records; } @@ -95,10 +110,6 @@ public class SObjectRepositoryMocks { return this.returnListOfSObjects(); } - public virtual IQueryBuilder getQueryBuilder() { - return new QueryBuilder(this.records.getSObjectType(), null, null); - } - private void createMockObjects() { // We would expect that for the Ids passed in, there will be a corresponding number of records returned of the exact same // SObjectType as their Ids. diff --git a/src/classes/SObjectRepositoryMocks_Tests.cls b/src/classes/SObjectRepositoryMocks_Tests.cls index 2a0c1ac..d8f47c3 100644 --- a/src/classes/SObjectRepositoryMocks_Tests.cls +++ b/src/classes/SObjectRepositoryMocks_Tests.cls @@ -17,9 +17,9 @@ private class SObjectRepositoryMocks_Tests { @isTest static void it_should_fake_returning_by_field_and_value() { Id testId = getTestId(); - SObjectField field = getField(); + QueryField field = getField(); - List queryFilters = new List{new QueryFilter(field, QueryOperator.EQUALS, getFieldValue())}; + List queryFilters = new List{new QueryFilter().filterByField(field, QueryOperator.EQUALS, getFieldValue())}; SObject returnedObj = new SObjectRepositoryMocks.Base(null).getByIdAndQueryFilters(new Set{testId}, queryFilters)[0]; System.assertEquals(getFieldValue(), returnedObj.get(String.valueOf(field))); @@ -27,7 +27,7 @@ private class SObjectRepositoryMocks_Tests { @isTest static void it_should_return_list_of_sobjects_when_mocking_sosl_search() { - System.assert(new SObjectRepositoryMocks.Base().getSearchResults(getFieldValue(),QuerySearchGroup.ALL_FIELDS) instanceof List); + System.assert(new SObjectRepositoryMocks.Base().getSearchResults(getFieldValue(), QuerySearchGroup.ALL_FIELDS) instanceof List); } @isTest @@ -38,8 +38,8 @@ private class SObjectRepositoryMocks_Tests { System.assert(base.getSearchResults(getFieldValue(),QuerySearchGroup.ALL_FIELDS) instanceof List); } - static SObjectField getField() { - return Schema.Lead.LeadSource; + static QueryField getField() { + return new QueryField(Schema.Lead.LeadSource); } static String getFieldValue() { diff --git a/src/classes/SObjectRepository_Tests.cls b/src/classes/SObjectRepository_Tests.cls index bba02f4..6f96c85 100644 --- a/src/classes/SObjectRepository_Tests.cls +++ b/src/classes/SObjectRepository_Tests.cls @@ -7,46 +7,38 @@ private class SObjectRepository_Tests { private class AccountRepository extends SObjectRepository { // Test subclass that extends SObjectRepository - public AccountRepository() { - super(Schema.Account.SObjectType, new List{Schema.Account.Id, Schema.Account.Name}); - } - - public AccountRepository(Schema.FieldSet fieldSet) { - super(Schema.Account.SObjectType, fieldSet); + public override Schema.SObjectType getSObjectType() { + return Schema.Account.SObjectType; } public List getMyRecentAccounts(Integer limitCount) { - return (List)this.Query + return (List)new SObjectQueryBuilder(this.getSObjectType()) .usingScope(QueryFilterScope.MINE) .limitCount(limitCount) - .orderBy(Schema.Account.LastActivityDate) + .orderBy(new QueryField(Schema.Account.LastActivityDate)) .getQueryResults(); } public Account getAccountAndContactsWithEmails(Id accountId) { - return (Account)this.Query - .filterBy(new QueryFilter(Schema.Account.Id, QueryOperator.EQUALS, accountId)) + return (Account)new SObjectQueryBuilder(this.getSObjectType()) + .filterBy(new QueryFilter().filterByField(new QueryField(Schema.Account.Id), QueryOperator.EQUALS, accountId)) .includeChildrenRecords( Schema.Contact.AccountId, - new ContactRepository() + new SObjectQueryBuilder(Schema.Contact.SObjectType) + .addAllStandardFields() + .filterBy(new QueryFilter().filterByField(new QueryField(Schema.Contact.Email), QueryOperator.NOT_EQUAL_TO, null)) ) .getFirstQueryResult(); } } - private class ContactRepository extends SObjectRepository { - public ContactRepository() { - super(Schema.Contact.SObjectType, new List{Schema.Contact.Id, Schema.Contact.AccountId, Schema.Contact.Email, Schema.Contact.LastName}); - } - } - private class UserRepository extends SObjectRepository { - public UserRepository() { - super(Schema.User.SObjectType); + public override Schema.SObjectType getSObjectType() { + return Schema.User.SObjectType; } - public override void upsertRecords(SObject record, Schema.SObjectField externalIdField) { - super.upsertRecords(record, externalIdField); + public override List upsertRecords(SObject record, Schema.SObjectField externalIdField) { + return super.upsertRecords(record, externalIdField); } } @@ -81,7 +73,7 @@ private class SObjectRepository_Tests { Schema.FieldSet fieldSet; Test.startTest(); - SObjectRepository_Tests.AccountRepository accountRepo = new SObjectRepository_Tests.AccountRepository(fieldSet); + SObjectRepository_Tests.AccountRepository accountRepo = new SObjectRepository_Tests.AccountRepository(); System.assertNotEquals(null, accountRepo); Test.stopTest(); } @@ -100,7 +92,7 @@ private class SObjectRepository_Tests { @isTest static void it_should_get_by_id_using_query_filter() { Account expectedAccount = [SELECT Id FROM Account LIMIT 1]; - QueryFilter queryFilter = new QueryFilter(Schema.Account.Id, QueryOperator.EQUALS, expectedAccount.Id); + IQueryFilter queryFilter = new QueryFilter().filterByField(new QueryField(Schema.Account.Id), QueryOperator.EQUALS, expectedAccount.Id); Test.startTest(); Account returnedAccount = ((List)new SObjectRepository_Tests.AccountRepository().get(queryFilter))[0]; @@ -112,10 +104,10 @@ private class SObjectRepository_Tests { @isTest static void it_should_get_by_id_using_list_of_query_filters() { Account expectedAccount = [SELECT Id FROM Account LIMIT 1]; - QueryFilter queryFilter = new QueryFilter(Schema.Account.Id, QueryOperator.EQUALS, expectedAccount.Id); + IQueryFilter queryFilter = new QueryFilter().filterByField(new QueryField(Schema.Account.Id), QueryOperator.EQUALS, expectedAccount.Id); Test.startTest(); - Account returnedAccount = ((List)new SObjectRepository_Tests.AccountRepository().get(new List{queryFilter}))[0]; + Account returnedAccount = ((List)new SObjectRepository_Tests.AccountRepository().get(new List{queryFilter}))[0]; Test.stopTest(); System.assertEquals(expectedAccount.Id, returnedAccount.Id); @@ -153,8 +145,8 @@ private class SObjectRepository_Tests { ISObjectRepository repo = new SObjectRepository_Tests.AccountRepository(); Test.startTest(); - QueryFilter queryFilter = new QueryFilter(Schema.Account.Name, QueryOperator.EQUALS, 'My Test Company'); - List queryFilters = new List{queryFilter}; + IQueryFilter queryFilter = new QueryFilter().filterByField(new QueryField(Schema.Account.Name), QueryOperator.EQUALS, 'My Test Company'); + List queryFilters = new List{queryFilter}; List returnedAccountList = (List)repo.getByIdAndQueryFilters(expectedAccountMap.keyset(), queryFilters); Test.stopTest(); @@ -170,8 +162,8 @@ private class SObjectRepository_Tests { ISObjectRepository repo = new SObjectRepository_Tests.AccountRepository(); Test.startTest(); - QueryFilter queryFilter = new QueryFilter(Schema.Account.Name, QueryOperator.EQUALS, 'My Test Company'); - List queryFilters = new List{queryFilter}; + IQueryFilter queryFilter = new QueryFilter().filterByField(new QueryField(Schema.Account.Name), QueryOperator.EQUALS, 'My Test Company'); + List queryFilters = new List{queryFilter}; List returnedAccountList = (List)repo.getByIdAndQueryFilters(new List(expectedAccountMap.keyset()), queryFilters); Test.stopTest(); @@ -185,12 +177,13 @@ private class SObjectRepository_Tests { Account account = [SELECT Id FROM Account LIMIT 1]; Contact contact = createContact(account.Id); + contact.Email = 'test@email.com'; insert contact; contact = [SELECT Id, AccountId FROM Contact WHERE Id = :contact.Id]; System.assertEquals(account.Id, contact.AccountId); Account expectedAccount = [ - SELECT Id, (SELECT Id, AccountId FROM Contacts) + SELECT Id, (SELECT Id, AccountId FROM Contacts WHERE Email != null) FROM Account WHERE Id = :account.Id ]; @@ -257,8 +250,8 @@ private class SObjectRepository_Tests { System.assertNotEquals(0, expectedAccountList.size()); Test.startTest(); - QueryFilter queryFilter = new QueryFilter(Schema.Account.CreatedDate, QueryOperator.EQUALS, QueryDateLiteral.TODAY); - List queryFilters = new List{queryFilter}; + IQueryFilter queryFilter = new QueryFilter().filterByField(new QueryField(Schema.Account.CreatedDate), QueryOperator.EQUALS, QueryDateLiteral.TODAY); + List queryFilters = new List{queryFilter}; List returnedAccountList = (List)new SObjectRepository_Tests.AccountRepository().getSearchResults(searchTerm, QuerySearchGroup.NAME_FIELDS, queryFilters); Test.stopTest(); @@ -331,7 +324,7 @@ private class SObjectRepository_Tests { new SObjectRepository_Tests.UserRepository().upsertRecords(existingUser, Schema.User.Username); Test.stopTest(); - existingUser = [SELECT Id, LastModifiedDate FROM User WHERE Id = :existingUser.Id]; + existingUser = [SELECT Id, LastModifiedDate FROM User LIMIT 1]; System.assert(existingUser.LastModifiedDate > originalLastModifiedDate, existingUser); } diff --git a/src/classes/SObjectTriggerHandler.cls b/src/classes/SObjectTriggerHandler.cls index 34eefde..e4802b5 100644 --- a/src/classes/SObjectTriggerHandler.cls +++ b/src/classes/SObjectTriggerHandler.cls @@ -2,6 +2,14 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Trigger Handler +* +* @description TODO +* +*/ public abstract class SObjectTriggerHandler extends NebulaCore implements ISObjectTriggerHandler { private static Map> hashCodesForProcessedRecords = new Map>(); @@ -21,11 +29,10 @@ public abstract class SObjectTriggerHandler extends NebulaCore implements ISObje public SObjectTriggerHandler() { this(false); + this.currentModule = NebulaCore.Module.TRIGGER_HANDLER; } protected SObjectTriggerHandler(Boolean isTestMode) { - this.currentModule = NebulaCore.Module.TRIGGER_HANDLER; - this.isTestMode = isTestMode; this.isTriggerExecuting = Trigger.isExecuting; @@ -60,7 +67,7 @@ public abstract class SObjectTriggerHandler extends NebulaCore implements ISObje else sobjectType = Trigger.new == null ? String.valueOf(Trigger.old.getSObjectType()) : String.valueOf(Trigger.new.getSObjectType()); Logger.addEntry(this, 'Starting execute method for: ' + sobjectType); - Logger.addEntry(this, 'Hash codes already processed: ' + SObjectTriggerHandler.hashCodesForProcessedRecords); + Logger.addEntry(this, 'Hash codes already processed: ' + hashCodesForProcessedRecords); Logger.addEntry(this, 'Hash code for current records: ' + this.hashCode); Logger.addEntry(this, 'Trigger context for current records: ' + this.currentTriggerContext); Logger.addEntry(this, 'Number of current records: ' + Trigger.size); @@ -166,11 +173,11 @@ public abstract class SObjectTriggerHandler extends NebulaCore implements ISObje // BEFORE_INSERT doesn't have record IDs yet, so the hash here will never match the other hashes // Since Salesforce makes it impossible to recursively run "insert record", we can let the platform handle it return false; - } else if(!SObjectTriggerHandler.hashCodesForProcessedRecords.containsKey(this.hashCode)) { - SObjectTriggerHandler.hashCodesForProcessedRecords.put(this.hashCode, new Set{this.currentTriggerContext}); + } else if(!hashCodesForProcessedRecords.containsKey(this.hashCode)) { + hashCodesForProcessedRecords.put(this.hashCode, new Set{this.currentTriggerContext}); return false; - } else if(!SObjectTriggerHandler.hashCodesForProcessedRecords.get(this.hashCode).contains(this.currentTriggerContext)) { - SObjectTriggerHandler.hashCodesForProcessedRecords.get(this.hashCode).add(this.currentTriggerContext); + } else if(!hashCodesForProcessedRecords.get(this.hashCode).contains(this.currentTriggerContext)) { + hashCodesForProcessedRecords.get(this.hashCode).add(this.currentTriggerContext); return false; } else { return true; diff --git a/src/classes/SObjectTypeDescriber.cls b/src/classes/SObjectTypeDescriber.cls deleted file mode 100644 index 51672cf..0000000 --- a/src/classes/SObjectTypeDescriber.cls +++ /dev/null @@ -1,50 +0,0 @@ -/************************************************************************************************* -* This file is part of the Nebula Framework project, released under the MIT License. * -* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * -*************************************************************************************************/ -public without sharing class SObjectTypeDescriber { - - public Schema.SObjectType SObjectType {get; private set;} - - public SObjectTypeDescriber(Id recordId) { - this(recordId.getSObjectType()); - } - - public SObjectTypeDescriber(SObject record) { - this(record.getSObjectType()); - } - - public SObjectTypeDescriber(Schema.SObjectType sobjectType) { - this.SObjectType = sobjectType; - } - - public String getChildRelationshipName(Schema.SObjectField childSObjectField) { - Schema.SObjectType parentSObjectType = new SObjectFieldDescriber(childSObjectField).getParentSObjectType(); - Schema.ChildRelationship childRelationship = this.getSObjectTypeChildRelationshipMap(parentSObjectType).get(childSObjectField); - - return childRelationship.getRelationshipName(); - } - - public Boolean validateSObjectFieldExists(Schema.SObjectField expectedSObjectField) { - SObjectFieldDescriber sobjectFieldDescriber = new SObjectFieldDescriber(expectedSObjectField); - Map fieldMap = this.SObjectType.getDescribe().fields.getMap(); - - Boolean sobjectTypesMatch = sobjectFieldDescriber.validateSObjectType(this.SObjectType); - Boolean sobjectTypesHasMatchingField = fieldMap.containsKey(sobjectFieldDescriber.getFieldName()); - - return sobjectTypesMatch && sobjectTypesHasMatchingField; - } - - private Map getSObjectTypeChildRelationshipMap(Schema.SObjectType parentSObjectType) { - Map childRelationshipMap = new Map(); - - for(Schema.ChildRelationship childRelationship : parentSObjectType.getDescribe().getChildRelationships()) { - if(childRelationship.getRelationshipName() == null) continue; - - childRelationshipMap.put(childRelationship.getField(), childRelationship); - } - - return childRelationshipMap; - } - -} \ No newline at end of file diff --git a/src/classes/SObjectTypeDescriber_Tests.cls b/src/classes/SObjectTypeDescriber_Tests.cls deleted file mode 100644 index afc814a..0000000 --- a/src/classes/SObjectTypeDescriber_Tests.cls +++ /dev/null @@ -1,72 +0,0 @@ -/************************************************************************************************* -* This file is part of the Nebula Framework project, released under the MIT License. * -* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * -*************************************************************************************************/ -@isTest -private class SObjectTypeDescriber_Tests { - - @testSetup - static void setupData() { - Lead lead = new Lead( - Company = 'My Test Company', - LastName = 'Gillespie' - ); - - insert lead; - } - - @isTest - static void it_should_return_the_sobject_type_for_id() { - Id leadId = [SELECT Id FROM Lead LIMIT 1].Id; - Schema.SObjectType expectedSObjectType = leadId.getSobjectType(); - - Test.startTest(); - - SObjectTypeDescriber sobjectTypeDescriber = new SObjectTypeDescriber(leadId); - System.assertEquals(expectedSObjectType, sobjectTypeDescriber.SObjectType); - - Test.stopTest(); - } - - @isTest - static void it_should_return_the_sobject_type_for_record() { - Lead lead = [SELECT Id FROM Lead LIMIT 1]; - Schema.SObjectType expectedSObjectType = lead.getSobjectType(); - - Test.startTest(); - - SObjectTypeDescriber sobjectTypeDescriber = new SObjectTypeDescriber(lead); - System.assertEquals(expectedSObjectType, sobjectTypeDescriber.SObjectType); - - Test.stopTest(); - } - - @isTest - static void it_should_return_the_sobject_type_for_sobject_type() { - Schema.SObjectType expectedSObjectType = Schema.Lead.SObjectType; - - Test.startTest(); - SObjectTypeDescriber sobjectTypeDescriber = new SObjectTypeDescriber(expectedSObjectType); - System.assertEquals(expectedSObjectType, sobjectTypeDescriber.SObjectType); - Test.stopTest(); - } - - @isTest - static void it_should_validate_sobject_field_exists() { - SObjectTypeDescriber sobjectTypeDescriber = new SObjectTypeDescriber(Schema.Lead.SObjectType); - - Test.startTest(); - System.assertEquals(true, sobjectTypeDescriber.validateSObjectFieldExists(Schema.Lead.Id)); - Test.stopTest(); - } - - @isTest - static void it_should_validate_sobject_field_does_not_exist() { - SObjectTypeDescriber sobjectTypeDescriber = new SObjectTypeDescriber(Schema.Lead.SObjectType); - - Test.startTest(); - System.assertEquals(false, sobjectTypeDescriber.validateSObjectFieldExists(Schema.Task.Id)); - Test.stopTest(); - } - -} \ No newline at end of file diff --git a/src/classes/Scheduler.cls b/src/classes/Scheduler.cls index ee48d48..18b149e 100644 --- a/src/classes/Scheduler.cls +++ b/src/classes/Scheduler.cls @@ -2,7 +2,16 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Asynchronous Apex +* +* @description TODO +* +*/ public class Scheduler { + //CRON order - Seconds, Minutes, Hours, Day_of_month, Month, Day_of_week, Optional_year public final static String DAILY_CRON = '0 {0} {1} * * ?'; public final static String HOURLY_CRON = '0 {0} * * * ?'; @@ -25,4 +34,5 @@ public class Scheduler { public String schedule(String jobName, String cronExpression) { return System.schedule(jobName, cronExpression, this.scheduledClass); } + } \ No newline at end of file diff --git a/src/classes/Scheduler_Tests.cls b/src/classes/Scheduler_Tests.cls index 96322da..6d0a5bd 100644 --- a/src/classes/Scheduler_Tests.cls +++ b/src/classes/Scheduler_Tests.cls @@ -3,25 +3,27 @@ * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ @isTest -global class Scheduler_Tests { - private static final String SCHEDULABLE_JOB_ID_STRING = '7'; +private class Scheduler_Tests { - global class TestSchedulable implements Schedulable { - global void execute(SchedulableContext sc) {} + private static final String SCHEDULABLE_JOB_ID = '7'; + private static final String DAILY_CRON_EXP = '0 59 23 * * ?'; + + private class TestSchedulable implements Schedulable { + public void execute(SchedulableContext sc) {} } @isTest static void it_should_successfully_schedule_daily() { - String CRON_EXP = '0 59 23 * * ?'; - String jobId = new Scheduler(new TestSchedulable()).scheduleDaily(SCHEDULABLE_JOB_ID_STRING, '59', '23'); - CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger Where Id = :jobId]; - System.assertEquals(CRON_EXP, ct.CronExpression); + String jobId = new Scheduler(new TestSchedulable()).scheduleDaily(SCHEDULABLE_JOB_ID, '59', '23'); + CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger WHERE Id = :jobId]; + System.assertEquals(DAILY_CRON_EXP, ct.CronExpression); } @isTest static void it_should_successfully_schedule_hourly() { - String jobId = new Scheduler(new TestSchedulable()).scheduleHourly(SCHEDULABLE_JOB_ID_STRING, '59'); - CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger Where Id = :jobId]; - System.assertEquals(String.format(Scheduler.HOURLY_CRON,new List{'59'}), ct.CronExpression); + String jobId = new Scheduler(new TestSchedulable()).scheduleHourly(SCHEDULABLE_JOB_ID, '59'); + CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger WHERE Id = :jobId]; + System.assertEquals(String.format(Scheduler.HOURLY_CRON, new List{'59'}), ct.CronExpression); } + } \ No newline at end of file diff --git a/src/classes/SearchQueryBuilder.cls b/src/classes/SearchQueryBuilder.cls new file mode 100644 index 0000000..b84bac0 --- /dev/null +++ b/src/classes/SearchQueryBuilder.cls @@ -0,0 +1,67 @@ +/************************************************************************************************* +* This file is part of the Nebula Framework project, released under the MIT License. * +* See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * +*************************************************************************************************/ + +/** +* +* @group Query Builder +* +* @description A builder class that generates dynamic SOSL queries & returns a list of SObjects or list of a list of SObjects +* +*/ +public class SearchQueryBuilder extends QueryBuilder implements ISearchQueryBuilder { + + private String searchTerm; + private QuerySearchGroup searchGroup; + private List sobjectQueryBuilders; + private List sobjectQueries; + + public SearchQueryBuilder(String searchTerm, ISObjectQueryBuilder sobjectQueryBuilder) { + this(searchTerm, new List{sobjectQueryBuilder}); + } + + public SearchQueryBuilder(String searchTerm, List sobjectQueryBuilders) { + this.searchTerm = searchTerm; + this.sobjectQueryBuilders = sobjectQueryBuilders; + + this.searchGroup = QuerySearchGroup.ALL_FIELDS; + this.sobjectQueries = new List(); + + this.parseSObjectQueryBuilders(); + } + + public ISearchQueryBuilder setQuerySearchGroup(QuerySearchGroup searchGroup) { + this.searchGroup = searchGroup; + return this; + } + + public String getQuery() { + String query = 'FIND ' + new QueryArgumentFormatter(this.searchTerm.toLowerCase()).getValue() + + '\nIN ' + this.searchGroup.name().replace('_', ' ') + + '\nRETURNING ' + this.getSObjectQueriesString(); + + return query; + } + + public List getFirstSearchResult() { + return this.getSearchResults()[0]; + } + + public List> getSearchResults() { + return super.doGetSearchResults(this.getQuery()); + } + + private ISearchQueryBuilder parseSObjectQueryBuilders() { + for(ISObjectQueryBuilder sobjectQueryBuilder : this.sobjectQueryBuilders) { + this.sobjectQueries.add(sobjectQueryBuilder.getSearchQuery()); + } + return this; + } + + private String getSObjectQueriesString() { + this.sobjectQueries.sort(); + return String.join(this.sobjectQueries, ', '); + } + +} \ No newline at end of file diff --git a/src/classes/SearchQueryBuilder.cls-meta.xml b/src/classes/SearchQueryBuilder.cls-meta.xml new file mode 100644 index 0000000..8b061c8 --- /dev/null +++ b/src/classes/SearchQueryBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 39.0 + Active + diff --git a/src/classes/TestingUtils.cls b/src/classes/TestingUtils.cls index f3a552f..18ccd36 100644 --- a/src/classes/TestingUtils.cls +++ b/src/classes/TestingUtils.cls @@ -2,6 +2,14 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Utils +* +* @description TODO +* +*/ @isTest public class TestingUtils { diff --git a/src/classes/UUID.cls b/src/classes/UUID.cls index 63caf3b..3389bbb 100644 --- a/src/classes/UUID.cls +++ b/src/classes/UUID.cls @@ -2,6 +2,14 @@ * This file is part of the Nebula Framework project, released under the MIT License. * * See LICENSE file or go to https://github.com/jongpie/NebulaFramework for full license details. * *************************************************************************************************/ + +/** +* +* @group Utils +* +* @description TODO +* +*/ public without sharing class UUID { public static Boolean isEmpty(String uuid) { diff --git a/src/objects/NebulaRepositorySettings__c.object b/src/objects/NebulaRepositorySettings__c.object deleted file mode 100644 index efca705..0000000 --- a/src/objects/NebulaRepositorySettings__c.object +++ /dev/null @@ -1,46 +0,0 @@ - - - Hierarchy - Controls the behavior of the class SObjectRepository.cls - false - - IncludeCommonFields__c - true - If enabled, the following fields are auto-added to your queries if they exist on the SObject - -Id -CaseNumber -CreatedById -CreatedDate -IsClosed -LastModifiedById -LastModifiedDate -Name -OwnerId -ParentId -Subject -RecordTypeId -SystemModStamp - false - If enabled, the following fields are auto-added to your queries if they exist on the SObject - -Id -CaseNumber -CreatedById -CreatedDate -IsClosed -LastModifiedById -LastModifiedDate -Name -OwnerId -ParentId -Subject -RecordTypeId -SystemModStamp - - false - Checkbox - - - Public - diff --git a/src/objects/NebulaSObjectQueryBuilderSettings__c.object b/src/objects/NebulaSObjectQueryBuilderSettings__c.object new file mode 100644 index 0000000..89ff491 --- /dev/null +++ b/src/objects/NebulaSObjectQueryBuilderSettings__c.object @@ -0,0 +1,15 @@ + + + Hierarchy + false + + IncludeCommonFields__c + true + false + + false + Checkbox + + + Public + diff --git a/src/package.xml b/src/package.xml index 190e200..aceaaa3 100644 --- a/src/package.xml +++ b/src/package.xml @@ -12,7 +12,7 @@ NebulaLog__c NebulaLoggerSettings__c NebulaRecordTypesSettings__c - NebulaRepositorySettings__c + NebulaSObjectQueryBuilderSettings__c NebulaTriggerHandlerSettings__c CustomObject diff --git a/tools/apexdoc/apexdoc.bat b/tools/apexdoc/apexdoc.bat new file mode 100644 index 0000000..f30e9f4 --- /dev/null +++ b/tools/apexdoc/apexdoc.bat @@ -0,0 +1 @@ +java -jar apexdoc.jar -s ..\..\src\classes -t ..\.. -p public;protected -g https://github.com/jongpie/NebulaFramework -a header.html \ No newline at end of file diff --git a/tools/apexdoc/apexdoc.jar b/tools/apexdoc/apexdoc.jar new file mode 100644 index 0000000..5e5159b Binary files /dev/null and b/tools/apexdoc/apexdoc.jar differ diff --git a/tools/apexdoc/header.html b/tools/apexdoc/header.html new file mode 100644 index 0000000..9e21d7f --- /dev/null +++ b/tools/apexdoc/header.html @@ -0,0 +1 @@ +testing \ No newline at end of file diff --git a/tools/codeclimate/apex/security.xml b/tools/codeclimate/apex/security.xml index 446812d..dcc901b 100644 --- a/tools/codeclimate/apex/security.xml +++ b/tools/codeclimate/apex/security.xml @@ -7,7 +7,7 @@ These rules deal with different security problems that can occur within Apex. - @@ -23,7 +23,7 @@ public without sharing class Foo { } ]]> - + --> - + The rule validates you are checking for access permissions before a SOQL/SOSL/DML operation. -Since Apex runs in system mode not having proper permissions checks results in escalation of +Since Apex runs in system mode not having proper permissions checks results in escalation of privilege and may produce runtime errors. This check forces you to handle such scenarios. 3 @@ -227,7 +227,7 @@ public class Foo { ]]> - + - +