godcrm/backend/utils/formulaParser.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

297 lines
7 KiB
JavaScript

/**
* Formula Parser Utility - ADR-026
* Extracts dependencies and provides formula functions
*
* @module utils/formulaParser
*/
/**
* Extract column references from formula ({{column_name}} syntax)
* @param {string} formula - Formula string
* @returns {string[]} Array of unique column names
*/
export function extractColumns(formula) {
if (!formula || typeof formula !== 'string') {
return [];
}
const columnPattern = /\{\{(\w+)\}\}/g;
const matches = [...formula.matchAll(columnPattern)];
const columns = matches.map(m => m[1]);
// Return unique values
return [...new Set(columns)];
}
/**
* Extract variable references from formula ($variable_name syntax)
* @param {string} formula - Formula string
* @returns {string[]} Array of unique variable names with $ prefix
*/
export function extractVariables(formula) {
if (!formula || typeof formula !== 'string') {
return [];
}
// Match $word_with_underscores but not just numbers after $
const variablePattern = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
const matches = [...formula.matchAll(variablePattern)];
const variables = matches.map(m => '$' + m[1]);
// Return unique values
return [...new Set(variables)];
}
/**
* Extract function calls from formula (FUNCTION_NAME(...) syntax)
* @param {string} formula - Formula string
* @returns {string[]} Array of unique function names (uppercase)
*/
export function extractFunctions(formula) {
if (!formula || typeof formula !== 'string') {
return [];
}
// Match FUNCTION_NAME followed by (
const functionPattern = /([A-Z][A-Z0-9_]*)\s*\(/g;
const matches = [...formula.matchAll(functionPattern)];
const functions = matches.map(m => m[1]);
// Return unique values
return [...new Set(functions)];
}
/**
* Parse formula and extract all dependencies
* @param {string} formula - Formula string
* @returns {{columns: string[], variables: string[], functions: string[]}}
*/
export function parseFormulaDependencies(formula) {
return {
columns: extractColumns(formula),
variables: extractVariables(formula),
functions: extractFunctions(formula)
};
}
/**
* Registry of supported formula functions
* @type {Object.<string, Function>}
*/
export const FORMULA_FUNCTIONS = {
// ============================================================
// Aggregation functions
// ============================================================
/**
* Sum of all values
* @param {number[]} values
* @returns {number}
*/
SUM: (values) => values.reduce((a, b) => a + b, 0),
/**
* Average of all values
* @param {number[]} values
* @returns {number}
*/
AVG: (values) => values.length === 0 ? 0 : values.reduce((a, b) => a + b, 0) / values.length,
/**
* Minimum value
* @param {number[]} values
* @returns {number}
*/
MIN: (values) => Math.min(...values),
/**
* Maximum value
* @param {number[]} values
* @returns {number}
*/
MAX: (values) => Math.max(...values),
/**
* Count of values
* @param {any[]} values
* @returns {number}
*/
COUNT: (values) => values.length,
/**
* Count values matching condition
* @param {any[]} values
* @param {string} condition
* @returns {number}
*/
COUNTIF: (values, condition) => {
return values.filter(v => v === condition).length;
},
// ============================================================
// Math functions
// ============================================================
/**
* Absolute value
* @param {number} value
* @returns {number}
*/
ABS: (value) => Math.abs(value),
/**
* Round to decimals
* @param {number} value
* @param {number} decimals
* @returns {number}
*/
ROUND: (value, decimals = 0) => {
const factor = Math.pow(10, decimals);
return Math.round(value * factor) / factor;
},
/**
* Floor value
* @param {number} value
* @returns {number}
*/
FLOOR: (value) => Math.floor(value),
/**
* Ceiling value
* @param {number} value
* @returns {number}
*/
CEIL: (value) => Math.ceil(value),
// ============================================================
// Text functions
// ============================================================
/**
* Concatenate strings
* @param {...string} args
* @returns {string}
*/
CONCAT: (...args) => args.join(''),
/**
* Uppercase string
* @param {string} value
* @returns {string}
*/
UPPER: (value) => String(value).toUpperCase(),
/**
* Lowercase string
* @param {string} value
* @returns {string}
*/
LOWER: (value) => String(value).toLowerCase(),
/**
* String length
* @param {string} value
* @returns {number}
*/
LEN: (value) => String(value).length,
// ============================================================
// Date functions
// ============================================================
/**
* Current date/time
* @returns {Date}
*/
NOW: () => new Date(),
/**
* Today at midnight
* @returns {Date}
*/
TODAY: () => {
const now = new Date();
now.setHours(0, 0, 0, 0);
return now;
},
/**
* Difference in days between two dates
* @param {Date} date1
* @param {Date} date2
* @returns {number}
*/
DATEDIFF: (date1, date2) => {
const d1 = new Date(date1);
const d2 = new Date(date2);
const diffTime = Math.abs(d2 - d1);
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
},
// ============================================================
// Conditional functions
// ============================================================
/**
* If condition
* @param {boolean} condition
* @param {any} trueValue
* @param {any} falseValue
* @returns {any}
*/
IF: (condition, trueValue, falseValue) => condition ? trueValue : falseValue,
/**
* Switch statement
* @param {any} value
* @param {...any} cases - pairs of (match, result)
* @returns {any}
*/
SWITCH: (value, ...cases) => {
for (let i = 0; i < cases.length - 1; i += 2) {
if (value === cases[i]) {
return cases[i + 1];
}
}
// Return last value as default if odd number of cases
if (cases.length % 2 === 1) {
return cases[cases.length - 1];
}
return null;
},
/**
* Handle errors gracefully
* @param {any} expression
* @param {any} fallback
* @returns {any}
*/
IFERROR: (expression, fallback) => {
try {
if (expression === null || expression === undefined || Number.isNaN(expression)) {
return fallback;
}
return expression;
} catch {
return fallback;
}
}
};
/**
* Check if a function name is supported
* @param {string} functionName - Function name (uppercase)
* @returns {boolean}
*/
export function isSupportedFunction(functionName) {
return functionName in FORMULA_FUNCTIONS;
}
/**
* Get list of all supported function names
* @returns {string[]}
*/
export function getSupportedFunctions() {
return Object.keys(FORMULA_FUNCTIONS);
}