UNPKG

@react-query-builder-express/core

Version:
621 lines (567 loc) 19.8 kB
import { getFieldConfig, getOperatorConfig, getFieldWidgetConfig, getFuncConfig, extendConfig, getFieldParts, } from "../utils/configUtils"; import { getWidgetForFieldOp, formatFieldName, getFieldPartsConfigs, completeValue } from "../utils/ruleUtils"; import pick from "lodash/pick"; import { getOpCardinality, logger, widgetDefKeysToOmit, opDefKeysToOmit, omit } from "../utils/stuff"; import { defaultConjunction } from "../utils/defaultUtils"; import { List, Map } from "immutable"; import { spelEscape } from "../utils/export"; // https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/expressions.html#expressions export const compareToSign = "${0}.compareTo(${1})"; const TypesWithCompareTo = { datetime: true, time: true, date: true, }; export const spelFormat = (tree, config) => { return _spelFormat(tree, config, false); }; export const _spelFormat = (tree, config, returnErrors = true) => { //meta is mutable let meta = { errors: [], }; const extendedConfig = extendConfig(config, undefined, false); const res = formatItem(tree, extendedConfig, meta, null); if (returnErrors) { return [res, meta.errors]; } else { if (meta.errors.length) console.warn("Errors while exporting to SpEL:", meta.errors); return res; } }; const formatItem = (item, config, meta, parentField = null) => { if (!item) return undefined; const type = item.get("type"); if (type === "group" || type === "rule_group") { return formatGroup(item, config, meta, parentField); } else if (type === "rule") { return formatRule(item, config, meta, parentField); } else if (type == "switch_group") { return formatSwitch(item, config, meta, parentField); } else if (type == "case_group") { return formatCase(item, config, meta, parentField); } return undefined; }; const formatCase = (item, config, meta, parentField = null) => { const type = item.get("type"); if (type != "case_group") { meta.errors.push(`Unexpected child of type ${type} inside switch`); return undefined; } const properties = item.get("properties") || new Map(); const [formattedValue, valueSrc, valueType] = formatItemValue( config, properties, meta, null, parentField, "!case_value" ); const cond = formatGroup(item, config, meta, parentField); return [cond, formattedValue]; }; const formatSwitch = (item, config, meta, parentField = null) => { const properties = item.get("properties") || new Map(); const children = item.get("children1"); if (!children) return undefined; const cases = children .map((currentChild) => formatCase(currentChild, config, meta, null)) .filter((currentChild) => typeof currentChild !== "undefined") .valueSeq() .toArray(); if (!cases.length) return undefined; if (cases.length == 1 && !cases[0][0]) { // only 1 case without condition return cases[0][1]; } let filteredCases = []; for (let i = 0; i < cases.length; i++) { if (i != cases.length - 1 && !cases[i][0]) { meta.errors.push(`No condition for case ${i}`); } else { filteredCases.push(cases[i]); if (i == cases.length - 1 && cases[i][0]) { // no default - add null as default filteredCases.push([undefined, null]); } } } let left = "", right = ""; for (let i = 0; i < filteredCases.length; i++) { let [cond, value] = filteredCases[i]; if (value == undefined) value = "null"; if (cond == undefined) cond = "true"; if (i != filteredCases.length - 1) { left += `(${cond} ? ${value} : `; right += ")"; } else { left += `${value}`; } } return left + right; }; const formatGroup = (item, config, meta, parentField = null) => { const type = item.get("type"); const properties = item.get("properties") || new Map(); const mode = properties.get("mode"); const children = item.get("children1") || new List(); const field = properties.get("field"); let conjunction = properties.get("conjunction"); if (!conjunction) conjunction = defaultConjunction(config); const conjunctionDefinition = config.conjunctions[conjunction]; const not = properties.get("not"); const isRuleGroup = type === "rule_group"; const isRuleGroupArray = isRuleGroup && mode != "struct"; const groupField = isRuleGroupArray ? field : parentField; const groupFieldDef = getFieldConfig(config, groupField) || {}; const isSpelArray = groupFieldDef.isSpelArray; const { fieldSeparator } = config.settings; // check op for reverse let groupOperator = properties.get("operator"); if (!groupOperator && (!mode || mode == "some")) { groupOperator = "some"; } const realGroupOperator = checkOp(config, groupOperator, field); const isGroupOpRev = realGroupOperator != groupOperator; const realGroupOperatorDefinition = (groupOperator && getOperatorConfig(config, realGroupOperator, field)) || null; const isGroup0 = isRuleGroup && (!realGroupOperator || realGroupOperatorDefinition.cardinality == 0); // build value for aggregation op const [formattedValue, valueSrc, valueType] = formatItemValue( config, properties, meta, realGroupOperator, parentField, null ); // build filter in aggregation const list = children .map((currentChild) => formatItem(currentChild, config, meta, groupField)) .filter((currentChild) => typeof currentChild !== "undefined"); if (isRuleGroupArray && !isGroup0) { // "count" rule can have no "having" children, but should have number value if (formattedValue == undefined) return undefined; } else { if (!list.size) return undefined; } const omitBrackets = isRuleGroup; const filter = list.size ? conjunctionDefinition.spelFormatConj.call(config.ctx, list, conjunction, not, omitBrackets) : null; // build result let ret; if (isRuleGroupArray) { const formattedField = formatField(meta, config, field, parentField); const sep = fieldSeparator || "."; const getSize = sep + (isSpelArray ? "length" : "size()"); const fullSize = `${formattedField}${getSize}`; // https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/expressions.html#expressions-collection-selection const filteredSize = filter ? `${formattedField}.?[${filter}]${getSize}` : fullSize; const groupValue = isGroup0 ? fullSize : formattedValue; // format expression ret = formatExpression( meta, config, properties, filteredSize, groupValue, realGroupOperator, valueSrc, valueType, isGroupOpRev ); } else { ret = filter; } return ret; }; const buildFnToFormatOp = (operator, operatorDefinition, valueType) => { const spelOp = operatorDefinition.spelOp; if (!spelOp) return undefined; const isSign = spelOp.includes("${0}"); const isCompareTo = TypesWithCompareTo[valueType]; let sop = spelOp; let fn; const cardinality = getOpCardinality(operatorDefinition); if (isCompareTo) { // date1.compareTo(date2) >= 0 // instead of // date1 >= date2 fn = (field, op, values, valueSrc, valueType, opDef, operatorOptions, fieldDef) => { const compareRes = compareToSign.replace(/\${(\w+)}/g, (_, k) => k == 0 ? field : cardinality > 1 ? values[k - 1] : values ); return `${compareRes} ${sop} 0`; }; } else if (isSign) { fn = (field, op, values, valueSrc, valueType, opDef, operatorOptions, fieldDef) => { return spelOp.replace(/\${(\w+)}/g, (_, k) => (k == 0 ? field : cardinality > 1 ? values[k - 1] : values)); }; } else if (cardinality == 0) { // should not be fn = (field, op, values, valueSrc, valueType, opDef, operatorOptions, fieldDef) => { return `${field} ${sop}`; }; } else if (cardinality == 1) { fn = (field, op, values, valueSrc, valueType, opDef, operatorOptions, fieldDef) => { return `${field} ${sop} ${values}`; }; } return fn; }; const formatExpression = ( meta, config, properties, formattedField, formattedValue, operator, valueSrc, valueType, isRev = false ) => { const field = properties.get("field"); const opDef = getOperatorConfig(config, operator, field) || {}; const fieldDef = getFieldConfig(config, field) || {}; const operatorOptions = properties.get("operatorOptions"); //find fn to format expr const fn = opDef.spelFormatOp || buildFnToFormatOp(operator, opDef, valueType); if (!fn) { meta.errors.push(`Operator ${operator} is not supported`); return undefined; } //format expr const args = [ formattedField, operator, formattedValue, valueSrc, valueType, omit(opDef, opDefKeysToOmit), operatorOptions, fieldDef, ]; let ret; ret = fn.call(config.ctx, ...args); //rev if (isRev) { ret = config.settings.spelFormatReverse.call(config.ctx, ret); } if (ret === undefined) { meta.errors.push(`Operator ${operator} is not supported for value source ${valueSrc}`); } return ret; }; const checkOp = (config, operator, field) => { if (!operator) return undefined; let opDef = getOperatorConfig(config, operator, field) || {}; let reversedOp = opDef.reversedOp; let revOpDef = getOperatorConfig(config, reversedOp, field) || {}; let isRev = false; const canFormatOp = opDef.spelOp || opDef.spelFormatOp; const canFormatRevOp = revOpDef.spelOp || revOpDef.spelFormatOp; if (!canFormatOp && !canFormatRevOp) { return undefined; } if (!canFormatOp && canFormatRevOp) { isRev = true; [operator, reversedOp] = [reversedOp, operator]; [opDef, revOpDef] = [revOpDef, opDef]; } return operator; }; const formatRule = (item, config, meta, parentField = null) => { const properties = item.get("properties") || new Map(); const field = properties.get("field"); const fieldSrc = properties.get("fieldSrc"); let operator = properties.get("operator"); if (field == null || operator == null) return undefined; // check op for reverse const realOp = checkOp(config, operator, field); if (!realOp) { meta.errors.push(`Operator ${operator} is not supported`); return undefined; } const isRev = realOp != operator; //format value const [formattedValue, valueSrc, valueType] = formatItemValue(config, properties, meta, realOp, parentField, null); if (formattedValue === undefined) return undefined; //format field const formattedField = formatLhs(meta, config, field, fieldSrc, parentField); if (formattedField === undefined) return undefined; // format expression let res = formatExpression( meta, config, properties, formattedField, formattedValue, realOp, valueSrc, valueType, isRev ); return res; }; const formatLhs = (meta, config, field, fieldSrc, parentField = null) => { if (fieldSrc === "func") return formatFunc(meta, config, field, parentField); else return formatField(meta, config, field, parentField); }; const formatItemValue = (config, properties, meta, operator, parentField, expectedValueType = null) => { let field = properties.get("field"); const iValueSrc = properties.get("valueSrc"); const iValueType = properties.get("valueType"); if (expectedValueType == "!case_value" || (iValueType && iValueType.get(0) == "case_value")) { field = "!case_value"; } const fieldDef = getFieldConfig(config, field) || {}; const operatorDefinition = getOperatorConfig(config, operator, field) || {}; const cardinality = getOpCardinality(operatorDefinition); const iValue = properties.get("value"); const asyncListValues = properties.get("asyncListValues"); let valueSrcs = []; let valueTypes = []; let formattedValue; if (iValue != undefined) { const fvalue = iValue.map((currentValue, ind) => { const valueSrc = iValueSrc ? iValueSrc.get(ind) : null; const valueType = iValueType ? iValueType.get(ind) : null; const cValue = completeValue(currentValue, valueSrc, config); const widget = getWidgetForFieldOp(config, field, operator, valueSrc); const fieldWidgetDef = getFieldWidgetConfig(config, field, operator, widget, valueSrc, { forExport: true }); const fv = formatValue( meta, config, cValue, valueSrc, valueType, fieldWidgetDef, fieldDef, operator, operatorDefinition, parentField, asyncListValues ); if (fv !== undefined) { valueSrcs.push(valueSrc); valueTypes.push(valueType); } return fv; }); const hasUndefinedValues = fvalue.filter((v) => v === undefined).size > 0; if (!(fvalue.size < cardinality || hasUndefinedValues)) { formattedValue = cardinality > 1 ? fvalue.toArray() : cardinality == 1 ? fvalue.first() : null; } } return [ formattedValue, valueSrcs.length > 1 ? valueSrcs : valueSrcs[0], valueTypes.length > 1 ? valueTypes : valueTypes[0], ]; }; const formatValue = ( meta, config, currentValue, valueSrc, valueType, fieldWidgetDef, fieldDef, operator, operatorDef, parentField = null, asyncListValues ) => { if (currentValue === undefined) return undefined; let ret; if (valueSrc === "field") { ret = formatField(meta, config, currentValue, parentField); } else if (valueSrc === "func") { ret = formatFunc(meta, config, currentValue, parentField); } else { if (typeof fieldWidgetDef?.spelFormatValue === "function") { const fn = fieldWidgetDef.spelFormatValue; const args = [ currentValue, { ...pick(fieldDef, ["fieldSettings", "listValues"]), asyncListValues, }, //useful options: valueFormat for date/time omit(fieldWidgetDef, widgetDefKeysToOmit), ]; if (operator) { args.push(operator); args.push(operatorDef); } if (valueSrc == "field") { const valFieldDefinition = getFieldConfig(config, currentValue) || {}; args.push(valFieldDefinition); } ret = fn.call(config.ctx, ...args); } else { ret = spelEscape(currentValue); } } return ret; }; const formatField = (meta, config, field, parentField = null) => { if (!field) return; const { fieldSeparator } = config.settings; const fieldDefinition = getFieldConfig(config, field) || {}; const fieldParts = getFieldParts(field, config); const fieldPartsConfigs = getFieldPartsConfigs(field, config, parentField); const formatFieldFn = config.settings.formatSpelField; const fieldName = formatFieldName(field, config, meta, parentField); const fieldPartsMeta = fieldPartsConfigs.map(([key, cnf, parentCnf]) => { let parent; if (parentCnf) { if (parentCnf.type == "!struct" || (parentCnf.type == "!group" && parentCnf.mode == "struct")) parent = cnf.isSpelMap ? "map" : "class"; else if (parentCnf.type == "!group") parent = cnf.isSpelItemMap ? "[map]" : "[class]"; else parent = "class"; } const isSpelVariable = cnf?.isSpelVariable; return { key, parent, isSpelVariable, fieldSeparator, }; }); const formattedField = formatFieldFn.call( config.ctx, fieldName, parentField, fieldParts, fieldPartsMeta, fieldDefinition, config ); return formattedField; }; const formatFunc = (meta, config, currentValue, parentField = null) => { const funcKey = currentValue.get?.("func"); const args = currentValue.get?.("args"); const funcConfig = getFuncConfig(config, funcKey); if (!funcConfig) { meta.errors.push(`Func ${funcKey} is not defined in config`); return undefined; } let formattedArgs = {}; let gaps = []; let missingArgKeys = []; for (const argKey in funcConfig.args) { const argConfig = funcConfig.args[argKey]; const fieldDef = getFieldConfig(config, argConfig); const { defaultValue, isOptional } = argConfig; const defaultValueSrc = defaultValue?.func ? "func" : "value"; const argVal = args ? args.get(argKey) : undefined; let argValue = argVal ? argVal.get("value") : undefined; const argValueSrc = argVal ? argVal.get("valueSrc") : undefined; if (argValueSrc !== "func" && argValue?.toJS) { // value should not be Immutable argValue = argValue.toJS(); } const argAsyncListValues = argVal ? argVal.get("asyncListValues") : undefined; const doEscape = argConfig.spelEscapeForFormat ?? true; const operator = null; const widget = getWidgetForFieldOp(config, argConfig, operator, argValueSrc); const fieldWidgetDef = getFieldWidgetConfig(config, argConfig, operator, widget, argValueSrc, { forExport: true }); const formattedArgVal = formatValue( meta, config, argValue, argValueSrc, argConfig.type, fieldWidgetDef, fieldDef, null, null, parentField, argAsyncListValues ); if (argValue != undefined && formattedArgVal === undefined) { if (argValueSrc != "func") // don't triger error if args value is another incomplete function meta.errors.push(`Can't format value of arg ${argKey} for func ${funcKey}`); return undefined; } let formattedDefaultVal; if (formattedArgVal === undefined && !isOptional && defaultValue != undefined) { const defaultWidget = getWidgetForFieldOp(config, argConfig, operator, defaultValueSrc); const defaultFieldWidgetDef = getFieldWidgetConfig(config, argConfig, operator, defaultWidget, defaultValueSrc, { forExport: true, }); formattedDefaultVal = formatValue( meta, config, defaultValue, defaultValueSrc, argConfig.type, defaultFieldWidgetDef, fieldDef, null, null, parentField, argAsyncListValues ); if (formattedDefaultVal === undefined) { if (defaultValueSrc != "func") // don't triger error if args value is another incomplete function meta.errors.push(`Can't format default value of arg ${argKey} for func ${funcKey}`); return undefined; } } const finalFormattedVal = formattedArgVal ?? formattedDefaultVal; if (finalFormattedVal !== undefined) { if (gaps.length) { for (const missedArgKey of gaps) { formattedArgs[missedArgKey] = undefined; } gaps = []; } formattedArgs[argKey] = doEscape ? finalFormattedVal : argValue ?? defaultValue; } else { if (!isOptional) missingArgKeys.push(argKey); gaps.push(argKey); } } if (missingArgKeys.length) { //meta.errors.push(`Missing vals for args ${missingArgKeys.join(", ")} for func ${funcKey}`); return undefined; // incomplete } let ret; if (typeof funcConfig.spelFormatFunc === "function") { const fn = funcConfig.spelFormatFunc; const args = [formattedArgs]; ret = fn.call(config.ctx, ...args); } else if (funcConfig.spelFunc) { // fill arg values ret = funcConfig.spelFunc.replace(/\${(\w+)}/g, (found, argKey) => { return formattedArgs[argKey] ?? found; }); // remove optional args (from end only) const optionalArgs = Object.keys(funcConfig.args || {}) .reverse() .filter((argKey) => !!funcConfig.args[argKey].isOptional); for (const argKey of optionalArgs) { if (formattedArgs[argKey] != undefined) break; ret = ret.replace(new RegExp("(, )?" + "\\${" + argKey + "}", "g"), ""); } // missing required arg vals ret = ret.replace(/\${(\w+)}/g, "null"); } else { meta.errors.push(`Func ${funcKey} is not supported`); } return ret; };