@react-query-builder-express/core
Version:
User-friendly query builder for React. Core
1,330 lines (1,237 loc) • 40 kB
JavaScript
import { SpelExpressionEvaluator } from "spel2js";
import uuid from "../utils/uuid";
import {getFieldConfig, getFuncConfig, extendConfig, normalizeField, iterateFuncs} from "../utils/configUtils";
import {getWidgetForFieldOp} from "../utils/ruleUtils";
import {loadTree} from "./tree";
import {defaultConjunction, defaultGroupConjunction} from "../utils/defaultUtils";
import {getOpCardinality, logger, isJsonCompatible} from "../utils/stuff";
import moment from "moment";
import {compareToSign} from "../export/spel";
// https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/expressions.html#expressions
// spel type => raqb type
const SpelPrimitiveTypes = {
number: "number",
string: "text",
boolean: "boolean",
null: "null" // should not be
};
// spel class => raqb type
const SpelPrimitiveClasses = {
String: "text",
};
const ListValueType = "multiselect";
const isFuncableProperty = (p) => ["length"].includes(p);
const isObject = (v) => (typeof v == "object" && v !== null && !Array.isArray(v));
export const loadFromSpel = (logicTree, config) => {
return _loadFromSpel(logicTree, config, true);
};
export const _loadFromSpel = (spelStr, config, returnErrors = true) => {
//meta is mutable
let meta = {
errors: []
};
const extendedConfig = extendConfig(config, undefined, false);
const conv = buildConv(extendedConfig);
let compiledExpression;
let convertedObj;
let jsTree = undefined;
try {
const compileRes = SpelExpressionEvaluator.compile(spelStr);
compiledExpression = compileRes._compiledExpression;
} catch (e) {
meta.errors.push(e);
}
if (compiledExpression) {
//logger.debug("compiledExpression:", compiledExpression);
convertedObj = postprocessCompiled(compiledExpression, meta);
logger.debug("convertedObj:", convertedObj, meta);
jsTree = convertToTree(convertedObj, conv, extendedConfig, meta);
if (jsTree && jsTree.type != "group" && jsTree.type != "switch_group") {
jsTree = wrapInDefaultConj(jsTree, extendedConfig, convertedObj["not"]);
}
logger.debug("jsTree:", jsTree);
}
const immTree = jsTree ? loadTree(jsTree) : undefined;
if (returnErrors) {
return [immTree, meta.errors];
} else {
if (meta.errors.length)
console.warn("Errors while importing from SpEL:", meta.errors);
return immTree;
}
};
const postprocessCompiled = (expr, meta, parentExpr = null) => {
const type = expr.getType();
let children = expr.getChildren().map(child => postprocessCompiled(child, meta, expr));
// flatize OR/AND
if (type == "op-or" || type == "op-and") {
children = children.reduce((acc, child) => {
const canFlatize = child.type == type && !child.not;
const flat = canFlatize ? child.children : [child];
return [...acc, ...flat];
}, []);
}
// unwrap NOT
if (type == "op-not") {
if (children.length != 1) {
meta.errors.push(`Operator NOT should have 1 child, but got ${children.length}}`);
}
return {
...children[0],
not: !(children[0].not || false)
};
}
if (type == "compound") {
// remove `.?[true]`
children = children.filter(child => {
const isListFix = child.type == "selection" && child.children.length == 1 && child.children[0].type == "boolean" && child.children[0].val == true;
return !isListFix;
});
// aggregation
// eg. `results.?[product == 'abc'].length`
const selection = children.find(child =>
child.type == "selection"
);
if (selection && selection.children.length != 1) {
meta.errors.push(`Selection should have 1 child, but got ${selection.children.length}`);
}
const filter = selection ? selection.children[0] : null;
let lastChild = children[children.length - 1];
const isSize = lastChild.type == "method" && lastChild.val.methodName == "size"
|| lastChild.type == "!func" && lastChild.methodName == "size";
const isLength = lastChild.type == "property" && lastChild.val == "length";
const sourceParts = children.filter(child =>
child !== selection && child !== lastChild
);
const source = {
type: "compound",
children: sourceParts
};
const isAggr = (isSize || isLength) && convertPath(sourceParts) != null;
if (isAggr) {
return {
type: "!aggr",
filter,
source
};
}
// remove `#this`, `#root`
children = children.filter(child => {
const isThis = child.type == "variable" && child.val == "this";
const isRoot = child.type == "variable" && child.val == "root";
return !(isThis || isRoot);
});
// indexer
children = children.map(child => {
if (child.type == "indexer" && child.children.length == 1) {
return {
type: "indexer",
val: child.children[0].val,
itype: child.children[0].type
};
} else {
return child;
}
});
// method
// if (lastChild.type == "method") {
// // seems like obsolete code!
// debugger
// const obj = children.filter(child =>
// child !== lastChild
// );
// return {
// type: "!func",
// obj,
// methodName: lastChild.val.methodName,
// args: lastChild.val.args
// };
// }
// !func
if (lastChild.type == "!func") {
const ret = {};
let curr = ret;
do {
Object.assign(curr, lastChild);
children = children.filter(child => child !== lastChild);
lastChild = children[children.length - 1];
if (lastChild?.type == "!func") {
curr.obj = {};
curr = curr.obj;
} else {
if (children.length > 1) {
curr.obj = {
type: "compound",
children
};
} else {
curr.obj = lastChild;
}
}
} while(lastChild?.type == "!func");
return ret;
}
}
// getRaw || getValue
let val;
try {
if (expr.getRaw) { // use my fork
val = expr.getRaw();
} else if (expr.getValue.length == 0) { // getValue not requires context arg -> can use
val = expr.getValue();
}
} catch(e) {
logger.error("[spel2js] Error in getValue()", e);
}
// ternary
if (type == "ternary") {
val = flatizeTernary(children);
}
// convert method/function args
if (typeof val === "object" && val !== null) {
if (val.methodName || val.functionName) {
val.args = val.args.map(child => postprocessCompiled(child, meta, expr));
}
}
// convert list
if (type == "list") {
val = val.map(item => postprocessCompiled(item, meta, expr));
// fix whole expression wrapped in `{}`
if (!parentExpr && val.length == 1) {
return val[0];
}
}
// convert constructor
if (type == "constructorref") {
const qid = children.find(child => child.type == "qualifiedidentifier");
const cls = qid?.val;
if (!cls) {
meta.errors.push(`Can't find qualifiedidentifier in constructorref children: ${JSON.stringify(children)}`);
return undefined;
}
const args = children.filter(child => child.type != "qualifiedidentifier");
return {
type: "!new",
cls,
args
};
}
// convert type
if (type == "typeref") {
const qid = children.find(child => child.type == "qualifiedidentifier");
const cls = qid?.val;
if (!cls) {
meta.errors.push(`Can't find qualifiedidentifier in typeref children: ${JSON.stringify(children)}`);
return undefined;
}
const _args = children.filter(child => child.type != "qualifiedidentifier");
return {
type: "!type",
cls
};
}
// convert function/method
if (type == "function" || type == "method") {
// `foo()` is method, `#foo()` is function
// let's use common property `methodName` and just add `isVar` for function
const {functionName, methodName, args} = val;
return {
type: "!func",
methodName: functionName || methodName,
isVar: type == "function",
args
};
}
return {
type,
children,
val,
};
};
const flatizeTernary = (children) => {
let flat = [];
function _processTernaryChildren(tern) {
let [cond, if_val, else_val] = tern;
flat.push([cond, if_val]);
if (else_val?.type == "ternary") {
_processTernaryChildren(else_val.children);
} else {
flat.push([undefined, else_val]);
}
}
_processTernaryChildren(children);
return flat;
};
const buildConv = (config) => {
let operators = {};
for (let opKey in config.operators) {
const opConfig = config.operators[opKey];
if (opConfig.spelOps) {
// examples: "==", "eq", ".contains", "matches" (can be used for starts_with, ends_with)
opConfig.spelOps.forEach(spelOp => {
const opk = spelOp; // + "/" + getOpCardinality(opConfig);
if (!operators[opk])
operators[opk] = [];
operators[opk].push(opKey);
});
} else if (opConfig.spelOp) {
const opk = opConfig.spelOp; // + "/" + getOpCardinality(opConfig);
if (!operators[opk])
operators[opk] = [];
operators[opk].push(opKey);
} else {
logger.log(`[spel] No spelOp for operator ${opKey}`);
}
}
let conjunctions = {};
for (let conjKey in config.conjunctions) {
const conjunctionDefinition = config.conjunctions[conjKey];
const ck = conjunctionDefinition.spelConj || conjKey.toLowerCase();
conjunctions[ck] = conjKey;
}
let funcs = {};
for (const [funcPath, funcConfig] of iterateFuncs(config)) {
let fks = [];
const {spelFunc} = funcConfig;
if (typeof spelFunc === "string") {
const optionalArgs = Object.keys(funcConfig.args || {})
.reverse()
.filter(argKey => !!funcConfig.args[argKey].isOptional || funcConfig.args[argKey].defaultValue != undefined);
const funcSignMain = spelFunc
.replace(/\${(\w+)}/g, (_, _k) => "?");
const funcSignsOptional = optionalArgs
.reduce((acc, argKey) => (
[
...acc,
[
argKey,
...(acc[acc.length-1] || []),
]
]
), [])
.map(optionalArgKeys => (
spelFunc
.replace(/(?:, )?\${(\w+)}/g, (found, a) => (
optionalArgKeys.includes(a) ? "" : found
))
.replace(/\${(\w+)}/g, (_, _k) => "?")
));
fks = [
funcSignMain,
...funcSignsOptional,
];
}
for (const fk of fks) {
if (!funcs[fk])
funcs[fk] = [];
funcs[fk].push(funcPath);
}
}
let valueFuncs = {};
for (let w in config.widgets) {
const widgetDef = config.widgets[w];
const {spelImportFuncs, type} = widgetDef;
if (spelImportFuncs) {
for (const fk of spelImportFuncs) {
if (typeof fk === "string") {
const fs = fk.replace(/\${(\w+)}/g, (_, k) => "?");
const argsOrder = [...fk.matchAll(/\${(\w+)}/g)].map(([_, k]) => k);
if (!valueFuncs[fs])
valueFuncs[fs] = [];
valueFuncs[fs].push({
w,
argsOrder
});
}
}
}
}
let opFuncs = {};
for (let op in config.operators) {
const opDef = config.operators[op];
const {spelOp} = opDef;
if (spelOp?.includes("${0}")) {
const fs = spelOp.replace(/\${(\w+)}/g, (_, k) => "?");
const argsOrder = [...spelOp.matchAll(/\${(\w+)}/g)].map(([_, k]) => k);
if (!opFuncs[fs])
opFuncs[fs] = [];
opFuncs[fs].push({
op,
argsOrder
});
}
}
// Special .compareTo()
const compareToSS = compareToSign.replace(/\${(\w+)}/g, (_, k) => "?");
opFuncs[compareToSS] = [{
op: "!compare",
argsOrder: ["0", "1"]
}];
return {
operators,
conjunctions,
funcs,
valueFuncs,
opFuncs,
};
};
const convertToTree = (spel, conv, config, meta, parentSpel = null) => {
if (!spel) return undefined;
spel._groupField = spel._groupField ?? parentSpel?._groupField;
let res, canParseAsArg = true;
if (spel.type.indexOf("op-") === 0 || spel.type === "matches") {
res = convertOp(spel, conv, config, meta, parentSpel);
} else if (spel.type == "!aggr") {
const groupFieldValue = convertToTree(spel.source, conv, config, meta, spel);
spel._groupField = groupFieldValue?.value;
let groupFilter = convertToTree(spel.filter, conv, config, meta, spel);
if (groupFilter?.type == "rule") {
groupFilter = wrapInDefaultConj(groupFilter, config, spel.filter.not);
}
res = {
groupFilter,
groupFieldValue
};
if (!parentSpel) {
// !aggr can't be in root, it should be compared with something
res = undefined;
meta.errors.push("Unexpected !aggr in root");
canParseAsArg = false;
}
} else if (spel.type == "ternary") {
const children1 = {};
spel.val.forEach(v => {
const [cond, val] = v;
const caseI = buildCase(cond, val, conv, config, meta, spel);
if (caseI) {
children1[caseI.id] = caseI;
}
});
res = {
type: "switch_group",
id: uuid(),
children1,
properties: {}
};
}
if (!res && canParseAsArg) {
res = convertArg(spel, conv, config, meta, parentSpel);
}
if (res && !res.type && !parentSpel) {
// res is not a rule, it's value at root
// try to parse whole `"1"` as ternary
const sw = buildSimpleSwitch(spel, conv, config, meta);
if (sw) {
res = sw;
} else {
res = undefined;
meta.errors.push(`Can't convert rule of type ${spel.type}, it looks like var/literal`);
}
}
return res;
};
const convertOp = (spel, conv, config, meta, parentSpel = null) => {
let res;
let op = spel.type.startsWith("op-") ? spel.type.slice("op-".length) : spel.type;
// unary
const isUnary = (op == "minus" || op == "plus") && spel.children.length == 1;
if (isUnary) {
let negative = spel.negative;
if (op == "minus") {
negative = !negative;
}
spel.children[0].negative = negative;
return convertToTree(spel.children[0], conv, config, meta, parentSpel);
}
// between
const isBetweenNormal = (op == "and" && spel.children.length == 2 && spel.children[0].type == "op-ge" && spel.children[1].type == "op-le");
const isBetweenRev = (op == "or" && spel.children.length == 2 && spel.children[0].type == "op-lt" && spel.children[1].type == "op-gt");
const isBetween = isBetweenNormal || isBetweenRev;
if (isBetween) {
const [left, from] = spel.children[0].children;
const [right, to] = spel.children[1].children;
const isSameSource = compareArgs(left, right, spel, conv, config, meta, parentSpel);
if (isSameSource) {
const _fromValue = from.val;
const _toValue = to.val;
const oneSpel = {
type: "op-between",
children: [
left,
from,
to
],
not: isBetweenRev,
};
oneSpel._groupField = parentSpel?._groupField;
return convertOp(oneSpel, conv, config, meta, parentSpel);
}
}
// find op
let opKeys = conv.operators[op];
if (op == "eq" && spel.children[1].type == "null") {
opKeys = ["is_null"];
} else if (op == "ne" && spel.children[1].type == "null") {
opKeys = ["is_not_null"];
} else if (op == "le" && spel.children[1].type == "string" && spel.children[1].val == "") {
opKeys = ["is_empty"];
} else if (op == "gt" && spel.children[1].type == "string" && spel.children[1].val == "") {
opKeys = ["is_not_empty"];
} else if (op == "between") {
opKeys = ["between"];
}
// convert children
const convertChildren = () => {
let newChildren = spel.children.map(child =>
convertToTree(child, conv, config, meta, spel)
);
if (newChildren.length >= 2 && newChildren?.[0]?.type == "!compare") {
newChildren = newChildren[0].children;
}
return newChildren;
};
if (op == "and" || op == "or") {
const children1 = {};
const vals = convertChildren();
vals.forEach(v => {
if (v) {
const id = uuid();
v.id = id;
if (v.type != undefined) {
children1[id] = v;
} else {
meta.errors.push(`Bad item in AND/OR: ${JSON.stringify(v)}`);
}
}
});
res = {
type: "group",
id: uuid(),
children1,
properties: {
conjunction: conv.conjunctions[op],
not: spel.not
}
};
} else if (opKeys) {
const vals = convertChildren();
const fieldObj = vals[0];
let convertedArgs = vals.slice(1);
const groupField = fieldObj?.groupFieldValue?.value;
const opArg = convertedArgs?.[0];
let opKey = opKeys[0];
if (opKeys.length > 1) {
const valueType = vals[0]?.valueType || vals[1]?.valueType;
//todo: it's naive, use valueType
const field = fieldObj?.value;
const widgets = opKeys.map(op => ({op, widget: getWidgetForFieldOp(config, field, op)}));
logger.warn(`[spel] Spel operator ${op} can be mapped to ${opKeys}.`,
"widgets:", widgets, "vals:", vals, "valueType=", valueType);
if (op == "eq" || op == "ne") {
const ws = widgets.find(({ op, widget }) => (widget && widget != "field"));
if (ws) {
opKey = ws.op;
}
}
}
// some/all/none
if (fieldObj?.groupFieldValue) {
if (opArg && opArg.groupFieldValue && opArg.groupFieldValue.valueSrc == "field" && opArg.groupFieldValue.value == groupField) {
// group.?[...].size() == group.size()
opKey = "all";
convertedArgs = [];
} else if (opKey == "equal" && opArg.valueSrc == "value" && opArg.valueType == "number" && opArg.value == 0) {
opKey = "none";
convertedArgs = [];
} else if (opKey == "greater" && opArg.valueSrc == "value" && opArg.valueType == "number" && opArg.value == 0) {
opKey = "some";
convertedArgs = [];
}
}
let opConfig = config.operators[opKey];
const reversedOpConfig = config.operators[opConfig?.reversedOp];
const opNeedsReverse = spel.not && ["between"].includes(opKey);
const opCanReverse = !!reversedOpConfig;
const canRev = opCanReverse && (!!config.settings.reverseOperatorsForNot || opNeedsReverse);
const needRev = spel.not && canRev || opNeedsReverse;
if (needRev) {
opKey = opConfig.reversedOp;
opConfig = config.operators[opKey];
spel.not = !spel.not;
}
const needWrapWithNot = !!spel.not;
spel.not = false; // handled with needWrapWithNot
if (!fieldObj) {
// LHS can't be parsed
} else if (fieldObj.groupFieldValue) {
// 1. group
if (fieldObj.groupFieldValue.valueSrc != "field") {
meta.errors.push(`Expected group field ${JSON.stringify(fieldObj)}`);
}
res = buildRuleGroup(fieldObj, opKey, convertedArgs, config, meta);
} else {
// 2. not group
if (fieldObj.valueSrc != "field" && fieldObj.valueSrc != "func") {
meta.errors.push(`Expected field/func at LHS, but got ${JSON.stringify(fieldObj)}`);
}
const field = fieldObj.value;
res = buildRule(config, meta, field, opKey, convertedArgs, spel);
}
if (needWrapWithNot) {
if (res.type !== "group") {
res = wrapInDefaultConj(res, config, true);
} else {
res.properties.not = !res.properties.not;
}
}
} else {
if (!parentSpel) {
// try to parse whole `"str" + prop + #var` as ternary
res = buildSimpleSwitch(spel, conv, config, meta);
}
// if (!res) {
// meta.errors.push(`Can't convert op ${op}`);
// }
}
return res;
};
const convertPath = (parts, meta = {}, expectingField = false) => {
let isError = false;
const res = parts.map(c => {
if (c.type == "variable" || c.type == "property" || c.type == "indexer" && c.itype == "string") {
return c.val;
} else {
isError = true;
expectingField && meta?.errors?.push?.(`Unexpected item in field path compound: ${JSON.stringify(c)}`);
}
});
return !isError ? res : undefined;
};
const convertArg = (spel, conv, config, meta, parentSpel = null) => {
if (spel == undefined)
return undefined;
const {fieldSeparator} = config.settings;
spel._groupField = spel._groupField ?? parentSpel?._groupField;
if (spel.type == "variable" || spel.type == "property") {
// normal field
const field = normalizeField(config, spel.val, spel._groupField);
const fieldConfig = getFieldConfig(config, field);
const isVariable = spel.type == "variable";
return {
valueSrc: "field",
valueType: fieldConfig?.type,
isVariable,
value: field,
};
} else if (spel.type == "compound") {
// complex field
const parts = convertPath(spel.children, meta);
if (parts) {
const field = normalizeField(config, parts.join(fieldSeparator), spel._groupField);
const fieldConfig = getFieldConfig(config, field);
const isVariable = spel.children?.[0]?.type == "variable";
return {
valueSrc: "field",
valueType: fieldConfig?.type,
isVariable,
value: field,
};
} else {
// it's not complex field
}
} else if (SpelPrimitiveTypes[spel.type]) {
let value = spel.val;
const valueType = SpelPrimitiveTypes[spel.type];
if (spel.negative) {
value = -value;
}
return {
valueSrc: "value",
valueType,
value,
};
} else if (spel.type == "!new" && SpelPrimitiveClasses[spel.cls.at(-1)]) {
const args = spel.args.map(v => convertArg(v, conv, config, meta, spel));
const value = args?.[0];
const valueType = SpelPrimitiveClasses[spel.cls.at(-1)];
return {
...value,
valueType,
};
} else if (spel.type == "list") {
const values = spel.val.map(v => convertArg(v, conv, config, meta, spel));
const _itemType = values.length ? values[0]?.valueType : null;
const value = values.map(v => v?.value);
const valueType = ListValueType;
return {
valueSrc: "value",
valueType,
value,
};
} else if (spel.type === "op-plus" && parentSpel?.type === "ternary" && config.settings.caseValueField?.type === "case_value") {
/**
* @deprecated
*/
return buildCaseValueConcat(spel, conv, config, meta);
}
let maybe = convertFunc(spel, conv, config, meta, parentSpel);
if (maybe !== undefined) {
return maybe;
}
meta.errors.push(`Can't convert arg of type ${spel.type}`);
return undefined;
};
const buildFuncSignatures = (spel) => {
// branches
const brns = [
{s: "", params: [], objs: []}
];
_buildFuncSignatures(spel, brns);
return brns.map(({s, params}) => ({s, params})).reverse().filter(({s}) => s !== "" && s !== "?");
};
// a.toLower().toUpper()
// ->
// ?.toLower().toUpper()
// ?.toUpper()
const _buildFuncSignatures = (spel, brns) => {
let params = [], s = "";
const { type, methodName, val, obj, args, isVar, cls, children } = spel;
const lastChild = children?.[children.length-1];
let currBrn = brns[brns.length-1];
if (type === "!func") {
// T(DateTimeFormat).forPattern(?).parseDateTime(?) -- ok
// T(LocalDateTime).parse(?, T(DateTimeFormatter).ofPattern(?)) -- will not work
let o = obj;
while (o) {
const [s1, params1] = _buildFuncSignatures({...o, obj: null}, [{}]);
if (s1 !== "?") {
// start new branch
const newBrn = {
s: currBrn.s,
params: [...currBrn.params],
objs: [...currBrn.objs]
};
// finish old branch
currBrn.objs.unshift("?");
currBrn.params.unshift(o);
// switch
brns.push(newBrn);
currBrn = brns[brns.length-1];
}
// step
currBrn.objs.unshift(s1);
currBrn.params.unshift(...params1);
o = o.type === "!func" ? o.obj : null;
}
for (const brn of brns) {
params = [
...(brn?.params || []),
...(args || []),
];
s = "";
if (brn?.objs?.length)
s += brn.objs.join(".") + ".";
s += (isVar ? "#" : "") + methodName;
s += "(" + (args || []).map(_ => "?").join(", ") + ")";
brn.s = s;
brn.params = params;
}
} else if (type === "!new") {
// new java.text.SimpleDateFormat('HH:mm:ss').parse('...')
params = args || [];
s = `new ${cls.join(".")}(${params.map(_ => "?").join(", ")})`;
} else if (type === "!type") {
// T(java.time.LocalTime).parse('...')
s = `T(${cls.join(".")})`;
} else if (type === "compound" && lastChild.type === "property" && isFuncableProperty(lastChild.val)) {
// {1,2}.length -- ok
// 'Hello World'.bytes.length -- will not work
s = children.map((c) => {
if (c === lastChild)
return c.val;
const [s1, params1] = _buildFuncSignatures({...c, obj: null}, [{}]);
params.push(...params1);
return s1;
}).join(".");
} else {
params = [spel];
s = "?";
}
if (currBrn) {
currBrn.s = s;
currBrn.params = params;
}
return [s, params];
};
const convertFunc = (spel, conv, config, meta, parentSpel = null) => {
// Build signatures
const convertFuncArg = v => convertToTree(v, conv, config, meta, spel);
const fsigns = buildFuncSignatures(spel);
const firstSign = fsigns?.[0]?.s;
if (fsigns.length)
logger.debug("Signatures for ", spel, ":", firstSign, fsigns);
// 1. Try to parse as value
let maybeValue = convertFuncToValue(spel, conv, config, meta, parentSpel, fsigns, convertFuncArg);
if (maybeValue !== undefined)
return maybeValue;
// 2. Try to parse as op
let maybeOp = convertFuncToOp(spel, conv, config, meta, parentSpel, fsigns, convertFuncArg);
if (maybeOp !== undefined)
return maybeOp;
// 3. Try to parse as func
let funcKey, funcConfig, argsObj;
// try func signature matching
for (const {s, params} of fsigns) {
const funcKeys = conv.funcs[s];
if (funcKeys) {
// todo: here we can check arg types, if we have function overloading
funcKey = funcKeys[0];
funcConfig = getFuncConfig(config, funcKey);
const {spelFunc} = funcConfig;
const argsArr = params.map(convertFuncArg);
const argsOrder = [...spelFunc.matchAll(/\${(\w+)}/g)].map(([_, k]) => k);
argsObj = Object.fromEntries(
argsOrder.map((argKey, i) => [argKey, argsArr[i]])
);
break;
}
}
// try `spelImport`
if (!funcKey) {
for (const [f, fc] of iterateFuncs(config)) {
if (fc.spelImport) {
let parsed;
try {
parsed = fc.spelImport(spel);
} catch(_e) {
// can't be parsed
}
if (parsed) {
funcKey = f;
funcConfig = getFuncConfig(config, funcKey);
argsObj = {};
for (let argKey in parsed) {
argsObj[argKey] = convertFuncArg(parsed[argKey]);
}
}
}
}
}
// convert
if (funcKey) {
const funcArgs = {};
for (let argKey in funcConfig.args) {
const argConfig = funcConfig.args[argKey];
let argVal = argsObj[argKey];
if (argVal === undefined) {
argVal = argConfig?.defaultValue;
if (argVal === undefined) {
if (argConfig?.isOptional) {
//ignore
} else {
meta.errors.push(`No value for arg ${argKey} of func ${funcKey}`);
return undefined;
}
} else {
argVal = {
value: argVal,
valueSrc: argVal?.func ? "func" : "value",
valueType: argConfig.type,
};
}
}
if (argVal)
funcArgs[argKey] = argVal;
}
return {
valueSrc: "func",
value: {
func: funcKey,
args: funcArgs
},
valueType: funcConfig.returnType,
};
}
const {methodName} = spel;
if (methodName)
meta.errors.push(`Signature ${firstSign} - failed to convert`);
return undefined;
};
const convertFuncToValue = (spel, conv, config, meta, parentSpel, fsigns, convertFuncArg) => {
let errs, foundSign, foundWidget;
const candidates = [];
for (let w in config.widgets) {
const widgetDef = config.widgets[w];
const {spelImportFuncs} = widgetDef;
if (spelImportFuncs) {
for (let i = 0 ; i < spelImportFuncs.length ; i++) {
const fj = spelImportFuncs[i];
if (isObject(fj)) {
const bag = {};
if (isJsonCompatible(fj, spel, bag)) {
for (const k in bag) {
bag[k] = convertFuncArg(bag[k]);
}
candidates.push({
s: `widgets.${w}.spelImportFuncs[${i}]`,
w,
argsObj: bag,
});
}
}
}
}
}
for (const {s, params} of fsigns) {
const found = conv.valueFuncs[s] || [];
for (const {w, argsOrder} of found) {
const argsArr = params.map(convertFuncArg);
const argsObj = Object.fromEntries(
argsOrder.map((argKey, i) => [argKey, argsArr[i]])
);
candidates.push({
s,
w,
argsObj,
});
}
}
for (const {s, w, argsObj} of candidates) {
const widgetDef = config.widgets[w];
const {spelImportValue, type} = widgetDef;
foundWidget = w;
foundSign = s;
errs = [];
for (const k in argsObj) {
if (!["value"].includes(argsObj[k].valueSrc)) {
errs.push(`${k} has unsupported value src ${argsObj[k].valueSrc}`);
}
}
let value = argsObj.v.value;
if (spelImportValue && !errs.length) {
[value, errs] = spelImportValue.call(config.ctx, argsObj.v, widgetDef, argsObj);
if (errs && !Array.isArray(errs))
errs = [errs];
}
if (!errs.length) {
return {
valueSrc: "value",
valueType: type,
value,
};
}
}
if (foundWidget && errs.length) {
meta.errors.push(`Signature ${foundSign} - looks like convertable to ${foundWidget}, but: ${errs.join("; ")}`);
}
return undefined;
};
const convertFuncToOp = (spel, conv, config, meta, parentSpel, fsigns, convertFuncArg) => {
let errs, opKey, foundSign;
for (const {s, params} of fsigns) {
const found = conv.opFuncs[s] || [];
for (const {op, argsOrder} of found) {
const argsArr = params.map(convertFuncArg);
opKey = op;
if (op === "!compare") {
if (
parentSpel.type.startsWith("op-")
&& parentSpel.children.length == 2
&& parentSpel.children[1].type == "number"
&& parentSpel.children[1].val === 0
) {
return {
type: "!compare",
children: argsArr,
};
} else {
errs.push("Result of compareTo() should be compared to 0");
}
}
foundSign = s;
errs = [];
const opDef = config.operators[opKey];
const {spelOp, valueTypes} = opDef;
const argsObj = Object.fromEntries(
argsOrder.map((argKey, i) => [argKey, argsArr[i]])
);
const field = argsObj["0"];
const convertedArgs = Object.keys(argsObj).filter(k => parseInt(k) > 0).map(k => argsObj[k]);
const valueType = argsArr.filter(a => !!a).find(({valueSrc}) => valueSrc === "value")?.valueType;
if (valueTypes && valueType && !valueTypes.includes(valueType)) {
errs.push(`Op supports types ${valueTypes}, but got ${valueType}`);
}
if (!errs.length) {
return buildRule(config, meta, field, opKey, convertedArgs, spel);
}
}
}
if (opKey && errs.length) {
meta.errors.push(`Signature ${foundSign} - looks like convertable to ${opKey}, but: ${errs.join("; ")}`);
}
return undefined;
};
const buildRule = (config, meta, field, opKey, convertedArgs, spel) => {
if (convertedArgs.filter(v => v === undefined).length) {
return undefined;
}
let fieldSrc = field?.func ? "func" : "field";
if (isObject(field) && field.valueSrc) {
// if comed from convertFuncToOp()
fieldSrc = field.valueSrc;
field = field.value;
}
const fieldConfig = getFieldConfig(config, field);
if (!fieldConfig) {
meta.errors.push(`No config for field ${field}`);
return undefined;
}
const parentFieldConfig = getFieldConfig(config, spel?._groupField);
const isRuleGroup = fieldConfig.type == "!group";
const isGroupArray = isRuleGroup && fieldConfig.mode == "array";
const isInRuleGroup = parentFieldConfig?.type == "!group";
let opConfig = config.operators[opKey];
const reversedOpConfig = config.operators[opConfig?.reversedOp];
const opNeedsReverse = spel?.not && ["between"].includes(opKey);
const opCanReverse = !!reversedOpConfig;
const canRev = opCanReverse && (
!!config.settings.reverseOperatorsForNot
|| opNeedsReverse
|| !isRuleGroup && isInRuleGroup // 2+ rules in rule-group should be flat. see inits.with_not_and_in_some in test
);
const needRev = spel?.not && canRev || opNeedsReverse;
if (needRev) {
// todo: should be already handled at convertOp ? or there are special cases to handle here, like rule-group ?
opKey = opConfig.reversedOp;
opConfig = config.operators[opKey];
spel.not = !spel.not;
}
const needWrapWithNot = !!spel?.not;
const widget = getWidgetForFieldOp(config, field, opKey);
const widgetConfig = config.widgets[widget || fieldConfig.mainWidget];
const asyncListValuesArr = convertedArgs.map(v => v.asyncListValues).filter(v => v != undefined);
const asyncListValues = asyncListValuesArr.length ? asyncListValuesArr[0] : undefined;
let res = {
type: "rule",
id: uuid(),
properties: {
field,
fieldSrc,
operator: opKey,
value: convertedArgs.map(v => v.value),
valueSrc: convertedArgs.map(v => v.valueSrc),
valueType: convertedArgs.map(v => {
if (v.valueSrc == "value") {
return widgetConfig?.type || fieldConfig?.type || v.valueType;
}
return v.valueType;
}),
...(asyncListValues ? {asyncListValues} : {}),
}
};
if (needWrapWithNot) {
res = wrapInDefaultConj(res, config, spel.not);
// spel.not = !spel.not; // why I added this line?
}
return res;
};
const buildRuleGroup = ({groupFilter, groupFieldValue}, opKey, convertedArgs, config, meta) => {
if (groupFieldValue.valueSrc != "field")
throw `Bad groupFieldValue: ${JSON.stringify(groupFieldValue)}`;
const groupField = groupFieldValue.value;
let groupOpRule = buildRule(config, meta, groupField, opKey, convertedArgs);
if (!groupOpRule)
return undefined;
const fieldConfig = getFieldConfig(config, groupField);
const mode = fieldConfig?.mode;
let res;
if (groupFilter?.type === "group") {
res = {
...(groupFilter || {}),
type: "rule_group",
properties: {
...groupOpRule.properties,
...(groupFilter?.properties || {}),
mode
}
};
} else if (groupFilter) {
// rule_group in rule_group
res = {
...(groupOpRule || {}),
type: "rule_group",
children1: [ groupFilter ],
properties: {
...groupOpRule.properties,
mode
}
};
} else {
res = {
...(groupOpRule || {}),
type: "rule_group",
properties: {
...groupOpRule.properties,
mode
}
};
}
if (!res.id)
res.id = uuid();
return res;
};
const compareArgs = (left, right, spel, conv, config, meta) => {
if (left.type == right.type) {
if (left.type == "!aggr") {
const [leftSource, rightSource] = [left.source, right.source].map(v => convertArg(v, conv, config, meta, spel));
//todo: check same filter
return leftSource.value == rightSource.value;
} else {
const [leftVal, rightVal] = [left, right].map(v => convertArg(v, conv, config, meta, spel));
return leftVal.value == rightVal.value;
}
}
return false;
};
const buildSimpleSwitch = (val, conv, config, meta) => {
let children1 = {};
const cond = null;
const caseI = buildCase(cond, val, conv, config, meta);
if (caseI) {
children1[caseI.id] = caseI;
}
let res = {
type: "switch_group",
id: uuid(),
children1,
properties: {}
};
return res;
};
const buildCase = (cond, val, conv, config, meta, spel = null) => {
const valProperties = buildCaseValProperties(config, meta, conv, val, spel);
let caseI;
if (cond) {
caseI = convertToTree(cond, conv, config, meta, spel);
if (caseI && caseI.type) {
if (caseI.type != "group") {
caseI = wrapInDefaultConj(caseI, config);
}
caseI.type = "case_group";
} else {
meta.errors.push(`Unexpected case: ${JSON.stringify(caseI)}`);
caseI = undefined;
}
} else {
caseI = {
id: uuid(),
type: "case_group",
properties: {}
};
}
if (caseI) {
caseI.properties = {
...caseI.properties,
...valProperties
};
}
return caseI;
};
/**
* @deprecated
*/
const buildCaseValueConcat = (spel, conv, config, meta) => {
let flat = [];
function _processConcatChildren(children) {
children.map(child => {
if (child.type === "op-plus") {
_processConcatChildren(child.children);
} else {
const convertedChild = convertArg(child, conv, config, meta, spel);
if (convertedChild) {
flat.push(convertedChild);
} else {
meta.errors.push(`Can't convert ${child.type} in concatenation`);
}
}
});
}
_processConcatChildren(spel.children);
return {
valueSrc: "value",
valueType: "case_value",
value: flat,
};
};
const buildCaseValProperties = (config, meta, conv, val, spel = null) => {
let valProperties = {};
let convVal;
let widget;
let widgetConfig;
const caseValueFieldConfig = getFieldConfig(config, "!case_value");
if (val?.type === "op-plus" && config.settings.caseValueField?.type === "case_value") {
/**
* @deprecated
*/
widget = "case_value";
convVal = buildCaseValueConcat(val, conv, config, meta);
} else {
widget = caseValueFieldConfig?.mainWidget;
widgetConfig = config.widgets[widget];
convVal = convertArg(val, conv, config, meta, spel);
if (convVal && convVal.valueSrc === "value") {
convVal.valueType = widgetConfig?.type || caseValueFieldConfig?.type || convVal.valueType;
}
}
const widgetDef = config.widgets[widget];
if (widget === "case_value") {
/**
* @deprecated
*/
const importCaseValue = widgetDef?.spelImportValue;
if (importCaseValue) {
const [normVal, normErrors] = importCaseValue.call(config.ctx, convVal);
normErrors.map(e => meta.errors.push(e));
if (normVal != undefined) {
valProperties = {
value: [normVal],
valueSrc: ["value"],
valueType: [widgetDef?.type ?? "case_value"],
field: "!case_value",
};
}
}
} else if (convVal != undefined && convVal?.value != undefined) {
valProperties = {
value: [convVal.value],
valueSrc: [convVal.valueSrc],
valueType: [convVal.valueType],
field: "!case_value",
};
}
return valProperties;
};
// const wrapInDefaultConjRuleGroup = (rule, parentField, parentFieldConfig, config, conj) => {
// if (!rule) return undefined;
// return {
// type: "rule_group",
// id: uuid(),
// children1: { [rule.id]: rule },
// properties: {
// conjunction: conj || defaultGroupConjunction(config, parentFieldConfig),
// not: false,
// field: parentField,
// }
// };
// };
const wrapInDefaultConj = (rule, config, not = false) => {
return {
type: "group",
id: uuid(),
children1: { [rule.id]: rule },
properties: {
conjunction: defaultConjunction(config),
not: not || false
}
};
};