@react-query-builder-express/core
Version:
User-friendly query builder for React. Core
424 lines (381 loc) • 14.6 kB
JavaScript
import {
getFieldConfig, getOperatorConfig, getFieldWidgetConfig, getFuncConfig, getFieldParts, extendConfig,
} from "../utils/configUtils";
import {
getFieldPathLabels, getWidgetForFieldOp, formatFieldName, completeValue
} from "../utils/ruleUtils";
import pick from "lodash/pick";
import {getOpCardinality, widgetDefKeysToOmit, opDefKeysToOmit, omit} from "../utils/stuff";
import {defaultConjunction} from "../utils/defaultUtils";
import {List, Map} from "immutable";
export const queryString = (item, config, isForDisplay = false, isDebugMode = false) => {
//meta is mutable
let meta = {
errors: [],
settings: {
isForDisplay,
isDebugMode,
}
};
const extendedConfig = extendConfig(config, undefined, false);
const res = formatItem(item, extendedConfig, meta, null);
if (meta.errors.length)
console.warn("Errors while exporting to string:", meta.errors);
return res;
};
const formatItem = (item, config, meta, parentField = null) => {
if (!item) return undefined;
const type = item.get("type");
const children = item.get("children1");
if ((type === "group" || type === "rule_group") ) {
return formatGroup(item, config, meta, parentField);
} else if (type === "rule") {
return formatRule(item, config, meta, parentField);
}
return undefined;
};
const formatGroup = (item, config, meta, parentField = null) => {
const { isForDisplay, isDebugMode } = meta.settings;
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 isRuleGroup = (type === "rule_group");
// TIP: don't cut group for mode == 'struct' and don't do aggr format (maybe later)
const groupField = isRuleGroup && mode == "array" ? properties.get("field") : null;
const groupOperator = type === "rule_group" ? properties.get("operator") : null;
const groupOperatorCardinality = groupOperator ? config.operators[groupOperator]?.cardinality ?? 1 : undefined;
const canHaveEmptyChildren = isRuleGroup && mode === "array" && groupOperatorCardinality >= 1;
const not = properties.get("not");
const list = children
.map((currentChild) => formatItem(currentChild, config, meta, groupField))
.filter((currentChild) => typeof currentChild !== "undefined");
if (!canHaveEmptyChildren && !list.size && !isDebugMode) {
return undefined;
}
let conjunction = properties.get("conjunction");
if (!conjunction)
conjunction = defaultConjunction(config);
const conjunctionDefinition = config.conjunctions[conjunction];
const conjStr = list.size ? conjunctionDefinition.formatConj.call(config.ctx, list, conjunction, not, isForDisplay) : null;
let ret;
if (groupField) {
const aggrArgs = formatRule(item, config, meta, parentField, true);
if (aggrArgs) {
const isRev = aggrArgs.pop();
const args = [
conjStr,
...aggrArgs
];
ret = config.settings.formatAggr.call(config.ctx, ...args);
if (isRev) {
ret = config.settings.formatReverse.call(config.ctx, ret, null, null, null, null, isForDisplay);
}
}
} else {
ret = conjStr;
}
if (isDebugMode && ret == null) {
ret = "?";
}
return ret;
};
const formatItemValue = (config, properties, meta, _operator, parentField) => {
const { isForDisplay, isDebugMode } = meta.settings;
const field = properties.get("field");
const iValueSrc = properties.get("valueSrc");
const iValueType = properties.get("valueType");
const fieldDef = getFieldConfig(config, field) || {};
const operator = _operator || properties.get("operator");
const operatorDef = getOperatorConfig(config, operator, field) || {};
const cardinality = getOpCardinality(operatorDef);
const iValue = properties.get("value");
const asyncListValues = properties.get("asyncListValues");
let valueSrcs = [];
let valueTypes = [];
let formattedValue;
let fvalue;
if (iValue != undefined) {
fvalue = iValue.map((currentValue, ind) => {
const valueSrc = iValueSrc ? iValueSrc.get(ind) : null;
const valueType = iValueType ? iValueType.get(ind) : null;
const cValue = !isDebugMode ? completeValue(currentValue, valueSrc, config) : currentValue;
const widget = getWidgetForFieldOp(config, field, operator, valueSrc);
const fieldWidgetDef = getFieldWidgetConfig(config, field, operator, widget, valueSrc, { forExport: true });
let fv = formatValue(
config, meta, cValue, valueSrc, valueType, fieldWidgetDef, fieldDef, operator, operatorDef, parentField, asyncListValues
);
if (fv !== undefined) {
valueSrcs.push(valueSrc);
valueTypes.push(valueType);
}
return fv;
});
const hasUndefinedValues = fvalue.filter(v => v === undefined).size > 0;
const isOK = !hasUndefinedValues && fvalue.size === cardinality;
if (isOK) {
formattedValue = (cardinality == 1 ? fvalue.first() : fvalue);
}
}
if (isDebugMode && !formattedValue) {
formattedValue = cardinality > 1 ? new List(Array.from({length: cardinality}).map(
(_, i) => fvalue?.get(i) ?? "?")
) : "?";
}
return [
formattedValue,
(valueSrcs.length > 1 ? valueSrcs : valueSrcs[0]),
(valueTypes.length > 1 ? valueTypes : valueTypes[0]),
];
};
const buildFnToFormatOp = (operator, operatorDefinition, meta) => {
const { isDebugMode } = meta.settings;
const fop = operatorDefinition?.labelForFormat || operator;
const cardinality = getOpCardinality(operatorDefinition);
let fn;
if (cardinality == 0) {
fn = (field, op, values, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => {
return `${field} ${fop}`;
};
} else if (cardinality == 1) {
fn = (field, op, values, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => {
if (isDebugMode && op === "?" && values === "?") {
return field && field !== "?" ? `${field} ?` : "?";
}
return `${field} ${fop} ${values}`;
};
} else if (cardinality == 2) {
// between
fn = (field, op, values, valueSrc, valueType, opDef, operatorOptions, isForDisplay) => {
const valFrom = values?.first?.();
const valTo = values?.get?.(1);
return `${field} ${fop} ${valFrom} AND ${valTo}`;
};
}
return fn;
};
const formatRule = (item, config, meta, parentField = null, returnArgs = false) => {
const { isForDisplay, isDebugMode } = meta.settings;
const properties = item.get("properties") || new Map();
const field = properties.get("field");
const fieldSrc = properties.get("fieldSrc");
let operator = properties.get("operator");
let operatorOptions = properties.get("operatorOptions");
if ((field == null || operator == null) && !isDebugMode)
return undefined;
const fieldDef = getFieldConfig(config, field) || {};
let operatorDef = getOperatorConfig(config, operator, field) || {};
let reversedOp = operatorDef.reversedOp;
let revOperatorDef = getOperatorConfig(config, reversedOp, field) || {};
//check op
let isRev = false;
let fn = operatorDef.formatOp;
if (!fn && reversedOp) {
fn = revOperatorDef.formatOp;
if (fn) {
isRev = true;
[operator, reversedOp] = [reversedOp, operator];
[operatorDef, revOperatorDef] = [revOperatorDef, operatorDef];
}
}
if (isDebugMode && !operator) {
operator = "?";
}
//find fn to format expr
if (!fn)
fn = buildFnToFormatOp(operator, operatorDef, meta);
if (!fn)
return undefined;
//format field
const formattedField = fieldSrc === "func"
? formatFunc(config, meta, field, parentField)
: formatField(config, meta, field, parentField);
if (formattedField == undefined)
return undefined;
//format value
const [formattedValue, valueSrc, valueType] = formatItemValue(
config, properties, meta, operator, parentField
);
if (formattedValue === undefined) {
return undefined;
}
const args = [
formattedField,
operator,
formattedValue,
valueSrc,
valueType,
omit(operatorDef, opDefKeysToOmit),
operatorOptions,
isForDisplay,
fieldDef,
isRev,
];
if (returnArgs) {
return args;
} else {
//format expr
let ret = fn.call(config.ctx, ...args);
//rev
if (isRev) {
ret = config.settings.formatReverse.call(config.ctx, ret, operator, reversedOp, operatorDef, revOperatorDef, isForDisplay);
}
return ret;
}
};
const formatValue = (config, meta, value, valueSrc, valueType, fieldWidgetDef, fieldDef, operator, opDef, parentField = null, asyncListValues) => {
const { isForDisplay, isDebugMode } = meta.settings;
if (value === undefined) {
if (isDebugMode) {
if (fieldWidgetDef?.jsType === "array") {
return [];
}
return "?";
} else {
return undefined;
}
}
let ret;
if (valueSrc == "field") {
ret = formatField(config, meta, value, parentField);
} else if (valueSrc == "func") {
ret = formatFunc(config, meta, value, parentField);
} else {
if (typeof fieldWidgetDef?.formatValue === "function") {
const fn = fieldWidgetDef.formatValue;
const args = [
value,
{
...pick(fieldDef, ["fieldSettings", "listValues"]),
asyncListValues
},
//useful options: valueFormat for date/time
omit(fieldWidgetDef, widgetDefKeysToOmit),
isForDisplay
];
if (operator) {
args.push(operator);
args.push(opDef);
}
if (valueSrc == "field") {
const valFieldDefinition = getFieldConfig(config, value) || {};
args.push(valFieldDefinition);
}
ret = fn.call(config.ctx, ...args);
} else {
ret = value;
}
}
return ret;
};
const formatField = (config, meta, field, parentField = null, cutParentField = true) => {
const { isForDisplay, isDebugMode } = meta.settings;
const {fieldSeparator, fieldSeparatorDisplay} = config.settings;
let ret = null;
if (field) {
const fieldDefinition = getFieldConfig(config, field) || {};
const fieldParts = getFieldParts(field, config);
const fieldPartsLabels = getFieldPathLabels(field, config, cutParentField ? parentField : null);
const fieldFullLabel = fieldPartsLabels ? fieldPartsLabels.join(fieldSeparatorDisplay) : null;
const fieldLabel2 = fieldDefinition.label2 || fieldFullLabel;
const formatFieldFn = config.settings.formatField;
const fieldName = formatFieldName(field, config, meta, cutParentField ? parentField : null, {useTableName: true});
ret = formatFieldFn(fieldName, fieldParts, fieldLabel2, fieldDefinition, config, isForDisplay);
} else if(isDebugMode) {
ret = "?";
}
return ret;
};
const formatFunc = (config, meta, funcValue, parentField = null) => {
const { isForDisplay, isDebugMode } = meta.settings;
const funcKey = funcValue?.get?.("func");
if (!funcKey) {
return isDebugMode ? "?()" : undefined;
}
const args = funcValue.get?.("args");
const funcConfig = getFuncConfig(config, funcKey);
if (!funcConfig) {
if (!isDebugMode) {
meta.errors.push(`Func ${funcKey} is not defined in config`);
return undefined;
}
}
const funcParts = getFieldParts(funcKey, config);
const funcLastKey = funcParts[funcParts.length-1];
const funcName = isForDisplay && funcConfig?.label || funcLastKey;
let formattedArgs = {};
let gaps = [];
let missingArgKeys = [];
let formattedArgsWithNames = {};
const argsKeys = funcConfig ? Object.keys(funcConfig.args || {}) : args?.keySeq?.().toArray() || [];
for (const argKey of argsKeys) {
const argConfig = funcConfig?.args[argKey];
const fieldDef = getFieldConfig(config, argConfig);
const {defaultValue, isOptional} = argConfig || {};
const defaultValueSrc = defaultValue?.func ? "func" : "value";
const argName = isForDisplay && argConfig?.label || argKey;
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 formattedArgVal = formatValue(
config, meta, argValue, argValueSrc, argConfig?.type, fieldDef, argConfig, 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}`);
}
let formattedDefaultVal;
if (formattedArgVal === undefined && !isOptional && defaultValue != undefined) {
formattedDefaultVal = formatValue(
config, meta, defaultValue, defaultValueSrc, argConfig?.type, fieldDef, argConfig, 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, missedArgName] of argKey) {
formattedArgs[missedArgKey] = undefined;
//formattedArgsWithNames[missedArgName] = undefined;
}
gaps = [];
}
formattedArgs[argKey] = finalFormattedVal;
formattedArgsWithNames[argName] = finalFormattedVal;
} else {
if (!isOptional)
missingArgKeys.push(argKey);
gaps.push([argKey, argName]);
}
}
if (missingArgKeys.length) {
//meta.errors.push(`Missing vals for args ${missingArgKeys.join(", ")} for func ${funcKey}`);
if (!isDebugMode) {
return undefined; // incomplete
}
}
let ret = null;
if (typeof funcConfig?.formatFunc === "function") {
const fn = funcConfig.formatFunc;
const args = [
formattedArgs,
isForDisplay
];
ret = fn.call(config.ctx, ...args);
} else {
const argsStr = Object.entries(isForDisplay ? formattedArgsWithNames : formattedArgs)
.map(([k, v]) => (isForDisplay ? `${k}: ${v}` : `${v}`))
.join(", ");
ret = `${funcName}(${argsStr})`;
}
return ret;
};