From 2948ab27ab9fcaac368bd696e692acf083582e9d Mon Sep 17 00:00:00 2001 From: Jonathan Gillespie Date: Tue, 29 Aug 2017 14:55:12 +0200 Subject: [PATCH] Release 2.0 QueryBuilder.cls split into 3 new query builder classes - SObjectQueryBuilder (standard SOQL queries, AggregateResultQueryBuilder (aggregate SOQL queries) and SearchQueryBuilder (SOSL queries) * QueryBuilder.cls is now an abstract class, used by the 3 types of query builders for shared logic * Added QueryField.cls - parses an SObjectField or list of SObjectFields into the SOQL/SOSL string version. This can be used as a field in your 'SELECT' statement, as a field for QueryFilter, and in the 'ORDER BY' statement * Added QueryDate.cls - this represents date functions for date & datetime fields, like 'CALENDAR_MONTH(CreatedDate)'. QueryDates can be used in QueryFilter, as an aggregate result field, and in the 'ORDER BY' statement - DML methods now return a list of corresponding database result types - Began adding ApexDoc to classes --- .atom-build.yml | 2 + src/classes/AggregateResultQueryBuilder.cls | 188 ++++++++ ... AggregateResultQueryBuilder.cls-meta.xml} | 0 .../AggregateResultQueryBuilder_Tests.cls | 31 ++ ...gateResultQueryBuilder_Tests.cls-meta.xml} | 0 src/classes/CollectionUtils.cls | 63 +++ src/classes/CollectionUtils_Tests.cls | 31 ++ src/classes/DML.cls | 73 +-- src/classes/DMLMock.cls | 57 ++- src/classes/Environment.cls | 21 +- src/classes/Environment_Tests.cls | 6 + src/classes/IAggregateResultQueryBuilder.cls | 57 +++ ...IAggregateResultQueryBuilder.cls-meta.xml} | 0 src/classes/IDML.cls | 32 +- src/classes/INebulaCore.cls | 8 + src/classes/IQueryArgumentFormatter.cls | 8 + src/classes/IQueryBuilder.cls | 31 -- src/classes/IQueryField.cls | 17 + ....cls-meta.xml => IQueryField.cls-meta.xml} | 0 src/classes/IQueryFilter.cls | 20 +- src/classes/ISObjectQueryBuilder.cls | 51 +++ src/classes/ISObjectQueryBuilder.cls-meta.xml | 5 + src/classes/ISObjectRecordTypes.cls | 16 + src/classes/ISObjectRepository.cls | 14 +- src/classes/ISObjectTriggerHandler.cls | 8 + src/classes/ISearchQueryBuilder.cls | 10 + src/classes/ISearchQueryBuilder.cls-meta.xml | 5 + src/classes/Logger.cls | 12 +- src/classes/NebulaCore.cls | 10 +- src/classes/NebulaSettings.cls | 30 +- src/classes/NebulaSettings_Tests.cls | 14 +- src/classes/QueryArgumentFormatter.cls | 25 +- src/classes/QueryArgumentFormatter_Tests.cls | 12 + src/classes/QueryBuilder.cls | 242 +++------- src/classes/QueryBuilder_Tests.cls | 47 -- src/classes/QueryDate.cls | 97 ++++ src/classes/QueryDate.cls-meta.xml | 5 + src/classes/QueryDateLiteral.cls | 14 +- src/classes/QueryDateLiteral_Tests.cls | 2 +- src/classes/QueryDate_Tests.cls | 92 ++++ src/classes/QueryDate_Tests.cls-meta.xml | 5 + src/classes/QueryField.cls | 55 +++ src/classes/QueryField.cls-meta.xml | 5 + src/classes/QueryField_Tests.cls | 29 ++ src/classes/QueryField_Tests.cls-meta.xml | 5 + src/classes/QueryFilter.cls | 146 ++++-- src/classes/QueryFilterScope.cls | 11 +- src/classes/QueryFilter_Tests.cls | 81 ++-- src/classes/QueryNullSortOrder.cls | 8 + src/classes/QueryOperator.cls | 13 +- src/classes/QuerySearchGroup.cls | 8 + src/classes/QuerySortOrder.cls | 8 + src/classes/SObjectFieldDescriber.cls | 22 +- src/classes/SObjectFieldDescriber_Tests.cls | 18 +- src/classes/SObjectQueryBuilder.cls | 261 +++++++++++ src/classes/SObjectQueryBuilder.cls-meta.xml | 5 + src/classes/SObjectQueryBuilder_Tests.cls | 416 ++++++++++++++++++ .../SObjectQueryBuilder_Tests.cls-meta.xml | 5 + src/classes/SObjectRecordTypes.cls | 30 +- src/classes/SObjectRecordTypes_Tests.cls | 30 +- src/classes/SObjectRepository.cls | 70 ++- src/classes/SObjectRepositoryMocks.cls | 29 +- src/classes/SObjectRepositoryMocks_Tests.cls | 10 +- src/classes/SObjectRepository_Tests.cls | 59 ++- src/classes/SObjectTriggerHandler.cls | 21 +- src/classes/SObjectTypeDescriber.cls | 50 --- src/classes/SObjectTypeDescriber_Tests.cls | 72 --- src/classes/Scheduler.cls | 10 + src/classes/Scheduler_Tests.cls | 24 +- src/classes/SearchQueryBuilder.cls | 67 +++ src/classes/SearchQueryBuilder.cls-meta.xml | 5 + src/classes/TestingUtils.cls | 8 + src/classes/UUID.cls | 8 + .../NebulaRepositorySettings__c.object | 46 -- ...ebulaSObjectQueryBuilderSettings__c.object | 15 + src/package.xml | 2 +- tools/apexdoc/apexdoc.bat | 1 + tools/apexdoc/apexdoc.jar | Bin 0 -> 131327 bytes tools/apexdoc/header.html | 1 + tools/codeclimate/apex/security.xml | 12 +- 80 files changed, 2283 insertions(+), 744 deletions(-) create mode 100644 .atom-build.yml create mode 100644 src/classes/AggregateResultQueryBuilder.cls rename src/classes/{IQueryBuilder.cls-meta.xml => AggregateResultQueryBuilder.cls-meta.xml} (100%) create mode 100644 src/classes/AggregateResultQueryBuilder_Tests.cls rename src/classes/{QueryBuilder_Tests.cls-meta.xml => AggregateResultQueryBuilder_Tests.cls-meta.xml} (100%) create mode 100644 src/classes/IAggregateResultQueryBuilder.cls rename src/classes/{SObjectTypeDescriber.cls-meta.xml => IAggregateResultQueryBuilder.cls-meta.xml} (100%) delete mode 100644 src/classes/IQueryBuilder.cls create mode 100644 src/classes/IQueryField.cls rename src/classes/{SObjectTypeDescriber_Tests.cls-meta.xml => IQueryField.cls-meta.xml} (100%) create mode 100644 src/classes/ISObjectQueryBuilder.cls create mode 100644 src/classes/ISObjectQueryBuilder.cls-meta.xml create mode 100644 src/classes/ISearchQueryBuilder.cls create mode 100644 src/classes/ISearchQueryBuilder.cls-meta.xml delete mode 100644 src/classes/QueryBuilder_Tests.cls create mode 100644 src/classes/QueryDate.cls create mode 100644 src/classes/QueryDate.cls-meta.xml create mode 100644 src/classes/QueryDate_Tests.cls create mode 100644 src/classes/QueryDate_Tests.cls-meta.xml create mode 100644 src/classes/QueryField.cls create mode 100644 src/classes/QueryField.cls-meta.xml create mode 100644 src/classes/QueryField_Tests.cls create mode 100644 src/classes/QueryField_Tests.cls-meta.xml create mode 100644 src/classes/SObjectQueryBuilder.cls create mode 100644 src/classes/SObjectQueryBuilder.cls-meta.xml create mode 100644 src/classes/SObjectQueryBuilder_Tests.cls create mode 100644 src/classes/SObjectQueryBuilder_Tests.cls-meta.xml delete mode 100644 src/classes/SObjectTypeDescriber.cls delete mode 100644 src/classes/SObjectTypeDescriber_Tests.cls create mode 100644 src/classes/SearchQueryBuilder.cls create mode 100644 src/classes/SearchQueryBuilder.cls-meta.xml delete mode 100644 src/objects/NebulaRepositorySettings__c.object create mode 100644 src/objects/NebulaSObjectQueryBuilderSettings__c.object create mode 100644 tools/apexdoc/apexdoc.bat create mode 100644 tools/apexdoc/apexdoc.jar create mode 100644 tools/apexdoc/header.html 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 0000000000000000000000000000000000000000..5e5159b042ac0e5094d39e4a8c08506e1591fd40 GIT binary patch literal 131327 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

8ZApoHL^ZY;DKyxNNXGaUOA0C6A0iBtJ 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 COr56y literal 0 HcmV?d00001 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 { ]]> - + - +