jexl-extended
Version:
Extended grammar for Javascript Expression Language (JEXL)
770 lines (769 loc) • 27.3 kB
JavaScript
"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;