diff --git a/.travis.yml b/.travis.yml index 020e8397..22f74c66 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,4 @@ node_js: install: - npm install -g jsforce-metadata-tools script: - - jsforce-deploy --checkOnly -u $DEPLOYMENT_USERNAME -p $DEPLOYMENT_PASSWORD$DEPLOYMENT_TOKEN -D $TRAVIS_BUILD_DIR/src -l $DEPLOYMENT_LOGIN_URL --rollbackOnError true --testLevel $DEPLOYMENT_TEST_LEVEL --pollTimeout $POLL_TIMEOUT --pollInterval $POLL_INTERVAL--verbose + - jsforce-deploy --checkOnly -u $DEPLOYMENT_USERNAME -p $DEPLOYMENT_PASSWORD$DEPLOYMENT_TOKEN -D $TRAVIS_BUILD_DIR/src -l $DEPLOYMENT_LOGIN_URL --rollbackOnError true --testLevel $DEPLOYMENT_TEST_LEVEL --pollTimeout $POLL_TIMEOUT --pollInterval $POLL_INTERVAL--verbose \ No newline at end of file diff --git a/README.md b/README.md index d8a4eab4..8da03f7a 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,16 @@ # Apex Query Generator - + Deploy to Salesforce -## Issue ---Coming soon-- +## Overview +This is an freestanding version of the [Nebula framework's](https://github.com/jongpie/NebulaFramework/) query engine - it has been updated to remove any dependencies on the rest of the Nebula framework. -## Goals -The overall goal of the project is to help auto-generate dynamic SOQL for commonly used queries -* Provide a structure to centralise frequently used queries for each SObject -* Provide a configurable way to change the query fields, while still preventing accidental deletion of fields being used -* Provide a way to create a WHERE statement as a string in Apex, while still preventing accidental deletion of fields being used - -## Implementation ---Coming soon-- - -### Example Implementation: LeadQueryRepository.cls \ No newline at end of file +## Features +The overall goal of the project is to generate dynamic SOQL & SOSL queries. Features currently include +* Leverage field-level security to dynamically include fields +* Dynamically include filter conditions (not possible with standard SOQL/SOSL) +* Retain Salesforce's compilation-time errors for invalid fields while still taking advantage of dynamic queries - this helps avoid issues with deleting fields, misspelled field names, etc that can occur when working with strings and dynamic queries +* Support for nearly all SOQL & SOSL features & keywords, including date literals, aggregate results and more +* Easy-to-use query caching \ No newline at end of file diff --git a/src/classes/AccountRepository_Tests.cls b/src/classes/AccountRepository_Tests.cls deleted file mode 100644 index 603ae30d..00000000 --- a/src/classes/AccountRepository_Tests.cls +++ /dev/null @@ -1,118 +0,0 @@ -@isTest -public class AccountRepository_Tests { - @testSetup - static void setupData() { - List accounts = new List(); - for(Integer i =0; i <3; i++) { - Account account = new Account(); - account.FirstName = 'George' + i; - account.LastName = 'Washington'; - - accounts.add(account); - } - - insert accounts; - } - - @isTest - static void it_should_return_an_account_by_id() { - //Given I have a known account Id - //When I query for that record in particular - //Then it should be returned - Account account = [SELECT Id FROM Account LIMIT 1]; - - Test.startTest(); - Account returnedAccount = new AccountRepository().getById(account.Id); - Test.stopTest(); - - System.assertEquals(account.Id,returnedAccount.Id); - } - - @isTest - static void it_should_return_accounts_by_id_list() { - //Given that I have known accounts - //When I query for them by Id - //Then the accounts should be returned - - List expectedAccounts = [SELECT Id FROM Account]; - List expectedAccountIds = new List(new Map(expectedAccounts).keySet()); - - Test.startTest(); - Map returnedAccountsMap = new Map(new AccountRepository().getById(expectedAccountIds)); - Test.stopTest(); - - System.assertEquals(expectedAccounts.size(),returnedAccountsMap.size()); - } - - @isTest - static void it_should_return_accounts_for_a_given_time_period() { - //Given that I have accounts - //When I query for them with a given field and time range - //Then only accounts that match both those criteria should be returned - List expectedAccounts = [SELECT Id FROM Account]; - - //Now create an account that should not be returned. - Account account = TestDataGenerator.createPersonAccount(); - account.CreatedDate = System.today().addDays(-1); - insert account; - - Test.startTest(); - Schema.SObjectField source = Schema.Account.AccountSource; - Map returnedAccountsMap = new Map(new AccountRepository().getByFieldAndTypeForGivenTimePeriod(source, 'Web', new DateLiterals().TODAY)); - Test.stopTest(); - - System.assertEquals(expectedAccounts.size(),returnedAccountsMap.size()); - for(Account acc : returnedAccountsMap.values()) { - System.assertNotEquals(account.Id,acc.Id); - } - } - - @isTest - static void it_should_return_accounts_by_field_for_a_set_of_ids() { - //Given I have a set of account Ids - //When I query for those accounts and a specific field - //Then the matching accounts should be returned - List expectedAccounts = [SELECT Id FROM Account]; - Set accountIds = new Set(new List(new Map(expectedAccounts).keySet())); - - Test.startTest(); - Schema.SObjectField source = Schema.Account.AccountSource; - List returnedAccounts = new AccountRepository().getByFieldForIds(source,'Web',accountIds); - Test.stopTest(); - - System.assertEquals(expectedAccounts.size(),returnedAccounts.size()); - } - - @isTest - static void it_should_return_accounts_by_field_for_a_list_of_ids() { - //Given I have a list of account Ids - //When I query for those accounts and a specific field - //Then the matching accounts should be returned - List expectedAccounts = [SELECT Id FROM Account]; - List accountIds = new List(new Map(expectedAccounts).keySet()); - - Test.startTest(); - Schema.SObjectField source = Schema.Account.AccountSource; - List returnedAccounts = new AccountRepository().getByFieldForIds(source,'Web',accountIds); - Test.stopTest(); - - System.assertEquals(expectedAccounts.size(),returnedAccounts.size()); - } - - @isTest - static void it_should_return_accounts_that_match_sosl_search_term() { - //Given that I have a string - //When I search accounts for that string - //Then the accounts with a matching string should be returned - - List expectedAccounts = (List)[FIND 'Web' IN ALL FIELDS RETURNING Account][0]; - - Test.startTest(); - Map returnedAccountsMap = new Map(new AccountRepository().searchInAllFields('Web')); - Test.stopTest(); - - for(Account account : expectedAccounts) { - System.assert(returnedAccountsMap.containsKey(account.Id)); - } - } -} \ No newline at end of file diff --git a/src/classes/CollectionUtils.cls b/src/classes/CollectionUtils.cls new file mode 100644 index 00000000..213be765 --- /dev/null +++ b/src/classes/CollectionUtils.cls @@ -0,0 +1,108 @@ +/************************************************************************************************* +* 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); + } + + public static Boolean isList(Object input) { + // If we can cast the object to a list of objects, then it's a list + try { + Object convertedInput = (List)input; + return true; + } catch(System.TypeException ex) { + return false; + } + } + + public static Boolean isSet(Object input) { + // We can't cast the object to a set of objects + // But if we try to cast it to a list of objects & it's a set, + // then a TypeException is thrown so we know it's a set + try { + Object convertedInput = (List)input; + return false; + } catch(System.TypeException ex) { + return ex.getMessage().contains('Set<'); + } + } + + public static Boolean isMap(Object input) { + // We can't cast the object to a map of objects + // But if we try to cast it to a list of objects & it's a map, + // then a TypeException is thrown so we know it's a map + try { + Object convertedInput = (List)input; + return false; + } catch(System.TypeException ex) { + return ex.getMessage().contains('Map<'); + } + } + +} \ No newline at end of file diff --git a/src/classes/AccountRepository.cls-meta.xml b/src/classes/CollectionUtils.cls-meta.xml similarity index 80% rename from src/classes/AccountRepository.cls-meta.xml rename to src/classes/CollectionUtils.cls-meta.xml index cbddff8c..94f6f064 100644 --- a/src/classes/AccountRepository.cls-meta.xml +++ b/src/classes/CollectionUtils.cls-meta.xml @@ -1,5 +1,5 @@ - 38.0 + 40.0 Active diff --git a/src/classes/CollectionUtils_Tests.cls b/src/classes/CollectionUtils_Tests.cls new file mode 100644 index 00000000..3e654f8f --- /dev/null +++ b/src/classes/CollectionUtils_Tests.cls @@ -0,0 +1,171 @@ +/************************************************************************************************* +* 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 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() { + List collectionToCheck = new List{'A', 'B', 'C'}; + + System.assertEquals(true, CollectionUtils.isCollection(collectionToCheck)); + System.assertEquals(true, CollectionUtils.isList(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isSet(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isMap(collectionToCheck)); + } + + @isTest + static void it_should_say_that_a_list_of_integers_is_a_list_and_a_collection() { + List collectionToCheck = new List{1, 2, 3}; + + System.assertEquals(true, CollectionUtils.isCollection(collectionToCheck)); + System.assertEquals(true, CollectionUtils.isList(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isSet(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isMap(collectionToCheck)); + } + + @isTest + static void it_should_say_that_a_list_of_users_is_a_list_and_a_collection() { + List collectionToCheck = [SELECT Id FROM User LIMIT 10]; + + System.assertEquals(true, CollectionUtils.isCollection(collectionToCheck)); + System.assertEquals(true, CollectionUtils.isList(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isSet(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isMap(collectionToCheck)); + } + + // Tests for sets + @isTest + static void it_should_say_that_a_set_of_strings_is_a_set_and_a_collection() { + Set collectionToCheck = new Set{'A', 'B', 'C'}; + + System.assertEquals(true, CollectionUtils.isCollection(collectionToCheck)); + System.assertEquals(true, CollectionUtils.isSet(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isList(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isMap(collectionToCheck)); + } + + @isTest + static void it_should_say_that_a_set_of_integers_is_a_set_and_a_collection() { + Set collectionToCheck = new Set{1, 2, 3}; + + System.assertEquals(true, CollectionUtils.isCollection(collectionToCheck)); + System.assertEquals(true, CollectionUtils.isSet(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isList(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isMap(collectionToCheck)); + } + + @isTest + static void it_should_say_that_a_set_of_users_is_a_set_and_a_collection() { + Set collectionToCheck = new Set([SELECT Id FROM User LIMIT 10]); + + System.assertEquals(true, CollectionUtils.isCollection(collectionToCheck)); + System.assertEquals(true, CollectionUtils.isSet(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isList(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isMap(collectionToCheck)); + } + + // Tests for maps + @isTest + static void it_should_say_that_a_map_of_strings_is_a_map_and_a_collection() { + Map collectionToCheck = new Map{ + 'First' => 1, + 'Second' => 2, + 'Third' => 3 + }; + + System.assertEquals(true, CollectionUtils.isCollection(collectionToCheck)); + System.assertEquals(true, CollectionUtils.isMap(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isList(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isSet(collectionToCheck)); + } + + @isTest + static void it_should_say_that_a_map_of_integers_is_a_map_and_a_collection() { + Map collectionToCheck = new Map{ + 1 => 'First', + 2 => 'Second', + 3 => 'Third' + }; + + System.assertEquals(true, CollectionUtils.isCollection(collectionToCheck)); + System.assertEquals(true, CollectionUtils.isMap(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isList(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isSet(collectionToCheck)); + } + + @isTest + static void it_should_say_that_a_map_of_users_is_a_map_and_a_collection() { + Map collectionToCheck = new Map([SELECT Id FROM User LIMIT 10]); + + System.assertEquals(true, CollectionUtils.isCollection(collectionToCheck)); + System.assertEquals(true, CollectionUtils.isMap(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isList(collectionToCheck)); + System.assertEquals(false, CollectionUtils.isSet(collectionToCheck)); + } + + // Negative tests + @isTest + static void it_should_say_that_a_string_is_not_collection() { + String valueToCheck = 'test string'; + + System.assertEquals(false, CollectionUtils.isCollection(valueToCheck)); + System.assertEquals(false, CollectionUtils.isList(valueToCheck)); + System.assertEquals(false, CollectionUtils.isSet(valueToCheck)); + System.assertEquals(false, CollectionUtils.isMap(valueToCheck)); + } + + @isTest + static void it_should_say_that_an_integer_is_not_collection() { + Integer valueToCheck = 1; + + System.assertEquals(false, CollectionUtils.isCollection(valueToCheck)); + System.assertEquals(false, CollectionUtils.isList(valueToCheck)); + System.assertEquals(false, CollectionUtils.isSet(valueToCheck)); + System.assertEquals(false, CollectionUtils.isMap(valueToCheck)); + } + + @isTest + static void it_should_say_that_a_user_is_not_collection() { + User valueToCheck = [SELECT Id FROM User WHERE Id = :UserInfo.getUserId()]; + + System.assertEquals(false, CollectionUtils.isCollection(valueToCheck)); + System.assertEquals(false, CollectionUtils.isList(valueToCheck)); + System.assertEquals(false, CollectionUtils.isSet(valueToCheck)); + System.assertEquals(false, CollectionUtils.isMap(valueToCheck)); + } + +} \ No newline at end of file diff --git a/src/classes/DateLiterals_UnitTests.cls-meta.xml b/src/classes/CollectionUtils_Tests.cls-meta.xml similarity index 80% rename from src/classes/DateLiterals_UnitTests.cls-meta.xml rename to src/classes/CollectionUtils_Tests.cls-meta.xml index cbddff8c..94f6f064 100644 --- a/src/classes/DateLiterals_UnitTests.cls-meta.xml +++ b/src/classes/CollectionUtils_Tests.cls-meta.xml @@ -1,5 +1,5 @@ - 38.0 + 40.0 Active diff --git a/src/classes/DateLiterals.cls b/src/classes/DateLiterals.cls deleted file mode 100644 index 4134b331..00000000 --- a/src/classes/DateLiterals.cls +++ /dev/null @@ -1,178 +0,0 @@ -public class DateLiterals { - private final String LAST_N = 'LAST_N_{0}: {1}'; - private final String NEXT_N = 'NEXT_N_{0}: {1}'; - - private final String DAYS = 'DAYS'; - private final String WEEKS = 'WEEKS'; - private final String MONTHS = 'MONTHS'; - private final String QUARTERS = 'QUARTERS'; - private final String FISCAL_QUARTERS = 'FISCAL_QUARTERS'; - private final String YEARS = 'YEARS'; - private final String FISCAL_YEARS = 'FISCAL_YEARS'; - - public String value {get; private set;} - - //Actual constant literals - public DateLiterals YESTERDAY { - get {return this.setValue('YESTERDAY');} - } - - public DateLiterals TODAY { - get {return this.setValue('TODAY');} - } - - public DateLiterals TOMORROW { - get {return this.setValue('TOMORROW');} - } - - public DateLiterals LAST_WEEK { - get {return this.setValue('LAST_WEEK');} - } - - public DateLiterals THIS_WEEK { - get {return this.setValue('THIS_WEEK');} - } - - public DateLiterals NEXT_WEEK { - get {return this.setValue('NEXT_WEEK');} - } - - public DateLiterals LAST_MONTH { - get {return this.setValue('LAST_MONTH');} - } - - public DateLiterals THIS_MONTH { - get {return this.setValue('THIS_MONTH');} - } - - public DateLiterals NEXT_MONTH { - get {return this.setValue('NEXT_MONTH');} - } - - public DateLiterals LAST_90_DAYS { - get {return this.setValue('LAST_90_DAYS');} - } - - public DateLiterals NEXT_90_DAYS { - get {return this.setValue('NEXT_90_DAYS');} - } - - public DateLiterals THIS_QUARTER { - get {return this.setValue('THIS_QUARTER');} - } - - public DateLiterals THIS_FISCAL_QUARTER { - get {return this.setValue('THIS_FISCAL_QUARTER');} - } - - public DateLiterals LAST_QUARTER { - get {return this.setValue('LAST_QUARTER');} - } - - public DateLiterals LAST_FISCAL_QUARTER { - get {return this.setValue('LAST_FISCAL_QUARTER');} - } - - public DateLiterals NEXT_QUARTER { - get {return this.setValue('NEXT_QUARTER');} - } - - public DateLiterals NEXT_FISCAL_QUARTER { - get {return this.setValue('NEXT_FISCAL_QUARTER');} - } - - public DateLiterals THIS_YEAR { - get {return this.setValue('THIS_YEAR');} - } - - public DateLiterals THIS_FISCAL_YEAR { - get {return this.setValue('THIS_FISCAL_YEAR');} - } - - public DateLiterals LAST_YEAR { - get {return this.setValue('LAST_YEAR');} - } - - public DateLiterals LAST_FISCAL_YEAR { - get {return this.setValue('LAST_FISCAL_YEAR');} - } - - public DateLiterals NEXT_YEAR { - get {return this.setValue('NEXT_YEAR');} - } - - public DateLiterals NEXT_FISCAL_YEAR { - get {return this.setValue('NEXT_FISCAL_YEAR');} - } - - ////Buildable literals - public DateLiterals LAST_N_DAYS(Integer num) { - String parsedValue = String.format(LAST_N, new List{DAYS,String.valueOf(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals LAST_N_WEEKS(Integer num) { - String parsedValue = String.format(LAST_N, new List{WEEKS, String.valueof(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals LAST_N_MONTHS(Integer num) { - String parsedValue = String.format(LAST_N, new List{MONTHS, String.valueOf(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals LAST_N_QUARTERS(Integer num) { - String parsedValue = String.format(LAST_N, new List{QUARTERS, String.valueOf(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals LAST_N_YEARS(Integer num) { - String parsedValue = String.format(LAST_N, new List{YEARS, String.valueOf(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals LAST_N_FISCAL_YEARS(Integer num) { - String parsedValue = String.format(LAST_N, new List{FISCAL_YEARS, String.valueOf(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals NEXT_N_DAYS(Integer num) { - String parsedValue = String.format(NEXT_N, new List{DAYS, String.valueOf(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals NEXT_N_WEEKS(Integer num) { - String parsedValue = String.format(NEXT_N, new List{WEEKS, String.valueOf(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals NEXT_N_MONTHS(Integer num) { - String parsedValue = String.format(NEXT_N, new List{MONTHS, String.valueOf(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals NEXT_N_QUARTERS(Integer num) { - String parsedValue = String.format(NEXT_N, new List{QUARTERS, String.valueOf(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals NEXT_N_FISCAL_QUARTERS(Integer num) { - String parsedValue = String.format(NEXT_N, new List{FISCAL_QUARTERS, String.valueOf(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals NEXT_N_YEARS(Integer num) { - String parsedValue = String.format(NEXT_N, new List{YEARS, String.valueOf(num)}); - return this.setValue(parsedValue); - } - - public DateLiterals NEXT_N_FISCAL_YEARS(Integer num) { - String parsedValue = String.format(NEXT_N, new List{FISCAL_YEARS, String.valueOf(num)}); - return this.setValue(parsedValue); - } - - private DateLiterals setValue(String value) { - this.value = value; - return this; - } -} \ No newline at end of file diff --git a/src/classes/DateLiterals_UnitTests.cls b/src/classes/DateLiterals_UnitTests.cls deleted file mode 100644 index d78831de..00000000 --- a/src/classes/DateLiterals_UnitTests.cls +++ /dev/null @@ -1,220 +0,0 @@ -@isTest -public class DateLiterals_UnitTests { - private static integer n_number = 5; - - @isTest - static void it_should_return_yesterday_string() { - DateLiterals dateLiteral = new DateLiterals().YESTERDAY; - System.assertEquals('YESTERDAY', dateLiteral.value); - } - - @isTest - static void it_should_return_today_string() { - DateLiterals dateLiteral = new DateLiterals().TODAY; - System.assertEquals('TODAY', dateLiteral.value); - } - - @isTest - static void it_should_return_tomorrow_string() { - DateLiterals dateLiteral = new DateLiterals().TOMORROW; - System.assertEquals('TOMORROW', dateLiteral.value); - } - - @isTest - static void it_should_return_last_week_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_WEEK; - System.assertEquals('LAST_WEEK', dateLiteral.value); - } - - @isTest - static void it_should_return_this_week_string() { - DateLiterals dateLiteral = new DateLiterals().THIS_WEEK; - System.assertEquals('THIS_WEEK', dateLiteral.value); - } - - @isTest - static void it_should_return_next_week_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_WEEK; - System.assertEquals('NEXT_WEEK', dateLiteral.value); - } - - @isTest - static void it_should_return_last_month_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_MONTH; - System.assertEquals('LAST_MONTH', dateLiteral.value); - } - - @isTest - static void it_should_return_this_month_string() { - DateLiterals dateLiteral = new DateLiterals().THIS_MONTH; - System.assertEquals('THIS_MONTH', dateLiteral.value); - } - - @isTest - static void it_should_return_next_month_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_MONTH; - System.assertEquals('NEXT_MONTH', dateLiteral.value); - } - - @isTest - static void it_should_return_last_ninety_days_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_90_DAYS; - System.assertEquals('LAST_90_DAYS', dateLiteral.value); - } - - @isTest - static void it_should_return_next_ninety_days_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_90_DAYS; - System.assertEquals('NEXT_90_DAYS', dateLiteral.value); - } - - @isTest - static void it_should_return_this_quarter_string() { - DateLiterals dateLiteral = new DateLiterals().THIS_QUARTER; - System.assertEquals('THIS_QUARTER', dateLiteral.value); - } - - @isTest - static void it_should_return_last_quarter_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_QUARTER; - System.assertEquals('LAST_QUARTER', dateLiteral.value); - } - - @isTest - static void it_should_return_last_fiscal_quarter_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_FISCAL_QUARTER; - System.assertEquals('LAST_FISCAL_QUARTER', dateLiteral.value); - } - - @isTest - static void it_should_return_this_fiscal_quarter_string() { - DateLiterals dateLiteral = new DateLiterals().THIS_FISCAL_QUARTER; - System.assertEquals('THIS_FISCAL_QUARTER', dateLiteral.value); - } - - @isTest - static void it_should_return_next_quarter_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_QUARTER; - System.assertEquals('NEXT_QUARTER', dateLiteral.value); - } - - @isTest - static void it_should_return_next_fiscal_quarter_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_FISCAL_QUARTER; - System.assertEquals('NEXT_FISCAL_QUARTER', dateLiteral.value); - } - - @isTest - static void it_should_return_this_year_string() { - DateLiterals dateLiteral = new DateLiterals().THIS_YEAR; - System.assertEquals('THIS_YEAR', dateLiteral.value); - } - - @isTest - static void it_should_return_this_fiscal_year_string() { - DateLiterals dateLiteral = new DateLiterals().THIS_FISCAL_YEAR; - System.assertEquals('THIS_FISCAL_YEAR', dateLiteral.value); - } - - @isTest - static void it_should_return_last_year_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_YEAR; - System.assertEquals('LAST_YEAR', dateLiteral.value); - } - - @isTest - static void it_should_return_last_fiscal_year_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_FISCAL_YEAR; - System.assertEquals('LAST_FISCAL_YEAR', dateLiteral.value); - } - - @isTest - static void it_should_return_next_year_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_YEAR; - System.assertEquals('NEXT_YEAR', dateLiteral.value); - } - - @isTest - static void it_should_return_next_fiscal_year_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_FISCAL_YEAR; - System.assertEquals('NEXT_FISCAL_YEAR', dateLiteral.value); - } - - @isTest - static void it_should_return_last_n_days_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_N_DAYS(n_number); - System.assertEquals('LAST_N_DAYS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_last_n_weeks_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_N_WEEKS(n_number); - System.assertEquals('LAST_N_WEEKS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_last_n_months_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_N_MONTHS(n_number); - System.assertEquals('LAST_N_MONTHS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_last_n_quarters_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_N_QUARTERS(n_number); - System.assertEquals('LAST_N_QUARTERS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_last_n_years_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_N_YEARS(n_number); - System.assertEquals('LAST_N_YEARS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_last_n_fiscal_years_string() { - DateLiterals dateLiteral = new DateLiterals().LAST_N_FISCAL_YEARS(n_number); - System.assertEquals('LAST_N_FISCAL_YEARS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_next_n_days_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_N_DAYS(n_number); - System.assertEquals('NEXT_N_DAYS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_next_n_weeks_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_N_WEEKS(n_number); - System.assertEquals('NEXT_N_WEEKS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_next_n_months_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_N_MONTHS(n_number); - System.assertEquals('NEXT_N_MONTHS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_next_n_quarters_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_N_QUARTERS(n_number); - System.assertEquals('NEXT_N_QUARTERS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_next_n_fiscal_quarters_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_N_FISCAL_QUARTERS(n_number); - System.assertEquals('NEXT_N_FISCAL_QUARTERS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_next_n_years_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_N_YEARS(n_number); - System.assertEquals('NEXT_N_YEARS: ' + n_number, dateLiteral.value); - } - - @isTest - static void it_should_return_next_n_fiscal_years_string() { - DateLiterals dateLiteral = new DateLiterals().NEXT_N_FISCAL_YEARS(n_number); - System.assertEquals('NEXT_N_FISCAL_YEARS: ' + n_number, dateLiteral.value); - } -} \ No newline at end of file diff --git a/src/classes/IQueryArgumentFormatter.cls b/src/classes/IQueryArgumentFormatter.cls new file mode 100644 index 00000000..be402c78 --- /dev/null +++ b/src/classes/IQueryArgumentFormatter.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 IQueryArgumentFormatter { + + String getValue(); + +} \ No newline at end of file diff --git a/src/classes/ISObjectRepository.cls-meta.xml b/src/classes/IQueryArgumentFormatter.cls-meta.xml similarity index 80% rename from src/classes/ISObjectRepository.cls-meta.xml rename to src/classes/IQueryArgumentFormatter.cls-meta.xml index cbddff8c..94f6f064 100644 --- a/src/classes/ISObjectRepository.cls-meta.xml +++ b/src/classes/IQueryArgumentFormatter.cls-meta.xml @@ -1,5 +1,5 @@ - 38.0 + 40.0 Active diff --git a/src/classes/IQueryField.cls b/src/classes/IQueryField.cls new file mode 100644 index 00000000..71ed982c --- /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/DateLiterals.cls-meta.xml b/src/classes/IQueryField.cls-meta.xml similarity index 80% rename from src/classes/DateLiterals.cls-meta.xml rename to src/classes/IQueryField.cls-meta.xml index cbddff8c..94f6f064 100644 --- a/src/classes/DateLiterals.cls-meta.xml +++ b/src/classes/IQueryField.cls-meta.xml @@ -1,5 +1,5 @@ - 38.0 + 40.0 Active diff --git a/src/classes/IQueryFilter.cls b/src/classes/IQueryFilter.cls new file mode 100644 index 00000000..7df35808 --- /dev/null +++ b/src/classes/IQueryFilter.cls @@ -0,0 +1,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. * +*************************************************************************************************/ + +/** +* +* @group Query Builder +* +*/ +public interface IQueryFilter { + + // 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/IQueryFilter.cls-meta.xml b/src/classes/IQueryFilter.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/IQueryFilter.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/ISObjectQueryBuilder.cls b/src/classes/ISObjectQueryBuilder.cls new file mode 100644 index 00000000..d6a86e5a --- /dev/null +++ b/src/classes/ISObjectQueryBuilder.cls @@ -0,0 +1,56 @@ +/************************************************************************************************* +* 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 +* +*/ +public interface ISObjectQueryBuilder { + + ISObjectQueryBuilder cacheResults(); + + // Field methods + ISObjectQueryBuilder addAllFields(); + ISObjectQueryBuilder addAllStandardFields(); + ISObjectQueryBuilder addAllCustomFields(); + ISObjectQueryBuilder addAllReadableFields(); + ISObjectQueryBuilder addAllEditableFields(); + ISObjectQueryBuilder addFields(List queryFields); + ISObjectQueryBuilder addFields(Schema.FieldSet fieldSet); + ISObjectQueryBuilder excludeFields(List queryFields); + ISObjectQueryBuilder excludeFields(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 offset(Integer numberOfRowsToSkip); + ISObjectQueryBuilder forReference(); + ISObjectQueryBuilder forUpdate(); + ISObjectQueryBuilder forView(); + 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 00000000..94f6f064 --- /dev/null +++ b/src/classes/ISObjectQueryBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/ISObjectRepository.cls b/src/classes/ISObjectRepository.cls deleted file mode 100644 index 50b85bfc..00000000 --- a/src/classes/ISObjectRepository.cls +++ /dev/null @@ -1,8 +0,0 @@ -public interface ISObjectRepository { - // SOQL - SObject getById(Id recordId); - List getById(List recordIdList); - List getByFieldAndTypeForGivenTimePeriod(Schema.SObjectField field, String operator, Object value); - // SOSL - List searchInAllFields(String searchTerm); -} \ No newline at end of file diff --git a/src/classes/ISearchQueryBuilder.cls b/src/classes/ISearchQueryBuilder.cls new file mode 100644 index 00000000..61369836 --- /dev/null +++ b/src/classes/ISearchQueryBuilder.cls @@ -0,0 +1,14 @@ +public interface ISearchQueryBuilder { + + ISearchQueryBuilder cacheResults(); + + ISearchQueryBuilder inQuerySearchGroup(QuerySearchGroup searchGroup); + ISearchQueryBuilder withHighlight(Boolean withHighlight); + ISearchQueryBuilder withSpellCorrection(Boolean withSpellCorrection); + + 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 00000000..94f6f064 --- /dev/null +++ b/src/classes/ISearchQueryBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/LeadRepository.cls b/src/classes/LeadRepository.cls deleted file mode 100644 index d68aced1..00000000 --- a/src/classes/LeadRepository.cls +++ /dev/null @@ -1,69 +0,0 @@ -public without sharing class LeadRepository extends SObjectRepository { - - private static final Schema.FieldSet DEFAULT_FIELD_SET = SObjectType.Lead.FieldSets.MyFieldSet; - - public LeadRepository() { - super(LeadRepository.DEFAULT_FIELD_SET); - // Any conditions added in the constructor will apply to all queries in this class - this.whereIsConverted(false); - } - - // Overload the constructor if you want to allow other code to specify the field set used - public LeadRepository(Schema.FieldSet fieldSet, Boolean addCommonQueryFields) { - super(fieldSet, addCommonQueryFields); - } - - // ISObjectRepository requires at least 2 methods, getRecord & getList - public Lead getRecord(Id leadId) { - return (Lead)this - .whereIdEquals(leadId) - .setAsUpdate() - .getFirstQueryResult(); - } - - public List getList(List leadIdList) { - return (List)this - .whereIdIn(leadIdList) - .setAsUpdate() - .getQueryResults(); - } - - // Add public methods needed that return the query results - // Only methods that return an SObject or collection of SObjects should be made public - public List getListForSources(List leadSourceList) { - return (List)this - .whereFieldIn(Schema.Lead.LeadSource, leadSourceList) - .orderBy(Schema.Lead.CreatedDate) - .getQueryResults(); - } - - public List getListForStatus(String status, Integer limitCount) { - return (List)this - .whereIsConverted(false) - .whereStatusEquals(status) - .limitCount(limitCount) - .orderBy(Schema.Lead.LastModifiedDate, SObjectRepository.SortOrder.DESCENDING) - .setAsUpdate() - .getQueryResults(); - } - - public List searchInAllFields(String searchTerm) { - return (List)this - .whereIsConverted(false) - .orderBy(Schema.Lead.CreatedDate, SObjectRepository.SortOrder.DESCENDING) - .limitCount(10) - .setAsUpdate() // SOSL cannot use FOR UPDATE. This will execute, but a warning debug statement will indicate that it is ignored - .getSearchResults(searchTerm, SObjectRepository.SearchGroup.ALL_FIELDS); - } - - // You can add additional builder methods for any commonly used filters for this SObject - // All builder methods should be kept as private or protected - private LeadRepository whereIsConverted(Boolean bool) { - return (LeadRepository)this.whereFieldEquals(Schema.Lead.IsConverted, bool); - } - - private LeadRepository whereStatusEquals(String status) { - return (LeadRepository)this.whereFieldEquals(Schema.Lead.Status, status); - } - -} \ No newline at end of file diff --git a/src/classes/LeadRepository.cls-meta.xml b/src/classes/LeadRepository.cls-meta.xml deleted file mode 100644 index cbddff8c..00000000 --- a/src/classes/LeadRepository.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 38.0 - Active - diff --git a/src/classes/LeadRepository_Tests.cls b/src/classes/LeadRepository_Tests.cls deleted file mode 100644 index 293a349f..00000000 --- a/src/classes/LeadRepository_Tests.cls +++ /dev/null @@ -1,101 +0,0 @@ -@isTest -private class LeadRepository_Tests { - - @testSetup - static void setup() { - List leadList = new List(); - for(Integer i = 0; i < 5; i++) { - Lead lead = new Lead( - Company = 'My Test Company', - LastName = 'Gillespie' - ); - leadList.add(lead); - } - insert leadList; - } - - @isTest - static void getRecord() { - Lead expectedLead = [SELECT Id FROM Lead LIMIT 1]; - - Test.startTest(); - - Lead returnedLead = new LeadRepository().getRecord(expectedLead.Id); - System.assertEquals(expectedLead.Id, returnedLead.Id); - - Test.stopTest(); - } - - @isTest - static void getRecord_WHEN_fieldSetIsSpecified() { - Schema.FieldSet expectedFieldSet = SObjectType.Lead.FieldSets.AnotherFieldSet; - Lead expectedLead = [SELECT Id FROM Lead LIMIT 1]; - - Test.startTest(); - - Lead returnedLead = new LeadRepository(expectedFieldSet, false).getRecord(expectedLead.Id); - System.assertEquals(expectedLead.Id, returnedLead.Id); - - Test.stopTest(); - } - - @isTest - static void getList() { - List expectedLeadList = [SELECT Id FROM Lead]; - List expectedLeadIdList = new List(new Map(expectedLeadList).keySet()); - - Test.startTest(); - - List returnedLeadList = new LeadRepository().getList(expectedLeadIdList); - System.assertEquals(expectedLeadList.size(), returnedLeadList.size()); - - Test.stopTest(); - } - - @isTest - static void getListForSources() { - String expectedLeadSource = 'GitHub'; - - List leadList = [SELECT Id, LeadSource FROM Lead LIMIT 2]; - for(Lead lead : leadList) lead.LeadSource = expectedLeadSource; - update leadList; - - Integer leadCountForExpectedLeadSource = [SELECT COUNT() FROM Lead WHERE LeadSource = :expectedLeadSource]; - - Test.startTest(); - - List returnedLeadList = new LeadRepository().getListForSources(new List{expectedLeadSource}); - System.assertEquals(leadCountForExpectedLeadSource, returnedLeadList.size()); - - Test.stopTest(); - } - - @isTest - static void getListForStatus() { - String expectedStatus = [SELECT Status FROM Lead LIMIT 1].Status; - Integer leadCountForExpectedStatus = [SELECT COUNT() FROM Lead WHERE Status = :expectedStatus]; - System.assert(leadCountForExpectedStatus > 0); - Integer limitCount = leadCountForExpectedStatus - 1; - - Test.startTest(); - - List returnedLeadList = new LeadRepository().getListForStatus(expectedStatus, limitCount); - System.assertEquals(limitCount, returnedLeadList.size()); - - Test.stopTest(); - } - - @isTest - static void searchInAllFields() { - String searchTerm = [SELECT LastName FROM Lead WHERE LastName != null LIMIT 1].LastName; - List expectedLeadList = (List)[FIND :searchTerm IN ALL FIELDS RETURNING Lead(Id WHERE IsConverted = false)][0]; - - Test.startTest(); - - List returnedLeadList = new LeadRepository().searchInAllFields(searchTerm); - System.assertEquals(expectedLeadList.size(), returnedLeadList.size()); - - Test.stopTest(); - } - -} \ No newline at end of file diff --git a/src/classes/LeadRepository_Tests.cls-meta.xml b/src/classes/LeadRepository_Tests.cls-meta.xml deleted file mode 100644 index cbddff8c..00000000 --- a/src/classes/LeadRepository_Tests.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 38.0 - Active - diff --git a/src/classes/QueryArgumentFormatter.cls b/src/classes/QueryArgumentFormatter.cls new file mode 100644 index 00000000..41958f54 --- /dev/null +++ b/src/classes/QueryArgumentFormatter.cls @@ -0,0 +1,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 Query Builder +* +* @description TODO +* +*/ +public virtual class QueryArgumentFormatter implements IQueryArgumentFormatter { + + private String value; + + public QueryArgumentFormatter(Object valueToFormat) { + this.value = this.objectToQueryString(valueToFormat); + } + + public virtual String getValue() { + return this.value; + } + + protected virtual String objectToQueryString(Object valueToFormat) { + if(valueToFormat == null) return null; + else if(CollectionUtils.isList(valueToFormat)) return this.listToQueryString((List)valueToFormat); + else if(CollectionUtils.isSet(valueToFormat)) return this.setToQueryString(valueToFormat); + else if(CollectionUtils.isMap(valueToFormat)) return this.mapToQueryString(valueToFormat); + else if(valueToFormat instanceof QueryDateLiteral) { + QueryDateLiteral dateLiteral = (QueryDateLiteral)valueToFormat; + return dateLiteral.getValue(); + } + else if(valueToFormat instanceof Boolean) return String.valueOf((Boolean)valueToFormat); + else if(valueToFormat instanceof Date) return String.valueOf((Date)valueToFormat); + else if(valueToFormat instanceof Datetime) { + Datetime datetimeValue = (Datetime)valueToFormat; + return datetimeValue.format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'Greenwich Mean Time'); + } + else if(valueToFormat instanceof Decimal) return String.valueOf((Decimal)valueToFormat); + else if(valueToFormat instanceof Double) return String.valueOf((Double)valueToFormat); + else if(valueToFormat instanceof Integer) return String.valueOf((Integer)valueToFormat); + else if(valueToFormat instanceof Long) return String.valueOf((Long)valueToFormat); + else if(valueToFormat instanceof SObject) { + SObject record = (SObject)valueToFormat; + return wrapInSingleQuotes(record.Id); + } + else if(valueToFormat instanceof Schema.SObjectType) { + Schema.SObjectType sobjectType = (Schema.SObjectType)valueToFormat; + return wrapInSingleQuotes(sobjectType.getDescribe().getName()); + } + 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); + } + + private String listToQueryString(List valueList) { + List parsedValueList = new List(); + for(Object value : valueList) parsedValueList.add(this.objectToQueryString(value)); + return '(' + String.join(parsedValueList, ', ') + ')'; + } + + private String setToQueryString(Object valueSet) { + String unformattedString = String.valueOf(valueSet).replace('{', '').replace('}', ''); + List parsedValueList = new List(); + for(String collectionItem : unformattedString.split(',')) { + parsedValueList.add(this.objectToQueryString(collectionItem)); + } + + return '(' + String.join(parsedValueList, ', ') + ')'; + } + + private String mapToQueryString(Object valueMap) { + Map m = (Map)JSON.deserializeUntyped(JSON.serialize(valueMap)); + + return this.setToQueryString(m.keySet()); + } + + private String wrapInSingleQuotes(String input) { + input = input.trim(); + + if(input.left(1) != '\'') input = '\'' + input; + if(input.right(1) != '\'') input = input + '\''; + return input; + } + +} \ No newline at end of file diff --git a/src/classes/QueryArgumentFormatter.cls-meta.xml b/src/classes/QueryArgumentFormatter.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryArgumentFormatter.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryArgumentFormatter_Tests.cls b/src/classes/QueryArgumentFormatter_Tests.cls new file mode 100644 index 00000000..949d3abb --- /dev/null +++ b/src/classes/QueryArgumentFormatter_Tests.cls @@ -0,0 +1,202 @@ +/************************************************************************************************* +* 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 QueryArgumentFormatter_Tests { + + @isTest + static void it_should_return_query_string_for_null() { + Object providedValue = null; + String expectedString = null; + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_list() { + List providedValueList = new List{1, 2, 3}; + String expectedString = '(1, 2, 3)'; + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValueList).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_set() { + Set providedValueSet = new Set{'A', 'B', 'C'}; + String expectedString = '(\'A\', \'B\', \'C\')'; + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValueSet).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_map() { + Map providedValueMap = new Map([SELECT Id FROM User LIMIT 10]); + List sortedIdList = new List(providedValueMap.keySet()); + sortedIdList.sort(); + String expectedString = '(\'' + String.join(sortedIdList, '\', \'') + '\')'; + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValueMap).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_query_date_literal() { + QueryDateLiteral providedValue = QueryDateLiteral.YESTERDAY; + String expectedString = providedValue.getValue(); + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_boolean() { + Boolean providedValue = true; + String expectedString = String.valueOf(providedValue); + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_date() { + Date providedValue = System.today(); + String expectedString = String.valueOf(providedValue); + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_datetime() { + Datetime providedValue = System.now(); + String expectedString = providedValue.format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'Greenwich Mean Time'); + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_decimal() { + Decimal providedValue = 1.1; + String expectedString = String.valueOf(providedValue); + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_double() { + Double providedValue = 1.1; + String expectedString = String.valueOf(providedValue); + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_integer() { + Integer providedValue = 10; + String expectedString = String.valueOf(providedValue); + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_long() { + Long providedValue = 1234567890; + String expectedString = String.valueOf(providedValue); + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_sobject() { + User providedValue = [SELECT Id FROM User LIMIT 1]; + String expectedString = '\'' + providedValue.Id + '\''; + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_sobject_type() { + Schema.SObjectType providedValue = Schema.Lead.SObjectType; + String expectedString = '\'' + providedValue.getDescribe().getName() + '\''; + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + System.assertEquals(expectedString, returnedValue); + } + + @isTest + static void it_should_return_query_string_for_string() { + String providedValue = 'test'; + String expectedString = '\'' + providedValue + '\''; + + Test.startTest(); + String returnedValue = new QueryArgumentFormatter(providedValue).getValue(); + Test.stopTest(); + + 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); + } + +} \ No newline at end of file diff --git a/src/classes/QueryArgumentFormatter_Tests.cls-meta.xml b/src/classes/QueryArgumentFormatter_Tests.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryArgumentFormatter_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryBuilder.cls b/src/classes/QueryBuilder.cls new file mode 100644 index 00000000..85356b16 --- /dev/null +++ b/src/classes/QueryBuilder.cls @@ -0,0 +1,146 @@ +/************************************************************************************************* +* 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 Abstract class that provides some shared properties & methods for SObjectQueryBuilder & AggregateResultQueryBuilder +* +*/ +public abstract class QueryBuilder { + + private static Map> cachedQueryResultsByHashCode = new Map>(); + private static Map>> cachedSearchResultsByHashCode = new Map>>(); + + protected List whereClauseList; + protected List orderByList; + protected Integer limitCount; + + protected SObjectType sobjectType; + protected Map sobjectTypeFieldMap; + + private Boolean cacheResults; + + public QueryBuilder() { + this.whereClauseList = new List(); + this.orderByList = new List(); + this.cacheResults = false; + } + + protected void doCacheResults() { + this.cacheResults = true; + } + + protected void doFilterBy(IQueryFilter queryFilter) { + this.doFilterBy(new List{queryFilter}); + } + + protected void doFilterBy(List queryFilters) { + if(queryFilters == null) return; + + for(IQueryFilter queryFilter : queryFilters) this.whereClauseList.add(queryFilter.getValue()); + } + + protected void doOrderBy(IQueryField orderByQueryField) { + this.doOrderBy(orderByQueryField, null, null); + } + + protected void doOrderBy(IQueryField orderByQueryField, QuerySortOrder sortOrder) { + this.doOrderBy(orderByQueryField, sortOrder, null); + } + + 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) sortOrderString += ' NULLS ' + nullsSortOrder; + + this.orderByList.add(orderByQueryField.getValue() + sortOrderString); + } + + protected void doLimitCount(Integer limitCount) { + this.limitCount = limitCount; + } + + protected String doGetWhereClauseString() { + if(this.whereClauseList.isEmpty()) return ''; + + // Dedupe + this.whereClauseList = new List(new Set(this.whereClauseList)); + + this.whereClauseList.sort(); + return '\nWHERE ' + String.join(this.whereClauseList, '\nAND '); + } + + protected String doGetOrderByString() { + if(this.orderByList.isEmpty()) return ''; + + // Dedupe + this.orderByList = new List(new Set(this.orderByList)); + + this.orderByList.sort(); + return '\nORDER BY ' + String.join(new List(orderByList), ', '); + } + + protected String doGetLimitCountString() { + return this.limitCount != null ? '\nLIMIT '+ this.limitCount : ''; + } + + protected List doGetQueryResults(String query) { + if(this.cacheResults) return this.getCachedQuery(query); + else return this.executeQuery(query); + } + + protected List> doGetSearchResults(String query) { + if(this.cacheResults) return this.getCachedSearch(query); + else return this.executeSearch(query); + } + + private void filterByWithSeparator(List queryFilters, String separator) { + if(queryFilters == null) return; + + List queryFilterValues = new List(); + for(IQueryFilter queryFilter : queryFilters) queryFilterValues.add(queryFilter.getValue()); + + String orStatement = '(' + String.join(queryFilterValues, ' ' + separator + ' ') + ')'; + this.whereClauseList.add(orStatement); + } + + private List getCachedQuery(String query) { + Integer hashCode = query.hashCode(); + + Boolean isCached = cachedQueryResultsByHashCode.containsKey(hashCode); + if(!isCached) cachedQueryResultsByHashCode.put(hashCode, this.executeQuery(query)); + + // Always return a deep clone so the original cached version is never modified + return cachedQueryResultsByHashCode.get(hashCode).deepClone(true, true, true); + } + + private List executeQuery(String query) { + List results = Database.query(query); + return results; + } + + private List> getCachedSearch(String query) { + Integer hashCode = query.hashCode(); + + Boolean isCached = cachedSearchResultsByHashCode.containsKey(hashCode); + if(!isCached) cachedSearchResultsByHashCode.put(hashCode, this.executeSearch(query)); + + // Always return a deep clone so the original cached version is never modified + List> cachedResults = cachedSearchResultsByHashCode.get(hashCode); + List> deepClonedResults = new List>(); + for(List cachedListOfResults : cachedResults) deepClonedResults.add(cachedListOfResults.deepClone(true, true, true)); + return deepClonedResults; + } + + private List> executeSearch(String query) { + List> results = Search.query(query); + return results; + } + +} \ No newline at end of file diff --git a/src/classes/QueryBuilder.cls-meta.xml b/src/classes/QueryBuilder.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryDate.cls b/src/classes/QueryDate.cls new file mode 100644 index 00000000..94b5c2c8 --- /dev/null +++ b/src/classes/QueryDate.cls @@ -0,0 +1,93 @@ +/************************************************************************************************* +* 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 { + + private Schema.SObjectField sobjectField; + private Schema.SObjectType sobjectType; + private String value; + + 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 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryDate.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryDateLiteral.cls b/src/classes/QueryDateLiteral.cls new file mode 100644 index 00000000..1eae5e38 --- /dev/null +++ b/src/classes/QueryDateLiteral.cls @@ -0,0 +1,123 @@ +/************************************************************************************************* +* 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 +* +*/ +public without sharing class QueryDateLiteral { + + private String value; + + public String getValue() { + return this.value; + } + + private QueryDateLiteral(String value) { + this.value = value; + } + + // Actual constant literals + public static final QueryDateLiteral YESTERDAY = new QueryDateLiteral('YESTERDAY'); + public static final QueryDateLiteral TODAY = new QueryDateLiteral('TODAY'); + public static final QueryDateLiteral TOMORROW = new QueryDateLiteral('TOMORROW'); + public static final QueryDateLiteral LAST_WEEK = new QueryDateLiteral('LAST_WEEK'); + public static final QueryDateLiteral THIS_WEEK = new QueryDateLiteral('THIS_WEEK'); + public static final QueryDateLiteral NEXT_WEEK = new QueryDateLiteral('NEXT_WEEK'); + public static final QueryDateLiteral LAST_MONTH = new QueryDateLiteral('LAST_MONTH'); + public static final QueryDateLiteral THIS_MONTH = new QueryDateLiteral('THIS_MONTH'); + public static final QueryDateLiteral NEXT_MONTH = new QueryDateLiteral('NEXT_MONTH'); + public static final QueryDateLiteral LAST_90_DAYS = new QueryDateLiteral('LAST_90_DAYS'); + public static final QueryDateLiteral NEXT_90_DAYS = new QueryDateLiteral('NEXT_90_DAYS'); + public static final QueryDateLiteral LAST_QUARTER = new QueryDateLiteral('LAST_QUARTER'); + public static final QueryDateLiteral THIS_QUARTER = new QueryDateLiteral('THIS_QUARTER'); + public static final QueryDateLiteral NEXT_QUARTER = new QueryDateLiteral('NEXT_QUARTER'); + public static final QueryDateLiteral LAST_FISCAL_QUARTER = new QueryDateLiteral('LAST_FISCAL_QUARTER'); + public static final QueryDateLiteral THIS_FISCAL_QUARTER = new QueryDateLiteral('THIS_FISCAL_QUARTER'); + public static final QueryDateLiteral NEXT_FISCAL_QUARTER = new QueryDateLiteral('NEXT_FISCAL_QUARTER'); + public static final QueryDateLiteral LAST_YEAR = new QueryDateLiteral('LAST_YEAR'); + public static final QueryDateLiteral THIS_YEAR = new QueryDateLiteral('THIS_YEAR'); + public static final QueryDateLiteral NEXT_YEAR = new QueryDateLiteral('NEXT_YEAR'); + public static final QueryDateLiteral LAST_FISCAL_YEAR = new QueryDateLiteral('LAST_FISCAL_YEAR'); + public static final QueryDateLiteral THIS_FISCAL_YEAR = new QueryDateLiteral('THIS_FISCAL_YEAR'); + public static final QueryDateLiteral NEXT_FISCAL_YEAR = new QueryDateLiteral('NEXT_FISCAL_YEAR'); + + private static final String LAST_N = 'LAST_N_{0}:{1}'; + private static final String NEXT_N = 'NEXT_N_{0}:{1}'; + private static final String N_DAYS_AGO = 'N_DAYS_AGO:{0}'; + + private static final String DAYS = 'DAYS'; + private static final String WEEKS = 'WEEKS'; + private static final String MONTHS = 'MONTHS'; + private static final String QUARTERS = 'QUARTERS'; + private static final String YEARS = 'YEARS'; + private static final String FISCAL_QUARTERS = 'FISCAL_QUARTERS'; + private static final String FISCAL_YEARS = 'FISCAL_YEARS'; + + // Buildable literals + public static QueryDateLiteral N_DAYS_AGO(Integer num) { + String parsedValue = String.format(N_DAYS_AGO, new List{String.valueOf(num)}); + return new QueryDateLiteral(parsedValue); + } + + public static QueryDateLiteral LAST_N_DAYS(Integer num) { + return buildQueryDateLiteral(LAST_N, DAYS, num); + } + + public static QueryDateLiteral LAST_N_WEEKS(Integer num) { + return buildQueryDateLiteral(LAST_N, WEEKS, num); + } + + public static QueryDateLiteral LAST_N_MONTHS(Integer num) { + return buildQueryDateLiteral(LAST_N, MONTHS, num); + } + + public static QueryDateLiteral LAST_N_QUARTERS(Integer num) { + return buildQueryDateLiteral(LAST_N, QUARTERS, num); + } + + public static QueryDateLiteral LAST_N_YEARS(Integer num) { + return buildQueryDateLiteral(LAST_N, YEARS, num); + } + + public static QueryDateLiteral LAST_N_FISCAL_YEARS(Integer num) { + return buildQueryDateLiteral(LAST_N, FISCAL_YEARS, num); + } + + public static QueryDateLiteral NEXT_N_DAYS(Integer num) { + return buildQueryDateLiteral(NEXT_N, DAYS, num); + } + + public static QueryDateLiteral NEXT_N_WEEKS(Integer num) { + return buildQueryDateLiteral(NEXT_N, WEEKS, num); + } + + public static QueryDateLiteral NEXT_N_MONTHS(Integer num) { + return buildQueryDateLiteral(NEXT_N, MONTHS, num); + } + + public static QueryDateLiteral NEXT_N_QUARTERS(Integer num) { + return buildQueryDateLiteral(NEXT_N, QUARTERS, num); + } + + public static QueryDateLiteral NEXT_N_FISCAL_QUARTERS(Integer num) { + return buildQueryDateLiteral(NEXT_N, FISCAL_QUARTERS, num); + } + + public static QueryDateLiteral NEXT_N_YEARS(Integer num) { + return buildQueryDateLiteral(NEXT_N, YEARS, num); + } + + public static QueryDateLiteral NEXT_N_FISCAL_YEARS(Integer num) { + return buildQueryDateLiteral(NEXT_N, FISCAL_YEARS, num); + } + + private static QueryDateLiteral buildQueryDateLiteral(String base, String period, Integer num) { + String parsedValue = String.format(base, new List{period, String.valueOf(num)}); + return new QueryDateLiteral(parsedValue); + } + +} \ No newline at end of file diff --git a/src/classes/QueryDateLiteral.cls-meta.xml b/src/classes/QueryDateLiteral.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryDateLiteral.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryDateLiteral_Tests.cls b/src/classes/QueryDateLiteral_Tests.cls new file mode 100644 index 00000000..e06ea6c1 --- /dev/null +++ b/src/classes/QueryDateLiteral_Tests.cls @@ -0,0 +1,232 @@ +/************************************************************************************************* +* 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 QueryDateLiteral_Tests { + + private static Integer offsetNumber = 5; + + @isTest + static void it_should_return_yesterday_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.YESTERDAY; + System.assertEquals('YESTERDAY', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_today_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.TODAY; + System.assertEquals('TODAY', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_tomorrow_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.TOMORROW; + System.assertEquals('TOMORROW', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_week_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_WEEK; + System.assertEquals('LAST_WEEK', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_this_week_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.THIS_WEEK; + System.assertEquals('THIS_WEEK', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_week_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_WEEK; + System.assertEquals('NEXT_WEEK', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_month_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_MONTH; + System.assertEquals('LAST_MONTH', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_this_month_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.THIS_MONTH; + System.assertEquals('THIS_MONTH', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_month_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_MONTH; + System.assertEquals('NEXT_MONTH', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_ninety_days_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_90_DAYS; + System.assertEquals('LAST_90_DAYS', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_ninety_days_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_90_DAYS; + System.assertEquals('NEXT_90_DAYS', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_this_quarter_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.THIS_QUARTER; + System.assertEquals('THIS_QUARTER', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_quarter_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_QUARTER; + System.assertEquals('LAST_QUARTER', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_fiscal_quarter_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_FISCAL_QUARTER; + System.assertEquals('LAST_FISCAL_QUARTER', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_this_fiscal_quarter_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.THIS_FISCAL_QUARTER; + System.assertEquals('THIS_FISCAL_QUARTER', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_quarter_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_QUARTER; + System.assertEquals('NEXT_QUARTER', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_fiscal_quarter_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_FISCAL_QUARTER; + System.assertEquals('NEXT_FISCAL_QUARTER', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_this_year_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.THIS_YEAR; + System.assertEquals('THIS_YEAR', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_this_fiscal_year_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.THIS_FISCAL_YEAR; + System.assertEquals('THIS_FISCAL_YEAR', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_year_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_YEAR; + System.assertEquals('LAST_YEAR', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_fiscal_year_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_FISCAL_YEAR; + System.assertEquals('LAST_FISCAL_YEAR', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_year_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_YEAR; + System.assertEquals('NEXT_YEAR', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_fiscal_year_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_FISCAL_YEAR; + System.assertEquals('NEXT_FISCAL_YEAR', dateLiteral.getValue()); + } + + @isTest + static void it_should_return_n_days_ago_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.N_DAYS_AGO(offsetNumber); + System.assertEquals('N_DAYS_AGO:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_n_days_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_N_DAYS(offsetNumber); + System.assertEquals('LAST_N_DAYS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_n_weeks_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_N_WEEKS(offsetNumber); + System.assertEquals('LAST_N_WEEKS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_n_months_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_N_MONTHS(offsetNumber); + System.assertEquals('LAST_N_MONTHS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_n_quarters_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_N_QUARTERS(offsetNumber); + System.assertEquals('LAST_N_QUARTERS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_n_years_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_N_YEARS(offsetNumber); + System.assertEquals('LAST_N_YEARS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_last_n_fiscal_years_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.LAST_N_FISCAL_YEARS(offsetNumber); + System.assertEquals('LAST_N_FISCAL_YEARS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_n_days_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_N_DAYS(offsetNumber); + System.assertEquals('NEXT_N_DAYS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_n_weeks_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_N_WEEKS(offsetNumber); + System.assertEquals('NEXT_N_WEEKS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_n_months_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_N_MONTHS(offsetNumber); + System.assertEquals('NEXT_N_MONTHS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_n_quarters_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_N_QUARTERS(offsetNumber); + System.assertEquals('NEXT_N_QUARTERS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_n_fiscal_quarters_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_N_FISCAL_QUARTERS(offsetNumber); + System.assertEquals('NEXT_N_FISCAL_QUARTERS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_n_years_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_N_YEARS(offsetNumber); + System.assertEquals('NEXT_N_YEARS:' + offsetNumber, dateLiteral.getValue()); + } + + @isTest + static void it_should_return_next_n_fiscal_years_string() { + QueryDateLiteral dateLiteral = QueryDateLiteral.NEXT_N_FISCAL_YEARS(offsetNumber); + System.assertEquals('NEXT_N_FISCAL_YEARS:' + offsetNumber, dateLiteral.getValue()); + } + +} \ No newline at end of file diff --git a/src/classes/QueryDateLiteral_Tests.cls-meta.xml b/src/classes/QueryDateLiteral_Tests.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryDateLiteral_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryDate_Tests.cls b/src/classes/QueryDate_Tests.cls new file mode 100644 index 00000000..d6c4fbfd --- /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 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryDate_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryField.cls b/src/classes/QueryField.cls new file mode 100644 index 00000000..2743e96e --- /dev/null +++ b/src/classes/QueryField.cls @@ -0,0 +1,56 @@ +/************************************************************************************************* +* 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 implements IQueryField, Comparable { + + private final List fields; + private final String value; + + public QueryField(SObjectField field) { + this(new List{field}); + } + + public QueryField(List fields) { + this.fields = fields; + this.value = this.parseFields(); + } + + 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; + } + + public String getValue() { + return this.value; + } + + public Schema.SObjectType getSObjectType() { + return new SObjectFieldDescriber(this.fields[0]).getSObjectType(); + } + + 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(); + } + +} \ 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 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryField.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryField_Tests.cls b/src/classes/QueryField_Tests.cls new file mode 100644 index 00000000..a5cbe5aa --- /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 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryField_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryFilter.cls b/src/classes/QueryFilter.cls new file mode 100644 index 00000000..23cb7f36 --- /dev/null +++ b/src/classes/QueryFilter.cls @@ -0,0 +1,134 @@ +/************************************************************************************************* +* 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 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 implements IQueryFilter, Comparable { + + private String value; + + /** + * @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; + } + + /** + * @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; + } + + /** + * @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); + } + + /** + * @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); + } + + /** + * @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.value; + } + + 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/QueryFilter.cls-meta.xml b/src/classes/QueryFilter.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryFilter.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryFilterScope.cls b/src/classes/QueryFilterScope.cls new file mode 100644 index 00000000..55dc3da6 --- /dev/null +++ b/src/classes/QueryFilterScope.cls @@ -0,0 +1,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 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/QueryFilterScope.cls-meta.xml b/src/classes/QueryFilterScope.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryFilterScope.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryFilter_Tests.cls b/src/classes/QueryFilter_Tests.cls new file mode 100644 index 00000000..c87958a8 --- /dev/null +++ b/src/classes/QueryFilter_Tests.cls @@ -0,0 +1,95 @@ +/************************************************************************************************* +* 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 QueryFilter_Tests { + + @isTest + static void it_should_return_the_query_filter_for_a_field() { + Schema.SObjectField sobjectField = Schema.User.CompanyName; + QueryOperator operator = QueryOperator.IS_IN; + List providedValues = new List{'derp', 'herp'}; + + Test.startTest(); + QueryFilter queryFilter = (QueryFilter)new QueryFilter().filterByField(new QueryField(sobjectField), operator, providedValues); + Test.stopTest(); + + 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() { + 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 = (QueryFilter)new QueryFilter().filterByField(parentFieldToFilter, operator, providedValue); + Test.stopTest(); + + 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() { + 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()); + } + + @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 = (QueryFilter)new QueryFilter().filterBySubquery(operator, lookupFieldOnRelatedSObject); + Test.stopTest(); + + 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 = 'OwnerId ' + operator.getValue() + ' (SELECT ' + lookupFieldOnRelatedSObject.getDescribe().getName() + ' FROM User)'; + System.assertEquals(expectedQueryFilter, queryFilter.getValue()); + } + +} \ No newline at end of file diff --git a/src/classes/QueryFilter_Tests.cls-meta.xml b/src/classes/QueryFilter_Tests.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryFilter_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryNullSortOrder.cls b/src/classes/QueryNullSortOrder.cls new file mode 100644 index 00000000..42855b8f --- /dev/null +++ b/src/classes/QueryNullSortOrder.cls @@ -0,0 +1,11 @@ +/************************************************************************************************* +* 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 +* +*/ +public enum QueryNullSortOrder { FIRST, LAST } \ No newline at end of file diff --git a/src/classes/QueryNullSortOrder.cls-meta.xml b/src/classes/QueryNullSortOrder.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryNullSortOrder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryOperator.cls b/src/classes/QueryOperator.cls new file mode 100644 index 00000000..3dcec1f3 --- /dev/null +++ b/src/classes/QueryOperator.cls @@ -0,0 +1,39 @@ +/************************************************************************************************* +* 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 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 { + + private String value; + + private QueryOperator(String value) { + this.value = value; + } + + public String getValue() { + return this.value; + } + + public static final QueryOperator EQUALS = new QueryOperator('='); + public static final QueryOperator NOT_EQUAL_TO = new QueryOperator('!='); + public static final QueryOperator GREATER_THAN = new QueryOperator('>'); + public static final QueryOperator GREATER_THAN_OR_EQUAL_TO = new QueryOperator('>='); + public static final QueryOperator LESS_THAN = new QueryOperator('<'); + public static final QueryOperator LESS_THAN_OR_EQUAL_TO = new QueryOperator('<='); + public static final QueryOperator IS_IN = new QueryOperator('IN'); + public static final QueryOperator IS_NOT_IN = new QueryOperator('NOT IN'); + public static final QueryOperator INCLUDES = new QueryOperator('INCLUDES'); + public static final QueryOperator EXCLUDES = new QueryOperator('EXCLUDES'); + public static final QueryOperator IS_LIKE = new QueryOperator('LIKE'); + public static final QueryOperator IS_NOT_LIKE = new QueryOperator('NOT LIKE'); + +} \ No newline at end of file diff --git a/src/classes/QueryOperator.cls-meta.xml b/src/classes/QueryOperator.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryOperator.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QueryOperator_Tests.cls b/src/classes/QueryOperator_Tests.cls new file mode 100644 index 00000000..1645beb6 --- /dev/null +++ b/src/classes/QueryOperator_Tests.cls @@ -0,0 +1,68 @@ +/************************************************************************************************* +* 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 QueryOperator_Tests { + + @isTest + static void it_should_return_EQUALS_string() { + System.assertEquals('=', QueryOperator.EQUALS.getValue()); + } + + @isTest + static void it_should_return_NOT_EQUAL_TO_string() { + System.assertEquals('!=', QueryOperator.NOT_EQUAL_TO.getValue()); + } + + @isTest + static void it_should_return_GREATER_THAN_string() { + System.assertEquals('>', QueryOperator.GREATER_THAN.getValue()); + } + + @isTest + static void it_should_return_GREATER_THAN_OR_EQUAL_TO_string() { + System.assertEquals('>=', QueryOperator.GREATER_THAN_OR_EQUAL_TO.getValue()); + } + + @isTest + static void it_should_return_LESS_THAN_string() { + System.assertEquals('<', QueryOperator.LESS_THAN.getValue()); + } + + @isTest + static void it_should_return_LESS_THAN_OR_EQUAL_TO_string() { + System.assertEquals('<=', QueryOperator.LESS_THAN_OR_EQUAL_TO.getValue()); + } + + @isTest + static void it_should_return_IS_IN_string() { + System.assertEquals('IN', QueryOperator.IS_IN.getValue()); + } + + @isTest + static void it_should_return_IS_NOT_IN_string() { + System.assertEquals('NOT IN', QueryOperator.IS_NOT_IN.getValue()); + } + + @isTest + static void it_should_return_INCLUDES_string() { + System.assertEquals('INCLUDES', QueryOperator.INCLUDES.getValue()); + } + + @isTest + static void it_should_return_EXCLUDES_string() { + System.assertEquals('EXCLUDES', QueryOperator.EXCLUDES.getValue()); + } + + @isTest + static void it_should_return_IS_LIKE_string() { + System.assertEquals('LIKE', QueryOperator.IS_LIKE.getValue()); + } + + @isTest + static void it_should_return_IS_NOT_LIKE_string() { + System.assertEquals('NOT LIKE', QueryOperator.IS_NOT_LIKE.getValue()); + } + +} \ No newline at end of file diff --git a/src/classes/QueryOperator_Tests.cls-meta.xml b/src/classes/QueryOperator_Tests.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QueryOperator_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QuerySearchGroup.cls b/src/classes/QuerySearchGroup.cls new file mode 100644 index 00000000..519e33af --- /dev/null +++ b/src/classes/QuerySearchGroup.cls @@ -0,0 +1,11 @@ +/************************************************************************************************* +* 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 +* +*/ +public enum QuerySearchGroup { ALL_FIELDS, NAME_FIELDS, EMAIL_FIELDS, PHONE_FIELDS, SIDEBAR_FIELDS } \ No newline at end of file diff --git a/src/classes/QuerySearchGroup.cls-meta.xml b/src/classes/QuerySearchGroup.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QuerySearchGroup.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/QuerySortOrder.cls b/src/classes/QuerySortOrder.cls new file mode 100644 index 00000000..0a25d39d --- /dev/null +++ b/src/classes/QuerySortOrder.cls @@ -0,0 +1,11 @@ +/************************************************************************************************* +* 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 +* +*/ +public enum QuerySortOrder { ASCENDING, DESCENDING } \ No newline at end of file diff --git a/src/classes/QuerySortOrder.cls-meta.xml b/src/classes/QuerySortOrder.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/QuerySortOrder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/SOQLUtils.cls b/src/classes/SOQLUtils.cls deleted file mode 100644 index 48273f39..00000000 --- a/src/classes/SOQLUtils.cls +++ /dev/null @@ -1,41 +0,0 @@ -public without sharing class SOQLUtils { - - public static String toSOQLString(List valueList) { - List parsedValueList = new List(); - for(Object value : valueList) parsedValueList.add(toSOQLString(value)); - return '(' + String.join(parsedValueList, ',') + ')'; - } - - public static String toSOQLString(Object value) { - if(value == null) return null; - else if(value instanceof DateLiterals) { - DateLiterals dateLiteral = (DateLiterals)value; - return dateLiteral.value; - } - else if(value instanceof Boolean) return String.valueOf((Boolean)value); - else if(value instanceof Date) return String.valueOf((Date)value); - else if(value instanceof Datetime) { - Datetime datetimeValue = (Datetime)value; - return datetimeValue.format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'Greenwich Mean Time'); - } - else if(value instanceof Decimal) return String.valueOf((Decimal) value); - else if(value instanceof Double) return String.valueOf((Double) value); - else if(value instanceof Integer) return String.valueOf((Integer) value); - else if(value instanceof Long) return String.valueOf((Long) value); - else if(value instanceof SObject) { - SObject record = (SObject)value; - return wrapInSingleQuotes(record.Id); - } - else if(value instanceof String) return wrapInSingleQuotes((String)value); - else return String.valueOf(value); - } - - public static String wrapInSingleQuotes(String input) { - if(input.toLowerCase() == 'null') return input; - - if(input.left(1) != '\'') input = '\'' + input; - if(input.right(1) != '\'') input = input + '\''; - return input; - } - -} \ No newline at end of file diff --git a/src/classes/SOQLUtils.cls-meta.xml b/src/classes/SOQLUtils.cls-meta.xml deleted file mode 100644 index cbddff8c..00000000 --- a/src/classes/SOQLUtils.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 38.0 - Active - diff --git a/src/classes/SObjectFieldDescriber.cls b/src/classes/SObjectFieldDescriber.cls new file mode 100644 index 00000000..5a56dc44 --- /dev/null +++ b/src/classes/SObjectFieldDescriber.cls @@ -0,0 +1,91 @@ +/************************************************************************************************* +* 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 +* +*/ +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. + // If a proper method is ever added to Apex to get the field's SObject Type, + // then we should consider removing this class. + + private static Map sobjectFieldHashCodeToSObjectTypeMap; + + private Schema.SObjectField sobjectField; + private Schema.SObjectType sobjectType; + + public SObjectFieldDescriber(Schema.SObjectField sobjectField) { + this.cacheSObjectTypeFieldHashCodes(); + + this.sobjectField = sobjectField; + this.setSObjectType(); + } + + public String getFieldName() { + return this.sobjectField.getDescribe().getName(); + } + + public String getFullFieldName() { + return this.sobjectType + '.' + this.getFieldName(); + } + + public Schema.SObjectType getSObjectType() { + return this.sobjectType; + } + + public Schema.SObjectType getParentSObjectType() { + Schema.DescribeFieldResult fieldDescribe = this.SObjectField.getDescribe(); + + Schema.SObjectType parentSObjectType; + if(!fieldDescribe.isNamePointing() && !fieldDescribe.getReferenceTo().isEmpty()) parentSObjectType = fieldDescribe.getReferenceTo()[0]; + + return parentSObjectType; + } + + public List getParentSObjectTypes() { + Schema.DescribeFieldResult fieldDescribe = this.SObjectField.getDescribe(); + + List parentSObjectTypes; + if(!fieldDescribe.isNamePointing() && !fieldDescribe.getReferenceTo().isEmpty()) parentSObjectTypes = fieldDescribe.getReferenceTo(); + + return parentSObjectTypes; + } + + public Boolean validateSObjectType(Schema.SObjectType expectedSObjectType) { + return this.SObjectType == expectedSObjectType; + } + + private void setSObjectType() { + Integer sobjectFieldHashCode = this.getHashCode(this.sobjectField); + this.SObjectType = sobjectFieldHashCodeToSObjectTypeMap.get(sobjectFieldHashCode); + } + + private void cacheSObjectTypeFieldHashCodes() { + // Describe calls are "free" but still add CPU time, so let's cache them + if(sobjectFieldHashCodeToSObjectTypeMap != null) return; + + sobjectFieldHashCodeToSObjectTypeMap = new Map(); + + // Build a map of hash codes for each fieldDescribe taken from Schema Global Describe + Map globalDescribe = Schema.getGlobalDescribe(); + for(String sobjTypeName : globalDescribe.keySet()) { + SObjectType sobjType = globalDescribe.get(sobjTypeName); + + for(Schema.SObjectField sobjField : sobjType.getDescribe().fields.getMap().values()) { + Integer sobjFieldHashCode = getHashCode(sobjField); + + sobjectFieldHashCodeToSObjectTypeMap.put(sobjFieldHashCode, sobjType); + } + } + } + + private Integer getHashCode(Schema.SObjectField sobjField) { + return ((Object)sobjField).hashCode(); + } + +} \ No newline at end of file diff --git a/src/classes/SObjectFieldDescriber.cls-meta.xml b/src/classes/SObjectFieldDescriber.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/SObjectFieldDescriber.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/SObjectFieldDescriber_Tests.cls b/src/classes/SObjectFieldDescriber_Tests.cls new file mode 100644 index 00000000..3117e271 --- /dev/null +++ b/src/classes/SObjectFieldDescriber_Tests.cls @@ -0,0 +1,61 @@ +/************************************************************************************************* +* 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 SObjectFieldDescriber_Tests { + + @isTest + static void it_should_return_the_sobject_type() { + Schema.SObjectType expectedSObjectType = Schema.Lead.SObjectType; + Schema.SObjectField sobjectField = Schema.Lead.Id; + + Test.startTest(); + + SObjectFieldDescriber sobjectFieldDescriber = new SObjectFieldDescriber(sobjectField); + System.assertEquals(expectedSObjectType, sobjectFieldDescriber.getSObjectType()); + + Test.stopTest(); + } + + @isTest + static void it_should_validate_the_expected_sobject_type() { + Schema.SObjectType expectedSObjectType = Schema.Lead.SObjectType; + Schema.SObjectField sobjectField = Schema.Lead.Id; + + Test.startTest(); + + SObjectFieldDescriber sobjectFieldDescriber = new SObjectFieldDescriber(sobjectField); + System.assert(sobjectFieldDescriber.validateSObjectType(expectedSObjectType)); + + Test.stopTest(); + } + + @isTest + static void it_should_return_the_full_field_name() { + Schema.SObjectType expectedSObjectType = Schema.Lead.SObjectType; + Schema.SObjectField sobjectField = Schema.Lead.Id; + String expectedFullFieldName = String.valueOf(expectedSObjectType) + '.' + String.valueOf(sobjectField); + + Test.startTest(); + + SObjectFieldDescriber sobjectFieldDescriber = new SObjectFieldDescriber(sobjectField); + System.assertEquals(expectedFullFieldName, sobjectFieldDescriber.getFullFieldName()); + + Test.stopTest(); + } + + @isTest + static void it_should_return_the_parent_field_sobject_type() { + Schema.SObjectField sobjectField = Schema.Lead.ConvertedAccountId; + Schema.SObjectType expectedParentSObjectType = Schema.Account.SObjectType; + + Test.startTest(); + + SObjectFieldDescriber sobjectFieldDescriber = new SObjectFieldDescriber(sobjectField); + System.assertEquals(expectedParentSObjectType, sobjectFieldDescriber.getParentSObjectType()); + + Test.stopTest(); + } + +} \ No newline at end of file diff --git a/src/classes/SObjectFieldDescriber_Tests.cls-meta.xml b/src/classes/SObjectFieldDescriber_Tests.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/SObjectFieldDescriber_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/SObjectQueryBuilder.cls b/src/classes/SObjectQueryBuilder.cls new file mode 100644 index 00000000..5b4b5c5a --- /dev/null +++ b/src/classes/SObjectQueryBuilder.cls @@ -0,0 +1,350 @@ +/************************************************************************************************* +* 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 Set queryFieldsToExclude; + private List childRelationshipQueries; + private QueryFilterScope filterScope; + private Integer offset; + private Boolean forReference; + private Boolean forUpdate; + private Boolean forView; + 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.queryFieldsToExclude = new Set(); + this.childRelationshipQueries = new List(); + this.forReference = false; + this.forUpdate = false; + this.forView = false; + + this.addCommonQueryFields(); + } + + public ISObjectQueryBuilder cacheResults() { + super.doCacheResults(); + return this; + } + + 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 excludeFields(List queryFields) { + for(IQueryField queryField : queryFields) this.excludeField(queryField); + return this; + } + + public ISObjectQueryBuilder excludeFields(Schema.FieldSet fieldSet) { + for(Schema.FieldSetMember field : fieldSet.getFields()) { + this.excludeField(this.sobjectTypeFieldMap.get(field.getFieldPath())); + } + 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 offset(Integer numberOfRowsToSkip) { + this.offset = numberOfRowsToSkip; + return this; + } + + public ISObjectQueryBuilder forReference() { + this.forReference = true; + return this; + } + + public ISObjectQueryBuilder forUpdate() { + this.forUpdate = true; + return this; + } + + public ISObjectQueryBuilder forView() { + this.forView = 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.getOffSetString() + + this.getForReferenceString() + + this.getForUpdateString() + + this.getForViewString(); + + 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 void excludeField(IQueryField queryField) { + // If it's a parent field, add it immediately + if(queryField.getValue().contains('.')) this.queryFieldsToExclude.add(queryField.getValue()); + // Otherwise, parse it to an SObjectField so we can reused the logic in excludeField(Schema.SObjectField field) + else { + Schema.SObjectField field = this.sobjectType.getDescribe().fields.getMap().get(queryField.getValue()); + this.excludeField(field); + } + } + + private void excludeField(Schema.SObjectField field) { + this.queryFieldsToExclude.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.queryFieldsToExclude.add(this.getParentSObjectNameField(field)); + } + } + + private String getParentSObjectNameField(Schema.SObjectField field) { + String relationshipName = field.getDescribe().getRelationshipName(); + Schema.SObjectType parentSObjectType = field.getDescribe().getReferenceTo()[0]; + + return relationshipName + '.' + this.getSObjectNameField(parentSObjectType); + } + + private String getSObjectNameField(Schema.SObjectType sobjectType) { + String nameField; + for(Schema.SObjectField parentField : sobjectType.getDescribe().fields.getMap().values()) { + if(parentField.getDescribe().isNameField()) { + nameField = parentField.getDescribe().getName(); + break; + } + } + return nameField; + } + + private String getQueryFieldString() { + // Remove any fields to be excluded + this.queryFields.removeAll(this.queryFieldsToExclude); + + // If no fields have been added, then add just the ID & name fields + if(this.queryFields.isEmpty()) { + Schema.SObjectField idField = this.sobjectTypeFieldMap.get('Id'); + this.addField(idField); + + Schema.SObjectField nameField = this.sobjectTypeFieldMap.get(this.getSObjectNameField(this.sobjectType)); + if(nameField != null) this.addField(nameField); + } + + 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 getOffSetString() { + return this.offset == null ? '' : '\nOFFSET ' + this.offset; + } + + private String getForReferenceString() { + return !this.forReference ? '' : '\nFOR REFERENCE'; + } + + private String getForUpdateString() { + return !this.forUpdate ? '' : '\nFOR UPDATE'; + } + + private String getForViewString() { + return !this.forView ? '' : '\nFOR VIEW'; + } + + private String getUsingScopeString() { + return this.filterScope == null ? '' : '\nUSING SCOPE ' + this.filterScope.name(); + } + + private void addCommonQueryFields() { + // 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 00000000..94f6f064 --- /dev/null +++ b/src/classes/SObjectQueryBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/SObjectQueryBuilder_Tests.cls b/src/classes/SObjectQueryBuilder_Tests.cls new file mode 100644 index 00000000..53081a23 --- /dev/null +++ b/src/classes/SObjectQueryBuilder_Tests.cls @@ -0,0 +1,432 @@ +/************************************************************************************************* +* 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_be_usable_after_construction() { + // Query builders should be usable as soon as it's constructed - it should be able to execute a query with some default values + ISObjectQueryBuilder opportunityQueryBuilder = new SObjectQueryBuilder(Schema.Opportunity.SObjectType); + + Test.startTest(); + + List results = (List)opportunityQueryBuilder.getQueryResults(); + + Test.stopTest(); + } + + @isTest + static void it_should_cache_results() { + ISObjectQueryBuilder opportunityQueryBuilder = new SObjectQueryBuilder(Schema.Opportunity.SObjectType); + opportunityQueryBuilder.cacheResults(); + + Test.startTest(); + + System.assertEquals(0, Limits.getQueries()); + for(Integer i = 0; i < 10; i++) { + System.debug(opportunityQueryBuilder.getQueryResults()); + } + + System.assertEquals(1, Limits.getQueries()); + + Test.stopTest(); + } + + @isTest + static void it_should_add_a_list_of_fields() { + 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 + //TODO fix, broken because common fields are always added + //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() { + 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 + //TODO fix, broken because common fields are always added + //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() { + 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 + //TODO fix, broken because common fields are always added + //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() { + 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 + //TODO fix, broken because common fields are always added + //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() { + 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() { + 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 + //TODO fix, broken because common fields are always added + //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.forUpdate(); + 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 00000000..94f6f064 --- /dev/null +++ b/src/classes/SObjectQueryBuilder_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/SObjectRepository.cls b/src/classes/SObjectRepository.cls deleted file mode 100644 index 2db11553..00000000 --- a/src/classes/SObjectRepository.cls +++ /dev/null @@ -1,187 +0,0 @@ -public abstract class SObjectRepository implements ISObjectRepository { - - public enum SortOrder { ASCENDING, DESCENDING } - public enum NullsSortOrder { FIRST, LAST } - - public enum SearchGroup { ALL_FIELDS, NAME_FIELDS, EMAIL_FIELDS, PHONE_FIELDS, SIDEBAR_FIELDS } - - private SObjectType sobjectType; - private Map sobjectTypeFieldMap; - private Set queryFields; - private String query; - private Boolean shouldAddCommonFields; - private Schema.FieldSet fieldSet; - private List whereClauseList; - private List orderByList; - private Integer limitCount; - private Boolean forUpdate; - - protected SObjectRepository(Schema.FieldSet fieldSet) { - this(fieldSet, true); - } - - protected SObjectRepository(Schema.FieldSet fieldSet, Boolean shouldAddCommonFields) { - this.fieldSet = fieldSet; - this.shouldAddCommonFields = shouldAddCommonFields; - - this.sobjectType = fieldSet.getSObjectType(); - this.sobjectTypeFieldMap = this.sobjectType.getDescribe().fields.getMap(); - this.queryFields = new Set(); - this.whereClauseList = new List(); - this.orderByList = new List(); - this.forUpdate = false; - - this.addCommonQueryFields(); - this.addFieldSetMembers(); - } - - protected SObjectRepository whereFieldOperatorEqualsValue(Schema.SObjectField field, String operator, Object value) { - return this.addCondition(field, operator, SOQLUtils.toSOQLString(value)); - } - - protected SObjectRepository whereFieldOperatorEqualsListValues(Schema.SObjectField field, String operator, List values) { - return this.addCondition(field, operator, SOQLUtils.toSOQLString(values)); - } - - protected SObjectRepository orderBy(Schema.SObjectField orderByField) { - return this.orderBy(orderByField, null, null); - } - - protected SObjectRepository orderBy(Schema.SObjectField orderByField, SObjectRepository.SortOrder sortOrder) { - return this.orderBy(orderByField, sortOrder, null); - } - - protected SObjectRepository orderBy(Schema.SObjectField orderByField, SObjectRepository.SortOrder sortOrder, SObjectRepository.NullsSortOrder nullsSortOrder) { - String sortOrderSoql = ''; - if(sortOrder == SObjectRepository.SortOrder.ASCENDING) sortOrderSoql = ' ASC'; - else if(sortOrder == SObjectRepository.SortOrder.DESCENDING) sortOrderSoql = ' DESC'; - - if(nullsSortOrder != null) sortOrderSoql += ' NULLS ' + nullsSortOrder; - - this.orderByList.add(orderByField.getDescribe().getName() + sortOrderSoql); - - return this; - } - - protected SObjectRepository limitCount(Integer limitCount) { - this.limitCount = limitCount; - return this; - } - - protected SObjectRepository setAsUpdate() { - this.forUpdate = true; - return this; - } - - protected SObject getFirstQueryResult() { - return this.getQueryResults()[0]; - } - - protected List getQueryResults() { - return Database.query(this.getQuery()); - } - - protected List getSearchResults(String searchTerm, SObjectRepository.SearchGroup searchGroup) { - return Search.query(this.getSearchQuery(searchTerm, searchGroup))[0]; - } - - - //CRUD - protected void doInsert(SObject record) {doInsert(new List{record});} - protected void doInsert(List records) {Database.insert(records);} - protected void doUpsert(List records) {Database.upsert(records);} - protected void doUpdate(List records) {Database.update(records);} - protected void doDelete(List records) {Database.delete(records);} - - private void addCommonQueryFields() { - if(!this.shouldAddCommonFields) return; - - // Auto-add the common fields that are available for the SObject Type - Set commonFieldNameList = new Set{ - 'Id', 'CaseNumber', 'CreatedById', 'CreatedDate', 'IsClosed', 'LastModifiedById', 'LastModifiedDate', - 'Name', 'OwnerId', 'Subject', 'RecordTypeId', 'SystemModStamp' - }; - - for(String commonFieldName : commonFieldNameList) { - //Verify that the field is available on the object being used - if(!this.sobjectTypeFieldMap.containsKey(commonFieldName)) continue; - - //Salesforce has some inconsistencies in casing for standard field names. We'll go lowercase - //here and in addFieldSetMembers() to ensure the set is unique - this.queryFields.add(commonFieldName.toLowerCase()); - } - } - - private void addFieldSetMembers() { - if(this.fieldSet == null) return; - - for(Schema.FieldSetMember field : this.fieldSet.getFields()) { - //Lowercase here as well to ensure strings added to the set are unique - this.queryFields.add(field.getFieldPath().toLowerCase()); - } - } - - private SObjectRepository addCondition(Schema.SObjectField field, String operator, String value) { - this.whereClauseList.add(field + ' ' + operator.trim() + ' ' + value); - return this; - } - - private String getQueryFieldString() { - return String.join(new List(this.queryFields), ','); - } - - private String getWhereClauseString() { - String whereClauseString = ''; - if(!this.whereClauseList.isEmpty()) whereClauseString = '\nWHERE ' + String.join(this.whereClauseList, '\nAND '); - return whereClauseString; - } - - private String getOrderByString() { - String orderByString = ''; - if(!this.orderByList.isEmpty()) orderByString = '\nORDER BY ' + String.join(new List(orderByList), ', '); - return orderByString; - } - - private String getLimitCountString() { - String limitString = ''; - if(this.limitCount != null) limitString = '\nLIMIT '+ this.limitCount; - return limitString; - } - - private String getForUpdateString() { - String forUpdateString = ''; - if(this.orderByList.isEmpty() && this.forUpdate) forUpdateString = '\nFOR UPDATE'; - return forUpdateString; - } - - private String getQuery() { - this.query = 'SELECT ' + this.getQueryFieldString() - + ' FROM ' + this.sobjectType - + this.getWhereClauseString() - + this.getOrderByString() - + this.getLimitCountString() - + this.getForUpdateString(); - - System.debug(this.query); - - return this.query; - } - - private String getSearchQuery(String searchTerm, SObjectRepository.SearchGroup searchGroup) { - this.query = 'FIND ' + SOQLUtils.toSOQLString(searchTerm) - + ' IN ' + searchGroup.name().replace('_', ' ') - + ' RETURNING ' + this.sobjectType + '(' - + this.getQueryFieldString() - + this.getWhereClauseString() - + this.getOrderByString() - + this.getLimitCountString() - + ')'; - - if(this.forUpdate) System.debug(LoggingLevel.WARN, 'SOSL Search Query method flagged as FOR UPDATE. SOSL cannot use FOR UPDATE, ignoring'); - - System.debug(this.query); - - return this.query; - } - -} \ No newline at end of file diff --git a/src/classes/SObjectRepository.cls-meta.xml b/src/classes/SObjectRepository.cls-meta.xml deleted file mode 100644 index cbddff8c..00000000 --- a/src/classes/SObjectRepository.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 38.0 - Active - diff --git a/src/classes/SearchQueryBuilder.cls b/src/classes/SearchQueryBuilder.cls new file mode 100644 index 00000000..531f9ec9 --- /dev/null +++ b/src/classes/SearchQueryBuilder.cls @@ -0,0 +1,94 @@ +/************************************************************************************************* +* 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; + private Boolean withHighlight; + private Boolean withSpellCorrection; + + 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 cacheResults() { + super.doCacheResults(); + return this; + } + + public ISearchQueryBuilder inQuerySearchGroup(QuerySearchGroup searchGroup) { + this.searchGroup = searchGroup; + return this; + } + + public ISearchQueryBuilder withHighlight(Boolean withHighlight) { + this.withHighlight = withHighlight; + return this; + } + + public ISearchQueryBuilder withSpellCorrection(Boolean withSpellCorrection) { + this.withHighlight = withSpellCorrection; + return this; + } + + public String getQuery() { + String query = 'FIND ' + new QueryArgumentFormatter(this.searchTerm.toLowerCase()).getValue() + + '\nIN ' + this.searchGroup.name().replace('_', ' ') + + '\nRETURNING ' + this.getSObjectQueriesString() + + this.getWithHighlightString() + + this.getWithSpellCorrectionString(); + + 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, ', '); + } + + private String getWithHighlightString() { + return (this.withHighlight == null || !this.withHighlight) ? '' : '\nWITH HIGHLIGHT'; + } + + private String getWithSpellCorrectionString() { + return this.withSpellCorrection == null ? '' : '\nWITH SPELL_CORRECTION = ' + this.withSpellCorrection; + } + +} \ 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 00000000..94f6f064 --- /dev/null +++ b/src/classes/SearchQueryBuilder.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/SearchQueryBuilder_Tests.cls b/src/classes/SearchQueryBuilder_Tests.cls new file mode 100644 index 00000000..b7699fd8 --- /dev/null +++ b/src/classes/SearchQueryBuilder_Tests.cls @@ -0,0 +1,68 @@ +/************************************************************************************************* +* 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 SearchQueryBuilder_Tests { + + @isTest + static void it_should_be_usable_after_construction() { + // Query builders should be usable as soon as it's constructed - it should be able to execute a query with some default values + String searchTerm = 'test'; + ISObjectQueryBuilder opportunityQueryBuilder = new SObjectQueryBuilder(Schema.Opportunity.SObjectType); + + ISearchQueryBuilder searchQueryBuilder = new SearchQueryBuilder(searchTerm, new List{opportunityQueryBuilder}); + + Test.startTest(); + + List> results = (List>)searchQueryBuilder.getSearchResults(); + + Test.stopTest(); + } + + @isTest + static void it_should_cache_results() { + String searchTerm = 'test'; + ISObjectQueryBuilder opportunityQueryBuilder = new SObjectQueryBuilder(Schema.Opportunity.SObjectType); + + ISearchQueryBuilder searchQueryBuilder = new SearchQueryBuilder(searchTerm, new List{opportunityQueryBuilder}); + searchQueryBuilder.cacheResults(); + + Test.startTest(); + + System.assertEquals(0, Limits.getSoslQueries()); + for(Integer i = 0; i < 10; i++) { + System.debug(searchQueryBuilder.getSearchResults()); + } + + System.assertEquals(1, Limits.getSoslQueries()); + + Test.stopTest(); + } + + @isTest + static void it_should_set_search_group() { + // TODO finish writing tests! + } + + @isTest + static void it_should_add_sobject_query_builder() { + // TODO finish writing tests! + } + + @isTest + static void it_should_get_query() { + // TODO finish writing tests! + } + + @isTest + static void it_should_get_first_query_results() { + // TODO finish writing tests! + } + + @isTest + static void it_should_get_query_results() { + // TODO finish writing tests! + } + +} \ No newline at end of file diff --git a/src/classes/SearchQueryBuilder_Tests.cls-meta.xml b/src/classes/SearchQueryBuilder_Tests.cls-meta.xml new file mode 100644 index 00000000..94f6f064 --- /dev/null +++ b/src/classes/SearchQueryBuilder_Tests.cls-meta.xml @@ -0,0 +1,5 @@ + + + 40.0 + Active + diff --git a/src/classes/TaskRepository.cls b/src/classes/TaskRepository.cls deleted file mode 100644 index 480e6f5a..00000000 --- a/src/classes/TaskRepository.cls +++ /dev/null @@ -1,82 +0,0 @@ -public without sharing class TaskRepository extends SObjectRepository { - - private static final Schema.FieldSet DEFAULT_FIELD_SET = SObjectType.Task.FieldSets.MyFieldSet; - - public TaskRepository() { - super(TaskRepository.DEFAULT_FIELD_SET); - } - - //SOQL - public Task getById(Id taskId) { - return (Task)this - .whereFieldOperatorEqualsValue(Schema.Task.Id, '=', taskId) - .setAsUpdate() - .getFirstQueryResult(); - } - - public List getById(List taskIdList) { - return (List)this - .whereFieldOperatorEqualsListValues(Schema.Task.Id, 'IN', taskIdList) - .setAsUpdate() - .getQueryResults(); - } - - public List getByWhoId(List taskWhoIdList) { - return (List)this - .whereFieldOperatorEqualsListValues(Schema.Task.WhoId, 'IN', taskWhoIdList) - .orderBy(Schema.Task.WhoId) - .orderBy(Schema.Task.CreatedDate,SObjectRepository.SortOrder.DESCENDING) - .getQueryResults(); - } - - public List getByWhatId(List taskWhatIdList) { - return (List)this - .whereFieldOperatorEqualsListValues(Schema.Task.WhatId,'IN', taskWhatIdList) - .orderBy(Schema.Task.CreatedDate,SObjectRepository.SortOrder.DESCENDING) - .getQueryResults(); - } - - public List getOpenTasksByWhoId(Id whoId) { - return getOpenTasksByWhoId(new List{whoId}); - } - - public List getOpenTasksByWhoId(List whoIdList) { - return (List)this - .whereFieldOperatorEqualsListValues(Schema.Task.WhoId, 'IN', whoIdList) - .whereFieldOperatorEqualsValue(Schema.Task.IsClosed, '=', false) - .orderBy(Schema.Task.WhoId) - .orderBy(Schema.Task.CreatedDate, SObjectRepository.SortOrder.DESCENDING) - .getQueryResults(); - } - - public List getByFieldForWhoIds(Schema.SObjectField field, String value, List whoIdList) { - return (List)this - .whereFieldOperatorEqualsListValues(Schema.Task.WhoId, 'IN', whoIdList) - .whereFieldOperatorEqualsValue(field, '=', value) - .orderBy(Schema.Task.WhoId) - .orderBy(Schema.Task.CreatedDate, SObjectRepository.SortOrder.DESCENDING) - .getQueryResults(); - } - - public List getCreatedSinceTimeValue(Object timeValue) { - return (List)this - .whereFieldOperatorEqualsValue(Schema.Lead.CreatedDate, '>=', timeValue) - .getQueryResults(); - } - - public List getByFieldAndTypeForGivenTimePeriod(Schema.SObjectField field, String operator, Object value) { - return(List)this - .whereFieldOperatorEqualsValue(field, '=', operator) - .whereFieldOperatorEqualsValue(Schema.Task.CreatedDate, '>=', value) - .getQueryResults(); - } - - //SOSL - public List searchInAllFields(String searchTerm) { - return (List)this - .whereFieldOperatorEqualsValue(Schema.Task.IsClosed, '=', false) - .orderBy(Schema.Task.WhoId) - .limitCount(10) - .getSearchResults(searchTerm, SObjectRepository.SearchGroup.ALL_FIELDS); - } -} \ No newline at end of file diff --git a/src/classes/TaskRepository.cls-meta.xml b/src/classes/TaskRepository.cls-meta.xml deleted file mode 100644 index cbddff8c..00000000 --- a/src/classes/TaskRepository.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 38.0 - Active - diff --git a/src/classes/TaskRepository_Tests.cls b/src/classes/TaskRepository_Tests.cls deleted file mode 100644 index a4cd3d51..00000000 --- a/src/classes/TaskRepository_Tests.cls +++ /dev/null @@ -1,222 +0,0 @@ -@isTest -private class TaskRepository_Tests { - @testSetup - static void setup() { - List leadList = new List(); - for(Integer i = 0; i < 5; i++) { - Lead lead = new Lead( - Company = 'My Test Company', - LastName = 'Gillespie' - ); - leadList.add(lead); - } - insert leadList; - - List taskList = new List(); - for(Lead lead : leadList) { - Task task = new Task( - Description = 'Call about the ' + staticString(), - Status = 'Not Started', - WhoId = lead.Id, - Type__c = TaskTypes.CALL_OUT - ); - - taskList.add(task); - } - insert taskList; - - } - - @isTest - static void it_should_return_specific_tasks_by_Id() { - //Given I know the Id of the task - //When I use getById - //Then that task should be returned - Task expectedTask = [SELECT Id FROM Task LIMIT 1]; - - Test.startTest(); - - Task returnedTask = new TaskRepository().getById(expectedTask.Id); - System.assertEquals(expectedTask.Id, returnedTask.Id); - - Test.stopTest(); - } - - @isTest - static void it_should_return_tasks_by_list() { - //Given that I know task Ids - //When I use getById - //Then those tasks should be returned - List expectedTaskList = [SELECT Id FROM Task]; - List expectedTaskIdList = new List(new Map(expectedTaskList).keySet()); - - Test.startTest(); - - List returnedTaskList = new TaskRepository().getById(expectedTaskIdList); - System.assertEquals(expectedTaskList.size(), returnedTaskList.size()); - - Test.stopTest(); - } - - @isTest - static void it_should_return_all_open_tasks_by_who_Id() { - //Given that I know an account or lead Id - //When I query for all open tasks - //Then only the open tasks should be returned - Lead lead = [SELECT Id FROM Lead LIMIT 1]; - - //Create closed task. We'll verify that this one in particular is NOT returned in the final asserts. - Task closedTask = TestDataGenerator.createCommunicationTask(lead.Id, null); - closedTask.Status = TaskStatuses.COMPLETED; - insert closedTask; - - Map expectedTaskMap = new Map([SELECT Id, WhoId FROM Task WHERE WhoId = :lead.Id AND IsClosed = false]); - System.assert(expectedTaskMap.size() > 0); - - Test.startTest(); - Map returnedTaskMap = new Map(new TaskRepository().getOpenTasksByWhoId(lead.Id)); - Test.stopTest(); - - System.assertEquals(returnedTaskMap.size(), returnedTaskMap.size()); - for(Id expectedTaskId : expectedTaskMap.keySet()) { - System.assert(returnedTaskMap.containsKey(expectedTaskId)); - System.assert(!returnedTaskMap.containsKey(closedTask.Id)); - } - } - - @isTest - static void it_should_return_matching_task_when_sObject_field_is_stipulated() { - //Given that I stipulate a tasks's specific field - //When I query for that specific field - //Then only the tasks matching that field's value should be returned - Lead lead = [SELECT Id FROM Lead LIMIT 1]; - - //Build the expected task map first - we don't want to include the task that's going to be inserted below - Map expectedTaskMap = new Map([SELECT Id, WhoId, Status FROM Task WHERE WhoId = :lead.Id]); - System.assert(expectedTaskMap.size() > 0); - - //Create a task that won't match the value we're querying for that's also associated with the same lead - Task nonMatchingFieldTask = TestDataGenerator.createCommunicationTask(lead.Id, null); - nonMatchingFieldTask.Status = 'Completed'; - insert nonMatchingFieldTask; - - Test.startTest(); - Schema.SObjectField field = Schema.Task.Status; - String status = expectedTaskMap.values()[0].Status; - Map returnedTaskMap = new Map(new TaskRepository().getByFieldForWhoIds(field, 'Not Started', new List{lead.Id})); - Test.stopTest(); - - System.assertEquals(expectedTaskMap.size(), returnedTaskMap.size()); - for(Task tsk : expectedTaskMap.values()) { - System.assert(returnedTaskMap.containsKey(tsk.Id)); - System.assert(!returnedTaskMap.containsKey(nonMatchingFieldTask.Id)); - } - } - - @isTest - static void it_should_not_return_a_task_created_more_than_two_weeks_ago_when_querying_for_last_week() { - //Given that I have tasks created at different times - //When I query for a date literal that should exclude certain tasks - //I should only get the tasks created within that date literals value - Lead lead = [SELECT Id FROM Lead LIMIT 1]; - - //Create an additional task that falls outside of the date literals value. - Task task = TestDataGenerator.createCommunicationTask(lead.Id, null); - task.CreatedDate = (Datetime)System.today().addDays(-15); - insert task; - - Test.startTest(); - Map returnedTaskMap = new Map(new TaskRepository().getCreatedSinceTimeValue(new DateLiterals().LAST_WEEK)); - Test.stopTest(); - - System.assert(returnedTaskMap.size() > 0); - System.assert(!returnedTaskMap.containsKey(task.Id)); - } - - @isTest - static void it_should_return_by_who_id() { - //Given that I have a known account or lead lookup - //When I query for tasks with that value - //I should only get the tasks for the whoId provided - Lead lead1 = [SELECT Id FROM Lead LIMIT 1]; - Lead lead2 = [SELECT Id FROM Lead WHERE Id != :lead1.Id LIMIT 1]; - - Test.startTest(); - Map returnedTaskMap = new Map(new TaskRepository().getByWhoId(new List{lead1.Id})); - Test.stopTest(); - - for(Task tsk : returnedTaskMap.values()) { - System.assertNotEquals(lead2.Id,tsk.WhoId); - System.assertEquals(lead1.Id,tsk.WhoId); - } - } - - @isTest - static void it_should_return_by_what_id() { - //Given that I have an object id related to an account or lead - //If I query for tasks related to that object - //Then only tasks related to that object's parent (lead or account) should be returned - Lead lead1 = [SELECT Id FROM Lead LIMIT 1]; - - WebConversion__c wc = TestDataGenerator.createWebConversion(lead1.Id); - insert wc; - - Lead lead2 = [SELECT Id FROM Lead WHERE Id != :lead1.Id LIMIT 1]; - - Test.startTest(); - Map returnedTaskMap = new Map(new TaskRepository().getByWhatId(new List{wc.Id})); - Test.stopTest(); - - for(Task tsk : returnedTaskMap.values()) { - System.assertNotEquals(lead2.Id,tsk.WhoId); - System.assertEquals(lead1.Id,tsk.WhoId); - } - } - - @isTest - static void it_should_return_by_stipulated_operator_and_soql_value() { - //Given I want to find something like all promotional material created today - //When I query for the tasks that match that description - //Then I should only get those tasks returned to me - - //Address is required for mailings. Create one to avoid an error being thrown when creating the request task! - //Trigger settings are required to set up the mailing address fields on the request task being inserted - TestDataGenerator.insertTriggerSettings(); - Address__c address = TestDataGenerator.createAddress(); - insert address; - - List leads = [SELECT Id FROM Lead LIMIT 1]; - - Task task = TestDataGenerator.createCatalogTask(leads[0].Id, address.Id); - insert task; - - Test.startTest(); - String catalogType = TaskTypes.PROMOTIONAL_MATERIAL; - Schema.SObjectField taskType = Schema.Task.Type__c; - Map returnedTaskMap = new Map(new TaskRepository().getByFieldAndTypeForGivenTimePeriod(taskType, catalogType, new DateLiterals().TODAY)); - - System.assertEquals(leads.size(),returnedTaskMap.size()); - System.assert(returnedTaskMap.containsKey(task.Id)); - } - - @isTest - static void it_should_return_appropriate_tasks_when_querying_with_SOSL() { - //Given that I have a string that I would like to find the first 10 matching tasks for - //When I query for that string - //Then I should get the tasks that match that string returned - String searchTerm = staticString(); - List expectedTaskList = (List)[FIND :searchTerm IN ALL FIELDS RETURNING Task(Id WHERE IsClosed = false)][0]; - - Test.startTest(); - - List returnedTaskList = new TaskRepository().searchInAllFields(searchTerm); - System.assertEquals(expectedTaskList.size(), returnedTaskList.size()); - - Test.stopTest(); - } - - private static String staticString() { - String value = 'thing'; - return value; - } -} \ No newline at end of file diff --git a/src/classes/TaskRepository_Tests.cls-meta.xml b/src/classes/TaskRepository_Tests.cls-meta.xml deleted file mode 100644 index cbddff8c..00000000 --- a/src/classes/TaskRepository_Tests.cls-meta.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 38.0 - Active - diff --git a/src/objects/Lead.object b/src/objects/Lead.object deleted file mode 100644 index f106ef1a..00000000 --- a/src/objects/Lead.object +++ /dev/null @@ -1,48 +0,0 @@ - - - - AnotherFieldSet - A second field set - - Email - false - false - - - EmailBouncedDate - false - false - - - EmailBouncedReason - false - false - - - HasOptedOutOfEmail - false - false - - - - - MyFieldSet - - Id - false - false - - Sample field set - - Email - false - false - - - Status - false - false - - - - diff --git a/src/objects/Task.object b/src/objects/Task.object deleted file mode 100644 index ed39f6ce..00000000 --- a/src/objects/Task.object +++ /dev/null @@ -1,33 +0,0 @@ - - - - MyFieldSet - Sample field set - - Description - false - false - - - WhoId - false - false - - - WhatId - false - false - - - Subject - false - false - - - Type - false - false - - - - diff --git a/src/package.xml b/src/package.xml index 9803cb67..b47bfac7 100644 --- a/src/package.xml +++ b/src/package.xml @@ -4,10 +4,5 @@ * ApexClass - - Lead.MyFieldSet - Task.MyFieldSet - FieldSet - 38.0 \ No newline at end of file