UNPKG

react-awesome-query-builder-pd

Version:

User-friendly query builder for React. Demo: https://ukrbublik.github.io/react-awesome-query-builder

355 lines (317 loc) 13.7 kB
import { getFieldConfig, getOperatorConfig, getFieldWidgetConfig, getFuncConfig, } from "./configUtils"; import {getOperatorsForField, getWidgetForFieldOp, getNewValueForFieldOp} from "../utils/ruleUtils"; import {defaultValue, deepEqual, getItemInListValues} from "../utils/stuff"; import {defaultOperatorOptions} from "../utils/defaultUtils"; import omit from "lodash/omit"; const typeOf = (v) => { if (typeof v == "object" && v !== null && Array.isArray(v)) return "array"; else return (typeof v); }; const isTypeOf = (v, type) => { if (typeOf(v) == type) return true; if (type == "number" && !isNaN(v)) return true; //can be casted return false; }; export const validateTree = (tree, _oldTree, config, oldConfig, removeEmptyGroups = false, removeInvalidRules = false) => { const c = { config, oldConfig, removeEmptyGroups, removeInvalidRules }; return validateItem(tree, [], null, {}, c); }; function validateItem (item, path, itemId, meta, c) { const type = item.get("type"); const children = item.get("children1"); if ((type === "group" || type === "rule_group") && children && children.size) { return validateGroup(item, path, itemId, meta, c); } else if (type === "rule") { return validateRule(item, path, itemId, meta, c); } else { return item; } } function validateGroup (item, path, itemId, meta, c) { const {removeEmptyGroups} = c; let id = item.get("id"); let children = item.get("children1"); const oldChildren = children; if (!id && itemId) { id = itemId; item = item.set("id", id); meta.sanitized = true; } //validate children let submeta = {}; children = children .map( (currentChild, childId) => validateItem(currentChild, path.concat(id), childId, submeta, c) ); if (removeEmptyGroups) children = children.filter((currentChild) => (currentChild != undefined)); let sanitized = submeta.sanitized || (oldChildren.size != children.size); if (!children.size && removeEmptyGroups && path.length) { sanitized = true; item = undefined; } if (sanitized) meta.sanitized = true; if (sanitized && item) item = item.set("children1", children); return item; } function validateRule (item, path, itemId, meta, c) { const {removeInvalidRules, config, oldConfig} = c; const {showErrorMessage} = config.settings; let id = item.get("id"); let properties = item.get("properties"); let field = properties.get("field"); let operator = properties.get("operator"); let operatorOptions = properties.get("operatorOptions"); let valueSrc = properties.get("valueSrc"); let value = properties.get("value"); let valueError = properties.get("valueError"); const oldSerialized = { field, operator, operatorOptions: operatorOptions ? operatorOptions.toJS() : {}, valueSrc: valueSrc ? valueSrc.toJS() : null, value: value ? value.toJS() : null, valueError: valueError ? valueError.toJS() : null, }; let _wasValid = field && operator && value && !value.find((v, ind) => (v === undefined)); if (!id && itemId) { id = itemId; item = item.set("id", id); meta.sanitized = true; } //validate field const fieldDefinition = field ? getFieldConfig(config, field) : null; if (!fieldDefinition) field = null; if (field == null) { properties = ["operator", "operatorOptions", "valueSrc", "value"].reduce((map, key) => map.delete(key), properties); operator = null; } //validate operator operator = properties.get("operator"); if (operator == "range_between" || operator == "range_not_between") { // fix obsolete operators operator = operator == "range_between" ? "between" : "not_between"; properties = properties.set("operator", operator); } const operatorDefinition = operator ? getOperatorConfig(config, operator, field) : null; if (!operatorDefinition) operator = null; const availOps = field ? getOperatorsForField(config, field) : []; if (!availOps) { console.warn(`Type of field ${field} is not supported`); operator = null; } else if (availOps.indexOf(operator) == -1) { operator = null; } if (operator == null) { properties = properties.delete("operatorOptions"); properties = properties.delete("valueSrc"); properties = properties.delete("value"); } //validate operator options operatorOptions = properties.get("operatorOptions"); let _operatorCardinality = operator ? defaultValue(operatorDefinition.cardinality, 1) : null; if (!operator || operatorOptions && !operatorDefinition.options) { operatorOptions = null; properties = properties.delete("operatorOptions"); } else if (operator && !operatorOptions && operatorDefinition.options) { operatorOptions = defaultOperatorOptions(config, operator, field); properties = properties.set("operatorOptions", operatorOptions); } //validate values valueSrc = properties.get("valueSrc"); value = properties.get("value"); let {newValue, newValueSrc, newValueError} = getNewValueForFieldOp(config, oldConfig, properties, field, operator, null, true); value = newValue; valueSrc = newValueSrc; valueError = newValueError; properties = properties.set("value", value); properties = properties.set("valueSrc", valueSrc); if (showErrorMessage) { properties = properties.set("valueError", valueError); } const newSerialized = { field, operator, operatorOptions: operatorOptions ? operatorOptions.toJS() : {}, valueSrc: valueSrc ? valueSrc.toJS() : null, value: value ? value.toJS() : null, valueError: valueError ? valueError.toJS() : null, }; const sanitized = !deepEqual(oldSerialized, newSerialized); const isValid = field && operator && value && !value.find((v, _ind) => (v === undefined)); if (sanitized) meta.sanitized = true; if (sanitized && !isValid && removeInvalidRules) item = undefined; if (sanitized && item) item = item.set("properties", properties); return item; } /** * * @param {bool} canFix true is useful for func values to remove bad args * @param {bool} isEndValue false if value is in process of editing by user * @param {bool} isRawValue false is used only internally from validateFuncValue * @return {array} [validError, fixedValue] - if validError === null and canFix == true, fixedValue can differ from value if was fixed */ export const validateValue = (config, leftField, field, operator, value, valueType, valueSrc, asyncListValues, canFix = false, isEndValue = false, isRawValue = true) => { let validError = null; let fixedValue = value; if (value != null) { if (valueSrc == "field") { [validError, fixedValue] = validateFieldValue(leftField, field, value, valueSrc, valueType, asyncListValues, config, operator, isEndValue, canFix); } else if (valueSrc == "func") { [validError, fixedValue] = validateFuncValue(leftField, field, value, valueSrc, valueType, asyncListValues, config, operator, isEndValue, canFix); } else if (valueSrc == "value" || !valueSrc) { [validError, fixedValue] = validateNormalValue(leftField, field, value, valueSrc, valueType, asyncListValues, config, operator, isEndValue, canFix); } if (!validError) { const fieldConfig = getFieldConfig(config, field); const w = getWidgetForFieldOp(config, field, operator, valueSrc); const fieldWidgetDefinition = omit(getFieldWidgetConfig(config, field, operator, w, valueSrc), ["factory"]); const rightFieldDefinition = (valueSrc == "field" ? getFieldConfig(config, value) : null); const fieldSettings = fieldWidgetDefinition; // widget definition merged with fieldSettings const fn = fieldWidgetDefinition.validateValue; if (typeof fn == "function") { const args = [ fixedValue, fieldSettings, ]; if (valueSrc == "field") args.push(rightFieldDefinition); const validResult = fn(...args); if (typeof validResult == "boolean") { if (validResult == false) validError = "Invalid value"; } else { validError = validResult; } } } } if (isRawValue && validError) { console.warn("[RAQB validate]", `Field ${field}: ${validError}`); } return [validError, validError ? value : fixedValue]; }; const validateValueInList = (value, listValues) => { if (value instanceof Array) { for (let i = 0 ; i < value.length ; i++) { const vv = getItemInListValues(listValues, value[i]); if (vv == undefined) { return [`Value ${value[i]} is not in list of values`, value]; } else { value[i] = vv.value; } } } else { const vv = getItemInListValues(listValues, value); if (vv == undefined) { return [`Value ${value} is not in list of values`, value]; } else { value = vv.value; } } return [null, value]; }; /** * */ const validateNormalValue = (leftField, field, value, valueSrc, valueType, asyncListValues, config, operator = null, isEndValue = false, canFix = false) => { let fixedValue = value; const fieldConfig = getFieldConfig(config, field); const w = getWidgetForFieldOp(config, field, operator, valueSrc); const wConfig = config.widgets[w]; const wType = wConfig.type; const jsType = wConfig.jsType; const fieldSettings = fieldConfig.fieldSettings; if (valueType != wType) return [`Value should have type ${wType}, but got value of type ${valueType}`, value]; if (jsType && !isTypeOf(value, jsType) && !fieldSettings.listValues) { //tip: can skip tye check for listValues return [`Value should have JS type ${jsType}, but got value of type ${typeof value}`, value]; } if (fieldSettings) { const listValues = asyncListValues || fieldSettings.listValues; if (listValues && !fieldSettings.allowCustomValues) { return validateValueInList(value, listValues); } if (fieldSettings.min != null && value < fieldSettings.min) { return [`Value ${value} < min ${fieldSettings.min}`, value]; } if (fieldSettings.max != null && value > fieldSettings.max) { return [`Value ${value} > max ${fieldSettings.max}`, value]; } } return [null, value]; }; /** * */ const validateFieldValue = (leftField, field, value, _valueSrc, valueType, asyncListValues, config, operator = null, isEndValue = false, canFix = false) => { const {fieldSeparator} = config.settings; const leftFieldStr = Array.isArray(leftField) ? leftField.join(fieldSeparator) : leftField; const rightFieldStr = Array.isArray(value) ? value.join(fieldSeparator) : value; const rightFieldDefinition = getFieldConfig(config, value); if (!rightFieldDefinition) return [`Unknown field ${value}`, value]; if (rightFieldStr == leftFieldStr) return [`Can't compare field ${leftField} with itself`, value]; if (valueType && valueType != rightFieldDefinition.type) return [`Field ${value} is of type ${rightFieldDefinition.type}, but expected ${valueType}`, value]; return [null, value]; }; /** * */ const validateFuncValue = (leftField, field, value, _valueSrc, valueType, asyncListValues, config, operator = null, isEndValue = false, canFix = false) => { let fixedValue = value; if (value) { const funcKey = value.get("func"); if (funcKey) { const funcConfig = getFuncConfig(config, funcKey); if (funcConfig) { if (valueType && funcConfig.returnType != valueType) return [`Function ${funcKey} should return value of type ${funcConfig.returnType}, but got ${valueType}`, value]; for (const argKey in funcConfig.args) { const argConfig = funcConfig.args[argKey]; const args = fixedValue.get("args"); const argVal = args ? args.get(argKey) : undefined; const fieldDef = getFieldConfig(config, argConfig); const argValue = argVal ? argVal.get("value") : undefined; const argValueSrc = argVal ? argVal.get("valueSrc") : undefined; if (argValue !== undefined) { const [argValidError, fixedArgVal] = validateValue( config, leftField, fieldDef, operator, argValue, argConfig.type, argValueSrc, asyncListValues, canFix, isEndValue, false ); if (argValidError !== null) { if (canFix) { fixedValue = fixedValue.deleteIn(["args", argKey]); if (argConfig.defaultValue !== undefined) { fixedValue = fixedValue.setIn(["args", argKey, "value"], argConfig.defaultValue); fixedValue = fixedValue.setIn(["args", argKey, "valueSrc"], "value"); } } else { return [`Invalid value of arg ${argKey} for func ${funcKey}: ${argValidError}`, value]; } } else if (fixedArgVal !== argValue) { fixedValue = fixedValue.setIn(["args", argKey, "value"], fixedArgVal); } } else if (isEndValue && argConfig.defaultValue === undefined && !canFix) { return [`Value of arg ${argKey} for func ${funcKey} is required`, value]; } } } else return [`Unknown function ${funcKey}`, value]; } // else it's not function value } // empty value return [null, fixedValue]; };