Skip to content

Commit 823ffee

Browse files
authored
Creating an in-memory database so queries don't have to go through Salesforce and instead can be executed through code. (#142)
1 parent 7922dba commit 823ffee

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+3749
-550
lines changed

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/dev-communities.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
{
22
"orgName": "Expression Dev",
33
"edition": "Developer",
4-
"features": ["EnableSetPasswordInApi", "Communities", "Sites"],
4+
"features": [
5+
"EnableSetPasswordInApi",
6+
"Communities",
7+
"Sites",
8+
"AuthorApex"
9+
],
510
"settings": {
611
"experienceBundleSettings": {
712
"enableExperienceBundleMetadata": true

docs/src/app/docs/functions/page.md

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Format: `CASE(expression,value1, result1, value2, result2,..., else_result)`
8585
```apex
8686
Account testAccount = new Account(Rating = 'Hot');
8787
Object result = expression.Evaluator.run(
88-
'CASE(Rating, "Hot", "🔥", "Cold", "🧊", "🤷")',
88+
'CASE(Rating, "Hot", "🔥", "Cold", "🧊", "🤷")',
8989
testAccount); // "🔥"
9090
```
9191

@@ -176,8 +176,10 @@ expression.Evaluator.run('LEN("Hello World")'); // 11
176176

177177
- `LIKE`
178178

179-
Returns TRUE if a text field matches a given pattern. The pattern can include regular characters and wildcard characters.
180-
The supported wildcard characters are the percent sign (%), which matches zero or more characters, and the underscore (_),
179+
Returns TRUE if a text field matches a given pattern. The pattern can include regular characters and wildcard
180+
characters.
181+
The supported wildcard characters are the percent sign (%), which matches zero or more characters, and the
182+
underscore (_),
181183
which matches exactly one character.
182184

183185
Accepts 2 arguments: the text to evaluate and the pattern to match.
@@ -277,7 +279,9 @@ Inserts a line break in a string of text.
277279

278280
When no arguments are provided, it inserts a line break. When a number is provided, it inserts that number of line
279281

280-
⚠️ Note that the inserted line break depends on the call context based on the [Request Quiddity](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_enum_System_Quiddity.htm). When called from
282+
⚠️ Note that the inserted line break depends on the call context based on
283+
the [Request Quiddity](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_enum_System_Quiddity.htm).
284+
When called from
281285
an Aura/LWC or Visualforce context it will insert a `<br>` tag, otherwise it will insert a newline character.
282286

283287
```apex
@@ -372,6 +376,16 @@ Accepts 3 arguments: the year, month, and day.
372376
expression.Evaluator.run('DATE(2020, 1, 1)'); // 2020-01-01
373377
```
374378

379+
- `DATETIME`
380+
381+
Returns a datetime value from the provided year, month, day, hour, minute, and second values.
382+
383+
Accepts 6 arguments: the year, month, day, hour, minute, and second.
384+
385+
```apex
386+
expression.Evaluator.run('DATETIME(2020, 1, 1, 12, 0, 0)'); // 2020-01-01 12:00:00
387+
```
388+
375389
- `ADDMONTHS`
376390

377391
Returns a date that is a specified number of months before or after a given date.
@@ -780,7 +794,8 @@ Accepts 2 arguments: List of objects and an expression to evaluate.
780794
Object result = expression.Evaluator.run('MAP(["a", "b", "c"], UPPER($current))'); // ["A", "B", "C"]
781795
```
782796

783-
To work with child records, you can specify the child relationship name as the first argument, and then the expression to evaluate on each
797+
To work with child records, you can specify the child relationship name as the first argument, and then the expression
798+
to evaluate on each
784799
child item as the second argument.
785800

786801
> When referencing child data through the record Id endpoint, the framework will take care of any necessary
@@ -820,12 +835,14 @@ expression.Evaluator.run('RANGE(1, 3)'); // (1, 2, 3)
820835

821836
- `REDUCE`
822837

823-
Reduces a list to a single value using the first argument as the context, the second argument as the expression to evaluate,
838+
Reduces a list to a single value using the first argument as the context, the second argument as the expression to
839+
evaluate,
824840
and the third argument as the initial value.
825841

826842
Accepts 3 arguments: List of objects, an expression to evaluate, and the initial value.
827843

828844
Provides 2 special variables in the inner expression:
845+
829846
- `$current` - the current item being iterated over
830847
- `$accumulator` - the current value of the accumulator that will be returned
831848

@@ -863,22 +880,27 @@ Sorts a list.
863880

864881
Accepts at least one argument: the list to sort.
865882
When sorting a list of Maps or a list of SObjects,
866-
two additional arguments can be provided: the field to sort by and the sort direction.
883+
three additional arguments can be provided: the field to sort by, the sort direction, and
884+
and the position of nulls (nulls first or nulls last).
867885

868-
The field to sort can either be a field name as a merge field (field name without quotes), or an expression that evaluates to a string
869-
representing the field name. Merge fields are only supported when sorting SObjects and are useful to get the framework to automatically
886+
The field to sort can either be a field name as a merge field (field name without quotes), or an expression that
887+
evaluates to a string
888+
representing the field name. Merge fields are only supported when sorting SObjects and are useful to get the framework
889+
to automatically
870890
query the field for you.
871891

872892
> The merge field must be a field on the SObject being sorted itself, not a relationship field.
873893
874894
The sort direction can either be the literal string (requires quotes) `ASC` or `DESC`.
895+
The position of nulls can either be the literal string (requires quotes) `NULLS_FIRST` or `NULLS_LAST`.
875896

876897
```apex
877898
expression.Evaluator.run('SORT([3, 2, 1])'); // (1, 2, 3)
878899
expression.Evaluator.run('SORT([{ "a": 3 }, { "a": 2 }, { "a": 1 }], "a")'); // ({ "a": 1 }, { "a": 2 }, { "a": 3 })
879900
expression.Evaluator.run('SORT([{ "a": 3 }, { "a": 2 }, { "a": 1 }], "a", "DESC")'); // ({ "a": 3 }, { "a": 2 }, { "a": 1 })
880901
expression.Evaluator.run('QUERY(Account["Name"]) -> SORT("Name")'); // ({"Name": "ACME"}, {"Name": "Another Account"})
881902
expression.Evaluator.run('SORT(ChildAccounts, NumberOfEmployees, "asc")', parentAccount.Id); // ({"NumberOfEmployees": 1}, {"NumberOfEmployees": 2})
903+
expression.Evaluator.run('SORT(ChildAccounts, NumberOfEmployees, "asc", "NULLS_LAST")', parentAccount.Id); // ({"NumberOfEmployees": 1}, {"NumberOfEmployees": 2}, {"NumberOfEmployees": null})
882904
```
883905

884906
- `SKIP`
@@ -1187,4 +1209,5 @@ Accepts 3 arguments: the first location, the second location, and the unit (eith
11871209
```apex
11881210
expression.Evaluator.run('DISTANCE(LOCATION(37.7749, 122.4194), LOCATION(40.7128, 74.0060), "mi")'); // 2565.6985207767134
11891211
```
1212+
11901213
---

docs/src/markdoc/config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const variables = {
77
tags,
88
nodes,
99
variables: {
10-
packageId: "04tRb000000yvufIAA",
10+
packageId: "04tRb000000zZVVIA2",
1111
componentPackageId: "04tRb000000sGz3IAE",
1212
urlPrefix: IS_PROD ? "/expression" : "",
1313
}

expression-src/main/editor/controllers/tests/PlaygroundControllerTest.cls

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ private class PlaygroundControllerTest {
2727
@IsTest
2828
static void validatesAValidExpressionWithAnId() {
2929
Account testAccount = new Account(Name = 'Test');
30-
insert testAccount;
30+
QDB.getInstance().doInsert(testAccount);
3131

3232
PlaygroundController.Result result = PlaygroundController.validate(
3333
'Name',
@@ -43,9 +43,9 @@ private class PlaygroundControllerTest {
4343
static void validatesAnExpressionWithManyIdsSeparatedByComma() {
4444
Account testAccount1 = new Account(Name = 'Test');
4545
Account testAccount2 = new Account(Name = 'Test');
46-
insert new List<Account>{
46+
QDB.getInstance().doInsert(new List<Account>{
4747
testAccount1, testAccount2
48-
};
48+
});
4949

5050
PlaygroundController.Result result = PlaygroundController.validate(
5151
'MAP(@Context, Name)',

expression-src/main/editor/lwc/functionLibrary/functions.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,9 @@ export const data = [
221221
{
222222
"name": "SORT",
223223
"autoCompleteValue": "SORT(",
224-
"description": "Sorts a list.<br/>Accepts at least one argument: the list to sort.<br/>When sorting a list of Maps or a list of SObjects,<br/>two additional arguments can be provided: the field to sort by and the sort direction.<br/>The field to sort can either be a field name as a merge field (field name without quotes), or an expression that evaluates to a string<br/>representing the field name. Merge fields are only supported when sorting SObjects and are useful to get the framework to automatically<br/>query the field for you.<br/>Note: The merge field must be a field on the SObject being sorted itself, not a relationship field.<br/>The sort direction can either be the literal string (requires quotes) `ASC` or `DESC`.",
224+
"description": "Sorts a list.<br/>Accepts at least one argument: the list to sort.<br/>When sorting a list of Maps or a list of SObjects,<br/>three additional arguments can be provided: the field to sort by, the sort direction, and the position of nulls<br/>(nulls first or nulls last).<br/>The field to sort can either be a field name as a merge field (field name without quotes), or an expression that evaluates to a string<br/>representing the field name. Merge fields are only supported when sorting SObjects and are useful to get the framework to automatically<br/>query the field for you.<br/>Note: The merge field must be a field on the SObject being sorted itself, not a relationship field.<br/>The sort direction can either be the literal string (requires quotes) `ASC` or `DESC`.<br/>The position of nulls can either be the literal string (requires quotes) `NULLS_FIRST` or `NULLS_LAST`.",
225225
"examples": [
226-
"SORT([{ \"a\": 3 }, { \"a\": 2 }, { \"a\": 1 }], \"a\", \"DESC\") // [{ \"a\": 3 }, { \"a\": 2 }, { \"a\": 1 }]"
226+
"SORT([{ \"a\": 3 }, { \"a\": 2 }, { \"a\": 1 }], \"a\", \"DESC\") // [{ \"a\": 3 }, { \"a\": 2 }, { \"a\": 1 }]\nSORT([SObject1, SObject2, SObject3], \"Name\", \"ASC\", \"NULLS_LAST\") // [SObject1, SObject2, SObject3]"
227227
],
228228
"icon": "utility:justify_text"
229229
},
@@ -327,6 +327,15 @@ export const data = [
327327
],
328328
"icon": "utility:date_input"
329329
},
330+
{
331+
"name": "DATETIME",
332+
"autoCompleteValue": "DATETIME(",
333+
"description": "Returns a datetime value from the provided year, month, day, hour, minute, and second values.<br/>Accepts 6 arguments: the year, month, day, hour, minute, and second.",
334+
"examples": [
335+
"DATETIME(2020, 1, 1, 12, 0, 0) // 2020-01-01 12:00:00"
336+
],
337+
"icon": "utility:date_input"
338+
},
330339
{
331340
"name": "DATETIMEFORMAT",
332341
"autoCompleteValue": "DATETIMEFORMAT(",

expression-src/main/src/helpers/QRunner.cls

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
public with sharing class SchemaUtils {
2+
/**
3+
* @description Returns the valid relationship name (accounting for casing) for a child relationship on an SObject.
4+
* @param potentiallyInvalidName The potentially invalid relationship name.
5+
* @param sType The SObjectType to check the relationship name against.
6+
* @return The valid relationship name.
7+
* @throws InvalidRelationshipException If the relationship name is not valid for the SObjectType.
8+
*/
9+
public static String getValidChildRelationshipName(String potentiallyInvalidName, SObjectType sType) {
10+
for (ChildRelationship relationship : sType.getDescribe().getChildRelationships()) {
11+
if (potentiallyInvalidName.equalsIgnoreCase(relationship.getRelationshipName())) {
12+
return relationship.getRelationshipName();
13+
}
14+
}
15+
16+
throw new InvalidRelationshipException(
17+
'The relationship name "' + potentiallyInvalidName + '" is not a valid relationship name for the ' + sType.getDescribe().getName() + ' object.'
18+
);
19+
}
20+
21+
public class InvalidRelationshipException extends Exception {}
22+
}
Lines changed: 13 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
/**
22
* @description Q is used to build SOQL statements
33
* @author Jean-Philippe Monette
4-
* @since 2017-03-21
54
*/
65
public class Q {
7-
private final String fromText;
8-
private Integer numberOfRows;
9-
private Integer numberOfRowsToSkip;
6+
public final String FromText { get; private set; }
7+
public Integer NumberOfRowsToQuery { get; private set; }
8+
public Integer NumberOfRowsToSkip { get; private set; }
109

11-
private final Set<String> fieldList = new Set<String>();
12-
private final List<QOrder> orders = new List<QOrder>();
13-
private final List<QICondition> conditions = new List<QICondition>();
14-
private final List<Q> subQueries = new List<Q>();
10+
public final Set<String> fieldList = new Set<String>();
11+
public final List<QOrder> orders = new List<QOrder>();
12+
public final List<QICondition> conditions = new List<QICondition>();
13+
public final List<Q> subQueries = new List<Q>();
1514

1615
public Q(SObjectType fromType) {
17-
this.fromText = String.valueOf(fromType);
18-
}
19-
public Q(String fromText) {
20-
this.fromText = fromText;
16+
this.FromText = String.valueOf(fromType);
2117
}
2218

19+
public Q(String FromText) {
20+
this.FromText = FromText;
21+
}
2322

2423
public static QOrder orderBy(String fieldName) {
2524
QOrder od = new QOrder(fieldName);
@@ -59,12 +58,12 @@ public class Q {
5958
}
6059

6160
public Q addLimit(Integer i) {
62-
this.numberOfRows = i;
61+
this.NumberOfRowsToQuery = i;
6362
return this;
6463
}
6564

6665
public Q addOffset(Integer i) {
67-
this.numberOfRowsToSkip = i;
66+
this.NumberOfRowsToSkip = i;
6867
return this;
6968
}
7069

@@ -77,70 +76,4 @@ public class Q {
7776
QAndGroup andGroup = new QAndGroup();
7877
return andGroup;
7978
}
80-
81-
public String buildSelect() {
82-
for (Q qb : this.subQueries) {
83-
this.fieldList.add('(' + qb.build() + ')');
84-
}
85-
86-
if (!this.fieldList.isEmpty()) {
87-
return 'SELECT ' + String.join(new List<String>(this.fieldList), ', ');
88-
} else {
89-
return 'SELECT Id';
90-
}
91-
}
92-
93-
public String buildConditions() {
94-
List<String> condList = new List<String>();
95-
96-
for (QICondition cond : this.conditions) {
97-
condList.add(cond.build());
98-
}
99-
100-
if (!this.conditions.isEmpty()) {
101-
return 'WHERE ' + String.join(condList, ' AND ');
102-
} else {
103-
return null;
104-
}
105-
}
106-
107-
public String buildOrderBy() {
108-
List<String> orderList = new List<String>();
109-
110-
for (QOrder order : this.orders) {
111-
orderList.add(order.build());
112-
}
113-
114-
if (!this.orders.isEmpty()) {
115-
return 'ORDER BY ' + String.join(orderList, ', ');
116-
} else {
117-
return '';
118-
}
119-
120-
}
121-
122-
public String build() {
123-
List<String> queryParts = new List<String>();
124-
125-
queryParts.add(this.buildSelect());
126-
queryParts.add('FROM ' + this.fromText);
127-
128-
if (!this.conditions.isEmpty()) {
129-
queryParts.add(this.buildConditions());
130-
}
131-
132-
if (!this.orders.isEmpty()) {
133-
queryParts.add(this.buildOrderBy());
134-
}
135-
136-
if (this.numberOfRows != null) {
137-
queryParts.add('LIMIT ' + this.numberOfRows);
138-
}
139-
140-
if (this.numberOfRowsToSkip != null) {
141-
queryParts.add('OFFSET ' + this.numberOfRowsToSkip);
142-
}
143-
144-
return String.join(queryParts, ' ');
145-
}
14679
}

0 commit comments

Comments
 (0)