json-expressions
Version:
A JavaScript expression engine for JSON-based dynamic computations and function composition
1,735 lines (1,556 loc) • 52.5 kB
JavaScript
'use strict';
var didYouMean = require('didyoumean');
var esToolkit = require('es-toolkit');
function isUnsafeProperty(key) {
return key === '__proto__';
}
function isDeepKey(key) {
switch (typeof key) {
case 'number':
case 'symbol': {
return false;
}
case 'string': {
return key.includes('.') || key.includes('[') || key.includes(']');
}
}
}
function toKey(value) {
if (typeof value === 'string' || typeof value === 'symbol') {
return value;
}
if (Object.is(value?.valueOf?.(), -0)) {
return '-0';
}
return String(value);
}
function toPath(deepKey) {
const result = [];
const length = deepKey.length;
if (length === 0) {
return result;
}
let index = 0;
let key = '';
let quoteChar = '';
let bracket = false;
if (deepKey.charCodeAt(0) === 46) {
result.push('');
index++;
}
while (index < length) {
const char = deepKey[index];
if (quoteChar) {
if (char === '\\' && index + 1 < length) {
index++;
key += deepKey[index];
}
else if (char === quoteChar) {
quoteChar = '';
}
else {
key += char;
}
}
else if (bracket) {
if (char === '"' || char === "'") {
quoteChar = char;
}
else if (char === ']') {
bracket = false;
result.push(key);
key = '';
}
else {
key += char;
}
}
else {
if (char === '[') {
bracket = true;
if (key) {
result.push(key);
key = '';
}
}
else if (char === '.') {
if (key) {
result.push(key);
key = '';
}
}
else {
key += char;
}
}
index++;
}
if (key) {
result.push(key);
}
return result;
}
function get(object, path, defaultValue) {
if (object == null) {
return defaultValue;
}
switch (typeof path) {
case 'string': {
if (isUnsafeProperty(path)) {
return defaultValue;
}
const result = object[path];
if (result === undefined) {
if (isDeepKey(path)) {
return get(object, toPath(path), defaultValue);
}
else {
return defaultValue;
}
}
return result;
}
case 'number':
case 'symbol': {
if (typeof path === 'number') {
path = toKey(path);
}
const result = object[path];
if (result === undefined) {
return defaultValue;
}
return result;
}
default: {
if (Array.isArray(path)) {
return getWithPath(object, path, defaultValue);
}
if (Object.is(path?.valueOf(), -0)) {
path = '-0';
}
else {
path = String(path);
}
if (isUnsafeProperty(path)) {
return defaultValue;
}
const result = object[path];
if (result === undefined) {
return defaultValue;
}
return result;
}
}
}
function getWithPath(object, path, defaultValue) {
if (path.length === 0) {
return defaultValue;
}
let current = object;
for (let index = 0; index < path.length; index++) {
if (current == null) {
return defaultValue;
}
if (isUnsafeProperty(path[index])) {
return defaultValue;
}
current = current[path[index]];
}
if (current === undefined) {
return defaultValue;
}
return current;
}
/**
* Creates a simple transformation expression that applies a function to the resolved operand.
* @param {function(any): any} transformFn - Function that transforms the resolved operand
* @param {string} evaluateErrorMessage - Error message for non-array operands in evaluate form
* @returns {object} Expression object with apply and evaluate methods
*/
const createSimpleExpression = (transformFn, evaluateErrorMessage) => ({
apply: (operand, inputData, { apply }) =>
transformFn(apply(operand, inputData)),
evaluate: (operand, { evaluate }) => {
if (!Array.isArray(operand)) {
throw new Error(evaluateErrorMessage);
}
const [value] = operand;
return transformFn(evaluate(value));
},
});
const $isDefined = createSimpleExpression(
(value) => value !== undefined,
"$isDefined evaluate form requires array operand: [value]",
);
const $get = {
apply: (operand, inputData, { apply }) => {
if (typeof operand === "string") {
return get(inputData, operand);
}
if (typeof operand === "object" && !Array.isArray(operand)) {
const { path, default: defaultValue } = operand;
if (path === undefined) {
throw new Error("$get object form requires 'path' property");
}
const evaluatedPath = apply(path, inputData);
const result = get(inputData, evaluatedPath);
return result !== undefined
? result
: defaultValue !== undefined
? apply(defaultValue, inputData)
: undefined;
}
throw new Error(
"$get operand must be string or object with {path, default?}",
);
},
evaluate: (operand, { evaluate }) => {
if (typeof operand !== "object" || Array.isArray(operand)) {
throw new Error(
"$get evaluate form requires object operand: {object, path, default?}",
);
}
const { object, path, default: defaultValue } = operand;
if (object === undefined || path === undefined) {
throw new Error(
"$get evaluate form requires 'object' and 'path' properties",
);
}
const result = get(evaluate(object), evaluate(path));
return result !== undefined
? result
: defaultValue !== undefined
? evaluate(defaultValue)
: undefined;
},
};
const $prop = {
apply: (operand, inputData, { apply }) => {
const property = apply(operand, inputData);
return inputData[property];
},
evaluate: (operand, { evaluate }) => {
if (!Array.isArray(operand)) {
throw new Error(
"$prop evaluate form requires array operand: [object, property]",
);
}
const [object, property] = operand;
const evaluatedObject = evaluate(object);
const evaluatedProperty = evaluate(property);
return evaluatedObject[evaluatedProperty];
},
};
const $literal = {
apply: (operand) => operand,
evaluate: (operand) => operand,
};
const $debug = {
apply: (operand, inputData, { apply }) => {
const value = apply(operand, inputData);
console.log(value);
return value;
},
evaluate: (operand, { evaluate }) => {
const value = evaluate(operand);
console.log(value);
return value;
},
};
/**
* Creates a composition expression that chains expressions together.
* @param {function(Array, function): any} composeFn - Function that takes (expressions, reduceFn) and returns result
* @returns {object} Expression object with apply and evaluate methods
*/
const createCompositionExpression = (composeFn) => ({
apply: (operand, inputData, { apply, isExpression }) => {
// Validate that all elements are expressions
operand.forEach((expr) => {
if (!isExpression(expr)) {
throw new Error(`${JSON.stringify(expr)} is not a valid expression`);
}
});
return composeFn(operand, (acc, expr) => apply(expr, acc), inputData);
},
evaluate: (operand, { apply, isExpression }) => {
const [expressions, initialValue] = operand;
// Validate that all elements are expressions
expressions.forEach((expr) => {
if (!isExpression(expr)) {
throw new Error(`${JSON.stringify(expr)} is not a valid expression`);
}
});
return composeFn(
expressions,
(acc, expr) => apply(expr, acc),
initialValue,
);
},
});
const $pipe = createCompositionExpression(
(expressions, reduceFn, initialValue) =>
expressions.reduce(reduceFn, initialValue),
);
var core = /*#__PURE__*/Object.freeze({
__proto__: null,
$debug: $debug,
$get: $get,
$isDefined: $isDefined,
$literal: $literal,
$pipe: $pipe,
$prop: $prop
});
/**
* Internal helper to validate a boolean condition and execute if/else logic.
* @param {boolean} condition - The condition to check
* @param {any} thenValue - Value to return if condition is true
* @param {any} elseValue - Value to return if condition is false
* @returns {any} The selected value based on condition
*/
const executeConditional = (condition, thenValue, elseValue) => {
if (typeof condition !== "boolean") {
throw new Error(
`$if.if must be a boolean or an expression that resolves to one, got ${JSON.stringify(condition)}`,
);
}
return condition ? thenValue : elseValue;
};
const $if = {
apply(operand, inputData, { apply }) {
const condition = apply(operand.if, inputData);
return executeConditional(
condition,
apply(operand.then, inputData),
apply(operand.else, inputData),
);
},
evaluate: (operand, { evaluate }) => {
const condition = evaluate(operand.if);
return executeConditional(
condition,
evaluate(operand.then),
evaluate(operand.else),
);
},
};
/**
* Check if an expression is a boolean predicate (comparison/logic operator)
* @param {any} expr - The expression to check
* @returns {boolean} True if it's a boolean predicate expression
*/
const isBooleanPredicate = (expr) => {
if (!expr || typeof expr !== "object" || Array.isArray(expr)) {
return false;
}
const [key] = Object.keys(expr);
// Boolean predicate operators that return true/false
const booleanOps = [
"$gt",
"$gte",
"$lt",
"$lte",
"$eq",
"$ne",
"$in",
"$nin",
"$isNull",
"$isNotNull",
"$between",
"$and",
"$or",
"$not",
"$matchesRegex",
"$matchesLike",
"$matchesGlob",
"$all",
"$any",
];
return booleanOps.includes(key);
};
/**
* Internal helper to find a matching case using flexible matching.
* Supports both literal comparisons and expression predicates.
* @param {any} value - The value to test against
* @param {Array} cases - Array of case objects with 'when' and 'then' properties
* @param {function} evaluateWhen - Function to evaluate the 'when' condition
* @param {function} isExpression - Function to check if a value is an expression
* @param {function} apply - Function to apply expressions with input data
* @returns {object|undefined} The matching case object or undefined
*/
const findFlexibleCase = (value, cases, evaluateWhen, isExpression, apply) => {
return cases.find((caseItem) => {
if (caseItem.when === undefined) {
throw new Error("Case item must have 'when' property");
}
if (isExpression(caseItem.when) && isBooleanPredicate(caseItem.when)) {
// Boolean predicate mode: apply the expression with value as input data
const condition = apply(caseItem.when, value);
if (typeof condition !== "boolean") {
throw new Error(
`$case.when expression must resolve to a boolean, got ${JSON.stringify(condition)}`,
);
}
return condition;
} else {
// Literal mode: evaluate when clause and deep equality comparison
const evaluatedWhen = evaluateWhen(caseItem.when);
return esToolkit.isEqual(value, evaluatedWhen);
}
});
};
const $case = {
apply(operand, inputData, { apply, isExpression }) {
const value = apply(operand.value, inputData);
const found = findFlexibleCase(
value,
operand.cases,
(when) => apply(when, inputData),
isExpression,
apply,
);
return found
? apply(found.then, inputData)
: apply(operand.default, inputData);
},
evaluate(operand, { evaluate, isExpression, apply }) {
// Handle array format for evaluate form
const caseOperand = Array.isArray(operand) ? operand[0] : operand;
const value = evaluate(caseOperand.value);
const found = findFlexibleCase(
value,
caseOperand.cases,
evaluate,
isExpression,
apply,
);
return found ? evaluate(found.then) : evaluate(caseOperand.default);
},
};
var conditionalExpressions = /*#__PURE__*/Object.freeze({
__proto__: null,
$case: $case,
$if: $if
});
/**
* Creates a comparative expression that applies a comparison function to resolved operands.
*
* @param {function(any, any): boolean} compareFn - Function that takes two values and returns a boolean comparison result
* @returns {object} Expression object with apply and evaluate methods
*/
const createComparativeExpression = (compareFn) => ({
apply(operand, inputData, { apply }) {
const resolvedOperand = apply(operand, inputData);
return compareFn(inputData, resolvedOperand);
},
evaluate: (operand, { evaluate }) => {
const [left, right] = operand;
return compareFn(evaluate(left), evaluate(right));
},
});
/**
* Creates an inclusion expression that checks if a value is in/not in an array.
*
* @param {function(any, Array): boolean} inclusionFn - Function that takes a value and array and returns boolean
* @param {string} expressionName - Name of the expression for error messages
* @returns {object} Expression object with apply and evaluate methods
*/
const createInclusionExpression = (expressionName, inclusionFn) => ({
apply(operand, inputData, { apply }) {
const resolvedOperand = apply(operand, inputData);
if (!Array.isArray(resolvedOperand)) {
throw new Error(`${expressionName} parameter must be an array`);
}
return inclusionFn(inputData, resolvedOperand);
},
evaluate: (operand, { evaluate }) => {
const [array, value] = evaluate(operand);
if (!Array.isArray(array)) {
throw new Error(`${expressionName} parameter must be an array`);
}
return inclusionFn(value, array);
},
});
const $eq = createComparativeExpression((a, b) => esToolkit.isEqual(a, b));
const $ne = createComparativeExpression((a, b) => !esToolkit.isEqual(a, b));
const $gt = createComparativeExpression((a, b) => a > b);
const $gte = createComparativeExpression((a, b) => a >= b);
const $lt = createComparativeExpression((a, b) => a < b);
const $lte = createComparativeExpression((a, b) => a <= b);
const $in = createInclusionExpression("$in", (value, array) =>
array.includes(value),
);
const $nin = createInclusionExpression(
"$nin",
(value, array) => !array.includes(value),
);
const $between = {
apply: (operand, inputData, { apply }) => {
const { min, max } = apply(operand, inputData);
return inputData >= min && inputData <= max;
},
evaluate: (operand, { evaluate }) => {
const { value, min, max } = evaluate(operand);
return value >= min && value <= max;
},
};
const $isNull = {
apply: (operand, inputData) => inputData == null,
evaluate: (operand, { evaluate }) => evaluate(operand) == null,
};
const $isNotNull = {
apply: (operand, inputData) => inputData != null,
evaluate: (operand, { evaluate }) => evaluate(operand) != null,
};
var comparativeExpressions = /*#__PURE__*/Object.freeze({
__proto__: null,
$between: $between,
$eq: $eq,
$gt: $gt,
$gte: $gte,
$in: $in,
$isNotNull: $isNotNull,
$isNull: $isNull,
$lt: $lt,
$lte: $lte,
$ne: $ne,
$nin: $nin
});
/**
* Creates an array iteration expression that applies a function to array elements.
* @param {function(Array, function): any} arrayMethodFn - Function that takes (array, itemFn) and returns result
* @param {string} expressionName - Name of the expression for evaluate form
* @returns {object} Expression object with apply and evaluate methods
*/
const createArrayIterationExpression = (arrayMethodFn, expressionName) => ({
apply: (operand, inputData, { apply }) =>
arrayMethodFn(inputData, (item) => apply(operand, item)),
evaluate: (operand, { apply }) => {
const [fn, items] = operand;
return apply({ [expressionName]: fn }, items);
},
});
/**
* Creates a simple array operation expression.
* @param {function(any, Array): any} operationFn - Function that takes (operand, inputData) and returns result
* @returns {object} Expression object with apply and evaluate methods
*/
const createArrayOperationExpression = (operationFn) => ({
apply: (operand, inputData) => operationFn(operand, inputData),
evaluate: (operand, { evaluate }) => {
const [arg1, arg2] = operand;
return operationFn(evaluate(arg1), evaluate(arg2));
},
});
/**
* Creates a simple array transformation expression (no operand needed).
* @param {function(Array): any} transformFn - Function that transforms an array
* @returns {object} Expression object with apply and evaluate methods
*/
const createArrayTransformExpression = (transformFn) => ({
apply: (_, inputData) => transformFn(inputData),
evaluate: (operand, { evaluate }) => {
const array = evaluate(operand);
return transformFn(array);
},
});
const $filter = createArrayIterationExpression(
(array, itemFn) => array.filter(itemFn),
"$filter",
);
const $flatMap = createArrayIterationExpression(
(array, itemFn) => array.flatMap(itemFn),
"$flatMap",
);
const $map = createArrayIterationExpression(
(array, itemFn) => array.map(itemFn),
"$map",
);
const $any = createArrayIterationExpression(
(array, itemFn) => array.some(itemFn),
"$any",
);
const $all = createArrayIterationExpression(
(array, itemFn) => array.every(itemFn),
"$all",
);
const $find = createArrayIterationExpression(
(array, itemFn) => array.find(itemFn),
"$find",
);
const $append = createArrayOperationExpression((arrayToConcat, baseArray) =>
baseArray.concat(arrayToConcat),
);
const $prepend = createArrayOperationExpression((arrayToPrepend, baseArray) =>
arrayToPrepend.concat(baseArray),
);
const $join = createArrayOperationExpression((separator, array) =>
array.join(separator),
);
const $reverse = createArrayTransformExpression((array) =>
array.slice().reverse(),
);
const $take = createArrayOperationExpression((count, array) =>
array.slice(0, count),
);
const $skip = createArrayOperationExpression((count, array) =>
array.slice(count),
);
const $concat = {
apply: (operand, inputData, { apply }) => {
const arrays = apply(operand, inputData);
return inputData.concat(...arrays);
},
evaluate: (operand, { evaluate }) => {
const [baseArray, ...arrays] = evaluate(operand);
return baseArray.concat(...arrays);
},
};
const $distinct = createArrayTransformExpression((array) => [
...new Set(array),
]);
/**
* Internal helper to find the first non-null value in an array.
* @param {Array} values - Array of values to check
* @returns {any} First non-null value or undefined
*/
const findFirstNonNull = (values) => values.find((value) => value != null);
const $coalesce = {
apply: (operand, inputData, { apply }) => {
const values = apply(operand, inputData);
return findFirstNonNull(values);
},
evaluate: (operand, { evaluate }) => {
const values = evaluate(operand);
return findFirstNonNull(values);
},
};
var iterativeExpressions = /*#__PURE__*/Object.freeze({
__proto__: null,
$all: $all,
$any: $any,
$append: $append,
$coalesce: $coalesce,
$concat: $concat,
$distinct: $distinct,
$filter: $filter,
$find: $find,
$flatMap: $flatMap,
$join: $join,
$map: $map,
$prepend: $prepend,
$reverse: $reverse,
$skip: $skip,
$take: $take
});
/**
* Base Pack - Near Universal Expressions
*
* Essential expressions used across almost all scenarios:
* - Data access ($get, $prop, $isDefined)
* - Basic conditionals ($if)
* - Common comparisons ($eq, $ne, $gt, $gte, $lt, $lte)
* - Operation chaining ($pipe)
* - Array operations ($filter, $map)
* - Utilities ($debug, $literal)
*/
// Export as grouped object
const base = {
$get,
$pipe,
$debug,
$literal,
$isDefined,
$prop,
$if,
$eq,
$ne,
$gt,
$gte,
$lt,
$lte,
$filter,
$map,
};
/**
* @typedef {object} ApplicativeExpression
*/
/**
* @typedef {object} Expression
*/
/**
* @template Args, Input, Output
* @typedef {object} Expression
* @property {function(any, Input): Output} apply
* @property {function(Args, Input, any): Output} [applyImplicit]
* @property {function(Input): Output} evaluate
* @property {string} [name]
* @property {object} schema
*/
/**
* @typedef {object} ExpressionEngine
* @property {function(Expression, any): any} apply
* @property {function(Expression): any} evaluate
* @property {string[]} expressionNames
* @property {function(Expression): boolean} isExpression
*/
/**
* @template Args, Input, Output
* @typedef {function(...any): Expression} FunctionExpression
*/
function looksLikeExpression(val) {
return (
val !== null &&
typeof val === "object" &&
!Array.isArray(val) &&
Object.keys(val).length === 1 &&
Object.keys(val)[0].startsWith("$")
);
}
/**
* @typedef {object} ExpressionEngineConfig
* @property {object[]} [packs] - Array of expression pack objects to include
* @property {object} [custom] - Custom expression definitions
* @property {boolean} [includeBase=true] - Whether to include base expressions
*/
/**
* @param {ExpressionEngineConfig} [config={}] - Configuration object for the expression engine
* @returns {ExpressionEngine}
*/
function createExpressionEngine(config = {}) {
const { packs = [], custom = {}, includeBase = true } = config;
const expressions = [...(includeBase ? [base] : []), ...packs, custom].reduce(
(acc, pack) => ({ ...acc, ...pack }),
{},
);
const isExpression = (val) =>
looksLikeExpression(val) && Object.keys(val)[0] in expressions;
const checkLooksLikeExpression = (val) => {
if (looksLikeExpression(val)) {
const [invalidOp] = Object.keys(val);
const availableOps = Object.keys(expressions);
const suggestion = didYouMean(invalidOp, availableOps);
const helpText = suggestion
? `Did you mean "${suggestion}"?`
: `Available operators: ${availableOps
.slice(0, 8)
.join(", ")}${availableOps.length > 8 ? ", ..." : ""}.`;
const message = `Unknown expression operator: "${invalidOp}". ${helpText} Use { $literal: ${JSON.stringify(val)} } if you meant this as a literal value.`;
throw new Error(message);
}
};
const apply = (val, inputData) => {
if (isExpression(val)) {
const [expressionName, operand] = Object.entries(val)[0];
const expressionDef = expressions[expressionName];
return expressionDef.apply(operand, inputData, { isExpression, apply });
}
checkLooksLikeExpression(val);
return Array.isArray(val)
? val.map((v) => apply(v, inputData))
: val !== null && typeof val === "object"
? esToolkit.mapValues(val, (v) => apply(v, inputData))
: val;
};
const evaluate = (val) => {
if (isExpression(val)) {
const [expressionName, operand] = Object.entries(val)[0];
const expressionDef = expressions[expressionName];
return expressionDef.evaluate(operand, { isExpression, evaluate, apply });
}
checkLooksLikeExpression(val);
return Array.isArray(val)
? val.map(evaluate)
: val !== null && typeof val === "object"
? esToolkit.mapValues(val, evaluate)
: val;
};
return {
apply,
evaluate,
expressionNames: Object.keys(expressions),
isExpression,
};
}
/**
* Creates an aggregative expression that applies a calculation function to resolved values.
*
* @param {function(Array): any} calculateFn - Function that takes an array of values and returns a calculated result
* @returns {object} Expression object with apply and evaluate methods
*/
const createAggregativeExpression = (calculateFn) => ({
apply(operand, inputData, { apply }) {
const values = apply(operand, inputData);
return calculateFn(values);
},
evaluate: (operand, { evaluate }) => {
const values = evaluate(operand);
return calculateFn(values);
},
});
const $count = createAggregativeExpression((values) => values.length);
const $max = createAggregativeExpression((values) => {
return values.length === 0
? undefined
: values.reduce((max, v) => Math.max(max, v));
});
const $min = createAggregativeExpression((values) => {
return values.length === 0
? undefined
: values.reduce((min, v) => Math.min(min, v));
});
const $sum = createAggregativeExpression((values) => {
return values.reduce((sum, v) => sum + v, 0);
});
const $mean = createAggregativeExpression((values) => {
return values.length === 0
? undefined
: values.reduce((sum, v) => sum + v, 0) / values.length;
});
const $median = createAggregativeExpression((values) => {
if (values.length === 0) return undefined;
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0
? (sorted[mid - 1] + sorted[mid]) / 2
: sorted[mid];
});
const $first = createAggregativeExpression((values) => {
return values.length === 0 ? undefined : values[0];
});
const $last = createAggregativeExpression((values) => {
return values.length === 0 ? undefined : values[values.length - 1];
});
var aggregative = /*#__PURE__*/Object.freeze({
__proto__: null,
$count: $count,
$first: $first,
$last: $last,
$max: $max,
$mean: $mean,
$median: $median,
$min: $min,
$sum: $sum
});
/**
* Aggregation Pack - Statistical Functions
*
* Statistical and aggregation functions for data analysis:
* - Basic aggregations ($count, $sum, $max, $min)
* - Statistical measures ($mean, $median)
* - Array reduction ($first, $last)
*/
// Export as grouped object
const aggregation = {
...aggregative,
};
/**
* Creates a math expression that performs binary operations.
* @param {function(number, number): number} operationFn - Function that takes (left, right) and returns result
* @param {function(number, number): void} [validateFn] - Optional validation function for divide by zero checks
* @returns {object} Expression object with apply and evaluate methods
*/
const createMathExpression = (operationFn, validateFn) => ({
apply: (operand, inputData) => {
if (validateFn) validateFn(inputData, operand);
return operationFn(inputData, operand);
},
evaluate: (operand, { evaluate }) => {
if (!Array.isArray(operand) || operand.length !== 2) {
throw new Error(
"Math expressions require array of exactly 2 elements in evaluate form",
);
}
const [left, right] = operand;
const leftValue = evaluate(left);
const rightValue = evaluate(right);
if (validateFn) validateFn(leftValue, rightValue);
return operationFn(leftValue, rightValue);
},
});
const $add = createMathExpression((left, right) => left + right);
const $subtract = createMathExpression((left, right) => left - right);
const $multiply = createMathExpression((left, right) => left * right);
const $divide = createMathExpression(
(left, right) => left / right,
(left, right) => {
if (right === 0) {
throw new Error("Division by zero");
}
},
);
const $modulo = createMathExpression(
(left, right) => left % right,
(left, right) => {
if (right === 0) {
throw new Error("Modulo by zero");
}
},
);
const $abs = {
apply: (operand, inputData) => Math.abs(inputData),
evaluate: (operand, { evaluate }) => Math.abs(evaluate(operand)),
};
const $pow = createMathExpression((left, right) => Math.pow(left, right));
const $sqrt = {
apply: (operand, inputData) => Math.sqrt(inputData),
evaluate: (operand, { evaluate }) => Math.sqrt(evaluate(operand)),
};
var mathExpressions = /*#__PURE__*/Object.freeze({
__proto__: null,
$abs: $abs,
$add: $add,
$divide: $divide,
$modulo: $modulo,
$multiply: $multiply,
$pow: $pow,
$sqrt: $sqrt,
$subtract: $subtract
});
/**
* Creates an array logical expression that applies a logical operation to an array of conditions.
* @param {function(Array, function): boolean} arrayMethodFn - Function that takes (array, predicate) and returns boolean
* @returns {object} Expression object with apply and evaluate methods
*/
const createArrayLogicalExpression = (arrayMethodFn) => ({
apply: (operand, inputData, { apply }) =>
arrayMethodFn(operand, (subexpr) => apply(subexpr, inputData)),
evaluate: (operand, { evaluate }) =>
arrayMethodFn(operand, (value) => {
return typeof value === "boolean" ? value : Boolean(evaluate(value));
}),
});
const $and = createArrayLogicalExpression((array, predicate) =>
array.every(predicate),
);
const $or = createArrayLogicalExpression((array, predicate) =>
array.some(predicate),
);
const $not = {
apply: (operand, inputData, { apply }) => !apply(operand, inputData),
evaluate: (operand, { evaluate }) => {
const value = typeof operand === "boolean" ? operand : evaluate(operand);
return !value;
},
};
var logicalExpressions = /*#__PURE__*/Object.freeze({
__proto__: null,
$and: $and,
$not: $not,
$or: $or
});
/**
* Creates a string pattern matching expression.
*
* @param {function(any, any): boolean} matchFn - Function that takes input and pattern and returns boolean
* @returns {object} Expression object with apply and evaluate methods
*/
const createStringPatternExpression = (matchFn) => ({
apply(operand, inputData, { apply }) {
const resolvedOperand = apply(operand, inputData);
return matchFn(inputData, resolvedOperand);
},
evaluate: (operand, { evaluate }) => {
const [pattern, inputData] = operand;
return matchFn(evaluate(inputData), evaluate(pattern));
},
});
/**
* Creates a string transformation expression.
*
* @param {function(string): any} transformFn - Function that transforms the string
* @returns {object} Expression object with apply and evaluate methods
*/
const createStringTransformExpression = (transformFn) => ({
apply: (operand, inputData) => transformFn(inputData),
evaluate: (operand, { evaluate }) => transformFn(evaluate(operand)),
});
/**
* Creates a string operation expression with parameters.
*
* @param {function(string, ...any): any} operationFn - Function that operates on string with params
* @returns {object} Expression object with apply and evaluate methods
*/
const createStringOperationExpression = (operationFn) => ({
apply: (operand, inputData, { apply }) => {
if (Array.isArray(operand)) {
const [param, ...rest] = operand;
return operationFn(
inputData,
apply(param, inputData),
...rest.map((r) => apply(r, inputData)),
);
}
return operationFn(inputData, apply(operand, inputData));
},
evaluate: (operand, { evaluate }) => {
const [str, ...params] = evaluate(operand);
return operationFn(str, ...params);
},
});
/**
* Internal helper to test if a string matches a regex pattern with flag parsing.
* @param {string} pattern - The regex pattern (possibly with inline flags)
* @param {string} inputData - The string to test
* @returns {boolean} Whether the pattern matches the input
*/
const testRegexPattern = (pattern, inputData) => {
if (typeof inputData !== "string") {
throw new Error("$matchesRegex requires string input");
}
// Extract inline flags and clean pattern
const flagMatch = pattern.match(/^\(\?([ims]*)\)(.*)/);
if (flagMatch) {
const [, flags, patternPart] = flagMatch;
let jsFlags = "";
if (flags.includes("i")) jsFlags += "i";
if (flags.includes("m")) jsFlags += "m";
if (flags.includes("s")) jsFlags += "s";
const regex = new RegExp(patternPart, jsFlags);
return regex.test(inputData);
}
// Check for unsupported inline flags and strip them
const unsupportedFlagMatch = pattern.match(/^\(\?[^)]*\)(.*)/);
if (unsupportedFlagMatch) {
const [, patternPart] = unsupportedFlagMatch;
const regex = new RegExp(patternPart);
return regex.test(inputData);
}
// No inline flags - use PCRE defaults
const regex = new RegExp(pattern);
return regex.test(inputData);
};
/**
* Tests if a string matches a regular expression pattern.
*
* **Uses PCRE (Perl Compatible Regular Expression) semantics** as the canonical standard
*
* Supports inline flags using the syntax (?flags)pattern where flags can be:
* - i: case insensitive matching
* - m: multiline mode (^ and $ match line boundaries)
* - s: dotall mode (. matches newlines)
*
* PCRE defaults (when no flags specified):
* - Case-sensitive matching
* - ^ and $ match string boundaries (not line boundaries)
* - . does not match newlines
*
* @example
* // Basic pattern matching
* apply("hello", "hello world") // true
* apply("\\d+", "abc123") // true
*
* @example
* // With inline flags
* apply("(?i)hello", "HELLO WORLD") // true (case insensitive)
* apply("(?m)^line2", "line1\nline2") // true (multiline)
* apply("(?s)hello.world", "hello\nworld") // true (dotall)
* apply("(?ims)^hello.world$", "HELLO\nWORLD") // true (combined flags)
*
* @example
* // In WHERE clauses
* { name: { $matchesRegex: "^[A-Z].*" } } // Names starting with capital letter
* { email: { $matchesRegex: "(?i).*@example\\.com$" } } // Case-insensitive email domain check
*/
const $matchesRegex = {
apply(operand, inputData, { apply }) {
const resolvedOperand = apply(operand, inputData);
return testRegexPattern(resolvedOperand, inputData);
},
evaluate: (operand, { evaluate }) => {
const [pattern, inputData] = operand;
const resolvedPattern = evaluate(pattern);
const resolvedInputData = evaluate(inputData);
return testRegexPattern(resolvedPattern, resolvedInputData);
},
};
/**
* Tests if a string matches a SQL LIKE pattern.
*
* Provides database-agnostic LIKE pattern matching with SQL standard semantics:
* - % matches any sequence of characters (including none)
* - _ matches exactly one character
* - Case-sensitive matching (consistent across databases)
*
* @example
* // Basic LIKE patterns
* apply("hello%", "hello world") // true
* apply("%world", "hello world") // true
* apply("h_llo", "hello") // true
* apply("h_llo", "hallo") // true
*
* @example
* // In WHERE clauses
* { name: { $matchesLike: "John%" } } // Names starting with "John"
* { email: { $matchesLike: "%@gmail.com" } } // Gmail addresses
* { code: { $matchesLike: "A_B_" } } // Codes like "A1B2", "AXBY"
*/
const $matchesLike = createStringPatternExpression((inputData, pattern) => {
if (typeof inputData !== "string") {
throw new Error("$matchesLike requires string input");
}
// Convert SQL LIKE pattern to JavaScript regex
let regexPattern = pattern
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") // Escape regex special chars
.replace(/%/g, ".*") // % becomes .*
.replace(/_/g, "."); // _ becomes .
// Anchor the pattern to match the entire string
regexPattern = "^" + regexPattern + "$";
const regex = new RegExp(regexPattern);
return regex.test(inputData);
});
/**
* Tests if a string matches a Unix shell GLOB pattern.
*
* Provides database-agnostic GLOB pattern matching with Unix shell semantics:
* - * matches any sequence of characters (including none)
* - ? matches exactly one character
* - [chars] matches any single character in the set
* - [!chars] or [^chars] matches any character not in the set
* - Case-sensitive matching
*
* @example
* // Basic GLOB patterns
* apply("hello*", "hello world") // true
* apply("*world", "hello world") // true
* apply("h?llo", "hello") // true
* apply("h?llo", "hallo") // true
* apply("[hw]ello", "hello") // true
* apply("[hw]ello", "wello") // true
* apply("[!hw]ello", "bello") // true
*
* @example
* // In WHERE clauses
* { filename: { $matchesGlob: "*.txt" } } // Text files
* { name: { $matchesGlob: "[A-Z]*" } } // Names starting with capital
* { code: { $matchesGlob: "IMG_[0-9][0-9][0-9][0-9]" } } // Image codes
*/
const $matchesGlob = createStringPatternExpression((inputData, pattern) => {
if (typeof inputData !== "string") {
throw new Error("$matchesGlob requires string input");
}
// Convert GLOB pattern to JavaScript regex
let regexPattern = "";
let i = 0;
while (i < pattern.length) {
const char = pattern[i];
if (char === "*") {
regexPattern += ".*";
} else if (char === "?") {
regexPattern += ".";
} else if (char === "[") {
// Handle character classes
let j = i + 1;
let isNegated = false;
// Check for negation
if (j < pattern.length && (pattern[j] === "!" || pattern[j] === "^")) {
isNegated = true;
j++;
}
// Find the closing bracket
let classContent = "";
while (j < pattern.length && pattern[j] !== "]") {
classContent += pattern[j];
j++;
}
if (j < pattern.length) {
// Valid character class
regexPattern +=
"[" +
(isNegated ? "^" : "") +
classContent.replace(/\\/g, "\\\\") +
"]";
i = j;
} else {
// No closing bracket, treat as literal
regexPattern += "\\[";
}
} else {
// Escape regex special characters
regexPattern += char.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
i++;
}
// Anchor the pattern to match the entire string
regexPattern = "^" + regexPattern + "$";
const regex = new RegExp(regexPattern);
return regex.test(inputData);
});
const $split = createStringOperationExpression((str, delimiter) =>
str.split(delimiter),
);
const $trim = createStringTransformExpression((str) => str.trim());
const $uppercase = createStringTransformExpression((str) => str.toUpperCase());
const $lowercase = createStringTransformExpression((str) => str.toLowerCase());
const $replace = createStringOperationExpression((str, search, replacement) =>
str.replace(new RegExp(search, "g"), replacement),
);
const $substring = createStringOperationExpression((str, start, length) =>
length !== undefined ? str.slice(start, start + length) : str.slice(start),
);
var stringExpressions = /*#__PURE__*/Object.freeze({
__proto__: null,
$lowercase: $lowercase,
$matchesGlob: $matchesGlob,
$matchesLike: $matchesLike,
$matchesRegex: $matchesRegex,
$replace: $replace,
$split: $split,
$substring: $substring,
$trim: $trim,
$uppercase: $uppercase
});
/**
* Creates a temporal expression that generates time-based values without needing operands or input data.
* @param {function(): any} generateFn - Function that generates a time-based value
* @returns {object} Expression object with apply and evaluate methods
*/
const createTemporalExpression = (generateFn) => ({
apply: generateFn,
evaluate: generateFn,
});
const $nowLocal = createTemporalExpression(() => {
const now = new Date();
const offset = -now.getTimezoneOffset();
const sign = offset >= 0 ? "+" : "-";
const hours = Math.floor(Math.abs(offset) / 60)
.toString()
.padStart(2, "0");
const minutes = (Math.abs(offset) % 60).toString().padStart(2, "0");
return now.toISOString().slice(0, -1) + sign + hours + ":" + minutes;
});
const $nowUTC = createTemporalExpression(() => new Date().toISOString());
const $timestamp = createTemporalExpression(() => Date.now());
/**
* Internal helper to add time to a date.
* @param {Date} date - The base date
* @param {number} amount - The amount to add
* @param {string} unit - The unit (milliseconds, seconds, minutes, hours, days, weeks, months, years)
* @returns {Date} New date with time added
*/
const addTimeToDate = (date, amount, unit) => {
switch (unit) {
case "milliseconds":
return new Date(date.getTime() + amount);
case "seconds":
return new Date(date.getTime() + amount * 1000);
case "minutes":
return new Date(date.getTime() + amount * 60 * 1000);
case "hours":
return new Date(date.getTime() + amount * 60 * 60 * 1000);
case "days":
return new Date(date.getTime() + amount * 24 * 60 * 60 * 1000);
case "weeks":
return new Date(date.getTime() + amount * 7 * 24 * 60 * 60 * 1000);
case "months": {
const result = new Date(date);
result.setMonth(result.getMonth() + amount);
return result;
}
case "years": {
const result = new Date(date);
result.setFullYear(result.getFullYear() + amount);
return result;
}
default:
throw new Error(`Unsupported time unit: ${unit}`);
}
};
const $timeAdd = {
apply: (operand, inputData, { apply }) => {
const { amount, unit } = apply(operand, inputData);
const date = new Date(inputData);
return addTimeToDate(date, amount, unit);
},
evaluate: (operand, { evaluate }) => {
const { date, amount, unit } = evaluate(operand);
const dateObj = new Date(date);
return addTimeToDate(dateObj, amount, unit);
},
};
/**
* Internal helper to convert millisecond difference to specified unit.
* @param {number} diffMs - Difference in milliseconds
* @param {string} unit - The unit to convert to
* @returns {number} The difference in the specified unit
*/
const convertTimeDifference = (diffMs, unit) => {
switch (unit) {
case "milliseconds":
return diffMs;
case "seconds":
return Math.floor(diffMs / 1000);
case "minutes":
return Math.floor(diffMs / (60 * 1000));
case "hours":
return Math.floor(diffMs / (60 * 60 * 1000));
case "days":
return Math.floor(diffMs / (24 * 60 * 60 * 1000));
case "weeks":
return Math.floor(diffMs / (7 * 24 * 60 * 60 * 1000));
default:
throw new Error(`Unsupported time unit: ${unit}`);
}
};
const $timeDiff = {
apply: (operand, inputData, { apply }) => {
const { endDate, unit = "milliseconds" } = apply(operand, inputData);
const startDate = new Date(inputData);
const end = new Date(endDate);
const diffMs = end.getTime() - startDate.getTime();
return convertTimeDifference(diffMs, unit);
},
evaluate: (operand, { evaluate }) => {
const { startDate, endDate, unit = "milliseconds" } = evaluate(operand);
const start = new Date(startDate);
const end = new Date(endDate);
const diffMs = end.getTime() - start.getTime();
return convertTimeDifference(diffMs, unit);
},
};
/**
* Internal helper to format a date according to a format string.
* @param {Date} date - The date to format
* @param {string} format - The format type (iso, date, time)
* @returns {string} The formatted date string
*/
const formatDate = (date, format) => {
return date.toLocaleString("en-US", {
...(format === "iso" && {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
...(format === "date" && {
year: "numeric",
month: "2-digit",
day: "2-digit",
}),
...(format === "time" && {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}),
});
};
const $formatTime = {
apply: (operand, inputData, { apply }) => {
const format = apply(operand, inputData);
const date = new Date(inputData);
return formatDate(date, format);
},
evaluate: (operand, { evaluate }) => {
const { date, format } = evaluate(operand);
const dateObj = new Date(date);
return formatDate(dateObj, format);
},
};
var temporalExpressions = /*#__PURE__*/Object.freeze({
__proto__: null,
$formatTime: $formatTime,
$nowLocal: $nowLocal,
$nowUTC: $nowUTC,
$timeAdd: $timeAdd,
$timeDiff: $timeDiff,
$timestamp: $timestamp
});
/**
* Creates a generative expression that produces values without needing input data or nested expressions.
* @param {function(any): any} generateFn - Function that takes operand and generates a value
* @returns {object} Expression object with apply and evaluate methods
*/
const createGenerativeExpression = (generateFn) => ({
apply: (operand) => generateFn(operand),
evaluate: (operand) => generateFn(operand),
});
const $random = createGenerativeExpression((operand = {}) => {
const { min = 0, max = 1, precision = null } = operand ?? {};
const value = Math.random() * (max - min) + min;
if (precision == null) {
return value;
}
if (precision >= 0) {
// Positive precision: decimal places
return Number(value.toFixed(precision));
} else {
// Negative precision: round to 10^(-precision)
const factor = Math.pow(10, -precision);
return Math.round(value / factor) * factor;
}
});
const $uuid = createGenerativeExpression(() => crypto.randomUUID());
var generative = /*#__PURE__*/Object.freeze({
__proto__: null,
$random: $random,
$uuid: $uuid
});
/**
* All Pack - Complete Expression Library
*
* Comprehensive collection of all available expressions:
* - Base expressions (data access, conditionals, comparisons, utilities)
* - Math operations (arithmetic, mathematical functions)
* - Aggregation functions (statistics, array reduction)
* - Logic operations (boolean logic, conditionals)
* - Comparison operations (scalar comparisons, range/existence checks)
* - Array operations (transformations, predicates, manipulations)
* - String operations (pattern matching, transformations)
* - Time operations (temporal functions, calculations, formatting)
* - Generative operations (random values, UUIDs)
*
* This pack is primarily intended for testing and comprehensive usage scenarios.
*/
// Export as grouped object containing all expressions
const all = {
...core,
...mathExpressions,
...aggregative,
...logicalExpressions,
...conditionalExpressions,
...comparativeExpressions,
...iterativeExpressions,
...stringExpressions,
...temporalExpressions,
...generative,
};
/**
* Array Pack - Complete Array Manipulation Toolkit
*
* All array operations for data transformation and processing:
* - Core transformations ($map, $filter, $find)
* - Predicates ($all, $any)
* - Advanced operations ($flatMap)
* - Array modifications ($append, $prepend, $reverse, $join)
* - Array slicing ($take, $skip)
* - Array operations ($concat, $distinct)
* - Utility ($coalesce)
*/
// Export as grouped object
const array = {
...iterativeExpressions,
};
/**
* Comparison Pack - Scalar Comparison Operations
*
* Basic comparison operations for WHERE clause logic:
* - Basic comparisons ($eq, $ne, $gt, $gte, $lt, $lte)
* - Membership tests ($in, $nin)
* - Range and existence checks ($between, $isNull, $isNotNull)
*/
// Export as grouped object
const comparison = {
...comparativeExpressions,
};
/**
* Filtering Pack - Comprehensive Data Filtering Toolkit
*
* Complete toolkit for WHERE clause logic and data filtering:
* - Field access ($get, $pipe)
* - Basic comparisons ($eq, $ne, $gt, $gte, $lt, $lte)
* - Logic operations ($and, $or, $not)
* - Membership tests ($in, $nin)
* - Existence checks ($isNull, $isNotNull)
* - Pattern matching ($matchesRegex, $matchesLike, $matchesGlob)
*/
// Export as grouped object
const filtering = {
// Field access
$get,
$pipe,
// Basic comparisons
$eq,
$ne,
$gt,
$gte,
$lt,
$lte,
// Logic operations
$and,
$or,
$not,
// Membership tests
$in,
$nin,
// Existence checks
$isNull,
$isNotNull,
// Pattern matching
$matchesRegex,
$matchesLike,
$matchesGlob,
};
/**
* Logic Pack - Boolean Logic and Conditionals
*
* All boolean logic and conditional operations:
* - Boolean operations ($and, $or, $not)
* - Conditional branching ($if, $case)
*/
// Export as grouped object
const logic = {
...logicalExpressions,
...conditionalExpressions,
};
/**
* Math Pack - Arithmetic Operations
*
* Pure arithmetic operations for mathematical calculations:
* - Basic operations ($add, $subtract, $multiply, $divide)
* - Modulo operation ($modulo)
* - Mathematical functions ($abs, $pow, $sqrt)
* - Random number generation ($random)
*/
// Export as grouped object
const math = {
...mathExpressions,
$random,
};
/**
* Projection Pack - Data Transformation and Projection Toolkit
*
* Complete toolkit for SELECT clause operations and data transformation:
* - Aggregation functions ($count, $sum, $min, $max, $mean)
* - Array transformations ($map, $filter, $flatMap, $distinct)
* - String/value operations ($concat, $substring, $uppercase, $lowercase, $join)
* - Conditionals for computed fields ($if, $case)
* - Field access ($get, $pipe)
*/
// Export as grouped object
const projection = {
// Field access and chaining
$get,
$pipe,
// Aggregation functions
$count,
$sum,
$min,
$max,
$mean,
// Array transformations
$map,
$filter,
$flatMap,
$distinct,
$concat,
$join,
// String/value transformations
$substring,
$uppercase,
$lowercase,
// Conditionals for computed fields
$if,
$case,
};
/**
* String Pack - String Processing Operations
*
* String processing and pattern matching functions:
* - Pattern matching ($matchesRegex, $matchesLike, $matchesGlob)
* - String transformations ($split, $trim, $uppercase, $lowercase)
* - String operations ($replace, $substring)
*/
// Export as grouped object
const string = {
...stri