|
| 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 | +} |
0 commit comments