@stackone/expressions
v0.26.0
Published
This package can be used to parse and evaluate string expressions with support for variables replacement, functions and operators.
Keywords
Readme
@stackone/expressions
Description
This package can be used to parse and evaluate string expressions with support for variables replacement, functions and operators.
Requirements
Node.js 20+ is required to run this project.
Installation
# install dependencies
$ npm run installAvailable commands
# clean build output
$ npm run clean# build package
$ npm run build# run tests
$ npm run test# run tests on watch mode
$ npm run test:watch# run linter
$ npm run lint# run linter and try to fix any error
$ npm run lint:fixAPI Reference
evaluate(expression: string, context?: object, options?: { incrementalJsonPath?: boolean })
Evaluates the given expression using the provided context.
- Returns the evaluated result
- Throws an error if the expression is invalid or evaluation fails
evaluate("$.user.name", { user: { name: "John" } }); // Returns "John"
evaluate("x + y", { x: 1, y: 2 }); // Returns 3- Setting incrementalJsonPath iterates through any JSON Path expression to check for failure, starting at the root.
- This exchanges performance for better error messages, useful for AI validation and self-repair.
evaluate(
"$.user.name",
{ user: { name: "John" } },
{ incrementalJsonPath: true }
); // Returns "John"
evaluate(
"$.user.details.age",
{ user: { details: { name: "John" } } },
{ incrementalJsonPath: true }
); // Throws Error: "Key 'age' not found at '$.user.details'. Available keys: name"isValidExpression(expression: string)
Checks if the given expression is valid.
- Returns
trueif the expression is valid - Returns
falseotherwise
isValidExpression("$.user.name"); // Returns true
isValidExpression("invalid $$$ expression"); // Returns falsesafeEvaluate(expression: string, context?: object)
Safely evaluates the expression without throwing errors.
- Returns the evaluated result if successful
- Returns
nullif evaluation fails or the expression is invalid
safeEvaluate("$.user.name", { user: { name: "John" } }); // Returns "John"
safeEvaluate("$ invalid expression", {}); // Returns nullExpression language syntax
There are three types of expressions supported:
JSON Path Expressions
When the expression starts with $, it is treated as a JSON Path expression and will be evaluated as such.
JSON Path Syntax
| JSON Path | Description |
| ------------------ | ------------------------------------------------------------------ |
| $ | The root object |
| . | Child operator |
| @ | The current object |
| * | Wildcard. All elements in an array, or all properties of an object |
| .. | Recursive descent |
| [] | Subscript operator |
| [,] | Union operator (e.g., $.a[b,c] for multiple properties) |
| [start:end:step] | Array slice operator |
| ?(expression) | Filter expression |
| () | Script expression |
Examples:
// Given the context: { user: { name: "John", age: 30 }, "info/email": "[email protected]" }
"$.user.name"; // Returns "John"
"$.user.age"; // Returns 30
"$.user[*]"; // Returns ["John", 30]
"$.user[name,age]"; // Returns ["John", 30] (union operator)
'$["info/email"]'; // Returns "[email protected]"For more information on JSON Path syntax, refer to the JSONPath Plus documentation and the original JSON Path specification.
JEXL Expressions
This kind of expression is enclosed in double brackets {{expression}}. It supports variables and operators.
Operators
| Operator | Description |
| -------- | ------------------------------ |
| ! | Logical NOT |
| + | Addition, string concatenation |
| - | Subtraction |
| * | Multiplication |
| / | Division |
| // | Floor division |
| % | Modulus |
| ^ | Exponentiation |
| && | Logical AND |
| \|\| | Logical OR |
| == | Equal |
| != | Not equal |
| > | Greater than |
| >= | Greater than or equal |
| < | Less than |
| <= | Less than or equal |
| in | Element of string or array |
| ? : | Ternary operator |
| ?? | Nullish coalescing operator |
Examples:
// Given the context: { x: 10, y: 5 }
"{{x + y}}"; // Returns 15
"{{x * 2}}"; // Returns 20
"{{x > y}}"; // Returns true
'{{x == 10 ? "yes" : "no"}}'; // Returns "yes"
"{{x in [1, 2, 3]}}"; // Returns false
"{{x != y}}"; // Returns true
"{{x ?? y}}"; // Returns 10Identifiers
Identifiers can be used to reference variables in the context.
// Given the context:
// {
// name: {
// first: "John",
// last: "Smith"
// },
// jobs: ["Developer", "Designer"]
// }
`{{name.first}}` // Returns "John"
`{{jobs[1]}}`; // Returns "Designer"Collections
Collections, or arrays of objects, can be filtered by including a filter expression in brackets.
// Given the context:
// {
// users: [
// { name: "John", age: 30 },
// { name: "Jane", age: 25 }
// ]
// }
`{{users[.name == "John"].age}}` // Returns 30
`{{users[.age > 25].name}}`; // Returns ["John"]Built-in Functions
The expression handler provides several built-in functions you can use in your expressions:
Date Functions
nextAnniversary(initialDate, format)
Calculates the next anniversary date for a given date.
initialDate(string): The initial date stringformat(string): The format of the date string (uses date-fns format)- Returns: Date object or null
// If today is April 10, 2025, and a birthday is December 25, 2000
"{{nextAnniversary('25/12/2000', 'dd/MM/yyyy')}}";
// Returns Date object for December 25, 2025 (this year's anniversary)
// If today is April 10, 2025, and a birthday is February 15, 1990
"{{nextAnniversary('15/02/1990', 'dd/MM/yyyy')}}";
// Returns Date object for February 15, 2026 (next year's anniversary)yearsElapsed(startDate, format, endDate?)
Calculates the number of complete years elapsed between two dates.
startDate(string): The start date stringformat(string): The format of the date string (uses date-fns format)endDate(string, optional): The end date string, defaults to current date if omitted- Returns: number of years or null
// Calculate years between two specific dates
"{{yearsElapsed('01/01/2015', 'dd/MM/yyyy', '01/01/2025')}}"; // Returns 10
// Calculate years from a date to today (assuming today is April 10, 2025)
"{{yearsElapsed('01/01/2015', 'dd/MM/yyyy')}}"; // Returns 10hasPassed(date, format, yearsToAdd?)
Determines if a given date (optionally with added years) has passed.
date(string): The date to checkformat(string): The format of the date string (uses date-fns format)yearsToAdd(number, optional): Number of years to add to the date before comparing- Returns: boolean
// Check if a date has passed (assuming today is April 10, 2025)
"{{hasPassed('01/01/2020', 'dd/MM/yyyy')}}"; // Returns true
// Check if a date has passed (assuming today is April 10, 2025)
"{{hasPassed('01/01/2026', 'dd/MM/yyyy')}}"; // Returns false
// Check if a date + 5 years has passed (2020 + 5 = 2025)
"{{hasPassed('01/01/2020', 'dd/MM/yyyy', 5)}}";
// Returns true if April 10, 2025 is after January 1, 2025now()
Returns the current date and time.
- Returns: string (ISO 8601 format)
// Get the current date and time
"{{now()}}"; // Returns "2025-04-10T12:00:00.000Z" (example)Array Functions
includes(array, value)
Checks if an array includes a specific value or all values from another array.
array(array): The array to checkvalue(any | array): The value(s) to search for in the array- Returns: boolean
// Check if an array includes a specific value
"{{includes([1, 2, 3], 2)}}"; // Returns true
// Check if an array includes a specific value
"{{includes([1, 2, 3], 5)}}"; // Returns false
// Can be used with context variables
"{{includes($.allowedRoles, $.currentUser.role)}}";
// Check if an array includes all of values of the second array
"{{includes([1, 2, 3, 4], [2, 4])}}"; // Returns trueincludesSome(array, value)
Checks if an array includes at least one value from another array or a single value.
array(array): The array to checkvalue(any | array): The value(s) to search for in the array- Returns: boolean
// Check if an array includes at least one value
"{{includesSome([1, 2, 3], 2)}}"; // Returns true
// Check if an array includes at least one value from another array
"{{includesSome([1, 2, 3], [2, 5])}}"; // Returns true
// Check if an array includes at least one value from another array (none match)
"{{includesSome([1, 2, 3], [4, 5])}}"; // Returns false
// Can be used with context variables
"{{includesSome($.allowedRoles, $.currentUser.roles)}}";groupBy(array, key)
Groups an array of objects by the value of a specified key. Items with missing or null values for the key are collected under "__missing__".
array(array): Array of objects to groupkey(string): The object key to group by- Returns: object mapping each unique key value to an array of matching items
// Group employees by department
"{{groupBy($.employees, 'dept')}}";
// { eng: [{dept: "eng", ...}, ...], hr: [{dept: "hr", ...}] }
// Group tickets by status
"{{groupBy($.tickets, 'status')}}";
// { open: [...], closed: [...] }reduce(array, operation, field?)
Applies an aggregate operation to an array. When field is specified, that field is extracted from each object before applying the operation.
array(array): The array to reduceoperation(string): One of"sum","avg","count","min","max","concat","flatten"field(string, optional): Object key to extract values from before reducing- Returns: the aggregated result, or null if invalid
// Sum numbers
"{{reduce([1, 2, 3], 'sum')}}"; // Returns 6
// Sum a field from objects
"{{reduce($.orders, 'sum', 'total')}}"; // Returns sum of all order totals
// Average scores
"{{reduce($.reviews, 'avg', 'rating')}}"; // Returns average rating
// Count items
"{{reduce($.users, 'count')}}"; // Returns number of users
// Concatenate arrays from a field
"{{reduce($.users, 'concat', 'permissions')}}"; // Returns all permissions mergedzipObject(keys, values)
Combines two parallel arrays into an object by pairing keys[i] with values[i]. If the arrays differ in length, the shorter length is used. Non-string keys are skipped.
keys(array): Array of string keysvalues(array): Array of values to pair with each key- Returns: object mapping each key to its corresponding value
// Combine field names and values into an object
"{{zipObject(['name', 'email'], ['John', '[email protected]'])}}"; // Returns { name: "John", email: "[email protected]" }
// Numeric values
"{{zipObject(['count', 'total'], [5, 100])}}"; // Returns { count: 5, total: 100 }
// From context variables
"{{zipObject($.fieldNames, $.fieldValues)}}";dedupe(array)
Removes duplicate entries from an array, returning a new array with unique values. Primitives are compared by type and value (so 1 and "1" are distinct). Objects are compared by their contents with keys sorted, so {a:1,b:2} and {b:2,a:1} are treated as duplicates.
array(array): The array to deduplicate- Returns: array with duplicates removed, preserving original order
// Dedupe numbers
"{{dedupe([1, 2, 2, 3, 1])}}"; // Returns [1, 2, 3]
// Dedupe strings
"{{dedupe(['a', 'b', 'a', 'c'])}}"; // Returns ["a", "b", "c"]
// Dedupe objects by value (key order doesn't matter)
"{{dedupe([{id: 1}, {id: 2}, {id: 1}])}}"; // Returns [{id: 1}, {id: 2}]
"{{dedupe([{a: 1, b: 2}, {b: 2, a: 1}])}}"; // Returns [{a: 1, b: 2}]
// Dedupe from context variable
"{{dedupe($.tags)}}";join(array, separator?)
Joins array elements into a string with a specified separator. Null and undefined values in the array are filtered out.
array(array): The array to joinseparator(string, optional): The string to use between elements (default: ',')- Returns: joined string, or empty string for invalid/non-array input
// Join with default comma separator
"{{join(['a', 'b', 'c'])}}"; // Returns "a,b,c"
// Join with custom separator
"{{join(['a', 'b', 'c'], ' - ')}}"; // Returns "a - b - c"
"{{join([1, 2, 3], '|')}}"; // Returns "1|2|3"
// Join with empty string (concatenate)
"{{join(['a', 'b', 'c'], '')}}"; // Returns "abc"
// Null/undefined values are filtered out
"{{join(['a', null, 'b', undefined, 'c'])}}"; // Returns "a,b,c"
// Join from context variable
"{{join($.tags, ', ')}}";
"{{join(data.items, ' | ')}}";Object Functions
present(object)
Checks if an object is present (not null or undefined).
object(object): The object to check- Returns: boolean
// Check if an object is present
"{{present({})}}"; // Returns true
"{{present(null)}}"; // Returns false
"{{present(undefined)}}"; // Returns false
"{{present('string')}}"; // Returns true
"{{present(0)}}"; // Returns true
"{{present([])}}"; // Returns truemissing(object)
Checks if an object is missing (null or undefined).
object(object): The object to check- Returns: boolean
// Check if an object is not present
"{{missing({})}}"; // Returns false
"{{missing(null)}}"; // Returns true
"{{missing(undefined)}}"; // Returns true
"{{missing('string')}}"; // Returns false
"{{missing(0)}}"; // Returns false
"{{missing([])}}"; // Returns falsekeys(object)
Returns the keys of an object as an array. If the input is not an object, null, or undefined, returns an empty array.
object(object): The object to get keys from- Returns: array of keys (string[])
// Get keys from an object
"{{keys({ a: 1, b: 2 })}}"; // Returns ["a", "b"]
// Get keys from a context variable
"{{keys($.user)}}";values(object)
Returns the values of an object as an array. If the input is not an object, null, or undefined, returns an empty array.
object(object): The object to get values from- Returns: array of values (any[])
// Get values from an object
"{{values({ a: 1, b: 2 })}}"; // Returns [1, 2]
// Get values from a context variable
"{{values($.user)}}";Math Functions
min(...values)
Returns the smallest value from the provided numbers.
values(number | number[]): Numbers to compare (individual values or an array)- Returns: smallest number, or null if no valid numbers provided
// Multiple arguments
"{{min(3, 1, 2)}}"; // Returns 1
// Array of numbers
"{{min([5, 2, 8])}}"; // Returns 2
// Mixed positive and negative
"{{min(-10, 0, 10)}}"; // Returns -10
// From context variables
"{{min(a, b, c)}}";
"{{min($.values)}}";
// Invalid input returns null
"{{min([])}}"; // Returns nullString Functions
capitalize(value, mode?)
Capitalizes characters in a string.
value(string): The string to capitalizemode(string, optional): 'first' to capitalize first character only, 'each' to capitalize each word (default: 'first')- Returns: capitalized string, or empty string for invalid input
// Capitalize first character (default)
"{{capitalize('hello')}}"; // Returns "Hello"
"{{capitalize('hello world')}}"; // Returns "Hello world"
// Capitalize each word
"{{capitalize('hello world', 'each')}}"; // Returns "Hello World"
"{{capitalize('the great gatsby', 'each')}}"; // Returns "The Great Gatsby"
// Capitalize from context variable
"{{capitalize($.name)}}";
"{{capitalize($.title, 'each')}}";decodeBase64(encodedValue)
Decodes a Base64 encoded string and returns the decoded result.
encodedValue(string): The Base64 encoded string to decode- Returns: decoded string, or empty string if input is invalid
// Decode a base64 string directly
"{{decodeBase64("SGVsbG8gV29ybGQ")}}" // Returns "Hello World"
// Decode from context variable
"{{decodeBase64($.encodedValue)}}" // Decodes the encodedValue from context
// Handles invalid cases gracefully
"{{decodeBase64(null)}}" // Returns ""
"{{decodeBase64("!")}}" // Returns ""
"{{decodeBase64(123)}}" // Returns ""encodeBase64(value)
Encodes a string to Base64 and returns the encoded result.
value(string): The string to encode- Returns: encoded string, or empty string if input is invalid
// Encode a string directly
"{{encodeBase64("Hello World")}}" // Returns "SGVsbG8gV29ybGQ"
// Encode from context variable
"{{encodeBase64($.value)}}" // Encodes the value from context
// Handles invalid cases gracefully
"{{encodeBase64(null)}}" // Returns ""
"{{encodeBase64(123)}}" // Returns ""
"{{encodeBase64("")}}" // Returns ""decodeUri(value)
Decodes a URI-encoded string, converting percent-encoded characters back to their original form. Useful for handling pre-encoded values (like pagination cursors) that would otherwise be double-encoded when passed as query parameters.
value(string): The URI-encoded string to decode- Returns: decoded string, or empty string if input is invalid
// Decode basic percent-encoded characters
"{{decodeUri('hello%20world')}}" // Returns "hello world"
"{{decodeUri('cursor%3Dabc%26page%3D2')}}" // Returns "cursor=abc&page=2"
// Decode Base64 padding characters (common in cursors)
"{{decodeUri('eyJpZCI6MTIzfQ%3D%3D')}}" // Returns "eyJpZCI6MTIzfQ=="
// Decode special characters
"{{decodeUri('%2B%2F%3D')}}" // Returns "+/="
// Already decoded strings are returned unchanged
"{{decodeUri('already decoded')}}" // Returns "already decoded"
// Handles invalid cases gracefully
"{{decodeUri(null)}}" // Returns ""
"{{decodeUri(123)}}" // Returns ""
// Malformed encoding returns original string
"{{decodeUri('invalid%ZZ')}}" // Returns "invalid%ZZ"
// Decode from context variable (Confluence pagination example)
"{{decodeUri($.response.cursor)}}"truncate(value, maxLength, suffix?)
Truncates a string to a specified maximum length, optionally appending a suffix.
value(string): The string to truncatemaxLength(number): Maximum length of the result (including suffix)suffix(string, optional): Suffix to append when truncating, defaults to "..."- Returns: truncated string, or original string if shorter than maxLength
// Truncate with default suffix
"{{truncate('Hello World', 8)}}"; // Returns "Hello..."
// Truncate with custom suffix
"{{truncate('Hello World', 8, '…')}}"; // Returns "Hello W…"
// Truncate with no suffix
"{{truncate('Hello World', 5, '')}}"; // Returns "Hello"
// No truncation needed
"{{truncate('Hi', 10)}}"; // Returns "Hi"
// Truncate from context variable
"{{truncate($.description, 100)}}";padStart(value, targetLength, padString?)
Pads the start of a string with another string until it reaches the target length.
value(string | number): The value to pad (numbers are converted to strings)targetLength(number): The target length of the resulting stringpadString(string, optional): The string to pad with, defaults to space " "- Returns: padded string, or original if already at or beyond target length
// Pad with zeros (common for formatting numbers)
"{{padStart('5', 3, '0')}}"; // Returns "005"
"{{padStart(5, 3, '0')}}"; // Returns "005" (numbers work too)
// Pad with default space
"{{padStart('hello', 10)}}"; // Returns " hello"
// Pad with custom character
"{{padStart('abc', 6, '*')}}"; // Returns "***abc"
// No padding needed if already long enough
"{{padStart('hello', 3)}}"; // Returns "hello"
// Pad from context variable
"{{padStart($.id, 8, '0')}}";replace(value, search, replacement, replaceAll?)
Replaces occurrences of a search string with a replacement string.
value(string): The string to perform replacement onsearch(string): The substring to search forreplacement(string): The string to replace matches withreplaceAll(boolean, optional): If true, replaces all occurrences; otherwise replaces only the first (default: false)- Returns: string with replacements made, or original string if search not found
// Replace first occurrence (default)
"{{replace('hello world', 'world', 'there')}}"; // Returns "hello there"
"{{replace('hello hello', 'hello', 'hi')}}"; // Returns "hi hello"
// Replace all occurrences
"{{replace('hello hello', 'hello', 'hi', true)}}"; // Returns "hi hi"
"{{replace('foo-bar-baz', '-', '_', true)}}"; // Returns "foo_bar_baz"
// Remove characters by replacing with empty string
"{{replace('a-b-c', '-', '', true)}}"; // Returns "abc"
// Replace from context variable
"{{replace($.text, ' ', '-', true)}}";regexMatch(value, pattern, groupIndex?)
Extracts a value from a string using a regular expression pattern. Returns the specified capture group, or null if no match is found.
value(string): The string to search inpattern(string): The regex pattern as a string (without delimiters)groupIndex(number, optional): Capture group index to return (default: 1 for first capture group, 0 for full match)- Returns: matched string from the specified group, or null if no match or group doesn't exist
// Extract parameter from URL or header
"{{regexMatch('<https://api.com?after=abc123&limit=2>; rel=\"next\"', 'after=([^&>]+)', 1)}}"; // Returns "abc123"
// Extract full match (group 0)
"{{regexMatch('Hello World', 'World', 0)}}"; // Returns "World"
// Extract with capture group
"{{regexMatch('user_id=12345', 'user_id=(\\d+)', 1)}}"; // Returns "12345"
// No match returns null
"{{regexMatch('Hello World', 'foo', 1)}}"; // Returns null
// Extract from context variable
"{{regexMatch($.linkHeader, 'after=([^&>]+)', 1)}}";urlParam(url, paramName)
Extracts a query parameter value from a URL string. Useful for parsing pagination URLs where the next page token is embedded in a full URL.
url(string): The URL string to parseparamName(string): The name of the query parameter to extract- Returns: the parameter value as a string, or null if not found or inputs are invalid
// Extract page number from pagination URL
"{{urlParam('https://api.example.com/v1/items?page=2&limit=10', 'page')}}"; // Returns "2"
// Extract cursor from next page URL
"{{urlParam('https://api.example.com/v1/items?cursor=eyJpZCI6MTIzfQ==&limit=50', 'cursor')}}"; // Returns "eyJpZCI6MTIzfQ=="
// Extract offset parameter
"{{urlParam('https://api.example.com/v1/items?offset=100&limit=25', 'offset')}}"; // Returns "100"
// Returns null when parameter doesn't exist
"{{urlParam('https://api.example.com?page=1', 'missing')}}"; // Returns null
// From context variable (common pagination use case)
"{{urlParam($.pagination.next_page, 'page')}}";
// Use in conditional for pagination
"{{urlParam(nextUrl, 'page') != null ? urlParam(nextUrl, 'page') : null}}";For more information on the JEXL syntax, refer to the JEXL Syntax documentation.
String Interpolation
To simplify strings usage, a more straightforward syntax is provided for string interpolation of variables using the ${var} syntax.
Examples:
// Given the context: { name: "John", age: 30 }
"Hello ${name}"; // Returns "Hello John"
"User is ${age}"; // Returns "User is 30"
// You can also use JEXL inside string syntax
"Status: ${age > 18 ? 'Adult' : 'Minor'}"; // Returns "Status: Adult"
"Age in 5 years: ${age + 5}"; // Returns "Age in 5 years: 35"Note: If the expression is a string without any of the patterns described above, it will be returned as is.
// Given the context: { name: "John", age: 30 }
"Hello world"; // Returns "Hello world"