UNPKG

@react-query-builder-express/core

Version:
501 lines (449 loc) 18.2 kB
import {getOpCardinality, widgetDefKeysToOmit, opDefKeysToOmit, omit} from "../utils/stuff"; import { getFieldConfig, getOperatorConfig, getFieldWidgetConfig, getFuncConfig, getFieldParts, extendConfig, } from "../utils/configUtils"; import {getFieldPathLabels, getWidgetForFieldOp, formatFieldName, completeValue, getOneChildOrDescendant} from "../utils/ruleUtils"; import {defaultConjunction} from "../utils/defaultUtils"; import pick from "lodash/pick"; import {List, Map} from "immutable"; // helpers const isObject = (v) => (typeof v == "object" && v !== null && !Array.isArray(v)); export const mongodbFormat = (tree, config) => { return _mongodbFormat(tree, config, false); }; export const _mongodbFormat = (tree, config, returnErrors = true) => { //meta is mutable let meta = { errors: [] }; const extendedConfig = extendConfig(config, undefined, false); const res = formatItem([], tree, extendedConfig, meta); if (returnErrors) { return [res, meta.errors]; } else { if (meta.errors.length) console.warn("Errors while exporting to MongoDb:", meta.errors); return res; } }; const formatItem = (parents, item, config, meta, _not = false, _canWrapExpr = true, _formatFieldName = undefined, _value = undefined) => { if (!item) return undefined; const type = item.get("type"); if ((type === "group" || type === "rule_group")) { return formatGroup(parents, item, config, meta, _not, _canWrapExpr, _formatFieldName, _value); } else if (type === "rule") { return formatRule(parents, item, config, meta, _not, _canWrapExpr, _formatFieldName, _value); } return undefined; }; const formatGroup = (parents, item, config, meta, _not = false, _canWrapExpr = true, _formatFieldName = undefined, _value = undefined) => { const type = item.get("type"); const properties = item.get("properties") || new Map(); const origNot = !!properties.get("not"); const children = item.get("children1") || new List(); const {canShortMongoQuery, fieldSeparator} = config.settings; const sep = fieldSeparator; const hasParentRuleGroup = parents.filter(it => it.get("type") == "rule_group").length > 0; const parentPath = parents .filter(it => it.get("type") == "rule_group") .map(it => it.get("properties").get("field")) .slice(-1).pop(); const realParentPath = hasParentRuleGroup && parentPath; const isRuleGroup = (type === "rule_group"); const groupField = isRuleGroup ? properties.get("field") : null; let groupOperator = isRuleGroup ? properties.get("operator") : null; let groupOperatorDef = groupOperator && getOperatorConfig(config, groupOperator, groupField) || null; const groupOperatorCardinality = groupOperator ? groupOperatorDef?.cardinality ?? 1 : undefined; const groupFieldName = formatFieldName(groupField, config, meta, realParentPath); const groupFieldDef = getFieldConfig(config, groupField) || {}; const mode = groupFieldDef.mode; //properties.get("mode"); const canHaveEmptyChildren = groupField && mode === "array" && groupOperatorCardinality >= 1; const isRuleGroupArray = isRuleGroup && mode != "struct"; const isRuleGroupWithChildren = isRuleGroup && children?.size > 0; const isRuleGroupWithoutChildren = isRuleGroup && !children?.size; // rev let revChildren = false; let not = origNot; let filterNot = false; if (isRuleGroupWithChildren) { // for rule_group `not` there should be 2 NOTs: from properties (for children) and from parent group (_not) filterNot = origNot; not = _not; } else { if (_not) { not = !not; } } let reversedGroupOp = groupOperatorDef?.reversedOp; let reversedGroupOpDef = getOperatorConfig(config, reversedGroupOp, groupField); const groupOpNeedsReverse = !groupOperatorDef?.mongoFormatOp && !!reversedGroupOpDef?.mongoFormatOp; const groupOpCanReverse = !!reversedGroupOpDef?.mongoFormatOp; const oneChildType = getOneChildOrDescendant(item)?.get("type"); const canRevChildren = !!config.settings.reverseOperatorsForNot && (!isRuleGroup && not && oneChildType === "rule" || filterNot && children?.size === 1); if (canRevChildren) { if (isRuleGroupWithChildren) { filterNot = !filterNot; } else { not = !not; } revChildren = true; } let canRevGroupOp = not && isRuleGroup && groupOpCanReverse && (!!config.settings.reverseOperatorsForNot || groupOpNeedsReverse); if (canRevGroupOp) { not = !not; [groupOperator, reversedGroupOp] = [reversedGroupOp, groupOperator]; [groupOperatorDef, reversedGroupOpDef] = [reversedGroupOpDef, groupOperatorDef]; } // conj let conjunction = properties.get("conjunction"); if (!conjunction) conjunction = defaultConjunction(config); let conjunctionDefinition = config.conjunctions[conjunction]; if (!conjunctionDefinition) return undefined; // rev conj const reversedConj = conjunctionDefinition.reversedConj; const canRev = not && conjunction?.toLowerCase() === "or" && reversedConj && !isRuleGroup && !!config.settings.canShortMongoQuery && !!config.settings.reverseOperatorsForNot; if (canRev) { conjunction = reversedConj; conjunctionDefinition = config.conjunctions[conjunction]; not = !not; revChildren = true; } const mongoConj = conjunctionDefinition.mongoConj; const list = children .map((currentChild) => formatItem( [...parents, item], currentChild, config, meta, revChildren, mode != "array", mode == "array" ? (f => `$$el${sep}${f}`) : undefined) ) .filter((formattedChild) => typeof formattedChild !== "undefined"); if (!canHaveEmptyChildren && !list.size) { return undefined; } let resultQuery; if (list.size == 1) { resultQuery = list.first(); } else if (list.size > 1) { const rules = list.toList().toJS(); const canShort = canShortMongoQuery && (mongoConj == "$and"); if (canShort) { resultQuery = rules.reduce((acc, rule) => { if (!acc) return undefined; for (let k in rule) { if (k[0] == "$") { acc = undefined; break; } if (acc[k] == undefined) { acc[k] = rule[k]; } else { // https://github.com/ukrbublik/react-awesome-query-builder/issues/182 let prev = acc[k], next = rule[k]; if (!isObject(prev)) { prev = {"$eq": prev}; } if (!isObject(next)) { next = {"$eq": next}; } const prevOp = Object.keys(prev)[0], nextOp = Object.keys(next)[0]; if (prevOp == nextOp) { acc = undefined; break; } acc[k] = Object.assign({}, prev, next); } } return acc; }, {}); } if (!resultQuery) { // can't be shorten resultQuery = { [mongoConj] : rules }; } } if (groupField) { if (mode == "array") { const totalQuery = { "$size": { "$ifNull": [ "$" + groupFieldName, [] ] } }; const filterQuery = resultQuery ? { "$size": { "$ifNull": [ { "$filter": { input: "$" + groupFieldName, as: "el", cond: resultQuery } }, [] ] } } : totalQuery; resultQuery = formatItem( parents, item.set("type", "rule"), config, meta, filterNot, false, (_f => filterQuery), totalQuery ); resultQuery = { "$expr": resultQuery }; } else { resultQuery = { [groupFieldName]: {"$elemMatch": resultQuery} }; } } if (not) { resultQuery = { "$not": resultQuery }; } return resultQuery; }; const formatRule = (parents, item, config, meta, _not = false, _canWrapExpr = true, _formatFieldName = undefined, _value = undefined) => { const properties = item.get("properties") || new Map(); const hasParentRuleGroup = parents.filter(it => it.get("type") == "rule_group").length > 0; const parentPath = parents .filter(it => it.get("type") == "rule_group") .map(it => it.get("properties").get("field")) .slice(-1).pop(); const realParentPath = hasParentRuleGroup && parentPath; let operator = properties.get("operator"); const operatorOptions = properties.get("operatorOptions"); const field = properties.get("field"); const fieldSrc = properties.get("fieldSrc"); const iValue = properties.get("value"); const iValueSrc = properties.get("valueSrc"); const iValueType = properties.get("valueType"); const asyncListValues = properties.get("asyncListValues"); if (field == null || operator == null || iValue === undefined) return undefined; const fieldDef = getFieldConfig(config, field); // check op let operatorDefinition = getOperatorConfig(config, operator, field); let reversedOp = operatorDefinition?.reversedOp; let revOperatorDefinition = getOperatorConfig(config, reversedOp, field); const cardinality = getOpCardinality(operatorDefinition); if (!operatorDefinition?.mongoFormatOp && !revOperatorDefinition?.mongoFormatOp) { meta.errors.push(`Operator ${operator} is not supported`); return undefined; } // try reverse let not = _not; const opNeedsReverse = !operatorDefinition?.mongoFormatOp && !!revOperatorDefinition?.mongoFormatOp; const opCanReverse = !!revOperatorDefinition?.mongoFormatOp; let canRev = opCanReverse && (!!config.settings.reverseOperatorsForNot || opNeedsReverse); const needRev = canRev && not || opNeedsReverse; let isRev = false; if (needRev) { [operator, reversedOp] = [reversedOp, operator]; [operatorDefinition, revOperatorDefinition] = [revOperatorDefinition, operatorDefinition]; not = !not; isRev = true; } let formattedField; let useExpr = false; if (fieldSrc == "func") { [formattedField, useExpr] = formatFunc(meta, config, field, realParentPath); } else { formattedField = formatFieldName(field, config, meta, realParentPath); if (_formatFieldName) { useExpr = true; formattedField = _formatFieldName(formattedField); } } if (formattedField == undefined) return undefined; //format value 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, fvUseExpr] = formatValue( meta, config, cValue, valueSrc, valueType, fieldWidgetDef, fieldDef, realParentPath, operator, operatorDefinition, asyncListValues ); if (fv !== undefined) { useExpr = useExpr || fvUseExpr; valueSrcs.push(valueSrc); valueTypes.push(valueType); } return fv; }); const hasUndefinedValues = fvalue.filter(v => v === undefined).size > 0; if (fvalue.size < cardinality || hasUndefinedValues) return undefined; formattedValue = cardinality > 1 ? fvalue.toArray() : (cardinality == 1 ? fvalue.first() : null); } const wrapExpr = useExpr && _canWrapExpr; //build rule const fn = operatorDefinition?.mongoFormatOp; const args = [ formattedField, operator, _value !== undefined && formattedValue == null ? _value : formattedValue, useExpr, (valueSrcs.length > 1 ? valueSrcs : valueSrcs[0]), (valueTypes.length > 1 ? valueTypes : valueTypes[0]), omit(operatorDefinition, opDefKeysToOmit), operatorOptions, fieldDef, ]; let ruleQuery = fn.call(config.ctx, ...args); if (wrapExpr) { ruleQuery = { "$expr": ruleQuery }; } if (not) { ruleQuery = { "$not": ruleQuery }; } return ruleQuery; }; const formatValue = (meta, config, currentValue, valueSrc, valueType, fieldWidgetDef, fieldDef, parentPath, operator, operatorDef, asyncListValues) => { if (currentValue === undefined) return [undefined, false]; let ret; let useExpr = false; if (valueSrc == "field") { [ret, useExpr] = formatRightField(meta, config, currentValue, parentPath); } else if (valueSrc == "func") { [ret, useExpr] = formatFunc(meta, config, currentValue, parentPath); } else { if (typeof fieldWidgetDef?.mongoFormatValue === "function") { const fn = fieldWidgetDef.mongoFormatValue; const args = [ currentValue, { ...(fieldDef ? pick(fieldDef, ["fieldSettings", "listValues"]) : {}), asyncListValues }, //useful options: valueFormat for date/time omit(fieldWidgetDef, widgetDefKeysToOmit), ]; if (operator) { args.push(operator); args.push(operatorDef); } ret = fn.call(config.ctx, ...args); } else { ret = currentValue; } } return [ret, useExpr]; }; const formatRightField = (meta, config, rightField, parentPath) => { const {fieldSeparator} = config.settings; let ret; const useExpr = true; if (rightField) { const rightFieldDefinition = getFieldConfig(config, rightField) || {}; const fieldParts = getFieldParts(rightField, config); const fieldPartsLabels = getFieldPathLabels(rightField, config); const fieldFullLabel = fieldPartsLabels ? fieldPartsLabels.join(fieldSeparator) : null; const formatFieldFn = config.settings.formatField; const rightFieldName = formatFieldName(rightField, config, meta, parentPath); const formattedField = formatFieldFn(rightFieldName, fieldParts, fieldFullLabel, rightFieldDefinition, config, false); ret = "$" + formattedField; } return [ret, useExpr]; }; const formatFunc = (meta, config, currentValue, parentPath) => { const useExpr = true; let ret; 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, false]; } const funcParts = getFieldParts(funcKey, config); const funcLastKey = funcParts[funcParts.length-1]; const funcName = funcConfig.mongoFunc || funcLastKey; const mongoArgsAsObject = funcConfig.mongoArgsAsObject; let formattedArgs = {}; let argsCnt = 0; let lastArg = undefined; let gaps = []; let missingArgKeys = []; for (const argKey in funcConfig.args) { argsCnt++; 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 operator = null; const widget = getWidgetForFieldOp(config, argConfig, operator, argValueSrc); const fieldWidgetDef = getFieldWidgetConfig(config, argConfig, operator, widget, argValueSrc, { forExport: true }); const [formattedArgVal, _argUseExpr] = formatValue( meta, config, argValue, argValueSrc, argConfig.type, fieldWidgetDef, fieldDef, parentPath, null, null, 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, false]; } 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 }); let _; ([formattedDefaultVal, _] = formatValue( meta, config, defaultValue, defaultValueSrc, argConfig.type, defaultFieldWidgetDef, fieldDef, parentPath, null, null, 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, false]; } } const finalFormattedVal = formattedArgVal ?? formattedDefaultVal; if (finalFormattedVal !== undefined) { if (gaps.length) { for (const missedArgKey of gaps) { formattedArgs[missedArgKey] = undefined; } gaps = []; } formattedArgs[argKey] = finalFormattedVal; lastArg = finalFormattedVal; } 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, false]; // incomplete } if (typeof funcConfig.mongoFormatFunc === "function") { const fn = funcConfig.mongoFormatFunc; const args = [ formattedArgs, ]; ret = fn.call(config.ctx, ...args); } else if (funcConfig.mongoFormatFunc === null) { meta.errors.push(`Functon ${funcName} is not supported`); return [undefined, false]; } else { if (mongoArgsAsObject) ret = { [funcName]: formattedArgs }; else if (argsCnt == 1 && lastArg !== undefined) ret = { [funcName]: lastArg }; else ret = { [funcName]: Object.values(formattedArgs) }; } return [ret, useExpr]; };