Skip to main content

Overview

This library allows parsing and composing (aka generating) SOQL queries from Salesforce using JavaScript or Typescript.

The playground is a great place to get familiar with soql-parser-js.

Quick Start

These are the most common functions exported by the library:

FunctionDescriptionArguments
parseQueryParse a SOQL query string into a Query data structure.soql: string, config?: ParseQueryConfig
composeQueryTurn a Query object back into a SOQL statement.query: Query, config?: SoqlComposeConfig
isQueryValidReturns true if the query was able to be parsed.soql: string, config?: ParseQueryConfig
formatQueryFormat a SOQL query string.soql: string, config?: FormatOptions

Parse a query​

import { parseQuery, Query } from '@jetstreamapp/soql-parser-js';

const query = parseQuery(`SELECT Id FROM Account WHERE Name = 'foo'`);

console.log(query);

// Generated Query
// {
// fields: [
// {
// type: 'Field',
// field: 'Id',
// },
// ],
// sObject: 'Account',
// where: {
// left: {
// field: 'Name',
// operator: '=',
// value: "'Foo'",
// literalType: 'STRING',
// },
// },
// }

Compose a query from an object​

Composing a query will take a query object and return a soql query.

import { composeQuery, Query } from '@jetstreamapp/soql-parser-js';

const query = {
fields: [
{
type: 'Field',
field: 'Id',
},
],
sObject: 'Account',
where: {
left: {
field: 'Name',
operator: '=',
value: "'Foo'",
literalType: 'STRING',
},
},
};

const soql = composeQuery(query);

console.log(soql);

// SELECT Id FROM Account WHERE Name = 'foo'

Compose using helper functions​

You can use the getField helper function to simplify the field generation. Read the api docs for more information.

import { composeQuery, getField } from '@jetstreamapp/soql-parser-js';

const soql = composeQuery({
fields: [
getField('Id'),
getField('Name'),
getField('EntityDefinitionId'),
getField('EntityDefinition.QualifiedApiName'),
getField('IsIdLookup'),
getField('DataType'),
getField('ValueTypeId'),
getField('ReferenceTo'),
getField('IsCreatable'),
getField('IsUpdatable'),
getField('Label'),
getField('MasterLabel'),
getField('QualifiedApiName'),
getField('RelationshipName'),
],
sObject: 'EntityParticle',
where: {
left: {
field: 'EntityDefinition.QualifiedApiName',
operator: 'IN',
value: sobjects,
literalType: 'STRING',
},
operator: 'AND',
right: {
left: {
field: 'QualifiedApiName',
operator: '!=',
value: 'Id',
literalType: 'STRING',
},
operator: 'AND',
right: {
left: {
field: 'DataType',
operator: 'IN',
value: ['string', 'phone', 'url', 'email'],
literalType: 'STRING',
},
},
},
},
orderBy: [
{
field: 'EntityDefinitionId',
},
{ field: 'Label' },
],
});

FORMULA() in WHERE​

The parser supports Salesforce's arithmetic FORMULA() WHERE function. The quoted expression must contain two field references separated by + or -. The expression is parsed into its own nested AST rather than retained as a raw string. Only comparison operators may follow FORMULA(), and Apex bind variables are rejected because Salesforce does not support them there. Salesforce ships this function as a Beta feature as of Summer '26.

const query = parseQuery("SELECT Id FROM Opportunity WHERE FORMULA('Amount - ExpectedRevenue') > 100");

console.log(query.where?.left);
// {
// fn: {
// functionName: 'FORMULA',
// parameters: ["'Amount - ExpectedRevenue'"],
// rawValue: "FORMULA('Amount - ExpectedRevenue')",
// formula: {
// type: 'BinaryExpression',
// operator: '-',
// left: { type: 'FieldReference', parts: ['Amount'] },
// right: { type: 'FieldReference', parts: ['ExpectedRevenue'] },
// },
// },
// operator: '>',
// value: '100',
// literalType: 'INTEGER',
// }

FORMULA() is only accepted in WHERE, not in SELECT, HAVING, GROUP BY, or ORDER BY. When formula is present the composer rebuilds FORMULA() from that AST and ignores rawValue; a FORMULA function without formula composes from rawValue like any other function. parameters holds the normalized expression and rawValue keeps the original text. The parser validates the arithmetic shape but cannot check Salesforce metadata-dependent restrictions, including field data types and compatibility.

Contributing​

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

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 | FormulaFunctionExp; // narrow with isFormulaFunction(); checking functionName alone does not narrow the type
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 type FormulaArithmeticOperator = '+' | '-';

export interface FormulaFieldReference {
type: 'FieldReference';
parts: string[];
}

export interface FormulaBinaryExpression {
type: 'BinaryExpression';
operator: FormulaArithmeticOperator;
left: FormulaFieldReference;
right: FormulaFieldReference;
}

export interface FormulaFunctionExp extends FunctionExp {
functionName: 'FORMULA';
formula: FormulaBinaryExpression;
}

export interface WithDataCategoryClause {
conditions: WithDataCategoryCondition[];
}

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