UNPKG

json-expressions

Version:

A JavaScript expression engine for JSON-based dynamic computations and function composition

1,735 lines (1,556 loc) 52.5 kB
'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