Repository: paustint/soql-parser-js (ts)
Issue: getFlattenedFields should allow Query or FieldSubquery or Subquery
<!--
  Please include the following classification.
  If your classification is not added, you can modify the list below
-->
- [ ] Feature
- [x] Bug Request

<!--If this is a bug, please include the steps to reproduce the bug-->
## Description

Now that a subquery has a defined type, `parseFields` no longer works for subqueries.
Pull request description: getFlattenedFields should allow Query or FieldSubquery or Subquery #81
Updated getFlattenedFields to allow being passed Subquery or a FieldSubquery
A new helper method isFieldSubquery(value: any) was added to allow determining if a Field is a FieldSubquery. This is used internally for getFlattenedFields().

resolves #81

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9c6e8af..aed5f44 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
 # Changelog
 
+## 2.1.0
+
+1. The method signature for `getFlattenedFields` has changed to allow `Query | Subquery | FieldSubquery` to be passed in. this is not being considered a breaking change because it is fully backwards compatible.
+2. A new helper method `isFieldSubquery(value: any)` was added to allow determining if a Field is a FieldSubquery. This is used internally for `getFlattenedFields()`.
+
 ## 2.0.0
 
 ### Summary
diff --git a/README.md b/README.md
index 1f00f28..4f3db73 100644
--- a/README.md
+++ b/README.md
@@ -46,11 +46,12 @@ isQueryValid('SELECT Id Foo FROM Baz'); // false
 
 ## Utility Functions
 
-| Function           | Description                                                                                                                                                               | Arguments                                  |
-| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
-| getField           | Convenience method to construct fields in the correct format when using `composeQuery()`. Look in the data models section below for the structure of `ComposeFieldInput`. | input: `string | ComposeFieldInput`        |
-| isSubquery         | Returns `true` if the data passed in is a subquery.                                                                                                                       | query: `Query | Subquery`                  |
-| getFlattenedFields | Flatten a Salesforce record based on the parsed SOQL Query.                                                                                                               | soql: Query<br> config?: SoqlComposeConfig |
+| Function           | Description                                                                                                                                                               | Arguments                                                                 |
+| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
+| getField           | Convenience method to construct fields in the correct format when using `composeQuery()`. Look in the data models section below for the structure of `ComposeFieldInput`. | input: `string | ComposeFieldInput`                                       |
+| isSubquery         | Returns `true` if the data passed in is a subquery.                                                                                                                       | query: `Query | Subquery`                                                 |
+| isFieldSubquery    | Returns `true` if the data passed in is a FieldSubquery.                                                                                                                  | value: `any`                                                              |
+| getFlattenedFields | Flatten a Salesforce record based on the parsed SOQL Query.                                                                                                               | soql: `Query | Subquery | FieldSubquery`<br> config?: `SoqlComposeConfig` |
 
 **ParseQueryConfig**
 
@@ -382,7 +383,7 @@ The following utility functions are available:
    2. returns one of the following data structures: `SoqlModels.FieldFunctionExpression | SoqlModels.Field | SoqlModels.FieldRelationship | SoqlModels.FieldSubquery | SoqlModels.FieldTypeOf`.
 2. `isSubquery(query: Query | Subquery)`
    1. Returns `true` if the data passed in is a subquery
-3. `getFlattenedFields(query: Query)`
+3. `getFlattenedFields(query: Query | Subquery | FieldSubquery)`
    1. Flatten a Salesforce record based on the parsed SOQL Query. this is useful if you have relationships in your query and want to show the results in a table, using `.` dot notation for the relationship field headings.
    2. Returns an array of strings.
    3. Refer to `tests/publicUtils.spec.ts` for usage examples.
diff --git a/src/api/public-utils.ts b/src/api/public-utils.ts
index 87527ed..a212900 100644
--- a/src/api/public-utils.ts
+++ b/src/api/public-utils.ts
@@ -8,6 +8,7 @@ import {
   isComposeFieldTypeof,
   isString,
   isSubquery,
+  isFieldSubquery,
 } from '../utils';
 
 export { isSubquery };
@@ -113,11 +114,14 @@ export function getField(input: string | ComposeFieldInput): SoqlModels.FieldTyp
  * @param [isAggregateResult] pass in true to force expr0...1 for all non-aliased functions even if field is not explicitly an aggregate expression
  * @returns flattened fields
  */
-export function getFlattenedFields(query: SoqlModels.Query, isAggregateResult?: boolean): string[] {
+export function getFlattenedFields(
+  query: SoqlModels.Query | SoqlModels.Subquery | SoqlModels.FieldSubquery,
+  isAggregateResult?: boolean,
+): string[] {
+  query = isFieldSubquery(query) ? query.subquery : query;
   const fields = query.fields;
   let currUnAliasedAggExp = -1;
-
-  let sObject = (query.sObject || '').toLowerCase();
+  let sObject = (isSubquery(query) ? query.relationshipName : query.sObject || '').toLowerCase();
   let sObjectAlias = (query.sObjectAlias || '').toLowerCase();
 
   const parsedFields = fields
diff --git a/src/utils.ts b/src/utils.ts
index 6efee75..0b8cc54 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,5 +1,5 @@
 import { IToken } from 'chevrotain';
-import { FieldFunctionExpression, LiteralType, Query, Subquery, WhereClause, ValueQuery, Condition } from './api/api-models';
+import { FieldFunctionExpression, LiteralType, Query, Subquery, WhereClause, ValueQuery, Condition, FieldSubquery } from './api/api-models';
 import { ComposeField, ComposeFieldFunction, ComposeFieldRelationship, ComposeFieldSubquery, ComposeFieldTypeof } from './api/public-utils';
 import { isUndefined } from 'util';
 
@@ -76,6 +76,10 @@ export function isSubquery(query: Query | Subquery): query is Subquery {
   return isString((query as any).relationshipName);
 }
 
+export function isFieldSubquery(value: any): value is FieldSubquery {
+  return value && value.type && value.type === 'FieldSubquery';
+}
+
 /**
  * Gets params from a FieldFunctionExpression. If there are multiple nested functions as multiple parameters
  * within another function, only the first argument will be considered.


Test patch:
diff --git a/test/public-utils-test-data.ts b/test/public-utils-test-data.ts
index 488e6f5..7d5a8e5 100644
--- a/test/public-utils-test-data.ts
+++ b/test/public-utils-test-data.ts
@@ -259,4 +259,24 @@ export const testCases: FlattenedObjTestCase[] = [
       },
     },
   },
+  {
+    testCase: 8,
+    expectedFields: ['Id', 'Contacts'],
+    query: {
+      fields: [
+        {
+          type: 'Field',
+          field: 'Id',
+        },
+        { type: 'FieldSubquery', subquery: { fields: [{ type: 'Field', field: 'LastName' }], relationshipName: 'Contacts' } },
+      ],
+      sObject: 'Account',
+    },
+    sfdcObj: {
+      Id: '0011800000ahbs3AAA',
+      Name: 'Amendment Demo',
+      BillingCity: 'Missoula',
+      Contacts: {},
+    },
+  },
 ];
diff --git a/test/public-utils.spec.ts b/test/public-utils.spec.ts
index 837a196..393890c 100644
--- a/test/public-utils.spec.ts
+++ b/test/public-utils.spec.ts
@@ -2,6 +2,7 @@ import * as utils from '../src/api/public-utils';
 import { expect } from 'chai';
 import 'mocha';
 import { testCases } from './public-utils-test-data';
+import { FieldSubquery, Query } from '../src';
 const lodashGet = require('lodash.get');
 
 describe('getField', () => {
@@ -214,4 +215,22 @@ describe('getFlattenedFields', () => {
       });
     });
   });
+
+  it(`Should allow a FieldSubquery to be passed in`, () => {
+    const fieldSubquery: FieldSubquery = {
+      type: 'FieldSubquery',
+      subquery: { fields: [{ type: 'Field', field: 'LastName' }], relationshipName: 'Contacts' },
+    };
+    const fields = utils.getFlattenedFields(fieldSubquery);
+    expect(fields).to.deep.equal(['LastName']);
+  });
+
+  it(`Should allow a Subquery to be passed in`, () => {
+    const fieldSubquery: FieldSubquery = {
+      type: 'FieldSubquery',
+      subquery: { fields: [{ type: 'Field', field: 'LastName' }], relationshipName: 'Contacts' },
+    };
+    const fields = utils.getFlattenedFields(fieldSubquery.subquery);
+    expect(fields).to.deep.equal(['LastName']);
+  });
 });