diff --git a/.idea/formula-evaluator.iml b/.idea/formula-evaluator.iml
index cefdb28b..a6df9590 100644
--- a/.idea/formula-evaluator.iml
+++ b/.idea/formula-evaluator.iml
@@ -18,6 +18,7 @@
+
diff --git a/docs/public/packages.json b/docs/public/packages.json
index 8d11d14f..9d058b4b 100644
--- a/docs/public/packages.json
+++ b/docs/public/packages.json
@@ -1,4 +1,4 @@
{
- "packageId": "04tRb0000044fYYIA",
+ "packageId": "04tRb0000045WPxIAM",
"componentPackageId": "04tRb0000012Mv8IAE"
}
diff --git a/expression-src/main/src/interpreter/ContextResolver.cls b/expression-src/main/src/interpreter/ContextResolver.cls
index 59ac79be..08336b64 100644
--- a/expression-src/main/src/interpreter/ContextResolver.cls
+++ b/expression-src/main/src/interpreter/ContextResolver.cls
@@ -272,7 +272,7 @@ public with sharing class ContextResolver implements Visitor {
// References to @Context fields will always go to the top level query
// as they are part of the global contextual context tied to the record Id
// from which the Evaluation was started.
- addFieldToQuery(this.topLevelQuery, referenceName.toLowerCase());
+ return addFieldToQuery(this.topLevelQuery, referenceName.toLowerCase());
} else {
this.queryContext.queryBuilder.selectField(referenceName);
}
@@ -320,7 +320,7 @@ public with sharing class ContextResolver implements Visitor {
// If context is being accessed, then we always want to run the query.
this.shouldExecuteQuery = true;
- // recordId migh be null when this is being run from within a subquery.
+ // recordId might be null when this is being run from within a subquery.
// If so, return early.
if (this.queryContext.recordId == null) {
return null;
diff --git a/expression-src/main/src/resolver/EvaluatorResolver.cls b/expression-src/main/src/resolver/EvaluatorResolver.cls
index 301adbe3..889eb89a 100644
--- a/expression-src/main/src/resolver/EvaluatorResolver.cls
+++ b/expression-src/main/src/resolver/EvaluatorResolver.cls
@@ -270,7 +270,12 @@ public with sharing abstract class EvaluatorResolver {
ContextResolver ctxInterpreter = new ContextResolver(contextsForType, contextPrefix, customFunctionDeclarations);
List queriedRecords = ctxInterpreter.build(expressions);
if (queriedRecords == null || queriedRecords.isEmpty()) {
- continue;
+ queriedRecords = new List();
+ for (Id recordId : contextByRecordId.keySet()) {
+ SObject emptyRecord = currentType.newSObject();
+ emptyRecord.Id = recordId;
+ queriedRecords.add(emptyRecord);
+ }
}
Map recordById = new Map(queriedRecords);
for (Id recordId : recordById.keySet()) {
diff --git a/package-lock.json b/package-lock.json
index 6a82e345..d24b89cc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,7 +8,7 @@
"license": "MIT",
"devDependencies": {
"@cparra/apex-reflection": "^2.4.1",
- "@cparra/apexdocs": "3.16.1",
+ "@cparra/apexdocs": "^3.17.0",
"@tailwindcss/forms": "^0.5.6",
"@types/node": "^20.10.2",
"js-yaml": "^4.1.0",
@@ -55,20 +55,20 @@
}
},
"node_modules/@cparra/apex-reflection": {
- "version": "2.21.1",
- "resolved": "https://registry.npmjs.org/@cparra/apex-reflection/-/apex-reflection-2.21.1.tgz",
- "integrity": "sha512-ciNzZnAK0ikruEn1Wj8+e0cA6K5ShFx91AwduzW2nYK6cXqs8Qr3QUAPk82WKA8437FNqKLDd7v2TJMSSvzayg==",
+ "version": "2.22.0",
+ "resolved": "https://registry.npmjs.org/@cparra/apex-reflection/-/apex-reflection-2.22.0.tgz",
+ "integrity": "sha512-c/TNhzzAmMDuzBoA1o26aaDRWnOVP8mDg5BgmjaBXbhrbQj0rInDUqeGDiM/1yU1/DdA+7Ph/dXKBFrkYbYuVQ==",
"dev": true,
"license": "ISC"
},
"node_modules/@cparra/apexdocs": {
- "version": "3.16.1",
- "resolved": "https://registry.npmjs.org/@cparra/apexdocs/-/apexdocs-3.16.1.tgz",
- "integrity": "sha512-PmwS8gvZ6+cvtLSk6pCPn3pkZRvrpyUYwaG/7k33MMTxLiHl2n3i9LD0GQ8C2v+LhBkxX8Oh2HDYOcLzEH3+ow==",
+ "version": "3.17.0",
+ "resolved": "https://registry.npmjs.org/@cparra/apexdocs/-/apexdocs-3.17.0.tgz",
+ "integrity": "sha512-ONSpQADzBj7gjC62tshhVJ9DA3Uk2dCQF98B1vNqkxXRk//Mu/LEoPmuZZKxD1IwC34TRksQ2L9XVW4sFhfG3g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@cparra/apex-reflection": "2.21.1",
+ "@cparra/apex-reflection": "2.22.0",
"@salesforce/source-deploy-retrieve": "^12.20.1",
"@types/js-yaml": "^4.0.9",
"@types/yargs": "^17.0.32",
@@ -84,6 +84,9 @@
},
"bin": {
"apexdocs": "dist/cli/generate.js"
+ },
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@cparra/apexdocs/node_modules/minimatch": {
diff --git a/package.json b/package.json
index 7c790d4c..0052db2d 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
"homepage": "https://github.com/cesarParra/formula-evaluator#readme",
"devDependencies": {
"@cparra/apex-reflection": "^2.4.1",
- "@cparra/apexdocs": "3.16.1",
+ "@cparra/apexdocs": "^3.17.0",
"@tailwindcss/forms": "^0.5.6",
"@types/node": "^20.10.2",
"js-yaml": "^4.1.0",
diff --git a/sfdx-project.json b/sfdx-project.json
index b881b7d9..69f02f99 100644
--- a/sfdx-project.json
+++ b/sfdx-project.json
@@ -11,6 +11,10 @@
{
"path": "src-pull",
"default": true
+ },
+ {
+ "path": "unpackaged/integration-tests",
+ "default": false
}
],
"name": "Expression",
diff --git a/sfdx-project_packaging.json b/sfdx-project_packaging.json
index 0e5dca50..ff4227e3 100644
--- a/sfdx-project_packaging.json
+++ b/sfdx-project_packaging.json
@@ -3,7 +3,7 @@
{
"package": "Expression",
"versionName": "Version 1.36",
- "versionNumber": "1.47.0.NEXT",
+ "versionNumber": "1.48.0.NEXT",
"path": "expression-src",
"default": false,
"versionDescription": "Expression core language",
@@ -76,6 +76,7 @@
"Expression@1.44.0-1": "04tRb000003z0hJIAQ",
"Expression@1.45.0-1": "04tRb0000042CNlIAM",
"Expression@1.46.0-1": "04tRb000004400jIAA",
- "Expression@1.47.0-1": "04tRb0000044fYYIAY"
+ "Expression@1.47.0-1": "04tRb0000044fYYIAY",
+ "Expression@1.48.0-1": "04tRb0000045WPxIAM"
}
}
\ No newline at end of file
diff --git a/unpackaged/integration-tests/classes/IntegrationTest.cls b/unpackaged/integration-tests/classes/IntegrationTest.cls
new file mode 100644
index 00000000..023522e9
--- /dev/null
+++ b/unpackaged/integration-tests/classes/IntegrationTest.cls
@@ -0,0 +1,52 @@
+@IsTest
+private class IntegrationTest {
+ @IsTest
+ static void usingChildRelationships() {
+ Account anyAccount = new Account(Name = 'Sample Account');
+ insert anyAccount;
+ Contact firstContact = new Contact(FirstName = 'First', LastName = 'Contact', AccountId = anyAccount.Id);
+ Contact secondContact = new Contact(FirstName = 'Second', LastName = 'Contact', AccountId = anyAccount.Id);
+ insert new List { firstContact, secondContact };
+
+ String expressionPiped = '@Context.Contacts -> WHERE(FirstName = "First") -> SIZE() = 1';
+ Boolean pipedResult = (Boolean)Evaluator.run(expressionPiped, anyAccount.Id);
+ Assert.isTrue(pipedResult, 'There should be exactly one contact with the first name "First".');
+
+ String expressionNested = 'SIZE(WHERE(@Context.Contacts, FirstName = "First")) = 1';
+ Boolean nestedResult = (Boolean)Evaluator.run(expressionNested, anyAccount.Id);
+ Assert.isTrue(nestedResult, 'There should be exactly one contact with the first name "First".');
+ }
+
+ @IsTest
+ static void hasPurchasedSomethingInThePast() {
+ List contexts = getContexts();
+
+ String pipedExpression = '@Customer.Assets -> WHERE(Product2Id = @Product.Id && Status = "Purchased") -> SIZE() > 0';
+ Boolean pipedResult = (Boolean)Evaluator.run(pipedExpression, contexts, new Configuration());
+ Assert.isTrue(pipedResult, 'The customer should have purchased the product in the past.');
+
+ String nestedExpression = 'SIZE(WHERE(@Customer.Assets, Product2Id = @Product.Id && Status = "Purchased")) > 0';
+ Boolean nestedResult = (Boolean)Evaluator.run(nestedExpression, contexts, new Configuration());
+ Assert.isTrue(nestedResult, 'The customer should have purchased the product in the past.');
+ }
+
+ private static List getContexts() {
+ Account someAccount = new Account(Name = 'Test Account');
+ insert someAccount;
+ Contact someone = new Contact(FirstName = 'Test', LastName = 'User', AccountId = someAccount.Id);
+ insert someone;
+ Product2 sampleProduct = new Product2(Name = 'Test Product', IsActive = true);
+ insert sampleProduct;
+
+ Asset sampleAsset = new Asset(
+ Name = 'Test Asset',
+ ContactId = someone.Id, Status = 'Purchased',
+ Product2Id = sampleProduct.Id);
+ insert sampleAsset;
+
+ CustomRecordContext customer = new CustomRecordContext('Customer', someone.Id);
+ CustomRecordContext product = new CustomRecordContext('Product', sampleProduct.Id);
+ List contexts = new List { customer, product };
+ return contexts;
+ }
+}
diff --git a/unpackaged/integration-tests/classes/IntegrationTest.cls-meta.xml b/unpackaged/integration-tests/classes/IntegrationTest.cls-meta.xml
new file mode 100644
index 00000000..82775b98
--- /dev/null
+++ b/unpackaged/integration-tests/classes/IntegrationTest.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 65.0
+ Active
+