diff --git a/force-app/factory/RepoFactoryMock.cls b/force-app/factory/RepoFactoryMock.cls index 6f79c34..f1c1eff 100644 --- a/force-app/factory/RepoFactoryMock.cls +++ b/force-app/factory/RepoFactoryMock.cls @@ -129,6 +129,11 @@ public class RepoFactoryMock { return this.results; } + public override List> getSosl(String searchTerm, List queries) { + QueriesMade.addAll(queries); + return new List>{ this.results }; + } + public override List aggregate(List aggregations, List queries) { AggregatesMade.addAll(aggregations); QueriesMade.addAll(queries); diff --git a/force-app/repository/IRepository.cls b/force-app/repository/IRepository.cls index 136f0eb..92c0c01 100644 --- a/force-app/repository/IRepository.cls +++ b/force-app/repository/IRepository.cls @@ -5,6 +5,10 @@ public interface IRepository extends IDML { List get(List queries); List getAll(); + List> getSosl(String searchTerm, Query query); + List> getSosl(String searchTerm, List queries); + IRepository setSearchGroup(SearchGroup searchGroup); + IRepository setAccessLevel(System.AccessLevel accessLevel); IRepository setLimit(Integer limitAmount); IRepository addSortOrder(Schema.SObjectField fieldToken, RepositorySortOrder sortOrder); diff --git a/force-app/repository/Query.cls b/force-app/repository/Query.cls index 07e68fa..7bcb2fe 100644 --- a/force-app/repository/Query.cls +++ b/force-app/repository/Query.cls @@ -207,6 +207,15 @@ public virtual class Query { return this.field + ' ' + this.getOperator() + ' ' + this.predicate; } + public String toSoslString() { + String startingString = this.toString(); + for (String key : this.bindVars.keySet()) { + startingString = startingString.replace(':' + key, this.getSoslPredicate(this.bindVars.get(key))); + } + startingString = startingString.replaceAll('= \\(', 'IN \\(').replaceAll('!= \\(', 'NOT IN \\('); + return startingString; + } + public Boolean equals(Object thatObject) { if (thatObject instanceof Query) { Query that = (Query) thatObject; @@ -269,4 +278,33 @@ public virtual class Query { } return builtUpFieldName; } + + private String getSoslPredicate(Object predicate) { + if (predicate == null) { + return 'null'; + } else if (predicate instanceof Datetime) { + // the most annoying one + Datetime dt = (Datetime) predicate; + return dt.format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'Greenwich Mean Time'); + } else if (predicate instanceof Iterable) { + Iterable localPredicates = (Iterable) predicate; + if (localPredicates.iterator().hasNext() == false) { + return ''; + } + List innerStrings = new List(); + for (Object innerPred : localPredicates) { + // recurse for string value + String innerString = this.getSoslPredicate(innerPred); + innerStrings.add(innerString); + } + String start = innerStrings.size() > 1 ? '(' : ''; + String ending = innerStrings.size() > 1 ? ')' : ''; + return start + String.join(innerStrings, ',') + ending; + } else if (predicate instanceof String) { + String input = (String) predicate; + return '\'' + String.escapeSingleQuotes(input) + '\''; + } + + return String.valueOf(predicate); + } } diff --git a/force-app/repository/Repository.cls b/force-app/repository/Repository.cls index d06eaef..f7af15b 100644 --- a/force-app/repository/Repository.cls +++ b/force-app/repository/Repository.cls @@ -9,9 +9,11 @@ public virtual without sharing class Repository implements IRepository { protected System.AccessLevel accessLevel = System.AccessLevel.SYSTEM_MODE; protected final Map fieldToSortOrder = new Map(); + private Boolean baseSelectUsed = false; + private Boolean isSosl = false; private Boolean shouldAddChildFields = true; private Integer limitAmount; - private Boolean baseSelectUsed = false; + private SearchGroup soslSearchGroup = SearchGroup.ALL_FIELDS; public Repository(Schema.SObjectType repoType, List queryFields, RepoFactory repoFactory) { this.dml = repoFactory.getDml(); @@ -176,14 +178,14 @@ public virtual without sharing class Repository implements IRepository { private String addWheres(List queries) { List wheres = new List(); for (Query qry : queries) { - wheres.add(qry.toString()); + wheres.add(this.isSosl ? qry.toSoslString() : qry.toString()); this.bindVars.putAll(qry.getBindVars()); } return wheres.isEmpty() ? '' : '\nWHERE ' + String.join(wheres, '\nAND '); } private List performQuery(String finalQuery) { - System.debug('Query: \n' + finalQuery); + System.debug('Query:\n' + finalQuery); List results = Database.queryWithBinds(finalQuery, this.bindVars, this.accessLevel); this.clearState(); System.debug('Number of results: ' + results.size() + '\nResults: \n' + results); @@ -207,6 +209,39 @@ public virtual without sharing class Repository implements IRepository { return (limitAmount != null ? '\nLIMIT ' + limitAmount : ''); } + // SOSL + + public List> getSosl(String searchTerm, Query queryFilter) { + return this.getSosl(searchTerm, new List{ queryFilter }); + } + + public virtual List> getSosl(String searchTerm, List queryFilters) { + this.isSosl = true; + String searchQuery = + 'FIND \'' + + String.escapeSingleQuotes(searchTerm) + + '\' IN ' + + this.soslSearchGroup.name().replace('_', ' ') + + ' RETURNING ' + + this.repoType + + '(' + + String.join(this.addSelectFields(), ',') + + this.addWheres(queryFilters) + + this.getLimitAmount(this.limitAmount) + + ')'; + System.debug('Search query:\n' + searchQuery); + List> results = Search.query(searchQuery, this.accessLevel); + System.debug('Number of results: ' + results.size() + '\nResults: \n' + results); + this.clearState(); + this.isSosl = false; + return results; + } + + public Repository setSearchGroup(SearchGroup searchGroup) { + this.soslSearchGroup = searchGroup; + return this; + } + // DML public Database.SaveResult doInsert(SObject record) { return this.dml.doInsert(record); diff --git a/force-app/repository/RepositoryTests.cls b/force-app/repository/RepositoryTests.cls index 60cca3e..7ba21d8 100644 --- a/force-app/repository/RepositoryTests.cls +++ b/force-app/repository/RepositoryTests.cls @@ -196,6 +196,32 @@ private class RepositoryTests { System.assertEquals(event, DMLMock.Published.firstOrDefault); } + @IsTest + static void it_performs_sosl_queries() { + ContactPointAddress cpa = new ContactPointAddress(Name = 'hello world', PreferenceRank = 1); + insert cpa; + Test.setFixedSearchResults(new List{ cpa.Id }); + + List> results = new ContactPointAddressRepo() + .setSearchGroup(SearchGroup.NAME_FIELDS) + .getSosl('hel', Query.equals(ContactPointAddress.PreferenceRank, 1)); + + System.assertEquals(cpa.Id, results.get(0).get(0).Id); + } + + @IsTest + static void it_performs_sosl_set_backed_queries() { + ContactPointAddress cpa = new ContactPointAddress(Name = 'hello world', PreferenceRank = 1); + insert cpa; + Test.setFixedSearchResults(new List{ cpa.Id }); + + List> results = new ContactPointAddressRepo() + .setSearchGroup(SearchGroup.NAME_FIELDS) + .getSosl('hel', Query.equals(ContactPointAddress.PreferenceRank, new Set{ 1, 2 })); + + System.assertEquals(cpa.Id, results.get(0).get(0).Id); + } + private class GroupMemberRepo extends Repository { public GroupMemberRepo() { super(GroupMember.SObjectType, new List{ GroupMember.GroupId }, new RepoFactory()); diff --git a/force-app/repository/SearchGroup.cls b/force-app/repository/SearchGroup.cls new file mode 100644 index 0000000..4420099 --- /dev/null +++ b/force-app/repository/SearchGroup.cls @@ -0,0 +1,7 @@ +public enum SearchGroup { + ALL_FIELDS, + EMAIL_FIELDS, + NAME_FIELDS, + PHONE_FIELDS, + SIDEBAR_FIELDS +} diff --git a/force-app/repository/SearchGroup.cls-meta.xml b/force-app/repository/SearchGroup.cls-meta.xml new file mode 100644 index 0000000..3a10d2e --- /dev/null +++ b/force-app/repository/SearchGroup.cls-meta.xml @@ -0,0 +1,5 @@ + + + 60.0 + Active + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f174c54..2a1d605 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "1.0.1", "license": "MIT", "devDependencies": { - "@salesforce/cli": "*", - "prettier": "2.6.2", + "@salesforce/cli": "latest", + "prettier": "3.2.5", "prettier-plugin-apex": "2.1.0" } }, @@ -16164,15 +16164,15 @@ } }, "node_modules/prettier": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz", - "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" diff --git a/package.json b/package.json index 2719f81..a0905b0 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "devDependencies": { "@salesforce/cli": "latest", "prettier-plugin-apex": "2.1.0", - "prettier": "2.6.2" + "prettier": "3.2.5" }, "repository": { "type": "git", diff --git a/scripts/validate-history-query.apex b/scripts/validate-history-query.apex index 2a7091c..ab362fa 100644 --- a/scripts/validate-history-query.apex +++ b/scripts/validate-history-query.apex @@ -1,9 +1,3 @@ -private class OpportunityFieldHistoryExample extends FieldLevelHistory { - protected override String getParentFieldName() { - return OpportunityFieldHistory.OpportunityId.getDescribe().getName(); - } -} - if ([SELECT COUNT() FROM OpportunityFieldHistory] == 0) { List opps = [SELECT Id, Name FROM Opportunity LIMIT 1]; if (opps.isEmpty()) { @@ -15,6 +9,7 @@ if ([SELECT COUNT() FROM OpportunityFieldHistory] == 0) { Opportunity opp = opps[0]; opp.Name = 'Something New'; update opp; + System.debug('Updated opportunity to create history record'); } else { System.debug('Histories already exist, continuing ...'); } @@ -23,11 +18,10 @@ Exception ex; List histories; try { histories = new FieldLevelHistoryRepo( - OpportunityFieldHistory.OpportunityId, OpportunityFieldHistory.SObjectType, - OpportunityFieldHistoryExample.class, + new List(), new RepoFactory() - ).getAllHistory(); + ).setParentField(OpportunityFieldHistory.OpportunityId).getAllHistory(); System.debug(histories); } catch (Exception e) { ex = e; diff --git a/sfdx-project.json b/sfdx-project.json index 46d3170..3586b18 100644 --- a/sfdx-project.json +++ b/sfdx-project.json @@ -10,5 +10,5 @@ ], "namespace": "", "sfdcLoginUrl": "https://login.salesforce.com", - "sourceApiVersion": "56.0" -} + "sourceApiVersion": "60.0" +} \ No newline at end of file