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:
| Function | Description | Arguments |
|---|---|---|
| parseQuery | Parse a SOQL query string into a Query data structure. | soql: string, config?: ParseQueryConfig |
| composeQuery | Turn a Query object back into a SOQL statement. | query: Query, config?: SoqlComposeConfig |
| isQueryValid | Returns true if the query was able to be parsed. | soql: string, config?: ParseQueryConfig |
| formatQuery | Format 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[];
}