@coocoon/react-awesome-query-builder
Version:
User-friendly query builder for React. Demo: https://ukrbublik.github.io/react-awesome-query-builder
573 lines (539 loc) • 16.5 kB
JavaScript
import {
getFieldConfig,
getOperatorConfig,
getFieldWidgetConfig,
getFuncConfig,
} from "./configUtils";
import {
getOperatorsForField,
getWidgetForFieldOp,
getNewValueForFieldOp,
} from "../utils/ruleUtils";
import {
defaultValue,
deepEqual,
getItemInListValues,
logger,
} from "../utils/stuff";
import { defaultOperatorOptions } from "../utils/defaultUtils";
import omit from "lodash/omit";
import { List } from "immutable";
import _ from "lodash";
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,
removeIncompleteRules
) => {
if (removeEmptyGroups === undefined) {
removeEmptyGroups = config.settings.removeEmptyGroupsOnLoad;
}
if (removeIncompleteRules === undefined) {
removeIncompleteRules = config.settings.removeIncompleteRulesOnLoad;
}
const c = {
config,
oldConfig,
removeEmptyGroups,
removeIncompleteRules,
};
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" ||
type == "case_group" ||
type == "switch_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 { removeIncompleteRules, config, oldConfig } = c;
const { showErrorMessage } = config.settings;
let id = item.get("id");
let properties = item.get("properties");
let field = properties.get("field") || null;
let operator = properties.get("operator") || null;
let operatorOptions = properties.get("operatorOptions");
let valueSrc = properties.get("valueSrc");
let value = properties.get("value");
let valueError = properties.get("valueError");
let isFunc = properties.get("isFunc");
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.includes(undefined);
if (!id && itemId) {
id = itemId;
item = item.set("id", id);
meta.sanitized = true;
}
//validate field
const fieldDefinition = field ? getFieldConfig(config, field) : null;
if(isFunc){
var methodConfig = config.funcs?.[field];
if(!methodConfig){
logger.warn(`No config for method ${field}`);
field = null;
}
}else{
if (field && !fieldDefinition) {
logger.warn(`No config for field ${field}`);
field = null;
}
}
if (field == null) {
properties = ["operator", "operatorOptions", "valueSrc", "value"].reduce(
(map, key) => map.delete(key),
properties
);
operator = null;
}
//validate operator
// Backward compatibility: obsolete operator range_between
if (operator == "range_between" || operator == "range_not_between") {
operator = operator == "range_between" ? "between" : "not_between";
console.info(`Fixed operator ${properties.get("operator")} to ${operator}`);
properties = properties.set("operator", operator);
}
const operatorDefinition = operator
? getOperatorConfig(config, operator, field)
: null;
if (operator && !operatorDefinition) {
console.warn(`No config for operator ${operator}`);
operator = null;
}
const availOps = field ? getOperatorsForField(config, field) : [];
if (!availOps) {
console.warn(`Type of field ${field} is not supported`);
operator = null;
} else if (operator && availOps.indexOf(operator) == -1) {
if (isFunc) {
var operatorsConfig = config.funcs?.[field].operators;
if(!operatorsConfig){
logger.warn(`No operators for method ${field}`);
operator = null;
}else{
if(_.indexOf(operatorsConfig, operator)<0){
logger.warn(`Operator ${operator} of method ${field} is not supported`);
operator = null;
}
}
} else {
//func
if (operator == "is_empty" || operator == "is_not_empty") {
// Backward compatibility: is_empty #494
operator = operator == "is_empty" ? "is_null" : "is_not_null";
console.info(
`Fixed operator ${properties.get(
"operator"
)} to ${operator} for ${field}`
);
properties = properties.set("operator", operator);
} else {
console.warn(
`Operator ${operator} is not supported for field ${field}`
);
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);
}
if(!isFunc){ //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 isComplete = field && operator && value && !value.includes(undefined);
if (sanitized) meta.sanitized = true;
if (!isComplete && removeIncompleteRules) item = undefined;
else if (sanitized) 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,
isFunc = false
) => {
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,
isFunc
);
}
if (!validError) {
const fieldConfig = getFieldConfig(config, field);
const w = getWidgetForFieldOp(config, field, operator, valueSrc);
const operatorDefinition = operator
? getOperatorConfig(config, operator, field)
: null;
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, operator, operatorDefinition];
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) => {
const values = List.isList(value)
? value.toJS()
: value instanceof Array
? [...value]
: undefined;
if (values) {
for (let i = 0; i < values.length; i++) {
const vv = getItemInListValues(listValues, values[i]);
if (vv == undefined) {
return [`Value ${value[i]} is not in list of values`, values];
} else {
values[i] = vv.value;
}
}
return [null, values];
} 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,
isFunc = false
) => {
let fixedValue = value;
if (field) {
const fieldConfig = getFieldConfig(config, field,isFunc);
const w = getWidgetForFieldOp(config, field, operator, valueSrc,isFunc);
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 isFuncArg = typeof field == "object" && field?._isFuncArg;
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 && !isFuncArg)
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];
};