Repository: paustint/soql-parser-js
Pull request #91 (2020-01-13 18:58:15)

Order by operator is case-sensitive #90
1. The `DESC` operator in the `ORDER BY` clause was treated as a case-sensitive field.
2. The following fields we treated as case-sensitive:
   1. `NEXT_N_FISCAL_QUARTERS`, `LAST_N_FISCAL_QUARTERS`, `N_FISCAL_QUARTERS_AGO`, `NEXT_N_FISCAL_YEARS`, `LAST_N_FISCAL_YEARS`,
   2. `mi`, `km` on `GEOLOCATION` functions
3. Updated the `DISTANCE` function to properly be tagged as `isAggregateFn=true` if used as a field
   1. This fixed an issue where `getFlattenedFields()` would throw an exception


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b015c2b..50ac19a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 # Changelog
 
+## 2.3.0
+
+Jan 13, 2020
+
+1. The `DESC` operator in the `ORDER BY` clause was treated as a case-sensitive field.
+2. The following fields we treated as case-sensitive:
+   1. `NEXT_N_FISCAL_QUARTERS`, `LAST_N_FISCAL_QUARTERS`, `N_FISCAL_QUARTERS_AGO`, `NEXT_N_FISCAL_YEARS`, `LAST_N_FISCAL_YEARS`,
+   2. `mi`, `km` on `GEOLOCATION` functions
+3. Updated the `DISTANCE` function to properly be tagged as `isAggregateFn=true` if used as a field
+   1. This fixed an issue where `getFlattenedFields()` would throw an exception
+
 ## 2.2.3
 
 Jan 4, 2020
diff --git a/src/parser/lexer.ts b/src/parser/lexer.ts
index 86e26dc..d233cbd 100644
--- a/src/parser/lexer.ts
+++ b/src/parser/lexer.ts
@@ -196,7 +196,7 @@ export const WhiteSpace = createToken({
 export const And = createToken({ name: 'AND', pattern: /AND/i, longer_alt: Identifier, categories: [Keyword, ReservedKeyword] });
 export const As = createToken({ name: 'AS', pattern: /AS/i, longer_alt: Identifier, categories: [Keyword, ReservedKeyword] });
 
-export const Desc = createToken({ name: 'DESC', pattern: /DESC/, longer_alt: Identifier, categories: [Keyword, ReservedKeyword] });
+export const Desc = createToken({ name: 'DESC', pattern: /DESC/i, longer_alt: Identifier, categories: [Keyword, ReservedKeyword] });
 export const Asc = createToken({ name: 'ASC', pattern: /ASC/i, longer_alt: Identifier, categories: [Keyword, ReservedKeyword] });
 
 // FIXME: split into two tokens, BY is a reserved keyword, order is not
@@ -658,37 +658,37 @@ export const NYearsAgo = createToken({
 });
 export const NextNFiscalQuarters = createToken({
   name: 'NEXT_N_FISCAL_QUARTERS',
-  pattern: /NEXT_N_FISCAL_QUARTERS/,
+  pattern: /NEXT_N_FISCAL_QUARTERS/i,
   longer_alt: Identifier,
   categories: [DateNLiteral, Identifier],
 });
 export const LastNFiscalQuarters = createToken({
   name: 'LAST_N_FISCAL_QUARTERS',
-  pattern: /LAST_N_FISCAL_QUARTERS/,
+  pattern: /LAST_N_FISCAL_QUARTERS/i,
   longer_alt: Identifier,
   categories: [DateNLiteral, Identifier],
 });
 export const NFiscalQuartersAgo = createToken({
   name: 'N_FISCAL_QUARTERS_AGO',
-  pattern: /N_FISCAL_QUARTERS_AGO/,
+  pattern: /N_FISCAL_QUARTERS_AGO/i,
   longer_alt: Identifier,
   categories: [DateNLiteral, Identifier],
 });
 export const NextNFiscalYears = createToken({
   name: 'NEXT_N_FISCAL_YEARS',
-  pattern: /NEXT_N_FISCAL_YEARS/,
+  pattern: /NEXT_N_FISCAL_YEARS/i,
   longer_alt: Identifier,
   categories: [DateNLiteral, Identifier],
 });
 export const LastNFiscalYears = createToken({
   name: 'LAST_N_FISCAL_YEARS',
-  pattern: /LAST_N_FISCAL_YEARS/,
+  pattern: /LAST_N_FISCAL_YEARS/i,
   longer_alt: Identifier,
   categories: [DateNLiteral, Identifier],
 });
 export const NFiscalYearsAgo = createToken({
   name: 'N_FISCAL_YEARS_AGO',
-  pattern: /N_FISCAL_YEARS_AGO/,
+  pattern: /N_FISCAL_YEARS_AGO/i,
   longer_alt: Identifier,
   categories: [DateNLiteral, Identifier],
 });
@@ -749,7 +749,7 @@ export const SignedInteger = createToken({
 });
 export const GeolocationUnit = createToken({
   name: 'GEOLOCATION_UNIT',
-  pattern: /'(mi|km)'/,
+  pattern: /'(mi|km)'/i,
   longer_alt: Identifier,
   categories: [Identifier],
 });
diff --git a/src/parser/visitor.ts b/src/parser/visitor.ts
index d0583d1..1b0f050 100644
--- a/src/parser/visitor.ts
+++ b/src/parser/visitor.ts
@@ -523,6 +523,10 @@ class SOQLVisitor extends BaseSoqlVisitor {
       },
     };
 
+    if (options.includeType) {
+      output.isAggregateFn = true;
+    }
+
     output.rawValue = `DISTANCE(${output.parameters[0]}, ${
       isString(output.parameters[1]) ? output.parameters[1] : output.parameters[1].rawValue
     }, ${output.parameters[2]})`;
diff --git a/src/utils.ts b/src/utils.ts
index 0b8cc54..6c89c3d 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -20,7 +20,7 @@ export function isStringArray(val: any[]): val is string[] {
   if (!val) {
     return false;
   }
-  return val.length > 0 && isString(val[0]);
+  return val.every(item => isString(item));
 }
 
 export function isNumber(val: any): val is number {
@@ -93,6 +93,9 @@ export function getParams(functionFieldExp: FieldFunctionExpression): string[] {
   if (isStringArray(functionFieldExp.parameters)) {
     return functionFieldExp.parameters;
   }
+  if (isString(functionFieldExp.parameters[0])) {
+    return [functionFieldExp.parameters[0]];
+  }
   return getParams(functionFieldExp.parameters[0] as FieldFunctionExpression);
 }
 


Test patch:
diff --git a/test/public-utils-test-data.ts b/test/public-utils-test-data.ts
index 7d5a8e5..bd82404 100644
--- a/test/public-utils-test-data.ts
+++ b/test/public-utils-test-data.ts
@@ -279,4 +279,38 @@ export const testCases: FlattenedObjTestCase[] = [
       Contacts: {},
     },
   },
+  {
+    testCase: 9,
+    expectedFields: ['Id', 'Name', 'Location__c', `expr0`],
+    query: {
+      fields: [
+        { type: 'Field', field: 'Id' },
+        { type: 'Field', field: 'Name' },
+        { type: 'Field', field: 'Location__c' },
+        {
+          type: 'FieldFunctionExpression',
+          functionName: 'DISTANCE',
+          isAggregateFn: true,
+          rawValue: `DISTANCE(Location__c, GEOLOCATION(-10.775, -10.775), 'MI')`,
+          parameters: [
+            'Location__c',
+            {
+              type: 'FieldFunctionExpression',
+              functionName: 'GEOLOCATION',
+              parameters: ['-10.775', '-10.775'],
+              rawValue: 'GEOLOCATION(-10.775, -10.775)',
+            },
+            `'MI'`,
+          ],
+        },
+      ],
+      sObject: 'CONTACT',
+    },
+    sfdcObj: {
+      Id: '0011800000ahbs3AAA',
+      Name: 'Amendment Demo',
+      Location__c: 'Location__c',
+      expr0: {},
+    },
+  },
 ];
diff --git a/test/public-utils.spec.ts b/test/public-utils.spec.ts
index 393890c..affaa1c 100644
--- a/test/public-utils.spec.ts
+++ b/test/public-utils.spec.ts
@@ -205,7 +205,7 @@ describe('getField', () => {
   });
 });
 
-describe('getFlattenedFields', () => {
+describe.only('getFlattenedFields', () => {
   testCases.forEach(testCase => {
     it(`Should create fields from query - Test Case: ${testCase.testCase}`, () => {
       const fields = utils.getFlattenedFields(testCase.query);
diff --git a/test/test-cases-for-is-valid.ts b/test/test-cases-for-is-valid.ts
index e57d07c..a1bb5e8 100644
--- a/test/test-cases-for-is-valid.ts
+++ b/test/test-cases-for-is-valid.ts
@@ -406,5 +406,25 @@ export const testCases: TestCaseForFormat[] = [
     soql: `SELECT Id, Name, Location, DISTANCE(GEOLOCATION(37.775,-122.418), warehouse_location__c, 'km') FROM CONTACT`,
     isValid: false,
   },
+  {
+    testCase: 147,
+    soql: `SELECT Id, Name FROM Account ORDER BY CreatedDate Desc`,
+    isValid: true,
+  },
+  {
+    testCase: 148,
+    soql: `SELECT Id, Name FROM Account ORDER BY CreatedDate desc`,
+    isValid: true,
+  },
+  {
+    testCase: 149,
+    soql: `SELECT Id, Name FROM Account ORDER BY CreatedDate Asc`,
+    isValid: true,
+  },
+  {
+    testCase: 150,
+    soql: `SELECT Id, Name FROM Account ORDER BY CreatedDate asc`,
+    isValid: true,
+  },
 ];
 export default testCases;
diff --git a/test/test-cases.ts b/test/test-cases.ts
index 5a7ab8a..3eb7b6e 100644
--- a/test/test-cases.ts
+++ b/test/test-cases.ts
@@ -1666,6 +1666,7 @@ export const testCases: TestCase[] = [
         {
           type: 'FieldFunctionExpression',
           functionName: 'DISTANCE',
+          isAggregateFn: true,
           rawValue: `DISTANCE(Location, GEOLOCATION(10, 10), 'mi')`,
           parameters: [
             'Location',
@@ -1701,6 +1702,34 @@ export const testCases: TestCase[] = [
       groupBy: { field: ['BillingState', 'BillingStreet'] },
     },
   },
+  {
+    testCase: 92,
+    soql: `SELECT Id, Name, Location__c, DISTANCE(Location__c, GEOLOCATION(-10.775, -10.775), 'MI') FROM CONTACT`,
+    output: {
+      fields: [
+        { type: 'Field', field: 'Id' },
+        { type: 'Field', field: 'Name' },
+        { type: 'Field', field: 'Location__c' },
+        {
+          type: 'FieldFunctionExpression',
+          functionName: 'DISTANCE',
+          isAggregateFn: true,
+          rawValue: `DISTANCE(Location__c, GEOLOCATION(-10.775, -10.775), 'MI')`,
+          parameters: [
+            'Location__c',
+            {
+              type: 'FieldFunctionExpression',
+              functionName: 'GEOLOCATION',
+              parameters: ['-10.775', '-10.775'],
+              rawValue: 'GEOLOCATION(-10.775, -10.775)',
+            },
+            `'MI'`,
+          ],
+        },
+      ],
+      sObject: 'CONTACT',
+    },
+  },
 ];
 
 export default testCases;
diff --git a/test/test.spec.ts b/test/test.spec.ts
index 73f59e7..6d08832 100644
--- a/test/test.spec.ts
+++ b/test/test.spec.ts
@@ -31,7 +31,7 @@ const replacements = [{ matching: / last /i, replace: ' LAST ' }];
 
 // describe.only('Test valid queries', () => {
 //   testCasesForIsValid
-//     .filter(testCase => testCase.testCase === 137)
+//     .filter(testCase => testCase.testCase === 147)
 //     .forEach(testCase => {
 //       it(`should identify validity of query - test case ${testCase.testCase} - ${testCase.soql}`, () => {
 //         const soqlQuery = parseQuery(testCase.soql, { logErrors: true, ...testCase.options });