Skip to content

Latest commit

 

History

History
1039 lines (770 loc) · 45.5 KB

File metadata and controls

1039 lines (770 loc) · 45.5 KB

Changelog

All notable changes to this project will be documented in this file.

8.0.0 - 2026-08-16

This is a major release. The composed and formatted output changes in the cases listed under Bug Fixes, and the internals of the formatter were rewritten. composeQuery, formatQuery, parseQuery and the FormatOptions type are source compatible - see Breaking Changes for the two exceptions.

Breaking Changes

  • The Formatter class has an entirely new shape. It is not exported from the package root, but it is reachable as the type of Compose.formatter, so it appears in the emitted type declarations. Its constructor is now new Formatter(options?: FormatOptions) (previously new Formatter(enabled: boolean, options: FormatOptions)), and the previous methods (formatFields, formatSubquery, formatClause, formatText, formatOrderByArray, formatParens, and the rest) along with the FieldData interface no longer exist. Code that only uses composeQuery, formatQuery, or the documented Compose.parse* methods is unaffected.
  • Compose.parseWhereOrHavingClause lost its third parameter. The signature is now parseWhereOrHavingClause(whereOrHaving, indent = 0). The removed priorConditionIsNegation parameter was only ever used internally. The second parameter also changed meaning: it was tabOffset (a number of tabs) and is now indent (an indentation level, only used when formatting), so a caller passing a number other than 0 will get different output.
  • SELECT, GROUP BY and ORDER BY items now all wrap using one shared rule. Two things changed. fieldMaxLineLength previously ignored the ", " separators when wrapping ORDER BY items, so those lines ran a few characters longer than the same setting produced for SELECT. Separately, a wrapped item's own width was discarded when starting a new row, so every continuation row began with an empty budget and overran fieldMaxLineLength by the width of its first item. All three clauses now count separators, and a wrapped item carries its own width onto the new row, so a row never exceeds fieldMaxLineLength unless a single item is longer than the whole budget. This changes SELECT field wrapping at the default fieldMaxLineLength of 60, not only at unusual values — continuation rows are now slightly shorter. Expect diffs if you commit formatted SOQL to source control.
  • FormatOptions.logging is deprecated and ignored. The rewritten formatter has no per-field intermediate state to log. The option remains in the type for source compatibility and will be removed in a future version. SoqlComposeConfig.logging (the Compose-level option) still works, but now logs only the final composed query instead of emitting a line after each clause as it was built.

Features

  • Added indentString format option — Controls the string used for one unit of indentation (default '\t'). Combined with numIndent, this allows indenting with spaces, e.g. formatQuery(soql, { indentString: ' ', numIndent: 2 }) indents each level with two spaces. The CLI format and compose --format commands accept the equivalent -t, --indent-string <string> flag.

Bug Fixes

  • numIndent is now honored everywhere — With numIndent greater than 1, subquery bodies and closing parens (in both the SELECT and WHERE clauses), TYPEOF fields, and ORDER BY continuation lines were indented by a hard-coded single tab, producing misaligned output. Every line is now indented consistently. (Completes the fix started in #272)
  • No more blank or whitespace-only lines with newLineAfterKeywords — A blank line containing a tab was emitted when the first SELECT field was a subquery or TYPEOF, and a whitespace-only line was emitted after WITH SECURITY_ENFORCED / WITH USER_MODE / WITH SYSTEM_MODE when another clause followed.
  • Subqueries inside parenthesized WHERE conditions are indented relative to their condition — e.g. WHERE (Name = 'a' OR Id IN (SELECT ...)) previously rendered the subquery at the same level as the condition and its closing paren one level too far to the left.
  • GROUP BY items now wrap like ORDER BY items — at fieldMaxLineLength, or one per line when newLineAfterKeywords is true. Previously the GROUP BY clause was always emitted on a single line.
  • Negations inside parenthesized groups are indented correctly — Conditions such as WHERE ((NOT A > 0) AND B < 2), WHERE (NOT A = 'a' AND B = '1'), or WHERE NOT ((NOT (x)) AND y) emitted lines at column 0 inside an indented group. (NOT <condition>) is kept inline as a single unit; every other paren group is now consistently indented with its closing paren on its own line.
  • An empty groupBy array no longer emits a dangling GROUP BYcomposeQuery({ ..., groupBy: [] }) produced SELECT Id FROM Account GROUP BY, which Salesforce rejects. Empty groupBy arrays are now skipped, matching how empty orderBy arrays were already handled.
  • A non-whitespace indentString is ignored instead of producing invalid SOQLformatQuery(soql, { indentString: 'XX' }) emitted XX into the query itself. Values that are not made up of spaces and/or tabs now fall back to the default tab.
  • composeQuery no longer mutates the provided query — Composing the same parsed query twice previously corrupted the sObjectPrefix of subqueries (e.g. Account.Contacts became Account.Contacts.Contacts on the second call).
  • WITH SECURITY_ENFORCED, WITH USER_MODE / WITH SYSTEM_MODE, and WITH DATA CATEGORY are now composed in the correct position — Between the WHERE and GROUP BY clauses, as required by SOQL. Previously they were emitted after LIMIT / OFFSET, which produced invalid SOQL when combined with GROUP BY, ORDER BY, LIMIT, or OFFSET. This applies to both composeQuery and formatQuery.

Changed

  • Changelog now follows the Keep a Changelog format — entries are added to the Unreleased section and stamped with a version and date automatically at release time, and GitHub release notes are generated from that content

Internal

  • The formatter was rewritten as a line-based printer that tracks indentation explicitly (src/formatter/formatter.ts), with the single-line composer (src/composer/plain.ts) and the formatter sharing the same leaf renderers (src/composer/leaf.ts). Apart from the wrapping and layout changes listed under Bug Fixes and Breaking Changes — which include a change to SELECT field wrapping at the default fieldMaxLineLength — previous formatted output is unchanged.
  • Clause order lives in one place. queryClauses() in src/composer/leaf.ts yields the clauses of a query in the order SOQL requires, and both the plain composer and the formatter walk that sequence - they only decide layout. Previously each maintained its own copy of the clause ordering and presence checks, which is what allowed the WITH SECURITY_ENFORCED positioning bug above. Supporting a new clause is now a single edit.
  • Added a format round-trip test suite that asserts every formatted query is valid, idempotent, parses to the same AST as the input, and contains no whitespace-only lines or trailing whitespace, across a representative matrix of format options.

7.4.1 - 2026-08-06

Changed

  • Updated devDependencies to latest versions (prettier, release-it, tsx, vitest) and docs site dependencies (Docusaurus 3.10.2, React 19.2.8, TypeScript 6) — no changes to the published library

Removed

[7.4.0] - 2026-07-16

Features

  • Added comment utilities: stripComments, hasComments, and getComments — These operate on the raw query string without parsing it, so they work on any input (including invalid SOQL) and never throw. stripComments removes comments without otherwise modifying the query — no reformatting, no whitespace or keyword normalization — making it safe for proxying user-authored queries to the Salesforce API, which does not accept comments. When the query contains no comments, the original string is returned as-is, so it can be called unconditionally. A single space is inserted where removing a comment would otherwise merge adjacent tokens (SELECT Id/*c*/FROM AccountSELECT Id FROM Account). Comment markers inside string literals are treated as literal text, and an unterminated /* is stripped through the end of the input rather than throwing. hasComments returns whether a query contains any comments, and getComments returns each comment's type (line | block), text, and position.

[7.3.0] - 2026-07-16

Features

  • Added support for line-level and block-level comments — Queries may now contain comments, which are ignored during parsing: // runs to the end of the line and /* ... */ may span multiple lines (non-nesting; an unterminated /* throws Unterminated comment at position N). Comment markers inside string literals (e.g. WHERE Url = 'https://example.com') are treated as literal text. Comments are not represented in the parsed Query, so they are not preserved by composeQuery/formatQuery. Applies to all entry points (parseQuery, isQueryValid, partial parsing, and the CLI). Note that Salesforce itself does not accept comments in SOQL, so compose the parsed query before sending it to the Salesforce API.

[7.2.2] - 2026-06-09

  • Updated dependencies to resolve security vulnerabilities

[7.2.1] - 2026-06-08

Bug Fixes

  • Escape single quotes and lone backslashes in composed STRING valuescomposeQuery previously wrapped STRING literal values in single quotes without escaping their contents, so a value like med'ia produced invalid SOQL (Industry = 'med'ia'). String values are now escaped before being wrapped: bare single quotes become \' and lone backslashes become \\. Backslash sequences that are already valid SOQL escapes (\n, \r, \t, \b, \f, \", \', \\, \_, \%, \uXXXX, including their uppercase variants) are left unchanged so callers can pre-escape values themselves. Values that are already wrapped in single quotes (e.g. round-tripped through parseQuery) are passed through unchanged.

[7.2.0] - 2026-05-11

LWC Distribution

The LWC build now produces two artifacts to support different deployment workflows:

  • Standalone JS file at dist/lwc/soqlParserJs.js — a single bundled file that SFDX users can drop directly into their project's lwc/soqlParserJs/ folder alongside their own .js-meta.xml. Also published to npm and attached to each GitHub Release.
  • Ready-to-deploy metadata package at dist/lwc-packaged/ — contains package.xml, lwc/soqlParserJs/soqlParserJs.js, and soqlParserJs.js-meta.xml. Deploy directly with sf project deploy start --metadata-dir dist/lwc-packaged --target-org <alias>, no SFDX project required.
  • Pre-zipped metadata package on GitHub Releases — every release attaches soql-parser-js-lwc-v<version>.zip (the metadata package, pre-zipped) so users can grab a single archive, and deploy without cloning or running npm install.
  • Fixed LWC compiler compatibility — lowered the LWC bundle's esbuild target to es2020, which down-levels ES2022 syntax (e.g. static class initialization blocks) that the Salesforce LWC compiler does not yet support.
  • Configurable Salesforce API version — added the salesforceApiVersion field to package.json (currently 66.0); the value is stamped into both package.xml and .js-meta.xml at build time, so bumping the API version is a one-line change.

Release Pipeline

  • npm tarball attached to GitHub Releases — every release now attaches jetstreamapp-soql-parser-js-<version>.tgz.
  • Version banner on all bundlesesm, cjs, cli, and lwc bundles all start with a /*! ... */ header listing the package name, version, repository URL, and license. Banner values are sourced from package.json, so they stay in sync automatically with each release.

[7.1.1] - 2026-04-25

  • Upgrade dependencies

[7.1.0] - 2026-03-21

Bug Fixes

  • Fixed error recovery dropping subsequent clauses - When using ignoreParseErrors: true, if a clause failed to parse, all subsequent clauses (ORDER BY, LIMIT, OFFSET, etc.) were silently dropped. The parser now correctly synchronizes forward and continues parsing remaining clauses.
  • Standardized NULL value casing in array expressions - NULL values inside IN / NOT IN / INCLUDES / EXCLUDES arrays now use uppercase 'NULL' in the parsed value field, consistent with scalar NULL comparisons. The composer still outputs lowercase null in SOQL strings. If you were comparing condition.value items with a case-sensitive === 'null' check, update to === 'NULL'.
  • Removed incorrect DOT token name alias - tokenTypeName(TokenKind.DOT) now returns 'DOT' instead of the incorrect 'DECIMAL'. This only affects consumers using the low-level tokenTypeName API.

Internal

  • Removed singleton parser pattern — parseQuery now creates a fresh parser instance per call, eliminating shared mutable state and making the parser safe for concurrent use in worker threads.

[7.0.0] - 2026-03-14

🚀🚀🚀🚀🚀 Performed a complete re-write using a hand-rolled parser, complements of Claude Code which did 100% of the work.

The new parser is ~4,600x faster and has a 75% smaller bundle size since there are no longer any external dependencies.

Old Parser (Chevrotain) New Parser (Hand-rolled)
Parse time per iteration 3,224 ms 0.7 ms
Bundle size (ESM, minified) 194 KB 48 KB
External dependencies 2 (chevrotain, lodash) 0

In addition, we re-wrote the CLI to drop all 3rd party dependencies since the CLI is very simple and doesn't need a library to manage it.

💥Breaking changes💥

This library no longer exports some of the types used by the previous parser, shown below. These types were only exported as they were used in some public APIs.

This is very unlikely to impact most users, and if it does, the required changes should be very minimal.

export type { CstNode, CstParser, ILexingError, IRecognitionException } from 'chevrotain';

[6.3.1] - 2025-12-15

  • Fix indent for query format

[6.3.0] - 2025-11-27

  • Add support for mru in USING SCOPE. (#268)
    • Thank you @jimmyhogerty

[6.2.4] - 2025-08-18

  • Ensure all types are emitted during the build process (#266)
    • Some types were missing from the build process which caused build errors for some typescript users

[6.2.2] / [6.2.3] - 2025-07-08

  • Ensure types are referenced in exports. (#263)

[6.2.1] - 2025-07-07

  • Updated build scripts to ensure esm build works correctly. (#259)

[6.2.0] - 2025-05-08

  • Updated build tooling and dependencies, no externally facing changes.

[6.1.0] - 2024-06-16

[6.0.0] - 2024-06-16

  • Package has been moved to @jetstreamapp npm organization

[5.0.2] - 2024-01-18

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

[5.0.1] - 2024-01-13

💥 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] - 2024-01-13

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] - 2024-01-13

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

  • 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] - 2023-07-24

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

[4.9.1] - 2023-05-29

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

[4.9.0] - 2023-05-23

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] - 2023-05-22

  • Included src folder in npm package

[4.8.2] - 2023-05-22

  • 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] - 2023-04-09

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

[4.8.0] - 2023-04-09

  • 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] - 2022-10-06

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

[4.6.1] - 2022-07-17

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

[4.6] - 2022-07-07

  • 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] - 2022-06-21

  • 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] - 2022-06-11

  • 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] - 2022-03-11

  • 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] - 2021-09-19

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

[4.2.2] - 2021-08-01

#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] - 2021-06-18

#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] - 2021-06-08

#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] - 2021-06-06

#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] - 2021-03-27

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] - 2021-03-27

  1. Added support for the FIELDS() function

[3.0.2] - 2021-03-06

  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] - 2020-10-14

🔥 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] - 2020-08-23

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

[2.5.4] - 2020-04-12

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

[2.5.3] - 2020-04-24

  1. Fixed nanoseconds on date (#102)

[2.5.2] - 2020-04-23

  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] - 2020-04-23

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

[2.5.0] - 2020-04-03

  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] - 2020-03-22

  1. Updated dependencies to resolve known security vulnerabilities.

[2.4] - 2020-02-25

  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] - 2020-01-13

  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] - 2020-01-04

  1. Added logo to README and updated docs.

[2.2.2] - 2019-12-02

  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] - 2019-11-17

  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] - 2019-11-06

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

[2.1.0] - 2019-10-28

  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] - 2019-10-06

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('@jetstreamapp/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.