UNPKG

jexl-extended

Version:

Extended grammar for Javascript Expression Language (JEXL)

770 lines (769 loc) 27.3 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.arrayEvery = exports.arrayAny = exports.arrayMap = exports.mapField = exports.arrayToObject = exports.arrayDistinct = exports.arraySort = exports.arrayShuffle = exports.arrayReverse = exports.arrayAppend = exports.switchCase = exports.not = exports.toBoolean = exports.average = exports.min = exports.max = exports.sum = exports.formatInteger = exports.formatBase = exports.formatNumber = exports.randomNumber = exports.sqrt = exports.power = exports.round = exports.ceil = exports.floor = exports.absoluteValue = exports.parseInteger = exports.toNumber = exports.formUrlEncoded = exports.base64Decode = exports.base64Encode = exports.replace = exports.arrayJoin = exports.split = exports.endsWith = exports.startsWith = exports.contains = exports.pad = exports.trim = exports.pascalCase = exports.camelCase = exports.lowercase = exports.uppercase = exports.substringAfter = exports.substringBefore = exports.substring = exports.length = exports.toJson = exports.toString = void 0; exports.uuid = exports._eval = exports.dateTimeAdd = exports.dateTimeToMillis = exports.dateTimeFormat = exports.toDateTime = exports.millis = exports.now = exports.objectMerge = exports.objectEntries = exports.objectValues = exports.objectKeys = exports.arrayReduce = exports.arrayFind = exports.arrayFilter = void 0; const date_fns_1 = require("date-fns"); const uuid_1 = require("uuid"); const _1 = __importDefault(require(".")); /** * Casts the input to a string. * * @example * ```jexl * string(123) // "123" * 123|string // "123" * ``` * @group Type Conversion * * @param input The input can be any type. * @param prettify If true, the output will be pretty-printed. */ const toString = (input, prettify = false) => { return JSON.stringify(input, null, prettify ? 2 : 0); }; exports.toString = toString; /** * Parses the string and returns a JSON object. * * @example * ```jexl * parseJson('{"key": "value"}') // { key: "value" } * '{"key": "value"}'|toJson // { key: "value" } */ const toJson = (input) => { return JSON.parse(input); }; exports.toJson = toJson; /** * Returns the number of characters in a string, or the length of an array. * * @example * ```jexl * length("hello") // 5 * length([1, 2, 3]) // 3 * ``` * * @param input The input can be a string, an array, or an object. * @returns The number of characters in a string, or the length of an array. */ const length = (input) => { if (typeof input === 'string') { return input.length; } if (Array.isArray(input)) { return input.length; } if (typeof input === 'object' && input !== null) { return Object.keys(input).length; } return 0; }; exports.length = length; /** * Gets a substring of a string. * * @example * ```jexl * substring("hello world", 0, 5) // "hello" * ``` * * @param input The input string. * @param start The starting index of the substring. * @param length The length of the substring. * @returns The substring of the input string. */ const substring = (input, start, length) => { let str = input; if (typeof str !== 'string') { str = JSON.stringify(str); } if (typeof str === 'string') { let startNum = start; let len = length !== null && length !== void 0 ? length : str.length; if (startNum < 0) { startNum = str.length + start; if (startNum < 0) { startNum = 0; } } if (startNum + len > str.length) { len = str.length - startNum; } if (len < 0) { len = 0; } return str.substring(startNum, startNum + len); } return ''; }; exports.substring = substring; /** * Returns the substring before the first occurrence of the character sequence chars in str. * * @example * ```jexl * substringBefore("hello world", " ") // "hello" * ``` * @param input The input string. * @param chars The character sequence to search for. * @returns The substring before the first occurrence of the character sequence chars in str. */ const substringBefore = (input, chars) => { const str = typeof input === 'string' ? input : JSON.stringify(input); const charsStr = typeof chars === 'string' ? chars : JSON.stringify(chars); const index = str.indexOf(charsStr); if (index === -1) { return str; } return str.substring(0, index); }; exports.substringBefore = substringBefore; /** * Returns the substring after the first occurrence of the character sequence chars in str. * * @example * ```jexl * substringAfter("hello world", " ") // "world" * ``` * * @param input The input string. * @param chars The character sequence to search for. * @returns The substring after the first occurrence of the character sequence chars in str. */ const substringAfter = (input, chars) => { const str = typeof input === 'string' ? input : JSON.stringify(input); const charsStr = typeof chars === 'string' ? chars : JSON.stringify(chars); const index = str.indexOf(charsStr); if (index === -1) { return ''; } return str.substring(index + charsStr.length); }; exports.substringAfter = substringAfter; /** Converts the input string to uppercase. */ const uppercase = (input) => { const str = typeof input === 'string' ? input : JSON.stringify(input); return str.toUpperCase(); }; exports.uppercase = uppercase; /** Converts the input string to lowercase. */ const lowercase = (input) => { const str = typeof input === 'string' ? input : JSON.stringify(input); return str.toLowerCase(); }; exports.lowercase = lowercase; const splitRegex = /(?<!^)(?=[A-Z])|[`~!@#%^&*()|+\\\-=?;:'.,\s_']+/; /** Converts the input string to camel case. */ const camelCase = (input) => { if (typeof input !== 'string') return ''; return input.split(splitRegex).map((word, index) => { if (index === 0) return word.toLowerCase(); return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); }).join(''); }; exports.camelCase = camelCase; /** Converts the input string to pascal case. */ const pascalCase = (input) => { if (typeof input !== 'string') return ''; return input.split(splitRegex).map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(''); }; exports.pascalCase = pascalCase; /** Trims whitespace from both ends of a string. */ const trim = (input, trimChar) => { if (typeof input === 'string') { if (trimChar) { return input.replace(new RegExp(`^${trimChar}+|${trimChar}+$`, 'g'), ''); } return input.trim(); } return ''; }; exports.trim = trim; /** Pads the input string on both sides to center it. */ const pad = (input, width, char = ' ') => { const str = typeof input !== 'string' ? JSON.stringify(input) : input; if (width > 0) { return str.padEnd(width, char); } else { return str.padStart(-width, char); } }; exports.pad = pad; /** Checks if the input string contains the specified substring. */ const contains = (input, search) => { if (typeof input === 'string' || Array.isArray(input)) { return input.includes(search); } return false; }; exports.contains = contains; /** Checks if the input string starts with the specified substring. */ const startsWith = (input, search) => { if (typeof input === 'string') { return input.startsWith(search); } return false; }; exports.startsWith = startsWith; /** Checks if the input string ends with the specified substring. */ const endsWith = (input, search) => { if (typeof input === 'string') { return input.endsWith(search); } return false; }; exports.endsWith = endsWith; /** Splits the input string into an array of substrings. */ const split = (input, separator) => { if (typeof input === 'string') { return input.split(separator); } return []; }; exports.split = split; /** Joins elements of an array into a string. */ const arrayJoin = (input, separator) => { if (Array.isArray(input)) { return input.join(separator); } return undefined; }; exports.arrayJoin = arrayJoin; /** Replaces occurrences of a specified string. */ const replace = (input, search, replacement) => { if (typeof input === 'string' && typeof search === 'string') { const _replacement = replacement === undefined ? '' : replacement; return input.replace(new RegExp(search, 'g'), _replacement); } return undefined; }; exports.replace = replace; /** Encodes a string to Base64. */ const base64Encode = (input) => { if (typeof input === 'string') { try { encodeURIComponent(input); const bytes = new TextEncoder().encode(input); const binString = String.fromCodePoint(...bytes); return btoa(binString); } catch (error) { return ''; } } return ''; }; exports.base64Encode = base64Encode; /** Decodes a Base64 encoded string. */ const base64Decode = (input) => { if (typeof input === 'string') { const binString = atob(input); const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0)); return new TextDecoder().decode(bytes); } return ''; }; exports.base64Decode = base64Decode; /** Encodes a string or object to URI. */ const formUrlEncoded = (input) => { if (typeof input === 'string') { return encodeURIComponent(input); } else if (typeof input === 'object') { return Object.keys(input).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(input[key])}`).join('&'); } return ''; }; exports.formUrlEncoded = formUrlEncoded; /** Converts the input to a number. */ const toNumber = (input) => { if (typeof input === 'number') return input; if (typeof input === 'string') return parseFloat(input); return NaN; }; exports.toNumber = toNumber; /** Parses a string and returns an integer. */ const parseInteger = (input) => { if (typeof input === 'string') { return parseInt(input, 10); } else if (typeof input === 'number') { return Math.floor(input); } return NaN; }; exports.parseInteger = parseInteger; /** Returns the absolute value of a number. */ const absoluteValue = (input) => { const num = (0, exports.toNumber)(input); return isNaN(num) ? NaN : Math.abs(num); }; exports.absoluteValue = absoluteValue; /** Rounds a number down to the nearest integer. */ const floor = (input) => { const num = (0, exports.toNumber)(input); return isNaN(num) ? NaN : Math.floor(num); }; exports.floor = floor; /** Rounds a number up to the nearest integer. */ const ceil = (input) => { const num = (0, exports.toNumber)(input); return isNaN(num) ? NaN : Math.ceil(num); }; exports.ceil = ceil; /** Rounds a number to the nearest integer. */ const round = (input, decimals) => { const num = (0, exports.toNumber)(input); return isNaN(num) ? NaN : decimals ? Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals) : Math.round(num); }; exports.round = round; /** Returns the value of a number raised to a power. */ const power = (input, exponent) => { const num = (0, exports.toNumber)(input); const exp = exponent === undefined ? 2 : exponent; return isNaN(num) ? NaN : Math.pow(num, exp); }; exports.power = power; /** Returns the square root of a number. */ const sqrt = (input) => { const num = (0, exports.toNumber)(input); return isNaN(num) ? NaN : Math.sqrt(num); }; exports.sqrt = sqrt; /** Generates a random number between 0 (inclusive) and 1 (exclusive). */ const randomNumber = () => { return Math.random(); }; exports.randomNumber = randomNumber; /** Casts the number to a string and formats it to a decimal representation as specified by the format string. */ const formatNumber = (input, format) => { var _a, _b, _c; const num = typeof input === 'number' ? input : parseInt((0, exports.toNumber)(input).toString(), 10); return isNaN(num) ? '' : num.toLocaleString('en-us', { minimumFractionDigits: (_a = format.split('.')[1]) === null || _a === void 0 ? void 0 : _a.length, maximumFractionDigits: (_b = format.split('.')[1]) === null || _b === void 0 ? void 0 : _b.length, useGrouping: (_c = format.split('.')[0]) === null || _c === void 0 ? void 0 : _c.includes(',') }); }; exports.formatNumber = formatNumber; /** Formats a number as a string in the specified base. */ const formatBase = (input, base) => { const num = typeof input === 'number' ? input : parseInt((0, exports.toNumber)(input).toString(), 10); return isNaN(num) ? '' : num.toString(base); }; exports.formatBase = formatBase; /** Formats a number as an integer. */ const formatInteger = (input, format) => { const num = (0, exports.toNumber)(input); return isNaN(num) ? '' : (0, exports.pad)(Math.floor(num).toString(), -format.length, '0'); }; exports.formatInteger = formatInteger; /** Calculates the sum of an array of numbers. */ const sum = (...input) => { if (!Array.isArray(input)) return NaN; return input.flat().reduce((acc, val) => acc + (0, exports.toNumber)(val), 0); }; exports.sum = sum; /** Finds the maximum value in an array of numbers. */ const max = (...input) => { if (!Array.isArray(input)) return NaN; return Math.max(...input.flat().map(exports.toNumber)); }; exports.max = max; /** Finds the minimum value in an array of numbers. */ const min = (...input) => { if (!Array.isArray(input)) return NaN; return Math.min(...input.flat().map(exports.toNumber)); }; exports.min = min; /** Calculates the average of an array of numbers. */ const average = (...input) => { if (!Array.isArray(input)) return NaN; const total = (0, exports.sum)(...input); return total / input.flat().length; }; exports.average = average; /** Converts the input to a boolean. */ const toBoolean = (input) => { if (typeof input === 'boolean') return input; if (typeof input === 'number') return input !== 0; if (typeof input === 'string') { if (input.trim().toLowerCase() === 'true' || input.trim() === '1') return true; if (input.trim().toLowerCase() === 'false' || input.trim() === '0') return false; else return undefined; } return Boolean(input); }; exports.toBoolean = toBoolean; /** Returns the logical NOT of the input. */ const not = (input) => { return !(0, exports.toBoolean)(input); }; exports.not = not; /** * Evaluates a list of predicates and returns the first result expression whose predicate is satisfied. * * @example * ```jexl * switch(expression, case1, result1, case2, result2, ..., default) * ``` * * @param args The arguments array where the first element is the expression to evaluate, followed by pairs of case and result, and optionally a default value. * @returns The result of the first case whose predicate is satisfied, or the default value if no case is satisfied. */ const switchCase = (...args) => { if (args.length < 3) return null; const expressionResult = args[0]; for (let i = 1; i < args.length - 1; i += 2) { const caseResult = args[i]; if (JSON.stringify(expressionResult) === JSON.stringify(caseResult)) { return args[i + 1]; } } // Return default if (args.length % 2 === 0) { const defaultResult = args[args.length - 1]; return defaultResult; } // Return null if no default specified return null; }; exports.switchCase = switchCase; /** Appends an element to an array. */ const arrayAppend = (...input) => { if (!Array.isArray(input)) return []; return [...input.flat()]; }; exports.arrayAppend = arrayAppend; /** Reverses the elements of an array. */ const arrayReverse = (...input) => { if (!Array.isArray(input)) return []; return [...input.flat()].reverse(); }; exports.arrayReverse = arrayReverse; /** Shuffles the elements of an array. */ const arrayShuffle = (input) => { if (!Array.isArray(input)) return []; for (let i = input.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [input[i], input[j]] = [input[j], input[i]]; } return input; }; exports.arrayShuffle = arrayShuffle; /** Sorts the elements of an array. */ const arraySort = (input, expression, descending) => { if (!Array.isArray(input)) return []; if (!expression) return [...input].sort(); const expr = _1.default.compile(expression); const compareFunction = (a, b) => { const aValue = expr.evalSync(a); const bValue = expr.evalSync(b); if (aValue < bValue) return descending ? -1 : 1; if (aValue > bValue) return descending ? 1 : -1; return 0; }; return [...input].sort(compareFunction); }; exports.arraySort = arraySort; /** Returns a new array with the elements of the input array with duplicates removed. */ const arrayDistinct = (input) => { if (!Array.isArray(input)) return []; return [...new Set(input)]; }; exports.arrayDistinct = arrayDistinct; /** Create a new object based on an array of key-value pairs. */ const arrayToObject = (input, val) => { if (typeof input === 'string') return { [input]: val }; if (!Array.isArray(input)) return {}; return input.reduce((acc, kv) => { if (Array.isArray(kv) && kv.length === 2) { acc[kv[0]] = kv[1]; return acc; } else if (typeof kv === 'string') { acc[kv] = val; return acc; } return acc; }, {}); }; exports.arrayToObject = arrayToObject; /** Returns a new array with the elements of the input array transformed by the specified map function. */ const mapField = (input, field) => { if (!Array.isArray(input)) return []; return input.map(item => item[field]); }; exports.mapField = mapField; /** * Returns an array containing the results of applying the expression parameter to each value in the array parameter. * The expression must be a valid JEXL expression string, which is applied to each element of the array. * The relative context provided to the expression is an object with the properties value, index and array (the original array). */ const arrayMap = (input, expression) => { if (!Array.isArray(input)) return undefined; const expr = _1.default.compile(expression); return input.map((value, index, array) => { return expr.evalSync({ value, index, array }); }); }; exports.arrayMap = arrayMap; /** * Checks whether the provided array has any elements that match the specified expression. * The expression must be a valid JEXL expression string, and is applied to each element of the array. * The relative context provided to the expression is an object with the properties value and array (the original array). */ const arrayAny = (input, expression) => { if (!Array.isArray(input)) return false; const expr = _1.default.compile(expression); return input.some((value, index, array) => { return expr.evalSync({ value, index, array }); }); }; exports.arrayAny = arrayAny; /** * Checks whether the provided array has all elements that match the specified expression. * The expression must be a valid JEXL expression string, and is applied to each element of the array. * The relative context provided to the expression is an object with the properties value and array (the original array). */ const arrayEvery = (input, expression) => { if (!Array.isArray(input)) return false; const expr = _1.default.compile(expression); return input.every((value, index, array) => { return expr.evalSync({ value, index, array }); }); }; exports.arrayEvery = arrayEvery; /** * Returns a new array with the elements of the input array that match the specified expression. * The expression must be a valid JEXL expression string, and is applied to each element of the array. * The relative context provided to the expression is an object with the properties value and array (the original array). */ const arrayFilter = (input, expression) => { if (!Array.isArray(input)) return []; const expr = _1.default.compile(expression); return input.filter((value, index, array) => { return expr.evalSync({ value, index, array }); }); }; exports.arrayFilter = arrayFilter; /** * Finds the first element in an array that matches the specified expression. * The expression must be a valid JEXL expression string, and is applied to each element of the array. * The relative context provided to the expression is an object with the properties value and array (the original array). */ const arrayFind = (input, expression) => { if (!Array.isArray(input)) return undefined; const expr = _1.default.compile(expression); return input.find((value, index, array) => { return expr.evalSync({ value, index, array }); }); }; exports.arrayFind = arrayFind; /** * Returns an aggregated value derived from applying the function parameter successively to each value in array in combination with the result of the previous application of the function. * The expression must be a valid JEXL expression string, and behaves like an infix operator between each value within the array. * The relative context provided to the expression is an object with the properties accumulator, value, index and array (the original array). */ const arrayReduce = (input, expression, initialValue) => { if (!Array.isArray(input)) return undefined; const expr = _1.default.compile(expression); return input.reduce((accumulator, value, index, array) => { return expr.evalSync({ accumulator, value, index, array }); }, initialValue); }; exports.arrayReduce = arrayReduce; /** * Returns the keys of an object. */ const objectKeys = (input) => { if (typeof input === 'object' && input !== null) { return Object.keys(input); } return undefined; }; exports.objectKeys = objectKeys; /** * Returns the values of an object. */ const objectValues = (input) => { if (typeof input === 'object' && input !== null) { return Object.values(input); } return undefined; }; exports.objectValues = objectValues; /** * Returns an array of key-value pairs from the input object. */ const objectEntries = (input) => { if (typeof input === 'object' && input !== null) { return Object.entries(input); } return undefined; }; exports.objectEntries = objectEntries; /** * Returns a new object with the properties of the input objects merged together. */ const objectMerge = (...args) => { return args.reduce((acc, obj) => { if (!Array.isArray(obj) && typeof obj === 'object' && obj !== null) { return { ...acc, ...obj }; } if (Array.isArray(obj)) { for (const item of obj) { if (typeof item === 'object' && item !== null) { acc = { ...acc, ...item }; } } } return acc; }, {}); }; exports.objectMerge = objectMerge; /** * Returns the current date and time in the ISO 8601 format. */ const now = () => { return new Date().toISOString(); }; exports.now = now; /** * Returns the current date and time in milliseconds since the Unix epoch. */ const millis = () => { return Date.now(); }; exports.millis = millis; /** * Parses the number of milliseconds since the Unix epoch or parses a string (with or without specified format) and returns the date and time in the ISO 8601 format. */ const toDateTime = (input, format) => { if (typeof input === 'number') { return new Date(input).toISOString(); } if (typeof input === 'string') { if (format) { // Add UTC as timezone if not provided const _format = (format.includes('x') || format.includes('X')) ? format : `${format} X`; const _input = (format.includes('x') || format.includes('X')) ? input : `${input} Z`; return (0, date_fns_1.parse)(_input, _format, new Date()).toISOString(); } return new Date(input).toISOString(); } if (input === undefined) { return new Date().toISOString(); } return undefined; }; exports.toDateTime = toDateTime; /** * Converts a date and time to a provided format. * * @example * ```typescript * dateTimeFormat(datetime, format) * $dateTimeFormat(datetime, format) * datetime|dateTimeFormat(format) * ``` * * @param input The input date and time, either as a string or number. * @param format The format to convert the date and time to. * @returns The date and time in the specified format. */ const dateTimeFormat = (input, format) => { let dateTime; if (typeof input === 'string') { dateTime = new Date(input); } else if (typeof input === 'number') { dateTime = new Date(input); } else { return null; } // Convert to UTC const utcDateTime = new Date(dateTime.getTime() + dateTime.getTimezoneOffset() * 60000); // Format the date return (0, date_fns_1.format)(utcDateTime, format); }; exports.dateTimeFormat = dateTimeFormat; /** * Parses the date and time in the ISO 8601 format and returns the number of milliseconds since the Unix epoch. */ const dateTimeToMillis = (input) => { return new Date(input).getTime(); }; exports.dateTimeToMillis = dateTimeToMillis; /** * Adds a time range to a date and time in the ISO 8601 format. */ const dateTimeAdd = (input, unit, value) => { // if unit doesn't end with 's' add it const _unit = unit.toLowerCase().endsWith('s') ? unit.toLowerCase() : `${unit.toLowerCase()}s`; const returnDate = (0, date_fns_1.add)(new Date(input), { [_unit]: value }); return returnDate.toISOString(); // dateAdd(new Date(input), { [unit]: value }); }; exports.dateTimeAdd = dateTimeAdd; /** * Evaluate provided and return the result. * If only one argument is provided, it is expected that the first argument is a JEXL expression. * If two arguments are provided, the first argument is the context (must be an object) and the second argument is the JEXL expression. * The expression uses the default JEXL extended grammar and can't use any custom defined functions or transforms. */ const _eval = (input, expression) => { if (expression === undefined) { const _input = typeof input === 'string' ? input : JSON.stringify(input); return _1.default.evalSync(_input); } if (typeof input === 'object') { return _1.default.evalSync(expression, input); } return undefined; }; exports._eval = _eval; /** * Generate a new UUID (Universally Unique Identifier). */ const uuid = () => { return (0, uuid_1.v4)(); }; exports.uuid = uuid;