Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

soql-parser-js

jetstreamapp48.1kMITdeprecated5.0.2TypeScript support: included

This package has been moved to the @jetstreamapp organization starting on version 6.0.0. Update your dependency to use @jetstreamapp/soql-parser-js.

Salesforce.com SOQL parser and composer

soql, salesforce, parse, compose, parser

readme

build npm version dependencies

Description

This library allows parsing and composing SOQL queries from Salesforce using JavaScript or Typescript.

Available Features:

  1. Parse SOQL queries into a common Query data structure.
  2. Deterministically compose a Query data structure back into a SOQL query string.
  3. Validate a query to check if the syntax is valid.
    1. Even if a query is returned as valid, it might still be invalid based on your Salesforce configuration

Migrating from version 1 to version 2? Check out the changelog for a full list of changes.

Migrating from version 2 to version 3? Check out the changelog for a full list of changes.

Documentation

Read the documentation on our docs site.

Compatibility

Node: version 11 or higher, or a polyfill for Array.flat
Browser: Tested in all modern browsers, may not work with older browsers.

The commander dependency is only required for the cli, the other two dependencies chevrotain and lodash.get are bundled with the non-cli code.

Quick Start

import { parseQuery, composeQuery, isQueryValid } from 'soql-parser-js';

const query = parseQuery(`SELECT Id FROM Account WHERE Id = 'FOO'`);
console.log('query', query);

const soql = composeQuery(query);
console.log('soql', soql); // SELECT Id FROM Account WHERE Id = 'FOO'

isQueryValid('SELECT Id, Foo FROM Baz'); // true
isQueryValid('SELECT Id Foo FROM Baz'); // false

Available Features

Function Description Arguments
parseQuery Parse a SOQL query string into a Query data structure. soql: Query
config?: ParseQueryConfig
isQueryValid Returns true if the query was able to be parsed. soql: Query
config?: ParseQueryConfig
composeQuery Turn a Query object back into a SOQL statement. soql: Query
config?: SoqlComposeConfig
formatQuery Format a SOQL query string. soql: Query
config?: FormatOptions

Utility Functions

General Utility

Many of hte utility functions are provided to easily determine the shape of specific data since there are many variants. If you are using Typescript in strict mode, you can use these to narrow your types.

Function Description Arguments
hasAlias Returns true if the field passed in has the alias property. input: string | ComposeFieldInput
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
getFlattenedFields 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. soql: Query | Subquery | FieldSubquery
config?: SoqlComposeConfig
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
isWhereClauseWithRightCondition Returns true if the value passed in is a WhereClause with an operator and right property value: WhereClause
isHavingClauseWithRightCondition Returns true if the value passed in is a HavingClause with an operator and right property value: HavingClause
isWhereOrHavingClauseWithRightCondition Returns true if the value passed in is a WhereClause or HavingClause with an operator and right property value: WhereClause | HavingClause
isValueCondition Returns true if the value passed in has field, operator and value properties value: Condition
isValueWithDateLiteralCondition Returns true if the value passed in has field, operator and value properties and has a literalType property that is DATE_LITERAL of ['DATE_LITERAL',...] value: Condition
isValueWithDateNLiteralCondition Returns true if the value passed in has field, operator, value and dateLiteralVariable properties value: Condition
isValueFunctionCondition Returns true if the value passed in has fn, operator and value properties value: Condition
isNegationCondition Returns true if the value passed in has a openParen property and does not have fn, field, operator, value, and closeParen properties value: Condition
isValueQueryCondition Returns true if the value passed in has field, operator and valueQuery properties and does not have a value property value: Condition | ValueQueryCondition
isOrderByField Returns true if the value passed in has field property value: OrderByClause
isOrderByFn Returns true if the value passed in has fn property value: OrderByClause
isGroupByField Returns true if the value passed in has field property value: GroupByClause
isGroupByFn Returns true if the value passed in has fn property value: GroupByClause

ParseQueryConfig

Property Type Description required default
allowApexBindVariables boolean Determines if apex variables are allowed in parsed query. Example: WHERE Id IN :accountIds. Only simple Apex is supported. Function calls are not supported. (e.x. accountMap.keyset() is not supported) FALSE FALSE
allowPartialQuery boolean If provided, you can provide an incomplete soql query. This is useful if you need to parse WHERE clauses, for example. Subqueries are required to be valid. FALSE FALSE
ignoreParseErrors boolean If set to true, then queries with partially invalid syntax will still be parsed, but any clauses with invalid parts will be omitted. The SELECT clause and FROM clause must always be valid, but all other clauses can contain invalid parts. FALSE FALSE
logErrors boolean If true, parsing and lexing errors will be logged to the console. FALSE FALSE

SoqlComposeConfig

Property Type Description required default
format boolean Apply formatting the the composed query. This will result in a multi-line soql statement. FALSE TRUE
formatOptions FormatOptions Options to apply to the formatter. FALSE
autoCompose boolean If you need to compose just part of a query, you can create your own instance of the Compose class and set this to false, then call any methods that you need to just for what you would like to turn into a SOQL query. FALSE TRUE
logging boolean Print out logging statements to the console about the format operation. FALSE FALSE

FormatOptions

Property Type Description required default
numIndent number The number of tab characters to indent. FALSE 1
fieldMaxLineLength number The number of characters that the fields should take up before making a new line. Set this to 1 to have every field on its own line. FALSE 60
fieldSubqueryParensOnOwnLine boolean If true, the opening and closing parentheses will be on their own line for subqueries. FALSE TRUE
newLineAfterKeywords boolean Adds a new line and indent after all keywords (such as SELECT, FROM, WHERE, ORDER BY, etc..) Setting this to true will add new lines in other places as well, such as complex WHERE clauses FALSE FALSE
whereClauseOperatorsIndented boolean Deprecated If true, indents the where clause operators. FALSE FALSE
logging boolean Print out logging statements to the console about the format operation. FALSE FALSE

Examples

Parsing Queries

Parsing a SOQL query can be completed by calling parseQuery(soqlQueryString). A Query data structure will be returned.

import { parseQuery } from 'soql-parser-js';

const soql = `
  SELECT UserId, COUNT(Id)
  FROM LoginHistory
  WHERE LoginTime > 2010-09-20T22:16:30.000Z
  AND LoginTime < 2010-09-21T22:16:30.000Z
  GROUP BY UserId
`;

const soqlQuery = parseQuery(soql);

console.log(JSON.stringify(soqlQuery, null, 2));
<summary>Results (click to show)</summary>
{
  "fields": [
    {
      "type": "Field",
      "field": "UserId"
    },
    {
      "type": "FieldFunctionExpression",
      "functionName": "COUNT",
      "parameters": ["Id"],
      "isAggregateFn": true,
      "rawValue": "COUNT(Id)"
    }
  ],
  "sObject": "LoginHistory",
  "where": {
    "left": {
      "field": "LoginTime",
      "operator": ">",
      "value": "2010-09-20T22:16:30.000Z",
      "literalType": "DATETIME"
    },
    "operator": "AND",
    "right": {
      "left": {
        "field": "LoginTime",
        "operator": "<",
        "value": "2010-09-21T22:16:30.000Z",
        "literalType": "DATETIME"
      }
    }
  },
  "groupBy": {
    "field": "UserId"
  }
}

Parsing a partial query

Added support for allowPartialQuery in version 4.4.0

import { parseQuery } from 'soql-parser-js';

const soql = `
  WHERE LoginTime > 2010-09-20T22:16:30.000Z
  AND LoginTime < 2010-09-21T22:16:30.000Z
  GROUP BY UserId
`;

const soqlQuery = parseQuery(soql, { allowPartialQuery: true });

console.log(JSON.stringify(soqlQuery, null, 2));
<summary>Results (click to show)</summary>
{
  "where": {
    "left": {
      "field": "LoginTime",
      "operator": ">",
      "value": "2010-09-20T22:16:30.000Z",
      "literalType": "DATETIME"
    },
    "operator": "AND",
    "right": {
      "left": {
        "field": "LoginTime",
        "operator": "<",
        "value": "2010-09-21T22:16:30.000Z",
        "literalType": "DATETIME"
      }
    }
  },
  "groupBy": {
    "field": "UserId"
  }
}

Validating Queries

import { isQueryValid } from 'soql-parser-js';

const invalidSoql = `SELECT UserId, COUNT(Id) Account`;
const validSoql = `SELECT UserId, COUNT(Id) Account`;

console.log(isQueryValid(soql));
console.log(isQueryValid(soql));

Composing Queries

Build a Query data structure to have it converted back into a SOQL query.

Composing a query will turn a Query object back to a SOQL query string. The exact same data structure returned from parseQuery() can be used, but depending on your use-case, you may need to build your own data structure to compose a query. These examples show building your own Query object with the minimum required fields.

Some utility methods have been provided to make it easier to build the field data structures.

Note: Some operators may be converted to uppercase (e.x. NOT, AND)

Note: There are a number of fields populated on the Query object when parseQuery() is called that are not required to compose a query. Look at the examples below and the comments in the data model for more information.

import { composeQuery, getField, Query } from 'soql-parser-js';

// Build a subquery
const oppLineItemsSubquery = {
  fields: [
    getField('Quantity'),
    getField('ListPrice'),
    getField({
      field: 'UnitPrice',
      relationships: ['PricebookEntry'],
    }),
    getField({
      field: 'Name',
      relationships: ['PricebookEntry'],
    }),
  ],
  relationshipName: 'OpportunityLineItems',
};

// build the main query and add the subquery as a field
const soqlQuery: Query = {
  fields: [
    getField('Id'),
    getField('Name'),
    getField({
      functionName: 'FORMAT',
      parameters: 'Amount',
      alias: 'MyFormattedAmount',
    }),
    getField({ subquery: oppLineItemsSubquery }),
  ],
  sObject: 'Opportunity',
  where: {
    left: {
      field: 'CreatedDate',
      operator: '>',
      value: 'LAST_N_YEARS:1',
    },
    operator: 'AND',
    right: {
      left: {
        field: 'StageName',
        operator: '=',
        value: 'Closed Won',
        // literalType is optional, but if set to STRING and our value is not already wrapped in "'", they will be added
        // All other literalType values are ignored when composing a query
        literalType: 'STRING',
      },
    },
  },
  limit: 150,
};

const composedQuery = composeQuery(soqlQuery, { format: true });

console.log(composedQuery);

Results

SELECT Id, Name, FORMAT(Amount) MyFormattedAmount,
  (
    SELECT Quantity, ListPrice, PricebookEntry.UnitPrice,
      PricebookEntry.Name
    FROM OpportunityLineItems
  )
FROM Opportunity
WHERE CreatedDate > LAST_N_YEARS:1
AND StageName = 'Closed Won'
LIMIT 150

Composing a partial query

Starting in version 4.4, compose will not fail if there are missing SELECT and FROM clauses in your query.

Partial compose support it supported without any additional steps.

import { Compose, parseQuery } from 'soql-parser-js';

const soql = `WHERE Name LIKE 'A%' AND MailingCity = 'California`;
const parsedQuery = parseQuery(soql, { allowPartialQuery: true });

// Results of Parsed Query:
/**
{
  where: {
    left: { field: 'Name', operator: 'LIKE', value: "'A%'", literalType: 'STRING' },
    operator: 'AND',
    right: { left: { field: 'MailingCity', operator: '=', value: "'California'", literalType: 'STRING' } },
  },
}
*/

const composedQuery = composeQuery(soqlQuery, { format: true });

console.log(composedQuery);

Results

WHERE Name LIKE 'A%' AND MailingCity = 'California
<summary>See the alternate way to compose partial queries by calling the Compose class directly</summary>

If you need to compose just a part of a query instead of the entire query, you can create an instance of the Compose class directly.

For example, if you just need the WHERE clause from a query as a string, you can do the following:

import { Compose, parseQuery } from 'soql-parser-js';

const soql = `SELECT Id FROM Account WHERE Name = 'Foo'`;
const parsedQuery = parseQuery(soql);

// Results of Parsed Query:
  // const parsedQuery = {
  //   fields: [
  //     {
  //       type: 'Field',
  //       field: 'Id',
  //     },
  //   ],
  //   sObject: 'Account',
  //   where: {
  //     left: {
  //       field: 'Name',
  //       operator: '=',
  //       value: "'Foo'",
  //       literalType: 'STRING',
  //     },
  //   },
  // };

  // Create a new instance of the compose class and set the autoCompose to false to avoid composing the entire query
  const composer = new Compose(parsedQuery, { autoCompose: false });


  const whereClause = composer.parseWhereOrHavingClause(parsedQuery.where);

  console.log(whereClause);
}

Available methods on the Compose class

These are used internally, but are public and available for use.

parseQuery(query: Query | Subquery): string;
parseFields(fields: FieldType[]): { text: string; typeOfClause?: string[] }[];
parseTypeOfField(typeOfField: FieldTypeOf): string[];
parseWhereOrHavingClause(whereOrHaving: WhereClause | HavingClause, tabOffset = 0, priorConditionIsNegation = false): string;
parseGroupByClause(groupBy: GroupByClause | GroupByClause[]): string;
parseOrderBy(orderBy: OrderByClause | OrderByClause[]): string;
parseWithDataCategory(withDataCategory: WithDataCategoryClause): string;

Format Query

This function is provided as a convenience and just calls parse and compose. Check out the demo to see the outcome of the various format options.

import { formatQuery } from 'soql-parser-js';

const query = `SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue, BillingAddress, BillingCity, BillingCountry, BillingGeocodeAccuracy, ShippingStreet, Sic, SicDesc, Site, SystemModstamp, TickerSymbol, Type, Website, (SELECT Id, Name, AccountId, Amount, CampaignId, CloseDate, CreatedById, Type FROM Opportunities), (SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue, BillingAddress, Website FROM ChildAccounts) FROM Account WHERE Name LIKE 'a%' OR Name LIKE 'b%' OR Name LIKE 'c%'`;

const formattedQuery1 = formatQuery(query);
const formattedQuery2 = formatQuery(query, {
  fieldMaxLineLength: 20,
  fieldSubqueryParensOnOwnLine: false,
  whereClauseOperatorsIndented: true,
});
const formattedQuery3 = formatQuery(query, { fieldSubqueryParensOnOwnLine: true, whereClauseOperatorsIndented: true });
-- formattedQuery1
SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue,
    BillingAddress, BillingCity, BillingCountry, BillingGeocodeAccuracy,
    ShippingStreet, Sic, SicDesc, Site, SystemModstamp, TickerSymbol, Type,
    Website,
    (
        SELECT Id, Name, AccountId, Amount, CampaignId, CloseDate,
            CreatedById, Type
        FROM Opportunities
    ),
    (
        SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue,
            BillingAddress, Website
        FROM ChildAccounts
    )
FROM Account
WHERE Name LIKE 'a%'
OR Name LIKE 'b%'
OR Name LIKE 'c%'

-- formattedQuery2
SELECT Id, Name,
    AccountNumber, AccountSource,
    AnnualRevenue, BillingAddress,
    BillingCity, BillingCountry,
    BillingGeocodeAccuracy, ShippingStreet,
    Sic, SicDesc, Site,
    SystemModstamp, TickerSymbol, Type,
    Website,
    (SELECT Id, Name,
        AccountId, Amount, CampaignId,
        CloseDate, CreatedById, Type
    FROM Opportunities),
    (SELECT Id, Name,
        AccountNumber, AccountSource,
        AnnualRevenue, BillingAddress,
        Website
    FROM ChildAccounts)
FROM Account
WHERE Name LIKE 'a%'
    OR Name LIKE 'b%'
    OR Name LIKE 'c%'


-- formattedQuery3
SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue,
    BillingAddress, BillingCity, BillingCountry, BillingGeocodeAccuracy,
    ShippingStreet, Sic, SicDesc, Site, SystemModstamp, TickerSymbol, Type,
    Website,
    (
        SELECT Id, Name, AccountId, Amount, CampaignId, CloseDate,
            CreatedById, Type
        FROM Opportunities
    ),
    (
        SELECT Id, Name, AccountNumber, AccountSource, AnnualRevenue,
            BillingAddress, Website
        FROM ChildAccounts
    )
FROM Account
WHERE Name LIKE 'a%'
    OR Name LIKE 'b%'
    OR Name LIKE 'c%'

CLI

Install globally or use npx to interact with the cli.

Available Commands

  • soql-parser-js --help (or using npx: npx soql-parser-js --help)
  • soql-parser-js parse --help
  • soql-parser-js compose --help
  • soql-parser-js format --help

Examples

Parse

npx soql-parser-js parse "SELECT Id FROM Account"

{"fields":[{"type":"Field","field":"Id"}],"sObject":"Account"}

Compose

npx soql-parser-js compose "{\"fields\":[{\"type\":\"Field\",\"field\":\"Id\"}],\"sObject\":\"Account\"}"

SELECT Id FROM Account

npx soql-parser-js compose "{\"fields\":[{\"type\":\"Field\",\"field\":\"Id\"}],\"sObject\":\"Account\"}" --json or -j

{ "query": "SELECT Id FROM Account" }

Format

npx soql-parser-js format "SELECT Name, COUNT(Id) FROM Account GROUP BY Name HAVING COUNT(Id) > 1"

SELECT Name, COUNT(Id)
FROM Account
GROUP BY Name
HAVING COUNT(Id) > 1

npx soql-parser-js format "SELECT Name, COUNT(Id) FROM Account GROUP BY Name HAVING COUNT(Id) > 1 -j

{ "query": "SELECT Name, COUNT(Id)\nFROM Account\nGROUP BY Name\nHAVING COUNT(Id) > 1" }

Is Valid

npx soql-parser-js valid "SELECT Id FROM Account"

true

npx soql-parser-js valid "SELECT Id invalid FROM Account"

ℹ️ this returns an exit code of 1

false

npx soql-parser-js valid "SELECT Id FROM Account" -j

{ "isValid": true }

npx soql-parser-js valid "SELECT Id invalid invalid FROM Account" -j

ℹ️ this returns an exit code of 0

{ "isValid": false }

List of options

soql-parser-js --help

Usage: soql-parser-js [options] [command]

Options:
  -h, --help                 output usage information

Commands:
  parse [options] <sql>
  compose [options] <query>
  format [options] <sql>
  valid <sql>

soql-parser-js parse --help

Usage: parse [options] <sql>

Options:
  -a, --allow-apex     allow apex bind variables
  -p, --allow-partial  allow partial queries
  -i, --ignore-errors  ignore parse errors, return as much of query as possible
  -h, --help           output usage information

soql-parser-js compose --help

Usage: compose [options] <query>

Options:
  -f, --format                   format output
  -i --indent <chars>            number of tab characters to indent (default: 1)
  -m --line-length <chars>       max number of characters per lins (default: 60)
  -s --subquery-parens-new-line  subquery parens on own line
  -k --keywords-new-line         new line after keywords
  -j, --json                     output as JSON
  -h, --help                     output usage information

soql-parser-js format --help

Usage: format [options] <sql>

Options:
  -a, --allow-apex     allow apex bind variables
  -p, --allow-partial  allow partial queries
  -i --indent <chars>            number of tab characters to indent (default: 1)
  -m --line-length <chars>       max number of characters per lins (default: 60)
  -s --subquery-parens-new-line  subquery parens on own line
  -k --keywords-new-line         new line after keywords
  -j, --json                     output as JSON
  -h, --help                     output usage information

soql-parser-js valid --help

Usage: valid [options] <sql>

Options:
  -a, --allow-apex     allow apex bind variables
  -p, --allow-partial  allow partial queries
  -j, --json  output as JSON
  -h, --help  output usage information

Data Models

Query

export type LogicalOperator = 'AND' | 'OR' | 'NOT';
export type Operator = '=' | '!=' | '<=' | '>=' | '>' | '<' | 'LIKE' | 'IN' | 'NOT IN' | 'INCLUDES' | 'EXCLUDES';
export type FieldTypeOfConditionType = 'WHEN' | 'ELSE';
export type GroupSelector = 'ABOVE' | 'AT' | 'BELOW' | 'ABOVE_OR_BELOW';
export type ForClause = 'VIEW' | 'UPDATE' | 'REFERENCE';
export type UpdateClause = 'TRACKING' | 'VIEWSTAT';
export type LiteralType =
  | 'STRING'
  | 'INTEGER'
  | 'DECIMAL'
  | 'INTEGER_WITH_CURRENCY_PREFIX'
  | 'DECIMAL_WITH_CURRENCY_PREFIX'
  | 'BOOLEAN'
  | 'NULL'
  | 'DATETIME'
  | 'DATE'
  | 'DATE_LITERAL'
  | 'DATE_N_LITERAL'
  | 'APEX_BIND_VARIABLE';
export type FieldType =
  | Field
  | FieldWithAlias
  | FieldFunctionExpression
  | FieldRelationship
  | FieldRelationshipWithAlias
  | FieldSubquery
  | FieldTypeOf;
export type OrderByCriterion = 'ASC' | 'DESC';
export type NullsOrder = 'FIRST' | 'LAST';
export type GroupByType = 'CUBE' | 'ROLLUP';
export type DateLiteral =
  | 'YESTERDAY'
  | 'TODAY'
  | 'TOMORROW'
  | 'LAST_WEEK'
  | 'THIS_WEEK'
  | 'NEXT_WEEK'
  | 'LAST_MONTH'
  | 'THIS_MONTH'
  | 'NEXT_MONTH'
  | 'LAST_90_DAYS'
  | 'NEXT_90_DAYS'
  | 'THIS_QUARTER'
  | 'LAST_QUARTER'
  | 'NEXT_QUARTER'
  | 'THIS_YEAR'
  | 'LAST_YEAR'
  | 'NEXT_YEAR'
  | 'THIS_FISCAL_QUARTER'
  | 'LAST_FISCAL_QUARTER'
  | 'NEXT_FISCAL_QUARTER'
  | 'THIS_FISCAL_YEAR'
  | 'LAST_FISCAL_YEAR'
  | 'NEXT_FISCAL_YEAR';

export type DateNLiteral =
  | 'YESTERDAY'
  | 'NEXT_N_DAYS'
  | 'LAST_N_DAYS'
  | 'N_DAYS_AGO'
  | 'NEXT_N_WEEKS'
  | 'LAST_N_WEEKS'
  | 'N_WEEKS_AGO'
  | 'NEXT_N_MONTHS'
  | 'LAST_N_MONTHS'
  | 'N_MONTHS_AGO'
  | 'NEXT_N_QUARTERS'
  | 'LAST_N_QUARTERS'
  | 'N_QUARTERS_AGO'
  | 'NEXT_N_YEARS'
  | 'LAST_N_YEARS'
  | 'N_YEARS_AGO'
  | 'NEXT_N_FISCAL_QUARTERS'
  | 'LAST_N_FISCAL_QUARTERS'
  | 'N_FISCAL_QUARTERS_AGO'
  | 'NEXT_N_FISCAL_YEARS'
  | 'LAST_N_FISCAL_YEARS'
  | 'N_FISCAL_YEARS_AGO';

export interface Field {
  type: 'Field';
  field: string;
  alias?: string;
}

export interface FieldWithAlias extends Field {
  objectPrefix: string;
  rawValue: string;
}

export interface FieldFunctionExpression {
  type: 'FieldFunctionExpression';
  functionName: string;
  parameters: (string | FieldFunctionExpression)[];
  alias?: string;
  isAggregateFn?: boolean; // not required for compose, will be populated if SOQL is parsed
  rawValue?: string; // not required for compose, will be populated if SOQL is parsed
}

export interface FieldRelationship {
  type: 'FieldRelationship';
  field: string;
  relationships: string[];
  rawValue?: string; // not required for compose, will be populated if SOQL is parsed with the raw value of the entire field
}

export interface FieldRelationshipWithAlias extends FieldRelationship {
  objectPrefix: string;
  alias: string;
}

export interface FieldSubquery {
  type: 'FieldSubquery';
  subquery: Subquery;
}

export interface FieldTypeOf {
  type: 'FieldTypeof';
  field: string;
  conditions: FieldTypeOfCondition[];
}

export interface FieldTypeOfCondition {
  type: FieldTypeOfConditionType;
  objectType?: string; // not present when ELSE
  fieldList: string[];
}

export interface QueryBase {
  fields?: FieldType[];
  sObjectAlias?: string;
  usingScope?: string;
  where?: WhereClause;
  limit?: number;
  offset?: number;
  groupBy?: GroupByClause;
  orderBy?: OrderByClause | OrderByClause[];
  withDataCategory?: WithDataCategoryClause;
  withSecurityEnforced?: boolean;
  withAccessLevel?: boolean;
  for?: ForClause;
  update?: UpdateClause;
}

export interface Query extends QueryBase {
  sObject?: string;
}

export interface Subquery extends QueryBase {
  relationshipName: string;
  sObjectPrefix?: string[];
}

export type WhereClause = WhereClauseWithoutOperator | WhereClauseWithRightCondition;

export interface WhereClauseWithoutOperator {
  left: ConditionWithValueQuery;
}

export interface WhereClauseWithRightCondition extends WhereClauseWithoutOperator {
  operator: LogicalOperator;
  right: WhereClause;
}

export type Condition =
  | ValueCondition
  | ValueWithDateLiteralCondition
  | ValueWithDateNLiteralCondition
  | ValueFunctionCondition
  | NegationCondition;

export type ConditionWithValueQuery = Condition | ValueQueryCondition;

export interface OptionalParentheses {
  openParen?: number;
  closeParen?: number;
}

export interface ValueCondition extends OptionalParentheses {
  field: string;
  operator: Operator;
  value: string | string[];
  literalType?: LiteralType | LiteralType[];
}

export interface ValueWithDateLiteralCondition extends OptionalParentheses {
  field: string;
  operator: Operator;
  value: DateLiteral | DateLiteral[];
  literalType?: 'DATE_LITERAL' | 'DATE_LITERAL'[];
}

export interface ValueWithDateNLiteralCondition extends OptionalParentheses {
  field: string;
  operator: Operator;
  value: string | string[];
  literalType?: 'DATE_N_LITERAL' | 'DATE_N_LITERAL'[];
  dateLiteralVariable: number | number[];
}

export interface ValueQueryCondition extends OptionalParentheses {
  field: string;
  operator: Operator;
  valueQuery: Query;
}

export interface ValueFunctionCondition extends OptionalParentheses {
  fn: FunctionExp;
  operator: Operator;
  value: string | string[];
  literalType?: LiteralType | LiteralType[];
}

export interface NegationCondition {
  openParen: number;
}

export type OrderByClause = OrderByFieldClause | OrderByFnClause;

export interface OrderByOptionalFieldsClause {
  order?: OrderByCriterion;
  nulls?: NullsOrder;
}

export interface OrderByFieldClause extends OrderByOptionalFieldsClause {
  field: string;
}

export interface OrderByFnClause extends OrderByOptionalFieldsClause {
  fn: FunctionExp;
}

export type GroupByClause = GroupByFieldClause | GroupByFnClause;

export interface GroupByOptionalFieldsClause {
  having?: HavingClause;
}

export interface GroupByFieldClause extends GroupByOptionalFieldsClause {
  field: string | string[];
}

export interface GroupByFnClause extends GroupByOptionalFieldsClause {
  fn: FunctionExp;
}

export type HavingClause = HavingClauseWithoutOperator | HavingClauseWithRightCondition;

export interface HavingClauseWithoutOperator {
  left: Condition;
}

export interface HavingClauseWithRightCondition extends HavingClauseWithoutOperator {
  operator: LogicalOperator;
  right: HavingClause;
}

export interface FunctionExp {
  rawValue?: string; // only used for compose fields if useRawValueForFn=true. Should be formatted like this: Count(Id)
  functionName?: string; // only used for compose fields if useRawValueForFn=false, will be populated if SOQL is parsed
  alias?: string;
  parameters?: (string | FunctionExp)[]; // only used for compose fields if useRawValueForFn=false, will be populated if SOQL is parsed
  isAggregateFn?: boolean; // not used for compose, will be populated if SOQL is parsed
}

export interface WithDataCategoryClause {
  conditions: WithDataCategoryCondition[];
}

export interface WithDataCategoryCondition {
  groupName: string;
  selector: GroupSelector;
  parameters: string[];
}

Contributing

All contributions are welcome on the project. Please read the contribution guidelines.

changelog

Changelog

5.0.2

Jan 18, 2024

  • Nested NOT negation WHERE clauses were not properly formed (#242)

5.0.1

Jan 13, 2024

💥 Breaking Changes Fixed a bug where with typescript types to properly represent that WhereClause can have a null value for left in the case of a negation operator. This was always the case, but prior to enabling strict typescript types, this went under the radar.

For Typescript consumers that have strict null checks enabled, they may need to make code changes depending on usage.

5.0.0

💥 Did not publish correct assets - package was marked as deprecated on npm.

4.10.1

Jan 13, 2024

Revert accidental breaking change to types. WhereClause left can have null in the negation case, but the types did not represent this. Updating types to match reality is a breaking change for consumers, so worked around issue and will publish version 5 with breaking change.

4.10.0

💥 Use 4.10.1, this version was marked as deprecated because of accidental breaking change with is reverted in 4.10.1

Jan 13, 2024

  • Fixed where clause's that have a field name beginning with In preceded by the NOT operator. These were parsed as NOT IN instead of NOT followed by a field name, example: NOT Invoice__c
  • Fixed queries that have two consecutive NOT operators (#237)
  • Enabled Typescript strict mode and made a number of minor fixes related to this.
  • When using getField which return FieldFunctionExpression will now always return an empty array even if no parameters are provided.

4.9.2

July 24, 2023

Ensure getFlattenedFields does not throw exception if query does not have fields property.

4.9.1

May 29, 2023

Fixed bug with composeQuery when some of the WHERE clause values were not strings.

4.9.0

May 23, 2023

Upgraded Chevrotain to version 10. Chevrotain dropped support for older browsers, but since this library has a build step, the target output from the processed build should remain in the same target format that was previously available.

4.8.3

May 22, 2023

  • Included src folder in npm package

4.8.2

May 22, 2023

  • Fix output files to ensure sourcemaps are included in npm package for esm_build. (#227)
    • There was a typo in one of the output paths.

4.8.1

April 9, 2023

  • Fix output files to ensure sourcemaps are included in npm package. (#227)

4.8.0

April 9, 2023

  • Fix isAggregateFn (#228)
    • Date functions, such as HOUR_IN_DAY(CreatedDate) did not properly have the isAggregateFn property set to true for the field.
    • As a result, getFlattenedFields would produce incorrect results for these fields if they were not aliased.
  • Upgraded webpack, migrated from Travis CI to Github Actions
  • Update dependencies to resolve reported security issues
  • Update build process to fix invalid sourcemap mapping (#227)
    • Removed non-minified version of application which had incorrect path to sourcemaps
    • Ensure webpack generates a sourcemap for the minified code output

4.7.1

January 23rd, 2023

The repository was moved from paustint to jetstreamapp. No code changes.

4.7.0

October 6, 2022

  • Ensure the literalType is populated on subqueries that are part of a WHERE clause.

4.6.1

July 17, 2022

  • Ensure boolean return from some utility functions.
  • Re-designed the documentation website.

4.6

July 7, 2022

  • Converted test framework from mocha to jest.
  • Fixed bug where non-string values passed in to a where clause would throw an exception. (#121)
  • Fixed bug where in some cases a soql query would be improperly formed if the operator was an array type, such as IN, but the value and literal types were not arrays. (#107)
  • Added additional test-cases for stand-alone compose functions to test non-standard usage.

4.5.0 / 4.5.1

June 21, 2022

  • Added support for accessLevel parameters in a WITH clause (#193)
    • Examples:
      • SELECT Id FROM Account WITH USER_MODE
      • SELECT Id FROM Account WITH SYSTEM_MODE
    • Thank you @ghingis
  • Patch release - changed property from accessLevel to withAccessLevel

4.4.1

June 11, 2022

  • Fixed parser error where 'mi' and 'km' could not be used in a WHERE clause because they were being parsed as a GeoLocationUnit (#188)
    • Thank you @divijklenty for reporting this.

4.4

March 11, 2022

  • Add support for partial parse and compose #182
    • Added support for parsing and composing partial queries. When parsing, the new option allowPartialQuery enables this functionality.
    • Added a third argument for formatQuery, allowing ParseQueryConfig options to be provided.
    • Some types on the Query interface were made optional to support partial queries
    • Updated CLI to include additional commands

4.3

September 19, 2021

  • Added cli support, check out the readme for usage details

4.2.2

August 1, 2021

160 - getFlattenedFields() Did not return correct results if a normal field used an alias, such as SELECT Count(Id), Name account_name FROM Account GROUP BY Name

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

155 - Apex bind variable support is improved to allow parsing of more complex Apex.

Review test cases 112 - 117 for examples of supported apex bind variables.

4.1.1

June 6, 2021

153 - A new parser option has been added named ignoreParseErrors, which will remove invalid parts of a query if there are parsing errors.

The general structure of the query must be valid and the SELECT and WHERE clauses must both be valid, but any other clause may be removed from the parsed output if there are errors parsing the query and ignoreParseErrors is set to true.

This option has been added to the documentation application.

4.0.0

April 13, 20201

💥 Breaking Changes 💥

Release 4.x has changed the way the groupBy and having clauses are parsed. (#149) Previously, the groupBy clause only allowed multiple entries for fields, but not functions.

The groupBy and orderBy are now always returned as arrays from parsed queries to normalize the returned data structure.

For backwards compatibility, a single groupBy or orderBy object is allowed to be passed in to composeQuery(), but a parsed query will always return an array.

The Query object now has

  • A list of group by clauses (a single groupBy clause is allowed if you build the data structure yourself)
  • A Having clause (this was previously nested in the groupBy clause)
  • A list of orderBy clauses (a single orderBy clause is allowed if you build the data structure yourself)
-groupBy?: GroupByClause;
+groupBy?: GroupByClause | GroupByClause[]; // a parsed query will always be undefined or an array
+having?: HavingClause;
orderBy?: OrderByClause | OrderByClause[]; // a parsed query will always be undefined or an array

Each groupBy clause

  • No longer has a nested having clause
  • Is an object with a single field or fn property
type GroupByClause = GroupByFieldClause | GroupByFnClause;

-interface GroupByOptionalFieldsClause {
-  having?: HavingClause;
-}

-interface GroupByFieldClause extends GroupByOptionalFieldsClause {
+interface GroupByFieldClause {
-  field: string | string[];
+  field: string;
}

-interface GroupByFnClause extends GroupByOptionalFieldsClause {
+interface GroupByFnClause {
  fn: FunctionExp;
}

Here are a few examples of how the groupBy is parsed or expected when composing a query:

SELECT UserId, CALENDAR_MONTH(LoginTime) month FROM LoginHistory WHERE NetworkId != NULL GROUP BY UserId, CALENDAR_MONTH(LoginTime)

{
  fields: [
    {
      type: 'Field',
      field: 'UserId',
    },
    {
      type: 'FieldFunctionExpression',
      functionName: 'CALENDAR_MONTH',
      rawValue: 'CALENDAR_MONTH(LoginTime)',
      parameters: ['LoginTime'],
      alias: 'month',
    },
  ],
  sObject: 'LoginHistory',
  where: {
    left: {
      field: 'NetworkId',
      operator: '!=',
      literalType: 'NULL',
      value: 'NULL',
    },
  },
  groupBy: [
    { field: 'UserId' },
    {
      fn: {
        functionName: 'CALENDAR_MONTH',
        rawValue: 'CALENDAR_MONTH(LoginTime)',
        parameters: ['LoginTime'],
      },
    },
  ],
}

SELECT ProductCode FROM Product2 GROUP BY ProductCode HAVING COUNT(Id) > 1 ORDER BY COUNT(Id) DESC

{
  fields: [{ type: 'Field', field: 'ProductCode' }],
  sObject: 'Product2',
  groupBy: [{
    field: 'ProductCode',
  }],
  having: {
    left: {
      operator: '>',
      value: '1',
      literalType: 'INTEGER',
      fn: { rawValue: 'COUNT(Id)', functionName: 'COUNT', parameters: ['Id'] },
    },
  },
  orderBy: [{
    fn: { rawValue: 'COUNT(Id)', functionName: 'COUNT', parameters: ['Id'] },
    order: 'DESC',
  }],
}

SELECT SBQQ__Product__r.Name foo, SBQQ__Quote__c foo1 FROM SBQQ__Quoteline__c GROUP BY SBQQ__Quote__c, SBQQ__Product__r.Name

{
  fields: [
    {
      type: 'FieldRelationship',
      field: 'Name',
      relationships: ['SBQQ__Product__r'],
      rawValue: 'SBQQ__Product__r.Name',
      alias: 'foo',
    },
    {
      type: 'Field',
      field: 'SBQQ__Quote__c',
      alias: 'foo1',
    },
  ],
  sObject: 'SBQQ__Quoteline__c',
  groupBy: [{ field: 'SBQQ__Quote__c' }, { field: 'SBQQ__Product__r.Name' }],
}

3.2.0

March 27, 2021

A number of improvements to the formatter have been made with this release.

  • The formatter option whereClauseOperatorsIndented has been deprecated and will always be applied.
  • A new boolean formatter option named newLineAfterKeywords has been added and will ensure that there is always a new line after any keyword. (#137)
  • TYPEOF fields will now always be included on their own line be default, or will span multiple lines, split by keywords if newLineAfterKeywords is set to true. (#135)

Example

SELECT Id, TYPEOF What WHEN Account THEN Phone, NumberOfEmployees WHEN Opportunity THEN Amount, CloseDate ELSE Name, Email END, Name FROM Event

formatOptions: { newLineAfterKeywords: true, fieldMaxLineLength: 1 },

SELECT
  Id,
  TYPEOF What
    WHEN
      Account
    THEN
      Phone, NumberOfEmployees
    WHEN
      Opportunity
    THEN
      Amount, CloseDate
    ELSE
      Name, Email
  END,
  Name
FROM
  Event

3.1.0

March 27, 2021

  1. Added support for the FIELDS() function

3.0.2

March 6, 2021

  1. Date functions were not properly parsed when used in order by clauses. (#139)
  2. Modified names of functions / types (internal)
  3. Removed improper import of isString from node utils

Changes also released to 2.5.6

3.0.1

January 7, 20201

  1. getFlattenedFields did not properly handle the alias for an aggregate function within an aggregate query. (#131)

3.0.0

October 14, 2020

🔥 Breaking Changes 🔥

This version changes the WHERE clause structure when using the NOT operator t0 fix issue #122, and has implemented stricter type definitions.

The NOT operator is now treated as a LogicalOperator and will be set in the operator field between left and right. In cases where this is populated, the preceding left condition will either be set to null or will at most have the openParens field populated.

The logicalPrefix property has been removed from Condition.

Example of the change in structure for queries using NOT - SELECT Id FROM Account WHERE NOT Id = '2'

{
  "fields": [
    {
      "type": "Field",
      "field": "Id"
    }
  ],
  "sObject": "Account",
  "where": {
-    "left": {
-      "logicalPrefix": "NOT",
-      "field": "Id",
-      "operator": "=",
-      "value": "'2'",
-      "literalType": "STRING"
-    }
+    "left": null
+    "operator": "NOT",
+    "right": {
+      "left": {
+        "field": "Id",
+        "operator": "=",
+        "value": "'2'",
+        "literalType": "STRING"
+      }
  }
}

If you are using Typescript in strict mode, you may encounter some breaking changes to your types depending on how you pre-checked for the presence of fields.

Field and FieldRelationship are now made up of two types, one with and one without alias. Condition is now made up of multiple individual interfaces that represent different data types based on what data is populated. OrderByClause is now made up of multiple individual interfaces that represent different data types based on what data is populated. GroupByClause is now made up of multiple individual interfaces that represent different data types based on what data is populated. HavingClause is now made up of multiple individual interfaces that represent different data types based on what data is populated.

Previously you could have just done null/undefined checks in Typescript strict mode. Now, to avoid using the any type, you can use the newly introduced utility methods that provide type detection and type narrowing.

  • hasAlias()
  • isFieldSubquery()
  • isGroupByField()
  • isGroupByFn()
  • isHavingClauseWithRightCondition()b
  • isNegationCondition()
  • isOrderByField()
  • isOrderByFn()
  • isString()
  • isSubquery()
  • isValueCondition()
  • isValueFunctionCondition()
  • isValueQueryCondition()
  • isValueWithDateLiteralCondition()
  • isValueWithDateNLiteralCondition()
  • isWhereClauseWithRightCondition()
  • isWhereOrHavingClauseWithRightCondition()

Here is a summary of the core changes, view the Readme for the comprehensive types.

export type FieldType =
   | Field
+  | FieldWithAlias
   | FieldFunctionExpression
   | FieldRelationship
+  | FieldRelationshipWithAlias
   | FieldSubquery
   | FieldTypeOf;
-export interface WhereClause {
-  left: Condition & ValueQuery;
-  right?: WhereClause;
-  operator?: LogicalOperator;
-}

+export type WhereClause = WhereClauseWithoutOperator | WhereClauseWithRightCondition;

-export interface Condition {
-  openParen?: number;
-  closeParen?: number;
-  logicalPrefix?: LogicalPrefix;
-  field?: string;
-  fn?: FunctionExp;
-  operator: Operator;
-  value?: string | string[];
-  literalType?: LiteralType | LiteralType[]; // If populated with STRING on compose, the value(s) will be wrapped in "'" if they are not already. - All other values ignored
-  dateLiteralVariable?: number | number[]; // not required for compose, will be populated if SOQL is parsed
-}

+export type Condition =
+  | ValueCondition
+  | ValueWithDateLiteralCondition
+  | ValueWithDateNLiteralCondition
+  | ValueFunctionCondition
+  | NegationCondition;
-export interface OrderByClause {
-  field?: string;
-  fn?: FunctionExp;
-  order?: OrderByCriterion;
-  nulls?: NullsOrder;
-}

+export type OrderByClause = OrderByFieldClause | OrderByFnClause;
-export interface GroupByClause {
-  field?: string | string[];
-  fn?: FunctionExp;
-  having?: HavingClause;
-}

+export type GroupByClause = GroupByFieldClause | GroupByFnClause;

-export interface HavingClause {
-  left: Condition;
-  right?: HavingClause;
-  operator?: LogicalOperator;
-}

+export type HavingClause = HavingClauseWithoutOperator | HavingClauseWithRightCondition;

2.5.5

Aug 23, 2020

  1. getFlattenedFields ignores typeof clauses in query. (#115)

2.5.4

April 12, 2020

  1. getFlattenedFields returns incorrect results if relationship field is grouped and you are grouping for only one field (#113)

2.5.3

April 24, 2020

  1. Fixed nanoseconds on date (#102)

2.5.2

April 23, 2020

  1. Added support for dates formatted with nanoseconds, such as 2020-04-15T02:40:03.000+0000. (#102)
  2. Added support for aggregate function in the ORDER BY clause (#103)
  3. Queries would not be properly composed if an order by had a function and also specified ASC or DESC (#104)

2.5.1

April 23, 2020

  1. Queries with date functions in a WHERE clause would throw an error when parsing. (#100)

2.5.0

April 3, 2020

  1. Passing in null or undefined to compose query no longer throws an exception, but instead returns an empty string. (#95)
  2. Regular fields in a select clause now allow aliases because this is allowed if the field is used as part of a group by clause. (#97)
  3. getFlattenedFields() now considers if a relationship field was used as part of a group by, and if so it returns just the field name instead of the entire field path, as this is how Salesforce will return the records. (#98)

2.4.1

Mar 22, 2020

  1. Updated dependencies to resolve known security vulnerabilities.

2.4

Feb 25, 2020

  1. Date literals were not properly parsed if they were included as part of a SET within a WHERE clause, such as WHERE IN (TODAY, LAST_N_DAYS:5).
    1. As part of this change, the dateLiteralVariable property in the Condition will be an array if a variable date literal is included in a SET where clause. Refer to test cases 93 through 98 for examples

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

  1. Added logo to README and updated docs.

2.2.2

Dec 2, 2019

  1. When composing a query, if an empty OrderBy array was provided, the composed query would still include the ORDER BY clause in the composed query.

2.2.1

Nov 17, 2019

  1. GROUP BY did not allow multiple fields to be listed, for example: SELECT BillingState, BillingStreet, COUNT(Id) FROM Account GROUP BY BillingState, BillingStreet would fail to parse.

2.2.0

Nov 6, 2019

  1. DISTANCE and GEOLOCATION functions failed to parse when used in a WHERE clauses and ORDER BY clauses.

2.1.0

Oct 28, 2019

  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

Oct 6, 2019

Summary

Version 2.0 brings some significant bundle size and performance improvements. This library now uses Chevrotain instead of antlr4. With this change, everything related to parsing had to be re-written from scratch. Chevrotain uses pure javascript to handle lexing, parsing, and visiting the generated ast/cst as opposed to using a grammar file and generating a javascript parser based on the grammar.

With this change, the data model was reviewed and analyzed, and there are some significant breaking changes to the data structures. Review the 🔥breaking changes🔥 below for a detailed description of each breaking change.

Bundle Size

To compare the bundle size, the following small program was written and then compiled using the default configuration of webpack, and the resulting webpack bundle was compared to determine the full size of the library.

Minified, uncompressed:

  • Version 1.x: 545kb
  • Version 2.0: 207kb
var soqlParser = require('soql-parser-js');

const query = soqlParser.parseQuery(`SELECT Id FROM Account WHERE Id = 'FOO'`);
console.log('query', query);
const soql = soqlParser.composeQuery(query);
console.log('soql', soql);

Benchmarks

Performance testing was done by iterating the unit tests 60K times, here are the results:

Version 1.x parser

Library import (startup time): 0.8671 milliseconds
Parsing: 58 X 1000 = 58000 iterations.
Duration: 5.7648 seconds
Average of 0.0994 milliseconds per query

Version 2.0 parser

Library import (startup time): 1.3793 milliseconds
Parsing: 87 X 1000 = 87000 iterations.
Duration: 3.6582 seconds
Average of 0.0420 milliseconds per query

Breaking Changes 🔥

General Changes

  • The CLI was removed.
  • The parseQuery() options have changed. The only option allowed is allowApexBindVariables with will allow parsing queries with apex variables.
  • rawValue will always have a space between parameters GROUPING(Id, BillingCountry)
  • Some literalType values may have differing case from prior versions, regardless of the data input.
    • TRUE, FALSE, and all functions except those listed below will always be returned in uppercase, regardless of case of input.
    • Exceptions:
      • toLabel, convertTimezone, convertCurrency will always be in camelCase.
    • Added types for DateLiteral and DateNLiteral values. If you are using TypeScript, you can utilize these types.
  • A new LiteralType value was added for APEX_BIND_VARIABLE.
  • When composing functions in a where clause or group by clause, the rawValue will be preferred (if it exists) (no change here), but if rawValue is not provided, then the function will be composed using the functionName and parameters.
  • A new LiteralType value was added for INTEGER_WITH_CURRENCY_PREFIX and DECIMAL_WITH_CURRENCY_PREFIX. e.x. USD500.01

Compose Query

  • getComposedField() is deprecated, you should now use getField(). getComposedField() will remain available for backward compatibility.
  • getField()/getComposedField() has the following changes:
    1. fn property is has been deprecated (but still exists), you should now use functionName instead.
    2. The from property has been removed for subqueries. The relationshipName is required to be populated to compose a subquery.
  • On the FormatOptions interface fieldMaxLineLen was renamed to fieldMaxLineLength.
  • Added support for usingScope - https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql_select_using_scope.htm?search_text=format()
  • The having clause condition (e.x. left) now uses the Condition interface instead of having it's own structure.
export interface FormatOptions {
  numIndent?: number;
- fieldMaxLineLen?: number;
+ fieldMaxLineLength?: number;
  fieldSubqueryParensOnOwnLine?: boolean;
  whereClauseOperatorsIndented?: boolean;
  logging?: boolean;
}

Parse Query

  • rawValue will now be included on Field if objectPrefix is defined.
  • alias may be included on Field, if defined.
  • On FieldFunctionExpression, fn was renamed to functionName. this was done because all other usages of fn were FunctionExp, but it was a string in this case.
  • The parameters type on FieldFunctionExpression was modified to allow an array of varying types.
  • Removed from property from FieldSubquery.
  • having was removed from QueryBase and now lives as a property on GroupByClause.
  • On the Condition object, literalType may be an array. This will be an array if value is an array and there are variable types within the value. For example: WHERE Foo IN ('a', null, 'b') would produce literalType: ['STRING', 'NULL', 'STRING'].
  • The GroupByClause has the following modifications:
    • field is now optional, and will be populated only if the grouping is on a single field.
    • type has been renamed to fn and will be populated when CUBE and ROLLUP are used.
    • The having clause has been added as a top-level property to the GroupByClause and will be populated only if a having clause is present.
  • The HavingCondition interface has been removed and now uses the same Condition interface that the WhereClause uses, but without valueQuery.
    • The parser uses the same code to process both of these, so the having clause now supports the exact same capability as a where clause.
  • FunctionExp has the following modifications
export type LiteralType =
  | 'STRING'
  | 'INTEGER'
  | 'DECIMAL'
+ | 'INTEGER_WITH_CURRENCY_PREFIX'
+ | 'DECIMAL_WITH_CURRENCY_PREFIX'
  | 'BOOLEAN'
  | 'NULL'
  | 'DATETIME'
  | 'DATE'
  | 'DATE_LITERAL'
  | 'DATE_N_LITERAL'
+ | 'APEX_BIND_VARIABLE';

+ export type DateLiteral =
+   | 'YESTERDAY'
+   | 'TODAY'
+   | 'TOMORROW'
+   | 'LAST_WEEK'
+   | 'THIS_WEEK'
+   | 'NEXT_WEEK'
+   | 'LAST_MONTH'
+   | 'THIS_MONTH'
+   | 'NEXT_MONTH'
+   | 'LAST_90_DAYS'
+   | 'NEXT_90_DAYS'
+   | 'THIS_QUARTER'
+   | 'LAST_QUARTER'
+   | 'NEXT_QUARTER'
+   | 'THIS_YEAR'
+   | 'LAST_YEAR'
+   | 'NEXT_YEAR'
+   | 'THIS_FISCAL_QUARTER'
+   | 'LAST_FISCAL_QUARTER'
+   | 'NEXT_FISCAL_QUARTER'
+   | 'THIS_FISCAL_YEAR'
+   | 'LAST_FISCAL_YEAR'
+   | 'NEXT_FISCAL_YEAR';

+ export type DateNLiteral =
+   | 'YESTERDAY'
+   | 'NEXT_N_DAYS'
+   | 'LAST_N_DAYS'
+   | 'N_DAYS_AGO'
+   | 'NEXT_N_WEEKS'
+   | 'LAST_N_WEEKS'
+   | 'N_WEEKS_AGO'
+   | 'NEXT_N_MONTHS'
+   | 'LAST_N_MONTHS'
+   | 'N_MONTHS_AGO'
+   | 'NEXT_N_QUARTERS'
+   | 'LAST_N_QUARTERS'
+   | 'N_QUARTERS_AGO'
+   | 'NEXT_N_YEARS'
+   | 'LAST_N_YEARS'
+   | 'N_YEARS_AGO'
+   | 'NEXT_N_FISCAL_QUARTERS'
+   | 'LAST_N_FISCAL_QUARTERS'
+   | 'N_FISCAL_QUARTERS_AGO'
+   | 'NEXT_N_FISCAL_YEARS'
+   | 'LAST_N_FISCAL_YEARS'
+   | 'N_FISCAL_YEARS_AGO';

export interface Field {
  type: 'Field';
  field: string;
  objectPrefix?: string;
+ rawValue?: string;
+ alias?: string;
}

export interface FieldFunctionExpression {
  type: 'FieldFunctionExpression';
- fn: string;
+ functionName: string;
- parameters?: string[] | FieldFunctionExpression[];
+ parameters: (string | FieldFunctionExpression)[];
  alias?: string;
  isAggregateFn?: boolean;
  rawValue?: string;
}

export interface FieldRelationship {
  type: 'FieldRelationship';
  field: string;
  relationships: string[];
  objectPrefix?: string;
  rawValue?: string;
+ alias?: string;
}

export interface FieldSubquery {
  type: 'FieldSubquery';
  subquery: Subquery;
- from?: string;
}

export interface QueryBase {
  fields: FieldType[];
  sObjectAlias?: string;
+ usingScope?: string;
  where?: WhereClause;
  limit?: number;
  offset?: number;
  groupBy?: GroupByClause;
- having?: HavingClause;
  orderBy?: OrderByClause | OrderByClause[];
  withDataCategory?: WithDataCategoryClause;
  withSecurityEnforced?: boolean;
  for?: ForClause;
  update?: UpdateClause;
}

export interface WhereClause {
- left: Condition;
+ left: Condition & ValueQuery;
  right?: WhereClause;
  operator?: LogicalOperator;
}


+ export interface ValueQuery {
+   valueQuery?: Query;
+ }

export interface Condition {
  openParen?: number;
  closeParen?: number;
  logicalPrefix?: LogicalPrefix;
  field?: string;
  fn?: FunctionExp;
  operator: Operator;
  value?: string | string[];
- valueQuery?: Query;
- literalType?: LiteralType;
+ literalType?: LiteralType | LiteralType[];
  dateLiteralVariable?: number;
}

export interface GroupByClause {
- field: string | string[];
+ field?: string | string[];
- type?: GroupByType;
+ fn?: FunctionExp;
+ having?: HavingClause;
}

export interface HavingClause {
+ left: Condition;
  right?: HavingClause;
  operator?: LogicalOperator;
}

- export interface HavingCondition {
-   openParen?: number;
-   closeParen?: number;
-   field?: string;
-   fn?: FunctionExp;
-   operator: string;
-   value: string | number;
- }

export interface FunctionExp {
- text?: string;
+ rawValue?: string;
- name?: string;
+ functionName?: string;
  alias?: string;
- parameter?: string | string[];
+ parameters?: (string | FunctionExp)[];
  isAggregateFn?: boolean;
- fn?: FunctionExp;
}

1.2.1

  • Queries with multiple consecutive left parens in a where clause were not correctly parsed. (#69)
  • Fixed npm reported security vulnerabilities.

1.2.0

  • Changed compose methods to public to allow external access (#65)
  • Fixed lodash security vulnerability
  • Updated README to reflect new changes and other minor changes

1.1.1

  • Removed files that accidentally got included with release with update of release-it

1.1.0

  • Updated Contributing.md with more detailed instructions on grammar updates
  • Added support for WITH SECURITY_ENFORCED (#61)

1.0.2

  • If a field in a query happened to have a function reserved word, such as Format, then parsing the query failed. (#59)

1.0.1

  • Ensured that nothing is logged directly to the console unless logging is enabled

1.0.0

Changed

!BREAKING CHANGES!

  • Added literal type information to fields to provide additional information about the field type. (#51)
    • WHERE clause fields have one of the following types 'STRING' | 'INTEGER' | 'DECIMAL' | 'BOOLEAN' | 'NULL' | 'DATE_LITERAL' | 'DATE_N_LITERAL'; stored in the condition.
    • For date literal fields that have variables, dateLiteralVariable will be populated with the value
  • Modified Field data structure to have explicit type information. (#46, #52)
    • The data structure for fields has been modified to include specific information about the structure of a given field to ease making sense of a parsed query,
    • To aid in creating compose fields, a new helper method is available - getComposedField(). This takes in a simple data structure (or even a string) and will return the structure needed to compose a query.

New

  • An additional queryUtils object is available with the following functions:
    • function getComposedField(input: string | ComposeFieldInput): FieldType
    • function getFlattenedFields(query: Query, isAggregateResult?: boolean): string[]
    • function isSubquery(query: Query | Subquery): query is Subquery
    • Look at the README and refer to the unit tests for example usage.