# sf-formula-parser > A TypeScript library for parsing and evaluating Salesforce formulas outside of Salesforce. Supports 90+ built-in functions, record context, related records, global variables, and prior value detection. This file contains all documentation content in a single document following the llmstxt.org standard. ## API Reference ## Functions ### `evaluateFormula(formula, context, options?)` Parse and evaluate a formula in a single call. This is the simplest way to use the library. ```typescript import { evaluateFormula } from '@jetstreamapp/sf-formula-parser'; const result = evaluateFormula('UPPER(LEFT(Name, 3))', { record: { Name: 'Acme Corp' } }); // "ACM" ``` **Parameters:** | Parameter | Type | Description | | --------- | ------------------- | ------------------------------ | | `formula` | `string` | The Salesforce formula string | | `context` | `FormulaContext` | The record to evaluate against | | `options` | `EvaluationOptions` | Optional evaluation settings | **Returns:** `FormulaValue` — the result of evaluating the formula. --- ### `parseFormula(formula)` Parse a formula string into an AST without evaluating it. Useful for caching parsed formulas, building linters, or inspecting formula structure. ```typescript import { parseFormula } from '@jetstreamapp/sf-formula-parser'; const ast = parseFormula('1 + 2 * 3'); // Returns an ASTNode tree ``` **Parameters:** | Parameter | Type | Description | | --------- | -------- | ----------------------------- | | `formula` | `string` | The Salesforce formula string | **Returns:** `ASTNode` — the root node of the parsed AST. --- ### `createEvaluator(registry?, context?, options?)` Create a reusable `Evaluator` instance. Useful when evaluating many formulas against the same context, or when using a custom function registry. ```typescript import { createEvaluator, createDefaultRegistry } from '@jetstreamapp/sf-formula-parser'; const evaluator = createEvaluator(createDefaultRegistry(), { record: { Status: 'Active', Amount: 100 } }); ``` **Parameters:** | Parameter | Type | Default | Description | | ---------- | ------------------- | ---------------- | ------------------------ | | `registry` | `FunctionRegistry` | Default registry | Function registry to use | | `context` | `FormulaContext` | `{ record: {} }` | Record context | | `options` | `EvaluationOptions` | `undefined` | Evaluation options | **Returns:** `Evaluator` --- ### `createDefaultRegistry()` Create a `FunctionRegistry` pre-loaded with all 90+ built-in Salesforce functions. ```typescript import { createDefaultRegistry } from '@jetstreamapp/sf-formula-parser'; const registry = createDefaultRegistry(); ``` **Returns:** `FunctionRegistry` --- ### `extractFields(formula)` Extract all field references from a formula string without evaluating it. Returns a deduplicated array of field names in the order they appear. Useful for determining which fields to query before evaluation. ```typescript import { extractFields } from '@jetstreamapp/sf-formula-parser'; const fields = extractFields('IF(Amount > 100, $User.FirstName, Name)'); // ['Amount', '$User.FirstName', 'Name'] ``` **Parameters:** | Parameter | Type | Description | | --------- | -------- | ----------------------------- | | `formula` | `string` | The Salesforce formula string | **Returns:** `string[]` — deduplicated array of dot-joined field reference strings. --- ### `extractFieldsByCategory(formula)` Extract and categorize field references from a formula string. Fields are grouped by their `$`-prefix into object fields, globals, custom metadata, custom labels, custom settings, and custom permissions. ```typescript import { extractFieldsByCategory } from '@jetstreamapp/sf-formula-parser'; const result = extractFieldsByCategory('IF($User.IsActive, $Label.Greeting & " " & Name, $Setup.Default__c.Value__c)'); // { // objectFields: ['Name'], // globals: { '$User': ['$User.IsActive'] }, // customMetadata: [], // customLabels: ['$Label.Greeting'], // customSettings: ['$Setup.Default__c.Value__c'], // customPermissions: [], // } ``` **Parameters:** | Parameter | Type | Description | | --------- | -------- | ----------------------------- | | `formula` | `string` | The Salesforce formula string | **Returns:** `ExtractedFields` — categorized field references. --- ### `walkAST(node, visitor)` Walk an AST tree in pre-order, calling the visitor function on each node. Useful for building custom analysis tools on top of the parsed AST. ```typescript import { parseFormula, walkAST } from '@jetstreamapp/sf-formula-parser'; const ast = parseFormula('IF(Amount > 100, Name, "default")'); const functionNames: string[] = []; walkAST(ast, node => { if (node.type === 'FunctionCall') { functionNames.push(node.name); } }); // functionNames: ['IF'] ``` **Parameters:** | Parameter | Type | Description | | --------- | ------------------------- | ---------------------------------- | | `node` | `ASTNode` | The root AST node to start walking | | `visitor` | `(node: ASTNode) => void` | Callback invoked on each node | --- ## Classes ### `FunctionRegistry` A registry that maps function names to implementations. Case-insensitive. ```typescript import { FunctionRegistry } from '@jetstreamapp/sf-formula-parser'; const registry = new FunctionRegistry(); // Register a custom function registry.register('DOUBLE', ctx => { const val = ctx.evaluate(ctx.args[0]); return (val as number) * 2; }); ``` **Methods:** | Method | Description | | -------------------- | --------------------------------------------------------- | | `register(name, fn)` | Register a function by name | | `get(name)` | Get a function by name (returns `undefined` if not found) | | `has(name)` | Check if a function is registered | --- ### `Evaluator` The tree-walking evaluator. Evaluates an AST node against a record context. ```typescript import { Evaluator, createDefaultRegistry } from '@jetstreamapp/sf-formula-parser'; const evaluator = new Evaluator(createDefaultRegistry(), { record: { x: 10 } }); ``` --- ### `Parser` The Pratt parser. Converts a formula string into an AST. ```typescript import { Parser } from '@jetstreamapp/sf-formula-parser'; const ast = Parser.parse('1 + 2'); ``` --- ## Types ### `FormulaValue` ```typescript type FormulaValue = number | string | boolean | Date | SfTime | GeoLocation | null; ``` The result type of any formula evaluation. ### `FormulaRecord` ```typescript type FormulaRecord = { [key: string]: FormulaValue | FormulaRecord }; ``` A flat record where field values and related records coexist as keys — the same shape as a SOQL query result. ### `FormulaContext` ```typescript interface FormulaContext { record: FormulaRecord; globals?: Record; priorRecord?: FormulaRecord; isNew?: boolean; isClone?: boolean; } ``` See [Record Context](/docs/record-context) for full documentation. ### `EvaluationOptions` ```typescript interface EvaluationOptions { returnType?: FormulaReturnType; // validate result type schema?: SchemaInput; // flat FieldSchema[] or Record for related/global schemas treatBlanksAsZeroes?: boolean; // default: true now?: Date; // override current time } ``` | Option | Type | Default | Description | | --------------------- | ------------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `returnType` | `FormulaReturnType` | `undefined` | When provided, validates the formula result matches the declared type. Throws `FormulaError` on mismatch. | | `schema` | `SchemaInput` | `undefined` | Flat `FieldSchema[]` for current object, or `Record` for related/global schemas. Compatible with `describeSObject().fields`. | | `treatBlanksAsZeroes` | `boolean` | `true` | When `true`, null/blank numeric fields are treated as `0` and blank text fields as `""`. Matches the Salesforce default. | | `now` | `Date` | `new Date()` | Override the current timestamp. Useful for deterministic tests with `NOW()` and `TODAY()`. | ### `FormulaReturnType` ```typescript type FormulaReturnType = 'number' | 'string' | 'boolean' | 'date' | 'datetime' | 'time'; ``` ### `FieldSchema` ```typescript interface FieldSchema { name: string; type: SalesforceFieldType; } ``` Intentionally compatible with the Salesforce `Field` type from `describeSObject()` — pass `describe.fields` directly without transformation. ### `SalesforceFieldType` ```typescript type SalesforceFieldType = | 'string' | 'boolean' | 'int' | 'double' | 'date' | 'datetime' | 'base64' | 'id' | 'reference' | 'currency' | 'textarea' | 'percent' | 'phone' | 'url' | 'email' | 'combobox' | 'picklist' | 'multipicklist' | 'anyType' | 'location' | 'time' | 'encryptedstring' | 'address' | 'complexvalue'; ``` ### `ExtractedFields` ```typescript interface ExtractedFields { objectFields: string[]; // Regular fields (no $ prefix) globals: Record; // e.g. { '$User': ['$User.FirstName'] } customMetadata: string[]; // $CustomMetadata.* fields customLabels: string[]; // $Label.* fields customSettings: string[]; // $Setup.* fields customPermissions: string[]; // $Permission.* fields } ``` Returned by `extractFieldsByCategory()`. The `globals` map uses the original `$`-prefix as the key (e.g., `$User`, `$Organization`, `$Profile`, `$UserRole`, `$System`, `$Api`). ### `ASTNode` The discriminated union type representing parsed formula nodes. Includes types for literals, field references, function calls, binary/unary operations, and more. ### `SfTime` ```typescript interface SfTime { timeInMillis: number; // 0–86400000 (ms since midnight, GMT) } ``` ### `GeoLocation` ```typescript interface GeoLocation { latitude: number; longitude: number; } ``` --- ## Error Classes ### `FormulaError` Base error class for all formula errors. ### `LexerError` extends `FormulaError` Thrown when the formula string contains invalid tokens. Includes `line` and `column` properties. ### `ParseError` extends `FormulaError` Thrown when the formula has a syntax error. Includes `line` and `column` properties. ```typescript import { FormulaError, LexerError, ParseError } from '@jetstreamapp/sf-formula-parser'; try { evaluateFormula('IF(', { record: {} }); } catch (e) { if (e instanceof ParseError) { console.log(e.line, e.column); // position of the error } } ``` See [Error Handling](/docs/error-handling) for more details. ## Utility Functions The library also exports helper functions for type checking and coercion: | Function | Description | | -------------------------- | ------------------------------------------------------------ | | `isBlank(value)` | Check if a value is null, undefined, or empty string | | `isSfTime(value)` | Type guard for `SfTime` values | | `isGeoLocation(value)` | Type guard for `GeoLocation` values | | `isDate(value)` | Type guard for `Date` values | | `isFormulaValue(value)` | Type guard — true for field values, false for nested records | | `isFormulaRecord(value)` | Type guard — true for nested records | | `toNumber(value)` | Coerce a formula value to a number | | `toText(value)` | Coerce a formula value to a string | | `toBoolean(value)` | Coerce a formula value to a boolean | | `toFormulaType(fieldType)` | Map a `SalesforceFieldType` to an internal `FormulaType` | --- ## Error Handling The library throws typed errors that you can catch and inspect. All errors extend the base `FormulaError` class. ## Error Hierarchy ``` FormulaError ├── LexerError — invalid tokens (unterminated strings, bad characters) └── ParseError — syntax errors (missing parens, invalid expressions) ``` Runtime evaluation errors (division by zero, type mismatches) throw the base `FormulaError`. ## Catching Errors ```typescript import { evaluateFormula, FormulaError, LexerError, ParseError } from '@jetstreamapp/sf-formula-parser'; try { evaluateFormula('IF(Amount >', { record: {} }); } catch (e) { if (e instanceof ParseError) { console.log('Syntax error:', e.message); console.log('Position:', e.line, e.column); } else if (e instanceof LexerError) { console.log('Tokenization error:', e.message); console.log('Position:', e.line, e.column); } else if (e instanceof FormulaError) { console.log('Runtime error:', e.message); } } ``` ## Error Properties ### `LexerError` | Property | Type | Description | | --------- | -------- | -------------------------------------- | | `message` | `string` | Human-readable error description | | `line` | `number` | Line number where the error occurred | | `column` | `number` | Column number where the error occurred | ### `ParseError` | Property | Type | Description | | --------- | -------- | -------------------------------------- | | `message` | `string` | Human-readable error description | | `line` | `number` | Line number where the error occurred | | `column` | `number` | Column number where the error occurred | ### `FormulaError` | Property | Type | Description | | --------- | -------- | -------------------------------- | | `message` | `string` | Human-readable error description | ## Common Error Scenarios ### Syntax errors ```typescript evaluateFormula('IF(', { record: {} }); // ParseError: Unexpected end of input at line 1, column 4 evaluateFormula('1 +', { record: {} }); // ParseError: Unexpected end of input at line 1, column 4 ``` ### Invalid tokens ```typescript evaluateFormula('"unterminated string', { record: {} }); // LexerError: Unterminated string literal at line 1, column 1 ``` ### Runtime errors ```typescript evaluateFormula('1 / 0', { record: {} }); // FormulaError or returns Infinity (matches Salesforce behavior) evaluateFormula('MID("hello", -1, 3)', { record: {} }); // FormulaError: Invalid arguments ``` ### Operator type errors ```typescript evaluateFormula('true + true', { record: {} }); // FormulaError: Incorrect parameter type for operator '+'. Expected Number, Date, Date/Time, received Boolean evaluateFormula('1 - "test"', { record: {} }); // FormulaError: Incorrect parameter type for operator '-'. Expected Number, Date, Date/Time, received Text ``` ### Return type mismatch ```typescript evaluateFormula('1 + 2', { record: {} }, { returnType: 'string' }); // FormulaError: Formula result is data type (Number), incompatible with expected data type (Text). ``` ### Field not found (with schema) ```typescript const schema = [{ name: 'Name', type: 'string' }]; evaluateFormula('MissingField', { record: {} }, { schema }); // FormulaError: Field MissingField does not exist. Check spelling. ``` ### Picklist field restriction (with schema) ```typescript const schema = [{ name: 'Status', type: 'picklist' }]; evaluateFormula('UPPER(Status)', { record: { Status: 'Open' } }, { schema }); // FormulaError: Field status is a picklist field. Picklist fields are only supported in certain functions. ``` ### Argument count errors ```typescript evaluateFormula('ABS()', { record: {} }); // FormulaError: Incorrect number of parameters for function 'ABS()'. Expected 1, received 0 evaluateFormula('IF(true)', { record: {} }); // FormulaError: Incorrect number of parameters for function 'IF()'. Expected 2-3, received 1 ``` ## Best Practices **Validate formulas before storing them.** Use `parseFormula()` to check syntax without evaluating: ```typescript import { parseFormula, ParseError } from '@jetstreamapp/sf-formula-parser'; function isValidFormula(formula: string): boolean { try { parseFormula(formula); return true; } catch (e) { return false; } } ``` **Use type narrowing for specific handling:** ```typescript import { FormulaError, ParseError } from '@jetstreamapp/sf-formula-parser'; function handleError(e: unknown): string { if (e instanceof ParseError) { return `Syntax error at line ${e.line}, column ${e.column}: ${e.message}`; } if (e instanceof FormulaError) { return `Formula error: ${e.message}`; } return 'Unknown error'; } ``` --- ## Date & Time Functions 21 functions for working with dates, times, and timestamps. ## Constructors ### `DATE(year, month, day)` Creates a date from year, month, and day values. ``` DATE(2024, 6, 15) // Date: 2024-06-15 ``` ### `DATEVALUE(textOrDate)` Converts a text string or datetime to a date-only value (time portion stripped). ``` DATEVALUE("2024-06-15") DATEVALUE("2024-06-15T10:30:00Z") ``` ### `DATETIMEVALUE(text)` Converts a text string to a datetime value. ``` DATETIMEVALUE("2024-06-15 10:30:00") ``` ### `TIMEVALUE(text)` Converts a text string to a time value (`SfTime`). ``` TIMEVALUE("10:30:00") // { timeInMillis: 37800000 } ``` ### `GEOLOCATION(latitude, longitude)` Creates a geographic location. (Also listed under Math.) ## Current Date/Time ### `NOW()` Returns the current date and time. Override with the `now` option for deterministic tests. ``` NOW() // Current datetime ``` ### `TODAY()` Returns today's date (no time component). ``` TODAY() // Current date ``` ### `TIMENOW()` Returns the current time as an `SfTime` value. ``` TIMENOW() // Current time ``` ## Date Parts ### `YEAR(date)` Returns the year from a date. ``` YEAR(DATE(2024, 6, 15)) // 2024 ``` ### `MONTH(date)` Returns the month number (1-12). ``` MONTH(DATE(2024, 6, 15)) // 6 ``` ### `DAY(date)` Returns the day of the month (1-31). ``` DAY(DATE(2024, 6, 15)) // 15 ``` ### `WEEKDAY(date)` Returns the day of the week. Sunday = 1, Saturday = 7. ``` WEEKDAY(DATE(2024, 6, 15)) // 7 (Saturday) ``` ### `ISOWEEK(date)` Returns the ISO week number (1-53). ``` ISOWEEK(DATE(2024, 1, 1)) // 1 ``` ### `ISOYEAR(date)` Returns the ISO year (may differ from calendar year at year boundaries). ``` ISOYEAR(DATE(2024, 12, 30)) // 2025 (belongs to ISO week 1 of 2025) ``` ### `DAYOFYEAR(date)` Returns the day number within the year (1-366). ``` DAYOFYEAR(DATE(2024, 3, 1)) // 61 (2024 is a leap year) ``` ## Time Parts ### `HOUR(timeOrDatetime)` Returns the hour (0-23). ``` HOUR(TIMEVALUE("14:30:00")) // 14 ``` ### `MINUTE(timeOrDatetime)` Returns the minutes (0-59). ``` MINUTE(TIMEVALUE("14:30:00")) // 30 ``` ### `SECOND(timeOrDatetime)` Returns the seconds (0-59). ``` SECOND(TIMEVALUE("14:30:45")) // 45 ``` ### `MILLISECOND(time)` Returns the milliseconds (0-999). ``` MILLISECOND(TIMEVALUE("14:30:45.123")) // 123 ``` ## Date Arithmetic ### `ADDMONTHS(date, months)` Adds a number of months to a date. Handles month-end rollover correctly. ``` ADDMONTHS(DATE(2024, 1, 31), 1) // 2024-02-29 (leap year) ADDMONTHS(DATE(2024, 3, 15), -2) // 2024-01-15 ``` ## Unix Timestamps ### `UNIXTIMESTAMP(datetime)` Converts a datetime to a Unix timestamp (seconds since epoch). ``` UNIXTIMESTAMP(DATETIMEVALUE("2024-01-01 00:00:00")) // 1704067200 ``` ### `FROMUNIXTIME(seconds)` Converts a Unix timestamp to a datetime. ``` FROMUNIXTIME(1704067200) // 2024-01-01T00:00:00.000Z ``` --- ## Logical Functions 16 functions for branching, null-checking, and change detection. ## Branching ### `IF(condition, valueIfTrue, valueIfFalse)` Returns the second argument if the condition is true, otherwise the third. The untaken branch is **not evaluated** (lazy evaluation). ``` IF(Amount > 1000, "Large", "Small") // Amount = 5000 → "Large" ``` ### `IFS(cond1, val1, cond2, val2, ..., default?)` Evaluates conditions in order, returns the value for the first true condition. An optional final unpaired argument is the default. ``` IFS( Score >= 90, "A", Score >= 80, "B", Score >= 70, "C", "F" ) ``` ### `CASE(expr, when1, then1, when2, then2, ..., default)` Matches an expression against values and returns the corresponding result. The final argument is the default. ``` CASE(Status, "New", "Just Created", "Active", "In Progress", "Unknown" ) ``` ## Boolean Logic ### `AND(cond1, cond2, ...)` Returns `true` if **all** arguments are true. ``` AND(IsActive, Amount > 0) ``` ### `OR(cond1, cond2, ...)` Returns `true` if **any** argument is true. ``` OR(Status = "Active", Status = "Pending") ``` ### `NOT(condition)` Returns the logical negation. ``` NOT(IsActive) // IsActive = true → false ``` ## Null / Blank Checking ### `ISBLANK(value)` Returns `true` if the value is null, undefined, or an empty string. ``` ISBLANK(Phone) // Phone = null → true // Phone = "" → true // Phone = "555-1234" → false ``` ### `ISNULL(value)` Returns `true` if the value is null or undefined. Unlike `ISBLANK`, an empty string returns `false`. ``` ISNULL(Phone) // Phone = null → true // Phone = "" → false ``` ### `ISNUMBER(value)` Returns `true` if the value is a number or a string that can be parsed as a number. ``` ISNUMBER("42") // true ISNUMBER("hello") // false ISNUMBER(3.14) // true ``` ### `BLANKVALUE(value, substitute)` Returns the value if not blank, otherwise returns the substitute. ``` BLANKVALUE(Phone, "No phone on file") // Phone = null → "No phone on file" // Phone = "555-1234" → "555-1234" ``` ### `NULLVALUE(value, substitute)` Returns the value if not null, otherwise returns the substitute. ``` NULLVALUE(Amount, 0) ``` ### `IFERROR(expression, errorValue)` Evaluates the expression and returns its result. If an error occurs, returns the error value instead. ``` IFERROR(VALUE("abc"), 0) // → 0 (VALUE("abc") throws, so the fallback is returned) ``` ## Change Detection These functions work with the `priorRecord`, `isNew`, and `isClone` properties on the `FormulaContext`. ### `ISCHANGED(field)` Returns `true` if the field's value differs from its prior value. ``` ISCHANGED(Status) // Status = "Closed", prior Status = "Open" → true ``` ### `PRIORVALUE(field)` Returns the previous value of the field. ``` PRIORVALUE(Amount) // Amount = 200, prior Amount = 100 → 100 ``` ### `ISNEW()` Returns `true` if the record is being created (no arguments). ``` ISNEW() // isNew = true → true ``` ### `ISCLONE()` Returns `true` if the record is being cloned (no arguments). ``` ISCLONE() // isClone = true → true ``` --- ## Math Functions 19 functions for arithmetic, rounding, and geolocation. ## Rounding ### `ROUND(value, scale)` Rounds a number to the specified number of decimal places. ``` ROUND(3.14159, 2) // 3.14 ROUND(2.5, 0) // 3 ``` ### `CEILING(value)` Rounds away from zero (positive numbers round up, negative numbers round down in magnitude). ``` CEILING(2.1) // 3 CEILING(-2.1) // -3 ``` ### `FLOOR(value)` Rounds toward zero. ``` FLOOR(2.9) // 2 FLOOR(-2.9) // -2 ``` ### `MCEILING(value)` Mathematical ceiling — always rounds toward positive infinity. ``` MCEILING(2.1) // 3 MCEILING(-2.1) // -2 ``` ### `MFLOOR(value)` Mathematical floor — always rounds toward negative infinity. ``` MFLOOR(2.9) // 2 MFLOOR(-2.9) // -3 ``` ### `TRUNC(value, scale?)` Truncates decimal places without rounding. Optional scale parameter. ``` TRUNC(3.789) // 3 TRUNC(3.789, 2) // 3.78 ``` ## Arithmetic ### `ABS(value)` Returns the absolute value. ``` ABS(-42) // 42 ABS(42) // 42 ``` ### `MOD(dividend, divisor)` Returns the remainder of division. ``` MOD(10, 3) // 1 MOD(7, 2) // 1 ``` ### `POWER(base, exponent)` Raises the base to the power of the exponent. ``` POWER(2, 10) // 1024 POWER(9, 0.5) // 3 ``` ### `SQRT(value)` Returns the square root. ``` SQRT(144) // 12 SQRT(2) // 1.4142135623730951 ``` ### `EXP(value)` Returns _e_ raised to the given power. ``` EXP(1) // 2.718281828459045 EXP(0) // 1 ``` ### `LN(value)` Returns the natural logarithm (base _e_). ``` LN(2.718281828459045) // 1 LN(1) // 0 ``` ### `LOG(value)` Returns the base-10 logarithm. ``` LOG(100) // 2 LOG(1000) // 3 ``` ## Aggregation ### `MAX(value1, value2, ...)` Returns the largest value among all arguments. ``` MAX(10, 20, 5) // 20 ``` ### `MIN(value1, value2, ...)` Returns the smallest value among all arguments. ``` MIN(10, 20, 5) // 5 ``` ## Constants ### `PI()` Returns the mathematical constant pi. ``` PI() // 3.141592653589793 ``` ### `RAND()` Returns a random number between 0 (inclusive) and 1 (exclusive). ``` RAND() // e.g. 0.7391482... ``` ## Geolocation ### `GEOLOCATION(latitude, longitude)` Creates a geographic location value. ``` GEOLOCATION(37.7749, -122.4194) ``` ### `DISTANCE(location1, location2, unit)` Calculates the distance between two locations. Unit is `"mi"` for miles or `"km"` for kilometers. ``` DISTANCE( GEOLOCATION(37.7749, -122.4194), GEOLOCATION(34.0522, -118.2437), "mi" ) // ~347.42 miles (San Francisco to Los Angeles) ``` --- ## Text Functions 30 functions for string manipulation, encoding, and pattern matching. ## String Manipulation ### `LEFT(text, length)` Returns the leftmost characters. ``` LEFT("Salesforce", 5) // "Sales" ``` ### `RIGHT(text, length)` Returns the rightmost characters. ``` RIGHT("Salesforce", 5) // "force" ``` ### `MID(text, startPosition, length)` Returns a substring. Position is 1-based (like Salesforce, not 0-based). ``` MID("Salesforce", 6, 5) // "force" ``` ### `LEN(text)` Returns the length of the string. ``` LEN("hello") // 5 ``` ### `TRIM(text)` Removes leading and trailing whitespace. ``` TRIM(" hello ") // "hello" ``` ### `UPPER(text)` Converts to uppercase. ``` UPPER("hello") // "HELLO" ``` ### `LOWER(text)` Converts to lowercase. ``` LOWER("HELLO") // "hello" ``` ### `INITCAP(text)` Capitalizes the first letter of each word. ``` INITCAP("hello world") // "Hello World" ``` ### `LPAD(text, length, padString?)` Left-pads the text to the specified length. ``` LPAD("42", 5, "0") // "00042" LPAD("hi", 5) // " hi" ``` ### `RPAD(text, length, padString?)` Right-pads the text to the specified length. ``` RPAD("hi", 5, ".") // "hi..." ``` ### `SUBSTITUTE(text, oldString, newString)` Replaces all occurrences of a substring. ``` SUBSTITUTE("Hello World", "World", "Salesforce") // "Hello Salesforce" ``` ## Search ### `FIND(search, text, startPosition?)` Returns the 1-based position of the first occurrence. Returns 0 if not found. ``` FIND("force", "Salesforce") // 6 FIND("xyz", "Salesforce") // 0 FIND("e", "Salesforce", 4) // 10 ``` ### `CONTAINS(text, search)` Returns `true` if the text contains the search string. Case-sensitive. ``` CONTAINS("Salesforce", "force") // true CONTAINS("Salesforce", "Force") // false ``` ### `BEGINS(text, search)` Returns `true` if the text starts with the search string. ``` BEGINS("Salesforce", "Sales") // true ``` ### `REGEX(text, pattern)` Returns `true` if the text matches the regular expression. ``` REGEX("415-555-1234", "[0-9]{3}-[0-9]{3}-[0-9]{4}") // true ``` ## Conversion ### `TEXT(value)` Converts any value to its text representation. ``` TEXT(42) // "42" TEXT(true) // "true" TEXT(TODAY()) // date string ``` ### `VALUE(text)` Converts a numeric string to a number. ``` VALUE("42") // 42 VALUE("3.14") // 3.14 ``` ### `CHR(charCode)` Returns the character for a character code. ``` CHR(65) // "A" CHR(97) // "a" ``` ### `ASCII(text)` Returns the character code of the first character. ``` ASCII("A") // 65 ASCII("a") // 97 ``` ## Encoding ### `HTMLENCODE(text)` Encodes HTML special characters. ``` HTMLENCODE("") // "<script>alert('xss')</script>" ``` ### `JSENCODE(text)` Encodes text for safe use in JavaScript strings. ``` JSENCODE("it's a \"test\"") // "it\\'s a \\\"test\\\"" ``` ### `JSINHTMLENCODE(text)` Encodes text for JavaScript embedded in HTML. ### `URLENCODE(text)` URL-encodes the text. ``` URLENCODE("hello world") // "hello%20world" ``` ## HTML Generation ### `BR()` Returns an HTML line break tag (``). ### `HYPERLINK(url, label, target?)` Creates an HTML anchor tag. ``` HYPERLINK("https://example.com", "Click here", "_blank") // 'Click here' ``` ### `IMAGE(url, alt, height?, width?)` Creates an HTML image tag. ``` IMAGE("/img/logo.png", "Logo", 50, 200) ``` ## Salesforce-Specific ### `CASESAFEID(id)` Converts a 15-character Salesforce ID to its 18-character case-safe version. ``` CASESAFEID("001000000000001") // "001000000000001AAA" (example) ``` ### `INCLUDES(multiSelectField, value)` Returns `true` if a multi-select picklist includes the specified value. ``` INCLUDES(Interests, "Technology") ``` ### `ISPICKVAL(picklistField, value)` Returns `true` if a picklist field equals the specified value. ``` ISPICKVAL(Status, "Active") ``` ### `GETSESSIONID()` Returns the current session ID. In this client-side implementation, returns an empty string. --- ## Getting Started Get up and running with `sf-formula-parser` in under two minutes. ## Installation ```bash npm install @jetstreamapp/sf-formula-parser ``` Or with your preferred package manager: ```bash yarn add @jetstreamapp/sf-formula-parser pnpm add @jetstreamapp/sf-formula-parser ``` ## Quick Example ```typescript import { evaluateFormula } from '@jetstreamapp/sf-formula-parser'; const result = evaluateFormula('IF(Amount > 1000, "Large Deal", "Small Deal")', { record: { Amount: 5000, }, }); console.log(result); // "Large Deal" ``` That's it — one function call, no setup required. ## How It Works The library processes formulas in three stages: ``` Formula String → Lexer → Tokens → Parser → AST → Evaluator → Result ↑ Record Context ``` 1. **Lexer** — tokenizes the formula string, handling string escapes, comments, and operators 2. **Parser** — a hand-rolled Pratt parser produces a typed AST 3. **Evaluator** — walks the AST, resolving field references against the record context you provide You can use `evaluateFormula()` for the common case, or work with each stage independently for advanced use cases. ## Core Concepts ### Record Context Every formula is evaluated against a **record context** — an object containing the Salesforce record as a flat object (the same shape as a SOQL query result): ```typescript const context = { record: { Name: 'Acme Corp', Amount: 50000, IsActive: true, CreatedDate: new Date('2024-01-15'), }, }; evaluateFormula('UPPER(Name)', context); // "ACME CORP" ``` ### Related Records Access parent relationships like `Account.Name` — related records are nested directly in the record: ```typescript const context = { record: { Account: { Name: 'Acme Corp', Industry: 'Technology', }, }, }; evaluateFormula('Account.Industry', context); // "Technology" ``` ### Type Safety The library is written in TypeScript and exports all its types: ```typescript import type { FormulaValue, FormulaContext, FormulaRecord, FormulaReturnType, FieldSchema, EvaluationOptions, ASTNode, } from '@jetstreamapp/sf-formula-parser'; ``` `FormulaValue` can be `number`, `string`, `boolean`, `Date`, `SfTime`, `GeoLocation`, or `null`. ## Next Steps - **[API Reference](/docs/api-reference)** — full details on every exported function and type - **[Record Context](/docs/record-context)** — related records, globals, prior values - **[Functions](/docs/functions/logical)** — all 90+ supported functions - **[Playground](/playground)** — try formulas live in your browser --- ## Record Context Every formula is evaluated against a `FormulaContext` — the object that represents the Salesforce record and its environment. ## Basic Fields The `record` property is a flat object mapping field names to their values — the same shape as a SOQL query result: ```typescript import { evaluateFormula } from '@jetstreamapp/sf-formula-parser'; evaluateFormula('Name & " - $" & TEXT(Amount)', { record: { Name: 'Acme Corp', Amount: 50000, }, }); // "Acme Corp - $50000" ``` Field values can be any `FormulaValue`: | Type | Example | | ------------- | --------------------------------------------- | | `number` | `42`, `3.14` | | `string` | `"Acme Corp"` | | `boolean` | `true`, `false` | | `Date` | `new Date('2024-01-15')` | | `SfTime` | `{ timeInMillis: 43200000 }` (noon) | | `GeoLocation` | `{ latitude: 37.7749, longitude: -122.4194 }` | | `null` | `null` (blank field) | ## Related Records Access fields on related records using dot-notation in the formula. Related records are nested directly in the `record` object: ```typescript // Formula: Account.Name evaluateFormula('Account.Name', { record: { Account: { Name: 'Acme Corp' }, }, }); // "Acme Corp" ``` ### Multi-Level Relationships Relationships can be nested: ```typescript // Formula: Contact.Account.Industry evaluateFormula('Contact.Account.Industry', { record: { Contact: { Account: { Industry: 'Technology' }, }, }, }); // "Technology" ``` ## Global Variables Salesforce global variables like `$User`, `$Profile`, and `$Organization` are supported via the `globals` property: ```typescript // Formula: $User.FirstName evaluateFormula('$User.FirstName', { record: {}, globals: { $User: { FirstName: 'Jane', LastName: 'Smith', Email: 'jane@example.com', }, }, }); // "Jane" ``` ## Prior Value Support For trigger-context formulas that use `ISCHANGED`, `PRIORVALUE`, `ISNEW`, or `ISCLONE`: ```typescript // ISCHANGED(Status) - true when Status differs from prior value evaluateFormula('ISCHANGED(Status)', { record: { Status: 'Closed' }, priorRecord: { Status: 'Open' }, }); // true // PRIORVALUE(Amount) - returns the previous value evaluateFormula('PRIORVALUE(Amount)', { record: { Amount: 200 }, priorRecord: { Amount: 100 }, }); // 100 // ISNEW() - true when the record is newly created evaluateFormula('ISNEW()', { record: {}, isNew: true, }); // true // ISCLONE() - true when the record is a clone evaluateFormula('ISCLONE()', { record: {}, isClone: true, }); // true ``` ## Evaluation Options Pass options as the third argument to `evaluateFormula`: ```typescript evaluateFormula(formula, context, { returnType: 'string', schema: describe.fields, treatBlanksAsZeroes: true, // default now: new Date('2024-06-15T12:00:00Z'), }); ``` ### `treatBlanksAsZeroes` **Default: `true`** When enabled (the Salesforce default), null/blank values are coerced: - Blank numbers become `0` - Blank text becomes `""` When disabled, null values propagate through calculations, which can be useful for detecting missing data. ### `now` Override the timestamp returned by `NOW()` and `TODAY()`. Essential for writing deterministic tests: ```typescript evaluateFormula('TEXT(TODAY())', context, { now: new Date('2024-06-15T12:00:00Z'), }); // Returns a consistent result regardless of when the test runs ``` ### Schema Validation Pass Salesforce field metadata to enable type-aware validation. The `schema` option accepts an array of `FieldSchema` objects — compatible with `describeSObject().fields`: ```typescript const describe = await conn.describeSObject('Account'); evaluateFormula('UPPER(Name)', context, { schema: describe.fields, // pass directly — no transformation needed }); ``` When schema is provided: - **Field existence** — referencing a direct field not in the schema throws `FormulaError` - **Picklist restrictions** — picklist fields can only be used in `TEXT()`, `ISPICKVAL()`, `CASE()`, `ISBLANK()`, `ISNULL()`, `NULLVALUE()`, `BLANKVALUE()`, `INCLUDES()`, `ISCHANGED()`, `PRIORVALUE()` - **All other behavior** is unchanged — schema is purely additive #### Simple schema (current object only) A flat `FieldSchema[]` validates only direct fields on the current object: ```typescript const schema = [ { name: 'Name', type: 'string' }, { name: 'Status', type: 'picklist' }, ]; evaluateFormula('Name', context, { schema }); // OK evaluateFormula('MissingField', context, { schema }); // throws evaluateFormula('Account.Name', context, { schema }); // OK (no schema for Account, bypassed) ``` #### Full schema (with related objects and globals) Pass a `Record` to validate related object fields and globals too. Use `'$record'` for the root object, relationship names as keys, and `$`-prefixed names for globals: ```typescript const schema = { $record: describeContact.fields, // current object Account: describeAccount.fields, // Account relationship $User: describeUser.fields, // $User global }; evaluateFormula('Name', context, { schema }); // validated against root schema evaluateFormula('Account.Name', context, { schema }); // validated against Account schema evaluateFormula('$User.FirstName', context, { schema }); // validated against $User schema evaluateFormula('Owner.Name', context, { schema }); // bypassed (Owner not in schema) evaluateFormula('Account.Website', context, { schema }); // throws (not in Account schema) ``` Relationships not included in the schema map bypass validation — you only need to provide schemas for the objects you want validated. ## Full Interface ```typescript // A flat record — fields and related records coexist as keys type FormulaRecord = { [key: string]: FormulaValue | FormulaRecord }; interface FormulaContext { record: FormulaRecord; globals?: Record; priorRecord?: FormulaRecord; isNew?: boolean; isClone?: boolean; } // Schema can be a flat array (current object) or a map (multiple objects) type SchemaInput = FieldSchema[] | Record; interface EvaluationOptions { returnType?: FormulaReturnType; schema?: SchemaInput; treatBlanksAsZeroes?: boolean; now?: Date; } type FormulaValue = number | string | boolean | Date | SfTime | GeoLocation | null; ```