Repository: paustint/soql-parser-js (ts)

Issue:
getFlattenedFields does not return accurate resulte when nested aggregate function
- [ ] Feature
- [x] Bug

<!--If this is a bug, please include the steps to reproduce the bug-->
## Description

If an aggregate function is nested within a FORMAT() call, `getFlattenedFields` does not return the correct field value.

```sql
SELECT COUNT(Id), FORMAT(MAX(CreatedDate)), Email
FROM Contact
GROUP BY Email
HAVING COUNT(Id) >= 0
```

`CreatedDate` is returned when it should actually be `expr1`

Pull request:
fixed getFlattenedFields bug #157
`getFlattenedFields()` Did not return correct results if the aggregate function was nested in another function, such as `FORMAT(MAX(CreatedDate))`

resolves #157

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4391a1d..9ccd113 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 # Changelog
 
+## 4.2.1
+
+June 18, 2021
+
+#157 - `getFlattenedFields()` Did not return correct results if the aggregate function was nested in another function, such as `FORMAT(MAX(CreatedDate))`.
+This bug only applied if there was not a field alias defined.
+
 ## 4.2.0
 
 June 8, 2021
diff --git a/src/api/public-utils.ts b/src/api/public-utils.ts
index db21e8d..82fa4b1 100644
--- a/src/api/public-utils.ts
+++ b/src/api/public-utils.ts
@@ -11,6 +11,7 @@ import {
   isGroupByFn,
   isHavingClauseWithRightCondition,
   isNegationCondition,
+  isNestedParamAggregateFunction,
   isOrderByField,
   isOrderByFn,
   isString,
@@ -209,7 +210,7 @@ export function getFlattenedFields(
             return field.alias;
           }
           // Non-aliased aggregate fields use computed name expr0, expr1, etc..
-          if (field.isAggregateFn || isAggregateResult) {
+          if (field.isAggregateFn || isNestedParamAggregateFunction(field) || isAggregateResult) {
             currUnAliasedAggExp++;
             return `expr${currUnAliasedAggExp}`;
           }
diff --git a/src/utils.ts b/src/utils.ts
index 61a043b..24f2249 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -115,6 +115,17 @@ export function getParams(functionFieldExp: FieldFunctionExpression): string[] {
   return getParams(functionFieldExp.parameters[0] as FieldFunctionExpression);
 }
 
+export function isNestedParamAggregateFunction(functionFieldExp: FieldFunctionExpression): boolean {
+  if (!functionFieldExp.parameters || functionFieldExp.parameters.length === 0) {
+    return false;
+  }
+  const parameter = functionFieldExp.parameters[0];
+  if (isString(parameter)) {
+    return false;
+  }
+  return !!parameter.isAggregateFn;
+}
+
 export function hasAlias(value: any): value is FieldWithAlias | FieldRelationshipWithAlias {
   return value && !isNil(value.alias);
 }


Test patch:
diff --git a/test/public-utils-test-data.ts b/test/public-utils-test-data.ts
index 975481a..1e9719d 100644
--- a/test/public-utils-test-data.ts
+++ b/test/public-utils-test-data.ts
@@ -446,4 +446,117 @@ export const testCases: FlattenedObjTestCase[] = [
       AcctCreatedDate: '2020-02-28T03:00:31.000+0000',
     },
   },
+  {
+    testCase: 14,
+    expectedFields: ['expr0', 'myDate', 'Email'],
+    query: {
+      fields: [
+        {
+          type: 'FieldFunctionExpression',
+          functionName: 'COUNT',
+          parameters: ['Id'],
+          isAggregateFn: true,
+          rawValue: 'COUNT(Id)',
+        },
+        {
+          type: 'FieldFunctionExpression',
+          functionName: 'FORMAT',
+          alias: 'myDate',
+          parameters: [
+            {
+              type: 'FieldFunctionExpression',
+              functionName: 'MAX',
+              parameters: ['CreatedDate'],
+              isAggregateFn: true,
+              rawValue: 'MAX(CreatedDate)',
+            },
+          ],
+          rawValue: 'FORMAT(MAX(CreatedDate))',
+        },
+        {
+          type: 'Field',
+          field: 'Email',
+        },
+      ],
+      sObject: 'Contact',
+      groupBy: [
+        {
+          field: 'Email',
+        },
+      ],
+      having: {
+        left: {
+          fn: {
+            functionName: 'COUNT',
+            parameters: ['Id'],
+            rawValue: 'COUNT(Id)',
+          },
+          operator: '>=',
+          value: '0',
+          literalType: 'INTEGER',
+        },
+      },
+    },
+    sfdcObj: {
+      expr0: 16,
+      myDate: '7/13/2018 7:07 AM',
+      Email: 'agreen@foo.com',
+    },
+  },
+  {
+    testCase: 15,
+    expectedFields: ['expr0', 'expr1', 'Email'],
+    query: {
+      fields: [
+        {
+          type: 'FieldFunctionExpression',
+          functionName: 'COUNT',
+          parameters: ['Id'],
+          isAggregateFn: true,
+          rawValue: 'COUNT(Id)',
+        },
+        {
+          type: 'FieldFunctionExpression',
+          functionName: 'FORMAT',
+          parameters: [
+            {
+              type: 'FieldFunctionExpression',
+              functionName: 'MAX',
+              parameters: ['CreatedDate'],
+              isAggregateFn: true,
+              rawValue: 'MAX(CreatedDate)',
+            },
+          ],
+          rawValue: 'FORMAT(MAX(CreatedDate))',
+        },
+        {
+          type: 'Field',
+          field: 'Email',
+        },
+      ],
+      sObject: 'Contact',
+      groupBy: [
+        {
+          field: 'Email',
+        },
+      ],
+      having: {
+        left: {
+          fn: {
+            functionName: 'COUNT',
+            parameters: ['Id'],
+            rawValue: 'COUNT(Id)',
+          },
+          operator: '>=',
+          value: '0',
+          literalType: 'INTEGER',
+        },
+      },
+    },
+    sfdcObj: {
+      expr0: 16,
+      expr1: '7/13/2018 7:07 AM',
+      Email: 'agreen@foo.com',
+    },
+  },
 ];
diff --git a/test/public-utils.spec.ts b/test/public-utils.spec.ts
index 393890c..e179c81 100644
--- a/test/public-utils.spec.ts
+++ b/test/public-utils.spec.ts
@@ -211,7 +211,7 @@ describe('getFlattenedFields', () => {
       const fields = utils.getFlattenedFields(testCase.query);
       expect(fields).to.deep.equal(testCase.expectedFields);
       fields.forEach(field => {
-        expect(lodashGet(testCase.sfdcObj, field)).to.not.be.undefined;
+        expect(lodashGet(testCase.sfdcObj, field), `${field} does not exist on sfdc record`).to.not.be.undefined;
       });
     });
   });