Skip to content

Commit 8021628

Browse files
authored
Feature/soql evaluator (#193)
* evaluate * Evaluate * Evaluate * SOQL Evaluator * SOQLEvaluator_Test * SOQLEvaluator_Test * SOQLEvaluator_Test * SOQLEvaluator_Test * Refactoring
1 parent c111b1f commit 8021628

File tree

9 files changed

+871
-37
lines changed

9 files changed

+871
-37
lines changed

force-app/main/default/classes/main/cached-soql/CacheManager.cls

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* - PropertyNamingConventions: It was intentional to make the lib more fluent and readable
88
* - FieldNamingConventions: It was intentional to make the lib more fluent and readable
99
**/
10-
@SuppressWarnings('PMD.CognitiveComplexity, PMD.PropertyNamingConventions, PMD.FieldNamingConventions')
10+
@SuppressWarnings('PMD.CognitiveComplexity,PMD.PropertyNamingConventions,PMD.FieldNamingConventions')
1111
public with sharing class CacheManager {
1212
public interface Cacheable {
1313
Boolean contains(String key);

force-app/main/default/classes/main/cached-soql/SOQLCache.cls

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
* Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev)
33
* Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE)
44
*
5+
* v6.0.0
6+
*
57
* PMD False Positives:
68
* - ExcessivePublicCount: It is a library class and exposes all necessary methods to construct a query
79
* - ExcessiveClassLength: It is a library and we tried to put everything into ONE class
@@ -10,9 +12,9 @@
1012
* - PropertyNamingConventions: It was intentional to make the lib more fluent and readable
1113
* - FieldDeclarationsShouldBeAtStart: Developer who uses lib should see what's important at start
1214
* - ApexDoc: Variable names are self-documented.
13-
* - ExcessiveParameterList - Make methods similar to native SOQL
15+
* - ExcessiveParameterList: Make methods similar to native SOQL
1416
**/
15-
@SuppressWarnings('PMD.ExcessivePublicCount, PMD.ExcessiveClassLength, PMD.CyclomaticComplexity, PMD.CognitiveComplexity, PMD.PropertyNamingConventions, PMD.FieldDeclarationsShouldBeAtStart, PMD.ApexDoc, PMD.ExcessiveParameterList')
17+
@SuppressWarnings('PMD.ExcessivePublicCount,PMD.ExcessiveClassLength,PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.PropertyNamingConventions,PMD.FieldDeclarationsShouldBeAtStart,PMD.ApexDoc,PMD.ExcessiveParameterList')
1618
public virtual inherited sharing class SOQLCache implements Cacheable {
1719
public interface Selector {
1820
Cacheable query();

force-app/main/default/classes/main/cached-soql/SOQLCache_Test.cls

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
* Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev)
33
* Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE)
44
*
5+
* v6.0.0
6+
*
57
* PMD False Positives:
68
* - CyclomaticComplexity: It is a library and we tried to put everything into ONE test class
79
* - CognitiveComplexity: It is a library and we tried to put everything into ONE class
810
* - ApexDoc: Variable names are self-documented.
9-
* - AvoidHardcodingId: Hardcoded Ids are used to mock SOQL results
1011
**/
11-
@SuppressWarnings('PMD.CyclomaticComplexity, PMD.CognitiveComplexity, PMD.ApexDoc, PMD.AvoidHardcodingId')
12+
@SuppressWarnings('PMD.CyclomaticComplexity,PMD.CognitiveComplexity,PMD.ApexDoc')
1213
@IsTest
1314
private class SOQLCache_Test {
1415
private static final String INITIAL_QUERY_MOCK_ID = 'cachedProfile';
@@ -172,9 +173,9 @@ private class SOQLCache_Test {
172173
@IsTest
173174
static void maxHoursWithoutRefreshRecordDoesNotExistAnymore() {
174175
// Setup
175-
SOQLCache.CacheItem cachedItem1 = new SOQLCache.CacheItem(new Profile(Id = '00e3V000000Nme3QAC', Name = 'ProfileNotExistName'));
176+
SOQLCache.CacheItem cachedItem1 = new SOQLCache.CacheItem(new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = 'ProfileNotExistName'));
176177
cachedItem1.cachedDate = DateTime.now().addHours(-6);
177-
SOQLCache.CacheItem cachedItem2 = new SOQLCache.CacheItem(new Profile(Id = '00e3V000000Nme3QAD', Name = 'System Administrator'));
178+
SOQLCache.CacheItem cachedItem2 = new SOQLCache.CacheItem(new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = 'System Administrator'));
178179

179180
CacheManager.ApexTransaction.put('Profile', new List<SOQLCache.CacheItem>{ cachedItem1, cachedItem2 });
180181

@@ -361,7 +362,7 @@ private class SOQLCache_Test {
361362
SOQLCache.of(Profile.SObjectType)
362363
.with(Profile.Id, Profile.Name)
363364
.whereEqual(Profile.Name, 'System Administrator')
364-
.whereEqual(Profile.Id, '00e3V000000NmefQAC')
365+
.whereEqual(Profile.Id, SOQL.IdGenerator.get(Profile.SObjectType))
365366
.toObject();
366367
} catch (SOQLCache.SoqlCacheException e) {
367368
soqlException = e;
@@ -406,7 +407,8 @@ private class SOQLCache_Test {
406407
@IsTest
407408
static void mockId() {
408409
// Setup
409-
SOQLCache.mock('ProfileQuery').thenReturn(new Profile(Id = '00e3V000000Nme3QAC', Name = 'System Administrator'));
410+
Id profileId = SOQL.IdGenerator.get(Profile.SObjectType);
411+
SOQLCache.mock('ProfileQuery').thenReturn(new Profile(Id = profileId, Name = 'System Administrator'));
410412

411413
// Test
412414
Profile profile = (Profile) new SOQL_ProfileCache().query()
@@ -415,7 +417,7 @@ private class SOQLCache_Test {
415417
.toObject();
416418

417419
// Verify
418-
Assert.areEqual('00e3V000000Nme3QAC', profile.Id, 'The profile id should be the same as the id assigned to the profile cache.');
420+
Assert.areEqual(profileId, profile.Id, 'The profile id should be the same as the id assigned to the profile cache.');
419421
Assert.areEqual('System Administrator', profile.Name, 'The profile name should be the same as the name assigned to the profile cache.');
420422
}
421423

@@ -535,8 +537,8 @@ private class SOQLCache_Test {
535537
static void toObjectWithMultipleRows() {
536538
// Setup
537539
SOQL.mock('ProfileQuery').thenReturn(new List<Profile>{
538-
new Profile(Id = '00e3V000000Nme3QAC', Name = 'System Administrator'),
539-
new Profile(Id = '00e3V000000Nme3QAC', Name = 'System Administrator')
540+
new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = 'System Administrator'),
541+
new Profile(Id = SOQL.IdGenerator.get(Profile.SObjectType), Name = 'System Administrator')
540542
});
541543

542544
QueryException queryException = null;
@@ -559,7 +561,8 @@ private class SOQLCache_Test {
559561
@IsTest
560562
static void recordNotFoundInCache() {
561563
// Setup
562-
SOQL.mock('ProfileQuery').thenReturn(new Profile(Id = '00e3V000000Nbc3QAC', Name = 'Guest User'));
564+
Id profileId = SOQL.IdGenerator.get(Profile.SObjectType);
565+
SOQL.mock('ProfileQuery').thenReturn(new Profile(Id = profileId, Name = 'Guest User'));
563566

564567
// Test
565568
Profile profile = (Profile) SOQLCache.of(Profile.SObjectType)
@@ -569,7 +572,7 @@ private class SOQLCache_Test {
569572
.toObject();
570573

571574
// Verify
572-
Assert.areEqual('00e3V000000Nbc3QAC', profile.Id, 'Record not found in cache should be retrieved from SOQL and cached.');
575+
Assert.areEqual(profileId, profile.Id, 'Record not found in cache should be retrieved from SOQL and cached.');
573576
Assert.areEqual('Guest User', profile.Name, 'Record not found in cache should be retrieved from SOQL and cached.');
574577
}
575578

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
/**
2+
* Copyright (c) 2025 Beyond The Cloud Sp. z o.o. (BeyondTheCloud.Dev)
3+
* Licensed under the MIT License (https://github.com/beyond-the-cloud-dev/soql-lib/blob/main/LICENSE)
4+
*
5+
* v6.0.0
6+
*
7+
* PMD False Positives:
8+
* - CognitiveComplexity: It is a library and we tried to put everything into ONE class
9+
**/
10+
@SuppressWarnings('PMD.CognitiveComplexity')
11+
public inherited sharing class SOQLEvaluator {
12+
public static SObjectEvaluable of(List<SObject> staticQuery) {
13+
return new SObjectEvaluator(staticQuery);
14+
}
15+
16+
public interface SObjectEvaluable {
17+
// FIELD-LEVEL SECURITY
18+
SObjectEvaluable stripInaccessible();
19+
SObjectEvaluable stripInaccessible(AccessType accessType);
20+
// MOCKING
21+
SObjectEvaluable mockId(String mockId);
22+
// RESULT
23+
Id toId();
24+
Set<Id> toIds();
25+
Set<Id> toIdsOf(SObjectField field);
26+
Set<Id> toIdsOf(String relationshipName, SObjectField field);
27+
Boolean doExist();
28+
Object toValueOf(SObjectField fieldToExtract);
29+
Set<String> toValuesOf(SObjectField fieldToExtract);
30+
SObject toObject();
31+
List<SObject> toList();
32+
Map<Id, SObject> toMap();
33+
Map<String, SObject> toMap(SObjectField keyField);
34+
Map<String, SObject> toMap(String relationshipName, SObjectField targetKeyField);
35+
Map<String, String> toMap(SObjectField keyField, SObjectField valueField);
36+
Map<String, List<SObject>> toAggregatedMap(SObjectField keyField);
37+
Map<String, List<SObject>> toAggregatedMap(String relationshipName, SObjectField targetKeyField);
38+
Map<String, List<String>> toAggregatedMap(SObjectField keyField, SObjectField valueField);
39+
}
40+
41+
public interface Mockable {
42+
// SObject
43+
Mockable thenReturn(SObject record);
44+
Mockable thenReturn(List<SObject> records);
45+
}
46+
47+
@TestVisible
48+
private static Mockable mock(String mockId) {
49+
if (!SOQLEvaluator.queryIdToMock.containsKey(mockId)) {
50+
SOQLEvaluator.queryIdToMock.put(mockId, new List<SoqlMock>());
51+
}
52+
SOQLEvaluator.queryIdToMock.get(mockId).add(new SoqlMock());
53+
return SOQLEvaluator.queryIdToMock.get(mockId).get(SOQLEvaluator.queryIdToMock.get(mockId).size() - 1);
54+
}
55+
56+
// Implementation
57+
58+
private static Map<String, List<SoqlMock>> queryIdToMock = new Map<String, List<SoqlMock>>();
59+
60+
private class SObjectEvaluator implements SObjectEvaluable {
61+
private List<SObject> records;
62+
private AccessType access;
63+
private SOQL.Converter converter;
64+
private List<SoqlMock> mocks = new List<SoqlMock>();
65+
66+
public SObjectEvaluator(List<SObject> records) {
67+
this.records = records;
68+
this.converter = new SOQL.Converter(records.getSObjectType().toString());
69+
}
70+
71+
public SObjectEvaluable mockId(String mockId) {
72+
this.mocks = SOQLEvaluator.queryIdToMock.get(mockId) ?? new List<SoqlMock>();
73+
return this;
74+
}
75+
76+
public SObjectEvaluable stripInaccessible() {
77+
return this.stripInaccessible(AccessType.READABLE);
78+
}
79+
80+
public SObjectEvaluable stripInaccessible(AccessType accessType) {
81+
this.access = accessType;
82+
return this;
83+
}
84+
85+
public Id toId() {
86+
return this.toObject().Id;
87+
}
88+
89+
public Set<Id> toIds() {
90+
return new Map<Id, SObject>(this.toList()).keySet();
91+
}
92+
93+
public Set<Id> toIdsOf(SObjectField field) {
94+
return this.converter.transform(this.toList()).toIdsOf(field);
95+
}
96+
97+
public Set<Id> toIdsOf(String relationshipName, SObjectField field) {
98+
return this.converter.transform(this.toList()).toIdsOf(relationshipName, field);
99+
}
100+
101+
public Boolean doExist() {
102+
return !this.toList().isEmpty();
103+
}
104+
105+
public Object toValueOf(SObjectField fieldToExtract) {
106+
return this.toObject()?.get(fieldToExtract);
107+
}
108+
109+
public Set<String> toValuesOf(SObjectField fieldToExtract) {
110+
return this.converter.transform(this.toList()).toValuesOf(fieldToExtract);
111+
}
112+
113+
public SObject toObject() {
114+
List<SObject> records = this.toList();
115+
116+
if (records.isEmpty()) {
117+
return null; // handle: List has no rows for assignment to SObject
118+
}
119+
120+
if (records.size() > 1) {
121+
throw new QueryException('List has more than 1 row for assignment to SObject');
122+
}
123+
124+
return records[0];
125+
}
126+
127+
public List<SObject> toList() {
128+
if (!this.mocks.isEmpty()) {
129+
return this.getMockedListProxy();
130+
}
131+
132+
if (this.access == null) {
133+
return this.records;
134+
}
135+
136+
return System.Security.stripInaccessible(this.access, this.records).getRecords();
137+
}
138+
139+
private List<SObject> getMockedListProxy() {
140+
if (this.mocks.size() == 1) {
141+
return this.mocks[0].sObjectMock.get();
142+
}
143+
return this.mocks.remove(0).sObjectMock.get();
144+
}
145+
146+
public Map<Id, SObject> toMap() {
147+
return this.converter.transform(this.toList()).toMap();
148+
}
149+
150+
public Map<String, SObject> toMap(SObjectField keyField) {
151+
return this.converter.transform(this.toList()).toMap(keyField);
152+
}
153+
154+
public Map<String, SObject> toMap(String relationshipName, SObjectField targetKeyField) {
155+
return this.converter.transform(this.toList()).toMap(relationshipName, targetKeyField);
156+
}
157+
158+
public Map<String, String> toMap(SObjectField keyField, SObjectField valueField) {
159+
return this.converter.transform(this.toList()).toMap(keyField, valueField);
160+
}
161+
162+
public Map<String, List<SObject>> toAggregatedMap(SObjectField keyField) {
163+
return this.converter.transform(this.toList()).toAggregatedMap(keyField);
164+
}
165+
166+
public Map<String, List<SObject>> toAggregatedMap(String relationshipName, SObjectField targetKeyField) {
167+
return this.converter.transform(this.toList()).toAggregatedMap(relationshipName, targetKeyField);
168+
}
169+
170+
public Map<String, List<String>> toAggregatedMap(SObjectField keyField, SObjectField valueField) {
171+
return this.converter.transform(this.toList()).toAggregatedMap(keyField, valueField);
172+
}
173+
}
174+
175+
public class SoqlMock implements Mockable {
176+
public SObjectMock sObjectMock = new SObjectMock();
177+
178+
public Mockable thenReturn(SObject record) {
179+
this.sObjectMock.add(record);
180+
return this;
181+
}
182+
183+
public Mockable thenReturn(List<SObject> records) {
184+
this.sObjectMock.add(records);
185+
return this;
186+
}
187+
}
188+
189+
public class SObjectMock {
190+
private List<SObject> mockedRecords = new List<SObject>();
191+
192+
public void add(SObject record) {
193+
this.mockedRecords.add(record);
194+
}
195+
196+
public void add(List<SObject> records) {
197+
this.mockedRecords.addAll(records);
198+
}
199+
200+
public List<SObject> get() {
201+
if (!this.mockedRecords.isEmpty()) {
202+
this.addIdToMockedRecords();
203+
}
204+
205+
return this.mockedRecords;
206+
}
207+
208+
private void addIdToMockedRecords() { // Id is always added to mirror standard SOQL behavior
209+
SObjectType sObjectType = this.mockedRecords[0].getSObjectType();
210+
String sObjectPrefix = sObjectType.getDescribe().getKeyPrefix();
211+
212+
for (SObject record : this.mockedRecords) {
213+
record.put('Id', record?.Id ?? SOQL.IdGenerator.get(sObjectPrefix));
214+
}
215+
}
216+
}
217+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3+
<apiVersion>64.0</apiVersion>
4+
<status>Active</status>
5+
</ApexClass>

0 commit comments

Comments
 (0)