UNPKG

@react-query-builder-express/core

Version:
1,056 lines (959 loc) 35.3 kB
import uuid from "../utils/uuid"; import {getOpCardinality, isJsonLogic, shallowEqual, logger} from "../utils/stuff"; import {getFieldConfig, extendConfig, normalizeField, getFuncConfig, iterateFuncs, getFieldParts} from "../utils/configUtils"; import {getWidgetForFieldOp} from "../utils/ruleUtils"; import {loadTree} from "./tree"; import {defaultConjunction, defaultGroupConjunction} from "../utils/defaultUtils"; import moment from "moment"; // http://jsonlogic.com/ // helpers const arrayUniq = (arr) => Array.from(new Set(arr)); // constants const jlFieldMarker = "jlField"; const jlArgsMarker = "jlArgs"; const jlEqOps = ["==", "!="]; const jlRangeOps = ["<", "<=", ">", ">="]; const multiselectOps = [ "multiselect_equals", "multiselect_not_equals", "multiselect_contains", "multiselect_not_contains" ]; const createMeta = (parentMeta) => { return { errors: [], settings: parentMeta?.settings, }; }; export const loadFromJsonLogic = (logicTree, config) => { return _loadFromJsonLogic(logicTree, config, false); }; export const _loadFromJsonLogic = (logicTree, config, returnErrors = true) => { //meta is mutable let meta = createMeta(); meta.settings = { allowUnknownFields: false, returnErrors, }; const extendedConfig = extendConfig(config, undefined, false); const conv = buildConv(extendedConfig); const jsTree = logicTree ? convertFromLogic(logicTree, conv, extendedConfig, ["rule", "group", "switch"], meta) : undefined; const immTree = jsTree ? loadTree(jsTree) : undefined; if (returnErrors) { return [immTree, meta.errors]; } else { if (meta.errors.length) console.warn("Errors while importing from JsonLogic:", meta.errors); return immTree; } }; const buildConv = (config) => { let operators = {}; let combinationOperators = {}; for (let opKey in config.operators) { const opConfig = config.operators[opKey]; if (typeof opConfig.jsonLogic == "string") { // example: "</2", "#in/1" const opk = opConfig.jsonLogic + "/" + getOpCardinality(opConfig); if (!operators[opk]) operators[opk] = []; operators[opk].push(opKey); } else if(typeof opConfig.jsonLogic2 == "string") { // example: all-in/1" const opk = opConfig.jsonLogic2 + "/" + getOpCardinality(opConfig); if (!operators[opk]) operators[opk] = []; operators[opk].push(opKey); if (!combinationOperators[opKey]) combinationOperators[opKey] = {}; combinationOperators[opKey] = { "template": opConfig.jsonLogic(jlFieldMarker, opKey, jlArgsMarker), "jsonLogic2": opConfig.jsonLogic2, "_jsonLogicIsExclamationOp": !!opConfig._jsonLogicIsExclamationOp }; } } let conjunctions = {}; for (let conjKey in config.conjunctions) { const conjunctionDefinition = config.conjunctions[conjKey]; const ck = conjunctionDefinition.jsonLogicConj || conjKey.toLowerCase(); conjunctions[ck] = conjKey; } let funcs = {}; for (const [funcPath, funcConfig] of iterateFuncs(config)) { let fk; if (funcConfig.jsonLogicIsMethod) { fk = "#" + funcConfig.jsonLogic; } else if (typeof funcConfig.jsonLogic == "string") { fk = funcConfig.jsonLogic; } if (fk) { if (!funcs[fk]) funcs[fk] = []; funcs[fk].push(funcPath); } } const {groupVarKey, altVarKey} = config.settings.jsonLogic; return { operators, conjunctions, funcs, varKeys: ["var", groupVarKey, altVarKey], combinationOperators, }; }; /** * This function checks a given jsonlogic object against a set of templates defined in 'conv'. * It determines if the jsonlogic object matches any of the specified templates. * * @param {*} jsonlogic The jsonlogic object to be matched against the templates. * @param {*} conv The object containing all potential templates and their associated logic for matching. * It is expected to have a 'combinationOperators' property that houses the templates. * @param {*} meta An object where any errors or metadata during the processing are stored. It's modified by reference. * @param {*} operatorsToCheck An optional array of operator keys that limits which operators in 'conv' are checked. * If null, all operators in 'conv' are considered. * @returns {Object|null} The response object containing the match result, and any relevant matched fields and * arguments if a match is found. Returns null if no match is found. */ const matchAgainstTemplates = (jsonlogic, conv, meta, operatorsToCheck = null) => { let response; if (conv?.combinationOperators) { for (const [key, value] of Object.entries(conv.combinationOperators)) { if ((operatorsToCheck == null || operatorsToCheck.includes(key))) { const tempResponse = isTemplateMatch(value.template, jsonlogic); // Found a match if (tempResponse.match) { if (!response) response = tempResponse; // Templates should be spesific enough that only one match can be found. This should not happen else meta.errors.push(`Operator matched against 2 templates: ${response.newOp} and ${key}`); // New op that is used to represent operator that is combosed of multiple operators response["newOp"] = value.jsonLogic2; } } } } // Returns undefined if no matches found return response; }; /** * This function recursively compares a jsonlogic object against a template to determine if they match structurally and content-wise. * It is used to support complex template matching where the template can include special markers indicating variable fields and arguments. * * @param {*} template The template object to match against, which can include special markers to denote fields and arguments. * @param {*} jsonlogic The jsonlogic object to test against the template. * @param {*} response An object to accumulate results such as whether a match is found, and to collect any fields or arguments identified * by the template markers. Default is initialized to a match state with empty fields and arguments. * @returns {Object} The updated response object after checking the current template level. It includes whether the current level * matches (match: true/false), any identified fields (jlField), and any arguments (jlArgs). */ const isTemplateMatch = (template, jsonlogic, response = {"match": true, "jlField": null, "jlArgs": []}) => { if (template == undefined || jsonlogic == undefined) { response.match = false; return response; } // This lets us compare order easily const tKeys = Object.keys(template); const jKeys = Object.keys(jsonlogic); if (tKeys.length !== jKeys.length) { // Both have same length response.match = false; return response; } for (let index = 0; index < tKeys.length; index++) { const key = tKeys[index]; const value = template[key]; if (key !== jKeys[index]) { // Checks that both have exact same key at exact same place. Kind of pointless for arrays but whatever response.match = false; return response; } else if (value === jlFieldMarker && isJsonLogic(jsonlogic[key])) { // If jlFieldMarker is found in template AND it's field or func we take the value from corresponding place in jsonlogic response.jlField = jsonlogic[key]; } else if (value === jlArgsMarker) { // If jlArgsMarker is found in template we take the value from corresponding place in jsonlogic response.jlArgs.push(jsonlogic[key]); } else if (typeof value === "object" && value !== null || Array.isArray(value)) { // Here we recurse thru objects and arrays of template until we have gone thru it completely response = isTemplateMatch(value, jsonlogic[key], response); } else if (value !== jsonlogic[key]) { // This is for cases of {var: ""}, which should be only case in default config that leads here response.match = false; return response; } } return response; }; // expectedTypes - "val", "rule", "group", "switch", "case_val" const convertFromLogic = (logic, conv, config, expectedTypes, meta, not = false, fieldConfig, widget, parentField = null, _isLockedLogic = false) => { let op, vals; if (isJsonLogic(logic)) { op = Object.keys(logic)[0]; vals = logic[op]; if (!Array.isArray(vals)) vals = [ vals ]; } let ret; const beforeErrorsCnt = meta.errors.length; const {lockedOp} = config.settings.jsonLogic; const isEmptyOp = op == "!" && (vals.length == 1 && vals[0] && isJsonLogic(vals[0]) && conv.varKeys.includes(Object.keys(vals[0])[0])); // If matchAgainstTemplates returns match then op is replaced with special jsonlogic2 value const match = matchAgainstTemplates(logic, conv, meta); if (match) { // We reset vals if match found vals = []; vals[0] = match.jlField; match.jlArgs.forEach(arg => vals.push(arg)); // We reset op to new op that represents multiple jsonlogic operators op = match.newOp; } const isNot = op == "!" && !isEmptyOp; const isLocked = lockedOp && op == lockedOp; const isSwitch = expectedTypes.includes("switch"); const isRoot = isSwitch; if (isLocked) { ret = convertFromLogic(vals[0], conv, config, expectedTypes, meta, not, fieldConfig, widget, parentField, true); } else if (isNot) { // apply not ret = convertFromLogic(vals[0], conv, config, expectedTypes, meta, !not, fieldConfig, widget, parentField); } else if(expectedTypes.includes("val")) { // not is not used here ret = convertFieldRhs(op, vals, conv, config, not, meta, parentField) || convertFuncRhs(op, vals, conv, config, not, fieldConfig, meta, parentField) || convertValRhs(logic, fieldConfig, widget, config, meta); } else { if (expectedTypes.includes("switch")) { ret = convertIf(op, vals, conv, config, not, meta, parentField); } if (ret == undefined && expectedTypes.includes("group")) { ret = convertConj(op, vals, conv, config, not, meta, parentField, false); } if (ret == undefined && expectedTypes.includes("rule")) { ret = convertOp(op, vals, conv, config, not, meta, parentField); } if (ret) { if (isRoot && !["group", "switch_group"].includes(ret.type)) { ret = wrapInDefaultConj(ret, config); } } } const afterErrorsCnt = meta.errors.length; if (op != "!" && ret === undefined && afterErrorsCnt == beforeErrorsCnt) { meta.errors.push(`Can't parse logic ${JSON.stringify(logic)}`); } if (isLocked) { ret.properties.isLocked = true; } return ret; }; const convertValRhs = (val, fieldConfig, widget, config, meta) => { if (val === undefined) val = fieldConfig?.defaultValue; if (val === undefined) return undefined; widget = widget || fieldConfig?.mainWidget; const widgetConfig = config.widgets[widget]; const fieldType = fieldConfig?.type; if (fieldType && !widgetConfig) { meta.errors.push(`No widget for type ${fieldType}`); return undefined; } if (isJsonLogic(val)) { meta.errors.push(`Unexpected logic in value: ${JSON.stringify(val)}`); return undefined; } // number of seconds -> time string if (fieldType === "time" && typeof val === "number") { const [h, m, s] = [Math.floor(val / 60 / 60) % 24, Math.floor(val / 60) % 60, val % 60]; const valueFormat = widgetConfig.valueFormat; if (valueFormat) { const dateVal = new Date(val); dateVal.setMilliseconds(0); dateVal.setHours(h); dateVal.setMinutes(m); dateVal.setSeconds(s); val = moment(dateVal).format(valueFormat); } else { val = `${h}:${m}:${s}`; } } // "2020-01-08T22:00:00.000Z" -> Date object if (["date", "datetime"].includes(fieldType) && val && !(val instanceof Date)) { try { const dateVal = new Date(val); if (dateVal instanceof Date && dateVal.toISOString() === val) { val = dateVal; } } catch(e) { meta.errors.push(`Can't convert value ${val} as Date`); val = undefined; } } // Date object -> formatted string if (val instanceof Date && fieldConfig) { const valueFormat = widgetConfig.valueFormat; if (valueFormat) { val = moment(val).format(valueFormat); } } let asyncListValues; if (val && fieldConfig?.fieldSettings?.asyncFetch) { const vals = Array.isArray(val) ? val : [val]; asyncListValues = vals; } if (widgetConfig?.jsonLogicImport) { try { val = widgetConfig.jsonLogicImport.call(config.ctx, val); } catch(e) { meta.errors.push(`Can't import value ${val} using import func of widget ${widget}: ${e?.message ?? e}`); val = undefined; } } return { valueSrc: "value", value: val, valueType: widgetConfig?.type, asyncListValues }; }; const convertFieldRhs = (op, vals, conv, config, not, meta, parentField = null) => { if (conv.varKeys.includes(op) && typeof vals[0] == "string") { const field = normalizeField(config, vals[0], parentField); const fieldConfig = getFieldConfig(config, field); if (!fieldConfig && !meta.settings?.allowUnknownFields) { meta.errors.push(`No config for field ${field}`); return undefined; } return { valueSrc: "field", value: field, valueType: fieldConfig?.type, }; } return undefined; }; const convertLhs = (isGroup0, jlField, args, conv, config, not = null, fieldConfig = null, meta, parentField = null) => { let k = Object.keys(jlField)[0]; let v = Object.values(jlField)[0]; const _parse = (k, v) => { return convertFieldLhs(k, v, conv, config, not, meta, parentField) || convertFuncLhs(k, v, conv, config, not, fieldConfig, meta, parentField); }; const beforeErrorsCnt = meta.errors.length; let field, fieldSrc, having, isGroup; const parsed = _parse(k, v); if (parsed) { field = parsed.field; fieldSrc = parsed.fieldSrc; } if (isGroup0) { isGroup = true; having = args[0]; args = []; } // reduce/filter for group ext if (k == "reduce" && Array.isArray(v) && v.length == 3) { let [filter, acc, init] = v; if (isJsonLogic(filter) && init == 0 && isJsonLogic(acc) && Array.isArray(acc["+"]) && acc["+"][0] == 1 && isJsonLogic(acc["+"][1]) && acc["+"][1]["var"] == "accumulator" ) { k = Object.keys(filter)[0]; v = Object.values(filter)[0]; if (k == "filter") { let [group, filter] = v; if (isJsonLogic(group)) { k = Object.keys(group)[0]; v = Object.values(group)[0]; const parsedGroup = _parse(k, v); if (parsedGroup) { field = parsedGroup.field; fieldSrc = parsedGroup.fieldSrc; having = filter; isGroup = true; } } } else { const parsedGroup = _parse(k, v); if (parsedGroup) { field = parsedGroup.field; fieldSrc = parsedGroup.fieldSrc; isGroup = true; } } } } const afterErrorsCnt = meta.errors.length; if (!field && afterErrorsCnt == beforeErrorsCnt) { meta.errors.push(`Unknown LHS ${JSON.stringify(jlField)}`); } if (!field) return; return { field, fieldSrc, having, isGroup, args }; }; const convertFieldLhs = (op, vals, conv, config, not, meta, parentField = null) => { if (!Array.isArray(vals)) vals = [ vals ]; const parsed = convertFieldRhs(op, vals, conv, config, not, meta, parentField); if (parsed) { return { fieldSrc: "field", field: parsed.value, }; } return undefined; }; const convertFuncLhs = (op, vals, conv, config, not, fieldConfig = null, meta, parentField = null) => { const parsed = convertFuncRhs(op, vals, conv, config, not, fieldConfig, meta, parentField); if (parsed) { return { fieldSrc: "func", field: parsed.value, }; } return undefined; }; const convertFuncRhs = (op, vals, conv, config, not, fieldConfig = null, meta, parentField = null) => { if (!op) return undefined; let func, argsArr, funcKey; const jsonLogicIsMethod = (op == "method"); if (jsonLogicIsMethod) { let obj, opts; [obj, func, ...opts] = vals; argsArr = [obj, ...opts]; } else { func = op; argsArr = vals; } const fk = (jsonLogicIsMethod ? "#" : "") + func; const returnType = fieldConfig?.type || fieldConfig?.returnType; const funcKeys = (conv.funcs[fk] || []).filter(k => (fieldConfig ? getFuncConfig(config, k).returnType == returnType : true) ); if (funcKeys.length) { funcKey = funcKeys[0]; } else { const v = {[op]: vals}; for (const [f, fc] of iterateFuncs(config)) { if (fc.jsonLogicImport && (returnType ? fc.returnType == returnType : true)) { let parsed; try { parsed = fc.jsonLogicImport(v); } catch(_e) { // given expression `v` can't be parsed into function } if (parsed) { funcKey = f; argsArr = parsed; } } } } if (!funcKey) return undefined; if (funcKey) { const funcConfig = getFuncConfig(config, funcKey); const argKeys = Object.keys(funcConfig.args || {}); let argsObj = argsArr.reduce((acc, val, ind) => { const argKey = argKeys[ind]; const argConfig = funcConfig.args[argKey]; let argVal; if (argConfig) { argVal = convertFromLogic(val, conv, config, ["val"], meta, false, argConfig, null, parentField); } return argVal !== undefined ? {...acc, [argKey]: argVal} : acc; }, {}); for (let argKey in funcConfig.args) { const argConfig = funcConfig.args[argKey]; let argVal = argsObj[argKey]; if (argVal === undefined) { argVal = argConfig?.defaultValue; if (argVal !== undefined) { argVal = { value: argVal, valueSrc: argVal?.func ? "func" : "value", valueType: argConfig.type, }; } if (argVal === undefined) { if (argConfig?.isOptional) { //ignore } else { meta.errors.push(`No value for arg ${argKey} of func ${funcKey}`); return undefined; } } else { argsObj[argKey] = argVal; } } } return { valueSrc: "func", value: { func: funcKey, args: argsObj }, valueType: funcConfig.returnType, }; } return undefined; }; const convertConj = (op, vals, conv, config, not, meta, parentField = null, isRuleGroup = false) => { const conjKey = conv.conjunctions[op]; const {fieldSeparator} = config.settings; // const parentFieldConfig = parentField ? getFieldConfig(config, parentField) : null; // const isParentGroup = parentFieldConfig?.type == "!group"; if (conjKey) { let type = "group"; const children = vals .map(v => convertFromLogic(v, conv, config, ["rule", "group"], meta, false, null, null, parentField)) .filter(r => r !== undefined) .reduce((acc, r) => ({...acc, [r.id] : r}), {}); const complexFields = Object.values(children) .map(v => v?.properties?.fieldSrc == "field" && v?.properties?.field) .filter(f => f?.includes?.(fieldSeparator)); const complexFieldsGroupAncestors = Object.fromEntries( arrayUniq(complexFields).map(f => { const parts = f.split(fieldSeparator); const ancs = Object.fromEntries( parts.slice(0, -1) .map((f, i, parts) => [...parts.slice(0, i), f]) .map(fp => [fp.join(fieldSeparator), getFieldConfig(config, fp)]) .filter(([_f, fc]) => fc?.type == "!group") ); return [f, Object.keys(ancs)]; }) ); // const childrenInRuleGroup = Object.values(children) // .map(v => v?.properties?.fieldSrc == "field" && v?.properties?.field) // .map(f => complexFieldsGroupAncestors[f]) // .filter(ancs => ancs && ancs.length); // const usedRuleGroups = arrayUniq(Object.values(complexFieldsGroupAncestors).flat()); // const usedTopRuleGroups = topLevelFieldsFilter(usedRuleGroups); let properties = { conjunction: conjKey, not: not }; const id = uuid(); let children1 = {}; let groupToId = {}; Object.entries(children).map(([k, v]) => { if (v?.type == "group" || v?.type == "rule_group") { // put as-is children1[k] = v; } else { const field = v?.properties?.field; const groupAncestors = complexFieldsGroupAncestors[field] || []; const groupField = groupAncestors[groupAncestors.length - 1]; if (!groupField) { // not in rule_group (can be simple field or in struct) - put as-is if (v) { children1[k] = v; } } else { // wrap field in rule_group (with creating hierarchy if need) let ch = children1; let parentFieldParts = getFieldParts(parentField, config); const groupPath = getFieldParts(groupField, config); const isInParent = shallowEqual(parentFieldParts, groupPath.slice(0, parentFieldParts.length)); if (!isInParent) parentFieldParts = []; // should not be const traverseGroupFields = groupField .split(fieldSeparator) .slice(parentFieldParts.length) .map((f, i, parts) => [...parentFieldParts, ...parts.slice(0, i), f].join(fieldSeparator)) .map((f) => ({f, fc: getFieldConfig(config, f) || {}})) .filter(({fc}) => (fc.type != "!struct")); traverseGroupFields.map(({f: gf, fc: gfc}, i) => { let groupId = groupToId[gf]; if (!groupId) { groupId = uuid(); groupToId[gf] = groupId; ch[groupId] = { type: "rule_group", id: groupId, children1: {}, properties: { conjunction: conjKey, not: false, field: gf, fieldSrc: "field", mode: gfc.mode, } }; } ch = ch[groupId].children1; }); ch[k] = v; } } }); // tip: for isRuleGroup=true correct type and properties will be set out of this func return { type: type, id: id, children1: children1, properties: properties }; } return undefined; }; // const topLevelFieldsFilter = (fields) => { // let arr = [...fields].sort((a, b) => (a.length - b.length)); // for (let i = 0 ; i < arr.length ; i++) { // for (let j = i + 1 ; j < arr.length ; j++) { // if (arr[j].indexOf(arr[i]) == 0) { // // arr[j] is inside arr[i] (eg. "a.b" inside "a") // arr.splice(j, 1); // j--; // } // } // } // return arr; // }; const wrapInDefaultConjRuleGroup = (rule, parentField, parentFieldConfig, config, conj = undefined, not = false) => { if (!rule) return undefined; return { type: "rule_group", id: uuid(), children1: { [rule.id]: rule }, properties: { conjunction: conj || defaultGroupConjunction(config, parentFieldConfig), not: not, field: parentField, } }; }; const wrapInDefaultConj = (rule, config, not = false) => { return { type: "group", id: uuid(), children1: { [rule.id]: rule }, properties: { conjunction: defaultConjunction(config), not: not } }; }; const parseRule = (op, arity, vals, parentField, conv, config, meta) => { const submeta = createMeta(meta); let res = _parseRule(op, arity, vals, parentField, conv, config, submeta); if (!res) { meta.errors.push(submeta.errors.join("; ") || `Unknown op ${op}/${arity}`); return undefined; } return res; }; const _parseRule = (op, arity, vals, parentField, conv, config, meta) => { // config.settings.groupOperators are used for group count (cardinality = 0 is exception) // but don't confuse with "all-in" or "some-in" for multiselect const isAllOrSomeInForMultiselect = multiselectOps .map((opName) => config.operators[opName]?.jsonLogic2) .includes(op); const isGroup0 = config.settings.groupOperators.includes(op) && !isAllOrSomeInForMultiselect; let cardinality = isGroup0 ? 0 : arity - 1; if (isGroup0) cardinality = 0; else if (jlEqOps.includes(op) && cardinality == 1 && vals[1] === null) { arity = 1; cardinality = 0; vals = [vals[0]]; } const opk = op + "/" + cardinality; let opKeys = conv.operators[opk]; if (!opKeys) return; let jlField, jlArgs = []; if (jlRangeOps.includes(op) && arity == 3) { jlField = vals[1]; jlArgs = [ vals[0], vals[2] ]; } else { [jlField, ...jlArgs] = vals; } if (!isJsonLogic(jlField)) { meta.errors.push(`Incorrect operands for ${op}: ${JSON.stringify(vals)}`); return; } const lhs = convertLhs(isGroup0, jlField, jlArgs, conv, config, null, null, meta, parentField); if (!lhs) return; const { field, fieldSrc, having, isGroup, args } = lhs; const fieldConfig = getFieldConfig(config, field); if (!fieldConfig && !meta.settings?.allowUnknownFields) { meta.errors.push(`No config for LHS ${field}`); return; } let opKey = opKeys[0]; if (opKeys.length > 1 && fieldConfig && fieldConfig.operators) { // eg. for "equal" and "select_equals" opKeys = opKeys .filter(k => fieldConfig.operators.includes(k)); if (opKeys.length == 0) { meta.errors.push(`No corresponding ops for LHS ${field}`); return; } opKey = opKeys[0]; } return { field, fieldSrc, fieldConfig, opKey, args, having }; }; const convertOp = (op, vals, conv, config, not, meta, parentField = null, _isOneRuleInRuleGroup = false) => { if (!op) return undefined; const jlConjs = Object.values(config.conjunctions).map(({jsonLogicConj}) => jsonLogicConj); const arity = vals.length; const parseRes = parseRule(op, arity, vals, parentField, conv, config, meta); if (!parseRes) return undefined; let {field, fieldSrc, fieldConfig, opKey, args, having} = parseRes; const parentFieldConfig = getFieldConfig(config, parentField); let opConfig = config.operators[opKey]; const reversedOpConfig = config.operators[opConfig?.reversedOp]; const opNeedsReverse = false; const opCanReverse = !!reversedOpConfig; // Group component in array mode can show NOT checkbox, so do nothing in this case // Otherwise try to reverse // const showNot = fieldConfig?.showNot !== undefined ? fieldConfig.showNot : config.settings.showNot; const isRuleGroup = fieldConfig.type == "!group"; // const isGroupArray = isRuleGroup && fieldConfig.mode == "array"; const isInRuleGroup = parentFieldConfig?.type == "!group"; let canRev = opCanReverse && ( !!config.settings.reverseOperatorsForNot || opNeedsReverse || isRuleGroup && !having // !(count == 2) -> count != 2 // because "NOT" is not visible inside rule_group if there are no children || !isRuleGroup && isInRuleGroup && !_isOneRuleInRuleGroup // 2+ rules in rule-group should be flat. see inits.with_not_and_in_some in test ); // if (isGroupArray && showNot) // canRev = false; const needRev = not && canRev || opNeedsReverse; let conj; let havingVals; let havingNot = false; const canRevHaving = !!config.settings.reverseOperatorsForNot; if (fieldConfig?.type == "!group" && having) { conj = Object.keys(having)[0]; havingVals = having[conj]; if (!Array.isArray(havingVals)) havingVals = [ havingVals ]; // Preprocess "!": Try to reverse op in single rule in having // Eg. use `not_equal` instead of `not` `equal` // We look for template matches here to make sure we dont reverse when "!" is // part of operator let match = matchAgainstTemplates(having, conv, meta); while (conj == "!" && !match) { const isEmptyOp = conj == "!" && ( havingVals.length == 1 && havingVals[0] && isJsonLogic(havingVals[0]) && conv.varKeys.includes(Object.keys(havingVals[0])[0]) ); if (isEmptyOp) { break; } havingNot = !havingNot; having = having["!"]; conj = Object.keys(having)[0]; havingVals = having[conj]; // Negation group with single rule is to be treated the same as ! if (canRevHaving && jlConjs.includes(conj) && havingVals.length == 1) { having = having[conj][0]; conj = Object.keys(having)[0]; havingVals = having[conj]; } // Another template matching const matchTemp = matchAgainstTemplates(having, conv, meta); match = matchTemp ? matchTemp : match; } if (!Array.isArray(havingVals)) { havingVals = [ havingVals ]; } // If template match found we act accordingly if (match) { // We reset vals if match found havingVals = []; havingVals[0] = match.jlField; match.jlArgs.forEach(arg => havingVals.push(arg)); // We reset op to new op that represents multiple jsonlogic operators conj = match.newOp; } } // Use reversed op if (needRev) { not = !not; opKey = opConfig.reversedOp; opConfig = config.operators[opKey]; } const widget = getWidgetForFieldOp(config, field, opKey, null); const convertedArgs = args .map(v => convertFromLogic(v, conv, config, ["val"], meta, false, fieldConfig, widget, parentField)); if (convertedArgs.filter(v => v === undefined).length) { //meta.errors.push(`Undefined arg for field ${field} and op ${opKey}`); return undefined; } let res; let fieldType = fieldConfig?.type; if (fieldType === "!group" || fieldType === "!struct") { fieldType = null; } if (fieldConfig?.type == "!group" && having) { if (conv.conjunctions[conj] !== undefined) { // group res = convertConj(conj, havingVals, conv, config, havingNot, meta, field, true); } else { // rule, need to be wrapped in `rule_group` res = convertOp(conj, havingVals, conv, config, havingNot, meta, field, true); if (res) { if (res.type === "rule_group" && res.properties?.field !== field) { res = wrapInDefaultConjRuleGroup(res, field, fieldConfig, config); } Object.assign(res.properties, { conjunction: defaultGroupConjunction(config, fieldConfig), }); } } if (!res) return undefined; res.type = "rule_group"; Object.assign(res.properties, { field: field, mode: fieldConfig.mode, operator: opKey, }); if (fieldConfig.mode == "array") { Object.assign(res.properties, { value: convertedArgs.map(v => v.value), valueSrc: convertedArgs.map(v => v.valueSrc), valueType: convertedArgs.map(v => v.valueType), }); } if (not) { // tip: don't set not to properties, only havingNot should affect it res = wrapInDefaultConj(res, config, not); } } else if (fieldConfig?.type == "!group" && !having) { res = { type: "rule_group", id: uuid(), children1: {}, properties: { conjunction: defaultGroupConjunction(config, fieldConfig), // tip: `not: true` have no effect if there are no children! "NOT" is hidden in UI and is ignored during export // So it's better to reverse group op (see `canRev =`), or wrap in conj with NOT as a last resort not: false, mode: fieldConfig.mode, field: field, operator: opKey, } }; if (fieldConfig.mode === "array") { Object.assign(res.properties, { value: convertedArgs.map(v => v.value), valueSrc: convertedArgs.map(v => v.valueSrc), valueType: convertedArgs.map(v => v.valueType), }); } if (not) { res = wrapInDefaultConj(res, config, not); } } else { const asyncListValuesArr = convertedArgs.map(v => v.asyncListValues).filter(v => v != undefined); const asyncListValues = asyncListValuesArr.length ? asyncListValuesArr[0] : undefined; res = { type: "rule", id: uuid(), properties: { field: field, fieldSrc: fieldSrc, operator: opKey, value: convertedArgs.map(v => v.value), valueSrc: convertedArgs.map(v => v.valueSrc), valueType: convertedArgs.map(v => v.valueType), ...(asyncListValues ? {asyncListValues} : {}), } }; if (not || _isOneRuleInRuleGroup) { res = wrapInDefaultConj(res, config, not); } } return res; }; const convertIf = (op, vals, conv, config, not, meta, parentField = null) => { if (op?.toLowerCase() !== "if") return undefined; const flat = flatizeTernary(vals); const cases = flat.map(([cond, val]) => ([ cond ? convertFromLogic(cond, conv, config, ["rule", "group"], meta, false, null, null, parentField) : null, buildCaseValProperties(config, meta, conv, val), ])); const children1 = cases.map(([cond, val]) => wrapInCase(cond, val, config, meta)); const switchI = { type: "switch_group", id: uuid(), children1, properties: {} }; return switchI; }; const flatizeTernary = (children) => { let flat = []; function _processTernaryChildren(tern) { let [cond, if_val, else_val] = tern; flat.push([cond, if_val]); const else_op = isJsonLogic(else_val) ? Object.keys(else_val)[0] : null; if (else_op?.toLowerCase() === "if") { _processTernaryChildren(else_val[else_op]); } else { flat.push([undefined, else_val]); } } _processTernaryChildren(children); return flat; }; const wrapInCase = (cond, valProperties, config, meta) => { let caseI; if (cond) { caseI = {...cond}; if (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 buildCaseValProperties = (config, meta, conv, val) => { const caseValueFieldConfig = getFieldConfig(config, "!case_value"); if (!caseValueFieldConfig) { meta.errors.push("Missing caseValueField in settings"); return undefined; } const widget = caseValueFieldConfig.mainWidget; const widgetDef = config.widgets[widget]; if (!widgetDef) { meta.errors.push(`No widget ${widget} for case value`); return undefined; } const convVal = convertFromLogic(val, conv, config, ["val", "case_val"], meta, false, caseValueFieldConfig, widget); if (convVal == undefined) { return undefined; } const { value, valueSrc, valueType} = convVal; let valProperties = { value: [value], valueSrc: [valueSrc ?? "value"], valueType: [valueType ?? widgetDef?.type], field: "!case_value", }; return valProperties; };