UNPKG

react-awesome-query-builder

Version:

User-friendly query builder for React. Please migrate to new @react-awesome-query-builder/* See https://github.com/ukrbublik/react-awesome-query-builder#migration-to-600

880 lines (827 loc) 27.9 kB
import { SpelExpressionEvaluator } from "spel2js"; import uuid from "../utils/uuid"; import {getFieldConfig, extendConfig, normalizeField} from "../utils/configUtils"; import {getWidgetForFieldOp} from "../utils/ruleUtils"; import {loadTree} from "./tree"; import {defaultConjunction, defaultGroupConjunction} from "../utils/defaultUtils"; import {logger} from "../utils/stuff"; import moment from "moment"; 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); 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 = convertCompiled(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 convertCompiled = (expr, meta, parentExpr = null) => { const type = expr.getType(); let children = expr.getChildren().map(child => convertCompiled(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; const 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 }; if (isSize || isLength) { 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") { 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 obj = children.filter(child => child !== lastChild ); return { ...lastChild, obj, }; } } // 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 => convertCompiled(child, meta, expr)); } } // convert list if (type == "list") { val = val.map(item => convertCompiled(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; // + "/" + defaultValue(opConfig.cardinality, 1); if (!operators[opk]) operators[opk] = []; operators[opk].push(opKey); }); } else if (opConfig.spelOp) { const opk = opConfig.spelOp; // + "/" + defaultValue(opConfig.cardinality, 1); 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 (let funcKey in config.funcs) { const funcConfig = config.funcs[funcKey]; let fk; if (typeof funcConfig.spelFunc == "string") { fk = funcConfig.spelFunc; } if (fk) { if (!funcs[fk]) funcs[fk] = []; funcs[fk].push(funcKey); } } return { operators, conjunctions, funcs, }; }; const convertPath = (parts, meta) => { 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; meta.errors.push(`Unexpected item in compound: ${JSON.stringify(c)}`); } }); return !isError ? res : undefined; }; const convertArg = (spel, conv, config, meta, parentSpel) => { if (spel == undefined) return undefined; const {fieldSeparator} = config.settings; const literalTypes = { number: "number", string: "text", boolean: "boolean", null: "null" // should not be }; const groupFieldParts = parentSpel?._groupField ? [parentSpel?._groupField] : []; if (spel.type == "compound") { // complex field const parts = convertPath(spel.children, meta); if (!parts) { return undefined; } const fullParts = [...groupFieldParts, ...parts]; const isVariable = spel.children?.[0]?.type == "variable"; return { valueSrc: "field", //valueType: todo isVariable, value: fullParts.join(fieldSeparator), }; } else if (spel.type == "variable" || spel.type == "property") { // normal field const fullParts = [...groupFieldParts, spel.val]; const isVariable = spel.type == "variable"; return { valueSrc: "field", //valueType: todo isVariable, value: fullParts.join(fieldSeparator), }; } else if (literalTypes[spel.type]) { let value = spel.val; let valueType = literalTypes[spel.type]; if (parentSpel?.isUnary) { value = -value; } return { valueSrc: "value", valueType, value, }; } 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 = "multiselect"; return { valueSrc: "value", valueType, value, }; } else if (spel.type == "!func") { const {obj, methodName, args, isVar} = spel; // todo: get from conv const funcToOpMap = { [".contains"]: "like", [".startsWith"]: "starts_with", [".endsWith"]: "ends_with", ["$contains"]: "select_any_in", [".equals"]: "multiselect_equals", //[".containsAll"]: "multiselect_contains", ["CollectionUtils.containsAny()"]: "multiselect_contains" }; const convertedArgs = args.map(v => convertArg(v, conv, config, meta, { ...spel, _groupField: parentSpel?._groupField })); //todo: make dynamic: use funcToOpMap and check obj type in basic config if (methodName == "contains" && obj && obj[0].type == "list") { const convertedObj = obj.map(v => convertArg(v, conv, config, meta, spel)); // {'yellow', 'green'}.?[true].contains(color) if (!( convertedArgs.length == 1 && convertedArgs[0].valueSrc == "field" )) { meta.errors.push(`Expected arg to method ${methodName} to be field but got: ${JSON.stringify(convertedArgs)}`); return undefined; } const field = convertedArgs[0].value; if (!( convertedObj.length == 1 && convertedObj[0].valueType == "multiselect" )) { meta.errors.push(`Expected object of method ${methodName} to be inline list but got: ${JSON.stringify(convertedObj)}`); return undefined; } const opKey = funcToOpMap["$"+methodName]; const list = convertedObj[0]; return buildRule(config, meta, field, opKey, [list]); } else if (obj && obj[0].type == "property" && funcToOpMap[obj[0].val + "." + methodName + "()"]) { // CollectionUtils.containsAny(multicolor, {'yellow', 'green'}) const opKey = funcToOpMap[obj[0].val + "." + methodName + "()"]; const field = convertedArgs[0].value; const args = convertedArgs.slice(1); return buildRule(config, meta, field, opKey, args); } else if (funcToOpMap["."+methodName]) { // user.login.startsWith('gg') const opKey = funcToOpMap["."+methodName]; const parts = convertPath(obj, meta); if (parts && convertedArgs.length == 1) { const fullParts = [...groupFieldParts, ...parts]; const field = fullParts.join(fieldSeparator); return buildRule(config, meta, field, opKey, convertedArgs); } } else if (methodName == "parse" && obj && obj[0].type == "!new" && obj[0].cls.at(-1) == "SimpleDateFormat") { // new java.text.SimpleDateFormat('yyyy-MM-dd').parse('2022-01-15') const args = obj[0].args.map(v => convertArg(v, conv, config, meta, { ...spel, _groupField: parentSpel?._groupField })); if (!( args.length == 1 && args[0].valueType == "text" )) { meta.errors.push(`Expected args of ${obj[0].cls.join(".")}.${methodName} to be 1 string but got: ${JSON.stringify(args)}`); return undefined; } if (!( convertedArgs.length == 1 && convertedArgs[0].valueType == "text" )) { meta.errors.push(`Expected args of ${obj[0].cls.join(".")} to be 1 string but got: ${JSON.stringify(convertedArgs)}`); return undefined; } const dateFormat = args[0].value; const dateString = convertedArgs[0].value; const valueType = dateFormat.includes(" ") ? "datetime" : "date"; const field = null; // todo const widget = valueType; const fieldConfig = getFieldConfig(config, field); const widgetConfig = config.widgets[widget || fieldConfig?.mainWidget]; const valueFormat = widgetConfig.valueFormat; const dateVal = moment(dateString, moment.ISO_8601); const value = dateVal.isValid() ? dateVal.format(valueFormat) : undefined; return { valueSrc: "value", valueType, value, }; } else if (methodName == "parse" && obj && obj[0].type == "!type" && obj[0].cls.at(-1) == "LocalTime") { // time == T(java.time.LocalTime).parse('02:03:00') if (!( convertedArgs.length == 1 && convertedArgs[0].valueType == "text" )) { meta.errors.push(`Expected args of ${obj[0].cls.join(".")} to be 1 string but got: ${JSON.stringify(convertedArgs)}`); return undefined; } const timeString = convertedArgs[0].value; const valueType = "time"; const field = null; // todo const widget = valueType; const fieldConfig = getFieldConfig(config, field); const widgetConfig = config.widgets[widget || fieldConfig?.mainWidget]; const valueFormat = widgetConfig.valueFormat; const dateVal = moment(timeString, "HH:mm:ss"); const value = dateVal.isValid() ? dateVal.format(valueFormat) : undefined; return { valueSrc: "value", valueType, value, }; } else { // todo: conv.funcs meta.errors.push(`Unsupported method ${methodName}`); } } else if (spel.type == "op-plus" && parentSpel?.type == "ternary") { return buildCaseValueConcat(spel, conv, config, meta); } else { meta.errors.push(`Can't convert arg of type ${spel.type}`); } return undefined; }; const buildRule = (config, meta, field, opKey, convertedArgs) => { if (convertedArgs.filter(v => v === undefined).length) { return undefined; } const fieldConfig = getFieldConfig(config, field); if (!fieldConfig) { meta.errors.push(`No config for field ${field}`); return undefined; } 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: field, 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, } }; 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 = { ...(groupFilter || {}), type: "rule_group", properties: { ...groupOpRule.properties, ...(groupFilter?.properties || {}), mode } }; if (!res.id) res.id = uuid(); return res; }; const compareArgs = (left, right, spel, conv, config, meta, parentSpel = null) => { if (left.type == right.type) { if (left.type == "!aggr") { const [leftSource, rightSource] = [left.source, right.source].map(v => convertArg(v, conv, config, meta, { ...spel, _groupField: parentSpel?._groupField })); //todo: check same filter return leftSource.value == rightSource.value; } else { const [leftVal, rightVal] = [left, right].map(v => convertArg(v, conv, config, meta, { ...spel, _groupField: parentSpel?._groupField })); return leftVal.value == rightVal.value; } } return false; }; const convertToTree = (spel, conv, config, meta, parentSpel = null) => { if(!spel) return undefined; let res; if (spel.type.indexOf("op-") == 0) { let op = spel.type.slice("op-".length); // unary const isUnary = (op == "minus" || op == "plus") && spel.children.length == 1; if (isUnary) { spel.isUnary = true; return convertToTree(spel.children[0], conv, config, meta, spel); } // between let isBetweenNormal = (op == "and" && spel.children.length == 2 && spel.children[0].type == "op-ge" && spel.children[1].type == "op-le"); let isBetweenRev = (op == "or" && spel.children.length == 2 && spel.children[0].type == "op-lt" && spel.children[1].type == "op-gt"); let isBetween = isBetweenNormal || isBetweenRev; if (isBetween) { const [left, from] = spel.children[0].children; const [right, to] = spel.children[1].children; const isNumbers = from.type == "number" && to.type == "number"; const isSameSource = compareArgs(left, right, spel, conv, config, meta, parentSpel); if (isNumbers && isSameSource) { const _fromValue = from.val; const _toValue = to.val; const oneSpel = { type: "op-between", children: [ left, from, to ] }; return convertToTree(oneSpel, conv, config, meta, parentSpel); } } // find op let opKeys = conv.operators[op]; let opKey; // todo: make dynamic, use basic config if (op == "eq" && spel.children[1].type == "null") { opKey = "is_null"; } else if (op == "ne" && spel.children[1].type == "null") { opKey = "is_not_null"; } else if (op == "le" && spel.children[1].type == "string" && spel.children[1].val == "") { opKey = "is_empty"; opKeys = ["is_empty"]; } else if (op == "gt" && spel.children[1].type == "string" && spel.children[1].val == "") { opKey = "is_not_empty"; opKeys = ["is_not_empty"]; } else if (op == "between") { opKey = "between"; opKeys = ["between"]; } // convert children const convertChildren = () => spel.children.map(child => convertToTree(child, conv, config, meta, { ...spel, _groupField: parentSpel?._groupField }) ); 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); opKey = opKeys[0]; 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)}`); } const groupField = fieldObj.groupFieldValue.value; // some/all/none const opArg = convertedArgs[0]; 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 = []; } res = buildRuleGroup(fieldObj, opKey, convertedArgs, config, meta); } else { // 2. not group if (fieldObj.valueSrc != "field") { meta.errors.push(`Expected field ${JSON.stringify(fieldObj)}`); } const field = fieldObj.value; if (opKeys.length > 1) { logger.warn(`[spel] Spel operator ${op} can be mapped to ${opKeys}`); //todo: it's naive const widgets = opKeys.map(op => ({op, widget: getWidgetForFieldOp(config, field, op)})); if (op == "eq" || op == "ne") { const ws = widgets.find(({ op, widget }) => (widget && widget != "field")); opKey = ws.op; } } res = buildRule(config, meta, field, opKey, convertedArgs); } } 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}`); } } } else if (spel.type == "!aggr") { const groupFieldValue = convertToTree(spel.source, conv, config, meta, { ...spel, _groupField: parentSpel?._groupField }); let groupFilter = convertToTree(spel.filter, conv, config, meta, { ...spel, _groupField: groupFieldValue?.value }); if (groupFilter?.type == "rule") { groupFilter = wrapInDefaultConj(groupFilter, config); } 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"); } } 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: {} }; } else { res = convertArg(spel, conv, config, meta, parentSpel); if (res && !res.type && !parentSpel) { // 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 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; }; 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; if (val?.type == "op-plus") { convVal = buildCaseValueConcat(val, conv, config, meta); } else { convVal = convertArg(val, conv, config, meta, spel); } const widgetDef = config.widgets["case_value"]; const importCaseValue = widgetDef?.spelImportValue; if (importCaseValue) { const [normVal, normErrors] = importCaseValue(convVal); normErrors.map(e => meta.errors.push(e)); if (normVal) { valProperties = { value: [normVal], valueSrc: ["value"], valueType: ["case_value"] }; } } else { meta.errors.push("No fucntion to import 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 } }; };