Skip to content

Commit 11c436d

Browse files
authored
release/v2.2.0 (#73)
* Btc 49 main filters group order (#72) * BTC-49 Control condition order for a main group * Documentation update * Date Literals Fix (#71) * SOQL Filter Group refactoring (#76) * SOQL Filter Group refactoring * Restore asDateLiteral * readme update
1 parent 18cc18b commit 11c436d

File tree

13 files changed

+234
-61
lines changed

13 files changed

+234
-61
lines changed

README.md

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
The SOQL Lib provides functional constructs for SOQL queries in Apex.
77

8-
For more details, please refer to the [documentation](https://soql-lib.vercel.app/). You may also find [this blog post](https://beyondthecloud.dev/blog/soql-lib) about SOQL Lib interesting
8+
For more details, please refer to the [documentation](https://soql-lib.vercel.app/).
9+
10+
You may also find [this blog post](https://beyondthecloud.dev/blog/soql-lib) about SOQL Lib interesting.
911

1012
## Examples
1113

@@ -23,6 +25,8 @@ List<Account> accounts = SOQL.of(Account.SObjectType)
2325

2426
## Selector
2527

28+
Read [how to build your selector class](https://soql-lib.vercel.app/building-your-selector).
29+
2630
```apex
2731
public inherited sharing class SOQL_Contact extends SOQL implements SOQL.Selector {
2832
public static SOQL_Contact query() {
@@ -69,23 +73,32 @@ public with sharing class ExampleController {
6973
src="https://raw.githubusercontent.com/afawcett/githubsfdeploy/master/deploy.png">
7074
</a>
7175

72-
## Read the documentation
73-
74-
[Query Selector documentation](https://soql-lib.vercel.app/)
75-
76-
## Assumptions
77-
78-
1. **Small Selector Classes** - The selector class should be small and contains ONLY query base configuration (fields, sharing settings) and very generic methods (`byId`, `byRecordType`). Why?
79-
- Huge classes are hard to manage.
80-
- A lot of merge conflicts.
81-
- Problems with methods naming.
82-
2. **Build SOQL inline in a place of need** - Business-specific SOQLs should be built inline via `SOQL` builder in a place of need.
83-
- Most of the queries on the project are case-specific and are not generic. There is no need to keep them in the Selector class.
84-
3. **Build SOQL dynamically via builder** - Developers should be able to adjust queries with specific fields, conditions, and other SOQL clauses.
85-
4. **Do not spend time on selector methods naming** - It can be difficult to find a proper name for a method that builds a query. The selector class contains methods like `selectByFieldAAndFieldBWithDescOrder`. It can be avoided by building SOQL inline in a place of need.
86-
5. **Control FLS and sharing settings** - Selector should allow to control Field Level Security and sharing settings by simple methods like `.systemMode()`, `.withSharing()`, `.withoutSharing()`.
87-
6. **Auto binding** - The selector should be able to bind variables dynamically without additional effort from the developer side.
88-
7. **Mock results in Unit Tests** - Selector should allow for mocking data in unit tests.
76+
## Documentation
77+
78+
[SOQL Lib documentation](https://soql-lib.vercel.app/)
79+
80+
## Features
81+
82+
Read about the features in the [documentation](https://soql-lib.vercel.app/docs/basic-features).
83+
84+
1. **Dynamic SOQL**
85+
2. **Automatic binding**
86+
3. **Control FLS**
87+
- 3.1 **User Mode**
88+
- 3.2 **System Mode**
89+
- 3.3 **stripInaccessible**
90+
4. **Control Sharings Mode**
91+
- 4.1 **with sharing**
92+
- 4.2 **without sharing**
93+
- 4.3 **inherited sharing**
94+
5. **Mocking**
95+
- 5.1 **Mock list of records**
96+
- 5.2 **Mock single record**
97+
- 5.3 **Mock with static resources**
98+
- 5.4 **Mock count result**
99+
6. **Avoid query duplicates**
100+
7. **The default configuration for all queries**
101+
8. **Dynamic conditions**
89102

90103
----
91104

force-app/main/default/classes/SOQL.cls

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ public virtual inherited sharing class SOQL implements Queryable {
7474
Queryable whereAre(FilterGroup filterGroup); // SOQL.FilterGroup
7575
Queryable whereAre(Filter filter); // SOQL.Filter
7676
Queryable whereAre(String conditions); // Conditions to evaluate
77+
Queryable conditionLogic(String order);
78+
Queryable anyConditionMatching();
7779

7880
Queryable groupBy(SObjectField field);
7981
Queryable groupByRollup(SObjectField field);
@@ -200,6 +202,7 @@ public virtual inherited sharing class SOQL implements Queryable {
200202
Filter excludesAll(Iterable<String> values); // join with ,
201203
Filter excludesSome(Iterable<String> values); // join with ;
202204

205+
Filter asDateLiteral();
203206
Filter ignoreWhen(Boolean logicExpression); // Condition will be removed when logicExpression evaluates to true
204207

205208
Boolean hasValue();
@@ -381,6 +384,16 @@ public virtual inherited sharing class SOQL implements Queryable {
381384
return this;
382385
}
383386

387+
public SOQL conditionLogic(String conditionLogic) {
388+
builder.conditions.conditionLogic(conditionLogic);
389+
return this;
390+
}
391+
392+
public SOQL anyConditionMatching() {
393+
builder.conditions.anyConditionMatching();
394+
return this;
395+
}
396+
384397
public SOQL groupBy(SObjectField field) {
385398
builder.groupBy.with(field);
386399
return this;
@@ -939,47 +952,31 @@ public virtual inherited sharing class SOQL implements Queryable {
939952
return '(' + buildNested() + ')';
940953
}
941954

942-
private void setDefaultOrderWhenEmpty() {
955+
private void setDefaultOrderWhenNotSpecified() {
943956
if (String.isNotEmpty(order)) {
944957
return;
945958
}
946959

947960
List<String> defaultOrder = new List<String>();
948961

949-
for (Integer i = 1; i <= queryConditions.size(); i++) {
950-
defaultOrder.add(String.valueOf(i));
962+
for (Integer i = 0; i < queryConditions.size(); i++) {
963+
defaultOrder.add(String.valueOf(i + 1));
951964
}
952965

953-
order = String.join(defaultOrder, ' ' + connector + ' '); // e.g (1 AND 2 AND 3)
966+
order = String.join(defaultOrder, ' ' + connector + ' ');
954967
}
955968

956969
public String buildNested() {
957-
setDefaultOrderWhenEmpty();
958-
959-
String conditions = applySpecialCharactersToOrder(); // e.g (*1* AND (*2* OR *3*))
960-
961-
for (Integer i = 0; i < queryConditions.size(); i++) {
962-
conditions = conditions.replace(
963-
conditionNumberWithSpecialCharacters(i + 1), // e.g *1*
964-
queryConditions.get(i).toString()
965-
);
966-
}
970+
setDefaultOrderWhenNotSpecified(); // e.g (0 AND 1 AND 2)
971+
addSpecialCharactersToOrder(); // e.g ({0} AND ({1} AND {2}))
967972

968-
return conditions;
973+
return String.format(order, queryConditions);
969974
}
970975

971-
private String applySpecialCharactersToOrder() {
972-
String orderWithSpecialCharacters = order;
973-
974-
for (Integer i = 1; i <= queryConditions.size(); i++) {
975-
orderWithSpecialCharacters = orderWithSpecialCharacters.replace(String.valueOf(i), conditionNumberWithSpecialCharacters(i));
976+
private void addSpecialCharactersToOrder() {
977+
for (Integer i = 0; i < queryConditions.size(); i++) {
978+
order = order.replace(String.valueOf(i + 1), '{' + i + '}');
976979
}
977-
978-
return orderWithSpecialCharacters; // e.g (*1* AND (*2* OR *3*))
979-
}
980-
981-
private String conditionNumberWithSpecialCharacters(Integer conditionNumber) {
982-
return '*' + conditionNumber + '*';
983980
}
984981
}
985982

@@ -1042,8 +1039,6 @@ public virtual inherited sharing class SOQL implements Queryable {
10421039
}
10431040

10441041
private class QFilter implements Filter {
1045-
private final List<DisplayType> TYPES_WITHOUT_BINDING = new List<DisplayType>{ DisplayType.DateTime };
1046-
10471042
private String field;
10481043
private String comperator;
10491044
private Object value;
@@ -1064,12 +1059,10 @@ public virtual inherited sharing class SOQL implements Queryable {
10641059
}
10651060

10661061
public Filter with(SObjectField field) {
1067-
skipBinding = TYPES_WITHOUT_BINDING.contains(field.getDescribe().getType());
10681062
return with(field.getDescribe().getName());
10691063
}
10701064

10711065
public Filter with(String relationshipName, SObjectField field) {
1072-
skipBinding = TYPES_WITHOUT_BINDING.contains(field.getDescribe().getType());
10731066
return with(relationshipName + '.' + field);
10741067
}
10751068

@@ -1133,7 +1126,7 @@ public virtual inherited sharing class SOQL implements Queryable {
11331126
public Filter endsWith(String value) {
11341127
return contains('%', formattedString(value), '');
11351128
}
1136-
1129+
11371130
public Filter notEndsWith(String value) {
11381131
return notLike().endsWith(value);
11391132
}
@@ -1222,12 +1215,18 @@ public virtual inherited sharing class SOQL implements Queryable {
12221215
}
12231216
return this;
12241217
}
1225-
1218+
1219+
public Filter asDateLiteral() {
1220+
// Date Literals can't be binded
1221+
skipBinding = true;
1222+
return this;
1223+
}
1224+
12261225
public override String toString() {
12271226
if (skipBinding) {
12281227
return String.format(wrapper, new List<String> { field + ' ' + comperator + ' ' + value });
12291228
}
1230-
1229+
12311230
return String.format(wrapper, new List<String> { field + ' ' + comperator + ' :' + binder.bind(value) });
12321231
}
12331232
}

force-app/main/default/classes/SOQL_Test.cls

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,7 @@ private class SOQL_Test {
998998
static void dateLiteral() {
999999
// Test
10001000
String soql = SOQL.of(Account.SObjectType)
1001-
.whereAre(SOQL.Filter.with(Account.CreatedDate).greaterThan('LAST_N_QUARTERS:2'))
1001+
.whereAre(SOQL.Filter.with(Account.CreatedDate).greaterThan('LAST_N_QUARTERS:2').asDateLiteral())
10021002
.toString();
10031003

10041004
// Verify
@@ -1056,7 +1056,7 @@ private class SOQL_Test {
10561056
}
10571057

10581058
@IsTest
1059-
static void anyConditionMatching() {
1059+
static void anyConditionMatchingForInnerGroup() {
10601060
// Test
10611061
SOQL builder = SOQL.of(Account.SObjectType)
10621062
.whereAre(SOQL.FilterGroup
@@ -1074,6 +1074,40 @@ private class SOQL_Test {
10741074
Assert.areEqual('Krakow', binding.get('v2'));
10751075
}
10761076

1077+
@IsTest
1078+
static void anyConditionMatchingForMainGroup() {
1079+
// Test
1080+
SOQL builder = SOQL.of(Account.SObjectType)
1081+
.whereAre(SOQL.Filter.with(Account.Name).equal('Test'))
1082+
.whereAre(SOQL.Filter.with(Account.BillingCity).equal('Krakow'))
1083+
.anyConditionMatching();
1084+
1085+
// Verify
1086+
String soql = builder.toString();
1087+
Assert.areEqual('SELECT Id FROM Account WHERE Name = :v1 OR BillingCity = :v2', soql);
1088+
1089+
Map<String, Object> binding = builder.binding();
1090+
Assert.areEqual('Test', binding.get('v1'));
1091+
Assert.areEqual('Krakow', binding.get('v2'));
1092+
}
1093+
1094+
@IsTest
1095+
static void conditionLogicForMainGroup() {
1096+
// Test
1097+
SOQL builder = SOQL.of(Account.SObjectType)
1098+
.whereAre(SOQL.Filter.with(Account.Name).equal('Test'))
1099+
.whereAre(SOQL.Filter.with(Account.BillingCity).equal('Krakow'))
1100+
.conditionLogic('1 OR 2');
1101+
1102+
// Verify
1103+
String soql = builder.toString();
1104+
Assert.areEqual('SELECT Id FROM Account WHERE Name = :v1 OR BillingCity = :v2', soql);
1105+
1106+
Map<String, Object> binding = builder.binding();
1107+
Assert.areEqual('Test', binding.get('v1'));
1108+
Assert.areEqual('Krakow', binding.get('v2'));
1109+
}
1110+
10771111
@IsTest
10781112
static void nestedFiltersGroup() {
10791113
// Test

force-app/main/default/classes/example/ExampleController.cls

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ public with sharing class ExampleController {
2828
}
2929

3030
@AuraEnabled
31-
public static List<Account> getAccountsByRecordType(String recordType) {
31+
public static List<Account> getAccountsByRecordTypeOrITIndustry(String recordType) {
3232
return SOQL_Account.query()
3333
.byRecordType(recordType)
3434
.byIndustry('IT')
35+
.anyConditionMatching()
3536
.with(Account.Industry, Account.AccountSource)
3637
.toList();
3738
}

website/docs/api/soql-filter.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ The following are methods for `Filter`.
5353

5454
[**ADDITIONAL**](#additional)
5555

56+
- [`asDateLiteral()`](#asdateliteral)
5657
- [`ignoreWhen(Boolean logicExpression)`](#ignorewhen)
5758

5859
## FIELDS
@@ -840,6 +841,34 @@ SOQL builder = SOQL.of(AccountContactRelation.SObjectType)
840841

841842
## ADDITIONAL
842843

844+
### asDateLiteral
845+
846+
[Date Literals](https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_dateformats.htm)
847+
848+
SOQL Lib automatically binds all variables; however, Date Literals cannot be binded.
849+
To skip binding for Date Literals, add `asDateLiteral()` to your `Filter`.
850+
851+
**Signature**
852+
853+
```apex
854+
Filter asDateLiteral();
855+
```
856+
857+
**Example**
858+
859+
```sql
860+
SELECT Id
861+
FROM Account
862+
WHERE CreatedDate > 'LAST_90_DAYS'
863+
```
864+
```apex
865+
String accountName = '';
866+
867+
SOQL.of(Account.SObjectType)
868+
.whereAre(SOQL.Filter.with(Account.CreatedDate).greaterThan('LAST_90_DAYS').asDateLiteral());
869+
.toList();
870+
```
871+
843872
### ignoreWhen
844873

845874
Condition will be removed when logic expression will evaluate to true.
@@ -849,7 +878,7 @@ Note! It does not work when [SOQL.FilterGroup.conditionLogic()](./soql-filters-g
849878
**Signature**
850879

851880
```apex
852-
Filter ignoreWhen(Boolean logicExpression);
881+
Filter ignoreWhen(Boolean logicExpression);
853882
```
854883

855884
**Example**

website/docs/api/soql-filters-group.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ SOQL.of(Account.SObjectType)
109109

110110
### anyConditionMatching
111111

112-
When the [conditionLogic](#anyconditionmatching) is not specified, all conditions are joined using the `AND` operator by default.
112+
When the [conditionLogic](#conditionlogic) is not specified, all conditions are joined using the `AND` operator by default.
113113

114114
To change the default condition logic, you can utilize the `anyConditionMatching` method, which joins conditions using the `OR` operator.
115115

0 commit comments

Comments
 (0)